Ivan Ivan - 3 months ago 30
Python Question

Django apps resolving to the wrong namespace

In my project I have three apps, "abc", "xyz" and "common." Common isn't a real app inasmuch as it just stores templates, models and views that are inherited and extended by both apps.

Project-level urls.py looks like so, and properly redirects requests to the respective app:

urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^abc/', include('abc.urls')),
url(r'^xyz/', include('xyz.urls')),
]


Both apps' url.py files look like so; the ONLY difference is replace every instance of ABC with XYZ:

from django.conf.urls import url
from views import ABCAlertList as AlertList
from views import ABCEventList as EventList
from views import ABCEventDetail as EventDetail
from views import ABCEventReplay as EventReplay
from views import ABCUploadView as UploadView

urlpatterns = [
url(r'^$', AlertList.as_view(), name='view_alerts'),
url(r'^entities/(?P<uid>\w+)/$', EventList.as_view(), name='view_uid'),
url(r'^entities/(?P<uid>\w+)/replay/$', EventReplay.as_view(), name='view_replay'),
url(r'^entities/(?P<uid>\w+)/event/(?P<eid>\w+)/$', EventDetail.as_view(), name='view_event'),

url(r'^upload/$', UploadView.as_view(), name='upload_file'),
]


Again, all the views are common between both apps so there is nothing app-specific to either of them. Both apps make use of the same line in the same common template:

<a href="{% url 'view_uid' alert.uid %}">


Now, the problem:

App ABC works fine on the top-level page. But the urls it's rendering to go past that point point to the wrong app.

For example, I'll be in

http://localhost:8888/abc/


and the urls on that page render as

http://localhost:8888/xyz/entities/262b3bce18e71c5459a41e1e6d52a946ab47e88f/


What gives? It looks like Django is reading the wrong app's urls.py.

Answer

Django can't tell the difference between the URLs under abc/ and xyz/ just by the view name and arguments. Since reversing will go through the patterns in reverse order, the patterns under xyz/ will always match first, so all links generated using reverse() or the {% url %} tag will point to the xyz app.

You need to give each pattern a unique name, or use a URL namespace. In Django 1.9+ you should set the app_name attribute:

app_name = 'abc'
urlpatterns = [
   url(r'^$', AlertList.as_view(), name='view_alerts'),
   url(r'^entities/(?P<uid>\w+)/$', EventList.as_view(), name='view_uid'),
   url(r'^entities/(?P<uid>\w+)/replay/$', EventReplay.as_view(), name='view_replay'),
   url(r'^entities/(?P<uid>\w+)/event/(?P<eid>\w+)/$', EventDetail.as_view(), name='view_event'),

   url(r'^upload/$', UploadView.as_view(), name='upload_file'),
]

In Django 1.8 you need to pass the namespace parameter to include():

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^abc/', include('abc.urls', namespace='abc')),
    url(r'^xyz/', include('xyz.urls', namespace='xyz')),
]

You can then reverse the url by passing the proper namespace:

<a href="{% url 'abc:view_uid' alert.uid %}">

If you need to use the same templates or functions for both apps, you need to set the application namespace of both apps to be the same, but use a different instance namespace.

In 1.9+, this means using the same app_name attribute, but passing a different namespace argument:

# myapp/urls.py
app_name = 'common_app_name'
urlpatterns = [
    # app urls
]

# myproject/urls.py
urlpatterns = [
    url(r'^abc/', include('abc.urls', namespace='abc')),
    url(r'^xyz/', include('xyz.urls', namespace='xyz')),
]

In templates, you need to use the application namespace to reverse urls. The current instance namespace is automatically taken into account. In calls to reverse() you need to pass the current namespace:

reverse('common_app_name:view_alerts', current_app=request.resolver_match.namespace)

In Django 1.8 you don't have the app_name attribute, you need to pass it as a parameter to include():

urlpatterns = [
    url(r'^abc/', include('abc.urls', namespace='abc', app_name='common_app_name')),
    url(r'^xyz/', include('xyz.urls', namespace='xyz', app_name='common_app_name')),
]

Django 1.8 also won't automatically use the current instance namespace in calls to the {% url %} tag. You need to set the request.current_app attribute for that:

def my_view(request):
    request.current_app = request.resolver_match.namespace
    ...