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.
- 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.


+ 19
- 8
hc/accounts/forms.py View File

@ -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()
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
# 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


+ 33
- 16
hc/accounts/tests/test_login.py View File

@ -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="[email protected]")
alice.save()
form = {"identity": "[email protected]"}
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": "[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):
alice = User(username="alice", email="[email protected]")
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="[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 = [
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"),


+ 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,
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")


+ 36
- 4
static/css/welcome.css View File

@ -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
- 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="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 %}

+ 8
- 3
templates/payments/pricing.html View File

@ -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…
Cancel
Save