2024-02-16 16:54:10 +01:00
|
|
|
from datetime import date, datetime
|
|
|
|
from django.shortcuts import render, get_object_or_404
|
|
|
|
from django.contrib.auth.decorators import login_required
|
|
|
|
from django.views.decorators.http import require_http_methods
|
|
|
|
from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
|
|
|
|
from django.db.models import Min, Avg, Max, Sum
|
|
|
|
from django.urls import reverse
|
|
|
|
from django.conf import settings
|
|
|
|
from django.contrib.auth import get_user_model
|
|
|
|
from django.contrib.contenttypes.models import ContentType
|
|
|
|
from django.core.mail import send_mail
|
|
|
|
|
|
|
|
import pendulum
|
|
|
|
|
|
|
|
from jarvis.core.models import Email
|
|
|
|
from jarvis.people.models import Gymnast
|
|
|
|
from jarvis.tools.date_week_transition import from_date_to_week_number
|
|
|
|
from jarvis.tools.models import Season
|
|
|
|
from .models import (
|
|
|
|
Chrono,
|
|
|
|
ChronoDetails,
|
|
|
|
)
|
|
|
|
|
|
|
|
from .forms import ChronoForm
|
|
|
|
|
|
|
|
from .models import (
|
|
|
|
SCORE_TYPE_CHOICE,
|
|
|
|
CHRONO_TYPE_CHOICE,
|
|
|
|
)
|
|
|
|
|
|
|
|
from .email_vars import MAIL_HEADER, MAIL_FOOTER
|
|
|
|
|
|
|
|
User = get_user_model()
|
|
|
|
|
|
|
|
|
|
|
|
@login_required
|
|
|
|
@require_http_methods(["GET"])
|
|
|
|
def jump_chrono_details(request, chrono_id):
|
|
|
|
"""Récupère toutes les informations détaillées d'un chrono. La fonction en profite pour
|
|
|
|
recalculer le total et s'assure que cela correspond à la valeur stockée dans le model
|
|
|
|
Chrono.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
chrono_id (int) identifiant chrono
|
|
|
|
|
|
|
|
QTF : Est-ce que je ne devrais pas faire un prefetch_related sur mon objet chrono pour
|
|
|
|
optimiser mon affichage ?
|
|
|
|
chrono = Chrono.object.get(pk=chrono_id).prefetch_related('chrono_details') ?
|
|
|
|
"""
|
|
|
|
|
|
|
|
chrono = get_object_or_404(Chrono, pk=chrono_id)
|
|
|
|
|
|
|
|
if not request.user.is_superuser and (
|
|
|
|
request.session.has_key("available_gymnast")
|
|
|
|
and chrono.gymnast.id not in request.session["available_gymnast"]
|
|
|
|
):
|
|
|
|
return chrono_listing(request)
|
|
|
|
|
|
|
|
sum_value = chrono.details.all().aggregate(total=Sum("value"))
|
|
|
|
|
|
|
|
if chrono.score != sum_value["total"]:
|
|
|
|
chrono.score = sum_value["total"]
|
|
|
|
if chrono.score_type == 0:
|
|
|
|
chrono.tof = Chrono.compute_tof(sum_value["total"])
|
|
|
|
chrono.save()
|
|
|
|
|
|
|
|
mean_value = chrono.details.all().aggregate(mean=Avg("value"))["mean"]
|
|
|
|
tmp_min_value = chrono.details.all().aggregate(min=Min("value"))["min"]
|
|
|
|
tmp_max_value = chrono.details.all().aggregate(max=Max("value"))["max"]
|
|
|
|
chart_min_value = mean_value - (tmp_min_value / 20)
|
|
|
|
chart_max_value = mean_value - (tmp_max_value / 20)
|
|
|
|
|
|
|
|
context = {
|
|
|
|
"chrono": chrono,
|
|
|
|
"mean_value": mean_value,
|
|
|
|
"chart_min_value": chart_min_value,
|
|
|
|
"chart_max_value": chart_max_value,
|
|
|
|
}
|
|
|
|
return render(request, "chronos/details.html", context)
|
|
|
|
|
|
|
|
|
|
|
|
@login_required
|
|
|
|
@require_http_methods(["GET"])
|
|
|
|
def average_jump_chrono_details_for_gymnast(
|
|
|
|
request, gymnast_id, routine_type=1, season=None, week_number=1
|
|
|
|
):
|
|
|
|
"""Récupère tout les chronos entre deux date pour un gymnaste et un type de série
|
|
|
|
|
|
|
|
Args:
|
|
|
|
gymnast_id (int) Identifiant d'un gymnaste
|
|
|
|
routine_type (int) Type de série (cf. jarvis/followup/models.py > ROUTINE_CHOICE)
|
|
|
|
season (string) Saison sous forme "xxxx-xxxy"
|
|
|
|
week_number (int) numéro de semaine (1, …, 52)
|
|
|
|
"""
|
|
|
|
if season is None:
|
|
|
|
today = pendulum.now().date()
|
|
|
|
season, week_number = from_date_to_week_number(today)
|
|
|
|
else:
|
|
|
|
season = Season(season).label
|
|
|
|
week_number = min(week_number, 52)
|
|
|
|
week_number = max(week_number, 1)
|
|
|
|
|
|
|
|
return average_jump_chrono_details_for_season_and_week(
|
|
|
|
request,
|
|
|
|
gymnast_id,
|
|
|
|
routine_type,
|
|
|
|
season,
|
|
|
|
week_number,
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@require_http_methods(["POST"])
|
|
|
|
def remove_jump_chrono_value(request):
|
|
|
|
"""
|
|
|
|
Recoit trois informations permettant de supprimer le chrono d'un saut à un chrono.
|
|
|
|
"""
|
|
|
|
chrono_id = request.POST.get("chrono_id", None)
|
|
|
|
order = request.POST.get("order", None)
|
|
|
|
chrono = get_object_or_404(Chrono, pk=chrono_id)
|
|
|
|
try:
|
|
|
|
ChronoDetails.objects.filter(chrono=chrono, order=order).delete()
|
|
|
|
except Exception:
|
|
|
|
return HttpResponse(409)
|
|
|
|
|
|
|
|
return HttpResponse(200)
|
|
|
|
|
|
|
|
|
|
|
|
@require_http_methods(["POST"])
|
|
|
|
def add_jump_chrono_value(request):
|
|
|
|
"""
|
|
|
|
Recoit trois informations permettant d'ajouter le chrono d'un saut à un chrono.
|
|
|
|
"""
|
|
|
|
chrono_id = request.POST.get("chrono_id", None)
|
|
|
|
order = request.POST.get("order", None)
|
|
|
|
value = request.POST.get("value", None)
|
|
|
|
|
|
|
|
chrono = get_object_or_404(Chrono, pk=chrono_id)
|
|
|
|
row, created = ChronoDetails.objects.get_or_create(
|
|
|
|
chrono=chrono, order=order, value=value
|
|
|
|
)
|
|
|
|
|
|
|
|
if created:
|
|
|
|
return HttpResponse(200, (row, created)) # devrait être un 201
|
|
|
|
|
|
|
|
return HttpResponse(400, (row, created))
|
|
|
|
|
|
|
|
|
|
|
|
@login_required
|
|
|
|
@require_http_methods(["GET"])
|
|
|
|
def jump_chrono_values_create_or_update(request, chrono_id):
|
|
|
|
"""
|
|
|
|
Ajoute des scores de saut à un chrono.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
chrono_id (int) identifiant chrono
|
|
|
|
"""
|
|
|
|
|
|
|
|
chrono = get_object_or_404(Chrono, pk=chrono_id)
|
|
|
|
jump_list = chrono.details.all()
|
|
|
|
number_of_jump = jump_list.count()
|
|
|
|
context = {
|
|
|
|
"chrono": chrono,
|
|
|
|
"jump_list": jump_list,
|
|
|
|
"number_of_jump": number_of_jump,
|
|
|
|
"score_type": chrono.score_type,
|
|
|
|
}
|
|
|
|
return render(request, "chronos/add_details.html", context)
|
|
|
|
|
|
|
|
|
|
|
|
@login_required
|
|
|
|
@require_http_methods(["GET"])
|
|
|
|
def average_jump_chrono_details_between_two_date(
|
|
|
|
request, gymnast_id, routine_type=1, date_begin=None, date_end=None
|
|
|
|
):
|
|
|
|
"""Récupère tout les chronos entre deux date pour un gymnaste et un type de série
|
|
|
|
|
|
|
|
Args:
|
|
|
|
gymnast_id (int) Identifiant d'un gymnaste
|
|
|
|
routine_type (int) type de série (cf. jarvis/followup/models.py > ROUTINE_CHOICE)
|
|
|
|
date_begin (date) date de début
|
|
|
|
date_end (date) date de fin
|
|
|
|
|
|
|
|
QTF : le cast en date devrait être dans un try mais comment gérer correctement l'erreur - si
|
|
|
|
erreur il y a ?
|
|
|
|
"""
|
|
|
|
|
|
|
|
if date_end:
|
|
|
|
try:
|
|
|
|
date_end = datetime.strptime(date_end, "%Y-%m-%d").date()
|
|
|
|
except (ValueError, TypeError):
|
|
|
|
date_end = pendulum.now().date()
|
|
|
|
else:
|
|
|
|
date_end = pendulum.now().date()
|
|
|
|
|
|
|
|
if date_begin:
|
|
|
|
try:
|
|
|
|
date_begin = datetime.strptime(date_begin, "%Y-%m-%d").date()
|
|
|
|
except (ValueError, TypeError):
|
|
|
|
date_begin = datetime(date_end.year, 9, 1)
|
|
|
|
else:
|
|
|
|
date_begin = datetime(date_end.year, 9, 1)
|
|
|
|
|
|
|
|
gymnast = get_object_or_404(Gymnast, pk=gymnast_id)
|
|
|
|
|
|
|
|
stat_values = (
|
|
|
|
ChronoDetails.objects.filter(
|
|
|
|
chrono__gymnast=gymnast_id,
|
|
|
|
chrono__chrono_type=routine_type,
|
|
|
|
chrono__date__gte=date_begin,
|
|
|
|
chrono__date__lte=date_end,
|
|
|
|
)
|
|
|
|
.values("order")
|
|
|
|
.annotate(
|
|
|
|
avg_score=Avg("value"), max_score=Max("value"), min_score=Min("value")
|
|
|
|
)
|
|
|
|
.order_by("order")
|
|
|
|
)
|
|
|
|
|
|
|
|
chrono_list = Chrono.objects.filter(
|
|
|
|
gymnast=gymnast_id,
|
|
|
|
date__gte=date_begin,
|
|
|
|
date__lte=date_end,
|
|
|
|
chrono_type=routine_type,
|
|
|
|
)
|
|
|
|
|
|
|
|
context = {
|
|
|
|
"gymnast": gymnast,
|
|
|
|
"date_begin": date_begin,
|
|
|
|
"date_end": date_end,
|
|
|
|
"chrono_list": chrono_list,
|
|
|
|
"stat_values": stat_values,
|
|
|
|
}
|
|
|
|
return render(request, "chronos/list_details.html", context)
|
|
|
|
|
|
|
|
|
|
|
|
@require_http_methods(["GET"])
|
|
|
|
def get_chrono_detail_distinct_season(request, gymnast_id):
|
|
|
|
"""Récupère toutes les saisons pour lesquelles le gymnaste a des chronos détaillés.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
gymnast_id (int) Identifiant d'un gymnaste
|
|
|
|
"""
|
|
|
|
gymnast = get_object_or_404(Gymnast, pk=gymnast_id)
|
|
|
|
season_list = list(
|
|
|
|
gymnast.chronos.values_list("season", flat=True)
|
|
|
|
.distinct("season")
|
|
|
|
.order_by("season")
|
|
|
|
)
|
|
|
|
return JsonResponse(season_list, safe=False)
|
|
|
|
|
|
|
|
|
|
|
|
@require_http_methods(["GET"])
|
|
|
|
def get_chrono_detail_distinct_weeknumber_for_season(request, gymnast_id, season):
|
|
|
|
"""Récupère toutes les week_number pour lesquelles le gymnaste a des chronos détaillés au cours
|
|
|
|
d'une saison.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
gymnast_id (int) Identifiant d'un gymnaste
|
|
|
|
season (string) Season
|
|
|
|
"""
|
|
|
|
gymnast = get_object_or_404(Gymnast, pk=gymnast_id)
|
|
|
|
weeknumber_list = list(
|
|
|
|
gymnast.chronos.values_list("week_number", flat=True)
|
|
|
|
.filter(season=season)
|
|
|
|
.distinct("week_number")
|
|
|
|
.order_by("week_number")
|
|
|
|
)
|
|
|
|
return JsonResponse(weeknumber_list, safe=False)
|
|
|
|
|
|
|
|
|
|
|
|
@require_http_methods(["GET"])
|
|
|
|
def get_average_jump_chrono_details_for_season_and_week(
|
|
|
|
request, gymnast_id, routine_type, season, week_number
|
|
|
|
):
|
|
|
|
"""Récupère tout les chronos moyen par saut pour une saison & semaine d'un gymnaste
|
|
|
|
|
|
|
|
Args:
|
|
|
|
gymnast_id (int) Identifiant d'un gymnaste
|
|
|
|
routine_type (int) Type de série (cf. jarvis/followup/models.py > ROUTINE_CHOICE)
|
|
|
|
season (string) Season
|
|
|
|
week_number (int) Numero de la semaine
|
|
|
|
"""
|
|
|
|
|
|
|
|
stat_values = list(
|
|
|
|
ChronoDetails.objects.filter(
|
|
|
|
chrono__gymnast=gymnast_id,
|
|
|
|
chrono__chrono_type=routine_type,
|
|
|
|
chrono__season=season,
|
|
|
|
chrono__week_number=week_number,
|
|
|
|
)
|
|
|
|
.values("order")
|
|
|
|
.annotate(avg_score=Avg("value"))
|
|
|
|
.order_by("order")
|
|
|
|
)
|
|
|
|
return JsonResponse(stat_values, safe=False)
|
|
|
|
|
|
|
|
|
|
|
|
@login_required
|
|
|
|
@require_http_methods(["GET"])
|
|
|
|
def average_jump_chrono_details_for_season_and_week(
|
|
|
|
request, gymnast_id, routine_type, season, week_number
|
|
|
|
):
|
|
|
|
"""Récupère tout les chronos entre deux date pour un gymnaste et un type de série
|
|
|
|
|
|
|
|
Args:
|
|
|
|
gymnast_id (int) Identifiant d'un gymnaste
|
|
|
|
routine_type (int) Type de série (cf. jarvis/followup/models.py > ROUTINE_CHOICE)
|
|
|
|
season (string) Season
|
|
|
|
week_number (int) Numero de la semaine
|
|
|
|
"""
|
|
|
|
|
|
|
|
gymnast = get_object_or_404(Gymnast, pk=gymnast_id)
|
|
|
|
|
|
|
|
stat_values = (
|
|
|
|
ChronoDetails.objects.filter(
|
|
|
|
chrono__gymnast=gymnast_id,
|
|
|
|
chrono__chrono_type=routine_type,
|
|
|
|
chrono__season=season,
|
|
|
|
chrono__week_number=week_number,
|
|
|
|
)
|
|
|
|
.values("order")
|
|
|
|
.annotate(
|
|
|
|
avg_score=Avg("value"), max_score=Max("value"), min_score=Min("value")
|
|
|
|
)
|
|
|
|
.order_by("order")
|
|
|
|
)
|
|
|
|
# print(stat_values)
|
|
|
|
|
|
|
|
distinct_season_list = (
|
|
|
|
gymnast.chronos.values_list("season", flat=True)
|
|
|
|
.distinct("season")
|
|
|
|
.order_by("season")
|
|
|
|
)
|
|
|
|
distinct_week_number_list = (
|
|
|
|
gymnast.chronos.values_list("week_number", flat=True)
|
|
|
|
.filter(season=season)
|
|
|
|
.distinct("week_number")
|
|
|
|
.order_by("week_number")
|
|
|
|
)
|
|
|
|
distinct_routine_type_list = (
|
|
|
|
gymnast.chronos.values_list("chrono_type", flat=True)
|
|
|
|
.distinct("chrono_type")
|
|
|
|
.order_by("chrono_type")
|
|
|
|
)
|
|
|
|
|
|
|
|
chrono_list = Chrono.objects.filter(
|
|
|
|
gymnast=gymnast_id,
|
|
|
|
season=season,
|
|
|
|
week_number=week_number,
|
|
|
|
chrono_type=routine_type,
|
|
|
|
)
|
|
|
|
# print(chrono_list)
|
|
|
|
|
|
|
|
context = {
|
|
|
|
"gymnast": gymnast,
|
|
|
|
"selected_season": season,
|
|
|
|
"selected_week_number": week_number,
|
|
|
|
"selected_routine_type": routine_type,
|
|
|
|
"chrono_list": chrono_list,
|
|
|
|
"stat_values": stat_values,
|
|
|
|
"distinct_season_list": distinct_season_list,
|
|
|
|
"distinct_week_number_list": distinct_week_number_list,
|
|
|
|
"distinct_routine_type_list": distinct_routine_type_list,
|
|
|
|
}
|
|
|
|
return render(request, "chronos/list_details.html", context)
|
|
|
|
|
|
|
|
|
|
|
|
@login_required
|
|
|
|
@require_http_methods(["GET"])
|
|
|
|
def chrono_listing(request, gymnast_id=None):
|
|
|
|
"""
|
|
|
|
Récupère les chronos des gymnastes autorisé(e)s.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
gymnast_id (int) identifiant d'un gymnaste
|
|
|
|
"""
|
|
|
|
|
|
|
|
gymnast = None
|
|
|
|
if gymnast_id and (
|
|
|
|
request.user.is_superuser
|
|
|
|
or (
|
|
|
|
request.session.has_key("available_gymnast")
|
|
|
|
and gymnast_id in request.session["available_gymnast"]
|
|
|
|
)
|
|
|
|
):
|
|
|
|
gymnast = Gymnast.objects.get(pk=gymnast_id)
|
2024-04-11 12:18:14 +02:00
|
|
|
chrono_list = Chrono.objects.filter(gymnast=gymnast_id).order_by("date")
|
|
|
|
base_queryset = chrono_list.values("date").annotate(score_avg=Avg("tof"))
|
|
|
|
|
|
|
|
context = {
|
|
|
|
"chrono_10c": base_queryset.filter(chrono_type=0),
|
|
|
|
"chrono_r1": base_queryset.filter(chrono_type=1),
|
|
|
|
"chrono_r2": base_queryset.filter(chrono_type=2),
|
|
|
|
"chrono_rf": base_queryset.filter(chrono_type=3),
|
|
|
|
}
|
|
|
|
|
2024-04-17 14:41:05 +02:00
|
|
|
personnal_best = Chrono.objects.filter(gymnast=gymnast_id).order_by("-tof").first()
|
2024-04-18 17:10:10 +02:00
|
|
|
context["personnal_best"] = personnal_best
|
2024-04-17 14:41:05 +02:00
|
|
|
|
2024-02-16 16:54:10 +01:00
|
|
|
else:
|
2024-04-13 07:40:29 +02:00
|
|
|
context = {}
|
2024-02-16 16:54:10 +01:00
|
|
|
if request.user.is_superuser:
|
|
|
|
chrono_list = Chrono.objects.all()
|
|
|
|
else:
|
|
|
|
chrono_list = Chrono.objects.filter(
|
|
|
|
gymnast__in=request.session["available_gymnast"]
|
|
|
|
)
|
|
|
|
|
2024-04-11 12:18:14 +02:00
|
|
|
context["chrono_list"] = chrono_list
|
|
|
|
context["gymnast"] = gymnast
|
2024-02-16 16:54:10 +01:00
|
|
|
return render(request, "chronos/list.html", context)
|
|
|
|
|
|
|
|
|
|
|
|
@login_required
|
|
|
|
@require_http_methods(["GET", "POST"])
|
|
|
|
def chrono_create_or_update(request, chrono_id=None, gymnast_id=None):
|
|
|
|
"""Création ou modification d'un chrono
|
|
|
|
|
|
|
|
Args:
|
|
|
|
chrono_id (int) identifiant d'un chrono
|
|
|
|
gymnast_id (int) identifiant d'un gymnaste
|
|
|
|
"""
|
|
|
|
|
|
|
|
if chrono_id:
|
|
|
|
chrono = get_object_or_404(Chrono, pk=chrono_id)
|
|
|
|
if not request.user.is_superuser and (
|
|
|
|
request.session.has_key("available_gymnast")
|
|
|
|
and chrono.gymnast.id not in request.session["available_gymnast"]
|
|
|
|
):
|
|
|
|
return chrono_listing(request)
|
|
|
|
|
|
|
|
data = {
|
|
|
|
"gymnast": chrono.gymnast.id,
|
|
|
|
"gymnast_related": str(chrono.gymnast),
|
|
|
|
}
|
|
|
|
else:
|
|
|
|
chrono = None
|
|
|
|
data = None
|
|
|
|
if gymnast_id is not None:
|
|
|
|
gymnast = get_object_or_404(Gymnast, pk=gymnast_id)
|
|
|
|
data = {"gymnast": gymnast_id, "gymnast_related": gymnast}
|
|
|
|
|
|
|
|
if request.method == "POST":
|
|
|
|
form = ChronoForm(request.POST, instance=chrono)
|
|
|
|
|
|
|
|
if form.is_valid():
|
|
|
|
new_chrono = form.save(commit=False)
|
|
|
|
|
|
|
|
if new_chrono.score_type == 1:
|
|
|
|
new_chrono.tof = new_chrono.score
|
|
|
|
else:
|
|
|
|
new_chrono.tof = Chrono.compute_tof(new_chrono.score)
|
|
|
|
|
|
|
|
new_chrono.save()
|
|
|
|
|
|
|
|
# notification
|
|
|
|
receivers = []
|
|
|
|
functionality = ContentType.objects.get(model="chrono")
|
|
|
|
for notification in new_chrono.gymnast.notifications.filter(
|
|
|
|
functionality=functionality
|
|
|
|
):
|
|
|
|
receivers.append(notification.user.email)
|
|
|
|
|
|
|
|
title = f"{new_chrono.gymnast} : Nouveau chrono"
|
|
|
|
body = f"""<p>Bonjour,</p><p>Nouveau chrono pour {new_chrono.gymnast} : {SCORE_TYPE_CHOICE[new_chrono.score_type][1]} {CHRONO_TYPE_CHOICE[new_chrono.chrono_type][1]} - {new_chrono.score}.</p>"""
|
|
|
|
|
|
|
|
Email.objects.create(
|
|
|
|
receivers=receivers,
|
|
|
|
title=title,
|
|
|
|
body=body,
|
|
|
|
)
|
|
|
|
|
|
|
|
send_mail(
|
|
|
|
title,
|
|
|
|
f"{new_chrono.gymnast} a enregistrer un nouveau chrono ({date})",
|
|
|
|
settings.EMAIL_HOST_USER,
|
|
|
|
receivers,
|
|
|
|
fail_silently=False,
|
|
|
|
html_message=body + MAIL_FOOTER,
|
|
|
|
)
|
|
|
|
|
|
|
|
return HttpResponseRedirect(
|
2024-04-11 16:29:15 +02:00
|
|
|
reverse("chrono_list_for_gymnast", args=(new_chrono.gymnast.id,))
|
2024-02-16 16:54:10 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
return render(request, "chronos/create.html", {"form": form})
|
|
|
|
|
|
|
|
form = ChronoForm(instance=chrono, initial=data)
|
|
|
|
context = {"form": form, "chrono_id": chrono_id}
|
|
|
|
return render(request, "chronos/create.html", context)
|