diff --git a/src/open_inwoner/accounts/tests/test_auth.py b/src/open_inwoner/accounts/tests/test_auth.py index 5c60b5213c..76823b2954 100644 --- a/src/open_inwoner/accounts/tests/test_auth.py +++ b/src/open_inwoner/accounts/tests/test_auth.py @@ -955,7 +955,7 @@ def test_non_digid_user_can_edit_profile(self): form = edit_page.forms["profile-edit"] form["first_name"] = "changed_first" form["last_name"] = "changed_last" - response = form.submit() + form.submit() user = User.objects.get(id=test_user.id) @@ -994,7 +994,7 @@ def test_any_page_for_digid_user_redirect_to_necessary_fields(self): urls = [ reverse("pages-root"), reverse("products:category_list"), - reverse("cases:open_cases"), + reverse("cases:index"), reverse("profile:detail"), reverse("profile:data"), reverse("collaborate:plan_list"), diff --git a/src/open_inwoner/cms/cases/tests/test_htmx.py b/src/open_inwoner/cms/cases/tests/test_htmx.py index 61adb728dc..f8d41b789e 100644 --- a/src/open_inwoner/cms/cases/tests/test_htmx.py +++ b/src/open_inwoner/cms/cases/tests/test_htmx.py @@ -358,22 +358,7 @@ def test_cases(self, m): context = self.browser.new_context(storage_state=self.user_login_state) page = context.new_page() - page.goto(self.live_reverse("cases:open_cases")) - - # expected anchors - menu_items = page.get_by_role( - "complementary", name=_("Secundaire paginanavigatie") - ).get_by_role("listitem") - - expect( - menu_items.get_by_role("link", name=_("Openstaande aanvragen")) - ).to_be_visible() - expect( - menu_items.get_by_role("link", name=_("Lopende aanvragen")) - ).to_be_visible() - expect( - menu_items.get_by_role("link", name=_("Afgeronde aanvragen")) - ).to_be_visible() + page.goto(self.live_reverse("cases:index")) # case title case_title = page.get_by_role("link", name=self.zaaktype["omschrijving"]) diff --git a/src/open_inwoner/cms/cases/urls.py b/src/open_inwoner/cms/cases/urls.py index 41030809d6..b1c283d37f 100644 --- a/src/open_inwoner/cms/cases/urls.py +++ b/src/open_inwoner/cms/cases/urls.py @@ -1,5 +1,4 @@ from django.urls import path -from django.views.generic import RedirectView from open_inwoner.accounts.views.contactmoments import ( KlantContactMomentDetailView, @@ -11,30 +10,14 @@ CaseDocumentDownloadView, CaseDocumentUploadFormView, InnerCaseDetailView, - InnerClosedCaseListView, - InnerOpenCaseListView, - InnerOpenSubmissionListView, + InnerCaseListView, OuterCaseDetailView, - OuterClosedCaseListView, - OuterOpenCaseListView, - OuterOpenSubmissionListView, + OuterCaseListView, ) app_name = "cases" urlpatterns = [ - path( - "closed/content/", - InnerClosedCaseListView.as_view(), - name="closed_cases_content", - ), - path("closed/", OuterClosedCaseListView.as_view(), name="closed_cases"), - path("forms/", OuterOpenSubmissionListView.as_view(), name="open_submissions"), - path( - "forms/content/", - InnerOpenSubmissionListView.as_view(), - name="open_submissions_content", - ), path( "contactmomenten/", KlantContactMomentListView.as_view(), @@ -70,7 +53,6 @@ CaseDocumentUploadFormView.as_view(), name="case_detail_document_form", ), - path("open/", RedirectView.as_view(pattern_name="cases:open_cases"), name="index"), - path("content/", InnerOpenCaseListView.as_view(), name="open_cases_content"), - path("", OuterOpenCaseListView.as_view(), name="open_cases"), + path("content/", InnerCaseListView.as_view(), name="cases_content"), + path("", OuterCaseListView.as_view(), name="index"), ] diff --git a/src/open_inwoner/cms/cases/views/__init__.py b/src/open_inwoner/cms/cases/views/__init__.py index 05d357737a..7afdddfb80 100644 --- a/src/open_inwoner/cms/cases/views/__init__.py +++ b/src/open_inwoner/cms/cases/views/__init__.py @@ -1,9 +1,4 @@ -from .cases import ( - InnerClosedCaseListView, - InnerOpenCaseListView, - OuterClosedCaseListView, - OuterOpenCaseListView, -) +from .cases import InnerCaseListView, OuterCaseListView from .status import ( CaseContactFormView, CaseDocumentDownloadView, @@ -11,4 +6,3 @@ InnerCaseDetailView, OuterCaseDetailView, ) -from .submissions import InnerOpenSubmissionListView, OuterOpenSubmissionListView diff --git a/src/open_inwoner/cms/cases/views/cases.py b/src/open_inwoner/cms/cases/views/cases.py index 60deea403f..19e3c82f46 100644 --- a/src/open_inwoner/cms/cases/views/cases.py +++ b/src/open_inwoner/cms/cases/views/cases.py @@ -1,121 +1,76 @@ from django.urls import reverse -from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ from django.views.generic import TemplateView -from view_breadcrumbs import BaseBreadcrumbMixin - from open_inwoner.htmx.mixins import RequiresHtmxMixin +from open_inwoner.openzaak.cases import fetch_cases, preprocess_data +from open_inwoner.openzaak.formapi import fetch_open_submissions +from open_inwoner.openzaak.models import OpenZaakConfig +from open_inwoner.openzaak.types import UniformCase +from open_inwoner.utils.mixins import PaginationMixin from open_inwoner.utils.views import CommonPageMixin -from .mixins import CaseAccessMixin, CaseListMixin, OuterCaseAccessMixin +from .mixins import CaseAccessMixin, CaseLogMixin, OuterCaseAccessMixin -class OuterOpenCaseListView( - OuterCaseAccessMixin, CommonPageMixin, BaseBreadcrumbMixin, TemplateView -): - template_name = "pages/cases/list_outer.html" +class OuterCaseListView(OuterCaseAccessMixin, CommonPageMixin, TemplateView): + """View on the case list while content is loaded via htmx""" - @cached_property - def crumbs(self): - return [(_("Mijn aanvragen"), reverse("cases:open_cases"))] + template_name = "pages/cases/list_outer.html" def page_title(self): - return _("Lopende aanvragen") - - def get_anchors(self) -> list: - return [ - (reverse("cases:open_submissions"), _("Openstaande aanvragen")), - ("#cases", _("Lopende aanvragen")), - (reverse("cases:closed_cases"), _("Afgeronde aanvragen")), - ] + return _("Mijn aanvragen") def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - # anchors are needed here as well for rendering the mobile ones - context["anchors"] = self.get_anchors() - context["hxget"] = reverse("cases:open_cases_content") + context["hxget"] = reverse("cases:cases_content") return context -class InnerOpenCaseListView( - RequiresHtmxMixin, CommonPageMixin, CaseAccessMixin, CaseListMixin, TemplateView +class InnerCaseListView( + RequiresHtmxMixin, + CommonPageMixin, + CaseAccessMixin, + CaseLogMixin, + PaginationMixin, + TemplateView, ): template_name = "pages/cases/list_inner.html" + paginate_by = 9 def page_title(self): - return _("Lopende aanvragen") + return _("Mijn aanvragen") def get_cases(self): - all_cases = super().get_cases() - - cases = [case for case in all_cases if not case.einddatum] - cases.sort(key=lambda case: case.startdatum, reverse=True) - return cases - - def get_anchors(self) -> list: - return [ - (reverse("cases:open_submissions"), _("Openstaande aanvragen")), - ("#cases", _("Lopende aanvragen")), - (reverse("cases:closed_cases"), _("Afgeronde aanvragen")), - ] - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["hxget"] = reverse("cases:open_cases_content") - return context - - -class OuterClosedCaseListView( - OuterCaseAccessMixin, CommonPageMixin, BaseBreadcrumbMixin, TemplateView -): - template_name = "pages/cases/list_outer.html" - - @cached_property - def crumbs(self): - return [(_("Mijn aanvragen"), reverse("cases:closed_cases"))] - - def page_title(self): - return _("Afgeronde aanvragen") + raw_cases = fetch_cases(self.request.user.bsn) + preprocessed_cases = preprocess_data(raw_cases) + preprocessed_cases.sort(key=lambda case: case.startdatum, reverse=True) + return preprocessed_cases - def get_anchors(self) -> list: - return [ - (reverse("cases:open_submissions"), _("Openstaande aanvragen")), - (reverse("cases:open_cases"), _("Lopende aanvragen")), - ("#cases", _("Afgeronde aanvragen")), - ] + def get_submissions(self): + subs = fetch_open_submissions(self.request.user.bsn) + subs.sort(key=lambda sub: sub.datum_laatste_wijziging, reverse=True) + return subs def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context["hxget"] = reverse("cases:closed_cases_content") - context["anchors"] = self.get_anchors() - return context - - -class InnerClosedCaseListView( - RequiresHtmxMixin, CommonPageMixin, CaseAccessMixin, CaseListMixin, TemplateView -): - template_name = "pages/cases/list_inner.html" + config = OpenZaakConfig.get_solo() - def page_title(self): - return _("Afgeronde aanvragen") + # update ctx with submissions + cases + open_submissions: list[UniformCase] = self.get_submissions() + preprocessed_cases: list[UniformCase] = self.get_cases() + paginator_dict = self.paginate_with_context( + [*open_submissions, *preprocessed_cases] + ) + case_dicts = [case.process_data() for case in paginator_dict["object_list"]] - def get_cases(self): - all_cases = super().get_cases() + context["cases"] = case_dicts + context.update(paginator_dict) - cases = [case for case in all_cases if case.einddatum] - cases.sort(key=lambda case: case.einddatum, reverse=True) - return cases + self.log_access_cases(case_dicts) - def get_anchors(self) -> list: - return [ - (reverse("cases:open_submissions"), _("Openstaande aanvragen")), - (reverse("cases:open_cases"), _("Lopende aanvragen")), - ("#cases", _("Afgeronde aanvragen")), - ] - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["hxget"] = reverse("cases:closed_cases_content") + # other data + context["hxget"] = reverse("cases:cases_content") + context["title_text"] = config.title_text return context diff --git a/src/open_inwoner/cms/cases/views/mixins.py b/src/open_inwoner/cms/cases/views/mixins.py index 977ab7b583..67ecd8905a 100644 --- a/src/open_inwoner/cms/cases/views/mixins.py +++ b/src/open_inwoner/cms/cases/views/mixins.py @@ -1,41 +1,38 @@ import logging -from collections import defaultdict -from typing import List, Optional +from typing import Optional from django.contrib.auth.mixins import AccessMixin, LoginRequiredMixin from django.template.response import TemplateResponse from django.utils.translation import gettext_lazy as _ -from glom import glom - from open_inwoner.openzaak.api_models import Zaak -from open_inwoner.openzaak.cases import ( - fetch_cases, - fetch_roles_for_case_and_bsn, - fetch_single_case, - fetch_single_status, -) -from open_inwoner.openzaak.catalog import ( - fetch_single_case_type, - fetch_single_status_type, -) -from open_inwoner.openzaak.models import ( - OpenZaakConfig, - StatusTranslation, - ZaakTypeStatusTypeConfig, -) -from open_inwoner.openzaak.utils import format_zaak_identificatie, is_zaak_visible -from open_inwoner.utils.mixins import PaginationMixin +from open_inwoner.openzaak.cases import fetch_roles_for_case_and_bsn, fetch_single_case +from open_inwoner.openzaak.catalog import fetch_single_case_type +from open_inwoner.openzaak.types import UniformCase +from open_inwoner.openzaak.utils import is_zaak_visible from open_inwoner.utils.views import LogMixin logger = logging.getLogger(__name__) class CaseLogMixin(LogMixin): - def log_case_access(self, case_identificatie: str): + def log_access_cases(self, cases: list[dict]): + """ + Log access to cases on the list view + + Creates a single log for all cases + """ + case_ids = (case["identification"] for case in cases) + + self.log_user_action( + self.request.user, + _("Zaken bekeken: {cases}").format(cases=", ".join(case_ids)), + ) + + def log_access_case_detail(self, case: UniformCase): self.log_user_action( self.request.user, - _("Zaak bekeken: {case}").format(case=case_identificatie), + _("Zaak bekeken: {case}").format(case=case.identification), ) @@ -52,8 +49,6 @@ class CaseAccessMixin(AccessMixin): - case confidentiality is not higher than globally configured """ - case: Zaak = None - def dispatch(self, request, *args, **kwargs): if not request.user.is_authenticated: logger.debug("CaseAccessMixin - permission denied: user not authenticated") @@ -105,91 +100,6 @@ def get_case(self, kwargs) -> Optional[Zaak]: return fetch_single_case(case_uuid) -class CaseListMixin(CaseLogMixin, PaginationMixin): - paginate_by = 9 - - def get_cases(self) -> List[Zaak]: - cases = fetch_cases(self.request.user.bsn) - - case_types = {} - case_types_set = {case.zaaktype for case in cases} - - mapping = { - zaaktype_statustype.statustype_url: zaaktype_statustype - for zaaktype_statustype in ZaakTypeStatusTypeConfig.objects.all() - } - - # fetch unique case types - for case_type_url in case_types_set: - # todo parallel - case_types[case_type_url] = fetch_single_case_type(case_type_url) - - # set resolved case types - for case in cases: - case.zaaktype = case_types[case.zaaktype] - - # filter visibility - cases = [case for case in cases if is_zaak_visible(case)] - - # fetch case status resources and attach resolved to case - status_types = defaultdict(list) - for case in cases: - if case.status: - # todo parallel - case.status = fetch_single_status(case.status) - status_types[case.status.statustype].append(case) - - case.statustype_config = mapping.get(case.status.statustype) - - for status_type_url, _cases in status_types.items(): - # todo parallel - status_type = fetch_single_status_type(status_type_url) - for case in _cases: - case.status.statustype = status_type - - return cases - - def process_cases(self, cases: List[Zaak]) -> List[dict]: - # Prepare data for frontend - config = OpenZaakConfig.get_solo() - status_translate = StatusTranslation.objects.get_lookup() - - updated_cases = [] - for case in cases: - case_dict = { - "identificatie": format_zaak_identificatie(case.identificatie, config), - "uuid": str(case.uuid), - "start_date": case.startdatum, - "end_date": getattr(case, "einddatum", None), - "description": case.zaaktype.omschrijving, - "current_status": status_translate.from_glom( - case, "status.statustype.omschrijving", default="" - ), - "statustype_config": getattr(case, "statustype_config", None), - } - updated_cases.append(case_dict) - return updated_cases - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - - raw_cases = self.get_cases() - paginator_dict = self.paginate_with_context(raw_cases) - cases = self.process_cases(paginator_dict["object_list"]) - - context["cases"] = cases - context.update(paginator_dict) - - for case in cases: - self.log_case_access(case["identificatie"]) - - context["anchors"] = self.get_anchors() - return context - - def get_anchors(self) -> list: - return [] - - class OuterCaseAccessMixin(LoginRequiredMixin): def dispatch(self, request, *args, **kwargs): if request.user.is_authenticated and not request.user.bsn: diff --git a/src/open_inwoner/cms/cases/views/status.py b/src/open_inwoner/cms/cases/views/status.py index 66c11330cc..d87bec77e0 100644 --- a/src/open_inwoner/cms/cases/views/status.py +++ b/src/open_inwoner/cms/cases/views/status.py @@ -44,11 +44,7 @@ ZaakTypeConfig, ZaakTypeInformatieObjectTypeConfig, ) -from open_inwoner.openzaak.utils import ( - format_zaak_identificatie, - get_role_name_display, - is_info_object_visible, -) +from open_inwoner.openzaak.utils import get_role_name_display, is_info_object_visible from open_inwoner.utils.translate import TranslationLookup from open_inwoner.utils.views import CommonPageMixin, LogMixin @@ -71,7 +67,7 @@ class OuterCaseDetailView( @cached_property def crumbs(self): return [ - (_("Mijn aanvragen"), reverse("cases:open_cases")), + (_("Mijn aanvragen"), reverse("cases:index")), ( _("Status"), reverse("cases:case_detail", kwargs=self.kwargs), @@ -91,11 +87,12 @@ class InnerCaseDetailView( template_name = "pages/cases/status_inner.html" form_class = CaseUploadForm contact_form_class = CaseContactForm + case: Zaak = None @cached_property def crumbs(self): return [ - (_("Mijn aanvragen"), reverse("cases:open_cases")), + (_("Mijn aanvragen"), reverse("cases:index")), ( _("Status"), reverse("cases:case_detail", kwargs=self.kwargs), @@ -108,8 +105,10 @@ def page_title(self): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) + # case is retrieved via CaseAccessMixin if self.case: - self.log_case_access(self.case.identificatie) + self.log_access_case_detail(self.case) + config = OpenZaakConfig.get_solo() status_translate = StatusTranslation.objects.get_lookup() @@ -137,9 +136,7 @@ def get_context_data(self, **kwargs): context["case"] = { "id": str(self.case.uuid), - "identification": format_zaak_identificatie( - self.case.identificatie, config - ), + "identification": self.case.identification, "initiator": self.get_initiator_display(self.case), "result": self.get_result_display(self.case), "start_date": self.case.startdatum, @@ -354,7 +351,7 @@ def handle_no_permission(self): raise PermissionDenied() -class CaseDocumentUploadFormView(CaseAccessMixin, CaseLogMixin, FormView): +class CaseDocumentUploadFormView(CaseAccessMixin, LogMixin, FormView): template_name = "pages/cases/document_form.html" form_class = CaseUploadForm @@ -437,7 +434,7 @@ def get_context_data(self, **kwargs): return context -class CaseContactFormView(CaseAccessMixin, CaseLogMixin, FormView): +class CaseContactFormView(CaseAccessMixin, LogMixin, FormView): template_name = "pages/cases/contact_form.html" form_class = CaseContactForm diff --git a/src/open_inwoner/cms/cases/views/submissions.py b/src/open_inwoner/cms/cases/views/submissions.py deleted file mode 100644 index 951b1d51c0..0000000000 --- a/src/open_inwoner/cms/cases/views/submissions.py +++ /dev/null @@ -1,72 +0,0 @@ -from django.urls import reverse -from django.utils.functional import cached_property -from django.utils.translation import gettext_lazy as _ -from django.views.generic import TemplateView - -from view_breadcrumbs import BaseBreadcrumbMixin - -from open_inwoner.htmx.mixins import RequiresHtmxMixin -from open_inwoner.openzaak.formapi import fetch_open_submissions -from open_inwoner.utils.views import CommonPageMixin - -from .mixins import CaseAccessMixin, OuterCaseAccessMixin - - -class OuterOpenSubmissionListView( - OuterCaseAccessMixin, CommonPageMixin, BaseBreadcrumbMixin, TemplateView -): - template_name = "pages/cases/submissions_outer.html" - - @cached_property - def crumbs(self): - return [(_("Mijn aanvragen"), reverse("cases:open_submissions"))] - - def page_title(self): - return _("Openstaande aanvragen") - - def get_anchors(self) -> list: - return [ - ("#submissions", _("Openstaande aanvragen")), - (reverse("cases:open_cases"), _("Lopende aanvragen")), - (reverse("cases:closed_cases"), _("Afgeronde aanvragen")), - ] - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["hxget"] = reverse("cases:open_submissions_content") - context["anchors"] = self.get_anchors() - return context - - -class InnerOpenSubmissionListView( - RequiresHtmxMixin, - CommonPageMixin, - BaseBreadcrumbMixin, - CaseAccessMixin, - TemplateView, -): - template_name = "pages/cases/submissions_inner.html" - - @cached_property - def crumbs(self): - return [(_("Mijn aanvragen"), reverse("cases:open_submissions"))] - - def page_title(self): - return _("Openstaande aanvragen") - - def get_submissions(self): - submissions = fetch_open_submissions(self.request.user.bsn) - return submissions - - def get_anchors(self) -> list: - return [ - ("#submissions", _("Openstaande aanvragen")), - (reverse("cases:open_cases"), _("Lopende aanvragen")), - (reverse("cases:closed_cases"), _("Afgeronde aanvragen")), - ] - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["submissions"] = self.get_submissions() - context["anchors"] = self.get_anchors() - return context diff --git a/src/open_inwoner/components/templates/components/Notification/Notification.html b/src/open_inwoner/components/templates/components/Notification/Notification.html index 8305b92862..b6c7f87ad1 100644 --- a/src/open_inwoner/components/templates/components/Notification/Notification.html +++ b/src/open_inwoner/components/templates/components/Notification/Notification.html @@ -1,5 +1,5 @@ {% load i18n button_tags icon_tags button_tags icon_tags %} -
+
{% if not icon == False %}
{% icon icon %} diff --git a/src/open_inwoner/components/templatetags/notification_tags.py b/src/open_inwoner/components/templatetags/notification_tags.py index 799147c5e4..ea9aa01db6 100644 --- a/src/open_inwoner/components/templatetags/notification_tags.py +++ b/src/open_inwoner/components/templatetags/notification_tags.py @@ -31,7 +31,7 @@ def notification(type, message, **kwargs): Add a notification to the screen. These will be places inline. Usage: - {% notification type="success" message="this is the message" closable=True %} + {% notification type="success" message="this is the message" closable=True ctx="cases" %} {% notification type="warning" title="title" message="this is the message" action="#" action_text="Verzoek opsturen" %} Variables: @@ -43,6 +43,8 @@ def notification(type, message, **kwargs): - action_text: string | The text of the button. - closable: bool | If a close button should be shown. - compact: boolean | Whether to use compact styling or not. + - ctx: string | The context in which the tag is rendered; used to add + classes to HTML element and override CSS """ message_types = { "debug": "bug_report", diff --git a/src/open_inwoner/openzaak/admin.py b/src/open_inwoner/openzaak/admin.py index 54b4e35b3d..95e5336465 100644 --- a/src/open_inwoner/openzaak/admin.py +++ b/src/open_inwoner/openzaak/admin.py @@ -47,6 +47,7 @@ class OpenZaakConfigAdmin(SingletonModelAdmin): "document_max_confidentiality", "max_upload_size", "allowed_file_extensions", + "title_text", ], }, ), diff --git a/src/open_inwoner/openzaak/api_models.py b/src/open_inwoner/openzaak/api_models.py index 2e2c0eca76..9ed56f4331 100644 --- a/src/open_inwoner/openzaak/api_models.py +++ b/src/open_inwoner/openzaak/api_models.py @@ -1,3 +1,5 @@ +import logging +import re from dataclasses import dataclass, field from datetime import date, datetime from typing import Dict, Optional, Union @@ -6,6 +8,9 @@ from zgw_consumers.api_models.base import Model, ZGWModel from zgw_consumers.api_models.constants import RolOmschrijving, RolTypes +logger = logging.getLogger(__name__) + + """ Modified ZGWModel's to work with both OpenZaak and e-Suite implementations, because there is an issue where e-Suite doesn't return all JSON fields the official API and dataclasses expect @@ -32,6 +37,55 @@ class Zaak(ZGWModel): # relevante_andere_zaken: list # zaakgeometrie: dict + @staticmethod + def _reformat_esuite_zaak_identificatie(identificatie: str) -> str: + """ + 0014ESUITE66392022 -> 6639-2022 + + Static utility function; only used in connection with `Zaak` instances + """ + exp = r"^\d+ESUITE(?P\d+?)(?P\d{4})$" + m = re.match(exp, identificatie) + if not m: + return identificatie + num = m.group("num") + year = m.group("year") + return f"{num}-{year}" + + def _format_zaak_identificatie(self) -> str: + from open_inwoner.openzaak.models import OpenZaakConfig + + zaak_config = OpenZaakConfig.get_solo() + + if zaak_config.reformat_esuite_zaak_identificatie: + return self._reformat_esuite_zaak_identificatie(self.identificatie) + return self.identificatie + + @property + def identification(self) -> str: + return self._format_zaak_identificatie() + + def process_data(self) -> dict: + """ + Prepare data for template + """ + from open_inwoner.openzaak.models import StatusTranslation + + status_translate = StatusTranslation.objects.get_lookup() + + return { + "identification": self.identification, + "uuid": str(self.uuid), + "start_date": self.startdatum, + "end_date": getattr(self, "einddatum", None), + "description": self.zaaktype.omschrijving, + "current_status": status_translate.from_glom( + self, "status.statustype.omschrijving", default="" + ), + "statustype_config": getattr(self, "statustype_config", None), + "case_type": "Zaak", + } + @dataclass class ZaakType(ZGWModel): diff --git a/src/open_inwoner/openzaak/cases.py b/src/open_inwoner/openzaak/cases.py index 64c68ae1ca..9dc7cb5f1a 100644 --- a/src/open_inwoner/openzaak/cases.py +++ b/src/open_inwoner/openzaak/cases.py @@ -1,3 +1,4 @@ +import copy import logging from typing import List, Optional @@ -9,10 +10,12 @@ from zgw_consumers.api_models.constants import RolOmschrijving, RolTypes from zgw_consumers.service import get_paginated_results +from ..utils.decorators import cache as cache_result from .api_models import Resultaat, Rol, Status, Zaak, ZaakInformatieObject +from .catalog import fetch_single_case_type, fetch_single_status_type from .clients import build_client -from .models import OpenZaakConfig -from .utils import cache as cache_result +from .models import OpenZaakConfig, ZaakTypeStatusTypeConfig +from .utils import is_zaak_visible logger = logging.getLogger(__name__) @@ -288,3 +291,64 @@ def connect_case_with_document(case_url: str, document_url: str) -> Optional[dic return return response + + +def resolve_zaak_type(case: Zaak) -> None: + """ + Resolve `case.zaaktype` (`str`) to a `ZaakType(ZGWModel)` object + + Note: the result of `fetch_single_case_type` is cached, hence a request + is only made for new case type urls + """ + case_type_url = case.zaaktype + case_type = fetch_single_case_type(case_type_url) + case.zaaktype = case_type + + +def resolve_status(case: Zaak) -> None: + """ + Resolve `case.status` (`str`) to a `Status(ZGWModel)` object + """ + case.status = fetch_single_status(case.status) + + +def resolve_status_type(case: Zaak) -> None: + """ + Resolve `case.statustype` (`str`) to a `StatusType(ZGWModel)` object + """ + statustype_url = case.status.statustype + case.status.statustype = fetch_single_status_type(statustype_url) + + +def add_status_type_config(case: Zaak) -> None: + """ + Add `ZaakTypeStatusTypeConfig` corresponding to the status type url of the case + + Note: must be called after `resolve_status_type` since we're getting the + status type url from `case.status.statustype` + """ + try: + case.statustype_config = ZaakTypeStatusTypeConfig.objects.get( + statustype_url=case.status.statustype.url + ) + except ZaakTypeStatusTypeConfig.DoesNotExist: + pass + + +def filter_visible(cases: list[Zaak]) -> list[Zaak]: + return [case for case in cases if is_zaak_visible(case)] + + +def preprocess_data(cases: list[Zaak]) -> list[Zaak]: + """ + Resolve zaaktype and statustype, add status type config, filter for visibility + """ + for case in cases: + resolve_zaak_type(case) + + if case.status: + resolve_status(case) + resolve_status_type(case) + add_status_type_config(case) + + return filter_visible(cases) diff --git a/src/open_inwoner/openzaak/catalog.py b/src/open_inwoner/openzaak/catalog.py index 0685fc984e..de40998b06 100644 --- a/src/open_inwoner/openzaak/catalog.py +++ b/src/open_inwoner/openzaak/catalog.py @@ -9,9 +9,10 @@ from zgw_consumers.api_models.catalogi import Catalogus from zgw_consumers.service import get_paginated_results +from ..utils.decorators import cache as cache_result from .api_models import InformatieObjectType, ResultaatType, StatusType, ZaakType from .clients import build_client -from .utils import cache as cache_result, get_retrieve_resource_by_uuid_url +from .utils import get_retrieve_resource_by_uuid_url logger = logging.getLogger(__name__) diff --git a/src/open_inwoner/openzaak/documents.py b/src/open_inwoner/openzaak/documents.py index 7e5e6c5c9a..b234b64508 100644 --- a/src/open_inwoner/openzaak/documents.py +++ b/src/open_inwoner/openzaak/documents.py @@ -16,7 +16,7 @@ from open_inwoner.openzaak.clients import build_client from open_inwoner.openzaak.models import OpenZaakConfig -from .utils import cache as cache_result +from ..utils.decorators import cache as cache_result logger = logging.getLogger(__name__) diff --git a/src/open_inwoner/openzaak/formapi.py b/src/open_inwoner/openzaak/formapi.py index 755249de8a..a027c86399 100644 --- a/src/open_inwoner/openzaak/formapi.py +++ b/src/open_inwoner/openzaak/formapi.py @@ -22,6 +22,25 @@ class OpenSubmission(Model): vervolg_link: Optional[str] = None eind_datum_geldigheid: Optional[datetime] = None + @property + def identification(self) -> str: + return f"{self.naam}: {self.uuid}" + + def process_data(self) -> dict: + """ + Prepare data for template + """ + return { + "identification": self.identification, + "url": self.url, + "uuid": self.uuid, + "naam": self.naam, + "vervolg_link": self.vervolg_link, + "datum_laatste_wijziging": self.datum_laatste_wijziging, + "eind_datum_geldigheid": self.eind_datum_geldigheid or "Geen", + "case_type": "OpenSubmission", + } + def fetch_open_submissions(bsn: str) -> List[OpenSubmission]: if not bsn: diff --git a/src/open_inwoner/openzaak/migrations/0025_openzaakconfig_title_text.py b/src/open_inwoner/openzaak/migrations/0025_openzaakconfig_title_text.py new file mode 100644 index 0000000000..ad6a75cbab --- /dev/null +++ b/src/open_inwoner/openzaak/migrations/0025_openzaakconfig_title_text.py @@ -0,0 +1,22 @@ +# Generated by Django 3.2.20 on 2023-10-11 07:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("openzaak", "0024_zaaktypeconfig_contact_subject_code"), + ] + + operations = [ + migrations.AddField( + model_name="openzaakconfig", + name="title_text", + field=models.TextField( + default="Hier vindt u een overzicht van al uw lopende an afgeronde aanvragen.", + help_text="The title/introductory text shown on the list view of 'Mijn aanvragen'.", + verbose_name="Title text", + ), + ), + ] diff --git a/src/open_inwoner/openzaak/migrations/0026_merge_20231024_0949.py b/src/open_inwoner/openzaak/migrations/0026_merge_20231024_0949.py new file mode 100644 index 0000000000..c4268d0751 --- /dev/null +++ b/src/open_inwoner/openzaak/migrations/0026_merge_20231024_0949.py @@ -0,0 +1,13 @@ +# Generated by Django 3.2.20 on 2023-10-24 07:49 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("openzaak", "0025_auto_20231016_0957"), + ("openzaak", "0025_openzaakconfig_title_text"), + ] + + operations = [] diff --git a/src/open_inwoner/openzaak/models.py b/src/open_inwoner/openzaak/models.py index 3dab53dde6..727398dcb3 100644 --- a/src/open_inwoner/openzaak/models.py +++ b/src/open_inwoner/openzaak/models.py @@ -131,6 +131,16 @@ class OpenZaakConfig(SingletonModel): default=False, ) + title_text = models.TextField( + verbose_name=_("Title text"), + help_text=_( + "The title/introductory text shown on the list view of 'Mijn aanvragen'." + ), + default=_( + "Hier vindt u een overzicht van al uw lopende an afgeronde aanvragen." + ), + ) + class Meta: verbose_name = _("Open Zaak configuration") diff --git a/src/open_inwoner/openzaak/notifications.py b/src/open_inwoner/openzaak/notifications.py index 05e4a159d2..45b221bfd8 100644 --- a/src/open_inwoner/openzaak/notifications.py +++ b/src/open_inwoner/openzaak/notifications.py @@ -35,7 +35,6 @@ UserCaseStatusNotification, ) from open_inwoner.openzaak.utils import ( - format_zaak_identificatie, get_zaak_type_config, get_zaak_type_info_object_type_config, is_info_object_visible, @@ -364,7 +363,7 @@ def send_case_update_email(user: User, case: Zaak): template = find_template("case_notification") context = { - "identification": format_zaak_identificatie(case.identificatie), + "identification": case.identification, "type_description": case.zaaktype.omschrijving, "start_date": case.startdatum, "case_link": case_detail_url, diff --git a/src/open_inwoner/openzaak/tests/factories.py b/src/open_inwoner/openzaak/tests/factories.py index 06df72472b..1195cf2292 100644 --- a/src/open_inwoner/openzaak/tests/factories.py +++ b/src/open_inwoner/openzaak/tests/factories.py @@ -3,13 +3,12 @@ from simple_certmanager.constants import CertificateTypes from simple_certmanager.models import Certificate from zgw_consumers.api_models.base import factory as zwg_factory -from zgw_consumers.api_models.catalogi import InformatieObjectType from zgw_consumers.api_models.constants import RolOmschrijving from zgw_consumers.models import Service from zgw_consumers.test import generate_oas_component from open_inwoner.accounts.tests.factories import UserFactory -from open_inwoner.openzaak.api_models import Notification, Rol, ZaakType +from open_inwoner.openzaak.api_models import Notification, Rol from open_inwoner.openzaak.models import ( CatalogusConfig, StatusTranslation, diff --git a/src/open_inwoner/openzaak/tests/mocks.py b/src/open_inwoner/openzaak/tests/mocks.py new file mode 100644 index 0000000000..0d573488b0 --- /dev/null +++ b/src/open_inwoner/openzaak/tests/mocks.py @@ -0,0 +1,42 @@ +from zgw_consumers.test import mock_service_oas_get + +from open_inwoner.openzaak.tests.shared import FORMS_ROOT + + +class ESuiteData: + def __init__(self): + self.submission_1 = { + "url": "https://dmidoffice2.esuite-development.net/formulieren-provider/api/v1/8e3ae29c-7bc5-4f7d-a27c-b0c83c13328e", + "uuid": "8e3ae29c-7bc5-4f7d-a27c-b0c83c13328e", + "naam": "Melding openbare ruimte", + "vervolgLink": "https://dloket2.esuite-development.net/formulieren-nieuw/formulier/start/8e3ae29c-7bc5-4f7d-a27c-b0c83c13328e", + "datumLaatsteWijziging": "2023-02-13T14:02:00.999+01:00", + "eindDatumGeldigheid": "2023-05-14T14:02:00.999+02:00", + } + self.submission_2 = { + "url": "https://dmidoffice2.esuite-development.net/formulieren-provider/api/v1/d14658b0-dcb4-4d3c-a61c-fd7d0c78f296", + "uuid": "d14658b0-dcb4-4d3c-a61c-fd7d0c78f296", + "naam": "Indienen bezwaarschrift", + "vervolgLink": "https://dloket2.esuite-development.net/formulieren-nieuw/formulier/start/d14658b0-dcb4-4d3c-a61c-fd7d0c78f296", + "datumLaatsteWijziging": "2023-02-13T14:10:26.197000+0100", + "eindDatumGeldigheid": "2023-05-14T14:10:26.197+02:00", + } + # note this is a weird esuite response without pagination links + self.response = { + "count": 2, + "results": [ + self.submission_1, + self.submission_2, + ], + } + + def setUpOASMocks(self, m): + mock_service_oas_get(m, FORMS_ROOT, "submissions-esuite") + + def install_mocks(self, m): + self.setUpOASMocks(m) + m.get( + f"{FORMS_ROOT}openstaande-inzendingen", + json=self.response, + ) + return self diff --git a/src/open_inwoner/openzaak/tests/test_case_detail.py b/src/open_inwoner/openzaak/tests/test_case_detail.py index 72b1c9e5e8..531524fc89 100644 --- a/src/open_inwoner/openzaak/tests/test_case_detail.py +++ b/src/open_inwoner/openzaak/tests/test_case_detail.py @@ -34,7 +34,6 @@ from ...utils.tests.helpers import AssertRedirectsMixin from ..api_models import Status, StatusType from ..models import OpenZaakConfig -from ..utils import format_zaak_identificatie from .factories import CatalogusConfigFactory, ServiceFactory from .shared import CATALOGI_ROOT, DOCUMENTEN_ROOT, ZAKEN_ROOT @@ -414,12 +413,13 @@ def test_page_reformats_zaak_identificatie(self, m): self._setUpMocks(m) with patch( - "open_inwoner.cms.cases.views.status.format_zaak_identificatie", - wraps=format_zaak_identificatie, + "open_inwoner.openzaak.api_models.Zaak._format_zaak_identificatie", ) as spy_format: self.app.get(self.case_detail_url, user=self.user) - spy_format.assert_called_once() + # _format_zaak_identificatie is called twice on requesting DetailVew: + # once for the log, once for adding case to context + spy_format.assert_called def test_page_translates_statuses(self, m): st1 = StatusTranslationFactory( diff --git a/src/open_inwoner/openzaak/tests/test_cases.py b/src/open_inwoner/openzaak/tests/test_cases.py index 6a936dc9b0..7078014e37 100644 --- a/src/open_inwoner/openzaak/tests/test_cases.py +++ b/src/open_inwoner/openzaak/tests/test_cases.py @@ -15,31 +15,28 @@ from open_inwoner.accounts.choices import LoginTypeChoices from open_inwoner.accounts.tests.factories import UserFactory -from open_inwoner.cms.cases.views.mixins import CaseListMixin +from open_inwoner.cms.cases.views.cases import InnerCaseListView +from open_inwoner.openzaak.tests.shared import FORMS_ROOT from open_inwoner.utils.test import ClearCachesMixin, paginated_response +from open_inwoner.utils.tests.helpers import AssertTimelineLogMixin, Lookups from ...utils.tests.helpers import AssertRedirectsMixin +from ..api_models import Zaak from ..constants import StatusIndicators -from ..models import OpenZaakConfig, ZaakTypeStatusTypeConfig -from ..utils import format_zaak_identificatie +from ..models import OpenZaakConfig from .factories import ( ServiceFactory, StatusTranslationFactory, ZaakTypeStatusTypeConfigFactory, ) +from .mocks import ESuiteData from .shared import CATALOGI_ROOT, ZAKEN_ROOT @override_settings(ROOT_URLCONF="open_inwoner.cms.tests.urls") class CaseListAccessTests(AssertRedirectsMixin, ClearCachesMixin, WebTest): - outer_urls = [ - reverse_lazy("cases:open_cases"), - reverse_lazy("cases:closed_cases"), - ] - inner_urls = [ - reverse_lazy("cases:open_cases_content"), - reverse_lazy("cases:closed_cases_content"), - ] + outer_url = reverse_lazy("cases:index") + inner_url = reverse_lazy("cases:cases_content") @classmethod def setUpTestData(cls): @@ -63,18 +60,14 @@ def test_user_access_is_forbidden_when_not_logged_in_via_digid(self): last_name="", login_type=LoginTypeChoices.default, ) - for url in self.outer_urls: - with self.subTest(url): - self.app.get(url, user=user, status=403) + self.app.get(self.outer_url, user=user, status=403) def test_anonymous_user_has_no_access_to_cases_page(self): user = AnonymousUser() - for url in self.outer_urls: - with self.subTest(url): - response = self.app.get(url, user=user) + response = self.app.get(self.outer_url, user=user) - self.assertRedirectsLogin(response, next=url) + self.assertRedirectsLogin(response, next=self.outer_url) def test_bad_request_when_no_htmx_in_inner_urls(self): user = UserFactory( @@ -83,9 +76,7 @@ def test_bad_request_when_no_htmx_in_inner_urls(self): login_type=LoginTypeChoices.default, ) - for url in self.inner_urls: - with self.subTest(url): - self.app.get(url, user=user, status=400) + self.app.get(self.inner_url, user=user, status=400) def test_missing_zaak_client_returns_empty_list(self): user = UserFactory( @@ -94,11 +85,11 @@ def test_missing_zaak_client_returns_empty_list(self): self.config.zaak_service = None self.config.save() - for url in self.inner_urls: - with self.subTest(url): - response = self.app.get(url, user=user, headers={"HX-Request": "true"}) + response = self.app.get( + self.inner_url, user=user, headers={"HX-Request": "true"} + ) - self.assertListEqual(response.context.get("cases"), []) + self.assertListEqual(response.context.get("cases"), []) @requests_mock.Mocker() def test_no_cases_are_retrieved_when_http_404(self, m): @@ -111,11 +102,11 @@ def test_no_cases_are_retrieved_when_http_404(self, m): status_code=404, ) - for url in self.inner_urls: - with self.subTest(url): - response = self.app.get(url, user=user, headers={"HX-Request": "true"}) + response = self.app.get( + self.inner_url, user=user, headers={"HX-Request": "true"} + ) - self.assertListEqual(response.context.get("cases"), []) + self.assertListEqual(response.context.get("cases"), []) @requests_mock.Mocker() def test_no_cases_are_retrieved_when_http_500(self, m): @@ -128,18 +119,17 @@ def test_no_cases_are_retrieved_when_http_500(self, m): status_code=500, ) - for url in self.inner_urls: - with self.subTest(url): - response = self.app.get(url, user=user, headers={"HX-Request": "true"}) + response = self.app.get( + self.inner_url, user=user, headers={"HX-Request": "true"} + ) - self.assertListEqual(response.context.get("cases"), []) + self.assertListEqual(response.context.get("cases"), []) @requests_mock.Mocker() @override_settings(ROOT_URLCONF="open_inwoner.cms.tests.urls") -class CaseListViewTests(ClearCachesMixin, WebTest): - inner_url_open = reverse_lazy("cases:open_cases_content") - inner_url_closed = reverse_lazy("cases:closed_cases_content") +class CaseListViewTests(AssertTimelineLogMixin, ClearCachesMixin, WebTest): + inner_url = reverse_lazy("cases:cases_content") @classmethod def setUpTestData(cls): @@ -234,7 +224,7 @@ def setUpTestData(cls): url=f"{ZAKEN_ROOT}zaken/e4d469b9-6666-4bdd-bf42-b53445298102", uuid="e4d469b9-6666-4bdd-bf42-b53445298102", zaaktype=cls.zaaktype["url"], - identificatie="ZAAK-2022-0008800002", + identificatie="0014ESUITE66392022", omschrijving="Coffee zaak 2", startdatum="2022-01-12", einddatum=None, @@ -296,6 +286,14 @@ def setUpTestData(cls): datumStatusGezet="2021-01-12", statustoelichting="", ) + cls.submission = generate_oas_component( + "zrc", + "schemas/Zaak", + url=f"{ZAKEN_ROOT}zaken/d8bbdeb7-770f-4ca9-b1ea-77b4ee0bf67d", + uuid="c8yudeb7-490f-2cw9-h8wa-44h9830bf67d", + naam="mysub", + datum_laatste_wijziging="2023-10-10", + ) def _setUpMocks(self, m): mock_service_oas_get(m, ZAKEN_ROOT, "zrc") @@ -326,11 +324,11 @@ def _setUpMocks(self, m): ]: m.get(resource["url"], json=resource) - def test_list_open_cases(self, m): + def test_list_cases(self, m): self._setUpMocks(m) response = self.app.get( - self.inner_url_open, user=self.user, headers={"HX-Request": "true"} + self.inner_url, user=self.user, headers={"HX-Request": "true"} ) self.assertListEqual( @@ -340,33 +338,37 @@ def test_list_open_cases(self, m): "uuid": self.zaak2["uuid"], "start_date": datetime.date.fromisoformat(self.zaak2["startdatum"]), "end_date": None, - "identificatie": self.zaak2["identificatie"], + "identification": self.zaak2["identificatie"], "description": self.zaaktype["omschrijving"], "current_status": self.status_type1["omschrijving"], "statustype_config": self.zt_statustype_config1, + "case_type": "Zaak", }, { "uuid": self.zaak1["uuid"], "start_date": datetime.date.fromisoformat(self.zaak1["startdatum"]), "end_date": None, - "identificatie": self.zaak1["identificatie"], + "identification": self.zaak1["identificatie"], "description": self.zaaktype["omschrijving"], "current_status": self.status_type1["omschrijving"], "statustype_config": self.zt_statustype_config1, + "case_type": "Zaak", + }, + { + "uuid": self.zaak3["uuid"], + "start_date": datetime.date.fromisoformat(self.zaak3["startdatum"]), + "end_date": datetime.date.fromisoformat(self.zaak3["einddatum"]), + "identification": self.zaak3["identificatie"], + "description": self.zaaktype["omschrijving"], + "current_status": self.status_type2["omschrijving"], + "statustype_config": None, + "case_type": "Zaak", }, ], ) - # don't show closed cases - self.assertNotContains(response, self.zaak3["identificatie"]) - self.assertNotContains(response, self.zaak3["omschrijving"]) - self.assertNotContains(response, self.zaak_intern["identificatie"]) + # don't show internal cases self.assertNotContains(response, self.zaak_intern["omschrijving"]) - - zaken_cards = response.html.find_all("div", {"class": "card"}) - - self.assertEqual(len(zaken_cards), 2) - self.assertTrue("U moet documenten toevoegen" in zaken_cards[0].text) - self.assertTrue("U moet documenten toevoegen" in zaken_cards[1].text) + self.assertNotContains(response, self.zaak_intern["identificatie"]) # check zaken request query parameters list_zaken_req = [ @@ -387,38 +389,76 @@ def test_list_open_cases(self, m): }, ) - def test_list_open_cases_reformats_zaak_identificatie(self, m): + def test_format_zaak_identificatie(self, m): + config = OpenZaakConfig.get_solo() self._setUpMocks(m) - with patch( - "open_inwoner.cms.cases.views.mixins.format_zaak_identificatie", - wraps=format_zaak_identificatie, - ) as spy_format: - self.app.get( - self.inner_url_open, user=self.user, headers={"HX-Request": "true"} + with self.subTest("formatting enabled"): + config.reformat_esuite_zaak_identificatie = True + config.save() + + response = self.app.get( + self.inner_url, user=self.user, headers={"HX-Request": "true"} + ) + + e_suite_case = next( + ( + case + for case in response.context["cases"] + if case["uuid"] == self.zaak2["uuid"] + ) ) - spy_format.assert_called() - self.assertEqual(spy_format.call_count, 2) + self.assertEqual(e_suite_case["identification"], "6639-2022") + + with self.subTest("formatting disabled"): + config.reformat_esuite_zaak_identificatie = False + config.save() - def test_list_open_cases_translates_status(self, m): + response = self.app.get( + self.inner_url, user=self.user, headers={"HX-Request": "true"} + ) + + e_suite_case = next( + ( + case + for case in response.context["cases"] + if case["uuid"] == self.zaak2["uuid"] + ) + ) + + self.assertEqual(e_suite_case["identification"], "0014ESUITE66392022") + + def test_reformat_esuite_zaak_identificatie(self, m): + tests = [ + ("0014ESUITE66392022", "6639-2022"), + ("4321ESUITE00011991", "0001-1991"), + ("4321ESUITE123456781991", "12345678-1991"), + ("12345678", "12345678"), + ("aaaaaa1234", "aaaaaa1234"), + ] + + for value, expected in tests: + with self.subTest(value=value, expected=expected): + actual = Zaak._reformat_esuite_zaak_identificatie(value) + self.assertEqual(actual, expected) + + def test_list_cases_translates_status(self, m): st1 = StatusTranslationFactory( status=self.status_type1["omschrijving"], translation="Translated Status Type", ) self._setUpMocks(m) response = self.app.get( - self.inner_url_open, user=self.user, headers={"HX-Request": "true"} + self.inner_url, user=self.user, headers={"HX-Request": "true"} ) self.assertNotContains(response, st1.status) self.assertContains(response, st1.translation) - def test_list_open_cases_logs_displayed_case_ids(self, m): + def test_list_cases_logs_displayed_case_ids(self, m): self._setUpMocks(m) - self.app.get( - self.inner_url_open, user=self.user, headers={"HX-Request": "true"} - ) + self.app.get(self.inner_url, user=self.user, headers={"HX-Request": "true"}) # check access logs for displayed cases logs = list(TimelineLog.objects.all()) @@ -437,104 +477,13 @@ def test_list_open_cases_logs_displayed_case_ids(self, m): self.assertEqual(self.user, case_log[0].user) self.assertEqual(self.user, case_log[0].content_object) - # no logs for non-displayed cases + # no logs for internal, hence non-displayed cases for log in logs: - self.assertNotIn(self.zaak3["identificatie"], log.extra_data["message"]) - - def test_list_closed_cases(self, m): - self._setUpMocks(m) - - response = self.app.get( - self.inner_url_closed, user=self.user, headers={"HX-Request": "true"} - ) - - self.assertListEqual( - response.context.get("cases"), - [ - { - "uuid": self.zaak3["uuid"], - "start_date": datetime.date.fromisoformat(self.zaak3["startdatum"]), - "end_date": datetime.date.fromisoformat(self.zaak3["einddatum"]), - "identificatie": self.zaak3["identificatie"], - "description": self.zaaktype["omschrijving"], - "current_status": self.status_type2["omschrijving"], - "statustype_config": None, - }, - ], - ) - # don't show closed cases - for open_zaak in [self.zaak1, self.zaak2]: - self.assertNotContains(response, open_zaak["identificatie"]) - self.assertNotContains(response, open_zaak["omschrijving"]) - - # check zaken request query parameters - list_zaken_req = [ - req - for req in m.request_history - if req.hostname == "zaken.nl" and req.path == "/api/v1/zaken" - ][0] - self.assertEqual(len(list_zaken_req.qs), 2) - self.assertEqual( - list_zaken_req.qs, - { - "rol__betrokkeneidentificatie__natuurlijkpersoon__inpbsn": [ - self.user.bsn - ], - "maximalevertrouwelijkheidaanduiding": [ - VertrouwelijkheidsAanduidingen.beperkt_openbaar - ], - }, - ) - - def test_list_closed_cases_reformats_zaak_identificatie(self, m): - self._setUpMocks(m) - - with patch( - "open_inwoner.cms.cases.views.mixins.format_zaak_identificatie", - wraps=format_zaak_identificatie, - ) as spy_format: - self.app.get( - self.inner_url_closed, user=self.user, headers={"HX-Request": "true"} + self.assertNotIn( + self.zaak_intern["identificatie"], log.extra_data["message"] ) - spy_format.assert_called() - self.assertEqual(spy_format.call_count, 1) - - def test_list_closed_cases_translates_status(self, m): - st1 = StatusTranslationFactory( - status=self.status_type2["omschrijving"], - translation="Translated Status Type", - ) - self._setUpMocks(m) - response = self.app.get( - self.inner_url_closed, user=self.user, headers={"HX-Request": "true"} - ) - self.assertNotContains(response, st1.status) - self.assertContains(response, st1.translation) - - def test_list_closed_cases_logs_displayed_case_ids(self, m): - self._setUpMocks(m) - - self.app.get( - self.inner_url_closed, user=self.user, headers={"HX-Request": "true"} - ) - - # check access logs for displayed cases - logs = list(TimelineLog.objects.all()) - - case_log = [ - l for l in logs if self.zaak3["identificatie"] in l.extra_data["message"] - ] - self.assertEqual(len(case_log), 1) - self.assertEqual(self.user, case_log[0].user) - self.assertEqual(self.user, case_log[0].content_object) - - # no logs for non-displayed cases - for log in logs: - self.assertNotIn(self.zaak1["identificatie"], log.extra_data["message"]) - self.assertNotIn(self.zaak2["identificatie"], log.extra_data["message"]) - - @patch.object(CaseListMixin, "paginate_by", 1) + @patch.object(InnerCaseListView, "paginate_by", 1) def test_list_cases_paginated(self, m): """ show only one case and url to the next page @@ -543,7 +492,7 @@ def test_list_cases_paginated(self, m): # 1. test first page response_1 = self.app.get( - self.inner_url_open, user=self.user, headers={"HX-Request": "true"} + self.inner_url, user=self.user, headers={"HX-Request": "true"} ) self.assertListEqual( @@ -553,10 +502,11 @@ def test_list_cases_paginated(self, m): "uuid": self.zaak2["uuid"], "start_date": datetime.date.fromisoformat(self.zaak2["startdatum"]), "end_date": None, - "identificatie": self.zaak2["identificatie"], + "identification": self.zaak2["identificatie"], "description": self.zaaktype["omschrijving"], "current_status": self.status_type1["omschrijving"], "statustype_config": self.zt_statustype_config1, + "case_type": "Zaak", }, ], ) @@ -564,7 +514,7 @@ def test_list_cases_paginated(self, m): self.assertContains(response_1, "?page=2") # 2. test next page - next_page = f"{self.inner_url_open}?page=2" + next_page = f"{self.inner_url}?page=2" response_2 = self.app.get( next_page, user=self.user, headers={"HX-Request": "true"} ) @@ -576,59 +526,97 @@ def test_list_cases_paginated(self, m): "uuid": self.zaak1["uuid"], "start_date": datetime.date.fromisoformat(self.zaak1["startdatum"]), "end_date": None, - "identificatie": self.zaak1["identificatie"], + "identification": self.zaak1["identificatie"], "description": self.zaaktype["omschrijving"], "current_status": self.status_type1["omschrijving"], "statustype_config": self.zt_statustype_config1, + "case_type": "Zaak", }, ], ) self.assertNotContains(response_2, self.zaak2["identificatie"]) self.assertContains(response_2, "?page=1") - @patch.object(CaseListMixin, "paginate_by", 1) + @patch.object(InnerCaseListView, "paginate_by", 1) def test_list_cases_paginated_logs_displayed_case_ids(self, m): self._setUpMocks(m) - # 1. test first page - response = self.app.get( - self.inner_url_open, user=self.user, headers={"HX-Request": "true"} - ) - self.assertEqual(response.context.get("cases")[0]["uuid"], self.zaak2["uuid"]) - - # check access logs for displayed cases - logs = list(TimelineLog.objects.all()) + with self.subTest("first page"): + response = self.app.get( + self.inner_url, user=self.user, headers={"HX-Request": "true"} + ) + self.assertEqual( + response.context.get("cases")[0]["uuid"], self.zaak2["uuid"] + ) - case_log = [ - l for l in logs if self.zaak2["identificatie"] in l.extra_data["message"] - ] - self.assertEqual(len(case_log), 1) - self.assertEqual(self.user, case_log[0].user) - self.assertEqual(self.user, case_log[0].content_object) + self.assertTimelineLog(f"Zaken bekeken: {self.zaak2['identificatie']}") - # no logs for non-displayed cases - for log in logs: - self.assertNotIn(self.zaak1["identificatie"], log.extra_data["message"]) - self.assertNotIn(self.zaak3["identificatie"], log.extra_data["message"]) + with self.assertRaises(AssertionError): + self.assertTimelineLog( + self.zaak1["identificatie"], lookup=Lookups.icontains + ) + self.assertTimelineLog( + self.zaak3["identificatie"], lookup=Lookups.icontains + ) - # clear logs for testing TimelineLog.objects.all().delete() - # 2. test next page - next_page = f"{self.inner_url_open}?page=2" + with self.subTest("next page"): + next_page = f"{self.inner_url}?page=2" + response = self.app.get( + next_page, user=self.user, headers={"HX-Request": "true"} + ) + self.assertEqual( + response.context.get("cases")[0]["uuid"], self.zaak1["uuid"] + ) + + self.assertTimelineLog(f"Zaken bekeken: {self.zaak1['identificatie']}") + + with self.assertRaises(AssertionError): + self.assertTimelineLog( + self.zaak2["identificatie"], lookup=Lookups.icontains + ) + self.assertTimelineLog( + self.zaak3["identificatie"], lookup=Lookups.icontains + ) + + +@override_settings(ROOT_URLCONF="open_inwoner.cms.tests.urls") +class CaseSubmissionTest(WebTest): + inner_url = reverse_lazy("cases:cases_content") + + @classmethod + def setUpTestData(cls): + super().setUpTestData() + + cls.config = OpenZaakConfig.get_solo() + cls.config.form_service = ServiceFactory( + api_root=FORMS_ROOT, api_type=APITypes.orc + ) + cls.config.save() + + @requests_mock.Mocker() + def test_case_submission(self, m): + user = UserFactory( + login_type=LoginTypeChoices.digid, bsn="900222086", email="john@smith.nl" + ) + + data = ESuiteData().install_mocks(m) + response = self.app.get( - next_page, user=self.user, headers={"HX-Request": "true"} + self.inner_url, user=user, headers={"HX-Request": "true"} ) - self.assertEqual(response.context.get("cases")[0]["uuid"], self.zaak1["uuid"]) - # check access logs for displayed cases - logs = list(TimelineLog.objects.all()) - case_log = [ - l for l in logs if self.zaak1["identificatie"] in l.extra_data["message"] - ] - self.assertEqual(len(case_log), 1) + cases = response.context["cases"] - # no logs for non-displayed cases (after we cleared just above) - for log in logs: - self.assertNotIn(self.zaak2["identificatie"], log.extra_data["message"]) - self.assertNotIn(self.zaak3["identificatie"], log.extra_data["message"]) + self.assertEqual(len(cases), 2) + + # submission cases are sorted in reverse by `last modified` + self.assertEqual(cases[0]["url"], data.submission_2["url"]) + self.assertEqual(cases[0]["uuid"], data.submission_2["uuid"]) + self.assertEqual(cases[0]["naam"], data.submission_2["naam"]) + self.assertEqual(cases[0]["vervolg_link"], data.submission_2["vervolgLink"]) + self.assertEqual( + cases[0]["datum_laatste_wijziging"].strftime("%Y-%m-%dT%H:%M:%S.%f%z"), + data.submission_2["datumLaatsteWijziging"], + ) diff --git a/src/open_inwoner/openzaak/tests/test_cases_cache.py b/src/open_inwoner/openzaak/tests/test_cases_cache.py index 6ee80ad122..c8ff0d45bf 100644 --- a/src/open_inwoner/openzaak/tests/test_cases_cache.py +++ b/src/open_inwoner/openzaak/tests/test_cases_cache.py @@ -24,7 +24,7 @@ @requests_mock.Mocker() @override_settings(ROOT_URLCONF="open_inwoner.cms.tests.urls") class OpenCaseListCacheTests(ClearCachesMixin, WebTest): - inner_url = reverse_lazy("cases:open_cases_content") + inner_url = reverse_lazy("cases:cases_content") @classmethod def setUpTestData(cls): @@ -301,18 +301,3 @@ def test_cached_statuses_in_combination_with_new_ones(self, m): self.assertIsNotNone(cache.get(f"status:{self.new_status['url']}")) self.assertIsNotNone(cache.get(f"status:{self.status1['url']}")) self.assertIsNotNone(cache.get(f"status:{self.status2['url']}")) - - -class ClosedCaseListCacheTests(OpenCaseListCacheTests): - """ - run the same tests as for open cases - """ - - inner_url = reverse_lazy("cases:closed_cases_content") - - @classmethod - def setUpTestData(cls): - super().setUpTestData() - - for zaak in [cls.zaak1, cls.zaak2, cls.new_zaak]: - zaak["einddatum"] = "2022-01-16" diff --git a/src/open_inwoner/openzaak/tests/test_formapi.py b/src/open_inwoner/openzaak/tests/test_formapi.py deleted file mode 100644 index fda1f53eec..0000000000 --- a/src/open_inwoner/openzaak/tests/test_formapi.py +++ /dev/null @@ -1,115 +0,0 @@ -from django.test.utils import override_settings -from django.urls import reverse -from django.utils.translation import gettext_lazy as _ - -import requests_mock -from django_webtest import WebTest -from zgw_consumers.constants import APITypes -from zgw_consumers.test import mock_service_oas_get - -from open_inwoner.accounts.tests.factories import UserFactory -from open_inwoner.openzaak.formapi import fetch_open_submissions -from open_inwoner.openzaak.models import OpenZaakConfig -from open_inwoner.openzaak.tests.factories import ServiceFactory -from open_inwoner.openzaak.tests.shared import FORMS_ROOT -from open_inwoner.utils.test import ClearCachesMixin -from open_inwoner.utils.tests.helpers import AssertRedirectsMixin - - -class ESuiteData: - def __init__(self): - self.submission_1 = { - "url": "https://dmidoffice2.esuite-development.net/formulieren-provider/api/v1/8e3ae29c-7bc5-4f7d-a27c-b0c83c13328e", - "uuid": "8e3ae29c-7bc5-4f7d-a27c-b0c83c13328e", - "naam": "Melding openbare ruimte", - "vervolgLink": "https://dloket2.esuite-development.net/formulieren-nieuw/formulier/start/8e3ae29c-7bc5-4f7d-a27c-b0c83c13328e", - "datumLaatsteWijziging": "2023-02-13T14:02:00.999+01:00", - "eindDatumGeldigheid": "2023-05-14T14:02:00.999+02:00", - } - self.submission_2 = { - "url": "https://dmidoffice2.esuite-development.net/formulieren-provider/api/v1/d14658b0-dcb4-4d3c-a61c-fd7d0c78f296", - "uuid": "d14658b0-dcb4-4d3c-a61c-fd7d0c78f296", - "naam": "Indienen bezwaarschrift", - "vervolgLink": "https://dloket2.esuite-development.net/formulieren-nieuw/formulier/start/d14658b0-dcb4-4d3c-a61c-fd7d0c78f296", - "datumLaatsteWijziging": "2023-02-13T14:10:26.197+01:00", - "eindDatumGeldigheid": "2023-05-14T14:10:26.197+02:00", - } - # note this is a weird esuite response without pagination links - self.response = { - "count": 2, - "results": [ - self.submission_1, - self.submission_2, - ], - } - - def setUpOASMocks(self, m): - mock_service_oas_get(m, FORMS_ROOT, "submissions-esuite") - - def install_mocks(self, m): - self.setUpOASMocks(m) - m.get( - f"{FORMS_ROOT}openstaande-inzendingen", - json=self.response, - ) - return self - - -@requests_mock.Mocker() -@override_settings(ROOT_URLCONF="open_inwoner.cms.tests.urls") -class FormAPITest(AssertRedirectsMixin, ClearCachesMixin, WebTest): - config: OpenZaakConfig - - @classmethod - def setUpTestData(cls): - super().setUpTestData() - - cls.config = OpenZaakConfig.get_solo() - cls.config.form_service = ServiceFactory( - api_root=FORMS_ROOT, api_type=APITypes.orc - ) - cls.config.save() - - cls.user = UserFactory(bsn="900222086") - cls.outer_submissions_url = reverse("cases:open_submissions") - cls.inner_submissions_url = reverse("cases:open_submissions_content") - - def test_api_fetch(self, m): - data = ESuiteData().install_mocks(m) - - res = fetch_open_submissions(self.user.bsn) - - self.assertEqual(len(res), 2) - self.assertEqual(res[0].url, data.submission_1["url"]) - - def test_page_shows_open_submissions(self, m): - data = ESuiteData().install_mocks(m) - - response = self.app.get( - self.inner_submissions_url, user=self.user, headers={"HX-Request": "true"} - ) - - self.assertContains(response, data.submission_1["naam"]) - self.assertContains(response, data.submission_1["vervolgLink"]) - - self.assertContains(response, data.submission_2["naam"]) - self.assertContains(response, data.submission_2["vervolgLink"]) - - def test_page_shows_zero_submissions(self, m): - data = ESuiteData() - data.response["results"] = [] - data.response["count"] = 0 - data.install_mocks(m) - - response = self.app.get( - self.inner_submissions_url, user=self.user, headers={"HX-Request": "true"} - ) - - self.assertContains(response, _("Er zijn geen open aanvragen.")) - - def test_requires_auth(self, m): - response = self.app.get(self.outer_submissions_url) - self.assertRedirectsLogin(response, next=self.outer_submissions_url) - - def test_requires_bsn(self, m): - self.app.get(self.outer_submissions_url, user=UserFactory(bsn=""), status=403) diff --git a/src/open_inwoner/openzaak/tests/test_notification_utils.py b/src/open_inwoner/openzaak/tests/test_notification_utils.py index b8ef621259..f628f32642 100644 --- a/src/open_inwoner/openzaak/tests/test_notification_utils.py +++ b/src/open_inwoner/openzaak/tests/test_notification_utils.py @@ -1,4 +1,4 @@ -from unittest.mock import patch +from unittest import mock from django.core import mail from django.test import TestCase @@ -19,17 +19,12 @@ from open_inwoner.openzaak.tests.factories import generate_rol from ..api_models import Zaak, ZaakType -from ..utils import format_zaak_identificatie from .test_notification_data import MockAPIData @override_settings(ROOT_URLCONF="open_inwoner.cms.tests.urls") class NotificationHandlerUtilsTestCase(TestCase): - @patch( - "open_inwoner.openzaak.notifications.format_zaak_identificatie", - wraps=format_zaak_identificatie, - ) - def test_send_case_update_email(self, spy_format): + def test_send_case_update_email(self): config = SiteConfiguration.get_solo() data = MockAPIData() @@ -40,9 +35,16 @@ def test_send_case_update_email(self, spy_format): case_url = reverse("cases:case_detail", kwargs={"object_id": str(case.uuid)}) - send_case_update_email(user, case) + # mock `_format_zaak_identificatie`, but then continue with result of actual call + # (test redirect for invalid BSN that passes pattern validation) + ret_val = case._format_zaak_identificatie() + with mock.patch.object( + Zaak, "_format_zaak_identificatie" + ) as format_identificatie: + format_identificatie.return_value = ret_val + send_case_update_email(user, case) - spy_format.assert_called_once() + format_identificatie.assert_called_once() self.assertEqual(len(mail.outbox), 1) email = mail.outbox[0] diff --git a/src/open_inwoner/openzaak/tests/test_utils.py b/src/open_inwoner/openzaak/tests/test_utils.py index 22bbf6397f..96274edc56 100644 --- a/src/open_inwoner/openzaak/tests/test_utils.py +++ b/src/open_inwoner/openzaak/tests/test_utils.py @@ -8,11 +8,9 @@ from open_inwoner.openzaak.models import OpenZaakConfig from open_inwoner.openzaak.tests.factories import generate_rol from open_inwoner.openzaak.utils import ( - format_zaak_identificatie, get_role_name_display, is_info_object_visible, is_zaak_visible, - reformat_esuite_zaak_identificatie, ) from ...utils.test import ClearCachesMixin @@ -259,37 +257,6 @@ def test_get_role_name_display(self): expected = "Bazz, Foo van der" self.assertEqual(expected, get_role_name_display(role)) - def test_format_zaak_identificatie(self): - config = OpenZaakConfig.get_solo() - value = "0014ESUITE66392022" - - with self.subTest("enabled"): - config.reformat_esuite_zaak_identificatie = True - config.save() - actual = format_zaak_identificatie(value, config) - self.assertEqual(actual, "6639-2022") - - with self.subTest("disabled"): - config.reformat_esuite_zaak_identificatie = False - config.save() - actual = format_zaak_identificatie(value, config) - # no change - self.assertEqual(actual, value) - - def test_reformat_esuite_zaak_identificatie(self): - tests = [ - ("0014ESUITE66392022", "6639-2022"), - ("4321ESUITE00011991", "0001-1991"), - ("4321ESUITE123456781991", "12345678-1991"), - ("12345678", "12345678"), - ("aaaaaa1234", "aaaaaa1234"), - ] - - for value, expected in tests: - with self.subTest(value=value, expected=expected): - actual = reformat_esuite_zaak_identificatie(value) - self.assertEqual(actual, expected) - class TestHelpers(TestCase): def test_copy_with_new_uuid(self): diff --git a/src/open_inwoner/openzaak/types.py b/src/open_inwoner/openzaak/types.py new file mode 100644 index 0000000000..ccecb11a7a --- /dev/null +++ b/src/open_inwoner/openzaak/types.py @@ -0,0 +1,24 @@ +from typing import Protocol + + +class UniformCase(Protocol): + """ + Zaken and open submissions are classified as "cases" if they have an + `identification` property and a method `process_data` to prepare data + for the template + """ + + @property + def identification(self) -> str: + ... + + def process_data(self) -> dict: + """ + Prepare data for template + + Should include (at least) the following: + - identification (str) + - uuid (str) + - case_type (str) + """ + ... diff --git a/src/open_inwoner/openzaak/utils.py b/src/open_inwoner/openzaak/utils.py index 9bd3c4f83b..5b3626ca85 100644 --- a/src/open_inwoner/openzaak/utils.py +++ b/src/open_inwoner/openzaak/utils.py @@ -1,12 +1,7 @@ -import inspect import logging -import re -from functools import wraps -from typing import Callable, Optional, TypeVar, Union +from typing import Optional, Union from uuid import UUID -from django.core.cache import caches - from zds_client import get_operation_url from zgw_consumers.api_models.constants import RolTypes, VertrouwelijkheidsAanduidingen @@ -113,82 +108,6 @@ def join(*values): return display -RT = TypeVar("RT") - - -def cache(key: str, alias: str = "default", **set_options): - """ - Function-decorator for updating the django low-level cache. - - It determines if the key exists in cache and skips it by calling the decorated function - or creates it if doesn't exist. - - We can pass a keyword argument for the time we want the cache the data in - seconds (timeout=60). - """ - - def decorator(func: Callable[..., RT]) -> Callable[..., RT]: - argspec = inspect.getfullargspec(func) - - if argspec.defaults: - positional_count = len(argspec.args) - len(argspec.defaults) - defaults = dict(zip(argspec.args[positional_count:], argspec.defaults)) - else: - defaults = {} - - @wraps(func) - def wrapped(*args, **kwargs) -> RT: - skip_cache = kwargs.pop("skip_cache", False) - if skip_cache: - return func(*args, **kwargs) - - key_kwargs = defaults.copy() - named_args = dict(zip(argspec.args, args), **kwargs) - key_kwargs.update(**named_args) - - if argspec.varkw: - var_kwargs = { - key: value - for key, value in named_args.items() - if key not in argspec.args - } - key_kwargs[argspec.varkw] = var_kwargs - - cache_key = key.format(**key_kwargs) - - _cache = caches[alias] - result = _cache.get(cache_key) - - # The key exists in cache so we return the already cached data - if result is not None: - logger.debug("Cache key '%s' hit", cache_key) - return result - - # The key does not exist so we call the decorated function and set the cache - result = func(*args, **kwargs) - _cache.set(cache_key, result, **set_options) - - return result - - return wrapped - - return decorator - - -def get_retrieve_resource_by_uuid_url( - client, resource: str, uuid: Union[str, UUID] -) -> str: - op_suffix = client.operation_suffix_mapping["retrieve"] - operation_id = f"{resource}{op_suffix}" - path_kwargs = { - "uuid": uuid, - } - url = get_operation_url( - client.schema, operation_id, base_url=client.base_url, **path_kwargs - ) - return url - - def get_zaak_type_config(case_type: ZaakType) -> Optional[ZaakTypeConfig]: try: return ZaakTypeConfig.objects.filter_case_type(case_type).get() @@ -209,24 +128,15 @@ def get_zaak_type_info_object_type_config( return None -def format_zaak_identificatie( - identificatie: str, config: Optional[OpenZaakConfig] = None -): - config = config or OpenZaakConfig.get_solo() - if config.reformat_esuite_zaak_identificatie: - return reformat_esuite_zaak_identificatie(identificatie) - else: - return identificatie - - -def reformat_esuite_zaak_identificatie(identificatie: str): - """ - 0014ESUITE66392022 -> 6639-2022 - """ - exp = r"^\d+ESUITE(?P\d+?)(?P\d{4})$" - m = re.match(exp, identificatie) - if not m: - return identificatie - num = m.group("num") - year = m.group("year") - return f"{num}-{year}" +def get_retrieve_resource_by_uuid_url( + client, resource: str, uuid: Union[str, UUID] +) -> str: + op_suffix = client.operation_suffix_mapping["retrieve"] + operation_id = f"{resource}{op_suffix}" + path_kwargs = { + "uuid": uuid, + } + url = get_operation_url( + client.schema, operation_id, base_url=client.base_url, **path_kwargs + ) + return url diff --git a/src/open_inwoner/scss/components/Cases/Cases.scss b/src/open_inwoner/scss/components/Cases/Cases.scss index 1c6c777830..9b87137998 100644 --- a/src/open_inwoner/scss/components/Cases/Cases.scss +++ b/src/open_inwoner/scss/components/Cases/Cases.scss @@ -11,6 +11,11 @@ a:hover { cursor: pointer; } + + &__title_text { + padding-top: var(--spacing-medium); + padding-bottom: var(--spacing-extra-large); + } } #document-upload { diff --git a/src/open_inwoner/scss/components/Notification/_Notification.scss b/src/open_inwoner/scss/components/Notification/_Notification.scss index f77ab4fac9..b492c42f74 100644 --- a/src/open_inwoner/scss/components/Notification/_Notification.scss +++ b/src/open_inwoner/scss/components/Notification/_Notification.scss @@ -49,10 +49,6 @@ width: 24px; } - &__content { - width: 100%; - } - &__close { position: absolute; top: var(--spacing-small); @@ -78,4 +74,22 @@ @media (min-width: 768px) { margin: var(--spacing-tiny) 0; } + + // Overrides based on context + &--cases { + padding: var(--spacing-small); + margin-bottom: var(--spacing-medium); + align-items: center; + + .notification__icon { + display: flex; + padding-left: var(--spacing-medium); + align-items: center; + } + + .notification__close { + top: 0; + position: inherit; + } + } } diff --git a/src/open_inwoner/templates/pages/cases/list_inner.html b/src/open_inwoner/templates/pages/cases/list_inner.html index 5178cd1932..a1ff3a9d77 100644 --- a/src/open_inwoner/templates/pages/cases/list_inner.html +++ b/src/open_inwoner/templates/pages/cases/list_inner.html @@ -1,10 +1,9 @@ -{% load i18n anchor_menu_tags grid_tags icon_tags list_tags pagination_tags utils %} - -
- {% anchor_menu anchors desktop=True %} -
+{% load link_tags button_tags i18n grid_tags icon_tags list_tags pagination_tags utils %}

{% trans "Mijn aanvragen" %}

+

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

+

{{ title_text }}

+ {% render_grid %} {% if not cases %} {% trans "U heeft op dit moment nog geen lopende aanvragen." %} @@ -15,23 +14,45 @@

{% trans "Mijn aanvragen" %}

{% include "components/StatusIndicator/StatusIndicator.html" with status_indicator=case.statustype_config.status_indicator status_indicator_text=case.statustype_config.status_indicator_text %}
{% endrender_column %} diff --git a/src/open_inwoner/templates/pages/cases/list_outer.html b/src/open_inwoner/templates/pages/cases/list_outer.html index 631a56ec41..df470dd08b 100644 --- a/src/open_inwoner/templates/pages/cases/list_outer.html +++ b/src/open_inwoner/templates/pages/cases/list_outer.html @@ -1,10 +1,6 @@ {% extends 'master.html' %} {% load i18n icon_tags %} -{% block sidebar_content %} -
-{% endblock sidebar_content %} - {% block content %}
diff --git a/src/open_inwoner/templates/pages/cases/submissions_inner.html b/src/open_inwoner/templates/pages/cases/submissions_inner.html deleted file mode 100644 index 6ca42d5fa7..0000000000 --- a/src/open_inwoner/templates/pages/cases/submissions_inner.html +++ /dev/null @@ -1,36 +0,0 @@ -{% load i18n anchor_menu_tags button_tags link_tags %} - -
- {% anchor_menu anchors=anchors desktop=False %} -
- -
- {% anchor_menu anchors desktop=True %} -
- -

{{ page_title }} ({{ submissions|length }})

- - - - - - - - - - - - {% for submission in submissions %} - - - - - - - {% empty %} - - - - {% endfor %} - -
{% trans "Formulier" %}{% trans "Laatste wijziging" %}{% trans "Einde geldigheid" %} 
{% link href=submission.vervolg_link text=submission.naam blank=True %}{{submission.datum_laatste_wijziging|date:"d-m-Y"}}{{submission.eind_datum_geldigheid|date:"d-m-Y"}}{% button text=submission.naam hide_text=True href=submission.vervolg_link icon="arrow_forward" secondary=True icon_outlined=True transparent=True %}
{% trans "Er zijn geen open aanvragen." %}
diff --git a/src/open_inwoner/templates/pages/cases/submissions_outer.html b/src/open_inwoner/templates/pages/cases/submissions_outer.html deleted file mode 100644 index 5e1598de74..0000000000 --- a/src/open_inwoner/templates/pages/cases/submissions_outer.html +++ /dev/null @@ -1,21 +0,0 @@ -{% extends 'master.html' %} -{% load i18n icon_tags %} - -
- -{% block sidebar_content %} -
-{% endblock sidebar_content %} - -{% block content %} -

{% trans "Mijn aanvragen" %}

- -
- -
-
- {% icon icon="rotate_right" extra_classes="spinner-icon rotate" %} -
{% trans "Gegevens laden..." %}
-
-
-{% endblock content %} diff --git a/src/open_inwoner/urls.py b/src/open_inwoner/urls.py index 6397bde269..f8fda5df8c 100644 --- a/src/open_inwoner/urls.py +++ b/src/open_inwoner/urls.py @@ -110,7 +110,6 @@ path("apimock/", include("open_inwoner.apimock.urls")), # TODO move search to products cms app? path("", include("open_inwoner.search.urls", namespace="search")), - # path("uitkeringen/", include("open_inwoner.cms.ssd.urls", namespace="ssd")), re_path(r"^", include("cms.urls")), ] diff --git a/src/open_inwoner/utils/decorators.py b/src/open_inwoner/utils/decorators.py new file mode 100644 index 0000000000..83717ef9cb --- /dev/null +++ b/src/open_inwoner/utils/decorators.py @@ -0,0 +1,70 @@ +import inspect +import logging +from functools import wraps +from typing import Callable, TypeVar + +from django.core.cache import caches + +logger = logging.getLogger(__name__) + + +RT = TypeVar("RT") + + +def cache(key: str, alias: str = "default", **set_options): + """ + Decorator factory for updating the django low-level cache. + + It determines if the key exists in cache and skips it by calling the decorated function + or creates it if doesn't exist. + + We can pass a keyword argument for the time we want the cache the data in + seconds (timeout=60). + """ + + def decorator(func: Callable[..., RT]) -> Callable[..., RT]: + argspec = inspect.getfullargspec(func) + + if argspec.defaults: + positional_count = len(argspec.args) - len(argspec.defaults) + defaults = dict(zip(argspec.args[positional_count:], argspec.defaults)) + else: + defaults = {} + + @wraps(func) + def wrapped(*args, **kwargs) -> RT: + skip_cache = kwargs.pop("skip_cache", False) + if skip_cache: + return func(*args, **kwargs) + + key_kwargs = defaults.copy() + named_args = dict(zip(argspec.args, args), **kwargs) + key_kwargs.update(**named_args) + + if argspec.varkw: + var_kwargs = { + key: value + for key, value in named_args.items() + if key not in argspec.args + } + key_kwargs[argspec.varkw] = var_kwargs + + cache_key = key.format(**key_kwargs) + + _cache = caches[alias] + result = _cache.get(cache_key) + + # The key exists in cache so we return the already cached data + if result is not None: + logger.debug("Cache key '%s' hit", cache_key) + return result + + # The key does not exist so we call the decorated function and set the cache + result = func(*args, **kwargs) + _cache.set(cache_key, result, **set_options) + + return result + + return wrapped + + return decorator diff --git a/src/open_inwoner/utils/views.py b/src/open_inwoner/utils/views.py index 4cebb559bb..e090b6e815 100644 --- a/src/open_inwoner/utils/views.py +++ b/src/open_inwoner/utils/views.py @@ -88,8 +88,7 @@ def server_error(request, template_name=ERROR_500_TEMPLATE_NAME): class LogMixin(object): """ - Class based views mixin that adds simple wrappers to - the functions above. + CBV mixin that adds simple wrappers to logging functions """ def log_addition(self, instance, message=""):