Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

636 lines (412 sloc) 23.12 kB

Welcome to part 4 of the tutorial! In this part at how we can let users vote on our poll, in other words, web forms!. Hooray.

Tutorial 4: Using a form

Here's the outline of what we're going to do in this tutorial:

  • extend the FT to show Herbert voting on the poll
  • create a url, view and template to generate pages for individual polls
  • create a Django form to handle choices

Extending the FT to vote using radio buttons

Let's start by extending our FT, to show Herbert voting on a poll. In fts/tests.py:

If you run them, you'll find that they are still telling us the individual poll page isn't working:

NoSuchElementException: Message: u'Unable to locate element: {"method":"tag name","selector":"h1"}'

That because, currently, our poll view is just a placeholder function. We need to make into into a real Django view, which returns information about a poll.

Let's work on the unit tests for the poll view then. Make a new class for them in polls/tests.py:

Running the tests gives:

TypeError: poll() takes no arguments (2 given)

(I'm going to be shortening the test outputs from now on. You're a grown-up now, you can handle it! :-)

Let's make our view take two arguments:

Now we get:

ValueError: The view mysite.polls.views.poll didn't return an HttpResponse object.

Again, a minimal fix:

Now we get this error:

AssertionError: No templates used to render the response

Let's try fixing that - but deliberately using the wrong template (just to check we are testing it)

Good, looks like we are testiing it properly:

AssertionError: Template 'poll.html' was not a template used to render the response. Actual template(s) used: home.html

And changing it to poll.html gives us:

TemplateDoesNotExist: poll.html

Fine and dandy, let's make one:

touch polls/templates/poll.html

You might argue that an empty file, all 0 bytes of it, is a fairly minimal template! Still, it seems to satisfy the tests. Now they want us to pass a poll variable in the template's context:

KeyError: 'poll'

So let's do that, again, the minimum possible change to satisfy the tests:

And the tests get a little further on:

AssertionError: None != <Poll: life, the universe and everything>

And they even tell us what to do next - pass in the right Poll object:

This is the first time we've used the Django API to fetch a single database object, and objects.get is the helper function for this - it raises an error if it can't find the object, or if it finds more than one. The special keyword argument pk stands for primary key. In this case, Django is using the default for primary keys, which is an automatically generated integer id column.

That raises the question of what to do if a user types in a url for a poll that doesn't exist - /poll/0/ for example. We'll come back to this in a later tutorial.

In the meantime, what do the tests say:

self.assertIn(poll2.question, response.content)
AssertionError: 'life, the universe and everything' not found in ''

We need to get our template to include the poll's question. Let's make it into a page heading:

Now the tests want our 'no polls yet' message:

AssertionError: 'No-one has voted on this poll yet' not found in '<html>\n  <body>\n    <h2>life, the universe and everything</h2>\n  </body>\n</html>\n'

So let's include that:

And that's enough to make the unit tests happy:

----------------------------------------------------------------------
Ran 7 tests in 0.013s

OK

Mmmh, OK. And doughnuts. Let's see what the FTs think?:

NoSuchElementException: Message: u'Unable to locate element: {"method":"tag name","selector":"h1"}'

Ah, we forgot to include a general heading for the page - the FT is checking the h1 and h2 headings:

So, in our template, let's add an h1 with "Poll Results" in it:

Using a Django form for poll choices

Now what does the FT say?:

======================================================================
FAIL: test_voting_on_a_new_poll (tests.TestPolls)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/harry/workspace/mysite/fts/tests.py", line 100, in test_voting_on_a_new_poll
    self.assertEquals(len(choice_inputs), 3)
AssertionError: 0 != 3

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

Ah, we need to add the poll Choices as a series of radio inputs. Now the official Django tutorial shows you how to hard-code them in HTML:

https://docs.djangoproject.com/en/1.4/intro/tutorial04/

But Django can do even better than that - Django's forms system will generate radio buttons for us, if we can just give it the right incantations. Let's create a new test in tests.py:

You might prefer to put the import at the top of the file.

Looking through the code, you can see we instantiate a form, passing it a poll object. We then examine the form's fields attribute, find the one called vote (this will also be the name of the HTML input element), and we check the choices for that field.

For the test to even get off the ground, we may as well create something minimal for it to import! Create a file called polls/forms.py.

And let's start another test/code cycle, woo -:

python manage.py test polls

[...]
    form = PollVoteForm(poll=poll)
TypeError: object.__new__() takes no parameters

We override __init__.py to change the constructor:

...

self.assertEquals(form.fields.keys(), ['vote'])
AttributeError: 'PollVoteForm' object has no attribute 'fields'

To give the form a 'fields' attribute, we can make it inherit from a real Django form class, and call its parent constructor:

Now we get:

AssertionError: Lists differ: [] != ['vote']

Django form fields are defined a bit like model fields - using inline class attributes. There are various types of fields, in this case we want one that has choices - a ChoiceField. You can find out more about form fields here:

https://docs.djangoproject.com/en/1.4/ref/forms/fields/

Now we get:

AssertionError: Lists differ: [] != [(1, '42'), (2, 'The Ultimate ...

So now let's set the choices from the poll we passed into the constructor (you can read up on choices in Django here https://docs.djangoproject.com/en/1.4/ref/models/fields/#field-choices)

Mmmmmh, list comprehensions... That will now get the test almost to the end - we can instantiate a form using a poll object, and the form will automatically generate the choices based on the poll's choice_set.all() function, which gets related objects.

The final test is to make sure we have radio boxes as the HTML input type. We're using as_p(), a method provided on all Django forms which renders the form to HTML for us - we can see exactly what the HTML looks like in the next test output:

self.assertIn('input type="radio"', form.as_p())
AssertionError: 'input type="radio"' not found in u'<p><label for="id_vote">Vote:</label> <select name="vote" id="id_vote">\n<option value="1">42</option>\n<option value="2">The Ultimate Answer</option>\n</select></p>'

Django has defaulted to using a select/option input form. We can change this using a widget, in this case a RadioSelect

OK so far? Django forms have fields, some of which may have choices, and we can choose how the field will be displayed on page using a widget. Right.

And that should get the tests passing! If you're curious to see what the form HTML actually looks like, why not temporarily put a print form.as_p() at the end of the test? Print statements in tests can be very useful for exploratory programming... You could try form.as_table() too if you like...

Right, where where we? Let's do a quick check of the functional tests.

(incidentally, are you rather bored of watching the FT run through the admin test each time? If so, you can temporarily disable it by renaming its test method from test_can_create_new_poll_via_admin_site to DONTtest_can_create_new_poll_via_admin_site that's called "Dontifying"... remember to change it back before the end though!)

python manage.py test fts [...] AssertionError: 0 != 3

Ah yes, we still haven't actually used the form yet! Let's go back to our TestSinglePollView, and a new test that checks we use our form)

Now the unit tests give us:

python manage.py test polls
[...]
KeyError: 'form'

So back in views.py:

Now:

self.assertTrue(isinstance(response.context['form'], PollVoteForm))
AssertionError: False is not true

So:

And:

self.assertIn(choice3.choice, response.content)
AssertionError: 'PM' not found in '<html>\n  <body>\n    <h1>Poll Results</h1>\n\n    <h2>6 times 7</h2>\n    <p>No-one has voted on this poll yet</p>\n  </body>\n</html>\n\n'

So, in polls/templates/poll.html:

And re-running the tests - oh, a surprise!:

self.assertIn(choice4.choice, response.content)
AssertionError: "Gardener's" not found in '<html>\n  <body>\n    <h1>Poll Results</h1>\n    \n    <h2>time</h2>\n\n    <p>No-one has voted on this poll yet</p>\n\n    <h3>Add your vote</h3>\n    <p><label for="id_vote_0">Vote:</label> <ul>\n<li><label for="id_vote_0"><input type="radio" id="id_vote_0" value="3" name="vote" /> PM</label></li>\n<li><label for="id_vote_1"><input type="radio" id="id_vote_1" value="4" name="vote" /> Gardener&#39;s</label></li>\n</ul></p>\n\n    \n  </body>\n</html>\n'

Django has converted an apostrophe (') into an html-compliant &#39; for us. I suppose that's my come-uppance for trying to include British in-jokes in my tutorial. Let's implement a minor hack in our test:

And now we have passination:

........
----------------------------------------------------------------------
Ran 8 tests in 0.016s

OK

So let's ask the FTs again!:

======================================================================
FAIL: test_voting_on_a_new_poll (tests.TestPolls)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/harry/workspace/tddjango_site/source/mysite/fts/tests.py", line 84, in test_voting_on_a_new_poll
    'Moderately awesome',
AssertionError: Lists differ: [u'Vote:', u'Very awesome', u'... != ['Very awesome', 'Quite awesom...

First differing element 0:
Vote:
Very awesome

First list contains 1 additional elements.
First extra element 3:
Moderately awesome

- [u'Vote:', u'Very awesome', u'Quite awesome', u'Moderately awesome']
?  -----------                -                 -

+ ['Very awesome', 'Quite awesome', 'Moderately awesome']

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

Hm, not quite according to the original plan - our form has auto-generated an extra label which says "Vote:" above the radio buttons - well, since it doesn't do any harm, for now maybe it's easiest to just change the FT:

The FT should now get a little further:

NoSuchElementException: Message: u'Unable to locate element: {"method":"css selector","selector":"input[type=\'submit\']"}'

There's no submit button on our form! When Django generates a form, it only gives you the inputs for the fields you've defined, so no submit button (and no <form> tag either for that matter).

Well, a button is easy enough to add, although it may not do much... In the template:

And now... our tests get to the end!:

======================================================================
FAIL: test_voting_on_a_new_poll (tests.TestPolls)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/harry/workspace/tddjango_site/source/mysite/fts/tests.py", line 125, in test_voting_on_a_new_poll
    self.fail('TODO')
AssertionError: TODO
----------------------------------------------------------------------

Tune in next week for when we finish our tests, handle POST requests, and do super-fun form validation too...

Jump to Line
Something went wrong with that request. Please try again.