Url naming and namespaces
A project's internal links or url
references (e.g. <a href='/'>Home
Page</a>
) tend to be hard-coded, whether it's in view
methods to redirect users to certain locations or in templates to
provide adequate user navigation. Hard-coding links can present a
serious maintenance problem as a project grows, because it leads to
links that are difficult to detect and fix. Django offers a way to
name urls so it's easy to reference them in view methods and
templates.
The most basic technique to name
Django urls is to add the name
attribute to
url
definitions in urls.py
. Listing 2-16
shows how to name a project's home page, as well as how to
reference this url from a view method or template.
Listing 2-16 Django url using name
---------------------------------------------------------------------------- # Definition in coffeehouse/urls.py path('',TemplateView.as_view(template_name='homepage.html'),name="homepage") ---------------------------------------------------------------------------- # Definition in view method from django.http import HttpResponsePermanentRedirect from django.urls import reverse def method(request): .... return HttpResponsePermanentRedirect(reverse('homepage')) ---------------------------------------------------------------------------- # Definition in template <a href="{% url 'homepage' %}">Back to home page</a>
The url definition in listing
2-16 uses the path ''
which
translates into /
or the home page, also known as the
root directory. Notice the name
attribute with the
homepage
value. By assigning the url a
name
you can use this value as a reference in view
methods and templates, which means any future changes made to the
url path, automatically updates all url definitions in
view methods and templates.
Next in listing 2-16 you can see
a view method example that redirects control to
reverse('homepage')
. The Django reverse
method attempts to look up a url definition by the given name -- in
this case homepage
-- and substitutes it accordingly.
Similarly, the link sample <a href="{% url 'homepage'
%}">Back to home page</a>
in listing 2-16 makes use
of the Django {% url %}
tag, which attempts to look up
a url by its first argument -- in this case homepage
-- and substitute it accordingly.
This same naming and substitution process is available for more complex url definitions, such as those with url parameters. Listing 2-17 shows the process for a url with parameters.
Listing 2-17. Django url with arguments using name
---------------------------------------------------------------------------- # Definition in coffeehouse/urls.py path(r'^drinks/<str:drink_name>/',TemplateView.as_view(template_name='drinks/index.html'),name="drink"), ---------------------------------------------------------------------------- # Definition in view method from django.http import HttpResponsePermanentRedirect from django.urls import reverse def method(request): .... return HttpResponsePermanentRedirect(reverse('drink', args=(drink.name,))) ---------------------------------------------------------------------------- # Definition in template <a href="{% url 'drink' drink.name %}">Drink on sale</a> <a href="{% url 'drink' 'latte' %}">Drink on sale</a>
The url definition in listing
2-17 uses a more complex url path with a parameter that
translates into urls in the form /drinks/latte/
or
/drinks/espresso/
. In this case, the url is given the
argument name drink_name
.
Because the url uses a parameter,
the syntax for the reverse
method and {% url
%}
tag are slightly different. The reverse
method requires the url parameters be provided as a tuple to the
args
variable and the {% url %}
tag
requires the url arguments be provided as a list of values. Notice
in listing 2-17 the parameters can equally be variables or
hard-coded values, so long as it matches the url argument regular
expression type -- which in this case is non-digits.
For url definitions with more
than one argument, the approach to using reverse
and
{% url %}
is identical. For the reverse
method you pass it a tuple with all the necessary parameters and
for the {% url %}
tag you pass it a list of
values.
Caution Beware of invalid url definitions with
reverse and {% url %}. Django always checks at start up that all
reverse and {% url %} definitions are valid. This means that if you
make an error in a reverse method or {% url %} tag definition --
like a typo in the url name or the arguments types don't match the
regular expression -- the application won't start and throw an HTTP
500 internal error. The error for this kind of situation is
NoReverseMatch at....Reverse for 'urlname' with arguments
'()' and keyword arguments '{}' not found. X pattern(s)
tried
. If you look at the error stack you'll be able to
pinpoint where this is happening and correct it. Just be aware this
is a fatal error and is not isolated to the view or page where it
happens, it will stop the entire application at start
up.
Sometimes the use of the
name
attribute by itself is not sufficient to classify
urls. What happens if you have two or three index pages ? Or if you
have two urls that qualify as details, but one is for stores and
the other for drinks ?
A crude approach would be to use composite names (e.g. drink_details, store_details). However, the use of composite names in this form can lead to difficult to remember naming conventions and sloppy hierarchies. A cleaner approach is to use Django's app structure to manage groups of urls.
As described in the Set up content, understand Django urls, templates and apps section in Chapter 1, Django projects are structured around Django apps, which serve to separate and manage specific aspects of a project. Therefore, relying on the Django apps concept to manage url is a natural fit to navigate the possibility of having multiple urls with the same name and not have them conflict with one another.
Listing 2-18 illustrates a series
of url definitions that make use of the app_name
attribute with include
.
Listing 2-18 - Django urls.py with app_name attribute
---------------------------------------------------------------------------- # Contents coffeehouse/urls.py from django.urls import include, path urlpatterns = [ path('',TemplateView.as_view(template_name='homepage.html'),name="homepage"), path('about/',include('coffeehouse.about.urls')), path('stores/',include('coffeehouse.stores.urls')), ] ---------------------------------------------------------------------------- # Contents coffeehouse/about/urls.py from django.urls import path from . import views from . import apps # apps.AboutConfig.name = coffeehouse.about app_name = apps.AboutConfig.name urlpatterns = [ path('',views.index,name="index"), path('contact/',views.contact,name="contact"), ] ---------------------------------------------------------------------------- # Contents coffeehouse/stores/urls.py from django.urls import path from . import views from . import apps # apps.StoresConfig.name = coffeehouse.stores app_name = apps.StoresConfig.name urlpatterns = [ path('',views.index,name="index"), path('<int:store_id>/',views.detail,name="detail"), ) ---------------------------------------------------------------------------- # Definition in views.py file from django.http import HttpResponsePermanentRedirect from django.urls import reverse def method(request): .... return HttpResponsePermanentRedirect(reverse('coffeehouse.about:index')) ---------------------------------------------------------------------------- # Definition in template <a href="{% url 'coffeehouse.stores:index' %}">Back to stores index</a>
Listing 2-18 starts with a set of
include
definitions typical of a main Django
urls.py
file, like the ones presented earlier in listing 2-13. Next, you can see the
urls.py
files referenced in the main
urls.py
file, make use of the name
attribute described in the past example. Notice both the about and
stores urls.py
files have a url with
name='index'
.
More importantly, notice the app_name
attribute in each child urls.py
file. The value of the app_name
in each url file points to the app's name
value defined in the app's apps.py
configuration file. For the coffeehouse/about/urls.py
file its app_name='coffeehouse.about'
and for the coffeehouse/stores/urls.py
its app_name='coffeehouse.stores'
. With the help of the app_name
value, you're able to differentiate between the about url name='index'
and the stores url that also has a name='index'
.
To qualify a url name with app_name
you can use the syntax
<app_name>:<name>
. As you can see toward
the bottom of listing 2-18, to reference the index in the about
urls.py
you use coffeehouse.about:index
and to
reference the index in the stores urls.py
file you use
coffeehouse.stores:index
Tip An alternative to using the
app_name
attribute in childurls.py
files is to qualify the app name as part of theinclude
as a nested tuple definition in the mainurls.py
.For example, the
path('about/',include('coffeehouse.about.urls')),
statement in listing 2-18 with an app name would look likepath('about/',include(('coffeehouse.about.urls','coffeehouse.about'))),
, this in turn allows you to reference urls in'coffeehouse.about.urls'
with the app name'coffeehouse.about'
just a it's shown toward the end of listing 2-18. Although this alternative works, for such cases I recommend you use theapp_name
inside childurls.py
because it's cleaner and it will always take precedence over this alternative of passing the app name in theinclude
definition.Another example where this tuple technique is a better fit is when the main
urls.py
file literally imports theurlpatterns
list from a child file and as a consequence loses the visibility of theapp_name
value. In the mainurls.py
file you can have something likefrom coffeehouse.drinks.urls import urlpatterns as drinks_url_patterns
that imports theurlpatterns
list from thecoffeehouse.drinks.urls
file asdrinks_url_patterns
-- effectively losing sight of whateverapp_name
value is defined incoffeehouse.drinks.urls
-- and then in the same mainurls.py
file as part of itspatterns
value you can declarepath('drinks/', include((drinks_url_patterns,'coffeehouse.drinks'))),
so all the urls in thedrinks_url_patterns
list are accesible via thecoffeehouse.drinks
name.
In 95% of Django urls you can use
the name
, include
and app_name
parameters just as
they've been described. However, the app_name
parameter
takes on a slightly different meaning when you deploy multiple instances of the
same Django app in the same project.
Since Django apps are
self-contained units with url definitions, it raises an edge case when Django apps with a single app_name
are used multiple times. What happens if a Django
app uses app_name
X, but you want to deploy the app two or three
times in the same project ? How do you reference urls in each app,
given they're all written to use app_name
X ? This is where the
term app instance and the namespace
attribute come into the picture.
Let's walk through a scenario
that uses multiple instances of the same Django app to illustrate
this edge case associated with app_name
and how the namespace
attribute is used. Let's say you
develop a Django app called banners to display advertisements. The
banners app is built in such a way that it has to run on different
urls
(e.g./coffeebanners/
,/foodbanners/
,/teabanners/
)
to simplify the selection of banners. In essence, you require to
run multiple instances of the banners app in the same project, each
one on different urls.
So what's the problem of multiple app instances and url naming ? It has to do with using named urls that need to change dynamically based on the current app instance. This issue is easiest to understand with an example, so lets jump to the example in listing 2-19.
Listing 2-19 Django urls.py with multiple instances of the same app
---------------------------------------------------------------------------- # Contents coffeehouse/urls.py from django.urls import include, path urlpatterns = [ path('',TemplateView.as_view(template_name='homepage.html'),name="homepage"), path('coffeebanners/',include('coffeehouse.banners.urls'), namespace="coffee-banners"), path('foodbanners/',include('coffeehouse.banners.urls'), namespace="food-banners"), path('teabanners/',include('coffeehouse.banners.urls'), namespace="tea-banners"), ] ---------------------------------------------------------------------------- # Contents coffeehouse/banners/urls.py from django.urls import path from . import views from . import apps # apps.BannersConfig.name = coffeehouse.banners app_name = apps.BannersConfig.name urlpatterns = [ path('',views.index,name="index"), ] ---------------------------------------------------------------------------- # Definition in view method, using namespace value from django.http import HttpResponsePermanentRedirect from django.urls import reverse def method(request): .... return HttpResponsePermanentRedirect(reverse('food-banners:index')) ---------------------------------------------------------------------------- # Definition in template, using namespace value <a href="{% url 'coffee-banners:index' %}"> Got to coffee banners app instance</a> <a href="{% url 'food-banners:index' %}"> Got to food banners app instance</a> <a href="{% url 'tea-banners:index' %}"> Got to tea banners app instance</a>
In listing 2-19 you can see we
have three urls that point to the same
coffeehouse.banners.urls
file. Next, notice each of the path
declarations in addition to defining an include
statement also have a namespace
value. This namespace
value is what allows you to reference the different urls for the three different instances of the coffeehouse.banners.urls
file.
To qualify a url name with namespace
you can use the syntax <namespace>:<name>
. Notice toward the end of listing 2-19, how it's possible to use a namespace
value in both the reverse
method in views and {% url %}
tag in templates (e.g. the statement {% url 'tea-banners:index' %}
generates a link to /teabanners
; the statement reverse('food-banners:index')
generates a link to /foodbanners
url.
The ability to use <namespace>:<name>
to reference urls allows you to effectively access urls from multiple instances of the same Django app. However, by relying on just namespace
and name
url names can't dynamically adapt to different app instances, since you need to know beforehand (and hard-code) for which app instance you want to create a link (e.g. /coffeebanners
, foodbanners
, teabanners
).
Let's walk through a concrete scenario where the namespace
and name
values aren't sufficient. Suppose inside the banners app
you want to redirect control to the app's main index url (e.g. due
to an exception). Now put on an app designer hat, how would you
resolve this problem ? As an app designer you don't even know about
the coffee-banners, tea-banners or food-banners namespaces, as
these are deployment namespaces. How would you internally integrate
a redirect in the app that adapts to multiple instances of the app
being deployed ? This is where the app_name
parameter introduced in listing 2-18 comes into play once again.
Notice the urls.py
file in listing 2-19 of the banners app sets the
app_name
to point toward the app's name
value defined in the app's apps.py
configuration file. For the coffeehouse/banners/urls.py
file its app_name='coffeehouse.banners'
. Because Django relies on the same syntax convention to search for url names with app_name
or namespace
matches (i.e. <namespace>:<name>
or <app_name>:<name>
), it's perfectly valid to create a reverse
method as shown in listing 2.20.
Listing 2-20 Django redirect that leverages app_name with multiple app instances to determine url
---------------------------------------------------------------------------- # Definition in view method from django.http import HttpResponsePermanentRedirect from django.urls import reversew def method(request): .... return HttpResponsePermanentRedirect(reverse('coffeehouse.banners:index'))
So to what url do you think
coffeehose.banners:index
in listing 2-20 resolves to ? It all depends on
where the navigation takes place, it's dynamic! If a user is
navigating through the coffee-banners app instance (i.e. url
coffeebanners
) then Django resolves
coffeehose.banners:index
to the coffee-banners instance
index, if a user is navigating through the tea-banners app instance
(i.e. url teabanners
) then Django resolves
coffeehose.banners:index
to the tea-banners instance
index, and so on for any other number of instances. In case a user
is navigating outside of a banners app instance (i.e. there is no
app instance) then Django defaults to resolving
coffeehose.banners:index
to the last defined instance in
urls.py
which would be tea-banners.
In this manner and based on where
the request path instance a user is coming from (e.g if the user is
on a path with /coffeebanners/
or
/teabanners/
) the reverse method resolves
coffeehose.banners:index
dynamically to one of the three
url app instances vs. hard-coding specific url namespaces as shown
in listing 2-19.
Tip It's possible to override all of the above resolution behaviors by explicitly definingcurrent_app
value in arequest
object. For example, modifying therequest
torequest.current_app = 'food-banners'
prior to thereverse
statement, makes the url resolve to thefood-banners
app instance. The next section on View method requests contains more details about therequest
object.
Now let's assume the banners app
has an internal template with a link to the app's main
index
url. Similarly, how would you generate this link
in the template to take into account the possibility of multiple
app instances ? Relying on the same app_name
parameter
solves this problem for the template link illustrated in listing
2-21.
Listing 2-21 Django template link that leverages app_name to determine url
---------------------------------------------------------------------------- # template banners/index.html <a href="{% url 'coffeehouse.banners:index' %}">{% url 'coffeehouse.banners:index' %}</a>
Notice the {% url %}
tag in listing 2-21 points to coffeehouse.banners:index
, where coffeehouse.banners
is the app_name
and index
is the url's name
. The resolution process for the coffeehouse.banners:index
is the same outlined in the previous method example that uses the
reverse
method.
If a user is navigating through
the coffee-banners app instance (i.e. url
coffeebanners
) then Django resolves
coffeehouse.banners:index
to the coffee-banners instance
index, if a user is navigating through the tea-banners app instance
(i.e. url teabanners
) then Django resolves
coffeehouse.banners:index
to the tea-banners instance
index, and so on for any other number of instances. In case a user
is navigating outside of a banners app instance (i.e. there is no
app instance) then Django defaults to resolving
coffeehouse.banners:index
to the last defined instance in
urls.py
which would be food-banners
.
Tip It's possible to override all of the above resolution behaviors by explicitly definingcurrent_app
value in arequest
object. For example, modifying therequest
torequest.current_app = 'food-banners'
prior to sending control to a{% url %}
statement, makes the url resolve to thefood-banners
app instance. The next section on View method requests contains more details about therequest
object.
Finally, given the interchangeable syntax to access urls with either <namespace>:<name>
or <app_name>:<name>
, it's worth pointing out that it's also valid to have a nested <app_name>
with one or more <namespace>
values, followed by the url <name>
, with values separated by :
. Listing 2-22 shows an example with this kind of nested url name structure.
Listing 2-22. Django urls.py with nested app_name and namespace attribute
---------------------------------------------------------------------------- # Contents coffeehouse/urls.py from django.urls import include, path from django.views.generic import TemplateView urlpatterns = [ path('',TemplateView.as_view(template_name='homepage.html'),name="homepage"), path('stores/',include('coffeehouse.stores.urls'), ] ---------------------------------------------------------------------------- # Contents coffeehouse/stores/urls.py from django.urls import path from . import views from . import apps # apps.StoresConfig.name = coffeehouse.stores app_name = apps.StoresConfig.name urlpatterns = [ path('',views.index,name="index"), path('<int:store_id>/',views.detail,name="detail"), path('<int:store_id>/about/',include('coffeehouse.about.urls',namespace="nested-stores-about")), ] ---------------------------------------------------------------------------- # Contents coffeehouse/about/urls.py from django.urls import path from . import views from . import apps # apps.AboutConfig.name = coffeehouse.about app_name = apps.AboutConfig.name urlpatterns = [ path('',views.index,name="index"), path('contact/',views.contact,name="contact"), ] ---------------------------------------------------------------------------- # Definition in view method from django.http import HttpResponsePermanentRedirect from django.urls import reverse def method(request): .... return HttpResponsePermanentRedirect(reverse('coffeehouse.stores:nested-stores-about:contact', args=(store.id,))) ---------------------------------------------------------------------------- # Definition in template <a href="{% url 'coffeehouse.stores:nested-stores-about:contact' store.id %}">See about for {{store.name}}</a>
The url structure in listing 2-22
differs from the previous ones in that it creates about urls for each
store (e.g./stores/1/about/
) instead of having a
generic about url (e.g./about/
). At the top of listing
2-22 we keep using the app_name
to qualify all urls in
the stores urls.py
file to coffeehouse.stores
.
Next, inside the stores
urls.py
file notice there's another
include
element with namespace="nested-stores-about"
to
qualify all urls in the about urls.py
. And finally
inside the about urls.py
file, there are urls that
just use the name
attribute. In the last part of
listing 2-22, you can see how the nested values for <app_name>:<namespace>:<name>
are declared as coffeehouse.stores:nested-stores-about:contact
and used with the
reverse
method and {% url %}
tag.