diff --git a/src/open_inwoner/cms/products/cms_plugins.py b/src/open_inwoner/cms/products/cms_plugins.py index 9b423f0aa1..a5d8686ee0 100644 --- a/src/open_inwoner/cms/products/cms_plugins.py +++ b/src/open_inwoner/cms/products/cms_plugins.py @@ -29,7 +29,10 @@ def render(self, context, instance, placeholder): # Highlighted categories highlighted_categories = ( - Category.objects.published().filter(highlighted=True).order_by("path") + Category.objects.published() + .visible_for_user(request.user) + .filter(highlighted=True) + .order_by("path") ) if request.user.is_authenticated and request.user.selected_categories.exists(): categories = request.user.selected_categories.order_by("name")[: self.limit] diff --git a/src/open_inwoner/cms/products/tests/test_plugin_categories.py b/src/open_inwoner/cms/products/tests/test_plugin_categories.py index 752f6a3984..3a36ad0f74 100644 --- a/src/open_inwoner/cms/products/tests/test_plugin_categories.py +++ b/src/open_inwoner/cms/products/tests/test_plugin_categories.py @@ -23,14 +23,35 @@ def setUp(self): def test_only_highlighted_categories_exist_in_context_when_they_exist(self): CategoryFactory(name="Should be first") - highlighted_category = CategoryFactory( - name="This should be second", highlighted=True + highlighted_category1 = CategoryFactory( + name="This should be second", + highlighted=True, + visible_for_anonymous=True, + visible_for_authenticated=True, + ) + highlighted_category2 = CategoryFactory( + path="0002", + highlighted=True, + visible_for_anonymous=True, + visible_for_authenticated=False, + ) + highlighted_category3 = CategoryFactory( + path="0003", + highlighted=True, + visible_for_anonymous=False, + visible_for_authenticated=True, + ) + highlighted_category4 = CategoryFactory( + path="0004", + highlighted=True, + visible_for_anonymous=False, + visible_for_authenticated=False, ) html, context = cms_tools.render_plugin(CategoriesPlugin) self.assertEqual( list(context["categories"]), - [highlighted_category], + [highlighted_category1, highlighted_category2], ) def test_highlighted_categories_are_ordered_by_alphabetically(self): @@ -49,15 +70,36 @@ def test_highlighted_categories_are_ordered_by_alphabetically(self): def test_only_highlighted_categories_are_shown_when_they_exist(self): user = UserFactory() category = CategoryFactory(name="Should be first") - highlighted_category = CategoryFactory( - name="This should be second", highlighted=True + highlighted_category1 = CategoryFactory( + name="This should be second", + highlighted=True, + visible_for_anonymous=True, + visible_for_authenticated=True, + ) + highlighted_category2 = CategoryFactory( + path="0002", + highlighted=True, + visible_for_anonymous=True, + visible_for_authenticated=False, + ) + highlighted_category3 = CategoryFactory( + path="0003", + highlighted=True, + visible_for_anonymous=False, + visible_for_authenticated=True, + ) + highlighted_category4 = CategoryFactory( + path="0004", + highlighted=True, + visible_for_anonymous=False, + visible_for_authenticated=False, ) html, context = cms_tools.render_plugin(CategoriesPlugin, user=user) self.assertEqual( list(context["categories"]), - [highlighted_category], + [highlighted_category1, highlighted_category3], ) def test_category_selected(self): diff --git a/src/open_inwoner/components/tests/test_header.py b/src/open_inwoner/components/tests/test_header.py index 1c1f2048a8..28bb5f7e21 100644 --- a/src/open_inwoner/components/tests/test_header.py +++ b/src/open_inwoner/components/tests/test_header.py @@ -23,10 +23,32 @@ def setUpTestData(cls): # PrimaryNavigation.html requires apphook + categories create_apphook_page(ProductsApphook) cls.published1 = CategoryFactory( - path="0001", name="First one", slug="first-one" + path="0001", + name="First one", + slug="first-one", + visible_for_anonymous=True, + visible_for_authenticated=True, ) cls.published2 = CategoryFactory( - path="0002", name="Second one", slug="second-one" + path="0002", + name="Second one", + slug="second-one", + visible_for_anonymous=True, + visible_for_authenticated=False, + ) + cls.published3 = CategoryFactory( + path="0003", + name="Third one", + slug="third-one", + visible_for_anonymous=False, + visible_for_authenticated=True, + ) + cls.published4 = CategoryFactory( + path="0004", + name="Fourth one", + slug="fourth-one", + visible_for_anonymous=False, + visible_for_authenticated=False, ) def test_categories_hidden_from_anonymous_users(self): @@ -55,6 +77,37 @@ def test_categories_not_hidden_from_anonymous_users(self): self.assertEqual(categories[0].tag, "a") self.assertEqual(categories[1].tag, "button") + links = [x for x in doc.find("[title='Onderwerpen'] + ul li a").items()] + self.assertEqual(len(links), 4) + self.assertEqual(links[0].attr("href"), self.published1.get_absolute_url()) + self.assertEqual(links[1].attr("href"), self.published2.get_absolute_url()) + self.assertEqual(links[2].attr("href"), self.published1.get_absolute_url()) + self.assertEqual(links[3].attr("href"), self.published2.get_absolute_url()) + + def test_categories_visibility_for_authenticated_users(self): + config = SiteConfiguration.get_solo() + config.hide_categories_from_anonymous_users = False + config.save() + + self.client.force_login(self.user) + + response = self.client.get("/", user=self.user) + + doc = PyQuery(response.content) + + categories = doc.find("[title='Onderwerpen']") + + self.assertEqual(len(categories), 2) + self.assertEqual(categories[0].tag, "a") + self.assertEqual(categories[1].tag, "button") + + links = [x for x in doc.find("[title='Onderwerpen'] + ul li a").items()] + self.assertEqual(len(links), 4) + self.assertEqual(links[0].attr("href"), self.published1.get_absolute_url()) + self.assertEqual(links[1].attr("href"), self.published3.get_absolute_url()) + self.assertEqual(links[2].attr("href"), self.published1.get_absolute_url()) + self.assertEqual(links[3].attr("href"), self.published3.get_absolute_url()) + def test_search_bar_hidden_from_anonymous_users(self): config = SiteConfiguration.get_solo() config.hide_search_from_anonymous_users = True diff --git a/src/open_inwoner/pdc/admin/category.py b/src/open_inwoner/pdc/admin/category.py index 1f3db2675d..3ca7f9b0df 100644 --- a/src/open_inwoner/pdc/admin/category.py +++ b/src/open_inwoner/pdc/admin/category.py @@ -3,6 +3,8 @@ from django.forms import BaseModelFormSet from django.utils.translation import gettext as _ +from django_better_admin_arrayfield.admin.mixins import DynamicArrayMixin +from django_better_admin_arrayfield.forms.widgets import DynamicArrayWidget from import_export.admin import ImportExportMixin from import_export.formats import base_formats from ordered_model.admin import OrderedInlineModelAdminMixin, OrderedTabularInline @@ -32,7 +34,7 @@ class CategoryAdminForm(movenodeform_factory(Category)): class Meta: model = Category fields = "__all__" - widgets = {"description": CKEditorWidget} + widgets = {"description": CKEditorWidget, "zaaktypen": DynamicArrayWidget} def clean(self, *args, **kwargs): cleaned_data = super().clean(*args, **kwargs) @@ -71,7 +73,9 @@ def clean(self): @admin.register(Category) -class CategoryAdmin(OrderedInlineModelAdminMixin, ImportExportMixin, TreeAdmin): +class CategoryAdmin( + DynamicArrayMixin, OrderedInlineModelAdminMixin, ImportExportMixin, TreeAdmin +): change_list_template = "admin/category_change_list.html" form = CategoryAdminForm inlines = ( @@ -81,8 +85,23 @@ class CategoryAdmin(OrderedInlineModelAdminMixin, ImportExportMixin, TreeAdmin): prepopulated_fields = {"slug": ("name",)} search_fields = ("name",) ordering = ("path",) - list_display = ("name", "highlighted", "published") - list_editable = ("highlighted", "published") + list_display = ( + "name", + "highlighted", + "published", + "visible_for_anonymous", + "visible_for_authenticated", + "visible_for_companies", + "visible_for_citizens", + ) + list_editable = ( + "highlighted", + "published", + "visible_for_anonymous", + "visible_for_authenticated", + "visible_for_companies", + "visible_for_citizens", + ) exclude = ("path", "depth", "numchild") # import-export diff --git a/src/open_inwoner/pdc/managers.py b/src/open_inwoner/pdc/managers.py index 461225c257..d29937a5c7 100644 --- a/src/open_inwoner/pdc/managers.py +++ b/src/open_inwoner/pdc/managers.py @@ -3,6 +3,8 @@ from ordered_model.models import OrderedModelQuerySet from treebeard.mp_tree import MP_NodeQuerySet +from open_inwoner.accounts.models import User + class ProductQueryset(models.QuerySet): def published(self): @@ -22,6 +24,11 @@ def published(self): def draft(self): return self.filter(published=False) + def visible_for_user(self, user: User): + if user.is_authenticated: + return self.filter(visible_for_authenticated=True) + return self.filter(visible_for_anonymous=True) + class QuestionQueryset(OrderedModelQuerySet): def general(self): diff --git a/src/open_inwoner/pdc/migrations/0061_auto_20231106_1207.py b/src/open_inwoner/pdc/migrations/0061_auto_20231106_1207.py new file mode 100644 index 0000000000..dcc7765296 --- /dev/null +++ b/src/open_inwoner/pdc/migrations/0061_auto_20231106_1207.py @@ -0,0 +1,72 @@ +# Generated by Django 3.2.20 on 2023-11-06 11:07 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("pdc", "0060_content_collapsable"), + ] + + operations = [ + migrations.AddField( + model_name="category", + name="relevante_zaakperiode", + field=models.PositiveIntegerField( + blank=True, + help_text="Aantal maanden dat teruggekeken moet worden naar Zaken van deze zaaktypes.", + null=True, + verbose_name="Relevante zaakperiode", + ), + ), + migrations.AddField( + model_name="category", + name="visible_for_anonymous", + field=models.BooleanField( + default=True, + help_text="Of het onderwerp zichtbaar moet zijn op het anonieme deel (zonder inloggen).", + verbose_name="Anonieme deel", + ), + ), + migrations.AddField( + model_name="category", + name="visible_for_authenticated", + field=models.BooleanField( + default=True, + help_text="Of het onderwerp zichtbaar moet zijn op het beveiligde deel (achter inloggen).", + verbose_name="Beveiligde deel", + ), + ), + migrations.AddField( + model_name="category", + name="visible_for_citizens", + field=models.BooleanField( + default=True, + help_text="Of het onderwerp zichtbaar moet zijn wanneer iemand aangeeft een inwoner te zijn (of is ingelogd met BSN).", + verbose_name="Inwoner content", + ), + ), + migrations.AddField( + model_name="category", + name="visible_for_companies", + field=models.BooleanField( + default=True, + help_text="Of het onderwerp zichtbaar moet zijn wanneer iemand aangeeft een bedrijf te zijn (of is ingelogd met KvK).", + verbose_name="Bedrijven content", + ), + ), + migrations.AddField( + model_name="category", + name="zaaktypen", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField(blank=True, max_length=1000), + blank=True, + default=list, + help_text="Zaaktypen waarvoor bij aanwezigheid dit onderwerp getoond moet worden.", + size=None, + verbose_name="Zaaktypen", + ), + ), + ] diff --git a/src/open_inwoner/pdc/models/category.py b/src/open_inwoner/pdc/models/category.py index 305315d832..957038a245 100644 --- a/src/open_inwoner/pdc/models/category.py +++ b/src/open_inwoner/pdc/models/category.py @@ -1,3 +1,4 @@ +from django.contrib.postgres.fields import ArrayField from django.db import models from django.urls import reverse from django.utils.translation import ugettext_lazy as _ @@ -34,6 +35,51 @@ class Category(MP_Node): default=False, help_text=_("Whether the category should be published or not."), ) + visible_for_anonymous = models.BooleanField( + verbose_name=_("Anonieme deel"), + default=True, + help_text=_( + "Of het onderwerp zichtbaar moet zijn op het anonieme deel (zonder inloggen)." + ), + ) + visible_for_authenticated = models.BooleanField( + verbose_name=_("Beveiligde deel"), + default=True, + help_text=_( + "Of het onderwerp zichtbaar moet zijn op het beveiligde deel (achter inloggen)." + ), + ) + visible_for_companies = models.BooleanField( + verbose_name=_("Bedrijven content"), + default=True, + help_text=_( + "Of het onderwerp zichtbaar moet zijn wanneer iemand aangeeft een bedrijf te zijn (of is ingelogd met KvK)." + ), + ) + visible_for_citizens = models.BooleanField( + verbose_name=_("Inwoner content"), + default=True, + help_text=_( + "Of het onderwerp zichtbaar moet zijn wanneer iemand aangeeft een inwoner te zijn (of is ingelogd met BSN)." + ), + ) + zaaktypen = ArrayField( + models.CharField(max_length=1000, blank=True), + verbose_name=_("Zaaktypen"), + default=list, + blank=True, + help_text=_( + "Zaaktypen waarvoor bij aanwezigheid dit onderwerp getoond moet worden." + ), + ) + relevante_zaakperiode = models.PositiveIntegerField( + verbose_name=_("Relevante zaakperiode"), + blank=True, + null=True, + help_text=_( + "Aantal maanden dat teruggekeken moet worden naar Zaken van deze zaaktypes." + ), + ) highlighted = models.BooleanField( verbose_name=_("Highlighted"), default=False, diff --git a/src/open_inwoner/pdc/tests/test_category.py b/src/open_inwoner/pdc/tests/test_category.py index 471d69a8a2..f0c0097caf 100644 --- a/src/open_inwoner/pdc/tests/test_category.py +++ b/src/open_inwoner/pdc/tests/test_category.py @@ -16,16 +16,38 @@ class TestPublishedCategories(WebTest): def setUp(self): self.user = UserFactory() self.published1 = CategoryFactory( - path="0001", name="First one", slug="first-one" + path="0001", + name="First one", + slug="first-one", + visible_for_anonymous=True, + visible_for_authenticated=True, ) self.published2 = CategoryFactory( - path="0002", name="Second one", slug="second-one" + path="0002", + name="Second one", + slug="second-one", + visible_for_anonymous=True, + visible_for_authenticated=False, + ) + self.published3 = CategoryFactory( + path="0003", + name="Third one", + slug="third-one", + visible_for_anonymous=False, + visible_for_authenticated=True, + ) + self.published4 = CategoryFactory( + path="0004", + name="Fourth one", + slug="fourth-one", + visible_for_anonymous=False, + visible_for_authenticated=False, ) self.draft1 = CategoryFactory( - path="0003", name="Third one", slug="third-one", published=False + path="0005", name="Fifth one", slug="fifth-one", published=False ) self.draft2 = CategoryFactory( - path="0004", name="Wourth one", slug="wourth-one", published=False + path="0006", name="Sixth one", slug="sixth-one", published=False ) cms_tools.create_homepage() @@ -40,7 +62,7 @@ def test_only_published_categories_exist_in_breadcrumbs_when_logged_in(self): response = self.app.get("/", user=self.user) self.assertEqual( list(response.context["menu_categories"]), - [self.published1, self.published2], + [self.published1, self.published3], ) def test_only_published_categories_exist_in_list_page_when_anonymous(self): @@ -52,7 +74,7 @@ def test_only_published_categories_exist_in_list_page_when_anonymous(self): def test_only_published_categories_exist_in_list_page_when_logged_in(self): response = self.app.get(reverse("products:category_list"), user=self.user) self.assertEqual( - list(response.context["categories"]), [self.published1, self.published2] + list(response.context["categories"]), [self.published1, self.published3] ) def test_only_published_subcategories_exist_in_detail_page_when_anonymous(self): @@ -87,7 +109,7 @@ def test_only_published_categories_exist_in_my_categories_page(self): response = self.app.get(reverse("profile:categories"), user=self.user) self.assertEqual( list(response.context["form"].fields["selected_categories"].queryset.all()), - [self.published1, self.published2], + [self.published1, self.published2, self.published3, self.published4], ) diff --git a/src/open_inwoner/pdc/tests/test_views.py b/src/open_inwoner/pdc/tests/test_views.py index bb77c80fc3..f9ab3f78a8 100644 --- a/src/open_inwoner/pdc/tests/test_views.py +++ b/src/open_inwoner/pdc/tests/test_views.py @@ -61,6 +61,8 @@ def setUpTestData(cls): cls.category = CategoryFactory.create( name="test cat", description="A descriptive description", + visible_for_anonymous=False, + visible_for_authenticated=False, ) def test_category_detail_view_access_restricted(self): @@ -95,6 +97,18 @@ def test_category_detail_view_access_not_restricted(self): self.assertEqual(response.status_code, 200) + def test_category_detail_view_access_not_restricted_if_invisible(self): + config = SiteConfiguration.get_solo() + config.hide_categories_from_anonymous_users = False + config.save() + + url = reverse("products:category_detail", kwargs={"slug": self.category.slug}) + + # request with anonymous user + response = self.client.get(url) + + self.assertEqual(response.status_code, 200) + def test_category_detail_description_rendered(self): url = reverse("products:category_detail", kwargs={"slug": self.category.slug}) diff --git a/src/open_inwoner/pdc/views.py b/src/open_inwoner/pdc/views.py index a3992f6519..ef49cedae2 100644 --- a/src/open_inwoner/pdc/views.py +++ b/src/open_inwoner/pdc/views.py @@ -122,7 +122,7 @@ class CategoryListView( model = Category def get_queryset(self): - return Category.get_root_nodes().published() + return Category.get_root_nodes().published().visible_for_user(self.request.user) @cached_property def crumbs(self): diff --git a/src/open_inwoner/utils/context_processors.py b/src/open_inwoner/utils/context_processors.py index ffb0b8e4dd..fbafdf6d95 100644 --- a/src/open_inwoner/utils/context_processors.py +++ b/src/open_inwoner/utils/context_processors.py @@ -78,7 +78,9 @@ def settings(request): "cookie_link_text": config.cookie_link_text, "cookie_link_url": config.cookie_link_url, "extra_css": config.extra_css, - "menu_categories": Category.get_root_nodes().published(), + "menu_categories": ( + Category.get_root_nodes().published().visible_for_user(request.user) + ), # default SearchForm, might be overwritten by actual SearchView "search_form": SearchForm(auto_id=False), "search_filter_categories": config.search_filter_categories,