Class-based URL configuration for Django projects

This article introduces a new concept in the upcoming Viewflow 2.0 library, Class-based URL configuration — Viewset. Viewsets allow developers to create reusable Django packages, simplifies configuration and redefinition behavior of a bunch of views with common functionality for an end user. In this article. we will go through Viewset functionality, usage patterns and compare them with other class-based implementations, like django.contrib.adminand django-rest-framework.

Introduction

Django is a web-framework famous for its “batteries included” philosophy. There is a lot of built-in functionality in the library and the broad set of third-party packages solves any imaginable problem you could have. The only question with any pre-built package is how it can be tuned to suit your needs?

You can tune a package settings or subclass and change behavior of a single class-based View. But unfortunately, typical packages have no knobs to change the single view without copying and pasting the whole url_patterns list.

app_name = 'myapp'url_patterns = [
path('', IndexView.as_view()),
path('add/', CreateView.as_view()),
path('list/', ListView.as_view()),
]

How to change IndexView here? How can we redefine common permission checks for Index and List views? Things become tricky when we have a couple of dozens of views.

Those are the problems that the Viewflow library faced when trying to provide generic and flexible CRUD admin and Business Workflow implementation.

Let’s see how switching to a class-based URL configuration could help.

Viewset is a class with .urls property, suitable for inclusion into the root url_pattern list. With little help of python meta-classes, a Viewset collects all class attributes with the name suffixed by `_url` into .urlslist

from viewflow.urls import Viewsetclass CRUDViewset(Viewset):
app_name = 'myapp'
index_url = path('', IndexView.as_view())
list_url = path('list/', ListView.as_view())

url_patterns = [
path('', MyViewset().urls)
]

Now, to customize and change a single view, in a third-party application, you can subclass a Viewset and override the single class attribute.

class CustomViewset(CRUDViewset):
index_url = path('', IndexView.as_view(
template_name='home/index.html')
)

A Viewset can be even more flexible

class AuthViewset(Viewset):
app_name = 'auth'
login_view_class = views.LoginView
queryset = User.objects.filter(is_staff=True)
def get_login_view_kwargs(self, **kwargs):
return {'viewset': self} | kwargs
@viewprop
def login_view(self):
return self.login_view_class.as_view(
**self.get_login_view_kwargs()
)
@property
def login_path(self):
return path('login/', self.login_view, name='login')
...

A Viewset attribute, or even any instance property marked with @viewprop decorator can be changed by passing an argument to the class constructor

url_patterns = [
path('', MyViewset(
login_view=my_login_view,
queryset=User.objects.filter(is_admin=True)
)
)

Share common logic

Viewset is the natural place to share common logic between related Views. You can pass a Viewset to the View, and View could call the shared Viewset method.

class AuthViewset(Viewset):
app_name = 'auth'
queryset = User.objects.filter(is_staff=True)
def get_login_view_kwargs(self, **kwargs):
return {'viewset': self} | kwargs
def get_users_view_kwargs(self, **kwargs):
return {'viewset': self} | kwargs
def has_perm(self, request, obj=None):
return request.user.is_staff

class
UserListView(ListView):
viewset=None
def get_queryset(self):
return self.viewset.queryset

Combining Viewsets

One Viewset can be included into another.

from viewflow.urls import routeclass ProfileViewset(Viewset):
app_name = 'profile'
index = path('', ProfileView.as_view())
edit = path('edit/', ProfileEditView.as_view())
class AuthViewset(Viewset):
app_name='auth'
profile_url = route('profile/', ProfileViewset())

It works the same, as using django.urls.include, Viewset namespaces being combined one after another.

from django.urls import reverse>>> reverse('auth:index')
/
>>> reverse('auth:profile:edit')
/profile/edit/

Access in templates

One of the goals of Viewset introduction, is to avoid repetitive logic in templates and common context instantiation in a set of Views. Viewset instance are available in templates as request.resolver_match.viewset variable.

{% load viewflow %}{% with viewset=request.resolver_match.viewset %}
<h1>{{ viewset.site_title }}</h1>
<a href="{% reverse viewset 'list' %}">Edit users</a>
{% endwith %}

reverse template tag from the viewflow library, helps to get other views locations without hard-coding namespaces in the template.

Other solutions

django.contrib.admin application faces the same problems that Viewsets are trying to resolve. Unlike Viewflow, Django admin does not extract URL config to the separate class, and keeps all logic, including views in the single class. This approach reduces the glue code size but makes adding new functionality a bit cumbersome.

Writing custom views for Django admin is a bit tricky, not only because you have to override get_urls method, but also, because you need to figure out, what additional context variables view should be provided to make Django admin template work.

django-rest-framework has Routers the classes that generate a common set of URLs for REST views. It seems router classes are not used to share common logic, but in the rest approach, we had all views bundled into a single resource class that is responsible to handle CRUD logic and custom actions.

Check yourself!

Viewset is the new layer of abstraction, and makes a code-base more modular. Using Viewsets, the Viewflow library provides the flexible implementation for CRUD, Workflow, and overall Site navigation.

The overall concept of class-based URL configuration could improve the re-usability and flexibility of any third-party packages for Django

To test class-based configurations, you can install alpha version Viewflow right now, with this simple command:

pip install django-viewflow --pre

Reusable workflow library #django #python http://viewflow.io

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store