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 | # -*- coding: utf-8 -*- | 1 | # -*- coding: utf-8 -*- |
2 | 2 | ||
3 | -from collections import OrderedDict | ||
4 | from importlib import import_module | 3 | from importlib import import_module |
5 | 4 | ||
6 | from django import forms | 5 | from django import forms |
7 | from django.conf import settings | 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 | from django.core.urlresolvers import reverse | 10 | from django.core.urlresolvers import reverse |
12 | from django.utils.functional import lazy | 11 | from django.utils.functional import lazy |
13 | from django.utils.translation import ugettext_lazy as _ | 12 | from django.utils.translation import ugettext_lazy as _ |
14 | from django.utils.safestring import mark_safe | 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 | from .signals import user_created | 15 | from .signals import user_created |
22 | from .utils.validators import validate_social_account | 16 | from .utils.validators import validate_social_account |
@@ -169,7 +163,40 @@ class ListsForm(forms.Form): | @@ -169,7 +163,40 @@ class ListsForm(forms.Form): | ||
169 | choices=lazy(get_lists_choices, list)()) | 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 | A form that creates a user, with no privileges, from the given username and | 201 | A form that creates a user, with no privileges, from the given username and |
175 | password. | 202 | password. |
@@ -240,6 +267,8 @@ class UserCreationForm(UserForm): | @@ -240,6 +267,8 @@ class UserCreationForm(UserForm): | ||
240 | self.error_messages['password_mismatch'], | 267 | self.error_messages['password_mismatch'], |
241 | code='password_mismatch', | 268 | code='password_mismatch', |
242 | ) | 269 | ) |
270 | + | ||
271 | + super(UserCreationForm, self).clean_password2() | ||
243 | return password2 | 272 | return password2 |
244 | 273 | ||
245 | def save(self, commit=True): | 274 | def save(self, commit=True): |
@@ -293,248 +322,9 @@ class UserChangeForm(forms.ModelForm): | @@ -293,248 +322,9 @@ class UserChangeForm(forms.ModelForm): | ||
293 | return self.initial["password"] | 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,7 +10,7 @@ from django.test import TestCase, override_settings | ||
10 | from django.core.urlresolvers import reverse | 10 | from django.core.urlresolvers import reverse |
11 | 11 | ||
12 | from colab.accounts.forms import UserCreationForm, UserChangeForm,\ | 12 | from colab.accounts.forms import UserCreationForm, UserChangeForm,\ |
13 | - UserUpdateForm, UserForm, get_lists_choices, SetPasswordForm | 13 | + UserUpdateForm, UserForm, get_lists_choices, ColabSetPasswordForm |
14 | from colab.accounts import forms as accounts_forms | 14 | from colab.accounts import forms as accounts_forms |
15 | from colab.accounts.models import User | 15 | from colab.accounts.models import User |
16 | from colab.accounts.utils import mailman | 16 | from colab.accounts.utils import mailman |
@@ -37,21 +37,21 @@ class SetPasswordFormTestCase(TestCase): | @@ -37,21 +37,21 @@ class SetPasswordFormTestCase(TestCase): | ||
37 | 'new_password2': '12345'} | 37 | 'new_password2': '12345'} |
38 | 38 | ||
39 | def test_no_custom_validators(self): | 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 | self.assertTrue(form.is_valid(), True) | 41 | self.assertTrue(form.is_valid(), True) |
42 | 42 | ||
43 | @override_settings(COLAB_APPS=TEST_COLAB_APPS) | 43 | @override_settings(COLAB_APPS=TEST_COLAB_APPS) |
44 | @patch('colab.accounts.tests.utils.password_validator') | 44 | @patch('colab.accounts.tests.utils.password_validator') |
45 | def test_custom_validator(self, validator): | 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 | self.assertTrue(form.is_valid()) | 47 | self.assertTrue(form.is_valid()) |
48 | validator.assert_called_with('12345') | 48 | validator.assert_called_with('12345') |
49 | 49 | ||
50 | @override_settings(COLAB_APPS=TEST_COLAB_APPS) | 50 | @override_settings(COLAB_APPS=TEST_COLAB_APPS) |
51 | def test_custom_validator_raise_error(self): | 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 | self.assertFalse(form.is_valid()) | 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 | class FormTest(TestCase): | 57 | class FormTest(TestCase): |
colab/accounts/urls.py
@@ -5,7 +5,7 @@ from django.contrib.auth import views as auth_views | @@ -5,7 +5,7 @@ from django.contrib.auth import views as auth_views | ||
5 | 5 | ||
6 | from .views import (UserProfileDetailView, UserProfileUpdateView, | 6 | from .views import (UserProfileDetailView, UserProfileUpdateView, |
7 | ManageUserSubscriptionsView) | 7 | ManageUserSubscriptionsView) |
8 | -from .forms import PasswordChangeForm | 8 | +from .forms import ColabPasswordChangeForm, ColabSetPasswordForm |
9 | 9 | ||
10 | 10 | ||
11 | urlpatterns = patterns('', | 11 | urlpatterns = patterns('', |
@@ -21,7 +21,8 @@ urlpatterns = patterns('', | @@ -21,7 +21,8 @@ urlpatterns = patterns('', | ||
21 | 21 | ||
22 | url(r'^password-reset-confirm/(?P<uidb64>[0-9A-Za-z]+)-(?P<token>.+)/$', | 22 | url(r'^password-reset-confirm/(?P<uidb64>[0-9A-Za-z]+)-(?P<token>.+)/$', |
23 | auth_views.password_reset_confirm, | 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 | name="password_reset_confirm"), | 26 | name="password_reset_confirm"), |
26 | 27 | ||
27 | url(r'^password-reset/?$', auth_views.password_reset, | 28 | url(r'^password-reset/?$', auth_views.password_reset, |
@@ -30,7 +31,7 @@ urlpatterns = patterns('', | @@ -30,7 +31,7 @@ urlpatterns = patterns('', | ||
30 | 31 | ||
31 | url(r'^change-password/?$', auth_views.password_change, | 32 | url(r'^change-password/?$', auth_views.password_change, |
32 | {'template_name': 'registration/password_change_form_custom.html', | 33 | {'template_name': 'registration/password_change_form_custom.html', |
33 | - 'password_change_form': PasswordChangeForm}, | 34 | + 'password_change_form': ColabPasswordChangeForm}, |
34 | name='password_change'), | 35 | name='password_change'), |
35 | 36 | ||
36 | url(r'^change-password-done/?$', | 37 | url(r'^change-password-done/?$', |