changeset 659:8e6b8ffe5f34

For issue #31, implement a forgot username feature.
author Brian Neal <bgneal@gmail.com>
date Sat, 11 May 2013 23:39:46 -0500
parents 2adf01661ac5
children 0dd84cff2477
files accounts/forms.py accounts/tests/view_tests.py accounts/urls.py accounts/views.py sg101/templates/accounts/ajax_login_form.html sg101/templates/accounts/forgot_user_email.txt sg101/templates/accounts/login.html sg101/templates/accounts/username_query.html sg101/templates/accounts/username_sent.html
diffstat 9 files changed, 149 insertions(+), 23 deletions(-) [+]
line wrap: on
line diff
--- 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))
--- 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'))
--- 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'),
 )
 
--- 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})
--- 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 @@
       </fieldset>
    </form>
 <ul>
+<li>Forgot your username? You can recover it <a href="{% url 'accounts-username_query' %}">here</a>.</li>
 <li>Forgot your password? You can reset it <a href="{% url 'accounts-password_reset' %}">here</a>.</li>
 <li>Don't have an account? Why don't you <a href="{% url 'accounts-register' %}">register</a>?</li>
 <li>Having problems? Please <a href="{% url 'contact-form' %}">contact us</a>.</li>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sg101/templates/accounts/forgot_user_email.txt	Sat May 11 23:39:46 2013 -0500
@@ -0,0 +1,14 @@
+Hello,
+
+{{ site.name }} received a request to recover a forgotten username for the
+email address {{ user.email }}.
+
+The requested username is {{ user.username }}.
+
+If you did not request this information, simply ignore this email. If you have
+questions, please contact {{ admin_email }}.
+
+Regards,
+
+The staff at {{ site.name }}
+http://{{ site.domain }}
--- a/sg101/templates/accounts/login.html	Sat May 11 16:21:55 2013 -0500
+++ b/sg101/templates/accounts/login.html	Sat May 11 23:39:46 2013 -0500
@@ -13,6 +13,7 @@
 </form>
 
 <ul>
+<li>Forgot your username? You can recover it <a href="{% url 'accounts-username_query' %}">here</a>.</li>
 <li>Forgot your password? You can reset it <a href="{% url 'accounts-password_reset' %}">here</a>.</li>
 <li>Don't have an account? Why don't you <a href="{% url 'accounts-register' %}">register</a>?</li>
 <li>Having problems? Please <a href="{% url 'contact-form' %}">contact us</a>.</li>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sg101/templates/accounts/username_query.html	Sat May 11 23:39:46 2013 -0500
@@ -0,0 +1,13 @@
+{% extends 'base.html' %}
+{% block title %}Forgotten Username{% endblock %}
+{% block content %}
+<h2>Forgotten Username</h2>
+<p>
+Forget your username? No problem. Please enter the email address you used to
+register with the site below and we'll send your username to you.
+</p>
+<form action="." method="post">{% csrf_token %}
+   {{ form.as_p }}
+   <p><input type="submit" value="Submit" /></p>
+</form>
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sg101/templates/accounts/username_sent.html	Sat May 11 23:39:46 2013 -0500
@@ -0,0 +1,9 @@
+{% extends 'base.html' %}
+{% block title %}Username Sent{% endblock %}
+{% block content %}
+<h2>Username Sent</h2>
+<p>
+Thank you. We have emailed the associated username to the email address you
+provided. 
+</p>
+{% endblock %}