En el capítol 3, hem explicat les bases de les funcions vista de Django i de les URLconf. Aquest capítol entra en més detall en la funcionalitat avançada d'aquestes dues parts del marc-de-treball.
Considera aquesta URLconf, que hem fet en l'exemple del capítol 3:
from django.conf.urls.defaults import *
from mysite.views import current_datetime, hours_ahead, hours_behind, now_in_chicago, now_in_london
urlpatterns = patterns('',
(r'^now/$', current_datetime),
(r'^now/plus(\d{1,2})hours/$', hours_ahead),
(r'^now/minus(\d{1,2})hours/$', hours_behind),
(r'^now/in_chicago/$', now_in_chicago),
(r'^now/in_london/$', now_in_london),
)
Com hem explicat en el capítol 2, cada entrada de la URLconf inclou la seva funció vista associada, passada directament com una funció objecte. Això vol dir que necessàriament cal importar les funcions vista al principi del mòdul.
Però com que una aplicació Django creix de forma complexa, les seves URLconf també creixen, i mantenir aquests imports pot ser tediós de gestionar. (Per cada nova funció vista, hauràs de recordar d'importar-ho, i la línia import tendeix a créixer molt si ho utilitzes així). És possible evitar aquesta tediosa forma de treballar important el propi mòdul views. Aquest exemple de URLconf és equivalent a l'anterior:
from django.conf.urls.defaults import *
from mysite import views
urlpatterns = patterns('',
(r'^now/$', views.current_datetime),
(r'^now/plus(\d{1,2})hours/$', views.hours_ahead),
(r'^now/minus(\d{1,2})hours/$', views.hours_behind),
(r'^now/in_chicago/$', views.now_in_chicago),
(r'^now/in_london/$', views.now_in_london),
)
Django ofereix altres formes d'especificar les funcions vista d'un patró en particular en un URLconf: Pots passar-li una cadena de text que contingui el nom del mòdul i el nom de la funció en comptes de l'objecte funció. Continuant amb l'exemple tenim:
from django.conf.urls.defaults import *
urlpatterns = patterns('',
(r'^now/$', 'mysite.views.current_datetime'),
(r'^now/plus(\d{1,2})hours/$', 'mysite.views.hours_ahead'),
(r'^now/minus(\d{1,2})hours/$', 'mysite.views.hours_behind'),
(r'^now/in_chicago/$', 'mysite.views.now_in_chicago'),
(r'^now/in_london/$', 'mysite.views.now_in_london'),
)
Utilitzant aquesta tècnica, no caldrà importar les funcions vista; Django importa automàticament les funcions de vista apropiades el primer com que les necessitin, d'acord amb les cadenes de text que descriuen el nom i camí de la funció vista.
Una drecera més enllà podria usar-se quan en la tècnica de cadenes de text poguéssim treure un prefix comú. En el nostre exemple URLconf, cada una de les cadenes de la vista comencen amb 'mysyte.views', que és redundant. Podem factoritzar aquest prefix i passar-lo com a primer argument de la funció patterns(), així:
from django.conf.urls.defaults import *
urlpatterns = patterns('mysite.views',
(r'^now/$', 'current_datetime'),
(r'^now/plus(\d{1,2})hours/$', 'hours_ahead'),
(r'^now/minus(\d{1,2})hours/$', 'hours_behind'),
(r'^now/in_chicago/$', 'now_in_chicago'),
(r'^now/in_london/$', 'now_in_london'),
)
Fixa't que no posem el punt (".") en el prefix, ni en la resta de cadenes de les vistes. Django el posa automàticament.
Amb aquestes dues aproximacions en ment, quina és millor? Realment depèn de les teves necessitats personals i del teu estil de codificació.
Els avantatges que proporciona l'aproximació de cadena de text són:
Els avantatges de l'aproximació de l'objecte funció són:
Ambdues aproximacions són vàlides, i pots fins i tot barrejar-les amb el mateix URLconf. Has d'escollir.
En la pràctica, si utilitzes la tècnica de les cadenes de text, probablement acabaràs barrejant vistes fins al punt que aquestes vistes no tindran un prefix comú. No obstant, pots aprofitar la drecera del prefix vista per eliminar duplicacions. Només has d'afegir múltiples objectes patterns(), com aquests:
Vell:
from django.conf.urls.defaults import *
urlpatterns = patterns('',
(r'^/?$', 'mysite.views.archive_index'),
(r'^(\d{4})/([a-z]{3})/$', 'mysite.views.archive_month'),
(r'^tag/(\w+)/$', 'weblog.views.tag'),
)
Nou:
from django.conf.urls.defaults import *
urlpatterns = patterns('mysite.views',
(r'^/?$', 'archive_index'),
(r'^(\d{4})/([a-z]{3})/$','archive_month'),
)
urlpatterns += patterns('weblog.views',
(r'^tag/(\w+)/$', 'tag'),
)
Totes les crides a l'objecte patterns() treballen sobre la mateix variable anomenada urlpatterns. Aquesta variable pot construir-se dinàmicament, com en aquest example.
En tots els exemples de l'URLconf, hem usat grups d'expressions regulars simples sense nom -- p.e. hem posat parèntesi al voltant de les parts de la URL que volíem capturar, i Django ha passat el text captura a la funció vista com un argument posicional. En usos més avançats, és possible usar grups d'expressions regulars amb noms per capturar trossos d'URL i passar-los a arguments clau a la vista
Arguments amb nom versus arguments posicionals
Una funció Python pot cridar-se utilitzant arguments amb nom, especificant el nom dels arguments junt amb els seus valors. En una crida per posició d'argument, simplement passes els arguments sense explicitar quin argument equival a cada valor; l'associació és implícita a l'ordre dels arguments.Per exemple, considera aquesta funció simple
def sell(item, price, quantity): print "Selling %s unit(s) of %s at %s" % (quantity, item, price)Per cridar-la amb arguments posicionals, has d'especificar els arguments en l'ordre en que s'han llistat en la definició de la funció:
sell('Socks', '$2.50', 6)Per cridar amb arguments amb nom, especifica els noms dels arguments amb els valors. Les següents sentències són equivalents:
sell(item='Socks', price='$2.50', quantity=6) sell(item='Socks', quantity=6, price='$2.50') sell(price='$2.50', item='Socks', quantity=6) sell(price='$2.50', quantity=6, item='Socks') sell(quantity=6, item='Socks', price='$2.50') sell(quantity=6, price='$2.50', item='Socks')
A Python les expressions regulars, la sintaxi per a grups d'expressions amb nom és (?
Aquest és un exemple URLconf que utilitza grups sense nom:
month_archive(request, '2006', '03')
Aquest és el mateix exemple URLconf que utilitza grups amb nom:
month_archive(request, year='2006', month='03')
A la pràctica, utilitzar grups amb nom fa que les teves URLconf siguin més explícites i menys limitades a errors per l'ordre dels arguments -- i pots reodernar els arguments en les definicions de les teves funcions de vista. Seguint amb l'exemple anterior, si volem canviar les URL per incloure el més abans que l'any, i estem usant grups sense nom, haurem de recordar-nos de canviar l'ordre dels arguments en la vista month_archive. Si estem usant grups amb nom, canviant l'ordre dels paràmetres capturats en la URL no ens afectarà a la vista.
Per descomptat, els beneficis dels grups amb nom són a costa de la brevetat; alguns desenvolupadors troben la sintaxi de grups amb nom lletja i massa explícita.
Si utilitzes grups amb nom i sense en el mateix patró en el teu URLconf, hauràs d'anar amb compte com Django tracta aquest cas especial. Aquí hi ha l'algorisme que segueix URLconf per analitzar-lo, respecte els grups anomenats vers els no anomenats en les expressions regulars:
Algunes vegades et trobarà escrivint funcions vista que són molt semblants, amb només unes petites diferències. Per exemple, diguem que tens dues vistes que el contingut és idèntic excepte per l'ús de la plantilla:
# urls.py
from django.conf.urls.defaults import *
from mysite import views
urlpatterns = patterns('',
(r'^foo/$', views.foo_view),
(r'^bar/$', views.bar_view),
)
# views.py
from django.shortcuts import render_to_response
from mysite.models import MyModel
def foo_view(request):
m_list = MyModel.objects.filter(is_new=True)
return render_to_response('template1.html', {'m_list': m_list})
def bar_view(request):
m_list = MyModel.objects.filter(is_new=True)
return render_to_response('template2.html', {'m_list': m_list})
Hem repetit aquest codi, que no és massa elegant. Primer, pots pensar en eliminar la redundància utilitzant la mateixa URL en ambdós vistes, posant parèntesi en la URL per capturar-ne el tipus, i comprovant la URL dins de la vista per determinar la plantilla, així:.
# urls.py
from django.conf.urls.defaults import *
from mysite import views
urlpatterns = patterns('',
(r'^(foo)/$', views.foobar_view),
(r'^(bar)/$', views.foobar_view),
)
# views.py
from django.shortcuts import render_to_response
from mysite.models import MyModel
def foobar_view(request, url):
m_list = MyModel.objects.filter(is_new=True)
if url == 'foo':
template_name = 'template1.html'
elif url == 'bar':
template_name = 'template2.html'
return render_to_response(template_name, {'m_list': m_list})
El problema amb aquesta solució, és que s'acobla la teva URL en el teu codi. Si decideixes canviar el nom de la URL /foo/ a /fooey/, hauràs de recordar a canviar el codi de la vista.
Una solució elegant implica una característica anomenada opcions extra de l'URLconf. Cada patró en un URLconf pot incloure un tercer element -- un diccionar amb arguments amb clau per passar a la funció vista.
Amb això en ment, pots re-escriure el nostre exemple així:
# urls.py
from django.conf.urls.defaults import *
from mysite import views
urlpatterns = patterns('',
(r'^foo/$', views.foobar_view, {'template_name': 'template1.html'}),
(r'^bar/$', views.foobar_view, {'template_name': 'template2.html'}),
)
# views.py
from django.shortcuts import render_to_response
from mysite.models import MyModel
def foobar_view(request, template_name):
m_list = MyModel.objects.filter(is_new=True)
return render_to_response(template_name, {'m_list': m_list})
Com pots veure, l URLconf en aquest exemple especifica un template_name en la URLconf. La funció vista es crida amb un altre paràmetre.
Aquesta tècnica d'opció extra és una bona manera d'enviar informació addicional a les nostres funcions vista amb el mínim d'inconvenients. Tot just hem utilitzat algunes formes complexes de Django, més complexitat hi haurà en el sistema de les vistes genèriques, que tractarem en el capítol 9.
Aquestes són uns quantes idees de com pots utilitzar la tècnica de les opcions extres de l'URLconf en els teus propis projecte:
Diguem que tens un conjunt de vistes que encaixen amb un patró, a més de tenir unes URL que no encaixen amb el patró però que la lògica de la vista és la mateixa. En aquest cas, pots "falsificar" els valors capturats a la URL utilitzant les opcions extra de l'URLconf per capturar aquesta URL extra amb la mateixa vista.
Per exemple, pots tenir una aplicació que mostra dades d'un dia en particular, amb URLs com aquestes:
/mydata/jan/01/ /mydata/jan/02/ /mydata/jan/03/ # ... /mydata/dec/30/ /mydata/dec/31/
Aquest és bastant senzill de tractar; pots capturar-lo en una URLconf com aquesta (utilitzant la sintaxi de grups amb nom):
urlpatterns = patterns('',
(r'^mydata/(?P<month>\w{3})/(?P<day>\d\d)/$', views.my_view),
)
I la firma de la funció vista seria quelcom així:
def my_view(request, month, day): # ....
Això és directe -- no és res que no hàgim vist abans. El truc ve en quan pots afegir una altra URL que utilitza my_view però aquesta URL no inclou un month i/o day.
Per exemple, podries voler afegir una altra URL, /mydata/birthday/, que podria ser equivalent a /mydata/jan/06/. Podem aprofitar-nos de l'opció extra de l'URLconf així:
urlpatterns = patterns('',
(r'^mydata/birthday/$', views.my_view, {'month': 'jan', 'day': '06'}),
(r'^mydata/(?P<month>\w{3})/(?P<day>\d\d)/$', views.my_view),
)
Lo "guay" aquí és que no hem hagut de canviar la nostra funció vista. La funció vista només es preocupa que tingui els paràmetres month i/o day -- aquesta no es preocupa si provenen de la captura de la URL de paràmetres extra.
És una bona pràctica de programació, fer resums en el codi. Per exemple, amb aquestes dues funcions de Python:
def say_hello(person_name): print 'Hello, %s' % person_name def say_goodbye(person_name): print 'Goodbye, %s' % person_name
...nosaltres podem resumir-ho amb una salutació com a paràmetre:
def greet(person_name, greeting): print '%s, %s' % (greeting, person_name)
Pots aplicar aquesta mateixa filosofia a les teves vistes Django utilitzant paràmetres extra.
Amb això en ment, pots començar a fer abstraccions d'alt nivell de les teves vistes. En comptes de personar-ho tu mateix, "Aquesta vista mostra una llista d'objectes Event", i "Aquesta vista mostra una llista d'objecte BlogEntry", veiem que són ambdós casos de "Una vista que mostra una llista d'objectes, on el tipus de l'objecte és variables".
Utilitza aquest codi, per exemple:
# urls.py
from django.conf.urls.defaults import *
from mysite import views
urlpatterns = patterns('',
(r'^events/$', views.event_list),
(r'^blog/entries/$', views.entry_list),
)
# views.py
from django.shortcuts import render_to_response
from mysite.models import Event, BlogEntry
def event_list(request):
obj_list = Event.objects.all()
return render_to_response('mysite/event_list.html', {'event_list': obj_list})
def entry_list(request):
obj_list = BlogEntry.objects.all()
return render_to_response('mysite/blogentry_list.html', {'entry_list': obj_list})
Les dues vistes fan essencialment el mateix: mostres una llista d'objectes. Així pots factoritzar el tipus d'objecte que es mostraran:
# urls.py
from django.conf.urls.defaults import *
from mysite import models, views
urlpatterns = patterns('',
(r'^events/$', views.object_list, {'model': models.Event}),
(r'^blog/entries/$', views.object_list, {'model': models.BlogEntry}),
)
# views.py
from django.shortcuts import render_to_response
def object_list(request, model):
obj_list = model.objects.all()
template_name = 'mysite/%s_list.html' % model.__name__.lower()
return render_to_response(template_name, {'object_list': obj_list})
Amb aquests petits canvis, tenim un codi de vista re-usable, independent del model! A part d'ara, qualsevol cop que necessitem una vista que llisti un conjunt d'objectes, podem simplement re-utilitzar la vista object_list en comptes de re-escriure el codi de la vista. Aquí hi ha un conjunt de notes sobre hem fet:
Com que els llocs web basats en base de dades tenen molts patrons comuns, Django ve amb un conjunt de "vistes genèriques" que utilitzen aquesta mateixa tècnica per estalviar-te temps. Nosaltres tractarem aquestes vistes genèriques en el pròxim capítol.
Si estàs distribuint una aplicació Django, els teus usuaris volen poder canviar algunes configuracions. En aquest cas, és una bona idea afegir ganxos en les teves vistes per qualsevol opció de configuració que pensis que la gent podria voler canviar. Pots usar paràmetres extra per a aquest propòsit.
Un exemple comú d'un canvi configurable d'aplicació és el nom de la plantilla:
def my_view(request, template_name):
var = do_something()
return render_to_response(template_name, {'var': var})
Quan hi ha un conflicte, els paràmetres extra poden precedir les paràmetres capturats. En altres paraules, si les teves URLconf capturen una variable amb nom i un paràmetre extra inclou una variable amb el mateix nom, el valors del paràmetre extra serà l'utilitzat.
Per exemple, considera aquesta URLconf:
from django.conf.urls.defaults import *
urlpatterns = patterns('',
(r'^mydata/(?P<id>\d+)/$', views.my_view, {'id': 3}),
)
Aquí, tant l'expressió regular com el diccionari extra inclouen un id. El id codificat té preferència. Això significa que qualsevol petició -- p.e. /mydata/2/ or /mydata/432432/ -- serà tractada com si id fos 3, descartant el valor capturat en la URL.
Els lectors astuts s'hauran fixat que en aquest cas, és una pèrdua de temps capturar l'identificador en l'expressió regular, perquè el seu valor sempre serà substituït per valor del diccionari. Aquests lectors estarien en lo cert. Només ho diem per ajudar-te a evitar fer aquestes equivocacions.
Un altre truc interessant és especificar paràmetres per defecte per una vista. Això diu a la vista quins valors utilitzar en un paràmetre si aquest no s'especifica.
Per exemple:
# urls.py
from django.conf.urls.defaults import *
urlpatterns = patterns('',
(r'^blog/$', views.page),
(r'^blog/page(?P<num>\d+)/$', views.page),
)
# views.py
def page(request, num="1"):
# Output the appropriate page of blog entries, according to num.
# ...
Aquí, ambdós patrons apunten a la mateixa vista -- views.page -- però el primer patró no captura res de la URL. Si el primer patró encaixa la funció page() s'utilitzarà amb l'argument per defecte. Si el segon patró encaixa, page() utilitzarà el valor num que s'ha capturar.
És normal utilitzar aquesta tècnica juntament amb les opcions de configuració, explicades anteriorment. Aquest exemple fa un lleuger canvi en l'exemple de "Donant opcions a la configuració de les vistes" per proporcionar un valor per defecte al nom de la plantilla template_name:
def my_view(request, template_name='mysite/my_view.html'):
var = do_something()
return render_to_response(template_name, {'var': var})
Algunes vegades tindreu un patró en el teu URLconf que tracti un gran conjunt de URLs però necessitarem un cas especial per a ells. En aquest cas, aprofitem la forma lineal on una URLconf es processada i posa un primer cas especial.
Per exemple, les pàgines "afegeix un objecte" del lloc d'administració de Django es representen per aquesta línia URLconf:
urlpatterns = patterns('',
# ...
('^([^/]+)/([^/]+)/add/$', 'django.contrib.admin.views.main.add_stage'),
# ...
)
Aquesta URL encaixa amb /myblog/entries/Add/ i /auth/group/add/. Tanmateix, la pàgina "add" per a un objecte usuari (/auth/user/Add/) és un cas especial -- aquest no mostra tots els camps del formulari, sinó que mostra dos camps de password, etc. Podríem solucionar aquest cas especial de la vista, així:
def add_stage(request, app_label, model_name): if app_label == 'auth' and model_name == 'user': # do special-case code else: # do normal code
...però això no és elegant per una raó que s'ha tractat múltiples vegades en aquest capítol: això posa lògica URL en la vista. Per a una solució més elegant, podem aprofitar del fet que les URLconf es processen en ordre de dalt a baix:
urlpatterns = patterns('',
# ...
('^auth/user/add/$', 'django.contrib.admin.views.auth.user_add_stage'),
('^([^/]+)/([^/]+)/add/$', 'django.contrib.admin.views.main.add_stage'),
# ...
)
Amb això, una petició /auth/user/add/ serà tractat per la vista user_add_stage. Encara que aquesta URL encaixi amb el segon patró, primer ho farà amb el primer.
Cada argument capturat s'envia a la vista com a una cadena plana Python, sense preocupar-nos de quin tipus d'expressió regular fem. Per exemple, en aquesta línia URLconf:
(r'^articles/(?P<year>\d{4})/$', views.year_archive),
... l'argument
Això és important per tenir-ho present quan escrivim el codi de la vista. Moltes funcions Python són punyeteres (i legítimes també) sobre acceptar només objectes d'un cert tipus. Un error normal és intentar crear un objecte datetime.date amb valors de cadena de text en comptes de valors enters:
>>> import datetime
>>> datetime.date('1993', '7', '9')
Traceback (most recent call last):
...
TypeError: an integer is required
>>> datetime.date(1993, 7, 9)
datetime.date(1993, 7, 9)
Traslladat a una URLconf i vista, l'error seria com aquest:
# urls.py
from django.conf.urls.defaults import *
urlpatterns = patterns('',
(r'^articles/(\d{4})/(\d{2})/(\d{2})/$', views.day_archive),
)
# views.py
import datetime
def day_archive(request, year, month, day)
# The following statement raises a TypeError!
date = datetime.date(year, month, day)
En comptes de day_archive() pot escriure's correctament així:
def day_archive(request, year, month, day) date = datetime.date(int(year), int(month), int(day))
Fixa't que el propi int() llença un ValueError quan li passes una cadena que no està composada només per dígits, però evita aquest error en aquest cas perquè l'expressió regular en la nostra URLconf s'ha assegurat que només les cadenes que contenen fígits poden passar-se a la funció vista.
Quan arriba una petició. Django intenta encaixar-la en un patró URLconf, com una cadena de text Python normal (no com una cadena Unicode). Això no inclou els paràmetres GET i POST, o el nom del domini. Tampoc s'hi inclou la primera barra, perquè cada URL té una primera barra.
Per exemple, en la petició http://www.example.com/myapp/, Django intentarà encaixar myapp/.
En una petició http://www.example.com/myapp/?page=3, Django intentarà encaixar myapp/.
El mètode de la petició -- p.e. POST, GET, HEAD -- no es tenen en compte quan passem per la URLconf. En altres paraules, totes les peticions s'enrutaran a la mateixa funció de la mateixa UTL. És responsabilitat d'una funció de vista realitzar els salts basats en aquest mètode.
En qualsevol moment, la teva URLconf pot "include" incloure altres mòduls URLconf. Això, essencialment arrela un conjunt d'URLs sota altres.
Per exemple, aquesta URLconf inclou altres URLconf:
from django.conf.urls.defaults import *
urlpatterns = patterns('',
(r'^weblog/', include('mysite.blog.urls')),
(r'^photos/', include('mysite.photos.urls')),
(r'^about/$', 'mysite.views.about'),
)
Hi ha un tema important aquí: Les expressions regulars en aquest exemple que apunten a un include() no tenen el $ (el caràcter de final de cadena) però inclouen la barra. Sempre que Django troba un include(), extreu qualsevol part de la URL que hi encaixa i envia la resta de la cadena a la URLconf de l'include.
Continuant amb aquest exemple, aquí ha ha la URLconf mysite.blog.urls:
from django.conf.urls.defaults import *
urlpatterns = patterns('',
(r'^(\d\d\d\d)/$', 'mysite.blog.views.year_detail'),
(r'^(\d\d\d\d)/(\d\d)/$', 'mysite.blog.views.month_detail'),
)
Amb aquestes dues URLconf, aquí veiem uns quants exemples de com les peticions seran tractades:
Un URLconf inclòs rep qualsevol paràmetre capturat del URLconf pare. Per exemple:
# root urls.py
from django.conf.urls.defaults import *
urlpatterns = patterns('',
(r'^(?P<username>\w+)/blog/', include('foo.urls.blog')),
)
# foo/urls/blog.py
from django.conf.urls.defaults import *
urlpatterns = patterns('',
(r'^$', 'foo.views.blog_index'),
(r'^archive/$', 'foo.views.blog_archive'),
)
En aquest exemple, la variable username capturada es passa a la URLconf inclosa i, per tant, a cada funció vista dins d'aquesta URLconf.
Fixa't que els paràmetres capturats sempre es passen a cada línia de l'URLconf inclosa, encara que la vista no accepti aquests paràmetres com a vàlids. Per aquesta raó, aquesta tècnica només és útil si estàs segur que totes les vistes en la URLconf incloses accepten els paràmetres que els hi passes.
De forma semblant, pots passar opcions extra a l'include(), igual que pots passar opcions a una vista normal -- com un diccionari. Quan ho fas, cada línia en l'URLconf inclosa serà passat a les opcions extra.
Per exemple, aquests dos conjunt d'URLconf funcionen de forma idèntic:
Conjunt ú:
# urls.py
from django.conf.urls.defaults import *
urlpatterns = patterns('',
(r'^blog/', include('inner'), {'blogid': 3}),
)
# inner.py
from django.conf.urls.defaults import *
urlpatterns = patterns('',
(r'^archive/$', 'mysite.views.archive'),
(r'^about/$', 'mysite.views.about'),
(r'^rss/$', 'mysite.views.rss'),
)
Conjunt dos:
# urls.py
from django.conf.urls.defaults import *
urlpatterns = patterns('',
(r'^blog/', include('inner')),
)
# inner.py
from django.conf.urls.defaults import *
urlpatterns = patterns('',
(r'^archive/$', 'mysite.views.archive', {'blogid': 3}),
(r'^about/$', 'mysite.views.about', {'blogid': 3}),
(r'^rss/$', 'mysite.views.rss', {'blogid': 3}),
)
Fixa't que les opcions extra sempre seran passades a cada línia del URLconf inclòs, encara que les vistes acceptin les opcions com a vàlides. Per aquesta raó, aquesta tècnica només és útil si estàs segur que totes les vistes en el URLconf inclòs accepta les opcions extra que estàs passant.
Aquest capítol no s'ha acabat.