Comment code, fix minor issues
This commit is contained in:
parent
c9b9d01523
commit
a561364bd0
|
@ -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')
|
||||||
|
|
|
@ -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__"
|
150
apps/api/urls.py
150
apps/api/urls.py
|
@ -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
|
||||||
|
|
|
@ -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)
|
|
@ -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.
|
||||||
|
"""
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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',)
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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")],
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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.
|
||||||
|
"""
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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..."),
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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"))
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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"
|
||||||
|
]
|
||||||
|
|
Loading…
Reference in New Issue