Django: Disable signals during migration

3.3k Views Asked by At

I am writing a small audit log app in Django, which hooks into various signals and writes events to the database. Unfortunately, some of those events are fired already before the corresponding table exists, leading to all sorts of trouble.

To avoid this, I tried to add some trigger to temporarily disable logging during a migration / syncdb - something like this:

from django.db.models.signals import pre_save, post_save, pre_delete
from django.db.models.signals import pre_syncdb, post_syncdb
from south.signals            import pre_migrate, post_migrate

IS_MIGRATING = False

@receiver(post_save)
def log_model_update(sender, **kwargs):
    if IS_MIGRATING:
        return
    ...

@receiver(pre_syncdb)
@receiver(pre_migrate)
def _disable_auditlog(sender, **kwargs):
    global IS_MIGRATING
    IS_MIGRATING = True

Unfortunately, this does not work as intended. What am I missing? Or is there an "official" way to do this?

4

There are 4 best solutions below

1
On

In Django it's possible to disconnect signals. So you can try disconnecting the signals somewhere before the migration code that's causing trouble

Must clarify that I haven't tried it so am not entirely sure if it will work but I hope it helps in some way

0
On

I've solved it now by doing an ugly hack and checking sys.argv and disabling the event handlers during test, syncdb and migrate. This is obviously not a good solution, but disabling the event handlers as shown in the question is not enough and causes interference with other django apps that's almost not traceable.

It needs to be disabled for testing, as Django/South obviously migrates the database during testing. Tests that involve event handlers need to activate event handling explicitly.

0
On

In Django you can use QuerySet.update() to bypass all signals.


Keep in mind that if you have a QuerySet, calling update(arg1=value1, arg2=value2) will write the same value in fields arg1 and arg2 in all the QuerySet items.

So if you need to set different values in each object, you can use this to update them one by one:

def update_and_skip_signals(my_obj: MyModel, **kwargs):
    # NOTE: do no use get() here, it will return a single instance 
    #       and we want a QuerySet instead
    qs = MyModel.objects.filter(pk=my_obj.pk)
    qs.update(**kwargs)

for item in my_queryset.all():
    update_and_skip_signals(item, arg1=value1, arg2=value2)
0
On

You can use:

#run inside signal function
if sender._meta.object_name != 'Migrate':
  #signal logic

Alternatively, you could create a list of models that the signal is run on, which is better because this way you know exactly when the signal is running:

ALLOWED_MODELS = [
   'model_1',
   'model_2',
]

#run inside signal function
if sender.meta.object_name in ALLOWED_MODELS:
   #signal logic

To avoid creating a list you could base it on any model that has a field:

#run inside signal function
if "field name" in [field.name for field in sender._meta.get_fields()]:
   #signal logic

If ever you want to know what object is triggering your signal, place this inside your signal:

print(sender._meta.object_name)