diff options
author | Jeremy Kerr <jk@ozlabs.org> | 2010-10-31 19:29:29 -0400 |
---|---|---|
committer | Jeremy Kerr <jk@ozlabs.org> | 2011-04-14 17:21:04 +0800 |
commit | c2c6a408c7764fa29389ce160f52776c9308d50a (patch) | |
tree | 1c47388b5494210aeed87f56f0c8b6e9fbeb4633 /apps | |
parent | 56e2243f3be7e859666ce0e4e1a8b8b94444f8d4 (diff) | |
download | patchwork-c2c6a408c7764fa29389ce160f52776c9308d50a.tar patchwork-c2c6a408c7764fa29389ce160f52776c9308d50a.tar.gz |
registration: use EmailConfimation rather than separate registration app
Since we have infrastructure for email confirmations, we no longer need
the separate registration app.
Requires a migration script, which will delete all inactive users,
including those newly added and pending confirmation. Use carefully.
Signed-off-by: Jeremy Kerr <jk@ozlabs.org>
Diffstat (limited to 'apps')
-rw-r--r-- | apps/patchwork/forms.py | 43 | ||||
-rw-r--r-- | apps/patchwork/models.py | 4 | ||||
-rw-r--r-- | apps/patchwork/tests/__init__.py | 2 | ||||
-rw-r--r-- | apps/patchwork/tests/registration.py | 150 | ||||
-rw-r--r-- | apps/patchwork/tests/user.py | 11 | ||||
-rw-r--r-- | apps/patchwork/tests/utils.py | 2 | ||||
-rw-r--r-- | apps/patchwork/urls.py | 18 | ||||
-rw-r--r-- | apps/patchwork/views/base.py | 1 | ||||
-rw-r--r-- | apps/patchwork/views/user.py | 54 | ||||
-rw-r--r-- | apps/settings.py | 5 | ||||
-rw-r--r-- | apps/urls.py | 10 |
11 files changed, 260 insertions, 40 deletions
diff --git a/apps/patchwork/forms.py b/apps/patchwork/forms.py index 1ff2bd0..f83c27a 100644 --- a/apps/patchwork/forms.py +++ b/apps/patchwork/forms.py @@ -22,34 +22,33 @@ from django.contrib.auth.models import User from django import forms from patchwork.models import Patch, State, Bundle, UserProfile -from registration.forms import RegistrationFormUniqueEmail -from registration.models import RegistrationProfile -class RegistrationForm(RegistrationFormUniqueEmail): +class RegistrationForm(forms.Form): first_name = forms.CharField(max_length = 30, required = False) last_name = forms.CharField(max_length = 30, required = False) - username = forms.CharField(max_length=30, label=u'Username') + username = forms.RegexField(regex = r'^\w+$', max_length=30, + label=u'Username') email = forms.EmailField(max_length=100, label=u'Email address') password = forms.CharField(widget=forms.PasswordInput(), label='Password') - password1 = forms.BooleanField(required = False) - password2 = forms.BooleanField(required = False) - - def save(self, profile_callback = None): - user = RegistrationProfile.objects.create_inactive_user( \ - username = self.cleaned_data['username'], - password = self.cleaned_data['password'], - email = self.cleaned_data['email'], - profile_callback = profile_callback) - user.first_name = self.cleaned_data.get('first_name', '') - user.last_name = self.cleaned_data.get('last_name', '') - user.save() - - # saving the userprofile causes the firstname/lastname to propagate - # to the person objects. - user.get_profile().save() - - return user + + def clean_username(self): + value = self.cleaned_data['username'] + try: + user = User.objects.get(username__iexact = value) + except User.DoesNotExist: + return self.cleaned_data['username'] + raise forms.ValidationError('This username is already taken. ' + \ + 'Please choose another.') + + def clean_email(self): + value = self.cleaned_data['email'] + try: + user = User.objects.get(email__iexact = value) + except User.DoesNotExist: + return self.cleaned_data['email'] + raise forms.ValidationError('This email address is already in use ' + \ + 'for the account "%s".\n' % user.username) def clean(self): return self.cleaned_data diff --git a/apps/patchwork/models.py b/apps/patchwork/models.py index ee6748f..806875b 100644 --- a/apps/patchwork/models.py +++ b/apps/patchwork/models.py @@ -21,6 +21,7 @@ from django.db import models from django.contrib.auth.models import User from django.core.urlresolvers import reverse from django.contrib.sites.models import Site +from django.conf import settings from patchwork.parser import hash_patch import re @@ -374,9 +375,10 @@ class BundlePatch(models.Model): ordering = ['order'] class EmailConfirmation(models.Model): - validity = datetime.timedelta(days = 30) + validity = datetime.timedelta(days = settings.CONFIRMATION_VALIDITY_DAYS) type = models.CharField(max_length = 20, choices = [ ('userperson', 'User-Person association'), + ('registration', 'Registration'), ]) email = models.CharField(max_length = 200) user = models.ForeignKey(User, null = True) diff --git a/apps/patchwork/tests/__init__.py b/apps/patchwork/tests/__init__.py index 9618d1f..db096d8 100644 --- a/apps/patchwork/tests/__init__.py +++ b/apps/patchwork/tests/__init__.py @@ -24,3 +24,5 @@ from patchwork.tests.mboxviews import * from patchwork.tests.updates import * from patchwork.tests.filters import * from patchwork.tests.confirm import * +from patchwork.tests.registration import * +from patchwork.tests.user import * diff --git a/apps/patchwork/tests/registration.py b/apps/patchwork/tests/registration.py new file mode 100644 index 0000000..18b781f --- /dev/null +++ b/apps/patchwork/tests/registration.py @@ -0,0 +1,150 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2010 Jeremy Kerr <jk@ozlabs.org> +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import unittest +from django.test import TestCase +from django.test.client import Client +from django.core import mail +from django.core.urlresolvers import reverse +from django.contrib.auth.models import User +from patchwork.models import EmailConfirmation, Person +from patchwork.tests.utils import create_user + +def _confirmation_url(conf): + return reverse('patchwork.views.confirm', kwargs = {'key': conf.key}) + +class TestUser(object): + firstname = 'Test' + lastname = 'User' + username = 'testuser' + email = 'test@example.com' + password = 'foobar' + +class RegistrationTest(TestCase): + def setUp(self): + self.user = TestUser() + self.client = Client() + self.default_data = {'username': self.user.username, + 'first_name': self.user.firstname, + 'last_name': self.user.lastname, + 'email': self.user.email, + 'password': self.user.password} + self.required_error = 'This field is required.' + self.invalid_error = 'Enter a valid value.' + + def testRegistrationForm(self): + response = self.client.get('/register/') + self.assertEquals(response.status_code, 200) + self.assertTemplateUsed(response, 'patchwork/registration_form.html') + + def testBlankFields(self): + for field in ['username', 'email', 'password']: + data = self.default_data.copy() + del data[field] + response = self.client.post('/register/', data) + self.assertEquals(response.status_code, 200) + self.assertFormError(response, 'form', field, self.required_error) + + def testInvalidUsername(self): + data = self.default_data.copy() + data['username'] = 'invalid user' + response = self.client.post('/register/', data) + self.assertEquals(response.status_code, 200) + self.assertFormError(response, 'form', 'username', self.invalid_error) + + def testExistingUsername(self): + user = create_user() + data = self.default_data.copy() + data['username'] = user.username + response = self.client.post('/register/', data) + self.assertEquals(response.status_code, 200) + self.assertFormError(response, 'form', 'username', + 'This username is already taken. Please choose another.') + + def testExistingEmail(self): + user = create_user() + data = self.default_data.copy() + data['email'] = user.email + response = self.client.post('/register/', data) + self.assertEquals(response.status_code, 200) + self.assertFormError(response, 'form', 'email', + 'This email address is already in use ' + \ + 'for the account "%s".\n' % user.username) + + def testValidRegistration(self): + response = self.client.post('/register/', self.default_data) + self.assertEquals(response.status_code, 200) + self.assertContains(response, 'confirmation email has been sent') + + # check for presence of an inactive user object + users = User.objects.filter(username = self.user.username) + self.assertEquals(users.count(), 1) + user = users[0] + self.assertEquals(user.username, self.user.username) + self.assertEquals(user.email, self.user.email) + self.assertEquals(user.is_active, False) + + # check for confirmation object + confs = EmailConfirmation.objects.filter(user = user, + type = 'registration') + self.assertEquals(len(confs), 1) + conf = confs[0] + self.assertEquals(conf.email, self.user.email) + + # check for a sent mail + self.assertEquals(len(mail.outbox), 1) + msg = mail.outbox[0] + self.assertEquals(msg.subject, 'Patchwork account confirmation') + self.assertTrue(self.user.email in msg.to) + self.assertTrue(_confirmation_url(conf) in msg.body) + + # ...and that the URL is valid + response = self.client.get(_confirmation_url(conf)) + self.assertEquals(response.status_code, 200) + +class RegistrationConfirmationTest(TestCase): + + def setUp(self): + self.user = TestUser() + self.default_data = {'username': self.user.username, + 'first_name': self.user.firstname, + 'last_name': self.user.lastname, + 'email': self.user.email, + 'password': self.user.password} + + def testRegistrationConfirmation(self): + self.assertEqual(EmailConfirmation.objects.count(), 0) + response = self.client.post('/register/', self.default_data) + self.assertEquals(response.status_code, 200) + self.assertContains(response, 'confirmation email has been sent') + + self.assertEqual(EmailConfirmation.objects.count(), 1) + conf = EmailConfirmation.objects.filter()[0] + self.assertFalse(conf.user.is_active) + self.assertTrue(conf.active) + + response = self.client.get(_confirmation_url(conf)) + self.assertEquals(response.status_code, 200) + self.assertTemplateUsed(response, 'patchwork/registration-confirm.html') + + conf = EmailConfirmation.objects.get(pk = conf.pk) + self.assertTrue(conf.user.is_active) + self.assertFalse(conf.active) + + diff --git a/apps/patchwork/tests/user.py b/apps/patchwork/tests/user.py index c9e5be3..e96e6c5 100644 --- a/apps/patchwork/tests/user.py +++ b/apps/patchwork/tests/user.py @@ -22,9 +22,9 @@ from django.test import TestCase from django.test.client import Client from django.core import mail from django.core.urlresolvers import reverse +from django.conf import settings from django.contrib.auth.models import User from patchwork.models import EmailConfirmation, Person -from patchwork.utils import userprofile_register_callback def _confirmation_url(conf): return reverse('patchwork.views.confirm', kwargs = {'key': conf.key}) @@ -39,7 +39,6 @@ class TestUser(object): self.password = User.objects.make_random_password() self.user = User.objects.create_user(self.username, self.email, self.password) - userprofile_register_callback(self.user) class UserPersonRequestTest(TestCase): def setUp(self): @@ -119,3 +118,11 @@ class UserPersonConfirmTest(TestCase): # need to reload the confirmation to check this. conf = EmailConfirmation.objects.get(pk = self.conf.pk) self.assertEquals(conf.active, False) + +class UserLoginRedirectTest(TestCase): + + def testUserLoginRedirect(self): + url = '/user/' + response = self.client.get(url) + self.assertRedirects(response, settings.LOGIN_URL + '?next=' + url) + diff --git a/apps/patchwork/tests/utils.py b/apps/patchwork/tests/utils.py index f1c95e8..1cb5dfb 100644 --- a/apps/patchwork/tests/utils.py +++ b/apps/patchwork/tests/utils.py @@ -59,7 +59,7 @@ class defaults(object): _user_idx = 1 def create_user(): global _user_idx - userid = 'test-%d' % _user_idx + userid = 'test%d' % _user_idx email = '%s@example.com' % userid _user_idx += 1 diff --git a/apps/patchwork/urls.py b/apps/patchwork/urls.py index 27c79fd..6810e3e 100644 --- a/apps/patchwork/urls.py +++ b/apps/patchwork/urls.py @@ -19,6 +19,7 @@ from django.conf.urls.defaults import * from django.conf import settings +from django.contrib.auth import views as auth_views urlpatterns = patterns('', # Example: @@ -46,6 +47,23 @@ urlpatterns = patterns('', (r'^user/link/$', 'patchwork.views.user.link'), (r'^user/unlink/(?P<person_id>[^/]+)/$', 'patchwork.views.user.unlink'), + # password change + url(r'^user/password-change/$', auth_views.password_change, + name='auth_password_change'), + url(r'^user/password-change/done/$', auth_views.password_change_done, + name='auth_password_change_done'), + + # login/logout + url(r'^user/login/$', auth_views.login, + {'template_name': 'patchwork/login.html'}, + name = 'auth_login'), + url(r'^user/logout/$', auth_views.logout, + {'template_name': 'patchwork/logout.html'}, + name = 'auth_logout'), + + # registration + (r'^register/', 'patchwork.views.user.register'), + # public view for bundles (r'^bundle/(?P<username>[^/]*)/(?P<bundlename>[^/]*)/$', 'patchwork.views.bundle.public'), diff --git a/apps/patchwork/views/base.py b/apps/patchwork/views/base.py index 1539472..590a3b6 100644 --- a/apps/patchwork/views/base.py +++ b/apps/patchwork/views/base.py @@ -62,6 +62,7 @@ def confirm(request, key): import patchwork.views.user views = { 'userperson': patchwork.views.user.link_confirm, + 'registration': patchwork.views.user.register_confirm, } conf = get_object_or_404(EmailConfirmation, key = key) diff --git a/apps/patchwork/views/user.py b/apps/patchwork/views/user.py index 759a6e3..3d28f4b 100644 --- a/apps/patchwork/views/user.py +++ b/apps/patchwork/views/user.py @@ -21,9 +21,12 @@ from django.contrib.auth.decorators import login_required from patchwork.requestcontext import PatchworkRequestContext from django.shortcuts import render_to_response, get_object_or_404 +from django.contrib import auth +from django.contrib.sites.models import Site from django.http import HttpResponseRedirect from patchwork.models import Project, Bundle, Person, EmailConfirmation, State -from patchwork.forms import UserProfileForm, UserPersonLinkForm +from patchwork.forms import UserProfileForm, UserPersonLinkForm, \ + RegistrationForm from patchwork.filters import DelegateFilter from patchwork.views import generic_list from django.template.loader import render_to_string @@ -31,6 +34,55 @@ from django.conf import settings from django.core.mail import send_mail import django.core.urlresolvers +def register(request): + context = PatchworkRequestContext(request) + if request.method == 'POST': + form = RegistrationForm(request.POST) + if form.is_valid(): + data = form.cleaned_data + # create inactive user + user = auth.models.User.objects.create_user(data['username'], + data['email'], + data['password']) + user.is_active = False; + user.first_name = data.get('first_name', '') + user.last_name = data.get('last_name', '') + user.save() + + # create confirmation + conf = EmailConfirmation(type = 'registration', user = user, + email = user.email) + conf.save() + + # send email + mail_ctx = {'site': Site.objects.get_current(), + 'confirmation': conf} + + subject = render_to_string('patchwork/activation_email_subject.txt', + mail_ctx).replace('\n', ' ').strip() + + message = render_to_string('patchwork/activation_email.txt', + mail_ctx) + + send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, + [conf.email]) + + # setting 'confirmation' in the template indicates success + context['confirmation'] = conf + + else: + form = RegistrationForm() + + return render_to_response('patchwork/registration_form.html', + { 'form': form }, + context_instance=context) + +def register_confirm(request, conf): + conf.user.is_active = True + conf.user.save() + conf.deactivate() + return render_to_response('patchwork/registration-confirm.html') + @login_required def profile(request): context = PatchworkRequestContext(request) diff --git a/apps/settings.py b/apps/settings.py index f56da70..8f091d0 100644 --- a/apps/settings.py +++ b/apps/settings.py @@ -64,7 +64,7 @@ MIDDLEWARE_CLASSES = ( ROOT_URLCONF = 'apps.urls' -LOGIN_URL = '/accounts/login' +LOGIN_URL = '/user/login/' LOGIN_REDIRECT_URL = '/user/' # If you change the ROOT_DIR setting in your local_settings.py, you'll need to @@ -96,13 +96,12 @@ INSTALLED_APPS = ( 'django.contrib.sites', 'django.contrib.admin', 'patchwork', - 'registration', ) DEFAULT_PATCHES_PER_PAGE = 100 DEFAULT_FROM_EMAIL = 'Patchwork <patchwork@patchwork.example.com>' -ACCOUNT_ACTIVATION_DAYS = 7 +CONFIRMATION_VALIDITY_DAYS = 7 # Set to True to enable the Patchwork XML-RPC interface ENABLE_XMLRPC = False diff --git a/apps/urls.py b/apps/urls.py index 3894708..4ddef9e 100644 --- a/apps/urls.py +++ b/apps/urls.py @@ -23,9 +23,6 @@ from django.conf.urls.defaults import * from django.conf import settings from django.contrib import admin -from registration.views import register -from patchwork.forms import RegistrationForm - admin.autodiscover() htdocs = os.path.join(settings.ROOT_DIR, 'htdocs') @@ -34,13 +31,6 @@ urlpatterns = patterns('', # Example: (r'^', include('patchwork.urls')), - # override the default registration form - url(r'^accounts/register/$', - register, {'form_class': RegistrationForm}, - name='registration_register'), - - (r'^accounts/', include('registration.urls')), - # Uncomment this for admin: (r'^admin/', include(admin.site.urls)), |