diff --git a/apps/participation/forms.py b/apps/participation/forms.py index 86c58a2..ad98eed 100644 --- a/apps/participation/forms.py +++ b/apps/participation/forms.py @@ -9,7 +9,7 @@ from django.core.exceptions import ValidationError from django.utils import formats from django.utils.translation import gettext_lazy as _ -from .models import Participation, Team, Tournament, Solution, Pool +from .models import Participation, Passage, Pool, Team, Tournament, Solution class TeamForm(forms.ModelForm): @@ -145,3 +145,20 @@ class PoolTeamsForm(forms.ModelForm): widgets = { "participations": forms.CheckboxSelectMultiple, } + + +class PassageForm(forms.ModelForm): + def clean(self): + cleaned_data = super().clean() + if "defender" in cleaned_data and "opponent" in cleaned_data and "reporter" in cleaned_data \ + and len({cleaned_data["defender"], cleaned_data["opponent"], cleaned_data["reporter"]}) < 3: + self.add_error(None, _("The defender, the opponent and the reporter must be different.")) + if "defender" in self.cleaned_data and "solution_number" in self.cleaned_data \ + and not Solution.objects.filter(participation=cleaned_data["defender"], + problem=cleaned_data["solution_number"]).exists(): + self.add_error("solution_number", _("This defender did not work on this problem.")) + return cleaned_data + + class Meta: + model = Passage + fields = ('solution_number', 'place', 'defender', 'opponent', 'reporter',) diff --git a/apps/participation/models.py b/apps/participation/models.py index 6de2376..d85adf0 100644 --- a/apps/participation/models.py +++ b/apps/participation/models.py @@ -340,6 +340,13 @@ class Passage(models.Model): default="Non indiqué", ) + solution_number = models.PositiveSmallIntegerField( + verbose_name=_("defended solution"), + choices=[ + (i, format_lazy(_("Problem #{problem}"), problem=i)) for i in range(1, settings.PROBLEM_COUNT + 1) + ], + ) + defender = models.ForeignKey( Participation, on_delete=models.PROTECT, @@ -361,6 +368,16 @@ class Passage(models.Model): related_name="+", ) + @property + def defended_solution(self) -> "Solution": + return Solution.objects.get( + participation=self.defender, + problem=self.solution_number, + final_solution=self.pool.tournament.final) + + def get_absolute_url(self): + return reverse_lazy("participation:passage_detail", args=(self.pk,)) + def clean(self): if self.defender not in self.pool.participations.all(): raise ValidationError(_("Team {trigram} is not registered in the pool.") @@ -373,6 +390,10 @@ class Passage(models.Model): .format(trigram=self.reporter.team.trigram)) return super().clean() + def __str__(self): + return _("Passage of {defender} for problem {problem}")\ + .format(defender=self.defender.team, problem=self.solution_number) + class Meta: verbose_name = _("passage") verbose_name_plural = _("passages") diff --git a/apps/participation/templates/participation/passage_detail.html b/apps/participation/templates/participation/passage_detail.html new file mode 100644 index 0000000..38a5203 --- /dev/null +++ b/apps/participation/templates/participation/passage_detail.html @@ -0,0 +1,55 @@ +{% extends "base.html" %} + +{% load i18n %} + +{% block content %} +{% trans "any" as any %} +
+
+

{{ passage }}

+
+
+
+
{% trans "Pool:" %}
+
{{ passage.pool }}
+ +
{% trans "Defender:" %}
+
{{ passage.defender.team }}
+ +
{% trans "Opponent:" %}
+
{{ passage.opponent.team }}
+ +
{% trans "Reporter:" %}
+
{{ passage.reporter.team }}
+ +
{% trans "Defended solution:" %}
+
{{ passage.defended_solution }}
+ +
{% trans "Place:" %}
+
{{ passage.place }}
+
+
+ {% if user.registration.is_admin %} + + {% endif %} +
+ + {% trans "Update passage" as modal_title %} + {% trans "Update" as modal_button %} + {% url "participation:passage_update" pk=passage.pk as modal_action %} + {% include "base_modal.html" with modal_id="updatePassage" %} +{% endblock %} + +{% block extrajavascript %} + +{% endblock %} diff --git a/apps/participation/templates/participation/passage_form.html b/apps/participation/templates/participation/passage_form.html new file mode 100644 index 0000000..daabd59 --- /dev/null +++ b/apps/participation/templates/participation/passage_form.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% load crispy_forms_filters i18n %} + +{% block content %} +
+
+ {% csrf_token %} + {{ form|crispy }} +
+ +
+{% endblock content %} diff --git a/apps/participation/templates/participation/pool_detail.html b/apps/participation/templates/participation/pool_detail.html index c13a2d1..0dbf035 100644 --- a/apps/participation/templates/participation/pool_detail.html +++ b/apps/participation/templates/participation/pool_detail.html @@ -9,31 +9,50 @@
-
{% trans "Tournament:" %}
-
{{ pool.tournament }}
+
{% trans "Tournament:" %}
+
{{ pool.tournament }}
-
{% trans "Round:" %}
-
{{ pool.get_round_display }}
+
{% trans "Round:" %}
+
{{ pool.get_round_display }}
-
{% trans "Teams:" %}
-
+
{% trans "Teams:" %}
+
{% for participation in pool.participations.all %} {{ participation.team }}{% if not forloop.last %}, {% endif %} {% endfor %}
-
{% trans "Juries:" %}
-
{{ pool.juries.all|join:", " }}
+
{% trans "Juries:" %}
+
{{ pool.juries.all|join:", " }}
+ +
{% trans "Passages:" %}
+
+ {% for passage in pool.passages.all %} + {{ passage }}{% if not forloop.last %}, {% endif %} + {% endfor %} +
+
{% trans "Defended solutions:" %}
+
+ {% for passage in pool.passages.all %} + {{ passage.defended_solution }}{% if not forloop.last %}, {% endif %} + {% endfor %} +
{% if user.registration.is_admin %} {% endif %} + {% trans "Add passage" as modal_title %} + {% trans "Add" as modal_button %} + {% url "participation:passage_create" pk=pool.pk as modal_action %} + {% include "base_modal.html" with modal_id="addPassage" modal_button_type="success" %} + {% trans "Update pool" as modal_title %} {% trans "Update" as modal_button %} {% url "participation:pool_update" pk=pool.pk as modal_action %} @@ -59,6 +78,12 @@ if (!modalBody.html().trim()) modalBody.load("{% url "participation:pool_update_teams" pk=pool.pk %} #form-content") }); + + $('button[data-target="#addPassageModal"]').click(function() { + let modalBody = $("#addPassageModal div.modal-body"); + if (!modalBody.html().trim()) + modalBody.load("{% url "participation:passage_create" pk=pool.pk %} #form-content") + }); }); {% endblock %} diff --git a/apps/participation/urls.py b/apps/participation/urls.py index c3df0e7..026c2e8 100644 --- a/apps/participation/urls.py +++ b/apps/participation/urls.py @@ -5,7 +5,8 @@ from django.urls import path from django.views.generic import TemplateView from .views import CreateTeamView, JoinTeamView, \ - MyParticipationDetailView, MyTeamDetailView, ParticipationDetailView, PoolCreateView, PoolDetailView, \ + MyParticipationDetailView, MyTeamDetailView, ParticipationDetailView, \ + PassageCreateView, PassageDetailView, PassageUpdateView, PoolCreateView, PoolDetailView, \ PoolUpdateView, PoolUpdateTeamsView, TeamAuthorizationsView, TeamDetailView, TeamLeaveView, TeamListView, \ TeamUpdateView, TournamentCreateView, TournamentDetailView, TournamentListView, TournamentUpdateView, \ SolutionUploadView @@ -33,5 +34,8 @@ urlpatterns = [ path("pools//", PoolDetailView.as_view(), name="pool_detail"), path("pools//update/", PoolUpdateView.as_view(), name="pool_update"), path("pools//update-teams/", PoolUpdateTeamsView.as_view(), name="pool_update_teams"), + path("pools/passages/add//", PassageCreateView.as_view(), name="passage_create"), + path("pools/passages//", PassageDetailView.as_view(), name="passage_detail"), + path("pools/passages//update/", PassageUpdateView.as_view(), name="passage_update"), path("chat/", TemplateView.as_view(template_name="participation/chat.html"), name="chat") ] diff --git a/apps/participation/views.py b/apps/participation/views.py index 253ef34..6151f86 100644 --- a/apps/participation/views.py +++ b/apps/participation/views.py @@ -23,9 +23,9 @@ from tfjm.lists import get_sympa_client from tfjm.matrix import Matrix from tfjm.views import AdminMixin -from .forms import JoinTeamForm, ParticipationForm, PoolForm, PoolTeamsForm, RequestValidationForm, SolutionForm, \ - TeamForm, TournamentForm, ValidateParticipationForm -from .models import Participation, Team, Tournament, Solution, Pool +from .forms import JoinTeamForm, ParticipationForm, PassageForm, PoolForm, PoolTeamsForm, RequestValidationForm, \ + SolutionForm, TeamForm, TournamentForm, ValidateParticipationForm +from .models import Participation, Passage, Pool, Team, Tournament, Solution from .tables import TeamTable, TournamentTable, ParticipationTable, PoolTable @@ -487,3 +487,33 @@ class PoolUpdateView(AdminMixin, UpdateView): class PoolUpdateTeamsView(AdminMixin, UpdateView): model = Pool form_class = PoolTeamsForm + + +class PassageCreateView(AdminMixin, CreateView): + model = Passage + form_class = PassageForm + + def dispatch(self, request, *args, **kwargs): + qs = Pool.objects.filter(pk=self.kwargs["pk"]) + if not qs.exists(): + raise Http404 + self.pool = qs.get() + return super().dispatch(request, *args, **kwargs) + + def get_form(self, form_class=None): + form = super().get_form(form_class) + form.instance.pool = self.pool + form.fields["defender"].queryset = self.pool.participations.all() + form.fields["opponent"].queryset = self.pool.participations.all() + form.fields["reporter"].queryset = self.pool.participations.all() + return form + + +class PassageDetailView(AdminMixin, DetailView): + model = Passage + + +class PassageUpdateView(AdminMixin, UpdateView): + model = Passage + form_class = PassageForm +