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): """ Receives three pieces of information to add the time of a jump to a 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) 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.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"""

Bonjour,

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}.

""" 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)