Ir al contenido

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 con runserver y la ventana llama a sus vistas JSON (cliente y servidor).

Una ventana de escritorio con encuestas de Django, resultados en barras y botones para votar

El proyecto de Django, como lo construye el tutorial

Sección titulada «El proyecto de Django, como lo construye el tutorial»
  1. 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 polls

    y añade "polls.apps.PollsConfig" a INSTALLED_APPS en mysite/settings.py.

  2. 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)
  3. Crea las tablas y un usuario administrador:

    Windows, macOS y Linux
    uv run manage.py migrate
    uv run manage.py createsuperuser
  4. Registra el admin (parte 7), con las opciones en línea, para poder añadir encuestas en http://127.0.0.1:8000/admin/.

  5. Escribe las vistas y las URLs (partes 3 y 4): IndexView, DetailView, ResultsView y vote, que usa una expresión F() 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/ y api/questions/<id>/vote/.

  6. Ejecuta el sitio:

    Windows, macOS y Linux
    uv run manage.py runserver

    Las encuestas están en http://127.0.0.1:8000/polls/.

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.

Windows, macOS y Linux
uv run manage.py migrate # una vez
uv run desktop.py
flowchart 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
  1. Llama a django.setup() antes de importar los modelos:

    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings")
    django.setup()
    from polls.models import Question
  2. Ejecuta las consultas del ORM en hilos de trabajo. Son bloqueantes, así que Command.perform las 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_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. Valida con las reglas del propio modelo. full_clean() comprueba max_length, los campos obligatorios y los validadores propios; pz.validation_errors convierte el ValidationError de Django en {campo: mensaje} para 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 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:

Windows, macOS y Linux
uv run manage.py runserver # terminal 1
uv run client.py # terminal 2 (o: 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["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()

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.

"""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"))