Skip to content

Commit c2c6a40

Browse files
committed
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>
1 parent 56e2243 commit c2c6a40

24 files changed

+293
-78
lines changed

apps/patchwork/forms.py

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22,34 +22,33 @@
2222
from django import forms
2323

2424
from patchwork.models import Patch, State, Bundle, UserProfile
25-
from registration.forms import RegistrationFormUniqueEmail
26-
from registration.models import RegistrationProfile
2725

28-
class RegistrationForm(RegistrationFormUniqueEmail):
26+
class RegistrationForm(forms.Form):
2927
first_name = forms.CharField(max_length = 30, required = False)
3028
last_name = forms.CharField(max_length = 30, required = False)
31-
username = forms.CharField(max_length=30, label=u'Username')
29+
username = forms.RegexField(regex = r'^\w+$', max_length=30,
30+
label=u'Username')
3231
email = forms.EmailField(max_length=100, label=u'Email address')
3332
password = forms.CharField(widget=forms.PasswordInput(),
3433
label='Password')
35-
password1 = forms.BooleanField(required = False)
36-
password2 = forms.BooleanField(required = False)
37-
38-
def save(self, profile_callback = None):
39-
user = RegistrationProfile.objects.create_inactive_user( \
40-
username = self.cleaned_data['username'],
41-
password = self.cleaned_data['password'],
42-
email = self.cleaned_data['email'],
43-
profile_callback = profile_callback)
44-
user.first_name = self.cleaned_data.get('first_name', '')
45-
user.last_name = self.cleaned_data.get('last_name', '')
46-
user.save()
47-
48-
# saving the userprofile causes the firstname/lastname to propagate
49-
# to the person objects.
50-
user.get_profile().save()
51-
52-
return user
34+
35+
def clean_username(self):
36+
value = self.cleaned_data['username']
37+
try:
38+
user = User.objects.get(username__iexact = value)
39+
except User.DoesNotExist:
40+
return self.cleaned_data['username']
41+
raise forms.ValidationError('This username is already taken. ' + \
42+
'Please choose another.')
43+
44+
def clean_email(self):
45+
value = self.cleaned_data['email']
46+
try:
47+
user = User.objects.get(email__iexact = value)
48+
except User.DoesNotExist:
49+
return self.cleaned_data['email']
50+
raise forms.ValidationError('This email address is already in use ' + \
51+
'for the account "%s".\n' % user.username)
5352

5453
def clean(self):
5554
return self.cleaned_data

apps/patchwork/models.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from django.contrib.auth.models import User
2222
from django.core.urlresolvers import reverse
2323
from django.contrib.sites.models import Site
24+
from django.conf import settings
2425
from patchwork.parser import hash_patch
2526

2627
import re
@@ -374,9 +375,10 @@ class Meta:
374375
ordering = ['order']
375376

376377
class EmailConfirmation(models.Model):
377-
validity = datetime.timedelta(days = 30)
378+
validity = datetime.timedelta(days = settings.CONFIRMATION_VALIDITY_DAYS)
378379
type = models.CharField(max_length = 20, choices = [
379380
('userperson', 'User-Person association'),
381+
('registration', 'Registration'),
380382
])
381383
email = models.CharField(max_length = 200)
382384
user = models.ForeignKey(User, null = True)

apps/patchwork/tests/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,5 @@
2424
from patchwork.tests.updates import *
2525
from patchwork.tests.filters import *
2626
from patchwork.tests.confirm import *
27+
from patchwork.tests.registration import *
28+
from patchwork.tests.user import *
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
# Patchwork - automated patch tracking system
2+
# Copyright (C) 2010 Jeremy Kerr <jk@ozlabs.org>
3+
#
4+
# This file is part of the Patchwork package.
5+
#
6+
# Patchwork is free software; you can redistribute it and/or modify
7+
# it under the terms of the GNU General Public License as published by
8+
# the Free Software Foundation; either version 2 of the License, or
9+
# (at your option) any later version.
10+
#
11+
# Patchwork is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
# GNU General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU General Public License
17+
# along with Patchwork; if not, write to the Free Software
18+
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19+
20+
import unittest
21+
from django.test import TestCase
22+
from django.test.client import Client
23+
from django.core import mail
24+
from django.core.urlresolvers import reverse
25+
from django.contrib.auth.models import User
26+
from patchwork.models import EmailConfirmation, Person
27+
from patchwork.tests.utils import create_user
28+
29+
def _confirmation_url(conf):
30+
return reverse('patchwork.views.confirm', kwargs = {'key': conf.key})
31+
32+
class TestUser(object):
33+
firstname = 'Test'
34+
lastname = 'User'
35+
username = 'testuser'
36+
email = 'test@example.com'
37+
password = 'foobar'
38+
39+
class RegistrationTest(TestCase):
40+
def setUp(self):
41+
self.user = TestUser()
42+
self.client = Client()
43+
self.default_data = {'username': self.user.username,
44+
'first_name': self.user.firstname,
45+
'last_name': self.user.lastname,
46+
'email': self.user.email,
47+
'password': self.user.password}
48+
self.required_error = 'This field is required.'
49+
self.invalid_error = 'Enter a valid value.'
50+
51+
def testRegistrationForm(self):
52+
response = self.client.get('/register/')
53+
self.assertEquals(response.status_code, 200)
54+
self.assertTemplateUsed(response, 'patchwork/registration_form.html')
55+
56+
def testBlankFields(self):
57+
for field in ['username', 'email', 'password']:
58+
data = self.default_data.copy()
59+
del data[field]
60+
response = self.client.post('/register/', data)
61+
self.assertEquals(response.status_code, 200)
62+
self.assertFormError(response, 'form', field, self.required_error)
63+
64+
def testInvalidUsername(self):
65+
data = self.default_data.copy()
66+
data['username'] = 'invalid user'
67+
response = self.client.post('/register/', data)
68+
self.assertEquals(response.status_code, 200)
69+
self.assertFormError(response, 'form', 'username', self.invalid_error)
70+
71+
def testExistingUsername(self):
72+
user = create_user()
73+
data = self.default_data.copy()
74+
data['username'] = user.username
75+
response = self.client.post('/register/', data)
76+
self.assertEquals(response.status_code, 200)
77+
self.assertFormError(response, 'form', 'username',
78+
'This username is already taken. Please choose another.')
79+
80+
def testExistingEmail(self):
81+
user = create_user()
82+
data = self.default_data.copy()
83+
data['email'] = user.email
84+
response = self.client.post('/register/', data)
85+
self.assertEquals(response.status_code, 200)
86+
self.assertFormError(response, 'form', 'email',
87+
'This email address is already in use ' + \
88+
'for the account "%s".\n' % user.username)
89+
90+
def testValidRegistration(self):
91+
response = self.client.post('/register/', self.default_data)
92+
self.assertEquals(response.status_code, 200)
93+
self.assertContains(response, 'confirmation email has been sent')
94+
95+
# check for presence of an inactive user object
96+
users = User.objects.filter(username = self.user.username)
97+
self.assertEquals(users.count(), 1)
98+
user = users[0]
99+
self.assertEquals(user.username, self.user.username)
100+
self.assertEquals(user.email, self.user.email)
101+
self.assertEquals(user.is_active, False)
102+
103+
# check for confirmation object
104+
confs = EmailConfirmation.objects.filter(user = user,
105+
type = 'registration')
106+
self.assertEquals(len(confs), 1)
107+
conf = confs[0]
108+
self.assertEquals(conf.email, self.user.email)
109+
110+
# check for a sent mail
111+
self.assertEquals(len(mail.outbox), 1)
112+
msg = mail.outbox[0]
113+
self.assertEquals(msg.subject, 'Patchwork account confirmation')
114+
self.assertTrue(self.user.email in msg.to)
115+
self.assertTrue(_confirmation_url(conf) in msg.body)
116+
117+
# ...and that the URL is valid
118+
response = self.client.get(_confirmation_url(conf))
119+
self.assertEquals(response.status_code, 200)
120+
121+
class RegistrationConfirmationTest(TestCase):
122+
123+
def setUp(self):
124+
self.user = TestUser()
125+
self.default_data = {'username': self.user.username,
126+
'first_name': self.user.firstname,
127+
'last_name': self.user.lastname,
128+
'email': self.user.email,
129+
'password': self.user.password}
130+
131+
def testRegistrationConfirmation(self):
132+
self.assertEqual(EmailConfirmation.objects.count(), 0)
133+
response = self.client.post('/register/', self.default_data)
134+
self.assertEquals(response.status_code, 200)
135+
self.assertContains(response, 'confirmation email has been sent')
136+
137+
self.assertEqual(EmailConfirmation.objects.count(), 1)
138+
conf = EmailConfirmation.objects.filter()[0]
139+
self.assertFalse(conf.user.is_active)
140+
self.assertTrue(conf.active)
141+
142+
response = self.client.get(_confirmation_url(conf))
143+
self.assertEquals(response.status_code, 200)
144+
self.assertTemplateUsed(response, 'patchwork/registration-confirm.html')
145+
146+
conf = EmailConfirmation.objects.get(pk = conf.pk)
147+
self.assertTrue(conf.user.is_active)
148+
self.assertFalse(conf.active)
149+
150+

apps/patchwork/tests/user.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@
2222
from django.test.client import Client
2323
from django.core import mail
2424
from django.core.urlresolvers import reverse
25+
from django.conf import settings
2526
from django.contrib.auth.models import User
2627
from patchwork.models import EmailConfirmation, Person
27-
from patchwork.utils import userprofile_register_callback
2828

2929
def _confirmation_url(conf):
3030
return reverse('patchwork.views.confirm', kwargs = {'key': conf.key})
@@ -39,7 +39,6 @@ def __init__(self):
3939
self.password = User.objects.make_random_password()
4040
self.user = User.objects.create_user(self.username,
4141
self.email, self.password)
42-
userprofile_register_callback(self.user)
4342

4443
class UserPersonRequestTest(TestCase):
4544
def setUp(self):
@@ -119,3 +118,11 @@ def testUserPersonConfirm(self):
119118
# need to reload the confirmation to check this.
120119
conf = EmailConfirmation.objects.get(pk = self.conf.pk)
121120
self.assertEquals(conf.active, False)
121+
122+
class UserLoginRedirectTest(TestCase):
123+
124+
def testUserLoginRedirect(self):
125+
url = '/user/'
126+
response = self.client.get(url)
127+
self.assertRedirects(response, settings.LOGIN_URL + '?next=' + url)
128+

apps/patchwork/tests/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ class defaults(object):
5959
_user_idx = 1
6060
def create_user():
6161
global _user_idx
62-
userid = 'test-%d' % _user_idx
62+
userid = 'test%d' % _user_idx
6363
email = '%s@example.com' % userid
6464
_user_idx += 1
6565

apps/patchwork/urls.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
from django.conf.urls.defaults import *
2121
from django.conf import settings
22+
from django.contrib.auth import views as auth_views
2223

2324
urlpatterns = patterns('',
2425
# Example:
@@ -46,6 +47,23 @@
4647
(r'^user/link/$', 'patchwork.views.user.link'),
4748
(r'^user/unlink/(?P<person_id>[^/]+)/$', 'patchwork.views.user.unlink'),
4849

50+
# password change
51+
url(r'^user/password-change/$', auth_views.password_change,
52+
name='auth_password_change'),
53+
url(r'^user/password-change/done/$', auth_views.password_change_done,
54+
name='auth_password_change_done'),
55+
56+
# login/logout
57+
url(r'^user/login/$', auth_views.login,
58+
{'template_name': 'patchwork/login.html'},
59+
name = 'auth_login'),
60+
url(r'^user/logout/$', auth_views.logout,
61+
{'template_name': 'patchwork/logout.html'},
62+
name = 'auth_logout'),
63+
64+
# registration
65+
(r'^register/', 'patchwork.views.user.register'),
66+
4967
# public view for bundles
5068
(r'^bundle/(?P<username>[^/]*)/(?P<bundlename>[^/]*)/$',
5169
'patchwork.views.bundle.public'),

apps/patchwork/views/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ def confirm(request, key):
6262
import patchwork.views.user
6363
views = {
6464
'userperson': patchwork.views.user.link_confirm,
65+
'registration': patchwork.views.user.register_confirm,
6566
}
6667

6768
conf = get_object_or_404(EmailConfirmation, key = key)

apps/patchwork/views/user.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,68 @@
2121
from django.contrib.auth.decorators import login_required
2222
from patchwork.requestcontext import PatchworkRequestContext
2323
from django.shortcuts import render_to_response, get_object_or_404
24+
from django.contrib import auth
25+
from django.contrib.sites.models import Site
2426
from django.http import HttpResponseRedirect
2527
from patchwork.models import Project, Bundle, Person, EmailConfirmation, State
26-
from patchwork.forms import UserProfileForm, UserPersonLinkForm
28+
from patchwork.forms import UserProfileForm, UserPersonLinkForm, \
29+
RegistrationForm
2730
from patchwork.filters import DelegateFilter
2831
from patchwork.views import generic_list
2932
from django.template.loader import render_to_string
3033
from django.conf import settings
3134
from django.core.mail import send_mail
3235
import django.core.urlresolvers
3336

37+
def register(request):
38+
context = PatchworkRequestContext(request)
39+
if request.method == 'POST':
40+
form = RegistrationForm(request.POST)
41+
if form.is_valid():
42+
data = form.cleaned_data
43+
# create inactive user
44+
user = auth.models.User.objects.create_user(data['username'],
45+
data['email'],
46+
data['password'])
47+
user.is_active = False;
48+
user.first_name = data.get('first_name', '')
49+
user.last_name = data.get('last_name', '')
50+
user.save()
51+
52+
# create confirmation
53+
conf = EmailConfirmation(type = 'registration', user = user,
54+
email = user.email)
55+
conf.save()
56+
57+
# send email
58+
mail_ctx = {'site': Site.objects.get_current(),
59+
'confirmation': conf}
60+
61+
subject = render_to_string('patchwork/activation_email_subject.txt',
62+
mail_ctx).replace('\n', ' ').strip()
63+
64+
message = render_to_string('patchwork/activation_email.txt',
65+
mail_ctx)
66+
67+
send_mail(subject, message, settings.DEFAULT_FROM_EMAIL,
68+
[conf.email])
69+
70+
# setting 'confirmation' in the template indicates success
71+
context['confirmation'] = conf
72+
73+
else:
74+
form = RegistrationForm()
75+
76+
return render_to_response('patchwork/registration_form.html',
77+
{ 'form': form },
78+
context_instance=context)
79+
80+
def register_confirm(request, conf):
81+
conf.user.is_active = True
82+
conf.user.save()
83+
conf.deactivate()
84+
return render_to_response('patchwork/registration-confirm.html')
85+
3486
@login_required
3587
def profile(request):
3688
context = PatchworkRequestContext(request)

0 commit comments

Comments
 (0)