diff --git a/src/open_inwoner/cms/cases/forms.py b/src/open_inwoner/cms/cases/forms.py index 44d5491baa..f21ac5a228 100644 --- a/src/open_inwoner/cms/cases/forms.py +++ b/src/open_inwoner/cms/cases/forms.py @@ -79,26 +79,3 @@ class CaseContactForm(forms.Form): widget=forms.Textarea(attrs={"rows": "5"}), required=True, ) - - -class CaseFilterForm(forms.Form): - def __init__( - self, - status_freqs: dict[str, int], - status_initial: list[str] | None = None, - *args, - **kwargs, - ): - super().__init__(*args, **kwargs) - - self.fields["status"].choices = ( - (status, f"{status} ({frequency})") - for status, frequency in status_freqs.items() - ) - self.fields["status"].initial = status_initial or [] - - status = forms.MultipleChoiceField( - label=_("Filter by status"), - widget=forms.CheckboxSelectMultiple, - choices=dict(), - ) diff --git a/src/open_inwoner/cms/cases/views/cases.py b/src/open_inwoner/cms/cases/views/cases.py index 6a4f33e893..6321d21a2d 100644 --- a/src/open_inwoner/cms/cases/views/cases.py +++ b/src/open_inwoner/cms/cases/views/cases.py @@ -1,4 +1,5 @@ import concurrent.futures +import enum import logging from dataclasses import dataclass @@ -22,13 +23,15 @@ from open_inwoner.utils.mixins import PaginationMixin from open_inwoner.utils.views import CommonPageMixin -from ..forms import CaseFilterForm from .mixins import CaseAccessMixin, CaseLogMixin, OuterCaseAccessMixin logger = logging.getLogger(__name__) -SUBMISSION_STATUS_OPEN = _("Openstaande aanvraag") +class CaseFilterFormOption(enum.Enum): + OPEN_SUBMISSION = _("Openstaande formulieren") + OPEN_CASE = _("Lopende aanvragen") + CLOSED_CASE = _("Afgeronde aanvragen") @dataclass(frozen=True) @@ -86,16 +89,25 @@ def get_submissions(self): return subs - def get_case_status_frequencies(self): + @staticmethod + def get_case_filter_status(zaak: Zaak) -> CaseFilterFormOption: + if zaak.einddatum: + return CaseFilterFormOption.CLOSED_CASE + + return CaseFilterFormOption.OPEN_CASE + + def get_case_status_frequencies(self) -> dict[CaseFilterFormOption, int]: cases = self.get_cases() submissions = self.get_submissions() - case_statuses = [case.zaak.status_text for case in cases] + case_statuses = [self.get_case_filter_status(case.zaak) for case in cases] # add static text for open submissions - case_statuses += [SUBMISSION_STATUS_OPEN for submission in submissions] + case_statuses += [CaseFilterFormOption.OPEN_SUBMISSION for _ in submissions] - return {status: case_statuses.count(status) for status in case_statuses} + return { + status: case_statuses.count(status) for status in list(CaseFilterFormOption) + } class OuterCaseListView( @@ -144,38 +156,43 @@ def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) config = OpenZaakConfig.get_solo() case_service = CaseListService(self.request) - form = None + context["filter_form_enabled"] = config.zaken_filter_enabled - # update ctx with open submissions, cases, form for filtering + # update ctx with open submissions and cases (possibly fitered) open_submissions: list[UniformCase] = case_service.get_submissions() preprocessed_cases: list[UniformCase] = case_service.get_cases() - statuses = self.request.GET.getlist("status") - # GET.getlist returns [''] if no query params are passed - if config.zaken_filter_enabled and any(statuses): - form = CaseFilterForm( - status_freqs=case_service.get_case_status_frequencies(), - status_initial=statuses, - data={"status": statuses}, - ) - - # error message to user would be unhelpful; - # we pass silently over invalid input and send a ping to Sentry - if not form.is_valid(): - form.errors["status"] = [] - logger.error( - "Invalid data (%s) for case filtering by %s", - self.request.GET, - self.request.user, - ) - else: + if config.zaken_filter_enabled: + case_status_frequencies = case_service.get_case_status_frequencies() + # Separate frequency data from statusname + context["status_freqs"] = [ + (status.value, frequency) + for status, frequency in case_status_frequencies.items() + ] + + # Validate statuses are valid according to the options enum + statuses: list[CaseFilterFormOption] = [] + for status in self.request.GET.getlist("status"): + try: + statuses.append(CaseFilterFormOption(status)) + except ValueError: + logger.error( + "Invalid data (%s) for case filtering by %s", + self.request.GET, + self.request.user, + ) + + # Actually filter the submissions + if statuses: open_submissions = ( - open_submissions if SUBMISSION_STATUS_OPEN in statuses else [] + open_submissions + if CaseFilterFormOption.OPEN_SUBMISSION in statuses + else [] ) preprocessed_cases = [ case for case in preprocessed_cases - if case.zaak.status_text in statuses + if case_service.get_case_filter_status(case.zaak) in statuses ] paginator_dict = self.paginate_with_context( @@ -188,18 +205,6 @@ def get_context_data(self, **kwargs): self.log_access_cases(case_dicts) - if config.zaken_filter_enabled: - context["filter_form"] = form or CaseFilterForm( - status_freqs=case_service.get_case_status_frequencies(), - status_initial=statuses, - ) - - # Separate frequency data from statusname - context["status_freqs"] = [ - (status, frequency) - for status, frequency in case_service.get_case_status_frequencies().items() - ] - # other data context["hxget"] = reverse("cases:cases_content") context["title_text"] = config.title_text diff --git a/src/open_inwoner/openzaak/tests/test_cases.py b/src/open_inwoner/openzaak/tests/test_cases.py index 2dcc4f9f91..936fa28d67 100644 --- a/src/open_inwoner/openzaak/tests/test_cases.py +++ b/src/open_inwoner/openzaak/tests/test_cases.py @@ -3,24 +3,24 @@ import random import uuid from unittest.mock import patch +from urllib.parse import urlencode from django.conf import settings from django.contrib.auth.models import AnonymousUser -from django.test import TestCase, TransactionTestCase +from django.test import TransactionTestCase from django.test.utils import override_settings from django.urls import reverse_lazy -from django.utils.translation import gettext as _ import requests_mock from django_webtest import TransactionWebTest from furl import furl +from pyquery import PyQuery as PQ from timeline_logger.models import TimelineLog from zgw_consumers.api_models.constants import VertrouwelijkheidsAanduidingen from open_inwoner.accounts.choices import LoginTypeChoices from open_inwoner.accounts.tests.factories import UserFactory, eHerkenningUserFactory -from open_inwoner.cms.cases.forms import CaseFilterForm -from open_inwoner.cms.cases.views.cases import InnerCaseListView +from open_inwoner.cms.cases.views.cases import CaseFilterFormOption, InnerCaseListView from open_inwoner.openzaak.tests.shared import FORMS_ROOT from open_inwoner.utils.test import ( ClearCachesMixin, @@ -606,7 +606,34 @@ def test_list_cases(self, m): ) # case filter form is disabled by default - self.assertIsNone(response.context.get("filter_form")) + self.assertFalse(response.context.get("filter_form_enabled")) + + def test_filter_widget_is_controlled_by_zaken_filter_enabled(self, m): + self.client.force_login(user=self.user) + + for flag in True, False: + with self.subTest(f"zaken_filter_enabled={flag}"): + self.config.zaken_filter_enabled = flag + self.config.save() + + response = self.client.get(self.inner_url, HTTP_HX_REQUEST="true") + + self.assertEqual(response.context.get("filter_form_enabled"), flag) + + doc = PQ(response.rendered_content) + self.assertEqual(len(doc.find("#filterBar")), 1 if flag else 0) + + @staticmethod + def _encode_statuses( + status_or_statuses: CaseFilterFormOption | list[CaseFilterFormOption], + ): + statuses = ( + [status_or_statuses] + if isinstance(status_or_statuses, CaseFilterFormOption) + else status_or_statuses + ) + parts = [urlencode({"status": status.value}) for status in statuses] + return "&".join(parts) def test_filter_cases_simple(self, m): for mock in self.mocks: @@ -616,21 +643,16 @@ def test_filter_cases_simple(self, m): self.config.save() self.client.force_login(user=self.user) - inner_url = f"{reverse_lazy('cases:cases_content')}?status=Initial+request" + inner_url = f"{reverse_lazy('cases:cases_content')}?{self._encode_statuses(CaseFilterFormOption.OPEN_CASE)}" response = self.client.get(inner_url, HTTP_HX_REQUEST="true") - # check filter form - filter_form = response.context["filter_form"] - self.assertTrue(filter_form.is_valid()) - self.assertEqual(filter_form.cleaned_data.get("status"), ["Initial request"]) - # check cases cases = response.context["cases"] self.assertEqual(len(cases), 4) for case in cases: - self.assertEqual(case["current_status"], "Initial request") + self.assertIsNone(case["end_date"]) def test_filter_cases_multiple(self, m): for mock in self.mocks: @@ -640,26 +662,19 @@ def test_filter_cases_multiple(self, m): self.config.save() self.client.force_login(user=self.user) - inner_url = f"{reverse_lazy('cases:cases_content')}?status=Initial+request&status=Statustekst+finish" + filter_param = self._encode_statuses( + [CaseFilterFormOption.CLOSED_CASE, CaseFilterFormOption.OPEN_SUBMISSION] + ) + inner_url = f"{reverse_lazy('cases:cases_content')}?{filter_param}" response = self.client.get(inner_url, HTTP_HX_REQUEST="true") - # check filter form - filter_form = response.context["filter_form"] - self.assertTrue(filter_form.is_valid()) - self.assertEqual( - filter_form.cleaned_data.get("status"), - ["Initial request", "Statustekst finish"], - ) - # check cases cases = response.context["cases"] - self.assertEqual(len(cases), 6) + self.assertEqual(len(cases), 4) for case in cases: - self.assertIn( - case["current_status"], ["Initial request", "Statustekst finish"] - ) + self.assertIsNotNone(case["end_date"]) @set_kvk_branch_number_in_session(None) def test_list_cases_for_eherkenning_user(self, m): @@ -1125,29 +1140,3 @@ def test_case_submission(self, m): cases[0]["datum_laatste_wijziging"].strftime("%Y-%m-%dT%H:%M:%S.%f%z"), data.submission_2["datumLaatsteWijziging"], ) - - -class CaseFilterFormTest(TestCase): - def test_case_filter_form_valid(self): - form = CaseFilterForm( - status_freqs={"Start": 2, "Intermediate": 2, "Finish": 3}, - data={"status": ["Start", "Intermediate"]}, - ) - - self.assertTrue(form.is_valid()) - - def test_case_filter_form_errors(self): - form = CaseFilterForm( - status_freqs={"Start": 2, "Intermediate": 2, "Finish": 3}, - data={"status": ["Start", "Bogus"]}, - ) - - self.assertFalse(form.is_valid()) - self.assertEqual( - form.errors, - { - "status": [ - _("Selecteer een geldige keuze. Bogus is geen beschikbare keuze.") - ] - }, - ) diff --git a/src/open_inwoner/templates/pages/cases/list_inner.html b/src/open_inwoner/templates/pages/cases/list_inner.html index 9685c0da72..d302ab9463 100644 --- a/src/open_inwoner/templates/pages/cases/list_inner.html +++ b/src/open_inwoner/templates/pages/cases/list_inner.html @@ -3,7 +3,7 @@

{{ page_title }} ({{ paginator.count }})

{{ title_text }}

-{% if filter_form %} +{% if filter_form_enabled %}
{% trans 'Filter op' %}: