Browse Source

Users can update their email addresses. Fixes #105

pull/133/head
Pēteris Caune 7 years ago
parent
commit
2393dad09e
16 changed files with 346 additions and 40 deletions
  1. +13
    -0
      hc/accounts/forms.py
  2. +12
    -0
      hc/accounts/models.py
  3. +41
    -0
      hc/accounts/tests/test_change_email.py
  4. +22
    -5
      hc/accounts/tests/test_profile.py
  5. +8
    -3
      hc/accounts/urls.py
  6. +59
    -21
      hc/accounts/views.py
  7. +4
    -0
      hc/lib/emails.py
  8. +18
    -0
      static/css/profile.css
  9. +72
    -0
      templates/accounts/change_email.html
  10. +18
    -0
      templates/accounts/change_email_done.html
  11. +2
    -2
      templates/accounts/link_sent.html
  12. +50
    -9
      templates/accounts/profile.html
  13. +1
    -0
      templates/base.html
  14. +13
    -0
      templates/emails/change-email-body-html.html
  15. +11
    -0
      templates/emails/change-email-body-text.html
  16. +2
    -0
      templates/emails/change-email-subject.html

+ 13
- 0
hc/accounts/forms.py View File

@ -1,4 +1,5 @@
from django import forms
from django.contrib.auth.models import User
class LowercaseEmailField(forms.EmailField):
@ -21,6 +22,18 @@ class SetPasswordForm(forms.Form):
password = forms.CharField()
class ChangeEmailForm(forms.Form):
error_css_class = "has-error"
email = LowercaseEmailField()
def clean_email(self):
v = self.cleaned_data["email"]
if User.objects.filter(email=v).exists():
raise forms.ValidationError("%s is not available" % v)
return v
class InviteTeamMemberForm(forms.Form):
email = LowercaseEmailField()


+ 12
- 0
hc/accounts/models.py View File

@ -79,6 +79,18 @@ class Profile(models.Model):
}
emails.set_password(self.user.email, ctx)
def send_change_email_link(self):
token = str(uuid.uuid4())
self.token = make_password(token)
self.save()
path = reverse("hc-change-email", args=[token])
ctx = {
"button_text": "Change Email",
"button_url": settings.SITE_ROOT + path
}
emails.change_email(self.user.email, ctx)
def set_api_key(self):
self.api_key = base64.urlsafe_b64encode(os.urandom(24))
self.save()


+ 41
- 0
hc/accounts/tests/test_change_email.py View File

@ -0,0 +1,41 @@
from django.contrib.auth.hashers import make_password
from hc.test import BaseTestCase
class ChangeEmailTestCase(BaseTestCase):
def test_it_shows_form(self):
self.profile.token = make_password("foo")
self.profile.save()
self.client.login(username="[email protected]", password="password")
r = self.client.get("/accounts/change_email/foo/")
self.assertContains(r, "Change Account's Email Address")
def test_it_changes_password(self):
self.profile.token = make_password("foo")
self.profile.save()
self.client.login(username="[email protected]", password="password")
payload = {"email": "[email protected]"}
self.client.post("/accounts/change_email/foo/", payload)
self.alice.refresh_from_db()
self.assertEqual(self.alice.email, "[email protected]")
self.assertFalse(self.alice.has_usable_password())
def test_it_requires_unique_email(self):
self.profile.token = make_password("foo")
self.profile.save()
self.client.login(username="[email protected]", password="password")
payload = {"email": "[email protected]"}
r = self.client.post("/accounts/change_email/foo/", payload)
self.assertContains(r, "[email protected] is not available")
self.alice.refresh_from_db()
self.assertEqual(self.alice.email, "[email protected]")

+ 22
- 5
hc/accounts/tests/test_profile.py View File

@ -22,7 +22,7 @@ class ProfileTestCase(BaseTestCase):
# And an email should have been sent
self.assertEqual(len(mail.outbox), 1)
expected_subject = 'Set password on {0}'.format(getattr(settings, "SITE_NAME"))
expected_subject = "Set password on %s" % settings.SITE_NAME
self.assertEqual(mail.outbox[0].subject, expected_subject)
def test_it_creates_api_key(self):
@ -30,7 +30,7 @@ class ProfileTestCase(BaseTestCase):
form = {"create_api_key": "1"}
r = self.client.post("/accounts/profile/", form)
assert r.status_code == 200
self.assertEqual(r.status_code, 200)
self.alice.profile.refresh_from_db()
api_key = self.alice.profile.api_key
@ -64,7 +64,7 @@ class ProfileTestCase(BaseTestCase):
form = {"invite_team_member": "1", "email": "[email protected]"}
r = self.client.post("/accounts/profile/", form)
assert r.status_code == 200
self.assertEqual(r.status_code, 200)
member_emails = set()
for member in self.alice.profile.member_set.all():
@ -90,7 +90,7 @@ class ProfileTestCase(BaseTestCase):
form = {"remove_team_member": "1", "email": "[email protected]"}
r = self.client.post("/accounts/profile/", form)
assert r.status_code == 200
self.assertEqual(r.status_code, 200)
self.assertEqual(Member.objects.count(), 0)
@ -102,7 +102,7 @@ class ProfileTestCase(BaseTestCase):
form = {"set_team_name": "1", "team_name": "Alpha Team"}
r = self.client.post("/accounts/profile/", form)
assert r.status_code == 200
self.assertEqual(r.status_code, 200)
self.alice.profile.refresh_from_db()
self.assertEqual(self.alice.profile.team_name, "Alpha Team")
@ -123,3 +123,20 @@ class ProfileTestCase(BaseTestCase):
# to user's default team.
self.bobs_profile.refresh_from_db()
self.assertEqual(self.bobs_profile.current_team, self.bobs_profile)
def test_it_sends_change_email_link(self):
self.client.login(username="[email protected]", password="password")
form = {"change_email": "1"}
r = self.client.post("/accounts/profile/", form)
assert r.status_code == 302
# profile.token should be set now
self.alice.profile.refresh_from_db()
token = self.alice.profile.token
self.assertTrue(len(token) > 10)
# And an email should have been sent
self.assertEqual(len(mail.outbox), 1)
expected_subject = "Change email address on %s" % settings.SITE_NAME
self.assertEqual(mail.outbox[0].subject, expected_subject)

+ 8
- 3
hc/accounts/urls.py View File

@ -7,8 +7,8 @@ urlpatterns = [
url(r'^login_link_sent/$',
views.login_link_sent, name="hc-login-link-sent"),
url(r'^set_password_link_sent/$',
views.set_password_link_sent, name="hc-set-password-link-sent"),
url(r'^link_sent/$',
views.link_sent, name="hc-link-sent"),
url(r'^check_token/([\w-]+)/([\w-]+)/$',
views.check_token, name="hc-check-token"),
@ -24,8 +24,13 @@ urlpatterns = [
url(r'^set_password/([\w-]+)/$',
views.set_password, name="hc-set-password"),
url(r'^change_email/done/$',
views.change_email_done, name="hc-change-email-done"),
url(r'^change_email/([\w-]+)/$',
views.change_email, name="hc-change-email"),
url(r'^switch_team/([\w-]+)/$',
views.switch_team, name="hc-switch-team"),
]

+ 59
- 21
hc/accounts/views.py View File

@ -13,9 +13,10 @@ from django.core import signing
from django.http import HttpResponseForbidden, HttpResponseBadRequest
from django.shortcuts import redirect, render
from django.views.decorators.http import require_POST
from hc.accounts.forms import (EmailPasswordForm, InviteTeamMemberForm,
RemoveTeamMemberForm, ReportSettingsForm,
SetPasswordForm, TeamNameForm)
from hc.accounts.forms import (ChangeEmailForm, EmailPasswordForm,
InviteTeamMemberForm, RemoveTeamMemberForm,
ReportSettingsForm, SetPasswordForm,
TeamNameForm)
from hc.accounts.models import Profile, Member
from hc.api.models import Channel, Check
from hc.lib.badges import get_badge_url
@ -114,8 +115,8 @@ def login_link_sent(request):
return render(request, "accounts/login_link_sent.html")
def set_password_link_sent(request):
return render(request, "accounts/set_password_link_sent.html")
def link_sent(request):
return render(request, "accounts/link_sent.html")
def check_token(request, username, token):
@ -156,21 +157,33 @@ def profile(request):
profile.current_team = profile
profile.save()
show_api_key = False
ctx = {
"page": "profile",
"profile": profile,
"show_api_key": False,
"api_status": "default",
"team_status": "default"
}
if request.method == "POST":
if "set_password" in request.POST:
if "change_email" in request.POST:
profile.send_change_email_link()
return redirect("hc-link-sent")
elif "set_password" in request.POST:
profile.send_set_password_link()
return redirect("hc-set-password-link-sent")
return redirect("hc-link-sent")
elif "create_api_key" in request.POST:
profile.set_api_key()
show_api_key = True
messages.success(request, "The API key has been created!")
ctx["show_api_key"] = True
ctx["api_key_created"] = True
ctx["api_status"] = "success"
elif "revoke_api_key" in request.POST:
profile.api_key = ""
profile.save()
messages.info(request, "The API key has been revoked!")
ctx["api_key_revoked"] = True
ctx["api_status"] = "info"
elif "show_api_key" in request.POST:
show_api_key = True
ctx["show_api_key"] = True
elif "invite_team_member" in request.POST:
if not profile.team_access_allowed:
return HttpResponseForbidden()
@ -185,7 +198,9 @@ def profile(request):
user = _make_user(email)
profile.invite(user)
messages.success(request, "Invitation to %s sent!" % email)
ctx["team_member_invited"] = email
ctx["team_status"] = "success"
elif "remove_team_member" in request.POST:
form = RemoveTeamMemberForm(request.POST)
if form.is_valid():
@ -198,7 +213,8 @@ def profile(request):
Member.objects.filter(team=profile,
user=farewell_user).delete()
messages.info(request, "%s removed from team!" % email)
ctx["team_member_removed"] = email
ctx["team_status"] = "info"
elif "set_team_name" in request.POST:
if not profile.team_access_allowed:
return HttpResponseForbidden()
@ -207,13 +223,8 @@ def profile(request):
if form.is_valid():
profile.team_name = form.cleaned_data["team_name"]
profile.save()
messages.success(request, "Team Name updated!")
ctx = {
"page": "profile",
"profile": profile,
"show_api_key": show_api_key
}
ctx["team_name_updated"] = True
ctx["team_status"] = "success"
return render(request, "accounts/profile.html", ctx)
@ -301,6 +312,33 @@ def set_password(request, token):
return render(request, "accounts/set_password.html", {})
@login_required
def change_email(request, token):
profile = request.user.profile
if not check_password(token, profile.token):
return HttpResponseBadRequest()
if request.method == "POST":
form = ChangeEmailForm(request.POST)
if form.is_valid():
request.user.email = form.cleaned_data["email"]
request.user.set_unusable_password()
request.user.save()
profile.token = ""
profile.save()
return redirect("hc-change-email-done")
else:
form = ChangeEmailForm()
return render(request, "accounts/change_email.html", {"form": form})
def change_email_done(request):
return render(request, "accounts/change_email_done.html")
def unsubscribe_reports(request, username):
try:
signing.Signer().unsign(request.GET.get("token"))


+ 4
- 0
hc/lib/emails.py View File

@ -44,6 +44,10 @@ def set_password(to, ctx):
send("set-password", to, ctx)
def change_email(to, ctx):
send("change-email", to, ctx)
def alert(to, ctx, headers={}):
send("alert", to, ctx, headers)


+ 18
- 0
static/css/profile.css View File

@ -0,0 +1,18 @@
.panel-success .panel-footer {
background: #dff0d8;
color: #3c763d;
font-size: small;
text-align: center;
border-top: 0;
padding: 6px 15px;
}
.panel-info .panel-footer {
background: #d9edf7;
color: #31708f;
font-size: small;
text-align: center;
border-top: 0;
padding: 8px 15px;
}

+ 72
- 0
templates/accounts/change_email.html View File

@ -0,0 +1,72 @@
{% extends "base.html" %}
{% load hc_extras %}
{% block content %}
<div class="row">
<div class="col-sm-6 col-sm-offset-3">
<div class="hc-dialog">
<h1>Change Account's Email Address</h1>
<div class="dialog-body">
<p>
Your account's email address is used for sending
the sign-in links and monthly reports.
<strong>
Make sure you can receive emails at the new address.
</strong>
Otherwise, you may get locked out of
your {% site_name %} account.
</p>
{% if request.user.has_usable_password %}
<p>
Note: Changing the email address will also
<strong>reset your current password</strong>
and log you out.
</p>
{% endif %}
</div>
<form class="form-horizontal" method="post">
{% csrf_token %}
<div class="form-group">
<label class="col-sm-3 control-label">Current Email</label>
<div class="col-sm-9">
<input
type="text"
class="form-control"
disabled
value="{{ request.user.email }}">
</div>
</div>
<div class="form-group {{ form.email.css_classes }}">
<label for="ce-email" class="col-sm-3 control-label">New Email</label>
<div class="col-sm-9">
<input
type="email"
class="form-control"
id="ce-email"
name="email"
placeholder="[email protected]">
{% if form.email.errors %}
<div class="help-block">
{{ form.email.errors|join:"" }}
</div>
{% endif %}
</div>
</div>
<div class="clearfix">
<button type="submit" class="btn btn-lg btn-primary pull-right">
Change Email
</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

+ 18
- 0
templates/accounts/change_email_done.html View File

@ -0,0 +1,18 @@
{% extends "base.html" %}
{% block content %}
<div class="row">
<div class="col-sm-6 col-sm-offset-3">
<div class="hc-dialog">
<h1>Email Address Updated</h1>
<br />
<p>
Your account's email address has been updated.
You can now <a href="{% url 'hc-login' %}">sign in</a>
with the new email address.
</p>
</div>
</div>
</div>
{% endblock %}

templates/accounts/set_password_link_sent.html → templates/accounts/link_sent.html View File


+ 50
- 9
templates/accounts/profile.html View File

@ -19,7 +19,6 @@
</div>
<div class="row">
<div class="col-sm-3">
<ul class="nav nav-pills nav-stacked">
<li class="active"><a href="{% url 'hc-profile' %}">Account</a></li>
@ -33,17 +32,29 @@
<div class="panel-body settings-block">
<form method="post">
{% csrf_token %}
<h2>Set Password</h2>
Attach a password to your {% site_name %} account
<button
type="submit"
name="set_password"
class="btn btn-default pull-right">Set Password</button>
<h2>Email and Password</h2>
<p>
Your account's email address is
<code>{{ request.user.email }}</code>
<button
type="submit"
name="change_email"
class="btn btn-default pull-right">Change Email</button>
</p>
<p class="clearfix"></p>
<p>
Attach a password to your {% site_name %} account
<button
type="submit"
name="set_password"
class="btn btn-default pull-right">Set Password</button>
</p>
</form>
</div>
</div>
<div class="panel panel-default">
<div class="panel panel-{{ api_status }}">
<div class="panel-body settings-block">
<h2>API Access</h2>
{% if profile.api_key %}
@ -78,9 +89,21 @@
</form>
{% endif %}
</div>
{% if api_key_created %}
<div class="panel-footer">
API key created
</div>
{% endif %}
{% if api_key_revoked %}
<div class="panel-footer">
API key revoked
</div>
{% endif %}
</div>
<div class="panel panel-default">
<div class="panel panel-{{ team_status }}">
<div class="panel-body settings-block">
<h2>Team Access</h2>
{% if profile.member_set.count %}
@ -135,6 +158,24 @@
data-target="#invite-team-member-modal">Invite a Team Member</a>
{% endif %}
</div>
{% if team_member_invited %}
<div class="panel-footer">
{{ team_member_invited }} invited to team
</div>
{% endif %}
{% if team_member_removed %}
<div class="panel-footer">
{{ team_member_removed }} removed from team
</div>
{% endif %}
{% if team_name_updated %}
<div class="panel-footer">
Team name updated
</div>
{% endif %}
</div>
<div class="panel panel-default">


+ 1
- 0
templates/base.html View File

@ -33,6 +33,7 @@
<link rel="stylesheet" href="{% static 'css/add_pushover.css' %}" type="text/css">
<link rel="stylesheet" href="{% static 'css/settings.css' %}" type="text/css">
<link rel="stylesheet" href="{% static 'css/last_ping.css' %}" type="text/css">
<link rel="stylesheet" href="{% static 'css/profile.css' %}" type="text/css">
{% endcompress %}
</head>
<body class="page-{{ page }}">


+ 13
- 0
templates/emails/change-email-body-html.html View File

@ -0,0 +1,13 @@
{% extends "emails/base.html" %}
{% load hc_extras %}
{% block content %}
Hello,<br />
To change the email address for your account on {% site_name %}, please press
the button below:</p>
{% endblock %}
{% block content_more %}
Regards,<br />
The {% escaped_site_name %} Team
{% endblock %}

+ 11
- 0
templates/emails/change-email-body-text.html View File

@ -0,0 +1,11 @@
{% load hc_extras %}
Hello,
Here's a link to change the email address for your account on {% site_name %}:
{{ button_url }}
--
Regards,
{% site_name %}

+ 2
- 0
templates/emails/change-email-subject.html View File

@ -0,0 +1,2 @@
{% load hc_extras %}
Change email address on {% site_name %}

Loading…
Cancel
Save