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
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"
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.