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

Commit

Permalink
Checkpoint 3
Browse files Browse the repository at this point in the history
Includes: #4424 #4428 4435
  • Loading branch information
chadwhitacre committed May 5, 2017
1 parent 9898a38 commit 49c9359
Show file tree
Hide file tree
Showing 11 changed files with 337 additions and 166 deletions.
53 changes: 35 additions & 18 deletions gratipay/models/exchange_route.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,59 +8,76 @@ class ExchangeRoute(Model):

typname = "exchange_routes"

def __eq__(self, other):
if not isinstance(other, ExchangeRoute):
return False
return self.id == other.id

def __ne__(self, other):
if not isinstance(other, ExchangeRoute):
return True
return self.id != other.id

def __repr__(self):
return '<ExchangeRoute: %s on %s>' % (repr(self.address), repr(self.network))


# Constructors
# ============

@classmethod
def from_id(cls, id):
r = cls.db.one("""
def from_id(cls, id, cursor=None):
route = (cursor or cls.db).one("""
SELECT r.*::exchange_routes
FROM exchange_routes r
WHERE id = %(id)s
""", locals())
if r:
if route:
from gratipay.models.participant import Participant # XXX Red hot hack!
r.set_attributes(participant=Participant.from_id(r.participant))
return r
route.set_attributes(participant=Participant.from_id(route.participant))
return route

@classmethod
def from_network(cls, participant, network):
def from_network(cls, participant, network, cursor=None):
participant_id = participant.id
r = cls.db.one("""
route = (cursor or cls.db).one("""
SELECT r.*::exchange_routes
FROM current_exchange_routes r
WHERE participant = %(participant_id)s
AND network = %(network)s
""", locals())
if r:
r.set_attributes(participant=participant)
return r
if route:
route.set_attributes(participant=participant)
return route

@classmethod
def from_address(cls, participant, network, address):
def from_address(cls, participant, network, address, cursor=None):
participant_id = participant.id
r = cls.db.one("""
route = (cursor or cls.db).one("""
SELECT r.*::exchange_routes
FROM exchange_routes r
WHERE participant = %(participant_id)s
AND network = %(network)s
AND address = %(address)s
""", locals())
if r:
r.set_attributes(participant=participant)
return r
if route:
route.set_attributes(participant=participant)
return route

@classmethod
def insert(cls, participant, network, address, fee_cap=None, cursor=None):
participant_id = participant.id
error = ''
r = (cursor or cls.db).one("""
route = (cursor or cls.db).one("""
INSERT INTO exchange_routes
(participant, network, address, error, fee_cap)
VALUES (%(participant_id)s, %(network)s, %(address)s, %(error)s, %(fee_cap)s)
RETURNING exchange_routes.*::exchange_routes
""", locals())
if network == 'braintree-cc':
participant.update_giving_and_teams()
r.set_attributes(participant=participant)
return r
route.set_attributes(participant=participant)
return route

def invalidate(self):
if self.network == 'braintree-cc':
Expand Down
97 changes: 13 additions & 84 deletions gratipay/models/participant/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
import uuid

from aspen.utils import utcnow
import balanced
import braintree
from dependency_injection import resolve_dependencies
from postgres.orm import Model
from psycopg2 import IntegrityError
Expand All @@ -23,22 +21,20 @@
BadAmount,
)

from gratipay.billing.instruments import CreditCard
from gratipay.models.account_elsewhere import AccountElsewhere
from gratipay.models.exchange_route import ExchangeRoute
from gratipay.models.team import Team
from gratipay.models.team.takes import ZERO
from gratipay.utils import (
i18n,
is_card_expiring,
markdown,
notifications,
pricing,
)
from gratipay.utils.username import safely_reserve_a_username

from .identity import Identity
from .email import Email
from .identity import Identity
from .exchange_routes import ExchangeRoutes

MAX_TIP = MAX_PAYMENT = Decimal('1000.00')
MIN_TIP = MIN_PAYMENT = Decimal('0.00')
Expand All @@ -52,7 +48,7 @@
USERNAME_MAX_SIZE = 32


class Participant(Model, Email, Identity):
class Participant(Model, Email, Identity, ExchangeRoutes):
"""Represent a Gratipay participant.
"""

Expand Down Expand Up @@ -117,6 +113,16 @@ def _from_thing(cls, thing, value):
""".format(thing), (value,))


# URLs
# ====

@property
def url_path(self):
"""The path part of the URL for this participant on Gratipay.
"""
return '/~{}/'.format(self.username)


# Session Management
# ==================

Expand Down Expand Up @@ -397,16 +403,6 @@ def add_signin_notifications(self):
elif self.credit_card_expiring():
self.add_notification('credit_card_expires')

def credit_card_expiring(self):
route = ExchangeRoute.from_network(self, 'braintree-cc')
if not route:
return
card = CreditCard.from_route(route)
year, month = card.expiration_year, card.expiration_month
if not (year and month):
return False
return is_card_expiring(int(year), int(month))

def remove_notification(self, name):
id = self.id
r = self.db.one("""
Expand All @@ -432,73 +428,6 @@ def render_notifications(self, state):
return r


# Exchange-related stuff
# ======================

def get_paypal_error(self):
return getattr(ExchangeRoute.from_network(self, 'paypal'), 'error', None)

def get_credit_card_error(self):
return getattr(ExchangeRoute.from_network(self, 'braintree-cc'), 'error', None)

@property
def has_payout_route(self):
for network in ('paypal',):
route = ExchangeRoute.from_network(self, network)
if route and not route.error:
return True
return False

def get_balanced_account(self):
"""Fetch or create the balanced account for this participant.
"""
if not self.balanced_customer_href:
customer = balanced.Customer(meta={
'username': self.username,
'participant_id': self.id,
}).save()
r = self.db.one("""
UPDATE participants
SET balanced_customer_href=%s
WHERE id=%s
AND balanced_customer_href IS NULL
RETURNING id
""", (customer.href, self.id))
if not r:
return self.get_balanced_account()
else:
customer = balanced.Customer.fetch(self.balanced_customer_href)
return customer

def get_braintree_account(self):
"""Fetch or create a braintree account for this participant.
"""
if not self.braintree_customer_id:
customer = braintree.Customer.create({
'custom_fields': {'participant_id': self.id}
}).customer

r = self.db.one("""
UPDATE participants
SET braintree_customer_id=%s
WHERE id=%s
AND braintree_customer_id IS NULL
RETURNING id
""", (customer.id, self.id))

if not r:
return self.get_braintree_account()
else:
customer = braintree.Customer.find(self.braintree_customer_id)
return customer

def get_braintree_token(self):
account = self.get_braintree_account()

token = braintree.ClientToken.generate({'customer_id': account.id})
return token


# Elsewhere-related stuff
# =======================

Expand Down
56 changes: 38 additions & 18 deletions gratipay/models/participant/email.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,18 @@

EMAIL_HASH_TIMEOUT = timedelta(hours=24)

( VERIFICATION_MISSING
, VERIFICATION_FAILED
, VERIFICATION_EXPIRED
, VERIFICATION_REDUNDANT
, VERIFICATION_STYMIED
, VERIFICATION_SUCCEEDED
) = range(6)

#: Signal that verifying an email address failed.
VERIFICATION_FAILED = object()

#: Signal that verifying an email address was redundant.
VERIFICATION_REDUNDANT = object()

#: Signal that an email address is already verified for a different :py:class:`Participant`.
VERIFICATION_STYMIED = object()

#: Signal that email verification succeeded.
VERIFICATION_SUCCEEDED = object()


class Email(object):
Expand Down Expand Up @@ -228,27 +233,41 @@ def _set_primary_email(self, email, cursor):


def finish_email_verification(self, email, nonce):
"""Given an email address and a nonce as strings, return a three-tuple:
- a ``VERIFICATION_*`` constant;
- a list of packages if ``VERIFICATION_SUCCEEDED`` (``None``
otherwise), and
- a boolean indicating whether the participant's PayPal address was
updated if applicable (``None`` if not).
"""
if '' in (email.strip(), nonce.strip()):
return VERIFICATION_MISSING
return VERIFICATION_FAILED, None, None
with self.db.get_cursor() as cursor:
record = self.get_email(email, cursor, and_lock=True)
if record is None:
return VERIFICATION_FAILED
return VERIFICATION_FAILED, None, None
packages = self.get_packages_claiming(cursor, nonce)
if record.verified and not packages:
assert record.nonce is None # and therefore, order of conditions matters
return VERIFICATION_REDUNDANT
return VERIFICATION_REDUNDANT, None, None
if not constant_time_compare(record.nonce, nonce):
return VERIFICATION_FAILED
return VERIFICATION_FAILED, None, None
if (utcnow() - record.verification_start) > EMAIL_HASH_TIMEOUT:
return VERIFICATION_EXPIRED
return VERIFICATION_FAILED, None, None
try:
paypal_updated = False
if packages:
self.finish_package_claims(cursor, nonce, *packages)
self.save_email_address(cursor, email)
has_no_paypal = not self.get_payout_routes(good_only=True)
if packages and has_no_paypal:
self.set_paypal_address(email, cursor)
paypal_updated = True
except IntegrityError:
return VERIFICATION_STYMIED
return VERIFICATION_SUCCEEDED
return VERIFICATION_STYMIED, None, None
return VERIFICATION_SUCCEEDED, packages, paypal_updated


def get_packages_claiming(self, cursor, nonce):
Expand All @@ -261,6 +280,7 @@ def get_packages_claiming(self, cursor, nonce):
JOIN claims c
ON p.id = c.package_id
WHERE c.nonce=%s
ORDER BY p.name ASC
""", (nonce,))


Expand Down Expand Up @@ -328,21 +348,21 @@ def get_email(self, address, cursor=None, and_lock=False):
return (cursor or self.db).one(sql, (self.id, address))


def get_emails(self):
def get_emails(self, cursor=None):
"""Return a list of all email addresses on file for this participant.
"""
return self.db.all("""
return (cursor or self.db).all("""
SELECT *
FROM emails
WHERE participant_id=%s
ORDER BY id
""", (self.id,))


def get_verified_email_addresses(self):
def get_verified_email_addresses(self, cursor=None):
"""Return a list of verified email addresses on file for this participant.
"""
return [email.address for email in self.get_emails() if email.verified]
return [email.address for email in self.get_emails(cursor) if email.verified]


def remove_email(self, address):
Expand Down
Loading

0 comments on commit 49c9359

Please sign in to comment.