angrybeanie_wagtail/env/lib/python3.12/site-packages/wagtail/users/forms.py
2025-07-25 21:32:16 +10:00

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,
)