Separate sign up and login forms.

This commit is contained in:
Pēteris Caune 2018-10-12 10:55:15 +03:00
parent 371eebe1f2
commit 9214265136
No known key found for this signature in database
GPG Key ID: E28D7679E9A9EDE2
12 changed files with 258 additions and 106 deletions

View File

@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file.
- Content updates in the "Welcome" page. - Content updates in the "Welcome" page.
- Added "Docs > Third-Party Resources" page. - Added "Docs > Third-Party Resources" page.
- Improved layout and styling in "Login" page. - Improved layout and styling in "Login" page.
- Separate "sign Up" and "Log In" forms.
### Bug Fixes ### Bug Fixes
- Timezones were missing in the "Change Schedule" dialog, fixed. - Timezones were missing in the "Change Schedule" dialog, fixed.

View File

@ -1,6 +1,6 @@
from datetime import timedelta as td from datetime import timedelta as td
from django import forms from django import forms
from django.conf import settings
from django.contrib.auth import authenticate from django.contrib.auth import authenticate
from django.contrib.auth.models import User from django.contrib.auth.models import User
@ -12,19 +12,30 @@ class LowercaseEmailField(forms.EmailField):
return value.lower() return value.lower()
class EmailForm(forms.Form): class AvailableEmailForm(forms.Form):
# Call it "identity" instead of "email"
# to avoid some of the dumber bots
identity = LowercaseEmailField(error_messages={'required': 'Please enter your email address.'})
def clean_identity(self):
v = self.cleaned_data["identity"]
if User.objects.filter(email=v).exists():
raise forms.ValidationError("An account with this email address already exists.")
return v
class ExistingEmailForm(forms.Form):
# Call it "identity" instead of "email" # Call it "identity" instead of "email"
# to avoid some of the dumber bots # to avoid some of the dumber bots
identity = LowercaseEmailField() identity = LowercaseEmailField()
def clean_identity(self): def clean_identity(self):
v = self.cleaned_data["identity"] v = self.cleaned_data["identity"]
try:
# If registration is not open then validate if an user self.user = User.objects.get(email=v)
# account with this address exists- except User.DoesNotExist:
if not settings.REGISTRATION_OPEN: raise forms.ValidationError("Incorrect email address.")
if not User.objects.filter(email=v).exists():
raise forms.ValidationError("Incorrect email address.")
return v return v

View File

@ -1,45 +1,34 @@
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core import mail from django.core import mail
from django.test import TestCase from django.test import TestCase
from django.test.utils import override_settings
from hc.accounts.models import Profile from hc.accounts.models import Profile
from hc.api.models import Check
from django.conf import settings from django.conf import settings
class LoginTestCase(TestCase): class LoginTestCase(TestCase):
def test_it_sends_link(self): def test_it_sends_link(self):
alice = User(username="alice", email="alice@example.org")
alice.save()
form = {"identity": "alice@example.org"} form = {"identity": "alice@example.org"}
r = self.client.post("/accounts/login/", form) r = self.client.post("/accounts/login/", form)
assert r.status_code == 302 assert r.status_code == 302
# An user should have been created # Alice should be the only existing user
self.assertEqual(User.objects.count(), 1) self.assertEqual(User.objects.count(), 1)
# And email sent # And email should have been sent
self.assertEqual(len(mail.outbox), 1) self.assertEqual(len(mail.outbox), 1)
subject = "Log in to %s" % settings.SITE_NAME subject = "Log in to %s" % settings.SITE_NAME
self.assertEqual(mail.outbox[0].subject, subject) self.assertEqual(mail.outbox[0].subject, subject)
# And check should be associated with the new user
check = Check.objects.get()
self.assertEqual(check.name, "My First Check")
def test_it_pops_bad_link_from_session(self): def test_it_pops_bad_link_from_session(self):
self.client.session["bad_link"] = True self.client.session["bad_link"] = True
self.client.get("/accounts/login/") self.client.get("/accounts/login/")
assert "bad_link" not in self.client.session assert "bad_link" not in self.client.session
@override_settings(REGISTRATION_OPEN=False)
def test_it_obeys_registration_open(self):
form = {"identity": "dan@example.org"}
r = self.client.post("/accounts/login/", form)
assert r.status_code == 200
self.assertContains(r, "Incorrect email")
def test_it_ignores_case(self): def test_it_ignores_case(self):
alice = User(username="alice", email="alice@example.org") alice = User(username="alice", email="alice@example.org")
alice.save() alice.save()
@ -54,3 +43,31 @@ class LoginTestCase(TestCase):
profile = Profile.objects.for_user(alice) profile = Profile.objects.for_user(alice)
self.assertIn("login", profile.token) self.assertIn("login", profile.token)
def test_it_handles_password(self):
alice = User(username="alice", email="alice@example.org")
alice.set_password("password")
alice.save()
form = {
"action": "login",
"email": "alice@example.org",
"password": "password"
}
r = self.client.post("/accounts/login/", form)
self.assertEqual(r.status_code, 302)
def test_it_handles_wrong_password(self):
alice = User(username="alice", email="alice@example.org")
alice.set_password("password")
alice.save()
form = {
"action": "login",
"email": "alice@example.org",
"password": "wrong password"
}
r = self.client.post("/accounts/login/", form)
self.assertContains(r, "Incorrect email or password")

View File

@ -0,0 +1,55 @@
from django.contrib.auth.models import User
from django.core import mail
from django.test import TestCase
from django.test.utils import override_settings
from hc.api.models import Check
from django.conf import settings
class SignupTestCase(TestCase):
def test_it_sends_link(self):
form = {"identity": "alice@example.org"}
r = self.client.post("/accounts/signup/", form)
self.assertContains(r, "Account created")
# An user should have been created
self.assertEqual(User.objects.count(), 1)
# And email sent
self.assertEqual(len(mail.outbox), 1)
subject = "Log in to %s" % settings.SITE_NAME
self.assertEqual(mail.outbox[0].subject, subject)
# And check should be associated with the new user
check = Check.objects.get()
self.assertEqual(check.name, "My First Check")
@override_settings(REGISTRATION_OPEN=False)
def test_it_obeys_registration_open(self):
form = {"identity": "dan@example.org"}
r = self.client.post("/accounts/signup/", form)
self.assertEqual(r.status_code, 403)
def test_it_ignores_case(self):
form = {"identity": "ALICE@EXAMPLE.ORG"}
self.client.post("/accounts/signup/", form)
# There should be exactly one user:
q = User.objects.filter(email="alice@example.org")
self.assertTrue(q.exists)
def test_it_checks_for_existing_users(self):
alice = User(username="alice", email="alice@example.org")
alice.save()
form = {"identity": "alice@example.org"}
r = self.client.post("/accounts/signup/", form)
self.assertContains(r, "already exists")
def test_it_checks_syntax(self):
form = {"identity": "alice at example org"}
r = self.client.post("/accounts/signup/", form)
self.assertContains(r, "Enter a valid email address")

View File

@ -4,6 +4,7 @@ from hc.accounts import views
urlpatterns = [ urlpatterns = [
path('login/', views.login, name="hc-login"), path('login/', views.login, name="hc-login"),
path('logout/', views.logout, name="hc-logout"), path('logout/', views.logout, name="hc-logout"),
path('signup/', views.signup, name="hc-signup"),
path('login_link_sent/', path('login_link_sent/',
views.login_link_sent, name="hc-login-link-sent"), views.login_link_sent, name="hc-login-link-sent"),

View File

@ -17,7 +17,8 @@ from django.views.decorators.http import require_POST
from hc.accounts.forms import (ChangeEmailForm, EmailPasswordForm, from hc.accounts.forms import (ChangeEmailForm, EmailPasswordForm,
InviteTeamMemberForm, RemoveTeamMemberForm, InviteTeamMemberForm, RemoveTeamMemberForm,
ReportSettingsForm, SetPasswordForm, ReportSettingsForm, SetPasswordForm,
TeamNameForm, EmailForm) TeamNameForm, AvailableEmailForm,
ExistingEmailForm)
from hc.accounts.models import Profile, Member from hc.accounts.models import Profile, Member
from hc.api.models import Channel, Check from hc.api.models import Channel, Check
from hc.lib.badges import get_badge_url from hc.lib.badges import get_badge_url
@ -59,7 +60,7 @@ def _ensure_own_team(request):
def login(request): def login(request):
form = EmailPasswordForm() form = EmailPasswordForm()
magic_form = EmailForm() magic_form = ExistingEmailForm()
if request.method == 'POST': if request.method == 'POST':
if request.POST.get("action") == "login": if request.POST.get("action") == "login":
@ -69,19 +70,11 @@ def login(request):
return redirect("hc-checks") return redirect("hc-checks")
else: else:
magic_form = EmailForm(request.POST) magic_form = ExistingEmailForm(request.POST)
if magic_form.is_valid(): if magic_form.is_valid():
email = magic_form.cleaned_data["identity"] profile = Profile.objects.for_user(magic_form.user)
user = None profile.send_instant_login_link()
try: return redirect("hc-login-link-sent")
user = User.objects.get(email=email)
except User.DoesNotExist:
if settings.REGISTRATION_OPEN:
user = _make_user(email)
if user:
profile = Profile.objects.for_user(user)
profile.send_instant_login_link()
return redirect("hc-login-link-sent")
bad_link = request.session.pop("bad_link", None) bad_link = request.session.pop("bad_link", None)
ctx = { ctx = {
@ -98,6 +91,25 @@ def logout(request):
return redirect("hc-index") return redirect("hc-index")
@require_POST
def signup(request):
if not settings.REGISTRATION_OPEN:
return HttpResponseForbidden()
ctx = {}
form = AvailableEmailForm(request.POST)
if form.is_valid():
email = form.cleaned_data["identity"]
user = _make_user(email)
profile = Profile.objects.for_user(user)
profile.send_instant_login_link()
ctx["created"] = True
else:
ctx = {"form": form}
return render(request, "accounts/signup_result.html", ctx)
def login_link_sent(request): def login_link_sent(request):
return render(request, "accounts/login_link_sent.html") return render(request, "accounts/login_link_sent.html")

View File

@ -9,7 +9,7 @@
.get-started-bleed { .get-started-bleed {
background: #e5ece5; background: #e5ece5;
padding-bottom: 3em; padding: 3em 0;
} }
.footer-jumbo-bleed { .footer-jumbo-bleed {
@ -51,8 +51,10 @@
margin-bottom: 0; margin-bottom: 0;
} }
#get-started { #get-started h1 {
margin-top: 4em; font-size: 20px;
line-height: 1.5;
margin: 0 0 20px 0;
} }
.tour-title { .tour-title {
@ -76,7 +78,7 @@
padding: 20px 0; padding: 20px 0;
margin: 0 20px 20px 0; margin: 0 20px 20px 0;
text-align: center; text-align: center;
width: 175px; width: 150px;
} }
#welcome-integrations img { #welcome-integrations img {
@ -120,3 +122,33 @@
.tab-pane.tab-pane-email { .tab-pane.tab-pane-email {
border: none; border: none;
} }
#signup-modal .modal-header {
border-bottom: 0;
}
#signup-modal .modal-body {
padding: 0 50px 50px 50px;
}
#signup-modal h1 {
text-align: center;
margin: 0 0 50px 0;
}
#signup-modal #link-instruction {
text-align: center;
}
#signup-result {
margin-top: 20px;
text-align: center;
font-size: 18px;
display: none;
}
#footer-cta p {
max-width: 800px;
margin-left: auto;
margin-right: auto;
}

20
static/js/signup.js Normal file
View File

@ -0,0 +1,20 @@
$(function () {
$("#signup-go").on("click", function() {
var email = $("#signup-email").val();
var token = $('input[name=csrfmiddlewaretoken]').val();
$.ajax({
url: "/accounts/signup/",
type: "post",
headers: {"X-CSRFToken": token},
data: {"identity": email},
success: function(data) {
$("#signup-result").html(data).show();
}
});
return false;
});
});

View File

@ -0,0 +1,7 @@
{% for error in form.identity.errors %}
<p class="text-danger">{{ error }}</p>
{% endfor %}
{% if created %}
<p class="text-success">Account created, please check your email!</p>
{% endif %}

View File

@ -0,0 +1,33 @@
<div id="signup-modal" class="modal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
</div>
<div class="modal-body">
<h1>Create Your Account</h1>
<p>Enter your <strong>email address</strong>.</p>
<input
type="email"
class="form-control input-lg"
id="signup-email"
value="{{ magic_form.email.value|default:"" }}"
placeholder="you@example.org"
autocomplete="off">
<p id="link-instruction">
We will email you a magic sign in link.
</p>
{% csrf_token %}
<button id="signup-go" class="btn btn-lg btn-primary btn-block">
Email Me a Link
</button>
<div id="signup-result"></div>
</div>
</div>
</div>
</div>

View File

@ -127,30 +127,12 @@
<div class="get-started-bleed"> <div class="get-started-bleed">
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div id="get-started" class="col-sm-6 col-sm-offset-3"> <div id="get-started" class="col-sm-8 col-sm-offset-2 text-center">
<h2>E-mail Address to Receive Alerts:</h2> <h1>{% site_name %} monitors the heartbeat messages sent by your cron jobs, services and APIs.
<form action="{% url 'hc-login' %}" method="post"> Get immediate alerts you when they don't arrive on schedule. </h1>
{% csrf_token %} <a href="#" data-toggle="modal" data-target="#signup-modal" class="btn btn-lg btn-primary">
Sign Up It's Free
<div class="form-group"> </a>
<div class="input-group input-group-lg">
<div class="input-group-addon">@</div>
<input
type="email"
class="form-control"
name="identity"
autocomplete="off"
placeholder="Email">
</div>
</div>
<div class="clearfix">
<button type="submit" class="btn btn-lg btn-primary pull-right">
Set up my Ping URLs…
</button>
</div>
</form>
</div> </div>
</div> </div>
</div> </div>
@ -435,50 +417,25 @@
{% if registration_open %} {% if registration_open %}
<div class="footer-jumbo-bleed"> <div class="footer-jumbo-bleed">
<div class="col-sm-12"> <div class="col-sm-10 col-sm-offset-1">
<div class="jumbotron"> <div id="footer-cta" class="jumbotron text-center">
<div class="row"> <p>{% site_name %} is a <strong>free</strong> and
<div class="col-sm-7"> <a href="https://github.com/healthchecks/healthchecks">open source</a> service.
<p>{% site_name %} is a <strong>free</strong> and Setting up monitoring for your cron jobs only takes minutes.
<a href="https://github.com/healthchecks/healthchecks">open source</a> service. Start sleeping better at nights!</p>
Setting up monitoring for your cron jobs only takes minutes. <a href="#" data-toggle="modal" data-target="#signup-modal" class="btn btn-lg btn-primary">
Start sleeping better at nights!</p> Sign Up
</a>
</div>
<div class="col-sm-1"></div>
<div class="col-sm-4">
<form action="{% url 'hc-login' %}" method="post">
{% csrf_token %}
<div class="form-group">
<div class="input-group input-group-lg">
<div class="input-group-addon">@</div>
<input
type="email"
class="form-control"
name="identity"
autocomplete="off"
placeholder="Email">
</div>
</div>
<div class="clearfix">
<button type="submit" class="btn btn-lg btn-primary pull-right">
Sign up for free
</button>
</div>
</form>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
{% include "front/signup_modal.html" %}
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
@ -487,5 +444,6 @@
<script src="{% static 'js/bootstrap.min.js' %}"></script> <script src="{% static 'js/bootstrap.min.js' %}"></script>
<script src="{% static 'js/clipboard.min.js' %}"></script> <script src="{% static 'js/clipboard.min.js' %}"></script>
<script src="{% static 'js/snippet-copy.js' %}"></script> <script src="{% static 'js/snippet-copy.js' %}"></script>
<script src="{% static 'js/signup.js' %}"></script>
{% endcompress %} {% endcompress %}
{% endblock %} {% endblock %}

View File

@ -87,7 +87,7 @@
</ul> </ul>
{% if not request.user.is_authenticated %} {% if not request.user.is_authenticated %}
<div class="panel-footer"> <div class="panel-footer">
<a class="btn btn-lg btn-success" href="{% url 'hc-login' %}">Get Started</a> <a href="#" data-toggle="modal" data-target="#signup-modal" class="btn btn-lg btn-success">Get Started</a>
</div> </div>
{% endif %} {% endif %}
</div> </div>
@ -113,7 +113,7 @@
</ul> </ul>
{% if not request.user.is_authenticated %} {% if not request.user.is_authenticated %}
<div class="panel-footer"> <div class="panel-footer">
<a class="btn btn-lg btn-primary" href="{% url 'hc-login' %}"> <a href="#" data-toggle="modal" data-target="#signup-modal" class="btn btn-lg btn-primary">
Get Started Get Started
</a> </a>
</div> </div>
@ -141,7 +141,7 @@
</ul> </ul>
{% if not request.user.is_authenticated %} {% if not request.user.is_authenticated %}
<div class="panel-footer"> <div class="panel-footer">
<a class="btn btn-lg btn-primary" href="{% url 'hc-login' %}"> <a href="#" data-toggle="modal" data-target="#signup-modal" class="btn btn-lg btn-primary">
Get Started Get Started
</a> </a>
</div> </div>
@ -227,6 +227,10 @@
</div> </div>
</div> </div>
</section> </section>
{% if not request.user.is_authenticated %}
{% include "front/signup_modal.html" %}
{% endif %}
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
@ -234,5 +238,6 @@
<script src="{% static 'js/jquery-2.1.4.min.js' %}"></script> <script src="{% static 'js/jquery-2.1.4.min.js' %}"></script>
<script src="{% static 'js/bootstrap.min.js' %}"></script> <script src="{% static 'js/bootstrap.min.js' %}"></script>
<script src="{% static 'js/pricing.js' %}"></script> <script src="{% static 'js/pricing.js' %}"></script>
<script src="{% static 'js/signup.js' %}"></script>
{% endcompress %} {% endcompress %}
{% endblock %} {% endblock %}