strona główna

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", a context_instance to standardowy RequestContext.
  • 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.

3 Responses to “Skróty: render_to_response”

  1. urban
    October 13th, 2008 17:35
    1

    czy jest możliwośc, aby nazwa szablonu była podawana jako parametr do decoratora (np. @with_template(’szablon’)?

  2. Marcin Kaszyński
    October 13th, 2008 20:40
    2

    Dobry pomysł. Zmieniłem dekorator tak, żeby można go było użyć też w taki sposób.

    Dzięki :)

  3. Robert
    October 16th, 2008 21:09
    3

    Coś podobnego jest też tutaj:

    http://code.google.com/p/django-page-cms/source/browse/trunk/pages/utils.py

Dodaj komentarz