1
0
mirror of https://gitlab.com/animath/si/plateforme.git synced 2025-06-26 03:57:41 +02:00

Comment code, fix minor issues

This commit is contained in:
Yohann D'ANELLO
2020-05-11 14:08:19 +02:00
parent c9b9d01523
commit a561364bd0
22 changed files with 650 additions and 179 deletions

View File

@ -1,23 +1,31 @@
from django.contrib.auth.admin import admin
from tournament.models import Team, Tournament, Pool, Payment
from .models import Team, Tournament, Pool, Payment
@admin.register(Team)
class TeamAdmin(admin.ModelAdmin):
pass
"""
Django admin page for teams.
"""
@admin.register(Tournament)
class TournamentAdmin(admin.ModelAdmin):
pass
"""
Django admin page for tournaments.
"""
@admin.register(Pool)
class PoolAdmin(admin.ModelAdmin):
pass
"""
Django admin page for pools.
"""
@admin.register(Payment)
class PaymentAdmin(admin.ModelAdmin):
pass
"""
Django admin page for payments.
"""

View File

@ -3,5 +3,8 @@ from django.utils.translation import gettext_lazy as _
class TournamentConfig(AppConfig):
"""
The tournament app handles all that is related to the tournaments.
"""
name = 'tournament'
verbose_name = _('tournament')

View File

@ -12,6 +12,11 @@ from tournament.models import Tournament, Team, Pool
class TournamentForm(forms.ModelForm):
"""
Create and update tournaments.
"""
# Only organizers can organize tournaments. Well, that's pretty normal...
organizers = forms.ModelMultipleChoiceField(
TFJMUser.objects.filter(Q(role="0admin") | Q(role="1volunteer")).order_by('role'),
label=_("Organizers"),
@ -44,6 +49,10 @@ class TournamentForm(forms.ModelForm):
class OrganizerForm(forms.ModelForm):
"""
Register an organizer in the website.
"""
class Meta:
model = TFJMUser
fields = ('last_name', 'first_name', 'email', 'is_superuser',)
@ -64,6 +73,9 @@ class OrganizerForm(forms.ModelForm):
class TeamForm(forms.ModelForm):
"""
Add and update a team.
"""
tournament = forms.ModelChoiceField(
Tournament.objects.filter(date_inscription__gte=timezone.now(), final=False),
)
@ -94,6 +106,10 @@ class TeamForm(forms.ModelForm):
class JoinTeam(forms.Form):
"""
Form to join a team with an access code.
"""
access_code = forms.CharField(
label=_("Access code"),
max_length=6,
@ -117,6 +133,10 @@ class JoinTeam(forms.Form):
class SolutionForm(forms.ModelForm):
"""
Form to upload a solution.
"""
problem = forms.ChoiceField(
label=_("Problem"),
choices=[(str(i), _("Problem #{problem:d}").format(problem=i)) for i in range(1, 9)],
@ -128,12 +148,21 @@ class SolutionForm(forms.ModelForm):
class SynthesisForm(forms.ModelForm):
"""
Form to upload a synthesis.
"""
class Meta:
model = Synthesis
fields = ('file', 'source', 'round',)
class PoolForm(forms.ModelForm):
"""
Form to add a pool.
Should not be used: prefer to pass by API and auto-add pools with the results of the draw.
"""
team1 = forms.ModelChoiceField(
Team.objects.filter(validation_status="2valid").all(),
empty_label=_("Choose a team..."),

View File

@ -9,6 +9,10 @@ from django.utils.translation import gettext_lazy as _
class Tournament(models.Model):
"""
Store the information of a tournament.
"""
name = models.CharField(
max_length=255,
verbose_name=_("name"),
@ -18,10 +22,12 @@ class Tournament(models.Model):
'member.TFJMUser',
related_name="organized_tournaments",
verbose_name=_("organizers"),
help_text=_("List of all organizers that can see and manipulate data of the tournament and the teams."),
)
size = models.PositiveSmallIntegerField(
verbose_name=_("size"),
help_text=_("Number of teams that are allowed to join the tournament."),
)
place = models.CharField(
@ -31,6 +37,7 @@ class Tournament(models.Model):
price = models.PositiveSmallIntegerField(
verbose_name=_("price"),
help_text=_("Price asked to participants. Free with a scholarship."),
)
description = models.TextField(
@ -74,6 +81,7 @@ class Tournament(models.Model):
final = models.BooleanField(
verbose_name=_("final tournament"),
help_text=_("It should be only one final tournament."),
)
year = models.PositiveIntegerField(
@ -83,27 +91,43 @@ class Tournament(models.Model):
@property
def teams(self):
"""
Get all teams that are registered to this tournament, with a distinction for the final tournament.
"""
return self._teams if not self.final else Team.objects.filter(selected_for_final=True)
@property
def linked_organizers(self):
"""
Display a list of the organizers with links to their personal page.
"""
return ['<a href="{url}">'.format(url=reverse_lazy("member:information", args=(user.pk,))) + str(user) + '</a>'
for user in self.organizers.all()]
@property
def solutions(self):
"""
Get all sent solutions for this tournament.
"""
from member.models import Solution
return Solution.objects.filter(final=self.final) if self.final \
else Solution.objects.filter(team__tournament=self)
else Solution.objects.filter(team__tournament=self, final=False)
@property
def syntheses(self):
"""
Get all sent syntheses for this tournament.
"""
from member.models import Synthesis
return Synthesis.objects.filter(final=self.final) if self.final \
else Synthesis.objects.filter(team__tournament=self)
else Synthesis.objects.filter(team__tournament=self, final=False)
@classmethod
def get_final(cls):
"""
Get the final tournament.
This should exist and be unique.
"""
return cls.objects.get(year=os.getenv("TFJM_YEAR"), final=True)
class Meta:
@ -111,6 +135,12 @@ class Tournament(models.Model):
verbose_name_plural = _("tournaments")
def send_mail_to_organizers(self, template_name, subject="Contact TFJM²", **kwargs):
"""
Send a mail to all organizers of the tournament.
The template of the mail should be found either in templates/mail_templates/<template_name>.html for the HTML
version and in templates/mail_templates/<template_name>.txt for the plain text version.
The context of the template contains the tournament and the user. Extra context can be given through the kwargs.
"""
context = kwargs
context["tournament"] = self
for user in self.organizers.all():
@ -130,6 +160,10 @@ class Tournament(models.Model):
class Team(models.Model):
"""
Store information about a registered team.
"""
name = models.CharField(
max_length=255,
verbose_name=_("name"),
@ -138,6 +172,7 @@ class Team(models.Model):
trigram = models.CharField(
max_length=3,
verbose_name=_("trigram"),
help_text=_("The trigram should be composed of 3 capitalize letters, that is a funny acronym for the team."),
)
tournament = models.ForeignKey(
@ -145,6 +180,7 @@ class Team(models.Model):
on_delete=models.PROTECT,
related_name="_teams",
verbose_name=_("tournament"),
help_text=_("The tournament where the team is registered."),
)
inscription_date = models.DateTimeField(
@ -191,31 +227,59 @@ class Team(models.Model):
return self.validation_status == "0invalid"
@property
def encadrants(self):
def coaches(self):
"""
Get all coaches of a team.
"""
return self.users.all().filter(role="2coach")
@property
def linked_encadrants(self):
def linked_coaches(self):
"""
Get a list of the coaches of a team with html links to their pages.
"""
return ['<a href="{url}">'.format(url=reverse_lazy("member:information", args=(user.pk,))) + str(user) + '</a>'
for user in self.encadrants]
for user in self.coaches]
@property
def participants(self):
"""
Get all particpants of a team, coaches excluded.
"""
return self.users.all().filter(role="3participant")
@property
def linked_participants(self):
"""
Get a list of the participants of a team with html links to their pages.
"""
return ['<a href="{url}">'.format(url=reverse_lazy("member:information", args=(user.pk,))) + str(user) + '</a>'
for user in self.participants]
@property
def future_tournament(self):
"""
Get the last tournament where the team is registered.
Only matters if the team is selected for final: if this is the case, we return the final tournament.
Useful for deadlines.
"""
return Tournament.get_final() if self.selected_for_final else self.tournament
@property
def can_validate(self):
"""
Check if a given team is able to ask for validation.
A team can validate if:
* All participants filled the photo consent
* Minor participants filled the parental consent
* Minor participants filled the sanitary plug
* Teams sent their motivation letter
* The team contains at least 4 participants
* The team contains at least 1 coach
"""
# TODO In a normal time, team needs a motivation letter and authorizations.
return self.encadrants.exists() and self.participants.count() >= 4
return self.coaches.exists() and self.participants.count() >= 4\
and self.tournament.date_inscription <= timezone.now()
class Meta:
verbose_name = _("team")
@ -223,6 +287,12 @@ class Team(models.Model):
unique_together = (('name', 'year',), ('trigram', 'year',),)
def send_mail(self, template_name, subject="Contact TFJM²", **kwargs):
"""
Send a mail to all members of a team with a given template.
The template of the mail should be found either in templates/mail_templates/<template_name>.html for the HTML
version and in templates/mail_templates/<template_name>.txt for the plain text version.
The context of the template contains the team and the user. Extra context can be given through the kwargs.
"""
context = kwargs
context["team"] = self
for user in self.users.all():
@ -236,6 +306,12 @@ class Team(models.Model):
class Pool(models.Model):
"""
Store information of a pool.
A pool is only a list of accessible solutions to some teams and some juries.
TODO: check that the set of teams is equal to the set of the teams that have a solution in this set.
TODO: Moreover, a team should send only one solution.
"""
teams = models.ManyToManyField(
Team,
related_name="pools",
@ -264,14 +340,24 @@ class Pool(models.Model):
@property
def problems(self):
"""
Get problem numbers of the sent solutions as a list of integers.
"""
return list(d["problem"] for d in self.solutions.values("problem").all())
@property
def tournament(self):
"""
Get the concerned tournament.
We assume that the pool is correct, so all solutions belong to the same tournament.
"""
return self.solutions.first().tournament
@property
def syntheses(self):
"""
Get the syntheses of the teams that are in this pool, for the correct round.
"""
from member.models import Synthesis
return Synthesis.objects.filter(team__in=self.teams.all(), round=self.round, final=self.tournament.final)
@ -281,6 +367,10 @@ class Pool(models.Model):
class Payment(models.Model):
"""
Store some information about payments, to recover data.
TODO: handle it...
"""
user = models.OneToOneField(
'member.TFJMUser',
on_delete=models.CASCADE,

View File

@ -9,6 +9,10 @@ from .models import Tournament, Team, Pool
class TournamentTable(tables.Table):
"""
List all tournaments.
"""
name = tables.LinkColumn(
"tournament:detail",
args=[A("pk")],
@ -31,6 +35,10 @@ class TournamentTable(tables.Table):
class TeamTable(tables.Table):
"""
Table of some teams. Can be filtered with a queryset (for example, teams of a tournament)
"""
name = tables.LinkColumn(
"tournament:team_detail",
args=[A("pk")],
@ -46,6 +54,10 @@ class TeamTable(tables.Table):
class SolutionTable(tables.Table):
"""
Display a table of some solutions.
"""
team = tables.LinkColumn(
"tournament:team_detail",
args=[A("team.pk")],
@ -81,6 +93,10 @@ class SolutionTable(tables.Table):
class SynthesisTable(tables.Table):
"""
Display a table of some syntheses.
"""
team = tables.LinkColumn(
"tournament:team_detail",
args=[A("team.pk")],
@ -116,6 +132,10 @@ class SynthesisTable(tables.Table):
class PoolTable(tables.Table):
"""
Display a table of some pools.
"""
problems = tables.Column(
verbose_name=_("Problems"),
orderable=False,

View File

@ -24,6 +24,10 @@ from .tables import TournamentTable, TeamTable, SolutionTable, SynthesisTable, P
class AdminMixin(LoginRequiredMixin):
"""
If a view extends this mixin, then the view will be only accessible to administrators.
"""
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated or not request.user.admin:
raise PermissionDenied
@ -31,6 +35,10 @@ class AdminMixin(LoginRequiredMixin):
class OrgaMixin(LoginRequiredMixin):
"""
If a view extends this mixin, then the view will be only accessible to administrators or organizers.
"""
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated or not request.user.organizes:
raise PermissionDenied
@ -38,6 +46,10 @@ class OrgaMixin(LoginRequiredMixin):
class TeamMixin(LoginRequiredMixin):
"""
If a view extends this mixin, then the view will be only accessible to users that are registered in a team.
"""
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated or not request.user.team:
raise PermissionDenied
@ -45,6 +57,10 @@ class TeamMixin(LoginRequiredMixin):
class TournamentListView(SingleTableView):
"""
Display the list of all tournaments, ordered by start date then name.
"""
model = Tournament
table_class = TournamentTable
extra_context = dict(title=_("Tournaments list"),)
@ -64,6 +80,10 @@ class TournamentListView(SingleTableView):
class TournamentCreateView(AdminMixin, CreateView):
"""
Create a tournament. Only accessible to admins.
"""
model = Tournament
form_class = TournamentForm
extra_context = dict(title=_("Add tournament"),)
@ -73,6 +93,11 @@ class TournamentCreateView(AdminMixin, CreateView):
class TournamentDetailView(DetailView):
"""
Display the detail of a tournament.
Accessible to all, including not authenticated users.
"""
model = Tournament
def get_context_data(self, **kwargs):
@ -96,7 +121,20 @@ class TournamentDetailView(DetailView):
return context
class TournamentUpdateView(AdminMixin, UpdateView):
class TournamentUpdateView(OrgaMixin, UpdateView):
"""
Update the data of a tournament.
Reserved to admins and organizers of the tournament.
"""
def dispatch(self, request, *args, **kwargs):
"""
Restrict the view to organizers of tournaments, then process the request.
"""
if self.request.user.role == "1volunteer" and self.request.user not in self.get_object().organizers.all():
raise PermissionDenied
return super().dispatch(request, *args, **kwargs)
model = Tournament
form_class = TournamentForm
extra_context = dict(title=_("Update tournament"),)
@ -106,9 +144,16 @@ class TournamentUpdateView(AdminMixin, UpdateView):
class TeamDetailView(LoginRequiredMixin, DetailView):
"""
View the detail of a team.
Restricted to this team, admins and organizers of its tournament.
"""
model = Team
def dispatch(self, request, *args, **kwargs):
"""
Protect the page and process the request.
"""
if not request.user.is_authenticated or \
(not request.user.admin and self.request.user not in self.get_object().tournament.organizers.all()
and self.get_object() != request.user.team):
@ -116,7 +161,15 @@ class TeamDetailView(LoginRequiredMixin, DetailView):
return super().dispatch(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
print(request.POST)
"""
Process POST requests. Supported requests:
- get the solutions of the team as a ZIP archive
- a user leaves its team (if the composition is not validated yet)
- the team requests the validation
- Organizers can validate or invalidate the request
- Admins can delete teams
- Admins can select teams for the final tournament
"""
team = self.get_object()
if "zip" in request.POST:
solutions = team.solutions.all()
@ -140,7 +193,7 @@ class TeamDetailView(LoginRequiredMixin, DetailView):
if not team.users.exists():
team.delete()
return redirect('tournament:detail', pk=team.tournament.pk)
elif "request_validation" in request.POST and request.user.participates:
elif "request_validation" in request.POST and request.user.participates and team.can_validate:
team.validation_status = "1waiting"
team.save()
team.tournament.send_mail_to_organizers("request_validation", "Demande de validation TFJM²", team=team)
@ -159,6 +212,7 @@ class TeamDetailView(LoginRequiredMixin, DetailView):
team.delete()
return redirect('tournament:detail', pk=team.tournament.pk)
elif "select_final" in request.POST and request.user.admin and not team.selected_for_final and team.pools:
# We copy all solutions for solutions for the final
for solution in team.solutions.all():
alphabet = "0123456789abcdefghijklmnopqrstuvwxyz0123456789"
id = ""
@ -194,6 +248,11 @@ class TeamDetailView(LoginRequiredMixin, DetailView):
class TeamUpdateView(LoginRequiredMixin, UpdateView):
"""
Update the information about a team.
Team members, admins and organizers are allowed to do this.
"""
model = Team
form_class = TeamForm
extra_context = dict(title=_("Update team"),)
@ -206,6 +265,12 @@ class TeamUpdateView(LoginRequiredMixin, UpdateView):
class AddOrganizerView(AdminMixin, CreateView):
"""
Add a new organizer account. No password is created, the user should reset its password using the link
sent by mail. Only name and email are requested.
Only admins are granted to do this.
"""
model = TFJMUser
form_class = OrganizerForm
extra_context = dict(title=_("Add organizer"),)
@ -223,6 +288,10 @@ class AddOrganizerView(AdminMixin, CreateView):
class SolutionsView(TeamMixin, BaseFormView, SingleTableView):
"""
Upload and view solutions for a team.
"""
model = Solution
table_class = SolutionTable
form_class = SolutionForm
@ -288,6 +357,11 @@ class SolutionsView(TeamMixin, BaseFormView, SingleTableView):
class SolutionsOrgaListView(OrgaMixin, SingleTableView):
"""
View all solutions sent by teams for the organized tournaments. Juries can view solutions of their pools.
Organizers can download a ZIP archive for each organized tournament.
"""
model = Solution
table_class = SolutionTable
template_name = "tournament/solutions_orga_list.html"
@ -333,6 +407,9 @@ class SolutionsOrgaListView(OrgaMixin, SingleTableView):
class SynthesesView(TeamMixin, BaseFormView, SingleTableView):
"""
Upload and view syntheses for a team.
"""
model = Synthesis
table_class = SynthesisTable
form_class = SynthesisForm
@ -407,6 +484,10 @@ class SynthesesView(TeamMixin, BaseFormView, SingleTableView):
class SynthesesOrgaListView(OrgaMixin, SingleTableView):
"""
View all syntheses sent by teams for the organized tournaments. Juries can view syntheses of their pools.
Organizers can download a ZIP archive for each organized tournament.
"""
model = Synthesis
table_class = SynthesisTable
template_name = "tournament/syntheses_orga_list.html"
@ -452,6 +533,10 @@ class SynthesesOrgaListView(OrgaMixin, SingleTableView):
class PoolListView(LoginRequiredMixin, SingleTableView):
"""
View the list of visible pools.
Admins see all, juries see their own pools, organizers see the pools of their tournaments.
"""
model = Pool
table_class = PoolTable
extra_context = dict(title=_("Pools"))
@ -469,6 +554,10 @@ class PoolListView(LoginRequiredMixin, SingleTableView):
class PoolCreateView(AdminMixin, CreateView):
"""
Create a pool manually.
This page should not be used: prefer send automatically data from the drawing bot.
"""
model = Pool
form_class = PoolForm
extra_context = dict(title=_("Create pool"))
@ -478,6 +567,15 @@ class PoolCreateView(AdminMixin, CreateView):
class PoolDetailView(LoginRequiredMixin, DetailView):
"""
See the detail of a pool.
Teams and juries can download here defended solutions of the pool.
If this is the second round, teams can't download solutions of the other teams before the date when they
should be available.
Juries see also syntheses. They see of course solutions immediately.
This is also true for organizers and admins.
All can be downloaded as a ZIP archive.
"""
model = Pool
extra_context = dict(title=_("Pool detail"))