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