@@ -332,7 +332,7 @@ Let's start by seeing how the template syntax lets us include a Python object in
332
332
The notation is `{{ ... }}`, which displays the object as a string:
333
333
334
334
[role="sourcecode small-code"]
335
- .lists/templates/home.html (ch05l008)
335
+ .lists/templates/home.html (ch05l008)
336
336
====
337
337
[source,html]
338
338
----
@@ -1330,16 +1330,70 @@ OK
1330
1330
1331
1331
We're at green, time for a little refactor!
1332
1332
1333
+ Looking at views.py, there's a small change
1333
1334
1334
- TODO little tidy up, do Item.objects.create()? Maybe get rid of new_item_text too?
1335
+ [role="sourcecode currentcontents"]
1336
+ .list/views.py
1337
+ ====
1338
+ [source,python]
1339
+ ----
1340
+ def home_page(request):
1341
+ if request.method == "POST":
1342
+ item = Item() # <1>
1343
+ item.text = request.POST["item_text"] # <1>
1344
+ item.save() # <1>
1345
+ return redirect("/")
1346
+
1347
+ return render(
1348
+ request,
1349
+ "home.html",
1350
+ {"new_item_text": request.POST.get("item_text", "")}, # <2>
1351
+ )
1352
+ ----
1353
+
1354
+ <1> There's a quicker way to do these 3 lines with `.objects.create()`
1355
+ <2> This line doesn't seem quite right now, in fact it won't work at all.
1356
+ Let's make a note on our scratchpad to sort out passing list items to the template.
1357
+ It's actually closely related to "Display multiple items",
1358
+ so we'll put it next to that one:
1359
+
1360
+ [role="scratchpad"]
1361
+ *****
1362
+ * '[strikethrough line-through]#Don't save blank items for every request#'
1363
+ * 'Code smell: POST test is too long?'
1364
+ * 'Pass existing list items to the template somehow'
1365
+ * 'Display multiple items in the table'
1366
+ * 'Support more than one list!'
1367
+ *****
1368
+
1369
+ And here's the refactored version of _views.py_ using the `.objects.create()`
1370
+ helper method that Django provides, for one-line creation of objects:
1371
+
1372
+ [role="sourcecode"]
1373
+ .lists/views.py (ch05l031)
1374
+ ====
1375
+ [source,python]
1376
+ ----
1377
+ def home_page(request):
1378
+ if request.method == "POST":
1379
+ Item.objects.create(text=request.POST["item_text"])
1380
+ return redirect("/")
1381
+
1382
+ return render(
1383
+ request,
1384
+ "home.html",
1385
+ {"new_item_text": request.POST.get("item_text", "")},
1386
+ )
1387
+
1388
+ ----
1389
+ ====
1335
1390
1336
1391
1337
1392
=== Better Unit Testing Practice: Each Test Should Test One Thing
1338
1393
1339
1394
((("unit tests", "testing only one thing")))
1340
1395
((("testing best practices")))
1341
- Our view now does a redirect after a POST, which is good practice,
1342
- and we've shortened the unit test somewhat, but we can still do better.
1396
+ Let's address the "POST test is too long" code smell.
1343
1397
1344
1398
Good unit testing practice says that each test should only test one thing. The
1345
1399
reason is that it makes it easier to track down bugs. Having multiple
@@ -1352,7 +1406,7 @@ You may not always write perfect unit tests with single assertions on your
1352
1406
first go, but now feels like a good time to separate out our concerns:
1353
1407
1354
1408
[role="sourcecode"]
1355
- .lists/tests.py
1409
+ .lists/tests.py (ch05l032)
1356
1410
====
1357
1411
[source,python]
1358
1412
----
@@ -1364,8 +1418,7 @@ first go, but now feels like a good time to separate out our concerns:
1364
1418
1365
1419
def test_redirects_after_POST(self):
1366
1420
response = self.client.post("/", data={"item_text": "A new list item"})
1367
- self.assertEqual(response.status_code, 302)
1368
- self.assertEqual(response["location"], "/")
1421
+ self.assertRedirects(response, "/")
1369
1422
----
1370
1423
====
1371
1424
@@ -1381,69 +1434,76 @@ OK
1381
1434
----
1382
1435
1383
1436
1384
- Rendering Items in the Template
1385
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1386
-
1387
-
1437
+ === Rendering Items in the Template
1388
1438
1389
- ((("database testing", "rendering items in the template", id="DTrender05")))Much
1390
- better! Back to our to-do list:
1439
+ ((("database testing", "rendering items in the template", id="DTrender05")))
1440
+ Much better! Back to our to-do list:
1391
1441
1392
1442
[role="scratchpad"]
1393
1443
*****
1394
1444
* '[strikethrough line-through]#Don't save blank items for every request#'
1395
1445
* '[strikethrough line-through]#Code smell: POST test is too long?#'
1446
+ * 'Pass existing list items to the template somehow'
1396
1447
* 'Display multiple items in the table'
1397
1448
* 'Support more than one list!'
1398
1449
*****
1399
1450
1400
1451
Crossing things off the list is almost as satisfying as seeing tests pass!
1401
1452
1402
- ((("list items")))The
1403
- third item is the last of the "easy" ones. Let's have a new unit test
1404
- that checks that the template can also display multiple list items:
1453
+ The third and fourth items are the last of the "easy" ones.
1454
+ Our view now does the right thing for POST requests,
1455
+ it saves new list items to the database.
1456
+ Now we want GET requests to load all currently existing list items,
1457
+ and pass them to the template for rendering.
1458
+ Let's have a new unit test for that:
1405
1459
1406
1460
[role="sourcecode"]
1407
- .lists/tests.py
1461
+ .lists/tests.py (ch05l033)
1408
1462
====
1409
1463
[source,python]
1410
1464
----
1411
1465
class HomePageTest(TestCase):
1412
- [...]
1466
+ def test_uses_home_template(self):
1467
+ [...]
1413
1468
1414
1469
def test_displays_all_list_items(self):
1415
1470
Item.objects.create(text="itemey 1")
1416
1471
Item.objects.create(text="itemey 2")
1417
-
1418
1472
response = self.client.get("/")
1473
+ self.assertContains(response, "itemey 1")
1474
+ self.assertContains(response, "itemey 2")
1419
1475
1420
- self.assertIn("itemey 1", response.content.decode())
1421
- self.assertIn("itemey 2", response.content.decode())
1476
+ def test_can_save_a_POST_request(self):
1477
+ [...]
1422
1478
----
1423
1479
====
1424
1480
1425
1481
1482
+ ////
1483
+ TODO: find a new home for this:
1426
1484
NOTE: Are you wondering about the line spacing in the test? I'm grouping
1427
1485
together two lines at the beginning which set up the test, one line in
1428
1486
the middle which actually calls the code under test, and the
1429
1487
assertions at the end. This isn't obligatory, but it does help see the
1430
1488
structure of the test. Arrange-Act-Assert is the typical structure
1431
1489
for a unit test.
1490
+ ////
1432
1491
1433
1492
1434
1493
That fails as expected:
1435
1494
1436
1495
----
1437
- AssertionError: 'itemey 1' not found in '<html>\n <head>\n [...]
1496
+ AssertionError: False is not true : Couldn't find 'itemey 1' in response
1438
1497
----
1439
1498
1440
- ((("templates", "tags", "{% for ... endfor %}")))((("{% for ... endfor %}")))The
1441
- Django template syntax has a tag for iterating through lists,
1442
- `{% for .. in .. %}`; we can use it like this:
1499
+ ((("templates", "tags", "{% for ... endfor %}")))
1500
+ ((("{% for ... endfor %}")))
1501
+ The Django template syntax has a tag for iterating through lists,
1502
+ `{% for .. in .. %}`; we can use it like this:
1443
1503
1444
1504
1445
1505
[role="sourcecode"]
1446
- .lists/templates/home.html
1506
+ .lists/templates/home.html (ch05l034)
1447
1507
====
1448
1508
[source,html]
1449
1509
----
@@ -1460,13 +1520,13 @@ will render with multiple `<tr>` rows, one for each item in the variable
1460
1520
`items`. Pretty neat! I'll introduce a few more bits of Django template
1461
1521
magic as we go, but at some point you'll want to go and read up on the rest of
1462
1522
them in the
1463
- https://docs.djangoproject.com/en/1.11 /topics/templates/[Django docs].
1523
+ https://docs.djangoproject.com/en/4.2 /topics/templates/[Django docs].
1464
1524
1465
1525
Just changing the template doesn't get our tests to green; we need to actually
1466
1526
pass the items to it from our home page view:
1467
1527
1468
1528
[role="sourcecode"]
1469
- .lists/views.py
1529
+ .lists/views.py (ch05l035)
1470
1530
====
1471
1531
[source,python]
1472
1532
----
@@ -1490,11 +1550,12 @@ $ pass:quotes[*python functional_tests.py*]
1490
1550
AssertionError: 'To-Do' not found in 'OperationalError at /'
1491
1551
----
1492
1552
1493
- ((("", startref="DTrender05")))((("debugging", "manual visits")))Oops, apparently not. Let's use another functional test debugging technique,
1494
- and it's one of the most straightforward: manually visiting the site! Open
1495
- up pass:[<em>http://localhost:8000</em>] in your web browser, and you'll see a Django debug
1496
- page saying "no such table: lists_item", as in <<operationalerror>>.
1497
-
1553
+ ((("", startref="DTrender05")))
1554
+ ((("debugging", "manual visits")))
1555
+ Oops, apparently not. Let's use another functional test debugging technique,
1556
+ and it's one of the most straightforward: manually visiting the site!
1557
+ Open up pass:[<em>http://localhost:8000</em>] in your web browser,
1558
+ and you'll see a Django debug page saying "no such table: lists_item", as in <<operationalerror>>.
1498
1559
1499
1560
1500
1561
[[operationalerror]]
@@ -1504,22 +1565,20 @@ image::images/twp2_0502.png["OperationalError at / no such table: lists_item"]
1504
1565
1505
1566
1506
1567
[role="pagebreak-before less_space"]
1507
- Creating Our Production Database with migrate
1508
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1568
+ === Creating Our Production Database with migrate
1509
1569
1570
+ ((("database testing", "production database creation", id="DTproduction05")))
1571
+ ((("database migrations")))
1572
+ Another helpful error message from Django,
1573
+ which is basically complaining that we haven't set up the database properly.
1574
+ How come everything worked fine in the unit tests, I hear you ask?
1575
+ Because Django creates a special 'test database' for unit tests;
1576
+ it's one of the magical things that Django's `TestCase` does.
1510
1577
1511
-
1512
- ((("database testing", "production database creation", id="DTproduction05")))((("database migrations")))Another
1513
- helpful error message from Django, which is basically complaining that
1514
- we haven't set up the database properly. How come everything worked fine
1515
- in the unit tests, I hear you ask? Because Django creates a special 'test
1516
- database' for unit tests; it's one of the magical things that Django's
1517
- `TestCase` does.
1518
-
1519
- To set up our "real" database, we need to create it. SQLite databases
1520
- are just a file on disk, and you'll see in 'settings.py' that Django,
1521
- by default, will just put it in a file called 'db.sqlite3' in the base
1522
- project directory:
1578
+ To set up our "real" database, we need to explicitly create it.
1579
+ SQLite databases are just a file on disk,
1580
+ and you'll see in 'settings.py' that Django, by default, will just put it in a file
1581
+ called 'db.sqlite3' in the base project directory:
1523
1582
1524
1583
[role="sourcecode currentcontents"]
1525
1584
.superlists/settings.py
@@ -1528,7 +1587,7 @@ project directory:
1528
1587
----
1529
1588
[...]
1530
1589
# Database
1531
- # https://docs.djangoproject.com/en/4.1 /ref/settings/#databases
1590
+ # https://docs.djangoproject.com/en/4.2 /ref/settings/#databases
1532
1591
1533
1592
DATABASES = {
1534
1593
"default": {
@@ -1539,10 +1598,10 @@ DATABASES = {
1539
1598
----
1540
1599
====
1541
1600
1542
- We've told Django everything it needs to create the database, first via
1543
- 'models.py' and then when we created the migrations file. To actually apply
1544
- it to creating a real database, we use another Django Swiss Army knife
1545
- 'manage.py' command, `migrate`:
1601
+ We've told Django everything it needs to create the database,
1602
+ first via 'models.py' and then when we created the migrations file.
1603
+ To actually apply it to creating a real database,
1604
+ we use another Django Swiss Army knife 'manage.py' command, `migrate`:
1546
1605
1547
1606
1548
1607
[subs="specialcharacters,macros"]
@@ -1584,18 +1643,12 @@ AssertionError: '2: Use peacock feathers to make a fly' not found in ['1: Buy
1584
1643
peacock feathers', '1: Use peacock feathers to make a fly']
1585
1644
----
1586
1645
1587
- // -- usually fails instead with:
1588
-
1589
- // selenium.common.exceptions.InvalidSelectorException: Message: Given css'
1590
- // selector expression "tr" is invalid: TypeError: can\'t access dead object'
1591
-
1592
-
1593
1646
1594
1647
So close! We just need to get our list numbering right. Another awesome
1595
1648
Django template tag, `forloop.counter`, will help here:
1596
1649
1597
1650
[role="sourcecode"]
1598
- .lists/templates/home.html
1651
+ .lists/templates/home.html (ch05l036)
1599
1652
====
1600
1653
[source,html]
1601
1654
----
@@ -1608,11 +1661,18 @@ Django template tag, `forloop.counter`, will help here:
1608
1661
1609
1662
If you try it again, you should now see the FT get to the end:
1610
1663
1664
+ [subs="specialcharacters,macros"]
1611
1665
----
1612
- self.fail("Finish the test!")
1613
- AssertionError: Finish the test!
1666
+ $ pass:quotes[*python functional_tests.py*]
1667
+ .
1668
+ ---------------------------------------------------------------------
1669
+ Ran 1 test in 5.036s
1670
+
1671
+ OK
1614
1672
----
1615
1673
1674
+ Hooray!
1675
+
1616
1676
But, as it's running, you may notice something is amiss, like in
1617
1677
<<items_left_over_from_previous_run>>.
1618
1678
@@ -1650,7 +1710,8 @@ And then (after restarting your server!) reassure yourself that the FT still
1650
1710
passes.
1651
1711
1652
1712
Apart from that little bug in our functional testing, we've got some code
1653
- that's more or less working. Let's do a commit.((("", startref="DTproduction05")))
1713
+ that's more or less working. Let's do a commit.
1714
+ ((("", startref="DTproduction05")))
1654
1715
1655
1716
1656
1717
Start by doing a *`git status`* and a *`git diff`*, and you should see changes
0 commit comments