Creation of default site not triggering post_save signal

134 views
Skip to first unread message

Matt Thompson

unread,
Sep 10, 2016, 9:59:56 PM9/10/16
to Django users
Hi All,

I have a small Django app that uses the sites framework and has a post_save signal on Site that does some bits and bobs when a new site is created.  On 1.9.7, when you run migrations for the first time, it correctly triggers the post_save signal.  However, after upgrading to 1.10.1, dropping the database, and re-running migrations, the post_save signal I have setup on Site doesn't seem to be triggering.  I tried reading the 1.10 release notes and while there was mention of a pre_migrate() and post_migrate() signals, I couldn't make heads or tails on whether that is related.

If this is a deliberate change, can someone please point me to the specifics?  If it's a bug, I'd be more than happy to file a bug.

Thanks!

--Matt

Simon Charette

unread,
Sep 10, 2016, 10:42:16 PM9/10/16
to Django users
Hi Matt,

I worked on the changes that broke your code.

Starting with 1.10 the migration signals are passed an `apps` kwarg representing
the state of models before and after the specified migrations are applied.

This allows signal receivers to manipulate model definitions that are
synchronized with their actual database representation just like a data
migrations would (e.g. `RunPython` operations).

Before this change was introduced the signal receivers were performing queries
using the globally available model definitions which could lead to failures
when querying the underlying tables as their schema were not synchronized with
their global model definitions.

In your case, the signals you attached to the global `Site` model are not fired
anymore as the queries are performed using the migration specific one which
you cannot attach listeners to.

It's hard for me to come up with a complete solution without the actual code
attached to `post_save(sender=Site)` but something along these lines should do:

@receiver(post_migrate)
def sync_site(plan, **kwargs):
    # A migration of the `django.contrib.sites` app was applied.
    if plan and any(migration.app_label == 'sites' for migration, _ in plan):
        # ... perform actions

Let me know if you need more details,
Simon

Matt Thompson

unread,
Sep 12, 2016, 10:59:34 PM9/12/16
to Django users
Hi Simon,

Thank you very much for your prompt reply!  I've added the following to my app, which seems to work just fine:

@receiver(models.signals.post_migrate)
def gen_site_config_post_migrate(plan, **kwargs):
    # A migration of the `django.contrib.sites` app was applied.
    if plan and any(migration.app_label == 'sites' for migration, _ in plan):
        try:
            site = Site.objects.get(name='example.com')
        except ObjectDoesNotExist:
            pass
        else:
            SiteConfig.objects.get_or_create(
                site=site,
                admin_email='ad...@example.com',
                remote=False
            )

It seems the code inside the if statement is called several times (7, to be exact) when I run initial migrations, with the site finally getting created at the end.  This is why I've had to add some exception handling for when the site doesn't exist.

Simon, one last question for you -- where should this code live?  I currently have it shoved in my model file but that doesn't feel quite right.

Thank you again!

--Matt

Simon Charette

unread,
Sep 13, 2016, 8:58:55 AM9/13/16
to Django users
Hi Matt,

The recommended pattern is to register signal receivers in your app config's
ready() method[1]. From the Django documentation:

> Where should this code live?
>
> Strictly speaking, signal handling and registration code can live anywhere you
> like, although it’s recommended to avoid the application’s root module and its
> models module to minimize side-effects of importing code.
>
> In practice, signal handlers are usually defined in a signals submodule of the
> application they relate to. Signal receivers are connected in the ready()
> method of your application configuration class. If you’re using the receiver()
> decorator, simply import the signals submodule inside ready().

Cheers,
Simon

[1] https://docs.djangoproject.com/en/1.10/topics/signals/#connecting-receiver-functions
Reply all
Reply to author
Forward
0 new messages