Checkpoint: Production Django

Reviewed & published by Brayan K

This checkpoint consolidates the advanced half of the course — context processors, custom user models, formsets, view mixins, async views, Channels, and Celery — into one runnable build challenge and a short self-quiz.

Part of the free Django course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

You'll recall each idea, then design a real feature that combines a custom-user permission mixin with a background task, build it from a starter, check it against a full solution, and confirm your understanding with a checkpoint quiz.

What This Checkpoint Reviews

1 Recap: The Seven Advanced Skills

Before the build challenge, here is the through-line of everything you covered in this stretch. Each row is a tool you'll reach for in real, production Django.

LessonCore idea
Context ProcessorsFunctions that merge a dict into every template's context
Custom User ModelAUTH_USER_MODEL plus a manager; set it at project start
FormsetsValidate and save many forms together (management form!)
CBV MixinsCompose access checks and context via the MRO
Async Viewsasync def + gather to overlap I/O under ASGI
ChannelsWebSocket consumers and group broadcast over Redis
CeleryEnqueue slow work with .delay(); a worker runs it

2 Review: A Custom User Manager

Before the build challenge, one fully worked piece: what create_user() on a custom user model's manager does. It normalises the email (lower-cases only the domain part, because that half is case-insensitive), refuses an empty address, and stores a hash instead of the plain password. The has_perm method is what the permission mixin in the build challenge calls. Read the comments, run it, and check the output against the claim at the bottom.

# WORKED EXAMPLE: a custom user model's manager in plain Python.
# On a custom user model, UserManager.create_user() normalises the email,
# refuses an empty one, and hashes the password before saving. This mirrors that.

class User:
    def __init__(self, email, password_hash, perms):
        self.email = email
        self.password_hash = password_hash
        self.perms = set(perms)

    def has_perm(self, perm):            # what PermissionRequiredMixin calls
        return perm in self.perms

class UserManager:
    def normalize_email(self, email):
        local, _, domain = email.partition("@")
        return f"{local}@{domain.lower()}"     # only the domain is case-insensitive

    def create_user(self, email, password, perms=()):
        if not email:
            raise ValueError("The Email field must be set")
        email = self.normalize_email(email)
        return User(email, f"hashed-{len(password)}-chars", perms)   # never the plain text

manager = UserManager()
boss = manager.create_user("Boss@Example.COM", "s3cret", perms=["reports.export"])
print("email:", boss.email)                        # domain lower-cased, local part kept
print("password stored as:", boss.password_hash)
print("can export:", boss.has_perm("reports.export"))
print("can delete:", boss.has_perm("reports.delete"))

try:
    manager.create_user("", "nope")
except ValueError as err:
    print("error:", err)

# ✅ Expected output:
# email: Boss@example.com
# password stored as: hashed-6-chars
# can export: True
# can delete: False
# error: The Email field must be set

🏗️ Build Challenge: A Guarded, Backgrounded Feature

Time to put it together. Design an Export Report feature that combines the two skills that pair up most often in real services:

The starter below has three ___ blanks — the success status code, the denied request, and the index of the task name. Fill them in, then run to see the feature respond. Try it yourself before opening the full solution.

# 🎯 BUILD CHALLENGE (starter): combine TWO advanced skills into one feature —
# a custom-user PERMISSION MIXIN that guards a view, and a BACKGROUND TASK
# that the view enqueues instead of running inline.
# Fill in the three ___ blanks, then run to see it work.

QUEUE = []   # the "broker" — tasks wait here for a worker

def shared_task(func):
    func.delay = lambda *a: QUEUE.append((func.__name__, a)) or "queued"
    return func

@shared_task
def export_report(user_id):
    return f"report for user {user_id}"

class PermissionRequiredMixin:
    permission_required = None
    def dispatch(self, request):
        # custom user carries a set of permission strings
        if self.permission_required not in request["user"]["perms"]:
            return {"status": 403, "detail": "Forbidden"}
        return self.handle(request)

class ExportReportView(PermissionRequiredMixin):
    permission_required = "reports.export"
    def handle(self, request):
        # guard passed -> enqueue the slow work, return immediately
        export_report.delay(request["user"]["id"])
        return {"status": ___, "detail": "Report queued"}   # 👉 replace ___ with 202

view = ExportReportView()

allowed = {"user": {"id": 5, "perms": ["reports.export"]}}
denied  = {"user": {"id": 9, "perms": []}}

print("allowed:", view.dispatch(allowed))
print("denied:", view.dispatch(___))           # 👉 replace ___ with denied
print("queued tasks:", QUEUE)
print("task name queued:", QUEUE[0][___])       # 👉 replace ___ with 0

# ✅ Expected output:
# allowed: {'status': 202, 'detail': 'Report queued'}
# denied: {'status': 403, 'detail': 'Forbidden'}
# queued tasks: [('export_report', (5,))]
# task name queued: export_report

A fuller version wires up a real custom User with a has_perm method, a tiny Celery-like app with a broker, worker, and result backend, and a view that guards the request then enqueues the task — the plain-Python core of a PermissionRequiredMixin on a custom user calling task.delay():

# BUILD CHALLENGE (full solution): a production-shaped feature that ties
# together a custom user model, a permission mixin, AND a background task
# with a result backend. This is the runnable core of:
#   - a custom User carrying permissions (custom-user lesson)
#   - a PermissionRequiredMixin guarding a view (cbv-mixins lesson)
#   - Celery .delay() + a worker + result backend (celery lesson)

# --- custom user model (carries perms, like AUTH_USER_MODEL would) ---
class User:
    def __init__(self, id, email, perms):
        self.id = id
        self.email = email
        self.perms = set(perms)
    def has_perm(self, perm):
        return perm in self.perms

# --- a tiny Celery-like app: broker + worker + result backend ---
class TaskApp:
    def __init__(self):
        self.broker = []
        self.results = {}
        self._n = 0
    def task(self, func):
        def delay(*args):
            self._n += 1
            tid = f"task-{self._n}"
            self.broker.append((tid, func, args))
            return tid
        func.delay = delay
        return func
    def work(self):
        while self.broker:
            tid, func, args = self.broker.pop(0)
            self.results[tid] = func(*args)
    def result(self, tid):
        return self.results.get(tid)

app = TaskApp()

@app.task
def export_report(user_id):
    # pretend this is slow work (DB crunch, PDF render, etc.)
    return f"report.pdf for user {user_id}"

# --- the permission mixin (from the cbv-mixins lesson) ---
class PermissionRequiredMixin:
    permission_required = None
    def dispatch(self, request):
        user = request["user"]
        if not user.has_perm(self.permission_required):
            return {"status": 403, "detail": "Forbidden"}
        return self.handle(request)

# --- the view: guard, then enqueue the background task ---
class ExportReportView(PermissionRequiredMixin):
    permission_required = "reports.export"
    def handle(self, request):
        task_id = export_report.delay(request["user"].id)
        return {"status": 202, "detail": "Report queued", "task_id": task_id}

view = ExportReportView()

manager = User(1, "boss@site.com", ["reports.export"])
intern = User(2, "intern@site.com", [])

# 1) Authorized user: guard passes, task is enqueued (not run yet)
resp = view.dispatch({"user": manager})
print("authorized:", resp)
print("broker before worker:", len(app.broker))

# 2) Unauthorized user: blocked with 403, nothing queued
print("unauthorized:", view.dispatch({"user": intern}))

# 3) Run the worker, then read the finished result by task id
app.work()
print("broker after worker:", len(app.broker))
print("result:", app.result(resp["task_id"]))

# EXPECTED:
# authorized: {'status': 202, 'detail': 'Report queued', 'task_id': 'task-1'}
# broker before worker: 1
# unauthorized: {'status': 403, 'detail': 'Forbidden'}
# broker after worker: 0
# result: report.pdf for user 1

🎯 Your Turn: Context Processor, Management Form, group_send

The build challenge combined a mixin with Celery. This exercise covers three of the other tools from this module, each reduced to the plain-Python logic underneath: a context processor (a function whose dict is merged into every template's context), the hidden management form a formset needs, and a Channels group broadcast that turns an event type into a handler name. Fill the three blanks, then run and compare with the expected output.

# 🎯 YOUR TURN — three production pieces in plain Python: a context
# processor, a formset's management form, and a Channels group broadcast.
# Fill in the three ___ blanks, then press Run.

# 1) CONTEXT PROCESSOR: a function that takes the request and returns a dict
#    that is merged into EVERY template's context.
def site_settings(request):
    return {"site_name": "BookShelf", "year": 2026}

def build_context(request, view_context, processors):
    context = dict(view_context)
    for processor in processors:
        context.update(processor(___))       # 👉 replace ___ with request
    return context

print(build_context({"path": "/"}, {"title": "Home"}, [site_settings]))

# 2) FORMSETS: the hidden management form says how many forms were posted.
#    Without it, validation fails with "ManagementForm data is missing".
def validate_formset(post):
    total = post.get("form-TOTAL_FORMS")
    if total is None:
        return ["ManagementForm data is missing or has been tampered with"]
    errors = []
    for i in range(int(total)):
        if post.get(f"form-{i}-title", "") == "":
            errors.append(f"form {i}: title is required")
    return errors

post = {"form-TOTAL_FORMS": ___, "form-INITIAL_FORMS": "0",   # 👉 replace ___ with "2"
        "form-0-title": "Dune", "form-1-title": ""}
print(validate_formset(post))
print(validate_formset({"form-0-title": "Dune"}))      # no management form

# 3) CHANNELS: group_send broadcasts an event to every consumer in a group;
#    the event's "type" names the handler method, with dots as underscores.
class ChatConsumer:
    def __init__(self, name):
        self.name = name
    def chat_message(self, event):
        print(f"{self.name} got: {event['text']}")

GROUPS = {"room1": [ChatConsumer("ada"), ChatConsumer("ben")]}

def group_send(group, event):
    handler_name = event["type"].replace(".", "_")
    for consumer in GROUPS[group]:
        getattr(consumer, handler_name)(event)

group_send("room1", {"type": ___, "text": "hello"})   # 👉 replace ___ with "chat.message"

# ✅ Expected output:
# {'title': 'Home', 'site_name': 'BookShelf', 'year': 2026}
# ['form 1: title is required']
# ['ManagementForm data is missing or has been tampered with']
# ada got: hello
# ben got: hello

📝 Checkpoint Quiz

Try to answer each in your head first, then expand to check.

A: You wrote the function but never registered it. Add its dotted path to the context_processors list inside TEMPLATES["OPTIONS"]. Also make sure the processor actually returns a dict — returning None raises a TypeError.

A: Because Django can't cleanly swap the user model once tables hold foreign keys to it. Define a custom user (even an empty AbstractUser subclass), set AUTH_USER_MODEL, and reference settings.AUTH_USER_MODEL in foreign keys before the first migration — that keeps the door open to add fields later.

A: The management form, {{ formset.management_form }}. It carries TOTAL_FORMS and INITIAL_FORMS so Django knows how many forms were submitted. Without it you get "ManagementForm data is missing or has been tampered with."

A: Because the MRO runs earlier-listed classes first, so the mixin's dispatch check runs before the view executes. The mixin checks the user (login, permission, or test_func) and either denies the request or calls super().dispatch() to continue down the chain.

A: When the view makes several independent I/O calls that can overlap. Launching them with asyncio.gather runs them concurrently, so three 100ms calls finish in about 100ms instead of 300ms. Async does not speed up CPU-bound work or a single quick query.

A: Calling the function directly runs it inline and blocks; .delay() serializes the arguments onto the broker and returns instantly so a worker runs it later. In Channels, group_send broadcasts an event to every consumer in a named group, and the event's type names the handler method invoked on each member.

❓ Frequently Asked Questions

Checkpoint complete — you can build production-grade features!

You recalled context processors, custom user models, formsets, view mixins, async views, Channels, and Celery, then combined a custom-user permission mixin with a background task into one runnable feature and confirmed it against a full solution and quiz.

Practice quiz

A variable from your context processor isn't appearing in templates. What's the most likely cause?

  • DEBUG is set to False
  • The template extends the wrong base
  • The processor's dotted path was never added to the context_processors list in TEMPLATES
  • The variable name starts with an underscore

Answer: The processor's dotted path was never added to the context_processors list in TEMPLATES. A context processor must be registered in TEMPLATES['OPTIONS']['context_processors'] and must return a dict.

Why should you set AUTH_USER_MODEL before your first migration?

  • Django can't cleanly swap the user model once tables hold foreign keys to it
  • It improves query speed
  • It is required for the admin to load
  • Migrations fail if it is set later, but only on PostgreSQL

Answer: Django can't cleanly swap the user model once tables hold foreign keys to it. Swapping the user model after FKs exist is painful, so define and set it before the first migration.

What hidden piece must every formset template include?

  • {{ formset.csrf_token }}
  • {{ formset.hidden_fields }}
  • {{ formset.media }}
  • {{ formset.management_form }}

Answer: {{ formset.management_form }}. The management form carries TOTAL_FORMS and INITIAL_FORMS; without it Django raises a ManagementForm error.

Why must an access-control mixin be listed before the base view in the class definition?

  • Alphabetical order is required
  • The MRO runs earlier-listed classes first, so the mixin's dispatch check runs before the view executes
  • It only matters for async views
  • Python ignores the order entirely

Answer: The MRO runs earlier-listed classes first, so the mixin's dispatch check runs before the view executes. Method resolution order runs the leftmost classes first, so the guard's dispatch runs before the view.

When does an async view actually make a page faster?

  • When the view makes several independent I/O calls that can overlap with asyncio.gather
  • Always, for every request
  • Only for CPU-bound computations
  • When DEBUG is True

Answer: When the view makes several independent I/O calls that can overlap with asyncio.gather. Async helps overlap independent I/O; it does not speed up CPU-bound work or a single quick query.

What is the difference between calling a Celery task directly and calling .delay()?

  • There is no difference
  • .delay() runs it twice for reliability
  • Direct call runs inline and blocks; .delay() puts it on the broker and returns instantly for a worker to run
  • Direct call requires Redis; .delay() does not

Answer: Direct call runs inline and blocks; .delay() puts it on the broker and returns instantly for a worker to run. .delay() serializes the args onto the broker and returns immediately so a worker runs the task later.

What does a Channels group_send do?

  • Sends an email to a group of users
  • Broadcasts an event to every consumer in a named group, with the event's type naming the handler
  • Closes all WebSocket connections
  • Migrates the database schema

Answer: Broadcasts an event to every consumer in a named group, with the event's type naming the handler. group_send broadcasts to all consumers in a group; the event 'type' selects the handler method to call.

Under which server interface do async views and Channels run?

  • WSGI
  • CGI
  • FastCGI
  • ASGI

Answer: ASGI. Async views and WebSocket consumers require the ASGI interface, not the synchronous WSGI one.

In the build challenge, which status code signals the report was queued for background processing?

  • 200 OK
  • 202 Accepted
  • 201 Created
  • 204 No Content

Answer: 202 Accepted. 202 Accepted means the request was accepted and the slow work was enqueued, not yet completed.

What does a custom user model's manager typically provide?

  • WebSocket broadcasting
  • Automatic pagination
  • Methods like create_user and create_superuser for building users correctly
  • Template context injection

Answer: Methods like create_user and create_superuser for building users correctly. A custom user manager defines create_user/create_superuser so users are created with proper fields and hashing.

Continue this course

Frequently asked questions

What does this checkpoint cover?

It consolidates the advanced, production-facing half of the Django course: context processors that inject site-wide variables, a custom user model with its own manager, formsets for editing many records at once, class-based view mixins for access control, async views and ASGI, real-time WebSockets with Channels, and background work with Celery. The build challenge combines the two pieces that pair up most often in real apps — a permission mixin tied to a custom user, and a background task the view enqueues rather than runs inline.

Do I need a running Django project to do the build challenge?

No. Because a full Django project cannot boot inside the editor, the build challenge is written as a faithful plain-Python simulation that actually runs and prints output. It mirrors exactly what a guarded view does — check the custom user's permissions through a mixin, enqueue a Celery task with .delay(), and later read the result from the backend — so you can practise the logic and watch it work without any server, broker, or installed packages.

Is this checkpoint graded or required?

No. A checkpoint is a self-assessment milestone, not a graded exam. It exists so you can pause, recall what you learned across the recent advanced lessons, and prove to yourself that the ideas stuck before moving on. Work the build challenge and quiz at your own pace, and revisit any earlier lesson — context processors, custom user, formsets, mixins, async, channels, or celery — whose concept still feels shaky.

What comes after this checkpoint?

The capstone: a complete Blog application that pulls together everything from the entire course — models and the ORM, views, templates, forms, authentication, an admin, pagination, and an API. This checkpoint is your final tune-up before that larger project, so make sure permission mixins, custom users, and background tasks feel comfortable before you continue.