mirror of
https://gitlab.com/animath/si/plateforme.git
synced 2025-06-21 16:38:23 +02:00
Compare commits
22 Commits
main
...
bc06cf4903
Author | SHA1 | Date | |
---|---|---|---|
bc06cf4903
|
|||
6d43c4b97e
|
|||
0499885fc8
|
|||
63c96ff2d2
|
|||
efeb2628ad
|
|||
56aad288f4
|
|||
b33a69410a
|
|||
0a80e03b58
|
|||
73b94d5578
|
|||
97eea3b11a
|
|||
702c8d8c9e
|
|||
ca0601fb24
|
|||
d315c8371a
|
|||
7488d3eae1
|
|||
cfaf7c4287
|
|||
e3c216e44e
|
|||
73012bd61e
|
|||
bdf181e7e4
|
|||
c57ad854fe
|
|||
a2e5ab5f6a
|
|||
758a2c9a00
|
|||
fb10df77e5
|
@ -1,6 +1,12 @@
|
|||||||
stages:
|
stages:
|
||||||
- test
|
- test
|
||||||
- quality-assurance
|
- quality-assurance
|
||||||
|
- build
|
||||||
|
- release
|
||||||
|
|
||||||
|
variables:
|
||||||
|
CONTAINER_TEST_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
|
||||||
|
CONTAINER_RELEASE_IMAGE: $CI_REGISTRY_IMAGE:latest
|
||||||
|
|
||||||
py312:
|
py312:
|
||||||
stage: test
|
stage: test
|
||||||
@ -27,3 +33,29 @@ linters:
|
|||||||
- pip install tox --no-cache-dir
|
- pip install tox --no-cache-dir
|
||||||
script: tox -e linters
|
script: tox -e linters
|
||||||
allow_failure: true
|
allow_failure: true
|
||||||
|
|
||||||
|
build-image:
|
||||||
|
image: docker
|
||||||
|
stage: build
|
||||||
|
services:
|
||||||
|
- docker:dind
|
||||||
|
before_script:
|
||||||
|
- echo "$CI_REGISTRY_PASSWORD" | docker login $CI_REGISTRY -u $CI_REGISTRY_USER --password-stdin
|
||||||
|
script:
|
||||||
|
- docker build --pull -t $CONTAINER_TEST_IMAGE .
|
||||||
|
- docker push $CONTAINER_TEST_IMAGE
|
||||||
|
|
||||||
|
release-image:
|
||||||
|
image: docker
|
||||||
|
stage: release
|
||||||
|
services:
|
||||||
|
- docker:dind
|
||||||
|
before_script:
|
||||||
|
- echo "$CI_REGISTRY_PASSWORD" | docker login $CI_REGISTRY -u $CI_REGISTRY_USER --password-stdin
|
||||||
|
script:
|
||||||
|
- docker pull $CONTAINER_TEST_IMAGE
|
||||||
|
- docker tag $CONTAINER_TEST_IMAGE $CONTAINER_RELEASE_IMAGE
|
||||||
|
- docker push $CONTAINER_RELEASE_IMAGE
|
||||||
|
rules:
|
||||||
|
- if: $CI_COMMIT_BRANCH == "main"
|
||||||
|
|
||||||
|
@ -37,4 +37,4 @@ RUN ln -s /code/.bashrc /root/.bashrc
|
|||||||
ENTRYPOINT ["/code/entrypoint.sh"]
|
ENTRYPOINT ["/code/entrypoint.sh"]
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|
||||||
CMD ["./manage.py", "shell_plus", "--ipython"]
|
CMD ["./manage.py", "shell"]
|
||||||
|
@ -224,7 +224,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
|
|
||||||
# Update user interface
|
# Update user interface
|
||||||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||||
{'tid': self.tournament_id, 'type': 'draw.start', 'fmt': fmt, 'draw': draw})
|
{'tid': self.tournament_id, 'type': 'draw.start', 'fmt': fmt})
|
||||||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||||
{'tid': self.tournament_id, 'type': 'draw.set_info',
|
{'tid': self.tournament_id, 'type': 'draw.set_info',
|
||||||
'info': await self.tournament.draw.ainformation()})
|
'info': await self.tournament.draw.ainformation()})
|
||||||
@ -235,7 +235,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||||
{'tid': self.tournament_id, 'type': 'draw.notify',
|
{'tid': self.tournament_id, 'type': 'draw.notify',
|
||||||
'title': 'Tirage au sort du TFJM²',
|
'title': 'Tirage au sort du TFJM²',
|
||||||
'body': _("The draw of tournament {tournament} started!")
|
'body': str(_("The draw of tournament {tournament} started!"))
|
||||||
.format(tournament=self.tournament.name)})
|
.format(tournament=self.tournament.name)})
|
||||||
|
|
||||||
async def draw_start(self, content) -> None:
|
async def draw_start(self, content) -> None:
|
||||||
@ -405,15 +405,15 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
await self.channel_layer.group_send(
|
await self.channel_layer.group_send(
|
||||||
f"team-{dup.participation.team.trigram}",
|
f"team-{dup.participation.team.trigram}",
|
||||||
{'tid': self.tournament_id, 'type': 'draw.notify', 'title': 'Tirage au sort du TFJM²',
|
{'tid': self.tournament_id, 'type': 'draw.notify', 'title': 'Tirage au sort du TFJM²',
|
||||||
'body': _("Your dice score is identical to the one of one or multiple teams. "
|
'body': str(_("Your dice score is identical to the one of one or multiple teams. "
|
||||||
"Please relaunch it.")}
|
"Please relaunch it."))}
|
||||||
)
|
)
|
||||||
# Alert the tournament
|
# Alert the tournament
|
||||||
await self.channel_layer.group_send(
|
await self.channel_layer.group_send(
|
||||||
f"tournament-{self.tournament.id}",
|
f"tournament-{self.tournament.id}",
|
||||||
{'tid': self.tournament_id, 'type': 'draw.alert',
|
{'tid': self.tournament_id, 'type': 'draw.alert',
|
||||||
'message': _('Dices from teams {teams} are identical. Please relaunch your dices.').format(
|
'message': str(_('Dices from teams {teams} are identical. Please relaunch your dices.').format(
|
||||||
teams=', '.join(td.participation.team.trigram for td in dups)),
|
teams=', '.join(td.participation.team.trigram for td in dups))),
|
||||||
'alert_type': 'warning'})
|
'alert_type': 'warning'})
|
||||||
error = True
|
error = True
|
||||||
|
|
||||||
@ -537,7 +537,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
async for next_round in self.tournament.draw.round_set.filter(number__gte=2).all():
|
async for next_round in self.tournament.draw.round_set.filter(number__gte=2).all():
|
||||||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||||
{'tid': self.tournament_id, 'type': 'draw.send_poules',
|
{'tid': self.tournament_id, 'type': 'draw.send_poules',
|
||||||
'round': r.number,
|
'round': next_round.number,
|
||||||
'poules': [
|
'poules': [
|
||||||
{
|
{
|
||||||
'letter': pool.get_letter_display(),
|
'letter': pool.get_letter_display(),
|
||||||
@ -612,8 +612,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
# Notify the team that it can draw a problem
|
# Notify the team that it can draw a problem
|
||||||
await self.channel_layer.group_send(f"team-{tds[0].participation.team.trigram}",
|
await self.channel_layer.group_send(f"team-{tds[0].participation.team.trigram}",
|
||||||
{'tid': self.tournament_id, 'type': 'draw.notify',
|
{'tid': self.tournament_id, 'type': 'draw.notify',
|
||||||
'title': _("Your turn!"),
|
'title': str(_("Your turn!")),
|
||||||
'body': _("It's your turn to draw a problem!")})
|
'body': str(_("It's your turn to draw a problem!"))})
|
||||||
|
|
||||||
async def select_problem(self, **kwargs):
|
async def select_problem(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
@ -752,8 +752,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
# Notify the team that it can draw a problem
|
# Notify the team that it can draw a problem
|
||||||
await self.channel_layer.group_send(f"team-{new_trigram}",
|
await self.channel_layer.group_send(f"team-{new_trigram}",
|
||||||
{'tid': self.tournament_id, 'type': 'draw.notify',
|
{'tid': self.tournament_id, 'type': 'draw.notify',
|
||||||
'title': _("Your turn!"),
|
'title': str(_("Your turn!")),
|
||||||
'body': _("It's your turn to draw a problem!")})
|
'body': str(_("It's your turn to draw a problem!"))})
|
||||||
else:
|
else:
|
||||||
# Pool is ended
|
# Pool is ended
|
||||||
await self.end_pool(pool)
|
await self.end_pool(pool)
|
||||||
@ -829,8 +829,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
# Notify the team that it can draw a dice
|
# Notify the team that it can draw a dice
|
||||||
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
|
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
|
||||||
{'tid': self.tournament_id, 'type': 'draw.notify',
|
{'tid': self.tournament_id, 'type': 'draw.notify',
|
||||||
'title': _("Your turn!"),
|
'title': str(_("Your turn!")),
|
||||||
'body': _("It's your turn to launch the dice!")})
|
'body': str(_("It's your turn to launch the dice!"))})
|
||||||
|
|
||||||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||||
{'tid': self.tournament_id, 'type': 'draw.dice_visibility',
|
{'tid': self.tournament_id, 'type': 'draw.dice_visibility',
|
||||||
@ -863,8 +863,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
# Notify the team that it can draw a dice
|
# Notify the team that it can draw a dice
|
||||||
await self.channel_layer.group_send(f"team-{participation.team.trigram}",
|
await self.channel_layer.group_send(f"team-{participation.team.trigram}",
|
||||||
{'tid': self.tournament_id, 'type': 'draw.notify',
|
{'tid': self.tournament_id, 'type': 'draw.notify',
|
||||||
'title': _("Your turn!"),
|
'title': str(_("Your turn!")),
|
||||||
'body': _("It's your turn to launch the dice!")})
|
'body': str(_("It's your turn to launch the dice!"))})
|
||||||
|
|
||||||
# Reorder dices
|
# Reorder dices
|
||||||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||||
@ -988,8 +988,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
# Notify the team that it can draw a problem
|
# Notify the team that it can draw a problem
|
||||||
await self.channel_layer.group_send(f"team-{new_trigram}",
|
await self.channel_layer.group_send(f"team-{new_trigram}",
|
||||||
{'tid': self.tournament_id, 'type': 'draw.notify',
|
{'tid': self.tournament_id, 'type': 'draw.notify',
|
||||||
'title': _("Your turn!"),
|
'title': str(_("Your turn!")),
|
||||||
'body': _("It's your turn to draw a problem!")})
|
'body': str(_("It's your turn to draw a problem!"))})
|
||||||
|
|
||||||
@ensure_orga
|
@ensure_orga
|
||||||
async def export(self, **kwargs):
|
async def export(self, **kwargs):
|
||||||
@ -1039,7 +1039,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
# Send notification to everyone
|
# Send notification to everyone
|
||||||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||||
{'tid': self.tournament_id, 'type': 'draw.notify',
|
{'tid': self.tournament_id, 'type': 'draw.notify',
|
||||||
'title': _("Draw") + " " + settings.APP_NAME,
|
'title': str(_("Draw")) + " " + settings.APP_NAME,
|
||||||
'body': str(_("The draw of the second round is starting!"))})
|
'body': str(_("The draw of the second round is starting!"))})
|
||||||
|
|
||||||
if settings.TFJM_APP == "TFJM":
|
if settings.TFJM_APP == "TFJM":
|
||||||
@ -1092,8 +1092,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
# Notify the team that it can draw a problem
|
# Notify the team that it can draw a problem
|
||||||
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
|
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
|
||||||
{'tid': self.tournament_id, 'type': 'draw.notify',
|
{'tid': self.tournament_id, 'type': 'draw.notify',
|
||||||
'title': _("Your turn!"),
|
'title': str(_("Your turn!")),
|
||||||
'body': _("It's your turn to draw a problem!")})
|
'body': str(_("It's your turn to draw a problem!"))})
|
||||||
else:
|
else:
|
||||||
async for td in r2.team_draws.prefetch_related('participation__team'):
|
async for td in r2.team_draws.prefetch_related('participation__team'):
|
||||||
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
|
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
|
||||||
|
@ -221,9 +221,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
elem.innerText = `${trigram} 🎲 ${result}`
|
elem.innerText = `${trigram} 🎲 ${result}`
|
||||||
}
|
}
|
||||||
|
|
||||||
let nextTeam = document.querySelector(` div[id="dices-${tid}"] > div > div[class*="text-bg-warning"]`).getAttribute("data-team")
|
let nextTeamDiv = document.querySelector(` div[id="dices-${tid}"] > div > div[class*="text-bg-warning"]`)
|
||||||
if (nextTeam) {
|
if (nextTeamDiv) {
|
||||||
// If there is one team that does not have launched its dice, then we update the debug section
|
// If there is one team that does not have launched its dice, then we update the debug section
|
||||||
|
let nextTeam = nextTeamDiv.getAttribute("data-team")
|
||||||
let debugSpan = document.getElementById(`debug-dice-${tid}-team`)
|
let debugSpan = document.getElementById(`debug-dice-${tid}-team`)
|
||||||
if (debugSpan)
|
if (debugSpan)
|
||||||
debugSpan.innerText = nextTeam
|
debugSpan.innerText = nextTeam
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.db.models.signals import post_save, pre_save
|
from django.db.models.signals import post_save, pre_save
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class ParticipationConfig(AppConfig):
|
class ParticipationConfig(AppConfig):
|
||||||
@ -10,6 +11,7 @@ class ParticipationConfig(AppConfig):
|
|||||||
The participation app contains the data about the teams, solutions, ...
|
The participation app contains the data about the teams, solutions, ...
|
||||||
"""
|
"""
|
||||||
name = 'participation'
|
name = 'participation'
|
||||||
|
verbose_name = _("participations")
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
from participation import signals
|
from participation import signals
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.management import BaseCommand
|
from django.core.management import BaseCommand
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
from django.template.defaultfilters import slugify
|
||||||
from participation.models import Team, Tournament
|
from participation.models import Team, Tournament
|
||||||
from registration.models import ParticipantRegistration, VolunteerRegistration
|
from registration.models import ParticipantRegistration, VolunteerRegistration
|
||||||
from tfjm.lists import get_sympa_client
|
from tfjm.lists import get_sympa_client
|
||||||
@ -36,7 +37,7 @@ class Command(BaseCommand):
|
|||||||
"education", raise_error=False)
|
"education", raise_error=False)
|
||||||
|
|
||||||
for tournament in Tournament.objects.all():
|
for tournament in Tournament.objects.all():
|
||||||
slug = tournament.name.lower().replace(" ", "-")
|
slug = slugify(tournament.name)
|
||||||
sympa.create_list(f"equipes-{slug}", f"Equipes du tournoi {tournament.name}", "hotline",
|
sympa.create_list(f"equipes-{slug}", f"Equipes du tournoi {tournament.name}", "hotline",
|
||||||
f"Liste de diffusion pour contacter toutes les equipes du tournoi {tournament.name}"
|
f"Liste de diffusion pour contacter toutes les equipes du tournoi {tournament.name}"
|
||||||
" du TFJM2.", "education", raise_error=False)
|
" du TFJM2.", "education", raise_error=False)
|
||||||
@ -54,7 +55,7 @@ class Command(BaseCommand):
|
|||||||
for team in Team.objects.filter(participation__valid=True).all():
|
for team in Team.objects.filter(participation__valid=True).all():
|
||||||
team.create_mailing_list()
|
team.create_mailing_list()
|
||||||
sympa.unsubscribe(team.email, "equipes-non-valides", True)
|
sympa.unsubscribe(team.email, "equipes-non-valides", True)
|
||||||
sympa.subscribe(team.email, f"equipes-{team.participation.tournament.name.lower().replace(' ', '-')}",
|
sympa.subscribe(team.email, f"equipes-{slugify(team.participation.tournament.name)}",
|
||||||
True, f"Equipe {team.name}")
|
True, f"Equipe {team.name}")
|
||||||
|
|
||||||
for team in Team.objects.filter(Q(participation__valid=False) | Q(participation__valid__isnull=True)).all():
|
for team in Team.objects.filter(Q(participation__valid=False) | Q(participation__valid__isnull=True)).all():
|
||||||
@ -62,16 +63,16 @@ class Command(BaseCommand):
|
|||||||
sympa.subscribe(team.email, "equipes-non-valides", True, f"Equipe {team.name}")
|
sympa.subscribe(team.email, "equipes-non-valides", True, f"Equipe {team.name}")
|
||||||
|
|
||||||
for participant in ParticipantRegistration.objects.filter(team__isnull=False).all():
|
for participant in ParticipantRegistration.objects.filter(team__isnull=False).all():
|
||||||
sympa.subscribe(participant.user.email, f"equipe-{participant.team.trigram.lower()}",
|
sympa.subscribe(participant.user.email, f"equipe-{slugify(participant.team.trigram)}",
|
||||||
True, f"{participant}")
|
True, f"{participant}")
|
||||||
|
|
||||||
for volunteer in VolunteerRegistration.objects.all():
|
for volunteer in VolunteerRegistration.objects.all():
|
||||||
for organized_tournament in volunteer.organized_tournaments.all():
|
for organized_tournament in volunteer.organized_tournaments.all():
|
||||||
slug = organized_tournament.name.lower().replace(" ", "-")
|
slug = slugify(organized_tournament.name)
|
||||||
sympa.subscribe(volunteer.user.email, f"organisateurs-{slug}", True)
|
sympa.subscribe(volunteer.user.email, f"organisateurs-{slug}", True)
|
||||||
|
|
||||||
for jury_in in volunteer.jury_in.all():
|
for jury_in in volunteer.jury_in.all():
|
||||||
slug = jury_in.tournament.name.lower().replace(" ", "-")
|
slug = slugify(jury_in.tournament.name)
|
||||||
sympa.subscribe(volunteer.user.email, f"jurys-{slug}", True)
|
sympa.subscribe(volunteer.user.email, f"jurys-{slug}", True)
|
||||||
|
|
||||||
for admin in VolunteerRegistration.objects.filter(admin=True).all():
|
for admin in VolunteerRegistration.objects.filter(admin=True).all():
|
||||||
|
@ -15,6 +15,12 @@ from ...models import Tournament
|
|||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
|
"""
|
||||||
|
Création de notifications Google Drive pour récupérer les modifications sur les tableurs de notes.
|
||||||
|
|
||||||
|
Documentation de l'API : https://developers.google.com/calendar/api/guides/push?hl=fr
|
||||||
|
"""
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--tournament', '-t', help="Tournament name to update (if not set, all tournaments will be updated)",
|
'--tournament', '-t', help="Tournament name to update (if not set, all tournaments will be updated)",
|
||||||
|
@ -3,13 +3,13 @@
|
|||||||
|
|
||||||
from datetime import date, timedelta
|
from datetime import date, timedelta
|
||||||
import math
|
import math
|
||||||
import os
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Index
|
from django.db.models import Index, Q
|
||||||
|
from django.template.defaultfilters import slugify
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils import timezone, translation
|
from django.utils import timezone, translation
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
@ -210,14 +210,14 @@ class Team(models.Model):
|
|||||||
"""
|
"""
|
||||||
:return: The mailing list to contact the team members.
|
:return: The mailing list to contact the team members.
|
||||||
"""
|
"""
|
||||||
return f"equipe-{self.trigram.lower()}@{os.getenv('SYMPA_HOST', 'localhost')}"
|
return f"equipe-{slugify(self.trigram)}@{settings.SYMPA_HOST}"
|
||||||
|
|
||||||
def create_mailing_list(self):
|
def create_mailing_list(self):
|
||||||
"""
|
"""
|
||||||
Create a new Sympa mailing list to contact the team.
|
Create a new Sympa mailing list to contact the team.
|
||||||
"""
|
"""
|
||||||
get_sympa_client().create_list(
|
get_sympa_client().create_list(
|
||||||
f"equipe-{self.trigram.lower()}",
|
f"equipe-{slugify(self.trigram)}",
|
||||||
f"Equipe {self.name} ({self.trigram})",
|
f"Equipe {self.name} ({self.trigram})",
|
||||||
"hotline", # TODO Use a custom sympa template
|
"hotline", # TODO Use a custom sympa template
|
||||||
f"Liste de diffusion pour contacter l'equipe {self.name} du TFJM2",
|
f"Liste de diffusion pour contacter l'equipe {self.name} du TFJM2",
|
||||||
@ -231,7 +231,7 @@ class Team(models.Model):
|
|||||||
"""
|
"""
|
||||||
if self.participation.valid: # pragma: no cover
|
if self.participation.valid: # pragma: no cover
|
||||||
get_sympa_client().unsubscribe(
|
get_sympa_client().unsubscribe(
|
||||||
self.email, f"equipes-{self.participation.tournament.name.lower().replace(' ', '-')}", False)
|
self.email, f"equipes-{slugify(self.participation.tournament.name)}", False)
|
||||||
else:
|
else:
|
||||||
get_sympa_client().unsubscribe(self.email, "equipes-non-valides", False)
|
get_sympa_client().unsubscribe(self.email, "equipes-non-valides", False)
|
||||||
get_sympa_client().delete_list(f"equipe-{self.trigram}")
|
get_sympa_client().delete_list(f"equipe-{self.trigram}")
|
||||||
@ -391,28 +391,28 @@ class Tournament(models.Model):
|
|||||||
"""
|
"""
|
||||||
:return: The mailing list to contact the team members.
|
:return: The mailing list to contact the team members.
|
||||||
"""
|
"""
|
||||||
return f"equipes-{self.name.lower().replace(' ', '-')}@{os.getenv('SYMPA_HOST', 'localhost')}"
|
return f"equipes-{slugify(self.name)}@{settings.SYMPA_HOST}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def organizers_email(self):
|
def organizers_email(self):
|
||||||
"""
|
"""
|
||||||
:return: The mailing list to contact the team members.
|
:return: The mailing list to contact the team members.
|
||||||
"""
|
"""
|
||||||
return f"organisateurs-{self.name.lower().replace(' ', '-')}@{os.getenv('SYMPA_HOST', 'localhost')}"
|
return f"organisateurs-{slugify(self.name)}@{settings.SYMPA_HOST}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def jurys_email(self):
|
def jurys_email(self):
|
||||||
"""
|
"""
|
||||||
:return: The mailing list to contact the team members.
|
:return: The mailing list to contact the team members.
|
||||||
"""
|
"""
|
||||||
return f"jurys-{self.name.lower().replace(' ', '-')}@{os.getenv('SYMPA_HOST', 'localhost')}"
|
return f"jurys-{slugify(self.name)}@{settings.SYMPA_HOST}"
|
||||||
|
|
||||||
def create_mailing_lists(self):
|
def create_mailing_lists(self):
|
||||||
"""
|
"""
|
||||||
Create a new Sympa mailing list to contact the team.
|
Create a new Sympa mailing list to contact the team.
|
||||||
"""
|
"""
|
||||||
get_sympa_client().create_list(
|
get_sympa_client().create_list(
|
||||||
f"equipes-{self.name.lower().replace(' ', '-')}",
|
f"equipes-{slugify(self.name)}",
|
||||||
f"Equipes du tournoi de {self.name}",
|
f"Equipes du tournoi de {self.name}",
|
||||||
"hotline", # TODO Use a custom sympa template
|
"hotline", # TODO Use a custom sympa template
|
||||||
f"Liste de diffusion pour contacter les equipes du tournoi {self.name} du TFJM²",
|
f"Liste de diffusion pour contacter les equipes du tournoi {self.name} du TFJM²",
|
||||||
@ -420,7 +420,7 @@ class Tournament(models.Model):
|
|||||||
raise_error=False,
|
raise_error=False,
|
||||||
)
|
)
|
||||||
get_sympa_client().create_list(
|
get_sympa_client().create_list(
|
||||||
f"organisateurs-{self.name.lower().replace(' ', '-')}",
|
f"organisateurs-{slugify(self.name)}",
|
||||||
f"Organisateurs du tournoi de {self.name}",
|
f"Organisateurs du tournoi de {self.name}",
|
||||||
"hotline", # TODO Use a custom sympa template
|
"hotline", # TODO Use a custom sympa template
|
||||||
f"Liste de diffusion pour contacter les equipes du tournoi {self.name} du TFJM²",
|
f"Liste de diffusion pour contacter les equipes du tournoi {self.name} du TFJM²",
|
||||||
@ -846,6 +846,8 @@ class Participation(models.Model):
|
|||||||
return _("Participation of the team {name} ({trigram})").format(name=self.team.name, trigram=self.team.trigram)
|
return _("Participation of the team {name} ({trigram})").format(name=self.team.name, trigram=self.team.trigram)
|
||||||
|
|
||||||
def important_informations(self):
|
def important_informations(self):
|
||||||
|
from survey.models import Survey
|
||||||
|
|
||||||
informations = []
|
informations = []
|
||||||
|
|
||||||
missing_payments = Payment.objects.filter(registrations__in=self.team.participants.all(), valid=False)
|
missing_payments = Payment.objects.filter(registrations__in=self.team.participants.all(), valid=False)
|
||||||
@ -864,6 +866,19 @@ class Participation(models.Model):
|
|||||||
'content': content,
|
'content': content,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if self.valid:
|
||||||
|
for survey in Survey.objects.filter(Q(tournament__isnull=True) | Q(tournament=self.tournament), Q(invite_team=True),
|
||||||
|
~Q(completed_teams=self.team)).all():
|
||||||
|
text = _("Please answer to the survey \"{name}\". You can go to the survey on <a href=\"{survey_link}\">that link</a>, "
|
||||||
|
"using the token code you received by mail.")
|
||||||
|
content = format_lazy(text, name=survey.name, survey_link=f"{settings.LIMESURVEY_URL}/index.php/{survey.survey_id}")
|
||||||
|
informations.append({
|
||||||
|
'title': _("Required answer to survey"),
|
||||||
|
'type': "warning",
|
||||||
|
'priority': 12,
|
||||||
|
'content': content
|
||||||
|
})
|
||||||
|
|
||||||
if self.tournament:
|
if self.tournament:
|
||||||
informations.extend(self.informations_for_tournament(self.tournament))
|
informations.extend(self.informations_for_tournament(self.tournament))
|
||||||
if self.final:
|
if self.final:
|
||||||
@ -1399,8 +1414,8 @@ class Pool(models.Model):
|
|||||||
|
|
||||||
if has_observer:
|
if has_observer:
|
||||||
merge_cells.append(f"{getcol(9 + i * passage_width)}2:{getcol(10 + i * passage_width)}2")
|
merge_cells.append(f"{getcol(9 + i * passage_width)}2:{getcol(10 + i * passage_width)}2")
|
||||||
merge_cells.append(f"{getcol(9 + i * passage_width)}{max_row + 3}"
|
merge_cells.append(f"{getcol(9 + i * passage_width)}{max_row + 3}"
|
||||||
f":{getcol(10 + i * passage_width)}{max_row + 3}")
|
f":{getcol(10 + i * passage_width)}{max_row + 3}")
|
||||||
merge_cells.append(f"A{max_row + 1}:B{max_row + 1}")
|
merge_cells.append(f"A{max_row + 1}:B{max_row + 1}")
|
||||||
merge_cells.append(f"A{max_row + 2}:B{max_row + 2}")
|
merge_cells.append(f"A{max_row + 2}:B{max_row + 2}")
|
||||||
merge_cells.append(f"A{max_row + 3}:B{max_row + 3}")
|
merge_cells.append(f"A{max_row + 3}:B{max_row + 3}")
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.template.defaultfilters import slugify
|
||||||
from participation.models import Note, Participation, Passage, Pool, Team, Tournament
|
from participation.models import Note, Participation, Passage, Pool, Team, Tournament
|
||||||
from registration.models import Payment
|
from registration.models import Payment
|
||||||
from tfjm.lists import get_sympa_client
|
from tfjm.lists import get_sympa_client
|
||||||
@ -34,10 +35,10 @@ def update_mailing_list(instance: Team, raw, **_):
|
|||||||
instance.create_mailing_list()
|
instance.create_mailing_list()
|
||||||
# Subscribe all team members in the mailing list
|
# Subscribe all team members in the mailing list
|
||||||
for student in instance.students.all():
|
for student in instance.students.all():
|
||||||
get_sympa_client().subscribe(student.user.email, f"equipe-{instance.trigram.lower()}", False,
|
get_sympa_client().subscribe(student.user.email, f"equipe-{slugify(instance.trigram)}", False,
|
||||||
f"{student.user.first_name} {student.user.last_name}")
|
f"{student.user.first_name} {student.user.last_name}")
|
||||||
for coach in instance.coaches.all():
|
for coach in instance.coaches.all():
|
||||||
get_sympa_client().subscribe(coach.user.email, f"equipe-{instance.trigram.lower()}", False,
|
get_sympa_client().subscribe(coach.user.email, f"equipe-{slugify(instance.trigram)}", False,
|
||||||
f"{coach.user.first_name} {coach.user.last_name}")
|
f"{coach.user.first_name} {coach.user.last_name}")
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,3 +1,2 @@
|
|||||||
{{ object.name }}
|
{{ object.name }}
|
||||||
{{ object.place }}
|
|
||||||
{{ object.description }}
|
{{ object.description }}
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
{{ object.link }}
|
|
||||||
{{ object.participation.team.name }}
|
|
||||||
{{ object.participation.team.trigram }}
|
|
||||||
{{ object.participation.problem }}
|
|
||||||
{{ object.participation.get_problem_display }}
|
|
@ -22,6 +22,7 @@ from django.db import transaction
|
|||||||
from django.db.models import F
|
from django.db.models import F
|
||||||
from django.http import FileResponse, Http404, HttpResponse
|
from django.http import FileResponse, Http404, HttpResponse
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
|
from django.template.defaultfilters import slugify
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils import timezone, translation
|
from django.utils import timezone, translation
|
||||||
@ -88,7 +89,7 @@ class CreateTeamView(LoginRequiredMixin, CreateView):
|
|||||||
registration.save()
|
registration.save()
|
||||||
|
|
||||||
# Subscribe the user mail address to the team mailing list
|
# Subscribe the user mail address to the team mailing list
|
||||||
get_sympa_client().subscribe(user.email, f"equipe-{form.instance.trigram.lower()}", False,
|
get_sympa_client().subscribe(user.email, f"equipe-{slugify(form.instance.trigram)}", False,
|
||||||
f"{user.first_name} {user.last_name}")
|
f"{user.first_name} {user.last_name}")
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
@ -130,7 +131,7 @@ class JoinTeamView(LoginRequiredMixin, FormView):
|
|||||||
registration.save()
|
registration.save()
|
||||||
|
|
||||||
# Subscribe to the team mailing list
|
# Subscribe to the team mailing list
|
||||||
get_sympa_client().subscribe(user.email, f"equipe-{form.instance.trigram.lower()}", False,
|
get_sympa_client().subscribe(user.email, f"equipe-{slugify(form.instance.trigram)}", False,
|
||||||
f"{user.first_name} {user.last_name}")
|
f"{user.first_name} {user.last_name}")
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
@ -229,10 +230,11 @@ class TeamDetailView(LoginRequiredMixin, FormMixin, ProcessFormView, DetailView)
|
|||||||
self.object.participation.save()
|
self.object.participation.save()
|
||||||
|
|
||||||
mail_context = dict(team=self.object, domain=Site.objects.first().domain)
|
mail_context = dict(team=self.object, domain=Site.objects.first().domain)
|
||||||
mail_plain = render_to_string("participation/mails/request_validation.txt", mail_context)
|
with translation.override(settings.PREFERRED_LANGUAGE_CODE):
|
||||||
mail_html = render_to_string("participation/mails/request_validation.html", mail_context)
|
mail_plain = render_to_string("participation/mails/request_validation.txt", mail_context)
|
||||||
send_mail(f"[{settings.APP_NAME}] {_('Team validation')}", mail_plain, settings.DEFAULT_FROM_EMAIL,
|
mail_html = render_to_string("participation/mails/request_validation.html", mail_context)
|
||||||
[self.object.participation.tournament.organizers_email], html_message=mail_html)
|
send_mail(f"[{settings.APP_NAME}] {_('Team validation')}", mail_plain, settings.DEFAULT_FROM_EMAIL,
|
||||||
|
[self.object.participation.tournament.organizers_email], html_message=mail_html)
|
||||||
|
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
@ -264,19 +266,21 @@ class TeamDetailView(LoginRequiredMixin, FormMixin, ProcessFormView, DetailView)
|
|||||||
message=form.cleaned_data["message"])
|
message=form.cleaned_data["message"])
|
||||||
mail_context_html = dict(domain=domain, registration=registration, team=self.object, payment=payment,
|
mail_context_html = dict(domain=domain, registration=registration, team=self.object, payment=payment,
|
||||||
message=form.cleaned_data["message"].replace('\n', '<br>'))
|
message=form.cleaned_data["message"].replace('\n', '<br>'))
|
||||||
mail_plain = render_to_string("participation/mails/team_validated.txt", mail_context_plain)
|
with translation.override(settings.PREFERRED_LANGUAGE_CODE):
|
||||||
mail_html = render_to_string("participation/mails/team_validated.html", mail_context_html)
|
mail_plain = render_to_string("participation/mails/team_validated.txt", mail_context_plain)
|
||||||
registration.user.email_user(f"[{settings.APP_NAME}] {_('Team validated')}", mail_plain,
|
mail_html = render_to_string("participation/mails/team_validated.html", mail_context_html)
|
||||||
html_message=mail_html)
|
registration.user.email_user(f"[{settings.APP_NAME}] {_('Team validated')}", mail_plain,
|
||||||
|
html_message=mail_html)
|
||||||
elif "invalidate" in self.request.POST:
|
elif "invalidate" in self.request.POST:
|
||||||
self.object.participation.valid = None
|
self.object.participation.valid = None
|
||||||
self.object.participation.save()
|
self.object.participation.save()
|
||||||
mail_context_plain = dict(team=self.object, message=form.cleaned_data["message"])
|
mail_context_plain = dict(team=self.object, message=form.cleaned_data["message"])
|
||||||
mail_context_html = dict(team=self.object, message=form.cleaned_data["message"].replace('\n', '<br>'))
|
mail_context_html = dict(team=self.object, message=form.cleaned_data["message"].replace('\n', '<br>'))
|
||||||
mail_plain = render_to_string("participation/mails/team_not_validated.txt", mail_context_plain)
|
with translation.override(settings.PREFERRED_LANGUAGE_CODE):
|
||||||
mail_html = render_to_string("participation/mails/team_not_validated.html", mail_context_html)
|
mail_plain = render_to_string("participation/mails/team_not_validated.txt", mail_context_plain)
|
||||||
send_mail(f"[{settings.APP_NAME}] {_('Team not validated')}", mail_plain,
|
mail_html = render_to_string("participation/mails/team_not_validated.html", mail_context_html)
|
||||||
None, [self.object.email], html_message=mail_html)
|
send_mail(f"[{settings.APP_NAME}] {_('Team not validated')}", mail_plain,
|
||||||
|
None, [self.object.email], html_message=mail_html)
|
||||||
else:
|
else:
|
||||||
form.add_error(None, _("You must specify if you validate the registration or not."))
|
form.add_error(None, _("You must specify if you validate the registration or not."))
|
||||||
return self.form_invalid(form)
|
return self.form_invalid(form)
|
||||||
@ -313,6 +317,7 @@ class TeamUpdateView(LoginRequiredMixin, UpdateView):
|
|||||||
instance=self.object.participation)
|
instance=self.object.participation)
|
||||||
if not self.request.user.registration.is_volunteer:
|
if not self.request.user.registration.is_volunteer:
|
||||||
del context["participation_form"].fields['final']
|
del context["participation_form"].fields['final']
|
||||||
|
context["participation_form"].helper.layout.remove('final')
|
||||||
context["title"] = _("Update team {trigram}").format(trigram=self.object.trigram)
|
context["title"] = _("Update team {trigram}").format(trigram=self.object.trigram)
|
||||||
return context
|
return context
|
||||||
|
|
||||||
@ -321,6 +326,7 @@ class TeamUpdateView(LoginRequiredMixin, UpdateView):
|
|||||||
participation_form = ParticipationForm(data=self.request.POST or None, instance=self.object.participation)
|
participation_form = ParticipationForm(data=self.request.POST or None, instance=self.object.participation)
|
||||||
if not self.request.user.registration.is_volunteer:
|
if not self.request.user.registration.is_volunteer:
|
||||||
del participation_form.fields['final']
|
del participation_form.fields['final']
|
||||||
|
participation_form.helper.layout.remove('final')
|
||||||
if not participation_form.is_valid():
|
if not participation_form.is_valid():
|
||||||
return self.form_invalid(form)
|
return self.form_invalid(form)
|
||||||
|
|
||||||
@ -517,7 +523,7 @@ class TeamLeaveView(LoginRequiredMixin, TemplateView):
|
|||||||
team = request.user.registration.team
|
team = request.user.registration.team
|
||||||
request.user.registration.team = None
|
request.user.registration.team = None
|
||||||
request.user.registration.save()
|
request.user.registration.save()
|
||||||
get_sympa_client().unsubscribe(request.user.email, f"equipe-{team.trigram.lower()}", False)
|
get_sympa_client().unsubscribe(request.user.email, f"equipe-{slugify(team.trigram)}", False)
|
||||||
if team.students.count() + team.coaches.count() == 0:
|
if team.students.count() + team.coaches.count() == 0:
|
||||||
team.delete()
|
team.delete()
|
||||||
return redirect(reverse_lazy("index"))
|
return redirect(reverse_lazy("index"))
|
||||||
@ -1144,15 +1150,18 @@ class PoolJuryView(VolunteerMixin, FormView, DetailView):
|
|||||||
# Send welcome mail
|
# Send welcome mail
|
||||||
subject = f"[{settings.APP_NAME}] " + str(_("New jury account"))
|
subject = f"[{settings.APP_NAME}] " + str(_("New jury account"))
|
||||||
site = Site.objects.first()
|
site = Site.objects.first()
|
||||||
message = render_to_string('registration/mails/add_organizer.txt', dict(user=user,
|
with translation.override(settings.PREFERRED_LANGUAGE_CODE):
|
||||||
inviter=self.request.user,
|
message = render_to_string('registration/mails/add_organizer.txt',
|
||||||
password=password,
|
dict(user=user,
|
||||||
domain=site.domain))
|
inviter=self.request.user,
|
||||||
html = render_to_string('registration/mails/add_organizer.html', dict(user=user,
|
password=password,
|
||||||
inviter=self.request.user,
|
domain=site.domain))
|
||||||
password=password,
|
html = render_to_string('registration/mails/add_organizer.html',
|
||||||
domain=site.domain))
|
dict(user=user,
|
||||||
user.email_user(subject, message, html_message=html)
|
inviter=self.request.user,
|
||||||
|
password=password,
|
||||||
|
domain=site.domain))
|
||||||
|
user.email_user(subject, message, html_message=html)
|
||||||
|
|
||||||
# Add the user in the jury
|
# Add the user in the jury
|
||||||
self.object.juries.add(reg)
|
self.object.juries.add(reg)
|
||||||
@ -1842,10 +1851,9 @@ class NotationSheetTemplateView(VolunteerMixin, DetailView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
def render_to_response(self, context, **response_kwargs):
|
def render_to_response(self, context, **response_kwargs):
|
||||||
translation.activate(settings.PREFERRED_LANGUAGE_CODE)
|
|
||||||
|
|
||||||
template_name = self.get_template_names()[0]
|
template_name = self.get_template_names()[0]
|
||||||
tex = render_to_string(template_name, context=context, request=self.request)
|
with translation.override(settings.PREFERRED_LANGUAGE_CODE):
|
||||||
|
tex = render_to_string(template_name, context=context, request=self.request)
|
||||||
temp_dir = mkdtemp()
|
temp_dir = mkdtemp()
|
||||||
with open(os.path.join(temp_dir, "texput.tex"), "w") as f:
|
with open(os.path.join(temp_dir, "texput.tex"), "w") as f:
|
||||||
f.write(tex)
|
f.write(tex)
|
||||||
@ -1952,6 +1960,13 @@ class NotationSheetsArchiveView(VolunteerMixin, DetailView):
|
|||||||
|
|
||||||
@method_decorator(csrf_exempt, name='dispatch')
|
@method_decorator(csrf_exempt, name='dispatch')
|
||||||
class GSheetNotificationsView(View):
|
class GSheetNotificationsView(View):
|
||||||
|
"""
|
||||||
|
Cette vue gère les notifications envoyées par Google Drive en cas de
|
||||||
|
modifications d'un tableur de notes sur Google Sheets.
|
||||||
|
|
||||||
|
Documentation de l'API : https://developers.google.com/calendar/api/guides/push?hl=fr
|
||||||
|
"""
|
||||||
|
|
||||||
async def post(self, request, *args, **kwargs):
|
async def post(self, request, *args, **kwargs):
|
||||||
if not await Tournament.objects.filter(pk=kwargs['pk']).aexists():
|
if not await Tournament.objects.filter(pk=kwargs['pk']).aexists():
|
||||||
return HttpResponse(status=404)
|
return HttpResponse(status=404)
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.db.models.signals import post_save, pre_save
|
from django.db.models.signals import post_save, pre_save
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class RegistrationConfig(AppConfig):
|
class RegistrationConfig(AppConfig):
|
||||||
@ -10,6 +11,7 @@ class RegistrationConfig(AppConfig):
|
|||||||
Registration app contains the detail about users only.
|
Registration app contains the detail about users only.
|
||||||
"""
|
"""
|
||||||
name = 'registration'
|
name = 'registration'
|
||||||
|
verbose_name = _("registrations")
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
from registration import signals
|
from registration import signals
|
||||||
|
@ -7,8 +7,6 @@ from django.contrib.auth.forms import UserCreationForm
|
|||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.forms import FileInput
|
from django.forms import FileInput
|
||||||
from django.utils import timezone
|
|
||||||
from django.utils.text import format_lazy
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from .models import CoachRegistration, ParticipantRegistration, Payment, \
|
from .models import CoachRegistration, ParticipantRegistration, Payment, \
|
||||||
@ -38,19 +36,6 @@ class SignupForm(UserCreationForm):
|
|||||||
self.add_error("email", _("This email address is already used."))
|
self.add_error("email", _("This email address is already used."))
|
||||||
return email
|
return email
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
# Check that registrations are opened
|
|
||||||
now = timezone.now()
|
|
||||||
if now < settings.REGISTRATION_DATES['open']:
|
|
||||||
self.add_error(None, format_lazy(_("Registrations are not opened yet. "
|
|
||||||
"They will open on the {opening_date:%Y-%m-%d %H:%M}."),
|
|
||||||
opening_date=settings.REGISTRATION_DATES['open']))
|
|
||||||
elif now > settings.REGISTRATION_DATES['close']:
|
|
||||||
self.add_error(None, format_lazy(_("Registrations for this year are closed since "
|
|
||||||
"{closing_date:%Y-%m-%d %H:%M}."),
|
|
||||||
closing_date=settings.REGISTRATION_DATES['close']))
|
|
||||||
return super().clean()
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields["first_name"].required = True
|
self.fields["first_name"].required = True
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
# Generated by Django 5.1.5 on 2025-03-27 19:18
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("registration", "0014_participantregistration_country"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="participantregistration",
|
||||||
|
name="gender",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[("female", "Female"), ("male", "Male"), ("other", "Other")],
|
||||||
|
max_length=6,
|
||||||
|
verbose_name="gender",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -8,6 +8,7 @@ from django.contrib.sites.models import Site
|
|||||||
from django.core.mail import send_mail
|
from django.core.mail import send_mail
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.db.models import Q
|
||||||
from django.template import loader
|
from django.template import loader
|
||||||
from django.urls import reverse, reverse_lazy
|
from django.urls import reverse, reverse_lazy
|
||||||
from django.utils import timezone, translation
|
from django.utils import timezone, translation
|
||||||
@ -166,7 +167,6 @@ class ParticipantRegistration(Registration):
|
|||||||
("male", _("Male")),
|
("male", _("Male")),
|
||||||
("other", _("Other")),
|
("other", _("Other")),
|
||||||
],
|
],
|
||||||
default="other",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
address = models.CharField(
|
address = models.CharField(
|
||||||
@ -260,6 +260,8 @@ class ParticipantRegistration(Registration):
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def registration_informations(self):
|
def registration_informations(self):
|
||||||
|
from survey.models import Survey
|
||||||
|
|
||||||
informations = []
|
informations = []
|
||||||
if not self.team:
|
if not self.team:
|
||||||
text = _("You are not in a team. You can <a href=\"{create_url}\">create one</a> "
|
text = _("You are not in a team. You can <a href=\"{create_url}\">create one</a> "
|
||||||
@ -300,6 +302,20 @@ class ParticipantRegistration(Registration):
|
|||||||
'content': content,
|
'content': content,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if self.team.participation.valid:
|
||||||
|
for survey in Survey.objects.filter(Q(tournament__isnull=True) | Q(tournament=self.team.participation.tournament),
|
||||||
|
Q(invite_team=False), Q(invite_coaches=True) | Q(invite_coaches=self.is_coach),
|
||||||
|
~Q(completed_registrations=self)):
|
||||||
|
text = _("Please answer to the survey \"{name}\". You can go to the survey on <a href=\"{survey_link}\">that link</a>, "
|
||||||
|
"using the token code you received by mail.")
|
||||||
|
content = format_lazy(text, name=survey.name, survey_link=f"{settings.LIMESURVEY_URL}/index.php/{survey.survey_id}")
|
||||||
|
informations.append({
|
||||||
|
'title': _("Required answer to survey"),
|
||||||
|
'type': "warning",
|
||||||
|
'priority': 12,
|
||||||
|
'content': content
|
||||||
|
})
|
||||||
|
|
||||||
informations.extend(self.team.important_informations())
|
informations.extend(self.team.important_informations())
|
||||||
|
|
||||||
return informations
|
return informations
|
||||||
@ -308,27 +324,27 @@ class ParticipantRegistration(Registration):
|
|||||||
"""
|
"""
|
||||||
The team is selected for final.
|
The team is selected for final.
|
||||||
"""
|
"""
|
||||||
translation.activate(settings.PREFERRED_LANGUAGE_CODE)
|
with translation.override(settings.PREFERRED_LANGUAGE_CODE):
|
||||||
subject = f"[{settings.APP_NAME}] " + str(_("Team selected for the final tournament"))
|
subject = f"[{settings.APP_NAME}] " + str(_("Team selected for the final tournament"))
|
||||||
site = Site.objects.first()
|
site = Site.objects.first()
|
||||||
from participation.models import Tournament
|
from participation.models import Tournament
|
||||||
tournament = Tournament.final_tournament()
|
tournament = Tournament.final_tournament()
|
||||||
payment = self.payments.filter(final=True).first() if self.is_student else None
|
payment = self.payments.filter(final=True).first() if self.is_student else None
|
||||||
message = loader.render_to_string('registration/mails/final_selection.txt',
|
message = loader.render_to_string('registration/mails/final_selection.txt',
|
||||||
{
|
{
|
||||||
'user': self.user,
|
'user': self.user,
|
||||||
'domain': site.domain,
|
'domain': site.domain,
|
||||||
'tournament': tournament,
|
'tournament': tournament,
|
||||||
'payment': payment,
|
'payment': payment,
|
||||||
})
|
})
|
||||||
html = loader.render_to_string('registration/mails/final_selection.html',
|
html = loader.render_to_string('registration/mails/final_selection.html',
|
||||||
{
|
{
|
||||||
'user': self.user,
|
'user': self.user,
|
||||||
'domain': site.domain,
|
'domain': site.domain,
|
||||||
'tournament': tournament,
|
'tournament': tournament,
|
||||||
'payment': payment,
|
'payment': payment,
|
||||||
})
|
})
|
||||||
self.user.email_user(subject, message, html_message=html)
|
self.user.email_user(subject, message, html_message=html)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("participant registration")
|
verbose_name = _("participant registration")
|
||||||
@ -802,35 +818,35 @@ class Payment(models.Model):
|
|||||||
return checkout_intent
|
return checkout_intent
|
||||||
|
|
||||||
def send_remind_mail(self):
|
def send_remind_mail(self):
|
||||||
translation.activate(settings.PREFERRED_LANGUAGE_CODE)
|
with translation.override(settings.PREFERRED_LANGUAGE_CODE):
|
||||||
subject = f"[{settings.APP_NAME}] " + str(_("Reminder for your payment"))
|
subject = f"[{settings.APP_NAME}] " + str(_("Reminder for your payment"))
|
||||||
site = Site.objects.first()
|
site = Site.objects.first()
|
||||||
for registration in self.registrations.all():
|
for registration in self.registrations.all():
|
||||||
message = loader.render_to_string('registration/mails/payment_reminder.txt',
|
message = loader.render_to_string('registration/mails/payment_reminder.txt',
|
||||||
dict(registration=registration, payment=self, domain=site.domain))
|
dict(registration=registration, payment=self, domain=site.domain))
|
||||||
html = loader.render_to_string('registration/mails/payment_reminder.html',
|
html = loader.render_to_string('registration/mails/payment_reminder.html',
|
||||||
dict(registration=registration, payment=self, domain=site.domain))
|
dict(registration=registration, payment=self, domain=site.domain))
|
||||||
registration.user.email_user(subject, message, html_message=html)
|
registration.user.email_user(subject, message, html_message=html)
|
||||||
|
|
||||||
def send_helloasso_payment_confirmation_mail(self):
|
def send_helloasso_payment_confirmation_mail(self):
|
||||||
translation.activate(settings.PREFERRED_LANGUAGE_CODE)
|
with translation.override(settings.PREFERRED_LANGUAGE_CODE):
|
||||||
subject = f"[{settings.APP_NAME}] " + str(_("Payment confirmation"))
|
subject = f"[{settings.APP_NAME}] " + str(_("Payment confirmation"))
|
||||||
site = Site.objects.first()
|
site = Site.objects.first()
|
||||||
for registration in self.registrations.all():
|
for registration in self.registrations.all():
|
||||||
message = loader.render_to_string('registration/mails/payment_confirmation.txt',
|
message = loader.render_to_string('registration/mails/payment_confirmation.txt',
|
||||||
dict(registration=registration, payment=self, domain=site.domain))
|
dict(registration=registration, payment=self, domain=site.domain))
|
||||||
html = loader.render_to_string('registration/mails/payment_confirmation.html',
|
html = loader.render_to_string('registration/mails/payment_confirmation.html',
|
||||||
dict(registration=registration, payment=self, domain=site.domain))
|
dict(registration=registration, payment=self, domain=site.domain))
|
||||||
registration.user.email_user(subject, message, html_message=html)
|
registration.user.email_user(subject, message, html_message=html)
|
||||||
|
|
||||||
payer = self.get_checkout_intent()['order']['payer']
|
payer = self.get_checkout_intent()['order']['payer']
|
||||||
payer_name = f"{payer['firstName']} {payer['lastName']}"
|
payer_name = f"{payer['firstName']} {payer['lastName']}"
|
||||||
if not self.registrations.filter(user__email=payer['email']).exists():
|
if not self.registrations.filter(user__email=payer['email']).exists():
|
||||||
message = loader.render_to_string('registration/mails/payment_confirmation.txt',
|
message = loader.render_to_string('registration/mails/payment_confirmation.txt',
|
||||||
dict(registration=payer_name, payment=self, domain=site.domain))
|
dict(registration=payer_name, payment=self, domain=site.domain))
|
||||||
html = loader.render_to_string('registration/mails/payment_confirmation.html',
|
html = loader.render_to_string('registration/mails/payment_confirmation.html',
|
||||||
dict(registration=payer_name, payment=self, domain=site.domain))
|
dict(registration=payer_name, payment=self, domain=site.domain))
|
||||||
send_mail(subject, message, None, [payer['email']], html_message=html)
|
send_mail(subject, message, None, [payer['email']], html_message=html)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse_lazy("registration:update_payment", args=(self.pk,))
|
return reverse_lazy("registration:update_payment", args=(self.pk,))
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
from django.template.defaultfilters import slugify
|
||||||
from tfjm.lists import get_sympa_client
|
from tfjm.lists import get_sympa_client
|
||||||
|
|
||||||
from .models import Registration, VolunteerRegistration
|
from .models import Registration, VolunteerRegistration
|
||||||
@ -29,8 +30,8 @@ def send_email_link(instance, **_):
|
|||||||
registration.send_email_validation_link()
|
registration.send_email_validation_link()
|
||||||
|
|
||||||
if registration.participates and registration.team:
|
if registration.participates and registration.team:
|
||||||
get_sympa_client().unsubscribe(old_instance.email, f"equipe-{registration.team.trigram.lower()}", False)
|
get_sympa_client().unsubscribe(old_instance.email, f"equipe-{slugify(registration.team.trigram)}", False)
|
||||||
get_sympa_client().subscribe(instance.email, f"equipe-{registration.team.trigram.lower()}", False,
|
get_sympa_client().subscribe(instance.email, f"equipe-{slugify(registration.team.trigram)}", False,
|
||||||
f"{instance.first_name} {instance.last_name}")
|
f"{instance.first_name} {instance.last_name}")
|
||||||
|
|
||||||
|
|
||||||
|
@ -10,13 +10,13 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% now "c" as now %}
|
{% now "c" as now %}
|
||||||
{% if now < TFJM.REGISTRATION_DATES.open.isoformat %}
|
{% if now < TFJM.REGISTRATION_DATES.open.isoformat and not user.registration.is_admin %}
|
||||||
<div class="alert alert-warning">
|
<div class="alert alert-warning">
|
||||||
{% trans "Thank you for your great interest, but registrations are not opened yet!" %}
|
{% trans "Thank you for your great interest, but registrations are not opened yet!" %}
|
||||||
{% trans "They will open on:" %} {{ TFJM.REGISTRATION_DATES.open|date:'DATETIME_FORMAT' }}.
|
{% trans "They will open on:" %} {{ TFJM.REGISTRATION_DATES.open|date:'DATETIME_FORMAT' }}.
|
||||||
{% trans "Please come back at this time to register!" %}
|
{% trans "Please come back at this time to register!" %}
|
||||||
</div>
|
</div>
|
||||||
{% elif now > TFJM.REGISTRATION_DATES.close.isoformat %}
|
{% elif now > TFJM.REGISTRATION_DATES.close.isoformat and not user.registration.is_admin %}
|
||||||
<div class="alert alert-danger">
|
<div class="alert alert-danger">
|
||||||
{% trans "Registrations are closed for this year. We hope to see you next year!" %}
|
{% trans "Registrations are closed for this year. We hope to see you next year!" %}
|
||||||
{% trans "If needed, you can contact us by mail." %}
|
{% trans "If needed, you can contact us by mail." %}
|
||||||
|
@ -66,11 +66,19 @@ organisé \`a :
|
|||||||
Iel se rendra au lieu indiqu\'e ci-dessus le samedi matin et quittera les lieux l'après-midi du dimanche par
|
Iel se rendra au lieu indiqu\'e ci-dessus le samedi matin et quittera les lieux l'après-midi du dimanche par
|
||||||
ses propres moyens et sous la responsabilité du/de la représentant\cdt{}e légal\cdt{}e.
|
ses propres moyens et sous la responsabilité du/de la représentant\cdt{}e légal\cdt{}e.
|
||||||
|
|
||||||
|
{% if tournament.name == "Lyon" %}
|
||||||
|
Un hébergement à titre gratuit sera organisée la nuit du 10 au 11 mai 2025.
|
||||||
|
Le/la participant\cdt{}e sera logé\cdt{}e soit dans les résidences de l'ENS de Lyon situées
|
||||||
|
sur les campus de l'école soit dans l'hotel Ibis Gerland Mérieux situé 246 rue Marcel Mérieux – 69007 LYON.
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
\vspace{8ex}
|
\vspace{8ex}
|
||||||
|
|
||||||
Fait à \vrule width 10cm height 0pt depth 0.4pt, le \phantom{232323}/\phantom{XXX}/{% now "Y" %},
|
Fait à \vrule width 10cm height 0pt depth 0.4pt, le \phantom{232323}/\phantom{XXX}/{% now "Y" %}
|
||||||
|
|
||||||
|
\vspace{4ex}
|
||||||
|
|
||||||
|
Signature :
|
||||||
|
|
||||||
\vfill
|
\vfill
|
||||||
\vfill
|
\vfill
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
{{ object.user.last_name }}
|
{{ object.user.last_name }}
|
||||||
{{ object.user.first_name }}
|
{{ object.user.first_name }}
|
||||||
{{ object.user.email }}
|
{{ object.user.email }}
|
||||||
{{ object.type }}
|
|
||||||
{{ object.role }}
|
|
||||||
|
@ -1,11 +1,4 @@
|
|||||||
{{ object.user.first_name }}
|
{{ object.user.first_name }}
|
||||||
{{ object.user.last_name }}
|
{{ object.user.last_name }}
|
||||||
{{ object.user.email }}
|
{{ object.user.email }}
|
||||||
{{ object.type }}
|
|
||||||
{{ object.professional_activity }}
|
|
||||||
{{ object.address }}
|
|
||||||
{{ object.zip_code }}
|
|
||||||
{{ object.city }}
|
|
||||||
{{ object.phone_number }}
|
{{ object.phone_number }}
|
||||||
{{ object.team.name }}
|
|
||||||
{{ object.team.trigram }}
|
|
||||||
|
@ -1,16 +1,7 @@
|
|||||||
{{ object.user.first_name }}
|
{{ object.user.first_name }}
|
||||||
{{ object.user.last_name }}
|
{{ object.user.last_name }}
|
||||||
{{ object.user.email }}
|
{{ object.user.email }}
|
||||||
{{ object.type }}
|
|
||||||
{{ object.get_student_class_display }}
|
|
||||||
{{ object.school }}
|
|
||||||
{{ object.birth_date }}
|
|
||||||
{{ object.address }}
|
|
||||||
{{ object.zip_code }}
|
|
||||||
{{ object.city }}
|
|
||||||
{{ object.phone_number }}
|
{{ object.phone_number }}
|
||||||
{{ object.responsible_name }}
|
{{ object.responsible_name }}
|
||||||
{{ object.reponsible_phone }}
|
{{ object.reponsible_phone }}
|
||||||
{{ object.reponsible_email }}
|
{{ object.reponsible_email }}
|
||||||
{{ object.team.name }}
|
|
||||||
{{ object.team.trigram }}
|
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
{{ object.user.last_name }}
|
{{ object.user.last_name }}
|
||||||
{{ object.user.first_name }}
|
{{ object.user.first_name }}
|
||||||
{{ object.user.email }}
|
{{ object.user.email }}
|
||||||
{{ object.type }}
|
|
||||||
{{ object.professional_activity }}
|
|
||||||
|
@ -18,7 +18,7 @@ from django.http import FileResponse, Http404
|
|||||||
from django.shortcuts import redirect, resolve_url
|
from django.shortcuts import redirect, resolve_url
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils import translation
|
from django.utils import timezone, translation
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.http import urlsafe_base64_decode
|
from django.utils.http import urlsafe_base64_decode
|
||||||
from django.utils.text import format_lazy
|
from django.utils.text import format_lazy
|
||||||
@ -60,6 +60,22 @@ class SignupView(CreateView):
|
|||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
def get_form(self, form_class=None):
|
||||||
|
form = super().get_form(form_class)
|
||||||
|
if self.request.method in ("POST", "PUT") \
|
||||||
|
and (not self.request.user.is_authenticated or not self.request.user.registration.is_admin):
|
||||||
|
# Check that registrations are opened
|
||||||
|
now = timezone.now()
|
||||||
|
if now < settings.REGISTRATION_DATES['open']:
|
||||||
|
form.add_error(None, format_lazy(_("Registrations are not opened yet. "
|
||||||
|
"They will open on the {opening_date:%Y-%m-%d %H:%M}."),
|
||||||
|
opening_date=settings.REGISTRATION_DATES['open']))
|
||||||
|
elif now > settings.REGISTRATION_DATES['close']:
|
||||||
|
form.add_error(None, format_lazy(_("Registrations for this year are closed since "
|
||||||
|
"{closing_date:%Y-%m-%d %H:%M}."),
|
||||||
|
closing_date=settings.REGISTRATION_DATES['close']))
|
||||||
|
return form
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
role = form.cleaned_data["role"]
|
role = form.cleaned_data["role"]
|
||||||
@ -121,16 +137,17 @@ class AddOrganizerView(VolunteerMixin, CreateView):
|
|||||||
form.instance.set_password(password)
|
form.instance.set_password(password)
|
||||||
form.instance.save()
|
form.instance.save()
|
||||||
|
|
||||||
subject = f"[{settings.APP_NAME}] " + str(_("New organizer account"))
|
with translation.override(settings.PREFERRED_LANGUAGE_CODE):
|
||||||
site = Site.objects.first()
|
subject = f"[{settings.APP_NAME}] " + str(_("New organizer account"))
|
||||||
message = render_to_string('registration/mails/add_organizer.txt', dict(user=registration.user,
|
site = Site.objects.first()
|
||||||
inviter=self.request.user,
|
message = render_to_string('registration/mails/add_organizer.txt', dict(user=registration.user,
|
||||||
password=password,
|
inviter=self.request.user,
|
||||||
domain=site.domain))
|
password=password,
|
||||||
html = render_to_string('registration/mails/add_organizer.html', dict(user=registration.user,
|
domain=site.domain))
|
||||||
inviter=self.request.user,
|
html = render_to_string('registration/mails/add_organizer.html', dict(user=registration.user,
|
||||||
password=password,
|
inviter=self.request.user,
|
||||||
domain=site.domain))
|
password=password,
|
||||||
|
domain=site.domain))
|
||||||
registration.user.email_user(subject, message, html_message=html)
|
registration.user.email_user(subject, message, html_message=html)
|
||||||
|
|
||||||
if registration.is_admin:
|
if registration.is_admin:
|
||||||
@ -445,10 +462,9 @@ class AuthorizationTemplateView(TemplateView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
def render_to_response(self, context, **response_kwargs):
|
def render_to_response(self, context, **response_kwargs):
|
||||||
translation.activate(settings.PREFERRED_LANGUAGE_CODE)
|
|
||||||
|
|
||||||
template_name = self.get_template_names()[0]
|
template_name = self.get_template_names()[0]
|
||||||
tex = render_to_string(template_name, context=context, request=self.request)
|
with translation.override(settings.PREFERRED_LANGUAGE_CODE):
|
||||||
|
tex = render_to_string(template_name, context=context, request=self.request)
|
||||||
temp_dir = mkdtemp()
|
temp_dir = mkdtemp()
|
||||||
with open(os.path.join(temp_dir, "texput.tex"), "w") as f:
|
with open(os.path.join(temp_dir, "texput.tex"), "w") as f:
|
||||||
f.write(tex)
|
f.write(tex)
|
||||||
|
@ -1,29 +1,28 @@
|
|||||||
channels[daphne]~=4.1.0
|
channels[daphne]~=4.2.2
|
||||||
channels-redis~=4.2.0
|
channels-redis~=4.2.1
|
||||||
crispy-bootstrap5~=2024.10
|
citric~=1.4.0
|
||||||
Django>=5.1.2,<6.0
|
crispy-bootstrap5~=2025.4
|
||||||
django-crispy-forms~=2.3
|
Django>=5.2,<6.0
|
||||||
django-extensions~=3.2.3
|
django-crispy-forms~=2.4
|
||||||
django-filter~=24.3
|
django-filter~=25.1
|
||||||
django-haystack~=3.3.0
|
django-haystack~=3.3.0
|
||||||
django-mailer~=2.3.2
|
django-mailer~=2.3.2
|
||||||
django-phonenumber-field~=8.0.0
|
django-phonenumber-field~=8.1.0
|
||||||
django-pipeline~=3.1.0
|
django-pipeline~=4.0.0
|
||||||
django-polymorphic~=3.1.0
|
django-polymorphic~=3.1.0
|
||||||
django-tables2~=2.7.0
|
django-tables2~=2.7.5
|
||||||
djangorestframework~=3.15.2
|
djangorestframework~=3.16.0
|
||||||
django-rest-polymorphic~=0.1.10
|
django-rest-polymorphic~=0.1.10
|
||||||
elasticsearch~=7.17.9
|
elasticsearch~=7.17.9
|
||||||
gspread~=6.1.4
|
gspread~=6.2.0
|
||||||
gunicorn~=23.0.0
|
gunicorn~=23.0.0
|
||||||
odfpy~=1.4.1
|
odfpy~=1.4.1
|
||||||
pandas~=2.2.3
|
pandas~=2.2.3
|
||||||
phonenumbers~=8.13.47
|
phonenumbers~=9.0.3
|
||||||
psycopg~=3.2.3
|
psycopg~=3.2.6
|
||||||
pypdf~=5.0.1
|
pypdf~=5.4.0
|
||||||
ipython~=8.28.0
|
|
||||||
python-magic~=0.4.27
|
python-magic~=0.4.27
|
||||||
requests~=2.32.3
|
requests~=2.32.3
|
||||||
sympasoap~=1.1
|
sympasoap~=1.1
|
||||||
uvicorn~=0.32.0
|
uvicorn~=0.34.2
|
||||||
websockets~=13.1
|
websockets~=15.0.1
|
0
survey/__init__.py
Normal file
0
survey/__init__.py
Normal file
13
survey/admin.py
Normal file
13
survey/admin.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# Copyright (C) 2025 by Animath
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from .models import Survey
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Survey)
|
||||||
|
class SurveyAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('survey_id', 'name', 'invite_team', 'invite_coaches', 'tournament',)
|
||||||
|
list_filter = ('invite_team', 'invite_coaches', 'tournament',)
|
||||||
|
search_fields = ('name',)
|
11
survey/apps.py
Normal file
11
survey/apps.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# Copyright (C) 2025 by Animath
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from django.apps import AppConfig
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class SurveyConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "survey"
|
||||||
|
verbose_name = _("surveys")
|
28
survey/forms.py
Normal file
28
survey/forms.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
from django import forms
|
||||||
|
|
||||||
|
from .models import Survey
|
||||||
|
|
||||||
|
|
||||||
|
class SurveyForm(forms.ModelForm):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
if 'survey_id' in self.initial:
|
||||||
|
self.fields['survey_id'].disabled = True
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Survey
|
||||||
|
exclude = ('completed_registrations', 'completed_teams',)
|
||||||
|
widgets = {
|
||||||
|
'completed_registrations': forms.SelectMultiple(attrs={
|
||||||
|
'class': 'selectpicker',
|
||||||
|
'data-live-search': 'true',
|
||||||
|
'data-live-search-normalize': 'true',
|
||||||
|
'data-width': 'fit',
|
||||||
|
}),
|
||||||
|
'completed_teams': forms.SelectMultiple(attrs={
|
||||||
|
'class': 'selectpicker',
|
||||||
|
'data-live-search': 'true',
|
||||||
|
'data-live-search-normalize': 'true',
|
||||||
|
'data-width': 'fit',
|
||||||
|
}),
|
||||||
|
}
|
13
survey/management/commands/fetch_survey_completion_data.py
Normal file
13
survey/management/commands/fetch_survey_completion_data.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# Copyright (C) 2025 by Animath
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.management import BaseCommand
|
||||||
|
|
||||||
|
from ...models import Survey
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
def handle(self, *args, **kwargs):
|
||||||
|
for survey in Survey.objects.all():
|
||||||
|
survey.fetch_completion_data()
|
83
survey/migrations/0001_initial.py
Normal file
83
survey/migrations/0001_initial.py
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
# Generated by Django 5.1.5 on 2025-03-19 21:12
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
(
|
||||||
|
"participation",
|
||||||
|
"0023_tournament_unified_registration",
|
||||||
|
),
|
||||||
|
("registration", "0014_participantregistration_country"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Survey",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"survey_id",
|
||||||
|
models.IntegerField(
|
||||||
|
help_text="The numeric identifier of the Limesurvey.",
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="survey identifier",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("name", models.CharField(max_length=255, verbose_name="display name")),
|
||||||
|
(
|
||||||
|
"invite_team",
|
||||||
|
models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="When this field is checked, teams will get only one survey invitation instead of one per person.",
|
||||||
|
verbose_name="invite whole team",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"invite_coaches",
|
||||||
|
models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
help_text="When this field is checked, coaches will also be invited in the survey. No effect when the whole team is invited.",
|
||||||
|
verbose_name="invite coaches",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"completed_registrations",
|
||||||
|
models.ManyToManyField(
|
||||||
|
related_name="completed_surveys",
|
||||||
|
to="registration.participantregistration",
|
||||||
|
verbose_name="participants that completed the survey",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"completed_teams",
|
||||||
|
models.ManyToManyField(
|
||||||
|
related_name="completed_surveys",
|
||||||
|
to="participation.team",
|
||||||
|
verbose_name="teams that completed the survey",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"tournament",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
default=None,
|
||||||
|
help_text="When this field is filled, the survey participants will be restricted to this tournament members.",
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to="participation.tournament",
|
||||||
|
verbose_name="tournament restriction",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "survey",
|
||||||
|
"verbose_name_plural": "surveys",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,53 @@
|
|||||||
|
# Generated by Django 5.1.5 on 2025-03-19 22:51
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
(
|
||||||
|
"participation",
|
||||||
|
"0023_tournament_unified_registration",
|
||||||
|
),
|
||||||
|
("registration", "0014_participantregistration_country"),
|
||||||
|
("survey", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="survey",
|
||||||
|
name="completed_registrations",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name="completed_surveys",
|
||||||
|
to="registration.participantregistration",
|
||||||
|
verbose_name="participants that completed the survey",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="survey",
|
||||||
|
name="completed_teams",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name="completed_surveys",
|
||||||
|
to="participation.team",
|
||||||
|
verbose_name="teams that completed the survey",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="survey",
|
||||||
|
name="tournament",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
default=None,
|
||||||
|
help_text="When this field is filled, the survey participants will be restricted to this tournament members.",
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="surveys",
|
||||||
|
to="participation.tournament",
|
||||||
|
verbose_name="tournament restriction",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
0
survey/migrations/__init__.py
Normal file
0
survey/migrations/__init__.py
Normal file
137
survey/models.py
Normal file
137
survey/models.py
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
# Copyright (C) 2025 by Animath
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from citric import Client
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import models
|
||||||
|
from django.urls import reverse_lazy
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from participation.models import Team, Tournament
|
||||||
|
from registration.models import ParticipantRegistration, StudentRegistration
|
||||||
|
|
||||||
|
|
||||||
|
class Survey(models.Model):
|
||||||
|
"""
|
||||||
|
Ce modèle représente un sondage LimeSurvey afin de faciliter l'import des
|
||||||
|
participant⋅es au sondage et d'effectuer le suivi.
|
||||||
|
"""
|
||||||
|
survey_id = models.IntegerField(
|
||||||
|
primary_key=True,
|
||||||
|
verbose_name=_("survey identifier"),
|
||||||
|
help_text=_("The numeric identifier of the Limesurvey."),
|
||||||
|
)
|
||||||
|
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
verbose_name=_("display name"),
|
||||||
|
)
|
||||||
|
|
||||||
|
invite_team = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name=_("invite whole team"),
|
||||||
|
help_text=_("When this field is checked, teams will get only one survey invitation instead of one per person."),
|
||||||
|
)
|
||||||
|
|
||||||
|
invite_coaches = models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
verbose_name=_("invite coaches"),
|
||||||
|
help_text=_("When this field is checked, coaches will also be invited in the survey. No effect when the whole team is invited."),
|
||||||
|
)
|
||||||
|
|
||||||
|
tournament = models.ForeignKey(
|
||||||
|
Tournament,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
default=None,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name="surveys",
|
||||||
|
verbose_name=_("tournament restriction"),
|
||||||
|
help_text=_("When this field is filled, the survey participants will be restricted to this tournament members."),
|
||||||
|
)
|
||||||
|
|
||||||
|
completed_registrations = models.ManyToManyField(
|
||||||
|
ParticipantRegistration,
|
||||||
|
blank=True,
|
||||||
|
related_name="completed_surveys",
|
||||||
|
verbose_name=_("participants that completed the survey"),
|
||||||
|
)
|
||||||
|
|
||||||
|
completed_teams = models.ManyToManyField(
|
||||||
|
Team,
|
||||||
|
blank=True,
|
||||||
|
related_name="completed_surveys",
|
||||||
|
verbose_name=_("teams that completed the survey"),
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def participants(self):
|
||||||
|
if self.invite_team:
|
||||||
|
teams = Team.objects.filter(participation__valid=True)
|
||||||
|
if self.tournament:
|
||||||
|
teams = teams.filter(participation__tournament=self.tournament)
|
||||||
|
return teams.all()
|
||||||
|
else:
|
||||||
|
if self.invite_coaches:
|
||||||
|
registrations = ParticipantRegistration.objects.filter(team__participation__valid=True)
|
||||||
|
else:
|
||||||
|
registrations = StudentRegistration.objects.filter(team__participation__valid=True)
|
||||||
|
if self.tournament:
|
||||||
|
registrations = registrations.filter(team__participation__tournament=self.tournament)
|
||||||
|
return registrations.all()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def completed(self):
|
||||||
|
if self.invite_team:
|
||||||
|
return self.completed_teams
|
||||||
|
else:
|
||||||
|
return self.completed_registrations
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse_lazy("survey:survey_detail", args=(self.survey_id,))
|
||||||
|
|
||||||
|
def generate_participants_data(self):
|
||||||
|
participants_data = []
|
||||||
|
if self.invite_team:
|
||||||
|
for team in self.participants:
|
||||||
|
participant_data = {"firstname": team.name, "lastname": f"(équipe {team.trigram})", "email": team.email}
|
||||||
|
participants_data.append(participant_data)
|
||||||
|
else:
|
||||||
|
for reg in self.participants:
|
||||||
|
participant_data = {"firstname": reg.user.first_name, "lastname": reg.user.last_name, "email": reg.user.email}
|
||||||
|
participants_data.append(participant_data)
|
||||||
|
return participants_data
|
||||||
|
|
||||||
|
def invite_all(self):
|
||||||
|
participants_data = self.generate_participants_data()
|
||||||
|
with Client(f"{settings.LIMESURVEY_URL}/index.php/admin/remotecontrol", settings.LIMESURVEY_USER, settings.LIMESURVEY_PASSWORD) as client:
|
||||||
|
try:
|
||||||
|
current_participants = client.list_participants(self.survey_id, limit=10000)
|
||||||
|
except:
|
||||||
|
current_participants = []
|
||||||
|
current_participants_email = set(participant['participant_info']['email'] for participant in current_participants)
|
||||||
|
participants_data = [participant_data for participant_data in participants_data if participant_data['email'] not in current_participants_email]
|
||||||
|
try:
|
||||||
|
client.activate_tokens(self.survey_id)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
new_participants = client.add_participants(self.survey_id, participant_data=participants_data)
|
||||||
|
if new_participants:
|
||||||
|
client.invite_participants(self.survey_id, token_ids=[participant['tid'] for participant in new_participants])
|
||||||
|
return new_participants
|
||||||
|
|
||||||
|
def fetch_completion_data(self):
|
||||||
|
with Client(f"{settings.LIMESURVEY_URL}/index.php/admin/remotecontrol", settings.LIMESURVEY_USER, settings.LIMESURVEY_PASSWORD) as client:
|
||||||
|
participants = client.list_participants(self.survey_id, limit=10000, attributes=['completed'])
|
||||||
|
if self.invite_team:
|
||||||
|
team_names = [participant['participant_info']['firstname'] for participant in participants if participant['completed'] != 'N']
|
||||||
|
self.completed_teams.set(list(Team.objects.filter(name__in=team_names).values_list('id', flat=True)))
|
||||||
|
else:
|
||||||
|
mails = [participant['participant_info']['email'] for participant in participants if participant['completed'] != 'N']
|
||||||
|
self.completed_registrations.set(list(ParticipantRegistration.objects.filter(user__email__in=mails).values_list('id', flat=True)))
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("survey")
|
||||||
|
verbose_name_plural = _("surveys")
|
31
survey/tables.py
Normal file
31
survey/tables.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Copyright (C) 2025 by Animath
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
import django_tables2 as tables
|
||||||
|
|
||||||
|
from .models import Survey
|
||||||
|
|
||||||
|
|
||||||
|
class SurveyTable(tables.Table):
|
||||||
|
survey_id = tables.LinkColumn(
|
||||||
|
'survey:survey_detail',
|
||||||
|
args=[tables.A('survey_id')],
|
||||||
|
verbose_name=lambda: _("survey identifier").capitalize(),
|
||||||
|
)
|
||||||
|
|
||||||
|
nb_completed = tables.Column(
|
||||||
|
verbose_name=_("completed").capitalize,
|
||||||
|
accessor='survey_id'
|
||||||
|
)
|
||||||
|
|
||||||
|
def render_nb_completed(self, record):
|
||||||
|
return f"{record.completed.count()}/{record.participants.count()}"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
attrs = {
|
||||||
|
'class': 'table table-condensed table-striped',
|
||||||
|
}
|
||||||
|
model = Survey
|
||||||
|
fields = ('survey_id', 'name', 'invite_team', 'invite_coaches', 'tournament', 'nb_completed',)
|
||||||
|
order_by = ('survey_id',)
|
84
survey/templates/survey/survey_detail.html
Normal file
84
survey/templates/survey/survey_detail.html
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
{% load crispy_forms_filters %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="card bg-body shadow">
|
||||||
|
<div class="card-header text-center">
|
||||||
|
<h4>
|
||||||
|
{% trans "survey"|capfirst %} {{ survey.survey_id }}
|
||||||
|
<a href="{{ TFJM.LIMESURVEY_URL }}/index.php/{{ survey.survey_id }}" target="_blank"><i class="fas fa-arrow-up-right-from-square"></i></a>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<dl class="row">
|
||||||
|
<dt class="col-sm-6 text-sm-end">{% trans "Name:" %}</dt>
|
||||||
|
<dd class="col-sm-6">{{ survey.name }}</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-6 text-sm-end">{% trans "One answer per team:" %}</dt>
|
||||||
|
<dd class="col-sm-6">{{ survey.invite_team|yesno }}</dd>
|
||||||
|
|
||||||
|
{% if not survey.invite_team %}
|
||||||
|
<dt class="col-sm-6 text-sm-end">{% trans "Coaches can answer the survey:" %}</dt>
|
||||||
|
<dd class="col-sm-6">{{ survey.invite_coaches|yesno }}</dd>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if survey.tournament %}
|
||||||
|
<dt class="col-sm-6 text-sm-end">{% trans "Tournament restriction:" %}</dt>
|
||||||
|
<dd class="col-sm-6">{{ survey.tournament }}</dd>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<dt class="col-sm-6 text-sm-end">{% trans "Completion rate:" %}</dt>
|
||||||
|
<dd class="col-sm-6">
|
||||||
|
{{ survey.completed.count }}/{{ survey.participants.count }}
|
||||||
|
<a href="{% url "survey:survey_refresh_completed" pk=survey.pk %}"><i class="fas fa-arrow-rotate-right" alt="refresh"></i></a>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer text-center">
|
||||||
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#updateSurveyModal">{% trans "Update" %}</button>
|
||||||
|
<a class="btn btn-secondary" href="{% url "survey:survey_invite" pk=survey.pk %}">{% trans "Send invites" %}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<table class="table table-condensed table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "participant"|capfirst %}</th>
|
||||||
|
<th>{% trans "completed"|capfirst %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for participant in survey.participants %}
|
||||||
|
<tr class="{% if participant in survey.completed.all %}table-success{% else %}table-danger{% endif %}">
|
||||||
|
{% if survey.invite_team %}
|
||||||
|
<td>{% trans "Team" %} {{ participant.name }} ({{ participant.trigram }})</td>
|
||||||
|
{% else %}
|
||||||
|
<td>{{ participant.user.first_name }} {{ participant.user.last_name }} ({% trans "team" %} {{ participant.team.trigram }})</td>
|
||||||
|
{% endif %}
|
||||||
|
{% if participant in survey.completed.all %}
|
||||||
|
<td>{% trans "Yes" %}</td>
|
||||||
|
{% else %}
|
||||||
|
<td>{% trans "No" %}</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{% trans "Update survey" as modal_title %}
|
||||||
|
{% trans "Update" as modal_button %}
|
||||||
|
{% url "survey:survey_update" pk=survey.pk as modal_action %}
|
||||||
|
{% include "base_modal.html" with modal_id="updateSurvey" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extrajavascript %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
initModal("updateSurvey", "{% url "survey:survey_update" pk=survey.pk %}")
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
17
survey/templates/survey/survey_form.html
Normal file
17
survey/templates/survey/survey_form.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{% extends request.content_only|yesno:"empty.html,base.html" %}
|
||||||
|
|
||||||
|
{% load crispy_forms_filters i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form method="post">
|
||||||
|
<div id="form-content">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form|crispy }}
|
||||||
|
</div>
|
||||||
|
{% if object.pk %}
|
||||||
|
<button class="btn btn-primary" type="submit">{% trans "Update" %}</button>
|
||||||
|
{% else %}
|
||||||
|
<button class="btn btn-success" type="submit">{% trans "Create" %}</button>
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
|
{% endblock content %}
|
14
survey/templates/survey/survey_list.html
Normal file
14
survey/templates/survey/survey_list.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% load django_tables2 i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="d-grid">
|
||||||
|
<a href="{% url "survey:survey_create" %}" class="btn gap-0 btn-success">
|
||||||
|
<i class="fas fa-square-poll-horizontal"></i> {% trans "Add survey" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
{% render_table table %}
|
||||||
|
{% endblock %}
|
3
survey/tests.py
Normal file
3
survey/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
18
survey/urls.py
Normal file
18
survey/urls.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Copyright (C) 2025 by Animath
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from .views import SurveyCreateView, SurveyDetailView, SurveyInviteView, \
|
||||||
|
SurveyListView, SurveyRefreshCompletedView, SurveyUpdateView
|
||||||
|
|
||||||
|
app_name = "survey"
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("", SurveyListView.as_view(), name="survey_list"),
|
||||||
|
path("create/", SurveyCreateView.as_view(), name="survey_create"),
|
||||||
|
path("<int:pk>/", SurveyDetailView.as_view(), name="survey_detail"),
|
||||||
|
path("<int:pk>/invite/", SurveyInviteView.as_view(), name="survey_invite"),
|
||||||
|
path("<int:pk>/refresh/", SurveyRefreshCompletedView.as_view(), name="survey_refresh_completed"),
|
||||||
|
path("<int:pk>/update/", SurveyUpdateView.as_view(), name="survey_update"),
|
||||||
|
]
|
56
survey/views.py
Normal file
56
survey/views.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# Copyright (C) 2025 by Animath
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.views.generic import CreateView, DetailView, UpdateView
|
||||||
|
from django_tables2 import SingleTableView
|
||||||
|
|
||||||
|
from tfjm.views import AdminMixin
|
||||||
|
from .forms import SurveyForm
|
||||||
|
from .models import Survey
|
||||||
|
from .tables import SurveyTable
|
||||||
|
|
||||||
|
|
||||||
|
class SurveyListView(AdminMixin, SingleTableView):
|
||||||
|
model = Survey
|
||||||
|
table_class = SurveyTable
|
||||||
|
template_name = "survey/survey_list.html"
|
||||||
|
|
||||||
|
|
||||||
|
class SurveyCreateView(AdminMixin, CreateView):
|
||||||
|
model = Survey
|
||||||
|
form_class = SurveyForm
|
||||||
|
|
||||||
|
|
||||||
|
class SurveyDetailView(AdminMixin, DetailView):
|
||||||
|
model = Survey
|
||||||
|
|
||||||
|
|
||||||
|
class SurveyInviteView(AdminMixin, DetailView):
|
||||||
|
model = Survey
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
survey = self.get_object()
|
||||||
|
new_participants = survey.invite_all()
|
||||||
|
if new_participants:
|
||||||
|
messages.success(request, _("Invites sent!"))
|
||||||
|
else:
|
||||||
|
messages.warning(request, _("All invites were already sent."))
|
||||||
|
return redirect("survey:survey_detail", survey.pk)
|
||||||
|
|
||||||
|
|
||||||
|
class SurveyRefreshCompletedView(AdminMixin, DetailView):
|
||||||
|
model = Survey
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
survey = self.get_object()
|
||||||
|
survey.fetch_completion_data()
|
||||||
|
messages.success(request, _("Completion data refreshed!"))
|
||||||
|
return redirect("survey:survey_detail", survey.pk)
|
||||||
|
|
||||||
|
|
||||||
|
class SurveyUpdateView(AdminMixin, UpdateView):
|
||||||
|
model = Survey
|
||||||
|
form_class = SurveyForm
|
13
tfjm.cron
13
tfjm.cron
@ -11,15 +11,16 @@
|
|||||||
7 3 * * * cd /code && python manage.py fix_sympa_lists &> /dev/null
|
7 3 * * * cd /code && python manage.py fix_sympa_lists &> /dev/null
|
||||||
|
|
||||||
# Check payments from Hello Asso
|
# Check payments from Hello Asso
|
||||||
*/6 * * * * cd /code && python manage.py check_hello_asso &> /dev/null
|
*/30 * * 03-05 * cd /code && python manage.py check_hello_asso -v 0
|
||||||
# Send reminders for payments
|
|
||||||
30 6 * * 1 cd /code && python manage.py remind_payments &> /dev/null
|
|
||||||
|
|
||||||
# Check notation sheets every 15 minutes from 08:00 to 23:00 on fridays to mondays in april and may
|
# Send reminders for payments
|
||||||
# */15 8-23 * 4-5 5,6,7,1 cd /code && python manage.py parse_notation_sheets -v 0
|
30 6 * 03-05 1 cd /code && python manage.py remind_payments -v 0
|
||||||
|
|
||||||
# Update Google Drive notifications daily
|
# Update Google Drive notifications daily
|
||||||
0 0 * * * cd /code && python manage.py renew_gdrive_notifications &> /dev/null
|
0 0 * * * cd /code && python manage.py renew_gdrive_notifications -v 0
|
||||||
|
|
||||||
|
# Fetch LimeSurvey completion data
|
||||||
|
*/15 * * 03-06 * cd /code && python manage.py fetch_survey_completion_data -v 0
|
||||||
|
|
||||||
# Clean temporary files
|
# Clean temporary files
|
||||||
30 * * * * rm -rf /tmp/*
|
30 * * * * rm -rf /tmp/*
|
||||||
|
@ -13,6 +13,7 @@ def tfjm_context(request):
|
|||||||
'HAS_OBSERVER': settings.HAS_OBSERVER,
|
'HAS_OBSERVER': settings.HAS_OBSERVER,
|
||||||
'HAS_FINAL': settings.HAS_FINAL,
|
'HAS_FINAL': settings.HAS_FINAL,
|
||||||
'HOME_PAGE_LINK': settings.HOME_PAGE_LINK,
|
'HOME_PAGE_LINK': settings.HOME_PAGE_LINK,
|
||||||
|
'LIMESURVEY_URL': settings.LIMESURVEY_URL,
|
||||||
'LOGO_PATH': "tfjm/img/" + settings.LOGO_FILE,
|
'LOGO_PATH': "tfjm/img/" + settings.LOGO_FILE,
|
||||||
'NB_ROUNDS': settings.NB_ROUNDS,
|
'NB_ROUNDS': settings.NB_ROUNDS,
|
||||||
'ML_MANAGEMENT': settings.ML_MANAGEMENT,
|
'ML_MANAGEMENT': settings.ML_MANAGEMENT,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# Copyright (C) 2020 by Animath
|
# Copyright (C) 2020 by Animath
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
import os
|
from django.conf import settings
|
||||||
|
|
||||||
_client = None
|
_client = None
|
||||||
|
|
||||||
@ -9,10 +9,10 @@ _client = None
|
|||||||
def get_sympa_client():
|
def get_sympa_client():
|
||||||
global _client
|
global _client
|
||||||
if _client is None:
|
if _client is None:
|
||||||
if os.getenv("SYMPA_PASSWORD", None): # pragma: no cover
|
if settings.SYMPA_PASSWORD is not None: # pragma: no cover
|
||||||
from sympasoap import Client
|
from sympasoap import Client
|
||||||
_client = Client("https://" + os.getenv("SYMPA_URL"))
|
_client = Client("https://" + settings.SYMPA_URL)
|
||||||
_client.login(os.getenv("SYMPA_EMAIL"), os.getenv("SYMPA_PASSWORD"))
|
_client.login(settings.SYMPA_EMAIL, settings.SYMPA_PASSWORD)
|
||||||
else:
|
else:
|
||||||
_client = FakeSympaSoapClient()
|
_client = FakeSympaSoapClient()
|
||||||
return _client
|
return _client
|
||||||
|
@ -74,11 +74,11 @@ INSTALLED_APPS = [
|
|||||||
'draw',
|
'draw',
|
||||||
'registration',
|
'registration',
|
||||||
'participation',
|
'participation',
|
||||||
|
'survey',
|
||||||
]
|
]
|
||||||
|
|
||||||
if "test" not in sys.argv: # pragma: no cover
|
if "test" not in sys.argv: # pragma: no cover
|
||||||
INSTALLED_APPS += [
|
INSTALLED_APPS += [
|
||||||
'django_extensions',
|
|
||||||
'mailer',
|
'mailer',
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -300,6 +300,12 @@ CHANNEL_LAYERS = {
|
|||||||
PHONENUMBER_DB_FORMAT = 'NATIONAL'
|
PHONENUMBER_DB_FORMAT = 'NATIONAL'
|
||||||
PHONENUMBER_DEFAULT_REGION = 'FR'
|
PHONENUMBER_DEFAULT_REGION = 'FR'
|
||||||
|
|
||||||
|
# Sympa configuration
|
||||||
|
SYMPA_HOST = os.getenv("SYMPA_HOST", "localhost")
|
||||||
|
SYMPA_URL = os.getenv("SYMPA_URL", "localhost")
|
||||||
|
SYMPA_EMAIL = os.getenv("SYMPA_EMAIL", "contact@localhost")
|
||||||
|
SYMPA_PASSWORD = os.getenv("SYMPA_PASSWORD", None)
|
||||||
|
|
||||||
# Hello Asso API creds
|
# Hello Asso API creds
|
||||||
HELLOASSO_CLIENT_ID = os.getenv('HELLOASSO_CLIENT_ID', 'CHANGE_ME_IN_ENV_SETTINGS')
|
HELLOASSO_CLIENT_ID = os.getenv('HELLOASSO_CLIENT_ID', 'CHANGE_ME_IN_ENV_SETTINGS')
|
||||||
HELLOASSO_CLIENT_SECRET = os.getenv('HELLOASSO_CLIENT_SECRET', 'CHANGE_ME_IN_ENV_SETTINGS')
|
HELLOASSO_CLIENT_SECRET = os.getenv('HELLOASSO_CLIENT_SECRET', 'CHANGE_ME_IN_ENV_SETTINGS')
|
||||||
@ -322,6 +328,10 @@ GOOGLE_SERVICE_CLIENT = {
|
|||||||
# The ID of the Google Drive folder where to store the notation sheets
|
# The ID of the Google Drive folder where to store the notation sheets
|
||||||
NOTES_DRIVE_FOLDER_ID = os.getenv("NOTES_DRIVE_FOLDER_ID", "CHANGE_ME_IN_ENV_SETTINGS")
|
NOTES_DRIVE_FOLDER_ID = os.getenv("NOTES_DRIVE_FOLDER_ID", "CHANGE_ME_IN_ENV_SETTINGS")
|
||||||
|
|
||||||
|
LIMESURVEY_URL = os.getenv("LIMESURVEY_URL", "https://survey.example.com")
|
||||||
|
LIMESURVEY_USER = os.getenv("LIMESURVEY_USER", "CHANGE_ME_IN_ENV_SETTINGS")
|
||||||
|
LIMESURVEY_PASSWORD = os.getenv("LIMESURVEY_PASSWORD", "CHANGE_ME_IN_ENV_SETTINGS")
|
||||||
|
|
||||||
# Custom parameters
|
# Custom parameters
|
||||||
FORBIDDEN_TRIGRAMS = [
|
FORBIDDEN_TRIGRAMS = [
|
||||||
"BIT",
|
"BIT",
|
||||||
@ -379,14 +389,14 @@ if TFJM_APP == "TFJM":
|
|||||||
)
|
)
|
||||||
|
|
||||||
PROBLEMS = [
|
PROBLEMS = [
|
||||||
"Triominos",
|
"Une bonne humeur contagieuse",
|
||||||
"Rassemblements mathématiques",
|
"Drôles de toboggans",
|
||||||
"Tournoi de ping-pong",
|
"Plats à tarte gradués",
|
||||||
"Dépollution de la Seine",
|
"Transformation de papillons",
|
||||||
"Électron libre",
|
"Gerrymandering",
|
||||||
"Pièces truquées",
|
"Le cauchemar de la ligne 20-25",
|
||||||
"Drôles de cookies",
|
"Taxes routières",
|
||||||
"Création d'un jeu",
|
"Points colorés sur un cercle",
|
||||||
]
|
]
|
||||||
elif TFJM_APP == "ETEAM":
|
elif TFJM_APP == "ETEAM":
|
||||||
PREFERRED_LANGUAGE_CODE = 'en'
|
PREFERRED_LANGUAGE_CODE = 'en'
|
||||||
|
6
tfjm/static/bootstrap/css/bootstrap.min.css
vendored
6
tfjm/static/bootstrap/css/bootstrap.min.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,9 +1,9 @@
|
|||||||
function initModal(target, url, content_id = 'form-content') {
|
function initModal(target, url, content_id = 'form-content', always_refetch = false) {
|
||||||
document.querySelectorAll('[data-bs-target="#' + target + 'Modal"]')
|
document.querySelectorAll('[data-bs-target="#' + target + 'Modal"]')
|
||||||
.forEach(elem => elem.addEventListener('click', () => {
|
.forEach(elem => elem.addEventListener('click', () => {
|
||||||
let modalBody = document.querySelector("#" + target + "Modal div.modal-body")
|
let modalBody = document.querySelector("#" + target + "Modal div.modal-body")
|
||||||
|
|
||||||
if (!modalBody.innerHTML.trim()) {
|
if (!modalBody.innerHTML.trim() || always_refetch) {
|
||||||
if (url instanceof Function) url = url()
|
if (url instanceof Function) url = url()
|
||||||
|
|
||||||
fetch(url, {headers: {'CONTENT-ONLY': '1'}})
|
fetch(url, {headers: {'CONTENT-ONLY': '1'}})
|
||||||
|
@ -106,7 +106,7 @@
|
|||||||
{% if user.is_authenticated and user.registration.is_admin %}
|
{% if user.is_authenticated and user.registration.is_admin %}
|
||||||
initModal("search",
|
initModal("search",
|
||||||
() => "{% url "haystack_search" %}?q=" + encodeURI(document.getElementById("search-term").value),
|
() => "{% url "haystack_search" %}?q=" + encodeURI(document.getElementById("search-term").value),
|
||||||
"search-results")
|
"search-results", true)
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if not user.is_authenticated %}
|
{% if not user.is_authenticated %}
|
||||||
|
@ -30,15 +30,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="alert alert-warning">
|
|
||||||
<h3 class="alert-heading"><i class="fas fa-warning"></i> {% trans "New in 2025" %}</h3>
|
|
||||||
{% blocktrans trimmed %}
|
|
||||||
Registration for Ile-de-France tournaments is now unified.
|
|
||||||
If you live in or near the Ile-de-France region, your registration will be pooled with each of the region's tournaments,
|
|
||||||
and the organizers will take care of team allocation. However, date constraints can be indicated in the motivation letter.
|
|
||||||
{% endblocktrans %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="jumbotron p-5 border rounded-5">
|
<div class="jumbotron p-5 border rounded-5">
|
||||||
<h5 class="display-4">{% trans "How does it work?" %}</h5>
|
<h5 class="display-4">{% trans "How does it work?" %}</h5>
|
||||||
<p>
|
<p>
|
||||||
|
@ -74,6 +74,9 @@
|
|||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if user.registration.is_admin %}
|
{% if user.registration.is_admin %}
|
||||||
|
<li class="nav-item active">
|
||||||
|
<a class="nav-link" href="{% url "survey:survey_list" %}"><i class="fas fa-square-poll-horizontal"></i> {% trans "surveys"|capfirst %}</a>
|
||||||
|
</li>
|
||||||
<li class="nav-item active">
|
<li class="nav-item active">
|
||||||
<a class="nav-link" href="{% url "admin:index" %}"><i class="fas fa-cog"></i> {% trans "Administration" %}</a>
|
<a class="nav-link" href="{% url "admin:index" %}"><i class="fas fa-cog"></i> {% trans "Administration" %}</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -44,6 +44,7 @@ urlpatterns = [
|
|||||||
path('draw/', include('draw.urls')),
|
path('draw/', include('draw.urls')),
|
||||||
path('participation/', include('participation.urls')),
|
path('participation/', include('participation.urls')),
|
||||||
path('registration/', include('registration.urls')),
|
path('registration/', include('registration.urls')),
|
||||||
|
path('survey/', include('survey.urls')),
|
||||||
|
|
||||||
path('media/authorization/photo/<str:filename>/', PhotoAuthorizationView.as_view(),
|
path('media/authorization/photo/<str:filename>/', PhotoAuthorizationView.as_view(),
|
||||||
name='photo_authorization'),
|
name='photo_authorization'),
|
||||||
|
Reference in New Issue
Block a user