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 %}