Browse Source

Separate sign up and login forms.

pull/193/head
Pēteris Caune 6 years ago
parent
commit
9214265136
No known key found for this signature in database GPG Key ID: E28D7679E9A9EDE2
12 changed files with 258 additions and 106 deletions
  1. +1
    -0
      CHANGELOG.md
  2. +19
    -8
      hc/accounts/forms.py
  3. +33
    -16
      hc/accounts/tests/test_login.py
  4. +55
    -0
      hc/accounts/tests/test_signup.py
  5. +1
    -0
      hc/accounts/urls.py
  6. +26
    -14
      hc/accounts/views.py
  7. +36
    -4
      static/css/welcome.css
  8. +20
    -0
      static/js/signup.js
  9. +7
    -0
      templates/accounts/signup_result.html
  10. +33
    -0
      templates/front/signup_modal.html
  11. +19
    -61
      templates/front/welcome.html
  12. +8
    -3
      templates/payments/pricing.html

+ 1
- 0
CHANGELOG.md 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.


+ 19
- 8
hc/accounts/forms.py 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" # Call it "identity" instead of "email"
# to avoid some of the dumber bots # to avoid some of the dumber bots
identity = LowercaseEmailField()
identity = LowercaseEmailField(error_messages={'required': 'Please enter your email address.'})
def clean_identity(self): def clean_identity(self):
v = self.cleaned_data["identity"] 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
# 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.")
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"]
try:
self.user = User.objects.get(email=v)
except User.DoesNotExist:
raise forms.ValidationError("Incorrect email address.")
return v return v


+ 33
- 16
hc/accounts/tests/test_login.py 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="[email protected]")
alice.save()
form = {"identity": "[email protected]"} form = {"identity": "[email protected]"}
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": "[email protected]"}
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="[email protected]") alice = User(username="alice", email="[email protected]")
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="[email protected]")
alice.set_password("password")
alice.save()
form = {
"action": "login",
"email": "[email protected]",
"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="[email protected]")
alice.set_password("password")
alice.save()
form = {
"action": "login",
"email": "[email protected]",
"password": "wrong password"
}
r = self.client.post("/accounts/login/", form)
self.assertContains(r, "Incorrect email or password")

+ 55
- 0
hc/accounts/tests/test_signup.py 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": "[email protected]"}
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": "[email protected]"}
r = self.client.post("/accounts/signup/", form)
self.assertEqual(r.status_code, 403)
def test_it_ignores_case(self):
form = {"identity": "[email protected]"}
self.client.post("/accounts/signup/", form)
# There should be exactly one user:
q = User.objects.filter(email="[email protected]")
self.assertTrue(q.exists)
def test_it_checks_for_existing_users(self):
alice = User(username="alice", email="[email protected]")
alice.save()
form = {"identity": "[email protected]"}
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")

+ 1
- 0
hc/accounts/urls.py 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"),


+ 26
- 14
hc/accounts/views.py 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"]
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) 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")


+ 36
- 4
static/css/welcome.css 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 {
margin-top: 4em;
#get-started h1 {
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
- 0
static/js/signup.js 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;
});
});

+ 7
- 0
templates/accounts/signup_result.html 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 %}

+ 33
- 0
templates/front/signup_modal.html 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="[email protected]"
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>

+ 19
- 61
templates/front/welcome.html 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">
<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> </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="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> </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 %}

+ 8
- 3
templates/payments/pricing.html 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 %}

Loading…
Cancel
Save