Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#1799] Updated cases CMS plugin to run async with htmx #828

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 26 additions & 4 deletions src/open_inwoner/cms/cases/cms_plugins.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from django.core.exceptions import BadRequest
from django.shortcuts import render
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _

from cms.plugin_base import CMSPluginBase
Expand Down Expand Up @@ -25,27 +28,46 @@ class CasesPlugin(CMSActiveAppMixin, CMSPluginBase):
def render(self, context, instance, placeholder):
request = context["request"]
user = request.user
if check_user_auth(user, digid_required=True):
context["hxget"] = reverse("cases:cases_plugin_content")
return context

@classmethod
def render_htmx_content(cls, request):
user = request.user
context = dict()
context["page_title"] = "Page title"
context["title_text"] = "Title text"

if not check_user_auth(user, digid_required=True):
context["cases"] = None
return context

raw_cases = [case for case in fetch_cases(user.bsn) if not case.einddatum]

if not all(check_user_access_rights(user, case.url) for case in raw_cases):
context["cases"] = None
return context

# TODO
# preprocessed_cases = preprocess_data(raw_cases)

subs = fetch_open_submissions(request.user.bsn)

# replce raw_cases with preprocessed_cases
# replace raw_cases with preprocessed_cases
all_cases = raw_cases + subs

# processed_cases = [case.preprocess_data() for case in all_cases]

context["cases"] = all_cases[: self.limit]
context["cases"] = all_cases[: cls.limit]

return context

@classmethod
def as_htmx_view(cls):
# this could be generalized to a mixin
def _view(request):
if not request.htmx:
raise BadRequest("requires htmx")
context = cls.render_htmx_content(request)
return render(request, cls.render_template, context)

return _view
76 changes: 58 additions & 18 deletions src/open_inwoner/cms/cases/tests/test_plugin_cases.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from unittest.mock import patch

from django.template.loader import render_to_string
from django.test import TestCase, override_settings
from django.urls import reverse

import lxml
import requests_mock
from pyquery import PyQuery as pq

Expand All @@ -12,10 +13,20 @@
from open_inwoner.utils.test import ClearCachesMixin

from ...tests import cms_tools
from ...tests.cms_tools import get_request
from ..cms_apps import CasesApphook
from ..cms_plugins import CasesPlugin


def render_htmx_plugin(plugin_class, *, user=None) -> tuple[str, dict]:
# this could be moved to cmstools when re-used
request = get_request(user=user, htmx=True)
context = plugin_class.render_content(request)
html = render_to_string(plugin_class.render_template, context, request=request)
html = html.strip()
return html, context


@requests_mock.Mocker()
@patch.object(CasesPlugin, "limit", 2)
@override_settings(ROOT_URLCONF="open_inwoner.cms.tests.urls")
Expand All @@ -31,13 +42,8 @@ def test_cms_plugin_cases_not_rendered_for_anonymous_user(self, m):
# anonymous user
html, context = cms_tools.render_plugin(CasesPlugin)

self.assertIsNone(context["cases"])

# check that html empty
with self.assertRaises(lxml.etree.ParserError) as ctx:
pq(html)

self.assertEqual(str(ctx.exception), "Document is empty")
self.assertIsNone(context)
self.assertEqual(html, "")

def test_cms_plugin_cases_not_rendered_for_non_digid_user(self, m):
self.setUpMocks(m)
Expand All @@ -48,20 +54,25 @@ def test_cms_plugin_cases_not_rendered_for_non_digid_user(self, m):

html, context = cms_tools.render_plugin(CasesPlugin, user=user)

self.assertIsNone(context["cases"])
self.assertIsNone(context)
self.assertEqual(html, "")

# check that html empty
with self.assertRaises(lxml.etree.ParserError) as ctx:
pq(html)
def test_cms_plugin_renders_htmx_trigger(self, m):
self.setUpMocks(m)
html, context = cms_tools.render_plugin(CasesPlugin, user=self.user)

self.assertEqual(str(ctx.exception), "Document is empty")
self.assertEqual(context["hxget"], reverse("cases:cases_plugin_content"))

def test_cms_plugin_cases_rendered(self, m):
doc = pq(html)
trigger = doc.find("#spinner")
self.assertEqual(trigger.attr["hx-get"], reverse("cases:cases_plugin_content"))
self.assertEqual(trigger.attr["hx-trigger"], "load")

def test_cms_plugin_view_renders_cases(self, m):
self.setUpMocks(m)
self.setUpMocksExtra(m) # create additional zaken
self.setUpMocksExtra(m)

# the ZakenTestMixin user is a digid user
html, context = cms_tools.render_plugin(CasesPlugin, user=self.user)
html, context = render_htmx_plugin(CasesPlugin, user=self.user)

cases = context["cases"]

Expand All @@ -83,4 +94,33 @@ def test_cms_plugin_cases_rendered(self, m):
html_case_links = doc.find("a")

for html_link, path in zip(html_case_links, case_link_paths):
self.assertEqual(html_link.attrib["href"], f"/cases/{path}/status/")
# TODO reverse
self.assertEqual(html_link.attrib["href"], rf"/cases/{path}/status/")

def test_cms_plugin_view_requires_digid_and_htmx(self, m):
self.setUpMocks(m)
self.setUpMocksExtra(m)

url = reverse("cases:cases_plugin_content")

with self.subTest("anonymous"):
response = self.client.get(url)
self.assertEqual(response.status_code, 400)

with self.subTest("not bsn"):
user = UserFactory()
user.login_type = LoginTypeChoices.default
user.save()
self.client.force_login(user)
response = self.client.get(url)
self.assertEqual(response.status_code, 400)

with self.subTest("digid"):
self.client.force_login(self.user)
response = self.client.get(url)
self.assertEqual(response.status_code, 400)

with self.subTest("digid and htmx"):
self.client.force_login(self.user)
response = self.client.get(url, HTTP_HX_REQUEST="true")
self.assertEqual(response.status_code, 200)
6 changes: 6 additions & 0 deletions src/open_inwoner/cms/cases/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
KlantContactMomentListView,
)

from .cms_plugins import CasesPlugin
from .views import (
CaseContactFormView,
CaseDocumentDownloadView,
Expand All @@ -23,6 +24,11 @@
app_name = "cases"

urlpatterns = [
path(
"plugins/cases/",
CasesPlugin.as_htmx_view(),
name="cases_plugin_content",
),
path(
"closed/content/",
InnerClosedCaseListView.as_view(),
Expand Down
15 changes: 13 additions & 2 deletions src/open_inwoner/cms/tests/cms_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from cms.app_base import CMSApp
from cms.models import Placeholder
from cms.plugin_rendering import ContentRenderer
from django_htmx.middleware import HtmxDetails

from open_inwoner.cms.extensions.models import CommonExtension

Expand Down Expand Up @@ -43,12 +44,19 @@ def _init_plugin(plugin_class, plugin_data=None) -> Tuple[dict, str]:
return model_instance


def get_request(*, user=None):
request = RequestFactory()
def get_request(*, user=None, htmx=False):
headers = {}
if htmx:
headers["HX-Request"] = "true"
request = RequestFactory().get("/", headers=headers)
if user:
request.user = user
else:
request.user = AnonymousUser()
if htmx:
request.htmx = HtmxDetails(request)
else:
request.htmx = None
return request


Expand All @@ -63,6 +71,9 @@ def render_plugin(plugin_class, plugin_data=None, *, user=None) -> Tuple[str, di
renderer = ContentRenderer(request=request)
html = renderer.render_plugin(model_instance, context)

# clean up newlines from template that decides to render nothing
html = html.strip()

# let's check for output
if html:
plugin_instance = model_instance.get_plugin_class_instance()
Expand Down
2 changes: 1 addition & 1 deletion src/open_inwoner/cms/utils/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ def check_user_auth(user, digid_required: bool = False) -> bool:

def check_user_access_rights(user, case_url) -> bool:
if not fetch_roles_for_case_and_bsn(case_url, user.bsn):
f"Permission denied: no role for the case {case_url}"
logger.debug(f"Permission denied: no role for the case {case_url}")
return False
return True
9 changes: 6 additions & 3 deletions src/open_inwoner/templates/cms/cases/cases_plugin.html
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
{% load i18n grid_tags icon_tags list_tags pagination_tags utils %}

{% if cases %}
{% if hxget %}
<div class="spinner" id="spinner-id" hx-get="{{ hxget }}" hx-trigger="load">
{% icon icon="rotate_right" extra_classes="spinner-icon rotate" %}
<div class="spinner__content">{% trans "Gegevens laden..." %}</div>
</div>
{% elif cases %}
<h2 class="h2" id="cases">{{ page_title }} ({{ paginator.count }})</h2>
<p class="cases__title_text">{{ title_text }}</p>
{% render_grid %}

{% for case in cases %}
{% render_column start=forloop.counter_0|multiply:4 span=4 %}
<div class="card card--compact card--stretch">
Expand Down
Loading