Django Master Class: Three-Day Classroom Course

A three-day classroom master class for experienced Django developers, taught on the code of a production Django platform. Over 18 hours, students take apart the parts of Smarter that go past Django’s defaults: settings validated before Django reads them, a startup sequence that copes without a database, ten custom middleware classes, many hosts in one site, an ORM extended for resource ownership, signals, caching, Celery, async streaming over Redis publish-subscribe, API contracts in Pydantic, OAuth, template tags that host React apps, and the tests and hooks that keep it all honest. Each topic is a one-hour module, a 30-minute lecture and a 30-minute lab on the student’s own running copy of Smarter. Day 1 covers configuration, startup and the request, day 2 data, asynchrony and Redis, and day 3 APIs, identity, the front end and quality. See Course Formats and Grading for the grading scale and the course policies, and Django Master Class for an overview of the subject.

At a Glance

Students

Professional Python developers with prior Django experience who maintain, or will maintain, a large Django codebase.

Format

Three days of six hours of instruction, each day six one-hour modules. Breaks and lunch are not counted.

Deployment

Smarter from source on each student’s laptop, under Docker Compose, working before day 1.

Workload

About 2 hours each evening for the day’s assignment, and 8 to 12 hours for the capstone in the week after the course.

Cost

Free, plus an LLM provider API key for the modules that send prompts: a few dollars at most.

Learning Objectives

By the end of the course, students can:

  1. Manage a Django project’s configuration from the environment through a validated settings class, in keeping with the twelve factors.

  2. Explain Django’s startup sequence and the request’s path through middleware, and write middleware, host routing and feature flags that fit into them safely.

  3. Extend the ORM with abstract models, managers and querysets, including ownership and sharing of resources between accounts.

  4. Choose between signals, caching, Celery tasks and async views for a given job, and implement each correctly, including invalidation, routing and streaming over Redis publish-subscribe.

  5. Build APIs with Django REST Framework, Pydantic contracts and custom authentication, and add OAuth sign-in.

  6. Write template tags, serve complex static assets such as React apps, and style pages with Bootstrap or Tailwind.

  7. Keep a large codebase healthy with tests, pre-commit hooks and coverage targets.

Technical Prerequisites

  • Required: proficient Python programming.

  • Required: prior experience building a Django application: models, views, templates, URLs and the admin.

  • Required: the Model-View-Template pattern, LAMP and similar web application stacks.

  • Required: working in a terminal with common Linux commands, and Git.

  • Required: familiarity with The Twelve-Factor App.

  • Required before day 1: Docker, and Smarter running from source as described in Developer Setup Guide. Send students the instructions at least a week ahead, and ask for a screenshot of the running web console.

  • Helpful: Redis, Celery, and reading React and TypeScript.

Schedule

Each module is described under The Modules.

Day

Morning (3 hours)

Afternoon (3 hours)

Evening

1

Modules 1 to 3: 12-Factor and settings; the startup sequence; logging.

Modules 4 to 6: the request and middleware chain; writing middleware; many hosts and Waffle.

Assignment 1, due at the start of day 2.

2

Modules 7 to 9: customizing the ORM; the user and ownership; signals and receivers.

Modules 10 to 12: caching complex objects; Celery and Beat; ASGI and publish-subscribe.

Assignment 2, due at the start of day 3.

3

Modules 13 to 15: API best practices; OAuth; mixins, enums and data helpers.

Modules 16 to 18: template tags, static assets and styles; testing and quality; the terminal window.

Assignment 3, due two days after the course. Capstone choice.

Each day opens with 10 minutes of questions on the previous evening’s assignment, taken from the first lecture’s time. The capstone is due one week after the course.

The Modules

Each module is a 30-minute lecture followed by a lab of about 30 minutes. Labs are done on each student’s own clone of Smarter, running under Docker Compose, on a branch per lab. The file references are listed in full under The Modules in the Source Code.

#

Lecture

Lab

1

Django and 12-Factor; managing settings. The twelve factors as they apply to Django. Why Smarter reads every setting from the environment into a Pydantic Settings class, smarter_settings, and copies it into Django’s settings only afterwards: types, defaults, secrets as SecretStr, and placeholders that count as missing. One settings module per environment. ADR 007.

Add a typed, validated setting to smarter_settings, with a default, set it in .env, and use it in a view. Show what happens when it is set to an invalid value.

2

The startup sequence. What runs, in order, from manage.py or asgi.py to the first request: settings, the app registry, each AppConfig.ready(), and URL loading. Why ready() must not query the database or the cache, and how Smarter’s Waffle helpers and lazy cache cope when they are not yet available.

Add logging to an app’s ready(), then make it query the database, and run makemigrations against an empty database to see why that fails. Fix it the way is_database_ready() does.

3

Getting more out of Python logging. Loggers, handlers, filters and formatters; one logger factory; verbose logging switched on per subsystem with Waffle, not by editing code; request context added to every record by middleware; logs to standard output. ADR 011.

Add a Waffle-switched logger to a module, turn it on in the Django admin, and filter its records by user with the log context middleware.

4

The request object and the middleware chain. How each middleware sees the request on the way in and the response on the way out, in MIDDLEWARE order. What Smarter’s chain adds to the request (the host’s URLconf, the user, the API key’s user, the log context), and how SmarterRequestMixin reads it back.

Turn on the diagnostic middleware, then add a middleware that prints the request’s attributes at three points in the chain, and explain each difference.

5

Writing middleware. Function and class middleware, process_view and process_exception, sync and async capable middleware, and short-circuiting with a response. Smarter’s replacements for Django’s CORS, CSRF and security middleware, and why each one was needed.

Write a middleware that adds a request id to the request and the response, and logs it. Test it with Django’s RequestFactory.

6

Many hosts in one site; Waffle. django-hosts and ROOT_HOSTCONF: the console, the API and every chatbot’s host routed to their own URLconfs, and reverse() across hosts. Feature flags, switches and samples with Waffle, and Smarter’s switch registry. ADR 022.

Add a host pattern for a new subdomain with its own URLconf, and gate one of its views with a new Waffle switch.

7

Customizing the ORM. Abstract base models, custom managers and querysets, chaining queryset methods, typing generic managers, select_related and prefetch_related, and the N+1 problem. ADR 017.

Add a queryset method to a model’s manager that chains with filter(), and use the Django Debug Toolbar to remove an N+1 query from a list page.

8

The user and resource ownership. A custom AUTH_USER_MODEL against a profile model, and why Smarter keeps Django’s User and adds UserProfile. Accounts, owners and shared resources in MetaDataWithOwnershipModelManager: what a user owns, what is shared with them, and what they may change.

Create a model on MetaDataWithOwnershipModel, and write tests that one account’s user cannot read another account’s rows until they are shared.

9

Signals and receivers. Built-in model signals, custom signals, signals.py and receivers.py loaded from ready(), transaction.on_commit, and when a signal is the wrong tool.

Declare a custom signal, send it from a view, and write two receivers: one that logs, and one that invalidates a cache entry.

10

Caching complex objects. What can be pickled and what should not be; caching querysets, model instances and Pydantic models; deterministic keys from arguments; caching None; invalidation; and why a security check is never cached. ADR 018.

Cache an expensive query with @cache_results, invalidate it from a post_save receiver, and prove both with tests.

11

Celery and Celery Beat. Brokers, workers, queues and routing; retries and idempotency; a Beat schedule; why .delay() is not eager in tests; and passing ids, not model instances. ADR 009, ADR 030.

Write a task that aggregates rows for an account, route it to the right queue, schedule it in Beat, and watch it run in the worker’s logs.

12

ASGI and publish-subscribe. WSGI against ASGI, uvicorn, Channels’ ProtocolTypeRouter, async views, sync_to_async and SynchronousOnlyOperation, Server-Sent Events, and Redis publish-subscribe as a message bus between processes. How Smarter streams a prompt’s progress while the prompt itself runs synchronously.

Write an async view that streams Server-Sent Events from a Redis channel, and publish to that channel from the Celery task of Module 11.

13

API best practices. Django REST Framework viewsets and serializers, Pydantic models as the API contract, a published JSON Schema, versioning, and custom authentication with hashed API keys in an authentication class and a middleware. ADR 023, ADR 031.

Add an endpoint whose response is built from a Pydantic model and dumped to JSON, authenticated by API key. Call it with curl and a throwaway key.

14

Working with OAuth. The OAuth 2.0 authorization code flow, Python Social Auth’s backends and pipeline, adding Smarter’s own steps to the pipeline, handling its exceptions in middleware, and keeping client secrets in smarter_settings.

Register a GitHub OAuth app for localhost, sign in with it, and add a pipeline step that logs each sign-in.

15

Mixins, enums and Django’s data helpers. Smarter’s helper mixins and enumerated classes; django.utils.timezone and aware datetimes; django.utils.text and Smarter’s case conversion; validators; and File, Storage and default_storage.

Write a validator, with tests, for a field that Module 8’s model will store, and use an enumerated class for its choices.

16

Template tags, static assets and styles. Simple and inclusion tags; how a template tag reads a React app’s Vite manifest and renders its scripts and root element; collectstatic and WhiteNoise; the Keen theme, built on Bootstrap, in the web console; Tailwind in the Smarter Chat package. ADR 019, ADR 026.

Write a template tag that renders an account’s resource count as a Bootstrap badge, and use it on a console page.

17

Testing, pre-commit and coverage. Test base classes, fixtures built once, cleanup without transaction rollback, mocking Celery and outside services, the pre-commit hooks (black, isort, flake8, bandit and more), and coverage targets per app. ADR 013, ADR 028.

Install the pre-commit hooks, run them on the course branch and fix what they find, then raise the coverage of the Module 8 model and validator to 90%.

18

Putting it all together: the terminal window. The web console’s terminal window, from the setting and the Waffle switch that enable it, through the template tag and the React app, to the logging handler that publishes each record to Redis and the view that streams it to the browser.

Add a filter to the terminal window’s stream, for example by logger name, from the query string through the view, with a test.

The Modules in the Source Code

Every lecture, lab and assignment in this course rests on the files below. Every reference is to the main branch of github.com/smarter-sh/smarter, so that students can open each one during class. Python paths are under smarter/smarter/.

Configuration and Startup

Module

What to read

Where to look

  1. Django, 12-Factor and settings

Every setting is read from the environment, validated and typed by a Pydantic Settings class, and only then copied into Django’s settings. One settings module per environment inherits from base.py. ADR 004, ADR 007.

common/conf/settings.py, common/conf/env.py, common/conf/defaults.py, settings/, .env.example

  1. The startup sequence

Every app’s ready() imports its signals and receivers. The database may not be reachable, or migrated, when ready() runs, so Waffle checks first. The cache is resolved lazily, so that Django does not fall back to a local-memory cache when django.core.cache is imported too early.

apps/dashboard/apps.py, lib/django/waffle/ready.py, lib/cache/lazy_cache.py, common/mixins/helper_mixin.py (SmarterReadyState)

  1. Python logging

A logger factory, loggers switched on per subsystem by Waffle, a filter, a middleware that adds the user to every record, and a handler that publishes records to Redis. ADR 011.

lib/logging/ (get_smarter_logger.py, waffle_switched_logger.py, filters.py, middleware.py, redis_log_handler.py)

The Request

Module

What to read

Where to look

  1. The request and the middleware chain

The MIDDLEWARE list, commented block by block, and the order in which each class sees the request and the response. SmarterRequestMixin reads what middleware attached to the request. A diagnostic middleware checks what every class in the chain returns.

settings/base.py (MIDDLEWARE), lib/django/request.py, common/utils/request.py, lib/django/middleware/debug.py

  1. Writing middleware

A shared base class, replacements for Django’s CORS, CSRF and security middleware, an API key authentication middleware, and middleware that blocks probes for sensitive files and bursts of 404s.

lib/django/middleware/, common/mixins/middleware_mixin.py, lib/drf/middleware.py, apps/llmclient/middleware/security.py

  1. Many hosts; Waffle

django-hosts maps the console, the API and each chatbot’s host to its own URLconf. Waffle switches are declared once, created on first use, and checked safely before the database is ready. ADR 022.

hosts.py, urls/, lib/django/waffle/ (switches.py, is_active.py)

Data

Module

What to read

Where to look

  1. Customizing the ORM

Abstract base models for timestamps and metadata (name, description, version, tags and annotations), and generic querysets and managers whose methods return the subclass’s own types. ADR 017.

lib/django/models/ (timestamped_model.py, metadata_model.py), apps/account/models/user_profile.py (SmarterBaseQuerySetWithPermissions, SmarterBaseModelManager)

  1. The user; ownership and sharing

Smarter keeps Django’s User and adds a UserProfile that joins a user to an account. Every resource inherits an owner, and its manager filters by what a user owns, what is shared with them, and what they may change.

apps/account/models/user_profile.py, apps/account/models/metadata_with_ownership.py, apps/account/mixins.py, common/enum.py (SmarterResourceOwnershipFilterEnum)

  1. Signals and receivers

Every app declares its own signals in signals.py and handles them in receivers.py, for logging, cache invalidation, the journal and prompt progress.

apps/llmclient/signals.py, apps/llmclient/receivers.py, lib/drf/receivers.py, lib/journal/receivers.py

Asynchrony and Redis

Module

What to read

Where to look

  1. Caching complex objects

A decorator that builds a deterministic key from a function’s arguments, caches None correctly with sentinels, and gives each function an .invalidate(). Cached querysets of API keys, invalidated by receivers. ADR 018.

lib/cache/ (decorators.py, cache_sentinel.py, lazy_cache.py), lib/drf/caching.py

  1. Celery and Celery Beat

One Celery app, two queues with a worker each, and a Beat schedule that names each task’s queue. ADR 009, ADR 030.

lib/celery_conf/, workers/ (celery.py, celerybeat.py), apps/prompt/tasks.py, docker-compose.yml

  1. ASGI and publish-subscribe

The ASGI application routes HTTP and WebSockets. A prompt runs in a worker thread while an async generator streams its progress as Server-Sent Events, forwarded from signals through a ContextVar. Log records are published to Redis channels and streamed to the browser.

asgi.py, consumers.py, apps/prompt/progress.py, lib/logging/redis_log_handler.py, terminal_emulator/api/streams.py

APIs and Identity

Module

What to read

Where to look

  1. DRF, Pydantic and authentication

A TokenAuthentication subclass for hashed API keys, a YAML parser, and the Smarter Chat API contract: Pydantic models with a published JSON Schema and a test that it is current. ADR 006, ADR 023, ADR 031.

lib/drf/ (token_authentication.py, models.py, parsers.py), apps/prompt/contract/, apps/llmclient/api/v1/views/base.py

  1. OAuth

Google and GitHub sign-in with Python Social Auth, a pipeline with Smarter’s own steps for creating users, copying their details and redirecting inactive accounts, custom backends, and a middleware for the errors that the pipeline raises.

settings/base.py (AUTHENTICATION_BACKENDS, SOCIAL_AUTH_PIPELINE), apps/account/pipeline.py, lib/social_core/backends/

  1. Mixins, enums and data helpers

Mixins shared by views, models and middleware; enumerated classes; a singleton metaclass; camelCase and snake_case conversion; validators; read-only YAML and CSV files; and time zone aware timestamps.

common/mixins/, common/enum.py, common/utils/conversion.py, lib/django/validators.py, common/utils/file_handlers.py, lib/django/models/timestamped_model.py

Front End, Quality and the Whole

Module

What to read

Where to look

  1. Template tags, static assets and styles

A template tag manager that reads a React app’s Vite manifest and renders its scripts and root element; one small tag per app; the web console’s Keen theme, built on Bootstrap; and Tailwind in the Smarter Chat package. ADR 019, ADR 025, ADR 026.

lib/django/templatetags/, apps/dashboard/templatetags/, templates/, smarter/react/packages/

  1. Testing, pre-commit and coverage

Test base classes without transaction rollback, an in-memory infrastructure provider, pre-commit hooks as the lint standard, and coverage targets per app. ADR 013, ADR 028.

lib/unittest/, apps/infrastructure/providers/memory.py, .pre-commit-config.yaml, Makefile, .github/workflows/test.yml

  1. The terminal window

A page gated by a setting and a Waffle switch, a template tag that hosts a React app, a login-protected view that streams from an async generator subscribed to a Redis channel, a logging handler that publishes to that channel, and the React component that renders the stream.

apps/dashboard/views/terminal_emulator/, apps/dashboard/templatetags/react_terminal_emulator.py, smarter-terminal-emulator

Grading

Component

Weight

Graded assignments

45% (3 × 15%)

Participation (labs and questions)

10%

Capstone project

45%

The grading scale, the rubric levels and the course policies are those of Course Formats and Grading.

Graded Assignments

Each assignment builds on the day’s labs, on the student’s branch of Smarter, and is submitted as a pull request against their own fork.

Assignment 1: Configuration and the Request

Students add a middleware that enforces a per-account request limit read from a new, validated setting in smarter_settings. A Waffle switch turns it on, a Waffle-switched logger records each refusal, and the middleware runs only for the API host.

Turn in: the pull request, with tests, and a paragraph on where the middleware sits in the chain and why.

Criterion

Pts

Exemplary

Developing

Configuration

5

Typed, defaulted and validated in smarter_settings.

Read from os.environ directly.

Middleware

7

Correct place in the chain, with the reason; API host only.

Appended to the end of MIDDLEWARE.

Flags and logging

4

Switch and logger both registered and working.

Hard-coded on.

Tests

4

Both sides of the switch and the limit.

None.

Assignment 2: An Owned, Cached, Asynchronous Resource

Students finish the model of Module 8: a cached per-account count, invalidated by receivers, a Celery task that recalculates it on a Beat schedule, and an event published to Redis whenever it changes.

Turn in: the pull request, with tests.

Criterion

Pts

Exemplary

Developing

Ownership

5

Uses the ownership manager, with a test across two accounts.

Filters by hand.

Caching

6

Plain-value cache key, invalidated by receivers, tested.

Stale values, or a model instance in the key.

Task

5

Idempotent, passes ids, on the right queue and schedule.

Passes model instances.

Publishing

4

One channel per account, published after commit.

One channel for everyone.

Assignment 3: An API with a Contract

Students expose the Module 8 model through an API endpoint whose response is a Pydantic model, authenticated by API key, show it on a console page with a template tag of their own, and bring everything they have written in the course through the pre-commit hooks and to 90% coverage.

Turn in: the pull request, and the coverage report.

Criterion

Pts

Exemplary

Developing

Contract

6

Built from the model and dumped, with extra="forbid".

A dict validated afterwards.

Authentication

4

API key required, with a test for refusal.

Session only.

Template tag

4

Small, tested, and used on a page.

Logic in the template.

Quality

6

Hooks pass, 90% coverage.

Hooks skipped.

Capstone Projects

Students choose one at the end of day 3, and submit it one week after the course.

Capstone A: A Live Activity Feed for Smarter

Learning objective. Build one feature that crosses every layer of a production Django platform, following the patterns that the platform already uses.

Students add an activity Django app to their clone of Smarter. Receivers of signals that Smarter already sends record each event in a model that inherits ownership, so that each account sees only its own. A cached summary per account is invalidated by the same receivers, and a Beat task rolls up yesterday’s events. Every new event is published to a Redis channel and streamed to a console page by an async view, and the page shows the summary with a template tag of their own. An API endpoint returns the feed as a Pydantic model, authenticated by API key. A setting in smarter_settings and a Waffle switch turn the feature on.

What students turn in:

  • A pull request against their fork, with the pre-commit hooks passing and at least 90% coverage of the new app.

  • A one-page design note: the signals chosen, the cache key and its invalidation, and the queue that the task runs on.

  • A six-minute recorded walkthrough, from enabling the switch to an event arriving in the browser.

How it is graded (100 points):

Criterion

Pts

Exemplary

Developing

Data and ownership

20

Model on the ownership base class, with tests that accounts cannot see each other’s rows.

Filters by owner by hand in each view.

Signals, caching and tasks

20

Cache invalidated by receivers, task routed to the right queue, both tested.

Stale cache, or a task that only runs eagerly.

Streaming

15

Async view, Redis channel per account, keepalives and clean shutdown.

Polling, or one channel for everyone.

API and configuration

15

Pydantic response, API key authentication, a validated setting and a Waffle switch.

Hand-built JSON, or configuration read from os.environ.

Quality

20

Hooks pass, 90% coverage, docstrings, and code that reads like the code around it.

Hooks skipped, or tests that do not clean up.

Walkthrough

10

Clear and on time.

Overruns, or the feature does not run.

Capstone B: The Patterns, Brought Home

Learning objective. Judge which of the course’s patterns a real Django project needs, and apply them well.

Students take a Django project of their own, their employer’s (with permission), or an open-source one, and apply at least six patterns from the course, for example a validated settings class, an ownership manager, a middleware, a cached and invalidated query, a Celery Beat task, an async streaming view, a Pydantic API contract, a template tag, or pre-commit and coverage in CI. For each pattern, a short note compares their implementation with Smarter’s and says what they changed and why. They also name one pattern they chose not to adopt, and why.

What students turn in:

  • A pull request or patch series against the project, with tests.

  • A four- to six-page design note, one section per pattern.

  • A six-minute recorded walkthrough.

How it is graded (100 points):

Criterion

Pts

Exemplary

Developing

Choice of patterns

15

Each solves a real problem in the project, and one is rejected with reasons.

Patterns added because the course covered them.

Implementation

30

Idiomatic, and consistent with the project’s own conventions.

Copied from Smarter without adapting.

Tests and quality

20

Tested, linted, and covered.

Untested.

Design note

25

Compares with Smarter, with trade-offs.

Describes the code only.

Walkthrough

10

Clear and on time.

Overruns.

Further Study