mirror of
https://github.com/healthchecks/healthchecks.git
synced 2025-04-12 00:01:19 +00:00
Separate sign up and login forms.
This commit is contained in:
parent
371eebe1f2
commit
9214265136
12 changed files with 258 additions and 106 deletions
|
@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file.
|
|||
- Content updates in the "Welcome" page.
|
||||
- Added "Docs > Third-Party Resources" page.
|
||||
- Improved layout and styling in "Login" page.
|
||||
- Separate "sign Up" and "Log In" forms.
|
||||
|
||||
### Bug Fixes
|
||||
- Timezones were missing in the "Change Schedule" dialog, fixed.
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from datetime import timedelta as td
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
|
||||
from django.contrib.auth import authenticate
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
|
@ -12,19 +12,30 @@ class LowercaseEmailField(forms.EmailField):
|
|||
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"
|
||||
# to avoid some of the dumber bots
|
||||
identity = LowercaseEmailField()
|
||||
|
||||
def clean_identity(self):
|
||||
v = self.cleaned_data["identity"]
|
||||
|
||||
# If registration is not open then validate if an user
|
||||
# account with this address exists-
|
||||
if not settings.REGISTRATION_OPEN:
|
||||
if not User.objects.filter(email=v).exists():
|
||||
raise forms.ValidationError("Incorrect email address.")
|
||||
try:
|
||||
self.user = User.objects.get(email=v)
|
||||
except User.DoesNotExist:
|
||||
raise forms.ValidationError("Incorrect email address.")
|
||||
|
||||
return v
|
||||
|
||||
|
|
|
@ -1,45 +1,34 @@
|
|||
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.accounts.models import Profile
|
||||
from hc.api.models import Check
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class LoginTestCase(TestCase):
|
||||
|
||||
def test_it_sends_link(self):
|
||||
alice = User(username="alice", email="alice@example.org")
|
||||
alice.save()
|
||||
|
||||
form = {"identity": "alice@example.org"}
|
||||
|
||||
r = self.client.post("/accounts/login/", form)
|
||||
assert r.status_code == 302
|
||||
|
||||
# An user should have been created
|
||||
# Alice should be the only existing user
|
||||
self.assertEqual(User.objects.count(), 1)
|
||||
|
||||
# And email sent
|
||||
# And email should have been 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")
|
||||
|
||||
def test_it_pops_bad_link_from_session(self):
|
||||
self.client.session["bad_link"] = True
|
||||
self.client.get("/accounts/login/")
|
||||
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):
|
||||
alice = User(username="alice", email="alice@example.org")
|
||||
alice.save()
|
||||
|
@ -54,3 +43,31 @@ class LoginTestCase(TestCase):
|
|||
|
||||
profile = Profile.objects.for_user(alice)
|
||||
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")
|
||||
|
|
55
hc/accounts/tests/test_signup.py
Normal file
55
hc/accounts/tests/test_signup.py
Normal 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")
|
|
@ -4,6 +4,7 @@ from hc.accounts import views
|
|||
urlpatterns = [
|
||||
path('login/', views.login, name="hc-login"),
|
||||
path('logout/', views.logout, name="hc-logout"),
|
||||
path('signup/', views.signup, name="hc-signup"),
|
||||
path('login_link_sent/',
|
||||
views.login_link_sent, name="hc-login-link-sent"),
|
||||
|
||||
|
|
|
@ -17,7 +17,8 @@ from django.views.decorators.http import require_POST
|
|||
from hc.accounts.forms import (ChangeEmailForm, EmailPasswordForm,
|
||||
InviteTeamMemberForm, RemoveTeamMemberForm,
|
||||
ReportSettingsForm, SetPasswordForm,
|
||||
TeamNameForm, EmailForm)
|
||||
TeamNameForm, AvailableEmailForm,
|
||||
ExistingEmailForm)
|
||||
from hc.accounts.models import Profile, Member
|
||||
from hc.api.models import Channel, Check
|
||||
from hc.lib.badges import get_badge_url
|
||||
|
@ -59,7 +60,7 @@ def _ensure_own_team(request):
|
|||
|
||||
def login(request):
|
||||
form = EmailPasswordForm()
|
||||
magic_form = EmailForm()
|
||||
magic_form = ExistingEmailForm()
|
||||
|
||||
if request.method == 'POST':
|
||||
if request.POST.get("action") == "login":
|
||||
|
@ -69,19 +70,11 @@ def login(request):
|
|||
return redirect("hc-checks")
|
||||
|
||||
else:
|
||||
magic_form = EmailForm(request.POST)
|
||||
magic_form = ExistingEmailForm(request.POST)
|
||||
if magic_form.is_valid():
|
||||
email = magic_form.cleaned_data["identity"]
|
||||
user = None
|
||||
try:
|
||||
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")
|
||||
profile = Profile.objects.for_user(magic_form.user)
|
||||
profile.send_instant_login_link()
|
||||
return redirect("hc-login-link-sent")
|
||||
|
||||
bad_link = request.session.pop("bad_link", None)
|
||||
ctx = {
|
||||
|
@ -98,6 +91,25 @@ def logout(request):
|
|||
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):
|
||||
return render(request, "accounts/login_link_sent.html")
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
.get-started-bleed {
|
||||
background: #e5ece5;
|
||||
padding-bottom: 3em;
|
||||
padding: 3em 0;
|
||||
}
|
||||
|
||||
.footer-jumbo-bleed {
|
||||
|
@ -51,8 +51,10 @@
|
|||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
#get-started {
|
||||
margin-top: 4em;
|
||||
#get-started h1 {
|
||||
font-size: 20px;
|
||||
line-height: 1.5;
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
|
||||
.tour-title {
|
||||
|
@ -76,7 +78,7 @@
|
|||
padding: 20px 0;
|
||||
margin: 0 20px 20px 0;
|
||||
text-align: center;
|
||||
width: 175px;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
#welcome-integrations img {
|
||||
|
@ -120,3 +122,33 @@
|
|||
.tab-pane.tab-pane-email {
|
||||
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
20
static/js/signup.js
Normal 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;
|
||||
});
|
||||
|
||||
});
|
7
templates/accounts/signup_result.html
Normal file
7
templates/accounts/signup_result.html
Normal 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 %}
|
33
templates/front/signup_modal.html
Normal file
33
templates/front/signup_modal.html
Normal 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">×</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>
|
|
@ -127,30 +127,12 @@
|
|||
<div class="get-started-bleed">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div id="get-started" class="col-sm-6 col-sm-offset-3">
|
||||
<h2>E-mail Address to Receive Alerts:</h2>
|
||||
<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">
|
||||
Set up my Ping URLs…
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id="get-started" class="col-sm-8 col-sm-offset-2 text-center">
|
||||
<h1>{% site_name %} monitors the heartbeat messages sent by your cron jobs, services and APIs.
|
||||
Get immediate alerts you when they don't arrive on schedule. </h1>
|
||||
<a href="#" data-toggle="modal" data-target="#signup-modal" class="btn btn-lg btn-primary">
|
||||
Sign Up – It's Free
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -435,50 +417,25 @@
|
|||
|
||||
{% if registration_open %}
|
||||
<div class="footer-jumbo-bleed">
|
||||
<div class="col-sm-12">
|
||||
<div class="jumbotron">
|
||||
<div class="row">
|
||||
<div class="col-sm-7">
|
||||
<p>{% site_name %} is a <strong>free</strong> and
|
||||
<a href="https://github.com/healthchecks/healthchecks">open source</a> service.
|
||||
Setting up monitoring for your cron jobs only takes minutes.
|
||||
Start sleeping better at nights!</p>
|
||||
|
||||
</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 class="col-sm-10 col-sm-offset-1">
|
||||
<div id="footer-cta" class="jumbotron text-center">
|
||||
<p>{% site_name %} is a <strong>free</strong> and
|
||||
<a href="https://github.com/healthchecks/healthchecks">open source</a> service.
|
||||
Setting up monitoring for your cron jobs only takes minutes.
|
||||
Start sleeping better at nights!</p>
|
||||
<a href="#" data-toggle="modal" data-target="#signup-modal" class="btn btn-lg btn-primary">
|
||||
Sign Up
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% include "front/signup_modal.html" %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
|
@ -487,5 +444,6 @@
|
|||
<script src="{% static 'js/bootstrap.min.js' %}"></script>
|
||||
<script src="{% static 'js/clipboard.min.js' %}"></script>
|
||||
<script src="{% static 'js/snippet-copy.js' %}"></script>
|
||||
<script src="{% static 'js/signup.js' %}"></script>
|
||||
{% endcompress %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -87,7 +87,7 @@
|
|||
</ul>
|
||||
{% if not request.user.is_authenticated %}
|
||||
<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>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
@ -113,7 +113,7 @@
|
|||
</ul>
|
||||
{% if not request.user.is_authenticated %}
|
||||
<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
|
||||
</a>
|
||||
</div>
|
||||
|
@ -141,7 +141,7 @@
|
|||
</ul>
|
||||
{% if not request.user.is_authenticated %}
|
||||
<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
|
||||
</a>
|
||||
</div>
|
||||
|
@ -227,6 +227,10 @@
|
|||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% if not request.user.is_authenticated %}
|
||||
{% include "front/signup_modal.html" %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
|
@ -234,5 +238,6 @@
|
|||
<script src="{% static 'js/jquery-2.1.4.min.js' %}"></script>
|
||||
<script src="{% static 'js/bootstrap.min.js' %}"></script>
|
||||
<script src="{% static 'js/pricing.js' %}"></script>
|
||||
<script src="{% static 'js/signup.js' %}"></script>
|
||||
{% endcompress %}
|
||||
{% endblock %}
|
||||
|
|
Loading…
Add table
Reference in a new issue