This commit is contained in:
Víðir Valberg Guðmundsson 2023-06-21 00:05:53 +02:00
parent a4882d30c2
commit 0e927e30a8
1 changed files with 63 additions and 14 deletions

View File

@ -10,7 +10,7 @@ Summary: Introducing django-view-decorator, a Django package which brings Locali
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:
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:
> The behaviour of a unit of code should be as obvious as possible by looking only at that unit of code
@ -31,9 +31,9 @@ It is not apparent how to access the view with HTTP. To see what URL the view is
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.
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 approach using a decorator which puts the URL information where the view is defined:
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:
:::python
# flask_example.py
@ -49,22 +49,37 @@ Just by looking at the decorator tied to the `foo` function we know that we can
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!
Can we apply the same pattern to 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:
First we setup our project URLconf to include URLs from `django-view-decorator`:
:::python
# project/urls.py (this is what we point the ROOT_URLCONF setting at)
from django.urls import path
from django_view_decorator import include_view_urls
urlpatterns = [
path("", include_view_urls()),
]
Then we can use the `view` decorator like so (we dive deeper in what the decorator does later on):
:::python
# foos/views.py
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.
We now have information about how the view is to be accessed right there next to the view itself.
Even class-based views are supported:
@ -75,7 +90,7 @@ Even class-based views are supported:
## More advanced usage
Not all views have only a single path, and you might have noticed that the argument is the plural `paths`:
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:
:::python
@view(
@ -104,6 +119,22 @@ Not all views have only a single path, and you might have noticed that the argum
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!
If we want different names for each path we can simply apply the decorator multiple times:
:::python
@view(
paths="/foo/",
name="foo_list",
namespace="foos",
)
@view(
paths="/foo/<int:id>/",
name="foo_detail",
namespace="foos",
)
def foo(request: HttpRequest, id: int | None = None) -> HttpResponse:
...
## Behind the scenes
So how does this work? Pretty much the same way as the `django.contrib.admin` module.
@ -181,15 +212,33 @@ At the time of writing `login_required`, `staff_required` and permission checkin
## 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?
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.
- `django.views.view` decorator
- Default `AppConfig.view_decorator` which can be configured like `namespaced_decorator_factory`
-
To cite Carlton Gibson from the previously mentioned talk:
> Put it in a third party package. See if there is community interest. Then maybe it gets merged to the core if there is.
So this is what I'm doing.
1. Write the third party package - check!
2. Write a blog post - check!
3. Gather community interest - ongoing
4. Merge!
[^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>
### How could this be manifested in Django?
I haven't started the work to write a PR to get this into Django just yet, but here are some of my initial thoughts on how this would work in Django.
First of I would probably locate the decorator at `django.views.view`. But it might event be a opt-in feature and live in `django.contrib.view_decorator` or something similary. Having it as a contrib package would make it "cleaner" to solve the "where do we put the auto discovery?" - it would be in `django.contrib.view_decorator.apps.ViewDecoratorAppConfig.ready`.
One idea I'm going to explore in `django-view-decorator` is to write an `AppConfig` subclass which uses `namespaced_decorator_factory` to generate a decorator for an app. This way there would be a way to get a decorator for a specific app using the "applications" framework in Django.
## What do you think?
So now I'm throwing the ball to the Django community!
What do you think? Should we just keep seperating views and URLs into different files or am I on to something? Does my solution have any major downsides which I have been blind to?
Come discuss on the Django forum in this dedicated thread:
<https://forum.djangoproject.com/>