416 lines
14 KiB
Python
416 lines
14 KiB
Python
from itertools import groupby
|
|
|
|
from django import forms
|
|
from django.conf import settings
|
|
from django.contrib.auth import get_user_model
|
|
from django.contrib.auth.models import Group, Permission
|
|
from django.contrib.auth.password_validation import (
|
|
password_validators_help_text_html,
|
|
validate_password,
|
|
)
|
|
from django.db import transaction
|
|
from django.template.loader import render_to_string
|
|
from django.utils.html import mark_safe
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
from wagtail import hooks
|
|
from wagtail.admin.forms.formsets import BaseFormSetMixin
|
|
from wagtail.admin.widgets import AdminPageChooser
|
|
from wagtail.models import (
|
|
PAGE_PERMISSION_CODENAMES,
|
|
PAGE_PERMISSION_TYPES,
|
|
GroupPagePermission,
|
|
Page,
|
|
)
|
|
|
|
User = get_user_model()
|
|
|
|
# The standard fields each user model is expected to have, as a minimum.
|
|
standard_fields = {"email", "first_name", "last_name", "is_superuser", "groups"}
|
|
|
|
|
|
class UsernameForm(forms.ModelForm):
|
|
"""
|
|
Intelligently sets up the username field if it is in fact a username. If the
|
|
User model has been swapped out, and the username field is an email or
|
|
something else, don't touch it.
|
|
"""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
if User.USERNAME_FIELD == "username":
|
|
field = self.fields["username"]
|
|
field.regex = r"^[\w.@+-]+$"
|
|
field.help_text = _("Required. Letters, digits and @/./+/-/_ only.")
|
|
field.error_messages = field.error_messages.copy()
|
|
field.error_messages.update(
|
|
{
|
|
"invalid": _(
|
|
"This value may contain only letters, numbers "
|
|
"and @/./+/-/_ characters."
|
|
)
|
|
}
|
|
)
|
|
|
|
@property
|
|
def username_field(self):
|
|
return self[User.USERNAME_FIELD]
|
|
|
|
def separate_username_field(self):
|
|
return User.USERNAME_FIELD not in standard_fields
|
|
|
|
|
|
class UserForm(UsernameForm):
|
|
required_css_class = "required"
|
|
|
|
@property
|
|
def password_required(self):
|
|
return getattr(settings, "WAGTAILUSERS_PASSWORD_REQUIRED", True)
|
|
|
|
@property
|
|
def password_enabled(self):
|
|
return getattr(settings, "WAGTAILUSERS_PASSWORD_ENABLED", True)
|
|
|
|
error_messages = {
|
|
"duplicate_username": _("A user with that username already exists."),
|
|
"password_mismatch": _("The two password fields didn't match."),
|
|
}
|
|
|
|
email = forms.EmailField(required=True, label=_("Email"))
|
|
first_name = forms.CharField(required=True, label=_("First Name"))
|
|
last_name = forms.CharField(required=True, label=_("Last Name"))
|
|
|
|
password1 = forms.CharField(
|
|
label=_("Password"),
|
|
required=False,
|
|
widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}),
|
|
help_text=_("Leave blank if not changing."),
|
|
strip=False,
|
|
)
|
|
password2 = forms.CharField(
|
|
label=_("Password confirmation"),
|
|
required=False,
|
|
widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}),
|
|
help_text=_("Enter the same password as above, for verification."),
|
|
strip=False,
|
|
)
|
|
|
|
is_superuser = forms.BooleanField(
|
|
label=_("Administrator"),
|
|
required=False,
|
|
help_text=_("Administrators have full access to manage any object or setting."),
|
|
)
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
|
|
if self.password_enabled:
|
|
if self.password_required:
|
|
self.fields["password1"].help_text = mark_safe(
|
|
password_validators_help_text_html()
|
|
)
|
|
self.fields["password1"].required = True
|
|
self.fields["password2"].required = True
|
|
else:
|
|
del self.fields["password1"]
|
|
del self.fields["password2"]
|
|
|
|
# We cannot call this method clean_username since this the name of the
|
|
# username field may be different, so clean_username would not be reliably
|
|
# called. We therefore call _clean_username explicitly in _clean_fields.
|
|
def _clean_username(self):
|
|
username_field = User.USERNAME_FIELD
|
|
# This method is called even if username if empty, contrary to clean_*
|
|
# methods, so we have to check again here that data is defined.
|
|
if username_field not in self.cleaned_data:
|
|
return
|
|
username = self.cleaned_data[username_field]
|
|
|
|
users = User._default_manager.all()
|
|
if self.instance.pk is not None:
|
|
users = users.exclude(pk=self.instance.pk)
|
|
if users.filter(**{username_field: username}).exists():
|
|
self.add_error(
|
|
User.USERNAME_FIELD,
|
|
forms.ValidationError(
|
|
self.error_messages["duplicate_username"],
|
|
code="duplicate_username",
|
|
),
|
|
)
|
|
return username
|
|
|
|
def clean_password2(self):
|
|
password1 = self.cleaned_data.get("password1")
|
|
password2 = self.cleaned_data.get("password2")
|
|
if password2 != password1:
|
|
self.add_error(
|
|
"password2",
|
|
forms.ValidationError(
|
|
self.error_messages["password_mismatch"],
|
|
code="password_mismatch",
|
|
),
|
|
)
|
|
|
|
return password2
|
|
|
|
def validate_password(self):
|
|
"""
|
|
Run the Django password validators against the new password. This must
|
|
be called after the user instance in self.instance is populated with
|
|
the new data from the form, as some validators rely on attributes on
|
|
the user model.
|
|
"""
|
|
password1 = self.cleaned_data.get("password1")
|
|
password2 = self.cleaned_data.get("password2")
|
|
if password1 and password2 and password1 == password2:
|
|
validate_password(password1, user=self.instance)
|
|
|
|
def _post_clean(self):
|
|
super()._post_clean()
|
|
try:
|
|
self.validate_password()
|
|
except forms.ValidationError as e:
|
|
self.add_error("password2", e)
|
|
|
|
def _clean_fields(self):
|
|
super()._clean_fields()
|
|
self._clean_username()
|
|
|
|
def save(self, commit=True):
|
|
user = super().save(commit=False)
|
|
|
|
if self.password_enabled:
|
|
password = self.cleaned_data["password1"]
|
|
if password:
|
|
user.set_password(password)
|
|
|
|
if commit:
|
|
user.save()
|
|
self.save_m2m()
|
|
return user
|
|
|
|
|
|
class UserCreationForm(UserForm):
|
|
class Meta:
|
|
model = User
|
|
fields = {User.USERNAME_FIELD} | standard_fields
|
|
widgets = {"groups": forms.CheckboxSelectMultiple}
|
|
|
|
|
|
class UserEditForm(UserForm):
|
|
password_required = False
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
editing_self = kwargs.pop("editing_self", False)
|
|
super().__init__(*args, **kwargs)
|
|
|
|
if editing_self:
|
|
del self.fields["is_active"]
|
|
del self.fields["is_superuser"]
|
|
|
|
class Meta:
|
|
model = User
|
|
fields = {User.USERNAME_FIELD, "is_active"} | standard_fields
|
|
widgets = {"groups": forms.CheckboxSelectMultiple}
|
|
|
|
|
|
class GroupForm(forms.ModelForm):
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.registered_permissions = Permission.objects.none()
|
|
for fn in hooks.get_hooks("register_permissions"):
|
|
self.registered_permissions = self.registered_permissions | fn()
|
|
self.fields[
|
|
"permissions"
|
|
].queryset = self.registered_permissions.select_related("content_type")
|
|
|
|
required_css_class = "required"
|
|
|
|
error_messages = {
|
|
"duplicate_name": _("A group with that name already exists."),
|
|
}
|
|
|
|
is_superuser = forms.BooleanField(
|
|
label=_("Administrator"),
|
|
required=False,
|
|
help_text=_("Administrators have full access to manage any object or setting."),
|
|
)
|
|
|
|
class Meta:
|
|
model = Group
|
|
fields = (
|
|
"name",
|
|
"permissions",
|
|
)
|
|
widgets = {
|
|
"permissions": forms.CheckboxSelectMultiple(),
|
|
}
|
|
|
|
def clean_name(self):
|
|
# Since Group.name is unique, this check is redundant,
|
|
# but it sets a nicer error message than the ORM. See #13147.
|
|
name = self.cleaned_data["name"]
|
|
try:
|
|
Group._default_manager.exclude(pk=self.instance.pk).get(name=name)
|
|
except Group.DoesNotExist:
|
|
return name
|
|
raise forms.ValidationError(self.error_messages["duplicate_name"])
|
|
|
|
def save(self, commit=True):
|
|
# We go back to the object to read (in order to reapply) the
|
|
# permissions which were set on this group, but which are not
|
|
# accessible in the wagtail admin interface, as otherwise these would
|
|
# be clobbered by this form.
|
|
try:
|
|
untouchable_permissions = self.instance.permissions.exclude(
|
|
pk__in=self.registered_permissions
|
|
)
|
|
bool(
|
|
untouchable_permissions
|
|
) # force this to be evaluated, as it's about to change
|
|
except ValueError:
|
|
# this form is not bound; we're probably creating a new group
|
|
untouchable_permissions = []
|
|
group = super().save(commit=commit)
|
|
group.permissions.add(*untouchable_permissions)
|
|
return group
|
|
|
|
|
|
class PagePermissionsForm(forms.Form):
|
|
"""
|
|
Note 'Permissions' (plural). A single instance of this form defines the permissions
|
|
that are assigned to an entity (i.e. group or user) for a specific page.
|
|
"""
|
|
|
|
page = forms.ModelChoiceField(
|
|
queryset=Page.objects.all(),
|
|
widget=AdminPageChooser(show_edit_link=False, can_choose_root=True),
|
|
)
|
|
permissions = forms.ModelMultipleChoiceField(
|
|
queryset=Permission.objects.filter(
|
|
content_type__app_label="wagtailcore",
|
|
content_type__model="page",
|
|
codename__in=PAGE_PERMISSION_CODENAMES,
|
|
)
|
|
.select_related("content_type")
|
|
.order_by("codename"),
|
|
# Use codename as the field to use for the option values rather than pk,
|
|
# to minimise the changes needed since we moved to the Permission model
|
|
# and to ease testing.
|
|
# Django advises `to_field_name` to be a unique field. While `codename`
|
|
# is not unique by itself, it is unique together with `content_type`, so
|
|
# it is unique in the context of the above queryset.
|
|
to_field_name="codename",
|
|
required=False,
|
|
widget=forms.CheckboxSelectMultiple,
|
|
)
|
|
|
|
|
|
class BaseGroupPagePermissionFormSet(BaseFormSetMixin, forms.BaseFormSet):
|
|
# defined here for easy access from templates
|
|
permission_types = PAGE_PERMISSION_TYPES
|
|
|
|
def __init__(self, data=None, files=None, instance=None, prefix="page_permissions"):
|
|
if instance is None:
|
|
instance = Group()
|
|
|
|
if instance.pk is None:
|
|
full_page_permissions = []
|
|
else:
|
|
full_page_permissions = instance.page_permissions.select_related(
|
|
"page", "permission"
|
|
).order_by("page")
|
|
|
|
self.instance = instance
|
|
|
|
initial_data = []
|
|
|
|
for page, page_permissions in groupby(
|
|
full_page_permissions,
|
|
lambda pp: pp.page,
|
|
):
|
|
initial_data.append(
|
|
{
|
|
"page": page,
|
|
"permissions": [pp.permission for pp in page_permissions],
|
|
}
|
|
)
|
|
|
|
super().__init__(data, files, initial=initial_data, prefix=prefix)
|
|
|
|
def clean(self):
|
|
"""Checks that no two forms refer to the same page object"""
|
|
if any(self.errors):
|
|
# Don't bother validating the formset unless each form is valid on its own
|
|
return
|
|
|
|
pages = [
|
|
form.cleaned_data["page"]
|
|
for form in self.forms
|
|
# need to check for presence of 'page' in cleaned_data,
|
|
# because a completely blank form passes validation
|
|
if form not in self.deleted_forms and "page" in form.cleaned_data
|
|
]
|
|
if len(set(pages)) != len(pages):
|
|
# pages list contains duplicates
|
|
raise forms.ValidationError(
|
|
_("You cannot have multiple permission records for the same page.")
|
|
)
|
|
|
|
@transaction.atomic
|
|
def save(self):
|
|
if self.instance.pk is None:
|
|
raise Exception(
|
|
"Cannot save a GroupPagePermissionFormSet for an unsaved group instance"
|
|
)
|
|
|
|
# get a set of (page, permission) tuples for all ticked permissions
|
|
forms_to_save = [
|
|
form
|
|
for form in self.forms
|
|
if form not in self.deleted_forms and "page" 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["page"], permission))
|
|
|
|
# fetch the group's existing page permission records, and from that, build a list
|
|
# of records to be created / deleted
|
|
permission_ids_to_delete = []
|
|
permission_records_to_keep = set()
|
|
|
|
for pp in self.instance.page_permissions.all():
|
|
if (pp.page, pp.permission) in final_permission_records:
|
|
permission_records_to_keep.add((pp.page, pp.permission))
|
|
else:
|
|
permission_ids_to_delete.append(pp.pk)
|
|
|
|
self.instance.page_permissions.filter(pk__in=permission_ids_to_delete).delete()
|
|
|
|
permissions_to_add = final_permission_records - permission_records_to_keep
|
|
GroupPagePermission.objects.bulk_create(
|
|
[
|
|
GroupPagePermission(
|
|
group=self.instance,
|
|
page=page,
|
|
permission=permission,
|
|
)
|
|
for (page, permission) in permissions_to_add
|
|
]
|
|
)
|
|
|
|
def as_admin_panel(self):
|
|
return render_to_string(
|
|
"wagtailusers/groups/includes/page_permissions_formset.html",
|
|
{"formset": self},
|
|
)
|
|
|
|
|
|
GroupPagePermissionFormSet = forms.formset_factory(
|
|
PagePermissionsForm,
|
|
formset=BaseGroupPagePermissionFormSet,
|
|
extra=0,
|
|
can_delete=True,
|
|
)
|