R – How to use django forms/models to represent choices between fields

djangodjango-formsdjango-models

How can I use boolean choices in a model field to enable/disable other fields. If a boolean value is true/false I want it to enable/disable other model fields. Is there a way to natively express these relationships using django models/forms/widgets? I keep writing custom templates to model these relationships, but can't figure out a good way to represent them in django without a special template.

For example:

class PointInTime(models.Model):
    is_absolute_time = models.BooleanField()
    absolute_time = models.DateTimeField()
    is_relative_time = models.BooleanField()
    days_before = models.IntegerField()

So if the is_absolute_time is True, I want the absolute_time entry to be editable in the GUI and the days_before entry to be grayed out and not-editable. If the 'is_relative_time' flag is True, I want the absolute_time entry to be grayed out, and the days_before value to be editable. So is_absolute_time and is_relative_time would be radio buttons in the same Group in the GUI and their two corresponding fields would only be editable when their radio button is selected. This is easy to do in a customized template, but is there a way to use a model/form in django to natively show this relationship?

Best Answer

It would be helpful to clarify what you mean by "natively show this relationship," and think clearly about separation of concerns.

If all you want is to "gray out" or disable a certain field based on the value of another field, this is purely a presentation/UI issue, so the template (and/or Javascript) is the appropriate place to handle it.

If you want to validate that the submitted data is internally consistent (i.e. absolute_time is filled in if is_absolute_time is True, etc), that's a form-validation issue. The place for that logic is in the clean() method of your Form or ModelForm object.

If you want to ensure that no PointInTime model can ever be saved to the database without being internally consistent, that's a data-layer concern. The place for that is in a custom save() method on your model object (Django 1.2 will include a more extensive model validation system).

All of those options involve writing imperative code to do what you need with these specific fields. It may be that you're looking for a way to represent the situation declaratively in your model so that the code in all three of the above cases can be written generically instead of specifically. There's no built-in Django way to do this, but you could certainly do something like:

class PointInTime(models.Model):
    field_dependencies = {'is_absolute_time': 'absolute_time',
                          'is_relative_time': 'days_before'}
    ... fields here ...

Then your model save() code (or your Form clean() code, or your template), could use this dictionary to determine which fields should be enabled/disabled depending on the value of which other one. This generalization is hardly worth the effort, though, unless you anticipate needing to do this same thing in a number of different models.

Lastly, a few schema design alternatives you may want to consider to get your data layer better normalized:

  • If there are only two valid states (absolute and relative), use a single boolean field instead of two. Then you avoid possible inconsistencies (what does it mean if both booleans are False? Or True?)

  • Or simplify further by eliminating the booleans entirely and just using Null values in one or the other of absolute_time/days_before.

  • If there might be more than two valid states, use a single IntegerField or CharField with choices instead of using two boolean fields. Same reason as above, but can accomodate more than two options.

  • Since a RelativeTime and an AbsoluteTime don't appear to share any data fields with each other, consider splitting them out into separate models entirely. If you have other models that need a ForeignKey to either one or the other, you could model that with inheritance (both RelativeTime and AbsoluteTime inherit from PointInTime, other models have ForeignKeys to PointInTime).