Custom Model.Meta Options and Extending Migrations Autodetection

73 views
Skip to first unread message

Benjamin Dummer

unread,
Jan 9, 2018, 12:22:29 PM1/9/18
to Django developers (Contributions to Django itself)
I'm working on a third party app that needs to integrate with the migrations framework and I'm having trouble finding any built in way to do that.

The app I'm working on allows you to defined "revision logs" that tracks changes to a source model. So you get something like this:
from django.db import models
from my_app.revision import RevisionLog, TrackModel


class Contact(models.Model):
   
""" The model to be tracked. """
    name
= models.CharField(max_length=100)
    email = models.EmailField()


class ContactRevisionLog(RevisionLog):
    """ Revision log tracking Contact model. """
    class Meta:
        copy_fields = {'name': 'name'}

    model = TrackModel(Contact)
    name = models.CharField(max_length=100)



The RevisionLog is a normal abstract model with some fields on it. The app works by creating SQL triggers that create a new RevisionLog every time a change is made to the model being tracked, basically an audit trail.
If you don't like SQL triggers please keep it to yourself, I'm aware of the signals framework...

Anyways, I've poured over the migrations autodetector and related code and there doesn't appear to be any possible way to hook into it. So my solution right now is to provide my own makemigrations management command which silently replaces the built in one (I had no idea Django allowed you to do this without patching). The command looks like this:
from django.core.management.commands import makemigrations
from my_app.migration_ops import inspect_changes


class Command(makemigrations.Command):
    def write_migration_files(changes):
        """ Override here so we can inspect the changes and possibly insert our own operations """
        changes = inspect_changes(changes)
        super(Command, self).write_migration_files(changes)


So that works well enough and at least I have some reasonable way to hook into makemigrations and insert my own operations. The idea is that a user of my_app can run makemigrations like normal and the appropriate operations will be generated, including operations that manage the SQL triggers that I need.

The last big hurdle to get this working the way I want is that Django doesn't allow custom attributes on Model.Meta. The copy_fields option seen above has an effect on the SQL trigger that is generated, so if a user changes its value and runs makemigrations a new migration should be made. Changes made to the builtin meta options are picked up by the migrations autodetector so I assumed I could take advantage of that and define my own but Django validates all the attributes.

It looks like my options are to either:
  1. monkeypatch the list of allowed meta options, although I'm wary that this might have strange side effects elsewhere
  2. use an attribute directly on the model class, but then there's no obvious way to have the migrations autodetector pick this up

Benjamin Dummer

unread,
Jan 9, 2018, 7:28:46 PM1/9/18
to Django developers (Contributions to Django itself)
I actually ended up solving this a different way. The RevisionLog model classes are required to have a TrackModel field defined, so I made copy_fields an option defined on the field instead of the model/meta. So I have something like:
from django.db import models


class TrackModel(models.ForeignKey):
   
def __init__(self, to, copy_fields, **kwargs):
       
self.copy_fields = copy_fields
       
super(TrackModel, self).super(to, **kwargs)
   
   
def deconstruct():
        name
, path, args, kwargs = super(TrackModel, self).deconstruct()
        kwargs
['copy_fields'] = self.copy_fields
        return name, path, args, kwargs


This is actually a nicer api for my use case, doesn't require any hacks/patches, and the migration autodetector will pick up changes.

Now the only problem I have is trying to figure out how to reference an implicit m2m through model in a FK field.... Given a m2m field, the through attribute is apparently abstract and so can't be referenced without requiring an explicit through model.
Reply all
Reply to author
Forward
0 new messages