diff --git a/colab/accounts/forms.py b/colab/accounts/forms.py index 5c85fb0..2bb66df 100644 --- a/colab/accounts/forms.py +++ b/colab/accounts/forms.py @@ -1,22 +1,16 @@ # -*- coding: utf-8 -*- -from collections import OrderedDict from importlib import import_module from django import forms from django.conf import settings -from django.contrib.auth import authenticate, get_user_model -from django.contrib.auth.tokens import default_token_generator -from django.contrib.auth.forms import ReadOnlyPasswordHashField +from django.contrib.auth import get_user_model +from django.contrib.auth.forms import (ReadOnlyPasswordHashField, + SetPasswordForm, PasswordChangeForm) from django.core.urlresolvers import reverse from django.utils.functional import lazy from django.utils.translation import ugettext_lazy as _ from django.utils.safestring import mark_safe -from django.utils.text import capfirst -from django.template import loader -from django.utils.encoding import force_bytes -from django.utils.http import urlsafe_base64_encode -from django.contrib.sites.shortcuts import get_current_site from .signals import user_created from .utils.validators import validate_social_account @@ -169,7 +163,40 @@ class ListsForm(forms.Form): choices=lazy(get_lists_choices, list)()) -class UserCreationForm(UserForm): +class ColabSetPasswordFormMixin(object): + + def apply_custom_validators(self, password): + for app in settings.COLAB_APPS.values(): + if 'password_validators' in app: + for validator_path in app.get('password_validators'): + module_path, func_name = validator_path.rsplit('.', 1) + module = import_module(module_path) + validator_func = getattr(module, func_name, None) + if validator_func: + validator_func(password) + + return password + + def clean_new_password2(self): + try: + password = super(ColabSetPasswordFormMixin, self).clean_new_password2() + except AttributeError: + password = self.cleaned_data['new_password2'] + + self.apply_custom_validators(password) + return password + + def clean_password2(self): + try: + password = super(ColabSetPasswordFormMixin, self).clean_password2() + except AttributeError: + password = self.cleaned_data['password2'] + + self.apply_custom_validators(password) + return password + + +class UserCreationForm(UserForm, ColabSetPasswordFormMixin): """ A form that creates a user, with no privileges, from the given username and password. @@ -240,6 +267,8 @@ class UserCreationForm(UserForm): self.error_messages['password_mismatch'], code='password_mismatch', ) + + super(UserCreationForm, self).clean_password2() return password2 def save(self, commit=True): @@ -293,248 +322,9 @@ class UserChangeForm(forms.ModelForm): return self.initial["password"] -class AuthenticationForm(forms.Form): - """ - Base class for authenticating users. Extend this to get a form that accepts - username/password logins. - """ - username = forms.CharField(max_length=254) - password = forms.CharField(label=_("Password"), widget=forms.PasswordInput) - - error_messages = { - 'invalid_login': _("Please enter a correct %(username)s and password. " - "Note that both fields may be case-sensitive."), - 'inactive': _("This account is inactive."), - } - - def __init__(self, request=None, *args, **kwargs): - """ - The 'request' parameter is set for custom auth use by subclasses. - The form data comes in via the standard 'data' kwarg. - """ - self.request = request - self.user_cache = None - super(AuthenticationForm, self).__init__(*args, **kwargs) - - # Set the label for the "username" field. - UserModel = get_user_model() - self.username_field = UserModel._meta.get_field( - UserModel.USERNAME_FIELD) - if self.fields['username'].label is None: - self.fields['username'].label = capfirst( - self.username_field.verbose_name) - - def clean(self): - username = self.cleaned_data.get('username') - password = self.cleaned_data.get('password') - - if username and password: - self.user_cache = authenticate(username=username, - password=password) - if self.user_cache is None: - raise forms.ValidationError( - self.error_messages['invalid_login'], - code='invalid_login', - params={'username': self.username_field.verbose_name}, - ) - else: - self.confirm_login_allowed(self.user_cache) - - return self.cleaned_data - - def confirm_login_allowed(self, user): - """ - Controls whether the given User may log in. This is a policy setting, - independent of end-user authentication. This default behavior is to - allow login by active users, and reject login by inactive users. - If the given user cannot log in, this method should raise a - ``forms.ValidationError``. - If the given user may log in, this method should return None. - """ - if not user.is_active: - raise forms.ValidationError( - self.error_messages['inactive'], - code='inactive', - ) - - def get_user_id(self): - if self.user_cache: - return self.user_cache.id - return None - - def get_user(self): - return self.user_cache - - -class PasswordResetForm(forms.Form): - email = forms.EmailField(label=_("Email"), max_length=254) - - def save(self, domain_override=None, - subject_template_name='registration/password_reset_subject.txt', - email_template_name='registration/password_reset_email.html', - use_https=False, token_generator=default_token_generator, - from_email=None, request=None, html_email_template_name=None): - """ - Generates a one-use only link for resetting password and sends to the - user. - """ - from django.core.mail import send_mail - UserModel = get_user_model() - email = self.cleaned_data["email"] - active_users = UserModel._default_manager.filter( - email__iexact=email, is_active=True) - for user in active_users: - # Make sure that no email is sent to a user that actually has - # a password marked as unusable - if not user.has_usable_password(): - continue - if not domain_override: - current_site = get_current_site(request) - site_name = current_site.name - domain = current_site.domain - else: - site_name = domain = domain_override - c = { - 'email': user.email, - 'domain': domain, - 'site_name': site_name, - 'uid': urlsafe_base64_encode(force_bytes(user.pk)), - 'user': user, - 'token': token_generator.make_token(user), - 'protocol': 'https' if use_https else 'http', - } - subject = loader.render_to_string(subject_template_name, c) - # Email subject *must not* contain newlines - subject = ''.join(subject.splitlines()) - email = loader.render_to_string(email_template_name, c) - - if html_email_template_name: - html_email = loader.render_to_string(html_email_template_name, - c) - else: - html_email = None - send_mail(subject, email, from_email, [user.email], - html_message=html_email) - - -class SetPasswordForm(forms.Form): - """ - A form that lets a user change set their password without entering the old - password - """ - error_messages = { - 'password_mismatch': _("The two password fields didn't match."), - } - new_password1 = forms.CharField(label=_("New password"), - widget=forms.PasswordInput) - new_password2 = forms.CharField(label=_("New password confirmation"), - widget=forms.PasswordInput) +class ColabSetPasswordForm(ColabSetPasswordFormMixin, SetPasswordForm): + pass - def __init__(self, user, *args, **kwargs): - self.user = user - super(SetPasswordForm, self).__init__(*args, **kwargs) - def clean_new_password1(self): - password = self.cleaned_data.get('new_password1') - - for app in settings.COLAB_APPS.values(): - if 'password_validators' in app: - for validator_path in app.get('password_validators'): - module_path, func_name = validator_path.rsplit('.', 1) - module = import_module(module_path) - validator_func = getattr(module, func_name, None) - if validator_func: - validator_func(password) - - return password - - def clean_new_password2(self): - password1 = self.cleaned_data.get('new_password1') - password2 = self.cleaned_data.get('new_password2') - if password1 and password2: - if password1 != password2: - raise forms.ValidationError( - self.error_messages['password_mismatch'], - code='password_mismatch', - ) - return password2 - - def save(self, commit=True): - self.user.set_password(self.cleaned_data['new_password1']) - if commit: - self.user.save() - return self.user - - -class PasswordChangeForm(SetPasswordForm): - """ - A form that lets a user change their password by entering their old - password. - """ - error_messages = dict(SetPasswordForm.error_messages, **{ - 'password_incorrect': _("Your old password was entered incorrectly. " - "Please enter it again."), - }) - old_password = forms.CharField(label=_("Old password"), - widget=forms.PasswordInput) - - def clean_old_password(self): - """ - Validates that the old_password field is correct. - """ - old_password = self.cleaned_data["old_password"] - if not self.user.check_password(old_password): - raise forms.ValidationError( - self.error_messages['password_incorrect'], - code='password_incorrect', - ) - return old_password - -PasswordChangeForm.base_fields = OrderedDict( - (k, PasswordChangeForm.base_fields[k]) - for k in ['old_password', 'new_password1', 'new_password2'] -) - - -class AdminPasswordChangeForm(forms.Form): - """ - A form used to change the password of a user in the admin interface. - """ - error_messages = { - 'password_mismatch': _("The two password fields didn't match."), - } - password1 = forms.CharField(label=_("Password"), - widget=forms.PasswordInput) - password2 = forms.CharField(label=_("Password (again)"), - widget=forms.PasswordInput) - - def __init__(self, user, *args, **kwargs): - self.user = user - super(AdminPasswordChangeForm, self).__init__(*args, **kwargs) - - def clean_password2(self): - password1 = self.cleaned_data.get('password1') - password2 = self.cleaned_data.get('password2') - if password1 and password2: - if password1 != password2: - raise forms.ValidationError( - self.error_messages['password_mismatch'], - code='password_mismatch', - ) - return password2 - - def save(self, commit=True): - """ - Saves the new password. - """ - self.user.set_password(self.cleaned_data["password1"]) - if commit: - self.user.save() - return self.user - - def _get_changed_data(self): - data = super(AdminPasswordChangeForm, self).changed_data - for name in self.fields.keys(): - if name not in data: - return [] - return ['password'] +class ColabPasswordChangeForm(ColabSetPasswordFormMixin, PasswordChangeForm): + pass diff --git a/colab/accounts/tests/test_forms.py b/colab/accounts/tests/test_forms.py index dd8e775..9908f7b 100644 --- a/colab/accounts/tests/test_forms.py +++ b/colab/accounts/tests/test_forms.py @@ -10,7 +10,7 @@ from django.test import TestCase, override_settings from django.core.urlresolvers import reverse from colab.accounts.forms import UserCreationForm, UserChangeForm,\ - UserUpdateForm, UserForm, get_lists_choices, SetPasswordForm + UserUpdateForm, UserForm, get_lists_choices, ColabSetPasswordForm from colab.accounts import forms as accounts_forms from colab.accounts.models import User from colab.accounts.utils import mailman @@ -37,21 +37,21 @@ class SetPasswordFormTestCase(TestCase): 'new_password2': '12345'} def test_no_custom_validators(self): - form = SetPasswordForm(self.user, data=self.valid_form_data) + form = ColabSetPasswordForm(self.user, data=self.valid_form_data) self.assertTrue(form.is_valid(), True) @override_settings(COLAB_APPS=TEST_COLAB_APPS) @patch('colab.accounts.tests.utils.password_validator') def test_custom_validator(self, validator): - form = SetPasswordForm(self.user, data=self.valid_form_data) + form = ColabSetPasswordForm(self.user, data=self.valid_form_data) self.assertTrue(form.is_valid()) validator.assert_called_with('12345') @override_settings(COLAB_APPS=TEST_COLAB_APPS) def test_custom_validator_raise_error(self): - form = SetPasswordForm(self.user, data=self.valid_form_data) + form = ColabSetPasswordForm(self.user, data=self.valid_form_data) self.assertFalse(form.is_valid()) - self.assertEqual(form.errors['new_password1'][0], 'Test error') + self.assertEqual(form.errors['new_password2'][0], 'Test error') class FormTest(TestCase): diff --git a/colab/accounts/urls.py b/colab/accounts/urls.py index de4b0bc..cb3c973 100644 --- a/colab/accounts/urls.py +++ b/colab/accounts/urls.py @@ -5,7 +5,7 @@ from django.contrib.auth import views as auth_views from .views import (UserProfileDetailView, UserProfileUpdateView, ManageUserSubscriptionsView) -from .forms import PasswordChangeForm +from .forms import ColabPasswordChangeForm, ColabSetPasswordForm urlpatterns = patterns('', @@ -21,7 +21,8 @@ urlpatterns = patterns('', url(r'^password-reset-confirm/(?P[0-9A-Za-z]+)-(?P.+)/$', auth_views.password_reset_confirm, - {'template_name':'registration/password_reset_confirm_custom.html'}, + {'template_name':'registration/password_reset_confirm_custom.html', + 'set_password_form': ColabSetPasswordForm}, name="password_reset_confirm"), url(r'^password-reset/?$', auth_views.password_reset, @@ -30,7 +31,7 @@ urlpatterns = patterns('', url(r'^change-password/?$', auth_views.password_change, {'template_name': 'registration/password_change_form_custom.html', - 'password_change_form': PasswordChangeForm}, + 'password_change_form': ColabPasswordChangeForm}, name='password_change'), url(r'^change-password-done/?$', -- libgit2 0.21.2