Django class based views, modele i formularze

Django class based views oferują szeroki wachlarz możliwości i mogą znacząco skrócić, uprościć kod widoków. Trzeba je tylko dobrze poznać. W tym artykule przedstawię widoki powiązane z formularzami i modelami (forms.ModelForm).

Testowa aplikacja

Na potrzeby przykładów stworzyłem dwa modele:
# -*- coding: utf-8 -*-
from django.contrib.auth.models import User
from django.db import models

class Category(models.Model):
    title = models.CharField(max_length=150)

    class Meta:
        verbose_name = 'Category'
        verbose_name_plural = 'Categories'

    def __unicode__(self):
        return self.title


class Entry(models.Model):
    category = models.ForeignKey(Category)
    user = models.ForeignKey(User)
    user_number = models.IntegerField()
    system_number = models.IntegerField()

    class Meta:
        verbose_name = 'Entry'
        verbose_name_plural = 'Entries'

Model "Category" jest prosty, natomiast dla modelu "Entry" przewidziałem formularz, który ma edytować tylko jedno pole (user_number). Pozostałe pola są wymagane i trzeba będzie zapewnić wartości dla nich.

Widoki

Oto widoki dotyczące kategorii - listowanie i dodawanie kategorii:
from random import randint

from django.core.urlresolvers import reverse
from django.views.generic import ListView
from django.views.generic.edit import CreateView
from django.views.generic.edit import DeleteView
from django.views.generic.edit import UpdateView

from demo.forms import CustomAddEntryForm
from demo.models import *

class CategoryListView(ListView):
    model = Category
    template_name = "category_list.html"
    context_object_name="category_list"

category_list = CategoryListView.as_view()


class AddCategoryView(CreateView):
    model = Category
    template_name = "add_category.html"

    def get_success_url(self):
        return reverse('category-list')

add_category = AddCategoryView.as_view()

Dokumentacja CreateView ujawnia jakie metody i atrybuty posiada dzięki swoim Mixinom. W najprostszym przypadku definiujemy get_success_url (lub success_url), na który zostaniemy przekierowaniu po dodaniu obiektu.

Teraz ciąg dalszy - widoki dla "Entry":

class EntryListView(ListView):
    model = Entry
    template_name = "entry_list.html"
    context_object_name="entry_list"

    def get_context_data(self, **kwargs):
        context = super(EntryListView, self).get_context_data(**kwargs)
        context['category_id'] = self.kwargs['category_id']
        return context
    
    def queryset(self):
        category = Category.objects.get(pk=self.kwargs['category_id'])
        return Entry.objects.filter(category=category)

entry_list = EntryListView.as_view()


class AddEntryView(CreateView):
    model = Entry
    template_name = "add_entry.html"

    def get_form_class(self):
        return CustomAddEntryForm

    def get_form_kwargs(self, **kwargs):
        kwargs = super(AddEntryView, self).get_form_kwargs(**kwargs)
        if 'data' in kwargs:
            category = Category.objects.get(pk=self.kwargs['category_id'])
            random_int = randint(1,10)
            instance = Entry(user=self.request.user, system_number=random_int, category=category)
            kwargs.update({'instance': instance})
        return kwargs

    def get_success_url(self):
        return reverse('entry-list', kwargs={'category_id': self.kwargs['category_id']})

add_entry = AddEntryView.as_view()


class EditEntryView(UpdateView):
    model = Entry
    template_name = "edit_entry.html"

    def get_form_class(self):
        return CustomAddEntryForm

    def get_success_url(self):
        return reverse('entry-list', kwargs={'category_id': self.kwargs['category_id']})

edit_entry = EditEntryView.as_view()


class DeleteEntryView(DeleteView):
    model = Entry
    template_name = "entry_confirm_delete.html"

    def get_success_url(self):
        return reverse('entry-list', kwargs={'category_id': self.kwargs['category_id']})

delete_entry = DeleteEntryView.as_view()

W widoku AddEntryView zdefiniowałem metodę "get_form_kwargs", w której można operować na elementach widoku (formularzu, danych z formularza jeżeli został wysłany itd.). Można tam także podać instancję obiektu ("instance") jaki ma zostać zapisany wraz z danymi z formularza. Pozwala to ustawić wymagane wartości dla pól, których nie było w formularzu. Sam formularz jest prosty:

from django import forms

from demo.models import Entry

class CustomAddEntryForm(forms.ModelForm):
    class Meta:
        model = Entry
        fields = ['user_number']

DeleteEntryView można użyć do kasowania rekordów. Widok ten wyświetli formularz potwierdzający chęć skasowania rekordu (formularz bez pól, tylko submit). UpdateView służy do edycji już istniejących rekordów - do obu widoków musi być dopasowany jeden konkretny obiekt. Jeżeli nie definiujemy "queryset" to robione jest to automatycznie po kluczu głównym - jeżeli zastosujemy odpowiednie mapowanie zmiennych w linkach (klucz główny - pk):

from django.conf.urls import patterns, include, url

from django.contrib import admin
admin.autodiscover()

urlpatterns = patterns('',
    url(r'^$', 'demo.views.category_list', name='category-list'),
    url(r'^category/add/$', 'demo.views.add_category', name='add-category'),
    url(r'^category/(?P<category_id>[0-9]+)/$', 'demo.views.entry_list', name='entry-list'),
    url(r'^category/(?P<category_id>[0-9]+)/add_entry/$', 'demo.views.add_entry', name='add-entry'),
    url(r'^category/(?P<category_id>[0-9]+)/edit_entry/(?P<pk>[0-9]+)/$', 'demo.views.edit_entry', name='edit-entry'),
    url(r'^category/(?P<category_id>[0-9]+)/delete_entry/(?P<pk>[0-9]+)/$', 'demo.views.delete_entry', name='delete-entry'),
    url(r'^admin/', include(admin.site.urls)),
)

Dynamiczna konfiguracja formularza

Jeżeli np. walidacja formularza zależy od danych powiązanych z użytkownikiem to można przekazać do obiektu formularza potrzebne dane:

class AddEntryView(CreateView):
    model = Entry
    template_name = "add_entry.html"
    form_class = CustomAddEntryForm
    
    def get_form_kwargs(self, **kwargs):
        kwargs = super(AddEntryView, self).get_form_kwargs(**kwargs)
        if 'data' in kwargs:
            category = Category.objects.get(pk=self.kwargs['category_id'])
            random_int = randint(1,10)
            instance = Entry(user=self.request.user, system_number=random_int, category=category)
            kwargs.update({'instance': instance})
        # add something extra to form __init__
        kwargs.update({'current_user': self.request.user})
        return kwargs

    def get_success_url(self):
        return reverse('entry-list', kwargs={'category_id': self.kwargs['category_id']})

add_entry = AddEntryView.as_view()
A w klasie formularza definiujemy __init__ gdzie obsługujemy przekazane do formularza dane:
class CustomAddEntryForm(forms.ModelForm):

    def __init__(self, current_user, **kwargs):
        super(CustomAddEntryForm, self).__init__(**kwargs)
        self.current_user = current_user

    class Meta:
        model = Entry
        fields = ['user_number']
Definiując metody clean dla pól lub dla całego formularza będziemy mogli korzystać z self.current_user - obektu User (obecnie zalogowanego użytkownika - jeżeli zalogowany).
RkBlog

Django, 27 May 2012

Comment article
Comment article RkBlog main page Search RSS Contact