From fb121513d41ebf0b5f6c259bf9445bf774fd0b42 Mon Sep 17 00:00:00 2001 From: Joe Alcorn Date: Thu, 2 May 2013 23:48:55 +0100 Subject: [PATCH 01/14] Add UI for linking email accounts. TODO: Actually store the email address and send email with confirmation link --- branch.sql | 3 ++ gittip/models/participant.py | 3 ++ templates/connected-accounts.html | 24 ++++++++++++++++ www/%username/email.json | 47 +++++++++++++++++++++++++++++++ www/assets/%version/profile.js | 37 ++++++++++++++++++++++++ 5 files changed, 114 insertions(+) create mode 100644 branch.sql create mode 100644 www/%username/email.json diff --git a/branch.sql b/branch.sql new file mode 100644 index 0000000000..85411bd120 --- /dev/null +++ b/branch.sql @@ -0,0 +1,3 @@ +ALTER TABLE participants ADD email_address text DEFAULT NULL; +ALTER TABLE participants ADD email_confirmed boolean NOT NULL DEFAULT FALSE; +ALTER TABLE participants ADD email_collected timestamp with time zone DEFAULT NULL; diff --git a/gittip/models/participant.py b/gittip/models/participant.py index 5f63c7bf97..e5b87fdc1c 100644 --- a/gittip/models/participant.py +++ b/gittip/models/participant.py @@ -54,6 +54,9 @@ class Participant(db.Model): last_ach_result = Column(Text) is_suspicious = Column(Boolean) type = Column(Enum('individual', 'group', 'open group', nullable=False)) + email_address = Column(Text) + email_confirmed = Column(Boolean, nullable=False, default=False) + email_collected = Column(TIMESTAMP(timezone=True), default=None) ### Relations ### accounts_elsewhere = relationship( "Elsewhere" diff --git a/templates/connected-accounts.html b/templates/connected-accounts.html index 96e2ec2e41..b744a4735d 100644 --- a/templates/connected-accounts.html +++ b/templates/connected-accounts.html @@ -103,6 +103,30 @@

Connected Accounts

>on Balanced Payments{% else %}on Balanced Payments{% end %} + {% if user == participant or user.is_admin %} + + + + + + + {% if participant.email_address is None %} + Link an email address + {% else %} + {{ participant.email_address }} + {% end %} +
+ + +
+
(not publicly viewable)
+ + + {% end %} diff --git a/www/%username/email.json b/www/%username/email.json new file mode 100644 index 0000000000..23ae40cb5d --- /dev/null +++ b/www/%username/email.json @@ -0,0 +1,47 @@ +""" +Change the currently authenticated user's email address. +This will need to send a confirmation email in the future. +""" +import json + +from aspen import Response + + + +# ========================================================================== ^L + +if not POST: + body = {'error': 'HTTP method must be POST'} + response = Response(405, json.dumps(body)) + # We can't raise() this response obj because + # aspen will ignore our custom body. + # We must ensure it's named response at the end of the file + # (see raising responses http://aspen.io/api/response/) + +elif user.ANON: + body = {"error": "you're not authenticated"} + response = Response(401, json.dumps(body)) + +elif 'email' not in request.body: + body = {'error': 'missing required parameters'} + response = Response(400, json.dumps(body)) + +else: + # regex validation of email gets real messy + # if you want to take care of all edge cases + + # there's not much point to it if we're making + # users confirm their email addresses anyway + + # but we should at least confirm the presence + # of a '@' and '.' + + address = request.body['email'] + + if not '@' in address or not '.' in address: + body = {'error': 'invalid email address'} + response = Response(400, json.dumps(body)) + else: + # Woohoo! valid request, store it! + + response.body = {'email': address} diff --git a/www/assets/%version/profile.js b/www/assets/%version/profile.js index b057fd0bfe..c2e4662e68 100644 --- a/www/assets/%version/profile.js +++ b/www/assets/%version/profile.js @@ -334,4 +334,41 @@ $(document).ready(function() } ); }); + + // Wire up email address input. + // ============================ + $('a.email').click(function() + { + // "Link email address" text or existing + // email was clicked, show the text box + $('.email').toggle(); + $('input.email').focus(); + }); + $('form.email-submit').submit(function(event) + { + event.preventDefault(); + + $('button.email').text('Saving ...'); + var address = $('input.email').val(); + + jQuery.ajax({ + url: "email.json", + type: "POST", + dataType: "json", + data: { + email: address + }, + success: function(data) { + $('a.email').text(data.email); + $('.email').toggle(); + $('button.email').text('Save'); + }, + error: function() { + $('button.email').text('Save'); + alert('Failed to save your email address. ' + + 'Please try again.' + ); + } + }); + }); }); From 40d0a152348a8d3b8969618a3dc9560b780ac8b8 Mon Sep 17 00:00:00 2001 From: Joe Alcorn Date: Sun, 5 May 2013 15:40:08 +0100 Subject: [PATCH 02/14] Update schema (http://git.io/375UHA) --- branch.sql | 47 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/branch.sql b/branch.sql index 85411bd120..c5af803da5 100644 --- a/branch.sql +++ b/branch.sql @@ -1,3 +1,44 @@ -ALTER TABLE participants ADD email_address text DEFAULT NULL; -ALTER TABLE participants ADD email_confirmed boolean NOT NULL DEFAULT FALSE; -ALTER TABLE participants ADD email_collected timestamp with time zone DEFAULT NULL; +BEGIN; + CREATE TYPE usage_type AS ENUM ('notifications', 'gravatar', 'paypal'); + CREATE TYPE email_address_with_confirmation AS + ( + address text, + confirmed boolean + ); + + ALTER TABLE participants ADD email_notifications email_address_with_confirmation DEFAULT NULL; + ALTER TABLE participants ADD email_gravatar email_address_with_confirmation DEFAULT NULL; + ALTER TABLE participants ADD email_paypal 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, + usage usage_type NOT NULL, + participant text NOT NULL REFERENCES participants ON UPDATE CASCADE ON DELETE RESTRICT + ); + + + CREATE RULE log_notification_email_changes AS ON UPDATE + TO participants WHERE (OLD.email_notifications IS NULL AND NOT NEW.email_notifications IS NULL) + OR (NEW.email_notifications IS NULL AND NOT OLD.email_notifications IS NULL) + OR NEW.email_notifications <> OLD.email_notifications + DO INSERT INTO emails (email, usage, participant) + VALUES (NEW.email_notifications, 'notifications', OLD.username); + + CREATE RULE log_gravatar_email_changes AS ON UPDATE + TO participants WHERE (OLD.email_gravatar IS NULL AND NOT NEW.email_gravatar IS NULL) + OR (NEW.email_gravatar IS NULL AND NOT OLD.email_gravatar IS NULL) + OR NEW.email_gravatar <> OLD.email_gravatar + DO INSERT INTO emails (email, usage, participant) + VALUES (NEW.email_gravatar, 'gravatar', OLD.username); + + CREATE RULE log_paypal_email_changes AS ON UPDATE + TO participants WHERE (OLD.email_paypal IS NULL AND NOT NEW.email_paypal IS NULL) + OR (NEW.email_paypal IS NULL AND NOT OLD.email_paypal IS NULL) + OR NEW.email_paypal <> OLD.email_paypal + DO INSERT INTO emails (email, usage, participant) + VALUES (NEW.email_paypal, 'paypal', OLD.username); + +END; From b0124f88bb3a3729eb054c2ed71690b106f684a5 Mon Sep 17 00:00:00 2001 From: Chad Whitacre Date: Fri, 2 Aug 2013 12:36:50 -0400 Subject: [PATCH 03/14] Start trimming email schema for a single email --- branch.sql | 38 ++++++++++++-------------------------- 1 file changed, 12 insertions(+), 26 deletions(-) diff --git a/branch.sql b/branch.sql index c5af803da5..d3f7b4ff76 100644 --- a/branch.sql +++ b/branch.sql @@ -1,44 +1,30 @@ BEGIN; - CREATE TYPE usage_type AS ENUM ('notifications', 'gravatar', 'paypal'); - CREATE TYPE email_address_with_confirmation AS + CREATE TYPE email_address_with_confirmation AS ( address text, confirmed boolean ); - ALTER TABLE participants ADD email_notifications email_address_with_confirmation DEFAULT NULL; - ALTER TABLE participants ADD email_gravatar email_address_with_confirmation DEFAULT NULL; - ALTER TABLE participants ADD email_paypal email_address_with_confirmation DEFAULT NULL; + 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, - usage usage_type NOT NULL, - participant text NOT NULL REFERENCES participants ON UPDATE CASCADE ON DELETE RESTRICT + ( 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_notification_email_changes AS ON UPDATE + CREATE RULE log_email_changes AS ON UPDATE TO participants WHERE (OLD.email_notifications IS NULL AND NOT NEW.email_notifications IS NULL) OR (NEW.email_notifications IS NULL AND NOT OLD.email_notifications IS NULL) OR NEW.email_notifications <> OLD.email_notifications DO INSERT INTO emails (email, usage, participant) VALUES (NEW.email_notifications, 'notifications', OLD.username); - CREATE RULE log_gravatar_email_changes AS ON UPDATE - TO participants WHERE (OLD.email_gravatar IS NULL AND NOT NEW.email_gravatar IS NULL) - OR (NEW.email_gravatar IS NULL AND NOT OLD.email_gravatar IS NULL) - OR NEW.email_gravatar <> OLD.email_gravatar - DO INSERT INTO emails (email, usage, participant) - VALUES (NEW.email_gravatar, 'gravatar', OLD.username); - - CREATE RULE log_paypal_email_changes AS ON UPDATE - TO participants WHERE (OLD.email_paypal IS NULL AND NOT NEW.email_paypal IS NULL) - OR (NEW.email_paypal IS NULL AND NOT OLD.email_paypal IS NULL) - OR NEW.email_paypal <> OLD.email_paypal - DO INSERT INTO emails (email, usage, participant) - VALUES (NEW.email_paypal, 'paypal', OLD.username); - END; From 9a7cb8edbd75af4fb3c9f467e3040500a50cad83 Mon Sep 17 00:00:00 2001 From: Chad Whitacre Date: Fri, 2 Aug 2013 12:37:58 -0400 Subject: [PATCH 04/14] ADD A WHITESPACE CHAraCTER --- branch.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/branch.sql b/branch.sql index d3f7b4ff76..dbb4b4ca8b 100644 --- a/branch.sql +++ b/branch.sql @@ -17,7 +17,7 @@ BEGIN; REFERENCES participants ON UPDATE CASCADE ON DELETE RESTRICT - ); + ); CREATE RULE log_email_changes AS ON UPDATE From b461ac881b658a4c4aad7f8af34bdc550e9a0682 Mon Sep 17 00:00:00 2001 From: Chad Whitacre Date: Fri, 2 Aug 2013 12:44:00 -0400 Subject: [PATCH 05/14] Fix email log rule so it can be created at least --- branch.sql | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/branch.sql b/branch.sql index dbb4b4ca8b..602ecd7e4b 100644 --- a/branch.sql +++ b/branch.sql @@ -21,10 +21,10 @@ BEGIN; CREATE RULE log_email_changes AS ON UPDATE - TO participants WHERE (OLD.email_notifications IS NULL AND NOT NEW.email_notifications IS NULL) - OR (NEW.email_notifications IS NULL AND NOT OLD.email_notifications IS NULL) - OR NEW.email_notifications <> OLD.email_notifications - DO INSERT INTO emails (email, usage, participant) - VALUES (NEW.email_notifications, 'notifications', OLD.username); + 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; From db4e30357983fc98467c1ab7326b0d512dae584f Mon Sep 17 00:00:00 2001 From: Neil Kistner Date: Tue, 6 Aug 2013 19:25:20 -0500 Subject: [PATCH 06/14] fixing model properties --- gittip/models/participant.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/gittip/models/participant.py b/gittip/models/participant.py index 8f35389fa9..370b4f6ce3 100644 --- a/gittip/models/participant.py +++ b/gittip/models/participant.py @@ -54,9 +54,7 @@ class Participant(db.Model): api_key = Column(Text) is_suspicious = Column(Boolean) number = Column(Enum('singular', 'plural', nullable=False)) - email_address = Column(Text) - email_confirmed = Column(Boolean, nullable=False, default=False) - email_collected = Column(TIMESTAMP(timezone=True), default=None) + email = Column(Text) ### Relations ### accounts_elsewhere = relationship( "Elsewhere" From 45988276b499b0cfe1a57201bcfc3e564233e70b Mon Sep 17 00:00:00 2001 From: Neil Kistner Date: Sun, 11 Aug 2013 12:52:29 -0500 Subject: [PATCH 07/14] added participant.set_email() along with passing tests --- gittip/orm/__init__.py | 1 + gittip/participant.py | 5 +++++ tests/test_participant.py | 12 ++++++++++++ 3 files changed, 18 insertions(+) diff --git a/gittip/orm/__init__.py b/gittip/orm/__init__.py index bee3b5b829..20588314c5 100644 --- a/gittip/orm/__init__.py +++ b/gittip/orm/__init__.py @@ -54,6 +54,7 @@ def make_declarative_base(self): def empty_tables(self): gittip.db.run("DELETE FROM memberships") # *sigh* gittip.db.run("DELETE FROM log_participant_number") # *sigh* + gittip.db.run("DELETE FROM emails") # *sigh* tables = reversed(self.metadata.sorted_tables) for table in tables: try: diff --git a/gittip/participant.py b/gittip/participant.py index a075d1db33..f31afa7586 100644 --- a/gittip/participant.py +++ b/gittip/participant.py @@ -213,6 +213,11 @@ def change_username(self, suggested): assert suggested == rec['username'] # sanity check self.username = suggested + @require_username + def set_email(self, email, confirmed=False): + gittip.db.run("UPDATE participants " + "SET email = (%s, %s) WHERE username=%s", + (email, confirmed, self.username)) @require_username def get_accounts_elsewhere(self): diff --git a/tests/test_participant.py b/tests/test_participant.py index c75caf580f..b37b152fa1 100644 --- a/tests/test_participant.py +++ b/tests/test_participant.py @@ -113,6 +113,18 @@ def setUp(self): twitter_account = TwitterAccount(idx, {'screen_name': username}) Participant(username).take_over(twitter_account) + def test_can_change_email(self): + Participant('alice').set_email('alice@gmail.com') + expected = '(alice@gmail.com,f)' + actual = Participant('alice').get_details()['email'] + assert_equals(actual, expected) + + def test_can_confirm_email(self): + Participant('alice').set_email('alice@gmail.com', True) + expected = '(alice@gmail.com,t)' + actual = Participant('alice').get_details()['email'] + assert_equals(actual, expected) + def test_cant_take_over_claimed_participant_without_confirmation(self): bob_twitter = StubAccount('twitter', '2') with assert_raises(NeedConfirmation): From dd192d1dbd968a4ed0a2043512f5239c4840f9d0 Mon Sep 17 00:00:00 2001 From: Neil Kistner Date: Mon, 9 Sep 2013 23:49:20 -0500 Subject: [PATCH 08/14] get tests passing --- templates/connected-accounts.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/connected-accounts.html b/templates/connected-accounts.html index 9801a88b2d..5a36c76a3b 100644 --- a/templates/connected-accounts.html +++ b/templates/connected-accounts.html @@ -127,7 +127,7 @@

Connected Accounts

>on Balanced Payments{% else %}on Balanced Payments{% end %} - {% if user == participant or user.is_admin %} + {% if user == participant or user.ADMIN %} From a7bd909c86ab83a411de5890cd02a0fd8dbbe3bc Mon Sep 17 00:00:00 2001 From: Neil Kistner Date: Tue, 10 Sep 2013 10:54:57 -0500 Subject: [PATCH 09/14] getting email to save --- gittip/models/participant.py | 11 ++- templates/connected-accounts.html | 13 ++-- www/%username/{email.json => email.json.spt} | 3 +- www/assets/%version/gittip/profile.js | 77 ++++++++++++++------ 4 files changed, 72 insertions(+), 32 deletions(-) rename www/%username/{email.json => email.json.spt} (93%) diff --git a/gittip/models/participant.py b/gittip/models/participant.py index eadbf8dfba..51b0f7175c 100644 --- a/gittip/models/participant.py +++ b/gittip/models/participant.py @@ -291,8 +291,17 @@ def change_username(self, suggested): def change_email(self, email, confirmed=False): gittip.db.run("UPDATE participants " - "SET email = (%s, %s) WHERE username=%s", + "SET email = ROW(%s, %s) WHERE username=%s", (email, confirmed, self.username)) + self.set_attributes(email=(email, confirmed)) + + + def get_email(self): + rec = gittip.db.one("SELECT (email).address, (email).confirmed " + "FROM participants WHERE username=%s", + (self.username,)) + + return rec def update_goal(self, goal): diff --git a/templates/connected-accounts.html b/templates/connected-accounts.html index 5a36c76a3b..a256376be4 100644 --- a/templates/connected-accounts.html +++ b/templates/connected-accounts.html @@ -127,25 +127,26 @@

Connected Accounts

>on Balanced Payments{% else %}on Balanced Payments{% end %} - {% if user == participant or user.ADMIN %} + {% if user.participant == participant or user.ADMIN %} - {% if participant.email_address is None %} - + {% if participant.email is None %} + {% else %} - + {% end %} diff --git a/www/%username/email.json b/www/%username/email.json.spt similarity index 93% rename from www/%username/email.json rename to www/%username/email.json.spt index 23ae40cb5d..c30283aee5 100644 --- a/www/%username/email.json +++ b/www/%username/email.json.spt @@ -8,7 +8,7 @@ from aspen import Response -# ========================================================================== ^L +[-----------------------------------------] if not POST: body = {'error': 'HTTP method must be POST'} @@ -43,5 +43,6 @@ else: response = Response(400, json.dumps(body)) else: # Woohoo! valid request, store it! + user.participant.change_email(address) response.body = {'email': address} diff --git a/www/assets/%version/gittip/profile.js b/www/assets/%version/gittip/profile.js index afd45b35cc..7bb8914e46 100644 --- a/www/assets/%version/gittip/profile.js +++ b/www/assets/%version/gittip/profile.js @@ -375,31 +375,60 @@ $(document).ready(function() $('.email').toggle(); $('input.email').focus(); }); - $('form.email-submit').submit(function(event) - { - event.preventDefault(); - - $('button.email').text('Saving ...'); - var address = $('input.email').val(); - - jQuery.ajax({ - url: "email.json", - type: "POST", - dataType: "json", - data: { - email: address - }, - success: function(data) { + + $('.email-submit') + .on('click', '[type=submit]', function () { + var $this = $(this); + + $this.text('Saving...'); + + $.post('email.json' + , { email: $('input.email').val() } + , 'json' + ).done(function (data) { $('a.email').text(data.email); $('.email').toggle(); - $('button.email').text('Save'); - }, - error: function() { - $('button.email').text('Save'); - alert('Failed to save your email address. ' + - 'Please try again.' - ); - } + $this.text('Save'); + }).fail(function (data) { + $this.text('Save'); + alert('Failed to save your email address. ' + + 'Please try again.'); + }); + + return false; + }) + .on('click', '[type=cancel]', function () { + $('.email').toggle(); + + return false; }); - }); + + + // $('form.email-submit').submit(function(event) + // { + // event.preventDefault(); + + // $('[type=submit]').text('Saving...'); + // var address = $('input.email').val(); + + // jQuery.ajax({ + // url: "email.json", + // type: "POST", + // dataType: "json", + // data: { + // email: address + // }, + // success: function(data) { + // $('a.email').text(data.email); + // $('.email').toggle(); + // $('[type=').text('Save'); + // }, + // error: function() { + // $('button.email').text('Save'); + // alert('Failed to save your email address. ' + + // 'Please try again.' + // ); + // } + // }); + // }); }); From 3779611609fa360cb3ee95c1241dfceda09c8c78 Mon Sep 17 00:00:00 2001 From: Chad Whitacre Date: Tue, 10 Sep 2013 13:21:39 -0400 Subject: [PATCH 10/14] Implement model for email address w/ confirmation https://botbot.me/freenode/gittip/msg/5901472/ --- gittip/models/email_address_with_confirmation.py | 6 ++++++ gittip/models/participant.py | 8 -------- gittip/wireup.py | 2 ++ templates/connected-accounts.html | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) create mode 100644 gittip/models/email_address_with_confirmation.py 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 51b0f7175c..464ca1c5f9 100644 --- a/gittip/models/participant.py +++ b/gittip/models/participant.py @@ -295,14 +295,6 @@ def change_email(self, email, confirmed=False): (email, confirmed, self.username)) self.set_attributes(email=(email, confirmed)) - - def get_email(self): - rec = gittip.db.one("SELECT (email).address, (email).confirmed " - "FROM participants WHERE username=%s", - (self.username,)) - - return rec - def update_goal(self, goal): typecheck(goal, (Decimal, None)) diff --git a/gittip/wireup.py b/gittip/wireup.py index f2b02eea48..02deede417 100644 --- a/gittip/wireup.py +++ b/gittip/wireup.py @@ -12,6 +12,7 @@ import gittip.utils.mixpanel from gittip.models.community import Community from gittip.models.participant import Participant +from gittip.models.email_address_with_confirmation import EmailAddressWithConfirmation from postgres import Postgres @@ -32,6 +33,7 @@ def db(): db.register_model(Community) db.register_model(Participant) + db.register_model(EmailAddressWithConfirmation) return db diff --git a/templates/connected-accounts.html b/templates/connected-accounts.html index a256376be4..38c0e8c809 100644 --- a/templates/connected-accounts.html +++ b/templates/connected-accounts.html @@ -137,7 +137,7 @@

Connected Accounts

{% if participant.email is None %} {% else %} - + {% end %}