Django migrations and customizable reusable apps

2020-07-18 03:42发布

问题:

I started writing my first reusable app about 3 weeks ago, and I'm having troubles to deal with migrations.

I want some points of my app to be customizable. Thus I have a conf submodule that defines custom settings and assign the reasonable defaults that will fit most cases.

This leads some of my model fields to look like this:

attachment = models.FilePathField(
    path=conf.ATTACHMENTS_DIR, recursive=True)

template_file = models.FileField(
    upload_to=conf.TEMPLATES_UPLOAD_DIR, blank=True)

prefix_subject = models.BooleanField(
    default=True, verbose_name=_("prefix subject"),
    help_text=_(
        "Whether to prefix the subject with \"{}\" or not."
    ).format(conf.SUBJECT_PREFIX))

Unfortunately, on projects using this app, this causes django-admin makemigrations to create migrations for it every time a setting changes. Or even, the first time they install the app for settings which default value depends on the host system.

I'm more than doubtful about the legitimacy of a project creating its own migrations inside his copy of an app. But if I'm wrong, tell me.

For prefix_subject in the above sample, I solved the issue with this solution. Considering that losing the help_text information in migrations was not very effective.

However, I'm not sure this is a suitable solution for all/most cases. Is it?

I thought about another solution that seems to work. This solution is to manually edit migrations and replace evaluated settings by the variable itself.

For instance, I would replace this:

migrations.CreateModel(
    name='MailStaticAttachment',
    fields=[
        ('id', ...),
        ('filename', ...)
        ('mime_type', ...)
        ('attachment', models.FilePathField(path='/home/antoine/Workspace/django-mailing/static/mailing/attachments', recursive=True, verbose_name='file')),
    ],
    options={...}
),

With :

from ..conf import ATTACHMENTS_DIR

migrations.CreateModel(
    name='MailStaticAttachment',
    fields=[
        ('id', ...),
        ('filename', ...)
        ('mime_type', ...)
        ('attachment', models.FilePathField(path=ATTACHMENTS_DIR, recursive=True, verbose_name='file')),
    ],
    options={...}
),

Does it look like a good solution to you?

What do you advise to do in such cases?

I think both Field.help_text, FilePathField.path and FileField.upload_to attributes are not used to create SQL statements. So in this case, there should not be specific issues due to "ignoring them in migrations". But what if I, hypothetically, want a customizable Field.default, Field.db_column or CharField.max_length for instance? That's probably a very bad idea that have no practical interest, but that's the only hypothetical situation I can find. :P I guess in this case it would be better to provide an abstract base model, intended to be extended by the host project.

回答1:

During the design of django.db.migration it was decided that all field attributes were to be tracked even if they didn't affect their schema representation.

For the upload_to case I suggest you simply pass a function defined at a module level as documented.

import os

def upload_to(instance, filename):
    return os.path.join([conf.TEMPLATES_UPLOAD_DIR, filename])

For the path and help_text I suggest you use an approach similar to what I did with django-sundial to provide configurable defaults. You'll just have to make sure you pass an instance of a class with a deconstruct() method returning the appropriate parameters.

from django.utils.six import python_2_unicode_compatible


@python_2_unicode_compatible
class StringConfReference(object):
    def __init__(self, name):
        self.name = name

    def __str__(self):
        return getattr(conf, self.name)

    def deconstruct(self):
        return "%s.%s" % (__name__, self.__class__.__name__), (self.name,), {}


attachment = models.FilePathField(
    path=StringConfReference('ATTACHMENT_DIR'), recursive=True
)