# una pasada y una linea en memoria: leer el
# archivo entero para contarlo cuesta de mas
with open(ruta, encoding="utf-8") as f:
lineas = sum(1 for _ in f)
@app.get("/alumnos/{dni}")
async def alumno(dni: str):
fila = await db.buscar(dni)
if not fila:
raise HTTPException(404, "no existe")
return fila
-- que no haya dos matriculas del mismo alumno
-- en el mismo ciclo lo garantiza la BASE,
-- no el codigo que escribe encima
CREATE TABLE matriculas (
dni TEXT NOT NULL,
ciclo TEXT NOT NULL,
UNIQUE (dni, ciclo)
);
Widget build(BuildContext context) {
return ListView.builder(
itemCount: alumnos.length,
itemBuilder: (_, i) => Ficha(alumnos[i]),
);
}
// la camara se suelta al salir de pantalla:
// dejarla abierta funde la bateria
override fun onPause() {
super.onPause()
lector.close()
}
// una escucha para toda la lista: mil botones
// con mil escuchas es por donde se va la RAM
lista.addEventListener("click", (e) => {
const boton = e.target.closest("button");
if (boton) activar(boton.dataset.clave);
});
# el 6 es el nivel con el que se midio el
# presupuesto de peso de este sitio: comprimir
# a 9 en disco y servir a 1 es medirse mal
gzip on;
gzip_comp_level 6;
# sin "set -e" un guion sigue publicando
# despues de que un paso haya fallado
set -euo pipefail
rsync -a --delete construido/ public/
SELECT ciclo, COUNT(*) AS alumnos
FROM matriculas
WHERE estado = 'ACTIVO'
GROUP BY ciclo
ORDER BY ciclo;
/* min() evita que una columna de 22rem
desborde a lo ancho en un movil de 320px */
.rejilla {
display: grid;
grid-template-columns: repeat(
auto-fit, minmax(min(100%, 22rem), 1fr));
}