diff --git a/apps/api/apps.py b/apps/api/apps.py
index fb08f6a..6e03468 100644
--- a/apps/api/apps.py
+++ b/apps/api/apps.py
@@ -3,5 +3,8 @@ from django.utils.translation import gettext_lazy as _
class APIConfig(AppConfig):
+ """
+ Manage the inscription through a JSON API.
+ """
name = 'api'
verbose_name = _('API')
diff --git a/apps/api/serializers.py b/apps/api/serializers.py
new file mode 100644
index 0000000..1685020
--- /dev/null
+++ b/apps/api/serializers.py
@@ -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__"
diff --git a/apps/api/urls.py b/apps/api/urls.py
index b8f5c3e..b2e617f 100644
--- a/apps/api/urls.py
+++ b/apps/api/urls.py
@@ -1,152 +1,8 @@
from django.conf.urls import url, include
-from django_filters.rest_framework import DjangoFilterBackend
-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 rest_framework import routers
+from .viewsets import UserViewSet, TeamViewSet, TournamentViewSet, AuthorizationViewSet, MotivationLetterViewSet, \
+ SolutionViewSet, SynthesisViewSet, PoolViewSet
# Routers provide an easy way of automatically determining the URL conf.
# Register each app API router and user viewset
diff --git a/apps/api/viewsets.py b/apps/api/viewsets.py
new file mode 100644
index 0000000..785e446
--- /dev/null
+++ b/apps/api/viewsets.py
@@ -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)
\ No newline at end of file
diff --git a/apps/member/admin.py b/apps/member/admin.py
index f275084..bbed356 100644
--- a/apps/member/admin.py
+++ b/apps/member/admin.py
@@ -5,35 +5,51 @@ from member.models import TFJMUser, Document, Solution, Synthesis, MotivationLet
@admin.register(TFJMUser)
class TFJMUserAdmin(UserAdmin):
+ """
+ Django admin page for users.
+ """
list_display = ('email', 'first_name', 'last_name', 'role', )
@admin.register(Document)
class DocumentAdmin(PolymorphicParentModelAdmin):
+ """
+ Django admin page for any documents.
+ """
child_models = (Authorization, MotivationLetter, Solution, Synthesis,)
polymorphic_list = True
@admin.register(Authorization)
class AuthorizationAdmin(PolymorphicChildModelAdmin):
- pass
+ """
+ Django admin page for Authorization.
+ """
@admin.register(MotivationLetter)
class MotivationLetterAdmin(PolymorphicChildModelAdmin):
- pass
+ """
+ Django admin page for Motivation letters.
+ """
@admin.register(Solution)
class SolutionAdmin(PolymorphicChildModelAdmin):
- pass
+ """
+ Django admin page for solutions.
+ """
@admin.register(Synthesis)
class SynthesisAdmin(PolymorphicChildModelAdmin):
- pass
+ """
+ Django admin page for syntheses.
+ """
@admin.register(Config)
class ConfigAdmin(admin.ModelAdmin):
- pass
+ """
+ Django admin page for configurations.
+ """
diff --git a/apps/member/apps.py b/apps/member/apps.py
index 6635e7e..61c9ae8 100644
--- a/apps/member/apps.py
+++ b/apps/member/apps.py
@@ -3,5 +3,8 @@ from django.utils.translation import gettext_lazy as _
class MemberConfig(AppConfig):
+ """
+ The member app handles the information that concern a user, its documents, ...
+ """
name = 'member'
verbose_name = _('member')
diff --git a/apps/member/forms.py b/apps/member/forms.py
index 9cbfde3..083b7b4 100644
--- a/apps/member/forms.py
+++ b/apps/member/forms.py
@@ -2,18 +2,22 @@ from django.contrib.auth.forms import UserCreationForm
from django import forms
from django.utils.translation import gettext_lazy as _
-from member.models import TFJMUser
+from .models import TFJMUser
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):
super().__init__(*args, **kwargs)
self.fields["first_name"].required = True
self.fields["last_name"].required = True
self.fields["role"].choices = [
('', _("Choose a role...")),
- ('participant', _("Participant")),
- ('encadrant', _("Encadrant")),
+ ('3participant', _("Participant")),
+ ('2coach', _("Coach")),
]
class Meta:
@@ -40,6 +44,9 @@ class SignUpForm(UserCreationForm):
class TFJMUserForm(forms.ModelForm):
+ """
+ Form to update our own information when we are participant.
+ """
class Meta:
model = TFJMUser
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):
+ """
+ Form to update our own information when we are coach.
+ """
class Meta:
model = TFJMUser
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):
+ """
+ Form to update our own information when we are organizer or admin.
+ """
class Meta:
model = TFJMUser
fields = ('last_name', 'first_name', 'email', 'phone_number', 'description',)
diff --git a/apps/member/management/commands/create_su.py b/apps/member/management/commands/create_su.py
index a3fac35..93ec091 100644
--- a/apps/member/management/commands/create_su.py
+++ b/apps/member/management/commands/create_su.py
@@ -7,6 +7,9 @@ from member.models import TFJMUser
class Command(BaseCommand):
def handle(self, *args, **options):
+ """
+ Little script that generate a superuser.
+ """
email = input("Email: ")
password = "1"
confirm_password = "2"
diff --git a/apps/member/management/commands/import_olddb.py b/apps/member/management/commands/import_olddb.py
index bc6cf25..d3ff94f 100644
--- a/apps/member/management/commands/import_olddb.py
+++ b/apps/member/management/commands/import_olddb.py
@@ -1,3 +1,5 @@
+import os
+
from django.core.management import BaseCommand, CommandError
from django.db import transaction
from member.models import TFJMUser, Document, Solution, Synthesis, Authorization, MotivationLetter
@@ -5,6 +7,11 @@ from tournament.models import Team, Tournament
class Command(BaseCommand):
+ """
+ Import the old database.
+ Tables must be found into the import_olddb folder, as CSV files.
+ """
+
def add_arguments(self, parser):
parser.add_argument('--tournaments', '-t', action="store", help="Import tournaments")
parser.add_argument('--teams', '-T', action="store", help="Import teams")
@@ -26,6 +33,9 @@ class Command(BaseCommand):
@transaction.atomic
def import_tournaments(self):
+ """
+ Import tournaments into the new database.
+ """
print("Importing tournaments...")
with open("import_olddb/tournaments.csv") as f:
first_line = True
@@ -75,6 +85,9 @@ class Command(BaseCommand):
@transaction.atomic
def import_teams(self):
+ """
+ Import teams into new database.
+ """
self.stdout.write("Importing teams...")
with open("import_olddb/teams.csv") as f:
first_line = True
@@ -120,6 +133,10 @@ class Command(BaseCommand):
@transaction.atomic
def import_users(self):
+ """
+ Import users into the new database.
+ :return:
+ """
self.stdout.write("Importing users...")
with open("import_olddb/users.csv") as f:
first_line = True
@@ -159,7 +176,7 @@ class Command(BaseCommand):
"team": Team.objects.get(pk=args[19]) if args[19] else None,
"year": args[20],
"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_superuser": args[18] == "ADMIN",
}
@@ -168,6 +185,7 @@ class Command(BaseCommand):
self.stdout.write(self.style.SUCCESS("Users imported"))
self.stdout.write("Importing organizers...")
+ # We also import the information about the organizers of a tournament.
with open("import_olddb/organizers.csv") as f:
first_line = True
for line in f:
@@ -188,6 +206,9 @@ class Command(BaseCommand):
@transaction.atomic
def import_documents(self):
+ """
+ Import the documents (authorizations, motivation letters, solutions, syntheses) from the old database.
+ """
self.stdout.write("Importing documents...")
with open("import_olddb/documents.csv") as f:
first_line = True
diff --git a/apps/member/models.py b/apps/member/models.py
index aa2f1a9..b05d6a2 100644
--- a/apps/member/models.py
+++ b/apps/member/models.py
@@ -10,12 +10,16 @@ from tournament.models import Team, Tournament
class TFJMUser(AbstractUser):
+ """
+ The model of registered users (organizers/juries/admins/coachs/participants)
+ """
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = []
email = models.EmailField(
unique=True,
verbose_name=_("email"),
+ help_text=_("This should be valid and will be controlled."),
)
team = models.ForeignKey(
@@ -24,6 +28,7 @@ class TFJMUser(AbstractUser):
on_delete=models.SET_NULL,
related_name="users",
verbose_name=_("team"),
+ help_text=_("Concerns only coaches and participants."),
)
birth_date = models.DateField(
@@ -141,14 +146,25 @@ class TFJMUser(AbstractUser):
@property
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"
@property
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"
@property
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"
class Meta:
@@ -156,6 +172,7 @@ class TFJMUser(AbstractUser):
verbose_name_plural = _("users")
def save(self, *args, **kwargs):
+ # We ensure that the username is the email of the user.
self.username = self.email
super().save(*args, **kwargs)
@@ -164,6 +181,9 @@ class TFJMUser(AbstractUser):
class Document(PolymorphicModel):
+ """
+ Abstract model of any saved document (solution, synthesis, motivation letter, authorization)
+ """
file = models.FileField(
unique=True,
verbose_name=_("file"),
@@ -184,6 +204,9 @@ class Document(PolymorphicModel):
class Authorization(Document):
+ """
+ Model for authorization papers (parental consent, photo consent, sanitary plug, ...)
+ """
user = models.ForeignKey(
TFJMUser,
on_delete=models.CASCADE,
@@ -211,6 +234,9 @@ class Authorization(Document):
class MotivationLetter(Document):
+ """
+ Model for motivation letters of a team.
+ """
team = models.ForeignKey(
Team,
on_delete=models.CASCADE,
@@ -227,6 +253,9 @@ class MotivationLetter(Document):
class Solution(Document):
+ """
+ Model for solutions of team for a given problem, for the regional or final tournament.
+ """
team = models.ForeignKey(
Team,
on_delete=models.CASCADE,
@@ -245,6 +274,11 @@ class Solution(Document):
@property
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
class Meta:
@@ -258,6 +292,9 @@ class Solution(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,
on_delete=models.CASCADE,
@@ -289,6 +326,11 @@ class Synthesis(Document):
@property
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
class Meta:
@@ -303,6 +345,9 @@ class Synthesis(Document):
class Config(models.Model):
+ """
+ Dictionary of configuration variables.
+ """
key = models.CharField(
max_length=255,
primary_key=True,
diff --git a/apps/member/tables.py b/apps/member/tables.py
index 6caad0d..779dc47 100644
--- a/apps/member/tables.py
+++ b/apps/member/tables.py
@@ -1,10 +1,13 @@
import django_tables2 as tables
from django_tables2 import A
-from member.models import TFJMUser
+from .models import TFJMUser
class UserTable(tables.Table):
+ """
+ Table of users that are matched with a given queryset.
+ """
last_name = tables.LinkColumn(
"member:information",
args=[A("pk")],
diff --git a/apps/member/templatetags/getconfig.py b/apps/member/templatetags/getconfig.py
index 46b3cfa..0c6d776 100644
--- a/apps/member/templatetags/getconfig.py
+++ b/apps/member/templatetags/getconfig.py
@@ -6,11 +6,17 @@ from member.models import Config
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]
return config.value
def get_env(value):
+ """
+ Get a specified environment variable.
+ """
return os.getenv(value)
diff --git a/apps/member/views.py b/apps/member/views.py
index a9b3913..6b2fbe4 100644
--- a/apps/member/views.py
+++ b/apps/member/views.py
@@ -12,26 +12,33 @@ from django.utils.translation import gettext_lazy as _
from django.views import View
from django.views.generic import CreateView, UpdateView, DetailView, FormView
from django_tables2 import SingleTableView
-
from tournament.forms import TeamForm, JoinTeam
from tournament.models import Team
from tournament.views import AdminMixin, TeamMixin
+
from .forms import SignUpForm, TFJMUserForm, AdminUserForm, CoachUserForm
from .models import TFJMUser, Document, Solution, MotivationLetter, Synthesis
from .tables import UserTable
class CreateUserView(CreateView):
+ """
+ Signup form view.
+ """
model = TFJMUser
form_class = SignUpForm
template_name = "registration/signup.html"
class MyAccountView(LoginRequiredMixin, UpdateView):
+ """
+ Update our personal data.
+ """
model = TFJMUser
template_name = "member/my_account.html"
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 \
if self.request.user.role == "3participant" else CoachUserForm
@@ -40,6 +47,10 @@ class MyAccountView(LoginRequiredMixin, UpdateView):
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
form_class = TFJMUserForm
context_object_name = "tfjmuser"
@@ -57,7 +68,10 @@ class UserDetailView(LoginRequiredMixin, DetailView):
return super().dispatch(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["admin"] = request.user.pk
obj = self.get_object()
@@ -74,6 +88,10 @@ class UserDetailView(LoginRequiredMixin, DetailView):
class AddTeamView(LoginRequiredMixin, CreateView):
+ """
+ Register a new team.
+ Users can choose the name, the trigram and a preferred tournament.
+ """
model = Team
form_class = TeamForm
@@ -86,6 +104,7 @@ class AddTeamView(LoginRequiredMixin, CreateView):
form.add_error('name', _("You are already in a team."))
return self.form_invalid(form)
+ # Generate a random access code
team = form.instance
alphabet = "0123456789abcdefghijklmnopqrstuvwxyz0123456789"
code = ""
@@ -107,6 +126,9 @@ class AddTeamView(LoginRequiredMixin, CreateView):
class JoinTeamView(LoginRequiredMixin, FormView):
+ """
+ Join a team with a given access code.
+ """
model = Team
form_class = JoinTeam
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."))
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."))
return self.form_invalid(form)
@@ -130,6 +152,9 @@ class JoinTeamView(LoginRequiredMixin, FormView):
form.add_error('access_code', _("This team is full of participants."))
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.save()
return super().form_valid(form)
@@ -139,11 +164,24 @@ class JoinTeamView(LoginRequiredMixin, FormView):
class MyTeamView(TeamMixin, View):
+ """
+ Redirect to the page of the information of our personal team.
+ """
+
def get(self, request, *args, **kwargs):
return redirect("tournament:team_detail", pk=request.user.team.pk)
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):
doc = Document.objects.get(file=self.kwargs["file"])
@@ -172,6 +210,9 @@ class DocumentView(LoginRequiredMixin, View):
class ProfileListView(AdminMixin, SingleTableView):
+ """
+ List all registered profiles.
+ """
model = TFJMUser
queryset = TFJMUser.objects.order_by("role", "last_name", "first_name")
table_class = UserTable
@@ -180,6 +221,9 @@ class ProfileListView(AdminMixin, SingleTableView):
class OrphanedProfileListView(AdminMixin, SingleTableView):
+ """
+ List all orphaned profiles, ie. participants that have no team.
+ """
model = TFJMUser
queryset = TFJMUser.objects.filter((Q(role="2coach") | Q(role="3participant")) & Q(team__isnull=True))\
.order_by("role", "last_name", "first_name")
@@ -189,6 +233,9 @@ class OrphanedProfileListView(AdminMixin, SingleTableView):
class OrganizersListView(AdminMixin, SingleTableView):
+ """
+ List all organizers.
+ """
model = TFJMUser
queryset = TFJMUser.objects.filter(Q(role="0admin") | Q(role="1volunteer"))\
.order_by("role", "last_name", "first_name")
@@ -198,6 +245,10 @@ class OrganizersListView(AdminMixin, SingleTableView):
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):
if "_fake_user_id" in request.session:
del request.session["_fake_user_id"]
diff --git a/apps/tournament/admin.py b/apps/tournament/admin.py
index cbe128d..c55cc4b 100644
--- a/apps/tournament/admin.py
+++ b/apps/tournament/admin.py
@@ -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.
+ """
diff --git a/apps/tournament/apps.py b/apps/tournament/apps.py
index 2df95c1..66a212b 100644
--- a/apps/tournament/apps.py
+++ b/apps/tournament/apps.py
@@ -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')
diff --git a/apps/tournament/forms.py b/apps/tournament/forms.py
index d46129d..4c4353d 100644
--- a/apps/tournament/forms.py
+++ b/apps/tournament/forms.py
@@ -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..."),
diff --git a/apps/tournament/models.py b/apps/tournament/models.py
index 9a5732e..ae26b7a 100644
--- a/apps/tournament/models.py
+++ b/apps/tournament/models.py
@@ -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 [''.format(url=reverse_lazy("member:information", args=(user.pk,))) + str(user) + ''
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/.html for the HTML
+ version and in templates/mail_templates/.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 [''.format(url=reverse_lazy("member:information", args=(user.pk,))) + str(user) + ''
- 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 [''.format(url=reverse_lazy("member:information", args=(user.pk,))) + str(user) + ''
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/.html for the HTML
+ version and in templates/mail_templates/.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,
diff --git a/apps/tournament/tables.py b/apps/tournament/tables.py
index 6fcf95e..33e39a1 100644
--- a/apps/tournament/tables.py
+++ b/apps/tournament/tables.py
@@ -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,
diff --git a/apps/tournament/views.py b/apps/tournament/views.py
index 45611e9..0211364 100644
--- a/apps/tournament/views.py
+++ b/apps/tournament/views.py
@@ -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"))
diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po
index 1971bdf..4bdbdf1 100644
--- a/locale/fr/LC_MESSAGES/django.po
+++ b/locale/fr/LC_MESSAGES/django.po
@@ -34,10 +34,6 @@ msgstr "Choisir un rôle ..."
msgid "Participant"
msgstr "Participant"
-#: apps/member/forms.py:16
-msgid "Encadrant"
-msgstr "Encadrant"
-
#: apps/member/models.py:18 templates/member/tfjmuser_detail.html:35
msgid "email"
msgstr "Adresse électronique"
diff --git a/templates/tournament/team_detail.html b/templates/tournament/team_detail.html
index 7bf88f9..21dbada 100644
--- a/templates/tournament/team_detail.html
+++ b/templates/tournament/team_detail.html
@@ -23,7 +23,7 @@
href="{% url "tournament:detail" pk=team.tournament.pk %}">{{ team.tournament }}
{% trans 'coachs'|capfirst %}
- {% autoescape off %}{{ team.linked_encadrants|join:", " }}{% endautoescape %}
+ {% autoescape off %}{{ team.linked_coaches|join:", " }}{% endautoescape %}
{% trans 'participants'|capfirst %}
diff --git a/tfjm/settings.py b/tfjm/settings.py
index 46a2edc..7b143b2 100644
--- a/tfjm/settings.py
+++ b/tfjm/settings.py
@@ -181,3 +181,6 @@ if os.getenv("TFJM_STAGE", "dev") == "prod":
from .settings_prod import *
else:
from .settings_dev import *
+ INSTALLED_APPS += [
+ "django_extensions"
+ ]