Skip to content

Commit

Permalink
create SQL composite type currency_basket
Browse files Browse the repository at this point in the history
  • Loading branch information
Changaco committed Oct 26, 2017
1 parent 27beaff commit 52e76ac
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 3 deletions.
3 changes: 3 additions & 0 deletions liberapay/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import re

from jinja2 import StrictUndefined
from mangopay.utils import Money
from pando.utils import utc


Expand Down Expand Up @@ -238,4 +239,6 @@ def make_standard_tip(label, weekly):

USERNAME_MAX_SIZE = 32

ZERO = {c: Money(D_ZERO, c) for c in ('EUR', 'USD', None)}

del _
3 changes: 2 additions & 1 deletion liberapay/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from liberapay.models.repository import refetch_repos
from liberapay.security import authentication, csrf, set_default_security_headers
from liberapay.utils import b64decode_s, b64encode_s, erase_cookie, http_caching, i18n, set_cookie
from liberapay.utils.currencies import fetch_currency_exchange_rates
from liberapay.utils.currencies import MoneyBasket, fetch_currency_exchange_rates
from liberapay.utils.state_chain import (
attach_environ_to_request, create_response_object, canonize, insert_constants,
_dispatch_path_to_filesystem, merge_exception_into_response, return_500_for_exception,
Expand All @@ -43,6 +43,7 @@
# ===================

json.register_encoder(Money, lambda m: {'amount': str(m.amount), 'currency': m.currency})
json.register_encoder(MoneyBasket, lambda b: list(b))

website.renderer_default = 'unspecified' # require explicit renderer, to avoid escaping bugs

Expand Down
58 changes: 57 additions & 1 deletion liberapay/utils/currencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import requests
import xmltodict

from liberapay.constants import D_CENT, D_ZERO
from liberapay.constants import D_CENT, D_ZERO, ZERO
from liberapay.website import website


Expand All @@ -28,6 +28,62 @@ def _convert(self, c):
Money.zero = lambda m: Money(D_ZERO, m.currency)


class MoneyBasket(object):

def __init__(self, eur=ZERO['EUR'], usd=ZERO['USD']):
assert eur.currency == 'EUR'
assert usd.currency == 'USD'
self.eur = eur
self.usd = usd

def __iter__(self):
return iter((self.eur, self.usd))

def __eq__(self, other):
if isinstance(other, self.__class__):
return self.__dict__ == other.__dict__
return False

def __add__(self, other):
r = self.__class__(self.eur, self.usd)
if isinstance(other, self.__class__):
for k, v in other.__dict__.items():
if k in r.__dict__:
r.__dict__[k] += v
else:
r.__dict__[k] = v
elif isinstance(other, Money):
k = other.currency.lower()
if k in r.__dict__:
r.__dict__[k] += other
else:
r.__dict__[k] = other
else:
raise TypeError(other)
return r

def __sub__(self, other):
r = self.__class__(self.eur, self.usd)
if isinstance(other, self.__class__):
for k, v in other.__dict__.items():
if k in r.__dict__:
r.__dict__[k] -= v
else:
r.__dict__[k] = -v
elif isinstance(other, Money):
k = other.currency.lower()
if k in r.__dict__:
r.__dict__[k] -= other
else:
r.__dict__[k] = -other
else:
raise TypeError(other)
return r

def __repr__(self):
return b'%s[%s, %s]' % (self.__class__.__name__, self.eur, self.usd)


def fetch_currency_exchange_rates(db):
currencies = set(db.one("SELECT array_to_json(enum_range(NULL::currency))"))
r = requests.get('https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml')
Expand Down
14 changes: 13 additions & 1 deletion liberapay/wireup.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
from liberapay.models import DB
from liberapay.security.authentication import ANON
from liberapay.utils import find_files, markdown, mkdir_p
from liberapay.utils.currencies import get_currency_exchange_rates
from liberapay.utils.currencies import MoneyBasket, get_currency_exchange_rates
from liberapay.utils.emails import compile_email_spt
from liberapay.utils.http_caching import asset_etag
from liberapay.utils.i18n import (
Expand Down Expand Up @@ -110,6 +110,18 @@ def cast_currency_amount(v, cursor):
oid = db.one("SELECT 'currency_amount'::regtype::oid")
register_type(new_type((oid,), _str('currency_amount'), cast_currency_amount))

def adapt_money_basket(b):
return AsIs(b'(%s,%s)::currency_basket' % (b.eur.amount, b.usd.amount))
register_adapter(MoneyBasket, adapt_money_basket)

def cast_currency_basket(v, cursor):
if v is None:
return None
eur, usd = v[1:-1].split(',')
return MoneyBasket(Money(eur, 'EUR'), Money(usd, 'USD'))
oid = db.one("SELECT 'currency_basket'::regtype::oid")
register_type(new_type((oid,), _str('currency_basket'), cast_currency_basket))

use_qc = not env.override_query_cache
qc1 = QueryCache(db, threshold=(1 if use_qc else 0))
qc5 = QueryCache(db, threshold=(5 if use_qc else 0))
Expand Down
92 changes: 92 additions & 0 deletions sql/currencies.sql
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,98 @@ CREATE OPERATOR <= (
);


-- Basket type: amounts in multiple currencies

CREATE TYPE currency_basket AS (EUR numeric, USD numeric);

CREATE FUNCTION currency_basket_add(currency_basket, currency_amount)
RETURNS currency_basket AS $$
BEGIN
IF ($2.currency = 'EUR') THEN
RETURN ($1.EUR + $2.amount, $1.USD);
ELSIF ($2.currency = 'USD') THEN
RETURN ($1.EUR, $1.USD + $2.amount);
ELSE
RAISE 'unknown currency %', $2.currency;
END IF;
END;
$$ LANGUAGE plpgsql IMMUTABLE STRICT;

CREATE OPERATOR + (
leftarg = currency_basket,
rightarg = currency_amount,
procedure = currency_basket_add,
commutator = +
);

CREATE FUNCTION currency_basket_add(currency_basket, currency_basket)
RETURNS currency_basket AS $$
BEGIN RETURN ($1.EUR + $2.EUR, $1.USD + $2.USD); END;
$$ LANGUAGE plpgsql IMMUTABLE STRICT;

CREATE OPERATOR + (
leftarg = currency_basket,
rightarg = currency_basket,
procedure = currency_basket_add,
commutator = +
);

CREATE FUNCTION currency_basket_sub(currency_basket, currency_amount)
RETURNS currency_basket AS $$
BEGIN
IF ($2.currency = 'EUR') THEN
RETURN ($1.EUR - $2.amount, $1.USD);
ELSIF ($2.currency = 'USD') THEN
RETURN ($1.EUR, $1.USD - $2.amount);
ELSE
RAISE 'unknown currency %', $2.currency;
END IF;
END;
$$ LANGUAGE plpgsql IMMUTABLE STRICT;

CREATE OPERATOR - (
leftarg = currency_basket,
rightarg = currency_amount,
procedure = currency_basket_sub
);

CREATE FUNCTION currency_basket_sub(currency_basket, currency_basket)
RETURNS currency_basket AS $$
BEGIN RETURN ($1.EUR - $2.EUR, $1.USD - $2.USD); END;
$$ LANGUAGE plpgsql IMMUTABLE STRICT;

CREATE OPERATOR - (
leftarg = currency_basket,
rightarg = currency_basket,
procedure = currency_basket_sub
);

CREATE FUNCTION currency_basket_contains(currency_basket, currency_amount)
RETURNS boolean AS $$
BEGIN
IF ($2.currency = 'EUR') THEN
RETURN ($1.EUR >= $2.amount);
ELSIF ($2.currency = 'USD') THEN
RETURN ($1.USD >= $2.amount);
ELSE
RAISE 'unknown currency %', $2.currency;
END IF;
END;
$$ LANGUAGE plpgsql IMMUTABLE STRICT;

CREATE OPERATOR >= (
leftarg = currency_basket,
rightarg = currency_amount,
procedure = currency_basket_contains
);

CREATE AGGREGATE basket_sum(currency_amount) (
sfunc = currency_basket_add,
stype = currency_basket,
initcond = '(0.00,0.00)'
);


-- Exchange rates

CREATE TABLE currency_exchange_rates
Expand Down

0 comments on commit 52e76ac

Please sign in to comment.