Class-based (generic) views in Django 1.3

by Łukasz Rekucki (lrekucki@gmail.com)

http://lqc.github.com/djangopiwo/april2011/

O mnie

2004-2009:

Student Kolegium MISMAP na UW (Informatyka, Matematyka i trochę Fizyki)

Około 2008/2009

Zacząłem pracować razem z Marekiem Stępniowskim nad projektem Wolnelektury.pl.

Marzec 2010

Dołączyłem do zespołu Smartupz.com.

Sierpień 2010

Mój pierszy patch przyjęty do Django. Yey! (nic wielkiego, ale zawsze :P)

Aktualnie

Nadal w Smartupz i nadal pracuję w Django.

[Porozmawiaj ze mną o integracji z Babel :)]

Smartupz Logo

Czym jest widok ?

view (n.):
Funkcja, która bierze jako argument WSGIRequest oraz parametry dopasowane do ścieżki URL i zwraca HTTPResponse.
1 def article_list(request):
2     articles = Article.objects.all()
3     return render_to_response("article_list.html", {"articles": articles}, 
4                context_instance=RequestContext(request))

Talk about how to extend this view to be more reusable

Be DRY!

 1 def object_list(request, queryset, paginate_by=None, page=None,
 2         allow_empty=True, template_name=None, template_loader=loader,
 3         extra_context=None, context_processors=None, template_object_name='object',
 4         mimetype=None):
 5     if extra_context is None: extra_context = {}
 6     queryset = queryset._clone()
 7     if paginate_by:
 8         paginator = Paginator(queryset, paginate_by, allow_empty_first_page=allow_empty)
 9         if not page:
10             page = request.GET.get('page', 1)
11         try:
12             page_number = int(page)
13         except ValueError:
14             if page == 'last':
15                 page_number = paginator.num_pages
16             else:
17                 # Page is not 'last', nor can it be converted to an int.
18                 raise Http404
19         try:
20             page_obj = paginator.page(page_number)
21         except InvalidPage:
22             raise Http404
23         c = RequestContext(request, {
24             '%s_list' % template_object_name: page_obj.object_list,
25             'paginator': paginator,
26             'page_obj': page_obj,
27             'is_paginated': page_obj.has_other_pages(),
28         }, context_processors)
29     else:
30         c = RequestContext(request, {
31             '%s_list' % template_object_name: queryset,
32             'paginator': None,
33             'page_obj': None,
34             'is_paginated': False,
35         }, context_processors)
36         if not allow_empty and len(queryset) == 0:
37             raise Http404
38     for key, value in extra_context.items():
39         if callable(value):
40             c[key] = value()
41         else:
42             c[key] = value
43     if not template_name:
44         model = queryset.model
45         template_name = "%s/%s_list.html" % (model._meta.app_label, model._meta.object_name.lower())
46     t = template_loader.get_template(template_name)
47     return HttpResponse(t.render(c), mimetype=mimetype)

Talk about how this view is incomplete: different Paginator, Jinja2 templates, GET field for page.

 1 def twitter_login_done(request):
 2     request_token = request.session.get('request_token', None)
 3     verifier = request.GET.get('oauth_verifier', None)
 4     denied = request.GET.get('denied', None)
 5     # If we've been denied, put them back to the signin page
 6     # They probably meant to sign in with facebook >:D
 7     if denied:
 8         return HttpResponseRedirect(reverse("socialauth_login_page"))
 9     # If there is no request_token for session,
10     # Means we didn't redirect user to twitter
11     if not request_token:
12         # Redirect the user to the login page,
13         return HttpResponseRedirect(reverse("socialauth_login_page"))
14     token = oauth.Token.from_string(request_token)
15     # If the token from session and token from twitter does not match
16     # means something bad happened to tokens
17     if token.key != request.GET.get('oauth_token', 'no-token'):
18         del_dict_key(request.session, 'request_token')
19         # Redirect the user to the login page
20         return HttpResponseRedirect(reverse("socialauth_login_page"))
21     try:
22         twitter = oauthtwitter.TwitterOAuthClient(TWITTER_CONSUMER_KEY, TWITTER_CONSUMER_SECRET)
23         access_token = twitter.fetch_access_token(token, verifier)
24         request.session['access_token'] = access_token.to_string()
25         user = authenticate(twitter_access_token=access_token)
26     except:
27         user = None
28     # if user is authenticated then login user
29     if user:
30         login(request, user)
31     else:
32         # We were not able to authenticate user
33         # Redirect to login page
34         del_dict_key(request.session, 'access_token')
35         del_dict_key(request.session, 'request_token')
36         return HttpResponseRedirect(reverse('socialauth_login_page'))
37     # authentication was successful, use is now logged in
38     next = request.session.get('twitter_login_next', None)
39     if next:
40         del_dict_key(request.session, 'twitter_login_next')
41         return HttpResponseRedirect(next)
42     else:
43         return HttpResponseRedirect(LOGIN_REDIRECT_URL)

Pony Magic

view (n.):
Funkcja Obiekt typu Callable, który bierze jako argument WSGIRequest oraz parametry dopasowane do ścieżki URL i zwraca HTTPResponse.

W Pythonie, dowolny obiekt może być wywoływalny, np.:

  • funkcja
  • klasa
  • instancje klas z metodą __call__

Krótka historia klasy View

  • Istnieje wiele różnorodnych podejść do problemu. Większość opisana na wiki.

    • __call__() and copy() (bad!)
    • __new__() (good!)
    • classmethod
    • classmethod2 (winner!)
  • Każde ma swoje wady i zalety. Jednak większość różnic jest czysto kosmetyczna → Ponad 200 wiadomości na django–developers na ten temat. Nie wszystkie bardzo wartościowe.

Klasa View

 1 class View(object):
 2 
 3     @classonlymethod
 4     def as_view(cls, **initargs):
 5         def view(*args, **kwargs):
 6             self = cls(**initargs)
 7             return self.dispatch(*args, **kwargs)
 8         update_wrapper(view, cls, updated=())
 9         update_wrapper(view, cls.dispatch, assigned=())
10         return view
11 
12     def dispatch(self, request, *args, **kwargs):
13         if request.method.lower() in self.http_method_names:
14             handler = getattr(self, request.method.lower(), 
15                 self.http_method_not_allowed)
16         else:
17             handler = self.http_method_not_allowed
18         self.request = request
19         self.args = args
20         self.kwargs = kwargs
21         return handler(request, *args, **kwargs)
22 
23 # urls.py
24 url('^sample-view/$', View.as_view(), name="sample_view")

Prosty przykład

 1 class ArticleList(View):
 2     template_name = "article_list.html"
 3 
 4    def get(self, request, *args, **kwargs):
 5         self.article_list = self.get_queryset()
 6         return self.render_to_response(
 7             self.get_context_data(articles=self.article_list))
 8 
 9     def render_to_response(self, context):
10         return render_to_response(self.get_template_names(),
11             context, RequestContext(self.request))
12 
13     def get_queryset(self):
14         return Article.objects.all()
15 
16     def get_template_names(self):
17         return [self.template_name] if self.template_name is not None else []
18 
19     def get_context_data(self, **kwargs):
20         return kwargs
21 
22 # urls.py
23 url('^articles/$', ArticleList.as_view(template_name="alternative.html"))

Mixins

Wielodziedziczenie pomaga w ponownym wykorzystaniu kodu:

 1 from django.views.generic.base import TemplateResponseMixin, View
 2 from django.views.generic.list import ListView, MultipleObjectMixin
 3 
 4 class ArticleList(TemplateResponseMixin, MultipleObjectMixin, View):
 5     model = Article 
 6     template_name = "articles/article_list.html"
 7 
 8     def get(self, request, *args, **kwargs):
 9         self.object_list = self.get_queryset()
10         context = self.get_context_data(object_list=self.object_list)
11         return self.render_to_response(context)
12 
13 class ArticleListTwo(ListView):
14     model = Article

Można też nadpisać funkcjonalność, a nie tylko rozszerzać:

1 class JSONArticles(JSONMixin, ArticleList):
2     pass

Więcej generycznych widoków

Wszystkie dotychczasowe generyczne widoki zostały zmigrowane do CBV.

  • direct_to_templateTemplateView
  • redirect_toRedirectView
  • object_listListView
  • object_detailDetailView
  • create_objectCreateView
  • archive_yearYearArchiveView
  • etc.

Są one posklejane z mixinów takich jak:

  • FormMixin - walidacja formularzy, przetwarzanie GET/POST
  • ModelFormMixin - j.w. ale działa z ModelForms.

Przykład przetwarzania formularza

 1 from django.views.generic import CreateView
 2 from myapp.signals import custom_signal
 3 
 4 class CreateArticleView(CreateView):
 5 
 6     def get_form_kwargs(self):
 7         kwargs = super(CreateArticleView, self).get_form_kwargs()
 8         # domyślnie: 'data', 'files' i 'initial'
 9         kwargs['author'] = self.request.user
10         return kwargs
11 
12     def form_valid(self, form):
13         """Called when the form is valid"""
14         response = super(CreateArticleView, self).form_valid(form)
15         if self.object:
16             custom_signal.send(sender=type(self.object), instance=self.object)
17         # dodaj `message`, dodaj zadanie do Celery, itp.
18         return response

Para niecnych tricków

 1 class DelayedModelFormMixin(ModelFormMixin):
 2 
 3     def form_valid(self, form):
 4         self.object = form.save(commit=False)
 5         self.prepare_object_for_save(self.object)
 6         self.object.save()
 7         self.object.save_m2m()
 8         # wyślij sygnał...
 9         return super(ModelFormMixin, self).form_valid(form)
10 
11     def prepare_object_for_save(self, obj):
12         pass
13 
14 class CreateCommentView(DelayedModelFormMixin, CreateView):
15 
16     def prepare_object_for_save(self, obj)
17         obj.author = self.request.user

Dekorowanie

w URLconf

Ponieważ metoda as_view() zwraca zwyczajną funkcję, można jej używać ze zwykłymi dekoratorami:


1 from django.views.generic import TemplateView
2 from django.contrib.auth.decorators import login_required
3 
4 url(r'^/protected/$', login_required(
5         TemplateView.as_view(template_name="secret.html")))

Jest to najprostsze podejście, jednak po dekoracji dostajemy funkcję. Nie da się w ten sposób zintegrować funkcjonalności na stałe z klasą tak jak to było w przypadku mixinów.

Dekorator na metodzie

Django ma ukryty skarb, który pozwala łatwo dekorować metody:


1 from django.utils.decorators import method_decorator
2 
3 class ProtectedView(View):
4 
5     @method_decorator(login_required):
6     def dispatch(self, request, *args, **kwargs):
7         return super(ProtectedView, self).dispatch(request, *args, **kwargs)

Note: Dekoratory takie jak csrf_except działają poprzez ustawienie atrybutu na widoku, który jest później sprawdzany przez middleware. Aby dało się ich używać z dispatch(), klasa View kopiuję wszystkie atrybuty z tej metody na funkcję zwracaną przez as_view().

Można też dekorować inne metody tj. get(), post(), put() czy delete(), ale csrf_exempt nie będzie na nich działał (i dobrze!).

Dekorator na klasie

Niestety ten kod nie znalazł się w Django 1.3, ale jest bardzo użyteczny (Python 2.6+):

 1 def view_decorator(fdec)
 2     def decorator(cls):
 3         original = cls.as_view.im_func
 4 
 5         @functools.wraps(original)
 6         def as_view(current, **initkwargs):
 7             return fdec(original(current, **initkwargs))
 8 
 9         cls.as_view = classmethod(as_view) 
10         return cls
11     return decorator
12 
13 # użycie:
14 
15 @view_decorator(login_required)
16 class ProtectedView(TemplateView):
17     pass
18 
19 # *UWAGA*: można sobie strzelić w stopę
20 
21 ProtectedView = view_decorator(login_required)(TemplateView)

Dziękuję !

Zdjęcia pochodzą z:

Pytania i dyskusja !