Summary: Introducing django-view-decorator, a Django package which brings Locality of Behaviour to your Views and URLs
---
It seems that "The Location of Behaviour principle" (shortened as LoB) is gaining traction these days. This has given me the urge to try to influence the direction of Django to bring more LoB to the connection between views and URLs.
But first, what is "LoB"? The principle is coined by the author of HTMX in [a short, but great, essay](https://htmx.org/essays/locality-of-behaviour/). The principle states:
Carlton Gibson [mentions this](https://youtu.be/_3oGI4RC52s?t=315) in is talk at DjangoCon Europe 2023, and why this means that he often puts view code in the same file as his URLs.
But why this disconnect? Other frameworks, like Flask and FastAPI use a rather simple "pattern" using a decorator which puts the URL information where the view is defined:
[`django-view-decorator`](https://github.com/valberg/django-view-decorator/) is my attempt to implement a decorator which can be used to apply this pattern to Django views.
Multiple URLs can point at the same view, and you might have noticed that the argument is the plural `paths`. This is because we can pass a list of paths which point at the same view. Like so:
Looking at the view we can grok that it is exposed on two paths, under the `foos` namespace, one which lists all `Foo` objects and one which given an integer gives us the detail for a single `Foo`. That's pretty powerful if you ask me!
`django-view-decorator` works by having a registry in which all views and their URL information is stored. In fact the mechanism to do most of this work is the same as is used by `django.contrib.admin`. The `@view` decorator is quite similar to the well-known `@admin.register` decorator.
1. When Django starts and the app registry is ready, the `ready` method of `django_view_decorator.apps.ViewDecoratorAppConf` gets run.
2. The `ready` method calls `autodiscover_modules` from `django.utils.module_loading`. This imports `views.py` files from all apps in `INSTALLED_APPS`. The admin does the same thing, it just imports `admin.py` files.
3. By importing a `views.py` file we run all `@view()` invocations
4. In `view` decorator we gather information provided as arguments to the decorator and store this in a registry which is located at `django_view_decorator.apps.ViewRegistry`.
5. We can now use `ViewRegistry.urlpatterns()` to get the `urlpatterns` for all registrered views. The `include_view_urls` helper function exists to do exactly this.
One thing to note is that since we are only looking for `views.py` modules, views placed in other modules will not be picked up. To solve this the `include_view_urls` function takes a `extra_modules` arguments which works like so:
:::python
# project/urls.py
from django.urls import path
from django_view_decorator import include_view_urls
urlpatterns = [
path("", include_view_urls(
extra_modules=["foos.special_views"]
))
]
This would include views from `foos/special_views.py` in the registry.
In our more advanced example you might have noticed the `namespace="foos"`, which probably is going to quite tedious to repeat over and over again. In vanilla Django URLconfs we get namespacing by using the `include` function. So how do we do namespacing with this new pattern?
By calling `namespaced_decorator_factory` we get a specialised decorator for our namespace and we can even provide it with a path which will be prepended to all URLs registered using it.
This opens up a quite nifty possibility of injecting URLs into a namespace from anywhere. For example:
Now we can treat `custom_view` as if it was a part of the `app_1` namespace. Ie. `reverse("app_1:custom-view")` would give us `app_1/my-custom-view/`. Neat!
In the process of writing this blog post and trying to figure out how all this could be implemented into Django, I came up with the idea to leverage the applications framework in Django to get a namespaced decorator for a given Django app.
I'm quite keen on this idea and I feel this might be an entry into introducing this pattern into Django.
Newcomers would learn that to hook up views to URLs they register their views into the app which the view belongs to - very much like how Flask does it with `@app`.
So, as I wrote initially, I have a mission to try to get this pattern into Django core. This is not going to be an easy feat, but I want to be upfront about my plan. Partly because I think it is going to enhance my chances of succeeding, but also so the current project can move in the right direction.
My best case scenario would be to land it in Django 5.1, but then I'm close to daydreaming.
No matter whether my plan to get this pattern, in some form or another, into Django will succeed, I will continue to maintain `django-view-decorator` as an alternative to the current `views.py/urls.py` approach.
What do you think? Should we just keep views and URLs separate or am I on to something? Does my solution have any major downsides which I have been blind to? Is there any missed opportunities in my implementation that would make it sing even more?