Initial commit

This commit is contained in:
2024-08-27 20:33:44 +02:00
commit 1f1832267d
14794 changed files with 1599592 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
from .assign_role import AssignRoleBulkAction
from .delete import DeleteBulkAction
from .set_active_state import SetActiveStateBulkAction
__all__ = ["AssignRoleBulkAction", "DeleteBulkAction", "SetActiveStateBulkAction"]

View File

@@ -0,0 +1,46 @@
from django import forms
from django.contrib.auth.models import Group
from django.utils.translation import gettext_lazy as _
from django.utils.translation import ngettext
from wagtail.users.views.bulk_actions.user_bulk_action import UserBulkAction
from wagtail.users.views.users import change_user_perm
class RoleForm(forms.Form):
role = forms.ModelChoiceField(queryset=Group.objects.all())
class AssignRoleBulkAction(UserBulkAction):
display_name = _("Assign role")
action_type = "assign_role"
aria_label = _("Assign role to selected users")
template_name = "wagtailusers/bulk_actions/confirm_bulk_assign_role.html"
action_priority = 30
form_class = RoleForm
def check_perm(self, obj):
return self.request.user.has_perm(change_user_perm)
def get_execution_context(self):
return {
"role": self.cleaned_form.cleaned_data["role"],
}
@classmethod
def execute_action(cls, objects, role=None, **kwargs):
if role is None:
return
role.user_set.add(*objects)
num_parent_objects = len(objects)
return num_parent_objects, 0
def get_success_message(self, num_parent_objects, num_child_objects):
return ngettext(
"%(num_parent_objects)d user has been assigned as %(role)s",
"%(num_parent_objects)d users have been assigned as %(role)s",
num_parent_objects,
) % {
"num_parent_objects": num_parent_objects,
"role": self.cleaned_form.cleaned_data["role"].name,
}

View File

@@ -0,0 +1,34 @@
from django.utils.translation import gettext_lazy as _
from django.utils.translation import ngettext
from wagtail.users.utils import user_can_delete_user
from wagtail.users.views.bulk_actions.user_bulk_action import UserBulkAction
class DeleteBulkAction(UserBulkAction):
display_name = _("Delete")
action_type = "delete"
aria_label = _("Delete selected users")
template_name = "wagtailusers/bulk_actions/confirm_bulk_delete.html"
action_priority = 10
classes = {"serious"}
def check_perm(self, obj):
return user_can_delete_user(self.request.user, obj)
def get_execution_context(self):
return {**super().get_execution_context(), "model": self.model}
@classmethod
def execute_action(cls, objects, model=None, **kwargs):
if model is None:
model = cls.get_default_model()
model.objects.filter(pk__in=[obj.pk for obj in objects]).delete()
return len(objects), 0
def get_success_message(self, num_parent_objects, num_child_objects):
return ngettext(
"%(num_parent_objects)d user has been deleted",
"%(num_parent_objects)d users have been deleted",
num_parent_objects,
) % {"num_parent_objects": num_parent_objects}

View File

@@ -0,0 +1,72 @@
from django import forms
from django.utils.translation import gettext_lazy as _
from django.utils.translation import ngettext
from wagtail.users.views.bulk_actions.user_bulk_action import UserBulkAction
from wagtail.users.views.users import change_user_perm
class ActivityForm(forms.Form):
mark_as_active = forms.TypedChoiceField(
choices=((True, _("Active")), (False, _("Inactive"))),
label=_("Mark as active"),
widget=forms.RadioSelect,
coerce=lambda x: x == "True",
)
class SetActiveStateBulkAction(UserBulkAction):
display_name = _("Set active state")
action_type = "set_active_state"
aria_label = _("Change the active state for selected users")
template_name = "wagtailusers/bulk_actions/confirm_bulk_set_active_state.html"
action_priority = 20
form_class = ActivityForm
def check_perm(self, obj):
return self.request.user.has_perm(change_user_perm)
def get_execution_context(self):
return {
"mark_as_active": self.cleaned_form.cleaned_data["mark_as_active"],
"user": self.request.user,
"model": self.model,
}
def get_actionable_objects(self):
objects, objects_without_access = super().get_actionable_objects()
user = self.request.user
users = list(filter(lambda x: x.pk != user.pk, objects))
if len(objects) != len(users):
objects_without_access["mark_self_as_inactive"] = [user]
return users, objects_without_access
@classmethod
def execute_action(cls, objects, mark_as_active=False, model=None, **kwargs):
if model is None:
model = cls.get_default_model()
user = kwargs.get("user", None)
if user is not None:
objects = list(filter(lambda x: x.pk != user.pk, objects))
num_parent_objects = model.objects.filter(
pk__in=[obj.pk for obj in objects]
).update(is_active=mark_as_active)
return num_parent_objects, 0
def get_success_message(self, num_parent_objects, num_child_objects):
if self.cleaned_form.cleaned_data["mark_as_active"]:
return ngettext(
"%(num_parent_objects)d user has been marked as active",
"%(num_parent_objects)d users have been marked as active",
num_parent_objects,
) % {
"num_parent_objects": num_parent_objects,
}
else:
return ngettext(
"%(num_parent_objects)d user has been marked as inactive",
"%(num_parent_objects)d users have been marked as inactive",
num_parent_objects,
) % {
"num_parent_objects": num_parent_objects,
}

View File

@@ -0,0 +1,25 @@
from django.contrib.auth import get_user_model
from wagtail.admin.views.bulk_action import BulkAction
from wagtail.admin.views.generic.permissions import PermissionCheckedMixin
from wagtail.permission_policies import ModelPermissionPolicy
from wagtail.users.views.users import get_users_filter_query
User = get_user_model()
class UserBulkAction(PermissionCheckedMixin, BulkAction):
models = [User]
permission_policy = ModelPermissionPolicy(User)
any_permission_required = ["add", "change", "delete"]
def get_all_objects_in_listing_query(self, parent_id):
listing_objects = self.model.objects.all().values_list("pk", flat=True)
if "q" in self.request.GET:
q = self.request.GET.get("q")
model_fields = {f.name for f in self.model._meta.get_fields()}
conditions = get_users_filter_query(q, model_fields)
listing_objects = listing_objects.filter(conditions)
return listing_objects

View File

@@ -0,0 +1,196 @@
from warnings import warn
from django.contrib.auth.models import Group
from django.shortcuts import get_object_or_404, redirect
from django.urls import re_path, reverse
from django.utils.functional import cached_property
from django.utils.translation import gettext
from django.utils.translation import gettext_lazy as _
from wagtail import hooks
from wagtail.admin.ui.tables import TitleColumn
from wagtail.admin.utils import set_query_params
from wagtail.admin.views import generic
from wagtail.admin.viewsets.model import ModelViewSet
from wagtail.admin.widgets.button import HeaderButton
from wagtail.users.forms import GroupForm, GroupPagePermissionFormSet
from wagtail.utils.deprecation import RemovedInWagtail70Warning
_permission_panel_classes = None
def get_permission_panel_classes():
global _permission_panel_classes
if _permission_panel_classes is None:
_permission_panel_classes = [GroupPagePermissionFormSet]
for fn in hooks.get_hooks("register_group_permission_panel"):
_permission_panel_classes.append(fn())
return _permission_panel_classes
class PermissionPanelFormsMixin:
def get_permission_panel_form_kwargs(self, cls):
kwargs = {}
if self.request.method in ("POST", "PUT"):
kwargs.update(
{
"data": self.request.POST,
"files": self.request.FILES,
}
)
if hasattr(self, "object"):
kwargs.update({"instance": self.object})
return kwargs
def get_permission_panel_forms(self):
return [
cls(**self.get_permission_panel_form_kwargs(cls))
for cls in get_permission_panel_classes()
]
def process_form(self):
form = self.get_form()
permission_panels = self.get_permission_panel_forms()
if form.is_valid() and all(panel.is_valid() for panel in permission_panels):
response = self.form_valid(form)
for panel in permission_panels:
panel.save()
return response
else:
return self.form_invalid(form)
def get_context_data(self, **kwargs):
if "permission_panels" not in kwargs:
kwargs["permission_panels"] = self.get_permission_panel_forms()
context = super().get_context_data(**kwargs)
# Add js/css media from the formsets to the existing media
for panel in context["permission_panels"]:
context["media"] += panel.media
return context
class IndexView(generic.IndexView):
page_title = _("Groups")
add_item_label = _("Add a group")
search_box_placeholder = _("Search groups")
search_fields = ["name"]
context_object_name = "groups"
paginate_by = 20
columns = [
TitleColumn(
"name",
label=_("Name"),
sort_key="name",
url_name="wagtailusers_groups:edit",
),
]
class CreateView(PermissionPanelFormsMixin, generic.CreateView):
page_title = _("Add group")
success_message = _("Group '%(object)s' created.")
def post(self, request, *args, **kwargs):
"""
Handle POST requests: instantiate a form instance with the passed
POST variables and then check if it's valid.
"""
# Create an object now so that the permission panel forms have something to link them against
self.object = Group()
return self.process_form()
class EditView(PermissionPanelFormsMixin, generic.EditView):
success_message = _("Group '%(object)s' updated.")
error_message = _("The group could not be saved due to errors.")
delete_item_label = _("Delete group")
context_object_name = "group"
@cached_property
def header_buttons(self):
return [
HeaderButton(
gettext("View users in this group"),
url=set_query_params(
reverse("wagtailusers_users:index"),
{"group": self.object.pk},
),
icon_name="user",
)
]
def post(self, request, *args, **kwargs):
"""
Handle POST requests: instantiate a form instance with the passed
POST variables and then check if it's valid.
"""
self.object = self.get_object()
return self.process_form()
class DeleteView(generic.DeleteView):
success_message = _("Group '%(object)s' deleted.")
page_title = _("Delete group")
confirmation_message = _("Are you sure you want to delete this group?")
class GroupViewSet(ModelViewSet):
icon = "group"
model = Group
ordering = ["name"]
add_to_reference_index = False
menu_name = "groups"
menu_label = _("Groups")
menu_order = 601
add_to_settings_menu = True
index_view_class = IndexView
add_view_class = CreateView
edit_view_class = EditView
delete_view_class = DeleteView
template_prefix = "wagtailusers/groups/"
@property
def users_view(self):
def view(request, pk):
legacy_url = reverse(self.get_url_name("users"), args=(pk,))
new_url = set_query_params(
reverse("wagtailusers_users:index"),
{"group": get_object_or_404(Group, pk=pk).pk},
)
warn(
f"Accessing the list of users in a group via {legacy_url} is "
f"deprecated, use {new_url} instead.",
RemovedInWagtail70Warning,
)
return redirect(new_url)
return view
def get_common_view_kwargs(self, **kwargs):
return super().get_common_view_kwargs(
**{
"usage_url_name": None,
**kwargs,
}
)
def get_form_class(self, for_update=False):
return GroupForm
def get_urlpatterns(self):
return super().get_urlpatterns() + [
re_path(r"(\d+)/users/$", self.users_view, name="users"),
]

View File

@@ -0,0 +1,441 @@
from warnings import warn
import django_filters
from django.conf import settings
from django.contrib.auth import (
get_user_model,
update_session_auth_hash,
)
from django.contrib.auth.models import Group
from django.core.exceptions import FieldDoesNotExist, PermissionDenied
from django.db.models import Q
from django.forms import CheckboxSelectMultiple
from django.template import RequestContext
from django.utils.functional import cached_property
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy
from wagtail import hooks
from wagtail.admin.filters import DateRangePickerWidget, WagtailFilterSet
from wagtail.admin.search import SearchArea
from wagtail.admin.ui.tables import (
BulkActionsCheckboxColumn,
Column,
DateColumn,
StatusTagColumn,
TitleColumn,
)
from wagtail.admin.utils import get_user_display_name
from wagtail.admin.views import generic
from wagtail.admin.viewsets.model import ModelViewSet
from wagtail.admin.widgets.boolean_radio_select import BooleanRadioSelect
from wagtail.admin.widgets.button import (
BaseDropdownMenuButton,
ButtonWithDropdown,
)
from wagtail.compat import AUTH_USER_APP_LABEL, AUTH_USER_MODEL_NAME
from wagtail.coreutils import accepts_kwarg
from wagtail.users.forms import UserCreationForm, UserEditForm
from wagtail.users.utils import user_can_delete_user
from wagtail.utils.deprecation import RemovedInWagtail70Warning
from wagtail.utils.loading import get_custom_form
User = get_user_model()
# Typically we would check the permission 'auth.change_user' (and 'auth.add_user' /
# 'auth.delete_user') for user management actions, but this may vary according to
# the AUTH_USER_MODEL setting. These are no longer used in the codebase in favour
# of ModelPermissionPolicy, but are kept here for backwards compatibility.
add_user_perm = f"{AUTH_USER_APP_LABEL}.add_{AUTH_USER_MODEL_NAME.lower()}"
change_user_perm = "{}.change_{}".format(
AUTH_USER_APP_LABEL, AUTH_USER_MODEL_NAME.lower()
)
delete_user_perm = "{}.delete_{}".format(
AUTH_USER_APP_LABEL, AUTH_USER_MODEL_NAME.lower()
)
def get_user_creation_form():
form_setting = "WAGTAIL_USER_CREATION_FORM"
if hasattr(settings, form_setting):
warn(
"The `WAGTAIL_USER_CREATION_FORM` setting is deprecated. Use a custom "
"`UserViewSet` subclass and override `get_form_class()` instead.",
RemovedInWagtail70Warning,
)
return get_custom_form(form_setting)
else:
return UserCreationForm
def get_user_edit_form():
form_setting = "WAGTAIL_USER_EDIT_FORM"
if hasattr(settings, form_setting):
warn(
"The `WAGTAIL_USER_EDIT_FORM` setting is deprecated. Use a custom "
"`UserViewSet` subclass and override `get_form_class()` instead.",
RemovedInWagtail70Warning,
)
return get_custom_form(form_setting)
else:
return UserEditForm
def get_users_filter_query(q, model_fields):
conditions = Q()
for term in q.split():
if "username" in model_fields:
conditions |= Q(username__icontains=term)
if "first_name" in model_fields:
conditions |= Q(first_name__icontains=term)
if "last_name" in model_fields:
conditions |= Q(last_name__icontains=term)
if "email" in model_fields:
conditions |= Q(email__icontains=term)
return conditions
class UserColumn(TitleColumn):
cell_template_name = "wagtailusers/users/user_cell.html"
class UserFilterSet(WagtailFilterSet):
is_superuser = django_filters.BooleanFilter(
label=gettext_lazy("Administrator"),
widget=BooleanRadioSelect,
)
last_login = django_filters.DateFromToRangeFilter(
label=gettext_lazy("Last login"),
widget=DateRangePickerWidget,
)
group = django_filters.ModelMultipleChoiceFilter(
field_name="groups",
queryset=Group.objects.all(),
label=gettext_lazy("Group"),
widget=CheckboxSelectMultiple,
)
def __init__(self, data=None, queryset=None, *, request=None, prefix=None):
super().__init__(data, queryset, request=request, prefix=prefix)
try:
self._meta.model._meta.get_field("is_active")
except FieldDoesNotExist:
pass
else:
self.filters["is_active"] = django_filters.BooleanFilter(
field_name="is_active",
label=gettext_lazy("Active"),
widget=BooleanRadioSelect,
)
self.filters.move_to_end("is_active", last=False)
class Meta:
model = User
fields = []
class IndexView(generic.IndexView):
"""
Lists the users for management within the admin.
"""
template_name = "wagtailusers/users/index.html"
results_template_name = "wagtailusers/users/index_results.html"
add_item_label = gettext_lazy("Add a user")
context_object_name = "users"
is_searchable = True
page_title = gettext_lazy("Users")
show_other_searches = True
@cached_property
def columns(self):
_UserColumn = self._get_title_column_class(UserColumn)
return [
BulkActionsCheckboxColumn("bulk_actions", obj_type="user"),
_UserColumn(
"name",
accessor=lambda u: get_user_display_name(u),
label=gettext_lazy("Name"),
sort_key="name"
if self.model_fields.issuperset({"first_name", "last_name"})
else None,
get_url=self.get_edit_url,
classname="name",
),
Column(
self.model.USERNAME_FIELD,
accessor="get_username",
label=gettext_lazy("Username"),
sort_key=self.model.USERNAME_FIELD,
classname="username",
width="20%",
),
Column(
"is_superuser",
accessor=lambda u: gettext_lazy("Admin") if u.is_superuser else None,
label=gettext_lazy("Access level"),
sort_key="is_superuser",
classname="level",
width="10%",
),
StatusTagColumn(
"is_active",
accessor=lambda u: gettext_lazy("Active")
if u.is_active
else gettext_lazy("Inactive"),
primary=lambda u: u.is_active,
label=gettext_lazy("Status"),
sort_key="is_active" if "is_active" in self.model_fields else None,
classname="status",
width="10%",
),
DateColumn(
"last_login",
label=gettext_lazy("Last login"),
sort_key="last_login",
classname="last-login",
width="15%",
),
]
@cached_property
def model_fields(self):
return {f.name for f in User._meta.get_fields()}
def get_delete_url(self, instance):
if user_can_delete_user(self.request.user, instance):
return super().get_delete_url(instance)
def get_list_buttons(self, instance):
more_buttons = self.get_list_more_buttons(instance)
list_buttons = []
for hook in hooks.get_hooks("register_user_listing_buttons"):
if accepts_kwarg(hook, "request_user"):
hook_buttons = hook(user=instance, request_user=self.request.user)
else:
# old-style hook that accepts a context argument instead of request_user
hook_buttons = hook(RequestContext(self.request), instance)
warn(
"`register_user_listing_buttons` hook functions should accept a `request_user` argument instead of `context` -"
f" {hook.__module__}.{hook.__name__} needs to be updated",
category=RemovedInWagtail70Warning,
)
for button in hook_buttons:
if isinstance(button, BaseDropdownMenuButton):
# If the button is a dropdown menu, add it to the top-level
# because we do not support nested dropdowns
list_buttons.append(button)
else:
# Otherwise, add it to the default "More" dropdown
more_buttons.append(button)
list_buttons.append(
ButtonWithDropdown(
buttons=sorted(more_buttons),
icon_name="dots-horizontal",
attrs={
"aria-label": _("More options for '%(title)s'")
% {"title": str(instance)},
},
)
)
return sorted(list_buttons)
def get_base_queryset(self):
users = User._default_manager.all()
if "wagtail_userprofile" in self.model_fields:
users = users.select_related("wagtail_userprofile")
return users
def order_queryset(self, queryset):
if self.ordering == "name":
return queryset.order_by("last_name", "first_name")
if self.ordering == "-name":
return queryset.order_by("-last_name", "-first_name")
return super().order_queryset(queryset)
def search_queryset(self, queryset):
if self.is_searching:
conditions = get_users_filter_query(self.search_query, self.model_fields)
return queryset.filter(conditions)
return queryset
class CreateView(generic.CreateView):
"""
Provide the ability to create a user within the admin.
"""
success_message = gettext_lazy("User '%(object)s' created.")
page_title = gettext_lazy("Add user")
def run_before_hook(self):
return self.run_hook(
"before_create_user",
self.request,
)
def run_after_hook(self):
return self.run_hook(
"after_create_user",
self.request,
self.object,
)
class EditView(generic.EditView):
"""
Provide the ability to edit a user within the admin.
"""
success_message = gettext_lazy("User '%(object)s' updated.")
error_message = gettext_lazy("The user could not be saved due to errors.")
context_object_name = "user"
def setup(self, request, *args, **kwargs):
super().setup(request, *args, **kwargs)
self.object = self.get_object()
self.can_delete = user_can_delete_user(request.user, self.object)
self.editing_self = request.user == self.object
def save_instance(self):
instance = super().save_instance()
if self.object == self.request.user and "password1" in self.form.changed_data:
# User is changing their own password; need to update their session hash
update_session_auth_hash(self.request, self.object)
return instance
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs.update(
{
"editing_self": self.editing_self,
}
)
return kwargs
def run_before_hook(self):
return self.run_hook(
"before_edit_user",
self.request,
self.object,
)
def run_after_hook(self):
return self.run_hook(
"after_edit_user",
self.request,
self.object,
)
def get_page_subtitle(self):
return get_user_display_name(self.object)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["can_delete"] = self.can_delete
return context
class DeleteView(generic.DeleteView):
"""
Provide the ability to delete a user within the admin.
"""
page_title = gettext_lazy("Delete user")
success_message = gettext_lazy("User '%(object)s' deleted.")
context_object_name = "user"
def dispatch(self, request, *args, **kwargs):
self.object = self.get_object()
if not user_can_delete_user(self.request.user, self.object):
raise PermissionDenied
return super().dispatch(request, *args, **kwargs)
def run_before_hook(self):
return self.run_hook(
"before_delete_user",
self.request,
self.object,
)
def run_after_hook(self):
return self.run_hook(
"after_delete_user",
self.request,
self.object,
)
class HistoryView(generic.HistoryView):
def get_page_subtitle(self):
return get_user_display_name(self.object)
class UserViewSet(ModelViewSet):
icon = "user"
model = User
ordering = "name"
add_to_reference_index = False
filterset_class = UserFilterSet
menu_name = "users"
menu_label = gettext_lazy("Users")
menu_order = 600
add_to_settings_menu = True
index_view_class = IndexView
add_view_class = CreateView
edit_view_class = EditView
delete_view_class = DeleteView
history_view_class = HistoryView
template_prefix = "wagtailusers/users/"
def get_common_view_kwargs(self, **kwargs):
return super().get_common_view_kwargs(
**{
"usage_url_name": None,
**kwargs,
}
)
def get_form_class(self, for_update=False):
if for_update:
return get_user_edit_form()
return get_user_creation_form()
@cached_property
def search_area_class(self):
class UsersSearchArea(SearchArea):
def is_shown(search_area, request):
return self.permission_policy.user_has_any_permission(
request.user, {"add", "change", "delete"}
)
return UsersSearchArea
def get_search_area(self):
return self.search_area_class(
gettext_lazy("Users"),
self.get_url_name("index"),
name="users",
icon_name="user",
order=600,
)
def register_search_area(self):
hooks.register("register_admin_search_area", self.get_search_area)
def on_register(self):
super().on_register()
self.register_search_area()