Wie bei newforms die Validierung abläuft

Djangos newforms Formularen stehen 3 Methoden zur Verfügung, Eingabedaten zu prüfen.

  • Feld.clean(): Die clean-Methode des Feldes selbst. Django bringt für die jeweiligen Felder schon grundsätzliche Prüfungen mit. In ein IntegerField dürfen eben nur Ziffern eingegeben werden und ein EmailField prüft die korrekte Syntax der E-Mail-Adresse.

In der Regel ist es nicht notwendig, bestehende Feld-Clean-Methoden zu ändern, denn dafür gibt es:

  • Formular.clean_<feldname>(): In dieser Funktion kannst du penibel festlegen, welche Daten für das Feld valide sind und einen Fehler ausgeben oder diese Daten ändern.

  • Formular.clean(): Diese Funktion wird am Ende ausgeführt, wenn alle Felder einzeln schon geprüft worden sind.

In allen Funktionen hat das Formular stets Zugriff auf die eingegeben Daten, denn diese stehen im Dict self.cleaned_data. Wofür ist also die "globale" clean() Methode gut?

Dazu ist es wichtig zu wissen, wie der Ablauf der Validierung von Newforms-Formularen stattfindet. Vergleiche einmal dieses Formular und dessen Ausgabe:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from django import newforms as forms

class Form(forms.Form):
    name = forms.CharField()
    plz = forms.IntegerField()
    ort = forms.CharField()

    def clean_name(self):
        print "Vorhandene Daten bei 'name': %s" % self.cleaned_data
        return self.cleaned_data['name']

    def clean_plz(self):
        print "Vorhandene Daten bei 'plz': %s" % self.cleaned_data
        return self.cleaned_data['plz']

    def clean_ort(self):
        print "Vorhandene Daten bei 'ort': %s" % self.cleaned_data
        return self.cleaned_data['ort']

    def clean(self):
        print "Die Formular clean() Methode. Spätestens jetzt sind alle Daten" \
              "vorhanden: %s" % self.cleaned_data
        return self.cleaned_data

test_data = {'name': 'Martin', 'plz': '18581', 'ort': 'Putbus'}
f = Form(test_data)
f.as_ul()

Ausgabe:

1
2
3
4
5
6
>python -u test.py
Vorhandene Daten bei 'name': {'name': u'Martin'}
Vorhandene Daten bei 'plz': {'name': u'Martin', 'plz': 18581}
Vorhandene Daten bei 'ort': {'ort': u'Putbus', 'name': u'Martin', 'plz': 18581}
Die Formular clean() Methode. Spätestens jetzt
sind alle Datenvorhanden: {'ort': u'Putbus', 'name': u'Martin', 'plz': 18581}

Zur Erläuterung. Die Validierung im Formular geht der Reihe nach, anhand der Felder von oben nach unten. In diesem Formular ist es also dieses Schema:

  1. 'name' den Wert aus den Daten zuweisen
  2. name.clean() - Die interne clean-Funktion eines CharFields
  3. f.clean_name() - Unsere selbst definierte clean-Funktion für 'name'
  4. 'plz' den Wert aus den Daten zuweisen
  5. plz.clean() - Die interne clean-Funktion eines IntegerFields
  6. f.clean_plz() - Unsere selbst definierte clean-Funktion für 'plz'
  7. 'ort' den Wert aus den Daten zuweisen
  8. ort.clean() - Die interne clean-Funktion eines CharFields
  9. f.clean_ort() - Unsere selbst definierte clean-Funktion für 'ort'
  10. f.clean() - Die "globale" Clean-Methode wird erst am Ende ausgeführt.

In der Ausgabe siehst du auch, das dir in der Funktion "clean_name" (unser erstes Feld) noch gar keine Werte für die Felder "plz" und "ort" zur Verfügung stehen. Nochmal: die Validierung und Zuweisung der Daten läuft von oben nach unten, in der Reihenfolge der Felder.

Warum kann man also schon im ersten Feld nicht auf alle Eingabedaten zugreifen? Hypothese: Wozu soll das Formular auch alle Datenfelder durchlaufen, wenn schon im ersten Feld 'name' ein Fehler (ValidationError) auftreten kann.

Aha, toll

Ok, kommen wir endlich zum springenden Punkt für diesen Artikel. In der Regel wird ein Formular nicht nur pro Feld validiert sondern auch in Kombination. Beispiele währen:

  • 2 Passwort-Felder müssen das selbe Passwort haben
  • Möchte ich per Lastschrift zahlen, muss ich auch Kontodaten hinterlegen
  • Wünsche ich Antwort per E-Mail, muss ich auch eine E-Mail-Adresse hinterlegen, sonst reicht mein Name

Und so weiter, das ganze hängt ja vom Anwendungsfall ab und die Möglichkeiten so eines Formulares sind unbegrenzt. Nun endlich der springende Punkt, und ich würde das ganze hier nicht schreiben, wenn es nicht ein häufiges (Anfänger)Problem währe:

Willst du mehrere Felder miteinander vergleichen gehört diese Logik in die clean()-Funktion des Formulares, nicht in die des Feldes! Der Grund dafür steht ja schon oben.

Nun aber das Aber. Der Grund warum so viele Leute die Validierungslogik in eine clean_<feldname> Funktion packen – obwohl es mehrere Felder betrifft – ist, dass Fehlermeldungen aus der clean() Funktion immer ganz oben, vor allen Feldern angezeigt werden. Die so genannten non_field_errors, also Fehlermeldungen die nicht an ein Feld gebunden sind.

Hier ein Beispiel dass die Felder 'ort' und 'name' miteinander prüft:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from django import newforms as forms

class Form(forms.Form):
    name = forms.CharField()
    plz = forms.IntegerField()
    ort = forms.CharField()

    def clean(self):
        if self.cleaned_data['name'] == 'Meier' and \
           self.cleaned_data['ort'] == 'Berlin':
            raise forms.ValidationError('Es gibt keinen "Meier" in Berlin.' \
                  'Bitte legen Sie sich einen anderen Namen zu.')

test_data = {'name': 'Meier', 'plz': u'10627', 'ort': 'Berlin'}
f = Form(test_data)
print f.as_ul()

In der Ausgabe des Formulares steht der Fehler ganz oben:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<!--
    non_field_erros Fehlermeldung
    (normalerweise ganz oben)
-->
<li><ul class="errorlist"><li>Es gibt keinen "Meier" in Berlin.Bitte legen Sie sich einen anderen Namen zu.</li></ul></li>

<!--
    Weiter gehts mit den Feldern, die aber
    keine eigenen Fehlermeldungen besitzen
-->
<li><label for="id_name">Name:</label> <input type="text" name="name" value="Meier" id="id_name" /></li>
<li><label for="id_plz">Plz:</label> <input type="text" name="plz" value="10627" id="id_plz" /></li>
<li><label for="id_ort">Ort:</label> <input type="text" name="ort" value="Berlin" id="id_ort" /></li>

Möchte man aber im Nachhinein noch einem Feld einen Fehlertext zuweisen, so geht das einfach über das Dict self.errors[<feldname>] (es erwartet für das Feld ein Liste oder Tupel, über die es iterieren kann):

1
2
3
4
5
6
7
8
#!/usr/bin/env python
class Form(forms.Form):
    # ...
    def clean(self):
        if self.cleaned_data['name'] == 'Meier' and \
           self.cleaned_data['ort'] == 'Berlin':
            self.errors['name'] = ('Es gibt keinen "Meier" in Berlin.',)
            raise forms.ValidationError('Es sind Fehler aufgetreten:')

In der Ausgabe hat unser Feld nun seine eigene Fehlermeldung:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<!--
    non_field_erros Fehlermeldung
    (normalerweise ganz oben)
-->
<li><ul class="errorlist"><li>Es sind Fehler aufgetreten:</li></ul></li>

<!--
    Unser Namensfeld mit einer eigenen Fehlermeldung
-->
<li>
    <ul class="errorlist"><li>Es gibt keinen &quot;Meier&quot; in Berlin.</li></ul>
    <label for="id_name">Name:</label> <input type="text" name="name" value="Meier" id="id_name" />
</li>
<li><label for="id_plz">Plz:</label> <input type="text" name="plz" value="10627" id="id_plz" /></li>
<li><label for="id_ort">Ort:</label> <input type="text" name="ort" value="Berlin" id="id_ort" /></li>

So weit... viel Spass weiterhin mit newforms. Smiley:  :-)

Python difflib Praxisbeispiel

Für eine Communityseite möchte ich, wenn ein Beitrag geändert wird, dem Vorautor eine E-Mail mit den Änderungen schicken. Die klassische Ausgabe eines Diffs gefällt mir besonders gut. Hier ist ein kleines Beispiel wie das Pythonmodul difflib zu gebrauchen ist:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import difflib

a = """
Lorem ipsum dolor sit amen, consectetuer adipiscing elit. Nunc ac ante sed
ante imperdiet auctor. Fusce dignissim, magna eu feugiat tincidunt, nibh metus
tincidunt augue, quis ullamcorper lorem pede a ante.
"""

b = """
Lorem ipsum dolor sit amen, consectetuer adipiscing eliot. Nunc ac ante sed
ante imperdiet auctor. Fusce dignissim, magna eu feugiat tincidunt, nibh metus
tincidunt augue, quis ullamcorper lorem pede a onte!
"""

d = difflib.unified_diff(
    a.splitlines(),
    b.splitlines(), 
    'Original',
    'Current',
    lineterm=''
)

print '\n'.join(d)

Und die Ausgabe:

1
2
3
4
5
6
7
8
9
--- Original 
+++ Current 
@@ -1,4 +1,4 @@

-Lorem ipsum dolor sit amen, consectetuer adipiscing elit. Nunc ac ante sed
+Lorem ipsum dolor sit amen, consectetuer adipiscing eliot. Nunc ac ante sed
 ante imperdiet auctor. Fusce dignissim, magna eu feugiat tincidunt, nibh metus
-tincidunt augue, quis ullamcorper lorem pede a ante.
+tincidunt augue, quis ullamcorper lorem pede a onte!

Wer eher nach einer farbigen, detailierten Ausgabe wie bei Trac sucht (hier im Beispiel übernimmt Pygments im Nachhinein das Highlighting), sollte sich auch das HtmlDiff Modul anschauen.

Zusätzliche Templatedaten in newforms-Formulare einfügen.

Ok, das ist schon ein kryptischer Titel aber es ist etwas ganz Einfaches gemeint.

Djangos newforms Formulare lassen sich auf einfache und schnelle Weise mit den Methoden as_ul(), as_p() und as_table() ausgeben. Leider werden die Felder dann aber der Reihe nach ausgegeben, einen Zwischentitel oder ähnliches einzufügen ist dann nicht mehr möglich.

1
2
3
<ol>
{{ form.as_ul }}
</ol>

Angenommen im Formular befinden sich folgende Felder:

  • username
  • emailadresse
  • passwort
  • passwort (wiederholung)
  • Vorname
  • Nachname
  • Homepage-URL

Wobei die ersten 4 Felder Pflichtfelder sind, die letzten 3 aber optional. Über diese letzten 3 Felder möchte ich eine Titelzeile "Die folgenden Felder sind optional" setzen.

Nun ist es einfacher weise möglich, das Formular per Hand aufzubauen:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<ol>
<li>
  <label>{{ form.username.label_tag }}</label>
  {{ form.username }}
  {{ form.username.help_text}}
</li>    
...    
<li class="title">Die folgenden Felder sind optional</li>
<li>
  <label>{{ form.first_name.label_tag }}</label>
  {{ form.first_name }}
  {{ form.first_name.help_text}}
</li>    
...
</ol>

Sicherlich erreicht man so die maximale Flexibilität aber es ist auch fehleranfällig wenn sich ein Feld(name) ändert und es muss immer Hand angelegt werden, wenn ein neues Feld hinzukommt oder entfernt wird. Also zurück zu den direkten Formular Ausgabemethoden:

1
2
3
<ol>
{{ form.as_ul }}
</ol>

Über die Felder im Formular lässt sich auch iterieren! form.as_ul() ist in der Ausgabe identisch zu diesem Schnipsel:

1
2
3
4
5
6
7
8
9
{{ form.non_field_errors }}
{% for field in form %}
<li>
    {% if field.errors %}{{ field.errors }}{% endif %}
    {{ field.label_tag }}
    {{ field }}
    {% if field.help_text %}{{ field.help_text }}{% endif %}
</li>
{% endfor %}

Ich wollte ja über die letzten 3 Felder eine Titelzeile "Die folgenden Felder sind optional" setzen. Das erste der 3 Felder ist "Vorname" und da wir wissen wie dieses Feld heißt, können wir auch darauf prüfen.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{{ form.non_field_errors }}
{% for field in form %}

{% ifequal field.label "First name" %}
<li class="spacer">
    Die folgenden Felder sind optional.
</li>
{% endifequal %}

<li>
    {% if field.errors %}{{ field.errors }}{% endif %}
    {{ field.label_tag }}
    {{ field }}
    {% if field.help_text %}{{ field.help_text }}{% endif %}
</li>
{% endfor %}

Auf diese Weise lassen sich einfach Daten vor, nach oder selbst in bestimmte Felder einfügen – ohne dass die Flexibilität von newforms Formularen gefährdet ist.