diff --git a/apps/api/urls.py b/apps/api/urls.py index 43923b6..6771c3c 100644 --- a/apps/api/urls.py +++ b/apps/api/urls.py @@ -9,7 +9,7 @@ from rest_framework.filters import SearchFilter from rest_framework.viewsets import ModelViewSet from member.models import TFJMUser, Authorization, Solution, Synthesis, MotivationLetter -from tournament.models import Team, Tournament +from tournament.models import Team, Tournament, Pool class UserSerializer(serializers.ModelSerializer): @@ -59,6 +59,12 @@ class SynthesisSerializer(serializers.ModelSerializer): fields = "__all__" +class PoolSerializer(serializers.ModelSerializer): + class Meta: + model = Pool + fields = "__all__" + + class UserViewSet(ModelViewSet): queryset = TFJMUser.objects.all() serializer_class = UserSerializer @@ -113,6 +119,13 @@ class SynthesisViewSet(ModelViewSet): filterset_fields = ['team', 'team__trigram', 'source', 'round', ] +class PoolViewSet(ModelViewSet): + queryset = Pool.objects.all() + serializer_class = PoolSerializer + filter_backends = [DjangoFilterBackend] + filterset_fields = ['teams', 'teams__trigram', 'round', ] + + # Routers provide an easy way of automatically determining the URL conf. # Register each app API router and user viewset router = routers.DefaultRouter() diff --git a/apps/member/views.py b/apps/member/views.py index 98aa7b5..6e088fe 100644 --- a/apps/member/views.py +++ b/apps/member/views.py @@ -1,4 +1,5 @@ import random +from datetime import datetime from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.models import AnonymousUser @@ -151,6 +152,16 @@ class DocumentView(LoginRequiredMixin, View): if isinstance(doc, Solution) or isinstance(doc, Synthesis) or isinstance(doc, MotivationLetter): grant = grant or doc.team == request.user.team or request.user in doc.team.tournament.organizers.all() + if isinstance(doc, Synthesis) and request.user.organizes: + grant = True + + if isinstance(doc, Solution): + for pool in doc.pools.all(): + if pool.round == 2 and datetime.now() < doc.tournament.date_solutions_2: + continue + if self.request.user.team in pool.teams.all(): + grant = True + if not grant: raise PermissionDenied diff --git a/apps/tournament/admin.py b/apps/tournament/admin.py index de69249..cbe128d 100644 --- a/apps/tournament/admin.py +++ b/apps/tournament/admin.py @@ -1,6 +1,6 @@ from django.contrib.auth.admin import admin -from tournament.models import Team, Tournament, Payment +from tournament.models import Team, Tournament, Pool, Payment @admin.register(Team) @@ -13,6 +13,11 @@ class TournamentAdmin(admin.ModelAdmin): pass +@admin.register(Pool) +class PoolAdmin(admin.ModelAdmin): + pass + + @admin.register(Payment) class PaymentAdmin(admin.ModelAdmin): pass diff --git a/apps/tournament/forms.py b/apps/tournament/forms.py index 2e50b64..a5eca06 100644 --- a/apps/tournament/forms.py +++ b/apps/tournament/forms.py @@ -3,14 +3,20 @@ import re from datetime import datetime from django import forms +from django.db.models import Q from django.utils.translation import gettext_lazy as _ from member.models import TFJMUser, Solution, Synthesis from tfjm.inputs import DatePickerInput, DateTimePickerInput, AmountInput -from tournament.models import Tournament, Team +from tournament.models import Tournament, Team, Pool class TournamentForm(forms.ModelForm): + organizers = forms.ModelMultipleChoiceField( + TFJMUser.objects.filter(Q(role="0admin") | Q(role="1volunteer")).order_by('role'), + label=_("Organizers"), + ) + def clean(self): cleaned_data = super().clean() @@ -122,3 +128,74 @@ class SynthesisForm(forms.ModelForm): class Meta: model = Synthesis fields = ('file', 'source', 'round',) + + +class PoolForm(forms.ModelForm): + team1 = forms.ModelChoiceField( + Team.objects.filter(validation_status="2valid").all(), + empty_label=_("Choose a team..."), + label=_("Team 1"), + ) + + problem1 = forms.IntegerField( + min_value=1, + max_value=8, + initial=1, + label=_("Problem defended by team 1"), + ) + + team2 = forms.ModelChoiceField( + Team.objects.filter(validation_status="2valid").all(), + empty_label=_("Choose a team..."), + label=_("Team 2"), + ) + + problem2 = forms.IntegerField( + min_value=1, + max_value=8, + initial=2, + label=_("Problem defended by team 2"), + ) + + team3 = forms.ModelChoiceField( + Team.objects.filter(validation_status="2valid").all(), + empty_label=_("Choose a team..."), + label=_("Team 3"), + ) + + problem3 = forms.IntegerField( + min_value=1, + max_value=8, + initial=3, + label=_("Problem defended by team 3"), + ) + + def clean(self): + cleaned_data = super().clean() + + team1, pb1 = cleaned_data["team1"], cleaned_data["problem1"] + team2, pb2 = cleaned_data["team2"], cleaned_data["problem2"] + team3, pb3 = cleaned_data["team3"], cleaned_data["problem3"] + + sol1 = Solution.objects.get(team=team1, problem=pb1, final=team1.selected_for_final) + sol2 = Solution.objects.get(team=team2, problem=pb2, final=team2.selected_for_final) + sol3 = Solution.objects.get(team=team3, problem=pb3, final=team3.selected_for_final) + + cleaned_data["teams"] = [team1, team2, team3] + cleaned_data["solutions"] = [sol1, sol2, sol3] + + return cleaned_data + + def save(self, commit=True): + pool = super().save(commit) + + pool.refresh_from_db() + pool.teams.set(self.cleaned_data["teams"]) + pool.solutions.set(self.cleaned_data["solutions"]) + pool.save() + + return pool + + class Meta: + model = Pool + fields = ('round', 'juries',) diff --git a/apps/tournament/models.py b/apps/tournament/models.py index bef6e5c..2a32b80 100644 --- a/apps/tournament/models.py +++ b/apps/tournament/models.py @@ -198,7 +198,52 @@ class Team(models.Model): unique_together = (('name', 'year',), ('trigram', 'year',),) def __str__(self): - return self.name + return self.trigram + " -- " + self.name + + +class Pool(models.Model): + teams = models.ManyToManyField( + Team, + related_name="pools", + verbose_name=_("teams"), + ) + + solutions = models.ManyToManyField( + "member.Solution", + related_name="pools", + verbose_name=_("solutions"), + ) + + round = models.PositiveIntegerField( + choices=[ + (1, _("Round 1")), + (2, _("Round 2")), + ], + verbose_name=_("round"), + ) + + juries = models.ManyToManyField( + "member.TFJMUser", + related_name="pools", + verbose_name=_("juries"), + ) + + @property + def problems(self): + return list(d["problem"] for d in self.solutions.values("problem").all()) + + @property + def tournament(self): + return self.solutions.first().tournament + + @property + def syntheses(self): + from member.models import Synthesis + return Synthesis.objects.filter(team__in=self.teams.all(), round=self.round, final=self.tournament.final) + + class Meta: + verbose_name = _("pool") + verbose_name_plural = _("pools") class Payment(models.Model): diff --git a/apps/tournament/tables.py b/apps/tournament/tables.py index 03bc156..05101f4 100644 --- a/apps/tournament/tables.py +++ b/apps/tournament/tables.py @@ -3,7 +3,7 @@ from django.utils.translation import gettext as _ from django_tables2 import A from member.models import Solution, Synthesis -from .models import Tournament, Team +from .models import Tournament, Team, Pool class TournamentTable(tables.Table): @@ -105,3 +105,21 @@ class SynthesisTable(tables.Table): attrs = { 'class': 'table table-condensed table-striped table-hover' } + + +class PoolTable(tables.Table): + def render_teams(self, value): + return ", ".join(team.trigram for team in value.all()) + + def render_problems(self, value): + return ", ".join([str(pb) for pb in value]) + + def render_juries(self, value): + return ", ".join(str(jury) for jury in value.all()) + + class Meta: + model = Pool + fields = ("teams", "problems", "round", "juries", ) + attrs = { + 'class': 'table table-condensed table-striped table-hover' + } diff --git a/apps/tournament/urls.py b/apps/tournament/urls.py index 9bb7b14..e09182b 100644 --- a/apps/tournament/urls.py +++ b/apps/tournament/urls.py @@ -1,8 +1,8 @@ from django.urls import path from .views import TournamentListView, TournamentCreateView, TournamentDetailView, TournamentUpdateView, \ - TeamDetailView, TeamUpdateView, AddOrganizerView, SolutionsView, SolutionsOrgaListView, SynthesesView,\ - SynthesesOrgaListView + TeamDetailView, TeamUpdateView, AddOrganizerView, SolutionsView, SolutionsOrgaListView, SynthesesView, \ + SynthesesOrgaListView, PoolListView, PoolCreateView, PoolDetailView app_name = "tournament" @@ -18,4 +18,7 @@ urlpatterns = [ path("all-solutions/", SolutionsOrgaListView.as_view(), name="all_solutions"), path("syntheses/", SynthesesView.as_view(), name="syntheses"), path("all_syntheses/", SynthesesOrgaListView.as_view(), name="all_syntheses"), + path("pools/", PoolListView.as_view(), name="pools"), + path("pools/add/", PoolCreateView.as_view(), name="create_pool"), + path("pool//", PoolDetailView.as_view(), name="pool_detail"), ] diff --git a/apps/tournament/views.py b/apps/tournament/views.py index 9b0fae3..ed9a6fe 100644 --- a/apps/tournament/views.py +++ b/apps/tournament/views.py @@ -17,9 +17,9 @@ from django.views.generic.edit import BaseFormView from django_tables2.views import SingleTableView from member.models import TFJMUser, Solution, Synthesis -from .forms import TournamentForm, OrganizerForm, SolutionForm, SynthesisForm, TeamForm -from .models import Tournament, Team -from .tables import TournamentTable, TeamTable, SolutionTable, SynthesisTable +from .forms import TournamentForm, OrganizerForm, SolutionForm, SynthesisForm, TeamForm, PoolForm +from .models import Tournament, Team, Pool +from .tables import TournamentTable, TeamTable, SolutionTable, SynthesisTable, PoolTable class AdminMixin(LoginRequiredMixin): @@ -411,3 +411,73 @@ class SynthesesOrgaListView(OrgaMixin, SingleTableView): return qs.order_by('team__tournament__date_start', 'team__tournament__name', 'team__trigram', 'round', 'source',) + +class PoolListView(LoginRequiredMixin, SingleTableView): + model = Pool + table_class = PoolTable + extra_context = dict(title=_("Pools")) + + def get_queryset(self): + qs = super().get_queryset() + user = self.request.user + if not user.admin and user.organizes: + qs = qs.filter(Q(jurys=user) | Q(solutions__tournament__organizers=user)) + elif user.participates: + qs = qs.filter(teams=user.team) + return qs.distinct() + + +class PoolCreateView(AdminMixin, CreateView): + model = Pool + form_class = PoolForm + extra_context = dict(title=_("Create pool")) + + def get_success_url(self): + return reverse_lazy("tournament:pools") + + +class PoolDetailView(LoginRequiredMixin, DetailView): + model = Pool + extra_context = dict(title=_("Pool detail")) + + def get_queryset(self): + qs = super().get_queryset() + user = self.request.user + if not user.admin and user.organizes: + qs = qs.filter(Q(jurys=user) | Q(solutions__tournament__organizers=user)) + elif user.participates: + qs = qs.filter(teams=user.team) + return qs.distinct() + + def post(self, request, *args, **kwargs): + user = request.user + pool = self.get_object() + + if "solutions_zip" in request.POST: + out = BytesIO() + zf = zipfile.ZipFile(out, "w") + + for solution in pool.solutions.all(): + zf.write(solution.file.path, str(solution) + ".pdf") + + zf.close() + + resp = HttpResponse(out.getvalue(), content_type="application/x-zip-compressed") + resp['Content-Disposition'] = 'attachment; filename={}' \ + .format(_("Solutions of a pool.zip").replace(" ", "%20")) + return resp + elif "syntheses_zip" in request.POST: + out = BytesIO() + zf = zipfile.ZipFile(out, "w") + + for synthesis in pool.syntheses.all(): + zf.write(synthesis.file.path, str(synthesis) + ".pdf") + + zf.close() + + resp = HttpResponse(out.getvalue(), content_type="application/x-zip-compressed") + resp['Content-Disposition'] = 'attachment; filename={}' \ + .format(_("Syntheses of a pool.zip").replace(" ", "%20")) + return resp + + return self.get(request, *args, **kwargs) \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index 64713ff..21de1c9 100644 --- a/templates/base.html +++ b/templates/base.html @@ -129,6 +129,9 @@ {% trans "Syntheses" %} {% endif %} + {% endif %}