Skróty: render_to_response
Twórcy Django są bardzo skuteczni w eliminowaniu miejsc, które wymagałyby złamania zasady DRY. Tym dziwniejszy jest wyjątek render_to_response: teoretycznie skrót, w praktyce – funkcja wymagająca codziennego wpisywania prawie identycznego kodu.
Wygląda to tak:
from django.shortcuts import render_to_response from django.template import RequestContext def some_view(request): return render_to_response('testapp/some_view.html', context_instance = RequestContext(request), dictionary = {'title': 'A very simple view', 'text': 'Some text here'})
Parametr context_instance w praktyce zawsze wygląda tak samo. Nazwa szablonu często, chociaż nie zawsze, przypomina nazwę funkcji obsługującej dany adres. Parametr dictionary bywa na tyle rozbudowany, że najczęściej buduje się go wcześniej, żeby uniknąć bardzo długich lub sztucznie podzielonych linii. Łączny poziom powtarzalności jest bardzo wyraźny po napisaniu kilku-kilkunastu widoków, zwłaszcza jeśli część z nich zawiera więcej niż jedną ścieżkę kończącą się wywołaniem render_to_response.
A przecież to może wyglądać tak:
@with_template def simple_view(request): return {'title': 'A very simple view', 'text': 'Some text here'}
W którymś momencie ta powtarzalność zirytowała mnie na tyle, że poświęciłem trochę czasu na napisanie odpowiedniego dekoratora. Dla lenistwa.
Działa tak:
- jeśli funkcja zwraca
HttpResponse, to dekorator nic nie zmienia – przekazuje ten wynik wyżej, - w przeciwnym wypadku zakłada, że wynikiem jest słownik (lub coś, co słownik udaje). W takiej sytuacji przekazuje go do
render_to_response, zakładając że wymagany szablon to "nazwa_aplikacji/nazwa_funkcji.html", acontext_instanceto standardowyRequestContext. - dodatkowo, funkcja może zmienić nazwę szablonu: wystarczy, że w słowniku zwróci element 'template_name'.
Przykłady:
# plik testapp/views.py @with_template def simple_view(request): return {'title': 'A very simple view', 'text': 'Some text here'} @with_template def simple_view_with_locals(request): title = 'Another simple view' text = 'Some text here' return locals() @with_template def complex_view(request): if request.REQUEST.get('redirect_me'): return HttpResponseRedirect('http://google.com/') else: template_name = 'testapp/some_other_template.html' return locals() @with_template('testapp/yet_another_template.html') def view_with_template_override(request): title = 'Another simple view' text = 'Some text here' return locals() @login_required @with_template def authenticated_view(request): text = 'Welcome, ' + request.user.username return locals()
Można też po prostu zwrócić locals() – słownik zawierający wszystkie zmienne lokalne. Możliwe jest też, jak w ostatnim przykładzie, użycie innych dekoratorów, na przykład login_required.
Dekorator with_template ma też dodatkową, dużą zaletę: ułatwia wprowadzenie konwencji, zgodnie z którą nazwa szablonu wynika z nazwy aplikacji i funkcji. Konwencje, ogólnie, to dobra rzecz. Ta konkretna sprawia, że oszczędza się czas który normalnie poświęca się na decyzję jak nazwać szablon, wpisanie jego nazwy oraz późniejsze przeglądanie kodu żeby dowiedzieć się, jakiego szablonu należy się spodziewać pod danym adresem.
Owszem, można – jak zauważył ktoś podczas europythonowej prezentacji – dopisać informację o nazwie szablonu do dokumentacji funkcji, ale konwencje działają jak makra lub funkcje w dokumentacji: określa się je w jednym miejscu i stosuje w wielu, bez potrzeby zaglądania do tekstu.
# plik testapp/urls.py from django.conf.urls.defaults import * urlpatterns = patterns('testapp.views', (r'^normal/', 'normal_view'), (r'^simple/', 'simple_view'), (r'^complex/', 'complex_view'), (r'^authenticated/', 'authenticated_view'), (r'^view_with_template_override/', 'view_with_template_override'), )
I trochę testów:
# plik testapp/tests.py class WithTemplateTestCase(TestCase): def test_views(self): response = self.client.get('/app/simple/') self.assertTemplateUsed(response, 'testapp/simple_view.html') response = self.client.get('/app/complex/') self.assertTemplateUsed(response, 'testapp/some_other_template.html') response = self.client.get('/app/complex/', data = {'redirect_me': '1'}) self.assertEqual(response.status_code, 302) response = self.client.get('/app/view_with_template_override/') self.assertTemplateUsed(response, 'testapp/yet_another_template.html') from django.contrib.auth.models import User user, created = User.objects.get_or_create(username = 'marcink') user.set_password('marcink') user.save() self.client.login(username='marcink', password = 'marcink') response = self.client.get('/app/authenticated/') self.assertTemplateUsed(response, 'testapp/authenticated_view.html')
A sam dekorator wygląda tak:
import re from django.http import HttpResponse from django.shortcuts import render_to_response from django.template import RequestContext try: # functools appeared in Python 2.5 from functools import wraps, update_wrapper except ImportError: # luckily for us, Django already backported it from django.utils.functional import wraps, update_wrapper def with_template(arg): """ A view decorator that handles rendering. If the view returns a HttpResponse, it is passed intact; otherwise the returned value is passed as dictionary to render_to_response. Usage samples: @with_template def view_func(request, ...): return ... @with_template('custom/template/name.html') def other_view_func(request, ...): return ... """ class TheWrapper(object): def __init__(self, default_template_name): self.default_template_name = default_template_name def __call__(self, func): def decorated_func(request, *args, **kwargs): ret = func(request, *args, **kwargs) if isinstance(ret, HttpResponse): return ret return render_to_response(ret.get('template_name', self.default_template_name), context_instance=RequestContext(request), dictionary=ret) update_wrapper(decorated_func, func) return decorated_func if not callable(arg): return TheWrapper(arg) else: app_name = re.search('([^.]+)[.]views', arg.__module__).group(1) default_template_name = ''.join([app_name, '/', arg.__name__, '.html']) return TheWrapper(default_template_name)(arg)
Pełen kod można znaleźć tutaj: testowy projekt with_template
Aktualizacja 2008/10/13: zgodnie z sugestią urbana dodałem możliwość przekazania nazwy szablonu jako parametru dekoratora.
O autorze: nazywam się Marcin Kaszyński i od ponad 10 lat zajmuję się tworzeniem oprogramowania, od projektowania przez programowanie do zarządzania projektami włącznie. Prowadzę warsztaty Django, będące szybkim i łatwym sposobem na poznanie tego środowiska i rozpoczęcie pracy z pełnym wykorzystaniem jego możliwości.

October 13th, 2008 17:35
czy jest możliwośc, aby nazwa szablonu była podawana jako parametr do decoratora (np. @with_template(’szablon’)?
October 13th, 2008 20:40
Dobry pomysł. Zmieniłem dekorator tak, żeby można go było użyć też w taki sposób.
Dzięki
October 16th, 2008 21:09
Coś podobnego jest też tutaj:
http://code.google.com/p/django-page-cms/source/browse/trunk/pages/utils.py