URLDispatcher

(Comments)

Django comes packaged with a URL dispatcher which let's you design cool elegant URL scheme. It let's you design URLs the way you want with no limitations as such. Also there is no URL extentions such as .php, .html or .cgi, which means a much more beautiful URL.


These URLs must be mapped to a view somehow and that is where URLconf (URL configuration) comes into picture. The elegance of Django is that we achieve this mapping by just one line of Python code. Also this line of code much more than mapping a view to a URL which we will check out later in the blog.



How Django processes a request

Before getting into details of URLconf lets understand how Django processes a request. According to the official documentation of Django a request is handled as follows:

    1. Django determines the root URLconf module to use. Ordinarily, this is the value of the ROOT_URLCONF setting, but if the incoming HttpRequest object has a urlconf attribute (set by middleware), its value will be used in place of the ROOT_URLCONF setting.
    2. Django loads that Python module and looks for the variable urlpatterns. This should be a Python list of django.conf.urls.url() instances.
    3. Django runs through each URL pattern, in order, and stops at the first one that matches the requested URL.
    4. Once one of the regexes matches, Django imports and calls the given view, which is a simple Python function (or a class-based view). The view gets passed the following arguments:
        • An instance of HttpRequest.
        • If the matched regular expression returned no named groups, then the matches from the regular expression are provided as positional arguments.
        • The keyword arguments are made up of any named groups matched by the regular expression, overridden by any arguments specified in the optional kwargs argument to django.conf.urls.url().
    5. If no regex matches, or if an exception is raised during any point in this process, Django invokes an appropriate error-handling view. See Error handling below.



Example

Listed below is a simple example of URLConf:

# Python code
# myproject/myapp/urls.py

from django.conf.urls import url

from . import views


urlpatterns = [
    url(r'^blogs/2017/$', views.all_blogs_2017),
    url(r'^blogs/([0-9]{4})/$', views.blog_archive_year),
    url(r'^blogs/([0-9]{4})/([0-9]{2})/$', views.blog_archive_month),
    url(r'^blogs/(\S+)/$', views.blog_detail)
]


Points to note:
    • To capture any value from the URL, you only have to put a parenthesis around it. We have put regexes in parenthesis which will be matched against the request URL and routed with the parameters captured from the URL.
    • There's no need to add a leading slash. It's ^blogs, and not ^/blogs.
    • Although optional, it's recommended to add the 'r' in front of each regex string. It tells PYTHON that the string is "raw" and nothing should be escaped in the string.
    • If using the Class Based Views then the method calling would be views.ViewClass.as_view().


Example requests:
    • A request to https://www.example.com/blogs/2017/02/ would match the third entry in the list. Django would call the function views.month_archive(request, '2017', '02').
    • https://www.example.com/blogs/2017/2/ would not match any URL patterns, because the third entry in the list requires two digits for the month.
    • https://www.example.com/blogs/2017/ would match the first pattern in the list, not the second one, because the patterns are tested in order, and the selected pattern (routed view) is first one to match. Here, Django would call the function views.all_blogs_2017(request).
    • https://www.example.com/blogs/2017 would not match any of these patterns, because each pattern requires that the URL end with a slash.
    • https://www.example.com/blogs/very-interesting-blog/ would match the final pattern. Django would call the function views.article_detail(request, 'very-interesting-blog').



Named Groups

In the above examples, we only had simple, unnamed regex groups to capture the parameters from the URL. It passed these parameters as positional arguments to the view. But a way more elegant and human readable format for passing these captured parameters is to pass them as keyword arguments to the view.


This is where the Named Groups comes into picture. In Python regular expressions, the syntax for named regular-expression groups is (?P<name>pattern), where name is the name of the group and pattern is some pattern to match.


Here’s the above example URLconf, rewritten to use named groups:

# Python code
# myproject/myapp/urls.py

from django.conf.urls import url

from . import views


urlpatterns = [
    url(r'^blogs/2017/$', views.all_blogs_2017),
    url(r'^blogs/(?P<year>[0-9]{4})/$', views.blog_archive_year),
    url(r'^blogs/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/$', views.blog_archive_month),
    url(r'^blogs/(?P<slug>\S+)/$', views.blog_detail)
]


This accomplishes exactly the same thing as the previous example, with one minor difference. Instead of passing the captured parameters as positional arguments rather they will be passed as keyword arguments. For example:
    • A request to /blogs/2017/02/ will call the function views.blog_archive_month(request, year='2017', month='02').
    • A request to /blogs/very-interesting-blog/ will call the function views.blog_detail(request, slug='very-interesting-blog').


An important point to note here is that irrespective of what sort of match the regular expressions makes, the captured arguments are always sent as plain string. The year and month arguments are passed '2017' and '02' respectively, not 2017 and 02.


Important note:

You must not have both named and unnamed groups in the same URL matching string. If you do so, the unnamed arguments will be ignored.

The django documentation states the following as the matching/ grouping algorithm:
    1. If there are any named arguments, it will use those, ignoring non-named arguments.
    2. Otherwise, it will pass all non-named arguments as positional arguments.



Specifying defaults for view arguments

A convenient trick is to specify default parameters for your views’ arguments. Here’s an example URLconf and view:

# Python code
# myproject/myapp/urls.py

from django.conf.urls import url

from . import views

urlpatterns = [
    url(r'^blog/single/$', views.blog_detail),
    url(r'^blog/single/(?P<slug>\S+)/$', views.blog_detail),
]


# Python code
# myproject/myapp/views.py

def blog_detail(request, slug='home-page'):
    # Output the appropriate blog post according to the slug...
    ...
    ...


In the above argument both the URLs use the same view. But one uses that with an captured argument ans the other without. so to handle this we specify a default in the view so that, if the slug keyword isn't set, it will fallback to this default value.


So the first url will call the view with no arguments and thus will make the argument slug fallback to the default value of 'home-page'.


Including other URLConfs

Django projects are split into apps and you can actually have a full hierarchy of apps and maintaining the urls for every view in every app in a project in a single file is very ugly way to go about it. So what Django does is it allows us to break up this dump of URLs into neat packets and them use the include function to include all these URLConfs into a single one. You could have a hierarchy of includes too.


Usually the URLconfs for any app is contained in itself and the then included into the root URLConf in project urls.py file.


For example, here’s an excerpt of the URLconf for the Django website itself. It includes a number of other URLconfs:

from django.conf.urls import include, url

urlpatterns = [
    # ... snip ...
    url(r'^community/', include('django_website.aggregator.urls')),
    url(r'^contact/', include('django_website.contact.urls')),
    # ... snip ...
]


In the above url()s the 'django_website.aggregator.urls' is the package name and the file name of the URLConf file in the app. In other words include URLconf from urls.py file from the package (app) django_website.aggregator. We could do it with our custom apps too.


Note that the regular expressions in this example 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.


This can be used to remove redundancy from URLconfs where a single pattern prefix is used repeatedly. For example, consider this URLconf:

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),
    ])),
]


Here all the view functions in include() will be passed the parameters of its parent url(). In this particular case the parameters page_slug and page_id. This works if you include a URLConf of an app as well.



Reverse resolution of URLs

A common need when working on a Django project is the possibility to obtain URLs in their final forms either for embedding in generated content (views and assets URLs, URLs shown to the user, etc.) or for handling of the navigation flow on the server side (redirections, etc.)


It is strongly desirable to avoid hard-coding these URLs (a laborious, non-scalable and error-prone strategy). Equally dangerous is devising ad-hoc mechanisms to generate URLs that are parallel to the design described by the URLconf, which can result in the production of URLs that become stale over time.


In other words, what’s needed is a DRY mechanism. Among other advantages it would allow evolution of the URL design without having to go over all the project source code to search and replace outdated URLs.


The primary piece of information we have available to get a URL is an identification (e.g. the name) of the view in charge of handling it. Other pieces of information that necessarily must participate in the lookup of the right URL are the types (positional, keyword) and values of the view arguments.


Django provides a solution such that the URL mapper is the only repository of the URL design. You feed it with your URLconf and then it can be used in both directions:
    • Starting with a URL requested by the user/browser, it calls the right Django view providing any arguments it might need with their values as extracted from the URL.
    • Starting with the identification of the corresponding Django view plus the values of arguments that would be passed to it, obtain the associated URL.


The first one is the usage we’ve been discussing in the previous sections. The second one is what is known as reverse resolution of URLs, reverse URL matching, reverse URL lookup, or simply URL reversing.


Django provides tools for performing URL reversing that match the different layers where URLs are needed:
    • In templates: Using the URL template tag.
    • In Python code: Using the reverse() function.
    • In higher level code related to handling of URLs of Django model instances: The get_absolute_url() method.


Example

Consider this URLconf entry:

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

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


You can obtain the fully formed URL for this url() in template code by using:

{# with a value #}
<a href="{% url 'blogs-archive-year' 2017 %}">2017 Blogs</a>

{# with a context variable #}
<ul>
{% for yearvariable in year_list %}
<li><a href="{% url 'blogs-archive-year' yearvariable %}">{{ yearvariable }} Blogs</a></li>
{% endfor %}
</ul>


For named groups you could do the following:

{# with a value #}
<a href="{% url 'blogs-archive-year' year=2017 %}">2017 Blogs</a>

where year is the group name.


In Python code we could get the reverse URLs as follows:

from django.urls import reverse
from django.http import HttpResponseRedirect

def redirect_to_year(request):
    # ...
    year = 2016
    # ...
    return HttpResponseRedirect(reverse('blogs-archive-year', args=(year,)))


Comments

Recent Posts

Archive

2019
2018
2017
2016
2015
2014

Tags

Authors

Feeds

RSS / Atom