Browse Source

Update the signup form to collect browser's timezone

pull/522/head
Pēteris Caune 4 years ago
parent
commit
548b2ac33c
No known key found for this signature in database GPG Key ID: E28D7679E9A9EDE2
5 changed files with 51 additions and 18 deletions
  1. +11
    -1
      hc/accounts/forms.py
  2. +2
    -2
      hc/accounts/management/commands/createsuperuser.py
  3. +23
    -10
      hc/accounts/tests/test_signup.py
  4. +8
    -4
      hc/accounts/views.py
  5. +7
    -1
      static/js/signup.js

+ 11
- 1
hc/accounts/forms.py View File

@ -28,12 +28,13 @@ class Base64Field(forms.CharField):
raise ValidationError(message="Cannot decode base64") raise ValidationError(message="Cannot decode base64")
class AvailableEmailForm(forms.Form):
class SignupForm(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."} error_messages={"required": "Please enter your email address."}
) )
tz = forms.CharField(required=False)
def clean_identity(self): def clean_identity(self):
v = self.cleaned_data["identity"] v = self.cleaned_data["identity"]
@ -47,6 +48,15 @@ class AvailableEmailForm(forms.Form):
return v return v
def clean_tz(self):
# Declare tz as "clean" only if we can find it in pytz.all_timezones
if self.cleaned_data["tz"] in pytz.all_timezones:
return self.cleaned_data["tz"]
# Otherwise, return None, and *don't* throw a validation exception:
# If user's browser reports a timezone we don't recognize, we
# should ignore the timezone but still save the rest of the form.
class EmailLoginForm(forms.Form): class EmailLoginForm(forms.Form):
# Call it "identity" instead of "email" # Call it "identity" instead of "email"


+ 2
- 2
hc/accounts/management/commands/createsuperuser.py View File

@ -1,7 +1,7 @@
import getpass import getpass
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from hc.accounts.forms import AvailableEmailForm
from hc.accounts.forms import SignupForm
from hc.accounts.views import _make_user from hc.accounts.views import _make_user
@ -14,7 +14,7 @@ class Command(BaseCommand):
while not email: while not email:
raw = input("Email address:") raw = input("Email address:")
form = AvailableEmailForm({"identity": raw})
form = SignupForm({"identity": raw})
if not form.is_valid(): if not form.is_valid():
self.stderr.write("Error: " + " ".join(form.errors["identity"])) self.stderr.write("Error: " + " ".join(form.errors["identity"]))
continue continue


+ 23
- 10
hc/accounts/tests/test_signup.py View File

@ -9,8 +9,8 @@ from django.conf import settings
class SignupTestCase(TestCase): class SignupTestCase(TestCase):
@override_settings(USE_PAYMENTS=False) @override_settings(USE_PAYMENTS=False)
def test_it_sends_link(self):
form = {"identity": "[email protected]"}
def test_it_works(self):
form = {"identity": "[email protected]", "tz": "Europe/Riga"}
r = self.client.post("/accounts/signup/", form) r = self.client.post("/accounts/signup/", form)
self.assertContains(r, "Account created") self.assertContains(r, "Account created")
@ -21,8 +21,10 @@ class SignupTestCase(TestCase):
# A profile should have been created # A profile should have been created
profile = Profile.objects.get() profile = Profile.objects.get()
self.assertEqual(profile.check_limit, 500)
self.assertEqual(profile.sms_limit, 500) self.assertEqual(profile.sms_limit, 500)
self.assertEqual(profile.call_limit, 500) self.assertEqual(profile.call_limit, 500)
self.assertEqual(profile.tz, "Europe/Riga")
# And email sent # And email sent
self.assertEqual(len(mail.outbox), 1) self.assertEqual(len(mail.outbox), 1)
@ -44,25 +46,25 @@ class SignupTestCase(TestCase):
self.assertEqual(channel.project, project) self.assertEqual(channel.project, project)
@override_settings(USE_PAYMENTS=True) @override_settings(USE_PAYMENTS=True)
def test_it_sets_high_limits(self):
form = {"identity": "[email protected]"}
def test_it_sets_limits(self):
form = {"identity": "[email protected]", "tz": ""}
self.client.post("/accounts/signup/", form) self.client.post("/accounts/signup/", form)
# A profile should have been created
profile = Profile.objects.get() profile = Profile.objects.get()
self.assertEqual(profile.check_limit, 20)
self.assertEqual(profile.sms_limit, 5) self.assertEqual(profile.sms_limit, 5)
self.assertEqual(profile.call_limit, 0) self.assertEqual(profile.call_limit, 0)
@override_settings(REGISTRATION_OPEN=False) @override_settings(REGISTRATION_OPEN=False)
def test_it_obeys_registration_open(self): def test_it_obeys_registration_open(self):
form = {"identity": "[email protected]"}
form = {"identity": "[email protected]", "tz": ""}
r = self.client.post("/accounts/signup/", form) r = self.client.post("/accounts/signup/", form)
self.assertEqual(r.status_code, 403) self.assertEqual(r.status_code, 403)
def test_it_ignores_case(self): def test_it_ignores_case(self):
form = {"identity": "[email protected]"}
form = {"identity": "[email protected]", "tz": ""}
self.client.post("/accounts/signup/", form) self.client.post("/accounts/signup/", form)
# There should be exactly one user: # There should be exactly one user:
@ -73,19 +75,30 @@ class SignupTestCase(TestCase):
alice = User(username="alice", email="[email protected]") alice = User(username="alice", email="[email protected]")
alice.save() alice.save()
form = {"identity": "[email protected]"}
form = {"identity": "[email protected]", "tz": ""}
r = self.client.post("/accounts/signup/", form) r = self.client.post("/accounts/signup/", form)
self.assertContains(r, "already exists") self.assertContains(r, "already exists")
def test_it_checks_syntax(self): def test_it_checks_syntax(self):
form = {"identity": "alice at example org"}
form = {"identity": "alice at example org", "tz": ""}
r = self.client.post("/accounts/signup/", form) r = self.client.post("/accounts/signup/", form)
self.assertContains(r, "Enter a valid email address") self.assertContains(r, "Enter a valid email address")
def test_it_checks_length(self): def test_it_checks_length(self):
aaa = "a" * 300 aaa = "a" * 300
form = {"identity": f"alice+{aaa}@example.org"}
form = {"identity": f"alice+{aaa}@example.org", "tz": ""}
r = self.client.post("/accounts/signup/", form) r = self.client.post("/accounts/signup/", form)
self.assertContains(r, "Address is too long.") self.assertContains(r, "Address is too long.")
self.assertFalse(User.objects.exists()) self.assertFalse(User.objects.exists())
@override_settings(USE_PAYMENTS=False)
def test_it_ignores_bad_tz(self):
form = {"identity": "[email protected]", "tz": "Foo/Bar"}
r = self.client.post("/accounts/signup/", form)
self.assertContains(r, "Account created")
self.assertIn("auto-login", r.cookies)
profile = Profile.objects.get()
self.assertEqual(profile.tz, "UTC")

+ 8
- 4
hc/accounts/views.py View File

@ -59,7 +59,7 @@ def _allow_redirect(redirect_url):
return match.url_name in POST_LOGIN_ROUTES return match.url_name in POST_LOGIN_ROUTES
def _make_user(email, with_project=True):
def _make_user(email, tz=None, with_project=True):
username = str(uuid.uuid4())[:30] username = str(uuid.uuid4())[:30]
user = User(username=username, email=email) user = User(username=username, email=email)
user.set_unusable_password() user.set_unusable_password()
@ -84,7 +84,10 @@ def _make_user(email, with_project=True):
channel.checks.add(check) channel.checks.add(check)
# Ensure a profile gets created # Ensure a profile gets created
Profile.objects.for_user(user)
profile = Profile.objects.for_user(user)
if tz:
profile.tz = tz
profile.save()
return user return user
@ -173,10 +176,11 @@ def signup(request):
return HttpResponseForbidden() return HttpResponseForbidden()
ctx = {} ctx = {}
form = forms.AvailableEmailForm(request.POST)
form = forms.SignupForm(request.POST)
if form.is_valid(): if form.is_valid():
email = form.cleaned_data["identity"] email = form.cleaned_data["identity"]
user = _make_user(email)
tz = form.cleaned_data["tz"]
user = _make_user(email, tz)
profile = Profile.objects.for_user(user) profile = Profile.objects.for_user(user)
profile.send_instant_login_link() profile.send_instant_login_link()
ctx["created"] = True ctx["created"] = True


+ 7
- 1
static/js/signup.js View File

@ -4,11 +4,17 @@ $(function () {
var base = document.getElementById("base-url").getAttribute("href").slice(0, -1); var base = document.getElementById("base-url").getAttribute("href").slice(0, -1);
var email = $("#signup-email").val(); var email = $("#signup-email").val();
try {
var tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
} catch(err) {
var tz = "UTC";
}
$("#signup-go").prop("disabled", true); $("#signup-go").prop("disabled", true);
$.ajax({ $.ajax({
url: base + "/accounts/signup/", url: base + "/accounts/signup/",
type: "post", type: "post",
data: {"identity": email},
data: {"identity": email, "tz": tz},
success: function(data) { success: function(data) {
$("#signup-result").html(data).show(); $("#signup-result").html(data).show();
$("#signup-go").prop("disabled", false); $("#signup-go").prop("disabled", false);


Loading…
Cancel
Save