From efc77762be6b16c5626d8925faf8fb41e45dda54 Mon Sep 17 00:00:00 2001
From: Henrik Levkowetz <henrik@levkowetz.com>
Date: Wed, 15 Feb 2017 16:59:23 +0000
Subject: [PATCH] Added the ability for logged-in users to change their login
 (username) to any of the active email addresses of the account.  Fixes ticket
 #2052.  - Legacy-Id: 12843

---
 ietf/ietfauth/forms.py                        | 22 ++++++++-
 ietf/ietfauth/tests.py                        | 47 +++++++++++++++++++
 ietf/ietfauth/urls.py                         |  3 +-
 ietf/ietfauth/views.py                        | 47 ++++++++++++++++++-
 ietf/templates/base/menu_user.html            |  4 +-
 .../registration/change_username.html         | 40 ++++++++++++++++
 .../registration/username_change_email.txt    | 10 ++++
 7 files changed, 167 insertions(+), 6 deletions(-)
 create mode 100644 ietf/templates/registration/change_username.html
 create mode 100644 ietf/templates/registration/username_change_email.txt

diff --git a/ietf/ietfauth/forms.py b/ietf/ietfauth/forms.py
index f30f8c103..eb8be4baa 100644
--- a/ietf/ietfauth/forms.py
+++ b/ietf/ietfauth/forms.py
@@ -173,7 +173,6 @@ from django import forms
 class ChangePasswordForm(forms.Form):
     current_password = forms.CharField(widget=forms.PasswordInput)
 
-
     new_password = forms.CharField(widget=PasswordStrengthInput)
     new_password_confirmation = forms.CharField(widget=PasswordConfirmationInput)
 
@@ -185,10 +184,29 @@ class ChangePasswordForm(forms.Form):
         password = self.cleaned_data.get('current_password', None)
         if not self.user.check_password(password):
             raise ValidationError('Invalid password')
+        return password
             
     def clean(self):
         new_password = self.cleaned_data.get('new_password', None)
         conf_password = self.cleaned_data.get('new_password_confirmation', None)
         if not new_password == conf_password:
             raise ValidationError("The password confirmation is different than the new password")
-            
+
+
+class ChangeUsernameForm(forms.Form):
+    username = forms.ChoiceField(choices=['-','--------'])
+    password = forms.CharField(widget=forms.PasswordInput, help_text="Confirm the change with your password")
+
+    def __init__(self, user, *args, **kwargs):
+        assert isinstance(user, User)
+        super(ChangeUsernameForm, self).__init__(*args, **kwargs)
+        self.user = user
+        emails = user.person.email_set.filter(active=True)
+        choices = [ (email.address, email.address) for email in emails ]
+        self.fields['username'] = forms.ChoiceField(choices=choices)
+
+    def clean_password(self):
+        password = self.cleaned_data['password']
+        if not self.user.check_password(password):
+            raise ValidationError('Invalid password')
+        return password
diff --git a/ietf/ietfauth/tests.py b/ietf/ietfauth/tests.py
index b8dfc6440..5687a5e38 100644
--- a/ietf/ietfauth/tests.py
+++ b/ietf/ietfauth/tests.py
@@ -449,3 +449,50 @@ class IetfAuthTests(TestCase):
         user = User.objects.get(username="someone@example.com")
         self.assertTrue(user.check_password(u'foobar'))
 
+    def test_change_username(self):
+
+        chun_url = urlreverse(ietf.ietfauth.views.change_username)
+        prof_url = urlreverse(ietf.ietfauth.views.profile)
+        login_url = urlreverse(django.contrib.auth.views.login)
+        redir_url = '%s?next=%s' % (login_url, chun_url)
+
+        # get without logging in
+        r = self.client.get(chun_url)
+        self.assertRedirects(r, redir_url)
+
+        user = User.objects.create(username="someone@example.com", email="someone@example.com")
+        user.set_password("password")
+        user.save()
+        p = Person.objects.create(name="Some One", ascii="Some One", user=user)
+        Email.objects.create(address=user.username, person=p)
+        Email.objects.create(address="othername@example.org", person=p)        
+
+        # log in
+        r = self.client.post(redir_url, {"username":user.username, "password":"password"})
+        self.assertRedirects(r, chun_url)
+
+        # wrong username
+        r = self.client.post(chun_url, {"username": "fiddlesticks",
+                                        "password": "password",
+                                       })
+        self.assertEqual(r.status_code, 200)
+        self.assertFormError(r, 'form', 'username',
+            "Select a valid choice. fiddlesticks is not one of the available choices.")
+
+        # wrong password
+        r = self.client.post(chun_url, {"username": "othername@example.org",
+                                        "password": "foobar",
+                                       })
+        self.assertEqual(r.status_code, 200)
+        self.assertFormError(r, 'form', 'password', 'Invalid password')
+
+        # correct username change
+        r = self.client.post(chun_url, {"username": "othername@example.org",
+                                        "password": "password",
+                                       })
+        self.assertRedirects(r, prof_url)
+        # refresh user object
+        prev = user
+        user = User.objects.get(username="othername@example.org")
+        self.assertEqual(prev, user)
+        self.assertTrue(user.check_password(u'password'))
diff --git a/ietf/ietfauth/urls.py b/ietf/ietfauth/urls.py
index ee278a4d4..3e110e2cd 100644
--- a/ietf/ietfauth/urls.py
+++ b/ietf/ietfauth/urls.py
@@ -18,5 +18,6 @@ urlpatterns = [
         url(r'^reset/confirm/(?P<auth>[^/]+)/$', views.confirm_password_reset),
         url(r'^review/$', views.review_overview),
         url(r'^testemail/$', views.test_email),
-        url(r'whitelist/add/?$', views.add_account_whitelist),
+        url(r'^username/$', views.change_username),
+        url(r'^whitelist/add/?$', views.add_account_whitelist),
 ]
diff --git a/ietf/ietfauth/views.py b/ietf/ietfauth/views.py
index 1380f6e3e..9a9e43164 100644
--- a/ietf/ietfauth/views.py
+++ b/ietf/ietfauth/views.py
@@ -52,8 +52,9 @@ from django.shortcuts import render, redirect, get_object_or_404
 import debug                            # pyflakes:ignore
 
 from ietf.group.models import Role, Group
-from ietf.ietfauth.forms import RegistrationForm, PasswordForm, ResetPasswordForm, TestEmailForm, WhitelistForm, ChangePasswordForm
-from ietf.ietfauth.forms import get_person_form, RoleEmailForm, NewEmailForm
+from ietf.ietfauth.forms import ( RegistrationForm, PasswordForm, ResetPasswordForm, TestEmailForm,
+                                WhitelistForm, ChangePasswordForm, get_person_form, RoleEmailForm,
+                                NewEmailForm, ChangeUsernameForm )
 from ietf.ietfauth.htpasswd import update_htpasswd_file
 from ietf.ietfauth.utils import role_required
 from ietf.mailinglists.models import Subscribed, Whitelisted
@@ -521,3 +522,45 @@ def change_password(request):
     })
 
     
+@login_required
+def change_username(request):
+    person = None
+
+    try:
+        person = request.user.person
+    except Person.DoesNotExist:
+        return render(request, 'registration/missing_person.html')
+
+    emails = [ e.address for e in Email.objects.filter(person=person, active=True) ]
+    emailz = [ e.address for e in person.email_set.filter(active=True) ]
+    assert emails == emailz
+    user = request.user
+
+    if request.method == 'POST':
+        form = ChangeUsernameForm(user, request.POST)
+        if form.is_valid():
+            new_username = form.cleaned_data["username"]
+            password = form.cleaned_data["password"]
+            assert new_username in emails
+
+            user.username = new_username.lower()
+            user.save()
+            # password is also stored in htpasswd file
+            update_htpasswd_file(user.username, password)
+            # keep the session
+            update_session_auth_hash(request, user)
+
+            send_mail(request, emails, None, "Datatracker username change notification", "registration/username_change_email.txt", {})
+
+            messages.success(request, "Your username was successfully changed")
+            return HttpResponseRedirect(urlreverse('ietf.ietfauth.views.profile'))
+
+    else:
+        form = ChangeUsernameForm(request.user)
+
+    return render(request, 'registration/change_username.html', {
+        'form': form,
+        'user': user,
+    })
+
+    
diff --git a/ietf/templates/base/menu_user.html b/ietf/templates/base/menu_user.html
index bf1232dfd..bd1e958c7 100644
--- a/ietf/templates/base/menu_user.html
+++ b/ietf/templates/base/menu_user.html
@@ -17,17 +17,19 @@
     {% if user.is_authenticated %}
       <li><a rel="nofollow" href="/accounts/logout/" >Sign out</a></li>
       <li><a rel="nofollow" href="/accounts/profile/">Account info</a></li>
+      <li><a href="{%url "ietf.cookies.views.preferences" %}" rel="nofollow">Preferences</a></li>
       <li><a rel="nofollow" href="/accounts/password/">Change password</a></li>
+      <li><a rel="nofollow" href="/accounts/username/">Change username</a></li>
     {% else %}
       <li><a rel="nofollow" href="/accounts/login/?next={{request.get_full_path|urlencode}}">Sign in</a></li>
       <li><a rel="nofollow" href="/accounts/reset/">Password reset</a></li>
+      <li><a href="{%url "ietf.cookies.views.preferences" %}" rel="nofollow">Preferences</a></li>
     {% endif %}
   {% endif %}
 
   {% if not request.user.is_authenticated %}
     <li><a href="{% url "ietf.ietfauth.views.create_account" %}">New account</a></li>
   {% endif %}
-  <li><a href="{%url "ietf.cookies.views.preferences" %}" rel="nofollow">Preferences</a></li>
 
   {% if user|has_role:"Reviewer" %}
     <li><a href="{% url "ietf.ietfauth.views.review_overview" %}">My reviews</a></li>
diff --git a/ietf/templates/registration/change_username.html b/ietf/templates/registration/change_username.html
new file mode 100644
index 000000000..4f32ec8d7
--- /dev/null
+++ b/ietf/templates/registration/change_username.html
@@ -0,0 +1,40 @@
+{% extends "base.html" %}
+{# Copyright The IETF Trust 2015, All Rights Reserved #}
+{% load origin %}
+
+{% load bootstrap3 %}
+{% load staticfiles %}
+
+{% block title %}Change username{% endblock %}
+
+
+{% block content %}
+  {% origin %}
+
+  <div class="row">
+    <div class="col-md-2 col-sm-0"></div>
+    <div class="col-md-8 col-sm-12">
+      <h1>Change username</h1>
+
+      <div class="help-block">
+	 This form lets you change your username (login) from {{ user.username }} to
+	 one of your other active email addresses.  If you want to change to a new
+	 email address, then please first
+	 <a href="{% url 'ietf.ietfauth.views.profile' %}">edit your profile</a>
+	 to add that email address to the active email addresses for your account.
+      </div>
+
+      <form method="post">
+	{% csrf_token %}
+	{% bootstrap_form form %}
+
+	{% buttons %}
+	  <button type="submit" class="btn btn-primary">Change username</button>
+	{% endbuttons %}
+      </form>
+
+    </div>
+    <div class="col-md-2 col-sm-0"></div>
+  </div>
+
+{% endblock %}
diff --git a/ietf/templates/registration/username_change_email.txt b/ietf/templates/registration/username_change_email.txt
new file mode 100644
index 000000000..f962eb201
--- /dev/null
+++ b/ietf/templates/registration/username_change_email.txt
@@ -0,0 +1,10 @@
+{% autoescape off %}
+Hello,
+
+{% filter wordwrap:73 %}The username (login name) for your datatracker account was just changed using the username change form.  If this was not done by you, please contact the secretariat at ietf-action@ietf.org for assistance.{% endfilter %}
+
+Best regards,
+
+	The datatracker account manager service
+	(for the IETF Secretariat)
+{% endautoescape %}