4

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.

Community
  • 1
  • 1
Antoine Pinsard
  • 33,148
  • 8
  • 67
  • 87

1 Answers1

2

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
)
Simon Charette
  • 5,009
  • 1
  • 25
  • 33
  • Thank you. I didn't expect a better solution. Here is the patch I applied: https://github.com/Aladom/django-mailing/commit/78ffcb81eea5e01d6e5ff112d8548fed9d1063f3 The resulting migration looks good. :) – Antoine Pinsard Feb 22 '16 at 14:37
  • Yeah using `deconstructible` is even better. The reason why I suggested using `python_2_unicode_compatible` is that if one of your referenced settings contains non-ascii characters it's going to crash on Python 2. – Simon Charette Feb 22 '16 at 17:58
  • Yep. I decided not to bother about Python 2 compatibility however. I think Python 3 is widely adopted enough. Even django will drop python 2 support in the foreseeable future. – Antoine Pinsard Feb 22 '16 at 18:05
  • Unfortunately, django logic uses `os.listdir(path)` which requires `path` to be a string, bytes or integer instance. I'm trying to fix this. – Antoine Pinsard Feb 25 '16 at 13:52
  • I opened a new question about this issue: http://stackoverflow.com/questions/35632376/django-migrations-and-deconstructible-string – Antoine Pinsard Feb 25 '16 at 16:04