# HG changeset patch # User Brian Neal # Date 1368333586 18000 # Node ID 8e6b8ffe5f34198376c296c0b5625c6b01d7d785 # Parent 2adf01661ac56904dd40069582df5026d8a03954 For issue #31, implement a forgot username feature. diff -r 2adf01661ac5 -r 8e6b8ffe5f34 accounts/forms.py --- a/accounts/forms.py Sat May 11 16:21:55 2013 -0500 +++ b/accounts/forms.py Sat May 11 23:39:46 2013 -0500 @@ -80,7 +80,7 @@ self._validation_error("A pending user with that email address already exists.", email) elif IllegalEmail.objects.filter(email=email).count(): self._validation_error("That email address is not allowed.", email) - + # email is ok return email @@ -141,7 +141,8 @@ }) subject = 'Registration Confirmation for ' + site.name - send_mail(subject, msg, admin_email, [self.cleaned_data['email']]) + send_mail(subject, msg, admin_email, [self.cleaned_data['email']], + defer=False) logging.info('Accounts/registration conf. email sent to %s for user %s; IP = %s', self.cleaned_data['email'], pending_user.username, self.ip) @@ -150,3 +151,32 @@ def _validation_error(self, msg, param=None): logging.error('Accounts/registration [%s]: %s (%s)', self.ip, msg, param) raise forms.ValidationError(msg) + + +class ForgotUsernameForm(forms.Form): + """Form used to recover lost username""" + email = forms.EmailField(widget=forms.TextInput(attrs={'class': 'text'})) + + def save(self): + """Email the username associated with the email address to the user.""" + email = self.cleaned_data['email'] + try: + user = User.objects.get(email=email) + except User.DoesNotExist: + # nothing to do; we don't tell the user as this gives away info + # about our database + return + + site = Site.objects.get_current() + admin_email = settings.ADMINS[0][1] + + subject = 'Forgotten username for %s' % site.name + msg = render_to_string('accounts/forgot_user_email.txt', { + 'user': user, + 'site': site, + 'admin_email': admin_email, + }) + send_mail(subject, msg, admin_email, [email], defer=False) + + logging.info('Forgotten username email sent to {} <{}>'.format( + user.username, email)) diff -r 2adf01661ac5 -r 8e6b8ffe5f34 accounts/tests/view_tests.py --- a/accounts/tests/view_tests.py Sat May 11 16:21:55 2013 -0500 +++ b/accounts/tests/view_tests.py Sat May 11 23:39:46 2013 -0500 @@ -6,6 +6,7 @@ from django.test import TestCase from django.core.urlresolvers import reverse +from django.core import mail from django.contrib.auth.models import User from django.contrib.auth.hashers import check_password @@ -252,3 +253,44 @@ self.assertTrue(datetime.datetime.now() - pending.date_joined < datetime.timedelta(minutes=1)) self.assertTrue(check_password('my_password', pending.password)) + + +class ForgotUsernameTest(TestCase): + + def setUp(self): + u = User.objects.create_user('existing_user', 'existing_user@example.com', 'pw') + u.save() + + def test_get_query_view(self): + """Test a simple get of the username query view""" + response = self.client.get(reverse('accounts-username_query')) + self.assertEqual(response.status_code, 200) + + def test_get_username_sent_view(self): + """Test a simple get of the username sent view""" + response = self.client.get(reverse('accounts-username_sent')) + self.assertEqual(response.status_code, 200) + + def test_invalid_email(self): + """Test form submittal of unknown email address.""" + response = self.client.post(reverse('accounts-username_query'), { + 'email': 'bad_address@example.com', + }, + follow=True) + + self.assertRedirects(response, reverse('accounts-username_sent')) + + self.assertEqual(len(mail.outbox), 0) + + def test_valid_email(self): + """Test form submittal of valid email address.""" + response = self.client.post(reverse('accounts-username_query'), { + 'email': 'existing_user@example.com', + }, + follow=True) + + self.assertRedirects(response, reverse('accounts-username_sent')) + + self.assertEqual(len(mail.outbox), 1) + if len(mail.outbox): + self.assertTrue(mail.outbox[0].subject.startswith('Forgotten username')) diff -r 2adf01661ac5 -r 8e6b8ffe5f34 accounts/urls.py --- a/accounts/urls.py Sat May 11 16:21:55 2013 -0500 +++ b/accounts/urls.py Sat May 11 23:39:46 2013 -0500 @@ -1,6 +1,7 @@ """urls for the accounts application""" from django.conf.urls import patterns, url from django.conf import settings +from django.views.generic import TemplateView urlpatterns = patterns('accounts.views', url(r'^login/ajax/$', 'login_ajax', name='accounts-login_ajax'), @@ -43,5 +44,11 @@ 'django.contrib.auth.views.password_reset_complete', kwargs={'template_name': 'accounts/password_reset_complete.html'}, name='accounts-password_reset_success'), + url(r'^username/query/$', + 'accounts.views.username_query', + name='accounts-username_query'), + url(r'^username/sent/$', + TemplateView.as_view(template_name='accounts/username_sent.html'), + name='accounts-username_sent'), ) diff -r 2adf01661ac5 -r 8e6b8ffe5f34 accounts/views.py --- a/accounts/views.py Sat May 11 16:21:55 2013 -0500 +++ b/accounts/views.py Sat May 11 23:39:46 2013 -0500 @@ -5,10 +5,9 @@ import datetime import logging -from django.shortcuts import render_to_response +from django.shortcuts import render from django.template import RequestContext from django.template.loader import render_to_string -from django.contrib.auth.models import User from django.http import HttpResponse, HttpResponseRedirect from django.core.urlresolvers import reverse from django.conf import settings @@ -17,7 +16,7 @@ from django.utils import simplejson from accounts.models import PendingUser -from accounts.forms import RegisterForm +from accounts.forms import RegisterForm, ForgotUsernameForm from accounts import create_new_user from antispam.decorators import rate_limit @@ -37,10 +36,7 @@ else: form = RegisterForm() - return render_to_response('accounts/register.html', { - 'form': form, - }, - context_instance = RequestContext(request)) + return render(request, 'accounts/register.html', {'form': form}) ####################################################################### @@ -48,8 +44,7 @@ if request.user.is_authenticated(): return HttpResponseRedirect(settings.LOGIN_REDIRECT_URL) - return render_to_response('accounts/register_thanks.html', - context_instance = RequestContext(request)) + return render(request, 'accounts/register_thanks.html') ####################################################################### @@ -66,24 +61,21 @@ pending_user = PendingUser.objects.get(username = username) except PendingUser.DoesNotExist: logging.error('Accounts register_confirm [%s]: user does not exist: %s', ip, username) - return render_to_response('accounts/register_failure.html', { - 'username': username, - }, - context_instance = RequestContext(request)) + return render(request, + 'accounts/register_failure.html', + {'username': username}) if pending_user.key != key: logging.error('Accounts register_confirm [%s]: key error: %s', ip, username) - return render_to_response('accounts/register_failure.html', { - 'username': username, - }, - context_instance = RequestContext(request)) + return render(request, + 'accounts/register_failure.html', + {'username': username}) create_new_user(pending_user, ip) - return render_to_response('accounts/register_success.html', { - 'username': username, - }, - context_instance = RequestContext(request)) + return render(request, + 'accounts/register_success.html', + {'username': username}) ####################################################################### @@ -115,3 +107,20 @@ return HttpResponse(simplejson.dumps(response), content_type='application/json') + +####################################################################### + +def username_query(request): + """This view handles forgotten username queries.""" + if request.user.is_authenticated(): + return HttpResponseRedirect(settings.LOGIN_REDIRECT_URL) + + if request.method == 'POST': + form = ForgotUsernameForm(data=request.POST) + if form.is_valid(): + form.save() + return HttpResponseRedirect(reverse('accounts-username_sent')) + else: + form = ForgotUsernameForm() + + return render(request, 'accounts/username_query.html', {'form': form}) diff -r 2adf01661ac5 -r 8e6b8ffe5f34 sg101/templates/accounts/ajax_login_form.html --- a/sg101/templates/accounts/ajax_login_form.html Sat May 11 16:21:55 2013 -0500 +++ b/sg101/templates/accounts/ajax_login_form.html Sat May 11 23:39:46 2013 -0500 @@ -10,6 +10,7 @@