Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100644 693 lines (467 sloc) 23.197 kB
7128bee @hjwp paragraphify t4
authored
1 Welcome to part 4 of the tutorial! In this part at how we can let users vote
2 on our poll, in other words, **web forms!**. Hooray.
9e64996 @hjwp placeholder for tutorial 4
authored
3
38577b4 @hjwp updates to tute 4
authored
4 Tutorial 4: Using a form
5 ========================
6
7 Here's the outline of what we're going to do in this tutorial:
8
9 * extend the FT to show Herbert voting on the poll
10
11 * create a url, view and template to generate pages for individual polls
12
13 * create a Django form to handle choices
14
15
16 Extending the FT to vote using radio buttons
17 --------------------------------------------
18
7128bee @hjwp paragraphify t4
authored
19 Let's start by extending our FT, to show Herbert voting on a poll. In
20 ``fts/tests.py``:
795258f @hjwp start on tutorial 4
authored
21
22 .. sourcecode:: python
64ae7bd @hjwp finish rewriting tutorial 4
authored
23 :filename: mysite/fts/tests.py
795258f @hjwp start on tutorial 4
authored
24
38577b4 @hjwp updates to tute 4
authored
25 [...]
795258f @hjwp start on tutorial 4
authored
26 # Now, Herbert the regular user goes to the homepage of the site. He
27 # sees a list of polls.
64ae7bd @hjwp finish rewriting tutorial 4
authored
28 self.browser.get(self.live_server_url)
795258f @hjwp start on tutorial 4
authored
29 heading = self.browser.find_element_by_tag_name('h1')
30 self.assertEquals(heading.text, 'Polls')
31
32 # He clicks on the link to the first Poll, which is called
33 # 'How awesome is test-driven development?'
34 first_poll_title = 'How awesome is Test-Driven Development?'
35 self.browser.find_element_by_link_text(first_poll_title).click()
36
37 # He is taken to a poll 'results' page, which says
38 # "no-one has voted on this poll yet"
39 main_heading = self.browser.find_element_by_tag_name('h1')
40 self.assertEquals(main_heading.text, 'Poll Results')
41 sub_heading = self.browser.find_element_by_tag_name('h2')
42 self.assertEquals(sub_heading.text, first_poll_title)
43 body = self.browser.find_element_by_tag_name('body')
44 self.assertIn('No-one has voted on this poll yet', body.text)
45
46 # He also sees a form, which offers him several choices.
3083ae0 @hjwp tweaks to ft, got through to form submit
authored
47 # There are three options with radio buttons
66f3c37 @hjwp rewrite 04 for 1.4. changes to view/forms tests. HP
authored
48 choice_inputs = self.browser.find_elements_by_css_selector(
795258f @hjwp start on tutorial 4
authored
49 "input[type='radio']"
50 )
3083ae0 @hjwp tweaks to ft, got through to form submit
authored
51 self.assertEquals(len(choice_inputs), 3)
52
53 # The buttons have labels to explain them
66f3c37 @hjwp rewrite 04 for 1.4. changes to view/forms tests. HP
authored
54 choice_labels = self.browser.find_elements_by_tag_name('label')
3083ae0 @hjwp tweaks to ft, got through to form submit
authored
55 choices_text = [c.text for c in choice_labels]
795258f @hjwp start on tutorial 4
authored
56 self.assertEquals(choices_text, [
57 'Very awesome',
58 'Quite awesome',
59 'Moderately awesome',
60 ])
3083ae0 @hjwp tweaks to ft, got through to form submit
authored
61 # He decided to select "very awesome", which is answer #1
795258f @hjwp start on tutorial 4
authored
62 chosen = self.browser.find_element_by_css_selector(
3083ae0 @hjwp tweaks to ft, got through to form submit
authored
63 "input[value='1']"
795258f @hjwp start on tutorial 4
authored
64 )
65 chosen.click()
66
67 # Herbert clicks 'submit'
68 self.browser.find_element_by_css_selector(
69 "input[type='submit']"
70 ).click()
71
72 # The page refreshes, and he sees that his choice
73 # has updated the results. they now say
74 # "100 %: very awesome".
38577b4 @hjwp updates to tute 4
authored
75 self.fail('TODO')
795258f @hjwp start on tutorial 4
authored
76
77 # The page also says "1 votes"
78
79 # Satisfied, he goes back to sleep
80
38577b4 @hjwp updates to tute 4
authored
81
7128bee @hjwp paragraphify t4
authored
82 If you run them, you'll find that they are still telling us the individual poll
83 page isn't working::
795258f @hjwp start on tutorial 4
authored
84
64ae7bd @hjwp finish rewriting tutorial 4
authored
85 NoSuchElementException: Message: u'Unable to locate element: {"method":"tag name","selector":"h1"}'
795258f @hjwp start on tutorial 4
authored
86
38577b4 @hjwp updates to tute 4
authored
87
7128bee @hjwp paragraphify t4
authored
88 That because, currently, our ``poll`` view is just a placeholder function. We
89 need to make into into a real Django view, which returns information about a
90 poll.
38577b4 @hjwp updates to tute 4
authored
91
7128bee @hjwp paragraphify t4
authored
92 Let's work on the unit tests for the ``poll`` view then. Make a new class for
93 them in ``polls/tests.py``:
795258f @hjwp start on tutorial 4
authored
94
95 .. sourcecode:: python
b992084 @hjwp Add filename captions to remaing source code blocks
authored
96 :filename: mysite/polls/tests.py
795258f @hjwp start on tutorial 4
authored
97
d8d89b7 @hjwp put fts into single class, paragraphify t3, test class renames to use
authored
98 class SinglePollViewTest(TestCase):
795258f @hjwp start on tutorial 4
authored
99
100 def test_page_shows_poll_title_and_no_votes_message(self):
101 # set up two polls, to check the right one is displayed
66f3c37 @hjwp rewrite 04 for 1.4. changes to view/forms tests. HP
authored
102 poll1 = Poll(question='6 times 7', pub_date=timezone.now())
795258f @hjwp start on tutorial 4
authored
103 poll1.save()
66f3c37 @hjwp rewrite 04 for 1.4. changes to view/forms tests. HP
authored
104 poll2 = Poll(question='life, the universe and everything', pub_date=timezone.now())
795258f @hjwp start on tutorial 4
authored
105 poll2.save()
106
64ae7bd @hjwp finish rewriting tutorial 4
authored
107 response = self.client.get('/poll/%d/' % (poll2.id, ))
795258f @hjwp start on tutorial 4
authored
108
66f3c37 @hjwp rewrite 04 for 1.4. changes to view/forms tests. HP
authored
109 # check we've used the poll template
110 self.assertTemplateUsed(response, 'poll.html')
111
6169d40 @hjwp added form to template. almost there...
authored
112 # check we've passed the right poll into the context
795258f @hjwp start on tutorial 4
authored
113 self.assertEquals(response.context['poll'], poll2)
6169d40 @hjwp added form to template. almost there...
authored
114
115 # check the poll's question appears on the page
144a6a3 @hjwp created first form, forms.py.
authored
116 self.assertIn(poll2.question, response.content)
6169d40 @hjwp added form to template. almost there...
authored
117
118 # check our 'no votes yet' message appears
795258f @hjwp start on tutorial 4
authored
119 self.assertIn('No-one has voted on this poll yet', response.content)
120
6169d40 @hjwp added form to template. almost there...
authored
121
795258f @hjwp start on tutorial 4
authored
122 Running the tests gives::
123
124 TypeError: poll() takes no arguments (2 given)
125
7128bee @hjwp paragraphify t4
authored
126 (*I'm going to be shortening the test outputs from now on. You're a TDD
127 veteran now, you can handle it! :-)*
795258f @hjwp start on tutorial 4
authored
128
129 Let's make our view take two arguments:
130
131 .. sourcecode:: python
b992084 @hjwp Add filename captions to remaing source code blocks
authored
132 :filename: mysite/polls/views.py
795258f @hjwp start on tutorial 4
authored
133
134 def poll(request, poll_id):
135 pass
136
137 Now we get::
138
139 ValueError: The view mysite.polls.views.poll didn't return an HttpResponse object.
140
141 Again, a minimal fix:
142
143 .. sourcecode:: python
b992084 @hjwp Add filename captions to remaing source code blocks
authored
144 :filename: mysite/polls/views.py
795258f @hjwp start on tutorial 4
authored
145
146 def poll(request, poll_id):
147 return HttpResponse()
148
149 Now we get this error::
150
66f3c37 @hjwp rewrite 04 for 1.4. changes to view/forms tests. HP
authored
151 AssertionError: No templates used to render the response
152
795258f @hjwp start on tutorial 4
authored
153
7128bee @hjwp paragraphify t4
authored
154 Let's try fixing that - but deliberately using the wrong template (just to
155 check we are testing it)
795258f @hjwp start on tutorial 4
authored
156
157 .. sourcecode:: python
b992084 @hjwp Add filename captions to remaing source code blocks
authored
158 :filename: mysite/polls/views.py
795258f @hjwp start on tutorial 4
authored
159
160 def poll(request, poll_id):
710847e @hjwp some renames, going over tute 3...
authored
161 return render(request, 'home.html')
795258f @hjwp start on tutorial 4
authored
162
163 Good, looks like we are testiing it properly::
164
66f3c37 @hjwp rewrite 04 for 1.4. changes to view/forms tests. HP
authored
165 AssertionError: Template 'poll.html' was not a template used to render the response. Actual template(s) used: home.html
795258f @hjwp start on tutorial 4
authored
166
167 And changing it to ``poll.html`` gives us::
168
169 TemplateDoesNotExist: poll.html
170
144a6a3 @hjwp created first form, forms.py.
authored
171 Fine and dandy, let's make one::
172
173 touch polls/templates/poll.html
174
7128bee @hjwp paragraphify t4
authored
175 You might argue that an empty file, all 0 bytes of it, is a fairly minimal
176 template! Still, it seems to satisfy the tests. Now they want us to pass a
177 ``poll`` variable in the template's context::
144a6a3 @hjwp created first form, forms.py.
authored
178
179 KeyError: 'poll'
180
181 So let's do that, again, the minimum possible change to satisfy the tests:
182
183 .. sourcecode:: python
b992084 @hjwp Add filename captions to remaing source code blocks
authored
184 :filename: mysite/polls/views.py
144a6a3 @hjwp created first form, forms.py.
authored
185
186 def poll(request, poll_id):
710847e @hjwp some renames, going over tute 3...
authored
187 return render(request, 'poll.html', {'poll': None})
144a6a3 @hjwp created first form, forms.py.
authored
188
189 And the tests get a little further on::
190
191 AssertionError: None != <Poll: life, the universe and everything>
192
193 And they even tell us what to do next - pass in the right `Poll` object:
194
195 .. sourcecode:: python
b992084 @hjwp Add filename captions to remaing source code blocks
authored
196 :filename: mysite/polls/views.py
144a6a3 @hjwp created first form, forms.py.
authored
197
198 def poll(request, poll_id):
199 poll = Poll.objects.get(pk=poll_id)
200 return render(request, 'poll.html', {'poll': poll})
201
7128bee @hjwp paragraphify t4
authored
202 This is the first time we've used the Django API to fetch a single database
203 object, and ``objects.get`` is the helper function for this - it raises an
204 error if it can't find the object, or if it finds more than one. The special
205 keyword argument ``pk`` stands for `primary key`. In this case, Django is using
206 the default for primary keys, which is an automatically generated integer
207 ``id`` column.
144a6a3 @hjwp created first form, forms.py.
authored
208
7128bee @hjwp paragraphify t4
authored
209 That raises the question of what to do if a user types in a url for a poll that
210 doesn't exist - ``/poll/0/`` for example. We'll come back to this in a later
211 tutorial.
144a6a3 @hjwp created first form, forms.py.
authored
212
213 In the meantime, what do the tests say::
214
215 self.assertIn(poll2.question, response.content)
216 AssertionError: 'life, the universe and everything' not found in ''
217
7128bee @hjwp paragraphify t4
authored
218 We need to get our template to include the poll's question. Let's make it into
219 a page heading:
144a6a3 @hjwp created first form, forms.py.
authored
220
221 .. sourcecode:: html+django
64ae7bd @hjwp finish rewriting tutorial 4
authored
222 :filename: mysite/polls/templates/poll.html
144a6a3 @hjwp created first form, forms.py.
authored
223
224 <html>
225 <body>
226 <h2>{{poll.question}}</h2>
227 </body>
228 </html>
229
230 Now the tests want our 'no polls yet' message::
231
232 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'
233
234 So let's include that:
235
236 .. sourcecode:: html+django
b992084 @hjwp Add filename captions to remaing source code blocks
authored
237 :filename: mysite/polls/templates/home.html
144a6a3 @hjwp created first form, forms.py.
authored
238
239 <html>
240 <body>
241
242 <h2>{{poll.question}}</h2>
243
244 <p>No-one has voted on this poll yet</p>
245
246 </body>
247 </html>
248
249 And that's enough to make the unit tests happy::
250
251 ----------------------------------------------------------------------
252 Ran 7 tests in 0.013s
253
254 OK
255
40cec88 @hjwp tweaking tute 4
authored
256 Mmmh, `OK`. And doughnuts. Let's see what the FTs think?::
144a6a3 @hjwp created first form, forms.py.
authored
257
258 NoSuchElementException: Message: u'Unable to locate element: {"method":"tag name","selector":"h1"}'
259
7128bee @hjwp paragraphify t4
authored
260 Ah, we forgot to include a general heading for the page - the FT is checking
261 the ``h1`` and ``h2`` headings:
144a6a3 @hjwp created first form, forms.py.
authored
262
263 .. sourcecode:: python
64ae7bd @hjwp finish rewriting tutorial 4
authored
264 :filename: mysite/fts/tests.py
144a6a3 @hjwp created first form, forms.py.
authored
265
266 main_heading = self.browser.find_element_by_tag_name('h1')
267 self.assertEquals(main_heading.text, 'Poll Results')
268 sub_heading = self.browser.find_element_by_tag_name('h2')
269 self.assertEquals(sub_heading.text, first_poll_title)
270
38577b4 @hjwp updates to tute 4
authored
271 So, in our template, let's add an ``h1`` with "Poll Results" in it:
144a6a3 @hjwp created first form, forms.py.
authored
272
273 .. sourcecode:: html+django
b992084 @hjwp Add filename captions to remaing source code blocks
authored
274 :filename: mysite/polls/templates/home.html
144a6a3 @hjwp created first form, forms.py.
authored
275
276 <html>
277 <body>
278 <h1>Poll Results</h1>
279
280 <h2>{{poll.question}}</h2>
281
282 <p>No-one has voted on this poll yet</p>
283
284 </body>
285 </html>
286
38577b4 @hjwp updates to tute 4
authored
287
288 Using a Django form for poll choices
289 ------------------------------------
290
291 Now what does the FT say?::
144a6a3 @hjwp created first form, forms.py.
authored
292
293 ======================================================================
64ae7bd @hjwp finish rewriting tutorial 4
authored
294 FAIL: test_voting_on_a_new_poll (tests.TestPolls)
144a6a3 @hjwp created first form, forms.py.
authored
295 ----------------------------------------------------------------------
296 Traceback (most recent call last):
64ae7bd @hjwp finish rewriting tutorial 4
authored
297 File "/home/harry/workspace/mysite/fts/tests.py", line 100, in test_voting_on_a_new_poll
66f3c37 @hjwp rewrite 04 for 1.4. changes to view/forms tests. HP
authored
298 self.assertEquals(len(choice_inputs), 3)
299 AssertionError: 0 != 3
144a6a3 @hjwp created first form, forms.py.
authored
300
301 ----------------------------------------------------------------------
302
7128bee @hjwp paragraphify t4
authored
303 Ah, we need to add the poll Choices as a series of radio inputs. Now the
304 official Django tutorial shows you how to hard-code them in HTML:
144a6a3 @hjwp created first form, forms.py.
authored
305
66f3c37 @hjwp rewrite 04 for 1.4. changes to view/forms tests. HP
authored
306 https://docs.djangoproject.com/en/1.4/intro/tutorial04/
144a6a3 @hjwp created first form, forms.py.
authored
307
7128bee @hjwp paragraphify t4
authored
308 But Django can do even better than that - Django's forms system will generate
309 radio buttons for us, if we can just give it the right incantations. Let's
1e963b1 @hjwp Shannon feedback: fix tests.py, broken images, "development" server.
authored
310 create a new test in ``polls/tests.py``:
144a6a3 @hjwp created first form, forms.py.
authored
311
312
313 .. sourcecode:: python
b992084 @hjwp Add filename captions to remaing source code blocks
authored
314 :filename: mysite/polls/tests.py
144a6a3 @hjwp created first form, forms.py.
authored
315
316 from polls.forms import PollVoteForm
317
d8d89b7 @hjwp put fts into single class, paragraphify t3, test class renames to use
authored
318 class PollsVoteFormTest(TestCase):
144a6a3 @hjwp created first form, forms.py.
authored
319
320 def test_form_renders_poll_choices_as_radio_inputs(self):
ad0496c @hjwp choices now working on form
authored
321 # set up a poll with a couple of choices
66f3c37 @hjwp rewrite 04 for 1.4. changes to view/forms tests. HP
authored
322 poll1 = Poll(question='6 times 7', pub_date=timezone.now())
ad0496c @hjwp choices now working on form
authored
323 poll1.save()
324 choice1 = Choice(poll=poll1, choice='42', votes=0)
144a6a3 @hjwp created first form, forms.py.
authored
325 choice1.save()
ad0496c @hjwp choices now working on form
authored
326 choice2 = Choice(poll=poll1, choice='The Ultimate Answer', votes=0)
144a6a3 @hjwp created first form, forms.py.
authored
327 choice2.save()
328
ad0496c @hjwp choices now working on form
authored
329 # set up another poll to make sure we only see the right choices
66f3c37 @hjwp rewrite 04 for 1.4. changes to view/forms tests. HP
authored
330 poll2 = Poll(question='time', pub_date=timezone.now())
ad0496c @hjwp choices now working on form
authored
331 poll2.save()
332 choice3 = Choice(poll=poll2, choice='PM', votes=0)
333 choice3.save()
334
335 # build a voting form for poll1
336 form = PollVoteForm(poll=poll1)
144a6a3 @hjwp created first form, forms.py.
authored
337
338 # check it has a single field called 'vote', which has right choices:
339 self.assertEquals(form.fields.keys(), ['vote'])
ad0496c @hjwp choices now working on form
authored
340
341 # choices are tuples in the format (choice_number, choice_text):
342 self.assertEquals(form.fields['vote'].choices, [
343 (choice1.id, choice1.choice),
344 (choice2.id, choice2.choice),
345 ])
144a6a3 @hjwp created first form, forms.py.
authored
346
347 # check it uses radio inputs to render
348 self.assertIn('input type="radio"', form.as_p())
349
40cec88 @hjwp tweaking tute 4
authored
350 You might prefer to put the import at the top of the file.
351
7128bee @hjwp paragraphify t4
authored
352 Looking through the code, you can see we instantiate a form, passing it a poll
353 object. We then examine the form's ``fields`` attribute, find the one called
354 ``vote`` (this will also be the ``name`` of the HTML input element), and we
355 check the ``choices`` for that field.
40cec88 @hjwp tweaking tute 4
authored
356
7128bee @hjwp paragraphify t4
authored
357 For the test to even get off the ground, we may as well create something
358 minimal for it to import! Create a file called ``polls/forms.py``.
144a6a3 @hjwp created first form, forms.py.
authored
359
360 .. sourcecode:: python
b992084 @hjwp Add filename captions to remaing source code blocks
authored
361 :filename: mysite/polls/forms.py
144a6a3 @hjwp created first form, forms.py.
authored
362
363 class PollVoteForm(object):
364 pass
365
366 And let's start another test/code cycle, woo -::
367
0901ca6 @hjwp WIP - changing ordering of tute 1
authored
368 python manage.py test polls
40cec88 @hjwp tweaking tute 4
authored
369
144a6a3 @hjwp created first form, forms.py.
authored
370 [...]
371 form = PollVoteForm(poll=poll)
372 TypeError: object.__new__() takes no parameters
373
40cec88 @hjwp tweaking tute 4
authored
374 We override ``__init__.py`` to change the constructor:
144a6a3 @hjwp created first form, forms.py.
authored
375
376 .. sourcecode:: python
b992084 @hjwp Add filename captions to remaing source code blocks
authored
377 :filename: mysite/polls/forms.py
144a6a3 @hjwp created first form, forms.py.
authored
378
379 class PollVoteForm(object):
380 def __init__(self, poll):
381 pass
382
40cec88 @hjwp tweaking tute 4
authored
383 ... ::
384
144a6a3 @hjwp created first form, forms.py.
authored
385 self.assertEquals(form.fields.keys(), ['vote'])
386 AttributeError: 'PollVoteForm' object has no attribute 'fields'
387
7128bee @hjwp paragraphify t4
authored
388 To give the form a 'fields' attribute, we can make it inherit from a real
389 Django form class, and call its parent constructor:
144a6a3 @hjwp created first form, forms.py.
authored
390
391 .. sourcecode:: python
b992084 @hjwp Add filename captions to remaing source code blocks
authored
392 :filename: mysite/polls/forms.py
144a6a3 @hjwp created first form, forms.py.
authored
393
394 from django import forms
395
396 class PollVoteForm(forms.Form):
397 def __init__(self, poll):
66f3c37 @hjwp rewrite 04 for 1.4. changes to view/forms tests. HP
authored
398 forms.Form.__init__(self)
144a6a3 @hjwp created first form, forms.py.
authored
399
66f3c37 @hjwp rewrite 04 for 1.4. changes to view/forms tests. HP
authored
400 Now we get::
144a6a3 @hjwp created first form, forms.py.
authored
401
402 AssertionError: Lists differ: [] != ['vote']
403
7128bee @hjwp paragraphify t4
authored
404 Django form fields are defined a bit like model fields - using inline class
405 attributes. There are various types of fields, in this case we want one that
406 has `choices` - a ``ChoiceField``. You can find out more about form fields
407 here:
144a6a3 @hjwp created first form, forms.py.
authored
408
66f3c37 @hjwp rewrite 04 for 1.4. changes to view/forms tests. HP
authored
409 https://docs.djangoproject.com/en/1.4/ref/forms/fields/
144a6a3 @hjwp created first form, forms.py.
authored
410
411 .. sourcecode:: python
b992084 @hjwp Add filename captions to remaing source code blocks
authored
412 :filename: mysite/polls/forms.py
144a6a3 @hjwp created first form, forms.py.
authored
413
414 class PollVoteForm(forms.Form):
415 vote = forms.ChoiceField()
416
417 def __init__(self, poll):
66f3c37 @hjwp rewrite 04 for 1.4. changes to view/forms tests. HP
authored
418 forms.Form.__init__(self)
144a6a3 @hjwp created first form, forms.py.
authored
419
420 Now we get::
421
ad0496c @hjwp choices now working on form
authored
422 AssertionError: Lists differ: [] != [(1, '42'), (2, 'The Ultimate ...
144a6a3 @hjwp created first form, forms.py.
authored
423
7128bee @hjwp paragraphify t4
authored
424 So now let's set the choices from the ``poll`` we passed into the constructor
425 (you can read up on choices in Django here
426 https://docs.djangoproject.com/en/1.4/ref/models/fields/#field-choices)
144a6a3 @hjwp created first form, forms.py.
authored
427
428 .. sourcecode:: python
b992084 @hjwp Add filename captions to remaing source code blocks
authored
429 :filename: mysite/polls/forms.py
144a6a3 @hjwp created first form, forms.py.
authored
430
431 def __init__(self, poll):
66f3c37 @hjwp rewrite 04 for 1.4. changes to view/forms tests. HP
authored
432 forms.Form.__init__(self)
ad0496c @hjwp choices now working on form
authored
433 self.fields['vote'].choices = [(c.id, c.choice) for c in poll.choice_set.all()]
434
7128bee @hjwp paragraphify t4
authored
435 Mmmmmh, list comprehensions... That will now get the test almost to the end -
436 we can instantiate a form using a poll object, and the form will automatically
437 generate the choices based on the poll's ``choice_set.all()`` function, which
438 gets related objects.
ad0496c @hjwp choices now working on form
authored
439
7128bee @hjwp paragraphify t4
authored
440 The final test is to make sure we have radio boxes as the HTML input type.
441 We're using ``as_p()``, a method provided on all Django forms which renders the
442 form to HTML for us - we can see exactly what the HTML looks like in the next
443 test output::
ad0496c @hjwp choices now working on form
authored
444
445 self.assertIn('input type="radio"', form.as_p())
446 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>'
447
7128bee @hjwp paragraphify t4
authored
448 Django has defaulted to using a ``select/option`` input form. We can change
449 this using a `widget`, in this case a ``RadioSelect``
ad0496c @hjwp choices now working on form
authored
450
451 .. sourcecode:: python
b992084 @hjwp Add filename captions to remaing source code blocks
authored
452 :filename: mysite/polls/forms.py
ad0496c @hjwp choices now working on form
authored
453
454 class PollVoteForm(forms.Form):
455 vote = forms.ChoiceField(widget=forms.RadioSelect())
456
457 def __init__(self, poll):
66f3c37 @hjwp rewrite 04 for 1.4. changes to view/forms tests. HP
authored
458 forms.Form.__init__(self)
ad0496c @hjwp choices now working on form
authored
459 self.fields['vote'].choices = [(c.id, c.choice) for c in poll.choice_set.all()]
460
7128bee @hjwp paragraphify t4
authored
461 OK so far? Django forms have *fields*, some of which may have *choices*, and
462 we can choose how the field will be displayed on page using a *widget*. Right.
40cec88 @hjwp tweaking tute 4
authored
463
7128bee @hjwp paragraphify t4
authored
464 And that should get the tests passing! If you're curious to see what the form
465 HTML actually looks like, why not temporarily put a ``print form.as_p()`` at
466 the end of the test? Print statements in tests can be very useful for
467 exploratory programming... You could try ``form.as_table()`` too if you like...
ad0496c @hjwp choices now working on form
authored
468
40cec88 @hjwp tweaking tute 4
authored
469 Right, where where we? Let's do a quick check of the functional tests.
470
7128bee @hjwp paragraphify t4
authored
471 (*incidentally, are you rather bored of watching the FT run through the admin
472 test each time? If so, you can temporarily disable it by renaming its test
473 method from* ``test_can_create_new_poll_via_admin_site`` *to*
474 ``DONTtest_can_create_new_poll_via_admin_site`` *that's called "Dontifying"...
475 you do have to be careful not to forget about your dontified tests though!*)
6169d40 @hjwp added form to template. almost there...
authored
476
01a12be @hjwp Finish rewriting tutorial 5
authored
477 python manage.py test fts
6169d40 @hjwp added form to template. almost there...
authored
478 [...]
66f3c37 @hjwp rewrite 04 for 1.4. changes to view/forms tests. HP
authored
479 AssertionError: 0 != 3
6169d40 @hjwp added form to template. almost there...
authored
480
7128bee @hjwp paragraphify t4
authored
481 Ah yes, we still haven't actually *used* the form yet! Let's go back to our
482 ``SinglePollViewTest``, and a new test that checks we use our form)
6169d40 @hjwp added form to template. almost there...
authored
483
484 .. sourcecode:: python
b992084 @hjwp Add filename captions to remaing source code blocks
authored
485 :filename: mysite/polls/tests.py
6169d40 @hjwp added form to template. almost there...
authored
486
d8d89b7 @hjwp put fts into single class, paragraphify t3, test class renames to use
authored
487 class SinglePollViewTest(TestCase):
6169d40 @hjwp added form to template. almost there...
authored
488
b992084 @hjwp Add filename captions to remaing source code blocks
authored
489 def test_page_shows_poll_title_and_no_votes_message(self):
66f3c37 @hjwp rewrite 04 for 1.4. changes to view/forms tests. HP
authored
490 [...]
491
492
493 def test_page_shows_choices_using_form(self):
494 # set up a poll with choices
495 poll1 = Poll(question='time', pub_date=timezone.now())
b992084 @hjwp Add filename captions to remaing source code blocks
authored
496 poll1.save()
66f3c37 @hjwp rewrite 04 for 1.4. changes to view/forms tests. HP
authored
497 choice1 = Choice(poll=poll1, choice="PM", votes=0)
b992084 @hjwp Add filename captions to remaing source code blocks
authored
498 choice1.save()
66f3c37 @hjwp rewrite 04 for 1.4. changes to view/forms tests. HP
authored
499 choice2 = Choice(poll=poll1, choice="Gardener's", votes=0)
b992084 @hjwp Add filename captions to remaing source code blocks
authored
500 choice2.save()
6169d40 @hjwp added form to template. almost there...
authored
501
64ae7bd @hjwp finish rewriting tutorial 4
authored
502 response = self.client.get('/poll/%d/' % (poll1.id, ))
6169d40 @hjwp added form to template. almost there...
authored
503
b992084 @hjwp Add filename captions to remaing source code blocks
authored
504 # check we've passed in a form of the right type
505 self.assertTrue(isinstance(response.context['form'], PollVoteForm))
6169d40 @hjwp added form to template. almost there...
authored
506
56b9a91 @hjwp More shannoney feedback
authored
507 # and check the form is being used in the template,
b992084 @hjwp Add filename captions to remaing source code blocks
authored
508 # by checking for the choice text
66f3c37 @hjwp rewrite 04 for 1.4. changes to view/forms tests. HP
authored
509 self.assertIn(choice1.choice, response.content)
510 self.assertIn(choice2.choice, response.content)
511
6169d40 @hjwp added form to template. almost there...
authored
512
513 Now the unit tests give us::
514
40cec88 @hjwp tweaking tute 4
authored
515 python manage.py test polls
516 [...]
6169d40 @hjwp added form to template. almost there...
authored
517 KeyError: 'form'
518
519 So back in ``views.py``:
520
521 .. sourcecode:: python
b992084 @hjwp Add filename captions to remaing source code blocks
authored
522 :filename: mysite/polls/views.py
6169d40 @hjwp added form to template. almost there...
authored
523
524 def poll(request, poll_id):
525 poll = Poll.objects.get(pk=poll_id)
526 return render(request, 'poll.html', {'poll': poll, 'form': None})
527
528 Now::
529
530 self.assertTrue(isinstance(response.context['form'], PollVoteForm))
531 AssertionError: False is not true
532
533 So:
534
535 .. sourcecode:: python
b992084 @hjwp Add filename captions to remaing source code blocks
authored
536 :filename: mysite/polls/views.py
6169d40 @hjwp added form to template. almost there...
authored
537
1a98104 @hjwp add missing import in example. thx @kevindebaere.
authored
538 from polls.forms import PollVoteForm
539
540 [...]
541
6169d40 @hjwp added form to template. almost there...
authored
542 def poll(request, poll_id):
543 poll = Poll.objects.get(pk=poll_id)
544 form = PollVoteForm(poll=poll)
545 return render(request, 'poll.html', {'poll': poll, 'form': form})
546
547 And::
548
549 self.assertIn(choice3.choice, response.content)
66f3c37 @hjwp rewrite 04 for 1.4. changes to view/forms tests. HP
authored
550 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'
551
6169d40 @hjwp added form to template. almost there...
authored
552
553 So, in ``polls/templates/poll.html``:
554
555 .. sourcecode:: html+django
b992084 @hjwp Add filename captions to remaing source code blocks
authored
556 :filename: mysite/polls/templates/home.html
6169d40 @hjwp added form to template. almost there...
authored
557
558 <html>
559 <body>
560 <h1>Poll Results</h1>
561
562 <h2>{{poll.question}}</h2>
563
564 <p>No-one has voted on this poll yet</p>
ad0496c @hjwp choices now working on form
authored
565
6169d40 @hjwp added form to template. almost there...
authored
566 <h3>Add your vote</h3>
567 {{form.as_p}}
568
569
570 </body>
571 </html>
572
573 And re-running the tests - oh, a surprise!::
574
575 self.assertIn(choice4.choice, response.content)
576 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'
577
7128bee @hjwp paragraphify t4
authored
578 Django has converted an apostrophe (``'``) into an html-compliant ``&#39;`` for
579 us. I suppose that's my come-uppance for trying to include British in-jokes in
580 my tutorial. Let's implement a minor hack in our test:
6169d40 @hjwp added form to template. almost there...
authored
581
582
b992084 @hjwp Add filename captions to remaing source code blocks
authored
583 .. sourcecode:: python
584 :filename: mysite/polls/tests.py
6169d40 @hjwp added form to template. almost there...
authored
585
66f3c37 @hjwp rewrite 04 for 1.4. changes to view/forms tests. HP
authored
586 self.assertIn(choice1.choice, response.content.replace('&#39;', "'"))
587 self.assertIn(choice2.choice, response.content.replace('&#39;', "'"))
6169d40 @hjwp added form to template. almost there...
authored
588
589 And now we have passination::
590
591 ........
592 ----------------------------------------------------------------------
593 Ran 8 tests in 0.016s
594
595 OK
144a6a3 @hjwp created first form, forms.py.
authored
596
3083ae0 @hjwp tweaks to ft, got through to form submit
authored
597 So let's ask the FTs again!::
598
599 ======================================================================
64ae7bd @hjwp finish rewriting tutorial 4
authored
600 FAIL: test_voting_on_a_new_poll (tests.TestPolls)
3083ae0 @hjwp tweaks to ft, got through to form submit
authored
601 ----------------------------------------------------------------------
602 Traceback (most recent call last):
64ae7bd @hjwp finish rewriting tutorial 4
authored
603 File "/home/harry/workspace/tddjango_site/source/mysite/fts/tests.py", line 84, in test_voting_on_a_new_poll
3083ae0 @hjwp tweaks to ft, got through to form submit
authored
604 'Moderately awesome',
605 AssertionError: Lists differ: [u'Vote:', u'Very awesome', u'... != ['Very awesome', 'Quite awesom...
606
607 First differing element 0:
608 Vote:
609 Very awesome
610
611 First list contains 1 additional elements.
612 First extra element 3:
613 Moderately awesome
614
615 - [u'Vote:', u'Very awesome', u'Quite awesome', u'Moderately awesome']
616 ? ----------- - -
617
618 + ['Very awesome', 'Quite awesome', 'Moderately awesome']
619
620 ----------------------------------------------------------------------
621
7128bee @hjwp paragraphify t4
authored
622 Hm, not quite according to the original plan - our form has auto-generated an
623 extra label which says "Vote:" above the radio buttons - well, since it doesn't
624 do any harm, for now maybe it's easiest to just change the FT:
3083ae0 @hjwp tweaks to ft, got through to form submit
authored
625
b95a8ae @hjwp fix rst syntax errors.
authored
626 .. sourcecode:: python
64ae7bd @hjwp finish rewriting tutorial 4
authored
627 :filename: mysite/fts/tests.py
3083ae0 @hjwp tweaks to ft, got through to form submit
authored
628
629 # He also sees a form, which offers him several choices.
630 # There are three options with radio buttons
631 choice_inputs = self.browser.find_elements_by_css_selector(
632 "input[type='radio']"
633 )
634 self.assertEquals(len(choice_inputs), 3)
635
636 # The buttons have labels to explain them
637 choice_labels = choice_inputs = self.browser.find_elements_by_tag_name('label')
638 choices_text = [c.text for c in choice_labels]
639 self.assertEquals(choices_text, [
640 'Vote:', # this label is auto-generated for the whole form
641 'Very awesome',
642 'Quite awesome',
643 'Moderately awesome',
644 ])
645
646
647 The FT should now get a little further::
648
649 NoSuchElementException: Message: u'Unable to locate element: {"method":"css selector","selector":"input[type=\'submit\']"}'
650
7128bee @hjwp paragraphify t4
authored
651 There's no submit button on our form! When Django generates a form, it only
652 gives you the inputs for the fields you've defined, so no submit button (and no
653 ``<form>`` tag either for that matter).
3083ae0 @hjwp tweaks to ft, got through to form submit
authored
654
7128bee @hjwp paragraphify t4
authored
655 Well, a button is easy enough to add, although it may not do much... In the
656 template:
3083ae0 @hjwp tweaks to ft, got through to form submit
authored
657
658 .. sourcecode:: html+django
64ae7bd @hjwp finish rewriting tutorial 4
authored
659 :filename: mysite/polls/templates/poll.html
3083ae0 @hjwp tweaks to ft, got through to form submit
authored
660
661 <html>
662 <body>
663 <h1>Poll Results</h1>
664
665 <h2>{{poll.question}}</h2>
666
667 <p>No-one has voted on this poll yet</p>
668
669 <h3>Add your vote</h3>
670 {{form.as_p}}
671 <input type="submit" />
672
673
674 </body>
675 </html>
676
677
133891b @hjwp finish t4 with TODO
authored
678 And now... our tests get to the end!::
b95a8ae @hjwp fix rst syntax errors.
authored
679
133891b @hjwp finish t4 with TODO
authored
680 ======================================================================
64ae7bd @hjwp finish rewriting tutorial 4
authored
681 FAIL: test_voting_on_a_new_poll (tests.TestPolls)
133891b @hjwp finish t4 with TODO
authored
682 ----------------------------------------------------------------------
683 Traceback (most recent call last):
64ae7bd @hjwp finish rewriting tutorial 4
authored
684 File "/home/harry/workspace/tddjango_site/source/mysite/fts/tests.py", line 125, in test_voting_on_a_new_poll
133891b @hjwp finish t4 with TODO
authored
685 self.fail('TODO')
686 AssertionError: TODO
3083ae0 @hjwp tweaks to ft, got through to form submit
authored
687 ----------------------------------------------------------------------
688
689
7128bee @hjwp paragraphify t4
authored
690 Tune in next week for when we finish our tests, handle POST requests, and do
691 super-fun form validation too...
3083ae0 @hjwp tweaks to ft, got through to form submit
authored
692
Something went wrong with that request. Please try again.