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:
"""