63 Stimmen

Inline-Formularvalidierung in Django

Ich möchte einen ganzen Inline-Formularsatz in einem Änderungsformular für Administratoren obligatorisch machen. Wenn ich in meinem derzeitigen Szenario auf einem Rechnungsformular (in der Verwaltung) auf Speichern drücke, ist das Inline-Bestellformular leer. Ich möchte verhindern, dass Leute Rechnungen erstellen, denen keine Bestellungen zugeordnet sind.

Kennt jemand eine einfache Möglichkeit, das zu tun?

Normale Validierung wie ( required=True ) für das Modellfeld scheint in diesem Fall nicht zu funktionieren.

92voto

Daniel Roseman Punkte 565786

Dies geschieht am besten durch die Definition eines benutzerdefinierten Formularsatzes mit einer sauberen Methode, die prüft, ob mindestens eine Rechnungsbestellung existiert.

class InvoiceOrderInlineFormset(forms.models.BaseInlineFormSet):
    def clean(self):
        # get forms that actually have valid data
        count = 0
        for form in self.forms:
            try:
                if form.cleaned_data:
                    count += 1
            except AttributeError:
                # annoyingly, if a subform is invalid Django explicity raises
                # an AttributeError for cleaned_data
                pass
        if count < 1:
            raise forms.ValidationError('You must have at least one order')

class InvoiceOrderInline(admin.StackedInline):
    formset = InvoiceOrderInlineFormset

class InvoiceAdmin(admin.ModelAdmin):
    inlines = [InvoiceOrderInline]

3 Stimmen

Ich habe festgestellt, dass es möglich ist, mit 0 Aufträgen zu validieren, wenn das Löschkästchen aktiviert ist. In meiner Antwort finden Sie eine überarbeitete Klasse, die dieses Problem löst.

0 Stimmen

Vielen Dank für diese Korrektur (und Dan für die Verbesserung). Als möglicher Hinweis für andere habe ich eine "Klasse MandatoryInlineFormSet(BaseInlineFormSet)" erstellt und dann InvoiceAdminFormSet davon abgeleitet. In meinem InvoiceAdminFormSet habe ich eine clean()-Methode, die eine benutzerdefinierte Validierung durchführt, aber zunächst MandatoryInlineFromSet.clean() aufruft.

1 Stimmen

Hat bei mir auch beim Löschen funktioniert: Ersetzen Sie ------ if form.cleaned_data: ------ durch ------ if form.cleaned_data und nicht form.cleaned_data.get('DELETE', False):

22voto

Dan Breen Punkte 11912

Daniels Antwort ist ausgezeichnet und es funktionierte für mich auf ein Projekt, aber dann erkannte ich aufgrund der Art und Weise Django Formulare arbeiten, wenn Sie mit can_delete und markieren Sie das Feld löschen beim Speichern, ist es möglich, ohne Aufträge (in diesem Fall) zu validieren.

Ich habe eine Weile versucht, herauszufinden, wie man das verhindern kann. Die erste Situation war einfach - die Formulare, die gelöscht werden sollen, sollten nicht mitgezählt werden. Die zweite Situation war schwieriger... wenn alle die Löschkästchen markiert sind, dann clean nicht angerufen wurde.

Der Code ist leider nicht ganz einfach. Die clean Methode wird aufgerufen von full_clean die aufgerufen wird, wenn die error Eigenschaft zugegriffen wird. Auf diese Eigenschaft wird nicht zugegriffen, wenn ein Unterformular gelöscht wird, also full_clean wird nie aufgerufen. Ich bin kein Django-Experte, so dass dies ein schrecklicher Weg, es zu tun sein könnte, aber es scheint zu funktionieren.

Hier ist die geänderte Klasse:

class InvoiceOrderInlineFormset(forms.models.BaseInlineFormSet):
    def is_valid(self):
        return super(InvoiceOrderInlineFormset, self).is_valid() and \
                    not any([bool(e) for e in self.errors])

    def clean(self):
        # get forms that actually have valid data
        count = 0
        for form in self.forms:
            try:
                if form.cleaned_data and not form.cleaned_data.get('DELETE', False):
                    count += 1
            except AttributeError:
                # annoyingly, if a subform is invalid Django explicity raises
                # an AttributeError for cleaned_data
                pass
        if count < 1:
            raise forms.ValidationError('You must have at least one order')

5voto

Kurt Punkte 2171
class MandatoryInlineFormSet(BaseInlineFormSet):  

    def is_valid(self):
        return super(MandatoryInlineFormSet, self).is_valid() and \
                    not any([bool(e) for e in self.errors])  
    def clean(self):          
        # get forms that actually have valid data
        count = 0
        for form in self.forms:
            try:
                if form.cleaned_data and not form.cleaned_data.get('DELETE', False):
                    count += 1
            except AttributeError:
                # annoyingly, if a subform is invalid Django explicity raises
                # an AttributeError for cleaned_data
                pass
        if count < 1:
            raise forms.ValidationError('You must have at least one of these.')  

class MandatoryTabularInline(admin.TabularInline):  
    formset = MandatoryInlineFormSet

class MandatoryStackedInline(admin.StackedInline):  
    formset = MandatoryInlineFormSet

class CommentInlineFormSet( MandatoryInlineFormSet ):

    def clean_rating(self,form):
        """
        rating must be 0..5 by .5 increments
        """
        rating = float( form.cleaned_data['rating'] )
        if rating < 0 or rating > 5:
            raise ValidationError("rating must be between 0-5")

        if ( rating / 0.5 ) != int( rating / 0.5 ):
            raise ValidationError("rating must have .0 or .5 decimal")

    def clean( self ):

        super(CommentInlineFormSet, self).clean()

        for form in self.forms:
            self.clean_rating(form)

class CommentInline( MandatoryTabularInline ):  
    formset = CommentInlineFormSet  
    model = Comment  
    extra = 1

0 Stimmen

Ist es möglich, das Gleiche mit extra = 0 zu tun?

1 Stimmen

@Siva - Ich habe es gerade überprüft, und ja, Sie können extra=0 haben. Wenn Sie jedoch wollen, dass ein Kommentar (in meinem Fall) obligatorisch ist, sollten Sie dem Benutzer wahrscheinlich ein leeres Formular geben oder ihn nicht obligatorisch machen.

5voto

Ahsan Punkte 10891

@Daniel Roseman Lösung ist in Ordnung, aber ich habe einige Änderungen mit etwas weniger Code, um das gleiche zu tun.

class RequiredFormSet(forms.models.BaseInlineFormSet):
      def __init__(self, *args, **kwargs):
          super(RequiredFormSet, self).__init__(*args, **kwargs)
          self.forms[0].empty_permitted = False

class InvoiceOrderInline(admin.StackedInline):
      model = InvoiceOrder
      formset = RequiredFormSet

class InvoiceAdmin(admin.ModelAdmin):
     inlines = [InvoiceOrderInline]

Versuchen Sie dies, es funktioniert auch :)

0 Stimmen

Ups, ich wollte das nicht hochstufen. Es funktioniert nicht, wenn die "Löschen"-Kontrollkästchen aktiviert sind.

0 Stimmen

Ich habe Ihre Frage nicht verstanden. Dieser Code stellt sicher, dass jeder Invoice muss eine haben InvoiceOrder darin. Und zu diesem Zeitpunkt gibt es keine Löschkästchen!

4voto

Alexander Klimenko Punkte 2112

Die Situation hat sich etwas gebessert, muss aber noch verbessert werden. Django bietet validate_min y min_num Attribute heutzutage, und wenn min_num wird entnommen aus Inline während der Instanziierung des Formularsatzes, validate_min kann nur als init formset Argument übergeben werden. Meine Lösung sieht also in etwa so aus:

class MinValidatedInlineMixIn:
    validate_min = True
    def get_formset(self, *args, **kwargs):
        return super().get_formset(validate_min=self.validate_min, *args, **kwargs)

class InvoiceOrderInline(MinValidatedInlineMixIn, admin.StackedInline):
    model = InvoiceOrder
    min_num = 1
    validate_min = True

class InvoiceAdmin(admin.ModelAdmin):
    inlines = [InvoiceOrderInline]

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X