diff --git a/apps/api/urls.py b/apps/api/urls.py index d08ac33..43923b6 100644 --- a/apps/api/urls.py +++ b/apps/api/urls.py @@ -110,7 +110,7 @@ class SynthesisViewSet(ModelViewSet): queryset = Synthesis.objects.all() serializer_class = SynthesisSerializer filter_backends = [DjangoFilterBackend] - filterset_fields = ['team', 'team__trigram', 'dest', 'round', ] + filterset_fields = ['team', 'team__trigram', 'source', 'round', ] # Routers provide an easy way of automatically determining the URL conf. diff --git a/apps/member/management/commands/import_olddb.py b/apps/member/management/commands/import_olddb.py index 3188455..27d0e47 100644 --- a/apps/member/management/commands/import_olddb.py +++ b/apps/member/management/commands/import_olddb.py @@ -263,7 +263,7 @@ class Command(BaseCommand): obj_dict = { "file": args[0], "team": Team.objects.get(pk=args[1]), - "dest": "defender" if args[3] == "DEFENDER" else "opponent" + "source": "defender" if args[3] == "DEFENDER" else "opponent" if args[4] == "OPPOSANT" else "rapporteur", "uploaded_at": args[4], } diff --git a/apps/member/models.py b/apps/member/models.py index 698d6d7..c6b6e65 100644 --- a/apps/member/models.py +++ b/apps/member/models.py @@ -243,6 +243,10 @@ class Solution(Document): verbose_name=_("final solution"), ) + @property + def tournament(self): + return Tournament.get_final() if self.final else self.team.tournament + class Meta: verbose_name = _("solution") verbose_name_plural = _("solutions") @@ -261,14 +265,14 @@ class Synthesis(Document): verbose_name=_("team"), ) - dest = models.CharField( + source = models.CharField( max_length=16, choices=[ ("defender", _("Defender")), ("opponent", _("Opponent")), ("rapporteur", _("Rapporteur")), ], - verbose_name=_("dest"), + verbose_name=_("source"), ) round = models.PositiveSmallIntegerField( @@ -284,14 +288,18 @@ class Synthesis(Document): verbose_name=_("final synthesis"), ) + @property + def tournament(self): + return Tournament.get_final() if self.final else self.team.tournament + class Meta: verbose_name = _("synthesis") verbose_name_plural = _("syntheses") - unique_together = ('team', 'dest', 'round', 'final',) + unique_together = ('team', 'source', 'round', 'final',) def __str__(self): - return _("Synthesis of team {trigram} that is {dest} for problem {problem}")\ - .format(trigram=self.team.trigram, dest=self.dest, problem=self.problem) + return _("Synthesis of team {trigram} that is {source} for the tournament {tournament}")\ + .format(trigram=self.team.trigram, source=self.source, tournament=self.tournament) class Config(models.Model): diff --git a/apps/tournament/forms.py b/apps/tournament/forms.py index d710cb0..cb16020 100644 --- a/apps/tournament/forms.py +++ b/apps/tournament/forms.py @@ -58,4 +58,4 @@ class SolutionForm(forms.ModelForm): class SynthesisForm(forms.ModelForm): class Meta: model = Synthesis - fields = ('file', 'dest',) + fields = ('file', 'source', 'round',) diff --git a/apps/tournament/models.py b/apps/tournament/models.py index 6fcb286..0e432e1 100644 --- a/apps/tournament/models.py +++ b/apps/tournament/models.py @@ -64,10 +64,22 @@ class Tournament(models.Model): verbose_name=_("year"), ) + @property + def linked_organizers(self): + return [''.format(url=reverse_lazy("member:information", args=(user.pk,))) + str(user) + '' + for user in self.organizers.all()] + @property def solutions(self): from member.models import Solution - return Solution.objects.filter(team__tournament=self) + return Solution.objects.filter(final=self.final) if self.final \ + else Solution.objects.filter(team__tournament=self) + + @property + def syntheses(self): + from member.models import Synthesis + return Synthesis.objects.filter(final=self.final) if self.final \ + else Synthesis.objects.filter(team__tournament=self) @classmethod def get_final(cls): diff --git a/apps/tournament/tables.py b/apps/tournament/tables.py index 2ee3478..03bc156 100644 --- a/apps/tournament/tables.py +++ b/apps/tournament/tables.py @@ -2,7 +2,7 @@ import django_tables2 as tables from django.utils.translation import gettext as _ from django_tables2 import A -from member.models import Solution +from member.models import Solution, Synthesis from .models import Tournament, Team @@ -49,8 +49,8 @@ class SolutionTable(tables.Table): tournament = tables.LinkColumn( "tournament:detail", - args=[A("team.tournament.pk")], - accessor=A("team.tournament"), + args=[A("tournament.pk")], + accessor=A("tournament"), ) file = tables.LinkColumn( @@ -75,6 +75,17 @@ class SolutionTable(tables.Table): class SynthesisTable(tables.Table): + team = tables.LinkColumn( + "tournament:team_detail", + args=[A("team.pk")], + ) + + tournament = tables.LinkColumn( + "tournament:detail", + args=[A("tournament.pk")], + accessor=A("tournament"), + ) + file = tables.LinkColumn( "document", args=[A("file")], @@ -89,8 +100,8 @@ class SynthesisTable(tables.Table): return _("Download") class Meta: - model = Team - fields = ("team", "team.tournament", "round", "dest", "uploaded_at", "file", ) + model = Synthesis + fields = ("team", "tournament", "round", "source", "uploaded_at", "file", ) attrs = { 'class': 'table table-condensed table-striped table-hover' } diff --git a/apps/tournament/urls.py b/apps/tournament/urls.py index f1d4800..9bb7b14 100644 --- a/apps/tournament/urls.py +++ b/apps/tournament/urls.py @@ -1,8 +1,8 @@ from django.urls import path -from django.views.generic import RedirectView -from .views import TournamentListView, TournamentCreateView, TournamentDetailView, TournamentUpdateView,\ - TeamDetailView, TeamUpdateView, AddOrganizerView, SolutionsView, SolutionsOrgaListView +from .views import TournamentListView, TournamentCreateView, TournamentDetailView, TournamentUpdateView, \ + TeamDetailView, TeamUpdateView, AddOrganizerView, SolutionsView, SolutionsOrgaListView, SynthesesView,\ + SynthesesOrgaListView app_name = "tournament" @@ -16,6 +16,6 @@ urlpatterns = [ path("add-organizer/", AddOrganizerView.as_view(), name="add_organizer"), path("solutions/", SolutionsView.as_view(), name="solutions"), path("all-solutions/", SolutionsOrgaListView.as_view(), name="all_solutions"), - path("syntheses/", RedirectView.as_view(pattern_name="index"), name="syntheses"), - path("all_syntheses/", RedirectView.as_view(pattern_name="index"), name="all_syntheses"), + path("syntheses/", SynthesesView.as_view(), name="syntheses"), + path("all_syntheses/", SynthesesOrgaListView.as_view(), name="all_syntheses"), ] diff --git a/apps/tournament/views.py b/apps/tournament/views.py index 57cbdac..2d4f32e 100644 --- a/apps/tournament/views.py +++ b/apps/tournament/views.py @@ -10,13 +10,13 @@ from django.shortcuts import redirect from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ from django.views.generic import DetailView, CreateView, UpdateView -from django.views.generic.edit import FormMixin, BaseFormView +from django.views.generic.edit import BaseFormView from django_tables2.views import SingleTableView -from member.models import TFJMUser, Solution -from .forms import TournamentForm, OrganizerForm, TeamForm, SolutionForm +from member.models import TFJMUser, Solution, Synthesis +from .forms import TournamentForm, OrganizerForm, TeamForm, SolutionForm, SynthesisForm from .models import Tournament, Team -from .tables import TournamentTable, TeamTable, SolutionTable +from .tables import TournamentTable, TeamTable, SolutionTable, SynthesisTable class AdminMixin(LoginRequiredMixin): @@ -26,6 +26,13 @@ class AdminMixin(LoginRequiredMixin): return super().dispatch(request, *args, **kwargs) +class OrgaMixin(LoginRequiredMixin): + def dispatch(self, request, *args, **kwargs): + if not request.user.organizes: + raise PermissionDenied + return super().dispatch(request, *args, **kwargs) + + class TeamMixin(LoginRequiredMixin): def dispatch(self, request, *args, **kwargs): if not request.user.team: @@ -181,14 +188,6 @@ class SolutionsView(TeamMixin, BaseFormView, SingleTableView): return super().post(request, *args, **kwargs) - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - - context["tournaments"] = \ - Tournament.objects if self.request.user.admin else self.request.user.organized_tournaments - - return context - def get_queryset(self): qs = super().get_queryset() if not self.request.user.admin: @@ -211,16 +210,11 @@ class SolutionsView(TeamMixin, BaseFormView, SingleTableView): return super().form_valid(form) - def form_invalid(self, form): - print(form.errors) - - return super().form_invalid(form) - def get_success_url(self): return reverse_lazy("tournament:solutions") -class SolutionsOrgaListView(AdminMixin, SingleTableView): +class SolutionsOrgaListView(OrgaMixin, SingleTableView): model = Solution table_class = SolutionTable template_name = "tournament/solutions_orga_list.html" @@ -262,3 +256,104 @@ class SolutionsOrgaListView(AdminMixin, SingleTableView): if not self.request.user.admin: qs = qs.filter(team__tournament__organizers=self.request.user) return qs.order_by('team__tournament__date_start', 'team__tournament__name', 'team__trigram', 'problem',) + + +class SynthesesView(TeamMixin, BaseFormView, SingleTableView): + model = Synthesis + table_class = SynthesisTable + form_class = SynthesisForm + template_name = "tournament/syntheses_list.html" + extra_context = dict(title=_("Syntheses")) + + def post(self, request, *args, **kwargs): + if "zip" in request.POST: + syntheses = request.user.team.syntheses + + out = BytesIO() + zf = zipfile.ZipFile(out, "w") + + for synthesis in syntheses: + 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 for team {team}.zip") + .format(team=str(request.user.team)).replace(" ", "%20")) + return resp + + return super().post(request, *args, **kwargs) + + def get_queryset(self): + qs = super().get_queryset() + if not self.request.user.admin: + qs = qs.filter(team=self.request.user.team) + return qs.order_by('team__tournament__date_start', 'team__tournament__name', 'team__trigram', 'round', + 'source',) + + def form_valid(self, form): + synthesis = form.instance + synthesis.team = self.request.user.team + synthesis.final = synthesis.team.selected_for_final + prev_syn = Synthesis.objects.filter(team=synthesis.team, round=synthesis.round, source=synthesis.source, + final=synthesis.final) + for syn in prev_syn.all(): + syn.delete() + alphabet = "0123456789abcdefghijklmnopqrstuvwxyz0123456789" + id = "" + for _ in range(64): + id += random.choice(alphabet) + synthesis.file.name = id + synthesis.save() + + return super().form_valid(form) + + def get_success_url(self): + return reverse_lazy("tournament:syntheses") + + +class SynthesesOrgaListView(OrgaMixin, SingleTableView): + model = Synthesis + table_class = SynthesisTable + template_name = "tournament/syntheses_orga_list.html" + extra_context = dict(title=_("All syntheses")) + + def post(self, request, *args, **kwargs): + if "tournament_zip" in request.POST: + tournament = Tournament.objects.get(pk=request.POST["tournament_zip"][0]) + syntheses = tournament.syntheses + if not request.user.admin and request.user not in tournament.organizers.all(): + raise PermissionDenied + + out = BytesIO() + zf = zipfile.ZipFile(out, "w") + + for synthesis in syntheses: + 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 for tournament {tournament}.zip") + .format(tournament=str(tournament)).replace(" ", "%20")) + return resp + + return self.get(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + context["tournaments"] = \ + Tournament.objects if self.request.user.admin else self.request.user.organized_tournaments + + return context + + def get_queryset(self): + qs = super().get_queryset() + if not self.request.user.admin: + qs = qs.filter(team__tournament__organizers=self.request.user) + return qs.order_by('team__tournament__date_start', 'team__tournament__name', 'team__trigram', 'round', + 'source',) + diff --git a/templates/member/tfjmuser_detail.html b/templates/member/tfjmuser_detail.html index 3fc25e4..e20e737 100644 --- a/templates/member/tfjmuser_detail.html +++ b/templates/member/tfjmuser_detail.html @@ -12,23 +12,33 @@
{% trans 'role'|capfirst %}
{{ tfjmuser.get_role_display }}
-
{% trans 'team'|capfirst %}
-
{{ tfjmuser.team }}
+ {% if tfjmuser.team %} +
{% trans 'team'|capfirst %}
+
{{ tfjmuser.team }}
+ {% endif %} -
{% trans 'birth date'|capfirst %}
-
{{ tfjmuser.birth_date }}
+ {% if tfjmuser.birth_date %} +
{% trans 'birth date'|capfirst %}
+
{{ tfjmuser.birth_date }}
+ {% endif %} -
{% trans 'gender'|capfirst %}
-
{{ tfjmuser.get_gender_display }}
+ {% if tfjmuser.participates %} +
{% trans 'gender'|capfirst %}
+
{{ tfjmuser.get_gender_display }}
+ {% endif %} -
{% trans 'address'|capfirst %}
-
{{ tfjmuser.address }}, {{ tfjmuser.postal_code }}, {{ tfjmuser.city }}{% if tfjmuser.country != "France" %}, {{ tfjmuser.country }}{% endif %}
+ {% if tfjmuser.address %} +
{% trans 'address'|capfirst %}
+
{{ tfjmuser.address }}, {{ tfjmuser.postal_code }}, {{ tfjmuser.city }}{% if tfjmuser.country != "France" %}, {{ tfjmuser.country }}{% endif %}
+ {% endif %}
{% trans 'email'|capfirst %}
{{ tfjmuser.email }}
-
{% trans 'phone number'|capfirst %}
-
{{ tfjmuser.phone_number }}
+ {% if tfjmuser.phone_number %} +
{% trans 'phone number'|capfirst %}
+
{{ tfjmuser.phone_number }}
+ {% endif %} {% if tfjmuser.role == '3participant' %}
{% trans 'school'|capfirst %}
@@ -53,9 +63,9 @@ {% endif %} {% endif %} - {% if tfjmuser.role == '2coach' %} + {% if tfjmuser.role != '3participant' %}
{% trans 'description'|capfirst %}
-
{{ tfjmuser.description }}
+
{{ tfjmuser.description|default_if_none:"" }}
{% endif %} diff --git a/templates/tournament/syntheses_list.html b/templates/tournament/syntheses_list.html new file mode 100644 index 0000000..2503997 --- /dev/null +++ b/templates/tournament/syntheses_list.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} + +{% load i18n crispy_forms_filters django_tables2 %} + +{% block content %} + {% if form %} +
+ {% csrf_token %} + {{ form|crispy }} + +
+
+ {% endif %} + {% render_table table %} +{% endblock %} diff --git a/templates/tournament/syntheses_orga_list.html b/templates/tournament/syntheses_orga_list.html new file mode 100644 index 0000000..fe83b4f --- /dev/null +++ b/templates/tournament/syntheses_orga_list.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} + +{% load i18n django_tables2 %} + +{% block content %} + {% render_table table %} + +
+ +
+ {% csrf_token %} +
+ {% for tournament in tournaments.all %} + + {% endfor %} +
+
+{% endblock %} diff --git a/templates/tournament/tournament_detail.html b/templates/tournament/tournament_detail.html index d5095da..89c0eaa 100644 --- a/templates/tournament/tournament_detail.html +++ b/templates/tournament/tournament_detail.html @@ -10,7 +10,7 @@
{% trans 'organizers'|capfirst %}
-
{{ tournament.organizers.all|join:", " }}
+
{% autoescape off %}{{ tournament.linked_organizers|join:", " }}{% endautoescape %}
{% trans 'size'|capfirst %}
{{ tournament.size }}