From 178e029be5eec1c623ead1e5e6157c3fe949ab25 Mon Sep 17 00:00:00 2001 From: Harry Date: Wed, 29 Jan 2025 21:26:05 +0000 Subject: [PATCH 1/6] A first round of changes in 24 --- chapter_24_outside_in.asciidoc | 79 +++++++++++++++++++++------------- 1 file changed, 50 insertions(+), 29 deletions(-) diff --git a/chapter_24_outside_in.asciidoc b/chapter_24_outside_in.asciidoc index 62ef5fc8..7cc047eb 100644 --- a/chapter_24_outside_in.asciidoc +++ b/chapter_24_outside_in.asciidoc @@ -105,15 +105,23 @@ from selenium.webdriver.common.by import By # She goes to the home page and starts a list self.browser.get(self.live_server_url) - self.add_list_item("Reticulate splines") + self.add_list_item("Reticulate splines") # <1> self.add_list_item("Immanentize eschaton") first_list_url = self.browser.current_url # She notices a "My lists" link, for the first time. self.browser.find_element(By.LINK_TEXT, "My lists").click() - # She sees that her list is in there, named according to its - # first list item + # She sees her email is there in the page heading + self.wait_for( + lambda: self.assertIn( + "edith@example.com", + self.browser.find_element(By.CSS_SELECTOR, "h1").text, + ) + ) + + # And she sees that her list is in there, + # named according to its first list item self.wait_for( lambda: self.browser.find_element(By.LINK_TEXT, "Reticulate splines") ) @@ -124,11 +132,10 @@ from selenium.webdriver.common.by import By ---- ==== +<1> We'll define this `add_list_item()` shortly. + Programming by wishful thinking, as always! -//TODO: add a check for email in my lists page header, reflow logic -// to fit better with workshops.. - -We create a list with a couple of items, +As you can see, we create a list with a couple of items, then we check that this list appears on a new "My Lists" page, and that it's "named" after the first item in the list. @@ -219,7 +226,6 @@ changes--see if you agree with me. A quick run of all FTs, a commit, and then back to the FT we're working on. The first error should look like this: -//IDEA: add a thing that looks for her email address in an h1? [subs="specialcharacters,macros"] ---- @@ -250,29 +256,30 @@ Here's the minimal code change:
Superlists {% if user.email %} - My lists + My lists Logged in as {{ user.email }}
[...] ---- ==== -Of course, that link doesn't actually go anywhere, but it does get us along to -the next failure: +Of course that link doesn't actually go anywhere, +but it does get us along to the next failure: -* TODO: address issue with default list item ordering here. -[subs="specialcharacters,macros"] +[subs=""] ---- -$ pass:quotes[*python src/manage.py test functional_tests.test_my_lists*] -[...] - self.wait_for( - ~~~~~~~~~~~~~^ - lambda: self.browser.find_element(By.LINK_TEXT, "Reticulate splines") - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +$ python src/manage.py test functional_tests.test_my_lists [...] -selenium.common.exceptions.NoSuchElementException: Message: Unable to locate -element: Reticulate splines; [...] + lambda: self.assertIn( + ~~~~~~~~~~~~~^ + "edith@example.com", + ^^^^^^^^^^^^^^^^^^^^ + self.browser.find_element(By.CSS_SELECTOR, "h1").text, + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + ) + ^ +AssertionError: 'edith@example.com' not found in 'Your To-Do list' ---- Which is telling us we're going to have to build a page that lists all of a @@ -288,7 +295,7 @@ URL and nothing else: [source,html] ---- {% if user.email %} - My lists + My lists ---- ==== @@ -360,7 +367,8 @@ def my_lists(request, email): ---- ==== -And a minimal template: +And a minimal template, with no real content +except for the header that shows the user's email address: [role="sourcecode"] .src/lists/templates/my_lists.html (ch22l010) @@ -369,13 +377,20 @@ And a minimal template: ---- {% extends 'base.html' %} -{% block header_text %}My Lists{% endblock %} +{% block header_text %}{{user.email}}'s Lists{% endblock %} ---- ==== -That gets our unit tests passing, but our FT is still at the same point, -saying that the "My Lists" page doesn't yet show any lists. It wants -them to be clickable links named after the first item: +That gets our unit tests passing. + +[subs="specialcharacters,quotes"] +---- +$ *./src/manage.py test lists* +[...] +OK +---- + +Let's rerun our FT: [subs="specialcharacters,macros"] ---- @@ -385,6 +400,10 @@ selenium.common.exceptions.NoSuchElementException: Message: Unable to locate element: Reticulate splines; [...] ---- +Sure enough, the FT get a little further because it finds our email in the `

`, +but it's now saying that the "My Lists" page doesn't yet show any lists. +It wants them to appear as clickable links, named after the first item. + === Another Pass, Outside-In @@ -473,7 +492,7 @@ and say it should be empty... ---- {% extends 'base.html' %} -{% block header_text %}My Lists{% endblock %} +{% block header_text %}{{user.email}}'s Lists{% endblock %} {% block list_form %}{% endblock %} ---- @@ -486,7 +505,9 @@ And then we can just work inside the `extra_content` block: ==== [source,html] ---- -[...] +{% extends 'base.html' %} + +{% block header_text %}{{user.email}}'s Lists{% endblock %} {% block list_form %}{% endblock %} From 8872683471a1b7d40a5a7b2d11921de8c15380f9 Mon Sep 17 00:00:00 2001 From: Harry Date: Thu, 30 Jan 2025 19:17:40 +0000 Subject: [PATCH 2/6] start working down. rename all commit refs --- chapter_24_outside_in.asciidoc | 101 ++++++++++++++++------------ tests/test_chapter_24_outside_in.py | 2 +- 2 files changed, 60 insertions(+), 43 deletions(-) diff --git a/chapter_24_outside_in.asciidoc b/chapter_24_outside_in.asciidoc index 7cc047eb..d02f8377 100644 --- a/chapter_24_outside_in.asciidoc +++ b/chapter_24_outside_in.asciidoc @@ -92,7 +92,7 @@ to look for a "My Lists" page: [role="sourcecode"] -.src/functional_tests/test_my_lists.py (ch22l001) +.src/functional_tests/test_my_lists.py (ch24l001) ==== [source,python] ---- @@ -145,7 +145,7 @@ The FT continues, and while we're at it, we check that only logged-in users can see the "My Lists" page: [role="sourcecode"] -.src/functional_tests/test_my_lists.py (ch22l002) +.src/functional_tests/test_my_lists.py (ch24l002) ==== [source,python] ---- @@ -184,7 +184,7 @@ We define it in 'base.py': [role="sourcecode small-code"] -.src/functional_tests/base.py (ch22l003) +.src/functional_tests/base.py (ch24l003) ==== [source,python] ---- @@ -205,8 +205,8 @@ And while we're at it we can use it in a few of the other FTs, like this for example: -[role="sourcecode dofirst-ch22l004-1"] -.src/functional_tests/test_layout_and_styling.py (ch22l004-2) +[role="sourcecode dofirst-ch24l004-1"] +.src/functional_tests/test_layout_and_styling.py (ch24l004-2) ==== [source,diff] ---- @@ -220,8 +220,8 @@ like this for example: ---- ==== -I think it makes the FTs a lot more readable. I made a total of six -changes--see if you agree with me. +I think it makes the FTs a lot more readable. +I made a total of six changes--see if you agree with me. A quick run of all FTs, a commit, and then back to the FT we're working on. The first error should look like this: @@ -245,10 +245,8 @@ We can address that at the presentation layer, in _base.html_, in our navigation Here's the minimal code change: -* TODO: update this link for latest bootstrap / style nicely - [role="sourcecode small-code"] -.src/lists/templates/base.html (ch22l005) +.src/lists/templates/base.html (ch24l005) ==== [source,html] ---- @@ -263,8 +261,8 @@ Here's the minimal code change: ---- ==== -Of course that link doesn't actually go anywhere, -but it does get us along to the next failure: +Of course the `href="#"` means that link doesn't actually go anywhere, +but it _does_ get our FT along to the next failure: [subs=""] @@ -282,15 +280,15 @@ $ python src/manage.py test functional_tests.test_my_lists AssertionError: 'edith@example.com' not found in 'Your To-Do list' ---- -Which is telling us we're going to have to build a page that lists all of a -user's lists by title. Let's start with the basics--a URL and a placeholder -template for it. +Which is telling us we're going to have to build a page +that at least has the user's email in its header. +Let's start with the basics--a URL and a placeholder template for it. -Again, we can go outside-in, starting at the presentation layer with just the -URL and nothing else: +Again, we can go outside-in, +starting at the presentation layer with just the URL and nothing else: [role="sourcecode"] -.src/lists/templates/base.html (ch22l006) +.src/lists/templates/base.html (ch24l006) ==== [source,html] ---- @@ -303,14 +301,33 @@ URL and nothing else: === Moving Down One Layer to View Functions (the Controller) ((("Outside-In TDD", "controller layer"))) -That will cause a template error, so we'll start to move down from the -presentation layer and URLs down to the controller layer, Django's view -functions. +That will cause a template error in the FT: + +[subs=""] +---- +$ ./src/manage.py test functional_tests.test_my_lists +[...] +Internal Server Error: / +[...] + File "...goat-book/src/lists/views.py", line 8, in home_page + return render(request, "home.html", {"form": ItemForm()}) +[...] +django.urls.exceptions.NoReverseMatch: Reverse for 'my_lists' not found. +'my_lists' is not a valid view function or pattern name. +[...] +ERROR: test_logged_in_users_lists_are_saved_as_my_lists [...] +[...] +selenium.common.exceptions.NoSuchElementException: [...] +---- + +To fix it, let's move down from the presentation layer and URLs +down to the controller layer, Django's view functions. -As always, we start with a test: +As always, we start with a test. +In this layer, a unit test is the way to go: [role="sourcecode"] -.src/lists/tests/test_views.py (ch22l007) +.src/lists/tests/test_views.py (ch24l007) ==== [source,python] ---- @@ -327,11 +344,11 @@ That gives: AssertionError: No templates used to render the response ---- -And we fix it, still at the presentation level, in 'urls.py': +And we fix it, still at the presentation level, in _urls.py_: [role="sourcecode"] -.src/lists/urls.py (ch22l008) +.src/lists/urls.py (ch24l008) ==== [source,python] ---- @@ -358,7 +375,7 @@ We move in from the presentation layer to the views layer, and create a minimal placeholder: [role="sourcecode"] -.src/lists/views.py (ch22l009) +.src/lists/views.py (ch24l009) ==== [source,python] ---- @@ -371,7 +388,7 @@ And a minimal template, with no real content except for the header that shows the user's email address: [role="sourcecode"] -.src/lists/templates/my_lists.html (ch22l010) +.src/lists/templates/my_lists.html (ch24l010) ==== [source,html] ---- @@ -424,7 +441,7 @@ from the code at the layers below. Currently there's no place in our base template for us to put any new content. [role="sourcecode"] -.src/lists/templates/base.html (ch22l011-2) +.src/lists/templates/base.html (ch24l011-2) ==== [source,html] ---- @@ -452,7 +469,7 @@ Also, the "My Lists" page doesn't need the new item form, so we'll put that into a block too, making it optional. [role="sourcecode"] -.src/lists/templates/base.html (ch22l011-1) +.src/lists/templates/base.html (ch24l011-1) ==== [source,diff] ---- @@ -486,7 +503,7 @@ and say it should be empty... // TODO: proper commits for these [role="sourcecode"] -.src/lists/templates/my_lists.html (ch22l010-1) +.src/lists/templates/my_lists.html (ch24l010-1) ==== [source,html] ---- @@ -501,7 +518,7 @@ and say it should be empty... And then we can just work inside the `extra_content` block: [role="sourcecode"] -.src/lists/templates/my_lists.html (ch22l010-2) +.src/lists/templates/my_lists.html (ch24l010-2) ==== [source,html] ---- @@ -580,7 +597,7 @@ by giving it the objects it needs. In this case, the list owner: [role="sourcecode"] -.src/lists/tests/test_views.py (ch22l011) +.src/lists/tests/test_views.py (ch24l011) ==== [source,python] ---- @@ -611,7 +628,7 @@ KeyError: 'owner' So: [role="sourcecode"] -.src/lists/views.py (ch22l012) +.src/lists/views.py (ch24l012) ==== [source,python] ---- @@ -632,7 +649,7 @@ the previous test. We just need to add a user for it as well: [role="sourcecode"] -.src/lists/tests/test_views.py (ch22l013) +.src/lists/tests/test_views.py (ch24l013) ==== [source,python] ---- @@ -663,7 +680,7 @@ Here's a first crack at writing the test: [role="sourcecode"] -.src/lists/tests/test_views.py (ch22l014) +.src/lists/tests/test_views.py (ch24l014) ==== [source,python] ---- @@ -691,7 +708,7 @@ AttributeError: 'List' object has no attribute 'owner' To fix this, we can try writing code like this: [role="sourcecode"] -.src/lists/views.py (ch22l015) +.src/lists/views.py (ch24l015) ==== [source,python] ---- @@ -762,7 +779,7 @@ Let's write a test for that: [role="sourcecode"] -.src/lists/tests/test_models.py (ch22l018) +.src/lists/tests/test_models.py (ch24l018) ==== [source,python] ---- @@ -810,7 +827,7 @@ that too: [role="sourcecode"] -.src/lists/tests/test_models.py (ch22l020) +.src/lists/tests/test_models.py (ch24l020) ==== [source,python] ---- @@ -822,7 +839,7 @@ that too: The correct implementation is this: [role="sourcecode"] -.src/lists/models.py (ch22l021) +.src/lists/models.py (ch24l021) ==== [source,python] ---- @@ -890,7 +907,7 @@ whose `.is_authenticated` is always `False`): [role="sourcecode"] -.src/lists/views.py (ch22l023) +.src/lists/views.py (ch24l023) ==== [source,python] ---- @@ -934,7 +951,7 @@ which wanted to be able to access a list "name" based on the text of its first item: [role="sourcecode"] -.src/lists/tests/test_models.py (ch22l024) +.src/lists/tests/test_models.py (ch24l024) ==== [source,python] ---- @@ -948,7 +965,7 @@ its first item: [role="sourcecode"] -.src/lists/models.py (ch22l025) +.src/lists/models.py (ch24l025) ==== [source,python] ---- diff --git a/tests/test_chapter_24_outside_in.py b/tests/test_chapter_24_outside_in.py index 15ce2607..c769bf8e 100644 --- a/tests/test_chapter_24_outside_in.py +++ b/tests/test_chapter_24_outside_in.py @@ -5,7 +5,7 @@ from book_tester import ChapterTest -class Chapter19Test(ChapterTest): +class Chapter24Test(ChapterTest): chapter_name = "chapter_24_outside_in" previous_chapter = "chapter_23_debugging_prod" From a5c27e3b98872017fbf0c27cc18913e65678048c Mon Sep 17 00:00:00 2001 From: Harry Date: Thu, 30 Jan 2025 19:26:04 +0000 Subject: [PATCH 3/6] tweak some wording about layers for the first pass. got to the second one --- chapter_24_outside_in.asciidoc | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/chapter_24_outside_in.asciidoc b/chapter_24_outside_in.asciidoc index d02f8377..97dcd501 100644 --- a/chapter_24_outside_in.asciidoc +++ b/chapter_24_outside_in.asciidoc @@ -320,8 +320,8 @@ ERROR: test_logged_in_users_lists_are_saved_as_my_lists [...] selenium.common.exceptions.NoSuchElementException: [...] ---- -To fix it, let's move down from the presentation layer and URLs -down to the controller layer, Django's view functions. +To fix it, we'll need to start to move from working at the presentation layer, +gradually into the controller layer, Django's view functions. As always, we start with a test. In this layer, a unit test is the way to go: @@ -344,7 +344,8 @@ That gives: AssertionError: No templates used to render the response ---- -And we fix it, still at the presentation level, in _urls.py_: +That's because the URL doesn't exist yet, and a 404 has no template. +Let's start our fix in _urls.py_: [role="sourcecode"] @@ -361,8 +362,10 @@ urlpatterns = [ ==== -That gives us a test failure, which informs us of what we should do as we -move down to the next level: +That gives us a new test failure, +which informs us of what we should do. +As you can see, it's pointing us at a _views.py_, +we're clearly in the controller layer: ---- path("users//", views.my_lists, name="my_lists"), @@ -371,8 +374,7 @@ AttributeError: module 'lists.views' has no attribute 'my_lists' ---- -We move in from the presentation layer to the views layer, and create a -minimal placeholder: +Let's create a minimal placeholder then: [role="sourcecode"] .src/lists/views.py (ch24l009) @@ -417,13 +419,16 @@ selenium.common.exceptions.NoSuchElementException: Message: Unable to locate element: Reticulate splines; [...] ---- -Sure enough, the FT get a little further because it finds our email in the `

`, +Sure enough, the FT get a little further. It _can_ now find the email in the `

`, but it's now saying that the "My Lists" page doesn't yet show any lists. It wants them to appear as clickable links, named after the first item. === Another Pass, Outside-In +// TODO resume here. + + ((("Outside-In TDD", "FT-driven development", id="OITDDft22"))) At each stage, we still let the FT drive what development we do. From 20e6e49e125fce704e1474f623de9578cdf405c6 Mon Sep 17 00:00:00 2001 From: Harry Date: Mon, 3 Feb 2025 20:32:24 +0000 Subject: [PATCH 4/6] wip on moving to include maybe --- chapter_24_outside_in.asciidoc | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/chapter_24_outside_in.asciidoc b/chapter_24_outside_in.asciidoc index 97dcd501..0f87530f 100644 --- a/chapter_24_outside_in.asciidoc +++ b/chapter_24_outside_in.asciidoc @@ -409,7 +409,7 @@ $ *./src/manage.py test lists* OK ---- -Let's rerun our FT: +And hopefully it will address the current error in our FT: [subs="specialcharacters,macros"] ---- @@ -419,31 +419,32 @@ selenium.common.exceptions.NoSuchElementException: Message: Unable to locate element: Reticulate splines; [...] ---- -Sure enough, the FT get a little further. It _can_ now find the email in the `

`, +Step by step! Sure enough, the FT get a little further. +It _can_ now find the email in the `

`, but it's now saying that the "My Lists" page doesn't yet show any lists. It wants them to appear as clickable links, named after the first item. === Another Pass, Outside-In -// TODO resume here. - ((("Outside-In TDD", "FT-driven development", id="OITDDft22"))) -At each stage, we still let the FT drive what development we do. +At each stage, we're still letting the FT drive what development we do. Starting again at the outside layer, in the template, we begin to write the template code we'd like to use -to get the "My Lists" page to work the way we want it to. +to get the "My Lists" page to work the way we want it to. As we do so, we start to specify the API we want from the code at the layers below. +Programming by wishful thinking, as always! ==== A Quick Restructure of the Template Inheritance Hierarchy ((("templates", "inheritance hierarchy"))) -Currently there's no place in our base template for us to put any new content. +Currently there's no place in our base template for us to put any new content, +so let's change that first by adding a new block called `extra_content`: [role="sourcecode"] .src/lists/templates/base.html (ch24l011-2) @@ -471,7 +472,7 @@ Currently there's no place in our base template for us to put any new content. ==== Also, the "My Lists" page doesn't need the new item form, -so we'll put that into a block too, making it optional. +so let's pull that out into an https://docs.djangoproject.com/en/5.1/ref/templates/builtins/#include[include] [role="sourcecode"] .src/lists/templates/base.html (ch24l011-1) From 1b6ed2597838db80bfb1c83f678e51a97770c896 Mon Sep 17 00:00:00 2001 From: Harry Date: Wed, 5 Feb 2025 18:18:04 +0000 Subject: [PATCH 5/6] do lots of includes in 24 --- chapter_24_outside_in.asciidoc | 334 ++++++++++++++++++++++++++++----- 1 file changed, 291 insertions(+), 43 deletions(-) diff --git a/chapter_24_outside_in.asciidoc b/chapter_24_outside_in.asciidoc index 0f87530f..d3c6baaf 100644 --- a/chapter_24_outside_in.asciidoc +++ b/chapter_24_outside_in.asciidoc @@ -439,92 +439,342 @@ from the code at the layers below. Programming by wishful thinking, as always! -==== A Quick Restructure of the Template Inheritance Hierarchy +==== A Quick Restructure of Our Template Composition +((("templates", "composition"))) +Let's take a look at our base template, _base.html_. +It currently has a lot of content that's specific to editing todo lists, +which our "My Lists" page doesn't need: -((("templates", "inheritance hierarchy"))) -Currently there's no place in our base template for us to put any new content, -so let's change that first by adding a new block called `extra_content`: - -[role="sourcecode"] -.src/lists/templates/base.html (ch24l011-2) +[role="sourcecode currentcontents"] +.src/lists/templates/base.html ==== [source,html] ---- -
-
- {% block table %} - {% endblock %} +
+ + + + {% if messages %} + [...] + {% endif %} + +
+
+

{% block header_text %}{% endblock %}

+ + <1> + [...] +
-
-
- {% block extra_content %} +
+
+ {% block table %} <2> {% endblock %}
+
- - [...] + <3> + [...] ---- ==== -Also, the "My Lists" page doesn't need the new item form, -so let's pull that out into an https://docs.djangoproject.com/en/5.1/ref/templates/builtins/#include[include] +<1> The `
` tag is definitely something we only want on pages where we edit lists. + Everything else up to this point is generic enough to be on any page. -[role="sourcecode"] -.src/lists/templates/base.html (ch24l011-1) +<2> Similarly the `{% block table %}` isn't something we'd need on the "My Lists" page. + +<3> Finally the ` +- ++ {% block scripts %} ++ {% endblock %} + + + ---- ==== +You can see we've replaced all the lists-specific stuff with 3 new blocks: +* `extra_header` for anything we want to put in the big header section +* `content` for the main content of the page +* `scripts` for any javascript we want to include. -==== Designing Our API Using the Template +Let's paste in the `` tag into a file at _src/lists/templates/form.html_: -((("templates", "designing APIs using"))) -Meanwhile, in _my_lists.html_ we override the `list_form` -and say it should be empty... -// TODO: proper commits for these +[role="sourcecode small-code"] +.src/lists/templates/form.html (ch24l010-2) +==== +[source,html] +---- + <1> + {% csrf_token %} + + {% if form.errors %} +
+ {{ form.errors.text.0 }} +
+ {% endif %} +
+---- +==== + +<1> This is the only change, + we've replaced the `{% block form_action %}` with `{{ form_action }}`. + + +Let's paste the scripts tags verbatim +into a new file at _src/lists/templates/scripts.html_: [role="sourcecode"] -.src/lists/templates/my_lists.html (ch24l010-1) +.src/lists/templates/scripts.html (ch24l010-3) +==== +[source,html] +---- + + + +---- +==== + +Now let's look at how to use the include, +and how the `form_action` change plays out, +in the changes to _home.html_: + +[role="sourcecode small-code"] +.src/lists/templates/home.html (ch24l010-4) ==== [source,html] ---- {% extends 'base.html' %} -{% block header_text %}{{user.email}}'s Lists{% endblock %} +{% block header_text %}Start a new To-Do list{% endblock %} -{% block list_form %}{% endblock %} +{% block extra_header %} + {% url 'new_list' as form_action %} <1> + {% include "form.html" with form=form form_action=form_action %} <2> +{% endblock %} + +{% block scripts %} <3> + {% include "scripts.html" %} +{% endblock %} ---- ==== -And then we can just work inside the `extra_content` block: +<1> The `{% url ... as %}` syntax lets us define a template variable in-line + +<2> Then we use `{% include ... with key=value... %}` + to pull in the contents of the `form.html` template, + with the appropriate context variables passed in--a bit like + calling a function. + +<3> The `scripts` block is just a straightforward `include` + with no variables. + +Now let's see it in _list.html_: [role="sourcecode"] -.src/lists/templates/my_lists.html (ch24l010-2) +.src/lists/templates/list.html (ch24l010-5) +==== +[source,diff] +---- +@@ -2,12 +2,24 @@ + + {% block header_text %}Your To-Do list{% endblock %} + +-{% block form_action %}{% url 'view_list' list.id %}{% endblock %} + +-{% block table %} ++{% block extra_header %} <1> ++ {% url 'view_list' list.id as form_action %} ++ {% include "form.html" with form=form form_action=form_action %} ++{% endblock %} ++ ++{% block content %} <2> ++
++
+ + {% for item in list.item_set.all %} + + {% endfor %} +
{{ forloop.counter }}: {{ item.text }}
++
++
++{% endblock %} ++ ++{% block scripts %} <3> ++ {% include "scripts.html" %} + {% endblock %} + +---- +==== + +<1> The `block table` becomes an `extra_header` block, + and we use the `include` to pull in the form. + +<2> The `block table` becomes a `content` block, + with all the html we need for our table. + +<3> And the scripts block is the same as the one from _home.html_. + + +We can have a little click around our site, +and then a little re-run of all our FTs to make sure we haven't broken anything, and then commit. + +[subs="specialcharacters,quotes"] +---- +$ *./src/manage.py test functional_tests* +[...] +selenium.common.exceptions.NoSuchElementException: Message: Unable to locate +element: Reticulate splines; [...] +[..] +Ran 8 tests in X.Xs + +FAILED (errors=1) +---- + +8 tests with 1 failure, the same one we had before, we haven't broken anything. Hooray! + +[subs="specialcharacters,quotes"] +---- +$ *git add src/lists/templates* +$ *git commit -m "refactor templates to use composition/includes"* +---- + + +Now let's get back to our outside-in process, +and working in our template to drive out the requirements +for our views layer: + + +==== Designing Our API Using the Template + +((("templates", "designing APIs using"))) +So, in _my_lists.html_ we can now work in the `content` block: + +[role="sourcecode"] +.src/lists/templates/my_lists.html (ch24l010-6) ==== [source,html] ---- @@ -532,9 +782,7 @@ And then we can just work inside the `extra_content` block: {% block header_text %}{{user.email}}'s Lists{% endblock %} -{% block list_form %}{% endblock %} - -{% block extra_content %} +{% block content %}

{{ owner.email }}'s lists

<1>
    {% for list in owner.list_set.all %} <2> From 866c35c8972ecc98240996dfe97dec98f8adf848 Mon Sep 17 00:00:00 2001 From: Harry Date: Wed, 12 Feb 2025 22:28:26 +0000 Subject: [PATCH 6/6] finish of and feed thru --- chapter_24_outside_in.asciidoc | 111 ++++++++++++---------- chapter_26_page_pattern.asciidoc | 8 +- source/chapter_24_outside_in/superlists | 2 +- source/chapter_25_CI/superlists | 2 +- source/chapter_26_page_pattern/superlists | 2 +- 5 files changed, 68 insertions(+), 57 deletions(-) diff --git a/chapter_24_outside_in.asciidoc b/chapter_24_outside_in.asciidoc index d3c6baaf..84180ebf 100644 --- a/chapter_24_outside_in.asciidoc +++ b/chapter_24_outside_in.asciidoc @@ -133,7 +133,8 @@ from selenium.webdriver.common.by import By ==== <1> We'll define this `add_list_item()` shortly. - Programming by wishful thinking, as always! + +// Programming by wishful thinking, as always! As you can see, we create a list with a couple of items, then we check that this list appears on a new "My Lists" page, @@ -436,7 +437,8 @@ we begin to write the template code we'd like to use to get the "My Lists" page to work the way we want it to. As we do so, we start to specify the API we want from the code at the layers below. -Programming by wishful thinking, as always! + +// Programming by wishful thinking, as always! ==== A Quick Restructure of Our Template Composition @@ -748,7 +750,7 @@ $ *./src/manage.py test functional_tests* [...] selenium.common.exceptions.NoSuchElementException: Message: Unable to locate element: Reticulate splines; [...] -[..] +[...] Ran 8 tests in X.Xs FAILED (errors=1) @@ -785,7 +787,7 @@ So, in _my_lists.html_ we can now work in the `content` block: {% block content %}

    {{ owner.email }}'s lists

    <1>
      - {% for list in owner.list_set.all %} <2> + {% for list in owner.lists.all %} <2>
    • {{ list.name }}
    • <3> {% endfor %}
    @@ -799,21 +801,21 @@ which are going to filter their way down through the code: <1> We want a variable called `owner` to represent the user in our template. <2> We want to be able to iterate through the lists created by the user - using `owner.list_set.all` - (I happen to know we get this for free from the Django ORM). + using `owner.lists.all` + (I happen to know how to make this work with the Django ORM). <3> We want to use `list.name` to print out the "name" of the list, which is currently specified as the text of its first element. NOTE: Outside-In TDD is sometimes called "programming by wishful thinking",footnote:[ - This phrase "programming by wishful thinking" was perhaps first used in + The phrase "programming by wishful thinking" was first popularised by the amazing, mind-expanding textbook https://en.wikipedia.org/wiki/Structure_and_Interpretation_of_Computer_Programs[SICP], which I _cannot_ recommend highly enough.] and you can see why. We start writing code at the higher levels - based on what we wish we had at the lower levels, - even though it doesn't exist yet! + based on what we _wish_ we had at the lower levels, even though it doesn't exist yet... + A bit like when we write test for code that doesn't exist yet! We can rerun our FTs, to check that we didn't break anything, @@ -898,8 +900,8 @@ def my_lists(request, email): ---- ==== -That gets our new test passing, but we'll also see an error from -the previous test. We just need to add a user for it as well: +That gets our new test passing, but we'll also see an error from the previous test. +We just need to add a user for it as well: [role="sourcecode"] @@ -913,8 +915,8 @@ the previous test. We just need to add a user for it as well: ---- ==== -((("", startref="OITDDft22")))And -we get to an OK: +And we get to an OK: +((("", startref="OITDDft22"))) ---- @@ -994,9 +996,6 @@ AttributeError: 'List' object has no attribute 'owner' ==== A Decision Point: Whether to Proceed to the Next Layer with a Failing Test -* TODO: rewrite this section if we do decide to drop the next chapter. - - ((("Outside-In TDD", "model layer", id="OITDDmodel21"))) In order to get this test passing, as it's written now, we have to move down to the model layer. @@ -1011,7 +1010,10 @@ and it can lead to tests that are harder to read. On the other hand, advocates of what's known as "London School" TDD are very keen on the approach. Read more in <>. -Let's do a commit, and then 'tag' the commit as a way of remembering our +For now we'll accept the tradeoff, moving down one layer with failing tests, +but avoiding the extra mocks. + +Let's do a commit, and then _tag_ the commit as a way of remembering our position for that appendix: [subs="specialcharacters,quotes"] @@ -1025,9 +1027,8 @@ $ *git tag revisit_this_point_with_isolated_tests* Our outside-in design has driven out two requirements for the model layer: we want to be able to assign an owner to a list using the attribute `.owner`, -and we want to be able to access the list's owner with the API `owner.list_set.all()`. +and we want to be able to access the list's owner with the API `owner.lists.all()`. -// TODO: let's make this owner.lists.all() ? Let's write a test for that: @@ -1051,7 +1052,7 @@ class ListModelTest(TestCase): def test_lists_can_have_owners(self): user = User.objects.create(email="a@b.com") mylist = List.objects.create(owner=user) - self.assertIn(mylist, user.list_set.all()) + self.assertIn(mylist, user.lists.all()) ---- ==== @@ -1102,7 +1103,11 @@ from django.conf import settings class List(models.Model): owner = models.ForeignKey( - settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.CASCADE + settings.AUTH_USER_MODEL, + related_name="lists", + blank=True, + null=True, + on_delete=models.CASCADE, ) def get_absolute_url(self): @@ -1130,7 +1135,7 @@ Migrations for 'lists': ---- //22 -We're almost there; a couple more failures: +We're almost there; a couple more failures in some of our old tests: ---- ERROR: test_can_save_a_POST_request @@ -1148,12 +1153,13 @@ be a "User" instance. ---- - We're moving back up to the views layer now, just doing a little tidying up. -Notice that these are in the old test for the `new_list` view, +Notice that these are in the existing test for the `new_list` view, when we haven't got a logged-in user. -We should only save the list owner when the user is actually logged in. -The `.is_authenticated` attribute we defined in <> + +The tests are reminding us to think of this use case too: +we should only save the list owner when the user is actually logged in. +The `.is_authenticated` attribute we came across in <> comes in useful now (when they're not logged in, Django represents users using a class called `AnonymousUser`, @@ -1241,17 +1247,22 @@ Ran 8 tests in 93.819s OK ---- +[[my-lists-page]] +.The "My Lists" page, in all its glory (and proof I did test on Windows) +image::images/twp2_2201.png["Screenshot of new My Lists page"] + .The @property Decorator in Python ******************************************************************************* -((("@property decorator")))((("decorators", "property decorator")))((("Python 3", "@property decorator")))If -you haven't seen it before, the `@property` decorator transforms a method +((("@property decorator"))) +((("decorators", "property decorator"))) +((("Python 3", "@property decorator"))) +If you haven't seen it before, the `@property` decorator transforms a method on a class to make it appear to the outside world like an attribute. - -((("duck typing")))This -is a powerful feature of the language, because it makes it easy to +((("duck typing"))) +This is a powerful feature of the language, because it makes it easy to implement "duck typing", to change the implementation of a property without changing the interface of the class. In other words, if we decide to change `.name` into being a "real" attribute on the model, which is stored as text in @@ -1268,40 +1279,40 @@ even if it didn't have `@property`, but that's a particularity of Django, and doesn't apply to Python in general... ******************************************************************************* -((("", startref="OITDDmodel21")))But -we know we cheated to get there. The Testing Goat is eyeing us +((("", startref="OITDDmodel21"))) +But we know we cheated to get there. The Testing Goat is eyeing us suspiciously. We left a test failing at one layer while we implemented its dependencies at the lower layer. Let's see how things would play out if we were to use better test isolation... -[[my-lists-page]] -.The "My Lists" page, in all its glory (and proof I did test on Windows) -image::images/twp2_2201.png["Screenshot of new My Lists page"] - .Outside-In TDD ******************************************************************************* Outside-In TDD:: - ((("Outside-In TDD", "defined")))A -methodology for building code, driven by tests, which proceeds by - starting from the "outside" layers (presentation, GUI), and moving - "inwards" step by step, via view/controller layers, down towards - the model layer. The idea is to drive the design of your code from - the use to which it is going to be put, rather than trying to anticipate - requirements from the ground up. + A methodology for building code, driven by tests, + which proceeds by starting from the "outside" layers (presentation, GUI), + and moving "inwards" step by step, via view/controller layers, + down towards the model layer. + The idea is to drive the design of your code from how it will be used, + rather than trying to anticipate requirements from the bottom up. + ((("Outside-In TDD", "defined"))) Programming by wishful thinking:: - ((("programming by wishful thinking")))The -outside-in process is sometimes called "programming by wishful - thinking". Actually, any kind of TDD involves some wishful thinking. + The outside-in process is sometimes called "programming by wishful thinking". + Actually, any kind of TDD involves some wishful thinking. We're always writing tests for things that don't exist yet. + ((("programming by wishful thinking"))) The pitfalls of outside-in:: - ((("Outside-In TDD", "drawbacks of")))Outside-in isn't a silver bullet. It encourages us to focus on things - that are immediately visible to the user, but it won't automatically - remind us to write other critical tests that are less user-visible--things like security, for example. You'll need to remember them yourself.((("", startref="TTDoutside22"))) + Outside-in isn't a silver bullet. + It encourages us to focus on things that are immediately visible to the user, + but it won't automatically remind us to write other critical tests + that are less user-visible--things like security, for example. + You'll need to remember them yourself. + ((("", startref="TTDoutside22"))) + ((("Outside-In TDD", "drawbacks of"))) ******************************************************************************* diff --git a/chapter_26_page_pattern.asciidoc b/chapter_26_page_pattern.asciidoc index 025b7cdb..6d66f63c 100644 --- a/chapter_26_page_pattern.asciidoc +++ b/chapter_26_page_pattern.asciidoc @@ -315,7 +315,7 @@ from .my_lists_page import MyListsPage # Onesiphorus now goes to the lists page with his browser self.browser = oni_browser - MyListsPage(self).go_to_my_lists_page() + MyListsPage(self).go_to_my_lists_page("onesiphorus@example.com") # He sees Edith's list in there! self.browser.find_element(By.LINK_TEXT, "Get help").click() @@ -336,13 +336,13 @@ class MyListsPage: def __init__(self, test): self.test = test - def go_to_my_lists_page(self): + def go_to_my_lists_page(self, email): self.test.browser.get(self.test.live_server_url) self.test.browser.find_element(By.LINK_TEXT, "My lists").click() self.test.wait_for( - lambda: self.test.assertEqual( + lambda: self.test.assertIn( + email, self.test.browser.find_element(By.TAG_NAME, "h1").text, - "My Lists", ) ) return self diff --git a/source/chapter_24_outside_in/superlists b/source/chapter_24_outside_in/superlists index d328428b..d3284a31 160000 --- a/source/chapter_24_outside_in/superlists +++ b/source/chapter_24_outside_in/superlists @@ -1 +1 @@ -Subproject commit d328428b49c3eeed096cd23252b41d673b6f0279 +Subproject commit d3284a316696e0dce9527e6aa3c23b60f210c7ff diff --git a/source/chapter_25_CI/superlists b/source/chapter_25_CI/superlists index d40d71e9..990bca7f 160000 --- a/source/chapter_25_CI/superlists +++ b/source/chapter_25_CI/superlists @@ -1 +1 @@ -Subproject commit d40d71e9074be4f4697e464519ff9fdb0efa9fcd +Subproject commit 990bca7fc18b5c7ee1b1fbda6a76d7f1d8b296e9 diff --git a/source/chapter_26_page_pattern/superlists b/source/chapter_26_page_pattern/superlists index 536a2d75..b251a502 160000 --- a/source/chapter_26_page_pattern/superlists +++ b/source/chapter_26_page_pattern/superlists @@ -1 +1 @@ -Subproject commit 536a2d75feff00cc33578963e692ff2ea5b7b924 +Subproject commit b251a5026febd8c03ad99278a8979c11600c89e3