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 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()

View File

@ -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

View File

@ -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

View File

@ -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',)

View File

@ -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):

View File

@ -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'
}

View File

@ -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/<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 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)

View File

@ -129,6 +129,9 @@
<a class="nav-link" href="{% url "tournament:all_syntheses" %}"><i class="fas fa-feather"></i> {% trans "Syntheses" %}</a>
</li>
{% 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 %}
<li class="nav-item active">
<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 %}