From ef27853a05d9c65ba9a14e3588890783b19d4fcf Mon Sep 17 00:00:00 2001 From: Mike Dingjan Date: Tue, 5 Sep 2017 14:40:32 +0200 Subject: [PATCH] Allow overriding login form via ``WAGTAILADMIN_USER_LOGIN_FORM`` setting --- CHANGELOG.txt | 1 + docs/advanced_topics/settings.rst | 7 ++++++ docs/releases/2.0.rst | 1 + wagtail/admin/forms.py | 23 +++++++++++-------- .../admin/templates/wagtailadmin/login.html | 12 ++++++++++ wagtail/admin/tests/test_forms.py | 18 +++++++++++++++ wagtail/admin/views/account.py | 11 ++++++++- wagtail/users/views/users.py | 17 +++----------- wagtail/utils/loading.py | 14 +++++++++++ 9 files changed, 80 insertions(+), 24 deletions(-) create mode 100644 wagtail/admin/tests/test_forms.py create mode 100644 wagtail/utils/loading.py diff --git a/CHANGELOG.txt b/CHANGELOG.txt index c075a7b2a1..297b5b9533 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -24,6 +24,7 @@ Changelog * Added `heading` kwarg to `InlinePanel` to allow heading to be set independently of button label (Adrian Turjak) * The value type returned from a `StructBlock` can now be customised (LB (Ben Johnston)) * Added `bgcolor` image operation (Karl Hobley) + * Added `WAGTAILADMIN_USER_LOGIN_FORM` setting for overriding the admin login form (Mike Dingjan) * Fix: Do not remove stopwords when generating slugs from non-ASCII titles, to avoid issues with incorrect word boundaries (Sævar Öfjörð Magnússon) * Fix: The PostgreSQL search backend now preserves ordering of the `QuerySet` when searching with `order_by_relevance=False` (Bertrand Bordage) * Fix: Using `modeladmin_register` as a decorator no longer replaces the decorated class with `None` (Tim Heap) diff --git a/docs/advanced_topics/settings.rst b/docs/advanced_topics/settings.rst index 4d69bd09c6..f0cb3f3058 100644 --- a/docs/advanced_topics/settings.rst +++ b/docs/advanced_topics/settings.rst @@ -224,6 +224,13 @@ Dashboard This setting lets you change the number of items shown at 'Your most recent edits' on the dashboard. +.. code-block:: python + + WAGTAILADMIN_USER_LOGIN_FORM = 'users.forms.LoginForm' + +Allows the default ``LoginForm`` to be extended with extra fields. + + Images ------ diff --git a/docs/releases/2.0.rst b/docs/releases/2.0.rst index efd96a4260..d618da7d39 100644 --- a/docs/releases/2.0.rst +++ b/docs/releases/2.0.rst @@ -37,6 +37,7 @@ Other features * Added ``heading`` kwarg to ``InlinePanel`` to allow heading to be set independently of button label (Adrian Turjak) * The value type returned from a ``StructBlock`` can now be customised. See :ref:`custom_value_class_for_structblock` (LB (Ben Johnston)) * Added `bgcolor` image operation (Karl Hobley) + * Added ``WAGTAILADMIN_USER_LOGIN_FORM`` setting for overriding the admin login form (Mike Dingjan) Bug fixes ~~~~~~~~~ diff --git a/wagtail/admin/forms.py b/wagtail/admin/forms.py index 5c76dbea43..812c4131b9 100644 --- a/wagtail/admin/forms.py +++ b/wagtail/admin/forms.py @@ -64,19 +64,24 @@ class EmailLinkChooserForm(forms.Form): class LoginForm(AuthenticationForm): username = forms.CharField( - max_length=254, - widget=forms.TextInput(attrs={'tabindex': '1'}), - ) + max_length=254, widget=forms.TextInput(attrs={'tabindex': '1'})) + password = forms.CharField( - widget=forms.PasswordInput(attrs={'placeholder': ugettext_lazy("Enter password"), - 'tabindex': '2', - }), - ) + widget=forms.PasswordInput(attrs={ + 'tabindex': '2', + 'placeholder': ugettext_lazy("Enter password"), + })) def __init__(self, request=None, *args, **kwargs): super().__init__(request=request, *args, **kwargs) - self.fields['username'].widget.attrs['placeholder'] = ugettext_lazy("Enter your %s") \ - % self.username_field.verbose_name + self.fields['username'].widget.attrs['placeholder'] = ( + ugettext_lazy("Enter your %s") % self.username_field.verbose_name) + + @property + def extra_fields(self): + for field_name, field in self.fields.items(): + if field_name not in ['username', 'password']: + yield field_name, field class PasswordResetForm(PasswordResetForm): diff --git a/wagtail/admin/templates/wagtailadmin/login.html b/wagtail/admin/templates/wagtailadmin/login.html index 4a44bf4056..ae293c4bd4 100644 --- a/wagtail/admin/templates/wagtailadmin/login.html +++ b/wagtail/admin/templates/wagtailadmin/login.html @@ -56,6 +56,18 @@

{% trans "Forgotten it?" %}

{% endif %} + + {% block extra_fields %} + {% for field_name, field in form.extra_fields %} +
  • + {{ field.label_tag }} +
    + {{ field }} +
    +
  • + {% endfor %} + {% endblock extra_fields %} + {% comment %} Removed until functionality exists
  • diff --git a/wagtail/admin/tests/test_forms.py b/wagtail/admin/tests/test_forms.py new file mode 100644 index 0000000000..065a8d07ce --- /dev/null +++ b/wagtail/admin/tests/test_forms.py @@ -0,0 +1,18 @@ +from django.forms.fields import CharField +from django.test import TestCase + +from wagtail.admin.forms import LoginForm + + +class CustomLoginForm(LoginForm): + captcha = CharField( + label='Captcha', help_text="should be in extra_fields()") + + +class TestLoginForm(TestCase): + + def test_extra_fields(self): + form = CustomLoginForm() + self.assertEqual(list(form.extra_fields), [ + ('captcha', form.fields['captcha']) + ]) diff --git a/wagtail/admin/views/account.py b/wagtail/admin/views/account.py index caddccbbda..fceda444b7 100644 --- a/wagtail/admin/views/account.py +++ b/wagtail/admin/views/account.py @@ -17,6 +17,15 @@ from wagtail.admin.utils import get_available_admin_languages from wagtail.core.models import UserPagePermissionsProxy from wagtail.users.forms import NotificationPreferencesForm, PreferredLanguageForm 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 forms.LoginForm # Helper functions to check password management settings to enable/disable views as appropriate. @@ -136,7 +145,7 @@ def login(request): return auth_views.login( request, template_name='wagtailadmin/login.html', - authentication_form=forms.LoginForm, + authentication_form=get_user_login_form(), extra_context={ 'show_password_reset': password_reset_enabled(), 'username_field': get_user_model().USERNAME_FIELD, diff --git a/wagtail/users/views/users.py b/wagtail/users/views/users.py index c3e1522ed2..2181e9da39 100644 --- a/wagtail/users/views/users.py +++ b/wagtail/users/views/users.py @@ -1,13 +1,12 @@ from django.conf import settings from django.contrib.auth import get_user_model -from django.core.exceptions import ImproperlyConfigured from django.db.models import Q from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse -from django.utils.module_loading import import_string from django.utils.translation import ugettext as _ from django.views.decorators.vary import vary_on_headers +from wagtail.utils.loading import get_custom_form from wagtail.utils.pagination import paginate from wagtail.admin import messages from wagtail.admin.forms import SearchForm @@ -28,20 +27,10 @@ change_user_perm = "{0}.change_{1}".format(AUTH_USER_APP_LABEL, AUTH_USER_MODEL_ delete_user_perm = "{0}.delete_{1}".format(AUTH_USER_APP_LABEL, AUTH_USER_MODEL_NAME.lower()) -def get_custom_user_form(form_setting): - try: - return import_string(getattr(settings, form_setting)) - except ImportError: - raise ImproperlyConfigured( - "%s refers to a form '%s' that is not available" % - (form_setting, getattr(settings, form_setting)) - ) - - def get_user_creation_form(): form_setting = 'WAGTAIL_USER_CREATION_FORM' if hasattr(settings, form_setting): - return get_custom_user_form(form_setting) + return get_custom_form(form_setting) else: return UserCreationForm @@ -49,7 +38,7 @@ def get_user_creation_form(): def get_user_edit_form(): form_setting = 'WAGTAIL_USER_EDIT_FORM' if hasattr(settings, form_setting): - return get_custom_user_form(form_setting) + return get_custom_form(form_setting) else: return UserEditForm diff --git a/wagtail/utils/loading.py b/wagtail/utils/loading.py new file mode 100644 index 0000000000..a1964791eb --- /dev/null +++ b/wagtail/utils/loading.py @@ -0,0 +1,14 @@ +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured +from django.utils.module_loading import import_string + + +def get_custom_form(form_setting): + """Return custom form class if defined and available""" + try: + return import_string(getattr(settings, form_setting)) + except ImportError: + raise ImproperlyConfigured( + "%s refers to a form '%s' that is not available" % + (form_setting, getattr(settings, form_setting)) + )