Django Forms

  • Help us quickly generate html to represent a form with special widgets
  • let us validate data
  • model forms create forms based on data and create/update

Setting Up

Create a forms.py file

Forms are a class

Inherit from forms.Form

    class SuggestionForm(forms.Form):

Elements made in a similar way to models

    name = forms.CharField()
    email = forms.EmailField()
    suggestion = forms.CharField(widget=forms.Textarea)

Validating a form

  • Validate as a whole - validate two or more fields in relation to each other. Like either or both.
  • validate individual fields - custom cleaning methods for complicated things
  • validate using django’s built in validators - take a value and return a specific error if value is wrong

A honeypot field

Used to catch bots

    honeypot = forms.CharField(widget=forms.HiddenInput, required=False)

The clean method

You can define a clean_x method in your form class which is repsonsible for cleaning and validating the data. Problem is it is not abstracted, it can’t be used for many forms.

    def clean_honeypot(self):
            honeypot = self.cleaned_data['honeypot']        
            if len(honeypot):
            raise forms.ValidationError(
                    "honeypot should be left empty. Bad bot!"
            )
            return honeypot

Utilising Validators

You can set the validators field to a form field

    honeypot = forms.CharField( required=False, 
                            widget=forms.HiddenInput,
                            label="Leave empty",
                            validators=[validators.MaxLengthValidator(0)]
                           )

You can define a custom validator

Add a method to the file called must_be_empty that takes the field value as an argument

To use it just add it to the validators field

The validators must be unquoted - they are functions in the file, not strings

Multi-value validation

Sometimes you need to validate 2 or more values as a combination.

Use the clean method without a field name:

    def clean(self):
            cleaned_data = super().clean()
            email = cleaned_data['email'] # or .get('email')
            verify = cleaned_data['verify_email']

            if email != verify:
            raise forms.ValidationError(
                    "You need to enter the same email in both fields"
            )

Say you have a related field on the model:

    company = models.ForeignKey('projects.Company', on_delete=models.PROTECT)

when you fill out the ModelForm for that model the company field will have different states.

    >>>> form.data['company']
    1

Only the cleaned_data actually gives you back the related object

    >>>> form.cleaned_data['company']
    <Company: Default Company>

When assigning the foreign key with kwargs you can use the instance or the pk or primary key.

Just ensure that you use company_id=3 when using integers and company=my_company when using the object instance.

Abstract Inheritance

Model inheritance that does not create a new table We call models without a db `abstract models

Django has 2 types of inheritance: * Abstract - Won’t actually have a database and you can’t query. It is used as a starter for other models. Other models can extend from it. * Multi-table - 2 tables even for models that inherit. No need to specify a foreign key as django will know that if parent model is not abstract

Most Django develoeprs will warn against using multi-table inheritance

Abstract Models

In the models Meta subclass add:

    class Meta:
        abstract = True

Specify how a model pluralises

class Meta:
    verbose_name_plural = "Quizzes"

Get Absolute Url

Tells django how to calculate the canonical url

ModelForm

Automatically generated form based on a model

from django import forms
  • Extend from forms.ModelForm
  • Requires 2 class Meta attibutes: model and fields or exclude
  • Explicit is better than implicit so better to not use exclude

    class QuizForm(forms.ModelForm): class Meta: model = models.Quiz fields = [ ‘title’, ‘description’, ‘order’, ‘total_questions’ ]

Showing multiple forms

We utilise formsets

To use formsets with models with use model formsets

    AnswerFormSet = forms.modelformset_factory(
            models.Answer, 
            form=AnswerForm,
    )

To control the associated answers you send in a queryst

    formSet = forms.AnswerFormSet(queryset=question.answer_set.all())


    forms.AnswerFormSet(request.POST, queryset=question.answer_set.all())

    if formset.is_valid():
            answers = formset.save(commit=False)

            for answer in answers:
                    answer.question = question
                    answer.save()
            return HttpResponseRedirect(question.quiz.get_absolute_url())

Formsets

Formsets by default don’t work with models

To create model formsets

    AnswerFormSet = forms.modelformset_factory(
            models.Answer,
            form=AnswerForm
    )

Then on processing the form:

    formset = forms.AnswerFormSet(queryset=question.answer_set.all())

So sets answers associated with that specific question

    if request.method == 'POST':
        formset = forms.AnswerFormSet(request.POST, queryset=question.answer_set.all())

        if formset.is_valid():
            # Haven't set question for answers yet
            answers = formset.save(commit=False)
            for answer in answers:
                answer.question = question
                answer.save()

Printing out the formset in the view is simple:

It loops and prints through formsets for us

Can set extra forms and min_count / max_count of forms by sending in the keyword args into the factory init

Inline formsets

Inline Formsets appear in the model form for another model

An important form for checking whether a form is valid

AnswerInlineFormSet = forms.inlineformset_factory(
        models.Question,
        models.Answer,
        extra=0,
        fields=['order', 'text', 'correct'],
        formset=AnswerFormSet,
        min_num=1
)

Getting a blank queryset

answer_forms = forms.AnswerInlineFormSet(
        queryset=models.Answer.object.None()
)

Handling:

answer_forms = forms.AnswerInlineFormSet(
        request.POST,
        queryset=form.instance.answer_set.all()
)

answers = answer_forms.save(commit=False)
for answer in answers:
    answer.question = question
    answer.save()

Set assets that come with a form

Use class Media

    class productForm(forms.modelForm):
        class Media:
            # all this stuff is in static
            css = {'all': ('courses/css/order.css',)},
            js = (
                    'courses/js/vendor/jquery.fn.sortable.min.js',
                    'courses/js/order.js'
            )

Still need to add to layout css and js blocks with:

Changing the widget type for a field on a model form

Use class Metawidgets attribute

To use checkboxes instead of a MultiSelect, first import

    from django.forms.widgets import CheckboxSelectMultiple

Then set the widget for your field

    class MyForm(forms.ModelForm):
    class Meta:
            model = MyModel
            fields = ('tasks',)
            widgets = {
            'tasks': CheckboxSelectMultiple
            }

Working with a many-to-many field with an intermediary model

Working with a many-to-many field with an intermediary model in the admin for this topic.

This same method can be used for editing outside of admin, in a normal view:

The many-to-many field and models

    class Project(models.Model):
            '''Project model
            '''
            name = models.CharField(max_length=255, unique=True)
            description = models.TextField()
            tasks = models.ManyToManyField(Task)
            users = models.ManyToManyField(
                    settings.AUTH_USER_MODEL,
                    through='ProjectMembership' )


    class ProjectMembership(models.Model):
            '''Project Membership model
            '''
            user = models.ForeignKey(
                    settings.AUTH_USER_MODEL,
                    on_delete=models.PROTECT
            )
            project = models.ForeignKey(Project, on_delete=models.PROTECT)
            is_project_manager = models.BooleanField(default=False)
            full_time_equivalent = models.DecimalField(
                    max_digits=5,
                    decimal_places=2,
                    default=100,
                    validators=[
                    MinValueValidator(Decimal(0)),
                    MaxValueValidator(Decimal(100))
                    ]
            )
  1. Create a form for the intermediary table in forms.py

    class ProjectMembershipForm(forms.ModelForm): class Meta: model = ProjectMembership fields = ( ‘user’, ‘project’, ‘is_project_manager’, ‘full_time_equivalent’ )

  2. Create a formset from the created form above in forms.py

    ProjectMembershipFormSet = inlineformset_factory( Project, ProjectMembership, form=ProjectMembershipForm )

  3. Ensure that the template of the classview the formset is in the template

    Create Project


    {% csrf_token %} {% bootstrap_form form %}
    {% bootstrap_formset projectmembership_formset layout='inline' %}
  4. Change the class-based view methods so that the formset is included and validated and data saved

    class ProjectCreateView(CreateView): model = Project form_class = ProjectForm

     def get_context_data(self, **kwargs):
         context = super().get_context_data(**kwargs)
         if self.request.POST:
             context['projectmembership_formset'] = ProjectMembershipFormSet(
                 self.request.POST
             )
         else:
             context['projectmembership_formset'] = ProjectMembershipFormSet()
         return context
    
     def form_valid(self, form):
         '''Handle saving of the project membership formset
         '''
         context = self.get_context_data()
         project_memberships = context['projectmembership_formset']
         if project_memberships.is_valid():
             self.object = form.save()
             project_memberships.instance = self.object
             project_memberships.save()
    
         return super().form_valid(form)
    

Sources: * Django class based views with inline model formset * Old Gist - Do not use

More on FormSets

A formset always comes with a ManagementForm which contains metadata about the formset such as form-TOTAL_FORMS, form-INITIAL_FORMS and form-MAX_NUM_FORMS.

If you are adding new forms via JavaScript, you should increment the count fields in this form as well.

if you are using JavaScript to allow deletion of existing objects, then you need to ensure the ones being removed are properly marked for deletion by including form-#-DELETE in the POST data