From 19080419a1a3946e3202d87cdf92b8a8c14f28f5 Mon Sep 17 00:00:00 2001 From: Pedro Romero Date: Tue, 13 May 2025 01:32:51 +0200 Subject: [PATCH] Functional Update + Docker deployment --- .env.example | 5 +- Dockerfile | 22 +++++ .../__pycache__/services.cpython-311.pyc | Bin 2458 -> 4875 bytes .../__pycache__/views.cpython-311.pyc | Bin 1824 -> 2521 bytes apps/participants/services.py | 78 ++++++++++++---- apps/participants/views.py | 11 +++ db.sqlite3 | Bin 143360 -> 143360 bytes docker-compose.yml | 29 ++++++ entrypoint.sh | 11 +++ static/css/styles.css | 10 +++ static/js/animations.js | 13 +++ templates/participants/results.html | 83 +++++++++++++----- 12 files changed, 223 insertions(+), 39 deletions(-) create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 entrypoint.sh create mode 100644 static/js/animations.js diff --git a/.env.example b/.env.example index 1035f1b..f47f82d 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,7 @@ -DJANGO_SECRET_KEY= +DJANGO_SECRET_KEY=tu_super_secreta_key +DB_ENGINE=postgres DB_NAME=trafoking DB_USER=postgres DB_PASSWORD=postgres DB_HOST=db -DB_PORT=5432 +DB_PORT=5432 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f9632dd --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +# Dockerfile + +# 1. Imagen base +FROM python:3.11-slim + +# 2. Variables de entorno para hacer Python más silencioso +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +# 3. Workdir en el contenedor +WORKDIR /app + +# 4. Copiamos requirements e instalamos +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# 5. Copiamos el proyecto +COPY . . + +# 6. Recolectar estáticos y preparar migraciones al arrancar +ENTRYPOINT ["/app/entrypoint.sh"] +CMD ["gunicorn", "trafoking.wsgi:application", "--bind", "0.0.0.0:8000"] diff --git a/apps/participants/__pycache__/services.cpython-311.pyc b/apps/participants/__pycache__/services.cpython-311.pyc index a9969e78bb713f54aa80f17d8eb2b0daa470523a..c0eb0bb48341b86a0becaf7742a2b31004164ef7 100644 GIT binary patch literal 4875 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 A float: return 0.6 def calculate_scores(): - """Recalcula y persiste 'score' para todos los participantes""" + """Recalcula y persiste sólo el campo 'score' (base + similitud + bonus objetivo).""" settings = GradingSettings.get() participants = list(Participant.objects.all()) - # 1) Base Entregable + Presentación + # 1) Base + similitud for p in participants: - p.score = settings.report_weight + settings.presentation_weight + base = settings.report_weight + settings.presentation_weight + sim_total = sum( + _factor(getattr(p, f"{fld}_diff")) * getattr(settings, f"{fld}_weight") + for fld in ("R", "L", "Ph", "Pcu") + ) + p.score = base + sim_total - # 2) Similitud - for diff, weight in [ - (p.R_diff, settings.R_weight), - (p.L_diff, settings.L_weight), - (p.Ph_diff, settings.Ph_weight), - (p.Pcu_diff,settings.Pcu_weight), - ]: - p.score += _factor(diff) * weight + # 2) Calculamos bonus de objetivo según ranking de s_vol_over_eta (mayor es mejor) + sorted_obj = sorted(participants, key=lambda x: x.s_vol_over_eta, reverse=True) + obj_bonus_map = {} + for idx, p in enumerate(sorted_obj, start=1): + bonus = max(settings.objective_weight - (idx - 1), 0) + obj_bonus_map[p.pk] = bonus - # 3) Objetivo (rank por métrica) - sorted_by_obj = sorted(participants, key=lambda x: x.s_vol_over_eta, reverse=True) - for idx, p in enumerate(sorted_by_obj, start=1): - p.score += max(settings.objective_weight - (idx - 1), 0) - - # 4) Persistimos en batch + # 3) Persistimos sumando el bonus a score with transaction.atomic(): for p in participants: + p.score += obj_bonus_map[p.pk] p.save(update_fields=["score"]) + + +def calculate_scores_details(): + """ + Devuelve una lista de dicts con desgloses: + [{ + participant: , + base: float, + sim_total: float, + obj_bonus: float, + total: float, + objective_metric: float + }, ...] + ordenada exclusivamente por 'objective_metric' (s_vol_over_eta) descendente. + """ + settings = GradingSettings.get() + participants = list(Participant.objects.all()) + + # 1) Calculamos base y similitud + details = [] + for p in participants: + base = settings.report_weight + settings.presentation_weight + sim_total = sum( + _factor(getattr(p, f"{fld}_diff")) * getattr(settings, f"{fld}_weight") + for fld in ("R", "L", "Ph", "Pcu") + ) + details.append({ + "participant": p, + "base": base, + "sim_total": sim_total, + "obj_bonus": 0, # se asignará más abajo + "total": base + sim_total, # se incrementará con obj_bonus + "objective_metric": p.s_vol_over_eta, + }) + + # 2) Asignamos bonus de objetivo según posición en objective_metric + sorted_by_obj = sorted(details, key=lambda d: d["objective_metric"], reverse=True) + for idx, entry in enumerate(sorted_by_obj, start=1): + bonus = max(settings.objective_weight - (idx - 1), 0) + entry["obj_bonus"] = bonus + entry["total"] += bonus + + # 3) Orden final sólo por objective_metric descendente + return sorted(details, key=lambda d: d["objective_metric"], reverse=True) diff --git a/apps/participants/views.py b/apps/participants/views.py index 46882f7..2a659fb 100644 --- a/apps/participants/views.py +++ b/apps/participants/views.py @@ -5,6 +5,8 @@ from django.shortcuts import redirect from .models import Participant from .forms import ParticipantForm from .services import calculate_scores +from django.views.generic import TemplateView +from .services import calculate_scores_details class ParticipantCreateView(CreateView): model = Participant @@ -21,3 +23,12 @@ class ResultsView(ListView): model = Participant template_name = "participants/results.html" context_object_name = "participants" + +class ResultsView(TemplateView): + template_name = "participants/results.html" + + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + # recalcula en memoria y entrega detalles + ctx["rankings"] = calculate_scores_details() + return ctx diff --git a/db.sqlite3 b/db.sqlite3 index 04242e931d61f75eb82820b3ac15f7c8144120bf..1445ad14c4f4be482f12be344032fd6b999b316f 100644 GIT binary patch delta 657 zcmZp8z|ru4V}dke)kGO*#;T18+vORzZ&oyT$2XZxKZKEE`ovsD>B(jK8gd+or6n2h zrNya53=9nX5MDuQQLceA1cash<_D- zG++5M0wPmOJwpRi6C-2XG69Ln`7A)d z${x%(dF_13dXP>7J##ZlV*_&xZEzX)qSAtV1w%<7NO6#y2?Dc#4s>yJlmB(jK8nT>;r6n2h zrNya53=9l>9AI8SYEiBX53?=f^!*8p5{%57ujm^XurcyiGVoV!7A$b!pX`+%;#83t zV4Pm!Zsz0YS{W4L7@i+sl4Rki;^PtN6O`j;UT9=m>7ME77U^N^1;B{@%)tK{D0qv1`V)O7831M9lluSw diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..bb1b6f0 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,29 @@ +services: + db: + image: postgres:15 + restart: unless-stopped + environment: + POSTGRES_DB: ${DB_NAME:-trafoking} + POSTGRES_USER: ${DB_USER:-postgres} + POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres} + volumes: + - ./pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER}"] + interval: 10s + timeout: 5s + retries: 5 + + trafokingweb: + build: . + command: ["gunicorn", "trafoking.wsgi:application", "--bind", "0.0.0.0:8000"] + volumes: + - ./:/app + ports: + - "8000:8000" + depends_on: + db: + condition: service_healthy + +volumes: + pgdata: diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..d257666 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,11 @@ +#!/bin/bash +set -e + +# Migraciones +python manage.py migrate --noinput + +# Recolectar estáticos +python manage.py collectstatic --noinput + +# Ejecutar el comando que se pase al contenedor +exec "$@" diff --git a/static/css/styles.css b/static/css/styles.css index ac41ed1..6a7dce5 100644 --- a/static/css/styles.css +++ b/static/css/styles.css @@ -101,3 +101,13 @@ label sup { position: relative; top: 0.25em; } + +/*–– Medallas ––*/ +.text-gold { color: #FFD700 !important; } +.text-silver { color: #C0C0C0 !important; } +.text-bronze { color: #CD7F32 !important; } + +/*–– Tabla animada ––*/ +#ranking-table tbody tr { + transition: transform .4s ease, opacity .4s ease; +} diff --git a/static/js/animations.js b/static/js/animations.js new file mode 100644 index 0000000..19a9143 --- /dev/null +++ b/static/js/animations.js @@ -0,0 +1,13 @@ +// static/js/animations.js +document.addEventListener("DOMContentLoaded", () => { + const rows = Array.from(document.querySelectorAll("#ranking-table tbody tr")); + // Ordenamos por rank y delay incremental + rows.sort((a, b) => + parseInt(a.dataset.rank) - parseInt(b.dataset.rank) + ).forEach((row, i) => { + setTimeout(() => { + row.style.opacity = 1; + row.style.transform = "scale(1)"; + }, 150 * i); + }); +}); diff --git a/templates/participants/results.html b/templates/participants/results.html index 50d1cca..fbd996e 100644 --- a/templates/participants/results.html +++ b/templates/participants/results.html @@ -1,27 +1,70 @@ - {% extends "base.html" %} +{% load static%} {% block title %}Resultados · Trafoking{% endblock %} + {% block content %} +

+ Ranking +

-

Ranking

+ {% if rankings %} + + + + + + + + + + {% for entry in rankings %} + {% with forloop.counter as rank %} + + + + + + + + + {% endwith %} + {% endfor %} + +
#GrupoTotal
+ + {{ rank }} + + + {{ entry.participant.group_name }} + + + {{ entry.total|floatformat:2 }} + +
+ Base: {{ entry.base|floatformat:2 }} +  | Similitud: {{ entry.sim_total|floatformat:2 }} +  | Objetivo: {{ entry.obj_bonus|floatformat:2 }} +
+ {% else %} +

+ No hay datos para mostrar el ranking. +

+ {% endif %} - - - - {% for p in participants %} - - - - - - {% empty %} - - {% endfor %} - -
#GrupoPuntuación
{{forloop.counter}}{{p.group_name}}{{p.score|floatformat:2}}
Sin participantes todavía.
- - - Añadir más grupos - + + Añadir más grupos + + {% endblock %}