Title: Bringing Locality of Behaviour to Django Views and URLs
Date: 2023-06-20
Status: hidden
Tags: django, views, locality-of-behaviour
Slug: bringing-locality-of-behaviour-to-django-views-and-urls
Authors: Víðir Valberg Guðmundsson
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[^0]. The principle states:

> The behaviour of a unit of code should be as obvious as possible by looking only at that unit of code

Given a very simple view:

    :::python
    # views.py
    def foo(request: HttpRequest) -> HttpResponse:
        return "bar"

It is not apparent how to access the view with HTTP. To see what URL the view is tied to we have to look at urls.py:

    :::python
    # urls.py
    from django.urls import path
    from .views import foo
    urlspatterns = [
        path("foo/", foo, name="foo")
    ]

Carlton Gibson mentions this in is talk at DjangoCon Europe 2023[^1], 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 approach using a decorator which puts the URL information where the view is defined:

    :::python
    # flask_example.py
    from flask import Flask

    app = Flask(__name__)

    @app.route("foo/")
    def foo():
        return "bar"

Just by looking at the decorator tied to the `foo` function we know that we can access it via `foo/`. 

Another pitfall due to this disconnect is that there is no guarantee that a view has a URL pointing at it.

Can we solve this in Django? Yes we can!

# Introducing django-view-decorator!

## Basics

At its core `django-view-decorator` has the `view` decorator, which, after a bit of setup[^2], works like so:

    :::python
    from django_view_decorator import view

    @view(paths="/foo/", name="foo")
    def foo(request: HttpRequest) -> HttpResponse:
        return "bar"

We now have information about how the view is accessed right there next to the view code itself.

Even class-based views are supported:

    ::python
    @view(paths="/foo/", name="foo-list")
    class FooList(ListView):
        model = Foo

## More advanced usage

Not all views have only a single path, and you might have noticed that the argument is the plural `paths`:

    :::python
    @view(
        paths=[
            "/foo/",
            "/foo/<int:id>/",
        ], 
        name="foo",
        namespace="foos",
    )
    def foo(request: HttpRequest, id: int | None = None) -> HttpResponse:
        foos = Foo.objects.all()

        if id:
            context = {"foo": get_object_or_404(foos, id=id)}
            template_name = "foo_detail.html"
        else:
            context = {"foos": foos}
            template_name = "foo_list.html"

        return render(
            request,
            template_name=template_name,
            context=context
        )

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!

## Behind the scenes

So how does this work? Pretty much the same way as the `django.contrib.admin` module.

The `django_view_decorator.apps.ViewDecoratorAppConf.ready` method runs `django.utils.module_loading.autodiscover_modules` which looks for all `views.py` modules in installed apps and imports these. This means that when `@view` occurrences are loaded in, we can put the views and associated metadata into a registry which we then can ask to write our `urlpatterns` list for us. 

So the `@view` decorator is quite similar to the well-known `@admin.register` decorator.

## Namespaces and the power of factories

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. One of the nice things about Django URLconfs is that we get namespacing by using the `include` function.

This is where the aptly named `namespaced_decorator_factory` comes into the picture. Let us look at an example:

    :::python
    # foo/views.py
    from django_view_decorator import namespaced_decorator_factory

    foo_view = namespaced_decorator_factory(
        namespace="foos", 
        base_path="foos/",
    )

    @foo_view(paths="", name="list")
    def foo_list(request: HttpRequest) -> HttpResponse:
        return "foo list"

    @foo_view(paths="<int:id>", name="detail")
    def foo_detail(request: HttpRequest, id: int) -> HttpResponse:
        return "foo detail"


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:

    :::python
    # app_1/views.py
    app_1_view = namespaced_decorator_factory(
        namespace="app_1", 
        base_path="app_1/",
    )

    # app_2/views.py
    from app_1.views import app_1_view

    @app_1_view(
        paths="my-custom-view/",
        name="custom-view"
    )
    def custom_view(request: HttpRequest) -> HttpResponse:
        return "I'm in another namespace"


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!

## One decorator to rule them all?

One feature of `django-view-decorator` which I'm not sure if makes sense, is the idea to embed other decorators into it. For example, instead of:

    :::python
    @view(paths="/foo/", name="foo")
    @login_required
    def foo(request: HttpRequest) -> HttpResponse:
        return "bar"

We can just write:

    :::python
    @view(paths="/foo/", name="foo", login_required=True)
    def foo(request: HttpRequest) -> HttpResponse:
        return "bar"

At the time of writing `login_required`, `staff_required` and permission checking is included in the decorator. But either the decorator should support, at least, all decorators in `django.views.decorators`, or not at all. 

## The path to Django core

So, as I wrote initially, I have a mission to try to get this into Django core. This is not going to be an easy feat. So how could this be manifested in Django?

- `django.views.view` decorator
- Default `AppConfig.view_decorator` which can be configured like `namespaced_decorator_factory` 
- 




[^0]: <https://htmx.org/essays/locality-of-behaviour/>
[^1]: <https://youtu.be/_3oGI4RC52s?t=315>
[^2]: <https://django-view-decorator.readthedocs.io/en/latest/quickstart.html>
