Pool support

This commit is contained in:
Yohann D'ANELLO 2020-05-05 04:45:38 +02:00
parent b6422c1a79
commit 104ca590a5
12 changed files with 348 additions and 10 deletions

View File

@ -9,7 +9,7 @@ from rest_framework.filters import SearchFilter
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from member.models import TFJMUser, Authorization, Solution, Synthesis, MotivationLetter 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): class UserSerializer(serializers.ModelSerializer):
@ -59,6 +59,12 @@ class SynthesisSerializer(serializers.ModelSerializer):
fields = "__all__" fields = "__all__"
class PoolSerializer(serializers.ModelSerializer):
class Meta:
model = Pool
fields = "__all__"
class UserViewSet(ModelViewSet): class UserViewSet(ModelViewSet):
queryset = TFJMUser.objects.all() queryset = TFJMUser.objects.all()
serializer_class = UserSerializer serializer_class = UserSerializer
@ -113,6 +119,13 @@ class SynthesisViewSet(ModelViewSet):
filterset_fields = ['team', 'team__trigram', 'source', 'round', ] 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. # Routers provide an easy way of automatically determining the URL conf.
# Register each app API router and user viewset # Register each app API router and user viewset
router = routers.DefaultRouter() router = routers.DefaultRouter()

View File

@ -1,4 +1,5 @@
import random import random
from datetime import datetime
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import AnonymousUser 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): 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() 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: if not grant:
raise PermissionDenied raise PermissionDenied

View File

@ -1,6 +1,6 @@
from django.contrib.auth.admin import admin 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) @admin.register(Team)
@ -13,6 +13,11 @@ class TournamentAdmin(admin.ModelAdmin):
pass pass
@admin.register(Pool)
class PoolAdmin(admin.ModelAdmin):
pass
@admin.register(Payment) @admin.register(Payment)
class PaymentAdmin(admin.ModelAdmin): class PaymentAdmin(admin.ModelAdmin):
pass pass

View File

@ -3,14 +3,20 @@ import re
from datetime import datetime from datetime import datetime
from django import forms from django import forms
from django.db.models import Q
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from member.models import TFJMUser, Solution, Synthesis from member.models import TFJMUser, Solution, Synthesis
from tfjm.inputs import DatePickerInput, DateTimePickerInput, AmountInput from tfjm.inputs import DatePickerInput, DateTimePickerInput, AmountInput
from tournament.models import Tournament, Team from tournament.models import Tournament, Team, Pool
class TournamentForm(forms.ModelForm): class TournamentForm(forms.ModelForm):
organizers = forms.ModelMultipleChoiceField(
TFJMUser.objects.filter(Q(role="0admin") | Q(role="1volunteer")).order_by('role'),
label=_("Organizers"),
)
def clean(self): def clean(self):
cleaned_data = super().clean() cleaned_data = super().clean()
@ -122,3 +128,74 @@ class SynthesisForm(forms.ModelForm):
class Meta: class Meta:
model = Synthesis model = Synthesis
fields = ('file', 'source', 'round',) 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',)

View File

@ -198,7 +198,52 @@ class Team(models.Model):
unique_together = (('name', 'year',), ('trigram', 'year',),) unique_together = (('name', 'year',), ('trigram', 'year',),)
def __str__(self): 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): class Payment(models.Model):

View File

@ -3,7 +3,7 @@ from django.utils.translation import gettext as _
from django_tables2 import A from django_tables2 import A
from member.models import Solution, Synthesis from member.models import Solution, Synthesis
from .models import Tournament, Team from .models import Tournament, Team, Pool
class TournamentTable(tables.Table): class TournamentTable(tables.Table):
@ -105,3 +105,21 @@ class SynthesisTable(tables.Table):
attrs = { attrs = {
'class': 'table table-condensed table-striped table-hover' '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'
}

View File

@ -1,8 +1,8 @@
from django.urls import path from django.urls import path
from .views import TournamentListView, TournamentCreateView, TournamentDetailView, TournamentUpdateView, \ from .views import TournamentListView, TournamentCreateView, TournamentDetailView, TournamentUpdateView, \
TeamDetailView, TeamUpdateView, AddOrganizerView, SolutionsView, SolutionsOrgaListView, SynthesesView,\ TeamDetailView, TeamUpdateView, AddOrganizerView, SolutionsView, SolutionsOrgaListView, SynthesesView, \
SynthesesOrgaListView SynthesesOrgaListView, PoolListView, PoolCreateView, PoolDetailView
app_name = "tournament" app_name = "tournament"
@ -18,4 +18,7 @@ urlpatterns = [
path("all-solutions/", SolutionsOrgaListView.as_view(), name="all_solutions"), path("all-solutions/", SolutionsOrgaListView.as_view(), name="all_solutions"),
path("syntheses/", SynthesesView.as_view(), name="syntheses"), path("syntheses/", SynthesesView.as_view(), name="syntheses"),
path("all_syntheses/", SynthesesOrgaListView.as_view(), name="all_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/<int:pk>/", PoolDetailView.as_view(), name="pool_detail"),
] ]

View File

@ -17,9 +17,9 @@ from django.views.generic.edit import BaseFormView
from django_tables2.views import SingleTableView from django_tables2.views import SingleTableView
from member.models import TFJMUser, Solution, Synthesis from member.models import TFJMUser, Solution, Synthesis
from .forms import TournamentForm, OrganizerForm, SolutionForm, SynthesisForm, TeamForm from .forms import TournamentForm, OrganizerForm, SolutionForm, SynthesisForm, TeamForm, PoolForm
from .models import Tournament, Team from .models import Tournament, Team, Pool
from .tables import TournamentTable, TeamTable, SolutionTable, SynthesisTable from .tables import TournamentTable, TeamTable, SolutionTable, SynthesisTable, PoolTable
class AdminMixin(LoginRequiredMixin): 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', return qs.order_by('team__tournament__date_start', 'team__tournament__name', 'team__trigram', 'round',
'source',) '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)

View File

@ -129,6 +129,9 @@
<a class="nav-link" href="{% url "tournament:all_syntheses" %}"><i class="fas fa-feather"></i> {% trans "Syntheses" %}</a> <a class="nav-link" href="{% url "tournament:all_syntheses" %}"><i class="fas fa-feather"></i> {% trans "Syntheses" %}</a>
</li> </li>
{% endif %} {% endif %}
<li class="nav-item active">
<a class="nav-link" href="{% url "tournament:pools" %}"><i class="fas fa-swimming-pool"></i> {% trans "Pools" %}</a>
</li>
{% endif %} {% endif %}
<li class="nav-item active"> <li class="nav-item active">
<a class="nav-link" href="https://www.helloasso.com/associations/animath/formulaires/5/widget"><i <a class="nav-link" href="https://www.helloasso.com/associations/animath/formulaires/5/widget"><i

View File

@ -0,0 +1,70 @@
{% extends "base.html" %}
{% load getconfig i18n django_tables2 %}
{% block content %}
<div class="card bg-light shadow">
<div class="card-header text-center">
<h4>{{ title }}</h4>
</div>
<div class="card-body">
<dl class="row">
<dt class="col-xl-6 text-right">{% trans 'juries'|capfirst %}</dt>
<dd class="col-xl-6">{{ pool.juries.all|join:", " }}</dd>
<dt class="col-xl-6 text-right">{% trans 'teams'|capfirst %}</dt>
<dd class="col-xl-6">{{ pool.teams.all|join:", " }}</dd>
<dt class="col-xl-6 text-right">{% trans 'round'|capfirst %}</dt>
<dd class="col-xl-6">{{ pool.round }}</dd>
<dt class="col-xl-6 text-right">{% trans 'tournament'|capfirst %}</dt>
<dd class="col-xl-6">{{ pool.tournament }}</dd>
</dl>
</div>
</div>
<hr>
<div class="card bg-light shadow">
<div class="card-header text-center">
<h4>{% trans "Solutions" %}</h4>
</div>
<div class="card-body">
<ul>
{% for solution in pool.solutions.all %}
<li><a data-turbolinks="false" href="{{ solution.file.url }}">{{ solution }}</a></li>
{% endfor %}
</ul>
</div>
<div class="card-footer text-center">
<form method="post">
{% csrf_token %}
<button class="btn btn-success" name="solutions_zip">{% trans "Download ZIP archive" %}</button>
</form>
</div>
</div>
{% if user.organizes %}
<hr>
<div class="card bg-light shadow">
<div class="card-header text-center">
<h4>{% trans "Syntheses" %}</h4>
</div>
<div class="card-body">
<ul>
{% for synthesis in pool.syntheses.all %}
<li><a data-turbolinks="false" href="{{ synthesis.file.url }}">{{ synthesis }}</a></li>
{% endfor %}
</ul>
</div>
<div class="card-footer text-center">
<form method="post">
{% csrf_token %}
<button class="btn btn-success" name="syntheses_zip">{% trans "Download ZIP archive" %}</button>
</form>
</div>
</div>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,11 @@
{% extends "base.html" %}
{% load i18n crispy_forms_filters %}
{% block content %}
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<input type="submit" class="btn btn-primary btn-block" value="{% trans "Submit" %}">
</form>
{% endblock %}

View File

@ -0,0 +1,12 @@
{% extends "base.html" %}
{% load i18n django_tables2 %}
{% block content %}
{% render_table table %}
{% if user.admin %}
<hr>
<a href="{% url "tournament:create_pool" %}"><button class="btn btn-secondary btn-block">{% trans "Add pool" %}</button></a>
{% endif %}
{% endblock %}