This week I had some fun with with forms in Django in my application frisor. Here is a post about my progress and issues I’ve encountered.

Quick reminder what is frisor

Frisor is a web application in Python/Django which is just a box for interesting urls. When using untrusted network I don’t want to login anywhere to save interesting content I found. More about this is here: Introduction to frisor.

This week I started to introduce tags for my urls. As I didn’t want to reinvent a wheel again, I decided to use some Django library for tagging, but it’s a topic for a different story. Also I did small fixes:

  • after url submit form is cleared
  • refactored saving data sent by url form
  • changed in my template: url list is now below url form
  • introduced tests

My project on github: firsor repo.

Currently my application looks like this:

screenshot

Form for model in Django

I wanted to have an auto-generated form for my model Url. The previous week my UrlFrom was derived from Form and I had to create and save Url object manually. My form was really simple:

    # forms.py - old approach
    from django import forms


    class UrlForm(forms.Form):
        url = forms.URLField(label='URL')
        title = forms.CharField(label='Title')
        nick = forms.CharField(label='Nick')

After POST request I had to extract all request data, create Url object instance with this data, and finally save Url object in database like this in my view:

    # views.py - old approach
    from django.views.generic import FormView

    from .models import Url

    class UrlView(FormView):
        # ...

        def form_valid(self, form):
            # ...
            title = self.request.POST.get('title')
            nick = self.request.POST.get('nick')
            url = self.request.POST.get('url')

            db_url = Url.create(url=url, title=title, nick=nick) # manually created
            db_url.save()
            # ...

But it happens that my form is based on a single model (Url) and there is a mechanism to tell Django to save model object automatically. Actually it’s possible to have a form which is able to handle more complicated models with foreign keys - in documentation there are described mappings between model and form fields.

Here is my form which derives from django.forms.ModelForm instead of django.forms.Form:

    # forms.py
    from django import forms

    from .models import Url


    class UrlForm(forms.ModelForm):
        url = forms.URLField(label='URL')
        title = forms.CharField(label='Title')
        nick = forms.CharField(label='Nick')

        class Meta:
            model = Url
            fields = ['url', 'title', 'nick']

I added a Meta class in which I defined that my UrlForm is based on model Url and I listed fields which are used by this model. After that it’s enough to use ModelForm saving mechanism in view like this:

    # views.py
    from django.contrib import messages
    from django.shortcuts import render
    from django.views.generic import FormView


    class UrlView(FormView):
        # ..

        def form_valid(self, form):
            # ...
            form.save()
            messages.success(self.request,
                             'Your new url: %s' % form.data.get('url'),
                             extra_tags='safe')
            # ...

Usage of form.save() does all the saving logic automatically. Data in form is stored as dictionary in form.data, so now it’s easy to access and I don’t have to extract it from self.request anymore to show message about successfully added url.

So adding url logic is much simplier, but what actually happen here?

More details about ModelForm

ModelForm is a Form which has a mechanism for automatic generation of certain fields in model. It generates template data (so HTML code for inputs for example) based on information from its Meta class. For minimal configuration you need to add to Meta class:

  • model - it declares for which model you want to have fields generated
  • fields list - if you want to have all fields simply: fields = '__all__', but it’s recommended to write them explicitly.

Minimal ModelForm can look like this:

    # forms.py
    from django import forms

    from .models import MyModel


    class MyModelForm(forms.ModelForm):
        class Meta:
            model = MyModel
            fields = '__all__'

Main difference between ModelForm and Form is that it performs two validations:

  • form validation
  • model instance validation

instead of only form validation.

Another difference is that in ModelForm there is save method introduced for saving an instance of an object in database. It performs validations mentioned above if there were not performed earlier.

Customized Meta class in ModelForm

Meta class of ModelForm can contain:

  • custom error messages: error_messages which is dictionary where keys are names of fields
  • excluded fields of model: excluded which should contain list of names of excluded fields
  • custom widgets defined by widgets which is dictionary where keys are names of fields and values are widgets which should be used for those fields
  • custom labels for fields in dictionary labels where keys are names of fields and values are labels
  • custom help texts for fields in dictionary help_texts where keys are names of fields and values are help texts
  • custom data types for fields in dictionary field_classes where keys are names of fields and values are types (class names)

Clearing form after submit

Last week I had an issue that my form wasn’t cleared after submission. My mistake was that in my view in method form_valid which is run after POST request I used a method django.shortcuts.render for rendering view after form submit.

    # views.py - old approach
    from django.shortcuts import render
    from django.views.generic import FormView


    class UrlView(FormView):
        # ...
        def form_valid(self, form):
            context = self.get_context_data()
            context['form'] = form
            # ... saving logic
            return render(self.request, 'index.html', context)

Better approach here is to use django.shortcuts.redirect, because after saving a data from form, my view should be refreshed to load urls data using self.get_context_data. It resolved an issue with not cleared form after submit:

    # views.py
    from django.shortcuts import redirect
    from django.views import View
    from django.views.generic import FormView


    class UrlView(FormView):
            # ...
        def form_valid(self, form):
            context = self.get_context_data()
            context['form'] = form
            form.save()
            return redirect('/', context)  # redirect instead of render

Leave a Comment