Django’s “Sites” app initial data migration

Note: Written using Django 3.1.3 July 2021

TL;DR: If you enable django.contrib.sites use your data migration to create not update the example.com values the sites app makes for you.

If you’re enabling the Django “Sites” framework (from django.contrib.sites) then you’ll notice that the documentation tells you to add:

SITE_ID = 1

To your settings.py file. This is the auto-field ID of the Site model instance in your database.

The documentation also states that this row is created using a post_migrate hook in the django.contrib.sites App definition. This initial model instance is created with example.com as the domain and example as the name.

Most of this is inconsequential, but if you’re serving multiple domains using the Sites framework then this value needs to be set correctly and the example.com value replaced in the database.

Handily the documentation states that you can update this field with a data migration.

The issue is that the post_migrate hook for the django.contrib.sites app is called after all migrations happen. This means when setting up a database from scratch, your data migration always runs before the row is created.

My initial migration looked something like this:

from django.db import migrations
from django.conf import settings

def update_default_site_config(apps, schema_change):
    SiteModel = apps.get_model('sites', 'Site')
    default_site = SiteModel.objects.get(id=settings.SITE_ID)
    default_site.domain = 'my-website.com'
    default_site.name = 'My Website'
    default_site.save()

class Migration(migrations.Migration):

    dependencies = [
        ('my_app', '0001_initial'),
        ('sites', '0002_alter_domain_unique'),
    ]

    operations = [
        migrations.RunPython(update_default_site_config)
    ]

Setting aside how janky SITE_ID=1 is in settings.py, this migration crashed with the following error:

    raise self.model.DoesNotExist(
__fake__.DoesNotExist: Site matching query does not exist.

This is because, at the point your migration runs, the post_migrate hook in the sites app hasn’t ran yet. The Django documentation makes it seem like you “update” the initial Site instance via a data migration.

It can’t possibly be that there’s no way to make an initial database without it always having example.com as your default site? That’d also mean ignoring the DoesNotExist in the migration and maybe force-running it?

None of that makes sense. Digging into the post_migrate hook in the django.contrib.sites app, we see the following code:

    if not Site.objects.using(using).exists():
        # The default settings set SITE_ID = 1, and some tests in Django's test
        # suite rely on this value. However, if database sequences are reused
        # (e.g. in the test suite after flush/syncdb), it isn't guaranteed that
        # the next id will be 1, so we coerce it. See #15573 and #16353. This
        # can also crop up outside of tests - see #15346.
        if verbosity >= 2:
            print("Creating example.com Site object")
        Site(pk=getattr(settings, 'SITE_ID', 1), domain="example.com", name="example.com").save(using=using)

This means that the post_migrate hook only sets up the default site if some prior hook or data migration didn’t create it first.

What the Django documentation means, is your data migration needs to create the site, not update one that Django creates by default. Here’s the working data migration:

from django.db import migrations

def update_default_site_config(apps, schema_change):
    SiteModel = apps.get_model('sites', 'Site')
    SiteModel.objects.get_or_create(domain='my-website.com', name='My Website')

class Migration(migrations.Migration):

    dependencies = [
        ('my_app', '0001_initial'),
        ('sites', '0002_alter_domain_unique'),
    ]

    operations = [
        migrations.RunPython(update_default_site_config)
    ]

This migration is always ran before the post_migrate hook in the Sites app. This will leave your intitial database creation in a “Sites friendly” state with correct values.

In my opinion, the Sites framework leaking primary key values into settings.py is really ugly and not at-all well thought out. I’d much rather see:

DEFAULT_SITE_DOMAIN = "my-website.com"
DEFAULT_SITE_NAME = "My Site"

In settings.py and let the internals take care of it.

The comments in the post-migrate function reveal lots of fallout from this leakiness. This issue in particular is a really good example of why the design is not ideal.

Poor documentation compounds the issue. The documentation isn’t very clear on how to setup your initial database in the best way.