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.