diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 40116ffb7e..08aa9d106a 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -24,6 +24,7 @@ Changelog * Support customizations to `UserViewSet` via the app config (Sage Abdullah) * Add word count and reading time metrics within the page editor (Albina Starykova. Sponsored by The Motley Fool) * Implement a new design for accessibility checks (Albina Starykova) + * Allow changing available privacy options per page model (Shlomo Markowitz) * Fix: Make `WAGTAILIMAGES_CHOOSER_PAGE_SIZE` setting functional again (Rohit Sharma) * Fix: Enable `richtext` template tag to convert lazy translation values (Benjamin Bach) * Fix: Ensure permission labels on group permissions page are translated where available (Matt Westcott) diff --git a/docs/reference/pages/model_reference.md b/docs/reference/pages/model_reference.md index 6c526020ef..bc497f69bb 100644 --- a/docs/reference/pages/model_reference.md +++ b/docs/reference/pages/model_reference.md @@ -325,6 +325,33 @@ See also [django-treebeard](https://django-treebeard.readthedocs.io/en/latest/in Controls the maximum number of pages of this type that can be created under any one parent page. + .. attribute:: private_page_options + + Controls what privacy options are available for the page type. + + The following options are available: + + - ``'password'`` - Can restrict to use a shared password + - ``'groups'`` - Can restrict to users in specific groups + - ``'login'`` - Can restrict to logged in users + + .. code-block:: python + + class BreadPage(Page): + ... + + # default + private_page_options = ['password', 'groups', 'login'] + + # disable shared password + private_page_options = ['groups', 'login'] + + # only shared password + private_page_options = ['password'] + + # no privacy options for this page model + private_page_options = [] + .. attribute:: exclude_fields_in_copy An array of field names that will not be included when a Page is copied. diff --git a/docs/releases/6.2.md b/docs/releases/6.2.md index 1b09cade9a..2b0675e072 100644 --- a/docs/releases/6.2.md +++ b/docs/releases/6.2.md @@ -44,6 +44,7 @@ This feature was developed by Albina Starykova and sponsored by The Motley Fool. * Make `routable_resolver_match` attribute available on RoutablePageMixin responses (Andy Chosak) * Support customizations to `UserViewSet` via the app config (Sage Abdullah) * Implement a new design for accessibility checks (Albina Starykova, sponsored by The Motley Fool) + * Allow changing available privacy options per page model (Shlomo Markowitz) ### Bug fixes diff --git a/wagtail/admin/forms/pages.py b/wagtail/admin/forms/pages.py index 8ecb7863cf..47e5f18cd9 100644 --- a/wagtail/admin/forms/pages.py +++ b/wagtail/admin/forms/pages.py @@ -125,6 +125,9 @@ class CopyForm(forms.Form): class PageViewRestrictionForm(BaseViewRestrictionForm): def __init__(self, *args, **kwargs): + # get the list of private page options from the page + private_page_options = kwargs.pop("private_page_options", []) + super().__init__(*args, **kwargs) if not getattr(settings, "WAGTAIL_PRIVATE_PAGE_OPTIONS", {}).get( @@ -136,6 +139,13 @@ class PageViewRestrictionForm(BaseViewRestrictionForm): if choice[0] != PageViewRestriction.PASSWORD ] del self.fields["password"] + # Remove the fields that are not allowed for the page + self.fields["restriction_type"].choices = [ + choice + for choice in self.fields["restriction_type"].choices + if choice[0] in private_page_options + or choice[0] == PageViewRestriction.NONE + ] class Meta: model = PageViewRestriction diff --git a/wagtail/admin/templates/wagtailadmin/page_privacy/no_privacy.html b/wagtail/admin/templates/wagtailadmin/page_privacy/no_privacy.html new file mode 100644 index 0000000000..13fe36b229 --- /dev/null +++ b/wagtail/admin/templates/wagtailadmin/page_privacy/no_privacy.html @@ -0,0 +1,2 @@ +{% load i18n %} +

{% trans "Changing the privacy for this page has been disabled." %}

\ No newline at end of file diff --git a/wagtail/admin/tests/test_privacy.py b/wagtail/admin/tests/test_privacy.py index fbfdf9e525..2ee53c9f22 100644 --- a/wagtail/admin/tests/test_privacy.py +++ b/wagtail/admin/tests/test_privacy.py @@ -544,3 +544,68 @@ class TestPrivacyIndicators(WagtailTestUtils, TestCase): self.assertContains( response, '
' ) + + def test_private_page_options_only_password_groups(self): + # change the private_page_options to password and login + original_private_page_options = self.public_page.private_page_options + self.public_page.specific.__class__.private_page_options = [ + "password", + "groups", + ] + + response = self.client.get( + reverse("wagtailadmin_pages:set_privacy", args=(self.public_page.id,)) + ) + + restriction_types = [ + choice[0] + for choice in response.context["form"].fields["restriction_type"].choices + ] + + # Check response + self.assertListEqual(restriction_types, ["none", "password", "groups"]) + + # Reset the private_page_options to previous value + self.public_page.specific.__class__.private_page_options = ( + original_private_page_options + ) + + def test_private_page_options_only_password_login(self): + # change the private_page_options to password and login + original_private_page_options = self.public_page.private_page_options + self.public_page.specific.__class__.private_page_options = ["password", "login"] + + response = self.client.get( + reverse("wagtailadmin_pages:set_privacy", args=(self.public_page.id,)) + ) + + restriction_types = [ + choice[0] + for choice in response.context["form"].fields["restriction_type"].choices + ] + + # Check response + self.assertListEqual(restriction_types, ["none", "password", "login"]) + + # Reset the private_page_options to previous value + self.public_page.specific.__class__.private_page_options = ( + original_private_page_options + ) + + def test_private_page_no_options(self): + # change the private_page_options to empty list + original_private_page_options = self.public_page.private_page_options + self.public_page.specific.__class__.private_page_options = [] + + response = self.client.get( + reverse("wagtailadmin_pages:set_privacy", args=(self.public_page.id,)) + ) + + # Check response + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "wagtailadmin/page_privacy/no_privacy.html") + + # Reset the private_page_options to previous value + self.public_page.specific.__class__.private_page_options = ( + original_private_page_options + ) diff --git a/wagtail/admin/views/page_privacy.py b/wagtail/admin/views/page_privacy.py index 91aab119b4..cf26c4ec7a 100644 --- a/wagtail/admin/views/page_privacy.py +++ b/wagtail/admin/views/page_privacy.py @@ -7,7 +7,7 @@ from wagtail.models import Page, PageViewRestriction def set_privacy(request, page_id): - page = get_object_or_404(Page, id=page_id) + page = get_object_or_404(Page, id=page_id).specific_deferred page_perms = page.permissions_for_user(request.user) if not page_perms.can_set_view_restrictions(): raise PermissionDenied @@ -16,13 +16,17 @@ def set_privacy(request, page_id): restrictions = page.get_view_restrictions().order_by("page__depth") if restrictions: restriction = restrictions[0] - restriction_exists_on_ancestor = restriction.page != page + restriction_exists_on_ancestor = restriction.page.id != page.id else: restriction = None restriction_exists_on_ancestor = False if request.method == "POST": - form = PageViewRestrictionForm(request.POST, instance=restriction) + form = PageViewRestrictionForm( + request.POST, + instance=restriction, + private_page_options=page.private_page_options, + ) if form.is_valid() and not restriction_exists_on_ancestor: if form.cleaned_data["restriction_type"] == PageViewRestriction.NONE: # remove any existing restriction @@ -49,10 +53,15 @@ def set_privacy(request, page_id): else: # request is a GET if not restriction_exists_on_ancestor: if restriction: - form = PageViewRestrictionForm(instance=restriction) + form = PageViewRestrictionForm( + instance=restriction, private_page_options=page.private_page_options + ) else: # no current view restrictions on this page - form = PageViewRestrictionForm(initial={"restriction_type": "none"}) + form = PageViewRestrictionForm( + initial={"restriction_type": "none"}, + private_page_options=page.private_page_options, + ) if restriction_exists_on_ancestor: # display a message indicating that there is a restriction at ancestor level - @@ -65,6 +74,12 @@ def set_privacy(request, page_id): "page_with_restriction": restriction.page, }, ) + elif len(page.private_page_options) == 0: + return render_modal_workflow( + request, + "wagtailadmin/page_privacy/no_privacy.html", + None, + ) else: # no restriction set at ancestor level - can set restrictions here return render_modal_workflow( diff --git a/wagtail/models/__init__.py b/wagtail/models/__init__.py index b7c9a61cfc..7740d470f5 100644 --- a/wagtail/models/__init__.py +++ b/wagtail/models/__init__.py @@ -1291,6 +1291,9 @@ class Page(AbstractPage, index.Indexed, ClusterableModel, metaclass=PageBase): promote_panels = [] settings_panels = [] + # Privacy options for page + private_page_options = ["password", "groups", "login"] + @staticmethod def route_for_request(request: "HttpRequest", path: str) -> RouteResult | None: """