diff --git a/branch.sql b/branch.sql new file mode 100644 index 0000000000..602ecd7e4b --- /dev/null +++ b/branch.sql @@ -0,0 +1,30 @@ +BEGIN; + CREATE TYPE email_address_with_confirmation AS + ( + address text, + confirmed boolean + ); + + ALTER TABLE participants ADD email email_address_with_confirmation + DEFAULT NULL; + + CREATE TABLE emails + ( id serial PRIMARY KEY + , email email_address_with_confirmation NOT NULL + , ctime timestamp with time zone NOT NULL + DEFAULT CURRENT_TIMESTAMP + , participant text NOT NULL + REFERENCES participants + ON UPDATE CASCADE + ON DELETE RESTRICT + ); + + + CREATE RULE log_email_changes AS ON UPDATE + TO participants WHERE (OLD.email IS NULL AND NOT NEW.email IS NULL) + OR (NEW.email IS NULL AND NOT OLD.email IS NULL) + OR NEW.email <> OLD.email + DO INSERT INTO emails (email, participant) + VALUES (NEW.email, OLD.username); + +END; diff --git a/gittip/models/email_address_with_confirmation.py b/gittip/models/email_address_with_confirmation.py new file mode 100644 index 0000000000..9996e87e6a --- /dev/null +++ b/gittip/models/email_address_with_confirmation.py @@ -0,0 +1,6 @@ +from postgres.orm import Model + + +class EmailAddressWithConfirmation(Model): + + typname = "email_address_with_confirmation" diff --git a/gittip/models/participant.py b/gittip/models/participant.py index f19aa0225d..e7585c00de 100644 --- a/gittip/models/participant.py +++ b/gittip/models/participant.py @@ -314,6 +314,13 @@ def change_username(self, suggested): return suggested + def update_email(self, email, confirmed=False): + with self.db.get_cursor() as c: + add_event(c, 'participant', dict(id=self.id, action='set', values=dict(current_email=email))) + c.one("UPDATE participants SET email = ROW(%s, %s) WHERE username=%s RETURNING id" + , (email, confirmed, self.username) + ) + self.set_attributes(email=(email, confirmed)) def update_goal(self, goal): typecheck(goal, (Decimal, None)) diff --git a/gittip/wireup.py b/gittip/wireup.py index fc1ec0faad..74412c6c91 100644 --- a/gittip/wireup.py +++ b/gittip/wireup.py @@ -19,6 +19,7 @@ from gittip.models.account_elsewhere import AccountElsewhere from gittip.models.community import Community from gittip.models.participant import Participant +from gittip.models.email_address_with_confirmation import EmailAddressWithConfirmation from gittip.models import GittipDB @@ -35,6 +36,7 @@ def db(env): db.register_model(Community) db.register_model(AccountElsewhere) db.register_model(Participant) + db.register_model(EmailAddressWithConfirmation) return db diff --git a/js/gittip/profile.js b/js/gittip/profile.js index 4c0b609672..6cc3d1e68b 100644 --- a/js/gittip/profile.js +++ b/js/gittip/profile.js @@ -356,4 +356,57 @@ Gittip.profile.init = function() { return false; }); + + // Wire up email address input. + // ============================ + $('.email').on("click", ".toggle-email", function() + { + $('.email').toggle(); + $('input.email').focus(); + }); + + // Wire up email form. + $('.email-submit') + .on('click', '[type=submit]', function () { + var $this = $(this); + + $this.text('Saving...'); + + function success(data) { + $('.email-address').text(data.email); + $('.email').toggle(); + if (data.email === '') { + html = "None" + html += ""; + } else { + html = "" + data.email + ""; + html += ""; + } + $('div.email').html(html); + $this.text('Save'); + } + + $.ajax({ + url: "email.json", + type: "POST", + dataType: 'json', + success: success, + error: function (data) { + $this.text('Save'); + Gittip.notification('Failed to save your email address. ' + + 'Please try again.', 'error'); + }, + data: { + email: $('input.email').val() + } + } + ) + + return false; + }) + .on('click', '[type=cancel]', function () { + $('.email').toggle(); + + return false; + }); }; diff --git a/scss/modules.scss b/scss/modules.scss index aee58e018a..37847c83d7 100644 --- a/scss/modules.scss +++ b/scss/modules.scss @@ -80,7 +80,7 @@ a.mini-user:hover { float: right; margin-top: -13px; } - .auth-button, .account-delete, .toggle-bitcoin { + .auth-button, .account-delete, .toggle-bitcoin, .toggle-email { float: right; margin-top: -13px; } diff --git a/scss/profile-edit.scss b/scss/profile-edit.scss index 0dd41a1a1a..10cb87efd7 100644 --- a/scss/profile-edit.scss +++ b/scss/profile-edit.scss @@ -57,7 +57,7 @@ } } } - .bitcoin-submit { + .email-submit, .bitcoin-submit { .address input { width: 100%; margin: 5px 0; diff --git a/templates/connected-accounts.html b/templates/connected-accounts.html index b624b47d1e..9487d21d1c 100644 --- a/templates/connected-accounts.html +++ b/templates/connected-accounts.html @@ -10,6 +10,46 @@

Social Profiles

{{ account_row(platform, accounts, auth_button) }} {% endif %} {% endfor %} + + {% if user.participant == participant or user.ADMIN %} + + + + + +
Email Address (Private)
+ {% if not user.ANON and user.participant == participant %} +
+ {% else %} +
+ {% endif %} + {% if participant.email %} + + + {% else %} + None + + {% endif %} +
+ +
+
+ +
+
+ + +
+
+ + + {% endif %} {% if not user.ANON and user.participant == participant %} @@ -104,3 +144,5 @@

Moving Money Every Week

+ + diff --git a/tests/py/test_email_json.py b/tests/py/test_email_json.py new file mode 100644 index 0000000000..153c3e12a1 --- /dev/null +++ b/tests/py/test_email_json.py @@ -0,0 +1,39 @@ +from __future__ import unicode_literals + +import json + +from gittip.testing import Harness + +class TestMembernameJson(Harness): + + def change_email_address(self, address, user='alice', should_fail=True): + self.make_participant("alice") + + if should_fail: + response = self.client.PxST("/alice/email.json" + , {'email': address,} + , auth_as=user + ) + else: + response = self.client.POST("/alice/email.json" + , {'email': address,} + , auth_as=user + ) + return response + + def test_participant_can_change_email(self): + response = self.change_email_address('alice@gittip.com', should_fail=False) + actual = json.loads(response.body)['email'] + assert actual == 'alice@gittip.com', actual + + def test_post_anon_returns_404(self): + response = self.change_email_address('anon@gittip.com', user=None) + assert response.code == 404, response.code + + def test_post_with_no_at_symbol_is_400(self): + response = self.change_email_address('gittip.com') + assert response.code == 400, response.code + + def test_post_with_no_period_symbol_is_400(self): + response = self.change_email_address('test@gittip') + assert response.code == 400, response.code diff --git a/tests/py/test_participant.py b/tests/py/test_participant.py index df74fa9a23..9dd575ebe7 100644 --- a/tests/py/test_participant.py +++ b/tests/py/test_participant.py @@ -185,6 +185,17 @@ def test_john_is_plural(self): actual = Participant.from_username('john').IS_PLURAL assert actual == expected + def test_can_change_email(self): + Participant.from_username('alice').update_email('alice@gittip.com') + expected = 'alice@gittip.com' + actual = Participant.from_username('alice').email.address + assert actual == expected + + def test_can_confirm_email(self): + Participant.from_username('alice').update_email('alice@gittip.com', True) + actual = Participant.from_username('alice').email.confirmed + assert actual == True + def test_cant_take_over_claimed_participant_without_confirmation(self): bob_twitter = self.make_elsewhere('twitter', '2', 'bob') with self.assertRaises(NeedConfirmation): diff --git a/www/%username/email.json.spt b/www/%username/email.json.spt new file mode 100644 index 0000000000..9e9647e7da --- /dev/null +++ b/www/%username/email.json.spt @@ -0,0 +1,26 @@ +""" +Change the currently authenticated user's email address. +This will need to send a confirmation email in the future. +""" +import json +import re + +from aspen import Response + +[-----------------------------------------] + +if user.ANON: + raise Response(404) +request.allow("POST") + +address = request.body['email'] + +# This checks for exactly one @ and at least one . after @ +# The real validation will happen when we send the email +if not re.match(r"[^@]+@[^@]+\.[^@]+", address): + raise Response(400) +else: + # Woohoo! valid request, store it! + user.participant.update_email(address) + + response.body = {'email': address} diff --git a/www/assets/%version/email.png b/www/assets/%version/email.png new file mode 100644 index 0000000000..20847fa67e Binary files /dev/null and b/www/assets/%version/email.png differ