9. Creating Forms from Models

Just as in Django, forms can be created from models and rendered by django-formset.

Say, we use the same model as described in the Django documentation, ie. myapp.models.Article, and then we use that model to create a form class:

class ArticleForm(ModelForm):
    class Meta:
        model = Article
        fields = ['pub_date', 'headline', 'content', 'reporter']

There however is a caveat here: django-formset offers some widgets, which greatly enhance the functionality of some input elements, compared to their pure HTML counterpart.

Replacing Widgets for Choice Fields

These widgets are the formset.widget.Selectize, formset.widget.SelectizeMultiple, and formset.widget.DualSelector. They shall be used as a replacement to default widgets offered by Django. This can be done by mapping the named fields to alternative widgets inside the form’s Meta class:

from formset.widgets import DualSelector, Selectize, SelectizeMultiple

class ArticleForm(ModelForm):
    class Meta:
        ...
        widgets = {
            'single_choice': Selectize,
            'multiple_choice': SelectizeMultiple,  # or DualSelector
            ...
        }

Please read the sections Selectize Widget and Dual Selector Widget for details about enhancing the <select> and <select multiple="multiple"> widgets.

Replacing Widgets for File- and Image Fields

In case we want to map a model field of type django.db.models.FileField or django.db.models.ImageField, we must replace the default input widget by formset.widgets.UploadedFileInput. This is required because in django-formset files are uploaded before form submission. Please read the section Uploading Files and Images for details about file uploading.

from formset.widgets import UploadFileInput

class ArticleForm(ModelForm):
    class Meta:
        ...
        widgets = {
            'image': UploadFileInput(),
            ...
        }

Replacing Widget for TextField

In case we want to offer a widget to Edit Rich Text but prefer to use the model field django.db.models.TextField, we have to map this widget in the Meta class of the form class instantiating the model.

from formset.richtext.widgets import RichTextarea

class ArticleForm(ModelForm):
    class Meta:
        ...
        widgets = {
            'text': RichTextarea(),
            ...
        }

Usually you don’t want to use the default control elements for that rich text editor, but instead configure your own preferences.

The model field formset.richtext.fields.RichTextField maps to widget RichTextarea by default, but again you may prefer to use your own configuration of control elements and hence you have to map the widget in the Meta class of the form class instantiating the model.

9.1. Detail View for ModelForm

In a CRUD application, we usually add a Django View to add, update and delete an instance of our model. The Django documentation proposes to create one view for each of these tasks, a CreateView, an UpdateView and a DeleteView.

With django-formset we instead can combine them into one view class. This is because we can add extra context data to the form control buttons, which then is submitted together with the form data. An example:

from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import UpdateView
from formset.views import FileUploadMixin, FormViewMixin

class ArticleEditView(FileUploadMixin, FormViewMixin, LoginRequiredMixin, UpdateView):
    model = Article
    template_name = 'myapp/edit-form.html'
    form_class = ArticleForm
    success_url = reverse_lazy('address-list')  # or whatever makes sense
    extra_context = None

    def get_object(self, queryset=None):
        if self.extra_context['add'] is False:
            return super().get_object(queryset)

    def form_valid(self, form):
        if extra_data := self.get_extra_data():
            if extra_data.get('delete') is True:
                self.object.delete()
                success_url = self.get_success_url()
                response_data = {'success_url': force_str(success_url)} if success_url else {}
                return JsonResponse(response_data)
        return super().form_valid(form)

We now must adopt the template used to render the edit form

<django-formset endpoint="{{ request.path }}" csrf-token="{{ csrf_token }}">
  {% render_form form %}
  {% if add %}
    <button type="button" click="submit({add: true}) -> proceed">{% trans "Add" %}</button>
  {% else %}
    <button type="button" click="submit({update: true}) -> proceed">{% trans "Update" %}</button>
    <button type="button" click="submit({delete: true}) -> proceed">{% trans "Delete" %}</button>
  {% endif %}
</django-formset>

The interesting part here is that we use the context variable add to distinguish between the Add- and the Update/Delete-Views. This context variable is added using the extra_context parameter, see below.

Additionally the submit buttons “Add”, “Update” and “Delete” have the ability to pass some extra data together with the submitted form data. We use that information in the form_valid-method in our view to distinguish between the creation, the update or the deletion of an instance, see above.

Finally we must attach that view class to our URL routing. Here we reuse our form view class ArticleEditView and use the parameter extra_context to modify the behavior of that view.

urlpatterns = [
    ...
    urlpatterns = [
    path('', AddressListView.as_view(), name='address-list'),  # list view not handled here
    path('add/', ArticleEditView.as_view(extra_context={'add': True}),
        name='address-add',
    ),
    path('<int:pk>/', ArticleEditView.as_view(extra_context={'add': False}),
        name='address-edit',
    ),
    ...
]

Note

The list view is not handled explicitly here, because it doesn’t differ compared to a classic Django view.