Skip to content

Commit efc7776

Browse files
committed
Added the ability for logged-in users to change their login (username) to any of the active email addresses of the account. Fixes ticket ietf-tools#2052.
- Legacy-Id: 12843
1 parent 44ad914 commit efc7776

7 files changed

Lines changed: 167 additions & 6 deletions

File tree

ietf/ietfauth/forms.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,6 @@ class Meta:
173173
class ChangePasswordForm(forms.Form):
174174
current_password = forms.CharField(widget=forms.PasswordInput)
175175

176-
177176
new_password = forms.CharField(widget=PasswordStrengthInput)
178177
new_password_confirmation = forms.CharField(widget=PasswordConfirmationInput)
179178

@@ -185,10 +184,29 @@ def clean_current_password(self):
185184
password = self.cleaned_data.get('current_password', None)
186185
if not self.user.check_password(password):
187186
raise ValidationError('Invalid password')
187+
return password
188188

189189
def clean(self):
190190
new_password = self.cleaned_data.get('new_password', None)
191191
conf_password = self.cleaned_data.get('new_password_confirmation', None)
192192
if not new_password == conf_password:
193193
raise ValidationError("The password confirmation is different than the new password")
194-
194+
195+
196+
class ChangeUsernameForm(forms.Form):
197+
username = forms.ChoiceField(choices=['-','--------'])
198+
password = forms.CharField(widget=forms.PasswordInput, help_text="Confirm the change with your password")
199+
200+
def __init__(self, user, *args, **kwargs):
201+
assert isinstance(user, User)
202+
super(ChangeUsernameForm, self).__init__(*args, **kwargs)
203+
self.user = user
204+
emails = user.person.email_set.filter(active=True)
205+
choices = [ (email.address, email.address) for email in emails ]
206+
self.fields['username'] = forms.ChoiceField(choices=choices)
207+
208+
def clean_password(self):
209+
password = self.cleaned_data['password']
210+
if not self.user.check_password(password):
211+
raise ValidationError('Invalid password')
212+
return password

ietf/ietfauth/tests.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,3 +449,50 @@ def test_change_password(self):
449449
user = User.objects.get(username="someone@example.com")
450450
self.assertTrue(user.check_password(u'foobar'))
451451

452+
def test_change_username(self):
453+
454+
chun_url = urlreverse(ietf.ietfauth.views.change_username)
455+
prof_url = urlreverse(ietf.ietfauth.views.profile)
456+
login_url = urlreverse(django.contrib.auth.views.login)
457+
redir_url = '%s?next=%s' % (login_url, chun_url)
458+
459+
# get without logging in
460+
r = self.client.get(chun_url)
461+
self.assertRedirects(r, redir_url)
462+
463+
user = User.objects.create(username="someone@example.com", email="someone@example.com")
464+
user.set_password("password")
465+
user.save()
466+
p = Person.objects.create(name="Some One", ascii="Some One", user=user)
467+
Email.objects.create(address=user.username, person=p)
468+
Email.objects.create(address="othername@example.org", person=p)
469+
470+
# log in
471+
r = self.client.post(redir_url, {"username":user.username, "password":"password"})
472+
self.assertRedirects(r, chun_url)
473+
474+
# wrong username
475+
r = self.client.post(chun_url, {"username": "fiddlesticks",
476+
"password": "password",
477+
})
478+
self.assertEqual(r.status_code, 200)
479+
self.assertFormError(r, 'form', 'username',
480+
"Select a valid choice. fiddlesticks is not one of the available choices.")
481+
482+
# wrong password
483+
r = self.client.post(chun_url, {"username": "othername@example.org",
484+
"password": "foobar",
485+
})
486+
self.assertEqual(r.status_code, 200)
487+
self.assertFormError(r, 'form', 'password', 'Invalid password')
488+
489+
# correct username change
490+
r = self.client.post(chun_url, {"username": "othername@example.org",
491+
"password": "password",
492+
})
493+
self.assertRedirects(r, prof_url)
494+
# refresh user object
495+
prev = user
496+
user = User.objects.get(username="othername@example.org")
497+
self.assertEqual(prev, user)
498+
self.assertTrue(user.check_password(u'password'))

ietf/ietfauth/urls.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,6 @@
1818
url(r'^reset/confirm/(?P<auth>[^/]+)/$', views.confirm_password_reset),
1919
url(r'^review/$', views.review_overview),
2020
url(r'^testemail/$', views.test_email),
21-
url(r'whitelist/add/?$', views.add_account_whitelist),
21+
url(r'^username/$', views.change_username),
22+
url(r'^whitelist/add/?$', views.add_account_whitelist),
2223
]

ietf/ietfauth/views.py

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,9 @@
5252
import debug # pyflakes:ignore
5353

5454
from ietf.group.models import Role, Group
55-
from ietf.ietfauth.forms import RegistrationForm, PasswordForm, ResetPasswordForm, TestEmailForm, WhitelistForm, ChangePasswordForm
56-
from ietf.ietfauth.forms import get_person_form, RoleEmailForm, NewEmailForm
55+
from ietf.ietfauth.forms import ( RegistrationForm, PasswordForm, ResetPasswordForm, TestEmailForm,
56+
WhitelistForm, ChangePasswordForm, get_person_form, RoleEmailForm,
57+
NewEmailForm, ChangeUsernameForm )
5758
from ietf.ietfauth.htpasswd import update_htpasswd_file
5859
from ietf.ietfauth.utils import role_required
5960
from ietf.mailinglists.models import Subscribed, Whitelisted
@@ -521,3 +522,45 @@ def change_password(request):
521522
})
522523

523524

525+
@login_required
526+
def change_username(request):
527+
person = None
528+
529+
try:
530+
person = request.user.person
531+
except Person.DoesNotExist:
532+
return render(request, 'registration/missing_person.html')
533+
534+
emails = [ e.address for e in Email.objects.filter(person=person, active=True) ]
535+
emailz = [ e.address for e in person.email_set.filter(active=True) ]
536+
assert emails == emailz
537+
user = request.user
538+
539+
if request.method == 'POST':
540+
form = ChangeUsernameForm(user, request.POST)
541+
if form.is_valid():
542+
new_username = form.cleaned_data["username"]
543+
password = form.cleaned_data["password"]
544+
assert new_username in emails
545+
546+
user.username = new_username.lower()
547+
user.save()
548+
# password is also stored in htpasswd file
549+
update_htpasswd_file(user.username, password)
550+
# keep the session
551+
update_session_auth_hash(request, user)
552+
553+
send_mail(request, emails, None, "Datatracker username change notification", "registration/username_change_email.txt", {})
554+
555+
messages.success(request, "Your username was successfully changed")
556+
return HttpResponseRedirect(urlreverse('ietf.ietfauth.views.profile'))
557+
558+
else:
559+
form = ChangeUsernameForm(request.user)
560+
561+
return render(request, 'registration/change_username.html', {
562+
'form': form,
563+
'user': user,
564+
})
565+
566+

ietf/templates/base/menu_user.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,19 @@
1717
{% if user.is_authenticated %}
1818
<li><a rel="nofollow" href="/accounts/logout/" >Sign out</a></li>
1919
<li><a rel="nofollow" href="/accounts/profile/">Account info</a></li>
20+
<li><a href="{%url "ietf.cookies.views.preferences" %}" rel="nofollow">Preferences</a></li>
2021
<li><a rel="nofollow" href="/accounts/password/">Change password</a></li>
22+
<li><a rel="nofollow" href="/accounts/username/">Change username</a></li>
2123
{% else %}
2224
<li><a rel="nofollow" href="/accounts/login/?next={{request.get_full_path|urlencode}}">Sign in</a></li>
2325
<li><a rel="nofollow" href="/accounts/reset/">Password reset</a></li>
26+
<li><a href="{%url "ietf.cookies.views.preferences" %}" rel="nofollow">Preferences</a></li>
2427
{% endif %}
2528
{% endif %}
2629

2730
{% if not request.user.is_authenticated %}
2831
<li><a href="{% url "ietf.ietfauth.views.create_account" %}">New account</a></li>
2932
{% endif %}
30-
<li><a href="{%url "ietf.cookies.views.preferences" %}" rel="nofollow">Preferences</a></li>
3133

3234
{% if user|has_role:"Reviewer" %}
3335
<li><a href="{% url "ietf.ietfauth.views.review_overview" %}">My reviews</a></li>
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{% extends "base.html" %}
2+
{# Copyright The IETF Trust 2015, All Rights Reserved #}
3+
{% load origin %}
4+
5+
{% load bootstrap3 %}
6+
{% load staticfiles %}
7+
8+
{% block title %}Change username{% endblock %}
9+
10+
11+
{% block content %}
12+
{% origin %}
13+
14+
<div class="row">
15+
<div class="col-md-2 col-sm-0"></div>
16+
<div class="col-md-8 col-sm-12">
17+
<h1>Change username</h1>
18+
19+
<div class="help-block">
20+
This form lets you change your username (login) from {{ user.username }} to
21+
one of your other active email addresses. If you want to change to a new
22+
email address, then please first
23+
<a href="{% url 'ietf.ietfauth.views.profile' %}">edit your profile</a>
24+
to add that email address to the active email addresses for your account.
25+
</div>
26+
27+
<form method="post">
28+
{% csrf_token %}
29+
{% bootstrap_form form %}
30+
31+
{% buttons %}
32+
<button type="submit" class="btn btn-primary">Change username</button>
33+
{% endbuttons %}
34+
</form>
35+
36+
</div>
37+
<div class="col-md-2 col-sm-0"></div>
38+
</div>
39+
40+
{% endblock %}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{% autoescape off %}
2+
Hello,
3+
4+
{% 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 %}
5+
6+
Best regards,
7+
8+
The datatracker account manager service
9+
(for the IETF Secretariat)
10+
{% endautoescape %}

0 commit comments

Comments
 (0)