mirror of
https://gitlab.com/animath/si/plateforme.git
synced 2024-12-25 06:22:22 +00:00
Reset project
This commit is contained in:
parent
30fa8b7840
commit
3d9bd88a41
@ -1,4 +1,3 @@
|
|||||||
__pycache__
|
__pycache__
|
||||||
media
|
media
|
||||||
import_olddb
|
|
||||||
db.sqlite3
|
db.sqlite3
|
||||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -38,10 +38,8 @@ coverage
|
|||||||
secrets.py
|
secrets.py
|
||||||
*.log
|
*.log
|
||||||
media/
|
media/
|
||||||
|
|
||||||
# Virtualenv
|
# Virtualenv
|
||||||
env/
|
env/
|
||||||
venv/
|
venv/
|
||||||
db.sqlite3
|
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
Block a user