This commit is contained in:
Pedro Jose Romero Gombau 2025-05-12 22:27:34 +02:00
parent 32b15cb99c
commit b3f0d2a966
51 changed files with 527 additions and 0 deletions

0
apps/__init__.py Normal file
View File

0
apps/core/__init__.py Normal file
View File

3
apps/core/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
apps/core/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class CoreConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.core'

View File

9
apps/core/models.py Normal file
View File

@ -0,0 +1,9 @@
# apps/core/models.py
from django.db import models
class TimeStampedModel(models.Model):
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
class Meta:
abstract = True

3
apps/core/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

3
apps/core/views.py Normal file
View File

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

View File

@ -0,0 +1,9 @@
# apps/participants/admin.py
from django.contrib import admin
from .models import Participant # ✅ Participant es el único modelo en esta app
@admin.register(Participant)
class ParticipantAdmin(admin.ModelAdmin):
list_display = ("group_name", "score", "s_vol_over_eta")
readonly_fields = ("score",)

View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class ParticipantsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.participants'

View File

@ -0,0 +1,10 @@
from django import forms
from .models import Participant
class ParticipantForm(forms.ModelForm):
class Meta:
model = Participant
fields = ["group_name","R_diff","L_diff","Ph_diff","Pcu_diff","s_vol_over_eta"]
widgets = {f: forms.NumberInput(attrs={"step": "0.1", "class": "input"})
for f in fields if f!="group_name"}
widgets["group_name"]= forms.TextInput(attrs={"class":"input"})

View File

@ -0,0 +1,32 @@
# Generated by Django 4.2.4 on 2025-05-12 16:10
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Participant',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('group_name', models.CharField(max_length=80, unique=True)),
('R_diff', models.FloatField()),
('L_diff', models.FloatField()),
('Ph_diff', models.FloatField()),
('Pcu_diff', models.FloatField()),
('s_vol_over_eta', models.FloatField(help_text='S*Vol/η')),
('score', models.FloatField(default=0)),
],
options={
'ordering': ['-score'],
},
),
]

View File

View File

@ -0,0 +1,21 @@
# apps/participants/models.py
from django.db import models
from apps.core.models import TimeStampedModel
class Participant(TimeStampedModel):
group_name = models.CharField(max_length=80, unique=True)
# diffs en porcentaje
R_diff = models.FloatField()
L_diff = models.FloatField()
Ph_diff = models.FloatField()
Pcu_diff= models.FloatField()
s_vol_over_eta = models.FloatField(help_text="S*Vol/η") # métrica objetiva
score = models.FloatField(default=0)
class Meta:
ordering = ["-score"]
def __str__(self):
return self.group_name

View File

@ -0,0 +1,39 @@
# apps/participants/services.py
from django.db import transaction
from .models import Participant
from apps.settingsapp.models import GradingSettings
def _factor(diff: float) -> float:
if diff <= 10:
return 1.0
elif diff <= 20:
return 0.8
return 0.6
def calculate_scores():
"""Recalcula y persiste 'score' para todos los participantes"""
settings = GradingSettings.get()
participants = list(Participant.objects.all())
# 1) Base Entregable + Presentación
for p in participants:
p.score = settings.report_weight + settings.presentation_weight
# 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
# 3) Objetivo (rank por métrica)
sorted_by_obj = sorted(participants, key=lambda x: x.s_vol_over_eta)
for idx, p in enumerate(sorted_by_obj, start=1):
p.score += max(settings.objective_weight - (idx - 1), 0)
# 4) Persistimos en batch
with transaction.atomic():
for p in participants:
p.save(update_fields=["score"])

View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@ -0,0 +1,8 @@
from django.urls import path
from .views import ParticipantCreateView, CalculateView, ResultsView
urlpatterns = [
path("", ParticipantCreateView.as_view(), name="participant_new"),
path("calcular/", CalculateView.as_view(), name="calculate"),
path("resultados/", ResultsView.as_view(), name="results"),
]

View File

@ -0,0 +1,23 @@
# apps/participants/views.py
from django.views.generic import ListView, CreateView, View, TemplateView
from django.urls import reverse_lazy
from django.shortcuts import redirect
from .models import Participant
from .forms import ParticipantForm
from .services import calculate_scores
class ParticipantCreateView(CreateView):
model = Participant
form_class = ParticipantForm
template_name = "participants/form.html"
success_url = reverse_lazy("participant_new") # vuelve a sí misma
class CalculateView(View):
def post(self, request, *args, **kwargs):
calculate_scores()
return redirect("results")
class ResultsView(ListView):
model = Participant
template_name = "participants/results.html"
context_object_name = "participants"

0
apps/scoring/__init__.py Normal file
View File

3
apps/scoring/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
apps/scoring/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class ScoringConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.scoring'

View File

3
apps/scoring/models.py Normal file
View File

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

3
apps/scoring/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

3
apps/scoring/views.py Normal file
View File

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

View File

@ -0,0 +1,9 @@
# apps/settingsapp/admin.py
from django.contrib import admin
from .models import GradingSettings
@admin.register(GradingSettings)
class SettingsAdmin(admin.ModelAdmin):
def has_add_permission(self, *_):
return False # solo debe existir una instancia

6
apps/settingsapp/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class SettingsappConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.settingsapp'

View File

@ -0,0 +1,9 @@
from django import forms
from .models import GradingSettings
class GradingSettingsForm(forms.ModelForm):
class Meta:
model = GradingSettings
fields = "__all__"
widgets = {f: forms.NumberInput(attrs={"step": "0.1", "class": "input"})
for f in fields}

View File

@ -0,0 +1,27 @@
# Generated by Django 4.2.4 on 2025-05-12 16:10
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='GradingSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('report_weight', models.FloatField(default=2)),
('presentation_weight', models.FloatField(default=1)),
('objective_weight', models.FloatField(default=3)),
('R_weight', models.FloatField(default=1)),
('L_weight', models.FloatField(default=1)),
('Ph_weight', models.FloatField(default=1)),
('Pcu_weight', models.FloatField(default=1)),
],
),
]

View File

View File

@ -0,0 +1,29 @@
# apps/settingsapp/models.py
from django.db import models
from django.core.exceptions import ValidationError
class GradingSettings(models.Model):
# NOTAS
report_weight = models.FloatField(default=2) # Entregable
presentation_weight = models.FloatField(default=1) # Presentación
objective_weight = models.FloatField(default=3) # Objetivo max
# SIMILITUD
R_weight = models.FloatField(default=1)
L_weight = models.FloatField(default=1)
Ph_weight = models.FloatField(default=1)
Pcu_weight= models.FloatField(default=1)
def clean(self):
if GradingSettings.objects.exclude(pk=self.pk).exists():
raise ValidationError("Solo puede existir un GradingSettings")
def save(self, *args, **kwargs):
self.full_clean()
super().save(*args, **kwargs)
@classmethod
def get(cls):
# devuelve la única instancia (la crea si no existe)
obj, _ = cls.objects.get_or_create(pk=1)
return obj

View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

4
apps/settingsapp/urls.py Normal file
View File

@ -0,0 +1,4 @@
from django.urls import path
from .views import SettingsUpdateView
urlpatterns = [ path("", SettingsUpdateView.as_view(), name="settings") ]

16
apps/settingsapp/views.py Normal file
View File

@ -0,0 +1,16 @@
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.views.generic import UpdateView
from .models import GradingSettings
from .forms import GradingSettingsForm
class SettingsUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
model = GradingSettings
form_class = GradingSettingsForm
template_name = "settingsapp/form.html"
success_url = "/"
def get_object(self, queryset=None):
return GradingSettings.get()
def test_func(self):
return self.request.user.is_staff

1
dev/Trafoking.drawio Normal file
View File

@ -0,0 +1 @@
<mxfile host="Electron" modified="2025-05-12T15:48:11.928Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/18.0.6 Chrome/100.0.4896.143 Electron/18.2.3 Safari/537.36" etag="mYNjJAf74T8tIM3Cm0co" version="18.0.6" type="device"><diagram id="kRK4T_47FRJ5cU9VMpoS" name="Página-1">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</diagram></mxfile>

BIN
dev/TrafokingBack.pdf Normal file

Binary file not shown.

BIN
dev/TrafokingFront.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 678 KiB

22
manage.py Normal file
View File

@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'trafoking.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

7
requirements.txt Normal file
View File

@ -0,0 +1,7 @@
# requirements.txt
Django==4.2.4
psycopg2-binary==2.9.*
python-dotenv==1.0.*
django-htmx==1.15.*
pytest==8.1.*
pytest-django==4.7.*

View File

@ -0,0 +1,9 @@
<!-- templates/auth/login.html -->
{% extends "base.html" %}
{% block content %}
<form method="post" class="space-y-2">
{% csrf_token %}
{{ form.as_p }}
<button class="px-4 py-2 bg-amber-500 rounded">Entrar</button>
</form>
{% endblock %}

14
templates/base.html Normal file
View File

@ -0,0 +1,14 @@
<!-- templates/base.html -->
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8"/>
<title>Trafoking</title>
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tailwindcss@3.4.1/dist/tailwind.min.css">
</head>
<body class="min-h-screen bg-slate-900 text-slate-100 flex flex-col items-center py-8">
<h1 class="text-4xl font-bold mb-4">Trafoking ⚡👑</h1>
{% block content %}{% endblock %}
</body>
</html>

View File

@ -0,0 +1,22 @@
<!-- templates/participants/form.html -->
{% extends "base.html" %}
{% block content %}
<!--Form: añadir grupo-->
<form method="post" action="" class="space-y-2">
{% csrf_token %}
{{ form.as_p }}
<button class="px-4 py-2 bg-amber-500 rounded">
Añadir
</button>
</form>
<!--Botón único: calcula + redirige al ranking-->
<form method="post" action="{% url 'calculate' %}" class="mt-4">
{% csrf_token %}
<button class="px-4 py-2 bg-lime-600 rounded">
Calcular&nbsp;y&nbsp;mostrar&nbsp;ranking
</button>
</form>
{% endblock %}

View File

@ -0,0 +1,18 @@
<!-- templates/participants/results.html -->
{% extends "base.html" %}
{% block content %}
<table class="table-auto border-collapse">
<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>{{ 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>
{% endblock %}

View File

@ -0,0 +1,10 @@
<!-- templates/settingsapp/form.html -->
{% extends "base.html" %}
{% block content %}
<h2 class="text-2xl mb-4">Ajustes de calificación</h2>
<form method="post" class="space-y-2">
{% csrf_token %}
{{ form.as_p }}
<button class="px-4 py-2 bg-amber-500 rounded">Guardar</button>
</form>
{% endblock %}

0
trafoking/__init__.py Normal file
View File

16
trafoking/asgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
ASGI config for trafoking project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'trafoking.settings')
application = get_asgi_application()

85
trafoking/settings.py Normal file
View File

@ -0,0 +1,85 @@
# trafoking/settings.py
import os
from pathlib import Path
from dotenv import load_dotenv
load_dotenv()
BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = os.getenv("DJANGO_SECRET_KEY")
DEBUG = False
ALLOWED_HOSTS = ["*"]
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"django_htmx",
"apps.core",
"apps.participants",
"apps.settingsapp",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django_htmx.middleware.HtmxMiddleware",
]
ROOT_URLCONF = "trafoking.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
"OPTIONS": {"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
]},
}
]
WSGI_APPLICATION = "trafoking.wsgi.application"
DB_ENGINE = os.getenv("DB_ENGINE", "sqlite") # sqlite | postgres
if DB_ENGINE == "postgres":
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": os.getenv("DB_NAME", "trafoking"),
"USER": os.getenv("DB_USER", "postgres"),
"PASSWORD": os.getenv("DB_PASSWORD", "postgres"),
"HOST": os.getenv("DB_HOST", "db"),
"PORT": os.getenv("DB_PORT", "5432"),
}
}
else: # SQLite por defecto
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}
AUTH_PASSWORD_VALIDATORS = [
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
]
LANGUAGE_CODE = "es-es"
TIME_ZONE = "Europe/Madrid"
USE_I18N = USE_L10N = USE_TZ = True
STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
LOGIN_URL = "/login/"
LOGIN_REDIRECT_URL = "/"

11
trafoking/urls.py Normal file
View File

@ -0,0 +1,11 @@
from django.contrib import admin
from django.urls import path, include
from django.contrib.auth.views import LoginView, LogoutView
urlpatterns = [
path("admin/", admin.site.urls),
path("login/", LoginView.as_view(template_name="auth/login.html"), name="login"),
path("logout/", LogoutView.as_view(), name="logout"),
path("", include("apps.participants.urls")),
path("ajustes/", include("apps.settingsapp.urls")),
]

16
trafoking/wsgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
WSGI config for trafoking project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'trafoking.settings')
application = get_wsgi_application()