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

Wire up homepage form to hit JSON endpoint #4613

Merged
merged 13 commits into from
Sep 10, 2017
1 change: 1 addition & 0 deletions defaults.env
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ BRAINTREE_SANDBOX_MODE=true
BRAINTREE_MERCHANT_ID=bk8h97tqzyqjhtfn
BRAINTREE_PUBLIC_KEY=xbty5dc9bgxpv5nb
BRAINTREE_PRIVATE_KEY=9d8646957c982bb0fb1aac764b582f7a
BRAINTREE_CLIENT_AUTHORIZATION=sandbox_cr9dyy9c_bk8h97tqzyqjhtfn

COINBASE_API_KEY=uETKVUrnPuXzVaVj
COINBASE_API_SECRET=32zAkQCcHHYkGGn29VkvEZvn21PM1lgO
Expand Down
23 changes: 23 additions & 0 deletions deploy/before.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
BEGIN;
CREATE TYPE follow_up AS ENUM ('monthly', 'quarterly', 'yearly', 'never');
CREATE TABLE payments_for_open_source
( uuid text PRIMARY KEY
, ctime timestamptz NOT NULL DEFAULT now()

-- card charge
, amount bigint NOT NULL
, braintree_transaction_id text UNIQUE DEFAULT NULL
, braintree_result_message text DEFAULT NULL

-- contact info
, name text NOT NULL
, follow_up follow_up NOT NULL
, email_address text NOT NULL

-- promotion details
, promotion_name text NOT NULL DEFAULT ''
, promotion_url text NOT NULL DEFAULT ''
, promotion_twitter text NOT NULL DEFAULT ''
, promotion_message text NOT NULL DEFAULT ''
);
END;
19 changes: 19 additions & 0 deletions emails/paid-for-open-source.spt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{{ _("Payment for open source") }}

[---] text/html
{{ _( "Congratulations! You now have a moral license to use community-built open source software, because you paid {amount} to the open source community through Gratipay. Thank you for your purchase!"
, amount=format_currency(amount, 'USD')
) }}
<br>
<br>
<a href="{{ receipt_url }}"
style="{{ button_style }}">{{ _("View Receipt") }}</a>

[---] text/plain
{{ _( "Congratulations! You now have a moral license to use community-built open source software, because you paid {amount} to the open source community through Gratipay. Thank you for your purchase!"
, amount=format_currency(amount, 'USD')
) }}

{{ _("Follow this link to view your receipt:") }}

{{ receipt_url }}
119 changes: 119 additions & 0 deletions gratipay/homepage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# -*- coding: utf-8 -*-
"""This is the Python library behind gratipay.com.
"""
from __future__ import absolute_import, division, print_function, unicode_literals

import braintree
from gratipay import utils
from gratipay.models.payment_for_open_source import PaymentForOpenSource


def _parse(raw):
"""Given a POST request.body, return (parsed<dict>, errors<list>).
"""

errors = []
x = lambda f: raw[f].strip() # KeyError -> 400

# amount
amount = x('amount')
if (not amount.isdigit()) or (int(amount) < 10):
errors.append('amount')
amount = ''.join(x for x in amount.split('.')[0] if x.isdigit())

# credit card nonce
payment_method_nonce = x('payment_method_nonce')
if len(payment_method_nonce) > 36:
errors.append('payment_method_nonce')
payment_method_nonce = ''

# name
name = x('name')
if len(name) > 256:
name = name[:256]
errors.append('name')

# email address
email_address = x('email_address')
if email_address and not utils.is_valid_email_address(email_address):
email_address = email_address[:255]
errors.append('email_address')

# follow_up
follow_up = x('follow_up')
if follow_up not in ('monthly', 'quarterly', 'yearly', 'never'):
follow_up = 'monthly'
errors.append('follow_up')

promotion_name = x('promotion_name')
if len(promotion_name) > 32:
promotion_name = promotion_name[:32]
errors.append('promotion_name')

promotion_url = x('promotion_url')
is_link = lambda x: (x.startswith('http://') or x.startswith('https://')) and '.' in x
if len(promotion_url) > 256 or (promotion_url and not is_link(promotion_url)):
promotion_url = promotion_url[:256]
errors.append('promotion_url')

promotion_twitter = x('promotion_twitter')
if len(promotion_twitter) > 32:
promotion_twitter = promotion_twitter[:32]
# TODO What are Twitter's rules?
errors.append('promotion_twitter')

promotion_message = x('promotion_message')
if len(promotion_message) > 128:
promotion_message = promotion_message[:128]
errors.append('promotion_message')

parsed = { 'amount': amount
, 'payment_method_nonce': payment_method_nonce
, 'name': name
, 'email_address': email_address
, 'follow_up': follow_up
, 'promotion_name': promotion_name
, 'promotion_url': promotion_url
, 'promotion_twitter': promotion_twitter
, 'promotion_message': promotion_message
}
return parsed, errors


def _store(parsed):
return PaymentForOpenSource.insert(**parsed)


def _charge(pfos, payment_method_nonce):
charge = braintree.Transaction.sale
result = charge({ 'amount': pfos.amount
, 'payment_method_nonce': payment_method_nonce
, 'options': {'submit_for_settlement': True}
, 'custom_fields': {'pfos_uuid': pfos.uuid}
})
pfos.process_result(result)


def _send(app, pfos):
app.email_queue.put( to=None
, template='paid-for-open-source'
, email=pfos.email_address
, amount=pfos.amount
, receipt_url=pfos.receipt_url
)


def pay_for_open_source(app, raw):
parsed, errors = _parse(raw)
out = {'errors': errors, 'receipt_url': None}
if not errors:
payment_method_nonce = parsed.pop('payment_method_nonce')
pfos = _store(parsed)
_charge(pfos, payment_method_nonce)
if pfos.succeeded:
out['receipt_url'] = pfos.receipt_url
if pfos.email_address:
_send(app, pfos)
else:
out['errors'].append('charging')
return out
4 changes: 3 additions & 1 deletion gratipay/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@
from .exchange_route import ExchangeRoute
from .package import Package
from .participant import Participant
from .payment_for_open_source import PaymentForOpenSource
from .team import Team


MODELS = (AccountElsewhere, Community, Country, ExchangeRoute, Package, Participant, Team)
MODELS = (AccountElsewhere, Community, Country, ExchangeRoute, Package, Participant,
PaymentForOpenSource, Team)


@contextmanager
Expand Down
80 changes: 80 additions & 0 deletions gratipay/models/payment_for_open_source.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function, unicode_literals

import gratipay
from uuid import uuid4
from postgres.orm import Model


class PaymentForOpenSource(Model):

typname = "payments_for_open_source"

def __repr__(self):
return '<PaymentForOpenSource: {}>'.format(repr(self.amount))


@property
def succeeded(self):
return self.braintree_result_message == ''


@property
def receipt_url(self):
if not self.succeeded:
return None
return '{}/browse/payments/{}/receipt.html'.format(gratipay.base_url, self.uuid)


@classmethod
def from_uuid(cls, uuid, cursor=None):
"""Take a uuid and return an object.
"""
return (cursor or cls.db).one("""
SELECT pfos.*::payments_for_open_source
FROM payments_for_open_source pfos
WHERE uuid = %s
""", (uuid,))


@classmethod
def insert(cls, amount, name, follow_up, email_address,
promotion_name, promotion_url, promotion_twitter, promotion_message,
cursor=None):
"""Take baseline info and insert into the database.
"""
uuid = uuid4().hex
return (cursor or cls.db).one("""
INSERT INTO payments_for_open_source
(uuid, amount, name, follow_up, email_address,
promotion_name, promotion_url, promotion_twitter, promotion_message)
VALUES (%s, %s, %s, %s, %s,
%s, %s, %s, %s)
RETURNING payments_for_open_source.*::payments_for_open_source
""", (uuid, amount, name, follow_up, email_address,
promotion_name, promotion_url, promotion_twitter, promotion_message))


def process_result(self, result):
"""Take a Braintree API result and update the database.
"""
result_message = '' if result.is_success else result.message
transaction_id = None
if result.transaction:
transaction_id = result.transaction.id

# Verify that Braintree is sending us the right payload.
# TODO This is hard to test and it should be a pretty tight guarantee,
# so I am commenting out for now. :(
#pfos_uuid = result.transaction.custom_fields['pfos_uuid']
#assert pfos_uuid == self.uuid, (pfos_uuid, transaction_id)

self.db.run("""
UPDATE payments_for_open_source
SET braintree_result_message=%s
, braintree_transaction_id=%s
WHERE uuid=%s
""", (result_message, transaction_id, self.uuid))
self.set_attributes( braintree_result_message=result_message
, braintree_transaction_id=transaction_id
)
18 changes: 18 additions & 0 deletions gratipay/testing/harness.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from gratipay.models.exchange_route import ExchangeRoute
from gratipay.models.package import NPM, Package
from gratipay.models.participant import Participant, MAX_TIP, MIN_TIP
from gratipay.models.payment_for_open_source import PaymentForOpenSource
from gratipay.models.team import Team
from gratipay.security import user
from gratipay.testing import P
Expand Down Expand Up @@ -142,6 +143,22 @@ def clear_tables(self):
self.db.run("INSERT INTO worker_coordination DEFAULT VALUES")


def make_payment_for_open_source(self, **info):
defaults = dict( amount='1000'
, name='Alice Liddell'
, email_address='[email protected]'
, follow_up='monthly'
, promotion_name='Wonderland'
, promotion_url='http://www.example.com/'
, promotion_twitter='thebestbutter'
, promotion_message='Love me! Love me! Say that you love me!'
)
for key, value in defaults.items():
if key not in info:
info[key] = value
return PaymentForOpenSource.insert(**info)


def make_elsewhere(self, platform, user_id, user_name, **kw):
"""Factory for :py:class:`~gratipay.models.account_elsewhere.AccountElsewhere`.
"""
Expand Down Expand Up @@ -318,6 +335,7 @@ def make_exchange(self, route, amount, fee, participant, status='succeeded', err
record_exchange_result(self.db, e_id, status, error, participant)
return e_id


def make_payment(self, participant, team, amount, direction, payday, timestamp=utcnow()):
"""Factory for payment"""

Expand Down
6 changes: 6 additions & 0 deletions gratipay/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import fnmatch
import random
import os
import re
from base64 import urlsafe_b64encode, urlsafe_b64decode
from datetime import datetime, timedelta

Expand All @@ -20,6 +21,11 @@
# card is considered as expiring
EXPIRING_DELTA = timedelta(days = 30)

_email_re = re.compile(r'^[^@]+@[^@]+\.[^@]+$')
# exactly one @, and at least one . after @ -- simple validation, send to be sure
def is_valid_email_address(email_address):
return len(email_address) < 255 and _email_re.match(email_address)


def dict_to_querystring(mapping):
if not mapping:
Expand Down
2 changes: 1 addition & 1 deletion gratipay/utils/http_caching.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def concat_files(files, root):
catted.append('/*' + filepath.center(68) + '*/\n')
catted.append('/' + ('*'*70) + '/' + '\n\n')
content = open(os.path.join(root, filepath)).read()
content = content.decode('ascii')
content = content.decode('utf8')
catted.append(content + '\n')
return "".join(catted)

Expand Down
3 changes: 2 additions & 1 deletion gratipay/wireup.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ def compile_assets(website):
headers[b'HTTP_HOST'] = str(url.netloc)
content = client.GET(urlpath, **headers).body
tmpfd, tmpfpath = mkstemp(dir='.')
os.write(tmpfd, content)
os.write(tmpfd, content.encode('utf8'))
os.close(tmpfd)
os.rename(tmpfpath, filepath)
atexit.register(lambda: clean_assets(website.www_root))
Expand Down Expand Up @@ -351,6 +351,7 @@ def env():
BRAINTREE_MERCHANT_ID = unicode,
BRAINTREE_PUBLIC_KEY = unicode,
BRAINTREE_PRIVATE_KEY = unicode,
BRAINTREE_CLIENT_AUTHORIZATION = unicode,
GITHUB_CLIENT_ID = unicode,
GITHUB_CLIENT_SECRET = unicode,
GITHUB_CALLBACK = unicode,
Expand Down
Loading