Skip to content

Commit

Permalink
Added a view to check shift cancelling rate
Browse files Browse the repository at this point in the history
  • Loading branch information
Theophile-Madet committed Sep 27, 2024
1 parent 6000e4e commit 9c049c7
Show file tree
Hide file tree
Showing 6 changed files with 272 additions and 0 deletions.
66 changes: 66 additions & 0 deletions tapir/shifts/services/shift_attendance_mode_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import datetime

from django.db.models import QuerySet, OuterRef, Value, Subquery, CharField
from django.db.models.functions import Coalesce
from django.utils import timezone

from tapir.shifts.models import ShiftUserData


class ShiftAttendanceModeService:
ANNOTATION_SHIFT_ATTENDANCE_MODE_AT_DATE = "attendance_mode_at_date"
ANNOTATION_SHIFT_ATTENDANCE_MODE_DATE_CHECK = "attendance_mode_date_check"

@classmethod
def get_attendance_mode(
cls, shift_user_data: ShiftUserData, at_date: datetime.date = None
):
if at_date is None:
at_date = timezone.now().date()

if not hasattr(shift_user_data, cls.ANNOTATION_SHIFT_ATTENDANCE_MODE_AT_DATE):
shift_user_data = (
cls.annotate_share_owner_queryset_with_investing_status_at_date(
ShiftUserData.objects.filter(id=shift_user_data.id), at_date
).first()
)

annotated_date = getattr(
shift_user_data, cls.ANNOTATION_SHIFT_ATTENDANCE_MODE_DATE_CHECK
)
if annotated_date != at_date:
raise ValueError(
f"Trying to get the investing status at date {at_date}, but the queryset has been "
f"annotated relative to {annotated_date}"
)
return getattr(shift_user_data, cls.ANNOTATION_SHIFT_ATTENDANCE_MODE_AT_DATE)

@classmethod
def annotate_shift_user_data_queryset_with_attendance_mode_at_date(
cls, queryset: QuerySet, at_date: datetime.date = None
):
if at_date is None:
at_date = timezone.now().date()

from tapir.shifts.models import UpdateShiftUserDataLogEntry

queryset = queryset.annotate(
attendance_mode_from_log_entry=Subquery(
UpdateShiftUserDataLogEntry.objects.filter(
user_id=OuterRef("user_id"),
created_date__gte=at_date,
)
.order_by("created_date")
.values("old_values__attendance_mode")[:1],
output_field=CharField(),
)
)

annotate_kwargs = {
cls.ANNOTATION_SHIFT_ATTENDANCE_MODE_AT_DATE: Coalesce(
"attendance_mode_from_log_entry",
"attendance_mode",
),
cls.ANNOTATION_SHIFT_ATTENDANCE_MODE_DATE_CHECK: Value(at_date),
}
return queryset.annotate(**annotate_kwargs)
37 changes: 37 additions & 0 deletions tapir/statistics/templates/statistics/shift_cancelling_rate.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{% extends "core/base.html" %}
{% load core %}
{% load django_bootstrap5 %}
{% load static %}
{% load i18n %}
{% load utils %}
{% block title %}
{% translate 'Main statistics' %}
{% endblock title %}
{% block head %}
<script src="{% static 'statistics/chart_4.4.0.js' %}"></script>
<script src="{% static 'statistics/tapir_charts.js' %}" defer></script>
{% endblock head %}
{% block content %}
<div class="row">
<div class="col-xl-6">
<div class="card mb-2">
<h5 class="card-header">{% translate "Shift cancellation rate" %}</h5>
<div class="card-body">
<p>
{% blocktranslate %}
# TODO explanations
{% endblocktranslate %}
</p>
<p>
<span class="{% tapir_button_link %}"
onclick="chartManager.show_stats_chart( this, '{% url "statistics:shift_cancelling_rate_json" %}', 'shift_cancelling_rate_canvas', )">
<span class="material-icons">leaderboard</span>
<span class="button-text">{% translate "Show graph: " %}{% translate "Shift cancellation rate" %}</span>
</span>
<canvas id="shift_cancelling_rate_canvas" style="display: none;"></canvas>
</p>
</div>
</div>
</div>
</div>
{% endblock content %}
10 changes: 10 additions & 0 deletions tapir/statistics/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,14 @@
views.BasketSumEvolutionJsonView.as_view(),
name="basket_sum_evolution_json",
),
path(
"shift_cancelling_rate",
views.ShiftCancellingRateView.as_view(),
name="shift_cancelling_rate",
),
path(
"shift_cancelling_rate_json",
views.ShiftCancellingRateJsonView.as_view(),
name="shift_cancelling_rate_json",
),
]
2 changes: 2 additions & 0 deletions tapir/statistics/views/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .main_view import *
from .private_views import *
File renamed without changes.
157 changes: 157 additions & 0 deletions tapir/statistics/views/private_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import datetime

from chartjs.views import JSONView
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.db.models import OuterRef, Subquery, CharField
from django.utils import timezone
from django.views import generic

from tapir.settings import PERMISSION_SHIFTS_MANAGE
from tapir.shifts.models import (
Shift,
ShiftAttendance,
ShiftAttendanceMode,
ShiftUserData,
)
from tapir.shifts.services.shift_attendance_mode_service import (
ShiftAttendanceModeService,
)
from tapir.statistics.utils import (
build_line_chart_data,
FORMAT_TICKS_PERCENTAGE,
)
from tapir.utils.shortcuts import get_first_of_next_month


# The main statistic view is intended for members and is accessible for all.
# The views from this file are intended for deciders. They are not accessible for all to avoid confusion,
# as they may be less well presented or harder to interpret


class ShiftCancellingRateView(
LoginRequiredMixin, PermissionRequiredMixin, generic.TemplateView
):
permission_required = PERMISSION_SHIFTS_MANAGE
template_name = "statistics/shift_cancelling_rate.html"

def get_context_data(self, **kwargs):
context_data = super().get_context_data(**kwargs)

return context_data


class ShiftCancellingRateJsonView(
LoginRequiredMixin, PermissionRequiredMixin, JSONView
):
permission_required = PERMISSION_SHIFTS_MANAGE
SELECTIONS = [
"abcd_members",
"flying_members",
"frozen_members",
"abcd_shifts",
"flying_shifts",
]

def __init__(self, **kwargs):
super().__init__(**kwargs)
self.dates_from_first_shift_to_today = None

def get_context_data(self, **kwargs):
return build_line_chart_data(
x_axis_values=[
date for date in self.get_and_cache_dates_from_first_shift_to_today()
],
y_axis_values=self.get_data(),
data_labels=self.SELECTIONS,
y_axis_min=0,
y_axis_max=1,
format_ticks=FORMAT_TICKS_PERCENTAGE,
)

def get_and_cache_dates_from_first_shift_to_today(self):
if self.dates_from_first_shift_to_today is None:
self.dates_from_first_shift_to_today = (
self.get_dates_from_first_shift_to_today()
)
return self.dates_from_first_shift_to_today

@staticmethod
def get_dates_from_first_shift_to_today():
first_shift = Shift.objects.order_by("start_time").first()
if not first_shift:
return []

current_date = first_shift.start_time.date().replace(day=1)
end_date = timezone.now().date() + datetime.timedelta(days=1)
dates = []
while current_date < end_date:
dates.append(current_date)
current_date = get_first_of_next_month(current_date)

if len(dates) > 0 and dates[-1] != end_date:
dates.append(end_date)

return dates

def get_data(self):
return [
self.get_cancel_rate_for_selection(selection)
for selection in self.SELECTIONS
]

def get_cancel_rate_for_selection(self, selection: str):
return [
self.get_cancel_rate_for_selection_at_date(selection, at_date)
for at_date in self.get_and_cache_dates_from_first_shift_to_today()
]

@staticmethod
def filter_attendance_by_attendance_mode_of_member_at_date(
attendances, attendance_mode, at_date
):
shift_user_datas = ShiftAttendanceModeService.annotate_shift_user_data_queryset_with_attendance_mode_at_date(
ShiftUserData.objects.all(), at_date
)
attendances = attendances.annotate(
attendance_mode=Subquery(
shift_user_datas.filter(user_id=OuterRef("user_id")).values(
ShiftAttendanceModeService.ANNOTATION_SHIFT_ATTENDANCE_MODE_AT_DATE
),
output_field=CharField(),
)
)

return attendances.filter(attendance_mode=attendance_mode)

def get_cancel_rate_for_selection_at_date(self, selection, at_date):
attendances = ShiftAttendance.objects.exclude(
state=ShiftAttendance.State.PENDING
)

# Only pick one attendance per slot, choosing the most recently updated one
attendances = attendances.order_by("slot", "-last_state_update").distinct(
"slot"
)

if selection == "abcd_members":
attendances = self.filter_attendance_by_attendance_mode_of_member_at_date(
attendances, ShiftAttendanceMode.REGULAR, at_date
)
elif selection == "flying_members":
attendances = self.filter_attendance_by_attendance_mode_of_member_at_date(
attendances, ShiftAttendanceMode.FLYING, at_date
)
elif selection == "frozen_members":
attendances = self.filter_attendance_by_attendance_mode_of_member_at_date(
attendances, ShiftAttendanceMode.FROZEN, at_date
)
elif selection == "abcd_shifts":
return 0
elif selection == "flying_shifts":
return 0

nb_total_attendances = attendances.count()
nb_attended = attendances.filter(state=ShiftAttendance.State.DONE).count()
nb_not_attended = nb_total_attendances - nb_attended

return nb_not_attended / (nb_total_attendances or 1)

0 comments on commit 9c049c7

Please sign in to comment.