diff --git a/hc/accounts/forms.py b/hc/accounts/forms.py index a93f3fa4..16f3f336 100644 --- a/hc/accounts/forms.py +++ b/hc/accounts/forms.py @@ -28,12 +28,13 @@ class Base64Field(forms.CharField): raise ValidationError(message="Cannot decode base64") -class AvailableEmailForm(forms.Form): +class SignupForm(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."} ) + tz = forms.CharField(required=False) def clean_identity(self): v = self.cleaned_data["identity"] @@ -47,6 +48,15 @@ class AvailableEmailForm(forms.Form): 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): # Call it "identity" instead of "email" diff --git a/hc/accounts/management/commands/createsuperuser.py b/hc/accounts/management/commands/createsuperuser.py index a90852ee..8257c3eb 100644 --- a/hc/accounts/management/commands/createsuperuser.py +++ b/hc/accounts/management/commands/createsuperuser.py @@ -1,7 +1,7 @@ import getpass 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 @@ -14,7 +14,7 @@ class Command(BaseCommand): while not email: raw = input("Email address:") - form = AvailableEmailForm({"identity": raw}) + form = SignupForm({"identity": raw}) if not form.is_valid(): self.stderr.write("Error: " + " ".join(form.errors["identity"])) continue diff --git a/hc/accounts/tests/test_signup.py b/hc/accounts/tests/test_signup.py index b4bba924..89e97459 100644 --- a/hc/accounts/tests/test_signup.py +++ b/hc/accounts/tests/test_signup.py @@ -9,8 +9,8 @@ from django.conf import settings class SignupTestCase(TestCase): @override_settings(USE_PAYMENTS=False) - def test_it_sends_link(self): - form = {"identity": "alice@example.org"} + def test_it_works(self): + form = {"identity": "alice@example.org", "tz": "Europe/Riga"} r = self.client.post("/accounts/signup/", form) self.assertContains(r, "Account created") @@ -21,8 +21,10 @@ class SignupTestCase(TestCase): # A profile should have been created profile = Profile.objects.get() + self.assertEqual(profile.check_limit, 500) self.assertEqual(profile.sms_limit, 500) self.assertEqual(profile.call_limit, 500) + self.assertEqual(profile.tz, "Europe/Riga") # And email sent self.assertEqual(len(mail.outbox), 1) @@ -44,25 +46,25 @@ class SignupTestCase(TestCase): self.assertEqual(channel.project, project) @override_settings(USE_PAYMENTS=True) - def test_it_sets_high_limits(self): - form = {"identity": "alice@example.org"} + def test_it_sets_limits(self): + form = {"identity": "alice@example.org", "tz": ""} self.client.post("/accounts/signup/", form) - # A profile should have been created profile = Profile.objects.get() + self.assertEqual(profile.check_limit, 20) self.assertEqual(profile.sms_limit, 5) self.assertEqual(profile.call_limit, 0) @override_settings(REGISTRATION_OPEN=False) def test_it_obeys_registration_open(self): - form = {"identity": "dan@example.org"} + form = {"identity": "dan@example.org", "tz": ""} r = self.client.post("/accounts/signup/", form) self.assertEqual(r.status_code, 403) def test_it_ignores_case(self): - form = {"identity": "ALICE@EXAMPLE.ORG"} + form = {"identity": "ALICE@EXAMPLE.ORG", "tz": ""} self.client.post("/accounts/signup/", form) # There should be exactly one user: @@ -73,19 +75,30 @@ class SignupTestCase(TestCase): alice = User(username="alice", email="alice@example.org") alice.save() - form = {"identity": "alice@example.org"} + form = {"identity": "alice@example.org", "tz": ""} r = self.client.post("/accounts/signup/", form) self.assertContains(r, "already exists") 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) self.assertContains(r, "Enter a valid email address") def test_it_checks_length(self): 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) self.assertContains(r, "Address is too long.") self.assertFalse(User.objects.exists()) + + @override_settings(USE_PAYMENTS=False) + def test_it_ignores_bad_tz(self): + form = {"identity": "alice@example.org", "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") diff --git a/hc/accounts/views.py b/hc/accounts/views.py index 09c12058..2f4e785c 100644 --- a/hc/accounts/views.py +++ b/hc/accounts/views.py @@ -59,7 +59,7 @@ def _allow_redirect(redirect_url): 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] user = User(username=username, email=email) user.set_unusable_password() @@ -84,7 +84,10 @@ def _make_user(email, with_project=True): channel.checks.add(check) # 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 @@ -173,10 +176,11 @@ def signup(request): return HttpResponseForbidden() ctx = {} - form = forms.AvailableEmailForm(request.POST) + form = forms.SignupForm(request.POST) if form.is_valid(): 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.send_instant_login_link() ctx["created"] = True diff --git a/static/js/signup.js b/static/js/signup.js index a674694f..15453011 100644 --- a/static/js/signup.js +++ b/static/js/signup.js @@ -4,11 +4,17 @@ $(function () { var base = document.getElementById("base-url").getAttribute("href").slice(0, -1); var email = $("#signup-email").val(); + try { + var tz = Intl.DateTimeFormat().resolvedOptions().timeZone; + } catch(err) { + var tz = "UTC"; + } + $("#signup-go").prop("disabled", true); $.ajax({ url: base + "/accounts/signup/", type: "post", - data: {"identity": email}, + data: {"identity": email, "tz": tz}, success: function(data) { $("#signup-result").html(data).show(); $("#signup-go").prop("disabled", false);