Commit 291441f94908c53e40ded8484f52b400995c6f94
1 parent
dd3df95b
Exists in
master
and in
4 other branches
Fixed bug for other ways of password set/reset
Signed-off-by: Gustavo Jaruga <darksshades@gmail.com> Signed-off-by: Sergio Oliveira <seocam@seocam.com>
Showing
3 changed files
with
52 additions
and
261 deletions
Show diff stats
colab/accounts/forms.py
| 1 | 1 | # -*- coding: utf-8 -*- |
| 2 | 2 | |
| 3 | -from collections import OrderedDict | |
| 4 | 3 | from importlib import import_module |
| 5 | 4 | |
| 6 | 5 | from django import forms |
| 7 | 6 | from django.conf import settings |
| 8 | -from django.contrib.auth import authenticate, get_user_model | |
| 9 | -from django.contrib.auth.tokens import default_token_generator | |
| 10 | -from django.contrib.auth.forms import ReadOnlyPasswordHashField | |
| 7 | +from django.contrib.auth import get_user_model | |
| 8 | +from django.contrib.auth.forms import (ReadOnlyPasswordHashField, | |
| 9 | + SetPasswordForm, PasswordChangeForm) | |
| 11 | 10 | from django.core.urlresolvers import reverse |
| 12 | 11 | from django.utils.functional import lazy |
| 13 | 12 | from django.utils.translation import ugettext_lazy as _ |
| 14 | 13 | from django.utils.safestring import mark_safe |
| 15 | -from django.utils.text import capfirst | |
| 16 | -from django.template import loader | |
| 17 | -from django.utils.encoding import force_bytes | |
| 18 | -from django.utils.http import urlsafe_base64_encode | |
| 19 | -from django.contrib.sites.shortcuts import get_current_site | |
| 20 | 14 | |
| 21 | 15 | from .signals import user_created |
| 22 | 16 | from .utils.validators import validate_social_account |
| ... | ... | @@ -169,7 +163,40 @@ class ListsForm(forms.Form): |
| 169 | 163 | choices=lazy(get_lists_choices, list)()) |
| 170 | 164 | |
| 171 | 165 | |
| 172 | -class UserCreationForm(UserForm): | |
| 166 | +class ColabSetPasswordFormMixin(object): | |
| 167 | + | |
| 168 | + def apply_custom_validators(self, password): | |
| 169 | + for app in settings.COLAB_APPS.values(): | |
| 170 | + if 'password_validators' in app: | |
| 171 | + for validator_path in app.get('password_validators'): | |
| 172 | + module_path, func_name = validator_path.rsplit('.', 1) | |
| 173 | + module = import_module(module_path) | |
| 174 | + validator_func = getattr(module, func_name, None) | |
| 175 | + if validator_func: | |
| 176 | + validator_func(password) | |
| 177 | + | |
| 178 | + return password | |
| 179 | + | |
| 180 | + def clean_new_password2(self): | |
| 181 | + try: | |
| 182 | + password = super(ColabSetPasswordFormMixin, self).clean_new_password2() | |
| 183 | + except AttributeError: | |
| 184 | + password = self.cleaned_data['new_password2'] | |
| 185 | + | |
| 186 | + self.apply_custom_validators(password) | |
| 187 | + return password | |
| 188 | + | |
| 189 | + def clean_password2(self): | |
| 190 | + try: | |
| 191 | + password = super(ColabSetPasswordFormMixin, self).clean_password2() | |
| 192 | + except AttributeError: | |
| 193 | + password = self.cleaned_data['password2'] | |
| 194 | + | |
| 195 | + self.apply_custom_validators(password) | |
| 196 | + return password | |
| 197 | + | |
| 198 | + | |
| 199 | +class UserCreationForm(UserForm, ColabSetPasswordFormMixin): | |
| 173 | 200 | """ |
| 174 | 201 | A form that creates a user, with no privileges, from the given username and |
| 175 | 202 | password. |
| ... | ... | @@ -240,6 +267,8 @@ class UserCreationForm(UserForm): |
| 240 | 267 | self.error_messages['password_mismatch'], |
| 241 | 268 | code='password_mismatch', |
| 242 | 269 | ) |
| 270 | + | |
| 271 | + super(UserCreationForm, self).clean_password2() | |
| 243 | 272 | return password2 |
| 244 | 273 | |
| 245 | 274 | def save(self, commit=True): |
| ... | ... | @@ -293,248 +322,9 @@ class UserChangeForm(forms.ModelForm): |
| 293 | 322 | return self.initial["password"] |
| 294 | 323 | |
| 295 | 324 | |
| 296 | -class AuthenticationForm(forms.Form): | |
| 297 | - """ | |
| 298 | - Base class for authenticating users. Extend this to get a form that accepts | |
| 299 | - username/password logins. | |
| 300 | - """ | |
| 301 | - username = forms.CharField(max_length=254) | |
| 302 | - password = forms.CharField(label=_("Password"), widget=forms.PasswordInput) | |
| 303 | - | |
| 304 | - error_messages = { | |
| 305 | - 'invalid_login': _("Please enter a correct %(username)s and password. " | |
| 306 | - "Note that both fields may be case-sensitive."), | |
| 307 | - 'inactive': _("This account is inactive."), | |
| 308 | - } | |
| 309 | - | |
| 310 | - def __init__(self, request=None, *args, **kwargs): | |
| 311 | - """ | |
| 312 | - The 'request' parameter is set for custom auth use by subclasses. | |
| 313 | - The form data comes in via the standard 'data' kwarg. | |
| 314 | - """ | |
| 315 | - self.request = request | |
| 316 | - self.user_cache = None | |
| 317 | - super(AuthenticationForm, self).__init__(*args, **kwargs) | |
| 318 | - | |
| 319 | - # Set the label for the "username" field. | |
| 320 | - UserModel = get_user_model() | |
| 321 | - self.username_field = UserModel._meta.get_field( | |
| 322 | - UserModel.USERNAME_FIELD) | |
| 323 | - if self.fields['username'].label is None: | |
| 324 | - self.fields['username'].label = capfirst( | |
| 325 | - self.username_field.verbose_name) | |
| 326 | - | |
| 327 | - def clean(self): | |
| 328 | - username = self.cleaned_data.get('username') | |
| 329 | - password = self.cleaned_data.get('password') | |
| 330 | - | |
| 331 | - if username and password: | |
| 332 | - self.user_cache = authenticate(username=username, | |
| 333 | - password=password) | |
| 334 | - if self.user_cache is None: | |
| 335 | - raise forms.ValidationError( | |
| 336 | - self.error_messages['invalid_login'], | |
| 337 | - code='invalid_login', | |
| 338 | - params={'username': self.username_field.verbose_name}, | |
| 339 | - ) | |
| 340 | - else: | |
| 341 | - self.confirm_login_allowed(self.user_cache) | |
| 342 | - | |
| 343 | - return self.cleaned_data | |
| 344 | - | |
| 345 | - def confirm_login_allowed(self, user): | |
| 346 | - """ | |
| 347 | - Controls whether the given User may log in. This is a policy setting, | |
| 348 | - independent of end-user authentication. This default behavior is to | |
| 349 | - allow login by active users, and reject login by inactive users. | |
| 350 | - If the given user cannot log in, this method should raise a | |
| 351 | - ``forms.ValidationError``. | |
| 352 | - If the given user may log in, this method should return None. | |
| 353 | - """ | |
| 354 | - if not user.is_active: | |
| 355 | - raise forms.ValidationError( | |
| 356 | - self.error_messages['inactive'], | |
| 357 | - code='inactive', | |
| 358 | - ) | |
| 359 | - | |
| 360 | - def get_user_id(self): | |
| 361 | - if self.user_cache: | |
| 362 | - return self.user_cache.id | |
| 363 | - return None | |
| 364 | - | |
| 365 | - def get_user(self): | |
| 366 | - return self.user_cache | |
| 367 | - | |
| 368 | - | |
| 369 | -class PasswordResetForm(forms.Form): | |
| 370 | - email = forms.EmailField(label=_("Email"), max_length=254) | |
| 371 | - | |
| 372 | - def save(self, domain_override=None, | |
| 373 | - subject_template_name='registration/password_reset_subject.txt', | |
| 374 | - email_template_name='registration/password_reset_email.html', | |
| 375 | - use_https=False, token_generator=default_token_generator, | |
| 376 | - from_email=None, request=None, html_email_template_name=None): | |
| 377 | - """ | |
| 378 | - Generates a one-use only link for resetting password and sends to the | |
| 379 | - user. | |
| 380 | - """ | |
| 381 | - from django.core.mail import send_mail | |
| 382 | - UserModel = get_user_model() | |
| 383 | - email = self.cleaned_data["email"] | |
| 384 | - active_users = UserModel._default_manager.filter( | |
| 385 | - email__iexact=email, is_active=True) | |
| 386 | - for user in active_users: | |
| 387 | - # Make sure that no email is sent to a user that actually has | |
| 388 | - # a password marked as unusable | |
| 389 | - if not user.has_usable_password(): | |
| 390 | - continue | |
| 391 | - if not domain_override: | |
| 392 | - current_site = get_current_site(request) | |
| 393 | - site_name = current_site.name | |
| 394 | - domain = current_site.domain | |
| 395 | - else: | |
| 396 | - site_name = domain = domain_override | |
| 397 | - c = { | |
| 398 | - 'email': user.email, | |
| 399 | - 'domain': domain, | |
| 400 | - 'site_name': site_name, | |
| 401 | - 'uid': urlsafe_base64_encode(force_bytes(user.pk)), | |
| 402 | - 'user': user, | |
| 403 | - 'token': token_generator.make_token(user), | |
| 404 | - 'protocol': 'https' if use_https else 'http', | |
| 405 | - } | |
| 406 | - subject = loader.render_to_string(subject_template_name, c) | |
| 407 | - # Email subject *must not* contain newlines | |
| 408 | - subject = ''.join(subject.splitlines()) | |
| 409 | - email = loader.render_to_string(email_template_name, c) | |
| 410 | - | |
| 411 | - if html_email_template_name: | |
| 412 | - html_email = loader.render_to_string(html_email_template_name, | |
| 413 | - c) | |
| 414 | - else: | |
| 415 | - html_email = None | |
| 416 | - send_mail(subject, email, from_email, [user.email], | |
| 417 | - html_message=html_email) | |
| 418 | - | |
| 419 | - | |
| 420 | -class SetPasswordForm(forms.Form): | |
| 421 | - """ | |
| 422 | - A form that lets a user change set their password without entering the old | |
| 423 | - password | |
| 424 | - """ | |
| 425 | - error_messages = { | |
| 426 | - 'password_mismatch': _("The two password fields didn't match."), | |
| 427 | - } | |
| 428 | - new_password1 = forms.CharField(label=_("New password"), | |
| 429 | - widget=forms.PasswordInput) | |
| 430 | - new_password2 = forms.CharField(label=_("New password confirmation"), | |
| 431 | - widget=forms.PasswordInput) | |
| 325 | +class ColabSetPasswordForm(ColabSetPasswordFormMixin, SetPasswordForm): | |
| 326 | + pass | |
| 432 | 327 | |
| 433 | - def __init__(self, user, *args, **kwargs): | |
| 434 | - self.user = user | |
| 435 | - super(SetPasswordForm, self).__init__(*args, **kwargs) | |
| 436 | 328 | |
| 437 | - def clean_new_password1(self): | |
| 438 | - password = self.cleaned_data.get('new_password1') | |
| 439 | - | |
| 440 | - for app in settings.COLAB_APPS.values(): | |
| 441 | - if 'password_validators' in app: | |
| 442 | - for validator_path in app.get('password_validators'): | |
| 443 | - module_path, func_name = validator_path.rsplit('.', 1) | |
| 444 | - module = import_module(module_path) | |
| 445 | - validator_func = getattr(module, func_name, None) | |
| 446 | - if validator_func: | |
| 447 | - validator_func(password) | |
| 448 | - | |
| 449 | - return password | |
| 450 | - | |
| 451 | - def clean_new_password2(self): | |
| 452 | - password1 = self.cleaned_data.get('new_password1') | |
| 453 | - password2 = self.cleaned_data.get('new_password2') | |
| 454 | - if password1 and password2: | |
| 455 | - if password1 != password2: | |
| 456 | - raise forms.ValidationError( | |
| 457 | - self.error_messages['password_mismatch'], | |
| 458 | - code='password_mismatch', | |
| 459 | - ) | |
| 460 | - return password2 | |
| 461 | - | |
| 462 | - def save(self, commit=True): | |
| 463 | - self.user.set_password(self.cleaned_data['new_password1']) | |
| 464 | - if commit: | |
| 465 | - self.user.save() | |
| 466 | - return self.user | |
| 467 | - | |
| 468 | - | |
| 469 | -class PasswordChangeForm(SetPasswordForm): | |
| 470 | - """ | |
| 471 | - A form that lets a user change their password by entering their old | |
| 472 | - password. | |
| 473 | - """ | |
| 474 | - error_messages = dict(SetPasswordForm.error_messages, **{ | |
| 475 | - 'password_incorrect': _("Your old password was entered incorrectly. " | |
| 476 | - "Please enter it again."), | |
| 477 | - }) | |
| 478 | - old_password = forms.CharField(label=_("Old password"), | |
| 479 | - widget=forms.PasswordInput) | |
| 480 | - | |
| 481 | - def clean_old_password(self): | |
| 482 | - """ | |
| 483 | - Validates that the old_password field is correct. | |
| 484 | - """ | |
| 485 | - old_password = self.cleaned_data["old_password"] | |
| 486 | - if not self.user.check_password(old_password): | |
| 487 | - raise forms.ValidationError( | |
| 488 | - self.error_messages['password_incorrect'], | |
| 489 | - code='password_incorrect', | |
| 490 | - ) | |
| 491 | - return old_password | |
| 492 | - | |
| 493 | -PasswordChangeForm.base_fields = OrderedDict( | |
| 494 | - (k, PasswordChangeForm.base_fields[k]) | |
| 495 | - for k in ['old_password', 'new_password1', 'new_password2'] | |
| 496 | -) | |
| 497 | - | |
| 498 | - | |
| 499 | -class AdminPasswordChangeForm(forms.Form): | |
| 500 | - """ | |
| 501 | - A form used to change the password of a user in the admin interface. | |
| 502 | - """ | |
| 503 | - error_messages = { | |
| 504 | - 'password_mismatch': _("The two password fields didn't match."), | |
| 505 | - } | |
| 506 | - password1 = forms.CharField(label=_("Password"), | |
| 507 | - widget=forms.PasswordInput) | |
| 508 | - password2 = forms.CharField(label=_("Password (again)"), | |
| 509 | - widget=forms.PasswordInput) | |
| 510 | - | |
| 511 | - def __init__(self, user, *args, **kwargs): | |
| 512 | - self.user = user | |
| 513 | - super(AdminPasswordChangeForm, self).__init__(*args, **kwargs) | |
| 514 | - | |
| 515 | - def clean_password2(self): | |
| 516 | - password1 = self.cleaned_data.get('password1') | |
| 517 | - password2 = self.cleaned_data.get('password2') | |
| 518 | - if password1 and password2: | |
| 519 | - if password1 != password2: | |
| 520 | - raise forms.ValidationError( | |
| 521 | - self.error_messages['password_mismatch'], | |
| 522 | - code='password_mismatch', | |
| 523 | - ) | |
| 524 | - return password2 | |
| 525 | - | |
| 526 | - def save(self, commit=True): | |
| 527 | - """ | |
| 528 | - Saves the new password. | |
| 529 | - """ | |
| 530 | - self.user.set_password(self.cleaned_data["password1"]) | |
| 531 | - if commit: | |
| 532 | - self.user.save() | |
| 533 | - return self.user | |
| 534 | - | |
| 535 | - def _get_changed_data(self): | |
| 536 | - data = super(AdminPasswordChangeForm, self).changed_data | |
| 537 | - for name in self.fields.keys(): | |
| 538 | - if name not in data: | |
| 539 | - return [] | |
| 540 | - return ['password'] | |
| 329 | +class ColabPasswordChangeForm(ColabSetPasswordFormMixin, PasswordChangeForm): | |
| 330 | + pass | ... | ... |
colab/accounts/tests/test_forms.py
| ... | ... | @@ -10,7 +10,7 @@ from django.test import TestCase, override_settings |
| 10 | 10 | from django.core.urlresolvers import reverse |
| 11 | 11 | |
| 12 | 12 | from colab.accounts.forms import UserCreationForm, UserChangeForm,\ |
| 13 | - UserUpdateForm, UserForm, get_lists_choices, SetPasswordForm | |
| 13 | + UserUpdateForm, UserForm, get_lists_choices, ColabSetPasswordForm | |
| 14 | 14 | from colab.accounts import forms as accounts_forms |
| 15 | 15 | from colab.accounts.models import User |
| 16 | 16 | from colab.accounts.utils import mailman |
| ... | ... | @@ -37,21 +37,21 @@ class SetPasswordFormTestCase(TestCase): |
| 37 | 37 | 'new_password2': '12345'} |
| 38 | 38 | |
| 39 | 39 | def test_no_custom_validators(self): |
| 40 | - form = SetPasswordForm(self.user, data=self.valid_form_data) | |
| 40 | + form = ColabSetPasswordForm(self.user, data=self.valid_form_data) | |
| 41 | 41 | self.assertTrue(form.is_valid(), True) |
| 42 | 42 | |
| 43 | 43 | @override_settings(COLAB_APPS=TEST_COLAB_APPS) |
| 44 | 44 | @patch('colab.accounts.tests.utils.password_validator') |
| 45 | 45 | def test_custom_validator(self, validator): |
| 46 | - form = SetPasswordForm(self.user, data=self.valid_form_data) | |
| 46 | + form = ColabSetPasswordForm(self.user, data=self.valid_form_data) | |
| 47 | 47 | self.assertTrue(form.is_valid()) |
| 48 | 48 | validator.assert_called_with('12345') |
| 49 | 49 | |
| 50 | 50 | @override_settings(COLAB_APPS=TEST_COLAB_APPS) |
| 51 | 51 | def test_custom_validator_raise_error(self): |
| 52 | - form = SetPasswordForm(self.user, data=self.valid_form_data) | |
| 52 | + form = ColabSetPasswordForm(self.user, data=self.valid_form_data) | |
| 53 | 53 | self.assertFalse(form.is_valid()) |
| 54 | - self.assertEqual(form.errors['new_password1'][0], 'Test error') | |
| 54 | + self.assertEqual(form.errors['new_password2'][0], 'Test error') | |
| 55 | 55 | |
| 56 | 56 | |
| 57 | 57 | class FormTest(TestCase): | ... | ... |
colab/accounts/urls.py
| ... | ... | @@ -5,7 +5,7 @@ from django.contrib.auth import views as auth_views |
| 5 | 5 | |
| 6 | 6 | from .views import (UserProfileDetailView, UserProfileUpdateView, |
| 7 | 7 | ManageUserSubscriptionsView) |
| 8 | -from .forms import PasswordChangeForm | |
| 8 | +from .forms import ColabPasswordChangeForm, ColabSetPasswordForm | |
| 9 | 9 | |
| 10 | 10 | |
| 11 | 11 | urlpatterns = patterns('', |
| ... | ... | @@ -21,7 +21,8 @@ urlpatterns = patterns('', |
| 21 | 21 | |
| 22 | 22 | url(r'^password-reset-confirm/(?P<uidb64>[0-9A-Za-z]+)-(?P<token>.+)/$', |
| 23 | 23 | auth_views.password_reset_confirm, |
| 24 | - {'template_name':'registration/password_reset_confirm_custom.html'}, | |
| 24 | + {'template_name':'registration/password_reset_confirm_custom.html', | |
| 25 | + 'set_password_form': ColabSetPasswordForm}, | |
| 25 | 26 | name="password_reset_confirm"), |
| 26 | 27 | |
| 27 | 28 | url(r'^password-reset/?$', auth_views.password_reset, |
| ... | ... | @@ -30,7 +31,7 @@ urlpatterns = patterns('', |
| 30 | 31 | |
| 31 | 32 | url(r'^change-password/?$', auth_views.password_change, |
| 32 | 33 | {'template_name': 'registration/password_change_form_custom.html', |
| 33 | - 'password_change_form': PasswordChangeForm}, | |
| 34 | + 'password_change_form': ColabPasswordChangeForm}, | |
| 34 | 35 | name='password_change'), |
| 35 | 36 | |
| 36 | 37 | url(r'^change-password-done/?$', | ... | ... |