Проверка зависимых строк в django admin
Я использую Django 1.4, и я хочу установить правила проверки, которые сравнивают значения разных строк.
У меня есть три простых класса
В models.py:
class Shopping(models.Model):
shop_name = models.CharField(max_length=200)
class Item(models.Model):
item_name = models.CharField(max_length=200)
cost = models.IntegerField()
item_shop = models.ForeignKey(Shopping)
class Buyer(models.Model):
buyer_name = models.CharField(max_length=200)
amount = models.IntegerField()
buyer_shop = models.ForeignKey(Shopping)
В admin.py:
class ItemInline(admin.TabularInline):
model = Item
class BuyerInline(admin.TabularInline):
model = Buyer
class ShoppingAdmin(admin.ModelAdmin):
inlines = (ItemInline, BuyerInline)
Так, например, можно купить бутылку rhum на 10 $и одну водки при 8 $. Майк платит 15 $, а Том платит 3 $.
Цель состоит в том, чтобы запретить пользователю сохранять экземпляр с суммами, которые не совпадают: то, что было оплачено, должно быть таким же, как сумма стоимости предмета (например, 10 + 8 = 15 + 3).
Я пробовал:
- повышение ValidationError в методе Shopping.clean. Но встроенные строки еще не обновлены в чистоте, поэтому суммы неверны.
- повышение ValidationError в методе ShoppingAdmin.save_related. Но повышение ValidationError здесь дает очень недружественную страницу ошибок пользователя, а не перенаправление на страницу с хорошим сообщением об ошибке.
Есть ли какое-либо решение этой проблемы? Является ли проверка на стороне клиента (javascript/ajax) самой простой?
Ответы
Ответ 1
Вы можете переопределить набор встроенных форм для достижения желаемого. В чистом методе набора форм у вас есть доступ к вашему экземпляру Shopping через "экземпляр". Поэтому вы можете использовать модель Покупки для временного хранения вычисленной суммы и передачи ваших форм. В models.py:
class Shopping(models.Model):
shop_name = models.CharField(max_length=200)
def __init__(self, *args, **kwargs)
super(Shopping, self).__init__(*args, **kwargs)
self.__total__ = None
в admin.py:
from django.forms.models import BaseInlineFormSet
class ItemInlineFormSet(BaseInlineFormSet):
def clean(self):
super(ItemInlineFormSet, self).clean()
total = 0
for form in self.forms:
if not form.is_valid():
return #other errors exist, so don't bother
if form.cleaned_data and not form.cleaned_data.get('DELETE'):
total += form.cleaned_data['cost']
self.instance.__total__ = total
class BuyerInlineFormSet(BaseInlineFormSet):
def clean(self):
super(BuyerInlineFormSet, self).clean()
total = 0
for form in self.forms:
if not form.is_valid():
return #other errors exist, so don't bother
if form.cleaned_data and not form.cleaned_data.get('DELETE'):
total += form.cleaned_data['cost']
#compare only if Item inline forms were clean as well
if self.instance.__total__ is not None and self.instance.__total__ != total:
raise ValidationError('Oops!')
class ItemInline(admin.TabularInline):
model = Item
formset = ItemInlineFormSet
class BuyerInline(admin.TabularInline):
model = Buyer
formset = BuyerInlineFormSet
Это единственный чистый способ, которым вы можете это сделать (насколько мне известно), и все будет размещено там, где оно должно быть.
EDIT: Добавлена проверка * if form.cleaned_data *, так как формы также содержат пустые строки.
Пожалуйста, дайте мне знать, как это работает для вас!
EDIT2: Добавлена проверка на удаление форм, как правильно указано в комментариях. Эти формы не должны участвовать в расчетах.
Ответ 2
Хорошо, у меня есть решение. Это связано с редактированием кода администратора django.
В django/contrib/admin/options.py в методах add_view (строка 924) и change_view (строка 1012) укажите эту часть:
[...]
if all_valid(formsets) and form_validated:
self.save_model(request, new_object, form, True)
[...]
и замените его на
if not hasattr(self, 'clean_formsets') or self.clean_formsets(form, formsets):
if all_valid(formsets) and form_validated:
self.save_model(request, new_object, form, True)
Теперь в вашем ModelAdmin вы можете сделать что-то вроде этого
class ShoppingAdmin(admin.ModelAdmin):
inlines = (ItemInline, BuyerInline)
def clean_formsets(self, form, formsets):
items_total = 0
buyers_total = 0
for formset in formsets:
if formset.is_valid():
if issubclass(formset.model, Item):
items_total += formset.cleaned_data[0]['cost']
if issubclass(formset.model, Buyer):
buyers_total += formset.cleaned_data[0]['amount']
if items_total != buyers_total:
# This is the most ugly part :(
if not form._errors.has_key(forms.forms.NON_FIELD_ERRORS):
form._errors[forms.forms.NON_FIELD_ERRORS] = []
form._errors[forms.forms.NON_FIELD_ERRORS].append('The totals don\'t match!')
return False
return True
Это скорее взлом, чем правильное решение. Любые предложения по улучшению? Кто-нибудь думает, что это должен быть запрос функции на django?