Contents Menu Expand Light mode Dark mode Auto light/dark, in light mode Auto light/dark, in dark mode
django-formset 2.2.3
django-formset 2.2.3
Logo

Contents:

  • 1. Introduction
  • 2. Installation
  • 3. Web Component <django-formset>
  • 4. Working with a single Form
  • 5. Styling Forms with django-formset
  • 6. Dark Mode User Interface
  • 7. Default Widgets
  • 8. Alternative Widgets
  • 9. Submit Button Controls
  • 10. Activators and Button Widgets
  • 11. Withholding Feedback
  • 12. Form Collections
  • 13. Fields Mapping
  • 14. Creating Forms from Models
  • 15. Creating Collections from related Models
    • 15.1. One-to-One Relations
    • 15.2. One-to-Many Relations
  • 16. Fieldsets
  • 17. Collection Fields
  • 18. Form Renderer
  • 19. Conditional Expressions
  • 20. Uploading Files and Images
  • 21. Selectize Widget
  • 22. Dual Selector Widget
  • 23. Dialog Forms
  • 24. Dialog Model Forms
  • 25. Form Stepper
  • 26. Edit Richtext
  • 27. Richtext Extensions
  • 28. Slug Input Field
  • 29. Date- and DateTime Input
  • 30. Date- and Date-Time Range
  • 31. Decimal Unit Field
  • 32. Country Select Field with Flag Symbol
  • 33. Phone Number Field
  • 34. Django-Admin Integration
  • 35. Developing in django-formset
  • 36. Contributing to the Project
  • 37. Frequently Asked Questions
  • 38. Changes
  • 39. History of django-formset
  • 40. A conversation with Chat-GPT
Back to top

15. Creating Collections from related Models¶

In more complex setups, we might want to change the contents of related models altogether. This is when we start to use Form Collections to edit more than one ModelForm. This is similar to what Django’s Model formsets functionality is intended for, but implemented in a more flexible way.

15.1. One-to-One Relations¶

Let’s start with a simple example. Say that we want to extend the Django User model with an extra field, for instance a phone number field. Since we don’t want to substitute the User model against a customized implementation, instead we must extend it using a one-to-one relation.

models.py¶
from django.conf import settings
from django.db import models

class ExtendUser(models.Model):
    user = models.OneToOneField(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name='extend_user',
    )
    phone_number = models.CharField(
        verbose_name="Phone Number",
        max_length=25,
        blank=True,
        null=True,
    )

In a typical application we might want to edit this model together with the default User model. If we do this in the Django admin, we have to create an InlineModelAdmin with exactly one extra form in the formset. This however implies that our model ExtendUser has a foreign relation with the User model rather than a one-to-one relation [1] . In django-formset we can handle this by declaring one ModelForm for User and ExtendUser each, and then group those two forms into one FormCollection.

collections.py¶
from django.forms.models import ModelForm, construct_instance, model_to_dict
from formset.collection import FormCollection
from testapp.models import ExtendUser, User

class UserForm(ModelForm):
    class Meta:
        model = User
        fields = '__all__'

class ExtendUserForm(ModelForm):
    class Meta:
        model = ExtendUser
        fields = ['phone_number']

    def model_to_dict(self, user):
        try:
            return model_to_dict(user.extend_user, fields=self._meta.fields, exclude=self._meta.exclude)
        except ExtendUser.DoesNotExist:
            return {}

    def construct_instance(self, user):
        try:
            extend_user = user.extend_user
        except ExtendUser.DoesNotExist:
            extend_user = ExtendUser(user=user)
        form = ExtendUserForm(data=self.cleaned_data, instance=extend_user)
        if form.is_valid():
            construct_instance(form, extend_user)
            form.save()

class UserCollection(FormCollection):
    user = UserForm()
    extend_user = ExtendUserForm()

When this form collection is rendered and completed by the user, the submitted data from both forms in this collection is, as expected, unrelated. We therefore have to tell one of the two forms, how their generating models relate to each other. For this to work, each FormCollection and each Django Form can implement two methods, model_to_dict(…) and construct_instance(…).

15.1.1. Collection Attributes Usage¶

model_to_dict(main_object)

This method creates the initial data for a form starting from main_object as reference. It is inspired by the Django global function model_to_dict(instance, fields=None, exclude=None) which returns a Python dict containing the data in argument instance suitable for passing as a form’s initial keyword argument.

The main_object is determined by the view (inheriting from formset.views.EditCollectionView) which handles our collection named UserCollection, using the get_object()-method (usually by resolving a primary key or slug).

construct_instance(main_object)

This method takes the cleaned_data from the validated form and applies it to one of the model objects which are related with the main_object. It is inspired by the Django global function construct_instance(form, instance, fields=None, exclude=None) which constructs and returns a model instance from the bound form’s cleaned_data, but does not save the returned instance to the database.

Since form collections can be nested, method model_to_dict(…) can be used to recursively create a dictionary to initialize the forms, starting from a main model object. After receiving the submitted form data by the client, method construct_instance can be used to recursively traverse the cleaned_data dictionary returned by the rendered form collection, in order to construct the model objects somehow related to the main_object.

To get this example to work, we therefore have to implement those two methods in our ExtendUserForm. They both resolve the relation starting from the main object, in this case the User object. Since we have a one-to-one relation, there can only be no or one related ExtendUser object. If there is none, create it.

Finally, our UserCollection must be made editable and served by a Django view class. Since this is a common use case, django-formset offers the class formset.views.EditCollectionView which is specialized in editing related models starting from a dedicated object. The latter usually is determined by using a unique identifier, for instance its primary key or a slug.

views.py¶
from formset.views import EditCollectionView
from testapp.models.user import User

class UserCollectionView(EditCollectionView):
    model = User
    collection_class = UserCollection
    template_name = 'form-collection.html'
Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.
Designates whether the user can log into this admin site.
Designates whether this user should be treated as active.
Designates that this user has all permissions without explicitly assigning them.

This view then must be connected to the urlpatterns in the usual way. The template referenced by this view shall contain HTML with a structure similar to this:

<django-formset endpoint="{{ request.path }}" csrf-token="{{ csrf_token }}">
  {{ form_collection }}
  <button type="button" df-click="submit -> proceed !~ scrollToError">Submit</button>
</django-formset>

15.2. One-to-Many Relations¶

The most prominent use-case for form collections is to edit a model object together with child objects referring to itself. By children we mean objects which point onto the main object using a Django ForeignKey. Let’s again explain this using an example. Say, we want to create models for the organization chart of a company. There is a model for a company, which may consist of different departments, which themselves can have different teams. In relational models this usually is done using a foreign key. For demonstration purposes the remaining part of the models is very lean and only stores their names.

models.py¶
from django.db import models

class Company(models.Model):
    name = models.CharField(
        verbose_name="Company name",
        max_length=50,
        help_text="The name of the company",
    )

class Department(models.Model):
    name = models.CharField(
        verbose_name="Department name",
        max_length=50,
    )
    company = models.ForeignKey(
        Company,
        on_delete=models.CASCADE,
        related_name='departments',
    )

    class Meta:
        unique_together = ['name', 'company']

class Team(models.Model):
    name = models.CharField(
        verbose_name="Team name",
        max_length=50,
    )
    department = models.ForeignKey(
        Department,
        on_delete=models.CASCADE,
        related_name='teams',
    )

    class Meta:
        unique_together = ['name', 'department']

We immediately see that these models assemble a hierarchy of three levels. With the builtin Django remedies, creating a form to edit them altogether is not an easy task. To solve this problem, django-formset offers the possibility to let form collections have siblings. We then can create forms and collection to edit the company, its departments and their teams such as:

collections.py¶
from django.forms.fields import IntegerField
from django.forms.widgets import HiddenInput
from django.forms.models import ModelForm
from formset.collection import AddSiblingActivator, FormCollection
from testapp.models import Company, Department, Team

class TeamForm(ModelForm):
    id = IntegerField(
        required=False,
        widget=HiddenInput,
    )

    class Meta:
        model = Team
        fields = ['id', 'name']

class TeamCollection(FormCollection):
    legend = "Teams"
    induce_add_sibling = '.add_team:active'
    related_field = 'department'
    team = TeamForm()
    min_siblings = 0

    add_team = AddSiblingActivator("Add Team")

    def get_or_create_instance(self, data):
        if data := data.get('team'):
            try:
                return self.instance.teams.get(id=data.get('id') or 0), False
            except (AttributeError, Team.DoesNotExist, ValueError):
                form = TeamForm(data=data)
                if form.is_valid():
                    return Team(name=form.cleaned_data['name'], department=self.instance), False
        return None, False

class DepartmentForm(ModelForm):
    id = IntegerField(
        required=False,
        widget=HiddenInput,
    )

    class Meta:
        model = Department
        fields = ['id', 'name']

class DepartmentCollection(FormCollection):
    legend = "Departments"
    induce_add_sibling = '.add_department:active'
    related_field = 'company'
    department = DepartmentForm()
    teams = TeamCollection()  # attribute name MUST match related_name (see note below)
    min_siblings = 0

    add_department = AddSiblingActivator("Add Department")

    def get_or_create_instance(self, data):
        if data := data.get('department'):
            try:
                return self.instance.departments.get(id=data.get('id') or 0), False
            except (AttributeError, Department.DoesNotExist, ValueError):
                form = DepartmentForm(data=data)
                if form.is_valid():
                    return Department(name=form.cleaned_data['name'], company=self.instance), False
        return None, False


class CompanyForm(ModelForm):
    class Meta:
        model = Company
        fields = '__all__'

class CompanyCollection(FormCollection):
    company = CompanyForm()
    departments = DepartmentCollection()  # attribute name MUST match related_name (see note below)

As we expect, we see that every Django model is represented by its form. Since we want to edit more instances of the same model type, we somehow need a way to distinguish them. This is where the form field named id comes into play. It is a hidden IntegerField and represents the primary key of the model instances for the Department or Team. Since newly created instances haven’t any primary key yet, they are marked with required=False to make them optional.

Note

Take care when naming related collections on a parent FormCollection. The name must either match the reverse accessor of the related field or must be explicitly set in the FormCollection using the attribute reverse_accessor – read section below for details.

Finally, our CompanyCollection must be made editable and served by a Django view class. Here we can use the the view class formset.views.EditCollectionView as in the previous example.

The name of the company
Departments

    views.py¶
    class CompanyCollectionView(EditCollectionView):
        model = Company
        collection_class = CompanyCollection
        template_name = 'form-collection.html'
    

    Note

    After submission, the content of these form collections is stored in the database. Therefore after reloading this page, the same content will reappear in the form.

    The view class CompanyCollectionView is specialized in editing related models starting from a dedicated object. The latter usually is determined by using a unique identifier, for instance its primary key or a slug.

    15.2.1. Collection Attributes Usage¶

    related_field

    In this example we have to implement the attribute related_field in our main collection class CompanyCollection. This is because django-formset otherwise does not know how the DepartmentCollection is related to model Company, and how the TeamCollection is related to model Department. Here, related_field refers to the name of the foreign key attribute in model Department pointing to model Company, and to the foreign key attribute in model Team pointing to model Department respectively.

    reverse_accessor

    This is the reverse relation from the main instance back to the child objects and required during the construction of the collection. It usually is the string used in related_name when declaring a foreign key for any given relation. In this example, these values are departments for the relation from Company to Department, and teams for the relation from Department to Team. However, since we already used these names in the declaration of our collections, we ommitted them here. They are only required, if the attribute name of the collection differs from the related_name of the foreign key.

    get_or_create_instance(data)

    We recall that in the form declaration, we added a hidden field named id to keep track of the primary key. During submission, we therefore must find the link between instances of type Department to their Company, or between instances of type Team to their Department. Forms which have been added using the buttons “Add Team” or “Add Department” have an empty id field, because for obvious reasons, no primary key yet exists. For this to work we therefore have to implement a custom method get_or_create_instance(data). This method is responsible to retrieve the wanted instance from the database, or if that hidden field is empty, must create an empty model instance. In some configurations, we might want to create such an instance and save it immediately. We then return the created object followed by True. Usually however, we just create an unsaved model instance which will be added to the database in a later step. Forms which have been deleted using the trash symbol on the upper right corner of each form, are marked for removal and will be removed from the associated object.

    Until version 2.1, this method was named retrieve_instance and did not distinguish between saved and unsafed instances. It therefore only returned that instance.

    form_collection_valid(form_collection)

    After all submitted forms have been successfully validated, the EditCollectionView calls the method form_collection_valid(form_collection) passing a nested structure of collections and their associated forms. If the default implementation, doesn’t match your needs, this method can be overwritten by a customized implementation. If, as in this example, models are interconnected by a straight relationship, the default implementation will probably suffice. Remember, that for more complicated relationships, you can always overwrite methods construct_instance(…) and model_to_dict(…) to customize the conversion from the model instances to their forms and vice versa.

    [1]

    In technical terms, a one-to-one relation is a foreign key with an additional unique constraint.

    Next
    16. Fieldsets
    Previous
    14. Creating Forms from Models
    Copyright 2025, Jacob Rief
    Made with django-sphinx-view and Furo Theme.
    On this page
    • 15. Creating Collections from related Models
      • 15.1. One-to-One Relations
        • 15.1.1. Collection Attributes Usage
      • 15.2. One-to-Many Relations
        • 15.2.1. Collection Attributes Usage