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/?$', | ... | ... |