Skip to content

Django

The demo is the project from Django’s tutorial, mysite with its polls app, built step by step as the tutorial does: models, migrations, the admin, generic views and templates. A Pyzed window then votes on the polls in two ways, which correspond to two architectures:

  • desktop.py: the window imports the models and uses the ORM directly (same process).
  • client.py: the site runs with runserver and the window calls its JSON views (client and server).

A desktop window with Django polls, results as bars and vote buttons

The Django project, as the tutorial builds it

Section titled “The Django project, as the tutorial builds it”
  1. Create the project and the app (already done in the demo folder):

    Windows, macOS and Linux
    uv run django-admin startproject mysite .
    uv run manage.py startapp polls

    and add "polls.apps.PollsConfig" to INSTALLED_APPS in mysite/settings.py.

  2. Write the models in polls/models.py (tutorial part 2):

    class Question(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField("date published")
    class Choice(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)
  3. Create the tables and an admin user:

    Windows, macOS and Linux
    uv run manage.py migrate
    uv run manage.py createsuperuser
  4. Register the admin (part 7), with the choices inline, so you can add polls at http://127.0.0.1:8000/admin/.

  5. Write the views and URLs (parts 3 and 4): IndexView, DetailView, ResultsView and vote, which uses an F() expression so concurrent votes don’t overwrite each other. The demo adds two small JSON views for the desktop client, api/questions/ and api/questions/<id>/vote/.

  6. Run the site:

    Windows, macOS and Linux
    uv run manage.py runserver

    The polls are at http://127.0.0.1:8000/polls/.

desktop.py sets up Django the way manage.py does and then imports the models. No web server is needed; the window reads and writes db.sqlite3 through the ORM.

Windows, macOS and Linux
uv run manage.py migrate # once
uv run desktop.py
flowchart LR
    subgraph p["uv run desktop.py"]
        W["PollsWindow"] -- "Command.perform" --> F["load · vote · create<br/>worker thread"]
        F --> ORM["Django ORM<br/>polls.models"]
    end
    ORM --> DB[("db.sqlite3")]
    S["manage.py runserver<br/>(optional, at the same time)"] --> DB
  1. Call django.setup() before importing models:

    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings")
    django.setup()
    from polls.models import Question
  2. Run ORM queries in worker threads. They block, so Command.perform runs them off the UI thread. Django opens one database connection per thread; closing them afterwards keeps the thread pool from holding connections:

    def in_thread(function):
    @functools.wraps(function)
    def wrapper(*args, **kwargs):
    try:
    return function(*args, **kwargs)
    finally:
    connections.close_all()
    return wrapper
    @in_thread
    def vote(question_id, choice_id):
    choice = Question.objects.get(pk=question_id).choice_set.get(pk=choice_id)
    choice.votes = F("votes") + 1
    choice.save()
  3. Validate with the model’s own rules. full_clean() checks max_length, required fields and custom validators; pz.validation_errors turns Django’s ValidationError into {field: message} for ui.field:

    question = Question(question_text=draft["question_text"], pub_date=timezone.now())
    try:
    question.full_clean()
    except ValidationError as error:
    return pz.validation_errors(error)
    with transaction.atomic():
    question.save()
    for text in draft["choices"]:
    question.choice_set.create(choice_text=text, votes=0)

client.py doesn’t import Django at all. It talks to the site’s JSON views with httpx, so the site can run on this computer or on a server:

Windows, macOS and Linux
uv run manage.py runserver # terminal 1
uv run client.py # terminal 2 (or: uv run client.py https://polls.example.com)
flowchart LR
    W["PollsWindow<br/>uv run client.py"] -- "httpx · JSON" --> V["polls/views.py<br/>api_questions · api_vote"]
    B["Browser<br/>/polls/ · /admin/"] --> V2["Tutorial views<br/>and admin"]
    subgraph s["uv run manage.py runserver"]
        V --> DB[("db.sqlite3")]
        V2 --> DB
    end
async def vote(question_id, choice_id):
async with httpx.AsyncClient(base_url=SITE, timeout=5) as client:
response = await client.post(f"/polls/api/questions/{question_id}/vote/", json={"choice": choice_id})
response.raise_for_status()
return response.json()

PollsWindow receives load, vote and optionally create as plain functions. desktop.py passes regular functions (run in threads) and client.py passes coroutines (run on the asyncio loop); Command.perform accepts both, so the window code is the same.

"""Desktop app on Django's ORM: the models, validation and database of the site, with no web server.
uv run manage.py migrate # once, as in Django's tutorial
uv run desktop.py
The window imports the polls app and queries it directly (architecture "same process").
`uv run manage.py runserver` can run at the same time on the same database: votes cast in
the window show up on /polls/ and in /admin/, and the other way round.
"""
import functools
import os
import django
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings")
django.setup() # what manage.py does before anything touches the models
from django.core.exceptions import ValidationError # noqa: E402
from django.db import connections, transaction # noqa: E402
from django.db.models import F # noqa: E402
from django.utils import timezone # noqa: E402
import pyzed as pz # noqa: E402
from polls.models import Question # noqa: E402
from polls.views import question_json # noqa: E402
from polls_window import PollsWindow # noqa: E402
def in_thread(function):
"""Pyzed runs these in a worker thread; Django opens one connection per thread, so close it."""
@functools.wraps(function)
def wrapper(*args, **kwargs):
try:
return function(*args, **kwargs)
finally:
connections.close_all()
return wrapper
@in_thread
def load():
questions = Question.objects.filter(pub_date__lte=timezone.now()).order_by("-pub_date").prefetch_related("choice_set")
return [question_json(q) for q in questions]
@in_thread
def vote(question_id, choice_id):
choice = Question.objects.get(pk=question_id).choice_set.get(pk=choice_id)
choice.votes = F("votes") + 1 # the database adds one, so two voters never overwrite each other
choice.save()
@in_thread
def create(draft):
question = Question(question_text=draft["question_text"], pub_date=timezone.now())
try:
question.full_clean() # the model's own rules: required, max_length=200
if len(draft["choices"]) < 2:
raise ValidationError("A poll needs at least two choices.")
except ValidationError as error:
return pz.validation_errors(error)
with transaction.atomic():
question.save()
for text in draft["choices"]:
question.choice_set.create(choice_text=text, votes=0)
return question_json(question)
if __name__ == "__main__":
pz.run(PollsWindow(load, vote, create, mode="Django ORM, same process"))