2012-11-23 17 views
12

Sto usando Django 1.4 e voglio impostare regole di validazione che confrontino valori di linee diverse.Validazione delle linee dipendenti in django admin

Ho tre semplici classi

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

In admin.py:

class ItemInline(admin.TabularInline): 
    model = Item 

class BuyerInline(admin.TabularInline): 
    model = Buyer 

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

Così, per esempio, è possibile acquistare una bottiglia di rhum a 10 $ e uno di vodka a 8 $. Mike paga 15 $ e Tom paga 3 $.

L'obiettivo è impedire all'utente di salvare un'istanza con somme non corrispondenti: ciò che è stato pagato deve essere uguale alla somma dei costi dell'articolo (ovvero 10 + 8 = 15 + 3).

Ho provato:

  • alzando ValidationError nel metodo Shopping.clean. Ma gli inline non sono ancora aggiornati in modo pulito quindi le somme non sono corrette
  • innalzamento di ValidationError nel metodo ShoppingAdmin.save_related. Tuttavia, sollevando ValidationError, viene visualizzata una pagina di errore molto scortese da parte dell'utente, anziché reindirizzare alla pagina di modifica con un messaggio di errore.

C'è qualche soluzione a questo problema? La validazione lato client (javascript/ajax) è la più semplice?

+0

ciao, hai inventato qualcosa per questo? Affronto lo stesso identico problema. L'unica soluzione che riesco a pensare è il metodo pulito del modello in linea, ma questo produrrebbe un grande sovraccarico di db. – ppetrid

+0

Immagino che una soluzione sia quella di modificare il comportamento dell'amministratore di django. Guarda django/contrib/admin/options.py, la linea del metodo add_view 924 – Rems

risposta

23

È possibile sostituire il formset in linea per ottenere ciò che si desidera. Nel metodo clean del formset hai accesso all'istanza Shopping tramite il membro 'instance'. Pertanto, è possibile utilizzare il modello Shopping per memorizzare temporaneamente il totale calcolato e comunicare i propri moduli. In 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 

in 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 

Questo è l'unico modo pulito si può fare (al meglio delle mie conoscenze) e tutto è posto dove dovrebbe essere .

MODIFICA: Aggiunto il controllo * if form.cleaned_data * poiché i moduli contengono anche linee vuote. Per favore fatemi sapere come funziona per voi!

EDIT2: Aggiunto il controllo per i moduli che devono essere eliminati, come correttamente indicato nei commenti. Queste forme non dovrebbero partecipare ai calcoli.

+0

Impressionante! È un peccato non poter votare la tua risposta, non ho abbastanza reputazione Modifica: NVM alcuni punti di reputazione apparivano magicamente – Rems

+1

Dovrebbe ignorare le righe cancellate con: '' se form.cleaned_data.get ('DELETE'): continue'' –

+1

Questa è una bella strategia, grazie. Ho un problema, però, perché quando non ci sono inline adde d, i messaggi di errore non si verificano. Nel mio codice, ho definito un solo formset inline perché lo sto confrontando con un campo nel modello principale (quindi nell'esempio sopra, in "BuyerInlineFormSet", utilizzerei il confronto "se self.instance.amount! = Total : raise ... '. Quando salvo l'istanza di' Shopping' con un importo> 0 e senza aggiungere 'Buyers', mi dice che il modulo è valido, anche se non lo è (perché la somma di nessun importo dell'acquirente è 0). – jenniwren

-2

OK ho una soluzione. Implica la modifica del codice di amministrazione di django.

In django/contrib/admin/options.py, nel add_view (linea 924) e change_view (linea 1012) metodi, individuare questa parte:

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

e sostituirlo con

 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) 

Ora nel tuo ModelAdmin, si può fare qualcosa di simile

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 

questo è più un hack che una soluzione adeguata t hough. Qualche suggerimento per il miglioramento? Qualcuno pensa che questa dovrebbe essere una richiesta di funzionalità su django?

+0

È più di un attacco perché dobbiamo aggiungere manualmente gli errori nell'elenco piuttosto che generare un errore Validation. Ma funziona comunque! Penso che questo sia fondamentalmente una questione di validazione del formset. In questo senso, potrebbe essere possibile creare una classe FormSet personalizzata, implementare un metodo pulito appropriato e utilizzare tale classe invece del formset predefinito nella riga inline. Solo un pensiero .. – ppetrid

+0

Suggerisci di creare manualmente ONE FormSet? Quindi in pratica non c'è più in linea, devi gestire i salvataggi legati a mano, non c'è "aggiungi un altro pulsante", ecc ... Hai appena perso tutta la potenza degli inline :( – Rems

+0

Scusa, forse non ero chiaro , Suggerisco di sovrascrivere le forme in linea. Ho finito per pubblicare una risposta separata da quando ho trovato una soluzione per un mio progetto. – ppetrid