diff --git a/apps/__init__.py b/apps/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/apps/core/__init__.py b/apps/core/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/apps/core/admin.py b/apps/core/admin.py
new file mode 100644
index 0000000..8c38f3f
--- /dev/null
+++ b/apps/core/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/apps/core/apps.py b/apps/core/apps.py
new file mode 100644
index 0000000..4143768
--- /dev/null
+++ b/apps/core/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class CoreConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'apps.core'
diff --git a/apps/core/migrations/__init__.py b/apps/core/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/apps/core/models.py b/apps/core/models.py
new file mode 100644
index 0000000..158280a
--- /dev/null
+++ b/apps/core/models.py
@@ -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
diff --git a/apps/core/tests.py b/apps/core/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/apps/core/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/apps/core/views.py b/apps/core/views.py
new file mode 100644
index 0000000..91ea44a
--- /dev/null
+++ b/apps/core/views.py
@@ -0,0 +1,3 @@
+from django.shortcuts import render
+
+# Create your views here.
diff --git a/apps/participants/__init__.py b/apps/participants/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/apps/participants/admin.py b/apps/participants/admin.py
new file mode 100644
index 0000000..f701c10
--- /dev/null
+++ b/apps/participants/admin.py
@@ -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",)
diff --git a/apps/participants/apps.py b/apps/participants/apps.py
new file mode 100644
index 0000000..dd961f2
--- /dev/null
+++ b/apps/participants/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class ParticipantsConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'apps.participants'
diff --git a/apps/participants/forms.py b/apps/participants/forms.py
new file mode 100644
index 0000000..b748798
--- /dev/null
+++ b/apps/participants/forms.py
@@ -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"})
diff --git a/apps/participants/migrations/0001_initial.py b/apps/participants/migrations/0001_initial.py
new file mode 100644
index 0000000..0ad3bce
--- /dev/null
+++ b/apps/participants/migrations/0001_initial.py
@@ -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'],
+ },
+ ),
+ ]
diff --git a/apps/participants/migrations/__init__.py b/apps/participants/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/apps/participants/models.py b/apps/participants/models.py
new file mode 100644
index 0000000..11a3b34
--- /dev/null
+++ b/apps/participants/models.py
@@ -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
diff --git a/apps/participants/services.py b/apps/participants/services.py
new file mode 100644
index 0000000..0a36c65
--- /dev/null
+++ b/apps/participants/services.py
@@ -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"])
diff --git a/apps/participants/tests.py b/apps/participants/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/apps/participants/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/apps/participants/urls.py b/apps/participants/urls.py
new file mode 100644
index 0000000..6ff159a
--- /dev/null
+++ b/apps/participants/urls.py
@@ -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"),
+]
diff --git a/apps/participants/views.py b/apps/participants/views.py
new file mode 100644
index 0000000..46882f7
--- /dev/null
+++ b/apps/participants/views.py
@@ -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"
diff --git a/apps/scoring/__init__.py b/apps/scoring/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/apps/scoring/admin.py b/apps/scoring/admin.py
new file mode 100644
index 0000000..8c38f3f
--- /dev/null
+++ b/apps/scoring/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/apps/scoring/apps.py b/apps/scoring/apps.py
new file mode 100644
index 0000000..e902cff
--- /dev/null
+++ b/apps/scoring/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class ScoringConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'apps.scoring'
diff --git a/apps/scoring/migrations/__init__.py b/apps/scoring/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/apps/scoring/models.py b/apps/scoring/models.py
new file mode 100644
index 0000000..71a8362
--- /dev/null
+++ b/apps/scoring/models.py
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/apps/scoring/tests.py b/apps/scoring/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/apps/scoring/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/apps/scoring/views.py b/apps/scoring/views.py
new file mode 100644
index 0000000..91ea44a
--- /dev/null
+++ b/apps/scoring/views.py
@@ -0,0 +1,3 @@
+from django.shortcuts import render
+
+# Create your views here.
diff --git a/apps/settingsapp/__init__.py b/apps/settingsapp/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/apps/settingsapp/admin.py b/apps/settingsapp/admin.py
new file mode 100644
index 0000000..7651f87
--- /dev/null
+++ b/apps/settingsapp/admin.py
@@ -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
diff --git a/apps/settingsapp/apps.py b/apps/settingsapp/apps.py
new file mode 100644
index 0000000..3fbf8ed
--- /dev/null
+++ b/apps/settingsapp/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class SettingsappConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'apps.settingsapp'
diff --git a/apps/settingsapp/forms.py b/apps/settingsapp/forms.py
new file mode 100644
index 0000000..b9909e7
--- /dev/null
+++ b/apps/settingsapp/forms.py
@@ -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}
diff --git a/apps/settingsapp/migrations/0001_initial.py b/apps/settingsapp/migrations/0001_initial.py
new file mode 100644
index 0000000..72dd9ef
--- /dev/null
+++ b/apps/settingsapp/migrations/0001_initial.py
@@ -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)),
+ ],
+ ),
+ ]
diff --git a/apps/settingsapp/migrations/__init__.py b/apps/settingsapp/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/apps/settingsapp/models.py b/apps/settingsapp/models.py
new file mode 100644
index 0000000..b44540d
--- /dev/null
+++ b/apps/settingsapp/models.py
@@ -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
diff --git a/apps/settingsapp/tests.py b/apps/settingsapp/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/apps/settingsapp/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/apps/settingsapp/urls.py b/apps/settingsapp/urls.py
new file mode 100644
index 0000000..7e26ba8
--- /dev/null
+++ b/apps/settingsapp/urls.py
@@ -0,0 +1,4 @@
+from django.urls import path
+from .views import SettingsUpdateView
+
+urlpatterns = [ path("", SettingsUpdateView.as_view(), name="settings") ]
diff --git a/apps/settingsapp/views.py b/apps/settingsapp/views.py
new file mode 100644
index 0000000..3b5158b
--- /dev/null
+++ b/apps/settingsapp/views.py
@@ -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
diff --git a/dev/Trafoking.drawio b/dev/Trafoking.drawio
new file mode 100644
index 0000000..5cc7798
--- /dev/null
+++ b/dev/Trafoking.drawio
@@ -0,0 +1 @@
+
# | Grupo | Puntuación |
---|---|---|
{{ forloop.counter }} | +{{ p.group_name }} | +{{ p.score|floatformat:2 }} | +
Sin participantes todavía. |