Formularze w Django (Oldforms)

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.

Manipulatory Add i Change

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

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

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.

Własne Manipulatory

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]
RkBlog

Django, 14 July 2008

Comment article
Comment article RkBlog main page Search RSS Contact