profile for joel goldstick at Stack Overflow, Q&A for professional and enthusiast programmers

JoelGoldstick.com

Django Forms

Categories: Djangopython

Published on April 3, 2016

Last Modified on April 11, 2016

Forms give websites the ability for the reader to participate. Every website I've ever written has used forms. Creating a basic form in html, and writing the backend code to process the input in the most bare bones way isn't too much work, but that's not enough. A form should look good, give hints to the user about how to properly fill it out, and rigorously check the data provided. If there is a problem in the form data, the user should be gently coaxed to correct the problems. In this regard, django Forms facility helps a lot.

Basic Form

bla blah

Some Simple Validation

comes out of the box

Fields that depend on other fields

Adding Bot Deterent Capability

Many sites use Captcha to deter automatic form submission by Bots. The idea is that a human can read some poorly displayed characters correctly and repeat them back to a form in a field. A Bot program will have a much harder time doing this, and so the Bot submissions will fail. Like many people, I find Captcha to be annoying. The letters are often too poorly displayed to figure out correctly.

As an alternative, I prefer the form ask the user a question that is easy for a human being to answer correctly, but not a Bot. In this example, I am using a randomly generated arithmetic equation -- the sum of 2 or 3 randomly generated integers. To keep it easy, I limit the integers to numbers from 1 to 20. This is probably not a very good test, since the answer will be from 3 to 60, and I can imagine a bot could work that out by brute force. Perhaps a better test would be to display some text and ask for the second word that starts with 'C', or something.

Whatever the test, the form can generate the question and the answer when entered. When the form is submitted, the answer is checked. The problem is that HTTP is stateless, and so we need a way to save the answer when it is generated, so that the submit request can use it to test it.

Here is my code for the test:

import random                                
def produce_expression():
    """
    return a tuple: expression (str), answer (int)
    """
    num_terms = random.randint(2,3)
    operands = []
    while num_terms:
        n = random.randint(1,21)
        operands.append(n)
        num_terms -= 1

    result = sum(operands)
    string_operands = map(str, operands)
    expression = " + ".join(string_operands)
    return expression, result

The view.py code looks like this:

def contact(request):

test_expression, answer = produce_expression()
request.session['last_answer'] = request.session.get('answer', None)
request.session['answer'] = answer

answer = request.session['answer']
if request.method == 'POST': # If the form has been submitted...
    #form = ContactForm(request.POST) # A form bound to the POST data
    form = ContactForm(request.POST, request=request) # A form bound to the POST data
    if form.is_valid(): # All validation rules pass
        # Process the data in form.cleaned_data
        request.session['answer'] = ""
        return HttpResponseRedirect('/thanks/') # Redirect after POST
else:
    form = ContactForm() # An unbound form

c = {'form': form, 'test': test_expression, 'answer': answer}
c.update(csrf(request))
return render_to_response('blog/contact_form.html', c)

Every time the contact page is created we get a new question and answer:

test_expression, answer = produce_expression()
request.session['last_answer'] = request.session.get('answer', None)
request.session['answer'] = answer

When the user enters his answer in the form, the form is once again created, so we need to keep the answer that was correct when the user entered his choice. The session object which is included in the request object will maintain state for us

When we instantiate the form with the POST data, we also pass the request object which contains the session where we have stored the answer. In the form itself, we use the clean method to compare results.

class ContactForm(forms.Form):
    subject = forms.CharField(max_length=100, required=True)
    message = forms.CharField(required=True)
    sender = forms.EmailField(required=True)
    test_result = forms.IntegerField(label="Test Result", required=True)
    cc_myself = forms.BooleanField(required=False)

    def __init__(self, *args,**kwargs):
        # overide __init__ to grab the request object, and then initialize the parent class
        self.request = kwargs.pop('request', None)
        super (ContactForm,self).__init__(*args,**kwargs)

    # django runs clean_<field name> for each field in your form
    def clean_test_result(self):
        data = self.cleaned_data['test_result']
        if self.request.session['last_answer'] <> data:
            raise forms.ValidationError("You didn't get the math right!")

And here is the template: