Niniejszy artykuł opisuje system formularzy i manipulatorów "starego" systemu. Dla Django >= 0.96 domyślnym systemem obsługi formularzy będzie nowy system zwany obecnie newforms.
Zakładamy że mamy taki model:
from django.db import models
PLACE_TYPES = (
(1, 'Bar'),
(2, 'Restauracja'),
(3, 'Kino'),
(4, 'Teatr'),
)
class Place(models.Model):
name = models.CharField(maxlength=100)
address = models.CharField(maxlength=100, blank=True)
city = models.CharField(maxlength=50, blank=True)
state = models.USStateField()
zip_code = models.CharField(maxlength=5, blank=True)
place_type = models.IntegerField(choices=PLACE_TYPES)
class Admin:
pass
def __str__(self):
return self.name
Generuje on nam śliczny formularz w Panelu Admina ale my chcemy dać użytkownikom możliwość dodawania miejsc (Place).
Manipulatory to najwyższego poziomu interfejs do dodawania i edycji obiektów. W django mamy dwa interesujące nas manipulatory:
AddManipulator i
ChangeManipulator.
Oto przykład pobierający dane wysłane POSTem i zapisujący dane jako nowy obiekt:
from django.shortcuts import render_to_response
from django.http import Http404, HttpResponse, HttpResponseRedirect
from django import forms
# importujemy nasz model
from mysite.myapp.models import Place
def naive_create_place(request):
# Tworzymy manipulator
manipulator = Place.AddManipulator()
# pobieramy dane z POST, tworzymy ich kopię
new_data = request.POST.copy()
# konwersja danych (wszystko to łańcuchy)
# na odpowiednie typy pythona dla danych pól.
manipulator.do_html2python(new_data)
# zapisujemy
new_place = manipulator.save(new_data)
# zrobione
return HttpResponse("Miejsce stworzone: %s" % new_place)
Powyższy przykład pokazuje działanie manipulatorów lecz nie jest kompletnym rozwiązaniem:
- Brak walidacji danych
- Trzeba stworzyć formularz i widok dla niego, co jest niepotrzebne...
Można stworzyć jeden widok obsługujący i formularz i zapis danych wraz z walidacją:
from django.shortcuts import render_to_response
from mojprojekt.model.models import *
from django.http import Http404, HttpResponse, HttpResponseRedirect
from django import forms
# widok formularza
def create_place(request):
manipulator = Place.AddManipulator()
if request.POST:
# dane wysłane POSTem, kopiujemy je i chcemy tworzyć nowy obiekt Place
new_data = request.POST.copy()
# Sprawdzamy błędy
# jeżeli będą formularz przeładuje się zachowując dane
# bo są one w new_data
errors = manipulator.get_validation_errors(new_data)
if not errors:
# Brak błędów czyli zapisujemy!
manipulator.do_html2python(new_data)
new_place = manipulator.save(new_data)
# przekierowanie na widok wpisów
# zawsze po wysłaniu formularza gdzieś
# przekierowuj by uniknąć dodawania klonów danych
return HttpResponseRedirect("/view/")
else:
# Brak danych POST, chcemy czysty formularz
errors = new_data = {}
# Tworzymy wrappera, szablon i resztę
form = forms.FormWrapper(manipulator, new_data, errors)
return render_to_response('create_form.html', {'form': form})
# widok wszystkich wpisów
def view_place(request):
all_entries = Place.objects.all()
return render_to_response('view.html', {'tup': all_entries})
Szablon
create_form.html:
<h1>Dodaj Miejsce:</h1>
{% if form.has_errors %}
Popraw błędy: {{ form.error_dict|pluralize }}:
{% endif %}
<form method="post" action=".">
<p>
Imię: {{ form.name }}
{% if form.name.errors %}*** {{ form.name.errors|join:", " }}{% endif %}
</p>
<p>
Adres: {{ form.address }}
{% if form.address.errors %}*** {{ form.address.errors|join:", " }}{% endif %}
</p>
<p>
Miasto: {{ form.city }}
{% if form.city.errors %}*** {{ form.city.errors|join:", " }}{% endif %}
</p>
<p>
Stan USA: {{ form.state }}
{% if form.state.errors %}*** {{ form.state.errors|join:", " }}{% endif %}
</p>
<p>
Kod pocztowy: {{ form.zip_code }}
{% if form.zip_code.errors %}*** {{ form.zip_code.errors|join:", " }}{% endif %}
</p>
<p>
Typ miejsca: {{ form.place_type }}
{% if form.place_type.errors %}*** {{ form.place_type.errors|join:", " }}{% endif %}
</p>
<input type="submit" />
</form>
Szablon
view.html:
{% for o in tup %}
<li>{{o.name}} - {{o.city}}</li>
{% endfor %}
urls.py:
(r'^form/$', 'projekt.aplikacja.views.create_place'),
(r'^view/$', 'projekt.aplikacja.views.view_place'),
Pod
/form/ mamy formularz a pod
/view/ listę wszystkich wpisów.
Edycja wygląda podobnie, oto widok:
def edit_place(request, place_id):
# jeżeli rekord o podanym id istnieje to stwórz ChangeManipulator
try:
manipulator = Place.ChangeManipulator(place_id)
except Place.DoesNotExist:
raise Http404
# Pobieramy oryginalne dane
place = manipulator.original_object
if request.POST:
new_data = request.POST.copy()
errors = manipulator.get_validation_errors(new_data)
if not errors:
manipulator.do_html2python(new_data)
manipulator.save(new_data)
# przekierowanie
return HttpResponseRedirect("/view/")
else:
errors = {}
# Dzięki temu formularz poprawnie rozpozna dane dla każdego pola
new_data = place.__dict__
form = forms.FormWrapper(manipulator, new_data, errors)
return render_to_response('edit_form.html', {'form': form, 'place': place})
ChangeManipulator potrzebuje numeru id, który ma być edytowany. Szablon jest identyczny jak ten dla dodawania. W
urls.py podłączyć możemy to tak:
(r'^edit/(\d+)/$', 'djn.test.views.edit_place'),
I pod
/edit/NUMER/ mieć formularz edycji wpisu o danym id.
Wysyłanie plików poprzez formularze może dotyczyć pól typu FileField i ImageField w danym modelu co obsługiwane jest przez FormWrapper lub niepowiązanego z modelem zwykłego formularza. Zaczniemy od tegu drugiego. Stwórz szablon o kodzie:
<form enctype="multipart/form-data" method="post" action=".">
<input type="file" name="plik"><br />
<input type="text" name="tytul"><br />
<input type="submit" value="Wyślij">
</form>
Jest to prosty formularz z polem typu file oraz, co jest konieczne zawiera atrybut:
enctype="multipart/form-data"
w tagu FORM.
Najprostszy widok obsługujący go wyglądałby tak:
from django.shortcuts import render_to_response
def test(request):
if request.POST:
data = request.POST.copy()
data.update(request.FILES)
print data
return render_to_response('test.html')
Po wysłaniu formularza dane o plikach wysłanych razem z formularzem dostępne są pod
request.FILES. W powyższym widoku
data zawiera dane POST z formularz jak i dane o plikach dołączone poprzez:
data.update(request.FILES)
W konsoli, w której działa serwer deweloperski zobaczymy coś takiego:
<MultiValueDict: {'plik': [{'content': 'ZAWARTOŚĆ', 'content-type': 'TYP MIME', 'filename': 'NAZWA PLIKU'}], 'tytul': ['WARTOŚĆ POLA']}>
Gdzie
plik to nazwa pola typu file z szablonu. Teraz trzeba zawartość pliku zapisać na serwerze.
Nie zapominaj o walidacji typów plików ładowanych przez użytkowników. Sprawdzaj typ MIME jak i rozszerzenie pliku! Katalog docelowy (przeważnie gdzieś w /site_media/) powinien znajdować się poza katalogiem serwera tak by przesłane skrypty (Perl, PHP itp.) nie mogły być wykorzystane.
Oto zmodyfikowany widok zapisujący tylko pliki tekstowe pod nazwą podaną w polu "tytul":
from django.shortcuts import render_to_response
from django.conf import settings
def test(request):
if request.POST:
data = request.POST.copy()
data.update(request.FILES)
if data.has_key('plik') and data['plik']['content-type'] == 'text/plain' and data['plik']['filename'].split('.')[-1] == 'txt':
a = open(settings.MEDIA_ROOT + data['tytul'] + '.txt', 'w')
a.write(data['plik']['content'])
a.close()
return render_to_response('test.html')
Zmienna
settings.MEDIA_ROOT to MEDIA_ROOT z settings.py i zawiera pełną ścieżkę do katalogu plików statycznych (/site_media)
W przypadku pól typu file powiązanych z modelem, generowany przez FormWrapper stosujemy w szablonie:
{{ form.POLE }}{{ form.POLE_file }}
Gdzie
POLE to nazwa pola typu FileField czy ImageField w naszym modelu. Od strony widoku różnic nie ma. Zalecam również zapoznanie się z
dokumentacją tych pól.
Można tworzyć własne manipulatory, jeżeli chcemy ustawić specjalne reguły walidacji na naszym formularzu. Przykład - wysyłanie emaili z formularza przez zalogowanych użytkowników:
# manipulator
class PMessage(forms.Manipulator):
def __init__(self):
self.fields = (
forms.TextField(field_name="subject", length=30, maxlength=200, is_required=True),
forms.LargeTextField(field_name="contents", is_required=True),
)
# widok
def send_pmessage(request, target_user):
# czy jestem zalogowany i nie wysyłam do siebie
if request.user.is_authenticated() and str(request.user) != str(target_user):
# używamy naszego manipulatora
manipulator = PMessage()
if request.POST:
new_data = request.POST.copy()
errors = manipulator.get_validation_errors(new_data)
if not errors:
manipulator.do_html2python(new_data)
# new_data zawiera dane gotowe do wykorzystania
# tutaj zamiast do bazy wysyłane są emailem
from django.core.mail import send_mail
ruser = User.objects.get(username=str(request.user))
send_mail(new_data['subject'], new_data['contents'], request.user.email, [ruser.email], fail_silently=True)
return HttpResponseRedirect("/user/")
else:
errors = new_data = {}
form = forms.FormWrapper(manipulator, new_data, errors)
return render_to_response('userpanel/pmessage.html', {'form': form})
else:
return HttpResponseRedirect("/user/")
Z formularzem:
<h2>{% trans "Send a Private Message" %}</h2><form action="." method="post">
<div class="content">
<table>
<tr class="rowA">
<td class="first" style="width:25%;"><b>{% trans "Subject" %}</b></td>
<td>{{ form.subject }}{% if form.subject.errors %}<br />*** {{ form.subject.errors|join:", " }}{% endif %}</td>
</tr>
<tr class="rowB">
<td><b>{% trans "Text" %}</b></td>
<td>{{ form.contents }}{% if form.contents.errors %}<br />*** {{ form.contents.errors|join:", " }}{% endif %}</td>
</tr>
</table>
</div>
<div class="box"><br /><div style="text-align:center;"><input type="submit" value="{% trans "Send Message" %}" style="actiontable"></div><br /></div>
</form>
Klasę manipulatora wraz z widokiem umieszczamy w views.py. Najważniejsza różnica w kodzie widoku to:
manipulator = PMessage()
Tworzymy instancję własnego manipulatora. Celem naszego manipulatora jest opisanie pól, ich nazw, typów oraz reguł walidacji:
self.fields = (
forms.TextField(field_name="subject", length=30, maxlength=200, is_required=True),
forms.LargeTextField(field_name="contents", is_required=True),
)
Mamy dwa pola
subject i
contents odpowiednio typu TextField i LargeTextField. Reguły walidacji są proste -
is_required oznacza iż pola są obowiązkowe do wypełnienia. Oprócz tego pole "subject" ma podane rozmiary. Zwróć uwagę że w szablonie stosujemy nazwy tych pól do budowy formularza:
{{ form.subject }} itd. Jeżeli chodzi o walidatory to mamy dostęp do serii prostych takich jak:
- isAlphaNumeric
- isAlphaNumericURL
- isSlug
- isLowerCase
- isUpperCase
- isCommaSeparatedIntegerList
- isCommaSeparatedEmailList
- isValidIPAddress4
- isNotEmpty
- isOnlyDigits
- isNotOnlyDigits
- isInteger
- isOnlyLetters
- isValidANSIDate
- isValidANSITime
- isValidEmail
- isValidImage
- isValidImageURL
- isValidPhone
- isValidQuicktimeVideoURL
- isValidURL
- isValidHTML
- isWellFormedXml
- isWellFormedXmlFragment
- isExistingURL
- isValidUSState
- hasNoProfanities
Manipulatory, FormWrapper i Walidatory zostaną zastąpione przez nowy system szablonów od Django 0.96. Stary system formularzy zostanie usunięty całkowicie w następnym wydaniu po 1.0. Tworząc nowe projekty w niedalekiej przyszłości lepszym rozwiązaniem może okazać się newforms.
Na koniec przykład własnego walidatora:
class LoginForm(forms.Manipulator):
def __init__(self):
self.fields = (forms.TextField(field_name="login", length=30, maxlength=200, is_required=True),
forms.PasswordField(field_name="password", length=30, maxlength=200, is_required=True),
forms.TextField(field_name="imgtext", is_required=True, validator_list=[self.hashcheck]),
forms.TextField(field_name="imghash", is_required=True),)
def hashcheck(self, field_data, all_data):
import sha
if not all_data['imghash'] == sha.new(field_data).hexdigest():
raise validators.ValidationError("Captcha Error.")
Jest to walidator formularza z captchą - tekstem na grafice, który trzeba wpisać. Dodatkowo w formularzu znajduje się hasz tekstu z grafiki - użytkownik wpisał poprawny tekst (pole imgtext) jeżeli hasz tego tekstu jest identyczny z tym przesłanym wraz z formularzem (pole imghash). By to sprawdzić potrzebny własny walidator definiowany jako metoda klasy
def hashcheck(self, field_data, all_data):
Gdzie nazwa metody staje się nazwą walidatora, field_data - dane pola, all_data - dane wszystkich pól. W przypadku niepoprawnej walidacji walidator powinien generować wyjątek:
raise validators.ValidationError("Tekst błędu")
Nasz walidator haszuje tekst i sprawdza czy zgadza się z haszem kontrolnym, jeżeli nie - wyjątek. Walidator przypisujemy do pola poprzez:
validator_list=[self.hashcheck]
- Dodane: 14.07.2008 przez riklaunim