From 02d5dfa1976cd398facdc8d95b8143b3eaea2369 Mon Sep 17 00:00:00 2001 From: Jason Antman Date: Sat, 28 Oct 2017 15:28:32 -0400 Subject: [PATCH 01/13] pin two unpinned dependencies --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 02fd5d5a..7d739db1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -35,5 +35,5 @@ selenium==3.6.0 six==1.10.0 versionfinder==0.1.0 # dependencies of biweeklybudget/vendored/wishlist -captain -pyvirtualdisplay +captain==0.4.9 +pyvirtualdisplay==0.2.1 From 6ff9a6ca289ee9a7349c27f4cd992f408449b7b6 Mon Sep 17 00:00:00 2001 From: Jason Antman Date: Sat, 28 Oct 2017 15:31:24 -0400 Subject: [PATCH 02/13] issue #140 - add babel requirement for currency formatting without locales --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 7d739db1..9fd3dbb0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,6 +10,7 @@ Werkzeug==0.12.1 alembic==0.9.1 appdirs==1.4.3 asn1crypto==0.22.0 +babel==2.5.1 beautifulsoup4==4.5.3 cffi==1.9.1 click==6.7 From e2ba0d7e7da9c2974169e927b99e1a3aacf5d487 Mon Sep 17 00:00:00 2001 From: Jason Antman Date: Sat, 28 Oct 2017 17:34:32 -0400 Subject: [PATCH 03/13] issue #140 - add LOCALE_NAME and CURRENCY_CODE settings --- biweeklybudget/settings.py | 60 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/biweeklybudget/settings.py b/biweeklybudget/settings.py index bdddb69b..da955c93 100644 --- a/biweeklybudget/settings.py +++ b/biweeklybudget/settings.py @@ -39,6 +39,8 @@ import importlib import logging from datetime import timedelta, datetime +from babel.numbers import validate_currency, UnknownCurrencyError +from babel import Locale, UnknownLocaleError logger = logging.getLogger(__name__) @@ -47,7 +49,8 @@ 'DEFAULT_ACCOUNT_ID', 'PAY_PERIOD_START_DATE', 'RECONCILE_BEGIN_DATE', - 'STALE_DATA_TIMEDELTA' + 'STALE_DATA_TIMEDELTA', + 'CURRENCY_CODE' ] _DATE_VARS = [ @@ -66,9 +69,27 @@ 'DB_CONNSTRING', 'STATEMENTS_SAVE_PATH', 'TOKEN_PATH', - 'VAULT_ADDR' + 'VAULT_ADDR', + 'LOCALE_NAME', + 'CURRENCY_CODE' ] +#: A `RFC 5646 / BCP 47 `_ Language Tag +#: with a Region suffix to use for number (currency) formatting, i.e. "en_US", +#: "en_GB", "de_DE", etc. If this is not specified (None), it will be looked up +#: from environment variables in the following order: LC_ALL, LC_MONETARY, LANG. +#: If none of those variables are set to a valid locale name (not including +#: the "C" locale, which does not specify currency formatting) and this variable +#: is not set, the application will default to "en_US". This setting only +#: effects how monetary values are displayed in the UI, logs, etc. +LOCALE_NAME = None + +#: An `ISO 4217 `_ Currency Code +#: specifying the currency to use for all monetary amounts, i.e. "USD", "EUR", +#: etc. This setting only effects how monetary values are displayed in the UI, +#: logs, etc. Currently defaults to "USD". +CURRENCY_CODE = 'USD' + #: string - SQLAlchemy database connection string. See the #: :ref:`SQLAlchemy Database URLS docs ` #: for further information. @@ -177,4 +198,39 @@ 'ERROR: setting or environment variable "%s" must be set' % varname ) +# Handle the "LOCALE_NAME" variable special logic for default if not specified. +if LOCALE_NAME is None or LOCALE_NAME == 'C' or LOCALE_NAME.startswith('C.'): + logger.debug('LOCALE_NAME unset or C locale') + for varname in ['LC_ALL', 'LC_MONETARY', 'LANG']: + val = os.environ.get(varname, '').strip() + if val != '' and val != 'C' and not val.startswith('C.'): + logger.debug( + 'Setting LOCALE_NAME to %s from env var %s', val, varname + ) + LOCALE_NAME = val + break + if LOCALE_NAME is None: + logger.debug('LOCALE_NAME not set; defaulting to en_US') + LOCALE_NAME = 'en_US' + +# Check for a valid locale +try: + Locale.parse(LOCALE_NAME) +except UnknownLocaleError: + raise SystemExit( + 'ERROR: LOCALE_NAME setting of "%s" is not a valid BCP 47 Language Tag.' + ' See for more information.' % + LOCALE_NAME + ) + +# Check for a valid currency code +try: + validate_currency(CURRENCY_CODE) +except UnknownCurrencyError: + raise SystemExit( + 'ERROR: CURRENCY_CODE setting of "%s" is not a valid currency code. See' + ' for more information and ' + 'a list of valid codes.' % CURRENCY_CODE + ) + logger.debug('Done loading settings.') From 7853e1a8859e013a57707d2c9c93393a504a54ae Mon Sep 17 00:00:00 2001 From: Jason Antman Date: Sat, 28 Oct 2017 17:54:24 -0400 Subject: [PATCH 04/13] issue #140 - use babel for all currency formatting in Python and views/templates --- biweeklybudget/db_event_handlers.py | 6 ++++-- biweeklybudget/flaskapp/app.py | 3 --- biweeklybudget/flaskapp/context_processors.py | 18 +++++++++++++++++- biweeklybudget/flaskapp/filters.py | 8 ++++---- biweeklybudget/flaskapp/notifications.py | 16 +++++++--------- .../flaskapp/templates/credit-payoffs.html | 2 +- biweeklybudget/flaskapp/templates/fuel.html | 4 ++-- .../flaskapp/views/credit_payoffs.py | 4 ++-- .../flaskapp/views/test_reconcile.py | 5 ++--- biweeklybudget/utils.py | 16 ++++++++++++++++ 10 files changed, 55 insertions(+), 27 deletions(-) diff --git a/biweeklybudget/db_event_handlers.py b/biweeklybudget/db_event_handlers.py index 6ed39389..9a25070d 100644 --- a/biweeklybudget/db_event_handlers.py +++ b/biweeklybudget/db_event_handlers.py @@ -42,6 +42,7 @@ from biweeklybudget.models.budget_model import Budget from biweeklybudget.models.transaction import Transaction +from biweeklybudget.utils import fmt_currency logger = logging.getLogger(__name__) @@ -118,9 +119,10 @@ def handle_new_transaction(session): old_amt = float(budg.current_balance) budg.current_balance = old_amt - float(obj.actual_amount) logger.info( - 'New transaction (%s) for $%s against standing budget id=%s; ' + 'New transaction (%s) for %s against standing budget id=%s; ' 'update budget current_balance from %s to %s', obj.description, - obj.actual_amount, budg.id, old_amt, budg.current_balance + fmt_currency(obj.actual_amount), budg.id, fmt_currency(old_amt), + fmt_currency(budg.current_balance) ) session.add(budg) updated += 1 diff --git a/biweeklybudget/flaskapp/app.py b/biweeklybudget/flaskapp/app.py index 9a3dc6e8..6d6e56cc 100644 --- a/biweeklybudget/flaskapp/app.py +++ b/biweeklybudget/flaskapp/app.py @@ -36,7 +36,6 @@ """ import logging -import locale from flask import Flask @@ -49,8 +48,6 @@ logging.basicConfig(level=logging.DEBUG, format=format) logger = logging.getLogger() -locale.setlocale(locale.LC_ALL, '') - fix_werkzeug_logger() app = Flask(__name__) diff --git a/biweeklybudget/flaskapp/context_processors.py b/biweeklybudget/flaskapp/context_processors.py index 4c588d99..1e6bd5e8 100644 --- a/biweeklybudget/flaskapp/context_processors.py +++ b/biweeklybudget/flaskapp/context_processors.py @@ -35,6 +35,8 @@ ################################################################################ """ +from babel.numbers import get_currency_symbol + from biweeklybudget.flaskapp.app import app from biweeklybudget.flaskapp.notifications import NotificationsController from biweeklybudget import settings as settingsmod @@ -56,7 +58,7 @@ def settings(): """ Add settings to template context for all templates. - :return: template context with notifications added + :return: template context with settings added :rtype: dict """ return {'settings': {x: getattr(settingsmod, x) for x in dir(settingsmod)}} @@ -73,3 +75,17 @@ def utilities(): def cast_float(x): return float(x) return dict(cast_float=cast_float) + + +@app.context_processor +def add_currency_symbol(): + """ + Context processor to inject the proper currency symbol into the Jinja2 + context as the "CURRENCY_SYM" variable. + + :return: proper currency symbol for our locale and currency + :rtype: str + """ + return dict(CURRENCY_SYM=get_currency_symbol( + settingsmod.CURRENCY_CODE, locale=settingsmod.LOCALE_NAME + )) diff --git a/biweeklybudget/flaskapp/filters.py b/biweeklybudget/flaskapp/filters.py index 4c7ec0d5..1e0a539d 100644 --- a/biweeklybudget/flaskapp/filters.py +++ b/biweeklybudget/flaskapp/filters.py @@ -39,7 +39,7 @@ from jinja2.runtime import Undefined from humanize import naturaltime -from biweeklybudget.utils import dtnow +from biweeklybudget.utils import dtnow, fmt_currency from biweeklybudget.flaskapp.app import app from biweeklybudget.models.account import AcctType @@ -112,15 +112,15 @@ def ago_filter(dt): @app.template_filter('dollars') def dollars_filter(x): """ - Format as USD currency. + Format as currency using :py:func:`~.utils.fmt_currency`. - :param x: dollar amount, int, float, decimal, etc. + :param x: currency amount, int, float, decimal, etc. :return: formatted currency :rtype: str """ if x == '' or x is None or isinstance(x, Undefined): return '' - return currency(x, grouping=True) + return fmt_currency(x) @app.template_filter('reddollars') diff --git a/biweeklybudget/flaskapp/notifications.py b/biweeklybudget/flaskapp/notifications.py index bc736922..3923b353 100644 --- a/biweeklybudget/flaskapp/notifications.py +++ b/biweeklybudget/flaskapp/notifications.py @@ -37,10 +37,9 @@ import logging from sqlalchemy import func -from locale import currency from biweeklybudget.db import db_session -from biweeklybudget.utils import dtnow +from biweeklybudget.utils import dtnow, fmt_currency from biweeklybudget.models.account import Account from biweeklybudget.models.budget_model import Budget from biweeklybudget.models.ofx_transaction import OFXTransaction @@ -193,14 +192,13 @@ def get_notifications(): 'period remaining; %s ' 'unreconciled)!' '' % ( - currency(accounts_bal, grouping=True), - currency( - (standing_bal + curr_pp + unrec_amt), - grouping=True + fmt_currency(accounts_bal), + fmt_currency( + (standing_bal + curr_pp + unrec_amt) ), - currency(standing_bal, grouping=True), - currency(curr_pp, grouping=True), - currency(unrec_amt, grouping=True) + fmt_currency(standing_bal), + fmt_currency(curr_pp), + fmt_currency(unrec_amt) ) }) unreconciled_ofx = NotificationsController.num_unreconciled_ofx() diff --git a/biweeklybudget/flaskapp/templates/credit-payoffs.html b/biweeklybudget/flaskapp/templates/credit-payoffs.html index c84be7d2..35bf6db6 100644 --- a/biweeklybudget/flaskapp/templates/credit-payoffs.html +++ b/biweeklybudget/flaskapp/templates/credit-payoffs.html @@ -84,7 +84,7 @@
- $ + {{ CURRENCY_SYM }}
diff --git a/biweeklybudget/flaskapp/templates/fuel.html b/biweeklybudget/flaskapp/templates/fuel.html index 4230ff44..923072d7 100644 --- a/biweeklybudget/flaskapp/templates/fuel.html +++ b/biweeklybudget/flaskapp/templates/fuel.html @@ -76,7 +76,7 @@ Start Fuel Level End Fuel Level Location - $/Gal. + {{ CURRENCY_SYM }}/Gal. Total Cost Total Gallons MPG (from veh.) @@ -94,7 +94,7 @@ Start Fuel Level End Fuel Level Location - $/Gal. + {{ CURRENCY_SYM }}/Gal. Total Cost Total Gallons MPG (from veh.) diff --git a/biweeklybudget/flaskapp/views/credit_payoffs.py b/biweeklybudget/flaskapp/views/credit_payoffs.py index 046dc616..4676680f 100644 --- a/biweeklybudget/flaskapp/views/credit_payoffs.py +++ b/biweeklybudget/flaskapp/views/credit_payoffs.py @@ -39,7 +39,6 @@ import json from decimal import Decimal, ROUND_UP from datetime import datetime -from locale import currency from flask.views import MethodView from flask import render_template, request, jsonify @@ -49,6 +48,7 @@ from biweeklybudget.db import db_session from biweeklybudget.interest import InterestHelper from biweeklybudget.models.dbsetting import DBSetting +from biweeklybudget.utils import fmt_currency logger = logging.getLogger(__name__) @@ -103,7 +103,7 @@ def _payoffs_list(self, ih): tmp['results'].append({ 'name': '%s (%d) (%s @ %s%%)' % ( acct.name, k, - currency(abs(acct.balance.ledger), grouping=True), + fmt_currency(abs(acct.balance.ledger)), (acct.effective_apr * Decimal('100')).quantize( Decimal('.01') ) diff --git a/biweeklybudget/tests/acceptance/flaskapp/views/test_reconcile.py b/biweeklybudget/tests/acceptance/flaskapp/views/test_reconcile.py index f19719d0..034c0ece 100644 --- a/biweeklybudget/tests/acceptance/flaskapp/views/test_reconcile.py +++ b/biweeklybudget/tests/acceptance/flaskapp/views/test_reconcile.py @@ -38,7 +38,6 @@ import pytest from datetime import datetime, date from pytz import UTC -from locale import currency import re import json from time import sleep @@ -49,7 +48,7 @@ from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC -from biweeklybudget.utils import dtnow +from biweeklybudget.utils import dtnow, fmt_currency from biweeklybudget.tests.acceptance_helpers import AcceptanceHelper from biweeklybudget.models import * import biweeklybudget.models.base # noqa @@ -74,7 +73,7 @@ def txn_div(id, dt, amt, acct_name, acct_id, ) s += '
' s += '
%s
' % dt.strftime('%Y-%m-%d') - s += '
%s
' % currency(amt, grouping=True) + s += '
%s
' % fmt_currency(amt) s += '
Acct: ' s += '' s += '%s (%s)' % (acct_id, acct_name, acct_id) diff --git a/biweeklybudget/utils.py b/biweeklybudget/utils.py index 10314715..49adc569 100644 --- a/biweeklybudget/utils.py +++ b/biweeklybudget/utils.py @@ -42,6 +42,7 @@ from datetime import datetime import pytz from contextlib import contextmanager +from babel.numbers import format_currency logger = logging.getLogger(__name__) @@ -62,6 +63,21 @@ def fix_werkzeug_logger(): wlog.removeHandler(h) +def fmt_currency(amt): + """ + Using :py:attr:`~.settings.LOCALE_NAME` and + :py:attr:`~.settings.CURRENCY_CODE`, return ``amt`` formatted as currency. + + :param amt: The amount to format + :type amt: Any numeric type. + :return: ``amt`` formatted for the appropriate locale and currency + :rtype: str + """ + return format_currency( + amt, settings.CURRENCY_CODE, locale=settings.LOCALE_NAME + ) + + def dtnow(): """ Return the current datetime as a timezone-aware DateTime object in UTC. From 3fbe95497092e5ccd0feb4f492c34f60972b99e5 Mon Sep 17 00:00:00 2001 From: Jason Antman Date: Sat, 28 Oct 2017 18:03:53 -0400 Subject: [PATCH 05/13] issue #140 - flakes fix --- biweeklybudget/flaskapp/filters.py | 1 - 1 file changed, 1 deletion(-) diff --git a/biweeklybudget/flaskapp/filters.py b/biweeklybudget/flaskapp/filters.py index 1e0a539d..d5fbb4ab 100644 --- a/biweeklybudget/flaskapp/filters.py +++ b/biweeklybudget/flaskapp/filters.py @@ -35,7 +35,6 @@ ################################################################################ """ -from locale import currency from jinja2.runtime import Undefined from humanize import naturaltime From b736d1da3b8be3010f6ca3e5ac686ec1f3dc9d31 Mon Sep 17 00:00:00 2001 From: Jason Antman Date: Sat, 28 Oct 2017 18:21:01 -0400 Subject: [PATCH 06/13] issue #140 - fix docs build --- biweeklybudget/settings.py | 2 +- biweeklybudget/utils.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/biweeklybudget/settings.py b/biweeklybudget/settings.py index da955c93..4cfe922d 100644 --- a/biweeklybudget/settings.py +++ b/biweeklybudget/settings.py @@ -76,7 +76,7 @@ #: A `RFC 5646 / BCP 47 `_ Language Tag #: with a Region suffix to use for number (currency) formatting, i.e. "en_US", -#: "en_GB", "de_DE", etc. If this is not specified (None), it will be looked up +#: "en_GB", "de_DE", etc. If this is not specified (None), it will be looked up #: from environment variables in the following order: LC_ALL, LC_MONETARY, LANG. #: If none of those variables are set to a valid locale name (not including #: the "C" locale, which does not specify currency formatting) and this variable diff --git a/biweeklybudget/utils.py b/biweeklybudget/utils.py index 49adc569..f2d647b9 100644 --- a/biweeklybudget/utils.py +++ b/biweeklybudget/utils.py @@ -65,11 +65,11 @@ def fix_werkzeug_logger(): def fmt_currency(amt): """ - Using :py:attr:`~.settings.LOCALE_NAME` and - :py:attr:`~.settings.CURRENCY_CODE`, return ``amt`` formatted as currency. + Using :py:attr:`~biweeklybudget.settings.LOCALE_NAME` and + :py:attr:`~biweeklybudget.settings.CURRENCY_CODE`, return ``amt`` formatted + as currency. - :param amt: The amount to format - :type amt: Any numeric type. + :param amt: The amount to format; any numeric type. :return: ``amt`` formatted for the appropriate locale and currency :rtype: str """ From 446b451056782cc3ae2902486437348d0a011197 Mon Sep 17 00:00:00 2001 From: Jason Antman Date: Sat, 28 Oct 2017 18:21:52 -0400 Subject: [PATCH 07/13] issue #140 - expose LOCALE_NAME, CURRENCY_CODE and CURRENCY_SYMBOL to Javascript via base template --- biweeklybudget/flaskapp/templates/base.html | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/biweeklybudget/flaskapp/templates/base.html b/biweeklybudget/flaskapp/templates/base.html index 320b500a..366ebb5a 100644 --- a/biweeklybudget/flaskapp/templates/base.html +++ b/biweeklybudget/flaskapp/templates/base.html @@ -26,6 +26,13 @@ + + + From 26339b98f8e06532f8e883b3369d1ad243e44c45 Mon Sep 17 00:00:00 2001 From: Jason Antman Date: Sat, 28 Oct 2017 18:23:00 -0400 Subject: [PATCH 08/13] issue #140 - js - replace all hard-coded '$' with CURRENCY_SYMBOL --- biweeklybudget/flaskapp/static/js/budget_charts.js | 4 ++-- biweeklybudget/flaskapp/static/js/credit_payoffs.js | 4 ++-- biweeklybudget/flaskapp/static/js/formBuilder.js | 2 +- biweeklybudget/flaskapp/static/js/fuel_charts.js | 2 +- biweeklybudget/flaskapp/static/js/index.js | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/biweeklybudget/flaskapp/static/js/budget_charts.js b/biweeklybudget/flaskapp/static/js/budget_charts.js index 9ffcc3b5..50dc1919 100644 --- a/biweeklybudget/flaskapp/static/js/budget_charts.js +++ b/biweeklybudget/flaskapp/static/js/budget_charts.js @@ -46,7 +46,7 @@ $(function() { pointSize: 2, hideHover: 'auto', resize: true, - preUnits: '$', + preUnits: CURRENCY_SYMBOL, continuousLine: true }); }); @@ -63,7 +63,7 @@ $(function() { pointSize: 2, hideHover: 'auto', resize: true, - preUnits: '$', + preUnits: CURRENCY_SYMBOL, continuousLine: true }); }); diff --git a/biweeklybudget/flaskapp/static/js/credit_payoffs.js b/biweeklybudget/flaskapp/static/js/credit_payoffs.js index aa37781c..1f9cb52d 100644 --- a/biweeklybudget/flaskapp/static/js/credit_payoffs.js +++ b/biweeklybudget/flaskapp/static/js/credit_payoffs.js @@ -53,7 +53,7 @@ function addIncrease(settings) { s = s + ' , increase sum of monthly payments to '; s = s + ''; s = s + '
'; - s = s + '$'; + s = s + '' + CURRENCY_SYMBOL + ''; s = s + '
. (remove)
'; s = s + ''; $('#payoff_increase_forms').append(s); @@ -104,7 +104,7 @@ function addOnetime(settings) { s = s + ' , add '; s = s + ''; s = s + '
'; - s = s + '$'; + s = s + '' + CURRENCY_SYMBOL + ''; s = s + '
to the payment amount. (remove)
'; s = s + ''; $('#payoff_onetime_forms').append(s); diff --git a/biweeklybudget/flaskapp/static/js/formBuilder.js b/biweeklybudget/flaskapp/static/js/formBuilder.js index 265e6380..2171aed6 100644 --- a/biweeklybudget/flaskapp/static/js/formBuilder.js +++ b/biweeklybudget/flaskapp/static/js/formBuilder.js @@ -167,7 +167,7 @@ FormBuilder.prototype.addCurrency = function(id, name, label, options) { if (options.groupHtml !== null) { this.html += ' ' + options.groupHtml; } this.html += '>' + '
' + - '$' + + '' + CURRENCY_SYMBOL + '' + '' + '
'; if (options.helpBlock !== null) { diff --git a/biweeklybudget/flaskapp/static/js/fuel_charts.js b/biweeklybudget/flaskapp/static/js/fuel_charts.js index 34f6c240..a7278224 100644 --- a/biweeklybudget/flaskapp/static/js/fuel_charts.js +++ b/biweeklybudget/flaskapp/static/js/fuel_charts.js @@ -63,7 +63,7 @@ function initCharts() { pointSize: 2, hideHover: 'auto', resize: true, - preUnits: '$', + preUnits: CURRENCY_SYMBOL, continuousLine: true }); }); diff --git a/biweeklybudget/flaskapp/static/js/index.js b/biweeklybudget/flaskapp/static/js/index.js index 27791bf2..9a6c9457 100644 --- a/biweeklybudget/flaskapp/static/js/index.js +++ b/biweeklybudget/flaskapp/static/js/index.js @@ -46,7 +46,7 @@ $(function() { pointSize: 2, hideHover: 'auto', resize: true, - preUnits: '$', + preUnits: CURRENCY_SYMBOL, continuousLine: true }); }); From 16f7522178f60c665e0845490288587c176fc308 Mon Sep 17 00:00:00 2001 From: Jason Antman Date: Sat, 28 Oct 2017 18:23:26 -0400 Subject: [PATCH 09/13] issue #140 - js - replace inline currency formatting with call to custom.js fmt_currency() --- biweeklybudget/flaskapp/static/js/fuel.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/biweeklybudget/flaskapp/static/js/fuel.js b/biweeklybudget/flaskapp/static/js/fuel.js index fb5fb1d7..8793ebd9 100644 --- a/biweeklybudget/flaskapp/static/js/fuel.js +++ b/biweeklybudget/flaskapp/static/js/fuel.js @@ -225,17 +225,13 @@ $(document).ready(function() { { data: "cost_per_gallon", "render": function(data, type, row) { - return type === "display" || type === "filter" ? - '$' + data.toString().replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1,") : - data; + return type === "display" || type === "filter" ? fmt_currency(data) : data; } }, { data: "total_cost", "render": function(data, type, row) { - return type === "display" || type === "filter" ? - '$' + data.toString().replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1,") : - data; + return type === "display" || type === "filter" ? fmt_currency(data) : data; } }, { From 7dfbf78ff7a008cd238a1cd7b222a6e64526f756 Mon Sep 17 00:00:00 2001 From: Jason Antman Date: Sat, 28 Oct 2017 18:33:06 -0400 Subject: [PATCH 10/13] issue #140 - use Intl.NumberFormat for currency formatting in javascript --- biweeklybudget/flaskapp/static/js/custom.js | 13 +++++++++---- biweeklybudget/flaskapp/templates/base.html | 4 ++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/biweeklybudget/flaskapp/static/js/custom.js b/biweeklybudget/flaskapp/static/js/custom.js index 61b0cf02..7d4ae622 100644 --- a/biweeklybudget/flaskapp/static/js/custom.js +++ b/biweeklybudget/flaskapp/static/js/custom.js @@ -49,16 +49,21 @@ function fmt_null(o) { } /** - * Format a float as currency + * Format a float as currency. If ``value`` is null, return `` ``. + * Otherwise, construct a new instance of ``Intl.NumberFormat`` and use it to + * format the currency to a string. The formatter is called with the + * ``LOCALE_NAME`` and ``CURRENCY_CODE`` variables, which are templated into + * the header of ``base.html`` using the values specified in the Python + * settings module. * * @param {number} value - the number to format * @returns {string} The number formatted as currency */ function fmt_currency(value) { if (value === null) { return ' '; } - return ( - '$' + value.toFixed(2).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1,") - ).replace('$-', '-$'); + return new Intl.NumberFormat( + LOCALE_NAME, { style: 'currency', currency: CURRENCY_CODE } + ).format(value); } /** diff --git a/biweeklybudget/flaskapp/templates/base.html b/biweeklybudget/flaskapp/templates/base.html index 366ebb5a..baf34713 100644 --- a/biweeklybudget/flaskapp/templates/base.html +++ b/biweeklybudget/flaskapp/templates/base.html @@ -30,6 +30,10 @@ From 4e3780c649a36f029691942c468ac53c7c87a85f Mon Sep 17 00:00:00 2001 From: Jason Antman Date: Sat, 28 Oct 2017 18:35:38 -0400 Subject: [PATCH 11/13] issue #140 - fix reference to removed function --- .../tests/acceptance/flaskapp/views/test_reconcile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/biweeklybudget/tests/acceptance/flaskapp/views/test_reconcile.py b/biweeklybudget/tests/acceptance/flaskapp/views/test_reconcile.py index 034c0ece..a183acf7 100644 --- a/biweeklybudget/tests/acceptance/flaskapp/views/test_reconcile.py +++ b/biweeklybudget/tests/acceptance/flaskapp/views/test_reconcile.py @@ -137,7 +137,7 @@ def ofx_div(dt_posted, amt, acct_name, acct_id, trans_type, fitid, name, ) s += '
' s += '
%s
' % dt_posted.strftime('%Y-%m-%d') - s += '
%s
' % currency(amt, grouping=True) + s += '
%s
' % fmt_currency(amt, grouping=True) s += '
Acct: ' s += '' s += '%s (%s)' % (acct_id, acct_name, acct_id) From da5456ed2c0ad4af2f3f606ed3d575bb7014d095 Mon Sep 17 00:00:00 2001 From: Jason Antman Date: Mon, 30 Oct 2017 16:48:23 -0400 Subject: [PATCH 12/13] issue #140 - docs for localization and currency formatting --- CHANGES.rst | 3 +++ README.rst | 6 +++++ biweeklybudget/settings.py | 7 +++-- docs/source/app_usage.rst | 52 ++++++++++++++++++++++++++++++++++++-- 4 files changed, 64 insertions(+), 4 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 8806bebc..e6b3386c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,9 @@ Changelog Unreleased Changes ------------------ +* `PR #140 `_ - Support user-configurable currencies and currency formatting. + This isn't all-out localization, but adds ``CURRENCY_CODE`` and ``LOCALE_NAME`` configuration settings to control the currency symbol + and formatting used in the user interface and logs. * `PR #141 `_ - Switch acceptance tests from PhantomJS to headless Chrome. * Switch docs build screenshot script to use headless Chrome instead of PhantomJS. diff --git a/README.rst b/README.rst index 5e5f7989..a5893ac9 100644 --- a/README.rst +++ b/README.rst @@ -52,6 +52,12 @@ application available to anything other than localhost, but if you do, you need application is **not** designed to be accessible in any way to anyone other than authorized users (i.e. if you just serve it over the web, someone *will* get your account numbers, or worse). +*Note:* Any potential users outside of the US should see the documentation section on +`Currency Formatting and Localization `_; +the short version is that I've done my best to make this configurable, but as far as I know I'm the +only person using this software. If anyone else wants to use it and it doesn't work for your currency +or locale, let me know and I'll fix it. + Important Warning +++++++++++++++++ diff --git a/biweeklybudget/settings.py b/biweeklybudget/settings.py index 4cfe922d..fd46ccb1 100644 --- a/biweeklybudget/settings.py +++ b/biweeklybudget/settings.py @@ -81,13 +81,16 @@ #: If none of those variables are set to a valid locale name (not including #: the "C" locale, which does not specify currency formatting) and this variable #: is not set, the application will default to "en_US". This setting only -#: effects how monetary values are displayed in the UI, logs, etc. +#: effects how monetary values are displayed in the UI, logs, etc. For further +#: information, see +#: :ref:`Currency Formatting and Localization `. LOCALE_NAME = None #: An `ISO 4217 `_ Currency Code #: specifying the currency to use for all monetary amounts, i.e. "USD", "EUR", #: etc. This setting only effects how monetary values are displayed in the UI, -#: logs, etc. Currently defaults to "USD". +#: logs, etc. Currently defaults to "USD". For further information, see +#: :ref:`Currency Formatting and Localization `. CURRENCY_CODE = 'USD' #: string - SQLAlchemy database connection string. See the diff --git a/docs/source/app_usage.rst b/docs/source/app_usage.rst index 00a57315..130006cb 100644 --- a/docs/source/app_usage.rst +++ b/docs/source/app_usage.rst @@ -6,5 +6,53 @@ Application Usage This documentation is a work in progress. I suppose if anyone other than me ever tries to use this, I'll document it a bit more. -Pay Periods ------------ +.. _app_usage.l10n: + +Currency Formatting and Localization +------------------------------------ + +biweeklybudget supports configurable currency symbols and display/formatting, +controlled by the :py:attr:`~biweeklybudget.settings.LOCALE_NAME` and +:py:attr:`~biweeklybudget.settings.CURRENCY_CODE` settings. The former must +specify a `RFC 5646 / BCP 47 `_ language tag +with a region identifier (i.e. "en_US", "en_GB", "de_DE", etc.). If it is not +set in the settings module or via a ``LOCALE_NAME`` environment variable, it +will be looked up from the ``LC_ALL``, ``LC_MONETARY``, or ``LANG`` environment +variables, in that order. It cannot be a "C" or "C." locale, as these do not +specify currency formatting. The latter, ``CURRENCY_CODE``, must be a valid +`ISO 4217 `_ Currency Code (i.e. +"USD", "EUR", etc.) and can also be set via a ``CURRENCY_CODE`` environment +variable. + +These settings only effect the display of monetary units in the user interface +and in log files. I haven't made any attempt at actual internationalization of +the text, mainly because as far as I know I'm the only person in the world using +this software. If anyone else uses it, I'll be happy to work to accomodate users +of other languages or localities. + +Right now, regarding localization and currency formatting, please keep in mind +the following caveats (which I'd be happy to fix if anyone needs it): + +* The currency specified in downloaded OFX files is ignored. Since currency + conversion and exchange rates are far outside the scope of this application, + it's assumed that all accounts will be in the currency defined in settings. +* The ``wishlist2project`` console script that parses Amazon Wishlists and + updates Projects / BoMs with their contents currently only supports items + priced in USD, and currently only supports wishlists on the US amazon.com + site; these are limitations of the upstream project used for wishlist + parsing. +* The Fuel Log currently calls the volume of fuel units "gallons", probably + specifies "dollars" or "$" in the UI, and calls the units of distance "miles". + There's nothing mathematical that would prevent it from handling Kilometers + per Liter or any other combination of distance, volume and cost. If anyone + outside of the US is interested in using it, I'll gladly make those parts of + the user interface configurable as well. +* The database storage of monetary values assumes that they will all be a + decimal number, and currently only allows for six digits to the left of the + decimal and four digits to the right; this applies to all monetary units from + transaction amounts to account balances. As such, if you have any + transactions, budgets or accounts (including bank and investment accounts + imported via OFX) with values outside of 999999.9999 to -999999.9999 + (inclusive), the application will not function. If anyone needs support for + larger numbers (or, at the rate I'm going, I'm still working and paying into + my pension in about 300 years), the change shouldn't be terribly difficult. From ebdef45bf709037503efe2457753ed4ffe098e5d Mon Sep 17 00:00:00 2001 From: Jason Antman Date: Mon, 30 Oct 2017 16:51:30 -0400 Subject: [PATCH 13/13] fixes #140 - fix some straggling uses of locale module --- .../tests/acceptance/flaskapp/views/test_reconcile.py | 2 +- biweeklybudget/tests/acceptance_helpers.py | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/biweeklybudget/tests/acceptance/flaskapp/views/test_reconcile.py b/biweeklybudget/tests/acceptance/flaskapp/views/test_reconcile.py index a183acf7..fafc0024 100644 --- a/biweeklybudget/tests/acceptance/flaskapp/views/test_reconcile.py +++ b/biweeklybudget/tests/acceptance/flaskapp/views/test_reconcile.py @@ -137,7 +137,7 @@ def ofx_div(dt_posted, amt, acct_name, acct_id, trans_type, fitid, name, ) s += '
' s += '
%s
' % dt_posted.strftime('%Y-%m-%d') - s += '
%s
' % fmt_currency(amt, grouping=True) + s += '
%s
' % fmt_currency(amt) s += '
Acct: ' s += '' s += '%s (%s)' % (acct_id, acct_name, acct_id) diff --git a/biweeklybudget/tests/acceptance_helpers.py b/biweeklybudget/tests/acceptance_helpers.py index 76f08279..ecd0187c 100644 --- a/biweeklybudget/tests/acceptance_helpers.py +++ b/biweeklybudget/tests/acceptance_helpers.py @@ -36,7 +36,6 @@ """ import logging -import locale from time import sleep from selenium.common.exceptions import ( StaleElementReferenceException, TimeoutException @@ -44,11 +43,10 @@ from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC +from biweeklybudget.utils import fmt_currency logger = logging.getLogger(__name__) -locale.setlocale(locale.LC_ALL, '') - class AcceptanceHelper(object): @@ -376,6 +374,6 @@ def sort_trans_rows(self, rows): tmp_rows.append(row) ret = [] for row in sorted(tmp_rows, key=lambda x: (x[0], x[1])): - row[1] = locale.currency(row[1], grouping=True) + row[1] = fmt_currency(row[1]) ret.append(row) return ret