Repository: ephios-dev/ephios

Issue:
Filter working hours by type
As a planner, I want to filter the working hours by event type or manual working hours.


Discussion:

Resolving patch:
diff --git a/ephios/core/views/workinghour.py b/ephios/core/views/workinghour.py
--- a/ephios/core/views/workinghour.py
+++ b/ephios/core/views/workinghour.py
@@ -15,10 +15,11 @@
 from django.utils.functional import cached_property
 from django.utils.translation import gettext as _
 from django.views.generic import DeleteView, DetailView, FormView, TemplateView, UpdateView
+from django_select2.forms import Select2Widget
 from guardian.shortcuts import get_objects_for_user
 
 from ephios.core.forms.users import WorkingHourRequestForm
-from ephios.core.models import LocalParticipation, UserProfile, WorkingHours
+from ephios.core.models import EventType, LocalParticipation, UserProfile, WorkingHours
 from ephios.extra.mixins import CustomPermissionRequiredMixin
 from ephios.extra.widgets import CustomDateInput
 
@@ -42,23 +43,43 @@ def _get_target_user(self):
         return get_object_or_404(UserProfile, pk=self.kwargs["pk"])
 
 
-class DateFilterForm(forms.Form):
+class WorkingHourFilterForm(forms.Form):
     start = forms.DateField(required=False, label=_("From"), widget=CustomDateInput)
     end = forms.DateField(required=False, label=_("To"), widget=CustomDateInput)
+    type = forms.ModelChoiceField(
+        label=_("Event type"),
+        queryset=EventType.objects.all(),
+        required=False,
+        widget=Select2Widget(
+            attrs={
+                "data-placeholder": _("Event type"),
+                "classes": "w-auto",
+            }
+        ),
+    )
 
 
 class WorkingHourOverview(CustomPermissionRequiredMixin, TemplateView):
     template_name = "core/workinghours_list.html"
     permission_required = "core.view_userprofile"
 
-    def _get_working_hours_stats(self, start: Optional[date], end: Optional[date]):
-        participations = (
-            LocalParticipation.objects.filter(
-                state=LocalParticipation.States.CONFIRMED,
-                start_time__date__gte=start,
-                end_time__date__lte=end,
+    def _get_working_hours_stats(self, start: date, end: date, eventtype: Optional[EventType]):
+        participations = LocalParticipation.objects.filter(
+            state=LocalParticipation.States.CONFIRMED,
+            start_time__date__gte=start,
+            end_time__date__lte=end,
+        )
+        workinghours = {}
+        if eventtype is not None:
+            participations = participations.filter(shift__event__type=eventtype)
+        else:
+            workinghours = (
+                WorkingHours.objects.filter(date__gte=start, date__lte=end)
+                .annotate(hour_sum=Sum("hours"))
+                .values_list("user__pk", "user__display_name", "hour_sum")
             )
-            .annotate(
+        participations = (
+            participations.annotate(
                 duration=ExpressionWrapper(
                     (F("end_time") - F("start_time")),
                     output_field=DurationField(),
@@ -67,11 +88,7 @@ def _get_working_hours_stats(self, start: Optional[date], end: Optional[date]):
             .annotate(hour_sum=Sum("duration"))
             .values_list("user__pk", "user__display_name", "hour_sum")
         )
-        workinghours = (
-            WorkingHours.objects.filter(date__gte=start, date__lte=end)
-            .annotate(hour_sum=Sum("hours"))
-            .values_list("user__pk", "user__display_name", "hour_sum")
-        )
+
         result = {}
         c = Counter()
         for user_pk, display_name, hours in chain(participations, workinghours):
@@ -89,7 +106,7 @@ def _get_working_hours_stats(self, start: Optional[date], end: Optional[date]):
         return sorted(result.values(), key=lambda x: x["hours"], reverse=True)
 
     def get_context_data(self, **kwargs):
-        filter_form = DateFilterForm(
+        filter_form = WorkingHourFilterForm(
             self.request.GET
             or {
                 # intitial data for initial page laod
@@ -103,6 +120,7 @@ def get_context_data(self, **kwargs):
         kwargs["users"] = self._get_working_hours_stats(
             start=filter_form.cleaned_data.get("start") or date.min,  # start/end are not required
             end=filter_form.cleaned_data.get("end") or date.max,
+            eventtype=filter_form.cleaned_data.get("type"),
         )
         kwargs["can_grant_for"] = set(
             get_objects_for_user(self.request.user, "decide_workinghours_for_group", klass=Group)


Test patch:
diff --git a/tests/core/test_workinghours.py b/tests/core/test_workinghours.py
--- a/tests/core/test_workinghours.py
+++ b/tests/core/test_workinghours.py
@@ -4,7 +4,7 @@
 from django.template.defaultfilters import floatformat
 from django.urls import reverse
 
-from ephios.core.models import WorkingHours
+from ephios.core.models import LocalParticipation, WorkingHours
 
 
 class TestWorkingHours:
@@ -93,3 +93,27 @@ def test_workinghours_add_nologin(self, django_app, volunteer):
             reverse("core:workinghours_add", kwargs={"pk": volunteer.pk}), status=302
         )
         assert "/accounts/login" in response.url
+
+    def test_workinghours_filter(
+        self,
+        django_app,
+        volunteer,
+        manager,
+        event,
+        service_event_type,
+        training_event_type,
+        workinghours,
+    ):
+        response = django_app.get(reverse("core:workinghours_list"), user=manager)
+        assert response.html.find(string=floatformat(volunteer.get_workhour_items()[0], arg=2))
+        participation = LocalParticipation.objects.create(
+            shift=event.shifts.first(), user=volunteer, state=LocalParticipation.States.CONFIRMED
+        )
+        response = django_app.get(
+            f'{reverse("core:workinghours_list")}?type={training_event_type.pk}', user=manager
+        )
+        assert response.html.find(string="No entries")
+        response = django_app.get(
+            f'{reverse("core:workinghours_list")}?type={service_event_type.pk}', user=manager
+        )
+        assert response.html.find(string=floatformat(participation.hours_value, arg=2))