441 lines
14 KiB
Python
441 lines
14 KiB
Python
from collections import OrderedDict
|
|
from functools import cached_property
|
|
|
|
from django.conf import settings
|
|
from django.contrib import messages
|
|
from django.contrib.auth import update_session_auth_hash
|
|
from django.contrib.auth import views as auth_views
|
|
from django.db import transaction
|
|
from django.forms import Media
|
|
from django.http import Http404
|
|
from django.shortcuts import redirect
|
|
from django.template.loader import render_to_string
|
|
from django.template.response import TemplateResponse
|
|
from django.urls import reverse, reverse_lazy
|
|
from django.utils.decorators import method_decorator
|
|
from django.utils.translation import gettext as _
|
|
from django.utils.translation import gettext_lazy, override
|
|
from django.views.decorators.debug import sensitive_post_parameters
|
|
from django.views.generic.base import TemplateView
|
|
|
|
from wagtail import hooks
|
|
from wagtail.admin.forms.account import (
|
|
AvatarPreferencesForm,
|
|
LocalePreferencesForm,
|
|
NameEmailForm,
|
|
NotificationPreferencesForm,
|
|
ThemePreferencesForm,
|
|
)
|
|
from wagtail.admin.forms.auth import LoginForm, PasswordChangeForm, PasswordResetForm
|
|
from wagtail.admin.localization import (
|
|
get_available_admin_languages,
|
|
get_available_admin_time_zones,
|
|
)
|
|
from wagtail.admin.views.generic import EditView, WagtailAdminTemplateMixin
|
|
from wagtail.log_actions import log
|
|
from wagtail.users.models import UserProfile
|
|
from wagtail.utils.loading import get_custom_form
|
|
|
|
|
|
def get_user_login_form():
|
|
form_setting = "WAGTAILADMIN_USER_LOGIN_FORM"
|
|
if hasattr(settings, form_setting):
|
|
return get_custom_form(form_setting)
|
|
else:
|
|
return LoginForm
|
|
|
|
|
|
def get_password_reset_form():
|
|
form_setting = "WAGTAILADMIN_USER_PASSWORD_RESET_FORM"
|
|
if hasattr(settings, form_setting):
|
|
return get_custom_form(form_setting)
|
|
else:
|
|
return PasswordResetForm
|
|
|
|
|
|
# Helper functions to check password management settings to enable/disable views as appropriate.
|
|
# These are functions rather than class-level constants so that they can be overridden in tests
|
|
# by override_settings
|
|
|
|
|
|
def password_management_enabled():
|
|
return getattr(settings, "WAGTAIL_PASSWORD_MANAGEMENT_ENABLED", True)
|
|
|
|
|
|
def email_management_enabled():
|
|
return getattr(settings, "WAGTAIL_EMAIL_MANAGEMENT_ENABLED", True)
|
|
|
|
|
|
def password_reset_enabled():
|
|
return getattr(
|
|
settings, "WAGTAIL_PASSWORD_RESET_ENABLED", password_management_enabled()
|
|
)
|
|
|
|
|
|
# Tabs
|
|
|
|
|
|
class SettingsTab:
|
|
def __init__(self, name, title, order=0):
|
|
self.name = name
|
|
self.title = title
|
|
self.order = order
|
|
|
|
|
|
profile_tab = SettingsTab("profile", gettext_lazy("Profile"), order=100)
|
|
notifications_tab = SettingsTab(
|
|
"notifications", gettext_lazy("Notifications"), order=200
|
|
)
|
|
|
|
|
|
# Panels
|
|
|
|
|
|
class BaseSettingsPanel:
|
|
name = ""
|
|
title = ""
|
|
tab = profile_tab
|
|
help_text = None
|
|
template_name = "wagtailadmin/account/settings_panels/base.html"
|
|
form_class = None
|
|
form_object = "user"
|
|
|
|
def __init__(self, request, user, profile):
|
|
self.request = request
|
|
self.user = user
|
|
self.profile = profile
|
|
|
|
def is_active(self):
|
|
"""
|
|
Returns True to display the panel.
|
|
"""
|
|
return True
|
|
|
|
def get_form(self):
|
|
"""
|
|
Returns an initialised form.
|
|
"""
|
|
kwargs = {
|
|
"instance": self.profile if self.form_object == "profile" else self.user,
|
|
"prefix": self.name,
|
|
}
|
|
|
|
if self.request.method == "POST":
|
|
return self.form_class(self.request.POST, self.request.FILES, **kwargs)
|
|
else:
|
|
return self.form_class(**kwargs)
|
|
|
|
def get_context_data(self):
|
|
"""
|
|
Returns the template context to use when rendering the template.
|
|
"""
|
|
return {"form": self.get_form()}
|
|
|
|
def render(self):
|
|
"""
|
|
Renders the panel using the template specified in .template_name and context from .get_context_data()
|
|
"""
|
|
return render_to_string(
|
|
self.template_name, self.get_context_data(), request=self.request
|
|
)
|
|
|
|
|
|
class NameEmailSettingsPanel(BaseSettingsPanel):
|
|
name = "name_email"
|
|
order = 100
|
|
form_class = NameEmailForm
|
|
|
|
@cached_property
|
|
def title(self):
|
|
if email_management_enabled():
|
|
return _("Name and Email")
|
|
return _("Name")
|
|
|
|
|
|
class AvatarSettingsPanel(BaseSettingsPanel):
|
|
name = "avatar"
|
|
title = gettext_lazy("Profile picture")
|
|
order = 300
|
|
template_name = "wagtailadmin/account/settings_panels/avatar.html"
|
|
form_class = AvatarPreferencesForm
|
|
form_object = "profile"
|
|
|
|
|
|
class NotificationsSettingsPanel(BaseSettingsPanel):
|
|
name = "notifications"
|
|
title = gettext_lazy("Notifications")
|
|
tab = notifications_tab
|
|
order = 100
|
|
form_class = NotificationPreferencesForm
|
|
form_object = "profile"
|
|
|
|
def is_active(self):
|
|
# Hide the panel if there are no notification preferences
|
|
return bool(self.get_form().fields)
|
|
|
|
|
|
class LocaleSettingsPanel(BaseSettingsPanel):
|
|
name = "locale"
|
|
title = gettext_lazy("Locale")
|
|
order = 400
|
|
form_class = LocalePreferencesForm
|
|
form_object = "profile"
|
|
|
|
def is_active(self):
|
|
return (
|
|
len(get_available_admin_languages()) > 1
|
|
or len(get_available_admin_time_zones()) > 1
|
|
)
|
|
|
|
|
|
class ThemeSettingsPanel(BaseSettingsPanel):
|
|
name = "theme"
|
|
title = gettext_lazy("Theme preferences")
|
|
order = 450
|
|
form_class = ThemePreferencesForm
|
|
form_object = "profile"
|
|
|
|
|
|
class ChangePasswordPanel(BaseSettingsPanel):
|
|
name = "password"
|
|
title = gettext_lazy("Password")
|
|
order = 500
|
|
form_class = PasswordChangeForm
|
|
|
|
def is_active(self):
|
|
return password_management_enabled() and self.user.has_usable_password()
|
|
|
|
def get_form(self):
|
|
# Note: don't bind the form unless a field is specified
|
|
# This prevents the validation error from displaying if the user wishes to ignore this
|
|
bind_form = False
|
|
if self.request.method == "POST":
|
|
bind_form = any(
|
|
[
|
|
self.request.POST.get(self.name + "-new_password1"),
|
|
self.request.POST.get(self.name + "-new_password2"),
|
|
]
|
|
)
|
|
|
|
if bind_form:
|
|
return self.form_class(self.user, self.request.POST, prefix=self.name)
|
|
else:
|
|
return self.form_class(self.user, prefix=self.name)
|
|
|
|
|
|
# Views
|
|
|
|
|
|
@method_decorator(sensitive_post_parameters(), name="post")
|
|
class AccountView(WagtailAdminTemplateMixin, TemplateView):
|
|
template_name = "wagtailadmin/account/account.html"
|
|
page_title = gettext_lazy("Account")
|
|
header_icon = "user"
|
|
|
|
def get_breadcrumbs_items(self):
|
|
return super().get_breadcrumbs_items() + [
|
|
{"url": "", "label": self.get_page_title()}
|
|
]
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
panels = self.get_panels()
|
|
context["panels_by_tab"] = self.get_panels_by_tab(panels)
|
|
context["menu_items"] = self.get_menu_items()
|
|
context["media"] = self.get_media(panels)
|
|
context["form_is_multipart"] = True
|
|
context["user"] = self.request.user
|
|
# Remove these when this view is refactored to a generic.EditView subclass.
|
|
# Avoid defining new translatable strings.
|
|
context["submit_button_label"] = EditView.submit_button_label
|
|
context["submit_button_active_label"] = EditView.submit_button_active_label
|
|
return context
|
|
|
|
def get_panels(self):
|
|
request = self.request
|
|
user = self.request.user
|
|
profile = UserProfile.get_for_user(user)
|
|
# Panels
|
|
panels = [
|
|
NameEmailSettingsPanel(request, user, profile),
|
|
AvatarSettingsPanel(request, user, profile),
|
|
NotificationsSettingsPanel(request, user, profile),
|
|
LocaleSettingsPanel(request, user, profile),
|
|
ThemeSettingsPanel(request, user, profile),
|
|
ChangePasswordPanel(request, user, profile),
|
|
]
|
|
for fn in hooks.get_hooks("register_account_settings_panel"):
|
|
panel = fn(request, user, profile)
|
|
if panel and panel.is_active():
|
|
panels.append(panel)
|
|
|
|
panels = [panel for panel in panels if panel.is_active()]
|
|
return panels
|
|
|
|
def get_panels_by_tab(self, panels):
|
|
# Get tabs and order them
|
|
tabs = list({panel.tab for panel in panels})
|
|
tabs.sort(key=lambda tab: tab.order)
|
|
|
|
# Get dict of tabs to ordered panels
|
|
panels_by_tab = OrderedDict([(tab, []) for tab in tabs])
|
|
for panel in panels:
|
|
panels_by_tab[panel.tab].append(panel)
|
|
for tab, tab_panels in panels_by_tab.items():
|
|
tab_panels.sort(key=lambda panel: panel.order)
|
|
return panels_by_tab
|
|
|
|
def get_menu_items(self):
|
|
# Menu items
|
|
menu_items = []
|
|
for fn in hooks.get_hooks("register_account_menu_item"):
|
|
item = fn(self.request)
|
|
if item:
|
|
menu_items.append(item)
|
|
return menu_items
|
|
|
|
def get_media(self, panels):
|
|
panel_forms = [panel.get_form() for panel in panels]
|
|
|
|
media = Media()
|
|
for form in panel_forms:
|
|
media += form.media
|
|
return media
|
|
|
|
def post(self, request):
|
|
panel_forms = [panel.get_form() for panel in self.get_panels()]
|
|
user = self.request.user
|
|
profile = UserProfile.get_for_user(user)
|
|
|
|
if all(form.is_valid() or not form.is_bound for form in panel_forms):
|
|
with transaction.atomic():
|
|
for form in panel_forms:
|
|
if form.is_bound:
|
|
form.save()
|
|
|
|
log(user, "wagtail.edit")
|
|
|
|
# Prevent a password change from logging this user out
|
|
update_session_auth_hash(request, user)
|
|
|
|
# Override the language when creating the success message
|
|
# If the user has changed their language in this request, the message should
|
|
# be in the new language, not the existing one
|
|
with override(profile.get_preferred_language()):
|
|
messages.success(
|
|
request, _("Your account settings have been changed successfully!")
|
|
)
|
|
|
|
return redirect("wagtailadmin_account")
|
|
|
|
return TemplateResponse(request, self.template_name, self.get_context_data())
|
|
|
|
|
|
class PasswordResetEnabledViewMixin:
|
|
"""
|
|
Class based view mixin that disables the view if password reset is disabled by one of the following settings:
|
|
- WAGTAIL_PASSWORD_RESET_ENABLED
|
|
- WAGTAIL_PASSWORD_MANAGEMENT_ENABLED
|
|
"""
|
|
|
|
def dispatch(self, *args, **kwargs):
|
|
if not password_reset_enabled():
|
|
raise Http404
|
|
|
|
return super().dispatch(*args, **kwargs)
|
|
|
|
|
|
class PasswordResetView(PasswordResetEnabledViewMixin, auth_views.PasswordResetView):
|
|
template_name = "wagtailadmin/account/password_reset/form.html"
|
|
email_template_name = "wagtailadmin/account/password_reset/email.txt"
|
|
subject_template_name = "wagtailadmin/account/password_reset/email_subject.txt"
|
|
success_url = reverse_lazy("wagtailadmin_password_reset_done")
|
|
|
|
def get_form_class(self):
|
|
return get_password_reset_form()
|
|
|
|
|
|
class PasswordResetDoneView(
|
|
PasswordResetEnabledViewMixin, auth_views.PasswordResetDoneView
|
|
):
|
|
template_name = "wagtailadmin/account/password_reset/done.html"
|
|
|
|
|
|
class PasswordResetConfirmView(
|
|
PasswordResetEnabledViewMixin, auth_views.PasswordResetConfirmView
|
|
):
|
|
template_name = "wagtailadmin/account/password_reset/confirm.html"
|
|
success_url = reverse_lazy("wagtailadmin_password_reset_complete")
|
|
|
|
|
|
class PasswordResetCompleteView(
|
|
PasswordResetEnabledViewMixin, auth_views.PasswordResetCompleteView
|
|
):
|
|
template_name = "wagtailadmin/account/password_reset/complete.html"
|
|
|
|
|
|
class LoginView(auth_views.LoginView):
|
|
template_name = "wagtailadmin/login.html"
|
|
|
|
def get_success_url(self):
|
|
return self.get_redirect_url() or reverse("wagtailadmin_home")
|
|
|
|
def get(self, *args, **kwargs):
|
|
# If user is already logged in, redirect them to the dashboard
|
|
if self.request.user.is_authenticated and self.request.user.has_perm(
|
|
"wagtailadmin.access_admin"
|
|
):
|
|
return redirect(self.get_success_url())
|
|
|
|
return super().get(*args, **kwargs)
|
|
|
|
def get_form_class(self):
|
|
return get_user_login_form()
|
|
|
|
def form_valid(self, form):
|
|
response = super().form_valid(form)
|
|
|
|
remember = form.cleaned_data.get("remember")
|
|
if remember:
|
|
self.request.session.set_expiry(settings.SESSION_COOKIE_AGE)
|
|
else:
|
|
self.request.session.set_expiry(0)
|
|
|
|
return response
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
|
|
context["show_password_reset"] = password_reset_enabled()
|
|
|
|
from django.contrib.auth import get_user_model
|
|
|
|
User = get_user_model()
|
|
context["username_field"] = User._meta.get_field(
|
|
User.USERNAME_FIELD
|
|
).verbose_name
|
|
|
|
return context
|
|
|
|
|
|
class LogoutView(auth_views.LogoutView):
|
|
next_page = "wagtailadmin_login"
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
response = super().dispatch(request, *args, **kwargs)
|
|
|
|
messages.success(self.request, _("You have been successfully logged out."))
|
|
# By default, logging out will generate a fresh sessionid cookie. We want to use the
|
|
# absence of sessionid as an indication that front-end pages are being viewed by a
|
|
# non-logged-in user and are therefore cacheable, so we forcibly delete the cookie here.
|
|
response.delete_cookie(
|
|
settings.SESSION_COOKIE_NAME,
|
|
domain=settings.SESSION_COOKIE_DOMAIN,
|
|
path=settings.SESSION_COOKIE_PATH,
|
|
)
|
|
|
|
# HACK: pretend that the session hasn't been modified, so that SessionMiddleware
|
|
# won't override the above and write a new cookie.
|
|
self.request.session.modified = False
|
|
|
|
return response
|