import django_filters 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.urls import reverse 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.users.forms import UserCreationForm, UserEditForm from wagtail.users.utils import user_can_delete_user 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_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" # We don't set search_fields and the model may not be indexed, but we override # search_queryset, so we set is_searchable to True to enable search 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"): hook_buttons = hook(user=instance, request_user=self.request.user) 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 UserEditForm return UserCreationForm @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"), reverse(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()