I started a newsletter!
You can read more about it here.

Nik Kantar

Friday, January 15, 2021

Django Site Dispatch

Django’s “sites” framework is quite cool, but what if you need to make some exceptions?

I’ve been hacking away at Microblot lately, and I’m using Django’s “sites” framework for the different blogs. However, there are some exceptions to this, and I put together a short dispatch view to handle it.

This post assumes you understand the following Djangoisms:

Background + Problem

Microblot is a blogging platform, and a registered blog gets its own subdomain (e.g., foo.microblot.io). All requests made to all subdomains of microblot.io are sent to the same Django app, which then uses the subdomain to find the relevant Site and Blog entries. Easy enough, and exactly the kind of use the “sites” framework is intended for, from what I can tell.

The first exception is the main site, which isn’t one of the blogs, but rather a few static pages—home page, privacy policy, terms of use, etc.—and the Slack integration API. Since the blogs are fundamentally something different, www.microblot.io/privacy and www.microblot.io/api/slack/new should work, but foo.microblot.io/privacy and foo.microblot.io/api/slack/new should return a 404, for example. As such, a different set of URLs and views is needed.

The second exception is the built-in URL shortener. For a full post URL foo.microblot.io/posts/asdf1234, the short URL blot.click/asdf1234 should redirect to it. This means the redirects need their own set of URLs and views as well.

Solution

Since the number of exceptions is actually fairly small and well known, I put together a function based view that acts as a dispatcher and determines which actual view to return based on the request domain.

Here it is in its entirety:

# microblot/core/views.py

from django.conf import settings
from django.http import Http404

def dispatch(request, main_class=None, cms_class=None, short_class=None, **kwargs):
    classes = {
        settings.FULL_DOMAIN: main_class,
        f"www.{settings.FULL_DOMAIN}": main_class,
        settings.SHORT_DOMAIN: short_class,
        f"www.{settings.SHORT_DOMAIN}": short_class,
    }
    target_class = classes.get(request.site.domain, cms_class)

    if target_class is None:
        raise Http404()

    return target_class.as_view()(request, **kwargs)

The logic is pretty simple:

Here it is in action:

# config/urls.py

from django.urls import include, path
from microblot.cms.views import BlogHomeView
from microblot.core.views import dispatch
from microblot.main.views import MainHomeView
from microblot.shortener.views import ShortenerHomeRedirectView

urlpatterns = [
    path(
        "",
        dispatch,
        {
            "main_class": MainHomeView,
            "cms_class": BlogHomeView,
            "short_class": ShortenerHomeRedirectView,
        },
        name="dispatch-home",
    ),
    path("", include("microblot.cms.urls")),
    path("", include("microblot.shortener.urls")),
]
# microblot/cms/urls.py

from django.urls import path
from microblot.core.views import dispatch
from .views import BlogPostView

# NOTE: See config/urls.py for some shared routes.

urlpatterns = [
    path(
        "posts/<post_short_code>",
        dispatch,
        {
            "cms_class": BlogPostView,
        },
        name="dispatch-post",
    ),
]
# microblot/shortener/urls.py

from django.urls import path
from microblot.core.views import dispatch
from .views import ShortenerPostRedirectView

# NOTE: See config/urls.py for some shared routes.

urlpatterns = [
    path(
        "<post_short_code>",
        dispatch,
        {
            "short_class": ShortenerPostRedirectView,
        },
        name="dispatch-short",
    ),
]

The real value is shown in the first example, in config/urls.py, where the same URL pattern is supported by multiple parts of the site. In this case, the “home pages” of www.microblot.io, foo.microblot.io, and blot.click all do different things:

Another example is the upcoming category support. Microblot will support specifying a category for each post, and said categories will get their own pages. Microblot may also have a global blog and feed, which would also support categories. In light of the above, a URL of category/<category_slug> may be supported by both the main site and the CMS, resulting in the following definition:

# added to config.urls.py

from django.urls import path
from microblot.main.views import MainCategoryView
from microblot.cms.views import BlogCategoryView

urlpatterns = [
    path(
        "category/<category_slug>",
        dispatch,
        {
            "main_class": MainCategoryView,
            "cms_class": BlogCategoryView,
        },
        name="dispatch-category",
    ),
]

There is one major downside to this approach: it doesn’t scale all that well if you have a lot of URLs that need to account for multiple classes, since those should probably live somewhere neutral, thus moving them away from their app.

Since I only have two exceptions, and at the moment only the top URL needs to account for more than one class, I think it’s fine. It’s certainly way cleaner than anything else I thought of, like decorating view classes all over the place to limit for which domains they run.

I probably wouldn’t say I love it, but it works! :D