mirror of
https://gitlab.com/animath/si/plateforme.git
synced 2025-06-21 13:18:25 +02:00
Reset project
This commit is contained in:
@ -1 +0,0 @@
|
||||
default_app_config = 'api.apps.APIConfig'
|
@ -1,10 +0,0 @@
|
||||
from django.apps import AppConfig
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class APIConfig(AppConfig):
|
||||
"""
|
||||
Manage the inscription through a JSON API.
|
||||
"""
|
||||
name = 'api'
|
||||
verbose_name = _('API')
|
@ -1,80 +0,0 @@
|
||||
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__"
|
@ -1,26 +0,0 @@
|
||||
from django.conf.urls import url, include
|
||||
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
|
||||
router = routers.DefaultRouter()
|
||||
router.register('user', UserViewSet)
|
||||
router.register('team', TeamViewSet)
|
||||
router.register('tournament', TournamentViewSet)
|
||||
router.register('authorization', AuthorizationViewSet)
|
||||
router.register('motivation_letter', MotivationLetterViewSet)
|
||||
router.register('solution', SolutionViewSet)
|
||||
router.register('synthesis', SynthesisViewSet)
|
||||
router.register('pool', PoolViewSet)
|
||||
|
||||
app_name = 'api'
|
||||
|
||||
# Wire up our API using automatic URL routing.
|
||||
# Additionally, we include login URLs for the browsable API.
|
||||
urlpatterns = [
|
||||
url('^', include(router.urls)),
|
||||
url('^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
|
||||
]
|
@ -1,124 +0,0 @@
|
||||
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)
|
@ -1 +0,0 @@
|
||||
default_app_config = 'member.apps.MemberConfig'
|
@ -1,56 +0,0 @@
|
||||
from django.contrib.auth.admin import admin
|
||||
from polymorphic.admin import PolymorphicParentModelAdmin, PolymorphicChildModelAdmin
|
||||
from member.models import TFJMUser, Document, Solution, Synthesis, MotivationLetter, Authorization, Config
|
||||
|
||||
|
||||
@admin.register(TFJMUser)
|
||||
class TFJMUserAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Django admin page for users.
|
||||
"""
|
||||
list_display = ('email', 'first_name', 'last_name', 'role', )
|
||||
search_fields = ('last_name', 'first_name',)
|
||||
|
||||
|
||||
@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):
|
||||
"""
|
||||
Django admin page for Authorization.
|
||||
"""
|
||||
|
||||
|
||||
@admin.register(MotivationLetter)
|
||||
class MotivationLetterAdmin(PolymorphicChildModelAdmin):
|
||||
"""
|
||||
Django admin page for Motivation letters.
|
||||
"""
|
||||
|
||||
|
||||
@admin.register(Solution)
|
||||
class SolutionAdmin(PolymorphicChildModelAdmin):
|
||||
"""
|
||||
Django admin page for solutions.
|
||||
"""
|
||||
|
||||
|
||||
@admin.register(Synthesis)
|
||||
class SynthesisAdmin(PolymorphicChildModelAdmin):
|
||||
"""
|
||||
Django admin page for syntheses.
|
||||
"""
|
||||
|
||||
|
||||
@admin.register(Config)
|
||||
class ConfigAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Django admin page for configurations.
|
||||
"""
|
@ -1,10 +0,0 @@
|
||||
from django.apps import AppConfig
|
||||
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')
|
@ -1,83 +0,0 @@
|
||||
from bootstrap_datepicker_plus import DatePickerInput
|
||||
from django.contrib.auth.forms import UserCreationForm
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
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...")),
|
||||
('3participant', _("Participant")),
|
||||
('2coach', _("Coach")),
|
||||
]
|
||||
|
||||
class Meta:
|
||||
model = TFJMUser
|
||||
fields = (
|
||||
'role',
|
||||
'email',
|
||||
'first_name',
|
||||
'last_name',
|
||||
'birth_date',
|
||||
'gender',
|
||||
'address',
|
||||
'postal_code',
|
||||
'city',
|
||||
'country',
|
||||
'phone_number',
|
||||
'school',
|
||||
'student_class',
|
||||
'responsible_name',
|
||||
'responsible_phone',
|
||||
'responsible_email',
|
||||
'description',
|
||||
)
|
||||
widgets = {
|
||||
"birth_date": DatePickerInput(),
|
||||
}
|
||||
|
||||
|
||||
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',
|
||||
'city', 'country', 'school', 'student_class', 'responsible_name', 'responsible_phone',
|
||||
'responsible_email',)
|
||||
widgets = {
|
||||
"birth_date": DatePickerInput(),
|
||||
}
|
||||
|
||||
|
||||
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',
|
||||
'city', 'country', 'description',)
|
||||
widgets = {
|
||||
"birth_date": DatePickerInput(),
|
||||
}
|
||||
|
||||
|
||||
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',)
|
@ -1,32 +0,0 @@
|
||||
import os
|
||||
from datetime import date
|
||||
from getpass import getpass
|
||||
from django.core.management import BaseCommand
|
||||
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"
|
||||
while password != confirm_password:
|
||||
password = getpass("Password: ")
|
||||
confirm_password = getpass("Confirm password: ")
|
||||
if password != confirm_password:
|
||||
self.stderr.write(self.style.ERROR("Passwords don't match."))
|
||||
|
||||
user = TFJMUser.objects.create(
|
||||
email=email,
|
||||
password="",
|
||||
role="admin",
|
||||
year=os.getenv("TFJM_YEAR", date.today().year),
|
||||
is_active=True,
|
||||
is_staff=True,
|
||||
is_superuser=True,
|
||||
)
|
||||
user.set_password(password)
|
||||
user.save()
|
@ -1,75 +0,0 @@
|
||||
import os
|
||||
from urllib.request import urlretrieve
|
||||
from shutil import copyfile
|
||||
|
||||
from django.core.management import BaseCommand
|
||||
from django.utils import translation
|
||||
from member.models import Solution
|
||||
from tournament.models import Tournament
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
PROBLEMS = [
|
||||
'Création de puzzles',
|
||||
'Départ en vacances',
|
||||
'Un festin stratégique',
|
||||
'Sauver les meubles',
|
||||
'Prêt à décoller !',
|
||||
'Ils nous espionnent !',
|
||||
'De joyeux bûcherons',
|
||||
'Robots auto-réplicateurs',
|
||||
]
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('dir',
|
||||
type=str,
|
||||
default='.',
|
||||
help="Directory where solutions should be saved.")
|
||||
parser.add_argument('--language', '-l',
|
||||
type=str,
|
||||
choices=['en', 'fr'],
|
||||
default='fr',
|
||||
help="Language of the title of the files.")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
"""
|
||||
Copy solutions elsewhere.
|
||||
"""
|
||||
d = options['dir']
|
||||
teams_dir = d + '/Par équipe'
|
||||
os.makedirs(teams_dir, exist_ok=True)
|
||||
|
||||
translation.activate(options['language'])
|
||||
|
||||
copied = 0
|
||||
|
||||
for tournament in Tournament.objects.all():
|
||||
os.mkdir(teams_dir + '/' + tournament.name)
|
||||
for team in tournament.teams.filter(validation_status='2valid'):
|
||||
os.mkdir(teams_dir + '/' + tournament.name + '/' + str(team))
|
||||
for sol in tournament.solutions:
|
||||
if not os.path.isfile('media/' + sol.file.name):
|
||||
self.stdout.write(self.style.WARNING(("Warning: solution '{sol}' is not found. Maybe the file"
|
||||
"was deleted?").format(sol=str(sol))))
|
||||
continue
|
||||
copyfile('media/' + sol.file.name, teams_dir + '/' + tournament.name
|
||||
+ '/' + str(sol.team) + '/' + str(sol) + '.pdf')
|
||||
copied += 1
|
||||
|
||||
self.stdout.write(self.style.SUCCESS("Successfully copied {copied} solutions!".format(copied=copied)))
|
||||
|
||||
os.mkdir(d + '/Par problème')
|
||||
|
||||
for pb in range(1, 9):
|
||||
sols = Solution.objects.filter(problem=pb).all()
|
||||
pbdir = d + '/Par problème/Problème n°{number} — {problem}'.format(number=pb, problem=self.PROBLEMS[pb - 1])
|
||||
os.mkdir(pbdir)
|
||||
for sol in sols:
|
||||
os.symlink('../../Par équipe/' + sol.tournament.name + '/' + str(sol.team) + '/' + str(sol) + '.pdf',
|
||||
pbdir + '/' + str(sol) + '.pdf')
|
||||
|
||||
self.stdout.write(self.style.SUCCESS("Symlinks by problem created!"))
|
||||
|
||||
urlretrieve('https://tfjm.org/wp-content/uploads/2020/01/Problemes2020_23_01_v1_1.pdf', d + '/Énoncés.pdf')
|
||||
|
||||
self.stdout.write(self.style.SUCCESS("Questions retrieved!"))
|
@ -1,309 +0,0 @@
|
||||
import os
|
||||
|
||||
from django.core.management import BaseCommand, CommandError
|
||||
from django.db import transaction
|
||||
from member.models import TFJMUser, Document, Solution, Synthesis, Authorization, MotivationLetter
|
||||
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")
|
||||
parser.add_argument('--users', '-u', action="store", help="Import users")
|
||||
parser.add_argument('--documents', '-d', action="store", help="Import all documents")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if "tournaments" in options:
|
||||
self.import_tournaments()
|
||||
|
||||
if "teams" in options:
|
||||
self.import_teams()
|
||||
|
||||
if "users" in options:
|
||||
self.import_users()
|
||||
|
||||
if "documents" in options:
|
||||
self.import_documents()
|
||||
|
||||
@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
|
||||
for line in f:
|
||||
if first_line:
|
||||
first_line = False
|
||||
continue
|
||||
|
||||
line = line[:-1].replace("\"", "")
|
||||
args = line.split(";")
|
||||
args = [arg if arg and arg != "NULL" else None for arg in args]
|
||||
|
||||
if Tournament.objects.filter(pk=args[0]).exists():
|
||||
continue
|
||||
|
||||
obj_dict = {
|
||||
"id": args[0],
|
||||
"name": args[1],
|
||||
"size": args[2],
|
||||
"place": args[3],
|
||||
"price": args[4],
|
||||
"description": args[5],
|
||||
"date_start": args[6],
|
||||
"date_end": args[7],
|
||||
"date_inscription": args[8],
|
||||
"date_solutions": args[9],
|
||||
"date_syntheses": args[10],
|
||||
"date_solutions_2": args[11],
|
||||
"date_syntheses_2": args[12],
|
||||
"final": args[13],
|
||||
"year": args[14],
|
||||
}
|
||||
with transaction.atomic():
|
||||
Tournament.objects.create(**obj_dict)
|
||||
print(self.style.SUCCESS("Tournaments imported"))
|
||||
|
||||
@staticmethod
|
||||
def validation_status(status):
|
||||
if status == "NOT_READY":
|
||||
return "0invalid"
|
||||
elif status == "WAITING":
|
||||
return "1waiting"
|
||||
elif status == "VALIDATED":
|
||||
return "2valid"
|
||||
else:
|
||||
raise CommandError("Unknown status: {}".format(status))
|
||||
|
||||
@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
|
||||
for line in f:
|
||||
if first_line:
|
||||
first_line = False
|
||||
continue
|
||||
|
||||
line = line[:-1].replace("\"", "")
|
||||
args = line.split(";")
|
||||
args = [arg if arg and arg != "NULL" else None for arg in args]
|
||||
|
||||
if Team.objects.filter(pk=args[0]).exists():
|
||||
continue
|
||||
|
||||
obj_dict = {
|
||||
"id": args[0],
|
||||
"name": args[1],
|
||||
"trigram": args[2],
|
||||
"tournament": Tournament.objects.get(pk=args[3]),
|
||||
"inscription_date": args[13],
|
||||
"validation_status": Command.validation_status(args[14]),
|
||||
"selected_for_final": args[15],
|
||||
"access_code": args[16],
|
||||
"year": args[17],
|
||||
}
|
||||
with transaction.atomic():
|
||||
Team.objects.create(**obj_dict)
|
||||
print(self.style.SUCCESS("Teams imported"))
|
||||
|
||||
@staticmethod
|
||||
def role(role):
|
||||
if role == "ADMIN":
|
||||
return "0admin"
|
||||
elif role == "ORGANIZER":
|
||||
return "1volunteer"
|
||||
elif role == "ENCADRANT":
|
||||
return "2coach"
|
||||
elif role == "PARTICIPANT":
|
||||
return "3participant"
|
||||
else:
|
||||
raise CommandError("Unknown role: {}".format(role))
|
||||
|
||||
@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
|
||||
for line in f:
|
||||
if first_line:
|
||||
first_line = False
|
||||
continue
|
||||
|
||||
line = line[:-1].replace("\"", "")
|
||||
args = line.split(";")
|
||||
args = [arg if arg and arg != "NULL" else None for arg in args]
|
||||
|
||||
if TFJMUser.objects.filter(pk=args[0]).exists():
|
||||
continue
|
||||
|
||||
obj_dict = {
|
||||
"id": args[0],
|
||||
"email": args[1],
|
||||
"username": args[1],
|
||||
"password": "bcrypt$" + args[2],
|
||||
"last_name": args[3],
|
||||
"first_name": args[4],
|
||||
"birth_date": args[5],
|
||||
"gender": "male" if args[6] == "M" else "female",
|
||||
"address": args[7],
|
||||
"postal_code": args[8],
|
||||
"city": args[9],
|
||||
"country": args[10],
|
||||
"phone_number": args[11],
|
||||
"school": args[12],
|
||||
"student_class": args[13].lower().replace('premiere', 'première') if args[13] else None,
|
||||
"responsible_name": args[14],
|
||||
"responsible_phone": args[15],
|
||||
"responsible_email": args[16],
|
||||
"description": args[17].replace("\\n", "\n") if args[17] else None,
|
||||
"role": Command.role(args[18]),
|
||||
"team": Team.objects.get(pk=args[19]) if args[19] else None,
|
||||
"year": args[20],
|
||||
"date_joined": args[23],
|
||||
"is_active": args[18] == "ADMIN" or os.getenv("TFJM_STAGE", "dev") == "prod",
|
||||
"is_staff": args[18] == "ADMIN",
|
||||
"is_superuser": args[18] == "ADMIN",
|
||||
}
|
||||
with transaction.atomic():
|
||||
TFJMUser.objects.create(**obj_dict)
|
||||
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:
|
||||
if first_line:
|
||||
first_line = False
|
||||
continue
|
||||
|
||||
line = line[:-1].replace("\"", "")
|
||||
args = line.split(";")
|
||||
args = [arg if arg and arg != "NULL" else None for arg in args]
|
||||
|
||||
with transaction.atomic():
|
||||
tournament = Tournament.objects.get(pk=args[2])
|
||||
organizer = TFJMUser.objects.get(pk=args[1])
|
||||
tournament.organizers.add(organizer)
|
||||
tournament.save()
|
||||
self.stdout.write(self.style.SUCCESS("Organizers imported"))
|
||||
|
||||
@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
|
||||
for line in f:
|
||||
if first_line:
|
||||
first_line = False
|
||||
continue
|
||||
|
||||
line = line[:-1].replace("\"", "")
|
||||
args = line.split(";")
|
||||
args = [arg if arg and arg != "NULL" else None for arg in args]
|
||||
|
||||
if Document.objects.filter(file=args[0]).exists():
|
||||
doc = Document.objects.get(file=args[0])
|
||||
doc.uploaded_at = args[5].replace(" ", "T")
|
||||
doc.save()
|
||||
continue
|
||||
|
||||
obj_dict = {
|
||||
"file": args[0],
|
||||
"uploaded_at": args[5],
|
||||
}
|
||||
if args[4] != "MOTIVATION_LETTER":
|
||||
obj_dict["user"] = TFJMUser.objects.get(args[1]),
|
||||
obj_dict["type"] = args[4].lower()
|
||||
else:
|
||||
try:
|
||||
obj_dict["team"] = Team.objects.get(pk=args[2])
|
||||
except Team.DoesNotExist:
|
||||
print("Team with pk {} does not exist, ignoring".format(args[2]))
|
||||
continue
|
||||
with transaction.atomic():
|
||||
if args[4] != "MOTIVATION_LETTER":
|
||||
Authorization.objects.create(**obj_dict)
|
||||
else:
|
||||
MotivationLetter.objects.create(**obj_dict)
|
||||
self.stdout.write(self.style.SUCCESS("Authorizations imported"))
|
||||
|
||||
with open("import_olddb/solutions.csv") as f:
|
||||
first_line = True
|
||||
for line in f:
|
||||
if first_line:
|
||||
first_line = False
|
||||
continue
|
||||
|
||||
line = line[:-1].replace("\"", "")
|
||||
args = line.split(";")
|
||||
args = [arg if arg and arg != "NULL" else None for arg in args]
|
||||
|
||||
if Document.objects.filter(file=args[0]).exists():
|
||||
doc = Document.objects.get(file=args[0])
|
||||
doc.uploaded_at = args[4].replace(" ", "T")
|
||||
doc.save()
|
||||
continue
|
||||
|
||||
obj_dict = {
|
||||
"file": args[0],
|
||||
"team": Team.objects.get(pk=args[1]),
|
||||
"problem": args[3],
|
||||
"uploaded_at": args[4],
|
||||
}
|
||||
with transaction.atomic():
|
||||
try:
|
||||
Solution.objects.create(**obj_dict)
|
||||
except:
|
||||
print("Solution exists")
|
||||
self.stdout.write(self.style.SUCCESS("Solutions imported"))
|
||||
|
||||
with open("import_olddb/syntheses.csv") as f:
|
||||
first_line = True
|
||||
for line in f:
|
||||
if first_line:
|
||||
first_line = False
|
||||
continue
|
||||
|
||||
line = line[:-1].replace("\"", "")
|
||||
args = line.split(";")
|
||||
args = [arg if arg and arg != "NULL" else None for arg in args]
|
||||
|
||||
if Document.objects.filter(file=args[0]).exists():
|
||||
doc = Document.objects.get(file=args[0])
|
||||
doc.uploaded_at = args[5].replace(" ", "T")
|
||||
doc.save()
|
||||
continue
|
||||
|
||||
obj_dict = {
|
||||
"file": args[0],
|
||||
"team": Team.objects.get(pk=args[1]),
|
||||
"source": "opponent" if args[3] == "1" else "rapporteur",
|
||||
"round": args[4],
|
||||
"uploaded_at": args[5],
|
||||
}
|
||||
with transaction.atomic():
|
||||
try:
|
||||
Synthesis.objects.create(**obj_dict)
|
||||
except:
|
||||
print("Synthesis exists")
|
||||
self.stdout.write(self.style.SUCCESS("Syntheses imported"))
|
@ -1,133 +0,0 @@
|
||||
# Generated by Django 3.1 on 2020-09-19 18:15
|
||||
|
||||
import django.contrib.auth.models
|
||||
import django.contrib.auth.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('auth', '0012_alter_user_first_name_max_length'),
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Config',
|
||||
fields=[
|
||||
('key', models.CharField(max_length=255, primary_key=True, serialize=False, verbose_name='key')),
|
||||
('value', models.TextField(default='', verbose_name='value')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'configuration',
|
||||
'verbose_name_plural': 'configurations',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Document',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('file', models.FileField(unique=True, upload_to='', verbose_name='file')),
|
||||
('uploaded_at', models.DateTimeField(auto_now_add=True, verbose_name='uploaded at')),
|
||||
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_member.document_set+', to='contenttypes.contenttype')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'document',
|
||||
'verbose_name_plural': 'documents',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Authorization',
|
||||
fields=[
|
||||
('document_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='member.document')),
|
||||
('type', models.CharField(choices=[('parental_consent', 'Parental consent'), ('photo_consent', 'Photo consent'), ('sanitary_plug', 'Sanitary plug'), ('scholarship', 'Scholarship')], max_length=32, verbose_name='type')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'authorization',
|
||||
'verbose_name_plural': 'authorizations',
|
||||
},
|
||||
bases=('member.document',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='MotivationLetter',
|
||||
fields=[
|
||||
('document_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='member.document')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'motivation letter',
|
||||
'verbose_name_plural': 'motivation letters',
|
||||
},
|
||||
bases=('member.document',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Solution',
|
||||
fields=[
|
||||
('document_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='member.document')),
|
||||
('problem', models.PositiveSmallIntegerField(verbose_name='problem')),
|
||||
('final', models.BooleanField(default=False, verbose_name='final solution')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'solution',
|
||||
'verbose_name_plural': 'solutions',
|
||||
},
|
||||
bases=('member.document',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Synthesis',
|
||||
fields=[
|
||||
('document_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='member.document')),
|
||||
('source', models.CharField(choices=[('opponent', 'Opponent'), ('rapporteur', 'Rapporteur')], max_length=16, verbose_name='source')),
|
||||
('round', models.PositiveSmallIntegerField(choices=[(1, 'Round 1'), (2, 'Round 2')], verbose_name='round')),
|
||||
('final', models.BooleanField(default=False, verbose_name='final synthesis')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'synthesis',
|
||||
'verbose_name_plural': 'syntheses',
|
||||
},
|
||||
bases=('member.document',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TFJMUser',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
||||
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
||||
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
||||
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||
('email', models.EmailField(help_text='This should be valid and will be controlled.', max_length=254, unique=True, verbose_name='email')),
|
||||
('birth_date', models.DateField(default=None, null=True, verbose_name='birth date')),
|
||||
('gender', models.CharField(choices=[('male', 'Male'), ('female', 'Female'), ('non-binary', 'Non binary')], default=None, max_length=16, null=True, verbose_name='gender')),
|
||||
('address', models.CharField(default=None, max_length=255, null=True, verbose_name='address')),
|
||||
('postal_code', models.PositiveIntegerField(default=None, null=True, verbose_name='postal code')),
|
||||
('city', models.CharField(default=None, max_length=255, null=True, verbose_name='city')),
|
||||
('country', models.CharField(default='France', max_length=255, null=True, verbose_name='country')),
|
||||
('phone_number', models.CharField(blank=True, default=None, max_length=20, null=True, verbose_name='phone number')),
|
||||
('school', models.CharField(default=None, max_length=255, null=True, verbose_name='school')),
|
||||
('student_class', models.CharField(choices=[('seconde', 'Seconde or less'), ('première', 'Première'), ('terminale', 'Terminale')], default=None, max_length=16, null=True, verbose_name='class')),
|
||||
('responsible_name', models.CharField(default=None, max_length=255, null=True, verbose_name='responsible name')),
|
||||
('responsible_phone', models.CharField(default=None, max_length=20, null=True, verbose_name='responsible phone')),
|
||||
('responsible_email', models.EmailField(default=None, max_length=254, null=True, verbose_name='responsible email')),
|
||||
('description', models.TextField(default=None, null=True, verbose_name='description')),
|
||||
('role', models.CharField(choices=[('0admin', 'Admin'), ('1volunteer', 'Organizer'), ('2coach', 'Coach'), ('3participant', 'Participant')], max_length=16)),
|
||||
('year', models.PositiveIntegerField(default=2020, verbose_name='year')),
|
||||
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'user',
|
||||
'verbose_name_plural': 'users',
|
||||
},
|
||||
managers=[
|
||||
('objects', django.contrib.auth.models.UserManager()),
|
||||
],
|
||||
),
|
||||
]
|
@ -1,57 +0,0 @@
|
||||
# Generated by Django 3.1 on 2020-09-19 18:15
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('member', '0001_initial'),
|
||||
('auth', '0012_alter_user_first_name_max_length'),
|
||||
('tournament', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='tfjmuser',
|
||||
name='team',
|
||||
field=models.ForeignKey(help_text='Concerns only coaches and participants.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='users', to='tournament.team', verbose_name='team'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='tfjmuser',
|
||||
name='user_permissions',
|
||||
field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='synthesis',
|
||||
name='team',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='syntheses', to='tournament.team', verbose_name='team'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='solution',
|
||||
name='team',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='solutions', to='tournament.team', verbose_name='team'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='motivationletter',
|
||||
name='team',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='motivation_letters', to='tournament.team', verbose_name='team'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='authorization',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='authorizations', to=settings.AUTH_USER_MODEL, verbose_name='user'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='synthesis',
|
||||
unique_together={('team', 'source', 'round', 'final')},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='solution',
|
||||
unique_together={('team', 'problem', 'final')},
|
||||
),
|
||||
]
|
@ -1,18 +0,0 @@
|
||||
# Generated by Django 3.1 on 2020-09-19 20:45
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('member', '0002_auto_20200919_2015'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='tfjmuser',
|
||||
name='email_confirmed',
|
||||
field=models.BooleanField(default=False, verbose_name='email confirmed'),
|
||||
),
|
||||
]
|
@ -1,399 +0,0 @@
|
||||
import os
|
||||
from datetime import date
|
||||
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.contrib.sites.models import Site
|
||||
from django.db import models
|
||||
from django.template import loader
|
||||
from django.utils.encoding import force_bytes
|
||||
from django.utils.http import urlsafe_base64_encode
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from polymorphic.models import PolymorphicModel
|
||||
from tournament.models import Team, Tournament
|
||||
|
||||
from .tokens import email_validation_token
|
||||
|
||||
|
||||
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(
|
||||
Team,
|
||||
null=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="users",
|
||||
verbose_name=_("team"),
|
||||
help_text=_("Concerns only coaches and participants."),
|
||||
)
|
||||
|
||||
birth_date = models.DateField(
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_("birth date"),
|
||||
)
|
||||
|
||||
gender = models.CharField(
|
||||
max_length=16,
|
||||
null=True,
|
||||
default=None,
|
||||
choices=[
|
||||
("male", _("Male")),
|
||||
("female", _("Female")),
|
||||
("non-binary", _("Non binary")),
|
||||
],
|
||||
verbose_name=_("gender"),
|
||||
)
|
||||
|
||||
address = models.CharField(
|
||||
max_length=255,
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_("address"),
|
||||
)
|
||||
|
||||
postal_code = models.PositiveIntegerField(
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_("postal code"),
|
||||
)
|
||||
|
||||
city = models.CharField(
|
||||
max_length=255,
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_("city"),
|
||||
)
|
||||
|
||||
country = models.CharField(
|
||||
max_length=255,
|
||||
default="France",
|
||||
null=True,
|
||||
verbose_name=_("country"),
|
||||
)
|
||||
|
||||
phone_number = models.CharField(
|
||||
max_length=20,
|
||||
null=True,
|
||||
blank=True,
|
||||
default=None,
|
||||
verbose_name=_("phone number"),
|
||||
)
|
||||
|
||||
school = models.CharField(
|
||||
max_length=255,
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_("school"),
|
||||
)
|
||||
|
||||
student_class = models.CharField(
|
||||
max_length=16,
|
||||
choices=[
|
||||
('seconde', _("Seconde or less")),
|
||||
('première', _("Première")),
|
||||
('terminale', _("Terminale")),
|
||||
],
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name="class",
|
||||
)
|
||||
|
||||
responsible_name = models.CharField(
|
||||
max_length=255,
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_("responsible name"),
|
||||
)
|
||||
|
||||
responsible_phone = models.CharField(
|
||||
max_length=20,
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_("responsible phone"),
|
||||
)
|
||||
|
||||
responsible_email = models.EmailField(
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_("responsible email"),
|
||||
)
|
||||
|
||||
description = models.TextField(
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_("description"),
|
||||
)
|
||||
|
||||
role = models.CharField(
|
||||
max_length=16,
|
||||
choices=[
|
||||
("0admin", _("Admin")),
|
||||
("1volunteer", _("Organizer")),
|
||||
("2coach", _("Coach")),
|
||||
("3participant", _("Participant")),
|
||||
]
|
||||
)
|
||||
|
||||
year = models.PositiveIntegerField(
|
||||
default=os.getenv("TFJM_YEAR", date.today().year),
|
||||
verbose_name=_("year"),
|
||||
)
|
||||
|
||||
email_confirmed = models.BooleanField(
|
||||
verbose_name=_("email confirmed"),
|
||||
default=False,
|
||||
)
|
||||
|
||||
@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:
|
||||
verbose_name = _("user")
|
||||
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)
|
||||
|
||||
def __str__(self):
|
||||
return self.first_name + " " + self.last_name
|
||||
|
||||
def send_email_validation_link(self):
|
||||
subject = "[TFJM²] " + str(_("Activate your Note Kfet account"))
|
||||
token = email_validation_token.make_token(self)
|
||||
uid = urlsafe_base64_encode(force_bytes(self.pk))
|
||||
site = Site.objects.first()
|
||||
message = loader.render_to_string('registration/mails/email_validation_email.txt',
|
||||
{
|
||||
'user': self,
|
||||
'domain': site.domain,
|
||||
'token': token,
|
||||
'uid': uid,
|
||||
})
|
||||
html = loader.render_to_string('registration/mails/email_validation_email.html',
|
||||
{
|
||||
'user': self,
|
||||
'domain': site.domain,
|
||||
'token': token,
|
||||
'uid': uid,
|
||||
})
|
||||
self.email_user(subject, message, html_message=html)
|
||||
|
||||
|
||||
class Document(PolymorphicModel):
|
||||
"""
|
||||
Abstract model of any saved document (solution, synthesis, motivation letter, authorization)
|
||||
"""
|
||||
file = models.FileField(
|
||||
unique=True,
|
||||
verbose_name=_("file"),
|
||||
)
|
||||
|
||||
uploaded_at = models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
verbose_name=_("uploaded at"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("document")
|
||||
verbose_name_plural = _("documents")
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
self.file.delete(True)
|
||||
return super().delete(*args, **kwargs)
|
||||
|
||||
|
||||
class Authorization(Document):
|
||||
"""
|
||||
Model for authorization papers (parental consent, photo consent, sanitary plug, ...)
|
||||
"""
|
||||
user = models.ForeignKey(
|
||||
TFJMUser,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="authorizations",
|
||||
verbose_name=_("user"),
|
||||
)
|
||||
|
||||
type = models.CharField(
|
||||
max_length=32,
|
||||
choices=[
|
||||
("parental_consent", _("Parental consent")),
|
||||
("photo_consent", _("Photo consent")),
|
||||
("sanitary_plug", _("Sanitary plug")),
|
||||
("scholarship", _("Scholarship")),
|
||||
],
|
||||
verbose_name=_("type"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("authorization")
|
||||
verbose_name_plural = _("authorizations")
|
||||
|
||||
def __str__(self):
|
||||
return _("{authorization} for user {user}").format(authorization=self.type, user=str(self.user))
|
||||
|
||||
|
||||
class MotivationLetter(Document):
|
||||
"""
|
||||
Model for motivation letters of a team.
|
||||
"""
|
||||
team = models.ForeignKey(
|
||||
Team,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="motivation_letters",
|
||||
verbose_name=_("team"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("motivation letter")
|
||||
verbose_name_plural = _("motivation letters")
|
||||
|
||||
def __str__(self):
|
||||
return _("Motivation letter of team {team} ({trigram})").format(team=self.team.name, trigram=self.team.trigram)
|
||||
|
||||
|
||||
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,
|
||||
related_name="solutions",
|
||||
verbose_name=_("team"),
|
||||
)
|
||||
|
||||
problem = models.PositiveSmallIntegerField(
|
||||
verbose_name=_("problem"),
|
||||
)
|
||||
|
||||
final = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("final solution"),
|
||||
)
|
||||
|
||||
@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:
|
||||
verbose_name = _("solution")
|
||||
verbose_name_plural = _("solutions")
|
||||
unique_together = ('team', 'problem', 'final',)
|
||||
|
||||
def __str__(self):
|
||||
if self.final:
|
||||
return _("Solution of team {trigram} for problem {problem} for final")\
|
||||
.format(trigram=self.team.trigram, problem=self.problem)
|
||||
else:
|
||||
return _("Solution of team {trigram} for problem {problem}")\
|
||||
.format(trigram=self.team.trigram, problem=self.problem)
|
||||
|
||||
|
||||
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,
|
||||
related_name="syntheses",
|
||||
verbose_name=_("team"),
|
||||
)
|
||||
|
||||
source = models.CharField(
|
||||
max_length=16,
|
||||
choices=[
|
||||
("opponent", _("Opponent")),
|
||||
("rapporteur", _("Rapporteur")),
|
||||
],
|
||||
verbose_name=_("source"),
|
||||
)
|
||||
|
||||
round = models.PositiveSmallIntegerField(
|
||||
choices=[
|
||||
(1, _("Round 1")),
|
||||
(2, _("Round 2")),
|
||||
],
|
||||
verbose_name=_("round"),
|
||||
)
|
||||
|
||||
final = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("final synthesis"),
|
||||
)
|
||||
|
||||
@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:
|
||||
verbose_name = _("synthesis")
|
||||
verbose_name_plural = _("syntheses")
|
||||
unique_together = ('team', 'source', 'round', 'final',)
|
||||
|
||||
def __str__(self):
|
||||
return _("Synthesis of team {trigram} that is {source} for the round {round} of tournament {tournament}")\
|
||||
.format(trigram=self.team.trigram, source=self.get_source_display().lower(), round=self.round,
|
||||
tournament=self.tournament)
|
||||
|
||||
|
||||
class Config(models.Model):
|
||||
"""
|
||||
Dictionary of configuration variables.
|
||||
"""
|
||||
key = models.CharField(
|
||||
max_length=255,
|
||||
primary_key=True,
|
||||
verbose_name=_("key"),
|
||||
)
|
||||
|
||||
value = models.TextField(
|
||||
default="",
|
||||
verbose_name=_("value"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("configuration")
|
||||
verbose_name_plural = _("configurations")
|
@ -1,26 +0,0 @@
|
||||
import django_tables2 as tables
|
||||
from django_tables2 import A
|
||||
|
||||
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")],
|
||||
)
|
||||
|
||||
first_name = tables.LinkColumn(
|
||||
"member:information",
|
||||
args=[A("pk")],
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = TFJMUser
|
||||
fields = ("last_name", "first_name", "role", "date_joined", )
|
||||
attrs = {
|
||||
'class': 'table table-condensed table-striped table-hover'
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
from django import template
|
||||
|
||||
import os
|
||||
|
||||
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)
|
||||
|
||||
|
||||
register = template.Library()
|
||||
register.filter('get_config', get_config)
|
||||
register.filter('get_env', get_env)
|
@ -1,26 +0,0 @@
|
||||
from django.contrib.auth.tokens import PasswordResetTokenGenerator
|
||||
|
||||
|
||||
class AccountActivationTokenGenerator(PasswordResetTokenGenerator):
|
||||
"""
|
||||
Create a unique token generator to confirm email addresses.
|
||||
"""
|
||||
def _make_hash_value(self, user, timestamp):
|
||||
"""
|
||||
Hash the user's primary key and some user state that's sure to change
|
||||
after an account validation to produce a token that invalidated when
|
||||
it's used:
|
||||
1. The user.profile.email_confirmed field will change upon an account
|
||||
validation.
|
||||
2. The last_login field will usually be updated very shortly after
|
||||
an account validation.
|
||||
Failing those things, settings.PASSWORD_RESET_TIMEOUT_DAYS eventually
|
||||
invalidates the token.
|
||||
"""
|
||||
# Truncate microseconds so that tokens are consistent even if the
|
||||
# database doesn't support microseconds.
|
||||
login_timestamp = '' if user.last_login is None else user.last_login.replace(microsecond=0, tzinfo=None)
|
||||
return str(user.pk) + str(user.email) + str(user.email_confirmed) + str(login_timestamp) + str(timestamp)
|
||||
|
||||
|
||||
email_validation_token = AccountActivationTokenGenerator()
|
@ -1,24 +0,0 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import CreateUserView, MyAccountView, UserDetailView, AddTeamView, JoinTeamView, MyTeamView, \
|
||||
ProfileListView, OrphanedProfileListView, OrganizersListView, ResetAdminView, UserValidationEmailSentView, \
|
||||
UserResendValidationEmailView, UserValidateView
|
||||
|
||||
app_name = "member"
|
||||
|
||||
urlpatterns = [
|
||||
path('signup/', CreateUserView.as_view(), name="signup"),
|
||||
path('validate_email/sent/', UserValidationEmailSentView.as_view(), name='email_validation_sent'),
|
||||
path('validate_email/resend/<int:pk>/', UserResendValidationEmailView.as_view(),
|
||||
name='email_validation_resend'),
|
||||
path('validate_email/<uidb64>/<token>/', UserValidateView.as_view(), name='email_validation'),
|
||||
path("my-account/", MyAccountView.as_view(), name="my_account"),
|
||||
path("information/<int:pk>/", UserDetailView.as_view(), name="information"),
|
||||
path("add-team/", AddTeamView.as_view(), name="add_team"),
|
||||
path("join-team/", JoinTeamView.as_view(), name="join_team"),
|
||||
path("my-team/", MyTeamView.as_view(), name="my_team"),
|
||||
path("profiles/", ProfileListView.as_view(), name="all_profiles"),
|
||||
path("orphaned-profiles/", OrphanedProfileListView.as_view(), name="orphaned_profiles"),
|
||||
path("organizers/", OrganizersListView.as_view(), name="organizers"),
|
||||
path("reset-admin/", ResetAdminView.as_view(), name="reset_admin"),
|
||||
]
|
@ -1,374 +0,0 @@
|
||||
import random
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin, AccessMixin
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.core.exceptions import PermissionDenied, ValidationError
|
||||
from django.db.models import Q
|
||||
from django.http import FileResponse, Http404
|
||||
from django.shortcuts import redirect, resolve_url
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils import timezone
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.http import urlsafe_base64_decode
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views import View
|
||||
from django.views.decorators.debug import sensitive_post_parameters
|
||||
from django.views.generic import CreateView, UpdateView, DetailView, FormView, TemplateView
|
||||
from django_tables2 import SingleTableView
|
||||
from tournament.forms import TeamForm, JoinTeam
|
||||
from tournament.models import Team, Tournament, Pool
|
||||
from tournament.views import AdminMixin, TeamMixin, OrgaMixin
|
||||
|
||||
from .forms import SignUpForm, TFJMUserForm, AdminUserForm, CoachUserForm
|
||||
from .models import TFJMUser, Document, Solution, MotivationLetter, Synthesis
|
||||
from .tables import UserTable
|
||||
from .tokens import email_validation_token
|
||||
|
||||
|
||||
class CreateUserView(CreateView):
|
||||
"""
|
||||
Signup form view.
|
||||
"""
|
||||
model = TFJMUser
|
||||
form_class = SignUpForm
|
||||
template_name = "registration/signup.html"
|
||||
|
||||
# When errors are reported from the signup view, don't send passwords to admins
|
||||
@method_decorator(sensitive_post_parameters('password1', 'password2',))
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.send_email_validation_link()
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('member:email_validation_sent')
|
||||
|
||||
|
||||
class UserValidateView(TemplateView):
|
||||
"""
|
||||
A view to validate the email address.
|
||||
"""
|
||||
title = _("Email validation")
|
||||
template_name = 'registration/email_validation_complete.html'
|
||||
extra_context = {"title": _("Validate email")}
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
"""
|
||||
With a given token and user id (in params), validate the email address.
|
||||
"""
|
||||
assert 'uidb64' in kwargs and 'token' in kwargs
|
||||
|
||||
self.validlink = False
|
||||
user = self.get_user(kwargs['uidb64'])
|
||||
token = kwargs['token']
|
||||
|
||||
# Validate the token
|
||||
if user is not None and email_validation_token.check_token(user, token):
|
||||
self.validlink = True
|
||||
user.email_confirmed = True
|
||||
user.save()
|
||||
return self.render_to_response(self.get_context_data(), status=200 if self.validlink else 400)
|
||||
|
||||
def get_user(self, uidb64):
|
||||
"""
|
||||
Get user from the base64-encoded string.
|
||||
"""
|
||||
try:
|
||||
# urlsafe_base64_decode() decodes to bytestring
|
||||
uid = urlsafe_base64_decode(uidb64).decode()
|
||||
user = TFJMUser.objects.get(pk=uid)
|
||||
except (TypeError, ValueError, OverflowError, TFJMUser.DoesNotExist, ValidationError):
|
||||
user = None
|
||||
return user
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['user_object'] = self.get_user(self.kwargs["uidb64"])
|
||||
context['login_url'] = resolve_url(settings.LOGIN_URL)
|
||||
if self.validlink:
|
||||
context['validlink'] = True
|
||||
else:
|
||||
context.update({
|
||||
'title': _('Email validation unsuccessful'),
|
||||
'validlink': False,
|
||||
})
|
||||
return context
|
||||
|
||||
|
||||
class UserValidationEmailSentView(TemplateView):
|
||||
"""
|
||||
Display the information that the validation link has been sent.
|
||||
"""
|
||||
template_name = 'registration/email_validation_email_sent.html'
|
||||
extra_context = {"title": _('Email validation email sent')}
|
||||
|
||||
|
||||
class UserResendValidationEmailView(LoginRequiredMixin, DetailView):
|
||||
"""
|
||||
Rensend the email validation link.
|
||||
"""
|
||||
model = TFJMUser
|
||||
extra_context = {"title": _("Resend email validation link")}
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
user = self.get_object()
|
||||
|
||||
user.profile.send_email_validation_link()
|
||||
|
||||
url = 'member:user_detail' if user.profile.registration_valid else 'member:future_user_detail'
|
||||
return redirect(url, user.id)
|
||||
|
||||
|
||||
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
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return self.request.user
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('member:my_account')
|
||||
|
||||
|
||||
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"
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if isinstance(request.user, AnonymousUser):
|
||||
raise PermissionDenied
|
||||
|
||||
self.object = self.get_object()
|
||||
|
||||
if not request.user.admin \
|
||||
and (self.object.team is not None and request.user not in self.object.team.tournament.organizers.all())\
|
||||
and (self.object.team is not None and self.object.team.selected_for_final
|
||||
and request.user not in Tournament.get_final().organizers.all())\
|
||||
and self.request.user != self.object:
|
||||
raise PermissionDenied
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""
|
||||
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.user.admin:
|
||||
session = request.session
|
||||
session["admin"] = request.user.pk
|
||||
obj = self.get_object()
|
||||
session["_fake_user_id"] = obj.pk
|
||||
return redirect(request.path)
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context["title"] = str(self.object)
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class AddTeamView(LoginRequiredMixin, CreateView):
|
||||
"""
|
||||
Register a new team.
|
||||
Users can choose the name, the trigram and a preferred tournament.
|
||||
"""
|
||||
model = Team
|
||||
form_class = TeamForm
|
||||
|
||||
def form_valid(self, form):
|
||||
if self.request.user.organizes:
|
||||
form.add_error('name', _("You can't organize and participate at the same time."))
|
||||
return self.form_invalid(form)
|
||||
|
||||
if self.request.user.team:
|
||||
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 = ""
|
||||
for i in range(6):
|
||||
code += random.choice(alphabet)
|
||||
team.access_code = code
|
||||
team.validation_status = "0invalid"
|
||||
|
||||
team.save()
|
||||
team.refresh_from_db()
|
||||
|
||||
self.request.user.team = team
|
||||
self.request.user.save()
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy("member:my_team")
|
||||
|
||||
|
||||
class JoinTeamView(LoginRequiredMixin, FormView):
|
||||
"""
|
||||
Join a team with a given access code.
|
||||
"""
|
||||
model = Team
|
||||
form_class = JoinTeam
|
||||
template_name = "tournament/team_form.html"
|
||||
|
||||
def form_valid(self, form):
|
||||
team = form.cleaned_data["team"]
|
||||
|
||||
if self.request.user.organizes:
|
||||
form.add_error('access_code', _("You can't organize and participate at the same time."))
|
||||
return self.form_invalid(form)
|
||||
|
||||
if self.request.user.team:
|
||||
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.coaches) == 3:
|
||||
form.add_error('access_code', _("This team is full of coachs."))
|
||||
return self.form_invalid(form)
|
||||
|
||||
if self.request.user.role == '3participant' and len(team.participants) == 6:
|
||||
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)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy("member:my_team")
|
||||
|
||||
|
||||
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(AccessMixin, 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):
|
||||
try:
|
||||
doc = Document.objects.get(file=self.kwargs["file"])
|
||||
except Document.DoesNotExist:
|
||||
raise Http404(_("No %(verbose_name)s found matching the query") %
|
||||
{'verbose_name': Document._meta.verbose_name})
|
||||
|
||||
if request.user.is_authenticated:
|
||||
grant = request.user.admin
|
||||
|
||||
if isinstance(doc, Solution) or isinstance(doc, Synthesis):
|
||||
grant = grant or doc.team == request.user.team or request.user in doc.tournament.organizers.all()
|
||||
elif isinstance(doc, MotivationLetter):
|
||||
grant = grant or doc.team == request.user.team or request.user in doc.team.tournament.organizers.all()
|
||||
grant = grant or doc.team.selected_for_final and request.user in Tournament.get_final().organizers.all()
|
||||
|
||||
if isinstance(doc, Solution):
|
||||
for pool in doc.pools.all():
|
||||
if request.user in pool.juries.all():
|
||||
grant = True
|
||||
break
|
||||
if pool.round == 2 and timezone.now() < doc.tournament.date_solutions_2:
|
||||
continue
|
||||
if self.request.user.team in pool.teams.all():
|
||||
grant = True
|
||||
elif isinstance(doc, Synthesis):
|
||||
for pool in request.user.pools.all(): # If the user is a jury in the pool
|
||||
if doc.team in pool.teams.all() and doc.final == pool.tournament.final:
|
||||
grant = True
|
||||
break
|
||||
else:
|
||||
pool = Pool.objects.filter(extra_access_token=self.request.session["extra_access_token"])
|
||||
if pool.exists():
|
||||
pool = pool.get()
|
||||
if isinstance(doc, Solution):
|
||||
grant = doc in pool.solutions.all()
|
||||
elif isinstance(doc, Synthesis):
|
||||
grant = doc.team in pool.teams.all() and doc.final == pool.tournament.final
|
||||
else:
|
||||
grant = False
|
||||
else:
|
||||
grant = False
|
||||
|
||||
if not grant:
|
||||
raise PermissionDenied
|
||||
|
||||
return FileResponse(doc.file, content_type="application/pdf", filename=str(doc) + ".pdf")
|
||||
|
||||
|
||||
class ProfileListView(AdminMixin, SingleTableView):
|
||||
"""
|
||||
List all registered profiles.
|
||||
"""
|
||||
model = TFJMUser
|
||||
queryset = TFJMUser.objects.order_by("role", "last_name", "first_name")
|
||||
table_class = UserTable
|
||||
template_name = "member/profile_list.html"
|
||||
extra_context = dict(title=_("All profiles"), type="all")
|
||||
|
||||
|
||||
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")
|
||||
table_class = UserTable
|
||||
template_name = "member/profile_list.html"
|
||||
extra_context = dict(title=_("Orphaned profiles"), type="orphaned")
|
||||
|
||||
|
||||
class OrganizersListView(OrgaMixin, SingleTableView):
|
||||
"""
|
||||
List all organizers.
|
||||
"""
|
||||
model = TFJMUser
|
||||
queryset = TFJMUser.objects.filter(Q(role="0admin") | Q(role="1volunteer"))\
|
||||
.order_by("role", "last_name", "first_name")
|
||||
table_class = UserTable
|
||||
template_name = "member/profile_list.html"
|
||||
extra_context = dict(title=_("Organizers"), type="organizers")
|
||||
|
||||
|
||||
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"]
|
||||
return redirect(request.GET["path"])
|
@ -1 +0,0 @@
|
||||
default_app_config = 'tournament.apps.TournamentConfig'
|
@ -1,31 +0,0 @@
|
||||
from django.contrib.auth.admin import admin
|
||||
|
||||
from .models import Team, Tournament, Pool, Payment
|
||||
|
||||
|
||||
@admin.register(Team)
|
||||
class TeamAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Django admin page for teams.
|
||||
"""
|
||||
|
||||
|
||||
@admin.register(Tournament)
|
||||
class TournamentAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Django admin page for tournaments.
|
||||
"""
|
||||
|
||||
|
||||
@admin.register(Pool)
|
||||
class PoolAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Django admin page for pools.
|
||||
"""
|
||||
|
||||
|
||||
@admin.register(Payment)
|
||||
class PaymentAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Django admin page for payments.
|
||||
"""
|
@ -1,10 +0,0 @@
|
||||
from django.apps import AppConfig
|
||||
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')
|
@ -1,262 +0,0 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
from django import forms
|
||||
from django.db.models import Q
|
||||
from django.template.defaultfilters import filesizeformat
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from member.models import TFJMUser, Solution, Synthesis
|
||||
from tfjm.inputs import DatePickerInput, DateTimePickerInput, AmountInput
|
||||
from tournament.models import Tournament, Team, 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"),
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
|
||||
if not self.instance.pk:
|
||||
if Tournament.objects.filter(name=cleaned_data["data"], year=os.getenv("TFJM_YEAR")):
|
||||
self.add_error("name", _("This tournament already exists."))
|
||||
if cleaned_data["final"] and Tournament.objects.filter(final=True, year=os.getenv("TFJM_YEAR")):
|
||||
self.add_error("name", _("The final tournament was already defined."))
|
||||
|
||||
return cleaned_data
|
||||
|
||||
class Meta:
|
||||
model = Tournament
|
||||
exclude = ('year',)
|
||||
widgets = {
|
||||
"price": AmountInput(),
|
||||
"date_start": DatePickerInput(),
|
||||
"date_end": DatePickerInput(),
|
||||
"date_inscription": DateTimePickerInput(),
|
||||
"date_solutions": DateTimePickerInput(),
|
||||
"date_syntheses": DateTimePickerInput(),
|
||||
"date_solutions_2": DateTimePickerInput(),
|
||||
"date_syntheses_2": DateTimePickerInput(),
|
||||
}
|
||||
|
||||
|
||||
class OrganizerForm(forms.ModelForm):
|
||||
"""
|
||||
Register an organizer in the website.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = TFJMUser
|
||||
fields = ('last_name', 'first_name', 'email', 'is_superuser',)
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
|
||||
if TFJMUser.objects.filter(email=cleaned_data["email"], year=os.getenv("TFJM_YEAR")).exists():
|
||||
self.add_error("email", _("This organizer already exist."))
|
||||
|
||||
return cleaned_data
|
||||
|
||||
def save(self, commit=True):
|
||||
user = self.instance
|
||||
user.role = '0admin' if user.is_superuser else '1volunteer'
|
||||
user.save()
|
||||
super().save(commit)
|
||||
|
||||
|
||||
class TeamForm(forms.ModelForm):
|
||||
"""
|
||||
Add and update a team.
|
||||
"""
|
||||
tournament = forms.ModelChoiceField(
|
||||
Tournament.objects.filter(date_inscription__gte=timezone.now(), final=False),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Team
|
||||
fields = ('name', 'trigram', 'tournament',)
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
|
||||
cleaned_data["trigram"] = cleaned_data["trigram"].upper()
|
||||
|
||||
if not re.match("[A-Z]{3}", cleaned_data["trigram"]):
|
||||
self.add_error("trigram", _("The trigram must be composed of three upcase letters."))
|
||||
|
||||
if not self.instance.pk:
|
||||
if Team.objects.filter(trigram=cleaned_data["trigram"], year=os.getenv("TFJM_YEAR")).exists():
|
||||
self.add_error("trigram", _("This trigram is already used."))
|
||||
|
||||
if Team.objects.filter(name=cleaned_data["name"], year=os.getenv("TFJM_YEAR")).exists():
|
||||
self.add_error("name", _("This name is already used."))
|
||||
|
||||
if cleaned_data["tournament"].date_inscription < timezone.now:
|
||||
self.add_error("tournament", _("This tournament is already closed."))
|
||||
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class JoinTeam(forms.Form):
|
||||
"""
|
||||
Form to join a team with an access code.
|
||||
"""
|
||||
|
||||
access_code = forms.CharField(
|
||||
label=_("Access code"),
|
||||
max_length=6,
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
|
||||
if not re.match("[a-z0-9]{6}", cleaned_data["access_code"]):
|
||||
self.add_error('access_code', _("The access code must be composed of 6 alphanumeric characters."))
|
||||
|
||||
team = Team.objects.filter(access_code=cleaned_data["access_code"])
|
||||
if not team.exists():
|
||||
self.add_error('access_code', _("This access code is invalid."))
|
||||
team = team.get()
|
||||
if not team.invalid:
|
||||
self.add_error('access_code', _("The team is already validated."))
|
||||
cleaned_data["team"] = team
|
||||
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class SolutionForm(forms.ModelForm):
|
||||
"""
|
||||
Form to upload a solution.
|
||||
"""
|
||||
|
||||
problem = forms.ChoiceField(
|
||||
label=_("Problem"),
|
||||
choices=[(str(i), _("Problem #%(problem)d") % {"problem": i}) for i in range(1, 9)],
|
||||
)
|
||||
|
||||
def clean_file(self):
|
||||
content = self.cleaned_data['file']
|
||||
content_type = content.content_type
|
||||
if content_type in ["application/pdf"]:
|
||||
if content.size > 5 * 2 ** 20:
|
||||
raise forms.ValidationError(
|
||||
_('Please keep filesize under %(max_size)s. Current filesize %(current_size)s') % {
|
||||
"max_size": filesizeformat(2 * 2 ** 20),
|
||||
"current_size": filesizeformat(content.size)
|
||||
})
|
||||
else:
|
||||
raise forms.ValidationError(_('The file should be a PDF file.'))
|
||||
return content
|
||||
|
||||
class Meta:
|
||||
model = Solution
|
||||
fields = ('file', 'problem',)
|
||||
|
||||
|
||||
class SynthesisForm(forms.ModelForm):
|
||||
"""
|
||||
Form to upload a synthesis.
|
||||
"""
|
||||
|
||||
def clean_file(self):
|
||||
content = self.cleaned_data['file']
|
||||
content_type = content.content_type
|
||||
if content_type in ["application/pdf"]:
|
||||
if content.size > 5 * 2 ** 20:
|
||||
raise forms.ValidationError(
|
||||
_('Please keep filesize under %(max_size)s. Current filesize %(current_size)s') % {
|
||||
"max_size": filesizeformat(2 * 2 ** 20),
|
||||
"current_size": filesizeformat(content.size)
|
||||
})
|
||||
else:
|
||||
raise forms.ValidationError(_('The file should be a PDF file.'))
|
||||
return content
|
||||
|
||||
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..."),
|
||||
label=_("Team 1"),
|
||||
)
|
||||
|
||||
problem1 = forms.IntegerField(
|
||||
min_value=1,
|
||||
max_value=8,
|
||||
initial=1,
|
||||
label=_("Problem defended by team 1"),
|
||||
)
|
||||
|
||||
team2 = forms.ModelChoiceField(
|
||||
Team.objects.filter(validation_status="2valid").all(),
|
||||
empty_label=_("Choose a team..."),
|
||||
label=_("Team 2"),
|
||||
)
|
||||
|
||||
problem2 = forms.IntegerField(
|
||||
min_value=1,
|
||||
max_value=8,
|
||||
initial=2,
|
||||
label=_("Problem defended by team 2"),
|
||||
)
|
||||
|
||||
team3 = forms.ModelChoiceField(
|
||||
Team.objects.filter(validation_status="2valid").all(),
|
||||
empty_label=_("Choose a team..."),
|
||||
label=_("Team 3"),
|
||||
)
|
||||
|
||||
problem3 = forms.IntegerField(
|
||||
min_value=1,
|
||||
max_value=8,
|
||||
initial=3,
|
||||
label=_("Problem defended by team 3"),
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
|
||||
team1, pb1 = cleaned_data["team1"], cleaned_data["problem1"]
|
||||
team2, pb2 = cleaned_data["team2"], cleaned_data["problem2"]
|
||||
team3, pb3 = cleaned_data["team3"], cleaned_data["problem3"]
|
||||
|
||||
sol1 = Solution.objects.get(team=team1, problem=pb1, final=team1.selected_for_final)
|
||||
sol2 = Solution.objects.get(team=team2, problem=pb2, final=team2.selected_for_final)
|
||||
sol3 = Solution.objects.get(team=team3, problem=pb3, final=team3.selected_for_final)
|
||||
|
||||
cleaned_data["teams"] = [team1, team2, team3]
|
||||
cleaned_data["solutions"] = [sol1, sol2, sol3]
|
||||
|
||||
return cleaned_data
|
||||
|
||||
def save(self, commit=True):
|
||||
pool = super().save(commit)
|
||||
|
||||
pool.refresh_from_db()
|
||||
pool.teams.set(self.cleaned_data["teams"])
|
||||
pool.solutions.set(self.cleaned_data["solutions"])
|
||||
pool.save()
|
||||
|
||||
return pool
|
||||
|
||||
class Meta:
|
||||
model = Pool
|
||||
fields = ('round', 'juries',)
|
@ -1,92 +0,0 @@
|
||||
# Generated by Django 3.1 on 2020-09-19 18:15
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('member', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Tournament',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255, verbose_name='name')),
|
||||
('size', models.PositiveSmallIntegerField(help_text='Number of teams that are allowed to join the tournament.', verbose_name='size')),
|
||||
('place', models.CharField(max_length=255, verbose_name='place')),
|
||||
('price', models.PositiveSmallIntegerField(help_text='Price asked to participants. Free with a scholarship.', verbose_name='price')),
|
||||
('description', models.TextField(verbose_name='description')),
|
||||
('date_start', models.DateField(default=django.utils.timezone.now, verbose_name='date start')),
|
||||
('date_end', models.DateField(default=django.utils.timezone.now, verbose_name='date end')),
|
||||
('date_inscription', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date of registration closing')),
|
||||
('date_solutions', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date of maximal solution submission')),
|
||||
('date_syntheses', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date of maximal syntheses submission for the first round')),
|
||||
('date_solutions_2', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date when solutions of round 2 are available')),
|
||||
('date_syntheses_2', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date of maximal syntheses submission for the second round')),
|
||||
('final', models.BooleanField(help_text='It should be only one final tournament.', verbose_name='final tournament')),
|
||||
('year', models.PositiveIntegerField(default=2020, verbose_name='year')),
|
||||
('organizers', models.ManyToManyField(help_text='List of all organizers that can see and manipulate data of the tournament and the teams.', related_name='organized_tournaments', to=settings.AUTH_USER_MODEL, verbose_name='organizers')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'tournament',
|
||||
'verbose_name_plural': 'tournaments',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Team',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255, verbose_name='name')),
|
||||
('trigram', models.CharField(help_text='The trigram should be composed of 3 capitalize letters, that is a funny acronym for the team.', max_length=3, verbose_name='trigram')),
|
||||
('inscription_date', models.DateTimeField(auto_now_add=True, verbose_name='inscription date')),
|
||||
('validation_status', models.CharField(choices=[('0invalid', 'Registration not validated'), ('1waiting', 'Waiting for validation'), ('2valid', 'Registration validated')], max_length=8, verbose_name='validation status')),
|
||||
('selected_for_final', models.BooleanField(default=False, verbose_name='selected for final')),
|
||||
('access_code', models.CharField(max_length=6, unique=True, verbose_name='access code')),
|
||||
('year', models.PositiveIntegerField(default=2020, verbose_name='year')),
|
||||
('tournament', models.ForeignKey(help_text='The tournament where the team is registered.', on_delete=django.db.models.deletion.PROTECT, related_name='_teams', to='tournament.tournament', verbose_name='tournament')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'team',
|
||||
'verbose_name_plural': 'teams',
|
||||
'unique_together': {('name', 'year'), ('trigram', 'year')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Pool',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('round', models.PositiveIntegerField(choices=[(1, 'Round 1'), (2, 'Round 2')], verbose_name='round')),
|
||||
('extra_access_token', models.CharField(default='', help_text='Let other users access to the pool data without logging in.', max_length=64, verbose_name='extra access token')),
|
||||
('juries', models.ManyToManyField(related_name='pools', to=settings.AUTH_USER_MODEL, verbose_name='juries')),
|
||||
('solutions', models.ManyToManyField(related_name='pools', to='member.Solution', verbose_name='solutions')),
|
||||
('teams', models.ManyToManyField(related_name='pools', to='tournament.Team', verbose_name='teams')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'pool',
|
||||
'verbose_name_plural': 'pools',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Payment',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('method', models.CharField(choices=[('not_paid', 'Not paid'), ('credit_card', 'Credit card'), ('check', 'Bank check'), ('transfer', 'Bank transfer'), ('cash', 'Cash'), ('scholarship', 'Scholarship')], default='not_paid', max_length=16, verbose_name='payment method')),
|
||||
('validation_status', models.CharField(choices=[('0invalid', 'Registration not validated'), ('1waiting', 'Waiting for validation'), ('2valid', 'Registration validated')], max_length=8, verbose_name='validation status')),
|
||||
('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to='tournament.team', verbose_name='team')),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='payment', to=settings.AUTH_USER_MODEL, verbose_name='user')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'payment',
|
||||
'verbose_name_plural': 'payments',
|
||||
},
|
||||
),
|
||||
]
|
@ -1,432 +0,0 @@
|
||||
import os
|
||||
import random
|
||||
|
||||
from django.core.mail import send_mail
|
||||
from django.db import models
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils import timezone
|
||||
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"),
|
||||
)
|
||||
|
||||
organizers = models.ManyToManyField(
|
||||
'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(
|
||||
max_length=255,
|
||||
verbose_name=_("place"),
|
||||
)
|
||||
|
||||
price = models.PositiveSmallIntegerField(
|
||||
verbose_name=_("price"),
|
||||
help_text=_("Price asked to participants. Free with a scholarship."),
|
||||
)
|
||||
|
||||
description = models.TextField(
|
||||
verbose_name=_("description"),
|
||||
)
|
||||
|
||||
date_start = models.DateField(
|
||||
default=timezone.now,
|
||||
verbose_name=_("date start"),
|
||||
)
|
||||
|
||||
date_end = models.DateField(
|
||||
default=timezone.now,
|
||||
verbose_name=_("date end"),
|
||||
)
|
||||
|
||||
date_inscription = models.DateTimeField(
|
||||
default=timezone.now,
|
||||
verbose_name=_("date of registration closing"),
|
||||
)
|
||||
|
||||
date_solutions = models.DateTimeField(
|
||||
default=timezone.now,
|
||||
verbose_name=_("date of maximal solution submission"),
|
||||
)
|
||||
|
||||
date_syntheses = models.DateTimeField(
|
||||
default=timezone.now,
|
||||
verbose_name=_("date of maximal syntheses submission for the first round"),
|
||||
)
|
||||
|
||||
date_solutions_2 = models.DateTimeField(
|
||||
default=timezone.now,
|
||||
verbose_name=_("date when solutions of round 2 are available"),
|
||||
)
|
||||
|
||||
date_syntheses_2 = models.DateTimeField(
|
||||
default=timezone.now,
|
||||
verbose_name=_("date of maximal syntheses submission for the second round"),
|
||||
)
|
||||
|
||||
final = models.BooleanField(
|
||||
verbose_name=_("final tournament"),
|
||||
help_text=_("It should be only one final tournament."),
|
||||
)
|
||||
|
||||
year = models.PositiveIntegerField(
|
||||
default=os.getenv("TFJM_YEAR", timezone.now().year),
|
||||
verbose_name=_("year"),
|
||||
)
|
||||
|
||||
@property
|
||||
def teams(self):
|
||||
"""
|
||||
Get all teams that are registered to this tournament, with a distinction for the final tournament.
|
||||
"""
|
||||
return self._teams if not self.final else Team.objects.filter(selected_for_final=True)
|
||||
|
||||
@property
|
||||
def linked_organizers(self):
|
||||
"""
|
||||
Display a list of the organizers with links to their personal page.
|
||||
"""
|
||||
return ['<a href="{url}">'.format(url=reverse_lazy("member:information", args=(user.pk,))) + str(user) + '</a>'
|
||||
for user in self.organizers.all()]
|
||||
|
||||
@property
|
||||
def solutions(self):
|
||||
"""
|
||||
Get all sent solutions for this tournament.
|
||||
"""
|
||||
from member.models import Solution
|
||||
return Solution.objects.filter(final=self.final) if self.final \
|
||||
else Solution.objects.filter(team__tournament=self, 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, 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:
|
||||
verbose_name = _("tournament")
|
||||
verbose_name_plural = _("tournaments")
|
||||
|
||||
def send_mail_to_organizers(self, template_name, subject="Contact TFJM²", **kwargs):
|
||||
"""
|
||||
Send a mail to all organizers of the tournament.
|
||||
The template of the mail should be found either in templates/mail_templates/<template_name>.html for the HTML
|
||||
version and in templates/mail_templates/<template_name>.txt for the plain text version.
|
||||
The context of the template contains the tournament and the user. Extra context can be given through the kwargs.
|
||||
"""
|
||||
context = kwargs
|
||||
context["tournament"] = self
|
||||
for user in self.organizers.all():
|
||||
context["user"] = user
|
||||
message = render_to_string("mail_templates/" + template_name + ".txt", context=context)
|
||||
message_html = render_to_string("mail_templates/" + template_name + ".html", context=context)
|
||||
send_mail(subject, message, "contact@tfjm.org", [user.email], html_message=message_html)
|
||||
from member.models import TFJMUser
|
||||
for user in TFJMUser.objects.get(is_superuser=True).all():
|
||||
context["user"] = user
|
||||
message = render_to_string("mail_templates/" + template_name + ".txt", context=context)
|
||||
message_html = render_to_string("mail_templates/" + template_name + ".html", context=context)
|
||||
send_mail(subject, message, "contact@tfjm.org", [user.email], html_message=message_html)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Team(models.Model):
|
||||
"""
|
||||
Store information about a registered team.
|
||||
"""
|
||||
|
||||
name = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_("name"),
|
||||
)
|
||||
|
||||
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(
|
||||
Tournament,
|
||||
on_delete=models.PROTECT,
|
||||
related_name="_teams",
|
||||
verbose_name=_("tournament"),
|
||||
help_text=_("The tournament where the team is registered."),
|
||||
)
|
||||
|
||||
inscription_date = models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
verbose_name=_("inscription date"),
|
||||
)
|
||||
|
||||
validation_status = models.CharField(
|
||||
max_length=8,
|
||||
choices=[
|
||||
("0invalid", _("Registration not validated")),
|
||||
("1waiting", _("Waiting for validation")),
|
||||
("2valid", _("Registration validated")),
|
||||
],
|
||||
verbose_name=_("validation status"),
|
||||
)
|
||||
|
||||
selected_for_final = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("selected for final"),
|
||||
)
|
||||
|
||||
access_code = models.CharField(
|
||||
max_length=6,
|
||||
unique=True,
|
||||
verbose_name=_("access code"),
|
||||
)
|
||||
|
||||
year = models.PositiveIntegerField(
|
||||
default=os.getenv("TFJM_YEAR", timezone.now().year),
|
||||
verbose_name=_("year"),
|
||||
)
|
||||
|
||||
@property
|
||||
def valid(self):
|
||||
return self.validation_status == "2valid"
|
||||
|
||||
@property
|
||||
def waiting(self):
|
||||
return self.validation_status == "1waiting"
|
||||
|
||||
@property
|
||||
def invalid(self):
|
||||
return self.validation_status == "0invalid"
|
||||
|
||||
@property
|
||||
def coaches(self):
|
||||
"""
|
||||
Get all coaches of a team.
|
||||
"""
|
||||
return self.users.all().filter(role="2coach")
|
||||
|
||||
@property
|
||||
def linked_coaches(self):
|
||||
"""
|
||||
Get a list of the coaches of a team with html links to their pages.
|
||||
"""
|
||||
return ['<a href="{url}">'.format(url=reverse_lazy("member:information", args=(user.pk,))) + str(user) + '</a>'
|
||||
for user in self.coaches]
|
||||
|
||||
@property
|
||||
def participants(self):
|
||||
"""
|
||||
Get all particpants of a team, coaches excluded.
|
||||
"""
|
||||
return self.users.all().filter(role="3participant")
|
||||
|
||||
@property
|
||||
def linked_participants(self):
|
||||
"""
|
||||
Get a list of the participants of a team with html links to their pages.
|
||||
"""
|
||||
return ['<a href="{url}">'.format(url=reverse_lazy("member:information", args=(user.pk,))) + str(user) + '</a>'
|
||||
for user in self.participants]
|
||||
|
||||
@property
|
||||
def future_tournament(self):
|
||||
"""
|
||||
Get the last tournament where the team is registered.
|
||||
Only matters if the team is selected for final: if this is the case, we return the final tournament.
|
||||
Useful for deadlines.
|
||||
"""
|
||||
return Tournament.get_final() if self.selected_for_final else self.tournament
|
||||
|
||||
@property
|
||||
def can_validate(self):
|
||||
"""
|
||||
Check if a given team is able to ask for validation.
|
||||
A team can validate if:
|
||||
* All participants filled the photo consent
|
||||
* Minor participants filled the parental consent
|
||||
* Minor participants filled the sanitary plug
|
||||
* Teams sent their motivation letter
|
||||
* The team contains at least 4 participants
|
||||
* The team contains at least 1 coach
|
||||
"""
|
||||
# TODO In a normal time, team needs a motivation letter and authorizations.
|
||||
return self.coaches.exists() and self.participants.count() >= 4\
|
||||
and self.tournament.date_inscription <= timezone.now()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("team")
|
||||
verbose_name_plural = _("teams")
|
||||
unique_together = (('name', 'year',), ('trigram', 'year',),)
|
||||
|
||||
def send_mail(self, template_name, subject="Contact TFJM²", **kwargs):
|
||||
"""
|
||||
Send a mail to all members of a team with a given template.
|
||||
The template of the mail should be found either in templates/mail_templates/<template_name>.html for the HTML
|
||||
version and in templates/mail_templates/<template_name>.txt for the plain text version.
|
||||
The context of the template contains the team and the user. Extra context can be given through the kwargs.
|
||||
"""
|
||||
context = kwargs
|
||||
context["team"] = self
|
||||
for user in self.users.all():
|
||||
context["user"] = user
|
||||
message = render_to_string("mail_templates/" + template_name + ".txt", context=context)
|
||||
message_html = render_to_string("mail_templates/" + template_name + ".html", context=context)
|
||||
send_mail(subject, message, "contact@tfjm.org", [user.email], html_message=message_html)
|
||||
|
||||
def __str__(self):
|
||||
return self.trigram + " — " + self.name
|
||||
|
||||
|
||||
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",
|
||||
verbose_name=_("teams"),
|
||||
)
|
||||
|
||||
solutions = models.ManyToManyField(
|
||||
"member.Solution",
|
||||
related_name="pools",
|
||||
verbose_name=_("solutions"),
|
||||
)
|
||||
|
||||
round = models.PositiveIntegerField(
|
||||
choices=[
|
||||
(1, _("Round 1")),
|
||||
(2, _("Round 2")),
|
||||
],
|
||||
verbose_name=_("round"),
|
||||
)
|
||||
|
||||
juries = models.ManyToManyField(
|
||||
"member.TFJMUser",
|
||||
related_name="pools",
|
||||
verbose_name=_("juries"),
|
||||
)
|
||||
|
||||
extra_access_token = models.CharField(
|
||||
max_length=64,
|
||||
default="",
|
||||
verbose_name=_("extra access token"),
|
||||
help_text=_("Let other users access to the pool data without logging in."),
|
||||
)
|
||||
|
||||
@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)
|
||||
|
||||
def save(self, **kwargs):
|
||||
if not self.extra_access_token:
|
||||
alphabet = "0123456789abcdefghijklmnopqrstuvwxyz0123456789"
|
||||
code = "".join(random.choice(alphabet) for _ in range(64))
|
||||
self.extra_access_token = code
|
||||
super().save(**kwargs)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("pool")
|
||||
verbose_name_plural = _("pools")
|
||||
|
||||
|
||||
class Payment(models.Model):
|
||||
"""
|
||||
Store some information about payments, to recover data.
|
||||
TODO: handle it...
|
||||
"""
|
||||
user = models.OneToOneField(
|
||||
'member.TFJMUser',
|
||||
on_delete=models.CASCADE,
|
||||
related_name="payment",
|
||||
verbose_name=_("user"),
|
||||
)
|
||||
|
||||
team = models.ForeignKey(
|
||||
Team,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="payments",
|
||||
verbose_name=_("team"),
|
||||
)
|
||||
|
||||
method = models.CharField(
|
||||
max_length=16,
|
||||
choices=[
|
||||
("not_paid", _("Not paid")),
|
||||
("credit_card", _("Credit card")),
|
||||
("check", _("Bank check")),
|
||||
("transfer", _("Bank transfer")),
|
||||
("cash", _("Cash")),
|
||||
("scholarship", _("Scholarship")),
|
||||
],
|
||||
default="not_paid",
|
||||
verbose_name=_("payment method"),
|
||||
)
|
||||
|
||||
validation_status = models.CharField(
|
||||
max_length=8,
|
||||
choices=[
|
||||
("0invalid", _("Registration not validated")),
|
||||
("1waiting", _("Waiting for validation")),
|
||||
("2valid", _("Registration validated")),
|
||||
],
|
||||
verbose_name=_("validation status"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("payment")
|
||||
verbose_name_plural = _("payments")
|
||||
|
||||
def __str__(self):
|
||||
return _("Payment of {user}").format(str(self.user))
|
@ -1,164 +0,0 @@
|
||||
import django_tables2 as tables
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.html import format_html
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_tables2 import A
|
||||
|
||||
from member.models import Solution, Synthesis
|
||||
from .models import Tournament, Team, Pool
|
||||
|
||||
|
||||
class TournamentTable(tables.Table):
|
||||
"""
|
||||
List all tournaments.
|
||||
"""
|
||||
|
||||
name = tables.LinkColumn(
|
||||
"tournament:detail",
|
||||
args=[A("pk")],
|
||||
)
|
||||
|
||||
date_start = tables.Column(
|
||||
verbose_name=_("dates").capitalize(),
|
||||
)
|
||||
|
||||
def render_date_start(self, record):
|
||||
return _("From {start:%b %d %Y} to {end:%b %d %Y}").format(start=record.date_start, end=record.date_end)
|
||||
|
||||
class Meta:
|
||||
model = Tournament
|
||||
fields = ("name", "date_start", "date_inscription", "date_solutions", "size", )
|
||||
attrs = {
|
||||
'class': 'table table-condensed table-striped table-hover'
|
||||
}
|
||||
order_by = ('date_start', 'name',)
|
||||
|
||||
|
||||
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")],
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Team
|
||||
fields = ("name", "trigram", "validation_status", )
|
||||
attrs = {
|
||||
'class': 'table table-condensed table-striped table-hover'
|
||||
}
|
||||
order_by = ('-validation_status', 'trigram',)
|
||||
|
||||
|
||||
class SolutionTable(tables.Table):
|
||||
"""
|
||||
Display a table of some solutions.
|
||||
"""
|
||||
|
||||
team = tables.LinkColumn(
|
||||
"tournament:team_detail",
|
||||
args=[A("team.pk")],
|
||||
)
|
||||
|
||||
tournament = tables.LinkColumn(
|
||||
"tournament:detail",
|
||||
args=[A("tournament.pk")],
|
||||
accessor=A("tournament"),
|
||||
order_by=("team__tournament__date_start", "team__tournament__name",),
|
||||
verbose_name=_("Tournament"),
|
||||
)
|
||||
|
||||
file = tables.LinkColumn(
|
||||
"document",
|
||||
args=[A("file")],
|
||||
attrs={
|
||||
"a": {
|
||||
"data-turbolinks": "false",
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
def render_file(self):
|
||||
return _("Download")
|
||||
|
||||
class Meta:
|
||||
model = Solution
|
||||
fields = ("team", "tournament", "problem", "uploaded_at", "file", )
|
||||
attrs = {
|
||||
'class': 'table table-condensed table-striped table-hover'
|
||||
}
|
||||
|
||||
|
||||
class SynthesisTable(tables.Table):
|
||||
"""
|
||||
Display a table of some syntheses.
|
||||
"""
|
||||
|
||||
team = tables.LinkColumn(
|
||||
"tournament:team_detail",
|
||||
args=[A("team.pk")],
|
||||
)
|
||||
|
||||
tournament = tables.LinkColumn(
|
||||
"tournament:detail",
|
||||
args=[A("tournament.pk")],
|
||||
accessor=A("tournament"),
|
||||
order_by=("team__tournament__date_start", "team__tournament__name",),
|
||||
verbose_name=_("tournament"),
|
||||
)
|
||||
|
||||
file = tables.LinkColumn(
|
||||
"document",
|
||||
args=[A("file")],
|
||||
attrs={
|
||||
"a": {
|
||||
"data-turbolinks": "false",
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
def render_file(self):
|
||||
return _("Download")
|
||||
|
||||
class Meta:
|
||||
model = Synthesis
|
||||
fields = ("team", "tournament", "round", "source", "uploaded_at", "file", )
|
||||
attrs = {
|
||||
'class': 'table table-condensed table-striped table-hover'
|
||||
}
|
||||
|
||||
|
||||
class PoolTable(tables.Table):
|
||||
"""
|
||||
Display a table of some pools.
|
||||
"""
|
||||
|
||||
problems = tables.Column(
|
||||
verbose_name=_("Problems"),
|
||||
orderable=False,
|
||||
)
|
||||
|
||||
tournament = tables.LinkColumn(
|
||||
"tournament:detail",
|
||||
args=[A("tournament.pk")],
|
||||
verbose_name=_("Tournament"),
|
||||
order_by=("teams__tournament__date_start", "teams__tournament__name",),
|
||||
)
|
||||
|
||||
def render_teams(self, record, value):
|
||||
return format_html('<a href="{url}">{trigrams}</a>',
|
||||
url=reverse_lazy('tournament:pool_detail', args=(record.pk,)),
|
||||
trigrams=", ".join(team.trigram for team in value.all()))
|
||||
|
||||
def render_problems(self, value):
|
||||
return ", ".join([str(pb) for pb in value])
|
||||
|
||||
class Meta:
|
||||
model = Pool
|
||||
fields = ("teams", "tournament", "problems", "round", )
|
||||
attrs = {
|
||||
'class': 'table table-condensed table-striped table-hover'
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import TournamentListView, TournamentCreateView, TournamentDetailView, TournamentUpdateView, \
|
||||
TeamDetailView, TeamUpdateView, AddOrganizerView, SolutionsView, SolutionsOrgaListView, SynthesesView, \
|
||||
SynthesesOrgaListView, PoolListView, PoolCreateView, PoolDetailView
|
||||
|
||||
app_name = "tournament"
|
||||
|
||||
urlpatterns = [
|
||||
path('list/', TournamentListView.as_view(), name="list"),
|
||||
path("add/", TournamentCreateView.as_view(), name="add"),
|
||||
path('<int:pk>/', TournamentDetailView.as_view(), name="detail"),
|
||||
path('<int:pk>/update/', TournamentUpdateView.as_view(), name="update"),
|
||||
path('team/<int:pk>/', TeamDetailView.as_view(), name="team_detail"),
|
||||
path('team/<int:pk>/update/', TeamUpdateView.as_view(), name="team_update"),
|
||||
path("add-organizer/", AddOrganizerView.as_view(), name="add_organizer"),
|
||||
path("solutions/", SolutionsView.as_view(), name="solutions"),
|
||||
path("all-solutions/", SolutionsOrgaListView.as_view(), name="all_solutions"),
|
||||
path("syntheses/", SynthesesView.as_view(), name="syntheses"),
|
||||
path("all_syntheses/", SynthesesOrgaListView.as_view(), name="all_syntheses"),
|
||||
path("pools/", PoolListView.as_view(), name="pools"),
|
||||
path("pool/add/", PoolCreateView.as_view(), name="create_pool"),
|
||||
path("pool/<int:pk>/", PoolDetailView.as_view(), name="pool_detail"),
|
||||
]
|
@ -1,662 +0,0 @@
|
||||
import random
|
||||
import zipfile
|
||||
from datetime import timedelta
|
||||
from io import BytesIO
|
||||
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin, AccessMixin
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.core.mail import send_mail
|
||||
from django.db.models import Q
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import redirect
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import DetailView, CreateView, UpdateView
|
||||
from django.views.generic.edit import BaseFormView
|
||||
from django_tables2.views import SingleTableView
|
||||
from member.models import TFJMUser, Solution, Synthesis
|
||||
|
||||
from .forms import TournamentForm, OrganizerForm, SolutionForm, SynthesisForm, TeamForm, PoolForm
|
||||
from .models import Tournament, Team, Pool
|
||||
from .tables import TournamentTable, TeamTable, SolutionTable, SynthesisTable, PoolTable
|
||||
|
||||
|
||||
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
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
|
||||
class OrgaMixin(AccessMixin):
|
||||
"""
|
||||
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 and not request.session["extra_access_token"]:
|
||||
return self.handle_no_permission()
|
||||
elif request.user.is_authenticated and not request.user.organizes:
|
||||
raise PermissionDenied
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
|
||||
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
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
|
||||
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"),)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
team_users = TFJMUser.objects.filter(Q(team__isnull=False) | Q(role="admin") | Q(role="organizer"))\
|
||||
.order_by('-role')
|
||||
valid_team_users = team_users.filter(
|
||||
Q(team__validation_status="2valid") | Q(role="admin") | Q(role="organizer"))
|
||||
|
||||
context["team_users_emails"] = [user.email for user in team_users]
|
||||
context["valid_team_users_emails"] = [user.email for user in valid_team_users]
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class TournamentCreateView(AdminMixin, CreateView):
|
||||
"""
|
||||
Create a tournament. Only accessible to admins.
|
||||
"""
|
||||
|
||||
model = Tournament
|
||||
form_class = TournamentForm
|
||||
extra_context = dict(title=_("Add tournament"),)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('tournament:detail', args=(self.object.pk,))
|
||||
|
||||
|
||||
class TournamentDetailView(DetailView):
|
||||
"""
|
||||
Display the detail of a tournament.
|
||||
Accessible to all, including not authenticated users.
|
||||
"""
|
||||
|
||||
model = Tournament
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context["title"] = _("Tournament of {name}").format(name=self.object.name)
|
||||
|
||||
if self.object.final:
|
||||
team_users = TFJMUser.objects.filter(team__selected_for_final=True)
|
||||
valid_team_users = team_users
|
||||
else:
|
||||
team_users = TFJMUser.objects.filter(
|
||||
Q(team__tournament=self.object)
|
||||
| Q(organized_tournaments=self.object)).order_by('role')
|
||||
valid_team_users = team_users.filter(
|
||||
Q(team__validation_status="2valid")
|
||||
| Q(role="admin")
|
||||
| Q(organized_tournaments=self.object))
|
||||
|
||||
context["team_users_emails"] = [user.email for user in team_users]
|
||||
context["valid_team_users_emails"] = [user.email for user in valid_team_users]
|
||||
|
||||
context["teams"] = TeamTable(self.object.teams.all())
|
||||
|
||||
return context
|
||||
|
||||
|
||||
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"),)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('tournament:detail', args=(self.object.pk,))
|
||||
|
||||
|
||||
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 not (self.get_object().selected_for_final
|
||||
and request.user in Tournament.get_final().organizers.all())
|
||||
and self.get_object() != request.user.team):
|
||||
raise PermissionDenied
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""
|
||||
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()
|
||||
|
||||
out = BytesIO()
|
||||
zf = zipfile.ZipFile(out, "w")
|
||||
|
||||
for solution in solutions:
|
||||
zf.write(solution.file.path, str(solution) + ".pdf")
|
||||
|
||||
zf.close()
|
||||
|
||||
resp = HttpResponse(out.getvalue(), content_type="application/x-zip-compressed")
|
||||
resp['Content-Disposition'] = 'attachment; filename={}'\
|
||||
.format(_("Solutions for team {team}.zip")
|
||||
.format(team=str(team)).replace(" ", "%20"))
|
||||
return resp
|
||||
elif "leave" in request.POST and request.user.participates:
|
||||
request.user.team = None
|
||||
request.user.save()
|
||||
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 and team.can_validate:
|
||||
team.validation_status = "1waiting"
|
||||
team.save()
|
||||
team.tournament.send_mail_to_organizers("request_validation", "Demande de validation TFJM²", team=team)
|
||||
return redirect('tournament:team_detail', pk=team.pk)
|
||||
elif "validate" in request.POST and request.user.organizes:
|
||||
team.validation_status = "2valid"
|
||||
team.save()
|
||||
team.send_mail("validate_team", "Équipe validée TFJM²")
|
||||
return redirect('tournament:team_detail', pk=team.pk)
|
||||
elif "invalidate" in request.POST and request.user.organizes:
|
||||
team.validation_status = "0invalid"
|
||||
team.save()
|
||||
team.send_mail("unvalidate_team", "Équipe non validée TFJM²")
|
||||
return redirect('tournament:team_detail', pk=team.pk)
|
||||
elif "delete" in request.POST and request.user.organizes:
|
||||
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 = ""
|
||||
for i in range(64):
|
||||
id += random.choice(alphabet)
|
||||
with solution.file.open("rb") as source:
|
||||
with open("/code/media/" + id, "wb") as dest:
|
||||
for chunk in source.chunks():
|
||||
dest.write(chunk)
|
||||
new_sol = Solution(
|
||||
file=id,
|
||||
team=team,
|
||||
problem=solution.problem,
|
||||
final=True,
|
||||
)
|
||||
new_sol.save()
|
||||
team.selected_for_final = True
|
||||
team.save()
|
||||
team.send_mail("select_for_final", "Sélection pour la finale, félicitations ! - TFJM²",
|
||||
final=Tournament.get_final())
|
||||
return redirect('tournament:team_detail', pk=team.pk)
|
||||
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context["title"] = _("Information about team")
|
||||
context["ordered_solutions"] = self.object.solutions.order_by('final', 'problem',).all()
|
||||
context["team_users_emails"] = [user.email for user in self.object.users.all()]
|
||||
|
||||
return context
|
||||
|
||||
|
||||
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"),)
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not request.user.admin and self.request.user not in self.get_object().tournament.organizers.all() \
|
||||
and self.get_object() != self.request.user.team:
|
||||
raise PermissionDenied
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
|
||||
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"),)
|
||||
template_name = "tournament/add_organizer.html"
|
||||
|
||||
def form_valid(self, form):
|
||||
user = form.instance
|
||||
msg = render_to_string("mail_templates/add_organizer.txt", context=dict(user=user))
|
||||
msg_html = render_to_string("mail_templates/add_organizer.html", context=dict(user=user))
|
||||
send_mail('Organisateur du TFJM² 2020', msg, 'contact@tfjm.org', [user.email], html_message=msg_html)
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('index')
|
||||
|
||||
|
||||
class SolutionsView(TeamMixin, BaseFormView, SingleTableView):
|
||||
"""
|
||||
Upload and view solutions for a team.
|
||||
"""
|
||||
|
||||
model = Solution
|
||||
table_class = SolutionTable
|
||||
form_class = SolutionForm
|
||||
template_name = "tournament/solutions_list.html"
|
||||
extra_context = dict(title=_("Solutions"))
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if "zip" in request.POST:
|
||||
solutions = request.user.team.solutions
|
||||
|
||||
out = BytesIO()
|
||||
zf = zipfile.ZipFile(out, "w")
|
||||
|
||||
for solution in solutions:
|
||||
zf.write(solution.file.path, str(solution) + ".pdf")
|
||||
|
||||
zf.close()
|
||||
|
||||
resp = HttpResponse(out.getvalue(), content_type="application/x-zip-compressed")
|
||||
resp['Content-Disposition'] = 'attachment; filename={}'\
|
||||
.format(_("Solutions for team {team}.zip")
|
||||
.format(team=str(request.user.team)).replace(" ", "%20"))
|
||||
return resp
|
||||
|
||||
return super().post(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
self.object_list = self.get_queryset()
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["now"] = timezone.now()
|
||||
context["real_deadline"] = self.request.user.team.future_tournament.date_solutions + timedelta(minutes=30)
|
||||
return context
|
||||
|
||||
def get_queryset(self):
|
||||
qs = super().get_queryset().filter(team=self.request.user.team)
|
||||
return qs.order_by('final', 'team__tournament__date_start', 'team__tournament__name', 'team__trigram',
|
||||
'problem',)
|
||||
|
||||
def form_valid(self, form):
|
||||
solution = form.instance
|
||||
solution.team = self.request.user.team
|
||||
solution.final = solution.team.selected_for_final
|
||||
|
||||
if timezone.now() > solution.tournament.date_solutions + timedelta(minutes=30):
|
||||
form.add_error('file', _("You can't publish your solution anymore. Deadline: {date:%m-%d-%Y %H:%M}.")
|
||||
.format(date=timezone.localtime(solution.tournament.date_solutions)))
|
||||
return super().form_invalid(form)
|
||||
|
||||
prev_sol = Solution.objects.filter(problem=solution.problem, team=solution.team, final=solution.final)
|
||||
for sol in prev_sol.all():
|
||||
sol.delete()
|
||||
alphabet = "0123456789abcdefghijklmnopqrstuvwxyz0123456789"
|
||||
id = ""
|
||||
for i in range(64):
|
||||
id += random.choice(alphabet)
|
||||
solution.file.name = id
|
||||
solution.save()
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy("tournament:solutions")
|
||||
|
||||
|
||||
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"
|
||||
extra_context = dict(title=_("All solutions"))
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if "tournament_zip" in request.POST:
|
||||
tournament = Tournament.objects.get(pk=int(request.POST["tournament_zip"]))
|
||||
solutions = tournament.solutions
|
||||
if not request.user.admin and request.user not in tournament.organizers.all():
|
||||
raise PermissionDenied
|
||||
|
||||
out = BytesIO()
|
||||
zf = zipfile.ZipFile(out, "w")
|
||||
|
||||
for solution in solutions:
|
||||
zf.write(solution.file.path, str(solution) + ".pdf")
|
||||
|
||||
zf.close()
|
||||
|
||||
resp = HttpResponse(out.getvalue(), content_type="application/x-zip-compressed")
|
||||
resp['Content-Disposition'] = 'attachment; filename={}'\
|
||||
.format(_("Solutions for tournament {tournament}.zip")
|
||||
.format(tournament=str(tournament)).replace(" ", "%20"))
|
||||
return resp
|
||||
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
if self.request.user.is_authenticated:
|
||||
context["tournaments"] = \
|
||||
Tournament.objects if self.request.user.admin else self.request.user.organized_tournaments
|
||||
|
||||
return context
|
||||
|
||||
def get_queryset(self):
|
||||
qs = super().get_queryset()
|
||||
if self.request.user.is_authenticated and not self.request.user.admin:
|
||||
if self.request.user in Tournament.get_final().organizers.all():
|
||||
qs = qs.filter(Q(team__tournament__organizers=self.request.user) | Q(pools__juries=self.request.user)
|
||||
| Q(final=True))
|
||||
else:
|
||||
qs = qs.filter(Q(team__tournament__organizers=self.request.user) | Q(pools__juries=self.request.user))
|
||||
elif not self.request.user.is_authenticated:
|
||||
qs = qs.filter(pools__extra_access_token=self.request.session["extra_access_token"])
|
||||
return qs.order_by('final', 'team__tournament__date_start', 'team__tournament__name', 'team__trigram',
|
||||
'problem',).distinct()
|
||||
|
||||
|
||||
class SynthesesView(TeamMixin, BaseFormView, SingleTableView):
|
||||
"""
|
||||
Upload and view syntheses for a team.
|
||||
"""
|
||||
model = Synthesis
|
||||
table_class = SynthesisTable
|
||||
form_class = SynthesisForm
|
||||
template_name = "tournament/syntheses_list.html"
|
||||
extra_context = dict(title=_("Syntheses"))
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if "zip" in request.POST:
|
||||
syntheses = request.user.team.syntheses
|
||||
|
||||
out = BytesIO()
|
||||
zf = zipfile.ZipFile(out, "w")
|
||||
|
||||
for synthesis in syntheses:
|
||||
zf.write(synthesis.file.path, str(synthesis) + ".pdf")
|
||||
|
||||
zf.close()
|
||||
|
||||
resp = HttpResponse(out.getvalue(), content_type="application/x-zip-compressed")
|
||||
resp['Content-Disposition'] = 'attachment; filename={}'\
|
||||
.format(_("Syntheses for team {team}.zip")
|
||||
.format(team=str(request.user.team)).replace(" ", "%20"))
|
||||
return resp
|
||||
|
||||
return super().post(request, *args, **kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
qs = super().get_queryset().filter(team=self.request.user.team)
|
||||
return qs.order_by('final', 'team__tournament__date_start', 'team__tournament__name', 'team__trigram',
|
||||
'round', 'source',)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
self.object_list = self.get_queryset()
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["now"] = timezone.now()
|
||||
context["real_deadline_1"] = self.request.user.team.future_tournament.date_syntheses + timedelta(minutes=30)
|
||||
context["real_deadline_2"] = self.request.user.team.future_tournament.date_syntheses_2 + timedelta(minutes=30)
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
synthesis = form.instance
|
||||
synthesis.team = self.request.user.team
|
||||
synthesis.final = synthesis.team.selected_for_final
|
||||
|
||||
if synthesis.round == '1' and timezone.now() > (synthesis.tournament.date_syntheses + timedelta(minutes=30)):
|
||||
form.add_error('file', _("You can't publish your synthesis anymore for the first round."
|
||||
" Deadline: {date:%m-%d-%Y %H:%M}.")
|
||||
.format(date=timezone.localtime(synthesis.tournament.date_syntheses)))
|
||||
return super().form_invalid(form)
|
||||
|
||||
if synthesis.round == '2' and timezone.now() > synthesis.tournament.date_syntheses_2 + timedelta(minutes=30):
|
||||
form.add_error('file', _("You can't publish your synthesis anymore for the second round."
|
||||
" Deadline: {date:%m-%d-%Y %H:%M}.")
|
||||
.format(date=timezone.localtime(synthesis.tournament.date_syntheses_2)))
|
||||
return super().form_invalid(form)
|
||||
|
||||
prev_syn = Synthesis.objects.filter(team=synthesis.team, round=synthesis.round, source=synthesis.source,
|
||||
final=synthesis.final)
|
||||
for syn in prev_syn.all():
|
||||
syn.delete()
|
||||
alphabet = "0123456789abcdefghijklmnopqrstuvwxyz0123456789"
|
||||
id = ""
|
||||
for i in range(64):
|
||||
id += random.choice(alphabet)
|
||||
synthesis.file.name = id
|
||||
synthesis.save()
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy("tournament:syntheses")
|
||||
|
||||
|
||||
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"
|
||||
extra_context = dict(title=_("All syntheses"))
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if "tournament_zip" in request.POST:
|
||||
tournament = Tournament.objects.get(pk=request.POST["tournament_zip"])
|
||||
syntheses = tournament.syntheses
|
||||
if not request.user.admin and request.user not in tournament.organizers.all():
|
||||
raise PermissionDenied
|
||||
|
||||
out = BytesIO()
|
||||
zf = zipfile.ZipFile(out, "w")
|
||||
|
||||
for synthesis in syntheses:
|
||||
zf.write(synthesis.file.path, str(synthesis) + ".pdf")
|
||||
|
||||
zf.close()
|
||||
|
||||
resp = HttpResponse(out.getvalue(), content_type="application/x-zip-compressed")
|
||||
resp['Content-Disposition'] = 'attachment; filename={}'\
|
||||
.format(_("Syntheses for tournament {tournament}.zip")
|
||||
.format(tournament=str(tournament)).replace(" ", "%20"))
|
||||
return resp
|
||||
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
if self.request.user.is_authenticated:
|
||||
context["tournaments"] = \
|
||||
Tournament.objects if self.request.user.admin else self.request.user.organized_tournaments
|
||||
|
||||
return context
|
||||
|
||||
def get_queryset(self):
|
||||
qs = super().get_queryset()
|
||||
if self.request.user.is_authenticated and not self.request.user.admin:
|
||||
if self.request.user in Tournament.get_final().organizers.all():
|
||||
qs = qs.filter(Q(team__tournament__organizers=self.request.user)
|
||||
| Q(team__pools__juries=self.request.user)
|
||||
| Q(final=True))
|
||||
else:
|
||||
qs = qs.filter(Q(team__tournament__organizers=self.request.user)
|
||||
| Q(team__pools__juries=self.request.user))
|
||||
elif not self.request.user.is_authenticated:
|
||||
pool = Pool.objects.filter(extra_access_token=self.request.session["extra_access_token"])
|
||||
if pool.exists():
|
||||
pool = pool.get()
|
||||
qs = qs.filter(team__pools=pool, final=pool.tournament.final)
|
||||
else:
|
||||
qs = qs.none()
|
||||
return qs.order_by('final', 'team__tournament__date_start', 'team__tournament__name', 'team__trigram',
|
||||
'round', 'source',).distinct()
|
||||
|
||||
|
||||
class PoolListView(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"))
|
||||
|
||||
def get_queryset(self):
|
||||
qs = super().get_queryset()
|
||||
user = self.request.user
|
||||
if user.is_authenticated:
|
||||
if not user.admin and user.organizes:
|
||||
qs = qs.filter(Q(juries=user) | Q(teams__tournament__organizers=user))
|
||||
elif user.participates:
|
||||
qs = qs.filter(teams=user.team)
|
||||
else:
|
||||
qs = qs.filter(extra_access_token=self.request.session["extra_access_token"])
|
||||
qs = qs.distinct().order_by('id')
|
||||
return qs
|
||||
|
||||
|
||||
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"))
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy("tournament:pools")
|
||||
|
||||
|
||||
class PoolDetailView(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"))
|
||||
|
||||
def get_queryset(self):
|
||||
qs = super().get_queryset()
|
||||
user = self.request.user
|
||||
if user.is_authenticated:
|
||||
if not user.admin and user.organizes:
|
||||
qs = qs.filter(Q(juries=user) | Q(teams__tournament__organizers=user))
|
||||
elif user.participates:
|
||||
qs = qs.filter(teams=user.team)
|
||||
else:
|
||||
qs = qs.filter(extra_access_token=self.request.session["extra_access_token"])
|
||||
return qs.distinct()
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
user = request.user
|
||||
pool = self.get_object()
|
||||
|
||||
if "solutions_zip" in request.POST:
|
||||
if user.is_authenticated and user.participates and pool.round == 2\
|
||||
and pool.tournament.date_solutions_2 > timezone.now():
|
||||
raise PermissionDenied
|
||||
|
||||
out = BytesIO()
|
||||
zf = zipfile.ZipFile(out, "w")
|
||||
|
||||
for solution in pool.solutions.all():
|
||||
zf.write(solution.file.path, str(solution) + ".pdf")
|
||||
|
||||
zf.close()
|
||||
|
||||
resp = HttpResponse(out.getvalue(), content_type="application/x-zip-compressed")
|
||||
resp['Content-Disposition'] = 'attachment; filename={}' \
|
||||
.format(_("Solutions of a pool for the round {round} of the tournament {tournament}.zip")
|
||||
.format(round=pool.round, tournament=str(pool.tournament)).replace(" ", "%20"))
|
||||
return resp
|
||||
elif "syntheses_zip" in request.POST and (not user.is_authenticated or user.organizes):
|
||||
out = BytesIO()
|
||||
zf = zipfile.ZipFile(out, "w")
|
||||
|
||||
for synthesis in pool.syntheses.all():
|
||||
zf.write(synthesis.file.path, str(synthesis) + ".pdf")
|
||||
|
||||
zf.close()
|
||||
|
||||
resp = HttpResponse(out.getvalue(), content_type="application/x-zip-compressed")
|
||||
resp['Content-Disposition'] = 'attachment; filename={}' \
|
||||
.format(_("Syntheses of a pool for the round {round} of the tournament {tournament}.zip")
|
||||
.format(round=pool.round, tournament=str(pool.tournament)).replace(" ", "%20"))
|
||||
return resp
|
||||
|
||||
return self.get(request, *args, **kwargs)
|
Reference in New Issue
Block a user