3

I'm building an open-source reusable app that needs users of the app to be able to set their own choices on some CharField fields in settings.

Is it possible/reasonable to have Django ignore changes to the choices field option and never (should that feature ever be added) implement database-level choice selection?

This isn't the actual code, but

This would be in models.py

class Owner(models.Model):
    city = models.CharField(
        verbose_name=u'City',
        max_length=255,
        blank=True,
        choices=settings.CITIES,
        db_index=True)

And this would be in settings.py

CITIES = (
    ('chicago', 'Chicago, IL'),
    ('milwaukee', 'Milwaukee, WI')
)

This would result in this migration

class Migration(migrations.Migration):
    operations = [
        migrations.CreateModel(
            name='owner',
            fields=[
                ('city', models.CharField(blank=True, max_length=3, db_index=True, choices=[(b'chicago', b'Chicago, IL'), (b'milwaukee', b'Milwaukee, WI')])),
    ]

Now, say an end-user wants to change thier app to instead have this in their settings.py

CITIES = (
    ('los_angeles', 'Los Angeles, CA'),
    ('san_fransisco', 'San Fransisco, CA')
)

This would cause another migration to be created with python manage.py makemigrations which looks like this:

class Migration(migrations.Migration):

    dependencies = [
        ('appname', '0001_initial'),
    ]

    operations = [
        migrations.AlterField(
            model_name='user',
            name='city',
            field=models.CharField(blank=True, max_length=255, verbose_name='City', db_index=True, choices=[(b'los_angeles', b'Los Angeles, CA'), (b'san_fransisco', b'San Fransisco, CA')]),
        ),
    ]

Even though other users of the app may have entirely different lists of supported cities.

This could cause conflicts later when I release new versions of the open source app with a 0002 migration number, and if there is ever database-level enforcement of choices this could cause havoc.

Is it possible to have Django ignore changes to the choices field during migration creation? Extending CharField seems reasonable.

Or do I have to refactor this using a ForeignKey that never changes and add select_related() to the manager?

For reference, here's the Django makemigrations inspector code

NickCatal
  • 718
  • 7
  • 16
  • 1
    It's by design, that's why I still use South to do my migrations. Here's a better explanation of your question http://stackoverflow.com/questions/26152633/why-does-django-1-7-creates-migrations-for-changes-in-field-choices – Shang Wang Jul 28 '15 at 14:10
  • Damn, was afraid of that. Found a hacky way to do it mid-day today after I read through that answer/those tickets and added that answer. Thanks for the heads up! – NickCatal Jul 29 '15 at 00:39

2 Answers2

2

It turns out this is not enabled for a reason (from this answer)

This is by design. There are several reasons, not least of which for me that datamigrations at points in history need to have a full accurate representation of the models, including all their options not just those which affect the database.

However, you can go back to the initial migration and do

class Migration(migrations.Migration):
    operations = [
        migrations.CreateModel(
            name='owner',
            fields=[
                ('city', models.CharField(choices=settings.CITIES, blank=True, max_length=3, db_index=True)),
    ]

Not very pretty and it could theoretically come back to bite you down the line, so refactoring with a ForeignKey and select_related('cities') in the get_queryset() in the manager might be the safest/least-hacky way to go about this, but this should never result in a new migration happening with a settings change that results in a choices change.

Community
  • 1
  • 1
NickCatal
  • 718
  • 7
  • 16
1

I wouldn't make it that difficult on me to validate everything database/ORM wise, but do the validating in the controller/the form.

Here's what I would do building upon your sample code:

models.py:

class Owner(models.Model):
    city = models.CharField(
        verbose_name=u'City', max_length=255, blank=True, db_index=True
    )

(No choices here!)

views.py:

class CitySelectionView(FormView):
    template_name = "city_selection.html"
    form_class = forms.CitySelectionForm

     def form_valid(self, form):
         obj = models.Owner(city=form.cleaned_data['city']
         obj.save()
         return redirect('index')

forms.py:

class CitySelectionForm(forms.Form):
    city = forms.MultipleChoiceField(choices=settings.CITIES)

If cities now is

CITIES = (('dusseldorf', 'Düsseldorf, Germany'), ('berlin', 'Berlin, Germany'))

the form will show Düsseldorf and Berlin, and if it's

CITIES = (('london', 'London, UK'), ('paris', 'Paris, France'))

the form will show London and Paris.

Chris Schäpers
  • 1,238
  • 10
  • 17
  • 1
    Right, that would work for forms. But `owner.get_city_display()` (in appcode) / `{{ owner.get_city_display }}` (in templates) wouldn't work in that situation. – NickCatal Jul 29 '15 at 00:24