Skip to content
This repository has been archived by the owner on Feb 8, 2018. It is now read-only.

Link venmo #1857

Merged
merged 30 commits into from
Jan 10, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
ba597f6
Began adding account linking with Venmo
Jan 7, 2014
ff9ab95
Add venmo api keys for dev, began associate
Jan 7, 2014
dcb34fd
typo
Jan 7, 2014
f167db9
WIP connecting venmo
simon-weber Jan 7, 2014
c3f780b
Save venmo information to db, including oauth
Jan 7, 2014
880663d
Refactor confirm
Jan 7, 2014
8df9b9c
create platform -> class registry for AccountElsewhere
simon-weber Jan 7, 2014
2d6bc4b
leave registry interface as dictionary
simon-weber Jan 7, 2014
3ed0f6e
wip refactor
Jan 7, 2014
6359aea
remove username helpers comment
simon-weber Jan 8, 2014
fbdb2ac
Merge branch 'D-refactor-platforms' into link-venmo
simon-weber Jan 8, 2014
e475555
Merge branch 'refactor-platforms' into link-venmo
simon-weber Jan 8, 2014
2a677c6
Merge branch 'master' into link-venmo
simon-weber Jan 8, 2014
32b7d70
Update/organize default env files
Jan 8, 2014
2d7ee98
use requests.json rather than json.loads
simon-weber Jan 8, 2014
277ca54
DRYer AccountElsewhere
Jan 8, 2014
b763a21
WIP link
Jan 8, 2014
ff7298e
note platform_classes requirement
simon-weber Jan 8, 2014
bcdaa14
Merge branch 'link-venmo' of github.com:venmo/www.gittip.com into lin…
simon-weber Jan 8, 2014
8f5915f
resolve platform_classes ordering
simon-weber Jan 8, 2014
c0029ac
prevent data leakage in %username/public.json
simon-weber Jan 9, 2014
427ae5e
ensure platform_classes is populated at config-time
simon-weber Jan 9, 2014
cac31ca
use platform_classes in configure-aspen
simon-weber Jan 9, 2014
2897cad
Merge branch 'master' into link-venmo
Jan 9, 2014
30988e8
Add openstreetmap to list of accounts
Jan 9, 2014
5262b2c
remove debug line from associate view
Jan 9, 2014
257647a
wrap schema changes in transaction
simon-weber Jan 10, 2014
8a8cffa
remove duplicated ProblemChangingUsername exception
simon-weber Jan 10, 2014
9780f0b
Merge upstream
simon-weber Jan 10, 2014
1d2ce2f
remove unused platforms_ordered import
simon-weber Jan 10, 2014
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions branch.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
BEGIN;
ALTER TABLE elsewhere ADD COLUMN access_token text DEFAULT NULL;
ALTER TABLE elsewhere ADD COLUMN refresh_token text DEFAULT NULL;
ALTER TABLE elsewhere ADD COLUMN expires timestamp with time zone DEFAULT NULL;
END;
16 changes: 9 additions & 7 deletions configure-aspen.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import division

from importlib import import_module
import os
import sys
import threading
Expand All @@ -11,6 +12,8 @@
from gittip import canonize, configure_payments
from gittip.security import authentication, csrf, x_frame_options
from gittip.utils import cache_static, timer
from gittip.elsewhere import platform_classes


from aspen import log_dammit

Expand Down Expand Up @@ -45,6 +48,11 @@
gittip.wireup.envvars(website)
tell_sentry = gittip.wireup.make_sentry_teller(website)

# this serves two purposes:
# 1) ensure all platform classes are created (and thus added to platform_classes)
# 2) keep the platform modules around to be added to the context below
platform_modules = {platform: import_module("gittip.elsewhere.%s" % platform)
for platform in platform_classes}

# The homepage wants expensive queries. Let's periodically select into an
# intermediate table.
Expand Down Expand Up @@ -117,14 +125,8 @@ def log_busy_threads():
# =================

def add_stuff_to_context(request):
from gittip.elsewhere import bitbucket, github, twitter, bountysource, openstreetmap
request.context['username'] = None
request.context['bitbucket'] = bitbucket
request.context['github'] = github
request.context['twitter'] = twitter
request.context['bountysource'] = bountysource
request.context['openstreetmap'] = openstreetmap

request.context.update(platform_modules)

def scab_body_onto_response(response):

Expand Down
23 changes: 19 additions & 4 deletions default_local.env
Original file line number Diff line number Diff line change
@@ -1,38 +1,53 @@
DATABASE_URL=postgres://gittip:gittip@localhost:5432/gittip
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*.env reorg looks good, thanks. :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing to note, swaddle now displays 11 warnings in a row about skipping blank lines.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, we must have missed that because we didn't regenerate our environments. Would running after a make clean show me these warnings?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't sweat it, we're porting away from swaddle (#468).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless @clone1018 wants to sweat it, and add line breaks under #468. :-)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Much lazy. Lack effort.


PYTHONDONTWRITEBYTECODE=true
CANONICAL_HOST=localhost:8537
CANONICAL_SCHEME=http
MIN_THREADS=10
DATABASE_URL=postgres://gittip:gittip@localhost:5432/gittip
DATABASE_MAXCONN=10

GITTIP_CSS_HREF=/assets/-/gittip.css
GITTIP_JS_SRC=/assets/-/gittip.js
GITTIP_CACHE_STATIC=no
GITTIP_COMPRESS_ASSETS=no

STRIPE_SECRET_API_KEY=1
STRIPE_PUBLISHABLE_API_KEY=1

BALANCED_API_SECRET=90bb3648ca0a11e1a977026ba7e239a9

GITHUB_CLIENT_ID=3785a9ac30df99feeef5
GITHUB_CLIENT_SECRET=e69825fafa163a0b0b6d2424c107a49333d46985
GITHUB_CALLBACK=http://127.0.0.1:8537/on/github/associate

BITBUCKET_CONSUMER_KEY=b8yzpsurhsmJufUqzh
BITBUCKET_CONSUMER_SECRET=WF3q2g7naRHeeGUjnxyRwPLVhMBU4dmP
BITBUCKET_CALLBACK=http://127.0.0.1:8537/on/bitbucket/associate

TWITTER_CONSUMER_KEY=QBB9vEhxO4DFiieRF68zTA
TWITTER_CONSUMER_SECRET=mUymh1hVMiQdMQbduQFYRi79EYYVeOZGrhj27H59H78
TWITTER_ACCESS_TOKEN=34175404-G6W8Hh19GWuUhIMEXK0LyZsy7N9aCMcy1bYJ9rI
TWITTER_ACCESS_TOKEN_SECRET=K6wxV1OCsihZAkEPkWtoLYDiRJnWajBBWn4UgliTRQ
TWITTER_CALLBACK=http://127.0.0.1:8537/on/twitter/associate
NANSWERS_THRESHOLD=2
NMEMBERS_THRESHOLD=50
UPDATE_HOMEPAGE_EVERY=10

BOUNTYSOURCE_API_SECRET=e2BbqjNY60kC7V-Uq1dv2oHgGavbWm9pUJmiRHCApFZHDiY9aZyAspInhZaZ94x9
BOUNTYSOURCE_API_HOST=https://staging-qa.bountysource.com
BOUNTYSOURCE_WWW_HOST=https://staging.bountysource.com
BOUNTYSOURCE_CALLBACK=http://127.0.0.1:8537/on/bountysource/associate

VENMO_CLIENT_ID=1534
VENMO_CLIENT_SECRET=55ckgsguYC3cj7xWW5c95PHvUzrwgZMA
VENMO_CALLBACK=http://127.0.0.1:8537/on/venmo/associate

OPENSTREETMAP_API=http://master.apis.dev.openstreetmap.org
OPENSTREETMAP_CONSUMER_KEY=J2SS5GM0A7tM1CIBjAHXUTMeCEkRBMYsTJzGONxe
OPENSTREETMAP_CONSUMER_SECRET=hgvZkbtWVOEoaJV5AzQPcBI9m8f7BylkpT0cP7wS
OPENSTREETMAP_CALLBACK=http://127.0.0.1:8537/on/openstreetmap/associate

NANSWERS_THRESHOLD=2
NMEMBERS_THRESHOLD=50

UPDATE_HOMEPAGE_EVERY=10
GOOGLE_ANALYTICS_ID=
SENTRY_DSN=
LOG_BUSY_THREADS_EVERY=0
Expand Down
23 changes: 19 additions & 4 deletions default_tests.env
Original file line number Diff line number Diff line change
@@ -1,38 +1,53 @@
DATABASE_URL=

PYTHONDONTWRITEBYTECODE=true
CANONICAL_HOST=
CANONICAL_SCHEME=http
MIN_THREADS=10
DATABASE_URL=
DATABASE_MAXCONN=10

GITTIP_CSS_HREF=
GITTIP_JS_SRC=
GITTIP_CACHE_STATIC=no
GITTIP_COMPRESS_ASSETS=no

STRIPE_SECRET_API_KEY=1
STRIPE_PUBLISHABLE_API_KEY=1

BALANCED_API_SECRET=90bb3648ca0a11e1a977026ba7e239a9

GITHUB_CLIENT_ID=3785a9ac30df99feeef5
GITHUB_CLIENT_SECRET=e69825fafa163a0b0b6d2424c107a49333d46985
GITHUB_CALLBACK=http://127.0.0.1:8537/on/github/associate

BITBUCKET_CONSUMER_KEY=b8yzpsurhsmJufUqzh
BITBUCKET_CONSUMER_SECRET=WF3q2g7naRHeeGUjnxyRwPLVhMBU4dmP
BITBUCKET_CALLBACK=http://127.0.0.1:8537/on/bitbucket/associate

TWITTER_CONSUMER_KEY=QBB9vEhxO4DFiieRF68zTA
TWITTER_CONSUMER_SECRET=mUymh1hVMiQdMQbduQFYRi79EYYVeOZGrhj27H59H78
TWITTER_ACCESS_TOKEN=34175404-G6W8Hh19GWuUhIMEXK0LyZsy7N9aCMcy1bYJ9rI
TWITTER_ACCESS_TOKEN_SECRET=K6wxV1OCsihZAkEPkWtoLYDiRJnWajBBWn4UgliTRQ
TWITTER_CALLBACK=http://127.0.0.1:8537/on/twitter/associate
NANSWERS_THRESHOLD=2
NMEMBERS_THRESHOLD=50
UPDATE_HOMEPAGE_EVERY=0

BOUNTYSOURCE_API_SECRET=e2BbqjNY60kC7V-Uq1dv2oHgGavbWm9pUJmiRHCApFZHDiY9aZyAspInhZaZ94x9
BOUNTYSOURCE_API_HOST=https://staging-qa.bountysource.com
BOUNTYSOURCE_WWW_HOST=https://staging.bountysource.com
BOUNTYSOURCE_CALLBACK=http://127.0.0.1:8537/on/bountysource/associate

VENMO_CLIENT_ID=1534
VENMO_CLIENT_SECRET=55ckgsguYC3cj7xWW5c95PHvUzrwgZMA
VENMO_CALLBACK=http://127.0.0.1:8537/on/venmo/associate

OPENSTREETMAP_API=http://master.apis.dev.openstreetmap.org
OPENSTREETMAP_CONSUMER_KEY=J2SS5GM0A7tM1CIBjAHXUTMeCEkRBMYsTJzGONxe
OPENSTREETMAP_CONSUMER_SECRET=hgvZkbtWVOEoaJV5AzQPcBI9m8f7BylkpT0cP7wS
OPENSTREETMAP_CALLBACK=http://127.0.0.1:8537/on/openstreetmap/associate

NANSWERS_THRESHOLD=2
NMEMBERS_THRESHOLD=50

UPDATE_HOMEPAGE_EVERY=0
GOOGLE_ANALYTICS_ID=
SENTRY_DSN=
LOG_BUSY_THREADS_EVERY=0
Expand Down
109 changes: 96 additions & 13 deletions gittip/elsewhere/__init__.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,81 @@
"""This subpackage contains functionality for working with accounts elsewhere.
"""
from __future__ import print_function, unicode_literals
from collections import OrderedDict

from aspen.utils import typecheck
from aspen import json
from psycopg2 import IntegrityError

import gittip
from gittip.exceptions import ProblemChangingUsername
from gittip.security.user import User
from gittip.models.participant import Participant, reserve_a_random_username
from gittip.exceptions import ProblemChangingUsername, UnknownPlatform
from gittip.utils.username import reserve_a_random_username


ACTIONS = [u'opt-in', u'connect', u'lock', u'unlock']


# to add a new elsewhere/platform:
# 1) add its name (also the name of its module) to this list.
# it's best to append it; this ordering is used in templates.
# 2) inherit from AccountElsewhere in the platform class
#
# platform_modules will populate the platform class automatically in configure-aspen.
platforms_ordered = (
'twitter',
'github',
'bitbucket',
'bountysource',
'venmo',
'openstreetmap'
)

# init-time key setup ensures the future ordering of platform_classes will match
# platforms_ordered, since overwriting entries will maintain their order.
platform_classes = OrderedDict([(platform, None) for platform in platforms_ordered])


class _RegisterPlatformMeta(type):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reminds me of something I was doing on the elsewhere refactor branch. Does this relate to that?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what I was thinking of:

https://github.com/gittip/www.gittip.com/pull/1369/files#diff-7698816a9bc10d53930d35de3e4bafdfR13

There I'm using __new__ to switch which AccountElsewhere subclass we're using to hydrate the result of any given database query.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, it accomplishes the same thing as the PlatformRegistry, but doesn't require the manual call to register.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I misunderstood. Yeah, I think hooking __new__ is the way to go when hydrating.

"""Tied to AccountElsewhere to enable registration by the platform field.
"""

def __new__(cls, name, bases, dct):
c = super(_RegisterPlatformMeta, cls).__new__(cls, name, bases, dct)

# * register the platform
# * verify it was added at init-time
# * register the subclass's json encoder with aspen
c_platform = getattr(c, 'platform')
if name == 'AccountElsewhere':
pass
elif c_platform not in platform_classes:
raise UnknownPlatform(c_platform) # has it been added to platform_classes init?
else:
platform_classes[c_platform] = c

# aspen's json encoder registry does not take class hierarchies into account,
# so we need to register the subclasses explicitly.
json.register_encoder(c, c.to_json_compatible_object)

return c

class AccountElsewhere(object):

__metaclass__ = _RegisterPlatformMeta

platform = None # set in subclass

def __init__(self, db, user_id, user_info=None):
"""Takes a user_id and user_info, and updates the database.
# only fields in this set will be encoded
json_encode_field_whitelist = set([
'id', 'is_locked', 'participant', 'platform', 'user_id', 'user_info',
])

def __init__(self, db, user_id, user_info=None, existing_record=None):
"""Either:
- Takes a user_id and user_info, and updates the database.

Or:
- Takes a user_id and existing_record, and constructs a "model" object out of the record
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if the existing_record hack is doing what #1369 is doing with the subclasses of AccountElsewhere.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, that's exactly what we were going for. We did it this way to avoid changing all the existing callsites, though hydrating with __new__ seems like the way to go in the future.

"""
typecheck(user_id, (int, unicode), user_info, (None, dict))
self.user_id = unicode(user_id)
Expand All @@ -33,10 +89,26 @@ def __init__(self, db, user_id, user_info=None):
self.is_locked = c
self.balance = d

self.user_info = user_info

# hack to make this into a weird pseudo-model that can share convenience methods
elif existing_record is not None:
self.participant = existing_record.participant
self.is_claimed, self.is_locked, self.balance = self.get_misc_info(self.participant)
self.user_info = existing_record.user_info
self.record = existing_record

def to_json_compatible_object(self):
"""
This is registered as an aspen.json encoder in configure-aspen
for all subclasses of this class.

def get_participant(self):
return Participant.query.get(username=self.participant)
It only exports fields in the whitelist.
"""
output = {k: v for (k,v) in self.record._asdict().items()
if k in self.json_encode_field_whitelist}

return output

def set_is_locked(self, is_locked):
self.db.run("""
Expand All @@ -51,6 +123,8 @@ def set_is_locked(self, is_locked):
def opt_in(self, desired_username):
"""Given a desired username, return a User object.
"""
from gittip.security.user import User

self.set_is_locked(False)
user = User.from_username(self.participant)
user.sign_in()
Expand Down Expand Up @@ -128,10 +202,9 @@ def upsert(self, user_info):

""", (user_info, self.platform, self.user_id))

return (username,) + self.get_misc_info(username)

# Get a little more info to return.
# =================================

def get_misc_info(self, username):
rec = self.db.one("""

SELECT claimed_time, balance, is_locked
Expand All @@ -145,9 +218,19 @@ def upsert(self, user_info):

assert rec is not None # sanity check


return ( username
, rec.claimed_time is not None
return ( rec.claimed_time is not None
, rec.is_locked
, rec.balance
)

def set_oauth_tokens(self, access_token, refresh_token, expires):
"""
Updates the elsewhere row with the given access token, refresh token, and Python datetime
"""

self.db.run("""
UPDATE elsewhere
SET (access_token, refresh_token, expires)
= (%s, %s, %s)
WHERE platform=%s AND user_id=%s
""", (access_token, refresh_token, expires, self.platform, self.user_id))
6 changes: 6 additions & 0 deletions gittip/elsewhere/bitbucket.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ def get_url(self):
url = "https://bitbucket.org/%s" % self.user_info["username"]
return url

def get_user_name(self):
return self.user_info['username']

def get_platform_icon(self):
return "/assets/icons/bitbucket.12.png"


def oauth_url(website, action, then=""):
"""Return a URL to start oauth dancing with Bitbucket.
Expand Down
6 changes: 6 additions & 0 deletions gittip/elsewhere/bountysource.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ def get_url(self):

raise NotImplementedError

def get_user_name(self):
return self.user_info['display_name']

def get_platform_icon(self):
return "/assets/icons/bountysource.12.png"


def oauth_url(website, participant, redirect_url=None):
"""Return a URL to authenticate with Bountysource.
Expand Down
6 changes: 6 additions & 0 deletions gittip/elsewhere/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ class GitHubAccount(AccountElsewhere):
def get_url(self):
return self.user_info['html_url']

def get_user_name(self):
return self.user_info['login']

def get_platform_icon(self):
return "/assets/icons/github.12.png"


def oauth_url(website, action, then=u""):
"""Given a website object and a string, return a URL string.
Expand Down
6 changes: 6 additions & 0 deletions gittip/elsewhere/twitter.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ class TwitterAccount(AccountElsewhere):
def get_url(self):
return "https://twitter.com/" + self.user_info['screen_name']

def get_user_name(self):
return self.user_info['screen_name']

def get_platform_icon(self):
return "/assets/icons/twitter.12.png"


def oauth_url(website, action, then=""):
"""Return a URL to start oauth dancing with Twitter.
Expand Down
Loading