2012-11-23 3 views
12

Używam Django 1.4 i chcę ustawić zasady sprawdzania poprawności, które porównują wartości różnych inlines.Walidacja zależnych inlines w django admin

Mam trzy proste zajęcia

W 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) 

W admin.py:

class ItemInline(admin.TabularInline): 
    model = Item 

class BuyerInline(admin.TabularInline): 
    model = Buyer 

class ShoppingAdmin(admin.ModelAdmin): 
    inlines = (ItemInline, BuyerInline) 

Tak na przykład możliwe jest, aby kupić butelkę rhum w 10 $ i jedna wódka w cenie 8 $. Mike płaci 15 $, a Tom płaci 3 $.

Celem jest uniemożliwienie użytkownikowi zapisania instancji z sumami, które nie pasują: kwota zapłacona musi być taka sama jak suma kosztów przedmiotu (np. 10 + 8 = 15 + 3).

Próbowałem:

  • podnoszenie ValidationError w metodzie Shopping.clean. Jednak inlines nie są jeszcze zaktualizowane, więc sumy nie są poprawne.
  • Podnoszenie ValidationError w metodzie ShoppingAdmin.save_related. Ale podniesienie tutaj ValidationError daje bardzo nieprzyjazną stronę błędu użytkownika zamiast przekierowania na stronę zmiany z ładnym komunikatem o błędzie.

Czy istnieje rozwiązanie tego problemu? Czy walidacja po stronie klienta (javascript/ajax) jest najprostsza?

+0

Witam, wymyśliłeś coś dla tego? Mam taki sam problem. Jedyne rozwiązanie, jakie mogę wymyślić, to czysta metoda wbudowanego modelu, ale spowodowałoby to duże obciążenie bazy danych. – ppetrid

+0

Domyślam się, że jednym z rozwiązań jest zmiana zachowania administratora django. Spójrz na django/contrib/admin/options.py, linia metodowa add_view 924 – Rems

Odpowiedz

23

Można zastąpić zestaw formularzy Inline, aby osiągnąć to, co chcesz. W czystej metodzie formularza masz dostęp do swojej instancji Shopping za pośrednictwem członka instancji. Dlatego możesz użyć modelu Zakupów, aby tymczasowo przechowywać wyliczoną sumę i komunikować się z formularzami. W 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 

w 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 

To tylko czysty sposób można to zrobić (do mojej najlepszej wiedzy) i wszystko jest umieszczony gdzie powinien być .

EDYCJA: Dodano sprawdzanie * jeśli form.cleaned_data *, ponieważ formularze zawierają również puste wiersze. Proszę dać mi znać, jak to działa dla Ciebie!

EDIT2: Dodano sprawdzanie formularzy, które mają zostać usunięte, co zostało poprawnie wskazane w komentarzach. Formularze te nie powinny brać udziału w obliczeniach.

+0

Awesome! To wstyd, nie mogę głosować na twoją odpowiedź, nie mam dość reputacji Edit: NVM pojawiły się magiczne punkty reputacji – Rems

+1

Powinno to zignorować usunięte wiersze z: '' if form.cleaned_data.get ('DELETE'): continue'' –

+1

To jest piękna strategia, dziękuję, ale mam problem, ponieważ gdy nie ma inlines adde d, komunikaty o błędach nie występują. W moim kodzie, mam tylko jeden wbudowany zestaw formularzy zdefiniowany, ponieważ porównuję go do pola w modelu głównym (więc w powyższym przykładzie, w 'BuyerInlineFormSet', użyłbym porównania' if self.instance.amount! = Total : raise ... '. Kiedy zapisuję instancję' Shopping' z kwotą> 0 i bez dodawania jakichkolwiek 'Buyers', to mówi mi, że formularz jest ważny, mimo że tak nie jest (ponieważ suma nie ma kwot Kupującego jest 0) – jenniwren

-2

Dobra, mam rozwiązanie. Obejmuje edytowanie kodu administratora django.

W django/contrib/administratora/options.py w add_view (linia 924), a change_view metody (linia 1012) na miejscu w tej części:

 [...] 
     if all_valid(formsets) and form_validated: 
      self.save_model(request, new_object, form, True) 
     [...] 

i zastąpić go

 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) 

teraz w ModelAdmin, można zrobić coś takiego

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 

to jest bardziej hack niż rozwiązanie prawidłowego t pęcina. Jakieś sugestie ulepszeń? Czy ktokolwiek myśli, że powinno to być żądanie funkcji na django?

+0

To bardziej hack, ponieważ musimy ręcznie dołączać błędy na liście, zamiast zgłaszać błąd ValidationError. Ale nadal działa! Myślę, że to w zasadzie kwestia sprawdzania formalności. W tym sensie można utworzyć niestandardową klasę FormSet, zaimplementować odpowiednią czystą metodę i użyć tej klasy zamiast domyślnego formularza w inline. Tylko jedna myśl .. – ppetrid

+0

Czy sugerujesz ręczne tworzenie ONE FormSet? Zasadniczo nie ma już więcej linii, musisz obsługiwać zapis związany ręcznie, nie ma "dodaj kolejnego przycisku", itd ... Po prostu tracisz całą moc inlines :( – Rems

+0

Przepraszam, może nie było jasne Sugeruję nadpisywanie wbudowanych zestawów formularzy, w końcu dostałem oddzielną odpowiedź, ponieważ wymyśliłem rozwiązanie dla własnego projektu – ppetrid