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

@ -3,5 +3,8 @@ from django.utils.translation import gettext_lazy as _
class APIConfig(AppConfig): class APIConfig(AppConfig):
"""
Manage the inscription through a JSON API.
"""
name = 'api' name = 'api'
verbose_name = _('API') verbose_name = _('API')

80
apps/api/serializers.py Normal file
View File

@ -0,0 +1,80 @@
from rest_framework import serializers
from member.models import TFJMUser, Authorization, MotivationLetter, Solution, Synthesis
from tournament.models import Team, Tournament, Pool
class UserSerializer(serializers.ModelSerializer):
"""
Serialize a User object into JSON.
"""
class Meta:
model = TFJMUser
exclude = (
'username',
'password',
'groups',
'user_permissions',
)
class TeamSerializer(serializers.ModelSerializer):
"""
Serialize a Team object into JSON.
"""
class Meta:
model = Team
fields = "__all__"
class TournamentSerializer(serializers.ModelSerializer):
"""
Serialize a Tournament object into JSON.
"""
class Meta:
model = Tournament
fields = "__all__"
class AuthorizationSerializer(serializers.ModelSerializer):
"""
Serialize an Authorization object into JSON.
"""
class Meta:
model = Authorization
fields = "__all__"
class MotivationLetterSerializer(serializers.ModelSerializer):
"""
Serialize a MotivationLetter object into JSON.
"""
class Meta:
model = MotivationLetter
fields = "__all__"
class SolutionSerializer(serializers.ModelSerializer):
"""
Serialize a Solution object into JSON.
"""
class Meta:
model = Solution
fields = "__all__"
class SynthesisSerializer(serializers.ModelSerializer):
"""
Serialize a Synthesis object into JSON.
"""
class Meta:
model = Synthesis
fields = "__all__"
class PoolSerializer(serializers.ModelSerializer):
"""
Serialize a Pool object into JSON.
"""
class Meta:
model = Pool
fields = "__all__"

View File

@ -1,152 +1,8 @@
from django.conf.urls import url, include from django.conf.urls import url, include
from django_filters.rest_framework import DjangoFilterBackend from rest_framework import routers
from rest_framework import routers, serializers, status
from rest_framework.filters import SearchFilter
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from member.models import TFJMUser, Authorization, Solution, Synthesis, MotivationLetter
from tournament.models import Team, Tournament, Pool
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = TFJMUser
exclude = (
'email',
'password',
'groups',
'user_permissions',
)
class TeamSerializer(serializers.ModelSerializer):
class Meta:
model = Team
fields = "__all__"
class TournamentSerializer(serializers.ModelSerializer):
class Meta:
model = Tournament
fields = "__all__"
class AuthorizationSerializer(serializers.ModelSerializer):
class Meta:
model = Authorization
fields = "__all__"
class MotivationLetterSerializer(serializers.ModelSerializer):
class Meta:
model = MotivationLetter
fields = "__all__"
class SolutionSerializer(serializers.ModelSerializer):
class Meta:
model = Solution
fields = "__all__"
class SynthesisSerializer(serializers.ModelSerializer):
class Meta:
model = Synthesis
fields = "__all__"
class PoolSerializer(serializers.ModelSerializer):
class Meta:
model = Pool
fields = "__all__"
class UserViewSet(ModelViewSet):
queryset = TFJMUser.objects.all()
serializer_class = UserSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['id', 'first_name', 'last_name', 'email', 'gender', 'student_class', 'role', 'year', 'team',
'team__trigram', 'is_superuser', 'is_staff', 'is_active', ]
search_fields = ['$first_name', '$last_name', ]
class TeamViewSet(ModelViewSet):
queryset = Team.objects.all()
serializer_class = TeamSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['name', 'trigram', 'validation_status', 'selected_for_final', 'access_code', 'tournament',
'year', ]
search_fields = ['$name', 'trigram', ]
class TournamentViewSet(ModelViewSet):
queryset = Tournament.objects.all()
serializer_class = TournamentSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['name', 'size', 'price', 'date_start', 'date_end', 'final', 'organizers', 'year', ]
search_fields = ['$name', ]
class AuthorizationViewSet(ModelViewSet):
queryset = Authorization.objects.all()
serializer_class = AuthorizationSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['user', 'type', ]
class MotivationLetterViewSet(ModelViewSet):
queryset = MotivationLetter.objects.all()
serializer_class = MotivationLetterSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['team', 'team__trigram', ]
class SolutionViewSet(ModelViewSet):
queryset = Solution.objects.all()
serializer_class = SolutionSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['team', 'team__trigram', 'problem', ]
class SynthesisViewSet(ModelViewSet):
queryset = Synthesis.objects.all()
serializer_class = SynthesisSerializer
filter_backends = [DjangoFilterBackend]
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', ]
def create(self, request, *args, **kwargs):
data = request.data
try:
spl = data.split(";")
if len(spl) >= 7:
round = int(spl[0])
teams = []
solutions = []
for i in range((len(spl) - 1) // 2):
trigram = spl[1 + 2 * i]
pb = int(spl[2 + 2 * i])
team = Team.objects.get(trigram=trigram)
solution = Solution.objects.get(team=team, problem=pb, final=team.selected_for_final)
teams.append(team)
solutions.append(solution)
pool = Pool.objects.create(round=round)
pool.teams.set(teams)
pool.solutions.set(solutions)
pool.save()
serializer = PoolSerializer(pool)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
except BaseException: # JSON data
pass
return super().create(request, *args, **kwargs)
from .viewsets import UserViewSet, TeamViewSet, TournamentViewSet, AuthorizationViewSet, MotivationLetterViewSet, \
SolutionViewSet, SynthesisViewSet, PoolViewSet
# 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

124
apps/api/viewsets.py Normal file
View File

@ -0,0 +1,124 @@
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import status
from rest_framework.filters import SearchFilter
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from member.models import TFJMUser, Authorization, MotivationLetter, Solution, Synthesis
from tournament.models import Team, Tournament, Pool
from .serializers import UserSerializer, TeamSerializer, TournamentSerializer, AuthorizationSerializer, \
MotivationLetterSerializer, SolutionSerializer, SynthesisSerializer, PoolSerializer
class UserViewSet(ModelViewSet):
"""
Display list of users.
"""
queryset = TFJMUser.objects.all()
serializer_class = UserSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['id', 'first_name', 'last_name', 'email', 'gender', 'student_class', 'role', 'year', 'team',
'team__trigram', 'is_superuser', 'is_staff', 'is_active', ]
search_fields = ['$first_name', '$last_name', ]
class TeamViewSet(ModelViewSet):
"""
Display list of teams.
"""
queryset = Team.objects.all()
serializer_class = TeamSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['name', 'trigram', 'validation_status', 'selected_for_final', 'access_code', 'tournament',
'year', ]
search_fields = ['$name', 'trigram', ]
class TournamentViewSet(ModelViewSet):
"""
Display list of tournaments.
"""
queryset = Tournament.objects.all()
serializer_class = TournamentSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['name', 'size', 'price', 'date_start', 'date_end', 'final', 'organizers', 'year', ]
search_fields = ['$name', ]
class AuthorizationViewSet(ModelViewSet):
"""
Display list of authorizations.
"""
queryset = Authorization.objects.all()
serializer_class = AuthorizationSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['user', 'type', ]
class MotivationLetterViewSet(ModelViewSet):
"""
Display list of motivation letters.
"""
queryset = MotivationLetter.objects.all()
serializer_class = MotivationLetterSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['team', 'team__trigram', ]
class SolutionViewSet(ModelViewSet):
"""
Display list of solutions.
"""
queryset = Solution.objects.all()
serializer_class = SolutionSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['team', 'team__trigram', 'problem', ]
class SynthesisViewSet(ModelViewSet):
"""
Display list of syntheses.
"""
queryset = Synthesis.objects.all()
serializer_class = SynthesisSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['team', 'team__trigram', 'source', 'round', ]
class PoolViewSet(ModelViewSet):
"""
Display list of pools.
If the request is a POST request and the format is "A;X;x;Y;y;Z;z;..." where A = 1 or 1 = 2,
X, Y, Z, ... are team trigrams, x, y, z, ... are numbers of problems, then this is interpreted as a
creation a pool for the round A with the solutions of problems x, y, z, ... of the teams X, Y, Z, ... respectively.
"""
queryset = Pool.objects.all()
serializer_class = PoolSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['teams', 'teams__trigram', 'round', ]
def create(self, request, *args, **kwargs):
data = request.data
try:
spl = data.split(";")
if len(spl) >= 7:
round = int(spl[0])
teams = []
solutions = []
for i in range((len(spl) - 1) // 2):
trigram = spl[1 + 2 * i]
pb = int(spl[2 + 2 * i])
team = Team.objects.get(trigram=trigram)
solution = Solution.objects.get(team=team, problem=pb, final=team.selected_for_final)
teams.append(team)
solutions.append(solution)
pool = Pool.objects.create(round=round)
pool.teams.set(teams)
pool.solutions.set(solutions)
pool.save()
serializer = PoolSerializer(pool)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
except BaseException: # JSON data
pass
return super().create(request, *args, **kwargs)

View File

@ -5,35 +5,51 @@ from member.models import TFJMUser, Document, Solution, Synthesis, MotivationLet
@admin.register(TFJMUser) @admin.register(TFJMUser)
class TFJMUserAdmin(UserAdmin): class TFJMUserAdmin(UserAdmin):
"""
Django admin page for users.
"""
list_display = ('email', 'first_name', 'last_name', 'role', ) list_display = ('email', 'first_name', 'last_name', 'role', )
@admin.register(Document) @admin.register(Document)
class DocumentAdmin(PolymorphicParentModelAdmin): class DocumentAdmin(PolymorphicParentModelAdmin):
"""
Django admin page for any documents.
"""
child_models = (Authorization, MotivationLetter, Solution, Synthesis,) child_models = (Authorization, MotivationLetter, Solution, Synthesis,)
polymorphic_list = True polymorphic_list = True
@admin.register(Authorization) @admin.register(Authorization)
class AuthorizationAdmin(PolymorphicChildModelAdmin): class AuthorizationAdmin(PolymorphicChildModelAdmin):
pass """
Django admin page for Authorization.
"""
@admin.register(MotivationLetter) @admin.register(MotivationLetter)
class MotivationLetterAdmin(PolymorphicChildModelAdmin): class MotivationLetterAdmin(PolymorphicChildModelAdmin):
pass """
Django admin page for Motivation letters.
"""
@admin.register(Solution) @admin.register(Solution)
class SolutionAdmin(PolymorphicChildModelAdmin): class SolutionAdmin(PolymorphicChildModelAdmin):
pass """
Django admin page for solutions.
"""
@admin.register(Synthesis) @admin.register(Synthesis)
class SynthesisAdmin(PolymorphicChildModelAdmin): class SynthesisAdmin(PolymorphicChildModelAdmin):
pass """
Django admin page for syntheses.
"""
@admin.register(Config) @admin.register(Config)
class ConfigAdmin(admin.ModelAdmin): class ConfigAdmin(admin.ModelAdmin):
pass """
Django admin page for configurations.
"""

View File

@ -3,5 +3,8 @@ from django.utils.translation import gettext_lazy as _
class MemberConfig(AppConfig): class MemberConfig(AppConfig):
"""
The member app handles the information that concern a user, its documents, ...
"""
name = 'member' name = 'member'
verbose_name = _('member') verbose_name = _('member')

View File

@ -2,18 +2,22 @@ from django.contrib.auth.forms import UserCreationForm
from django import forms from django import forms
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from member.models import TFJMUser from .models import TFJMUser
class SignUpForm(UserCreationForm): class SignUpForm(UserCreationForm):
"""
Coaches and participants register on the website through this form.
TODO: Check if this form works, render it better
"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields["first_name"].required = True self.fields["first_name"].required = True
self.fields["last_name"].required = True self.fields["last_name"].required = True
self.fields["role"].choices = [ self.fields["role"].choices = [
('', _("Choose a role...")), ('', _("Choose a role...")),
('participant', _("Participant")), ('3participant', _("Participant")),
('encadrant', _("Encadrant")), ('2coach', _("Coach")),
] ]
class Meta: class Meta:
@ -40,6 +44,9 @@ class SignUpForm(UserCreationForm):
class TFJMUserForm(forms.ModelForm): class TFJMUserForm(forms.ModelForm):
"""
Form to update our own information when we are participant.
"""
class Meta: class Meta:
model = TFJMUser model = TFJMUser
fields = ('last_name', 'first_name', 'email', 'phone_number', 'gender', 'birth_date', 'address', 'postal_code', fields = ('last_name', 'first_name', 'email', 'phone_number', 'gender', 'birth_date', 'address', 'postal_code',
@ -48,6 +55,9 @@ class TFJMUserForm(forms.ModelForm):
class CoachUserForm(forms.ModelForm): class CoachUserForm(forms.ModelForm):
"""
Form to update our own information when we are coach.
"""
class Meta: class Meta:
model = TFJMUser model = TFJMUser
fields = ('last_name', 'first_name', 'email', 'phone_number', 'gender', 'birth_date', 'address', 'postal_code', fields = ('last_name', 'first_name', 'email', 'phone_number', 'gender', 'birth_date', 'address', 'postal_code',
@ -55,6 +65,9 @@ class CoachUserForm(forms.ModelForm):
class AdminUserForm(forms.ModelForm): class AdminUserForm(forms.ModelForm):
"""
Form to update our own information when we are organizer or admin.
"""
class Meta: class Meta:
model = TFJMUser model = TFJMUser
fields = ('last_name', 'first_name', 'email', 'phone_number', 'description',) fields = ('last_name', 'first_name', 'email', 'phone_number', 'description',)

View File

@ -7,6 +7,9 @@ from member.models import TFJMUser
class Command(BaseCommand): class Command(BaseCommand):
def handle(self, *args, **options): def handle(self, *args, **options):
"""
Little script that generate a superuser.
"""
email = input("Email: ") email = input("Email: ")
password = "1" password = "1"
confirm_password = "2" confirm_password = "2"

View File

@ -1,3 +1,5 @@
import os
from django.core.management import BaseCommand, CommandError from django.core.management import BaseCommand, CommandError
from django.db import transaction from django.db import transaction
from member.models import TFJMUser, Document, Solution, Synthesis, Authorization, MotivationLetter from member.models import TFJMUser, Document, Solution, Synthesis, Authorization, MotivationLetter
@ -5,6 +7,11 @@ from tournament.models import Team, Tournament
class Command(BaseCommand): class Command(BaseCommand):
"""
Import the old database.
Tables must be found into the import_olddb folder, as CSV files.
"""
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument('--tournaments', '-t', action="store", help="Import tournaments") parser.add_argument('--tournaments', '-t', action="store", help="Import tournaments")
parser.add_argument('--teams', '-T', action="store", help="Import teams") parser.add_argument('--teams', '-T', action="store", help="Import teams")
@ -26,6 +33,9 @@ class Command(BaseCommand):
@transaction.atomic @transaction.atomic
def import_tournaments(self): def import_tournaments(self):
"""
Import tournaments into the new database.
"""
print("Importing tournaments...") print("Importing tournaments...")
with open("import_olddb/tournaments.csv") as f: with open("import_olddb/tournaments.csv") as f:
first_line = True first_line = True
@ -75,6 +85,9 @@ class Command(BaseCommand):
@transaction.atomic @transaction.atomic
def import_teams(self): def import_teams(self):
"""
Import teams into new database.
"""
self.stdout.write("Importing teams...") self.stdout.write("Importing teams...")
with open("import_olddb/teams.csv") as f: with open("import_olddb/teams.csv") as f:
first_line = True first_line = True
@ -120,6 +133,10 @@ class Command(BaseCommand):
@transaction.atomic @transaction.atomic
def import_users(self): def import_users(self):
"""
Import users into the new database.
:return:
"""
self.stdout.write("Importing users...") self.stdout.write("Importing users...")
with open("import_olddb/users.csv") as f: with open("import_olddb/users.csv") as f:
first_line = True first_line = True
@ -159,7 +176,7 @@ class Command(BaseCommand):
"team": Team.objects.get(pk=args[19]) if args[19] else None, "team": Team.objects.get(pk=args[19]) if args[19] else None,
"year": args[20], "year": args[20],
"date_joined": args[23], "date_joined": args[23],
"is_active": args[18] == "ADMIN", # TODO Replace it with "True" "is_active": args[18] == "ADMIN" or os.getenv("TFJM_STAGE", "dev") == "prod",
"is_staff": args[18] == "ADMIN", "is_staff": args[18] == "ADMIN",
"is_superuser": args[18] == "ADMIN", "is_superuser": args[18] == "ADMIN",
} }
@ -168,6 +185,7 @@ class Command(BaseCommand):
self.stdout.write(self.style.SUCCESS("Users imported")) self.stdout.write(self.style.SUCCESS("Users imported"))
self.stdout.write("Importing organizers...") self.stdout.write("Importing organizers...")
# We also import the information about the organizers of a tournament.
with open("import_olddb/organizers.csv") as f: with open("import_olddb/organizers.csv") as f:
first_line = True first_line = True
for line in f: for line in f:
@ -188,6 +206,9 @@ class Command(BaseCommand):
@transaction.atomic @transaction.atomic
def import_documents(self): def import_documents(self):
"""
Import the documents (authorizations, motivation letters, solutions, syntheses) from the old database.
"""
self.stdout.write("Importing documents...") self.stdout.write("Importing documents...")
with open("import_olddb/documents.csv") as f: with open("import_olddb/documents.csv") as f:
first_line = True first_line = True

View File

@ -10,12 +10,16 @@ from tournament.models import Team, Tournament
class TFJMUser(AbstractUser): class TFJMUser(AbstractUser):
"""
The model of registered users (organizers/juries/admins/coachs/participants)
"""
USERNAME_FIELD = 'email' USERNAME_FIELD = 'email'
REQUIRED_FIELDS = [] REQUIRED_FIELDS = []
email = models.EmailField( email = models.EmailField(
unique=True, unique=True,
verbose_name=_("email"), verbose_name=_("email"),
help_text=_("This should be valid and will be controlled."),
) )
team = models.ForeignKey( team = models.ForeignKey(
@ -24,6 +28,7 @@ class TFJMUser(AbstractUser):
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
related_name="users", related_name="users",
verbose_name=_("team"), verbose_name=_("team"),
help_text=_("Concerns only coaches and participants."),
) )
birth_date = models.DateField( birth_date = models.DateField(
@ -141,14 +146,25 @@ class TFJMUser(AbstractUser):
@property @property
def participates(self): def participates(self):
"""
Return True iff this user is a participant or a coach, ie. if the user is a member of a team that worked
for the tournament.
"""
return self.role == "3participant" or self.role == "2coach" return self.role == "3participant" or self.role == "2coach"
@property @property
def organizes(self): def organizes(self):
"""
Return True iff this user is a local or global organizer of the tournament. This includes juries.
"""
return self.role == "1volunteer" or self.role == "0admin" return self.role == "1volunteer" or self.role == "0admin"
@property @property
def admin(self): def admin(self):
"""
Return True iff this user is a global organizer, ie. an administrator. This should be equivalent to be
a superuser.
"""
return self.role == "0admin" return self.role == "0admin"
class Meta: class Meta:
@ -156,6 +172,7 @@ class TFJMUser(AbstractUser):
verbose_name_plural = _("users") verbose_name_plural = _("users")
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
# We ensure that the username is the email of the user.
self.username = self.email self.username = self.email
super().save(*args, **kwargs) super().save(*args, **kwargs)
@ -164,6 +181,9 @@ class TFJMUser(AbstractUser):
class Document(PolymorphicModel): class Document(PolymorphicModel):
"""
Abstract model of any saved document (solution, synthesis, motivation letter, authorization)
"""
file = models.FileField( file = models.FileField(
unique=True, unique=True,
verbose_name=_("file"), verbose_name=_("file"),
@ -184,6 +204,9 @@ class Document(PolymorphicModel):
class Authorization(Document): class Authorization(Document):
"""
Model for authorization papers (parental consent, photo consent, sanitary plug, ...)
"""
user = models.ForeignKey( user = models.ForeignKey(
TFJMUser, TFJMUser,
on_delete=models.CASCADE, on_delete=models.CASCADE,
@ -211,6 +234,9 @@ class Authorization(Document):
class MotivationLetter(Document): class MotivationLetter(Document):
"""
Model for motivation letters of a team.
"""
team = models.ForeignKey( team = models.ForeignKey(
Team, Team,
on_delete=models.CASCADE, on_delete=models.CASCADE,
@ -227,6 +253,9 @@ class MotivationLetter(Document):
class Solution(Document): class Solution(Document):
"""
Model for solutions of team for a given problem, for the regional or final tournament.
"""
team = models.ForeignKey( team = models.ForeignKey(
Team, Team,
on_delete=models.CASCADE, on_delete=models.CASCADE,
@ -245,6 +274,11 @@ class Solution(Document):
@property @property
def tournament(self): def tournament(self):
"""
Get the concerned tournament of a solution.
Generally the local tournament of a team, but it can be the final tournament if this is a solution for the
final tournament.
"""
return Tournament.get_final() if self.final else self.team.tournament return Tournament.get_final() if self.final else self.team.tournament
class Meta: class Meta:
@ -258,6 +292,9 @@ class Solution(Document):
class Synthesis(Document): class Synthesis(Document):
"""
Model for syntheses of a team for a given round and for a given role, for the regional or final tournament.
"""
team = models.ForeignKey( team = models.ForeignKey(
Team, Team,
on_delete=models.CASCADE, on_delete=models.CASCADE,
@ -289,6 +326,11 @@ class Synthesis(Document):
@property @property
def tournament(self): def tournament(self):
"""
Get the concerned tournament of a solution.
Generally the local tournament of a team, but it can be the final tournament if this is a solution for the
final tournament.
"""
return Tournament.get_final() if self.final else self.team.tournament return Tournament.get_final() if self.final else self.team.tournament
class Meta: class Meta:
@ -303,6 +345,9 @@ class Synthesis(Document):
class Config(models.Model): class Config(models.Model):
"""
Dictionary of configuration variables.
"""
key = models.CharField( key = models.CharField(
max_length=255, max_length=255,
primary_key=True, primary_key=True,

View File

@ -1,10 +1,13 @@
import django_tables2 as tables import django_tables2 as tables
from django_tables2 import A from django_tables2 import A
from member.models import TFJMUser from .models import TFJMUser
class UserTable(tables.Table): class UserTable(tables.Table):
"""
Table of users that are matched with a given queryset.
"""
last_name = tables.LinkColumn( last_name = tables.LinkColumn(
"member:information", "member:information",
args=[A("pk")], args=[A("pk")],

View File

@ -6,11 +6,17 @@ from member.models import Config
def get_config(value): def get_config(value):
"""
Return a value stored into the config table in the database with a given key.
"""
config = Config.objects.get_or_create(key=value)[0] config = Config.objects.get_or_create(key=value)[0]
return config.value return config.value
def get_env(value): def get_env(value):
"""
Get a specified environment variable.
"""
return os.getenv(value) return os.getenv(value)

View File

@ -12,26 +12,33 @@ from django.utils.translation import gettext_lazy as _
from django.views import View from django.views import View
from django.views.generic import CreateView, UpdateView, DetailView, FormView from django.views.generic import CreateView, UpdateView, DetailView, FormView
from django_tables2 import SingleTableView from django_tables2 import SingleTableView
from tournament.forms import TeamForm, JoinTeam from tournament.forms import TeamForm, JoinTeam
from tournament.models import Team from tournament.models import Team
from tournament.views import AdminMixin, TeamMixin from tournament.views import AdminMixin, TeamMixin
from .forms import SignUpForm, TFJMUserForm, AdminUserForm, CoachUserForm from .forms import SignUpForm, TFJMUserForm, AdminUserForm, CoachUserForm
from .models import TFJMUser, Document, Solution, MotivationLetter, Synthesis from .models import TFJMUser, Document, Solution, MotivationLetter, Synthesis
from .tables import UserTable from .tables import UserTable
class CreateUserView(CreateView): class CreateUserView(CreateView):
"""
Signup form view.
"""
model = TFJMUser model = TFJMUser
form_class = SignUpForm form_class = SignUpForm
template_name = "registration/signup.html" template_name = "registration/signup.html"
class MyAccountView(LoginRequiredMixin, UpdateView): class MyAccountView(LoginRequiredMixin, UpdateView):
"""
Update our personal data.
"""
model = TFJMUser model = TFJMUser
template_name = "member/my_account.html" template_name = "member/my_account.html"
def get_form_class(self): def get_form_class(self):
# The used form can change according to the role of the user.
return AdminUserForm if self.request.user.organizes else TFJMUserForm \ return AdminUserForm if self.request.user.organizes else TFJMUserForm \
if self.request.user.role == "3participant" else CoachUserForm if self.request.user.role == "3participant" else CoachUserForm
@ -40,6 +47,10 @@ class MyAccountView(LoginRequiredMixin, UpdateView):
class UserDetailView(LoginRequiredMixin, DetailView): class UserDetailView(LoginRequiredMixin, DetailView):
"""
View the personal information of a given user.
Only organizers can see this page, since there are personal data.
"""
model = TFJMUser model = TFJMUser
form_class = TFJMUserForm form_class = TFJMUserForm
context_object_name = "tfjmuser" context_object_name = "tfjmuser"
@ -57,7 +68,10 @@ class UserDetailView(LoginRequiredMixin, DetailView):
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
if "view_as" in request.POST: """
An administrator can log in through this page as someone else, and act as this other person.
"""
if "view_as" in request.POST and self.request.admin:
session = request.session session = request.session
session["admin"] = request.user.pk session["admin"] = request.user.pk
obj = self.get_object() obj = self.get_object()
@ -74,6 +88,10 @@ class UserDetailView(LoginRequiredMixin, DetailView):
class AddTeamView(LoginRequiredMixin, CreateView): class AddTeamView(LoginRequiredMixin, CreateView):
"""
Register a new team.
Users can choose the name, the trigram and a preferred tournament.
"""
model = Team model = Team
form_class = TeamForm form_class = TeamForm
@ -86,6 +104,7 @@ class AddTeamView(LoginRequiredMixin, CreateView):
form.add_error('name', _("You are already in a team.")) form.add_error('name', _("You are already in a team."))
return self.form_invalid(form) return self.form_invalid(form)
# Generate a random access code
team = form.instance team = form.instance
alphabet = "0123456789abcdefghijklmnopqrstuvwxyz0123456789" alphabet = "0123456789abcdefghijklmnopqrstuvwxyz0123456789"
code = "" code = ""
@ -107,6 +126,9 @@ class AddTeamView(LoginRequiredMixin, CreateView):
class JoinTeamView(LoginRequiredMixin, FormView): class JoinTeamView(LoginRequiredMixin, FormView):
"""
Join a team with a given access code.
"""
model = Team model = Team
form_class = JoinTeam form_class = JoinTeam
template_name = "tournament/team_form.html" template_name = "tournament/team_form.html"
@ -122,7 +144,7 @@ class JoinTeamView(LoginRequiredMixin, FormView):
form.add_error('access_code', _("You are already in a team.")) form.add_error('access_code', _("You are already in a team."))
return self.form_invalid(form) return self.form_invalid(form)
if self.request.user.role == '2coach' and len(team.encadrants) == 3: if self.request.user.role == '2coach' and len(team.coaches) == 3:
form.add_error('access_code', _("This team is full of coachs.")) form.add_error('access_code', _("This team is full of coachs."))
return self.form_invalid(form) return self.form_invalid(form)
@ -130,6 +152,9 @@ class JoinTeamView(LoginRequiredMixin, FormView):
form.add_error('access_code', _("This team is full of participants.")) form.add_error('access_code', _("This team is full of participants."))
return self.form_invalid(form) return self.form_invalid(form)
if not team.invalid:
form.add_error('access_code', _("This team is already validated or waiting for validation."))
self.request.user.team = team self.request.user.team = team
self.request.user.save() self.request.user.save()
return super().form_valid(form) return super().form_valid(form)
@ -139,11 +164,24 @@ class JoinTeamView(LoginRequiredMixin, FormView):
class MyTeamView(TeamMixin, View): class MyTeamView(TeamMixin, View):
"""
Redirect to the page of the information of our personal team.
"""
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
return redirect("tournament:team_detail", pk=request.user.team.pk) return redirect("tournament:team_detail", pk=request.user.team.pk)
class DocumentView(LoginRequiredMixin, View): class DocumentView(LoginRequiredMixin, View):
"""
View a PDF document, if we have the right.
- Everyone can see the documents that concern itself.
- An administrator can see anything.
- An organizer can see documents that are related to its tournament.
- A jury can see solutions and syntheses that are evaluated in their pools.
"""
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
doc = Document.objects.get(file=self.kwargs["file"]) doc = Document.objects.get(file=self.kwargs["file"])
@ -172,6 +210,9 @@ class DocumentView(LoginRequiredMixin, View):
class ProfileListView(AdminMixin, SingleTableView): class ProfileListView(AdminMixin, SingleTableView):
"""
List all registered profiles.
"""
model = TFJMUser model = TFJMUser
queryset = TFJMUser.objects.order_by("role", "last_name", "first_name") queryset = TFJMUser.objects.order_by("role", "last_name", "first_name")
table_class = UserTable table_class = UserTable
@ -180,6 +221,9 @@ class ProfileListView(AdminMixin, SingleTableView):
class OrphanedProfileListView(AdminMixin, SingleTableView): class OrphanedProfileListView(AdminMixin, SingleTableView):
"""
List all orphaned profiles, ie. participants that have no team.
"""
model = TFJMUser model = TFJMUser
queryset = TFJMUser.objects.filter((Q(role="2coach") | Q(role="3participant")) & Q(team__isnull=True))\ queryset = TFJMUser.objects.filter((Q(role="2coach") | Q(role="3participant")) & Q(team__isnull=True))\
.order_by("role", "last_name", "first_name") .order_by("role", "last_name", "first_name")
@ -189,6 +233,9 @@ class OrphanedProfileListView(AdminMixin, SingleTableView):
class OrganizersListView(AdminMixin, SingleTableView): class OrganizersListView(AdminMixin, SingleTableView):
"""
List all organizers.
"""
model = TFJMUser model = TFJMUser
queryset = TFJMUser.objects.filter(Q(role="0admin") | Q(role="1volunteer"))\ queryset = TFJMUser.objects.filter(Q(role="0admin") | Q(role="1volunteer"))\
.order_by("role", "last_name", "first_name") .order_by("role", "last_name", "first_name")
@ -198,6 +245,10 @@ class OrganizersListView(AdminMixin, SingleTableView):
class ResetAdminView(AdminMixin, View): class ResetAdminView(AdminMixin, View):
"""
Return to admin view, clear the session field that let an administrator to log in as someone else.
"""
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
if "_fake_user_id" in request.session: if "_fake_user_id" in request.session:
del request.session["_fake_user_id"] del request.session["_fake_user_id"]

View File

@ -1,23 +1,31 @@
from django.contrib.auth.admin import admin 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) @admin.register(Team)
class TeamAdmin(admin.ModelAdmin): class TeamAdmin(admin.ModelAdmin):
pass """
Django admin page for teams.
"""
@admin.register(Tournament) @admin.register(Tournament)
class TournamentAdmin(admin.ModelAdmin): class TournamentAdmin(admin.ModelAdmin):
pass """
Django admin page for tournaments.
"""
@admin.register(Pool) @admin.register(Pool)
class PoolAdmin(admin.ModelAdmin): class PoolAdmin(admin.ModelAdmin):
pass """
Django admin page for pools.
"""
@admin.register(Payment) @admin.register(Payment)
class PaymentAdmin(admin.ModelAdmin): 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): class TournamentConfig(AppConfig):
"""
The tournament app handles all that is related to the tournaments.
"""
name = 'tournament' name = 'tournament'
verbose_name = _('tournament') verbose_name = _('tournament')

View File

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

View File

@ -9,6 +9,10 @@ from django.utils.translation import gettext_lazy as _
class Tournament(models.Model): class Tournament(models.Model):
"""
Store the information of a tournament.
"""
name = models.CharField( name = models.CharField(
max_length=255, max_length=255,
verbose_name=_("name"), verbose_name=_("name"),
@ -18,10 +22,12 @@ class Tournament(models.Model):
'member.TFJMUser', 'member.TFJMUser',
related_name="organized_tournaments", related_name="organized_tournaments",
verbose_name=_("organizers"), verbose_name=_("organizers"),
help_text=_("List of all organizers that can see and manipulate data of the tournament and the teams."),
) )
size = models.PositiveSmallIntegerField( size = models.PositiveSmallIntegerField(
verbose_name=_("size"), verbose_name=_("size"),
help_text=_("Number of teams that are allowed to join the tournament."),
) )
place = models.CharField( place = models.CharField(
@ -31,6 +37,7 @@ class Tournament(models.Model):
price = models.PositiveSmallIntegerField( price = models.PositiveSmallIntegerField(
verbose_name=_("price"), verbose_name=_("price"),
help_text=_("Price asked to participants. Free with a scholarship."),
) )
description = models.TextField( description = models.TextField(
@ -74,6 +81,7 @@ class Tournament(models.Model):
final = models.BooleanField( final = models.BooleanField(
verbose_name=_("final tournament"), verbose_name=_("final tournament"),
help_text=_("It should be only one final tournament."),
) )
year = models.PositiveIntegerField( year = models.PositiveIntegerField(
@ -83,27 +91,43 @@ class Tournament(models.Model):
@property @property
def teams(self): 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) return self._teams if not self.final else Team.objects.filter(selected_for_final=True)
@property @property
def linked_organizers(self): 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>' return ['<a href="{url}">'.format(url=reverse_lazy("member:information", args=(user.pk,))) + str(user) + '</a>'
for user in self.organizers.all()] for user in self.organizers.all()]
@property @property
def solutions(self): def solutions(self):
"""
Get all sent solutions for this tournament.
"""
from member.models import Solution from member.models import Solution
return Solution.objects.filter(final=self.final) if self.final \ 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 @property
def syntheses(self): def syntheses(self):
"""
Get all sent syntheses for this tournament.
"""
from member.models import Synthesis from member.models import Synthesis
return Synthesis.objects.filter(final=self.final) if self.final \ 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 @classmethod
def get_final(cls): 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) return cls.objects.get(year=os.getenv("TFJM_YEAR"), final=True)
class Meta: class Meta:
@ -111,6 +135,12 @@ class Tournament(models.Model):
verbose_name_plural = _("tournaments") verbose_name_plural = _("tournaments")
def send_mail_to_organizers(self, template_name, subject="Contact TFJM²", **kwargs): 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 = kwargs
context["tournament"] = self context["tournament"] = self
for user in self.organizers.all(): for user in self.organizers.all():
@ -130,6 +160,10 @@ class Tournament(models.Model):
class Team(models.Model): class Team(models.Model):
"""
Store information about a registered team.
"""
name = models.CharField( name = models.CharField(
max_length=255, max_length=255,
verbose_name=_("name"), verbose_name=_("name"),
@ -138,6 +172,7 @@ class Team(models.Model):
trigram = models.CharField( trigram = models.CharField(
max_length=3, max_length=3,
verbose_name=_("trigram"), 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( tournament = models.ForeignKey(
@ -145,6 +180,7 @@ class Team(models.Model):
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name="_teams", related_name="_teams",
verbose_name=_("tournament"), verbose_name=_("tournament"),
help_text=_("The tournament where the team is registered."),
) )
inscription_date = models.DateTimeField( inscription_date = models.DateTimeField(
@ -191,31 +227,59 @@ class Team(models.Model):
return self.validation_status == "0invalid" return self.validation_status == "0invalid"
@property @property
def encadrants(self): def coaches(self):
"""
Get all coaches of a team.
"""
return self.users.all().filter(role="2coach") return self.users.all().filter(role="2coach")
@property @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>' 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 @property
def participants(self): def participants(self):
"""
Get all particpants of a team, coaches excluded.
"""
return self.users.all().filter(role="3participant") return self.users.all().filter(role="3participant")
@property @property
def linked_participants(self): 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>' return ['<a href="{url}">'.format(url=reverse_lazy("member:information", args=(user.pk,))) + str(user) + '</a>'
for user in self.participants] for user in self.participants]
@property @property
def future_tournament(self): 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 return Tournament.get_final() if self.selected_for_final else self.tournament
@property @property
def can_validate(self): 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. # 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: class Meta:
verbose_name = _("team") verbose_name = _("team")
@ -223,6 +287,12 @@ class Team(models.Model):
unique_together = (('name', 'year',), ('trigram', 'year',),) unique_together = (('name', 'year',), ('trigram', 'year',),)
def send_mail(self, template_name, subject="Contact TFJM²", **kwargs): 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 = kwargs
context["team"] = self context["team"] = self
for user in self.users.all(): for user in self.users.all():
@ -236,6 +306,12 @@ class Team(models.Model):
class Pool(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( teams = models.ManyToManyField(
Team, Team,
related_name="pools", related_name="pools",
@ -264,14 +340,24 @@ class Pool(models.Model):
@property @property
def problems(self): 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()) return list(d["problem"] for d in self.solutions.values("problem").all())
@property @property
def tournament(self): 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 return self.solutions.first().tournament
@property @property
def syntheses(self): def syntheses(self):
"""
Get the syntheses of the teams that are in this pool, for the correct round.
"""
from member.models import Synthesis from member.models import Synthesis
return Synthesis.objects.filter(team__in=self.teams.all(), round=self.round, final=self.tournament.final) 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): class Payment(models.Model):
"""
Store some information about payments, to recover data.
TODO: handle it...
"""
user = models.OneToOneField( user = models.OneToOneField(
'member.TFJMUser', 'member.TFJMUser',
on_delete=models.CASCADE, on_delete=models.CASCADE,

View File

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

View File

@ -24,6 +24,10 @@ from .tables import TournamentTable, TeamTable, SolutionTable, SynthesisTable, P
class AdminMixin(LoginRequiredMixin): class AdminMixin(LoginRequiredMixin):
"""
If a view extends this mixin, then the view will be only accessible to administrators.
"""
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated or not request.user.admin: if not request.user.is_authenticated or not request.user.admin:
raise PermissionDenied raise PermissionDenied
@ -31,6 +35,10 @@ class AdminMixin(LoginRequiredMixin):
class OrgaMixin(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): def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated or not request.user.organizes: if not request.user.is_authenticated or not request.user.organizes:
raise PermissionDenied raise PermissionDenied
@ -38,6 +46,10 @@ class OrgaMixin(LoginRequiredMixin):
class TeamMixin(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): def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated or not request.user.team: if not request.user.is_authenticated or not request.user.team:
raise PermissionDenied raise PermissionDenied
@ -45,6 +57,10 @@ class TeamMixin(LoginRequiredMixin):
class TournamentListView(SingleTableView): class TournamentListView(SingleTableView):
"""
Display the list of all tournaments, ordered by start date then name.
"""
model = Tournament model = Tournament
table_class = TournamentTable table_class = TournamentTable
extra_context = dict(title=_("Tournaments list"),) extra_context = dict(title=_("Tournaments list"),)
@ -64,6 +80,10 @@ class TournamentListView(SingleTableView):
class TournamentCreateView(AdminMixin, CreateView): class TournamentCreateView(AdminMixin, CreateView):
"""
Create a tournament. Only accessible to admins.
"""
model = Tournament model = Tournament
form_class = TournamentForm form_class = TournamentForm
extra_context = dict(title=_("Add tournament"),) extra_context = dict(title=_("Add tournament"),)
@ -73,6 +93,11 @@ class TournamentCreateView(AdminMixin, CreateView):
class TournamentDetailView(DetailView): class TournamentDetailView(DetailView):
"""
Display the detail of a tournament.
Accessible to all, including not authenticated users.
"""
model = Tournament model = Tournament
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
@ -96,7 +121,20 @@ class TournamentDetailView(DetailView):
return context 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 model = Tournament
form_class = TournamentForm form_class = TournamentForm
extra_context = dict(title=_("Update tournament"),) extra_context = dict(title=_("Update tournament"),)
@ -106,9 +144,16 @@ class TournamentUpdateView(AdminMixin, UpdateView):
class TeamDetailView(LoginRequiredMixin, DetailView): class TeamDetailView(LoginRequiredMixin, DetailView):
"""
View the detail of a team.
Restricted to this team, admins and organizers of its tournament.
"""
model = Team model = Team
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
"""
Protect the page and process the request.
"""
if not request.user.is_authenticated or \ if not request.user.is_authenticated or \
(not request.user.admin and self.request.user not in self.get_object().tournament.organizers.all() (not request.user.admin and self.request.user not in self.get_object().tournament.organizers.all()
and self.get_object() != request.user.team): and self.get_object() != request.user.team):
@ -116,7 +161,15 @@ class TeamDetailView(LoginRequiredMixin, DetailView):
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
def post(self, 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() team = self.get_object()
if "zip" in request.POST: if "zip" in request.POST:
solutions = team.solutions.all() solutions = team.solutions.all()
@ -140,7 +193,7 @@ class TeamDetailView(LoginRequiredMixin, DetailView):
if not team.users.exists(): if not team.users.exists():
team.delete() team.delete()
return redirect('tournament:detail', pk=team.tournament.pk) 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.validation_status = "1waiting"
team.save() team.save()
team.tournament.send_mail_to_organizers("request_validation", "Demande de validation TFJM²", team=team) team.tournament.send_mail_to_organizers("request_validation", "Demande de validation TFJM²", team=team)
@ -159,6 +212,7 @@ class TeamDetailView(LoginRequiredMixin, DetailView):
team.delete() team.delete()
return redirect('tournament:detail', pk=team.tournament.pk) 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: 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(): for solution in team.solutions.all():
alphabet = "0123456789abcdefghijklmnopqrstuvwxyz0123456789" alphabet = "0123456789abcdefghijklmnopqrstuvwxyz0123456789"
id = "" id = ""
@ -194,6 +248,11 @@ class TeamDetailView(LoginRequiredMixin, DetailView):
class TeamUpdateView(LoginRequiredMixin, UpdateView): class TeamUpdateView(LoginRequiredMixin, UpdateView):
"""
Update the information about a team.
Team members, admins and organizers are allowed to do this.
"""
model = Team model = Team
form_class = TeamForm form_class = TeamForm
extra_context = dict(title=_("Update team"),) extra_context = dict(title=_("Update team"),)
@ -206,6 +265,12 @@ class TeamUpdateView(LoginRequiredMixin, UpdateView):
class AddOrganizerView(AdminMixin, CreateView): 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 model = TFJMUser
form_class = OrganizerForm form_class = OrganizerForm
extra_context = dict(title=_("Add organizer"),) extra_context = dict(title=_("Add organizer"),)
@ -223,6 +288,10 @@ class AddOrganizerView(AdminMixin, CreateView):
class SolutionsView(TeamMixin, BaseFormView, SingleTableView): class SolutionsView(TeamMixin, BaseFormView, SingleTableView):
"""
Upload and view solutions for a team.
"""
model = Solution model = Solution
table_class = SolutionTable table_class = SolutionTable
form_class = SolutionForm form_class = SolutionForm
@ -288,6 +357,11 @@ class SolutionsView(TeamMixin, BaseFormView, SingleTableView):
class SolutionsOrgaListView(OrgaMixin, 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 model = Solution
table_class = SolutionTable table_class = SolutionTable
template_name = "tournament/solutions_orga_list.html" template_name = "tournament/solutions_orga_list.html"
@ -333,6 +407,9 @@ class SolutionsOrgaListView(OrgaMixin, SingleTableView):
class SynthesesView(TeamMixin, BaseFormView, SingleTableView): class SynthesesView(TeamMixin, BaseFormView, SingleTableView):
"""
Upload and view syntheses for a team.
"""
model = Synthesis model = Synthesis
table_class = SynthesisTable table_class = SynthesisTable
form_class = SynthesisForm form_class = SynthesisForm
@ -407,6 +484,10 @@ class SynthesesView(TeamMixin, BaseFormView, SingleTableView):
class SynthesesOrgaListView(OrgaMixin, 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 model = Synthesis
table_class = SynthesisTable table_class = SynthesisTable
template_name = "tournament/syntheses_orga_list.html" template_name = "tournament/syntheses_orga_list.html"
@ -452,6 +533,10 @@ class SynthesesOrgaListView(OrgaMixin, SingleTableView):
class PoolListView(LoginRequiredMixin, 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 model = Pool
table_class = PoolTable table_class = PoolTable
extra_context = dict(title=_("Pools")) extra_context = dict(title=_("Pools"))
@ -469,6 +554,10 @@ class PoolListView(LoginRequiredMixin, SingleTableView):
class PoolCreateView(AdminMixin, CreateView): class PoolCreateView(AdminMixin, CreateView):
"""
Create a pool manually.
This page should not be used: prefer send automatically data from the drawing bot.
"""
model = Pool model = Pool
form_class = PoolForm form_class = PoolForm
extra_context = dict(title=_("Create pool")) extra_context = dict(title=_("Create pool"))
@ -478,6 +567,15 @@ class PoolCreateView(AdminMixin, CreateView):
class PoolDetailView(LoginRequiredMixin, DetailView): 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 model = Pool
extra_context = dict(title=_("Pool detail")) extra_context = dict(title=_("Pool detail"))

View File

@ -34,10 +34,6 @@ msgstr "Choisir un rôle ..."
msgid "Participant" msgid "Participant"
msgstr "Participant" msgstr "Participant"
#: apps/member/forms.py:16
msgid "Encadrant"
msgstr "Encadrant"
#: apps/member/models.py:18 templates/member/tfjmuser_detail.html:35 #: apps/member/models.py:18 templates/member/tfjmuser_detail.html:35
msgid "email" msgid "email"
msgstr "Adresse électronique" msgstr "Adresse électronique"

View File

@ -23,7 +23,7 @@
href="{% url "tournament:detail" pk=team.tournament.pk %}">{{ team.tournament }}</a></dd> href="{% url "tournament:detail" pk=team.tournament.pk %}">{{ team.tournament }}</a></dd>
<dt class="col-xl-6 text-right">{% trans 'coachs'|capfirst %}</dt> <dt class="col-xl-6 text-right">{% trans 'coachs'|capfirst %}</dt>
<dd class="col-xl-6">{% autoescape off %}{{ team.linked_encadrants|join:", " }}{% endautoescape %}</dd> <dd class="col-xl-6">{% autoescape off %}{{ team.linked_coaches|join:", " }}{% endautoescape %}</dd>
<dt class="col-xl-6 text-right">{% trans 'participants'|capfirst %}</dt> <dt class="col-xl-6 text-right">{% trans 'participants'|capfirst %}</dt>
<dd class="col-xl-6"> <dd class="col-xl-6">

View File

@ -181,3 +181,6 @@ if os.getenv("TFJM_STAGE", "dev") == "prod":
from .settings_prod import * from .settings_prod import *
else: else:
from .settings_dev import * from .settings_dev import *
INSTALLED_APPS += [
"django_extensions"
]