@@ -310,7 +310,7 @@ And we fix it, still at the presentation level, in 'urls.py':
310
310
urlpatterns = [
311
311
path("new", views.new_list, name="new_list"),
312
312
path("<int:list_id>/", views.view_list, name="view_list"),
313
- path("users/<str:email>/", views.view_list , name="view_list"),
313
+ path("users/<str:email>/", views.my_list , name="view_list"),
314
314
]
315
315
----
316
316
====
@@ -323,29 +323,29 @@ That gives us a test failure, which informs us of what we should do as we
323
323
move down to the next level:
324
324
325
325
----
326
+ path("users/<str:email>/", views.my_lists, name="my_lists"),
327
+ ^^^^^^^^^^^^^^
326
328
AttributeError: module 'lists.views' has no attribute 'my_lists'
327
329
----
328
330
329
- * TODO: new django does not error in quite this way
330
-
331
331
332
332
We move in from the presentation layer to the views layer, and create a
333
333
minimal placeholder:
334
334
335
335
[role="sourcecode"]
336
- .lists/views.py (ch19l005 )
336
+ .lists/views.py (ch22l005 )
337
337
====
338
338
[source,python]
339
339
----
340
340
def my_lists(request, email):
341
- return render(request, ' my_lists.html' )
341
+ return render(request, " my_lists.html" )
342
342
----
343
343
====
344
344
345
345
And a minimal template:
346
346
347
347
[role="sourcecode"]
348
- .lists/templates/my_lists.html
348
+ .lists/templates/my_lists.html (ch22l006)
349
349
====
350
350
[source,html]
351
351
----
@@ -368,51 +368,47 @@ element: Reticulate splines
368
368
----
369
369
370
370
371
- Another Pass, Outside-In
372
- ~~~~~~~~~~~~~~~~~~~~~~~~
373
-
374
- ((("Outside-In TDD", "FT-driven development", id="OITDDft22")))At
375
- each stage, we still let the FT drive what development we do.
371
+ === Another Pass, Outside-In
376
372
377
- Starting again at the outside layer, in the template, we begin to
378
- write the template code we'd like to use to get the "My Lists" page to
379
- work the way we want it to. As we do so, we start to specify the API
380
- we want from the code at the layers below.
373
+ ((("Outside-In TDD", "FT-driven development", id="OITDDft22")))
374
+ At each stage, we still let the FT drive what development we do.
381
375
376
+ Starting again at the outside layer, in the template,
377
+ we begin to write the template code we'd like to use
378
+ to get the "My Lists" page to work the way we want it to.
379
+ As we do so, we start to specify the API we want
380
+ from the code at the layers below.
382
381
383
- A Quick Restructure of the Template Inheritance Hierarchy
384
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
385
382
383
+ ==== A Quick Restructure of the Template Inheritance Hierarchy
386
384
387
385
388
- ((("templates", "inheritance hierarchy")))Currently
389
- there's no place in our base template for us to put any new
390
- content. Also, the "My Lists" page doesn't need the new item form, so
391
- we'll put that into a block too, making it optional:
386
+ ((("templates", "inheritance hierarchy")))
387
+ Currently there's no place in our base template for us to put any new content.
388
+ Also, the "My Lists" page doesn't need the new item form,
389
+ so we'll put that into a block too, making it optional:
392
390
393
391
[role="sourcecode"]
394
392
.lists/templates/base.html (ch19l007-1)
395
393
====
396
- [source,html ]
394
+ [source,diff ]
397
395
----
398
- <div class="row">
399
- <div class="col-md-6 col-md-offset-3 jumbotron">
400
- <div class="text-center">
401
- <h1>{% block header_text %}{% endblock %}</h1>
402
- {% block list_form %}
403
- <form method="POST" action="{% block form_action %}{% endblock %}">
404
- {{ form.text }}
405
- {% csrf_token %}
406
- {% if form.errors %}
407
- <div class="form-group has-error">
408
- <div class="help-block">{{ form.text.errors }}</div>
409
- </div>
410
- {% endif %}
411
- </form>
412
- {% endblock %}
413
- </div>
414
- </div>
415
- </div>
396
+ @@ -57,6 +57,7 @@
397
+ <div class="col-lg-6 text-center">
398
+ <h1 class="display-1 mb-4">{% block header_text %}{% endblock %}</h1>
399
+
400
+ + {% block list_form %}
401
+ <form
402
+ method="POST"
403
+ action="{% block form_action %}{% endblock %}"
404
+ @@ -75,6 +76,8 @@
405
+ {% endif %}
406
+ {% csrf_token %}
407
+ </form>
408
+ + {% endblock %}
409
+ +
410
+ </div>
411
+ </div>
416
412
----
417
413
====
418
414
601
597
----
602
598
603
599
604
- The Next "Requirement" from the Views Layer: New Lists Should Record Owner
605
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
600
+ === The Next "Requirement" from the Views Layer: New Lists Should Record Owner
606
601
607
- ((("Outside-In TDD", "views layer")))Before
608
- we move down to the model layer, there's another part of the code
609
- at the views layer that will need to use our model: we need some way for
610
- newly created lists to be assigned to an owner, if the current user is
611
- logged in to the site.
602
+ ((("Outside-In TDD", "views layer")))
603
+ Before we move down to the model layer,
604
+ there's another part of the code at the views layer that will need to use our model:
605
+ we need some way for newly created lists to be assigned to an owner,
606
+ if the current user is logged in to the site.
612
607
613
608
Here's a first crack at writing the test:
614
609
615
610
616
611
[role="sourcecode"]
617
- .lists/tests/test_views.py (ch19l014 )
612
+ .lists/tests/test_views.py (ch22l014 )
618
613
====
619
614
[source,python]
620
615
----
621
616
class NewListTest(TestCase):
622
617
[...]
623
618
624
619
def test_list_owner_is_saved_if_user_is_authenticated(self):
625
- user = User.objects.create(email=' [email protected] ' )
620
+ user = User.objects.create(email=" [email protected] " )
626
621
self.client.force_login(user) #<1>
627
- self.client.post(' /lists/new' , data={' text': ' new item'})
628
- list_ = List.objects.first ()
629
- self.assertEqual(list_ .owner, user)
622
+ self.client.post(" /lists/new" , data={" text": " new item"})
623
+ new_list = List.objects.get ()
624
+ self.assertEqual(new_list .owner, user)
630
625
----
631
626
====
632
627
@@ -642,20 +637,20 @@ AttributeError: 'List' object has no attribute 'owner'
642
637
To fix this, we can try writing code like this:
643
638
644
639
[role="sourcecode"]
645
- .lists/views.py (ch19l015 )
640
+ .lists/views.py (ch22l015 )
646
641
====
647
642
[source,python]
648
643
----
649
644
def new_list(request):
650
645
form = ItemForm(data=request.POST)
651
646
if form.is_valid():
652
- list_ = List()
653
- list_ .owner = request.user
654
- list_ .save()
655
- form.save(for_list=list_ )
656
- return redirect(list_ )
647
+ nulist = List.objects.create ()
648
+ nulist .owner = request.user
649
+ nulist .save()
650
+ form.save(for_list=nulist )
651
+ return redirect(nulist )
657
652
else:
658
- return render(request, ' home.html' , {"form": form})
653
+ return render(request, " home.html" , {"form": form})
659
654
----
660
655
====
661
656
//015
@@ -671,30 +666,33 @@ AttributeError: 'List' object has no attribute 'owner'
671
666
----
672
667
673
668
674
- A Decision Point: Whether to Proceed to the Next Layer with a Failing Test
675
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
669
+ ==== A Decision Point: Whether to Proceed to the Next Layer with a Failing Test
676
670
671
+ * TODO: rewrite this section if we do decide to drop the next chapter.
677
672
678
- ((("Outside-In TDD", "model layer", id="OITDDmodel21")))In
679
- order to get this test passing, as it's written now, we have to move
680
- down to the model layer. However, it means doing more work with a failing
681
- test, which is not ideal.
682
673
683
- ((("mocks", "isolating tests using")))The
684
- alternative is to rewrite the test to make it more 'isolated' from the
685
- level below, using mocks.
674
+ ((("Outside-In TDD", "model layer", id="OITDDmodel21")))
675
+ In order to get this test passing, as it's written now,
676
+ we have to move down to the model layer.
677
+ However, it means doing more work with a failing test, which is not ideal.
686
678
687
- On the one hand, it's a lot more effort to use mocks, and it can lead to
688
- tests that are harder to read. On the other hand, imagine if our app was more
689
- complex, and there were several more layers between the outside and the inside.
690
- Imagine leaving three or four or five layers of tests, all failing while we
691
- wait to get to the bottom layer to implement our critical feature. While tests
692
- are failing, we're not sure that layer really works, on its own terms, or not.
679
+ ((("mocks", "isolating tests using")))
680
+ The alternative is to rewrite the test
681
+ to make it more _isolated_ from the level below, using mocks.
682
+
683
+ On the one hand, it's a lot more effort to use mocks,
684
+ and it can lead to tests that are harder to read.
685
+ On the other hand, imagine if our app was more complex,
686
+ and there were several more layers between the outside and the inside.
687
+ Imagine leaving three or four or five layers of tests, all failing
688
+ while we wait to get to the bottom layer to implement our critical feature.
689
+ While tests are failing, we're not sure that layer really works, on its own terms, or not.
693
690
We have to wait until we get to the bottom layer.
694
691
695
- This is a decision point you're likely to run into in your own projects. Let's
696
- investigate both approaches. We'll start by taking the shortcut, and leaving
697
- the test failing. In the next chapter, we'll come back to this exact point,
692
+ This is a decision point you're likely to run into in your own projects.
693
+ Let's investigate both approaches.
694
+ We'll start by taking the shortcut, and leaving the test failing.
695
+ In the next chapter, we'll come back to this exact point,
698
696
and investigate how things would have gone if we'd used more isolation.
699
697
700
698
Let's do a commit, and then 'tag' the commit as a way of remembering our
@@ -707,19 +705,19 @@ $ *git tag revisit_this_point_with_isolated_tests*
707
705
----
708
706
709
707
710
- Moving Down to the Model Layer
711
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
708
+ === Moving Down to the Model Layer
712
709
713
710
Our outside-in design has driven out two requirements for the model layer:
714
- we want to be able to assign an owner to a list using the attribute
715
- `.owner`, and we want to be able to access the list's owner with
716
- the API `owner.list_set.all`.
711
+ we want to be able to assign an owner to a list using the attribute `.owner`,
712
+ and we want to be able to access the list's owner with the API `owner.list_set.all()`.
713
+
714
+ // TODO: let's make this owner.lists.all() ?
717
715
718
716
Let's write a test for that:
719
717
720
718
721
719
[role="sourcecode"]
722
- .lists/tests/test_models.py (ch19l018 )
720
+ .lists/tests/test_models.py (ch22l018 )
723
721
====
724
722
[source,python]
725
723
----
@@ -765,7 +763,7 @@ that too:
765
763
766
764
767
765
[role="sourcecode"]
768
- .lists/tests/test_models.py (ch19l020 )
766
+ .lists/tests/test_models.py (ch22l020 )
769
767
====
770
768
[source,python]
771
769
----
@@ -777,18 +775,20 @@ that too:
777
775
The correct implementation is this:
778
776
779
777
[role="sourcecode"]
780
- .lists/models.py
778
+ .lists/models.py (ch22l021)
781
779
====
782
780
[source,python]
783
781
----
784
782
from django.conf import settings
785
783
[...]
786
784
787
785
class List(models.Model):
788
- owner = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True)
786
+ owner = models.ForeignKey(
787
+ settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.CASCADE
788
+ )
789
789
790
790
def get_absolute_url(self):
791
- return reverse(' view_list' , args=[self.id])
791
+ return reverse(" view_list" , args=[self.id])
792
792
----
793
793
====
794
794
//21
@@ -840,16 +840,17 @@ Django represents users using a class called `AnonymousUser`, whose
840
840
841
841
842
842
[role="sourcecode"]
843
- .lists/views.py (ch19l023 )
843
+ .lists/views.py (ch22l023 )
844
844
====
845
845
[source,python]
846
846
----
847
847
if form.is_valid():
848
- list_ = List()
848
+ nulist = List.objects.create ()
849
849
if request.user.is_authenticated:
850
- list_.owner = request.user
851
- list_.save()
852
- form.save(for_list=list_)
850
+ nulist.owner = request.user
851
+ nulist.save()
852
+ form.save(for_list=nulist)
853
+ return redirect(nulist)
853
854
[...]
854
855
----
855
856
====
0 commit comments