Reset project

This commit is contained in:
Yohann D'ANELLO 2020-12-27 11:14:35 +01:00
parent 30fa8b7840
commit 3d9bd88a41
Signed by: ynerant
GPG Key ID: 3A75C55819C8CF85
124 changed files with 1 additions and 8004 deletions

View File

@ -1,4 +1,3 @@
__pycache__
media
import_olddb
db.sqlite3

4
.gitignore vendored
View File

@ -38,10 +38,8 @@ coverage
secrets.py
*.log
media/
# Virtualenv
env/
venv/
db.sqlite3
# Don't git personal data
import_olddb/

View File

@ -1,31 +0,0 @@
FROM python:3-alpine
ENV PYTHONUNBUFFERED 1
# Install LaTeX requirements
RUN apk add --no-cache gettext texlive nginx gcc libc-dev libffi-dev postgresql-dev mariadb-connector-c-dev
RUN apk add --no-cache bash
RUN mkdir /code
WORKDIR /code
COPY requirements.txt /code/requirements.txt
RUN pip install -r requirements.txt --no-cache-dir
COPY . /code/
# Configure nginx
RUN mkdir /run/nginx
RUN ln -sf /dev/stdout /var/log/nginx/access.log && ln -sf /dev/stderr /var/log/nginx/error.log
RUN ln -sf /code/nginx_tfjm.conf /etc/nginx/conf.d/tfjm.conf
RUN rm /etc/nginx/conf.d/default.conf
RUN cp /code/tfjm.cron /etc/crontabs/tfjm
# With a bashrc, the shell is better
RUN ln -s /code/.bashrc /root/.bashrc
ENTRYPOINT ["/code/entrypoint.sh"]
EXPOSE 80
CMD ["./manage.py", "shell_plus", "--ptpython"]

View File

@ -1,65 +0,0 @@
# Plateforme d'inscription du TFJM²
La plateforme du TFJM² est née pour l'édition 2020 du tournoi. D'abord codée en PHP, elle a subi une refonte totale en
Python, à l'aide du framework Web [Django](https://www.djangoproject.com/).
Cette plateforme permet aux participants et encadrants de s'inscrire et de déposer leurs autorisations nécessaires.
Ils pourront ensuite déposer leurs solutions et notes de synthèse pour le premier tour en temps voulu. La plateforme
offre également un accès pour les organisateurs et les jurys leur permettant de communiquer avec les équipes et de
récupérer les documents nécessaires.
Un wiki plus détaillé arrivera ultérieurement. L'interface organisateur et jury est vouée à être plus poussée.
L'instance de production est disponible à l'adresse [inscription.tfjm.org](https://inscription.tfjm.org).
## Installation
Le plus simple pour installer la plateforme est d'utiliser l'image Docker incluse, qui fait tourner un serveur Nginx
exposé sur le port 80 avec le serveur Django. Ci-dessous une configuration Docker-Compose, à adapter selon vos besoins :
```yaml
inscription-tfjm:
build: ./inscription-tfjm
links:
- postgres
ports:
- "80:80"
env_file:
- ./inscription-tfjm.env
volumes:
# - ./inscription-tfjm:/code
- ./inscription-tfjm/media:/code/media
```
Le volume `/code` n'est à ajouter uniquement en développement, et jamais en production.
Il faut remplir les variables d'environnement suivantes :
```env
TFJM_STAGE= # dev ou prod
TFJM_YEAR=2021 # Année de la session du TFJM²
DJANGO_DB_TYPE= # MySQL, PostgreSQL ou SQLite (par défaut)
DJANGO_DB_HOST= # Hôte de la base de données
DJANGO_DB_NAME= # Nom de la base de données
DJANGO_DB_USER= # Utilisateur de la base de données
DJANGO_DB_PASSWORD= # Mot de passe pour accéder à la base de données
SMTP_HOST= # Hôte SMTP pour l'envoi de mails
SMTP_PORT=465 # Port du serveur SMTP
SMTP_HOST_USER= # Utilisateur du compte SMTP
SMTP_HOST_PASSWORD= # Mot de passe du compte SMTP
FROM_EMAIL=contact@tfjm.org # Nom de l'expéditeur des mails
SERVER_EMAIL=contact@tfjm.org # Adresse e-mail expéditrice
```
Si le type de base de données sélectionné est SQLite, la variable `DJANGO_DB_HOST` sera utilisée en guise de chemin vers
le fichier de base de données (par défaut, `db.sqlite3`).
En développement, il est recommandé d'utiliser SQLite pour des raisons de simplicité. Les paramètres de mail ne seront
pas utilisés, et les mails qui doivent être envoyés seront envoyés dans la console.
En production, il est recommandé de ne pas utiliser SQLite pour des raisons de performances.
La dernière différence entre le développment et la production est qu'en développement, chaque modification d'un fichier
est détectée et le serveur se relance automatiquement dès lors.
Une fois le site lancé, le premier compte créé sera un compte administrateur.

View File

@ -1 +0,0 @@
default_app_config = 'api.apps.APIConfig'

View File

@ -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')

View File

@ -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__"

View File

@ -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')),
]

View File

@ -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)

View File

@ -1 +0,0 @@
default_app_config = 'member.apps.MemberConfig'

View File

@ -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.
"""

View File

@ -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')

View File

@ -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',)

View File

@ -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()

View File

@ -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!"))

View File

@ -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"))

View File

@ -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()),
],
),
]

View File

@ -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')},
),
]

View File

@ -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'),
),
]

View File

@ -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")

View File

@ -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'
}

View File

@ -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)

View File

@ -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()

View File

@ -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"),
]

View File

@ -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"])

View File

@ -1 +0,0 @@
default_app_config = 'tournament.apps.TournamentConfig'

View File

@ -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.
"""

View File

@ -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')

View File

@ -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',)

View File

@ -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',
},
),
]

View File

@ -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))

View File

@ -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'
}

View File

@ -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"),
]

View File

@ -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)

View File

@ -1,12 +0,0 @@
#!/bin/sh
python manage.py compilemessages
python manage.py migrate
nginx
if [ "$TFJM_STAGE" = "prod" ]; then
gunicorn -b 0.0.0.0:8000 --workers=2 --threads=4 --worker-class=gthread tfjm.wsgi --access-logfile '-' --error-logfile '-';
else
./manage.py runserver 0.0.0.0:8000;
fi

View File

@ -1,12 +0,0 @@
<!doctype html>
<html lang="fr">
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<title>Erreur</title>
</head>
<body>
Le mode <i>Rewrite</i> n'est pas activé.
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -1,21 +0,0 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tfjm.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

View File

@ -1,19 +0,0 @@
upstream tfjm {
server 127.0.0.1:8000;
}
server {
listen 80;
server_name tfjm;
location / {
proxy_pass http://tfjm;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}
location /static {
alias /code/static/;
}
}

View File

@ -1,16 +0,0 @@
bcrypt
Django~=3.0
django-allauth
django-bootstrap-datepicker-plus
django-crispy-forms
django-extensions
django-filter
django-mailer
django-polymorphic
django-tables2
djangorestframework
django-rest-polymorphic
mysqlclient
psycopg2-binary
ptpython
gunicorn

View File

@ -1,8 +0,0 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<h1>{% trans "Bad request" %}</h1>
{% blocktrans %}Sorry, your request was bad. Don't know what could be wrong. An email has been sent to webmasters with the details of the error. You can now drink a coke.{% endblocktrans %}
{% endblock %}

View File

@ -1,13 +0,0 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<h1>{% trans "Permission denied" %}</h1>
{% blocktrans %}You don't have the right to perform this request.{% endblocktrans %}
{% if exception %}
<div>
{% trans "Exception message:" %} {{ exception }}
</div>
{% endif %}
{% endblock %}

View File

@ -1,13 +0,0 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<h1>{% trans "Page not found" %}</h1>
{% blocktrans %}The requested path <code>{{ request_path }}</code> was not found on the server.{% endblocktrans %}
{% if exception != "Resolver404" %}
<div>
{% trans "Exception message:" %} {{ exception }}
</div>
{% endif %}
{% endblock %}

View File

@ -1,8 +0,0 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<h1>{% trans "Server error" %}</h1>
{% blocktrans %}Sorry, an error occurred when processing your request. An email has been sent to webmasters with the detail of the error, and this will be fixed soon. You can now drink a beer.{% endblocktrans %}
{% endblock %}

View File

@ -1,11 +0,0 @@
<div class="input-group">
<input class="form-control mx-auto d-block" type="number" min="0" step="0.01"
{% if widget.value != None and widget.value != "" %}value="{{ widget.value }}"{% endif %}
name="{{ widget.name }}"
{% for name, value in widget.attrs.items %}
{% ifnotequal value False %}{{ name }}{% ifnotequal value True %}="{{ value|stringformat:'s' }}"{% endifnotequal %}{% endifnotequal %}
{% endfor %}>
<div class="input-group-append">
<span class="input-group-text"></span>
</div>
</div>

View File

@ -1,9 +0,0 @@
<input type="hidden" name="{{ widget.name }}" {% if widget.attrs.model_pk %}value="{{ widget.attrs.model_pk }}"{% endif %} id="{{ widget.attrs.id }}_pk">
<input type="text"
{% if widget.value != None and widget.value != "" %}value="{{ widget.value }}"{% endif %}
name="{{ widget.name }}_name" autocomplete="off"
{% for name, value in widget.attrs.items %}
{% ifnotequal value False %}{{ name }}{% ifnotequal value True %}="{{ value|stringformat:'s' }}"{% endifnotequal %}{% endifnotequal %}
{% endfor %}>
<ul class="list-group list-group-flush" id="{{ widget.attrs.id }}_list">
</ul>

View File

@ -1,227 +0,0 @@
{% load static i18n static getconfig %}
<!DOCTYPE html>
<html lang="en" class="position-relative h-100">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>
{% block title %}{{ title }}{% endblock title %} - Inscription au TFJM²
</title>
<meta name="description" content="{% trans "The inscription site of the TFJM²." %}">
{# Favicon #}
<link rel="shortcut icon" href="{% static "favicon.ico" %}">
<meta name="theme-color" content="#ffffff">
{% if no_cache %}
<meta name="turbolinks-cache-control" content="no-cache">
{% endif %}
{# Bootstrap CSS #}
<link rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"
integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh"
crossorigin="anonymous">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.13.0/css/all.css">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.13.0/css/v4-shims.css">
{# Custom CSS #}
<link rel="stylesheet" type="text/css" href="{% static "style.css" %}">
{# JQuery, Bootstrap and Turbolinks JavaScript #}
<script src="https://code.jquery.com/jquery-3.4.1.min.js"
integrity="sha384-vk5WoKIaW/vJyUAd9n/wmopsmNhiy+L2Z+SBxGYnUkunIxVxAv/UtMOhba/xskxh"
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"
integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo"
crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"
integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/turbolinks/5.2.0/turbolinks.js"
crossorigin="anonymous"></script>
{# Si un formulaire requiert des données supplémentaires (notamment JS), les données sont chargées #}
{% if form.media %}
{{ form.media }}
{% endif %}
<style>
.validate:hover {
cursor: pointer;
text-decoration: underline;
}
</style>
{% block extracss %}{% endblock %}
</head>
<body class="d-flex w-100 h-100 flex-column">
<main class="mb-auto">
<nav class="navbar navbar-expand-md navbar-light bg-light fixed-navbar shadow-sm">
<a class="navbar-brand" href="https://tfjm.org/">
<img src="{% static "logo.svg" %}" alt="Logo TFJM²" id="navbar-logo">
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse"
data-target="#navbarNavDropdown"
aria-controls="navbarNavDropdown" aria-expanded="false"
aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div id="navbarNavDropdown" class="collapse navbar-collapse">
<ul class="navbar-nav mr-auto">
<li class="nav-item active">
<a href="{% url "index" %}" class="nav-link"><i class="fas fa-home"></i> {% trans "Home" %}</a>
</li>
<li class="nav-item active">
<a class="nav-link" href="{% url "tournament:list" %}"><i class="fas fa-calendar"></i> {% trans "Tournament list" %}</a>
{% if user.organizes %}
<ul class="deroule">
{% if user.admin %}
<li class="nav-item active"><a class="nav-link" href="{% url "member:orphaned_profiles" %}"><i class="fas fa-user"></i> {% trans "Orphaned profiles" %}</a></li>
<li class="nav-item active"><a class="nav-link" href="{% url "member:all_profiles" %}"><i class="fas fa-users"></i> {% trans "All profiles" %}</a></li>
{% endif %}
<li class="nav-item active"><a class="nav-link" href="{% url "member:organizers" %}"><i class="fas fa-user-tie"></i> {% trans "Organizers" %}</a></li>
</ul>
{% endif %}
</li>
{% if user.is_authenticated %}
<li class="nav-item active">
<a class="nav-link" href="{% url "member:my_account" %}"><i class="fas fa-user"></i> {% trans "My account" %}</a>
</li>
{% if user.participates %}
{% if not user.team %}
<li class="nav-item active">
<a class="nav-link" href="{% url "member:add_team" %}"><i class="fas fa-folder-plus"></i> {% trans "Add a team" %}</a>
</li>
<li class="nav-item active">
<a class="nav-link" href="{% url "member:join_team" %}"><i class="fas fa-users"></i> {% trans "Join a team" %}</a>
</li>
{% else %}
<li class="nav-item active">
<a class="nav-link" href="{% url "member:my_team" %}"><i class="fas fa-users-cog"></i> {% trans "My team" %}</a>
</li>
{% if user.team.valid %}
<!-- <li class="nav-item active">
<a class="nav-link" href="/paiement">Paiement</a>
</li> -->
<li class="nav-item active">
<a class="nav-link" href="{% url "tournament:solutions" %}"><i class="fas fa-lightbulb"></i> {% trans "Solutions" %}</a>
</li>
<li class="nav-item active">
<a class="nav-link" href="{% url "tournament:syntheses" %}"><i class="fas fa-feather"></i> {% trans "Syntheses" %}</a>
</li>
{% endif %}
{% endif %}
{% endif %}
{% if user.organizes %}
<li class="nav-item active">
<a class="nav-link" href="{% url "tournament:all_solutions" %}"><i class="fas fa-lightbulb"></i> {% trans "Solutions" %}</a>
</li>
<li class="nav-item active">
<a class="nav-link" href="{% url "tournament:all_syntheses" %}"><i class="fas fa-feather"></i> {% trans "Syntheses" %}</a>
</li>
{% endif %}
<li class="nav-item active">
<a class="nav-link" href="{% url "tournament:pools" %}"><i class="fas fa-swimming-pool"></i> {% trans "Pools" %}</a>
</li>
{% endif %}
{% if not user.is_authenticated and request.session.extra_access_token %}
{# Juries can access to pool data without logging in. #}
<li class="nav-item active">
<a class="nav-link" href="{% url "tournament:all_solutions" %}"><i class="fas fa-lightbulb"></i> {% trans "Solutions" %}</a>
</li>
<li class="nav-item active">
<a class="nav-link" href="{% url "tournament:all_syntheses" %}"><i class="fas fa-feather"></i> {% trans "Syntheses" %}</a>
</li>
<li class="nav-item active">
<a class="nav-link" href="{% url "tournament:pools" %}"><i class="fas fa-swimming-pool"></i> {% trans "Pools" %}</a>
</li>
{% endif %}
<li class="nav-item active">
<a class="nav-link" href="https://www.helloasso.com/associations/animath/formulaires/5/widget"><i
class="fas fa-hand-holding-heart"></i> {% trans "Make a gift" %}</a>
</li>
{% if user.admin %}
<li class="nav-item active">
<a data-turbolinks="false" class="nav-link" href="{% url "admin:index" %}"><i class="fas fa-cog"></i> {% trans "Administration" %}</a>
</li>
{% endif %}
</ul>
<ul class="navbar-nav">
{% if "_fake_user_id" in request.session %}
<li class="nav-item active">
<a class="nav-link" href="{% url "member:reset_admin" %}?path={{ request.path }}"><i class="fas fa-tools"></i> {% trans "Return to admin view" %}</a>
</li>
{% endif %}
{% if not user.is_authenticated %}
<li class="nav-item active">
<a class="nav-link" href="{% url "login" %}"><i class="fas fa-sign-in-alt"></i> {% trans "Log in" %}</a>
</li>
<li class="nav-item active">
<a class="nav-link" href="{% url "member:signup" %}"><i class="fas fa-user-plus"></i> {% trans "Sign up" %}</a>
</li>
{% else %}
<li class="nav-item active">
<a class="nav-link" href="{% url "logout" %}"><i class="fas fa-sign-out-alt"></i> {% trans "Log out" %}</a>
</li>
{% endif %}
</ul>
</div>
</nav>
<div class="container-fluid my-3" style="max-width: 1600px;">
{% block contenttitle %}<h1>{{ title }}</h1>{% endblock %}
<div id="messages"></div>
{% block content %}
<p>Default content...</p>
{% endblock content %}
</div>
</main>
<footer class="bg-light mt-auto py-2">
<div class="container-fluid">
<div class="row">
<div class="col-sm">
<form action="{% url 'set_language' %}" method="post"
class="form-inline">
<span class="text-muted mr-1">
𝕋𝔽𝕁𝕄² &mdash;
<a href="mailto:contact@tfjm.org"
class="text-muted">Nous contacter</a> &mdash;
</span>
{% csrf_token %}
<select title="language" name="language"
class="custom-select custom-select-sm"
onchange="this.form.submit()">
{% get_current_language as LANGUAGE_CODE %}
{% get_available_languages as LANGUAGES %}
{% get_language_info_list for LANGUAGES as languages %}
{% for language in languages %}
<option value="{{ language.code }}"
{% if language.code == LANGUAGE_CODE %}
selected{% endif %}>
{{ language.name_local }} ({{ language.code }})
</option>
{% endfor %}
</select>
<noscript>
<input type="submit">
</noscript>
</form>
</div>
<div class="col-sm text-right">
<a href="#" data-turbolinks="false" class="text-muted">Retour en haut</a>
</div>
</div>
</div>
</footer>
<script>
CSRF_TOKEN = "{{ csrf_token }}";
</script>
{% block extrajavascript %}
{% endblock extrajavascript %}
</body>
</html>

View File

@ -1,9 +0,0 @@
{% extends "base.html" %}
{% load getconfig %}
{% block content %}
{% autoescape off %}
{{ "index_page"|get_config|safe }}
{% endautoescape %}
{% endblock %}

View File

@ -1,20 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Organisateur du TFJM²</title>
</head>
<body>
Bonjour {{ user }},<br />
<br />
Vous recevez ce message (envoyé automatiquement) car vous êtes organisateur d'un des tournois du TFJM<sup>2</sup>.<br /><br />
Un compte organisateur vous a été créé par l'un des administrateurs. Avant de vous connecter, vous devez réinitialiser votre
mot de passe sur le lien suivant : <a href="https://inscription.tfjm.org{% url "password_reset" %}">https://inscription.tfjm.org{% url "password_reset" %}</a>.
<br />
Une fois le mot de passe changé, vous pourrez vous <a href="https://inscription.tfjm.org{% url "login" %}">connecter sur la plateforme</a>.<br />
<br />
Merci beaucoup pour votre aide !<br />
<br />
Le comité national d'organisation du TFJM<sup>2</sup>
</body>
</html>

View File

@ -1,12 +0,0 @@
Bonjour {{ user }},
Vous recevez ce message (envoyé automatiquement) car vous êtes organisateur d'un des tournois du TFJM².
Un compte organisateur vous a été créé par l'un des administrateurs. Avant de vous connecter, vous devez réinitialiser votre
mot de passe sur le lien suivant : https://inscription.tfjm.org{% url "password_reset" %}.
Une fois le mot de passe changé, vous pourrez vous connecter sur la plateforme : https://inscription.tfjm.org{% url "login" %}.
Merci beaucoup pour votre aide !
Le comité national d'organisation du TFJM²

View File

@ -1,18 +0,0 @@
<!DOCTYPE html>
<!--suppress HtmlUnknownTarget -->
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Organisateur du tournoi de {TOURNAMENT_NAME} TFJM²</title>
</head>
<body>
Bonjour {FIRST_NAME} {SURNAME},<br />
<br />
Vous venez d'être promu organisateur du tournoi <a href="{URL_BASE}/tournoi/{TOURNAMENT_NAME}">{TOURNAMENT_NAME}</a> du TFJM<sup>2</sup> {YEAR}.<br />
Ce message vous a été envoyé automatiquement. En cas de problème, merci de répondre à ce message.
<br />
Avec toute notre bienveillance,<br />
<br />
Le comité national d'organisation du TFJM<sup>2</sup>
</body>
</html>

View File

@ -1,16 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Nouvelle équipe TFJM² {YEAR}</title>
</head>
<body>
Bonjour {FIRST_NAME} {SURNAME},<br />
<br />
Vous venez de créer l'équipe « {TEAM_NAME} » ({TRIGRAM}) pour le TFJM<sup>2</sup> de {TOURNAMENT_NAME} et nous vous en remercions.<br />
Afin de permettre aux autres membres de votre équipe de vous rejoindre, veuillez leur transmettre le code d'accès :
{ACCESS_CODE}<br/>
<br />
Le comité national d'organisation du TFJM<sup>2</sup>
</body>
</html>

View File

@ -1,16 +0,0 @@
<!DOCTYPE html>
<!--suppress HtmlUnknownTarget -->
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Changement d'adresse e-mail TFJM²</title>
</head>
<body>
Bonjour {FIRST_NAME} {SURNAME},<br/>
<br/>
Vous venez de changer votre adresse e-mail. Veuillez désormais la confirmer en cliquant ici : <a
href="{URL_BASE}/confirmer_mail/{TOKEN}">{URL_BASE}/confirmer_mail/{TOKEN}</a><br/>
<br/>
Le comité national d'organisation du TFJM<sup>2</sup>
</body>
</html>

View File

@ -1,18 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Mot de passe changé TFJM²</title>
</head>
<body>
Bonjour {FIRST_NAME} {SURNAME},<br/>
<br/>
Nous vous informons que votre mot de passe vient d'être modifié. Si vous n'êtes pas à l'origine de cette manipulation,
veuillez immédiatement vérifier vos accès à votre boîte mail et changer votre mot de passe sur la plateforme
d'inscription.<br/>
<br/>
Avec toute notre bienveillance,<br/>
<br/>
Le comité national d'organisation du TFJM<sup>2</sup>
</body>
</html>

View File

@ -1,18 +0,0 @@
<!DOCTYPE html>
<!--suppress HtmlUnknownTarget -->
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Inscription au TFJM² {YEAR}</title>
</head>
<body>
Bonjour {FIRST_NAME} {SURNAME},<br/>
<br/>
Vous êtes inscrit au TFJM<sup>2</sup> {YEAR} et nous vous en remercions.<br/>
Pour valider votre adresse e-mail, veuillez cliquer sur le lien : <a href="{URL_BASE}/confirmer_mail/{TOKEN}">{URL_BASE}/confirmer_mail/{TOKEN}</a><br/>
<br/>
Avec toute notre bienveillance,<br/>
<br/>
Le comité national d'organisation du TFJM<sup>2</sup>
</body>
</html>

View File

@ -1,20 +0,0 @@
<!DOCTYPE html>
<!--suppress HtmlUnknownTarget -->
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Mot de passe oublié TFJM²</title>
</head>
<body>
Bonjour,<br/>
<br/>
Vous avez indiqué avoir oublié votre mot de passe. Veuillez cliquer ici pour le réinitialiser : <a
href="{URL_BASE}/connexion/reinitialiser_mdp/{TOKEN}">{URL_BASE}/connexion/reinitialiser_mdp/{TOKEN}</a><br/>
<br/>
Si vous n'êtes pas à l'origine de cette manipulation, vous pouvez ignorer ce message.<br/>
<br/>
Avec toute notre bienveillance,<br/>
<br/>
Le comité national d'organisation du TFJM<sup>2</sup>
</body>
</html>

View File

@ -1,17 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Équipe rejointe TFJM² {YEAR}</title>
</head>
<body>
Bonjour {FIRST_NAME} {SURNAME},<br/>
<br/>
Vous venez de rejoindre l'équipe « {TEAM_NAME} » ({TRIGRAM}) pour le TFJM² de {TOURNAMENT_NAME} et nous vous en
remercions.<br/>
<br/>
Avec toute notre bienveillance,<br/>
<br/>
Le comité national d'organisation du TFJM<sup>2</sup>
</body>
</html>

View File

@ -1,16 +0,0 @@
<!DOCTYPE html>
<!--suppress HtmlUnknownTarget -->
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Inscription au TFJM² {YEAR}</title>
</head>
<body>
Bonjour {FIRST_NAME} {SURNAME},<br />
<br />
Vous venez de vous inscrire au TFJM<sup>2</sup> {YEAR} et nous vous en remercions.<br />
Pour valider votre adresse e-mail, veuillez cliquer sur le lien : <a href="{URL_BASE}/confirmer_mail/{TOKEN}">{URL_BASE}/confirmer_mail/{TOKEN}</a><br />
<br />
Le comité national d'organisation du TFJM<sup>2</sup>
</body>
</html>

View File

@ -1,26 +0,0 @@
<!DOCTYPE html>
<!--suppress HtmlUnknownTarget -->
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Demande de validation de paiement pour le TFJM² {YEAR}</title>
</head>
<body>
Bonjour {FIRST_NAME} {SURNAME},<br />
<br />
{USER_FIRST_NAME} {USER_SURNAME} de l'équipe {TEAM_NAME} ({TRIGRAM}) annonce avoir réglé sa participation pour le tournoi {TOURNAMENT_NAME}.
Les informations suivantes ont été communiquées :<br /><br />
<strong>Équipe :</strong> {TEAM_NAME} ({TRIGRAM})<br />
<strong>Tournoi :</strong> {TOURNAMENT_NAME}<br />
<strong>Moyen de paiement :</strong> {PAYMENT_METHOD}<br />
<strong>Montant :</strong> {AMOUNT} €<br />
<strong>Informations sur le paiement :</strong> {PAYMENT_INFOS}<br />
<br />
Vous pouvez désormais vérifier ces informations, puis valider (ou non) le paiement sur
<a href="{URL_BASE}/informations/{USER_ID}/">la page associée à ce participant</a>.
<br />
Avec toute notre bienveillance,
<br />
Le comité national d'organisation du TFJM<sup>2</sup>
</body>
</html>

View File

@ -1,19 +0,0 @@
<!DOCTYPE html>
<!--suppress HtmlUnknownTarget -->
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Demande de validation - TFJM²</title>
</head>
<body>
Bonjour {{ user }},<br />
<br />
L'équipe « {{ team.name }} » ({{ team.trigram }}) vient de demander à valider son équipe pour participer au tournoi
{{ tournament }} du TFJM². Vous pouvez décider d'accepter ou de refuser l'équipe en vous rendant sur la page de l'équipe :
<a href="https://inscription.tfjm.org{% url "tournament:team_detail" pk=team.pk %}">https://inscription.tfjm.org{% url "tournament:team_detail" pk=team.pk %}</a><br/>
<br/>
Avec toute notre bienveillance,<br/>
<br />
Le comité national d'organisation du TFJM<sup>2</sup>
</body>
</html>

View File

@ -1,9 +0,0 @@
Bonjour {{ user }},
L'équipe « {{ team.name }} » ({{ team.trigram }}) vient de demander à valider son équipe pour participer au tournoi
{{ tournament }} du TFJM². Vous pouvez décider d'accepter ou de refuser l'équipe en vous rendant sur la page de l'équipe :
https://inscription.tfjm.org{% url "tournament:team_detail" pk=team.pk %}.
Avec toute notre bienveillance,
Le comité national d'organisation du TFJM²

View File

@ -1,21 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Sélection pour la finale - TFJM²</title>
</head>
<body>
Bonjour {{ user }},<br>
<br>
Félicitations ! Votre équipe « {{ team.name }} » ({{ team.trigram }}) est sélectionnée pour la finale nationale !<br>
<br>
La finale aura lieu du {{ final.date_start }} au {{ final.date_end }}. Vous pouvez peaufiner vos solutions
si vous le souhaitez jusqu'au {{ final.date_solutions }}.<br>
<br>
Bravo encore !<br>
<br>
Avec toute notre bienveillance,<br>
<br>
Le comité national d'organisation du TFJM²
</body>
</html>

View File

@ -1,12 +0,0 @@
Bonjour {{ user }},
Félicitations ! Votre équipe « {{ team.name }} » ({{ team.trigram }}) est sélectionnée pour la finale nationale !
La finale aura lieu du {{ final.date_start }} au {{ final.date_end }}. Vous pouvez peaufiner vos solutions
si vous le souhaitez jusqu'au {{ final.date_solutions }}.
Bravo encore !
Avec toute notre bienveillance,
Le comité national d'organisation du TFJM²

View File

@ -1,24 +0,0 @@
<!DOCTYPE html>
<!--suppress HtmlUnknownTarget -->
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Non-validation du paiement pour le TFJM² {YEAR}</title>
</head>
<body>
Bonjour {FIRST_NAME} {SURNAME},<br />
<br />
Votre paiement pour le TFJM² {YEAR} a malheureusement été rejeté. Pour rappel, vous aviez fourni ces informations :<br /><br />
<strong>Équipe :</strong> {TEAM_NAME} ({TRIGRAM})<br />
<strong>Tournoi :</strong> {TOURNAMENT_NAME}<br />
<strong>Moyen de paiement :</strong> {PAYMENT_METHOD}<br />
<strong>Montant :</strong> {AMOUNT} €<br />
<strong>Informations sur le paiement :</strong> {PAYMENT_INFOS}<br />
<br />
{MESSAGE}
<br />
Avec toute notre bienveillance,
<br />
Le comité national d'organisation du TFJM<sup>2</sup>
</body>
</html>

View File

@ -1,26 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Équipe non validée TFJM²</title>
</head>
<body>
Bonjour {{ user }},<br/>
<br/>
Maleureusement, votre équipe « {{ team.name }} » ({{ team.trigram }}) n'a pas été validée. Veuillez vérifier que vos autorisations sont correctes.
{% if message %}
<p>
Le CNO vous adresse le message suivant :
<div>
{{ message }}
</div>
</p>
{% endif %}
<br />
N'hésitez pas à nous contacter à l'adresse <a href="mailto:contact@tfjm.org">contact@tfjm.org</a> pour plus d'informations.
<br/>
Avec toute notre bienveillance,<br/>
<br/>
Le comité national d'organisation du TFJM<sup>2</sup>
</body>
</html>

View File

@ -1,15 +0,0 @@
Bonjour {{ user }},
Maleureusement, votre équipe « {{ team.name }} » ({{ team.trigram }}) n'a pas été validée. Veuillez vérifier que vos autorisations sont correctes.
{% if message %}
Le CNO vous adresse le message suivant :
{{ message }}
{% endif %}
N'hésitez pas à nous contacter à l'adresse contact@tfjm.org pour plus d'informations.
Avec toute notre bienveillance,
Le comité national d'organisation du TFJM²

View File

@ -1,24 +0,0 @@
<!DOCTYPE html>
<!--suppress HtmlUnknownTarget -->
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Validation du paiement pour le TFJM² {YEAR}</title>
</head>
<body>
Bonjour {FIRST_NAME} {SURNAME},<br />
<br />
Votre paiement pour le TFJM² {YEAR} a bien été validé. Pour rappel, vous aviez fourni ces informations :<br /><br />
<strong>Équipe :</strong> {TEAM_NAME} ({TRIGRAM})<br />
<strong>Tournoi :</strong> {TOURNAMENT_NAME}<br />
<strong>Moyen de paiement :</strong> {PAYMENT_METHOD}<br />
<strong>Montant :</strong> {AMOUNT} €<br />
<strong>Informations sur le paiement :</strong> {PAYMENT_INFOS}<br />
<br />
{MESSAGE}
<br />
Avec toute notre bienveillance,
<br />
Le comité national d'organisation du TFJM<sup>2</sup>
</body>
</html>

View File

@ -1,25 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Équipe validée TFJM² {YEAR}</title>
</head>
<body>
Bonjour {{ user }},<br/>
<br/>
Félicitations ! Votre équipe « {{ team }} » ({{ team.trigram }}) est désormais validée ! Vous êtes désormais apte à travailler sur
vos problèmes et publier vos solutions sur la plateforme.
{% if message %}
<p>
Le CNO vous adresse le message suivant :
<div>
{{ message }}
</div>
</p>
{% endif %}
<br/>
Avec toute notre bienveillance,<br/>
<br/>
Le comité national d'organisation du TFJM<sup>2</sup>
</body>
</html>

View File

@ -1,13 +0,0 @@
Bonjour {{ user }},
Félicitations ! Votre équipe « {{ team }} » ({{ team.trigram }}) est désormais validée ! Vous êtes désormais apte à travailler sur
vos problèmes et publier vos solutions sur la plateforme.
{% if message %}
Le CNO vous adresse le message suivant :
{{ message }}
{% endif %}
Avec toute notre bienveillance,
Le comité national d'organisation du TFJM²

View File

@ -1,15 +0,0 @@
{% extends "base.html" %}
{% load i18n crispy_forms_filters %}
{% block content %}
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<input type="submit" class="btn btn-primary btn-block" value="{% trans "Submit" %}">
</form>
<hr>
<a class="btn btn-secondary btn-block" href="{% url "password_change" %}">{% trans "Update my password" %}</a>
{% endblock %}

View File

@ -1,11 +0,0 @@
{% extends "base.html" %}
{% load django_tables2 i18n %}
{% block content %}
{% render_table table %}
{% if type == "organizers" and user.admin %}
<hr>
<a class="btn btn-block btn-secondary" href="{% url "tournament:add_organizer" %}"><i class="fas fa-user-plus"></i> {% trans "Add an organizer" %}</a>
{% endif %}
{% endblock %}

View File

@ -1,87 +0,0 @@
{% extends "base.html" %}
{% load getconfig i18n django_tables2 static %}
{% block content %}
<div class="card bg-light shadow">
<div class="card-header text-center">
<h4>{{ tfjmuser }}</h4>
</div>
<div class="card-body">
<dl class="row">
<dt class="col-xl-6 text-right">{% trans 'role'|capfirst %}</dt>
<dd class="col-xl-6">{{ tfjmuser.get_role_display }}</dd>
{% if tfjmuser.team %}
<dt class="col-xl-6 text-right">{% trans 'team'|capfirst %}</dt>
<dd class="col-xl-6"><a href="{% url "tournament:team_detail" pk=tfjmuser.team.pk %}">{{ tfjmuser.team }}</a></dd>
{% endif %}
{% if tfjmuser.birth_date %}
<dt class="col-xl-6 text-right">{% trans 'birth date'|capfirst %}</dt>
<dd class="col-xl-6">{{ tfjmuser.birth_date }}</dd>
{% endif %}
{% if tfjmuser.participates %}
<dt class="col-xl-6 text-right">{% trans 'gender'|capfirst %}</dt>
<dd class="col-xl-6">{{ tfjmuser.get_gender_display }}</dd>
{% endif %}
{% if tfjmuser.address %}
<dt class="col-xl-6 text-right">{% trans 'address'|capfirst %}</dt>
<dd class="col-xl-6">{{ tfjmuser.address }}, {{ tfjmuser.postal_code }}, {{ tfjmuser.city }}{% if tfjmuser.country != "France" %}, {{ tfjmuser.country }}{% endif %}</dd>
{% endif %}
<dt class="col-xl-6 text-right">{% trans 'email'|capfirst %}</dt>
<dd class="col-xl-6"><a href="mailto:{{ tfjmuser.email }}">{{ tfjmuser.email }}</a></dd>
{% if tfjmuser.phone_number %}
<dt class="col-xl-6 text-right">{% trans 'phone number'|capfirst %}</dt>
<dd class="col-xl-6">{{ tfjmuser.phone_number }}</dd>
{% endif %}
{% if tfjmuser.role == '3participant' %}
<dt class="col-xl-6 text-right">{% trans 'school'|capfirst %}</dt>
<dd class="col-xl-6">{{ tfjmuser.school }}</dd>
<dt class="col-xl-6 text-right">{% trans 'class'|capfirst %}</dt>
<dd class="col-xl-6">{{ tfjmuser.get_student_class_display }}</dd>
{% if tfjmuser.responsible_name %}
<dt class="col-xl-6 text-right">{% trans 'responsible name'|capfirst %}</dt>
<dd class="col-xl-6">{{ tfjmuser.responsible_name }}</dd>
{% endif %}
{% if tfjmuser.responsible_phone %}
<dt class="col-xl-6 text-right">{% trans 'responsible phone'|capfirst %}</dt>
<dd class="col-xl-6">{{ tfjmuser.responsible_phone }}</dd>
{% endif %}
{% if tfjmuser.responsible_email %}
<dt class="col-xl-6 text-right">{% trans 'responsible email'|capfirst %}</dt>
<dd class="col-xl-6"><a href="{{ tfjmuser.responsible_email }}">{{ tfjmuser.responsible_email }}</a></dd>
{% endif %}
{% endif %}
{% if tfjmuser.role != '3participant' %}
<dt class="col-xl-6 text-right">{% trans 'description'|capfirst %}</dt>
<dd class="col-xl-6">{{ tfjmuser.description|default_if_none:"" }}</dd>
{% endif %}
</dl>
</div>
</div>
<hr>
<h4>{% trans "Documents" %}</h4>
{# TODO Display documents #}
{% if request.user.is_superuser %}
<hr>
<form method="post">
{% csrf_token %}
<button name="view_as" class="btn btn-block btn-warning">{% blocktrans %}View site as {{ tfjmuser }}{% endblocktrans %}</button>
</form>
{% endif %}
{% endblock %}

View File

@ -1,27 +0,0 @@
{% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n %}
{% block content %}
<div class="card bg-light">
<h3 class="card-header text-center">
{{ title }}
</h3>
<div class="card-body">
{% if validlink %}
<p>
{% trans "Your email have successfully been validated." %}
</p>
<p>
{% blocktrans %}You can now <a href="{{ login_url }}">log in</a>.{% endblocktrans %}
</p>
{% else %}
<p>
{% trans "The link was invalid. The token may have expired. Please send us an email to activate your account." %}
</p>
{% endif %}
</div>
</div>
{% endblock %}

View File

@ -1,18 +0,0 @@
{% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n %}
{% block content %}
<div class="card bg-light">
<h3 class="card-header text-center">
{% trans "Account activation" %}
</h3>
<div class="card-body">
<p>
{% trans "An email has been sent. Please click on the link to activate your account." %}
</p>
</div>
</div>
{% endblock %}

View File

@ -1,10 +0,0 @@
{% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n %}
{% block content %}
<p>{% trans "Thanks for spending some quality time with the Web site today." %}</p>
<p><a href="{% url 'index' %}">{% trans 'Log in again' %}</a></p>
{% endblock %}

View File

@ -1,25 +0,0 @@
{% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-2.0-or-later
{% endcomment %}
{% load i18n crispy_forms_filters %}
{% block title %}{% trans "Log in" %}{% endblock %}
{% block contenttitle %}<h1>{% trans "Log in" %}</h1>{% endblock %}
{% block content %}
{% if user.is_authenticated %}
<p class="errornote">
{% blocktrans trimmed %}
You are authenticated as {{ user }}, but are not authorized to
access this page. Would you like to login to a different account?
{% endblocktrans %}
</p>
{% endif %}
<form method="post" id="login-form">
{% csrf_token %}
{{ form | crispy }}
<input type="submit" value="{% trans 'Log in' %}" class="btn btn-primary">
<a href="{% url 'password_reset' %}" class="badge badge-light">{% trans 'Forgotten your password or username?' %}</a>
</form>
{% endblock %}

View File

@ -1,36 +0,0 @@
{% load i18n %}
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<p>
{% trans "Hi" %} {{ user.username }},
</p>
<p>
{% trans "You recently registered on the TFJM² platform. Please click on the link below to confirm your registration." %}
</p>
<p>
<a href="https://{{ domain }}{% url 'member:email_validation' uidb64=uid token=token %}">
https://{{ domain }}{% url 'member:email_validation' uidb64=uid token=token %}
</a>
</p>
<p>
{% trans "This link is only valid for a couple of days, after that you will need to contact us to validate your email." %}
</p>
<p>
{% trans "Thanks" %},
</p>
--
<p>
{% trans "The CNO." %}<br>
</p>

View File

@ -1,13 +0,0 @@
{% load i18n %}
{% trans "Hi" %} {{ user.username }},
{% trans "You recently registered on the TFJM² platform. Please click on the link below to confirm your registration." %}
https://{{ domain }}{% url 'member:email_validation' uidb64=uid token=token %}
{% trans "This link is only valid for a couple of days, after that you will need to contact us to validate your email." %}
{% trans "Thanks" %},
{% trans "The CNO." %}

View File

@ -1,9 +0,0 @@
{% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n %}
{% block content %}
<p>{% trans 'Your password was changed.' %}</p>
{% endblock %}

View File

@ -1,13 +0,0 @@
{% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n crispy_forms_tags %}
{% block content %}
<form method="post">{% csrf_token %}
<p>{% trans "Please enter your old password, for security's sake, and then enter your new password twice so we can verify you typed it in correctly." %}</p>
{{ form | crispy }}
<input class="btn btn-primary" type="submit" value="{% trans 'Change my password' %}">
</form>
{% endblock %}

View File

@ -1,12 +0,0 @@
{% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n %}
{% block content %}
<p>{% trans "Your password has been set. You may go ahead and log in now." %}</p>
<p>
<a href="{{ login_url }}" class="btn btn-success">{% trans 'Log in' %}</a>
</p>
{% endblock %}

View File

@ -1,17 +0,0 @@
{% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n crispy_forms_tags %}
{% block content %}
{% if validlink %}
<p>{% trans "Please enter your new password twice so we can verify you typed it in correctly." %}</p>
<form method="post">{% csrf_token %}
{{ form | crispy }}
<input class="btn btn-primary" type="submit" value="{% trans 'Change my password' %}">
</form>
{% else %}
<p>{% trans "The password reset link was invalid, possibly because it has already been used. Please request a new password reset." %}</p>
{% endif %}
{% endblock %}

View File

@ -1,10 +0,0 @@
{% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n %}
{% block content %}
<p>{% trans "We've emailed you instructions for setting your password, if an account exists with the email you entered. You should receive them shortly." %}</p>
<p>{% trans "If you don't receive an email, please make sure you've entered the address you registered with, and check your spam folder." %}</p>
{% endblock %}

View File

@ -1,13 +0,0 @@
{% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n crispy_forms_tags %}
{% block content %}
<p>{% trans "Forgotten your password? Enter your email address below, and we'll email instructions for setting a new one." %}</p>
<form method="post">{% csrf_token %}
{{ form | crispy }}
<input class="btn btn-primary" type="submit" value="{% trans 'Reset my password' %}">
</form>
{% endblock %}

View File

@ -1,17 +0,0 @@
<!-- templates/signup.html -->
{% extends 'base.html' %}
{% load crispy_forms_filters %}
{% load i18n %}
{% block title %}{% trans "Sign up" %}{% endblock %}
{% block content %}
<h2>{% trans "Sign up" %}</h2>
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<button class="btn btn-success" type="submit">
{% trans "Sign up" %}
</button>
</form>
{% endblock %}

View File

@ -1,11 +0,0 @@
{% extends "base.html" %}
{% load i18n crispy_forms_filters %}
{% block content %}
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<input type="submit" class="btn btn-primary btn-block" value="{% trans "Submit" %}">
</form>
{% endblock %}

View File

@ -1,94 +0,0 @@
{% extends "base.html" %}
{% load getconfig i18n django_tables2 static %}
{% block content %}
<div class="card bg-light shadow">
<div class="card-header text-center">
<h4>{{ title }}</h4>
</div>
<div class="card-body">
<dl class="row">
<dt class="col-xl-6 text-right">{% trans 'juries'|capfirst %}</dt>
<dd class="col-xl-6">{{ pool.juries.all|join:", " }}</dd>
<dt class="col-xl-6 text-right">{% trans 'teams'|capfirst %}</dt>
<dd class="col-xl-6">{{ pool.teams.all|join:", " }}</dd>
<dt class="col-xl-6 text-right">{% trans 'round'|capfirst %}</dt>
<dd class="col-xl-6">{{ pool.round }}</dd>
<dt class="col-xl-6 text-right">{% trans 'tournament'|capfirst %}</dt>
<dd class="col-xl-6">{{ pool.tournament }}</dd>
</dl>
</div>
</div>
<hr>
<div class="card bg-light shadow">
<div class="card-header text-center">
<h4>{% trans "Solutions" %}</h4>
</div>
<div class="card-body">
{% if pool.round == 2 %}
<div class="alert alert-warning">
{% trans "Solutions will be available here for teams from:" %} {{ pool.tournament.date_solutions_2 }}
</div>
{% endif %}
<ul>
{% for solution in pool.solutions.all %}
<li><a data-turbolinks="false" href="{{ solution.file.url }}">{{ solution }}</a></li>
{% endfor %}
</ul>
</div>
<div class="card-footer text-center">
<form method="post">
{% csrf_token %}
<button class="btn btn-success" name="solutions_zip">{% trans "Download ZIP archive" %}</button>
</form>
</div>
</div>
<hr>
<div class="card bg-light shadow">
<div class="card-header text-center">
<h4>{% trans "Syntheses" %}</h4>
</div>
<div class="card-body">
<div class="alert alert-warning">
{% trans "Templates for syntheses are available here:" %}
<a data-turbolinks="false" href="{% static "Fiche synthèse.pdf" %}">PDF</a><a data-turbolinks="false" href="{% static "Fiche synthèse.tex" %}">TEX</a>
</div>
{% if user.organizes or not user.is_authenticated %}
<ul>
{% for synthesis in pool.syntheses.all %}
<li><a data-turbolinks="false" href="{{ synthesis.file.url }}">{{ synthesis }}</a></li>
{% endfor %}
</ul>
<div class="card-footer text-center">
<form method="post">
{% csrf_token %}
<button class="btn btn-success" name="syntheses_zip">{% trans "Download ZIP archive" %}</button>
</form>
</div>
{% endif %}
</div>
</div>
<hr>
<div class="text-center">
<a class="btn btn-block btn-primary" href="{% url "tournament:pools" %}">{% trans "Pool list" %}</a>
</div>
{% if user.organizes or not user.is_authenticated %}
<hr>
<div class="alert alert-warning">
{% trans "Give this link to juries to access this page (warning: should stay confidential and only given to juries of this pool):" %}<br>
<a href="{% url "tournament:pool_detail" pk=pool.pk %}?extra_access_token={{ pool.extra_access_token }}">
https://{{ request.get_host }}{% url "tournament:pool_detail" pk=pool.pk %}?extra_access_token={{ pool.extra_access_token }}</a>
</div>
{% endif %}
{% endblock %}

View File

@ -1,11 +0,0 @@
{% extends "base.html" %}
{% load i18n crispy_forms_filters %}
{% block content %}
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<input type="submit" class="btn btn-primary btn-block" value="{% trans "Submit" %}">
</form>
{% endblock %}

View File

@ -1,12 +0,0 @@
{% extends "base.html" %}
{% load i18n django_tables2 %}
{% block content %}
{% render_table table %}
{% if user.admin %}
<hr>
<a href="{% url "tournament:create_pool" %}"><button class="btn btn-secondary btn-block">{% trans "Add pool" %}</button></a>
{% endif %}
{% endblock %}

View File

@ -1,29 +0,0 @@
{% extends "base.html" %}
{% load i18n crispy_forms_filters django_tables2 %}
{% block content %}
{% if form %}
{% if now < user.team.future_tournament.date_solutions %}
<div class="alert alert-warning">
{% blocktrans with deadline=user.team.future_tournament.date_solutions %}You can upload your solutions until {{ deadline }}.{% endblocktrans %}
</div>
{% else %}
<div class="alert alert-danger">
{% if now < real_deadline %}
{% trans "The deadline to send your solutions is reached. However, you have an extra time of 30 minutes to send your papers, no panic :)" %}
{% else %}
{% trans "You can't upload your solutions anymore." %}
{% endif %}
</div>
{% endif %}
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form|crispy }}
<button class="btn btn-block btn-success">{% trans "Submit" %}</button>
</form>
<hr>
{% endif %}
{% render_table table %}
{% endblock %}

View File

@ -1,18 +0,0 @@
{% extends "base.html" %}
{% load i18n django_tables2 %}
{% block content %}
{% render_table table %}
<hr>
<form method="post">
{% csrf_token %}
<div class="btn-group btn-block">
{% for tournament in tournaments.all %}
<button name="tournament_zip" value="{{ tournament.id }}" class="btn btn-success">{% blocktrans %}{{ tournament }} — ZIP{% endblocktrans %}</button>
{% endfor %}
</div>
</form>
{% endblock %}

View File

@ -1,45 +0,0 @@
{% extends "base.html" %}
{% load i18n crispy_forms_filters django_tables2 static %}
{% block content %}
<div class="alert alert-warning">
{% trans "Templates for syntheses are available here:" %}
<a data-turbolinks="false" href="{% static "Fiche synthèse.pdf" %}">PDF</a><a data-turbolinks="false" href="{% static "Fiche synthèse.tex" %}">TEX</a>
</div>
{% if form %}
{% if now < user.team.future_tournament.date_syntheses %}
<div class="alert alert-warning">
{% blocktrans with deadline=user.team.future_tournament.date_syntheses round=1 %}You can upload your syntheses for round {{ round }} until {{ deadline }}.{% endblocktrans %}
</div>
{% elif now < real_deadline_1 %}
<div class="alert alert-danger">
{% blocktrans with round=1 %}The deadline to send your syntheses for the round {{ round }} is reached. However, you have an extra time of 30 minutes to send your papers, no panic :){% endblocktrans %}
</div>
{% elif now < user.team.future_tournament.date_solutions_2 %}
<div class="alert alert-danger">
{% blocktrans with round=1 %}You can't upload your syntheses for the round {{ round }} anymore.{% endblocktrans %}
</div>
{% elif now < user.team.future_tournament.date_syntheses_2 %}
<div class="alert alert-warning">
{% blocktrans with deadline=user.team.future_tournament.date_syntheses_2 round=2 %}You can upload your syntheses for round {{ round }} until {{ deadline }}.{% endblocktrans %}
</div>
{% elif now < real_deadline_2 %}
<div class="alert alert-danger">
{% blocktrans with round=2 %}The deadline to send your syntheses for the round {{ round }} is reached. However, you have an extra time of 30 minutes to send your papers, no panic :){% endblocktrans %}
</div>
{% else %}
<div class="alert alert-danger">
{% blocktrans with round=2 %}You can't upload your syntheses for the round {{ round }} anymore.{% endblocktrans %}
</div>
{% endif %}
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form|crispy }}
<button class="btn btn-block btn-success">{% trans "Submit" %}</button>
</form>
<hr>
{% endif %}
{% render_table table %}
{% endblock %}

View File

@ -1,18 +0,0 @@
{% extends "base.html" %}
{% load i18n django_tables2 %}
{% block content %}
{% render_table table %}
<hr>
<form method="post">
{% csrf_token %}
<div class="btn-group btn-block">
{% for tournament in tournaments.all %}
<button name="tournament_zip" value="{{ tournament.id }}" class="btn btn-success">{% blocktrans %}{{ tournament }} — ZIP{% endblocktrans %}</button>
{% endfor %}
</div>
</form>
{% endblock %}

View File

@ -1,156 +0,0 @@
{% extends "base.html" %}
{% load getconfig i18n django_tables2 static %}
{% block content %}
<div class="card bg-light shadow">
<div class="card-header text-center">
<h4>{% trans "Team" %} {{ team.name }}</h4>
</div>
<div class="card-body">
<dl class="row">
<dt class="col-xl-6 text-right">{% trans 'name'|capfirst %}</dt>
<dd class="col-xl-6">{{ team.name }}</dd>
<dt class="col-xl-6 text-right">{% trans 'trigram'|capfirst %}</dt>
<dd class="col-xl-6">{{ team.trigram }}</dd>
<dt class="col-xl-6 text-right">{% trans 'access code'|capfirst %}</dt>
<dd class="col-xl-6">{{ team.access_code }}</dd>
<dt class="col-xl-6 text-right">{% trans 'tournament'|capfirst %}</dt>
<dd class="col-xl-6"><a
href="{% url "tournament:detail" pk=team.tournament.pk %}">{{ team.tournament }}</a></dd>
<dt class="col-xl-6 text-right">{% trans 'coachs'|capfirst %}</dt>
<dd class="col-xl-6">{% autoescape off %}{{ team.linked_coaches|join:", " }}{% endautoescape %}</dd>
<dt class="col-xl-6 text-right">{% trans 'participants'|capfirst %}</dt>
<dd class="col-xl-6">
{% autoescape off %}{{ team.linked_participants|join:", " }}{% endautoescape %}</dd>
<dt class="col-xl-6 text-right">{% trans 'validation status'|capfirst %}</dt>
<dd class="col-xl-6">{{ team.get_validation_status_display }}</dd>
</dl>
</div>
{% if user.is_authenticated and user.admin %}
<div class="alert alert-info">
<a href="mailto:contact@tfjm.org?subject=TFJM²%20{{ "TFJM_YEAR"|get_env }}&bcc={{ team_users_emails|join:"," }}">{% trans "Send a mail to people in this team" %}</a><br>
</div>
{% endif %}
{% if user.admin or user in team.tournament.organizers.all or team == user.team %}
<div class="card-footer text-center">
<form method="post">
{% csrf_token %}
{% if team.invalid or user.organizes %}
<a class="btn btn-secondary" href="{% url "tournament:team_update" pk=team.pk %}">
{% trans "Edit team" %}
</a>
{% endif %}
{% if team.valid and user.admin and not team.selected_for_final and team.pools %}
<button name="select_final" class="btn btn-success">{% trans "Select for final" %}</button>
{% endif %}
{% if team.invalid %}
<form method="post">
{% csrf_token %}
{% if user.admin %}
<button name="delete" class="btn btn-danger">{% trans "Delete team" %}</button>
{% elif team == user.team %}
<button name="leave" class="btn btn-danger">{% trans "Leave this team" %}</button>
{% endif %}
{% endif %}
</form>
</div>
{% endif %}
</div>
{% if user.participates and team.invalid %}
<hr>
{% if team.can_validate %}
<form method="post">
{% csrf_token %}
<label for="engage">Je m'engage à participer à l'intégralité du TFJM²*</label>
<input type="checkbox" name="engage" id="engage" required/>
<div class="alert alert-warning">
<strong>Attention !</strong> Une fois votre équipe validée, vous ne pourrez plus modifier le nom
de l'équipe, le trigramme ou la composition de l'équipe.
</div>
<input class="btn btn-success btn-block" type="submit" name="request_validation"
value="Demander la validation"/>
</form>
{% else %}
<div class="alert alert-warning">
Pour demander à valider votre équipe, vous devez avoir au moins un encadrant, quatre participants
et soumis une autorisation de droit à l'image, une fiche sanitaire et une autorisation
parentale (si besoin) par participant, ainsi qu'une lettre de motivation à transmettre aux
organisateurs.
Les encadrants doivent également fournir une autorisation de droit à l'image.
</div>
{% endif %}
<hr>
<div class="alert alert-danger">
En raison du changement de format du TFJM² 2020, il n'y a plus de document obligatoire à envoyer. Les
autorisations
précédemment envoyées ont été détruites. Seules les lettres de motivation ont été conservées, mais leur
envoi
n'est plus obligatoire.
</div>
{% endif %}
{% if team.waiting %}
<hr>
<div class="alert alert-warning">
{% trans "The team is waiting about validation." %}
</div>
{% if user.admin %}
<form method="POST">
{% csrf_token %}
<div class="form-group row">
<label for="message">{% trans "Message addressed to the team:" %}</label>
<textarea class="form-control" id="message" name="message"
placeholder="{% trans "Message..." %}"></textarea>
</div>
<div class="form-group">
<div class="btn-group btn-block">
<button class="btn btn-danger" name="invalidate">{% trans "Invalidate team" %}</button>
<button class="btn btn-success" name="validate">{% trans "Validate team" %}</button>
</div>
</div>
</form>
{% endif %}
{% endif %}
<hr>
<h4>{% trans "Documents" %}</h4>
{% if team.motivation_letters.count %}
<div class="alert alert-info">
<strong>{% blocktrans %}Motivation letter:{% endblocktrans %}</strong>
<a data-turbolinks="false"
href="{% url "document" file=team.motivation_letters.last.file %}">{% trans "Download" %}</a>
</div>
{% endif %}
{% if team.solutions.count %}
<div class="alert alert-info">
<ul>
{% for solution in ordered_solutions %}
<li><strong>{{ solution }} :</strong> <a data-turbolinks="false"
href="{% url "document" file=solution.file %}">{% trans "Download" %}</a>
</li>
{% endfor %}
</ul>
</div>
<div class="text-center">
<form method="post">
{% csrf_token %}
<button class="btn btn-success" name="zip">{% trans "Download solutions as ZIP" %}</button>
</form>
</div>
{% endif %}
{% endblock %}

View File

@ -1,11 +0,0 @@
{% extends "base.html" %}
{% load i18n crispy_forms_filters %}
{% block content %}
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<input type="submit" class="btn btn-primary btn-block" value="{% trans "Submit" %}">
</form>
{% endblock %}

Some files were not shown because too many files have changed in this diff Show More