Django
La demo es el proyecto del tutorial de Django,
mysite con su app polls, construido paso a paso como lo hace el tutorial: modelos,
migraciones, el admin, vistas genéricas y plantillas. Después, una ventana de Pyzed vota en las
encuestas de dos formas, que corresponden a dos arquitecturas:
desktop.py: la ventana importa los modelos y usa el ORM directamente (mismo proceso).client.py: el sitio corre conrunservery la ventana llama a sus vistas JSON (cliente y servidor).

El proyecto de Django, como lo construye el tutorial
Sección titulada «El proyecto de Django, como lo construye el tutorial»-
Crea el proyecto y la app (ya está hecho en la carpeta de la demo):
Windows, macOS y Linux uv run django-admin startproject mysite .uv run manage.py startapp pollsmacOS y Linux python3 -m venv .venvsource .venv/bin/activatepip install pyzed-ui django httpxdjango-admin startproject mysite .python manage.py startapp pollsWindows (PowerShell) py -m venv .venv.venv\Scripts\Activate.ps1pip install pyzed-ui django httpxdjango-admin startproject mysite .python manage.py startapp pollsmacOS y Linux python3 -m pip install pyzed-ui django httpxpython3 -m django startproject mysite .python3 manage.py startapp pollsWindows (PowerShell) py -m pip install pyzed-ui django httpxpy -m django startproject mysite .py manage.py startapp pollsy añade
"polls.apps.PollsConfig"aINSTALLED_APPSenmysite/settings.py. -
Escribe los modelos en
polls/models.py(parte 2 del tutorial):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) -
Crea las tablas y un usuario administrador:
Windows, macOS y Linux uv run manage.py migrateuv run manage.py createsuperuserWindows, macOS y Linux python manage.py migratepython manage.py createsuperusermacOS y Linux python3 manage.py migratepython3 manage.py createsuperuserWindows (PowerShell) py manage.py migratepy manage.py createsuperuser -
Registra el admin (parte 7), con las opciones en línea, para poder añadir encuestas en http://127.0.0.1:8000/admin/.
-
Escribe las vistas y las URLs (partes 3 y 4):
IndexView,DetailView,ResultsViewyvote, que usa una expresiónF()para que los votos simultáneos no se pisen. La demo añade dos pequeñas vistas JSON para el cliente de escritorio,api/questions/yapi/questions/<id>/vote/. -
Ejecuta el sitio:
Windows, macOS y Linux uv run manage.py runserverWindows, macOS y Linux python manage.py runservermacOS y Linux python3 manage.py runserverWindows (PowerShell) py manage.py runserverLas encuestas están en http://127.0.0.1:8000/polls/.
Forma 1: el ORM en el mismo proceso
Sección titulada «Forma 1: el ORM en el mismo proceso»desktop.py configura Django igual que manage.py y luego importa los modelos. No hace falta
ningún servidor web; la ventana lee y escribe db.sqlite3 a través del ORM.
uv run manage.py migrate # una vezuv run desktop.pypython manage.py migrate # una vezpython desktop.pypython3 manage.py migrate # una vezpython3 desktop.pypy manage.py migrate # una vezpy desktop.pyflowchart LR
subgraph p["uv run desktop.py"]
W["PollsWindow"] -- "Command.perform" --> F["load · vote · create<br/>hilo de trabajo"]
F --> ORM["ORM de Django<br/>polls.models"]
end
ORM --> DB[("db.sqlite3")]
S["manage.py runserver<br/>(opcional, a la vez)"] --> DB
-
Llama a
django.setup()antes de importar los modelos:os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings")django.setup()from polls.models import Question -
Ejecuta las consultas del ORM en hilos de trabajo. Son bloqueantes, así que
Command.performlas ejecuta fuera del hilo de la UI. Django abre una conexión a la base de datos por hilo; cerrarlas al terminar evita que el pool de hilos acumule conexiones:def in_thread(function):@functools.wraps(function)def wrapper(*args, **kwargs):try:return function(*args, **kwargs)finally:connections.close_all()return wrapper@in_threaddef vote(question_id, choice_id):choice = Question.objects.get(pk=question_id).choice_set.get(pk=choice_id)choice.votes = F("votes") + 1choice.save() -
Valida con las reglas del propio modelo.
full_clean()compruebamax_length, los campos obligatorios y los validadores propios;pz.validation_errorsconvierte elValidationErrorde Django en{campo: mensaje}paraui.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)
Forma 2: un cliente HTTP del sitio
Sección titulada «Forma 2: un cliente HTTP del sitio»client.py no importa Django en absoluto. Habla con las vistas JSON del sitio con httpx, así que
el sitio puede correr en este ordenador o en un servidor:
uv run manage.py runserver # terminal 1uv run client.py # terminal 2 (o: uv run client.py https://polls.example.com)python manage.py runserver # terminal 1python client.py # terminal 2 (o: python client.py https://polls.example.com)python3 manage.py runserver # terminal 1python3 client.py # terminal 2 (o: python3 client.py https://polls.example.com)py manage.py runserver # terminal 1py client.py # terminal 2 (o: py 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["Navegador<br/>/polls/ · /admin/"] --> V2["Vistas del tutorial<br/>y 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()Una ventana, dos backends
Sección titulada «Una ventana, dos backends»PollsWindow recibe load, vote y, opcionalmente, create como funciones normales.
desktop.py le pasa funciones normales (que corren en hilos) y client.py le pasa corrutinas
(que corren en el loop de asyncio); Command.perform acepta las dos, así que el código de la
ventana es el mismo.
Código completo
Sección titulada «Código completo»"""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 inthe window show up on /polls/ and in /admin/, and the other way round."""
import functoolsimport 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: E402from django.db import connections, transaction # noqa: E402from django.db.models import F # noqa: E402from django.utils import timezone # noqa: E402
import pyzed as pz # noqa: E402
from polls.models import Question # noqa: E402from polls.views import question_json # noqa: E402from 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_threaddef 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_threaddef 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_threaddef 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"))"""Desktop client for the Django site over HTTP: the site runs on its own, the window uses its JSON API.
uv run manage.py runserver # terminal 1: the site (http://127.0.0.1:8000/polls/) uv run client.py # terminal 2: the window uv run client.py https://polls.example.com
Use this when the site is deployed on a server and the desktop app is one more client. Thewindow needs no Django and no database, only httpx."""
import sys
import httpx
import pyzed as pz
from polls_window import PollsWindow
SITE = sys.argv[1] if len(sys.argv) > 1 else "http://127.0.0.1:8000"
async def load(): try: async with httpx.AsyncClient(base_url=SITE, timeout=5) as client: response = await client.get("/polls/api/questions/") response.raise_for_status() except httpx.HTTPError as error: return f"Can't reach {SITE}: {error or type(error).__name__}" return response.json()["questions"]
async def vote(question_id, choice_id): try: 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() except httpx.HTTPError as error: return f"The vote failed: {error or type(error).__name__}" return response.json()
if __name__ == "__main__": pz.run(PollsWindow(load, vote, mode=f"HTTP client of {SITE}"))"""The Pyzed window shared by desktop.py (Django ORM in this process) and client.py (HTTP to the site).
A backend is three functions returning plain dicts like ``polls.views.question_json``.They may be regular functions (Pyzed runs them in a thread) or ``async def`` (run onPyzed's asyncio loop): ``Command.perform`` accepts both."""
from __future__ import annotations
from datetime import datetime, timedelta, timezonefrom typing import Callable, Optional
import pyzed as pzfrom pyzed import Command, ui
class PollsWindow(pz.App): title = "Polls · Django" size = (960, 600)
def __init__(self, load: Callable, vote: Callable, create: Optional[Callable] = None, mode: str = ""): self.backend_load, self.backend_vote, self.backend_create = load, vote, create self.mode = mode self.questions: list[dict] = [] self.selected_id: int | None = None self.message = "" self.creating = False self.draft = {"question_text": "", "choices": ["", "", ""]} self.errors: dict[str, str] = {}
# -- backend calls ---------------------------------------------------------------------- def on_start(self): return self.reload()
def reload(self): return Command.perform(self.backend_load, self.loaded)
def loaded(self, result): if isinstance(result, str): # the backend couldn't answer self.message = result return self.questions, self.message = result, "" if self.questions and self.selected_id not in {q["id"] for q in self.questions}: self.selected_id = self.questions[0]["id"]
def vote(self, question_id: int, choice_id: int): return Command.perform(lambda: self.backend_vote(question_id, choice_id), self.voted)
def voted(self, result): if isinstance(result, str): self.message = result return None return self.reload()
def create(self): draft = {"question_text": self.draft["question_text"], "choices": [c for c in self.draft["choices"] if c.strip()]} return Command.perform(lambda: self.backend_create(draft), self.created)
def created(self, result): if "id" not in result: # {field: message} self.errors = result return None self.creating, self.selected_id = False, result["id"] self.draft = {"question_text": "", "choices": ["", "", ""]} return self.reload()
# -- view -------------------------------------------------------------------------------- @staticmethod def recent(question: dict) -> bool: # Question.was_published_recently, on the JSON published = datetime.fromisoformat(question["pub_date"]) now = datetime.now(timezone.utc) return now - timedelta(days=1) <= published <= now
def sidebar(self, t): rows = [ui.list_item(q["question_text"], lambda q=q: setattr(self, "selected_id", q["id"]), icon="vote", end=ui.badge("new", tint="success") if self.recent(q) else None, selected=q["id"] == self.selected_id, inset=True, height=30) for q in self.questions] new = ui.button("New question", lambda: setattr(self, "creating", True), icon="plus", full_width=True) \ if self.backend_create else ui.muted("Add questions in the Django admin.", size="sm") return pz.container(pz.column([ui.muted("QUESTIONS", size="xs", weight="semibold"), *rows, pz.vspace(), new], spacing=2), padding=8, width=300, height="fill", bg=t.panel)
def results(self): question = next((q for q in self.questions if q["id"] == self.selected_id), None) if question is None: return ui.empty_state("No polls are available.", icon="vote") total = sum(c["votes"] for c in question["choices"]) or 1 rows = [pz.row([ pz.container(ui.label(c["choice_text"]), width=200), pz.container(ui.progress(c["votes"] / total, height=8), width="fill"), pz.container(ui.muted(f"{c['votes']} vote{'s' if c['votes'] != 1 else ''}", size="sm"), width=70, align_x="end"), ui.button("Vote", lambda c=c: self.vote(question["id"], c["id"]), size="sm"), ], spacing=12, align="center") for c in question["choices"]] return pz.column([ui.heading(question["question_text"], level=2), ui.muted(f"Published {question['pub_date'][:16].replace('T', ' ')}", size="sm"), *rows], spacing=14, padding=24)
def dialog(self): if not self.creating: return None choices = self.draft["choices"]
def set_choice(i): return lambda value: choices.__setitem__(i, value)
form = pz.column([ ui.field("Question", ui.input(self.draft["question_text"], lambda v: self.draft.__setitem__("question_text", v), placeholder="What's new?", width=260), error=self.errors.get("question_text")), *[ui.field(f"Choice {i + 1}", ui.input(text, set_choice(i), width=260)) for i, text in enumerate(choices)], ui.callout(self.errors[""], tint="error") if self.errors.get("") else None, ], spacing=12) close = lambda: setattr(self, "creating", False) # noqa: E731 return ui.modal(form, title="New question", on_close=close, width=520, footer=[ui.button("Cancel", close, variant="ghost"), ui.button("Publish", self.create, icon="check", variant="solid")])
def view(self): t = self.resolved_theme() status = ui.status_bar([ui.status_item(self.message or self.mode, icon="circle-alert" if self.message else "database", icon_color="error" if self.message else "success")]) page = pz.column([pz.row([self.sidebar(t), pz.container(self.results(), width="fill", height="fill", bg=t.editor)]), status]) return ui.layer(page, self.dialog(), on_dismiss=lambda: setattr(self, "creating", False), backdrop="#00000066")"""The views from Django's tutorial (parts 3 and 4), plus a small JSON API for the desktop client.
Tutorial: https://docs.djangoproject.com/en/stable/intro/tutorial04/"""
import json
from django.db.models import Ffrom django.http import HttpResponseRedirect, JsonResponsefrom django.shortcuts import get_object_or_404, renderfrom django.urls import reversefrom django.utils import timezonefrom django.views import genericfrom django.views.decorators.csrf import csrf_exemptfrom django.views.decorators.http import require_GET, require_POST
from .models import Choice, Question
class IndexView(generic.ListView): template_name = "polls/index.html" context_object_name = "latest_question_list"
def get_queryset(self): """Return the last five published questions (not including those set to be published in the future).""" return Question.objects.filter(pub_date__lte=timezone.now()).order_by("-pub_date")[:5]
class DetailView(generic.DetailView): model = Question template_name = "polls/detail.html"
def get_queryset(self): """Excludes any questions that aren't published yet.""" return Question.objects.filter(pub_date__lte=timezone.now())
class ResultsView(generic.DetailView): model = Question template_name = "polls/results.html"
def vote(request, question_id): question = get_object_or_404(Question, pk=question_id) try: selected_choice = question.choice_set.get(pk=request.POST["choice"]) except (KeyError, Choice.DoesNotExist): # Redisplay the question voting form. return render(request, "polls/detail.html", {"question": question, "error_message": "You didn't select a choice."}) else: selected_choice.votes = F("votes") + 1 selected_choice.save() # Always return an HttpResponseRedirect after successfully dealing with POST data. return HttpResponseRedirect(reverse("polls:results", args=(question.id,)))
# -- JSON API for desktop clients (client.py) ---------------------------------------------------
def question_json(question): return { "id": question.id, "question_text": question.question_text, "pub_date": question.pub_date.isoformat(), "choices": [{"id": c.id, "choice_text": c.choice_text, "votes": c.votes} for c in question.choice_set.all()], }
@require_GETdef api_questions(request): questions = Question.objects.filter(pub_date__lte=timezone.now()).order_by("-pub_date").prefetch_related("choice_set") return JsonResponse({"questions": [question_json(q) for q in questions]})
@csrf_exempt # a desktop app has no CSRF cookie; protect a real API with tokens instead (e.g. Django REST framework)@require_POSTdef api_vote(request, question_id): question = get_object_or_404(Question, pk=question_id) try: choice_id = json.loads(request.body)["choice"] selected_choice = question.choice_set.get(pk=choice_id) except (KeyError, ValueError, Choice.DoesNotExist): return JsonResponse({"detail": "You didn't select a choice."}, status=400) selected_choice.votes = F("votes") + 1 selected_choice.save() return JsonResponse(question_json(question))