mirror of
https://github.com/Hopiu/wagtail.git
synced 2026-05-18 04:01:11 +00:00
Conflicts: wagtail/admin/tests/test_pages_views.py wagtail/admin/utils.py wagtail/admin/views/account.py wagtail/users/views/groups.py wagtail/users/views/users.py wagtail/users/wagtail_hooks.py
379 lines
14 KiB
Python
379 lines
14 KiB
Python
from __future__ import absolute_import, unicode_literals
|
|
|
|
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.db.models.fields import BLANK_CHOICE_DASH
|
|
from django.template.loader import render_to_string
|
|
from django.utils.html import mark_safe
|
|
from django.utils.translation import ugettext_lazy as _
|
|
|
|
from wagtail.admin.utils import get_available_admin_languages
|
|
from wagtail.admin.widgets import AdminPageChooser
|
|
from wagtail.core import hooks
|
|
from wagtail.core.models import (
|
|
PAGE_PERMISSION_TYPE_CHOICES, PAGE_PERMISSION_TYPES, GroupPagePermission, Page,
|
|
UserPagePermissionsProxy)
|
|
from wagtail.users.models import UserProfile
|
|
|
|
User = get_user_model()
|
|
|
|
# The standard fields each user model is expected to have, as a minimum.
|
|
standard_fields = set(['email', 'first_name', 'last_name', 'is_superuser', 'groups'])
|
|
# Custom fields
|
|
if hasattr(settings, 'WAGTAIL_USER_CUSTOM_FIELDS'):
|
|
custom_fields = set(settings.WAGTAIL_USER_CUSTOM_FIELDS)
|
|
else:
|
|
custom_fields = set()
|
|
|
|
|
|
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(UsernameForm, self).__init__(*args, **kwargs)
|
|
if User.USERNAME_FIELD == 'username':
|
|
field = self.fields['username']
|
|
field.regex = r"^[\w.@+-]+$"
|
|
field.help_text = _("Required. 30 characters or fewer. 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,
|
|
help_text=_("Leave blank if not changing."))
|
|
password2 = forms.CharField(
|
|
label=_("Password confirmation"), required=False,
|
|
widget=forms.PasswordInput,
|
|
help_text=_("Enter the same password as above, for verification."))
|
|
|
|
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(UserForm, self).__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',
|
|
))
|
|
|
|
if password1:
|
|
validate_password(password1, user=self.instance)
|
|
|
|
return password2
|
|
|
|
def _clean_fields(self):
|
|
super(UserForm, self)._clean_fields()
|
|
self._clean_username()
|
|
|
|
def save(self, commit=True):
|
|
user = super(UserForm, self).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 = set([User.USERNAME_FIELD]) | standard_fields | custom_fields
|
|
widgets = {
|
|
'groups': forms.CheckboxSelectMultiple
|
|
}
|
|
|
|
|
|
class UserEditForm(UserForm):
|
|
password_required = False
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
editing_self = kwargs.pop('editing_self', False)
|
|
super(UserEditForm, self).__init__(*args, **kwargs)
|
|
|
|
if editing_self:
|
|
del self.fields["is_active"]
|
|
del self.fields["is_superuser"]
|
|
|
|
class Meta:
|
|
model = User
|
|
fields = set([User.USERNAME_FIELD, "is_active"]) | standard_fields | custom_fields
|
|
widgets = {
|
|
'groups': forms.CheckboxSelectMultiple
|
|
}
|
|
|
|
|
|
class GroupForm(forms.ModelForm):
|
|
def __init__(self, *args, **kwargs):
|
|
super(GroupForm, self).__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):
|
|
# 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(GroupForm, self).save()
|
|
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)
|
|
)
|
|
permission_types = forms.MultipleChoiceField(
|
|
choices=PAGE_PERMISSION_TYPE_CHOICES,
|
|
required=False,
|
|
widget=forms.CheckboxSelectMultiple
|
|
)
|
|
|
|
|
|
class BaseGroupPagePermissionFormSet(forms.BaseFormSet):
|
|
permission_types = PAGE_PERMISSION_TYPES # defined here for easy access from templates
|
|
|
|
def __init__(self, data=None, files=None, instance=None, prefix='page_permissions'):
|
|
if instance is None:
|
|
instance = Group()
|
|
|
|
self.instance = instance
|
|
|
|
initial_data = []
|
|
|
|
for page, page_permissions in groupby(
|
|
instance.page_permissions.select_related('page').order_by('page'), lambda pp: pp.page
|
|
):
|
|
initial_data.append({
|
|
'page': page,
|
|
'permission_types': [pp.permission_type for pp in page_permissions]
|
|
})
|
|
|
|
super(BaseGroupPagePermissionFormSet, self).__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(BaseGroupPagePermissionFormSet, self).empty_form
|
|
empty_form.fields['DELETE'].widget = forms.HiddenInput()
|
|
return empty_form
|
|
|
|
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_type) 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_type in form.cleaned_data['permission_types']:
|
|
final_permission_records.add((form.cleaned_data['page'], permission_type))
|
|
|
|
# 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_type) in final_permission_records:
|
|
permission_records_to_keep.add((pp.page, pp.permission_type))
|
|
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_type=permission_type
|
|
)
|
|
for (page, permission_type) 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
|
|
)
|
|
|
|
|
|
class NotificationPreferencesForm(forms.ModelForm):
|
|
def __init__(self, *args, **kwargs):
|
|
super(NotificationPreferencesForm, self).__init__(*args, **kwargs)
|
|
user_perms = UserPagePermissionsProxy(self.instance.user)
|
|
if not user_perms.can_publish_pages():
|
|
del self.fields['submitted_notifications']
|
|
if not user_perms.can_edit_pages():
|
|
del self.fields['approved_notifications']
|
|
del self.fields['rejected_notifications']
|
|
|
|
class Meta:
|
|
model = UserProfile
|
|
fields = ("submitted_notifications", "approved_notifications", "rejected_notifications")
|
|
|
|
|
|
class PreferredLanguageForm(forms.ModelForm):
|
|
preferred_language = forms.ChoiceField(
|
|
required=False,
|
|
choices=lambda: sorted(BLANK_CHOICE_DASH + get_available_admin_languages(), key=lambda l: l[1])
|
|
)
|
|
|
|
class Meta:
|
|
model = UserProfile
|
|
fields = ("preferred_language",)
|