diff --git a/CHANGELOG.md b/CHANGELOG.md index db87c886..66487e63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/hc/accounts/forms.py b/hc/accounts/forms.py index ab389830..e3cec19c 100644 --- a/hc/accounts/forms.py +++ b/hc/accounts/forms.py @@ -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 diff --git a/hc/accounts/tests/test_login.py b/hc/accounts/tests/test_login.py index 0d640dbe..a61f756e 100644 --- a/hc/accounts/tests/test_login.py +++ b/hc/accounts/tests/test_login.py @@ -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") diff --git a/hc/accounts/tests/test_signup.py b/hc/accounts/tests/test_signup.py new file mode 100644 index 00000000..2d601e51 --- /dev/null +++ b/hc/accounts/tests/test_signup.py @@ -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") diff --git a/hc/accounts/urls.py b/hc/accounts/urls.py index 7485b442..ad3cef80 100644 --- a/hc/accounts/urls.py +++ b/hc/accounts/urls.py @@ -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"), diff --git a/hc/accounts/views.py b/hc/accounts/views.py index e85cd9a4..4abc2f83 100644 --- a/hc/accounts/views.py +++ b/hc/accounts/views.py @@ -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") diff --git a/static/css/welcome.css b/static/css/welcome.css index 180ff5cd..ec274fb5 100644 --- a/static/css/welcome.css +++ b/static/css/welcome.css @@ -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; +} \ No newline at end of file diff --git a/static/js/signup.js b/static/js/signup.js new file mode 100644 index 00000000..ba664d02 --- /dev/null +++ b/static/js/signup.js @@ -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; + }); + +}); \ No newline at end of file diff --git a/templates/accounts/signup_result.html b/templates/accounts/signup_result.html new file mode 100644 index 00000000..5a096fda --- /dev/null +++ b/templates/accounts/signup_result.html @@ -0,0 +1,7 @@ +{% for error in form.identity.errors %} +
{{ error }}
+{% endfor %} + +{% if created %} +Account created, please check your email!
+{% endif %} \ No newline at end of file diff --git a/templates/front/signup_modal.html b/templates/front/signup_modal.html new file mode 100644 index 00000000..65740798 --- /dev/null +++ b/templates/front/signup_modal.html @@ -0,0 +1,33 @@ +