All notes
UrlDispatch

URLs

Cool URIs don’t change.

Django also provides a way to translate URLs according to the active language.

How Django processes a request:

  1. Determines the root URLconf module to use. Ordinarily, this is the value of the ROOT_URLCONF setting, but "urlconf" attribute in the incoming HttpRequest object can overwrite it.
  2. Django loads that Python module URLconf and looks for the variable urlpatterns (a Python list of django.conf.urls.url() instances).
  3. Finds the first URL pattern that matches the requested URL.
  4. Imports and calls the given view. The view gets passed the following arguments: An instance of HttpRequest, positional arguments (when regexp has no named groups), keyword arguments (named groups).
  5. If no regex matches, or if an exception is raised during any point, invokes an appropriate error-handling view.

Sample URLconf file:


from django.conf.urls import url

from . import views

urlpatterns = [
    url(r'^articles/2003/$', views.special_case_2003),
    url(r'^articles/([0-9]{4})/$', views.year_archive),
    url(r'^articles/([0-9]{4})/([0-9]{2})/$', views.month_archive),
    url(r'^articles/([0-9]{4})/([0-9]{2})/([0-9]+)/$', views.article_detail),

    # named regular-expression groups:
    url(r'^articles/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/$', views.month_archive),

    # urlpatterns can “include” other URLconf modules.
    # Note: the regexp here don’t have a $ (end-of-string match character) but do include a trailing slash:
    url(r'^community/', include('django_website.aggregator.urls')),
    url(r'^contact/', include('django_website.contact.urls')),
]

# /articles/2005/03/ calls:
# views.month_archive(request, '2005', '03')
# /articles/2005/3/ calls:
# URL not found.
# /articles/2003/ calls:
# views.special_case_2003(request)
# /articles/2003 calls:
# URL not found.
# /articles/2003/03/03/
# views.article_detail(request, '2003', '03', '03')

# Assume there is only the named groups:
# /articles/2005/03/ calls:
# views.month_archive(request, year='2005', month='03')

There’s no need to add a leading slash, because every URL has that. For example, it’s ^articles, not ^/articles.

The 'r' in front is optional but recommended. It tells Python that a string is “raw” – that nothing in the string should be escaped.

By default, any request to a URL that doesn’t match a URLpattern and doesn’t end with a slash will be redirected to the same URL with a trailing slash. This is regulated by the APPEND_SLASH Django setting. All URLs end with slashes (which is the preference of Django’s developers).

If named groups and non-named groups are mixed in a regular expression, it will use those, ignoring non-named arguments.

Captured arguments are always strings.

Performance. Each regular expression in a urlpatterns is compiled the first time it’s accessed. This makes the system blazingly fast.

Include

urlpatterns can "include" other URLconf modules. The regexp don’t have a $ (end-of-string match character) but do include a trailing slash. Whenever Django encounters include() (django.conf.urls.include()), it chops off whatever part of the URL matched up to that point and sends the remaining string to the included URLconf for further processing.


from django.conf.urls import url
from . import views

urlpatterns = [
    url(r'^(?P<page_slug>[\w-]+)-(?P<page_id>\w+)/history/$', views.history),
    url(r'^(?P<page_slug>[\w-]+)-(?P<page_id>\w+)/edit/$', views.edit),
    url(r'^(?P<page_slug>[\w-]+)-(?P<page_id>\w+)/discuss/$', views.discuss),
    url(r'^(?P<page_slug>[\w-]+)-(?P<page_id>\w+)/permissions/$', views.permissions),
]

# We can improve this by stating the common path prefix only once and grouping the suffixes that differ:

from django.conf.urls import include, url
from . import views

urlpatterns = [
    url(r'^(?P<page_slug>[\w-]+)-(?P<page_id>\w+)/', include([
        url(r'^history/$', views.history),
        url(r'^edit/$', views.edit),
        url(r'^discuss/$', views.discuss),
        url(r'^permissions/$', views.permissions),
    ])),
]

Nested arguments, non-capturing argument


from django.conf.urls import url

urlpatterns = [
    url(r'blog/(page-(\d+)/)?$', blog_articles),                  # bad
    url(r'comments/(?:page-(?P<page_number>\d+)/)?$', comments),  # good
]

The outer argument in this case is a non-capturing argument (?:...).

Nested captured arguments create a strong coupling between the view arguments and the URL as illustrated by blog_articles: the view receives part of the URL (page-2/) instead of only the value the view is interested in. This coupling is even more pronounced when reversing, since to reverse the view we need to pass the piece of URL instead of the page number.

As a rule of thumb, only capture the values the view needs to work with and use non-capturing arguments when the regular expression needs an argument but the view ignores it.

Passing extra options to view


from django.conf.urls import url
from . import views

urlpatterns = [
    url(r'^blog/(?P<year>[0-9]{4})/$', views.year_archive, {'foo': 'bar'}),
]

For a request "/blog/2005/", Django will call views.year_archive(request, year='2005', foo='bar').

When there is conflict, the arguments in the dictionary will be used instead of the arguments captured in the URL.

Passing extra options to included URL lists, will make the options passed to all the urls in the lists:


# main.py
from django.conf.urls import include, url

urlpatterns = [
    url(r'^blog/', include('inner'), {'blogid': 3}),
]

# inner.py
from django.conf.urls import url
from mysite import views

urlpatterns = [
    url(r'^archive/$', views.archive),
    url(r'^about/$', views.about),
]

Reverse resolution of URLs

It is strongly desirable to avoid hard-coding these URLs (a laborious, non-scalable and error-prone strategy). We need a strategy which would allow evolution of the URL design without having to go over all the project source code to search and replace outdated URLs.

Django provides a solution such that the URL mapper (URLconf) is the only repository of the URL design.

Django provides tools for performing URL reversing that match the different layers where URLs are needed:

Named URL patterns

Consider a URLconf file:


from django.conf.urls import url

from . import views

urlpatterns = [
    url(r'^articles/([0-9]{4})/$', views.year_archive, name='news-year-archive'),
]

The template:


<a href="{% url 'news-year-archive' 2012 %}">2012 Archive</a>
{# Or with the year in a template context variable: #}
<ul>
{% for yearvar in year_list %}
<li><a href="{% url 'news-year-archive' yearvar %}">{{ yearvar }} Archive</a></li>
{% endfor %}
</ul>

Or in Python code:


from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect

def redirect_to_year(request):
    year = 2006
    return HttpResponseRedirect(reverse('news-year-archive', args=(year,)))

If there is any update, you would only need to change the entry in the URLconf.

When you name your URL patterns, make sure you use names that are unlikely to clash with any other application’s choice of names.

URL namespaces

It’s a good practice for third-party apps to always use namespaced URLs.

A URL namespace comes in two parts: application namespace, and instance namespace.

The named URL 'sports:polls:index' would look for a pattern named 'index' in the namespace 'polls' that is itself defined within the top-level namespace 'sports'.

To resolve a namespaced URL:

  1. First, Django looks for a matching application namespace. If there is a current application defined, Django finds and returns the URL resolver for that instance. The current application can be specified with the "current_app" argument to the reverse() function.
  2. If there is no current application, Django looks for a default application instance. The default application instance is the instance that has an instance namespace matching the application namespace.
  3. If there is no default application instance, Django will pick the last deployed instance of the application, whatever its instance name may be.
  4. If the provided namespace doesn’t match an application namespace in step 1, Django will attempt a direct lookup of the namespace as an instance namespace.

from django.conf.urls import include, url

urlpatterns = [
    url(r'^author-polls/', include('polls.urls', namespace='author-polls')),
    url(r'^publisher-polls/', include('polls.urls', namespace='publisher-polls')),
]

from django.conf.urls import include, url

from . import views

# Application namespace: polls
polls_patterns = ([
    url(r'^$', views.IndexView.as_view(), name='index'),
    url(r'^pk/$', views.DetailView.as_view(), name='detail'),
], 'polls')

url(r'^polls/', include(polls_patterns)),

A many-to-one relationship might exist between URLs and views, where the view name isn’t a good enough identifier. Using namespace solves this.