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

Commit

Permalink
Factor out an AccountElsewhere class; #406
Browse files Browse the repository at this point in the history
  • Loading branch information
chadwhitacre committed Dec 14, 2012
1 parent a6f58c6 commit db5bb15
Show file tree
Hide file tree
Showing 10 changed files with 206 additions and 190 deletions.
186 changes: 117 additions & 69 deletions gittip/elsewhere/__init__.py
Original file line number Diff line number Diff line change
@@ -1,97 +1,145 @@
from aspen import log
import gittip
from aspen import Response
from aspen.utils import typecheck
from gittip import db
from gittip.authentication import User
from gittip.participant import reserve_a_random_participant_id
from psycopg2 import IntegrityError


def _resolve(platform, username_key, username):
"""Given three unicodes, return a participant_id.
"""
typecheck(platform, unicode, username_key, unicode, username, unicode)
rec = gittip.db.fetchone("""
SELECT participant_id
FROM elsewhere
WHERE platform=%s
AND user_info->%s = %s
""", (platform, username_key, username,))
# XXX Do we want a uniqueness constraint on $username_key? Can we do that?

if rec is None:
raise Exception( "User %s on %s isn't known to us."
% (username, platform)
)
return rec['participant_id']


class AccountElsewhere(object):
pass

platform = None # set in subclass

def upsert(platform, user_id, username, user_info):
"""Given str, unicode, unicode, and dict, return unicode and boolean.
def __init__(self, user_id):
typecheck(user_id, (int, unicode))
self.user_id = unicode(user_id)

Platform is the name of a platform that we support (ASCII blah). User_id is
an immutable unique identifier for the given user on the given platform.
Username is the user's login/username on the given platform. It is only
used here for logging. Specifically, we don't reserve their username for
them on Gittip if they're new here. We give them a random participant_id
here, and they'll have a chance to change it if/when they opt in. User_id
and username may or may not be the same. User_info is a dictionary of
profile info per the named platform. All platform dicts must have an id key
that corresponds to the primary key in the underlying table in our own db.

The return value is a tuple: (participant_id [unicode], is_claimed
[boolean], is_locked [boolean], balance [Decimal]).
def set_is_locked(self, is_locked):
gittip.db.execute("""
"""
typecheck( platform, str
, user_id, (int, unicode)
, username, unicode
, user_info, dict
)
user_id = unicode(user_id)
UPDATE elsewhere
SET is_locked=%s
WHERE platform=%s AND user_id=%s
""", (is_locked, self.platform, self.user_id))

# Insert the account if needed.
# =============================
# Do this with a transaction so that if the insert fails, the
# participant we reserved for them is rolled back as well.

try:
with db.get_transaction() as txn:
participant_id = reserve_a_random_participant_id(txn)
INSERT = """\
def opt_in(self, participant_id, user_info):
"""Given a desired participant_id, return a User object.
"""
self.set_is_locked(False)
participant_id, is_claimed, is_locked, balance = self.upsert(user_info)
user = User.from_id(participant_id) # give them a session
if not is_claimed:
user.set_as_claimed()
try:
user.change_id(participant_id)
except (Response, IntegrityError):
pass
return user

INSERT INTO elsewhere
(platform, user_id, participant_id)
VALUES (%s, %s, %s)

"""
txn.execute(INSERT, (platform, user_id, participant_id))
except IntegrityError:
pass
def upsert(self, user_info):
"""Given a dict, return a tuple.
Platform is the name of a platform that we support (ASCII blah).
User_id is an immutable unique identifier for the given user on the
given platform. Username is the user's login/username on the given
platform. It is only used here for logging. Specifically, we don't
reserve their username for them on Gittip if they're new here. We give
them a random participant_id here, and they'll have a chance to change
it if/when they opt in. User_id and username may or may not be the
same. User_info is a dictionary of profile info per the named platform.
All platform dicts must have an id key that corresponds to the primary
key in the underlying table in our own db.
# Update their user_info.
# =======================
# Cast everything to unicode, because (I believe) hstore can take any type
# of value, but psycopg2 can't.
#
# https://postgres.heroku.com/blog/past/2012/3/14/introducing_keyvalue_data_storage_in_heroku_postgres/
# http://initd.org/psycopg/docs/extras.html#hstore-data-type
The return value is a tuple: (participant_id [unicode], is_claimed
[boolean], is_locked [boolean], balance [Decimal]).
for k, v in user_info.items():
user_info[k] = unicode(v)
"""
typecheck(user_info, dict)

UPDATE = """\

UPDATE elsewhere
SET user_info=%s
WHERE platform=%s AND user_id=%s
RETURNING participant_id
# Insert the account if needed.
# =============================
# Do this with a transaction so that if the insert fails, the
# participant we reserved for them is rolled back as well.

"""
try:
with gittip.db.get_transaction() as txn:
_participant_id = reserve_a_random_participant_id(txn)
txn.execute( "INSERT INTO elsewhere "
"(platform, user_id, participant_id) "
"VALUES (%s, %s, %s)"
, (self.platform, self.user_id, _participant_id)
)
except IntegrityError:
pass


# Update their user_info.
# =======================
# Cast everything to unicode, because (I believe) hstore can take any
# type of value, but psycopg2 can't.
#
# https://postgres.heroku.com/blog/past/2012/3/14/introducing_keyvalue_data_storage_in_heroku_postgres/
# http://initd.org/psycopg/docs/extras.html#hstore-data-type

for k, v in user_info.items():
user_info[k] = unicode(v)


participant_id = gittip.db.fetchone("""
UPDATE elsewhere
SET user_info=%s
WHERE platform=%s AND user_id=%s
RETURNING participant_id
""", (user_info, self.platform, self.user_id))['participant_id']


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

rec = db.fetchone(UPDATE, (user_info, platform, user_id))
participant_id = rec['participant_id']
rec = gittip.db.fetchone("""
SELECT claimed_time, balance, is_locked
FROM participants
JOIN elsewhere
ON participants.id=participant_id
WHERE platform=%s
AND participants.id=%s
# Get a little more info to return.
# =================================
""", (self.platform, participant_id))

rec = db.fetchone( "SELECT claimed_time, balance, is_locked "
"FROM participants "
"JOIN elsewhere ON participants.id = participant_id "
"WHERE participants.id=%s"
, (participant_id,)
)
assert rec is not None # sanity check
assert rec is not None # sanity check


return ( participant_id
, rec['claimed_time'] is not None
, rec['is_locked']
, rec['balance']
)
return ( participant_id
, rec['claimed_time'] is not None
, rec['is_locked']
, rec['balance']
)
31 changes: 7 additions & 24 deletions gittip/elsewhere/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
from aspen import json, log, Response
from aspen.website import Website
from aspen.utils import typecheck
from gittip import db, elsewhere
from gittip.elsewhere import AccountElsewhere, _resolve


def upsert(user_info):
return elsewhere.upsert( 'github'
, user_info['id']
, user_info['login']
, user_info
)
class GitHubAccount(AccountElsewhere):
platform = 'github'


def resolve(login):
return _resolve(u'github', u'login', login)


def oauth_url(website, action, then=u""):
Expand Down Expand Up @@ -76,20 +76,3 @@ def oauth_dance(website, qs):
% (user_info['login'], user_info['id']))

return user_info


def resolve(login):
"""Given two str, return a participant_id.
"""
FETCH = """\
SELECT participant_id
FROM elsewhere
WHERE platform = 'github'
AND user_info -> 'login' = %s
""" # XXX Uniqueness constraint on login?
rec = db.fetchone(FETCH, (login,))
if rec is None:
raise Exception("GitHub user %s has no participant." % (login))
return rec['participant_id']
27 changes: 5 additions & 22 deletions gittip/elsewhere/twitter.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,12 @@
from gittip import db, elsewhere
from gittip.elsewhere import AccountElsewhere, _resolve


def upsert(user_info):
return elsewhere.upsert( 'twitter'
, user_info['id']
, user_info['screen_name']
, user_info
)
class TwitterAccount(AccountElsewhere):
platform = 'twitter'


def resolve(user_id):
"""Given str, return a participant_id.
"""
FETCH = """\
SELECT participant_id
FROM elsewhere
WHERE platform='twitter'
AND user_info -> 'user_id' = %s
""" # XXX Uniqueness constraint on screen_name?
rec = db.fetchone(FETCH, (user_id,))
if rec is None:
raise Exception("Twitter user %s has no participant." % (user_id))
return rec['participant_id']
def resolve(screen_name):
return _resolve(u'twitter', u'screen_name', screen_name)


def oauth_url(website, action, then=""):
Expand Down
4 changes: 3 additions & 1 deletion gittip/participant.py
Original file line number Diff line number Diff line change
Expand Up @@ -708,7 +708,9 @@ def take_over(self, platform, user_id, have_confirmation=False):
# Do the deal.
# ============
# If other_is_not_a_stub, then other will have the account
# elsewhere taken away from them with this call.
# elsewhere taken away from them with this call. If there are other
# browsing sessions open from that account, they will stay open
# until they expire (XXX Is that okay?)

txn.execute( "UPDATE elsewhere SET participant_id=%s "
"WHERE platform=%s AND user_id=%s"
Expand Down
10 changes: 7 additions & 3 deletions gittip/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,11 @@ def create_schema(db):
]

def populate_db_with_dummy_data(db):
from gittip.elsewhere import github
from gittip.elsewhere.github import GitHubAccount
from gittip.participant import Participant
for user_id, login in GITHUB_USERS:
participant_id, a,b,c = github.upsert({"id": user_id, "login": login})
account = GitHubAccount(user_id)
participant_id, a,b,c = account.upsert({"id": user_id, "login": login})
Participant(participant_id).change_id(login)


Expand Down Expand Up @@ -378,10 +379,13 @@ def setup_tips(*recs):
elsewhere = []
for participant_id, crap in _participants.items():
(good_cc, is_suspicious, claimed, platform, user_id) = crap
username_key = "login" if platform == 'github' else "screen_name"
elsewhere.append({ "platform": platform
, "user_id": user_id
, "participant_id": participant_id
, "user_info": {}
, "user_info": { "id": user_id
, username_key: participant_id
}
})
rec = {"id": participant_id}
if good_cc is not None:
Expand Down
16 changes: 16 additions & 0 deletions tests/test_elsewhere.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from gittip.testing import tip_graph
from gittip.elsewhere import github, twitter


def test_github_resolve_resolves():
with tip_graph(('alice', 'bob', 1)):
expected = 'alice'
actual = github.resolve(u'alice')
assert actual == expected, actual


def test_twitter_resolve_resolves():
with tip_graph(('alice', 'bob', 1, True, False, False, "twitter", "2345")):
expected = 'alice'
actual = twitter.resolve(u'alice')
assert actual == expected, actual
Loading

0 comments on commit db5bb15

Please sign in to comment.