Files
old-saburly-wagtail-web/env/lib/python3.10/site-packages/wagtail/admin/forms/collections.py

385 lines
14 KiB
Python
Raw Normal View History

2024-08-27 20:33:44 +02:00
from itertools import groupby
from django import forms
from django.conf import settings
from django.contrib.auth.models import Group, Permission
from django.core.exceptions import ValidationError
from django.db import transaction
from django.db.models import Min
from django.template.loader import render_to_string
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy
from wagtail.models import (
Collection,
CollectionViewRestriction,
GroupCollectionPermission,
)
from .view_restrictions import BaseViewRestrictionForm
class CollectionViewRestrictionForm(BaseViewRestrictionForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not getattr(settings, "WAGTAILDOCS_PRIVATE_COLLECTION_OPTIONS", {}).get(
"SHARED_PASSWORD",
True,
):
self.fields["restriction_type"].choices = [
choice
for choice in CollectionViewRestriction.RESTRICTION_CHOICES
if choice[0] != CollectionViewRestriction.PASSWORD
]
del self.fields["password"]
class Meta:
model = CollectionViewRestriction
fields = ("restriction_type", "password", "groups")
class SelectWithDisabledOptions(forms.Select):
"""
Subclass of Django's select widget that allows disabling options.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.disabled_values = ()
def create_option(self, name, value, *args, **kwargs):
option_dict = super().create_option(name, value, *args, **kwargs)
if value in self.disabled_values:
option_dict["attrs"]["disabled"] = "disabled"
return option_dict
class CollectionChoiceField(forms.ModelChoiceField):
widget = SelectWithDisabledOptions
def __init__(self, *args, disabled_queryset=None, **kwargs):
super().__init__(*args, **kwargs)
self._indentation_start_depth = 2
self.disabled_queryset = disabled_queryset
def _get_disabled_queryset(self):
return self._disabled_queryset
def _set_disabled_queryset(self, queryset):
self._disabled_queryset = queryset
if queryset is None:
self.widget.disabled_values = ()
else:
self.widget.disabled_values = queryset.values_list(
self.to_field_name or "pk", flat=True
)
disabled_queryset = property(_get_disabled_queryset, _set_disabled_queryset)
def _set_queryset(self, queryset):
min_depth = self.queryset.aggregate(Min("depth"))["depth__min"]
if min_depth is None:
self._indentation_start_depth = 2
else:
self._indentation_start_depth = min_depth + 1
def label_from_instance(self, obj):
return obj.get_indented_name(self._indentation_start_depth, html=True)
class CollectionForm(forms.ModelForm):
parent = CollectionChoiceField(
label=gettext_lazy("Parent"),
queryset=Collection.objects.all(),
required=True,
help_text=gettext_lazy(
"Select hierarchical position. Note: a collection cannot become a child of itself or one of its "
"descendants."
),
)
class Meta:
model = Collection
fields = ("name",)
def clean_parent(self):
"""
Our rules about where a user may add or move a collection are as follows:
1. The user must have 'add' permission on the parent collection (or its ancestors)
2. We are not moving a collection used to assign permissions for this user
3. We are not trying to move a collection to be parented by one of their descendants
The first 2 items are taken care in the Create and Edit views by deleting the 'parent' field
from the edit form if the user cannot move the collection. This causes Django's form
machinery to ignore the parent field for parent regardless of what the user submits.
This methods enforces rule #3 when we are editing an existing collection.
"""
parent = self.cleaned_data["parent"]
if not self.instance._state.adding and not parent.pk == self.initial.get(
"parent"
):
old_descendants = list(
self.instance.get_descendants(inclusive=True).values_list(
"pk", flat=True
)
)
if parent.pk in old_descendants:
raise ValidationError(gettext_lazy("Please select another parent"))
return parent
class BaseCollectionMemberForm(forms.ModelForm):
"""
Abstract form handler for editing models that belong to a collection,
such as documents and images. These forms are (optionally) instantiated
with a 'user' kwarg, and take care of populating the 'collection' field's
choices with the collections the user has permission for, as well as
hiding the field when only one collection is available.
Subclasses must define a 'permission_policy' attribute.
"""
def __init__(self, *args, **kwargs):
user = kwargs.pop("user", None)
super().__init__(*args, **kwargs)
if user is None:
self.collections = Collection.objects.all()
else:
self.collections = (
self.permission_policy.collections_user_has_permission_for(user, "add")
)
if self.instance.pk:
# editing an existing document; ensure that the list of available collections
# includes its current collection
self.collections = self.collections | Collection.objects.filter(
id=self.instance.collection_id
)
if len(self.collections) == 0:
raise Exception(
"Cannot construct %s for a user with no collection permissions"
% type(self)
)
elif len(self.collections) == 1:
# don't show collection field if only one collection is available
del self.fields["collection"]
else:
self.fields["collection"].queryset = self.collections
def save(self, commit=True):
if len(self.collections) == 1:
# populate the instance's collection field with the one available collection
self.instance.collection = self.collections[0]
return super().save(commit=commit)
class BaseGroupCollectionMemberPermissionFormSet(forms.BaseFormSet):
"""
A base formset class for managing GroupCollectionPermissions for a
model with CollectionMember behaviour. Subclasses should provide attributes:
permission_types - a list of (codename, short_label, long_label) tuples for the permissions
being managed here
permission_queryset - a queryset of Permission objects for the above permissions
default_prefix - prefix to use on form fields if one is not specified in __init__
template = template filename
"""
def __init__(self, data=None, files=None, instance=None, prefix=None):
if prefix is None:
prefix = self.default_prefix
if instance is None:
instance = Group()
if instance.pk is None:
full_collection_permissions = []
else:
full_collection_permissions = (
instance.collection_permissions.filter(
permission__in=self.permission_queryset
)
.select_related("permission__content_type", "collection")
.order_by("collection")
)
self.instance = instance
initial_data = []
for collection, collection_permissions in groupby(
full_collection_permissions,
lambda cp: cp.collection,
):
initial_data.append(
{
"collection": collection,
"permissions": [cp.permission for cp in collection_permissions],
}
)
super().__init__(data, files, initial=initial_data, prefix=prefix)
for form in self.forms:
form.fields["DELETE"].widget = forms.HiddenInput()
@property
def empty_form(self):
empty_form = super().empty_form
empty_form.fields["DELETE"].widget = forms.HiddenInput()
return empty_form
def clean(self):
"""Checks that no two forms refer to the same collection object"""
if any(self.errors):
# Don't bother validating the formset unless each form is valid on its own
return
collections = [
form.cleaned_data["collection"]
for form in self.forms
# need to check for presence of 'collection' in cleaned_data,
# because a completely blank form passes validation
if form not in self.deleted_forms and "collection" in form.cleaned_data
]
if len(set(collections)) != len(collections):
# collections list contains duplicates
raise forms.ValidationError(
_(
"You cannot have multiple permission records for the same collection."
)
)
@transaction.atomic
def save(self):
if self.instance.pk is None:
raise Exception(
"Cannot save a GroupCollectionMemberPermissionFormSet "
"for an unsaved group instance"
)
# get a set of (collection, permission) tuples for all ticked permissions
forms_to_save = [
form
for form in self.forms
if form not in self.deleted_forms and "collection" in form.cleaned_data
]
final_permission_records = set()
for form in forms_to_save:
for permission in form.cleaned_data["permissions"]:
final_permission_records.add(
(form.cleaned_data["collection"], permission)
)
# fetch the group's existing collection permission records for this model,
# and from that, build a list of records to be created / deleted
permission_ids_to_delete = []
permission_records_to_keep = set()
for cp in self.instance.collection_permissions.filter(
permission__in=self.permission_queryset,
):
if (cp.collection, cp.permission) in final_permission_records:
permission_records_to_keep.add((cp.collection, cp.permission))
else:
permission_ids_to_delete.append(cp.id)
self.instance.collection_permissions.filter(
id__in=permission_ids_to_delete
).delete()
permissions_to_add = final_permission_records - permission_records_to_keep
GroupCollectionPermission.objects.bulk_create(
[
GroupCollectionPermission(
group=self.instance, collection=collection, permission=permission
)
for (collection, permission) in permissions_to_add
]
)
def as_admin_panel(self):
return render_to_string(
self.template,
{"formset": self},
)
def collection_member_permission_formset_factory(
model, permission_types, template, default_prefix=None
):
permission_queryset = Permission.objects.filter(
content_type__app_label=model._meta.app_label,
codename__in=[
codename for codename, short_label, long_label in permission_types
],
).select_related("content_type")
if default_prefix is None:
default_prefix = "%s_permissions" % model._meta.model_name
class PermissionMultipleChoiceField(forms.ModelMultipleChoiceField):
"""
Allows the custom labels from ``permission_types`` to be applied to
permission checkboxes for the ``CollectionMemberPermissionsForm`` below
"""
def label_from_instance(self, obj):
for codename, short_label, long_label in permission_types:
if codename == obj.codename:
return long_label
return str(obj)
class CollectionMemberPermissionsForm(forms.Form):
"""
For a given model with CollectionMember behaviour,
defines the permissions that are assigned to an entity
(such as a group or user) for a specific collection
"""
collection = CollectionChoiceField(
label=_("Collection"),
queryset=Collection.objects.all().prefetch_related("group_permissions"),
empty_label=None,
)
permissions = PermissionMultipleChoiceField(
queryset=permission_queryset,
required=False,
widget=forms.CheckboxSelectMultiple,
)
GroupCollectionMemberPermissionFormSet = type(
"GroupCollectionMemberPermissionFormSet",
(BaseGroupCollectionMemberPermissionFormSet,),
{
"permission_types": permission_types,
"permission_queryset": permission_queryset,
"default_prefix": default_prefix,
"template": template,
},
)
return forms.formset_factory(
CollectionMemberPermissionsForm,
formset=GroupCollectionMemberPermissionFormSet,
extra=0,
can_delete=True,
)
GroupCollectionManagementPermissionFormSet = (
collection_member_permission_formset_factory(
Collection,
[
("add_collection", _("Add"), _("Add collections")),
("change_collection", _("Edit"), _("Edit collections")),
("delete_collection", _("Delete"), _("Delete collections")),
],
"wagtailadmin/permissions/includes/collection_management_permissions_form.html",
)
)