Jarvis/jarvis/followup/views_chrono.py

556 lines
18 KiB
Python

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
"""
# Using prefetch_related to optimize database access for related details
chrono = get_object_or_404(Chrono.objects.prefetch_related('details'), pk=chrono_id)
# Access control for non-superusers
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)
# Aggregate values from details
details = chrono.details.all()
sum_value = details.aggregate(total=Sum("value"))['total']
mean_value = details.aggregate(mean=Avg("value"))['mean']
min_value = details.aggregate(min=Min("value"))['min']
max_value = details.aggregate(max=Max("value"))['max']
# Update chrono score if different
if chrono.score != sum_value:
chrono.score = sum_value
if chrono.score_type == 0:
chrono.tof = Chrono.compute_tof(sum_value)
chrono.save()
# Calculate chart values
chart_min_value = mean_value - (min_value / 20)
chart_max_value = mean_value + (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=None
# ):
# """Retrieves all the chronos for a gymnast and a type of routine within a specific season and week.
# 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 or week_number is None:
# today = pendulum.now().date()
# season, week_number = from_date_to_week_number(today)
# else:
# # Ensure week_number is within the valid range
# week_number = max(1, min(week_number, 52))
# # Ensure season is properly formatted or converted
# season = Season(season).label if isinstance(season, str) else season
# return average_jump_chrono_details_for_season_and_week(
# request,
# gymnast_id,
# routine_type,
# season,
# week_number,
# )
@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):
"""
Receives information to remove a jump time from a Chrono record.
"""
chrono_id = request.POST.get("chrono_id")
order = request.POST.get("order")
print("delete jump chrono")
# Validate required parameters
if not chrono_id or not order:
return HttpResponse(400, "Missing required parameters.")
# Retrieve the Chrono object or return 404 if not found
chrono = get_object_or_404(Chrono, pk=chrono_id)
# Attempt to delete the specified ChronoDetails record
deleted, _ = ChronoDetails.objects.filter(chrono=chrono, order=order).delete()
# Check if any records were deleted
if deleted:
return HttpResponse(200) # OK status, deletion successful
return HttpResponse(404, "Chrono detail not found.") # Not found status if no records were deleted
@require_http_methods(["POST"])
def add_jump_chrono_value(request):
"""
Receives three pieces of information to add the time of a jump to a <Chrono> record.
"""
chrono_id = request.POST.get("chrono_id")
order = request.POST.get("order")
value = request.POST.get("value")
# Validate required parameters
if not chrono_id or not order or value is None:
return HttpResponse(400, "Missing required parameters.")
# Retrieve the Chrono object or return 404 if not found
chrono = get_object_or_404(Chrono, pk=chrono_id)
# Attempt to create a new ChronoDetails record
row, created = ChronoDetails.objects.get_or_create(
# chrono=chrono, order=order, defaults={'value': value}
chrono=chrono, order=order, value=value
)
# Check if the record was created or just retrieved
if created:
return HttpResponse(201, f"New chrono detail added: {row}") # 201 Created
else:
# If the record was not created, it means it already exists with the same order and chrono
return HttpResponse(409, f"Chrono detail already exists: {row}") # 409 Conflict
@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)
context = {
"chrono": chrono,
"jump_list": chrono.details.all(),
"number_of_jump": jump_list.count(),
"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):
"""Retrieves all distinct seasons for which the gymnast has detailed chronos.
Args:
gymnast_id (int) Identifiant d'un gymnaste
"""
# Ensure the gymnast exists
get_object_or_404(Gymnast, pk=gymnast_id)
# Directly query the Chrono model for distinct seasons
season_list = list(
Chrono.objects.filter(gymnast_id=gymnast_id)
.values_list("season", flat=True)
.distinct()
.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):
"""Retrieves all distinct week numbers for which the gymnast has detailed chronos during a specific season.
Args:
gymnast_id (int) Identifiant d'un gymnaste
season (string) Season
"""
# Ensure the gymnast exists
get_object_or_404(Gymnast, pk=gymnast_id)
# Directly query the Chrono model for distinct week numbers in a specific season
weeknumber_list = list(
Chrono.objects.filter(gymnast_id=gymnast_id, season=season)
.values_list("week_number", flat=True)
.distinct()
.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
):
"""Retrieves average jump chronos per jump for a season and week for a gymnast.
Args:
gymnast_id (int) Gymnast ID
routine_type (int) Type of routine (cf. jarvis/followup/models.py > ROUTINE_CHOICE)
season (string) Season
week_number (int) Number of week
"""
# Optimize query by directly annotating and ordering in a single query
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")).order_by("order")
# Convert QuerySet to list for JSON serialization
stat_values_list = list(stat_values)
return JsonResponse(stat_values_list, 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.user.gymnast.id == gymnast_id
or (
request.session.has_key("available_gymnast")
and gymnast_id in request.session["available_gymnast"]
)
):
gymnast = Gymnast.objects.get(pk=gymnast_id)
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),
}
personnal_best_10 = Chrono.objects.filter(gymnast=gymnast_id, chrono_type=0).order_by("-tof").first()
personnal_best_q1r1 = Chrono.objects.filter(gymnast=gymnast_id, chrono_type=1).order_by("-tof").first()
personnal_best_q1r2 = Chrono.objects.filter(gymnast=gymnast_id, chrono_type=2).order_by("-tof").first()
context["personnal_best_10"] = personnal_best_10
context["personnal_best_q1r1"] = personnal_best_q1r1
context["personnal_best_q1r2"] = personnal_best_q1r2
else:
context = {}
if request.user.is_superuser:
chrono_list = Chrono.objects.all()
else:
chrono_list = Chrono.objects.filter(
gymnast__in=request.session["available_gymnast"]
)
context["chrono_list"] = chrono_list
context["gymnast"] = gymnast
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(
reverse("chrono_list_for_gymnast", args=(new_chrono.gymnast.id,))
)
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)