Django model forms with relationships

As you learned in the previous two chapters, Django models can have relationships between one another, which in turn makes models have a data type (e.g. ForeignKey, ManyToManyField) that references records in another model.

When models containing such data types are used in the context of model forms, Django relies on two special form fields. By default, ForeignKey model fields are converted to ModelChoiceField form fields and ManyToManyField model fields are converted to ModelMultipleChoiceField form fields.

The benefit of the ModelChoiceField and ModelMultipleChoiceField form fields is they generate a form field based on a Django model query. So instead of manually populating a form field with model data, the ModelChoiceField and ModelMultipleChoiceField form fields generate a friendly HTML <select>/<option> input field with model records.

ModelChoiceField and ModelMultipleChoiceField form field options: queryset, empty_label, to_field_name and label_from_instance

Tip ModelChoiceField and ModelMultipleChoiceField are standard form fields usable on any Django form that requires model data. They are used by default on model forms with relationships, but they're not restricted to model forms (i.e. forms inherited from forms.ModelForm).
Note ModelChoiceField and ModelMultipleChoiceField being standard form fields (i.e. part of the Django forms package), also accept the standard form options: required, widget, label, initial, help_text and limit_choices_to -- described in chapter 6.

Since ModelChoiceField and ModelMultipleChoiceField form fields use model records to source their data, they unequivocally require a model query. For model forms that inherit their behavior from forms.ModelForm and their underlying models contain a ForeignKey or ManyToManyField model field, this model query is set automatically. For example, if an Item model contains a ForeignKey to a Menu model, an Item model form presents all Menu records in a form to allow users to select a single Menu record. Similarly, if a Store model contains a ManyToManyField to an Amenity model, a Store model form presents all Amenity records in a form to allow users to select multiple Amenity records.

While this behavior is acceptable in most circumstances, it can be necessary to provide an explicit query to ModelChoiceField or ModelMultipleChoiceField form fields, either when you need to filter the default behavior to use all model records on a model form field or when these form fields are used in a regular form (i.e. that inherits forms.Form).

Listing 9-5 illustrates the two techniques to set a model query on either the ModelChoiceField and ModelMultipleChoiceField form fields using the queryset option.

Listing 9-5 Django model form and standard form with custom query for ModelChoiceField and ModelMultipleChoiceField form fields

from django import forms
from coffeehouse.stores.models import Amenity

class Menu(models.Model):
    name = models.CharField(max_length=30)
    def __str__(self):
        return "%s" % (self.name)

class Item(models.Model):
    menu = models.ForeignKey(Menu, on_delete=models.CASCADE)
    name = models.CharField(max_length=30)
    description = models.CharField(max_length=100)

class ItemForm(forms.ModelForm):
    menu = forms.ModelChoiceField(queryset=Menu.objects.filter(id=1))
    class Meta:      
        model = Item
        fields = '__all__'

class StoreForm(forms.Form):
    name = forms.CharField()    
    address = forms.CharField()
    amenities = forms.ModelMultipleChoiceField(queryset=None)            
    def __init__(self, *args, **kwargs):
        super(StoreForm, self).__init__(*args, **kwargs)
        self.fields['amenities'].queryset = Amenity.objects.filter(name__contains='W')

The first technique in listing 9-5 defines an in-line queryset value by overriding the menu field with a custom ModelChoiceField() on the ItemForm model form. In this case, instead of the ItemForm model form having a menu field with all Item records, the menu field is restricted to only the Item record with id=1.

The second technique in listing 9-5 defines an empty queryset value on a standard form that uses a forms.ModelMultipleChoiceField() form field on the amenities field. But inside the form's __init__ method, the amenities field is set to a query that restricts its records to Amenity records that contain the letter W.

It's worth mentioning, both queryset techniques illustrated in listing 9-5 are equally valid in both model forms and regular forms, as well as ModelChoiceField() and ModelMultipleChoiceField() form fields.

By default, ModelChoiceField() form fields that don't define an initial value are generated with the empty HTML <option>---------</option> choice as the default field value. It's possible to customize the value of this empty option with the empty_label option (e.g. empty_label='Please select a value', to output <option>Please select a value</option>). It's also possible to disable the inclusion of this empty option with empty_label=None.

By default, both ModelChoiceField() and ModelMultipleChoiceField() form fields generate their HTML <select>/<option> input field values from a model record's primary key value (i.e. id) and model __str__ method representation. For example, given the Menu model definition in listing 9-5, an HTML <select>/<option> input field for this model would look like the following snippet:

<select name="menu" required id="id_menu"> 
     <option value="" selected>---------</option>
     <option value="1">Breakfast</option>
     <option value="2">Salads</option>
     <option value="3">Sandwiches</option>
     <option value="4">Drinks</option>
</select>

Note each <option> value corresponds to a record's primary key id value and the <option> text corresponds to a record's name field returned by the model's __str__ method.

It's possible to customize the <option> value used in both ModelChoiceField() and ModelMultipleChoiceField() form fields with the to_field_name option. For example, setting to_field_name='name' in the context of this last snippet, changes the HTML <select>/<option> input field to the format <option value="Breakfast">Breakfast</option>.

Caution Using a to_field_name value breaks the underlying model form's ability to be saved to the database, since the model's relationship value is set to a different value than the primary key expected by the model relationship.

In addition to customizing the <option> value, it's also possible to customize the <option> text in form fields to something other than a model's __str__ method, by overriding a label_form_instance method in either a ModelChoiceField() and ModelMultipleChoiceField() form field. Listing 9-6 illustrate a custom form field designed for this purpose.

Listing 9-6 Django custom form field to customize <option> text for ModelChoiceField and ModelMultipleChoiceField form fields

from django import forms
from django.forms import ModelChoiceField

class MenuModelChoiceField(ModelChoiceField):
    def label_from_instance(self, obj):
        return "Menu #%s) %s" % (obj.id,obj.name)
    
class ItemForm(forms.ModelForm):
    menu = MenuModelChoiceField(queryset=Menu.objects.all())
    class Meta:      
        model = Item
        fields = '__all__'

# HTML menu form field output
<select name="menu" id="id_menu" required>
  <option value="" selected>---------</option>
  <option value="1">Menu #1) Breakfast</option>
  <option value="2">Menu #2) Salads</option>
  <option value="3">Menu #3) Sandwiches</option>
  <option value="4">Menu #4) Drinks</option>
</select>

The first step in listing 9-6 creates the MenuModelChoiceField custom form field that inherits its behavior from the ModelChoiceField form field and defines an implementation for the label_from_instance method. In this case, the label_from_instance method tells Django to generate <option> text values prefixed with the Menu # static string, followed by a model's id and name. Note this same technique can be used to customize a ModelMultipleChoiceField, just make sure to change the custom form field's inheriting class.

Next, the MenuModelChoiceField custom form field in listing 9-6 is added to the ItemForm model form in the same listing through the menu field. Because MenuModelChoiceField is a custom ModelChoiceField form field, it's necessary to specify an explicit queryset value to populate the form field, which in this case corresponds to all Menu model records.

Finally, listing 9-6 illustrates the HTML <option> text output for the menu field follows the pattern defined in the custom MenuModelChoiceField custom form field.