Entrega Incremental 3 - FastAPI + HTMX + Bootstrap
REQUIERE E1 (Dominio) y E2 (Testing) completadas. Se expondrá el mismo dominio probado vía web. Los tests de E2 deben pasar.
Estas clases preparan los conocimientos necesarios para esta evaluación:
Convertir el dominio de TaskFlow en una aplicación web interactiva usando FastAPI (backend) y HTMX (frontend dinámico sin JavaScript complejo).
Individual o en parejas (máximo 2 personas). Debe ser el mismo equipo de E1-E2.
Formato de entrega: Repositorio GitHub con: dominio + tests + API FastAPI + templates Jinja2. Incluir requirements.txt y README.md con instrucciones para ejecutar con uvicorn.
Descripción: Implementar endpoints CRUD para todas las entidades del dominio.
GET /usuarios, POST /usuarios, GET /usuarios/{id}GET /proyectos, POST /proyectos, GET /proyectos/{id}, POST /proyectos/{id}/tareasPATCH /tareas/{id}/completar, PATCH /tareas/{id}/prioridad200 OK: Operación exitosa201 Created: Recurso creado404 Not Found: Recurso no existe422 Unprocessable Entity: Validación fallida/docs debe mostrar Swagger UI funcionalDescripción: Validación automática de datos con Pydantic.
UsuarioCreate: Datos para crear usuario (username, email)UsuarioResponse: Respuesta con datos del usuarioProyectoCreate: Datos para crear proyectoTareaCreate: Datos para crear tareaTareaUpdate: Datos para actualizar tareaField(min_length=3, max_length=50) para stringsField(default=PrioridadTarea.MEDIA) con valores por defectoEmailStr para emails (importar de pydantic)Descripción: Interfaz web dinámica sin JavaScript complejo, usando atributos HTMX.
hx-get="/proyectos" hx-target="#lista"hx-posthx-gethx-patch, sin recargar páginahx-patchhx-get, hx-post, hx-patchhx-target: Donde insertar la respuestahx-swap="innerHTML" o outerHTMLhx-trigger="click" o changeDescripción: Renderizar HTML con Jinja2, no strings en Python.
templates/base.html: Layout base con Bootstrap 5templates/index.html: Página principaltemplates/proyectos/lista.html: Lista de proyectostemplates/proyectos/form.html: Formulario crear proyectotemplates/tareas/lista.html: Lista de tareas parcial (HTMX)templates/tareas/item.html: Item de tarea para HTMXfrom fastapi import FastAPI, Request
from fastapi.templating import Jinja2Templates
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel, Field, EmailStr
from typing import Optional
from src.domain.usuario import Usuario
from src.domain.proyecto import Proyecto
from src.domain.tarea import Tarea, PrioridadTarea, EstadoTarea
app = FastAPI(title="TaskFlow API", version="1.0.0")
templates = Jinja2Templates(directory="templates")
# Almacenamiento en memoria (se reemplazará en E4)
usuarios_db: dict[int, Usuario] = {}
proyectos_db: dict[int, Proyecto] = {}
class UsuarioCreate(BaseModel):
"""Modelo para crear usuario."""
username: str = Field(..., min_length=3, max_length=50)
email: EmailStr
nombre_completo: Optional[str] = None
class UsuarioResponse(BaseModel):
"""Modelo de respuesta de usuario."""
id: int
username: str
email: str
activo: bool
class Config:
from_attributes = True
@app.get("/", response_class=HTMLResponse)
async def home(request: Request):
"""Página principal."""
return templates.TemplateResponse(
"index.html",
{"request": request, "proyectos": list(proyectos_db.values())}
)
@app.get("/proyectos", response_class=HTMLResponse)
async def lista_proyectos(request: Request):
"""Lista de proyectos (parcial para HTMX)."""
return templates.TemplateResponse(
"proyectos/lista.html",
{"request": request, "proyectos": list(proyectos_db.values())}
)
@app.post("/proyectos", response_model=dict)
async def crear_proyecto(data: ProyectoCreate):
"""Crear nuevo proyecto."""
nuevo_id = len(proyectos_db) + 1
lider = usuarios_db.get(data.lider_id)
if not lider:
raise HTTPException(status_code=404, detail="Líder no encontrado")
proyecto = Proyecto(
nombre=data.nombre,
descripcion=data.descripcion,
lider=lider
)
proyectos_db[nuevo_id] = proyecto
return {"id": nuevo_id, "mensaje": "Proyecto creado"}
<div class="list-group-item d-flex justify-content-between align-items-center"
id="tarea-{{ tarea.id }}">
<div>
<span class="badge bg-{{ prioridad_color }} me-2">
{{ tarea.prioridad.name }}
</span>
<span class="{% if tarea.estado == 'completada' %}text-decoration-line-through text-muted{% endif %}">
{{ tarea.titulo }}
</span>
</div>
{% if tarea.estado != 'completada' %}
<button class="btn btn-sm btn-success"
hx-patch="/tareas/{{ tarea.id }}/completar"
hx-target="#tarea-{{ tarea.id }}"
hx-swap="outerHTML">
<i class="bi bi-check-lg"></i> Completar
</button>
{% endif %}
</div>
pip install fastapi uvicorn jinja2 python-multipart
UsuarioCreate, ProyectoCreate, TareaCreate, Response models
GET lista, POST crear, GET por ID
CRUD completo + agregar tareas
Completar y cambiar prioridad
Base layout, listas, formularios
Atributos hx-get, hx-post, hx-target en botones y forms
| Criterio | Excelente (5.0) | Aceptable (3.5) | Insuficiente | Peso |
|---|---|---|---|---|
| API REST | Todos los endpoints funcionan, status codes correctos, /docs completo | Endpoints básicos, algunos status incorrectos | API incompleta o no funciona | 30% |
| Frontend HTMX | Sin recargas, hx-post/get/patch bien usados, UX fluida | Algunas recargas, HTMX parcial | Sin HTMX o JavaScript puro | 25% |
| Templates Jinja2 | Templates organizados, herencia base.html, sin HTML en Python | Templates básicos sin herencia | HTML como strings en Python | 20% |
| Diseño Bootstrap | Responsivo, profesional, formularios y tablas bien estilados | Diseño funcional pero básico | Sin Bootstrap o diseño roto | 15% |
| Integración E1+E2 | Usa dominio real de E1, tests de E2 pasan | Dominio copiado/modificado | No usa el dominio de E1 | 10% |
El prototipo que construyas aquí perderá datos al reiniciar. En E4: Persistencia agregarás almacenamiento en archivos JSON.