I've been using django for almost a year and I was always frustrated
by its cumbersome urlpatterns system. While it is really flexible, it
doesn't provide any shortcuts for widely-used url and views naming
schemes.
Let me show in examples what I mean.
As everyone, I started with the tutorial and worked with the following
system:
=== urls.py ===
urlpatterns = patterns('',
(r'^$', 'apps.app1.views.index'),
(r'^news/$', 'apps.app1.views.news'),
(r'^members/profile/$', 'apps.app1.views.members.profile'),
(r'^members/secure/$', 'apps.app1.views.members.secure'),
(r'^app2/page/$', 'apps.app2.views.page'), # some other app
...
)
It becomes quite a large and hardy maintainable list when the number
of applications and views grows. I do believe in DRY principle and I
didn't like much that I had to repeat the URL bases and parent module
names again and again. So I kept reading the docs and involved the
system to the following:
=== urls.py ===
urlpatterns = patterns('',
(r'^', include('apps.app1.urls')),
(r'^app2', include('apps.app2.urls')),
)
=== apps/app1/urls/__init__.py ===
urlpatterns = patterns('apps.app1.views',
(r'^index/$', 'index'),
(r'^news/$', 'news'),
(r'^members', include('apps.app1.urls.members'),
)
=== apps/app1/urls/members.py ===
urlpatterns = patterns('apps.app1.views.members',
(r'^profile/$', 'profile'),
(r'^secure/$', 'secure'),
)
=== apps/app1/views/__init__.py ===
def index(request):
...
def news(request):
...
=== apps/app1/views/members.py ===
def profile(request):
....
def secure(request):
...
(I skipped app2.* files for easier reading)
While this system had less redundancy and easier to maintain (the DRY
benefits), it suffered from another DRY problem - there were two
packages with the same structure (apps.app1.urls.* and
apps.app1.views.*), with the highly related content. When I was
renaming a view, I had to browse through two package structures and
change the things twice. Still frustrating, you see.
So what I decided, why do we have to keep urlpatterns apart of the
views? Why wouldn't I put them in the same files? The architecture
became:
=== urls.py ===
urlpatterns = patterns('',
(r'^', include('apps.app1.views')), # note views, not urls
(r'^app2', include('apps.app2.views')),
)
=== apps/app1/views/__init__.py ===
urlpatterns = patterns(__name__, # sic!
(r'^index/$', 'index'),
(r'^news/$', 'news'),
(r'^members', include('apps.app1.views.members'),
)
def index(request):
...
def news(request):
...
=== apps/app1/views/members.py ===
urlpatterns = patterns(__name__,
(r'^profile/$', 'profile'),
(r'^secure/$', 'secure'),
)
def profile(request):
....
def secure(request):
...
This was a good change. I had 50% less files, and I put the related
info within the same modules. Also note the usage of __name__, which
increased the DRY factor a bit more :-)
Yet I wasn't fully satisfied. Whenever I renamed a view I had to patch
the urlpatterns as well.
I also remembered an old inconvenience I always felt with views
modules. If I place views functions in a views module, and place like
"normal" helper functions there as well, they got mixed. By looking at
the code it is sometimes hard to understand which function is a view,
and which function is a helper.
So what I created is the @url decorator which solved the both
problems:
=== urls.py ===
urlpatterns = patterns('',
(r'^', include('apps.app1.views')),
(r'^app2', include('apps.app2.views')),
)
=== apps/app1/views/__init__.py ===
@url(r'^index/$')
def index(request):
...
@url(r'^news/$')
def news(request):
...
urlpatterns += include_urlpatterns(r'^members',
'apps.app1.views.members')
=== apps/app1/views/members.py ===
@url(r'^profile/$)
def profile(request):
....
@url(r'^secure/$)
def secure(request):
...
@url(r'^path1/$', '^path2/$') # you can specify several patterns
def multipath_view(request):
...
def helper(): # easily distinguishable - no @url!
...
Summarizing, the benefits are:
- no more creating and supporting urlpattern maps (less files, less
code, more DRY)
- have the url associated with a view in-place
- easily see if a function is a view
- fully compatible with other chained decorators
Implementation problems, or possible improvements:
- it is hackish
- the speed isn't constant time, it is O(N) where N is the number of
currently loaded modules
- I would like to make it support the no-arguments syntax:
@url()
def profile(request):
....
and have the decorator automatically pull the function name as the url
pattern, but that doesn't seem possible if there are further
decorators (like @user_passes_test or @render_to)
The source code can be found at http://www.djangosnippets.org/snippets/395/