Pool support
This commit is contained in:
parent
b6422c1a79
commit
104ca590a5
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
|
|
|
@ -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"),
|
||||
]
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
|
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
Loading…
Reference in New Issue