The note form showed one mistake at a time, and saved spaces as a title
See the problem
Section titled “See the problem”The Notes app has an HTML form to edit a note. Ana leaves the title empty and types maybe in the pinned box. The page says “title is required”. She fixes it and saves. Now it says “pinned must be true or false”. One mistake at a time, one round trip each.
Then Ben types three spaces as a title, and the note is saved with a blank name. A checkbox sends "on", which the view stores as the text "on", not True. All of this is code the team wrote by hand, while Django ships a tool for exactly this job.
Define the task
Section titled “Define the task”Write validate_note(data), the way a Django form checks input.
- Input: a dict as sent by the browser or a JSON client:
title, and maybebodyandpinned. - Output: a pair
(cleaned, errors).cleanedholds typed values when all is well, or{}.errorsmaps each field to its messages. - The rules: the title is required after trimming spaces, at most 200 characters.
bodyis text, default"".pinnedis a bool;"on","true"and"1"meanTrue, and"off","false","0"and""meanFalse. - The hard part: report every problem in one go, and hand back data ready to save.
Example: validate_note({"title": " Tea ", "pinned": "on"}) gives ({"title": "Tea", "body": "", "pinned": True}, {}).
Pause and think
Section titled “Pause and think”Hints open one at a time:
Hint 1 of 3
Raising an error ends the function. To keep going, write the problem down instead.
Hint 2 of 3
A dict from field name to a list of messages lets the page show each one next to its box.
Hint 3 of 3
Build the cleaned values as you go: trimmed text, a real bool. Return them only if there were no errors.
Understand the idea
Section titled “Understand the idea”Django comes with the parts a web app needs. Flask asks you to choose; Django has made the choices. The Notes API is in examples/django/notes; start it with ./run.sh examples django.
A project holds apps. django-admin startproject config makes the settings, the root URLconf and the WSGI and ASGI entry points. manage.py startapp notes makes an app: one feature, with its own models, views, URLs, admin, migrations and tests. Add it to INSTALLED_APPS.
MTV is Django’s name for its layers:
- Model: a class per table, in
models.py.title = models.CharField(max_length=200). - Template: HTML with
{{ note.title }}, escaped by default. An API returns JSON instead. - View: a function (or class) that takes a request and returns a response.
URLconfs map paths to views. path("notes/<int:pk>/", views.note_detail). The project’s urls.py includes each app’s: path("api/", include("notes.urls")).
The ORM and migrations. Note.objects.filter(owner=request.user).order_by("-updated_at") builds a lazy QuerySet; the SQL runs when you read the rows. Change a model, run manage.py makemigrations, then manage.py migrate. The second practice problem builds Django’s field__lookup filters.
The admin is a full editing site for your models, from three lines in admin.py: admin.site.register(Note). It is for staff, not users.
Forms check input. A Form or ModelForm lists fields; form.is_valid() runs every check, then form.errors has each problem by field and form.cleaned_data has typed values. DRF’s serializers do the same for JSON.
Middleware and auth. settings.MIDDLEWARE lists the layers each request passes through: security headers, sessions, CSRF, authentication and more. request.user is the logged-in user; @login_required guards a view; authenticate() and login() check a password and start a session.
PATCH /api/notes/7/ in Django
Parts:
- Browser (the request is here)
- Middleware: session, CSRF, auth
- URLconf: notes/<int:pk>/
- View: note_detail
- ORM: Note.objects
- Database
Arrows:
- Browser sends requests to Middleware: session, CSRF, auth
- Middleware: session, CSRF, auth sends requests to URLconf: notes/<int:pk>/
- URLconf: notes/<int:pk>/ sends requests to View: note_detail
- View: note_detail sends requests to ORM: Note.objects
- ORM: Note.objects sends requests to Database
Step 1 of 7. The browser sends PATCH /api/notes/7/ with {"title": ""}, the session cookie and the CSRF token.
Climb the ladder
Section titled “Climb the ladder”Three versions of validate_note. The tests send several mistakes at once, spaces as a title, and a checkbox’s "on".
Raise at the first problem
The idea: Check each field in turn and raise ValueError on the first one that is wrong.
It stops bad data from being saved.
def validate_note(data: dict) -> tuple[dict, dict[str, list[str]]]: if not data.get("title"): raise ValueError("title: This field is required.") # stops at the first problem if len(data["title"]) > 200: raise ValueError("title: Ensure this value has at most 200 characters.") if data.get("pinned") not in (None, True, False): raise ValueError("pinned: Enter true or false.") return data, {}- Errors shown
One at a time- Data saved
As sent- Why
- Raising ends the function, so later fields are never checked.
Where it breaks: two mistakes take two round trips. And what it returns is the data as sent: " Tea " keeps its spaces, "on" stays text.
Collect every error
The idea: Write each problem into an errors dict, field by field, and return them all.
The waste in Okay: one round trip per mistake.
Highlighted lines are new in this rung.
def validate_note(data: dict) -> tuple[dict, dict[str, list[str]]]: errors: dict[str, list[str]] = {} title = data.get("title") if not title: errors.setdefault("title", []).append("This field is required.") elif len(title) > 200: errors.setdefault("title", []).append("Ensure this value has at most 200 characters.") if data.get("pinned") not in (None, True, False): errors.setdefault("pinned", []).append("Enter true or false.") return (data if not errors else {}), errors # every problem at once, but data as sent- Errors shown
All at oncewas One at a time- Data saved
As sent- Why
- The page can show every message next to its field in one go.
What changed from the previous rung
def validate_note(data: dict) -> tuple[dict, dict[str, list[str]]]: if not data.get("title"): raise ValueError("title: This field is required.") # stops at the first problem if len(data["title"]) > 200: raise ValueError("title: Ensure this value has at most 200 characters.") errors: dict[str, list[str]] = {} title = data.get("title") if not title: errors.setdefault("title", []).append("This field is required.") elif len(title) > 200: errors.setdefault("title", []).append("Ensure this value has at most 200 characters.") if data.get("pinned") not in (None, True, False): raise ValueError("pinned: Enter true or false.") return data, {} errors.setdefault("pinned", []).append("Enter true or false.") return (data if not errors else {}), errors # every problem at once, but data as sentWhere it breaks: it checks, but doesn’t clean. Three spaces pass as a title, "on" is refused, and the saved title keeps its spaces.
Check, clean and type each field
The idea: For each field: read it, normalize it (trim, turn on into True), check it, and store the clean value or an error.
The waste in Better: it checked the raw text, so it judged and saved the wrong values.
Highlighted lines are new in this rung.
def validate_note(data: dict) -> tuple[dict, dict[str, list[str]]]: errors: dict[str, list[str]] = {} cleaned: dict = {}
title = data.get("title") title = title.strip() if isinstance(title, str) else "" if not title: errors["title"] = ["This field is required."] elif len(title) > 200: errors["title"] = ["Ensure this value has at most 200 characters."] cleaned["title"] = title
body = data.get("body", "") if not isinstance(body, str): errors["body"] = ["Enter text."] cleaned["body"] = body if isinstance(body, str) else ""
pinned = data.get("pinned", False) if isinstance(pinned, str): # an HTML checkbox sends "on"; JSON clients send true pinned = { "on": True, "true": True, "1": True, "off": False, "false": False, "0": False, "": False, }.get(pinned.lower(), pinned) if not isinstance(pinned, bool): errors["pinned"] = ["Enter true or false."] cleaned["pinned"] = pinned
return ({} if errors else cleaned), errors- Errors shown
All at once- Data saved
Trimmed and typedwas As sent- Why
- This is what a Django form does: errors by field, and cleaned_data ready to save.
What changed from the previous rung
def validate_note(data: dict) -> tuple[dict, dict[str, list[str]]]: errors: dict[str, list[str]] = {} cleaned: dict = {}
title = data.get("title") title = title.strip() if isinstance(title, str) else "" if not title: errors.setdefault("title", []).append("This field is required.") errors["title"] = ["This field is required."] elif len(title) > 200: errors.setdefault("title", []).append("Ensure this value has at most 200 characters.") if data.get("pinned") not in (None, True, False): errors.setdefault("pinned", []).append("Enter true or false.") return (data if not errors else {}), errors # every problem at once, but data as sent errors["title"] = ["Ensure this value has at most 200 characters."] cleaned["title"] = title
body = data.get("body", "") if not isinstance(body, str): errors["body"] = ["Enter text."] cleaned["body"] = body if isinstance(body, str) else ""
pinned = data.get("pinned", False) if isinstance(pinned, str): # an HTML checkbox sends "on"; JSON clients send true pinned = { "on": True, "true": True, "1": True, "off": False, "false": False, "0": False, "": False, }.get(pinned.lower(), pinned) if not isinstance(pinned, bool): errors["pinned"] = ["Enter true or false."] cleaned["pinned"] = pinned
return ({} if errors else cleaned), errorsWhen Best isn’t best: use a ModelForm. It reads the rules from the model (max_length=200, blank=False), trims CharFields for you, and its BooleanField reads checkboxes. For JSON APIs, a DRF serializer does the same.
Write it yourself
Section titled “Write it yourself”Write validate_note(data) from the task. Press Run tests to check your code.
title: trim it. Empty means["This field is required."]; over 200 means["Ensure this value has at most 200 characters."].body: default""; anything not text means["Enter text."].pinned: defaultFalse. Texton,trueor1isTrue;off,false,0or empty isFalse, in any case. Anything else means["Enter true or false."].- Return
(cleaned, {}), or({}, errors)if there are any.
Check a note like a Django form
Ctrl + Enter runs- Not run yet:
validate_note({'title': 'Tea'})→[{'title': 'Tea', 'body': '', 'pinned': False}, {}] - Not run yet:
Two mistakes at once→[{}, {'title': ['This field is required.'], 'pinned': ['Enter true or false.']}] - Not run yet:
Spaces, and a checkbox’s "on"→[{'title': 'Tea', 'body': '', 'pinned': True}, {}] - Not run yet:
A title of three spaces→[{}, {'title': ['This field is required.']}] - Not run yet: Extra test
- Not run yet: Extra test
- Not run yet: Extra test
- Not run yet: Extra test
Extra tests check tricky cases. You see their input only if they fail.
Try your own input
Compare
Section titled “Compare”| Way | Errors shown | Data saved | Good | Not so good | Use it when |
|---|---|---|---|---|---|
| Okay: raise at the first | One at a time | As sent | Short | Many round trips; dirty data | A one-field check |
| Better: collect errors | All at once | As sent | Good messages | Spaces and "on" handled wrong | Checking data you won’t save |
| Best: check, clean, type | All at once | Trimmed and typed | What forms do | More code by hand | Never by hand in Django: use a form |
Common mistakes and edge cases
Section titled “Common mistakes and edge cases”- Forgetting
owner=request.user.Note.objects.get(pk=pk)lets anyone open any note. Filter by owner, and answer 404. - A QuerySet in a loop that reads a foreign key. One query per row: the N+1 problem. See The order list page is slow.
- Logic in templates or views that belongs in the model. Put rules about a note on
Note, so the admin, forms and API share them. DEBUG = Truein production. Error pages show settings and code. SetDEBUG = FalseandALLOWED_HOSTS, and runmanage.py check --deploy.- Editing the database by hand. Make a migration, so every environment gets the same change.
Say it in the interview
Section titled “Say it in the interview”Questions to ask first
- Is this a site with pages, an API, or both?
- Who edits the data: users, or staff who could use the admin?
- Which parts of Django will we lean on: ORM, admin, auth, forms?
A 60-second walkthrough
“A Django project is a set of apps, each with models, views, URLs and migrations. A request goes through the middleware: security, sessions, CSRF, auth. Then the URLconf picks a view, which uses the ORM and returns a template or JSON. Models define tables, makemigrations writes the change and migrate applies it. I check input with forms or DRF serializers, which give every error at once and typed cleaned data. I’d use the built-in auth and login_required, and filter every query by the user. I watch QuerySets for N+1, with select_related and prefetch_related. The admin gives staff an editing site at no extra cost.”
Likely follow-ups
- What is a QuerySet? A lazy query you can keep chaining. It runs when you read it, then caches the rows.
ForeignKeyvsManyToManyField? A foreign key is one related row per row, like a note’s owner; many-to-many uses a join table, like notes and tags.- Function views or class-based views? Functions are plain and easy to read. Class-based views and generics save code for standard list and edit pages.
- How does Django stop CSRF?
CsrfViewMiddlewarechecks a token from the cookie against one in the form or theX-CSRFTokenheader.
Practice
Section titled “Practice”Three problems, from easy to medium. Each has hints, tests you can run here, and its own Okay, Better and Best answers.
1. The pluralize filter (easy)
Section titled “1. The pluralize filter (easy)”Write pluralize(count, suffix="s"), Django’s template filter. It gives the ending for a word after a count: "" for one, suffix otherwise. If the suffix has a comma, like "y,ies", the part before it is for one and the part after it for many.
Example: pluralize(0) is "s", for “0 notes”. pluralize(1, "y,ies") is "y", for “1 entry”.
Hint 1 of 3
Zero is plural in English: “0 notes”.
Hint 2 of 3
Only exactly one is singular.
Hint 3 of 3
suffix.split(",") gives the one and many endings when there is a comma.
The pluralize filter
Ctrl + Enter runs- Not run yet:
2 note…→'s' - Not run yet:
0 note…→'s' - Not run yet:
1 note…→'' - Not run yet:
1 entr…→'y' - Not run yet:
5 entr…→'ies' - Not run yet: Extra test
Extra tests check tricky cases. You see their input only if they fail.
Try your own input
Show the Okay, Better and Best answers
More than one
The idea: Add the suffix when the count is over 1.
Right for 1, 2 and more.
def pluralize(count: int, suffix: str = "s") -> str: return suffix if count > 1 else ""- 0 notes
“0 note”- 1 entry
“1 entry”- Why
- It treats plural as “more than one”.
Where it breaks: zero is plural in English, so it shows “0 note”. And "y,ies" comes out whole.
Anything but one
The idea: Add the suffix unless the count is exactly 1.
The waste in Okay: it forgot zero.
Highlighted lines are new in this rung.
def pluralize(count: int, suffix: str = "s") -> str: return suffix if count != 1 else ""- 0 notes
“0 notes”was “0 note”- 5 entries
“5 entry,ies”was “1 entry”- Why
- Only one is singular.
What changed from the previous rung
def pluralize(count: int, suffix: str = "s") -> str: return suffix if count > 1 else "" return suffix if count != 1 else ""Where it breaks: words that change their ending, like entry and entries, need two endings, and Better pastes the whole suffix.
One ending and a many ending
The idea: Split the suffix on a comma into the one and many endings; use the one ending only for 1.
The waste in Better: it only knew words that add a letter.
Highlighted lines are new in this rung.
def pluralize(count: int, suffix: str = "s") -> str: # Like Django's filter: "s", "es", or "y,ies" for one word and many. one, many = suffix.split(",") if "," in suffix else ("", suffix) return one if count == 1 else many- 0 notes
“0 notes”- 5 entries
“5 entries”was “5 entry,ies”- Why
- It is Django’s rule: suffix, or singular,plural.
What changed from the previous rung
def pluralize(count: int, suffix: str = "s") -> str: return suffix if count != 1 else "" # Like Django's filter: "s", "es", or "y,ies" for one word and many. one, many = suffix.split(",") if "," in suffix else ("", suffix) return one if count == 1 else manyWhen Best isn’t best: in a template, write {{ count }} entr{{ count|pluralize:"y,ies" }}. For other languages, use Django’s translation ngettext, because plural rules differ.
2. Django-style lookups (medium)
Section titled “2. Django-style lookups (medium)”Write filter_rows(rows, **lookups), like QuerySet.filter. Each key is a field, or field__op. Support exact (the default), contains, icontains, in, gt, gte, lt and lte. Keep rows that pass every lookup. Raise ValueError for any other op, naming it.
Example: filter_rows(ROWS, title__icontains="milk") keeps rows 1 and 3.
Hint 1 of 3
key.partition("__") splits "stars__gte" into "stars", "__", "gte"; for "id" the op is "".
Hint 2 of 3
A dict from op name to a function of (field_value, value) keeps it short. operator.ge and friends help.
Hint 3 of 3
Check every key before filtering, so a typo fails even on an empty list.
Django-style lookups
Ctrl + Enter runs- Not run yet:
id=2→[{'id': 2, 'title': 'Call mum', 'stars': 5}] - Not run yet:
title__icontains="milk"→[{'id': 1, 'title': 'Buy Milk', 'stars': 3}, {'id': 3, 'title': 'milk again', 'stars': 1}] - Not run yet:
id__in=[1, 3]→[{'id': 1, 'title': 'Buy Milk', 'stars': 3}, {'id': 3, 'title': 'milk again', 'stars': 1}] - Not run yet:
stars__gte=3, id__lt=3→[{'id': 1, 'title': 'Buy Milk', 'stars': 3}, {'id': 2, 'title': 'Call mum', 'stars': 5}] - Not run yet:
title__startswith="B"→ raisesValueError - Not run yet: Extra test
- Not run yet: Extra test
Extra tests check tricky cases. You see their input only if they fail.
Try your own input
Show the Okay, Better and Best answers
Exact matches only
The idea: Compare row.get(key) with the value.
Right for id=2.
def filter_rows(rows: list[dict], **lookups) -> list[dict]: return [row for row in rows if all(row.get(k) == v for k, v in lookups.items())]- Ops
exact- A typo
Finds nothing- Why
- It reads title__contains as a field name.
Where it breaks: title__contains isn’t a field, so row.get gives None and nothing matches. No error says why.
Split on __
The idea: Split the key, and handle contains and in; treat anything else as exact.
The waste in Okay: it never looked inside the key.
Highlighted lines are new in this rung.
def filter_rows(rows: list[dict], **lookups) -> list[dict]: def ok(row: dict, key: str, value) -> bool: field, _, op = key.partition("__") if op == "contains": return value in row[field] if op == "in": return row[field] in value return row[field] == value # anything else is treated as an exact match
return [row for row in rows if all(ok(row, k, v) for k, v in lookups.items())]- Ops
exact, contains, inwas exact- A typo
Wrong rowswas Finds nothing- Why
- It understands the field__op shape.
What changed from the previous rung
def filter_rows(rows: list[dict], **lookups) -> list[dict]: return [row for row in rows if all(row.get(k) == v for k, v in lookups.items())] def ok(row: dict, key: str, value) -> bool: field, _, op = key.partition("__") if op == "contains": return value in row[field] if op == "in": return row[field] in value return row[field] == value # anything else is treated as an exact match
return [row for row in rows if all(ok(row, k, v) for k, v in lookups.items())]Where it breaks: stars__gte=3 quietly becomes stars == 3, which gives wrong rows with no error.
A table of ops, and refuse the rest
The idea: Map each op name to a test function; check every key first and raise on an unknown op.
The waste in Better: it guessed when it didn’t know.
Highlighted lines are new in this rung.
import operator
OPS = { "exact": operator.eq, "contains": lambda field, value: value in field, "icontains": lambda field, value: value.lower() in field.lower(), "in": lambda field, value: field in value, "gt": operator.gt, "gte": operator.ge, "lt": operator.lt, "lte": operator.le,}
def filter_rows(rows: list[dict], **lookups) -> list[dict]: checks = [] for key, value in lookups.items(): field, _, op = key.partition("__") if (op or "exact") not in OPS: raise ValueError(f"Unsupported lookup '{op}' for {field}") checks.append((field, OPS[op or "exact"], value)) return [row for row in rows if all(test(row[f], v) for f, test, v in checks)]- Ops
All eightwas exact, contains, in- A typo
ValueErrorwas Wrong rows- Why
- Adding an op is one line, and a mistake fails loudly.
What changed from the previous rung
import operator
OPS = { "exact": operator.eq, "contains": lambda field, value: value in field, "icontains": lambda field, value: value.lower() in field.lower(), "in": lambda field, value: field in value, "gt": operator.gt, "gte": operator.ge, "lt": operator.lt, "lte": operator.le,}
def filter_rows(rows: list[dict], **lookups) -> list[dict]: def ok(row: dict, key: str, value) -> bool: checks = [] for key, value in lookups.items(): field, _, op = key.partition("__") if op == "contains": return value in row[field] if op == "in": return row[field] in value return row[field] == value # anything else is treated as an exact match
return [row for row in rows if all(ok(row, k, v) for k, v in lookups.items())] if (op or "exact") not in OPS: raise ValueError(f"Unsupported lookup '{op}' for {field}") checks.append((field, OPS[op or "exact"], value)) return [row for row in rows if all(test(row[f], v) for f, test, v in checks)]When Best isn’t best: Django turns these into SQL, so the database does the work with indexes. Filtering in Python means loading every row first. Django also follows relations, like owner__username="ana".
3. Django’s get_page (medium)
Section titled “3. Django’s get_page (medium)”Write get_page(items, number, per_page), like Paginator.get_page. Give back items, number, pages, has_next and has_previous. An empty list has one page. If number isn’t a whole number, like "abc" or None, show page 1. If it is below 1 or past the end, show the last page.
Example: 45 items, 20 a page, number="9" shows page 3.
Hint 1 of 3
Pages: -(-len(items) // per_page), the ceiling, but at least 1.
Hint 2 of 3
int(number) inside try, catching TypeError and ValueError.
Hint 3 of 3
Out of range means the last page in Django’s get_page, even for 0.
Django’s get_page
Ctrl + Enter runs- Not run yet:
45 items, page "2"→{'items': [21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40], 'number': 2, 'pages': 3, 'has_next': True, 'has_previous': True} - Not run yet:
45 items, page "9"→{'items': [41, 42, 43, 44, 45], 'number': 3, 'pages': 3, 'has_next': False, 'has_previous': True} - Not run yet:
45 items, page "abc"→{'items': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], 'number': 1, 'pages': 3, 'has_next': True, 'has_previous': False} - Not run yet:
No items→{'items': [], 'number': 1, 'pages': 1, 'has_next': False, 'has_previous': False} - Not run yet: Extra test
- Not run yet: Extra test
Extra tests check tricky cases. You see their input only if they fail.
Try your own input
Show the Okay, Better and Best answers
Slice by the number
The idea: Turn the number into an int and slice.
Right for the pages that exist.
def get_page(items: list, number, per_page: int) -> dict: number = int(number) start = (number - 1) * per_page return {"items": items[start : start + per_page], "number": number}- ?page=9
An empty page- ?page=abc
Crashes- Why
- It trusts the number in the URL.
Where it breaks: anyone can type ?page=abc, which crashes with a 500, or ?page=9, which shows nothing.
Clamp to 1 through pages
The idea: Count the pages and clamp the number into range.
The waste in Okay: it never checked the range.
Highlighted lines are new in this rung.
def get_page(items: list, number, per_page: int) -> dict: pages = max(1, -(-len(items) // per_page)) number = min(max(int(number), 1), pages) # clamp to 1..pages start = (number - 1) * per_page return {"items": items[start : start + per_page], "number": number}- ?page=9
The last pagewas An empty page- ?page=abc
Crashes- Why
- Numbers past the end now show real notes.
What changed from the previous rung
def get_page(items: list, number, per_page: int) -> dict: number = int(number) pages = max(1, -(-len(items) // per_page)) number = min(max(int(number), 1), pages) # clamp to 1..pages start = (number - 1) * per_page return {"items": items[start : start + per_page], "number": number}Where it breaks: text still crashes, and there is no has_next for the template’s Next link.
Forgive any input, and say what’s around
The idea: Parse safely (page 1 if not a number), send out-of-range to the last page, and add pages, has_next and has_previous.
The waste in Better: it fixed the range but not the type.
Highlighted lines are new in this rung.
def get_page(items: list, number, per_page: int) -> dict: pages = max(1, -(-len(items) // per_page)) # ceiling division; an empty list has 1 page try: number = int(number) except (TypeError, ValueError): number = 1 # ?page=abc or no page at all if number < 1 or number > pages: number = pages # Django's get_page gives the last page for any number out of range start = (number - 1) * per_page return { "items": items[start : start + per_page], "number": number, "pages": pages, "has_next": number < pages, "has_previous": number > 1, }- ?page=9
The last page- ?page=abc
Page 1was Crashes- Why
- Every URL gives a page, and the template can draw its links.
What changed from the previous rung
def get_page(items: list, number, per_page: int) -> dict: pages = max(1, -(-len(items) // per_page)) number = min(max(int(number), 1), pages) # clamp to 1..pages pages = max(1, -(-len(items) // per_page)) # ceiling division; an empty list has 1 page try: number = int(number) except (TypeError, ValueError): number = 1 # ?page=abc or no page at all if number < 1 or number > pages: number = pages # Django's get_page gives the last page for any number out of range start = (number - 1) * per_page return {"items": items[start : start + per_page], "number": number} return { "items": items[start : start + per_page], "number": number, "pages": pages, "has_next": number < pages, "has_previous": number > 1, }Check yourself
Section titled “Check yourself”A few quick questions. Ones you miss are remembered, so you can find your weak spots later.
Flashcards
Section titled “Flashcards”Add these to your review deck. Each card comes back a little later every time you get it right.
Cheat sheet
Section titled “Cheat sheet”- Project = settings + root URLconf; apps = features (models, views, urls, admin, migrations, tests).
- MTV: models (tables), templates (HTML, escaped), views (request in, response out).
makemigrationsthenmigrate; QuerySets are lazy; filter every query byrequest.user.- Forms:
is_valid(),errorsby field,cleaned_datatyped. DRF serializers for JSON. - Built in: admin, auth (
login_required), sessions, CSRF,Paginator,pluralize.
Go deeper
Section titled “Go deeper”- Django: the tutorial, forms and QuerySet lookups.
- The runnable example:
examples/django/notesin the repository, with its tests. - Next on the Frameworks roadmap: Django REST Framework, for JSON APIs on Django.
Can you do these now?
If you’re not sure of any of these, scroll back to that part before moving on.
Status: Not started