Functional Update + Docker deployment
This commit is contained in:
parent
69b0fa02a4
commit
19080419a1
@ -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
|
22
Dockerfile
Normal file
22
Dockerfile
Normal file
@ -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"]
|
Binary file not shown.
Binary file not shown.
@ -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: <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)
|
||||
|
@ -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
|
||||
|
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
29
docker-compose.yml
Normal file
29
docker-compose.yml
Normal file
@ -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:
|
11
entrypoint.sh
Normal file
11
entrypoint.sh
Normal file
@ -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 "$@"
|
@ -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;
|
||||
}
|
||||
|
13
static/js/animations.js
Normal file
13
static/js/animations.js
Normal file
@ -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);
|
||||
});
|
||||
});
|
@ -1,27 +1,70 @@
|
||||
<!-- templates/participants/results.html -->
|
||||
{% extends "base.html" %}
|
||||
{% load static%}
|
||||
{% block title %}Resultados · Trafoking{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2 class="text-3xl font-bold mb-8 text-[color:var(--brand-yellow)]">
|
||||
Ranking
|
||||
</h2>
|
||||
|
||||
<h2 class="text-3xl font-bold mb-8 text-[color:var(--brand-yellow)]">Ranking</h2>
|
||||
{% if rankings %}
|
||||
<table id="ranking-table"
|
||||
class="w-full max-w-2xl mx-auto border-separate border-spacing-y-2">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-left px-4">#</th>
|
||||
<th class="text-left px-4">Grupo</th>
|
||||
<th class="text-right px-4">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for entry in rankings %}
|
||||
{% with forloop.counter as rank %}
|
||||
<tr class="transform transition duration-500 opacity-0 scale-95"
|
||||
data-rank="{{ rank }}">
|
||||
<td class="px-4 py-2">
|
||||
<span class="font-bold
|
||||
{% if rank == 1 %}text-2xl text-[#FFD700]
|
||||
{% elif rank == 2 %}text-xl text-[#C0C0C0]
|
||||
{% elif rank == 3 %}text-xl text-[#CD7F32]
|
||||
{% else %}text-base{% endif %}">
|
||||
{{ rank }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-2 font-semibold
|
||||
{% if rank == 1 %}text-2xl text-[#FFD700]
|
||||
{% elif rank == 2 %}text-xl text-[#C0C0C0]
|
||||
{% elif rank == 3 %}text-xl text-[#CD7F32]{% endif %}">
|
||||
{{ entry.participant.group_name }}
|
||||
</td>
|
||||
<td class="px-4 py-2 text-right">
|
||||
<span class="font-mono">
|
||||
{{ entry.total|floatformat:2 }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="bg-white bg-opacity-10 transform transition duration-500 opacity-0 scale-95"
|
||||
data-rank="{{ rank }}">
|
||||
<td colspan="3" class="px-4 py-2 text-sm">
|
||||
Base: {{ entry.base|floatformat:2 }}
|
||||
| Similitud: {{ entry.sim_total|floatformat:2 }}
|
||||
| Objetivo: {{ entry.obj_bonus|floatformat:2 }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="text-center text-red-500 text-lg">
|
||||
No hay datos para mostrar el ranking.
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<table class="hero-appear">
|
||||
<thead><tr><th>#</th><th>Grupo</th><th>Puntuación</th></tr></thead>
|
||||
<tbody>
|
||||
{% for p in participants %}
|
||||
<tr class="text-center">
|
||||
<td>{{forloop.counter}}</td>
|
||||
<td class="font-semibold">{{p.group_name}}</td>
|
||||
<td>{{p.score|floatformat:2}}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="3">Sin participantes todavía.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<a href="{% url 'participant_new' %}" class="btn btn-primary mt-10">
|
||||
Añadir más grupos
|
||||
</a>
|
||||
<a href="{% url 'participant_new' %}"
|
||||
class="btn btn-primary mt-10 inline-block">
|
||||
Añadir más grupos
|
||||
</a>
|
||||
|
||||
<script src="{% static 'js/animations.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
Loading…
Reference in New Issue
Block a user