@@ -56,6 +56,10 @@ selenium.common.exceptions.NoSuchElementException: Message: Unable to locate
56
56
element: .invalid-feedback; For documentation [...]
57
57
----
58
58
59
+ // CSANAD: I don't have the "For documentation" here:
60
+ // Unable to locate element: .invalid-feedback
61
+ // I only have the stack trace.
62
+
59
63
It's expecting to see an error message if the user tries to input an empty
60
64
item.
61
65
@@ -111,7 +115,7 @@ TIP: If you're new to Python, you may never have seen the `with` statement.
111
115
It's used with what are called "context managers", which wrap a block of code,
112
116
usually with some kind of setup, cleanup, or error-handling code.
113
117
There's a good write-up in the
114
- http ://docs.python.org/release/2.5/whatsnew/pep-343.html[Python 2.5 release notes].
118
+ https ://docs.python.org/release/2.5/whatsnew/pep-343.html[Python 2.5 release notes].
115
119
((("with statements")))
116
120
((("Python 3", "with statements")))
117
121
@@ -131,6 +135,8 @@ except ValidationError:
131
135
132
136
But the `with` formulation is neater. Now, we can try running the test,
133
137
and see its expected failure:
138
+ // CSANAD: I would highlight we are running the unit test here
139
+ // python src/manage.py test lists
134
140
135
141
----
136
142
with self.assertRaises(ValidationError):
@@ -145,16 +151,18 @@ A Django Quirk: Model Save Doesn't Run Validation
145
151
((("model-layer validation", "running full validation")))
146
152
And now we discover one of Django's little quirks.
147
153
_This test should already pass_.
148
- f you take a look at the
149
- http://bit.ly/SuxPJO[docs for the Django model fields],
150
- you'll see that `TextField` actually defaults to `blank=False`, which means
151
- that it 'should' disallow empty values.
154
+ If you take a look at the
155
+ https://docs.djangoproject.com/en/4.2/ref/models/fields/#blank[docs for the Django model fields],
156
+ you'll see under _Field options_ that the default setting for `blank` is `False`. Which means,
157
+ since TextField is a type of Field, it 'should' disallow empty values.
158
+ // CSANAD: I rephrased this a little, because `blank` is False for all `Field`s.
152
159
153
160
((("data integrity errors")))
154
161
So why is the test still failing?
155
162
Well, for
156
163
http://bit.ly/2v3SfRq[slightly counterintuitive historical reasons],
157
164
Django models don't run full validation on save.
165
+ // CSANAD: I'd argue against the use of shortened URLs.
158
166
As we'll see later,
159
167
any constraints that are actually implemented in the database
160
168
will raise errors on save,
@@ -167,6 +175,9 @@ But Django knows that SQLite doesn't support this type of constraint,
167
175
so if we try to run `makemigrations`, it will report there's nothing to do:
168
176
169
177
178
+ // CSANAD: maybe I'm missing something here but I don't think there would have
179
+ // been any changes detected here even with another DB. I don't remember
180
+ // changing the model since the last migration.
170
181
[subs="specialcharacters,macros"]
171
182
----
172
183
$ pass:quotes[*python src/manage.py makemigrations*]
@@ -177,7 +188,7 @@ No changes detected
177
188
((("full_clean method")))Django
178
189
does have a method to manually run full validation, however, called
179
190
`full_clean` (more info in
180
- http ://bit.ly/2u5SIxA [the docs]).
191
+ https ://docs.djangoproject.com/en/4.2/ref/models/instances/#django.db.models.Model.full_clean [the docs]).
181
192
Let's hack it in to see it work:
182
193
183
194
@@ -187,11 +198,17 @@ Let's hack it in to see it work:
187
198
[source,python]
188
199
----
189
200
with self.assertRaises(ValidationError):
190
- item.save()
191
201
item.full_clean()
202
+ item.save()
192
203
----
193
204
====
194
205
//19
206
+ // CSANAD: full_clean() should be executed before save() is called. It doesn't
207
+ // make a difference here since the test DB is destroyed anyway, but
208
+ // we are actually saving an invalid item and only then calling full_clean().
209
+ // Which results in raising the correct error. But I think we should always
210
+ // show the correct order even when it doesn't matter: validation first and
211
+ // only then attempting save().
195
212
196
213
That gets the unit test to pass:
197
214
@@ -211,7 +228,7 @@ Surfacing Model Validation Errors in the View
211
228
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
212
229
213
230
((("model-layer validation", "surfacing errors in the view", id="MLVsurfac13")))
214
- Let" s try to enforce our model validation in the views layer
231
+ Let' s try to enforce our model validation in the views layer
215
232
and bring it up through into our templates, so the user can see them.
216
233
Here's how we can optionally display an error in our HTML--we check whether the template has
217
234
been passed an error variable, and if so, we do this:
@@ -241,8 +258,15 @@ been passed an error variable, and if so, we do this:
241
258
242
259
((("Bootstrap", "documentation")))
243
260
((("form control classes (Bootstrap)")))
244
- Take a look at the http ://getbootstrap.com/css/# forms[Bootstrap docs] for more
261
+ Take a look at the https ://getbootstrap.com/docs/5.3/ forms/validation/#server-side [Bootstrap docs] for more
245
262
info on form controls.
263
+ // CSANAD: these are the new docs for Bootstrap, but for some reason they begin
264
+ // with saying "We recommend client-side validation" which is bad.
265
+ // Client side validation is fine for faster UI aesthetics, sure. But they are
266
+ // not emphasizing that it is only for faster feedback and for security,
267
+ // whatever reaches the server should be validated on the server side before
268
+ // accepting it as an input for literally anything... Maybe we should mention
269
+ // this in a side note?
246
270
247
271
Passing this error to the template is the job of the view function. Let's take
248
272
a look at the unit tests in the `NewListTest` class. I'm going to use two
@@ -406,7 +430,7 @@ self.assertContains(response, expected_error)
406
430
====
407
431
408
432
...will show us the cause—Django has
409
- https://docs.djangoproject.com/en/1.11 /ref/templates/builtins/#autoescape[HTML-escaped]
433
+ https://docs.djangoproject.com/en/4.2 /ref/templates/builtins/#autoescape[HTML-escaped]
410
434
the apostrophe:
411
435
412
436
----
@@ -538,7 +562,9 @@ element: .invalid-feedback; [...]
538
562
----
539
563
540
564
541
- Not quite, but they did get a little further. Checking 'line 31', we can
565
+ Not quite, but they did get a little further. Checking the line in which the
566
+ error occurred -- 'line 31' in my case -- , we can
567
+ // CSANAD: I think emphasizing what to look for in the error log is helpful.
542
568
see that we've got past the first part of the test, and are now onto the second
543
569
check--that submitting a second empty item also shows an error.
544
570
@@ -573,6 +599,19 @@ The current situation is that we have one view and URL for displaying a list,
573
599
and one view and URL for processing additions to that list. We're going to
574
600
combine them into one. So, in 'list.html', our form will have a different
575
601
target:
602
+ // CSANAD: Since we are working in a not completely working state, I would
603
+ // already mention or move the NOTE here from the end of the subsection:
604
+ // "Refactor: Transferring the new_item Functionality into view_list".
605
+ // And then just re-reference it from where it is right now.
606
+ //
607
+ // I think adding a few words on how this is not just a simple refactor but it's
608
+ // going to get us to our working state would help the reader recognise these
609
+ // situations later, independently.
610
+ // Something like pointing out how the FT is partially passing for:
611
+ // - adding one empty list item results in an error message
612
+ // - adding one valid list item is also passing
613
+ // - the error occurs when we try adding an empty list item again
614
+ // from which we conclude why we are refactoring the views first
576
615
577
616
[role="sourcecode"]
578
617
.src/lists/templates/list.html (ch11l030)
@@ -614,7 +653,10 @@ NOTE: In this section we're performing a refactor at the application level.
614
653
and things are back to working as before.
615
654
Have another look at the diagram from the end of <<chapter_04_philosophy_and_refactoring>>
616
655
if you need to get your bearings.
617
-
656
+ // CSANAD: my comment from above to say a few words on why we are refactoring
657
+ // despite the failing FT again feels justified since we are even
658
+ // mentioning the diagram too. It might feel confusing without telling the
659
+ // reader earlier what we are doing here.
618
660
619
661
620
662
==== Refactor: Transferring the new_item Functionality into view_list
@@ -952,7 +994,7 @@ a [keep-together]#parameter#:
952
994
====
953
995
954
996
See the
955
- http ://bit.ly/2uKaMzA [Django
997
+ https ://docs.djangoproject.com/en/4.2/topics/http/urls/#reverse-resolution-of-urls [Django
956
998
docs on reverse URL resolution] for more info. We run the tests again, and check that they all pass:
957
999
958
1000
[subs="specialcharacters,macros"]
@@ -1031,7 +1073,7 @@ AttributeError: 'List' object has no attribute 'get_absolute_url'
1031
1073
The implementation is to use Django's `reverse` function, which
1032
1074
essentially does the reverse of what Django normally does with 'urls.py'
1033
1075
(see the
1034
- https://docs.djangoproject.com/en/1.11 /topics/http/urls/#reverse-resolution-of-urls[docs]):
1076
+ https://docs.djangoproject.com/en/4.2 /topics/http/urls/#reverse-resolution-of-urls[docs]):
1035
1077
1036
1078
1037
1079
[role="sourcecode"]
@@ -1065,7 +1107,7 @@ def new_list(request):
1065
1107
====
1066
1108
1067
1109
There's more info in the
1068
- https://docs.djangoproject.com/en/1.11 /topics/http/shortcuts/#redirect[Django
1110
+ https://docs.djangoproject.com/en/4.2 /topics/http/shortcuts/#redirect[Django
1069
1111
docs]. Quick check that the unit tests still pass:
1070
1112
1071
1113
[subs="specialcharacters,macros"]
0 commit comments