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 child urls.py files is to qualify the app name as part of the include as a nested tuple definition in the main urls.py.

For example, the path('about/',include('coffeehouse.about.urls')), statement in listing 2-18 with an app name would look like path('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 the app_name inside child urls.py because it's cleaner and it will always take precedence over this alternative of passing the app name in the include definition.

Another example where this tuple technique is a better fit is when the main urls.py file literally imports the urlpatterns list from a child file and as a consequence loses the visibility of the app_name value. In the main urls.py file you can have something like from coffeehouse.drinks.urls import urlpatterns as drinks_url_patterns that imports the urlpatterns list from the coffeehouse.drinks.urls file as drinks_url_patterns -- effectively losing sight of whatever app_name value is defined in coffeehouse.drinks.urls -- and then in the same main urls.py file as part of its patterns value you can declare path('drinks/', include((drinks_url_patterns,'coffeehouse.drinks'))), so all the urls in the drinks_url_patterns list are accesible via the coffeehouse.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 defining current_app value in a request object. For example, modifying the request to request.current_app = 'food-banners' prior to the reverse statement, makes the url resolve to the food-banners app instance. The next section on View method requests contains more details about the request 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 defining current_app value in a request object. For example, modifying the request to request.current_app = 'food-banners' prior to sending control to a {% url %} statement, makes the url resolve to the food-banners app instance. The next section on View method requests contains more details about the request 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.