4

Summary

We would like to set the initial selection of the select-boxes representing different kinds of model relationships on a basic "add" or "change" page in the Django admin. The initial selection should be saved to the database, if the user clicks one of the "Save" buttons.

We specifically want to do this by setting the initial value on the form fields (for ModelAdmin or TabularInline).

The question is:

What is the proper "value" to assign to Field.initial, for different kinds of relationship form-fields?

Background

Suppose we have a simple Model with one or more relationship fields, i.e. OneToOneField, ForeignKey, or ManyToManyField. By default, the latter has an implicit through model, but it may also use an explicit through model, as explained in the docs. An explicit through model is a model with at least two ForeignKey fields.

The relationship fields, represented by select-boxes, appear automatically on the standard ModelAdmin "add" or "change" forms, except for the ManyToManyField with explicit through model. That one requires an inline, such as the TabularInline, as explained here in the docs.

Note that the implicit ManyToManyField is associated with a ModelMultipleChoiceField on the (admin) form, and the other relationship fields are associated with a ModelChoiceField. The latter includes the ManyToManyField with explicit through model, because that is actually represented by a ForeignKey field in the TabularInline.

Goal

Now consider a basic Django ModelAdmin "add" (or "change") page for this model, including inlines for any explicit through models. An example based on Django's Pizza-Topping is depicted below.

We want to achieve two goals:

  1. set the initial selection for one or more of the relationship fields
  2. the initial selection must be saved to the database if we click one of the "Save" buttons (assuming we did not change the selection manually)

Note that the second one seems trivial, but apparently isn't (see below).

django pizza admin example

Approach

As far as I know, there are several ways to achieve this:

  • set the model field default value, e.g. ForeignKey.default, optionally passing a callable (docs)
  • set the Form.initial value for the admin form (docs)
  • set the Field.initial value for the admin form-field (docs)

Regardless of which approach is "best" for a certain use case, this question is about the last approach: setting the value of Field.initial.

We do this by extending ModelAdmin.formfield_for_dbfield (or TabularInline.formfield_for_dbfield), as follows:

def formfield_for_dbfield(self, db_field, request, **kwargs):
    if db_field.name == 'some_relationship_field_name':
        kwargs['initial'] = value
    return super().formfield_for_dbfield(db_field, request, **kwargs)

The value in this example could be an obj.id, obj, [obj.id, ...], or [obj, ...], where obj is a Model instance.

Problem

Depending on the type of relationship field, different types of value may or may not work.

For the ManyToManyField with implicit through model, we can only assign a list of objects, so that one is easy.

For the other relationships, in some cases the initial select-box selection matches the assigned value, but is not saved (e.g. because Field.has_changed returns False). In other cases the initial select-box value is saved, but does not match the assigned value.

Question

So, the obvious question is:

What is the proper value to assign to Field.initial, for different kinds of relationship fields on an admin form?

Related

Despite having spent quite a lot of time searching, I could not find a clear answer in the documentation, nor on SO, nor anywhere else. Also tried to figure this out through the source code, but that turns out to be quite tricky.

Some similar questions:

djvg
  • 11,722
  • 5
  • 72
  • 103

1 Answers1

1

The table below shows the results of some experimentation with a dedicated minimal example, using Django 2.1. Based on the table, it appears that:

  • obj or obj.id only work for OneToOneField and ForeignKey
  • [obj.id] works for all relationships represented by a ModelChoiceField
  • [obj] only works for the implicit ManyToManyField, which is represented by a ModelMultipleChoiceField

However, I cannot say what is the intended way to do it for each case. For example, it does not make much sense to assign a list for a ForeignKey field. However, the funny thing is, that, even though the ManyToManyField with explicit through model has an inline with a ModelChoiceField for a ForeignKey, we cannot just assign obj or obj.id: it must be in a list.

Any comments are much appreciated here.

Key for table 1:

  • 1: select-box initial selection corresponds with value of Field.initial
  • 2: select-box initial selection is saved, after clicking "Save" (NOTE: if 1 fails, then the select-box initial selection is the first available option)
  • T: TypeError (object is not iterable)
  • A: AttributeError ('int' object has no attribute 'pk')

Note that our desired behavior is 1 & 2.

Table 1: result of assigning different values to Field.initial for different relationship fields

|                                                       |  value assigned to Field.initial  |
|-----------------|----------|--------------------------|-------|--------|-------|----------|
| model field     | through  | associated form field    | obj   | obj.id | [obj] | [obj.id] |
|-----------------|----------|--------------------------|-------|--------|-------|----------|
| OneToOneField   | n/a      | ModelChoiceField         | 1 & 2 | 1 & 2  | 2     | 1 & 2    |
| ForeignKey      | n/a      | ModelChoiceField         | 1 & 2 | 1 & 2  | 2     | 1 & 2    |
| ManyToManyField | explicit | ModelChoiceField         | 1     | 1      | 2     | 1 & 2    |
| ManyToManyField | implicit | ModelMultipleChoiceField | T     | T      | 1 & 2 | A        |
djvg
  • 11,722
  • 5
  • 72
  • 103