Skip to content
This repository has been archived by the owner. It is now read-only.

Link venmo #1857

Merged
merged 30 commits into from Jan 10, 2014
Merged

Link venmo #1857

Changes from 26 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 file types
Jump to…
Jump to file or symbol
Failed to load files and symbols.

Always

Just for now

@@ -0,0 +1,3 @@
ALTER TABLE elsewhere ADD COLUMN access_token text DEFAULT NULL;

This comment has been minimized.

Copy link
@chadwhitacre

chadwhitacre Jan 10, 2014

Contributor

Let's wrap these in a single BEGIN/END transaction block (see schema.sql for reference).

ALTER TABLE elsewhere ADD COLUMN refresh_token text DEFAULT NULL;
ALTER TABLE elsewhere ADD COLUMN expires timestamp with time zone DEFAULT NULL;
@@ -1,5 +1,6 @@
from __future__ import division

from importlib import import_module
import os
import threading
import time
@@ -10,6 +11,9 @@
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 platforms_ordered

This comment has been minimized.

Copy link
@chadwhitacre

chadwhitacre Jan 10, 2014

Contributor

This is unused in this file.

from gittip.elsewhere import platform_classes


from aspen import log_dammit

@@ -44,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.
@@ -117,14 +126,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)

algorithm = website.algorithm
algorithm.functions = [ timer.start
@@ -1,38 +1,53 @@
DATABASE_URL=postgres://gittip:gittip@localhost:5432/gittip

This comment has been minimized.

Copy link
@chadwhitacre

chadwhitacre Jan 10, 2014

Contributor

*.env reorg looks good, thanks. :)

This comment has been minimized.

Copy link
@clone1018

clone1018 Jan 10, 2014

Contributor

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

This comment has been minimized.

Copy link
@simon-weber

simon-weber Jan 10, 2014

Contributor

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

This comment has been minimized.

Copy link
@chadwhitacre

chadwhitacre Jan 10, 2014

Contributor

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

This comment has been minimized.

Copy link
@chadwhitacre

chadwhitacre Jan 10, 2014

Contributor

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

This comment has been minimized.

Copy link
@clone1018

clone1018 Jan 10, 2014

Contributor

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
@@ -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
@@ -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):

This comment has been minimized.

Copy link
@chadwhitacre

chadwhitacre Jan 10, 2014

Contributor

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

This comment has been minimized.

Copy link
@chadwhitacre

This comment has been minimized.

Copy link
@chadwhitacre

chadwhitacre Jan 10, 2014

Contributor

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.

This comment has been minimized.

Copy link
@simon-weber

simon-weber Jan 10, 2014

Contributor

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

This comment has been minimized.

Copy link
@simon-weber

simon-weber Jan 10, 2014

Contributor

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

This comment has been minimized.

Copy link
@chadwhitacre

chadwhitacre Jan 10, 2014

Contributor

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

This comment has been minimized.

Copy link
@simon-weber

simon-weber Jan 10, 2014

Contributor

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)
@@ -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("""
@@ -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()
@@ -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
@@ -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))
@@ -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.
@@ -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.
@@ -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.
@@ -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.
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.