Django Series

Lesson 1: Start

Lesson 2: Git

Lesson 3: MVC

Lesson 4: Forms

Lesson 5: Models

Appendix i: Setup

Appendix ii: Shell

Appendix iii: TDD

A Primer to Django Forms

 

If you want some interactivity with your users, it all starts with forms. Luckily Django provides some out of the box straightforward solution for us.

 

For this tutorial we are going to do a basic website for surveying a person's age, eye color, name and whether he wants to subscribe or not.

 

If you haven't followed along, you can initiate the tutorial repository if you download it from my Github account. Choose branch exercise3. Further instructions here.

 

So first of all, we have an idea that, we should implement a new feature. For that, we need to create a new “feature branch”. So we can freely experiment, and only merge it when the feature is properly implemented.

 

This new feature will be a form, so let's do this:

git checkout -b form

git branch

 

You can see that we have, two branches now:

* form

master

 

As good TDD development practice. Start by writing a test first. New feature deserves it's own test class. Also I know that I will need a new function from main.views. You will see that later.

***main/tests.py***

...

from main.views import home, form

class FormTest(TestCase):

 

    def test_form_renders_on_page_properly(self):

        request = HttpRequest()

        response = form(request)

        for i in ['form','input','human','color','age','name','email']:

            self.assertIn(i,response.content.decode())

 

Run the test. It should fail.

python3 manage.py test

 

Output:

Ran 5 tests in 0.321s

 

FAILED (failures=1)

Make sure it is a failure, not error (marked by E). Error means there is logic error (bug) in your test, failure means your test didn't pass.

 

So if you create a forms.py file in your application, django will automatically know it musts be a form.

touch main/forms.py

 

***main/forms.py***

from django import forms

 

class SurveyForm(forms.Form):

    human = forms.BooleanField(label="Are you human?",)

    age = forms.IntegerField(label="How old are you?")

    color = forms.ChoiceField(

        label="What color is your eye?",

        choices=(('bl','Blue'),('br','Brown'),('bl','Black'),('gr','Green')),)

    name = forms.CharField(label="What's your name?",)

    subscription = forms.BooleanField(label="Do you want to subscribe?",)

    email = forms.EmailField(label="Your email:",)

 

As you can see the forms library gives us various types of fields. These settings (“label” and occasional “choices”) are the absolute minimum to initiate a form field. If you want to see more options, check out the documentation here.

 

So we have a form.py file that describes the form, but that form also needs to be displayed somewhere. For simplicity, I suggest we make a new html template.

touch main/templates/form.html

 

You can see {{ form }} is a template variable. Don't mind the “as_p” now, it just only means that it will be rendered with a <p> tag. Above you can see {% csrf_token %}. That's Django's built in defense against Cross-Site Request Forgery. That's a malicious way to take your users sessions. Although it's optional to include, when it really comes to it, don't take risks. An input tag and all of this is wrapped between two form tags. This is the bare minimum to deploy a form on your website.

 

*** main/templates/form.html***

<!DOCTYPE HTML>

<head>

  <title>Django Forms</title>

</head>

<body>

  <h1>Survey</h1>

  <form method="POST">

    {% csrf_token %}

    {{ form.as_p }}

    <input type="submit" value="Submit" />

  </form>

</body>

</html>

 

As usual views.py will knit together the backend logic with the templates. We will deplare our form here and include it in our render function.

 

*** main/views.py ***

from django.shortcuts import render

from main.models import Article

from main.forms import SurveyForm

 

# Create your views here.

def home(request):

    article = Article.objects.last()

    return render(request,'index.html',{'article':article},)

 

def form(request):

    form = SurveyForm()

    return render(request,'form.html',{'form': form},)

 

To make things easy we will create a new URL address for the form.

*** MyTutorial/urls.py ***

 

from django.conf.urls import url

from django.contrib import admin

from main.views import home, form

 

urlpatterns = [

    url(r'^admin/', admin.site.urls),

    url(r'^form/',form),

    url(r'^$', home),

]

 

Try the test now:

python3 manage.py runtest

 

It should pass:

Creating test database for alias 'default'...

.....

----------------------------

Ran 5 tests in 0.032s

 

OK

 

Destroying test database for alias 'default'...

 

Even though we tested it, we also have to have a look on what's going on with the real site. Let's spin up the development server and have a look.

python3 manage.py runserver

 

Check http://localhost/form/ in your browser. Should look like this.:

 

 

Go back to your terminal and Ctrl+C to stop the development server.

 

All test passes and we reached our milestone. The branch is stable, so we can do a commit.

git status

 

Output:

...

modified: MyTutorial/urls.py

modified: main/tests.py

modified: main/views.py

...

main/forms.py

main/templates/form.html

...

git diff

git add .

git commit -m “Form is initiated and visible”

 

Okay. If you fill in the form and click the submit button. It seems like nothing is happening. Well, the data got submitted into your server, but for the user it's not so clear. Therefore we implement some redirection for the him to get some feedback.

 

As usual we first write a test. Second in the FormTest class. So the redirection will go like this: if you check to be subscribed, there will be a you are “You are subscribed.” page to be redirected, otherwise there will be a “Thank you!” page.:

***main/tests.py***

...

def test_form_redirection(self):

 

    def subs_n_test(subs,red_url):

        post_dict = {

            'human':True,

            'color':'bl',

            'age':29,

            'name':'David Fozo',

            'subscription':subs,

            'email':'example@gmail.com'}

        response = self.client.post('/form/',post_dict)

        self.assertRedirects(response,red_url)

 

    subs_n_test(False,'/thanks/')

    subs_n_test(True, '/subscribed/')

 

Run the test.

python3 manage.py test

 

As expected:

F.....

=============================

FAIL: test_form_redirection (main.tests.FormTest)

 

When it comes to redirecting and process the incoming data, it will be done in views.py. Import HttpResponseRedirect and do this major overwrite of the file.

***main/views.py***

from django.http import HttpResponseRedirect

...

def form(request):

    form = SurveyForm()

 

    def redirection(subscription):

        if subscription:

            return HttpResponseRedirect('/subscribed/')

        else:

            return HttpResponseRedirect('/thanks/')

 

    if request.method == 'POST':

        form = SurveyForm(request.POST)

        if form.is_valid():

            return redirection(form.cleaned_data['subscription'])

    else:

        form = SurveyForm()

 

    return render(request,'form.html',{'form': form},)

 

def subscribed(request):

    return render(request,'subscribed.html')

 

def thanks(request):

    return render(request,'thanks.html')

 

So what is happening here? So when someone sends a POST request (filled in form) to our /form/ url, it will pump that data into our form object (SurveyForm(request.POST)). That form object will do the validation and set it's is_valid method to False or True. If it's true there will be a redirection happening based on processed form data (in this case form.cleaned_data).

 

Let's create the two other destination websites with some command line magic:

tem=main/templates

touch $tem/thanks.html $tem/subscribed.html

 

***main/templates/thanks.html***

<!doctype html>

<html>

<head>

  <meta http-equiv="Content-type" content="text/html; charset=utf=8">

  <meta name="viewport" content="width=device-width, initial-scale=1">

</head>

<body>

  <h1>Thank you!</h1>

</body>

</html>

 

***main/templates/subscribed.html***

<!doctype html>

<html>

<head>

  <meta http-equiv="Content-type" content="text/html; charset=utf=8">

  <meta name="viewport" content="width=device-width, initial-scale=1">

</head>

<body>

  <h1>You are subscribed!</h1>

</body>

</html>

 

Give some URL address to these functions.

*** MyTutorial/urls.py***

 

from django.conf.urls import url

from django.contrib import admin

from main.views import home, form, subscribed, thanks # Changed!

 

urlpatterns = [

    url(r'^admin/', admin.site.urls),

    url(r'^form/',form),

    url(r'^thanks/',thanks),

    url(r'^subscribed/',subscribed),

    url(r'^$', home),

]

Try it out.

python3 manage.py runserver

 

Ctrl-C to finish. It seems okay, except it doesn't accept if you don't want to be subscribed. That is too aggressive for our users, so let's do some configuration in main/forms.py.

***main/forms.py***

...

    subscription = forms.BooleanField(label="Do you want to subscribe?",required=False)

Run the test:

python3 manage.py test

 

Okay, the test passes:

Creating test database for alias 'default'...

......

----------------------------------------------------------------------

Ran 6 tests in 0.042s

 

OK

Destroying test database for alias 'default'...

 

All test passes, ready to check out.

git status

...

modified: MyTutorial/urls.py

modified: main/forms.py

modified: main/tests.py

modified: main/views.py

 

Untracked files:

(use "git add <file>..." to include in what will be committed)

 

main/templates/subscribed.html

main/templates/thanks.html

git add .

git commit -m “Form redirects after user submission”

 

Okay, let's say you want to provide with some custom validation. For example, if someone says he is not human, he cannot have red eyes (silly example, right?). For this third test, I reorganized some of the code, this reorganization is called refactoring. Most important aspect is to not repeat yourself. If you have two pieces of code that seemed too similar figure out some way to make it into one variable or function. In this case I had request=HttpRequest() line and post_dict variable two times. Therefore I moved them up into the SetUp function.

 

***main/tests.py***

from main.forms import SurveyForm

...

class FormTest(TestCase):

 

    def setUp(self):

        self.request = HttpRequest()

        self.post_dict = {'human':True,'color':'bl','age':29,'name':'David Fozo','subscription':True,'email':'example@gmail.com'}

 

    def test_form_renders_on_page_properly(self):

        response = form(self.request)

        for i in ['form','input','human','color','age','name','email']:

            self.assertIn(i,response.content.decode())

 

    def test_form_redirection(self):

 

        def subs_n_test(subs,red_url):

            self.post_dict['subscription'] = subs

            response = self.client.post('/form/',self.post_dict)

            self.assertRedirects(response,red_url)

 

        subs_n_test(False,'/thanks/')

        subs_n_test(True, '/subscribed/')

 

    def test_not_human_raises_error(self):

        self.post_dict['human'] = False

        self.post_dict['color'] = 'rd'

        form = SurveyForm(self.post_dict)

        self.assertEqual(form.is_valid(),False)

 

Run the test.

python3 manage.py test

 

Output:

.......

---------------------------------

Ran 7 tests in 0.043s

 

Wow! It passes without us doing anything. That shouldn't happen. The reason is because SurveyForm doesn't allow for non-humans to submit yet, and also there is no red eye option. Let's change that. The form automatically rejects our post_dict dictionary.

***main/forms.py***

...

   human = forms.BooleanField(label="Are you human?", required=False)

   color = forms.ChoiceField(

   label="What color is your eye?",

   choices=(('bl','Blue'),('br','Brown'),('bl','Black'),('gr','Green'),('rd','Red')),)

...

Let's run a test now.

Finally! We actually have to work in order to have a failing test.

..F....

==============================

FAIL: test_not_human_raises_error (main.tests.FormTest)

 

Okay, so form validation logic is in the main/forms.py file and in our case it will be done by the clean function.

 

***main/forms.py***

class SurveyForm(forms.Form):

...

   def clean(self):

       cleaned_data = super(SurveyForm,self).clean()

       if cleaned_data['human'] is False and cleaned_data['color'] == 'rd':

       raise forms.ValidationError("Humans cannot have red eyes!")

       return cleaned_data

 

So what is happening here? super(SurveyForm,self).clean() means it's run one time and do it's usual routine BEFORE you do your own custom validation. It returns a dictionary for you of the processed data. This process happens without you writing a piece of code, but now you want to insert your own validation logic. So you take that cleaned data and raise an error if doesn't met your criteria, then return cleaned data.

 

Run the test:

python3 manage.py test

 

Output:

.......

---------------------

Ran 7 tests in 0.044s

 

OK

 

Okay, so test passes, time for a commit.

git status

...

modified: main/forms.py

modified: main/tests.py

...

git add .

git commit -m “Form validates eye color”

 

Merge it back to master:

git branch

* form

master

 

First we need to squash the commits together.

git rebase -i HEAD~3

 

Interactive menu will pop up, change the last two from “pick” to “squash”:

pick 43aa283 Form is initiated and visible

squash 7c88872 Form redirects after user submission

squash e43e886 Form validates eye color

 

Ctrl+O and Ctrl+X if you are using nano. Then, edit the second file to start this way:

Form visible, redirects and validates eye color.

 

# Please enter the commit message for your changes. Lines starting

# with '#' will be ignored, and an empty message aborts the commit.

...

Now, it's time to merge it back to master.

Git checkout master

git merge form

git log

 

You should see the newest commit - “Form visible,redirects and validates eye color. ” - on top. Now, you can delete the 'form' branch.

git branch -d form

 

Congratulations! You learned the basics of dealing with Django Forms. There are a lot more to it. If you want to see more of this, let me know in the comment section.

 

Until next time.

 

Tags: beginner , django , git , github , TDD , unit test , form , validation