Django – AJAX

This post is part of my Django series. You can see an overview of the series along with instruction on how to get all the source code here.

This article assumes you are comfortable creating a Django project, can create apps and register them into the INSTALLED_APPS list of the settings file. If not please read my Django HelloWorld article.

This article assumes you have a project called DjangoSandBox with an application called ajaxintroduction.

Overview

In these examples are we are going to create a page which uses AJAX to GET and POST data between the server.

The page is based around a ‘Quote’ object which contains the quote and the author. We will provide a single page which has the following functionality:

  • Get all quotes for an author
  • Get a random quote
  • Create a quote.

The following examples are split out but in the repository this is a single page. I strongly urge you to work from the source code within the repository.

Initial Page

So before we start on the AJAX calls we need an initial page up and running.

First lets create our model.

#models.py
from django.db import models
from django.db.models import Model

class Quote(Model):
    quote = models.TextField()
    author = models.CharField(max_length=50)

    def __str__(self):
        return "{0}: {1}".format(self.author, self.quote)

Make and run the migrations:

python manage.py makemigrations ajaxintroduction
python manage.py migrate

Next is our view, currently we simply want to return our template:

#views.py

from django.shortcuts import render

def get_quotes(request):
    return render(request, "ajaxintroduction/random_quotes.html")

Next our template; nothing more than a heading currently.

<!-- templates/ajaxintroduction/random_quotes.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Daily Quote!</title>

    <style>
        span {
            display: block;
        }
        span span {
            display: inline;
        }
    </style>
</head>
<body>

<h1>AJAX Examples</h1>
</body>

Then we hook in our view into our URL routing configuration.

# urls.py
from django.conf.urls import patterns, url

from . import views

urlpatterns = 
    patterns('',
    url(r'^$', views.get_quotes, name="index"),
)

Now hook our application into our project URL routing configuration:

#DjangoSandBox/urls.py

from django.conf.urls import patterns, include, url

from ajaxintroduction import urls as ajax_urls

urlpatterns = 
    patterns('',
             url(r'^ajax/', include(ajax_urls, namespace="ajax")),
             )

To test run the development server and navigate to:

http://127.0.0.1:8000/ajax/

You should see the template page loaded with our heading.

Static Files

Next we are going to configure our static files:

  • JQuery which we use for performing AJAX calls in JavaScript
  • JQuery-Cookies which will use to grab the CSRF token from our cookie
  • A new js file which we will place all our required JavaScript code in

First create an empty text file in ajaxintroduction/static/ajaxintroduction/js/ called ajax-example.js

// ajaxintroduction/static/ajaxintroduction/js/ajax-example.js

// Currently empty

Next download JQuery and JQueryCookies. You can download them from the references below if you have not downloaded the repository code of my examples. Alternatively you can download the files directly from GitHub

Note: if you are not downloading the files from my repository ensure you file names match with regards to the versions throughout the examples.

Copy in jquery and jquery.cookie into and DjangoSandBox/static/js

We now need to register these files as being static files which can be served directly to the user.

Within the settings file register the paths of the static directory of the project as well as the application directory of our ajaxintroduction application.

# settings.py
STATIC_URL = '/static/'

STATICFILES_DIRS =  (
    os.path.join(PROJECT_ROOT, 'static'),
    os.path.join(PROJECT_ROOT, 'ajaxintroduction', 'static'),
)

Now are are going to reference all these static files in our html head.

  • Call “load staticfiles” to enable the use of static files in our template
  • Reference the file with the static template tag. Files are referenced relative to the composition of STATICFILES_DIRS/STATIC_URL:
    • For our project files we reference js/file.js
    • For our application files we reference ajaxintroduction/js/file.js

Note: similar to templates we duplicate the application name within the directory path of our application static files as a way of name spacing our files.

<!-- templates/ajaxintroduction/random_quotes.html -->
<head>
    {% load staticfiles %}

    <script src="{% static "js/jquery-2.1.4.js" %}"></script>
    <script src="{% static "js/jquery.cookie.js" %}"></script>
    <script src="{% static "ajaxintroduction/js/ajax-example.js" %}"></script>    
</head>

To test run the development server and navigate to:

http://127.0.0.1:8000/ajax/

You should be able to use FireBug or Developer Tools to view the contents of the JavaScript files which have been downloaded along side our generated HTML.

If you look at the HTML code you will see the file path of the static file has been completed as a relative URL from our website route prefixed with static; static/ajaxintroduction/js/ajax-example.js

Note: if the files have not been downloaded use FireBug or Developer Tools to check the network requests of these resource files. Check the URL of the request is as expected. Check the response. Try entering in the full URL of the resource file into the browser:

http://127.0.0.1:8000/static/ajaxintroduction/js/ajax-example.js

Get – A Random Quote

We are now going to create the functionality to get a random quote. First we create a view which gets a random Quote record. We return the record as JSON data using the inbuilt Python json library:

import json
context_data = {random_quote.author: random_quote.quote}
json.dumps(context_data)

We then return the JSON data via a HTTPResponse setting the content_type to “application/json”. Add the following view function:

#views.py
import json
from random import randint

from django.http import HttpResponse

from .models import Quote

def get_random_quote(request):
    random_number = randint(0, Quote.objects.all().count() - 1)

    random_quote = Quote.objects.all()[random_number]
    context_data = {random_quote.author: random_quote.quote}

    return HttpResponse(json.dumps(context_data), content_type="application/json")

We then create a simple button and a span element to display our random quote. Both require an id as this is how we will be finding the element via JQuery. Add the following into our template file.

<!-- templates/ajaxintroduction/random_quotes.html -->

<h2>Get Random Quote:</h2>
<span>
    <button type="button" id="getquote">Get:</button>
    <span id="quote"></span><span id="author"></span>
</span>

Note: the above should be added into our existing html template file and not replacing its contents.

We now hook up our button to our view which returns our random quote as JSON data within our JavaScript file:

// ajaxintroduction/js/ajax-example.js
$(document).ready(function () {
    $("#getquote").click(function () {
        getQuote();
    });
});

function getQuote() {
    $.getJSON("/ajax/random", function (data) {
        console.log("getQuote.response:");
        console.log(data);
        $.each(data, function (author, quote) {
            $('#quote').text(quote);
            $('#author').text(author);
        });
    });
}
  • The $(document).ready is a special JQuery function which only runs when the html page has finished loading within the browser. We use it to bind our getQuote function onto the click event of our button.
  • The getJSON function is used to make a call to our view via AJAX. The first parameter is a relative URL which we will configure up below in our URL routing configuration. The second parameter is a call back function which is passed our returned random quote as JSON data.
  • We use the JQuery each function to iterate and unpack our JSON data which we display within our span elements,

We now hook in our view to our URL routing configuration.

# urls.py
from django.conf.urls import patterns, url

from . import views

urlpatterns = 
    patterns('',
    url(r'^random/$', views.get_random_quote, name="random"),
)

To test run the development server and navigate to:

http://127.0.0.1:8000/ajax/

Pressing the button will return and display a random quote.

Note: if the AJAX request does not appear to work use FireBug or Developer Tools to check the request. Check the URL, the parameters and it’s status. To see any errors which might be on the actual AJAX view navigate to the URL of the AJAX request.

You can also navigate directly to the new view function via:

http://127.0.0.1:8000/ajax/random

Get Multiple

In our next example we are going to allow the retrieval of all quotes for an author.

First we change our initial page view to return a list of all distinct authors.

Note: SQLite3 does not support the distinct SQL command so we use the Python set collection to create a distinct list of authors.

Secondly we create a new view which looks up all quotes for an author which we will pass in as a GET parameter. We return the quotes as a QuerySet.

The Django QuerySet is not serializable via the inbuilt Python json library. Django provides the serializers class which can.

#views.py
import json
from random import randint

from django.core import serializers
from django.http import HttpResponse, JsonResponse
from django.shortcuts import render

from .models import Quote

def get_quotes(request):
    authors = set(r.author for r in Quote.objects.all())

    return render(request, "ajaxintroduction/random_quotes.html", context={"authors": authors})


def get_author_quotes(request):
    if "author" not in request.GET or request.GET["author"] == "all":
        quotes = Quote.objects.all()
    else:
        author_name = request.GET["author"]
        quotes = Quote.objects.filter(author=author_name)

    return HttpResponse(serializers.serialize('json', quotes), content_type="application/json", )

We next now create our UI. This will consist of a choice list of the authors, a button and a span element for displaying the results.

<!-- templates/ajaxintroduction/random_quotes.html -->

<h2>Get Quotes By Author:</h2>
<span>
    <select id="authors" title="Fpop dfic">
        <option value="all">All</option>
        {% for author in authors %}
            <option value="{{ author }}">
                {{ author }}
            </option>
        {% endfor %}
    </select>
    <button type="button" id="getAuthorQuotes">Get</button>
</span>
<span id="quotesbyauthor"></span>

Now our JavaScript; first hook our function onto our button within the JQuery document ready function.

The only real difference between our call to getJSON in the previous example is that we pass in the selected author from the select box as a GET parameter. GET parameters are constructed from key value pairs represented by a JSON string.

Again we use the JQuery function ‘each’ to loop through all quotes unpacking them into a function as key value pair. We construct a series of list elements and add them into our span element.

// ajaxintroduction/js/ajax-example.js
$(document).ready(function () {
    $("#getAuthorQuotes").click(function () {
        getAuthorQuotes();
    });
});

function getAuthorQuotes() {
    $.getJSON("/ajax/author/", {author: $('#authors').val()}, function (data) {
            console.log("getAuthorQuotes.response:");
            console.log(data);

            var $quotesbyauthor = $('#quotesbyauthor');
            $quotesbyauthor.text("");

            var items = [];

            $.each(data, function (key, value) {
                items.push("<ul>" + value.fields["quote"] + ". " + value.fields["author"] + "</ul>");
            });

            $("<ul/>", {
                "class": "quotes",
                html: items.join("")
            }).appendTo($quotesbyauthor);
        }
    );
}

Now we simply need to hook in our view into our URL routing configuration.

# urls.py
from django.conf.urls import patterns, url

from . import views

urlpatterns = 
    patterns('',
    url(r'^author/$', views.get_author_quotes, )

To test run the development server and navigate to:

http://127.0.0.1:8000/sessionsandcookies/ajax/

You can now ask for all quotes for an author. If you cannot get this to work check the requests being made along with the results. Try navigating to the URL directly.

Post -Create A Quote

There is no real fundamental difference between a HTTP POST or a GET. The method is used as a status; both can contain URL parameters and both can contain a body.

It is advisable that GET requests should never perform CRUD operations. We will use a POST to create our Quote record.

We are going to allow the user to create a quote asynchronously from our page. We will create a new view function which takes our quote and author as strings, ensures they are set and then attempts to create the quote. We will use get_or_create to ensure we don’t create duplicate quotes.

We simply use request.POST[“XXX”] to get our form element value on the server side. For more complicated examples you should think about writing a dedicated Django model form.

We then return an indication of whether the quote was created, a validation message and also the new number of quotes in the database. We return the data as JSON but using the JsonResponse; this will automatically serialise the data to JSON as well as setting the content type of the response to JSON.

# views.py
import json
from random import randint

from django.core import serializers
from django.http import HttpResponse, JsonResponse
from django.shortcuts import render

from .models import Quote

# @csrf_exempt # Use for debugging only.
def new_quote(request):
    message = ""

    is_valid = True
    created = False

    if "quote" not in request.POST or "author" not in request.POST:
        message = "Invalid post data"
        is_valid = False

    if request.POST["quote"] == "" or request.POST["author"] == "":
        message = "Please provide a quote and an author"
        is_valid = False

    if is_valid:
        quote, created = Quote.objects.get_or_create(quote=request.POST["quote"], author=request.POST["author"])

        if created:
            message = "Quote Created"
        else:
            message = "Quote already exists"

    data = {"count": Quote.objects.all().count(), "message": message, "created": created}

    return JsonResponse(data)

We now add a form onto our template which will be used to capture the data required to create our Quote record. We also add a button. We include a csrf_token in our form to prevent from cross site forgery attacks.

<!-- templates/ajaxintroduction/random_quotes.html -->

<h2>Create Quote:</h2>
<form method="post" action="">
    {% csrf_token %}
    <table>
        <tr>
            <th>Quote</th>
            <th>Author</th>
        </tr>
        <tr>
            <td><input type="text" id="txtquote"/></td>
            <td><input type="text" id="txtauthor"/></td>
        </tr>
    </table>
    <button type="button" id="newquote">New Quote</button>
</form>

We now hook in our JavaScript; this is where it starts to get a bit tricky as we need to post back our csrf_token as part of the POST data.

Django writes the token value to a cookie called csrftoken which we can access in JavaScipt via the JQuery-Cookies library. We hooked this file in above as a static file. We should only include this if our HTTP method is not one of GET, HEAD, OPTIONS or TRACE. We set the token in the X-CSRFToken HTTP request header parameter. We use the JQuery ajaxSetup function to do this.

If you want to test posting back without Django validating the presence and validity of the token you can decorate your view with the @csrf_exempt decorator. This should only be used for testing/development purposes.

We then call our new view URL passing in the data of the new quote we would like to create.

We then display our validation message and the new count of elements.

// ajaxintroduction/js/ajax-example.js
function csrfSafeMethod(method) {
    // these HTTP methods do not require CSRF protection
    return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}

$.ajaxSetup({
    beforeSend: function (xhr, settings) {
        if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
            xhr.setRequestHeader("X-CSRFToken", $.cookie('csrftoken'));
        }
    }
});

$(document).ready(function () {
    $("#newquote").click(function () {
        createNewQuote();
    });
});

function createNewQuote() {
    $.ajax({
        type: "POST",
        url: "/ajax/new/",
        data: {
            'quote': $('#txtquote').val(),
            'author': $('#txtauthor').val()
        },
        success: function (data) {
            console.log("createNewQuote.response:");
            console.log(data);

            alert(data.message);

            if (data.created) {
                alert("There are now " + data.count + " quotes");
            }
        }
    });
}

The last thing we need to do is to hook our new view into our URL routing configuration.

# urls.py
from django.conf.urls import patterns, url

from . import views

urlpatterns = 
    patterns('',
    url(r'^new/$', views.new_quote, name="new"),
)

To test run the development server and navigate to:

http://127.0.0.1:8000/sessionsandcookies/ajax

##References## {#References}

Django: Testing

This post is part of my Django series. You can see an overview of the series along with instruction on how to get all the source code here.

This article assumes you are comfortable creating a Django project, can create apps and register them into the INSTALLED_APPS list of the settings file. If not please read my Django HelloWorld article.

Django uses the Python package unittest. This article assumes that you have a basic knowledge of creating unittests with this package. If this is not the case please see my blog post on unittest.

This article assumes you have a project called DjangoSandBox in an application called testintroduction.

This article tests the various parts we have built up over the series; please see here for any explanations of the code we are testing.

Testing Models

Models are pretty easy to test; our first example is using the HelloWorld class we have created previously. Nothing special, a name and date field, a __str()__ function and a Django get_absolute_url function.

#helloworld/models.py
from django.db import models
from django.core.urlresolvers import reverse

class HelloWorld(models.Model):
    first_name = models.CharField(max_length=20)
    now = models.DateTimeField()

    def __str__(self):
        return "{0} {1}".format(self.first_name, self.now)

    @staticmethod
    def get_absolute_url():
        return reverse('helloworld:model')

Testing these functions is nothing more than creating an instance of the class, calling the require function or property and asserting that our result is as required.

#test_models.py

from datetime import date
from django.test import TestCase
from helloworld.models import HelloWorld

class TestHelloWorld(TestCase):
    def setUp(self):
        self.first_name = "Luke"
        self.now = date.today()

        self.a_helloworld = HelloWorld(first_name=self.first_name, now=self.now)

    def test_str(self):
        self.assertEqual("{0} {1}".format(self.first_name, self.now), str(self.a_helloworld))

    def test_get_absolute_url(self):
        self.assertEqual("/helloworld/model/", self.a_helloworld.get_absolute_url())

Our model could contain validation. We can test validation by creating an instance of the model in an invalid state and calling full_clean. If the model is invalid an exception is raised with all the errors contained within a dictionary upon the message_dict property.

# test_models.py
from datetime import date
from django.core.exceptions import ValidationError
from django.test import TestCase
from modelsadvanced.models import ContactDetails, ProxyChild

class TestModelValidation(TestCase):

    def test_model_validation(self):

        try:
            a_contact = ContactDetails(name="Luke", age=101, contactDate=date(year=2015, month=7, day=2))
            a_contact.full_clean()
        except ValidationError as e:

            self.assertEqual({'name': ['Ensure this value has at least 10 characters (it has 4).', 'Luke is barred'],
                              'age': ['Ensure this value is less than or equal to 100.'],
                              'contactDate': ['2015-07-02 is not a future date.']}, e.message_dict)

If we want to test class meta data we can access this via the _meta property:

#test_models.py
class TestModelMeta(TestCase):
    def test_model_meta(self):
        self.assertEqual(True, ProxyChild()._meta.proxy)

We can use list and map to get a list of all field names contained within the fields property of the meta class.:

#test_models.py
class TestModelFields(TestCase):
    def test_model_meta(self):
        field_names = list(map((lambda x: x.name), ContactDetails._meta.fields))

        self.assertEqual(4, len(ContactDetails._meta.fields))
        self.assertEqual(['id', 'age', 'name', 'contactDate'], field_names)

Testing Views

When we test views we can use the test client; it acts as a dummy Web browser. We access it via the client property of our unit test class.

We call get upon the test client along with the URL.

response = self.client.get("/index")

We can use reverse to access the URL via our routing config. Don’t worry we will explicitly test the routing config later/

response = self.client.get(reverse("home:home"))

The response.status_code can be used to assure our response code is as expected.

The response.resolver_match can be use to ensure that the view function used was as expected.

The function self.assertTemplateUsed can be used to assure that the correct template was used.

We can assert that the html is as expected by calling self.assertContains along with sections of html or strings which should be contained within the rendered HTML.

#test_views.py

from django.core.urlresolvers import reverse
from django.test import TestCase
from home import views as home_views

class TestFunctionView(TestCase):
    def test_function_view(self):
        response = self.client.get(reverse("home:home"))

        self.assertEqual(200, response.status_code)
        self.assertEqual(home_views.home, response.resolver_match.func)
        self.assertTemplateUsed(response, template_name='home/home.html')

        self.assertContains(response, '<a href="/helloworld/index/">Hello World</a>')
        self.assertContains(response, '<a href="/templatesintroduction/index">Templates</a>')
        self.assertContains(response, '<a href="/formsintroduction/basic/">Forms</a>')
        self.assertContains(response, '<a href="/viewsintroduction/contact/list">Views</a>')

We can access the context data which was passed to the view via the context property of the response.

#test_views.py

from django.core.urlresolvers import reverse
from django.test import TestCase
from helloworld import views as helloworld_views

class TestContextData(TestCase):
    def test_context_data(self):
        response = self.client.get(reverse("helloworld:template"))

        self.assertEqual('Obelix', response.context["who"])
        self.assertEqual(False, response.context["islong"])

Testing Class Based Views

In the example two up we used the following code to ensure the correct view function was used:

self.assertEqual(home_views.home, response.resolver_match.func)

If we want to assure that the correct class view was used we need to compare the name of the class against that which was used:

self.assertEqual(PhoneAddressListView.as_view().__name__, response.resolver_match.func.__name__)

Where a base template is used we can simply call the function assertTemplateUsed with the base template name.

In the example below we test the list view with and without records to ensure our “no records here” message is displayed to the user.

When records are returned to our template we have a few ways we can test the results along with the assertContains function we have seen in the previous examples.

  • Check the record count of the object list in the contect view
  • Compare elements
  • Compare the query set. To do this we need to convert the expected query set to a list of strings from the repr function as this is what is contained in the response.
#test_views.py

class TestClassView(TestCase):
    def test_with_no_records(self):
        response = self.client.get(reverse("viewsintroduction:address_list"))

        self.assertEqual(200, response.status_code)
        self.assertEqual(PhoneAddressListView.as_view().__name__, response.resolver_match.func.__name__)

        self.assertTemplateUsed(response, template_name='viewsintroduction/base.html')
        self.assertTemplateUsed(response, template_name='viewsintroduction/phoneaddresslist.html')

        self.assertEqual(0, len(response.context["phoneaddress_list"]))
        self.assertContains(response, '<tr><td colspan="6">There are no addresses</td></tr>')

    def test_with_one_record(self):
        an_address = PhoneAddress(number=1, street_name="A", city="A City")
        an_address.save()

        response = self.client.get(reverse("viewsintroduction:address_list"))

        self.assertEqual(200, response.status_code)
        self.assertEqual(PhoneAddressListView.as_view().__name__, response.resolver_match.func.__name__)

        self.assertTemplateUsed(response, template_name='viewsintroduction/base.html')
        self.assertTemplateUsed(response, template_name='viewsintroduction/phoneaddresslist.html')

        context_addresses = response.context['phoneaddress_list']
        expected_addresses = [repr(r) for r in PhoneAddress.objects.all()]

        self.assertEqual(1, len(context_addresses))
        self.assertEqual(an_address, context_addresses.first())

        self.assertQuerysetEqual(context_addresses, expected_addresses, ordered=False)

        self.assertContains(response, an_address.city)
        self.assertContains(response, an_address.number)
        self.assertContains(response, an_address.street_name)

        self.assertNotContains(response, '<tr><td colspan="6">There are no addresses</td></tr>')

Testing Errors

We now move on to test some of our error views; by comparing the status code.

#test_views.py

class Test404Error(TestCase):
    def test_404_error_is_raised(self):
        response = self.client.get(reverse("helloworld:error_as_404"))
        self.assertEqual(404, response.status_code)

class TestNotAllowed(TestCase):
    def test_not_allowed(self):
        response = self.client.get(reverse("helloworld:not_allowed"))

        self.assertEqual(405, response.status_code)


class TestOnlyPost(TestCase):
    def test_raises_error_on_get(self):
        response = self.client.get(reverse("helloworld:error_if_not_post"))

        self.assertEqual(405, response.status_code)

    def test_all_ok_on_post(self):
        response = self.client.post(reverse("helloworld:error_if_not_post"), {'name': 'Jim'})

        self.assertEqual(200, response.status_code)
        self.assertContains(response, "{0}, this can only be called with a post".format("Jim"))

When we expect a redirect is made we can use the assertRedirects function to validate this. The function takes the status codes of our page load and also the page we direct to. 301 indicates that this is a page which has moved permanently.

#test_views.py

class TestRedirect(TestCase):
    def test_redirect(self):
        response = self.client.post(reverse("viewsintroduction:redirect_view"))

        self.assertTrue(301, response.status_code)
        self.assertRedirects(response, "http://djangoproject.com", 301, 200)  # Status codes Us/Them

Test URLS

When we test URLs we should test two things;

  • The reverse function returns the correct URL string
  • The resolve function returns the correct view function. This does not only include the function and URL name but the app name of the view, the URL namespace and any arguments which will be passed into the view function.
#test_urls.py

from django.core.urlresolvers import reverse, resolve
from django.test import TestCase

from home import views

class TestHomeUrls(TestCase):
    urls = 'home.urls'

    def test_reverse(self):
        self.assertEqual('/', reverse('home'))

    def test_resolve(self):
        resolver = resolve(reverse('home'))

        self.assertEqual(views.home, resolver.func)
        self.assertEqual('home', resolver.url_name)

        self.assertEqual((), resolver.args)
        self.assertEqual({}, resolver.kwargs)
        self.assertEqual(None, resolver.app_name)
        self.assertEqual('', resolver.namespace)

Note: Setting urls = ‘home.urls’ upon the test class above defines the URL routing config we are using. Here we test only an apps config which explains why we reference the URL by their name only without the namespace which we set in the project’s urls.py

When our URL config contains get query parameters we can pass them into the reverse function with the kwargs parameter which is a dictionary.

#test_urls.py

class TestViewsIntroductionUrls(TestCase):
    urls = 'viewsintroduction.urls'

    def test_reverse(self):
        self.assertEqual('/address/1/update/', reverse('address_update', kwargs={'pk': 1}))

Testing Forms

When testing forms we pass the input data as a dictionary into the constructor.

We can access the form fields via the fields property; below we ensure that we have the correct fields and that they contain the right configuration.

We can assert that the form validates as expected with form.is_valid(). Any errors raised will be found within the property errors on the form which is a dictionary of errors. Field errors will be found under a key named after the field name; it returns a list of errors.

#test_forms.py

from django.core.urlresolvers import reverse
from django.forms import DecimalField
from django.test import TestCase

from formsintroduction.forms.bastic_form_example import BasicFormExample
from formsintroduction.forms.class_form_example import ClassBasedForm
from viewsintroduction.models import PhoneAddress


class TestBasicForm(TestCase):
    def test_fields(self):
        form = BasicFormExample({})

        self.assertEqual(2, len(form.fields))
        self.assertTrue("height" in form.fields.keys())
        self.assertTrue("name" in form.fields.keys())
        self.assertIsInstance(form.fields['height'], DecimalField)
        self.assertEqual(2, form.fields['height'].max_value)

    def test_valid_data(self):
        form = BasicFormExample({"name": "Luke", "height": 1.11})

        self.assertTrue(form.is_valid())

    def test_invalid_data(self):
        form = BasicFormExample({"name": "Luke"})

        self.assertFalse(form.is_valid())
        self.assertTrue("name" not in form.errors.keys())
        self.assertTrue("height" in form.errors.keys())

        self.assertEqual(form.errors["height"], ['This field is required.'])

Testing Class Derived Forms

Class based views are tested in pretty much the same way. However when we call save we get an instance of the record created which we can test against.

In the example below we test the form meta data to ensure that the correct fields are set along with the right model class.

#test_forms.py

class TestClassBasedForm(TestCase):
    def test_valid_data(self):
        form = ClassBasedForm({"number": 123, "street_name": "The high street", "city": "Bristol"})

        self.assertTrue(form.is_valid())

        a_record = form.save()

        self.assertEqual(123, a_record.number)
        self.assertEqual("The high street", a_record.street_name)
        self.assertEqual("Bristol", a_record.city)

    def test_invalid_data(self):
        form = ClassBasedForm({})

        self.assertEqual(form.errors["number"],
                         ['We need a number of the house!', 'None of the fields have been set!!!'])

    def test_meta(self):
        self.assertEqual(PhoneAddress, ClassBasedForm.Meta.model)
        self.assertEqual(("city", "street_name", "number"), ClassBasedForm.Meta.fields)

Posting Forms

We use the test client to test posting a form to the server. We pass the form data as a dictionary along with the URL.

#test_forms.py

    def test_post_invalid(self):
        response = self.client.post(reverse("formsintroduction:form_class_create"),
                                    {"city": "Word_Word", "street_name": "street", "number": 1},
                                    follow=True)

        self.assertEqual(200, response.status_code)
        self.assertFormError(response, 'form', 'city', ["Name must be a capitalised single word"])

    def test_post_valid(self):
        response = self.client.post(reverse("formsintroduction:form_class_create"),
                                    {"city": "Foo", "street_name": "Foo", "number": 1},
                                    follow=True)

        self.assertEqual(200, response.status_code)
        self.assertRedirects(response, "http://testserver/viewsintroduction/address/1/", 302)

Testing An Authorised View

Unsuccessful Authentication

The Django authorisation system will automatically redirect a user to the registered login page when they try to access a view which requires authentication. Our first test case assures that we are redirected successfully.

from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.test import TestCase


class TestAuthedView(TestCase):
    def test_redirect_to_loging(self):
        response = self.client.post(reverse("auth:users"))

        self.assertTrue(302, response.status_code)
        self.assertRedirects(response, "auth/login/?next=/auth/users/", 302, 200)

In the above example the 302 and 200 parameters passed into assertRedirects are status codes of the HTTP for the initial page and then the redirect page. 302 is a redirect while 200 is a successful request.

Successful Authentication

The Django test web client comes with the ability to log a user in:

self.client.login(username='username', password='password')

Note: don’t forget that when we are running tests we start with a blank database so you will need to create a login first. The example below creates a new user and activates it in line; this should be moved to a test fixture set-up function.

from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.test import TestCase

class TestAuthedView(TestCase):

    def test_can_login(self):
        user = User.objects.create_user(username='FOOTESTFOO', password='pwd')
        user.is_active = True
        user.save()

        login = self.client.login(username='FOOTESTFOO', password='pwd')

        self.assertTrue(login)

        response = self.client.post(reverse("auth:users"))

        self.assertTrue(200, response.status_code)
        self.assertContains(response, "Registered Users")
        self.assertContains(response, "First Name")
        self.assertContains(response, "Last Name")
        self.assertContains(response, "FOOTESTFOO")

Testing Cookies

We can access cookies from the cookies property of the Django test web client once we have called post or get. It is a dictionary keyed upon the cookie name:

self.client.cookies
self.client.cookies["hits"]

The cookie is returned as a http.cookies.Morsel. We can access the cookie value with the value property. The class responds as a dictionary for other data, in the following example we ensure the expiry has been set correctly with the max-age key.

from django.core.urlresolvers import reverse
from django.test import TestCase

class TestSessions(TestCase):
    def test_cookie(self):
        response = self.client.post(reverse("sessionsandcookies:cookies"))

        self.assertTrue(200, response.status_code)

        self.assertTrue("hits" in self.client.cookies)
        self.assertEqual('1', self.client.cookies["hits"].value)
        self.assertEqual(30, self.client.cookies["hits"]["max-age"])

Testing Session Data

We can access session data upon the Django test web client after we have called post or get. It returns a SessionBase which responds as a dictionary to retrieve the session data from their key names. All functionality of the SessionBase class is available; below we call get_expiry_age to ensure our session expiry policy has worked as expected:

from django.core.urlresolvers import reverse
from django.test import TestCase


class TestSessions(TestCase): 

    def test_sessions(self):
        response = self.client.post(reverse("sessionsandcookies:sessions"))

        self.assertTrue(200, response.status_code)

        self.assertTrue("has_visited" in self.client.session)
        self.assertEqual('True', self.client.session["has_visited"])
        self.assertEqual(10, self.client.session.get_expiry_age())

Additional Assertions

As mentioned previously, Django provides some additional assert functions to the standard Python unittest. class We have covered many of them above though there are two others which I think will be quite useful.

The assertFieldOutput assertion allows us to validate against good and bad field input data. I tried to get this to work against the defined fields or a model or form however it seems to only work with the class type.

The assertNumQueries assertion will ensure that the database has been touched the correct number of times.

#test_additionl_assertions.py

from django.forms import EmailField, CharField
from django.test import TestCase

from viewsintroduction.models import PhoneAddress


class TestAdditionalAssertions(TestCase):
    def test_assertFieldOutput(self):
        self.assertFieldOutput(EmailField, {'a@a.com': 'a@a.com'}, {'aaa': ['Enter a valid email address.']})

        self.assertFieldOutput(CharField, {'aa': 'aa'},
                               {'word': ['Ensure this value has at most 3 characters (it has 4).'],
                                'a': ['Ensure this value has at least 2 characters (it has 1).']}, [],
                               {"max_length": 3, "min_length": 2})

    def test_assert_num_queries(self):
        with self.assertNumQueries(1):
            PhoneAddress.objects.create(city="Foo", street_name="Foo", number=1)

        self.assertNumQueries(1, lambda: PhoneAddress.objects.create(city="Foo", street_name="Foo", number=1))

References

Sessions & Cookies

This post is part of my Django series. You can see an overview of the series along with instruction on how to get all the source code here.

This article assumes you are comfortable creating a Django project, can create apps and register them into the INSTALLED_APPS list of the settings file. If not please read my Django HelloWorld article.

This article assumes you have a project called DjangoSandBox in an application called sessionsandcookies.

Sessions

By default Django comes with the sessions middleware and applications installed:

#settings.py

# Application definition
INSTALLED_APPS = (
    'django.contrib.sessions',
)

MIDDLEWARE_CLASSES = (
    'django.contrib.sessions.middleware.SessionMiddleware',
)

Note: If the settings were not present or you are starting a new project, ensure you have run migrate to create all the required schema and run all the required configuration scripts:

python manage.py migrate

Setting

Session data is stored as key value pairs within request.session which behaves as a dictionary. We set a value with [“key”] = value and get a value with get(“key”).

Session data is only valid for a certain amount of time. This is set to SESSION_COOKIE_AGE which defaults to two weeks.

We can use the get_expiry_date function to see the expiry date of the session data.

We can set the expiry with the set_expiry function which takes a time in seconds.

The following uses session data to set mark that a user has visited a page.

#views.py

from datetime import datetime

def sessions(request):
    context = {
        "has_visited": request.session.get("has_visited", False),
        "visited": request.session.get("visited", None),
        "expiry": request.session.get_expiry_date().strftime("%x %X")
    }

    if "has_visited" not in request.session:
        request.session['has_visited'] = str(True)
        request.session['visited'] = datetime.now().strftime("%x %X")
        request.session.set_expiry(10)  # Time in seconds. 0 expires when the browser is closed

    return render(request, 'sessionsandcookies/sessions.html', context)

A simple template to display the data we store and retrieve from the session data.

<!-- templates/sessionsandcookies/sessions.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Sessions</title>
</head>
<body>
<h1>Sessions</h1>

{% if has_visited %}
    <span>Visited: {{ visited }}</span>
    <span>Expires: {{ expiry }}</span>
{% else %}
    First Page Land
{% endif %}

<span>
    <a href="{% url 'sessionsandcookies:sessions_clear' %}">Clear</a>
</span>
</body>
</html>

Hooking in our view function into the URL routing config:

# urls.py
url(r'^sessions/$', views.sessions, name="sessions"),

Note: don’t forget to hook our app into the project URL routing config.

To test run the development server and navigate to:

http://127.0.0.1:8000/sessionsandcookies/sessions/

Clearing

We can use the clear function upon request.session to remove all session data. In this example we clear the session data and then redirect to the page in the previous example to prove we revert to the default value.

#view.py
def sessions_clear(request):
    if 'has_visited' in request.session:
        del request.session['has_visited']

    request.session.clear()

    return redirect(reverse("sessionsandcookies:sessions"))

Note: Above we call delete upon the session data element as well as clear; we can do both depending upon what we require.

Hooking in our view function into the URLrouting config.

#urls.py
url(r'^sessions/clear/$', views.sessions_clear, name="sessions_clear"),

To test run the development server and navigate to:

http://127.0.0.1:8000/sessionsandcookies/sessions/clear/

Cookies

Not all browsers accept cookies so our first example is a way to test if the browser accepts cookies. We call set_test_cookie and then test_cookie_worked. The latter returns true if we can create cookies.

Testing

#views.py
def cookies_test(request):
    can_create_cookie = False
    request.session.set_test_cookie()

    if request.session.test_cookie_worked():
        request.session.delete_test_cookie()
        can_create_cookie = True

    return render(request, "sessionsandcookies/cookies_test.html", {"can_create_cookie": can_create_cookie})

A simple view which displays our result.

<!-- templates/sessionsandcookies/cookies_test.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Test Cookie</title>
</head>
<body>
<h1>Test Cookie</h1>

{% if can_create_cookie %}
    <span>Test cookie worked</span>
{% else %}
    <span>Test cookie did not work</span>
{% endif %}

<span>
    <a href="{% url 'sessionsandcookies:cookies' %}">Back</a>
</span>
</body>
</html>

Hooking in our function view into the URL routing config.

#urls.py
url(r'^cookies/test$', views.cookies_test, name="cookies_test"),

To test run the development server and navigate to:

http://127.0.0.1:8000/sessionsandcookies/cookies/test

Setting

Setting cookie information is a little clunky in Django; we need to call set_cookie on the response object though we have to render our template to get hold of the response. We call render_to_response before calling set_cookies upon the returned response object.

#views.py
def cookies(request):
    hits = int(request.COOKIES.get('hits', '0'))
    first_hit = 'hits' not in request.COOKIES
    response = render_to_response('sessionsandcookies/cookies.html',
                                  {"first_hit": first_hit, "hits": hits}, RequestContext(request))

    response.set_cookie('hits', hits + 1, max_age=30)
    return response

A simple template view displays the number of hits which is passed into our template.

<!-- templates/sessionsandcookies/cookies.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Cookies</title>
</head>
<body>

<h1>Cookies</h1>

<span>
{% if first_hit %}
    Your first hit!
{% else %}
    Hits: {{ hits }}
{% endif %}
</span>

</body>
</html>

Hooking in our view function into our URL routing config.

#urls.py
url(r'^cookies/$', views.cookies, name="cookies"),

To test run the development server and navigate to:

http://127.0.0.1:8000/sessionsandcookies/cookies/

Expiring

We can expire our cookie by calling delete_cookie along with our cookie name.

#views.py
def cookies_expire(request):
    response =         render_to_response('sessionsandcookies/cookies_clear.html',
            {"first_hit": True, "hits": 0}, RequestContext(request))

    response.delete_cookie('hits')
    return response

A simple template which tells the user the cookies have been cleared along with a URL back to our page which shows the cookies data. We can then see the cookie hit count is reset to ‘first hit!’.

<! templates/sessionsandcookies/cookies_clear.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Cookie Clear</title>
</head>
<body>
<h1>Cookie Clear</h1>

<span>The cookie was cleared</span>

<span>
    <a href="{% url 'sessionsandcookies:cookies' %}">Back</a>
</span>
</body>
</html>

Hooking in our view function into our URLrouting config.

#urls.py
url(r'^cookies/expire', views.cookies_expire, name="cookies_expire"),

To test run the development server and navigate to:

http://127.0.0.1:8000/sessionsandcookies/cookies/expire/

References

Django: Errors and Logging

This post is part of my Django series. You can see an overview of the series along with instruction on how to get all the source code here.

This article assumes you are comfortable creating a Django project, can create apps and register them into the INSTALLED_APPS list of the settings file. If not please read my Django HelloWorld article.

This article assumes you have a project called DjangoSandBox in an application called errorsandlogging.

Errors

We can enforce HTTP GET/POST methods upon a view function with the @require_http_methods decorator. Access denied error methods are raised for other HTTP methods.

#views.py
from django.views.decorators.http import require_http_methods

@require_http_methods(["GET", "POST"])
def error_if_not_get_or_post(request):
    return HttpResponse("This can only be called with a get or a post")

If we only want HTTP POST calls to our view function we can use the @require_POST decorator; access denied errors are raised for other HTTP methods.

#views.py

from django.views.decorators.http import require_POST

@require_POST
def error_if_not_post(request):
    return HttpResponse("{0}, this can only be called with a post".format(request.POST.get("name", "")))

If we only want HTTP GET method calls to our view function we can use the @require_GET decorator; access denied errors are raised for other http methods.

#views.py

from django.views.decorators.http import require_GET

@require_GET
def error_if_not_get(request):
    return HttpResponse("This can only be called with a get")

We can return a HTTP 404 error (response not found) by raising a Http404 error. This will use the 404 html file placed within the site templates directory.

#views.py
from django.http import Http404

def error_as_404(request):
    raise Http404("There is nothing to see here")

We can also return a custom HTTP 404 error by returning an instance of HttpResponseNotFound. This is similar to raising a Http404 but allows us to use custom html rather than the stock site 404 page.

#views.py

from django.http import HttpResponseNotFound

def error_as_custom_404(request):
    return HttpResponseNotFound('<h1>There is nothing to see here</h1>')

We can return a HTTP 405 error (response not allowed) by returning an instance of HttpResponseNotAllowed.

#views.py
from django.http import HttpResponseNotAllowed

def not_allowed(request):
    return HttpResponseNotAllowed("<h1>You are not allowed to see this</h1>")

Logging

This assumes that you have a basic working knowledge of Python logging. If this is not the case then you can come up to speed with my post here.

The logging config is defined within the settings config of the Django project; any instances of the logger will then automatically pick these settings up.

Below we create two loggers as defined within the ‘handlers’ section.

  • A terminal logger which will log anything with a level of DEBUG or higher; it is called ‘console’.
  • A file logger which will log anything with a level of ERROR or higher; it is called ‘file’.

We create two formatters and assign them to a logger via the formatter variable.

  • A simple formatter which will prefix our log message with the time and the log level. It is called ‘simple’ and is assigned to the console logger.
  • A verbose formatter which will prefix our log message with the level, time, module, process id and thread id. It is called verbose and is assigned to the file logger.

The loggers or handlers are then assigned in the loggers section to our helloworld app.

#settings.py

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'verbose': {
            'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s'
        },
        'simple': {
            'format': '%(asctime)s %(levelname)s %(message)s'
        }
    },
    'handlers': {
        'console': {
            'level': 'DEBUG',
            'class': 'logging.StreamHandler',
            'formatter': 'simple'
        },
        'file': {
            'level': 'ERROR',
            'class': 'logging.FileHandler',
            'filename': 'debug.log',
            'formatter': 'verbose'
        },
    },
    'loggers': {
        'helloworld': {
            'handlers': ['console', 'file'],
            'level': 'WARNING',
            'propagate': True,
        },
    },
}

The following is a test view to check our custom Django log config is working. We throw a log message of each logging level at the logger as well as a trace stack and an exception.

#views.py

import logging
from django.http import HttpResponse

logger = logging.getLogger(__name__)

def with_logger(request):
    logger.debug('This is some debug')
    logger.info('This is information')
    logger.warning('This is a warning')
    logger.error('This is a error')
    logger.critical('This is a critical')

    logger.critical("Output with trace", exc_info=True)

    try:
        boom = 1 / 0
        logger.debug("Looks like we have broke the compiler as 1/0 = {0}".format(boom))
    except ZeroDivisionError as e:
        logger.exception(e)

    logger.critical('Final output to show we are logging still')

    return HttpResponse("Logged output")

The logging.getLogger function call passes in the name of the file; this ensures that we will only ever use one logger instance for any view function within this file; as long as they use the logger returned into the logger variable.

We can hook this view into our URL routing config.

#urls.py

from django.conf.urls import patterns, url

from . import views

urlpatterns = 
    patterns('',
             url(r'^logger/$', views.with_logger, name="logger")
             )

We hook into our errorsandlogging application into our project URL routing config:

# DjangoSandBox/urls.py
from django.conf.urls import patterns, include, url

from django.contrib import admin

from errorsandloggingimport urls as errorsandlogging_urls

urlpatterns = 
    patterns('',
url(r'^errorsandlogging/', include(errorsandlogging_urls, namespace="errorsandlogging")),
             )

To test run the development server and navigate to:

http://127.0.0.1:8000/errorsandlogging/logger/

References

Python: Parameter Scope

This is part of my Python & Django Series which can be found here including information on how to download all the source code.

Variables can be local or global.

Global Variables exist at the top level; i.e they are not inside a class or a method. They are implicitly global.

Local Variables are any variables inside a method. They will shadow a variable with the same name in an outer scope, i.e the calling method or class class etc. Changes to a local variable does not affect any variables which they shadow.

Global keyword is used to import a global variable. It allows a global variable to be used and set inside a method. It can only import global variables and not outer scope variables such as those within an outer method or class.

Nonlocal keyword is similar to global but will import any variables which are in an outer scope regardless if they are global or not global.

The following code and output can be used to demonstrate the usage of the above concepts.

Code:

def print_scope_msg(a_msg):
    print("Inside scope_test:", a_msg)

msg = "Initial"

def scope_test():

    def local_test():
        # msg is local. Changes are only existing inside the method
        msg = "local"
        print("Inside local_test():", msg)

    def nonlocal_test():
        # msg is non local. This allows the msg inside scope_test() to be imported and edited
        nonlocal msg
        msg = "nonlocal"
        print("Inside nonlocal_test:", msg)

    def global_test():
        # msg is global. This allows the msg inside global but not scope_test() to be imported and edited
        # Changes to msg affect the global msg but not the scope_test (it skips it)
        global msg
        msg = "global"
        print("Inside global_test: ", msg)

    msg = "scope"

    local_test()
    print_scope_msg(msg)

    nonlocal_test()
    print_scope_msg(msg)

    global_test()
    print_scope_msg(msg)

scope_test()
print("Inside global:", msg)

Output:

1: Inside local_test(): local
2: Inside scope_test: scope
3: Inside nonlocal_test: nonlocal
4: Inside scope_test: nonlocal
5: Inside global_test: global
6: Inside scope_test: nonlocal
7: Inside global: global

So what happened? Each numbered output above has an explanation as described below.

  1. Reports “local” as we have changed msg to local.
  2. Reports “scope” as the change inside local_test() was made to a shadowed version of msg which does not affect the value of msg inside scope_test().
  3. Reports “nonlocal” as we have changed msg to nonlocal.
  4. Reports “nonlocal” as inside nonlocal_test() we imported msg with the nonlocal keyword before changing the value.
  5. Reports “globaltest” as we have changed msg to globaltest.
  6. Reports “nonlocal” as the change to msg in global_test() used the global keyword which imported the global reference of msg and not that defined within scope_test() where we are now.
  7. Reports “globaltest” as the change to msg in global_test() used the global keyword.

Django Authorisation

Authorisation

This post is part of my Django series. You can see an overview of the series along with instruction on how to get all the source code here.

This article assumes you are comfortable creating a Django project, can create apps and register them into the INSTALLED_APPS list of the settings file. If not please read my Django HelloWorld article.

This article assumes you have a project called DjangoSandBox in an application called authmodel.

Prerequisites

Django provides authentication functionality through apps and middle ware.

Within the INSTALLED_APPS list of the settings.py add the following:

  • ‘django.contrib.auth’
  • ‘django.contrib.contenttypes’

Within the MIDDLEWARE_CLASSES list of the settings.py add the following:

  • SessionMiddleware
  • AuthenticationMiddleware
  • SessionAuthenticationMiddleware

If they were not added or you are starting a new project ensure to run migrate to create the required database schema and run any required configuration scripts:

python manage.py migrate

**Note: ** by default these apps and middle ware settings are already added into the settings.py

The User Model

By default the user class is django.contrib.auth.models.User and is found within the auth_user table. It contains many fields including the following; username, password, email, first_name, last_name

To create a user:

new_user = User.objects.create_user(username="user_login", email="foo@email.com", password="password", first_name="first_name", last_name="last_name")

By default the user is not created as active; it is presumed we would like some type of email confirmation from the user before activating the account. We can make the user active with the following code:

new_user.is_active = True
new_user.save()

We can reset their password with the set_password function. It is important that we use this method and not set the field directly to ensure their password is encrypted. Setting the field directly would mean their password would be stored in plain text which would not be secure.

new_user.set_password("new_pw")
new_user.save()

Logging in a user is a two step process. Initially we need to ensure that their username and password are valid, if this is the case then we log them in with the login function.

user = authenticate(username=username, password=password)

if user:
    login(request, user)

Django handles everything else; we simply need to tell Django which view functions require an authenticated user.

Django uses sessions to add the authentication into the request object.

The user template variable or the request.user property can be used to access the logged in user. If the user is not logged in then an instance of AnonymousUser is returned. The function user.is_authenticated() can be used to determine if the user is logged in:

if request.user.is_authenticated():
    # Authenticated 
else:
    # Anonymous 

Enforcing Authorisation

First lets make a view with some sensitive information. The following returns all user records:

#views.py

from django.contrib.auth.models import User
from django.shortcuts import render

def users(request):
    return render(request, 'authmodel/users.html', {"users": User.objects.all()})

To enforce a user to log in we wrap the view function with the login_required function in the URL routing config of our authmodel application:

# urls.py

from django.conf.urls import patterns, url
from django.contrib.auth.decorators import login_required

from . import views

url(r'^users/$', login_required(views.users), name="users"),

We need to hook in our authmodel application URL routing config into our project:

from django.conf.urls import patterns, include, url
from django.contrib import admin

from authmodel import urls as authmodel_urls

urlpatterns = 
    patterns('',
             url(r'^auth/', include(authmodel_urls, namespace="auth")),
             )

Now if a user tries to access http://127.0.0.1:8000/auth/users and they are not logged in they are redirected to the defined login URL which we set in the LOGIN_URL variable of our settings.py. We will fix this later on.

An alternative to using the login_require function in our URL routing config is to decorate our view function with the @login_required decorator:

#views.py

from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.shortcuts import render

from .forms import UserForm, UserLoginForm

@login_required
def users(request):
    return render(request, 'authmodel/users.html', {"users": User.objects.all()})

We can now remove the login_require function wrap within our url routing config:

#urls.py

from django.conf.urls import patterns, url

from . import views

urlpatterns = 
    patterns('',
             url(r'^users/$', views.users, name="users"),
             )

Here is our view which displays information about the users passed in to the context data as the template variable named users:

<!--templates/authmodel/users.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Registered Users</title>
</head>
<body>
<h2>Registered Users</h2>

<table>
    <tr>
        <th>First Name</th>
        <th>Last Name</th>
        <th>Email</th>
        <th>User Name</th>
    </tr>
    {% for user in users %}
        <tr>
            <td>{{ user.first_name }}</td>
            <td>{{ user.last_name }}</td>
            <td>{{ user.email }}</td>
            <td>{{ user.username }}</td>
        </tr>
    {% endfor %}
</table>

<span>
    <a href="{% url 'auth:index' %}">Back</a>
</span>
</body>
</html>

We now need to create pages for logging in and out as well as the ability to create a new login.

Create Login

We are going to create a login form which matches the data we want to collect from the user when they create a login.

We override the clean function of the form to validate that the password and the password_confirm are identical.

forms.py
from django.forms import Form, CharField, EmailField, PasswordInput, ValidationError

class UserForm(Form):
    first_name = CharField(max_length=30)
    last_name = CharField(max_length=30)
    login = CharField()
    email = EmailField()
    password = CharField(widget=PasswordInput)
    password_confirm = CharField(widget=PasswordInput)

    def clean(self):
        super(UserForm, self).clean()

        password = self.cleaned_data.get('password')
        password_confirm = self.cleaned_data.get('password_confirm')

        if password and password_confirm and password != password_confirm:
            raise ValidationError("Passwords do not match")

        return self.cleaned_data

We now hook this form into a function view; here we create a new user after the form has been posted and validates correctly.

Once we create the user we activate them. In a live system we might want to think about sending and receiving a confirmation email before we activate the user.

#views.py
def create_login(request):
    form = UserForm(request.POST or None)

    if request.POST and form.is_valid():
        email = form.cleaned_data['email']
        user_login = form.cleaned_data['login']
        password = form.cleaned_data['password']
        last_name = form.cleaned_data['last_name']
        first_name = form.cleaned_data['first_name']

        new_user = User.objects.create_user(username=user_login, email=email, password=password, first_name=first_name, last_name=last_name)

        new_user.is_active = True
        new_user.save()   

        return redirect(reverse('auth:index'))

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

A simple template which renders the required inputs via the form we have created above.

<!-- templates/authmodel/create_login.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Create Login</title>
</head>
<body>
<h1>Create Login</h1>

<form method="post" action="">
    {% csrf_token %}
    <table>{{ form.as_table }}</table>
    <p><input type="submit" value="Create"/></p>
</form>

<span>
    <a href="{% url 'auth:index' %}">Back</a>
</span>
</body>
</html>

Adding our new function into the url routing config:

#urls.py

from django.conf.urls import patterns, url

from . import views

urlpatterns = 
    patterns('',
             url(r'^login/create/$', views.create_login, name="login_create")
             )

To test run the development server and navigate to:

http://127.0.0.1:8000/auth/login/create/

Login

Our login page simply requires a text box for the username and password along with the ability to post this data back to the server.

First we create a form to represent the data required; username and password:

#forms.py
from django.forms import Form, CharField, EmailField, PasswordInput, ValidationError

class UserLoginForm(Form):
    login = CharField()
    password = CharField(widget=PasswordInput)

We then create a view function around the form we have just created.

If Django redirected the user to this page upon accessing a page which required authentication; the original URL will be available via the NEXT get query parameter. We will redirect the user back to this page if applicable.

#views.py

from django.contrib.auth import authenticate, login
from django.shortcuts import render, redirect

from .forms import UserLoginForm

def login_page(request):
    form = UserLoginForm(request.POST or None)

    if request.POST and form.is_valid():
        username = form.cleaned_data["login"]
        password = form.cleaned_data["password"]

        user = authenticate(username=username, password=password)

        if user:
            login(request, user)

            if request.GET.get('next') is not None:
                return redirect(request.GET['next'])
            else:
                return redirect(reverse('auth:index'))

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

Now we create the template which renders the user login form.

When a user is logged into any page we have access to their user details via the template variable called user. We use the user.is_authenticated property to determine if someone is logged in already.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Login</title>
    <style>
        span {
            display: block;
        }
    </style>
</head>
<body>

<h1>Login</h1>

{% if user.is_authenticated %}
    <span>{{ user.first_name }} {{ user.last_name }} you are already logged in!</span>
{% else %}
    <form method="post" action="">
        {% csrf_token %}
        <table>{{ form.as_table }}</table>
        <input type="submit" class="button" value="Login"/>
    </form>
{% endif %}

<span>
    <a href="{% url 'auth:index' %}">Back</a>
</span>
</body>
</html>

Adding in our new function into our URL routing config:

#urls.py

from django.conf.urls import patterns, url

from . import views

urlpatterns = 
    patterns('',
             url(r'^login/$', views.login_page, name="login"),
             )

Add our login URL into our settings file. Any attempt to access a page which requires authorisation will redirect here automatically if a user is not logged in.

#settings.py
LOGIN_URL = '/auth/login/'

To test run the development server and navigate to:

http://127.0.0.1:8000/auth/login/

Logout

We can log a user out by passing the request object to the logout function. We do this inside a view function:

#views.py

from django.contrib.auth import logout
from django.shortcuts import render


def logout_page(request):
    logout(request)
    return render(request, 'authmodel/logout.html')

A simple page which confirms to the user that they are now logged out:

<!--templates/authmodel/logout.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Logout</title>
    <style>
        span {
            display: block;
        }
    </style>

</head>
<body>
<h1>Logout</h1>

<span>You are now logged out</span>

<span>
    <a href="{% url 'auth:index' %}">Back</a>
</span>
</body>
</html>

Hooking in our logout page into our URL routing config:

#urls.py

from django.conf.urls import patterns, url

from . import views

urlpatterns = 
    patterns('',
             url(r'^logout/$', views.logout_page, name="logout"),
             )

To test run the development server and navigate to:

http://127.0.0.1:8000/auth/login/logout/

Extending User

If the default User profile is not suitable for your purposes there are a couple options.

Firstly you can create your own table with the additional fields and create a join to the existing user table, accessing it via ettings.AUTH_USER_MODEL:

# models.py
user = models.OneToOneField(settings.AUTH_USER_MODEL)

Alternatively you can subclass either AbstractUser or BaseUserManager. If you go down this route you will need to register the new table as replacing the default Djang Auth.User model table:

#settings.py
AUTH_USER_MODEL = "authapp.NewAuthUser"

References

https://docs.djangoproject.com/en/1.8/topics/auth/
https://docs.djangoproject.com/en/1.8/topics/auth/default/
https://docs.djangoproject.com/en/1.8/topics/auth/default/#permissions-and-authorization

Django Forms

This post is part of my Django series. You can see an overview of the series along with instruction on how to get all the source code here.

This article assumes you are comfortable creating a Django project, can create apps and register them into the INSTALLED_APPS list of the settings file. If not please read my Django HelloWorld article.

This article assumes you have a project called DjangoSandBox in an application called formsintroduction.

A Django Form instance automatically provides all the heavy work when working with HTTP forms; rendering UI, validation, cleaning and collection of data.

Basic Form

A basic Django Form is a class which inherits from forms.Form. It defines fields and looks similar to a Django model.

The following example defines a form which has a name and height field.

# forms.py
from django import forms

class BasicFormExample(forms.Form):
    name = forms.CharField(label="Name", min_length=3, max_length=30)

    height = forms.DecimalField(max_value=2, help_text="Height in meters", label="Height (M.CM)", decimal_places=2)  

We can now use an instance of BasicFormExample within a function view; we pass it to the context object dictionary with a key of ‘form’.

When posting back we create an instance of the form initiated with the POST data. We can then ask the form if it is valid; this returns false if the model or additional form validation (which we will add later) is invalid.

We can ask for cleaned_data of the fields from the form; this gets us validated data fields. In this example we simply collect the data and pass them into a HttpResponse instance.

If the form is not considered valid we pass the form instance back to the request object.

#views/py

from django.http import HttpResponse
from django.shortcuts import render

from .forms.bastic_form_example import BasicFormExample

def basic(request):
    if request.POST:
        form = BasicFormExample(request.POST)
        if form.is_valid():
            name = form.cleaned_data['name']
            height = form.cleaned_data['height']

            return HttpResponse("{0} is {1} tall".format(name, height))
        else:
            return render(request, "formsintroduction/basic_form_example.html", {'form': form})
    else:
        form = BasicFormExample()
        return render(request, 'formsintroduction/basic_form_example.html', {'form': form})

For our template we simply need to ask for the form to render itself. We use as_table function upon the form instance passed into the template context data.

Django provides the functions as_table, as_ul and as_p; this simply defines which html element to surrounds the rendered input element; TD, UI or P.

We also need to add a csrf_token to ensure we are protected from Cross-site request forgery attacks. When working with forms ‘{% csrf_token %}’ should always be included on the page; as long as we include this Django will take care of the rest.

We also add a button to submit the form.

<!-- templates/formsintroduction/basic_form_example.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
<form method="post" action="">
    {% csrf_token %}
    <table>
        {{ form.as_table }}
    </table>
    <p><input type="submit" value="Create"/></p>
</form>
</body>
</html>

We now hook in our view function into our URL routing config.

#formsintroduction/urls.py
from django.conf.urls import patterns, url

from . import views

urlpatterns = 
    patterns('',
url(r'^basic/$', views.basic, name="form_basic"),

We need to make sure that our app’s urls.py is configured within the project urls.py. I was working with an app called formsintroduction inside a project called DjangoSandBox.

#DjangoSandBox/urls.py
from django.conf.urls import patterns, include, url

from formsintroduction import urls as formsintroduction_urls

urlpatterns = 
    patterns('',
              url(r'^formsintroduction/', include(formsintroduction_urls, namespace="formsintroduction")),
             )

To test the page run the development server and navigate to:

http://127.0.0.1:8000/formsintroduction/basic/

Validation

This assumes that you have an understanding of adding validation onto Django models. If not you can read about this in my post here.

We automatically get server and client side validation for our model. For the example above this would ensure the name is of a minimum 3 characters and a maximum of 30. For the height it ensures a maximum value of 2 and a maximum number of two decimal places.

In short the constraints and validation that we place upon our model are automatically validated against in our form.

We can add additional validation onto our form.

Note: Where possible validation should be placed upon the model to support code reuse. Only bespoke validation for the page should be added directly onto the form.

Lets first write a custom validation function for our name field. We want to ensure that it starts with an upper case letter and then has at least 2 lower case letters. Upon failure we raise a django.core.exceptions.ValidationError.

# validators.py 
from re import match

from django.core.exceptions import ValidationError

def validate_name(a_string):
    a_match = match(r"[A-Z][a-z]{2,}$", a_string)
    if a_match:
        return True

    raise ValidationError("Name must be a capitalised single word")

We now change our form to look like the following:

# forms/basic_form_example.py
from django import forms
from ..validators import validate_name

error_name = {
    'required': 'You must enter a name!',
    'invalid': 'Invalid name format.'
}


class BasicFormExample(forms.Form):
    name = forms.CharField(label="Name", min_length=3, max_length=30, error_messages=error_name,
                           initial="Jim", validators=[validate_name])

    height = forms.DecimalField(max_value=2, help_text="Height in meters", label="Height (M.CM)", decimal_places=2)

    def clean(self):
        cleaned_data = super(BasicFormExample, self).clean()

        name = cleaned_data.get("name")
        height = cleaned_data.get("height")

        if name is None and height is None:
            msg = "Both name and height are required!!!"
            self.add_error('name', msg)
            self.add_error('height', msg)
            raise forms.ValidationError(msg)

        return self.cleaned_data

    def clean_name(self):
        name = self.cleaned_data.get("name")

        if " " in name:
            raise forms.ValidationError("No spaces in the name please")

        return name

In the example above:

  • Field.validators takes a list of custom validators or inbuilt Django validators. These have been covered in my post about Django Model Validation.
  • Field.error_messages takes a dictionary of error key to error message. We use it to provide custom error messages to the existing validation. This is the same funcitonality as per models. This has been covered in my post about Django Model Validation.
  • The clean_field provides a hook to place specific field validation. The above is a bad example as it could easily be added as a validator against the model or form field using regular expressions; however this is only an example.
    • Raising a ValidationError here automatically associates the error to the field
  • The clean function provides an additional hook to perform cross field validation or anything else which might not be applicable or possible anywhere else.
    • We can add validation messages against a field with the add_error function.
    • We can raise a general validation message against the form by raising a ValidationError.

Note: Validation error messages are associated to a field or their form. the association affects where they are displayed to the user. Field errors are displayed next to the field they arose from, form errors are displayed above all fields. These have been covered in my post about Django Model Validation.

To test the page run the development server and navigate to:

http://127.0.0.1:8000/formsintroduction/basic/

Basic Form With HTML

In the above example we use the form passed into the template context data to render itself. If we need more control over the placement or composition of the html there is nothing stopping us from writing the html ourselves.

This example renders the form using html and Django template operators.

<!-- templates/formsintroduction/basic_form_with_html_example.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
<form method="post" action="">
    {% csrf_token %}
    {{ form.non_field_errors }}
    <div class="fieldWrapper">
        {{ form.message.errors }}
        <label for="{{ form.name.id_for_label }}">Name:</label>
        {{ form.name }}
    </div>
    <div class="fieldWrapper">
        {{ form.height.errors }}
        {{ form.height.label_tag }}
        {{ form.height }}
    </div>
    <p><input type="submit" value="Create"/></p>
</form>
</body>
</html>

We have access to some properties on the form which come in handy when building a working form page:

  • The form.name and form.height renders the applicable input for the fields name and height respectively;. This includes any client side validation.
  • The form.height.errors and form.name.errors renders the fields errors when and if applicable.
  • The form.name.id_for_label reutrns the id to be used for the name field label.
  • The form.height.label_tag is used to render the label for the height column instead of constructing it ourselves with the id_for_label property.
  • The form.non_field_errors returns true or false depending upon if we have any validation errors which are not related directly to a field.
  • The form.message.errors renders all non field validation error messages.

We can now hook in our new template into what is a virtual copy of the view function called in the example above.

#views.py

from django.http import HttpResponse
from django.shortcuts import render

from .forms.class_form_example import ClassBasedForm
def basic_html(request):
    if request.POST:
        form = BasicFormExample(request.POST)
        if form.is_valid():
            name = form.cleaned_data['name']
            height = form.cleaned_data['height']

            return HttpResponse("{0} is {1} tall".format(name, height))
        else:
            return render(request, "formsintroduction/basic_form_with_html_example.html", {'form': form})
    else:
        form = BasicFormExample()
        return render(request, 'formsintroduction/basic_form_with_html_example.html', {'form': form})

Hook in our new view into our URL route config:

url(r'^basichtml/$', views.basic_html, name="form_basic_html"),

To test the page run the development server and navigate to:

http://127.0.0.1:8000/formsintroduction/basichtml/

Class Form

If we have a model class we can automatically generate a form from our model definition.

First our models; we are going to resuse the PhoneAddress and PhoneContact from the previous article when we looked at views. They sit in the viewsintroduction application.

# viewsintroduction/models.py
class PhoneAddress(Model):
    number = models.IntegerField()
    street_name = models.CharField(max_length=20)
    city = models.CharField(max_length=20)

    def __str__(self):
        return "{0} {1} {2}".format(self.number, self.street_name, self.city)

    def get_absolute_url(self):
        return reverse('viewsintroduction:address', args=[self.id])

We can create a form based upon our model by simply inheriting from ModleForm and setting the model property of the Meta internal class.

We also define the fields property which defines which fields of the model are going to be displayed; this is mandatory.

#forms/clased_based_form.py

.from django import forms
from django.forms import ModelForm

from viewsintroduction.models import PhoneAddress

class ClassBasedForm(ModelForm):        
    class Meta:
        model = PhoneAddress
        fields = ("city", "street_name", "number")
        labels = {'number': "House No."}
        help_texts = {'number': "This is the number of the house."}

With regards to validation:

  • We can implement the clean and clean_field functions
  • We can add custom validation messages via the error_messages property of the class meta. This takes a dictionary keyed upon each field name, which in itself takes a dictionary keyed upon each error type and the custom error message.
  • We can override the init function and set any settings as required; for example adding a custom validator onto a field.

A fuller example adding in validation and more meta information:

#forms/clased_based_form.py

from django import forms
from django.forms import ModelForm

from viewsintroduction.models import PhoneAddress
from ..validators import validate_name

class ClassBasedForm(ModelForm):
    def __init__(self, *args, **kwargs):
        super(ClassBasedForm, self).__init__(*args, **kwargs)
        self.fields["city"].validators.append(validate_name)

    def clean(self):
        cleaned_data = super(ClassBasedForm, self).clean()

        city = cleaned_data.get("city")
        street_name = cleaned_data.get("street_name")
        number = cleaned_data.get("number")

        if city is None and street_name is None and number is None:
            msg = "None of the fields have been set!!!"
            for a_field in ('city', 'street_name', 'number'):
                self.add_error(a_field, msg)
            raise forms.ValidationError(msg)

        return self.cleaned_data

    def clean_city(self):
        city = self.cleaned_data.get("city")

        if " " in city:
            raise forms.ValidationError("No spaces in the city name please")

        return city

    class Meta:
        model = PhoneAddress
        fields = ("city", "street_name", "number")
        labels = {'number': "House No."}
        help_texts = {'number': "This is the number of the house."}
        error_messages = {
            'number': {
                'required': "We need a number of the house!",
                'max_length': "We only accept houses up to 20!"}
        }

Our view function looks similar to previous examples with a few changes:

  • Our function view is going to be passed in a parameter called pk which defaults to none. We will strip this from the URL within our URL routing config shortly.
  • We use get_object_or_404 to return the record. It is passed the primary key value and the model class. It will return the record or raise a 404 HTTP response if no record exists.
  • We need to initiate the form with the post data if we are posting back otherwise the record instance. This is done with request.POST or None and instance=an_address being passed into the form constructor. If both are not set then we initiate the form from no data; i.e. the starting point.
  • Calling form.save will automatically create or update the address record.
  • Our address record knows its own URL via the get_absolute_url function. We can simply call redirect upon the record to redirect to the detail view we made in the previous article.
# views.py
from django.http import HttpResponse
from django.shortcuts import render, redirect, get_object_or_404

from .forms.class_form_example import ClassBasedForm
from viewsintroduction.models import PhoneAddress

def class_form(request, pk=None):
    if pk:
        an_address = get_object_or_404(PhoneAddress, pk=pk)
        form = ClassBasedForm(request.POST or None, instance=an_address)
    else:
        form = ClassBasedForm(request.POST or None, initial={'city': 'Plymouth'})

    if request.POST and form.is_valid():
        an_address = form.save(commit=True)
        return redirect(an_address)
    else:
        return render(request, 'formsintroduction/class_based_form_example.html', {'form': form})

In our template we will simply get our form to render itself:

<!-- templates/formsintroduction/class_based_form_example.html>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
<form method="post" action="">
    {% csrf_token %}
    <table>
        {{ form.as_table }}
    </table>
    <p><input type="submit" value="Create"/></p>
</form>
</body>
</html>

We now hook in our view function with two a create and edit URL:

#urls.py

url(r'^class/create/$', views.class_form, name="form_class_create"),
url(r'^class/(?P<pk>[0-9]+)/edit/$', views.class_form, name="form_class_edit"),

With regards to the edit URL matching. We match the URL class/edit/xxx/ where xxx is the primary key. The primary key is made up of one or more numerical digits as noted by [0-9]+. We assign the integral into a variable called pk. The string ?P reads make a parameter for reference later on.

To test the page run the development server and navigate to:

http://127.0.0.1:8000/formsintroduction/class/create
http://127.0.0.1:8000/formsintroduction/class/1/edit

Note: replace 1 with the id of your record

Model Form Factory

If we don’t want to customise the class based form we can use the modelform_factory class.

#views.py

from django.forms import modelform_factory
from django.shortcuts import render, redirect, get_object_or_404

from viewsintroduction.models import PhoneAddress
#views.py

def model_form_factory_form(request, pk=None):
    phone_address_form = modelform_factory(PhoneAddress, fields=("city", "street_name", "number"))

    if pk:
        an_address = get_object_or_404(PhoneAddress, pk=pk)
        form = phone_address_form(request.POST or None, instance=an_address)
    else:
        form = phone_address_form(request.POST or None)

    if request.POST and form.is_valid():
        an_address = form.save(commit=True)
        return redirect(an_address)
    else:
        return render(request, 'formsintroduction/form_factory_example.html', {'form': form})

A simple template which asks the form to render itself.

<!-- templates/formsintroduction/form_factory_example.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Form Factory Example</title>
</head>
<body>
<form method="post" action="">
    {% csrf_token %}
    <table>
        {{ form.as_table }}
    </table>
    <p><input type="submit" value="Create"/></p>
</form>
</body>
</html>

Again we set us two URLs; one for creating and one for editing.

#urls.py
url(r'^factory/create/$', views.model_form_factory_form, name="form_factory_create"),
url(r'^factory/(?P<pk>[0-9]+)/edit/$', views.model_form_factory_form, name="form_factory_edit"),

To test the page run the development server and navigate to:

http://127.0.0.1:8000/formsintroduction/factory/create
http://127.0.0.1:8000/formsintroduction/factory/1/edit

Note: replace 1 with the id of your record

Widgets

Widgets provide extra customisation onto our fields; for example we can add a PasswordInput widget onto a CharField to give us a password input box which displays * instead of the password.

PasswordInput = forms.CharField(max_length=10, widget=forms.PasswordInput)

We can can add a Textarea onto a CharField to allow multi-line text input.

TextField = forms.CharField(widget=forms.Textarea)

We can render an input as hidden:

HiddenInput = forms.CharField(max_length=10, widget=forms.HiddenInput, initial='a')

The normal choice field be be changed to radios or a multi select list box:

TITLE_CHOICES = (
    ('MR', 'Mr.'),
    ('MRS', 'Mrs.'),
    ('MS', 'Ms.'),
)

# Drop Down
ChoiceField = forms.CharField(max_length=3, widget=forms.Select(choices=TITLE_CHOICES))

# Radio
RadioSelect = forms.CharField(max_length=10, widget=forms.RadioSelect(choices=TITLE_CHOICES))

# Multiple display and select
CheckboxSelectMultiple = forms.CharField(max_length=10, widget=forms.CheckboxSelectMultiple(choices=TITLE_CHOICES))

We can create a date selection widget limited to certain years or months:

YEARS_ = years = (2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009)

SelectDateWidget = forms.CharField(max_length=10, widget=SelectDateWidget(years=YEARS_))

A lot of the widgets can take parameters for extra customisation. Check the Django documentation for available widgets and their options.

We provide a working example for all of these widgets in the “A Full Example” section below.

Joins

Joins fields for 1:1 and 1:n are implemented by a ModelChoiceField form field defining the possible records to be joined to with the queryset parameter.

ForeignKey = forms.ModelChoiceField(queryset=ChildOfMany.objects.all())

OneToOneField = forms.ModelChoiceField(queryset=ChildOfOne.objects.all())

Many to many join fields are implemented in the same way but with a ModelMultipleChoiceField form field.

ManyToManyField = forms.ModelMultipleChoiceField(queryset=ChildManyToMany.objects.all())

We provide a working example for all of these in the “A Full Example” section below.

A Full Example

For every model field there is a recommended form field and as such editor and potentially a widget. The following is an example with “one of virtually everything”.

Lets take a model which has a field of virtually every type including joins of all types; 1:1, 1:n and 1:m.

#models.py
from django.db import models
from django.db.models import Model

TITLE_CHOICES = (
    ('MR', 'Mr.'),
    ('MRS', 'Mrs.'),
    ('MS', 'Ms.'),
)


class ChildOfMany(Model):
    CharField = models.CharField(max_length=10)

    def __str__(self):
        return self.CharField


class ChildOfOne(Model):
    CharField = models.CharField(max_length=10)

    def __str__(self):
        return self.CharField


class ChildManyToMany(Model):
    CharField = models.CharField(max_length=10)

    def __str__(self):
        return self.CharField

    class Meta:
        verbose_name_plural = "children"


class ModelFieldsToFormFields(Model):
    ChoiceField = models.CharField(max_length=3, choices=TITLE_CHOICES)
    CharField = models.CharField(max_length=10)
    CommaSeparatedIntegerField = models.CharField(max_length=50)
    EmailField = models.EmailField()
    TextField = models.TextField()
    URLField = models.URLField()

    DateField = models.DateField()
    DateTimeField = models.DateTimeField()
    TimeField = models.TimeField()

    # FileField = models.FileField()
    # ImageField = models.ImageField()
    # FilePathField = models.FilePathField()

    BigIntegerField = models.BigIntegerField()
    BooleanField = models.BooleanField()
    NullBooleanField = models.NullBooleanField()

    PositiveIntegerField = models.PositiveIntegerField()
    PositiveSmallIntegerField = models.PositiveSmallIntegerField()
    SlugField = models.SlugField()
    SmallIntegerField = models.SmallIntegerField()
    DecimalField = models.DecimalField(decimal_places=2, max_digits=5)
    FloatField = models.FloatField()
    IntegerField = models.IntegerField()
    GenericIPAddressField = models.GenericIPAddressField()

    # Joins
    ForeignKey = models.ForeignKey(ChildOfMany)
    ManyToManyField = models.ManyToManyField(ChildManyToMany)
    OneToOneField = models.OneToOneField(ChildOfOne)

Our form, using Django advised mappings of form field and widgets to model fields will then look like this:

# forms/complex_form_with_widgets.py

from django import forms
from django.forms.extras.widgets import SelectDateWidget

from ..models import TITLE_CHOICES, ChildOfOne, ChildManyToMany, ChildOfMany, ModelFieldsToFormFields

YEARS_ = years = (2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009)


class ComplexFormWithWidgets(forms.ModelForm):
    ChoiceField = forms.CharField(max_length=3, widget=forms.Select(choices=TITLE_CHOICES))
    CharField = forms.CharField(max_length=10)
    CommaSeparatedIntegerField = forms.CharField()
    EmailField = forms.EmailField()
    TextField = forms.CharField(widget=forms.Textarea)
    URLField = forms.URLField()

    DateField = forms.DateField()
    DateTimeField = forms.DateTimeField()
    TimeField = forms.TimeField()

    # FileField = models.FileField()
    # ImageField = models.ImageField()
    # FilePathField = forms.FilePathField((match="*.py", recursive=True)

    BigIntegerField = forms.IntegerField(min_value=-9223372036854775808, max_value=9223372036854775807.)
    BooleanField = forms.BooleanField()
    NullBooleanField = forms.NullBooleanField()

    PositiveIntegerField = forms.IntegerField(min_value=0, max_value=2147483647)
    PositiveSmallIntegerField = forms.IntegerField(min_value=0, max_value=32767)
    SlugField = forms.SlugField()
    SmallIntegerField = forms.IntegerField(min_value=-32768, max_value=32767)
    DecimalField = forms.DecimalField()
    FloatField = forms.FloatField()
    IntegerField = forms.IntegerField()
    GenericIPAddressField = forms.GenericIPAddressField()

    # Joins
    ForeignKey = forms.ModelChoiceField(queryset=ChildOfMany.objects.all())
    ManyToManyField = forms.ModelMultipleChoiceField(queryset=ChildManyToMany.objects.all())
    OneToOneField = forms.ModelChoiceField(queryset=ChildOfOne.objects.all())

    # More Widgets
    PasswordInput = forms.CharField(max_length=10, widget=forms.PasswordInput)
    HiddenInput = forms.CharField(max_length=10, widget=forms.HiddenInput, initial='a')
    RadioSelect = forms.CharField(max_length=10, widget=forms.RadioSelect(choices=TITLE_CHOICES))
    CheckboxSelectMultiple = forms.CharField(max_length=10, widget=forms.CheckboxSelectMultiple(choices=TITLE_CHOICES))
    SelectDateWidget = forms.CharField(max_length=10, widget=SelectDateWidget(years=YEARS_))

    class Meta:
        model = ModelFieldsToFormFields
        exclude = ()  # Better to set fields explicitly

There is no real change in our view function from the model form example previously.

#views.py

from django.http import HttpResponse
from django.shortcuts import render, redirect, get_object_or_404

from .forms.complex_form_with_widgets import ComplexFormWithWidgets
from .models import ModelFieldsToFormFields
def complete_model_example(request, pk=None):
    if pk:
        an_instance = get_object_or_404(ModelFieldsToFormFields, pk=pk)
        form = ComplexFormWithWidgets(request.POST or None, instance=an_instance)
    else:
        form = ComplexFormWithWidgets(request.POST or None)

    if request.POST and form.is_valid():
        if form.is_valid():
            an_instance = form.save(commit=True)
            return HttpResponse("Created with id={0}".format(an_instance.id))
    else:
        return render(request, 'formsintroduction/complete_model_form_example.html', {'form': form})

A simple html template asking the form to render itself:

<!-- templates/formsintroduction/complete_model_form_example.html-->

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
<form method="post" action="">
    {% csrf_token %}
    <table>
        {{ form.as_table }}
    </table>
    <p><input type="submit" value="Create"/></p>
</form>
</body>
</html>

Hook in our view to a create and edit URL within our URL routing config:

# urls.py
url(r'^complete_model/create/$', views.complete_model_example, name="form_complete_create"),
url(r'^complete_model/(?P<pk>[0-9]+)/edit/$', views.complete_model_example, name="form_complete_edit")

To test the page run the development server and navigate to:

http://127.0.0.1:8000/formsintroduction/complete_model/create
http://127.0.0.1:8000/formsintroduction/complete_model/1/edit

Note: replace 1 with the id of your record

Additional Notes

We can pass data to the HTML via the attrs parameter which takes a dictionary of property to name. The following makes our char field take on a CSS class name.

CharField = forms.CharField(max_length=10, attrs={'class': 'special'})

References