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 """ chrono = get_object_or_404(Chrono.objects.prefetch_related('details'), 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) 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'] if chrono.score != sum_value: chrono.score = sum_value if chrono.score_type == 0: chrono.tof = Chrono.compute_tof(sum_value) chrono.save() 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, ) @login_required @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") if not chrono_id or not order: return HttpResponse(status=400) chrono = get_object_or_404(Chrono, pk=chrono_id) deleted, _ = ChronoDetails.objects.filter(chrono=chrono, order=order).delete() if deleted: return HttpResponse(status=200) return HttpResponse(status=404) @login_required @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") if not chrono_id or not order or value is None: return HttpResponse(status=400) chrono = get_object_or_404(Chrono, pk=chrono_id) row, created = ChronoDetails.objects.get_or_create( chrono=chrono, order=order, defaults={'value': value} ) if created: return HttpResponse(status=201) return HttpResponse(status=409) @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(request, gymnast_id, season=None): # """Retrieves all distinct seasons for which the gymnast has detailed chronos. # Args: # gymnast_id (int) Identifiant d'un gymnaste # """ # get_object_or_404(Gymnast, pk=gymnast_id) # if season: # result_list = list( # Chrono.objects.filter(gymnast_id=gymnast_id, season=season) # .values_list("week_number", flat=True) # .distinct() # .order_by("week_number") # ) # else: # result_list = list( # Chrono.objects.filter(gymnast_id=gymnast_id) # .values_list("season", flat=True) # .distinct() # .order_by("season") # ) # return JsonResponse(result_list, safe=False) @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 """ get_object_or_404(Gymnast, pk=gymnast_id) 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 """ get_object_or_404(Gymnast, pk=gymnast_id) 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 """ 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") 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"""

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)