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 a9969e7..c0eb0bb 100644 Binary files a/apps/participants/__pycache__/services.cpython-311.pyc and b/apps/participants/__pycache__/services.cpython-311.pyc differ diff --git a/apps/participants/__pycache__/views.cpython-311.pyc b/apps/participants/__pycache__/views.cpython-311.pyc index 6cb0f6b..39a82d3 100644 Binary files a/apps/participants/__pycache__/views.cpython-311.pyc and b/apps/participants/__pycache__/views.cpython-311.pyc differ diff --git a/apps/participants/services.py b/apps/participants/services.py index 14210e7..f37687a 100644 --- a/apps/participants/services.py +++ b/apps/participants/services.py @@ -1,4 +1,5 @@ # apps/participants/services.py + from django.db import transaction from .models import Participant from apps.settingsapp.models import GradingSettings @@ -11,29 +12,72 @@ def _factor(diff: float) -> 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 04242e9..1445ad1 100644 Binary files a/db.sqlite3 and b/db.sqlite3 differ 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 %}