[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[GNUnet-SVN] [taler-bank] branch stable updated (1bae352 -> 1238da4)
From: |
gnunet |
Subject: |
[GNUnet-SVN] [taler-bank] branch stable updated (1bae352 -> 1238da4) |
Date: |
Wed, 31 May 2017 17:15:57 +0200 |
This is an automated email from the git hooks/post-receive script.
marcello pushed a change to branch stable
in repository bank.
from 1bae352 kill navbar until we have a better one that works
add 60224f6 navbar on the side
add 921106c jinja conversion
add 0c773d6 list instead of table for accounts
add 39d772e eh, oops
add ef8ed34 table style / heading fix
add 5c035cf Fix public accounts table
add f6dd7ee public accounts markup
add a5293d8 public accounts markup
add 5b05c4d markup
add 41bee23 markup
add e1388d3 stylesheet from web-common
add 4b3027a copyright / js license
add 5ad6c1b various fixes
add 304b245 take URLs from env
add b1d91a7 fix variable name clash
add 4bf2350 submodules
add ef9a605 submodules
add c581f2c sidebar links
add 68e731e fix pin/tan form
add b2adea3 #4824, not finished yet.
add a37e84d fix #4824's related issues
add 635ea32 Getting rid of the sign in front of amount's "value" field,
and implementing a flag-based way of accounting debts.
add 0f99308 Managing uncovered cases related to debits.
add 976763e Check if the user respects his debt threshold.
add 11a7e1e Managing 3/4 of debt limit exceeded exceptions, and getting
rid of helper function used to create reserves.
add 6f7b1f5 Better creation of initial accounts, logging statements.
add 95810a8 Finish fixing #4949's minor issues.
add 573b2cc Minor fixes on debit mgmt.
add 5c2bdea Give defaults for debt limit
add 78334a4 typo
add 2abf4bb fix db dump
add c743f49 making /admin call authenticated
add 2789025 defining json schema for /history requests
add 22945cf authenticating /history request
add 0838e22 extract and json-format /history outcome
add 5d52ca2 fix db string in the default conf file
add 6c9203e Reintroducing fixes from #4603, as regressions appeared.
add bd80ef1 debug print
add 86285ee Getting again rid of --system as a pip3's option, as it fails
on pip3 9.0.1 ..
add b51725d readme and remove useless default config
add 288726e still on removing useless code
add 85168e6 including 'authtoken' in INSTALLED_APPS
add 02a6594 only use nojs-version
add b72f707 remove rest_framework from settings
add 1016679 remove ancient jsmin autocheck
add d62b569 get rid of multichoice regarding db backend: only postgres
for now.
add f6c3a64 restoring default config
add ee81eba additional fixes to provide default config
add 382ffb2 implementing auth type basic
add b3ad8ff moving "direction" parameter inside POSTed data to /history
add 27db538 implementing 'direction' in /history
add 9cb82c4 fix JsonResponse invocation
add 50b30d7 fixing error objects
add 70452e7 still polishing error objects
add 867bf7e moving auth credentials in the HTTP headers
add ae74b2d towards /history refactoring
add c1b5e96 getting http headers the working way
add 9d304a5 fix query string composition + related tests
add 9cdc7cd add dummy transactions in test
add 4c48839 deploy FIXME
add 5168b65 sign & delta dealt with. testing needed.
add 5b0ff06 fix query of non-existent future /history element
add 773c15c test latest /history record returned
add 938f20a test non existent future /history record
add c511745 additional fixes as of the 'start' /history's argument. new
crashes when trying to build the history response returning one element having
row id in the middle of the id samples.
add c20b274 fix ascending order in query set
add 6225b37 testing empty responses from /history
add 139edb5 querying non existent / non owned accounts
add 4e687bb simplifying errors handling when authenticating
add 3645d94 add --enable-debian-system option to install bank to proper
prefix even on Debian
add 2ecab8d include error message for invalid /admin/add/incoming POSTed
data.
add f97a7eb fix #5005
add fe1b84e fix default passwords for exchange's testing purposes
add 2591b26 make "+" sign in 'delta' /history's parameter optional.
add 9e25a68 mentioning bad json field in error message of
/admin/add/incoming
add 10ec225 fix 'sign' regex
add 6b2e201 remove comments leftovers
add 8e7202e dedicated class for currency mismatch errors
add c23a650 towards removing the 'admin' interface
add 5e18da7 still on removing admin interface
add 33360c4 remove dedicated testcase for admin interface
add 669fff8 fix +n for 'delta'
add 58127cb fix /history test, as moving admin test into the other tests
file altered the way row_ids are defined during the test run.
add fb9f8bb rename field to match exchange-lib
add b49d9ab remove string formatting where not needed
add 8c63853 fix fields in history to pacify the exchange's bank-lib
add 863de3a fix install-dev for latest pip version
add 32cb2ab add debian-specific option to install-dev
add 6013574 adapt amounts' format to the standard Taler format.
add f22f3bd fix logic for /history view selection to properly handle misc
cornercases
add 984401d adding --with-db option, commenting out obsolete command to
create sample data
add bbeba15 run 'migrate' from 'django' subparser handler, as virgin dbs
need that.
add 068bf61 checking if db exists in 'django' subparser handler (still
not swallowing the stacktrace)
add bcbe8f5 remove useless command
add 368f146 typo
add 7d862ca make check uses dedicated config file now (partly fixes #5017)
add a5f3123 improve installation instructions a bit
add 979ba26 update README
add 6f7391b add option to run tests by wrapper script: 'make
check_with_wrapper'
add d730ad2 fix 'make dist'
add 65dc8a1 adapt currency format in withdraw form to new format
add fb37e89 invert fields of prevoius fix
add 9430850 more diagnostics for #5033
add 85fea9b addressing #5013
add 32bde8f first tests under erroneous circumstances
add 40dbd51 Add manual authentication before logging a user in. Just
calling login() without authenticating the user before, returns 200 even for
wrong login attempts.
add a3aa430 testing operations with wrong currencies
add e96c9fb removing unneeded comments
add 05d86a1 commenting out faults-injected tests
add b3486f1 making "name" field in /pin/question URL optional
add 1238da4 fix typo
No new revisions were added by this update.
Summary of changes:
.gitignore | 1 +
Makefile.am | 18 +-
README | 32 +-
bank-admin.wsgi.in | 21 -
bank-check-alt.conf | 17 +
bank-check.conf | 15 +
bank.conf | 11 +-
bank.wsgi.in | 1 -
configure.ac | 27 +-
install-dev.py.in | 31 +
run_tests.py | 13 +
taler-bank-manage.in | 40 +-
talerbank/app/Makefile.am | 8 +-
talerbank/app/amounts.py | 65 +-
talerbank/app/checks.py | 4 +-
talerbank/app/management/commands/dump_talerdb.py | 3 +-
.../app/management/commands/provide_accounts.py | 16 +-
talerbank/app/migrations/0001_initial.py | 8 +-
talerbank/app/models.py | 33 +-
talerbank/app/schemas.py | 15 +-
talerbank/app/static/Makefile.am | 2 -
talerbank/app/static/pure.css | 1508 ++++++++++++++++++++
talerbank/app/static/style.css | 158 --
talerbank/app/static/web-common | 2 +-
talerbank/app/templates/Makefile.am | 8 +-
talerbank/app/templates/account_disabled.html | 20 +-
talerbank/app/templates/base.html | 54 +-
talerbank/app/templates/login.html | 39 +-
talerbank/app/templates/pin_tan.html | 54 +-
talerbank/app/templates/profile_page.html | 82 +-
talerbank/app/templates/public_accounts.html | 99 +-
talerbank/app/templates/register.html | 14 +-
talerbank/app/templatetags/__init__.py | 0
talerbank/app/templatetags/mystatic.py | 30 -
talerbank/app/templatetags/settings.py | 8 -
talerbank/app/tests.py | 199 ++-
talerbank/app/tests_admin.py | 62 -
talerbank/app/tests_err.py | 301 ++++
talerbank/app/urls.py | 4 +-
talerbank/app/urlsadmin.py | 23 -
talerbank/app/views.py | 365 +++--
talerbank/jinja2.py | 74 +
talerbank/settings.py | 196 ++-
talerbank/settings_admin.py | 2 -
talerbank/settings_base.py | 183 ---
45 files changed, 2990 insertions(+), 876 deletions(-)
delete mode 100644 bank-admin.wsgi.in
create mode 100644 bank-check-alt.conf
create mode 100644 bank-check.conf
create mode 100644 install-dev.py.in
create mode 100755 run_tests.py
create mode 100644 talerbank/app/static/pure.css
delete mode 100644 talerbank/app/static/style.css
delete mode 100644 talerbank/app/templatetags/__init__.py
delete mode 100644 talerbank/app/templatetags/mystatic.py
delete mode 100644 talerbank/app/templatetags/settings.py
delete mode 100644 talerbank/app/tests_admin.py
create mode 100644 talerbank/app/tests_err.py
delete mode 100644 talerbank/app/urlsadmin.py
create mode 100644 talerbank/jinja2.py
delete mode 100644 talerbank/settings_admin.py
delete mode 100644 talerbank/settings_base.py
diff --git a/.gitignore b/.gitignore
index 4c14e6b..e217b27 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@ compile
missing
install-sh
Makefile.in
+install-dev.py
*.pyc
*.pyo
*.swp
diff --git a/Makefile.am b/Makefile.am
index 21d25d0..70b02ce 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -5,7 +5,6 @@ EXTRA_DIST = \
requirements.txt \
taler-bank-manage.in \
bank.wsgi.in \
- bank.conf \
setup.py \
contrib/nginx/django_nginx.conf
@@ -15,22 +14,25 @@ pkgcfg_DATA = \
bank.conf
pkgdata_DATA = \
- bank.wsgi \
- bank-admin.wsgi
+ bank.wsgi
# link package under prefix to source tree
+.PHONY: install-dev
install-dev:
- @pip3 install -e . --install-option="address@hidden@"
+ @$(PYTHON) ./install-dev.py
check:
- @export DJANGO_SETTINGS_MODULE="talerbank.settings"
TALER_PREFIX="@prefix@" && python3 -m django test talerbank.app.tests
- @export DJANGO_SETTINGS_MODULE="talerbank.settings_admin"
TALER_PREFIX="@prefix@" && python3 -m django test talerbank.app.tests_admin
+ @export DJANGO_SETTINGS_MODULE="talerbank.settings"
TALER_PREFIX="@prefix@" TALER_CONFIG_FILE="bank-check.conf" && python3 -m
django test talerbank.app.tests
+# @export DJANGO_SETTINGS_MODULE="talerbank.settings"
TALER_PREFIX="@prefix@" TALER_CONFIG_FILE="bank-check-alt.conf" && python3 -m
django test talerbank.app.tests_err
+
+check_with_wrapper:
+ @export DJANGO_SETTINGS_MODULE="talerbank.settings"
TALER_PREFIX="@prefix@" TALER_CONFIG_FILE="bank-check.conf" && python3
run_tests.py
# install into prefix
install-exec-hook:
- @pip3 install . --install-option="address@hidden@"
+ @pip3 install . --install-option="address@hidden@" @DEBIAN_PIP3_SYSTEM@
@# force update when sources changed
- @pip3 install . --install-option="address@hidden@" --upgrade --no-deps
+ @pip3 install . --install-option="address@hidden@" @DEBIAN_PIP3_SYSTEM@
--upgrade --no-deps
app:
@tar czf taler-bank-$(PACKAGE_VERSION)-app.tgz `cat INCLUDE.APP`
diff --git a/README b/README
index 7148927..4ccf8b4 100644
--- a/README
+++ b/README
@@ -1,6 +1,20 @@
This code implements a bank Web portal that tightly integrates with
-the Taler payment system. The bank it primarily meant be used as part
-of a demonstrator for the Taler system.
+the GNU Taler payment system. The bank it primarily meant be used as
+part of a demonstrator for the Taler system.
+
+==================== Dependencies ==========================
+
+-----------
+For Debian:
+-----------
+
+First, you need to:
+
+# apt-get install -t unstable git python3-django python3-psycopg2
+
+Note that "make install" will re-download additional dependencies
+needed for "make check". For the above, at the time of writing, you
+need Debian unstable, with older versions I get obscure errors.
================== HOW TO INSTALL THE BANK =================
@@ -13,21 +27,23 @@ bank (mostly JavaScript includes), and create the configure
script.
The next step is to specify the install prefix, run
-$ ./configure --prefix=$HOME/local # Adapt to your needs.
+$ export PREFIX=$HOME/local # Adapt to your needs.
+$ ./configure --prefix=$PREFIX
Then the usual GNU-compatible commands, that are
-$ make
+$ make install
-and
+and optionally
-$ make install
+$ export PYTHONPATH=$PREFIX/lib/python3.5/site-packages/
+$ make check # run the tests
================== HOW TO CONFIGURE THE BANK =================
The bank obeys to the INI syntax for configuration files. When launched, the
bank
will by default look for a configuration file located at ~/.config/taler.conf.
-To ovveride this behaviour, give the -c option when launching the bank.
+To overide this behaviour, give the -c option when launching the bank.
In order to properly run, the bank needs the following parts to be configured
@@ -77,6 +93,8 @@ FRACTION = 100000000
# The bank will try to connect to a database called 'talerlocal'
# running under Postgresql. The sysadmin will have to make sure
# that the bank has all the rights to work on that database.
+# NOTE, this value is optional, and the bank will fallback to sqlite3
+# if not given.
DATABASE = postgres:///talerlocal
# The following sections are to configure the "admin" part of
diff --git a/bank-admin.wsgi.in b/bank-admin.wsgi.in
deleted file mode 100644
index 94dc5ed..0000000
--- a/bank-admin.wsgi.in
+++ /dev/null
@@ -1,21 +0,0 @@
-import os
-import sys
-import site
-
-if sys.version_info.major < 3:
- print("The taler bank needs to run with Python>=3.4")
- sys.exit(1)
-
-os.environ.setdefault("DJANGO_SETTINGS_MODULE", "talerbank.settings_admin")
-os.environ.setdefault("TALER_PREFIX", "@prefix@")
-site.addsitedir("%s/lib/python%d.%d/site-packages" % (
- "@prefix@",
- sys.version_info.major,
- sys.version_info.minor))
-
-import django
-django.setup()
-
-from django.core.wsgi import get_wsgi_application
-
-application = get_wsgi_application()
diff --git a/bank-check-alt.conf b/bank-check-alt.conf
new file mode 100644
index 0000000..9c77a2a
--- /dev/null
+++ b/bank-check-alt.conf
@@ -0,0 +1,17 @@
+# Config file containing intentional errors, used
+# to test how the bank reacts.
+
+[taler]
+
+CURRENCY = KUDOS
+
+[bank]
+
+# Which database should we use?
+DATABASE = postgres:///talerbank
+
+# FIXME
+MAX_DEBT = KUDOS:50
+
+# FIXME
+MAX_DEBT_BANK = KUDOS:0
diff --git a/bank-check.conf b/bank-check.conf
new file mode 100644
index 0000000..b38ccd0
--- /dev/null
+++ b/bank-check.conf
@@ -0,0 +1,15 @@
+
+[taler]
+
+CURRENCY = KUDOS
+
+[bank]
+
+# Which database should we use?
+DATABASE = postgres:///talerbank
+
+# FIXME
+MAX_DEBT = KUDOS:50
+
+# FIXME
+MAX_DEBT_BANK = KUDOS:0
diff --git a/bank.conf b/bank.conf
index 67d4455..c99c2f9 100644
--- a/bank.conf
+++ b/bank.conf
@@ -1,3 +1,10 @@
[bank]
-uwsgi_serve = tcp
-database = postgres://talerbank
+
+# Which database should we use?
+DATABASE = postgres:///talerbank
+
+# FIXME
+MAX_DEBT = KUDOS:50
+
+# FIXME
+MAX_DEBT_BANK = KUDOS:0
\ No newline at end of file
diff --git a/bank.wsgi.in b/bank.wsgi.in
index f276b99..c15e645 100644
--- a/bank.wsgi.in
+++ b/bank.wsgi.in
@@ -6,7 +6,6 @@ if sys.version_info.major < 3:
print("The taler bank needs to run with Python>=3.4")
sys.exit(1)
-os.environ.setdefault("DJANGO_SETTINGS_MODULE", "talerbank.settings")
os.environ.setdefault("TALER_PREFIX", "@prefix@")
site.addsitedir("%s/lib/python%d.%d/site-packages" % (
"@prefix@",
diff --git a/configure.ac b/configure.ac
index 94f1ba2..72c8047 100644
--- a/configure.ac
+++ b/configure.ac
@@ -33,6 +33,17 @@ AC_MSG_RESULT([$PIP_VERSION])
AX_COMPARE_VERSION([$PIP_VERSION],[lt],[6.0], [AC_MSG_ERROR([Please install
pip3>=6.0])])
+# On Debian systems, we may need to pass "--system" to pip3 to get
+# to the desired installation target directory
+AC_ARG_ENABLE(debian-system,
+ AS_HELP_STRING(--enable-debian-system, pass --system option to pip3 to make
Debian pip obey installation prefix),
+[if test x$enableval = xyes; then
+ DEBIAN_PIP3_SYSTEM="--system"
+else
+ DEBIAN_PIP3_SYSTEM=""
+fi])
+AC_SUBST(DEBIAN_PIP3_SYSTEM)
+
#
# Check for PostgreSQL
#
@@ -55,20 +66,6 @@ fi
AC_CHECK_PROG([tsc],[tsc],[yes],[no])
AM_CONDITIONAL([HAVE_TSC], [test "x$tsc" = xyes])
-#
-# Check for minifier
-#
-AC_MSG_CHECKING([Checking for jsmin])
-python3 -m jsmin &> /dev/null
-if test $? -ne 0;
- then
- AC_MSG_ERROR([Please install Python3 module 'jsmin'])
-fi
-
-#
-# Report
-#
-
if test x$pyheaders != x1; then
AC_MSG_WARN([Python headers not installed, might be required to build uwsgi])
fi
@@ -78,8 +75,8 @@ fi
#
AC_CONFIG_FILES([Makefile
+ install-dev.py
bank.wsgi
- bank-admin.wsgi
taler-bank-manage
talerbank/Makefile
talerbank/app/Makefile
diff --git a/install-dev.py.in b/install-dev.py.in
new file mode 100644
index 0000000..b4cfc6e
--- /dev/null
+++ b/install-dev.py.in
@@ -0,0 +1,31 @@
+#!/usr/bin/env python
+
+"""
+This file is in the public domain.
+
+Execute pip3 in the right environment and the right parameters to install this
+package in the correct path.
+
+This is not a common use-case for pip, and thus it needs some hand-holding.
+"""
+
+import sys
+import os
+
+prefix_path = "%s/lib/python%d.%d/site-packages" % (
+ "@prefix@",
+ sys.version_info.major,
+ sys.version_info.minor)
+
+current_paths = os.environ.get("PYTHONPATH", "").split(":")
+current_paths.append(prefix_path)
+current_paths.remove("")
+os.environ["PYTHONPATH"] = ":".join(current_paths)
+
+args = ["pip3", "install", '--install-option=--prefix=%s' % "@prefix@", "-e",
"."]
+if "@DEBIAN_PIP3_SYSTEM@":
+ args.push("@DEBIAN_PIP3_SYSTEM@")
+
+os.execvp("pip3", args)
+
+
diff --git a/run_tests.py b/run_tests.py
new file mode 100755
index 0000000..1fd4485
--- /dev/null
+++ b/run_tests.py
@@ -0,0 +1,13 @@
+#!/usr/bin/env python3
+
+from django.core.management import call_command
+from django.db.utils import OperationalError
+import django
+import sys
+
+django.setup()
+try:
+ call_command("test", "talerbank.app.tests")
+except OperationalError:
+ print("Catching DB hard error, skipping the test")
+ sys.exit(0)
diff --git a/taler-bank-manage.in b/taler-bank-manage.in
index 911782f..75ba391 100644
--- a/taler-bank-manage.in
+++ b/taler-bank-manage.in
@@ -27,6 +27,9 @@ uwsgi_logfmt = "%(ltime) %(proto) %(method) %(uri) %(proto)
=> %(status)"
def handle_django(args):
import django
django.setup()
+ from django.core.management import call_command
+ # always run 'migrate' first, in case a virgin db is being used.
+ call_command('migrate')
from django.core.management import execute_from_command_line
execute_from_command_line([sys.argv[0] + " django"] + args.command)
@@ -41,7 +44,7 @@ def handle_serve_http(args):
tc = TalerConfig.from_file(os.environ.get("TALER_CONFIG_FILE"))
port = args.port
if port is None:
- port = tc[token]["http_port"].value_int(required=True)
+ port = tc["bank"]["http_port"].value_int(required=True)
httpspec = ":%d" % (port,)
params = ["uwsgi", "uwsgi",
@@ -49,7 +52,7 @@ def handle_serve_http(args):
"--die-on-term",
"--http", httpspec,
"--log-format", uwsgi_logfmt,
- "--wsgi-file", "@prefix@/share/taler-bank/%s.wsgi" % token]
+ "--wsgi-file", "@prefix@/share/taler-bank/bank.wsgi"]
os.execlp(*params)
@@ -66,14 +69,14 @@ def handle_serve_uwsgi(args):
"--master",
"--die-on-term",
"--log-format", uwsgi_logfmt,
- "--wsgi-file", "@prefix@/share/taler-bank/%s.wsgi" % token]
+ "--wsgi-file", "@prefix@/share/taler-bank/bank.wsgi"]
if "tcp" == serve_uwsgi:
- port = tc[token]["uwsgi_port"].value_int(required=True)
+ port = tc["bank"]["uwsgi_port"].value_int(required=True)
spec = ":%d" % (port,)
params.extend(["--socket", spec])
else:
- spec = tc[token]["uwsgi_unixpath"].value_filename(required=True)
- mode = tc[token]["uwsgi_unixpath_mode"].value_filename(required=True)
+ spec = tc["bank"]["uwsgi_unixpath"].value_filename(required=True)
+ mode = tc["bank"]["uwsgi_unixpath_mode"].value_filename(required=True)
params.extend(["--socket", spec])
params.extend(["--chmod-socket="+mode])
os.makedirs(os.path.dirname(spec), exist_ok=True)
@@ -94,17 +97,19 @@ def handle_config(args):
parser = argparse.ArgumentParser()
parser.set_defaults(func=None)
-parser.add_argument('--config', '-c', help="configuration file to use",
metavar="CONFIG", type=str, dest="config", default=None)
-parser.add_argument('--with-db', help="use ALTERNATE_DB", type=str,
metavar="ALTERNATE_DB", dest="altdb")
-parser.add_argument("--admin", "-a", dest="admin", action="store_true",
help="Only run the \"admin\" interface")
+parser.add_argument('--config', '-c', help="configuration file to use",
+ metavar="CONFIG", type=str, dest="config", default=None)
+parser.add_argument('--with-db', help="use 'dbname' (currently only
'dbtype'=='postgres' is supported)",
+ type=str, metavar="dbtype:///dbname", dest="altdb")
sub = parser.add_subparsers()
p = sub.add_parser('django', help="Run django-admin command")
p.add_argument("command", nargs=argparse.REMAINDER)
p.set_defaults(func=handle_django)
-p = sub.add_parser('sampledata', help="Put sample data into the db")
-p.set_defaults(func=handle_sampledata)
+# FIXME: adapt to newest wire_transfer()
+# p = sub.add_parser('sampledata', help="Put sample data into the db")
+# p.set_defaults(func=handle_sampledata)
p = sub.add_parser('serve-http', help="Serve bank over HTTP")
p.add_argument("--port", "-p", dest="port", type=int, default=None,
metavar="PORT")
@@ -119,18 +124,11 @@ p.set_defaults(func=handle_config)
args = parser.parse_args()
-token = "bank%s" % ("-admin" if args.admin else "")
-
-settings_module = "talerbank.settings"
-if token == "bank-admin":
- settings_module = "talerbank.settings_admin"
-
-os.environ.setdefault("DJANGO_SETTINGS_MODULE", settings_module)
-
-logger.info("Setting token to %s" % token)
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "talerbank.settings")
if args.altdb:
- os.environ.setdefault("ALTDB", args.altdb)
+ logger.info("Setting alternate db: %s" % args.altdb)
+ os.environ.setdefault("TALER_BANK_ALTDB", args.altdb)
if getattr(args, 'func', None) is None:
parser.print_help()
diff --git a/talerbank/app/Makefile.am b/talerbank/app/Makefile.am
index cf8e06b..01543fd 100644
--- a/talerbank/app/Makefile.am
+++ b/talerbank/app/Makefile.am
@@ -2,17 +2,11 @@ SUBDIRS = . management migrations templates static
EXTRA_DIST = \
admin.py \
- captcha.py \
- errors.py \
- history.py \
- middleware.py \
schemas.py \
urls.py \
views.py \
amounts.py \
checks.py \
- funds.py \
__init__.py \
models.py \
- tests.py \
- user.py
+ tests.py
diff --git a/talerbank/app/amounts.py b/talerbank/app/amounts.py
index 93f65e6..f9bdd02 100644
--- a/talerbank/app/amounts.py
+++ b/talerbank/app/amounts.py
@@ -19,11 +19,63 @@
import re
import math
import logging
+from django.conf import settings
logger = logging.getLogger(__name__)
FRACTION = 100000000
+class CurrencyMismatchException(Exception):
+ def __init__(self, msg=None, status_code=0):
+ self.msg = msg
+ # HTTP status code to be returned as response for
+ # this exception
+ self.status_code = status_code
+
+class BadFormatAmount(Exception):
+ def __init__(self, msg=None, status_code=0):
+ self.msg = msg
+ # HTTP status code to be returned as response for
+ # this exception
+ self.status_code = status_code
+
+
+def check_currency(a1, a2):
+ if a1["currency"] != a2["currency"]:
+ logger.error("Different currencies given: %s vs %s" % (a1["currency"],
a2["currency"]))
+ raise CurrencyMismatchException
+
+def get_zero():
+ return dict(value=0, fraction=0, currency=settings.TALER_CURRENCY)
+
+def amount_add(a1, a2):
+ check_currency(a1, a2)
+ a1_float = floatify(a1)
+ a2_float = floatify(a2)
+ return parse_amount("%s:%s" % (a2["currency"], str(a1_float + a2_float)))
+
+def amount_sub(a1, a2):
+ check_currency(a1, a2)
+ a1_float = floatify(a1)
+ a2_float = floatify(a2)
+ sub = a1_float - a2_float
+ fmt = "%s:%s" % (a2["currency"], str(sub))
+ return parse_amount(fmt)
+
+# Return -1 if a1 < a2, 0 if a1 == a2, 1 if a1 > a2
+def amount_cmp(a1, a2):
+ check_currency(a1, a2)
+ a1_float = floatify(a1)
+ a2_float = floatify(a2)
+
+ if a1_float < a2_float:
+ return -1
+ elif a1_float == a2_float:
+ return 0
+
+ return 1
+
+
def floatify(amount_dict):
return amount_dict['value'] + (float(amount_dict['fraction']) /
float(FRACTION))
@@ -36,15 +88,14 @@ def parse_amount(amount_str):
Parse amount of return None if not a
valid amount string
"""
- parsed = re.search("^\s*([0-9]+)(\.[0-9]+)? ([-_*A-Za-z0-9]+)\s*$",
amount_str)
+ parsed = re.search("^\s*([-_*A-Za-z0-9]+):([0-9]+)(\.[0-9]+)?\s*$",
amount_str)
if not parsed:
- return None
- value = int(parsed.group(1))
+ raise BadFormatAmount
+ value = int(parsed.group(2))
fraction = 0
- if parsed.group(2) is not None:
- for i, digit in enumerate(parsed.group(2)[1:]):
+ if parsed.group(3) is not None:
+ for i, digit in enumerate(parsed.group(3)[1:]):
fraction += int(int(digit) * (FRACTION / 10 ** (i+1)))
-
return {'value': value,
'fraction': fraction,
- 'currency': parsed.group(3)}
+ 'currency': parsed.group(1)}
diff --git a/talerbank/app/checks.py b/talerbank/app/checks.py
index 3654816..6734d3a 100644
--- a/talerbank/app/checks.py
+++ b/talerbank/app/checks.py
@@ -1,5 +1,5 @@
from django.core.checks import register, Warning
-from django.db import OperationalError
+from django.db.utils import OperationalError
@register()
@@ -17,7 +17,7 @@ def example_check(app_configs, **kwargs):
))
except OperationalError:
errors.append(Warning(
- 'The bank user does not exist',
+ 'Presumably non existent database',
hint="create a database for the application",
id='talerbank.E002'
))
diff --git a/talerbank/app/management/commands/dump_talerdb.py
b/talerbank/app/management/commands/dump_talerdb.py
index 89c4933..ab587d9 100644
--- a/talerbank/app/management/commands/dump_talerdb.py
+++ b/talerbank/app/management/commands/dump_talerdb.py
@@ -19,6 +19,7 @@ from ...models import BankAccount, BankTransaction
from django.db.utils import OperationalError, ProgrammingError
import logging
import sys
+from ...amounts import floatify
# Rewrite to match new BankTransaction layout.
@@ -49,7 +50,7 @@ def dump_history():
# as the first/last character on a line makes flake8 complain
msg.append("+%s, " % item.credit_account.account_no)
msg.append("-%s, " % item.debit_account.account_no)
- msg.append("%.2f, " % item.amount)
+ msg.append("%.2f, " % floatify(item.amount_obj))
msg.append(item.subject)
print(''.join(msg))
except (OperationalError, ProgrammingError):
diff --git a/talerbank/app/management/commands/provide_accounts.py
b/talerbank/app/management/commands/provide_accounts.py
index 421f896..cf3f0cd 100644
--- a/talerbank/app/management/commands/provide_accounts.py
+++ b/talerbank/app/management/commands/provide_accounts.py
@@ -31,7 +31,7 @@ def demo_accounts():
try:
User.objects.get(username=name)
except User.DoesNotExist:
- u = User.objects.create_user(username=name, password='')
+ u = User.objects.create_user(username=name, password='x')
b = BankAccount(user=u,
currency=settings.TALER_CURRENCY,
is_public=True)
@@ -40,8 +40,10 @@ def demo_accounts():
def ensure_account(name):
+ logger.info("ensuring account '{}'".format(name))
+ user = None
try:
- User.objects.get(username=name)
+ user = User.objects.get(username=name)
except (OperationalError, ProgrammingError):
logger.error("likely causes: non existent DB or unmigrated project\n"
"(try 'taler-bank-manage django migrate' in the latter
case)",
@@ -49,12 +51,18 @@ def ensure_account(name):
exc_info=True)
sys.exit(1)
except User.DoesNotExist:
- user = User.objects.create_user(username=name, password='')
+ logger.info("Creating *user* account '{}'".format(name))
+ user = User.objects.create_user(username=name, password='x')
+
+ try:
+ BankAccount.objects.get(user=user)
+
+ except BankAccount.DoesNotExist:
acc = BankAccount(user=user,
currency=settings.TALER_CURRENCY,
is_public=True)
acc.save()
- logger.info("Creating account '%s', with number %s", name,
acc.account_no)
+ logger.info("Creating *bank* account number '{}' for user
'{}'".format(acc.account_no, name))
def basic_accounts():
diff --git a/talerbank/app/migrations/0001_initial.py
b/talerbank/app/migrations/0001_initial.py
index 310fc0c..ff05150 100644
--- a/talerbank/app/migrations/0001_initial.py
+++ b/talerbank/app/migrations/0001_initial.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# Generated by Django 1.10.3 on 2016-11-29 14:35
+# Generated by Django 1.10.3 on 2017-03-22 20:22
from __future__ import unicode_literals
from django.conf import settings
@@ -20,6 +20,9 @@ class Migration(migrations.Migration):
name='BankAccount',
fields=[
('is_public', models.BooleanField(default=False)),
+ ('debit', models.BooleanField(default=False)),
+ ('balance_value', models.IntegerField(default=0)),
+ ('balance_fraction', models.IntegerField(default=0)),
('balance', models.FloatField(default=0)),
('currency', models.CharField(default='', max_length=12)),
('account_no', models.AutoField(primary_key=True,
serialize=False)),
@@ -30,7 +33,8 @@ class Migration(migrations.Migration):
name='BankTransaction',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True,
serialize=False, verbose_name='ID')),
- ('amount', models.FloatField(default=0)),
+ ('amount_value', models.IntegerField(default=0)),
+ ('amount_fraction', models.IntegerField(default=0)),
('currency', models.CharField(max_length=12)),
('subject', models.CharField(default='(no subject given)',
max_length=200)),
('date', models.DateTimeField(auto_now=True)),
diff --git a/talerbank/app/models.py b/talerbank/app/models.py
index 1b84fe9..f1c3485 100644
--- a/talerbank/app/models.py
+++ b/talerbank/app/models.py
@@ -22,16 +22,41 @@ from django.db import models
class BankAccount(models.Model):
is_public = models.BooleanField(default=False)
+ debit = models.BooleanField(default=False)
+ balance_value = models.IntegerField(default=0)
+ balance_fraction = models.IntegerField(default=0)
balance = models.FloatField(default=0)
currency = models.CharField(max_length=12, default="")
account_no = models.AutoField(primary_key=True)
user = models.OneToOneField(User, on_delete=models.CASCADE)
-
+ def _get_balance(self):
+ return dict(value=self.balance_value,
+ fraction=self.balance_fraction,
+ currency=self.currency)
+ def _set_balance(self, amount):
+ self.balance_value = amount["value"]
+ self.balance_fraction = amount["fraction"]
+ self.currency = amount["currency"]
+ balance_obj = property(_get_balance, _set_balance)
class BankTransaction(models.Model):
- amount = models.FloatField(default=0)
+ amount_value = models.IntegerField(default=0)
+ amount_fraction = models.IntegerField(default=0)
currency = models.CharField(max_length=12)
- debit_account = models.ForeignKey(BankAccount, on_delete=models.CASCADE,
related_name="debit_account")
- credit_account = models.ForeignKey(BankAccount, on_delete=models.CASCADE,
related_name="credit_account")
+ debit_account = models.ForeignKey(BankAccount,
+ on_delete=models.CASCADE,
+ related_name="debit_account")
+ credit_account = models.ForeignKey(BankAccount,
+ on_delete=models.CASCADE,
+ related_name="credit_account")
subject = models.CharField(default="(no subject given)", max_length=200)
date = models.DateTimeField(auto_now=True)
+ def _get_amount(self):
+ return dict(value=self.amount_value,
+ fraction=self.amount_fraction,
+ currency=self.currency)
+ def _set_amount(self, amount):
+ self.amount_value = amount["value"]
+ self.amount_fraction = amount["fraction"]
+ self.currency = amount["currency"]
+ amount_obj = property(_get_amount, _set_amount)
diff --git a/talerbank/app/schemas.py b/talerbank/app/schemas.py
index d4ba21b..91771d1 100644
--- a/talerbank/app/schemas.py
+++ b/talerbank/app/schemas.py
@@ -31,12 +31,20 @@ wiredetails_schema = {
"type": {"type": "string"},
"account_number": {"type": "integer"},
"bank_uri": {"type": "string"},
- "name": {"type": "string"},
+ "name": {"type": "string", "required": False},
}
}
}
}
+auth_schema = {
+ "type": "object",
+ "properties": {
+ "type": {"type": "string"},
+ "data": {"type": "object", "required": False}
+ }
+}
+
amount_schema = {
"type": "object",
"properties": {
@@ -53,7 +61,7 @@ incoming_request_schema = {
"wtid": {"type": "string"},
"exchange_url": {"type": "string"},
"credit_account": {"type": "integer"},
- "debit_account": {"type": "integer"}
+ "auth": auth_schema
}
}
@@ -65,3 +73,6 @@ def validate_wiredetails(wiredetails):
def validate_incoming_request(incoming_request):
validictory.validate(incoming_request, incoming_request_schema)
+
+def validate_auth_basic(auth_basic):
+ validictory.validate(auth_basic, auth_basic_schema)
diff --git a/talerbank/app/static/Makefile.am b/talerbank/app/static/Makefile.am
index e45c105..2742b65 100644
--- a/talerbank/app/static/Makefile.am
+++ b/talerbank/app/static/Makefile.am
@@ -2,6 +2,4 @@ SUBDIRS = . web-common
EXTRA_DIST = \
favicon.ico \
- style.css \
- disabled-button.css \
chrome-store-link.js
diff --git a/talerbank/app/static/pure.css b/talerbank/app/static/pure.css
new file mode 100644
index 0000000..7391139
--- /dev/null
+++ b/talerbank/app/static/pure.css
@@ -0,0 +1,1508 @@
+/*!
+Pure v0.6.2
+Copyright 2013 Yahoo!
+Licensed under the BSD License.
+https://github.com/yahoo/pure/blob/master/LICENSE.md
+*/
+/*!
+normalize.css v^3.0 | MIT License | git.io/normalize
+Copyright (c) Nicolas Gallagher and Jonathan Neal
+*/
+/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */
+
+/**
+ * 1. Set default font family to sans-serif.
+ * 2. Prevent iOS and IE text size adjust after device orientation change,
+ * without disabling user zoom.
+ */
+
+html {
+ font-family: sans-serif; /* 1 */
+ -ms-text-size-adjust: 100%; /* 2 */
+ -webkit-text-size-adjust: 100%; /* 2 */
+}
+
+/**
+ * Remove default margin.
+ */
+
+body {
+ margin: 0;
+}
+
+/* HTML5 display definitions
+ ==========================================================================
*/
+
+/**
+ * Correct `block` display not defined for any HTML5 element in IE 8/9.
+ * Correct `block` display not defined for `details` or `summary` in IE 10/11
+ * and Firefox.
+ * Correct `block` display not defined for `main` in IE 11.
+ */
+
+article,
+aside,
+details,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+main,
+menu,
+nav,
+section,
+summary {
+ display: block;
+}
+
+/**
+ * 1. Correct `inline-block` display not defined in IE 8/9.
+ * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
+ */
+
+audio,
+canvas,
+progress,
+video {
+ display: inline-block; /* 1 */
+ vertical-align: baseline; /* 2 */
+}
+
+/**
+ * Prevent modern browsers from displaying `audio` without controls.
+ * Remove excess height in iOS 5 devices.
+ */
+
+audio:not([controls]) {
+ display: none;
+ height: 0;
+}
+
+/**
+ * Address `[hidden]` styling not present in IE 8/9/10.
+ * Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22.
+ */
+
+[hidden],
+template {
+ display: none;
+}
+
+/* Links
+ ==========================================================================
*/
+
+/**
+ * Remove the gray background color from active links in IE 10.
+ */
+
+a {
+ background-color: transparent;
+}
+
+/**
+ * Improve readability of focused elements when they are also in an
+ * active/hover state.
+ */
+
+a:active,
+a:hover {
+ outline: 0;
+}
+
+/* Text-level semantics
+ ==========================================================================
*/
+
+/**
+ * Address styling not present in IE 8/9/10/11, Safari, and Chrome.
+ */
+
+abbr[title] {
+ border-bottom: 1px dotted;
+}
+
+/**
+ * Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
+ */
+
+b,
+strong {
+ font-weight: bold;
+}
+
+/**
+ * Address styling not present in Safari and Chrome.
+ */
+
+dfn {
+ font-style: italic;
+}
+
+/**
+ * Address variable `h1` font-size and margin within `section` and `article`
+ * contexts in Firefox 4+, Safari, and Chrome.
+ */
+
+h1 {
+ font-size: 2em;
+ margin: 0.67em 0;
+}
+
+/**
+ * Address styling not present in IE 8/9.
+ */
+
+mark {
+ background: #ff0;
+ color: #000;
+}
+
+/**
+ * Address inconsistent and variable font size in all browsers.
+ */
+
+small {
+ font-size: 80%;
+}
+
+/**
+ * Prevent `sub` and `sup` affecting `line-height` in all browsers.
+ */
+
+sub,
+sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+}
+
+sup {
+ top: -0.5em;
+}
+
+sub {
+ bottom: -0.25em;
+}
+
+/* Embedded content
+ ==========================================================================
*/
+
+/**
+ * Remove border when inside `a` element in IE 8/9/10.
+ */
+
+img {
+ border: 0;
+}
+
+/**
+ * Correct overflow not hidden in IE 9/10/11.
+ */
+
+svg:not(:root) {
+ overflow: hidden;
+}
+
+/* Grouping content
+ ==========================================================================
*/
+
+/**
+ * Address margin not present in IE 8/9 and Safari.
+ */
+
+figure {
+ margin: 1em 40px;
+}
+
+/**
+ * Address differences between Firefox and other browsers.
+ */
+
+hr {
+ box-sizing: content-box;
+ height: 0;
+}
+
+/**
+ * Contain overflow in all browsers.
+ */
+
+pre {
+ overflow: auto;
+}
+
+/**
+ * Address odd `em`-unit font size rendering in all browsers.
+ */
+
+code,
+kbd,
+pre,
+samp {
+ font-family: monospace, monospace;
+ font-size: 1em;
+}
+
+/* Forms
+ ==========================================================================
*/
+
+/**
+ * Known limitation: by default, Chrome and Safari on OS X allow very limited
+ * styling of `select`, unless a `border` property is set.
+ */
+
+/**
+ * 1. Correct color not being inherited.
+ * Known issue: affects color of disabled elements.
+ * 2. Correct font properties not being inherited.
+ * 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
+ */
+
+button,
+input,
+optgroup,
+select,
+textarea {
+ color: inherit; /* 1 */
+ font: inherit; /* 2 */
+ margin: 0; /* 3 */
+}
+
+/**
+ * Address `overflow` set to `hidden` in IE 8/9/10/11.
+ */
+
+button {
+ overflow: visible;
+}
+
+/**
+ * Address inconsistent `text-transform` inheritance for `button` and `select`.
+ * All other form control elements do not inherit `text-transform` values.
+ * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
+ * Correct `select` style inheritance in Firefox.
+ */
+
+button,
+select {
+ text-transform: none;
+}
+
+/**
+ * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
+ * and `video` controls.
+ * 2. Correct inability to style clickable `input` types in iOS.
+ * 3. Improve usability and consistency of cursor style between image-type
+ * `input` and others.
+ */
+
+button,
+html input[type="button"], /* 1 */
+input[type="reset"],
+input[type="submit"] {
+ -webkit-appearance: button; /* 2 */
+ cursor: pointer; /* 3 */
+}
+
+/**
+ * Re-set default cursor for disabled elements.
+ */
+
+button[disabled],
+html input[disabled] {
+ cursor: default;
+}
+
+/**
+ * Remove inner padding and border in Firefox 4+.
+ */
+
+button::-moz-focus-inner,
+input::-moz-focus-inner {
+ border: 0;
+ padding: 0;
+}
+
+/**
+ * Address Firefox 4+ setting `line-height` on `input` using `!important` in
+ * the UA stylesheet.
+ */
+
+input {
+ line-height: normal;
+}
+
+/**
+ * It's recommended that you don't attempt to style these elements.
+ * Firefox's implementation doesn't respect box-sizing, padding, or width.
+ *
+ * 1. Address box sizing set to `content-box` in IE 8/9/10.
+ * 2. Remove excess padding in IE 8/9/10.
+ */
+
+input[type="checkbox"],
+input[type="radio"] {
+ box-sizing: border-box; /* 1 */
+ padding: 0; /* 2 */
+}
+
+/**
+ * Fix the cursor style for Chrome's increment/decrement buttons. For certain
+ * `font-size` values of the `input`, it causes the cursor style of the
+ * decrement button to change from `default` to `text`.
+ */
+
+input[type="number"]::-webkit-inner-spin-button,
+input[type="number"]::-webkit-outer-spin-button {
+ height: auto;
+}
+
+/**
+ * 1. Address `appearance` set to `searchfield` in Safari and Chrome.
+ * 2. Address `box-sizing` set to `border-box` in Safari and Chrome.
+ */
+
+input[type="search"] {
+ -webkit-appearance: textfield; /* 1 */
+ box-sizing: content-box; /* 2 */
+}
+
+/**
+ * Remove inner padding and search cancel button in Safari and Chrome on OS X.
+ * Safari (but not Chrome) clips the cancel button when the search input has
+ * padding (and `textfield` appearance).
+ */
+
+input[type="search"]::-webkit-search-cancel-button,
+input[type="search"]::-webkit-search-decoration {
+ -webkit-appearance: none;
+}
+
+/**
+ * Define consistent border, margin, and padding.
+ */
+
+fieldset {
+ border: 1px solid #c0c0c0;
+ margin: 0 2px;
+ padding: 0.35em 0.625em 0.75em;
+}
+
+/**
+ * 1. Correct `color` not being inherited in IE 8/9/10/11.
+ * 2. Remove padding so people aren't caught out if they zero out fieldsets.
+ */
+
+legend {
+ border: 0; /* 1 */
+ padding: 0; /* 2 */
+}
+
+/**
+ * Remove default vertical scrollbar in IE 8/9/10/11.
+ */
+
+textarea {
+ overflow: auto;
+}
+
+/**
+ * Don't inherit the `font-weight` (applied by a rule above).
+ * NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
+ */
+
+optgroup {
+ font-weight: bold;
+}
+
+/* Tables
+ ==========================================================================
*/
+
+/**
+ * Remove most spacing between table cells.
+ */
+
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+
+td,
+th {
+ padding: 0;
+}
+
+/*csslint important:false*/
+
+/* ==========================================================================
+ Pure Base Extras
+ ==========================================================================
*/
+
+/**
+ * Extra rules that Pure adds on top of Normalize.css
+ */
+
+/**
+ * Always hide an element when it has the `hidden` HTML attribute.
+ */
+
+.hidden,
+[hidden] {
+ display: none !important;
+}
+
+/**
+ * Add this class to an image to make it fit within it's fluid parent wrapper
while maintaining
+ * aspect ratio.
+ */
+.pure-img {
+ max-width: 100%;
+ height: auto;
+ display: block;
+}
+
+/*csslint regex-selectors:false, known-properties:false,
duplicate-properties:false*/
+
+.pure-g {
+ letter-spacing: -0.31em; /* Webkit: collapse white-space between units */
+ *letter-spacing: normal; /* reset IE < 8 */
+ *word-spacing: -0.43em; /* IE < 8: collapse white-space between units */
+ text-rendering: optimizespeed; /* Webkit: fixes text-rendering:
optimizeLegibility */
+
+ /*
+ Sets the font stack to fonts known to work properly with the above letter
+ and word spacings. See: https://github.com/yahoo/pure/issues/41/
+
+ The following font stack makes Pure Grids work on all known environments.
+
+ * FreeSans: Ships with many Linux distros, including Ubuntu
+
+ * Arimo: Ships with Chrome OS. Arimo has to be defined before Helvetica and
+ Arial to get picked up by the browser, even though neither is available
+ in Chrome OS.
+
+ * Droid Sans: Ships with all versions of Android.
+
+ * Helvetica, Arial, sans-serif: Common font stack on OS X and Windows.
+ */
+ font-family: FreeSans, Arimo, "Droid Sans", Helvetica, Arial, sans-serif;
+
+ /* Use flexbox when possible to avoid `letter-spacing` side-effects. */
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-flex-flow: row wrap;
+ -ms-flex-flow: row wrap;
+ flex-flow: row wrap;
+
+ /* Prevents distributing space between rows */
+ -webkit-align-content: flex-start;
+ -ms-flex-line-pack: start;
+ align-content: flex-start;
+}
+
+/* IE10 display: -ms-flexbox (and display: flex in IE 11) does not work inside
a table; fall back to block and rely on font hack */
address@hidden all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
+ table .pure-g {
+ display: block;
+ }
+}
+
+/* Opera as of 12 on Windows needs word-spacing.
+ The ".opera-only" selector is used to prevent actual prefocus styling
+ and is not required in markup.
+*/
+.opera-only :-o-prefocus,
+.pure-g {
+ word-spacing: -0.43em;
+}
+
+.pure-u {
+ display: inline-block;
+ *display: inline; /* IE < 8: fake inline-block */
+ zoom: 1;
+ letter-spacing: normal;
+ word-spacing: normal;
+ vertical-align: top;
+ text-rendering: auto;
+}
+
+/*
+Resets the font family back to the OS/browser's default sans-serif font,
+this the same font stack that Normalize.css sets for the `body`.
+*/
+.pure-g [class *= "pure-u"] {
+ font-family: sans-serif;
+}
+
+.pure-u-1,
+.pure-u-1-1,
+.pure-u-1-2,
+.pure-u-1-3,
+.pure-u-2-3,
+.pure-u-1-4,
+.pure-u-3-4,
+.pure-u-1-5,
+.pure-u-2-5,
+.pure-u-3-5,
+.pure-u-4-5,
+.pure-u-5-5,
+.pure-u-1-6,
+.pure-u-5-6,
+.pure-u-1-8,
+.pure-u-3-8,
+.pure-u-5-8,
+.pure-u-7-8,
+.pure-u-1-12,
+.pure-u-5-12,
+.pure-u-7-12,
+.pure-u-11-12,
+.pure-u-1-24,
+.pure-u-2-24,
+.pure-u-3-24,
+.pure-u-4-24,
+.pure-u-5-24,
+.pure-u-6-24,
+.pure-u-7-24,
+.pure-u-8-24,
+.pure-u-9-24,
+.pure-u-10-24,
+.pure-u-11-24,
+.pure-u-12-24,
+.pure-u-13-24,
+.pure-u-14-24,
+.pure-u-15-24,
+.pure-u-16-24,
+.pure-u-17-24,
+.pure-u-18-24,
+.pure-u-19-24,
+.pure-u-20-24,
+.pure-u-21-24,
+.pure-u-22-24,
+.pure-u-23-24,
+.pure-u-24-24 {
+ display: inline-block;
+ *display: inline;
+ zoom: 1;
+ letter-spacing: normal;
+ word-spacing: normal;
+ vertical-align: top;
+ text-rendering: auto;
+}
+
+.pure-u-1-24 {
+ width: 4.1667%;
+ *width: 4.1357%;
+}
+
+.pure-u-1-12,
+.pure-u-2-24 {
+ width: 8.3333%;
+ *width: 8.3023%;
+}
+
+.pure-u-1-8,
+.pure-u-3-24 {
+ width: 12.5000%;
+ *width: 12.4690%;
+}
+
+.pure-u-1-6,
+.pure-u-4-24 {
+ width: 16.6667%;
+ *width: 16.6357%;
+}
+
+.pure-u-1-5 {
+ width: 20%;
+ *width: 19.9690%;
+}
+
+.pure-u-5-24 {
+ width: 20.8333%;
+ *width: 20.8023%;
+}
+
+.pure-u-1-4,
+.pure-u-6-24 {
+ width: 25%;
+ *width: 24.9690%;
+}
+
+.pure-u-7-24 {
+ width: 29.1667%;
+ *width: 29.1357%;
+}
+
+.pure-u-1-3,
+.pure-u-8-24 {
+ width: 33.3333%;
+ *width: 33.3023%;
+}
+
+.pure-u-3-8,
+.pure-u-9-24 {
+ width: 37.5000%;
+ *width: 37.4690%;
+}
+
+.pure-u-2-5 {
+ width: 40%;
+ *width: 39.9690%;
+}
+
+.pure-u-5-12,
+.pure-u-10-24 {
+ width: 41.6667%;
+ *width: 41.6357%;
+}
+
+.pure-u-11-24 {
+ width: 45.8333%;
+ *width: 45.8023%;
+}
+
+.pure-u-1-2,
+.pure-u-12-24 {
+ width: 50%;
+ *width: 49.9690%;
+}
+
+.pure-u-13-24 {
+ width: 54.1667%;
+ *width: 54.1357%;
+}
+
+.pure-u-7-12,
+.pure-u-14-24 {
+ width: 58.3333%;
+ *width: 58.3023%;
+}
+
+.pure-u-3-5 {
+ width: 60%;
+ *width: 59.9690%;
+}
+
+.pure-u-5-8,
+.pure-u-15-24 {
+ width: 62.5000%;
+ *width: 62.4690%;
+}
+
+.pure-u-2-3,
+.pure-u-16-24 {
+ width: 66.6667%;
+ *width: 66.6357%;
+}
+
+.pure-u-17-24 {
+ width: 70.8333%;
+ *width: 70.8023%;
+}
+
+.pure-u-3-4,
+.pure-u-18-24 {
+ width: 75%;
+ *width: 74.9690%;
+}
+
+.pure-u-19-24 {
+ width: 79.1667%;
+ *width: 79.1357%;
+}
+
+.pure-u-4-5 {
+ width: 80%;
+ *width: 79.9690%;
+}
+
+.pure-u-5-6,
+.pure-u-20-24 {
+ width: 83.3333%;
+ *width: 83.3023%;
+}
+
+.pure-u-7-8,
+.pure-u-21-24 {
+ width: 87.5000%;
+ *width: 87.4690%;
+}
+
+.pure-u-11-12,
+.pure-u-22-24 {
+ width: 91.6667%;
+ *width: 91.6357%;
+}
+
+.pure-u-23-24 {
+ width: 95.8333%;
+ *width: 95.8023%;
+}
+
+.pure-u-1,
+.pure-u-1-1,
+.pure-u-5-5,
+.pure-u-24-24 {
+ width: 100%;
+}
+.pure-button {
+ /* Structure */
+ display: inline-block;
+ zoom: 1;
+ line-height: normal;
+ white-space: nowrap;
+ vertical-align: middle;
+ text-align: center;
+ cursor: pointer;
+ -webkit-user-drag: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ box-sizing: border-box;
+}
+
+/* Firefox: Get rid of the inner focus border */
+.pure-button::-moz-focus-inner {
+ padding: 0;
+ border: 0;
+}
+
+/* Inherit .pure-g styles */
+.pure-button-group {
+ letter-spacing: -0.31em; /* Webkit: collapse white-space between units */
+ *letter-spacing: normal; /* reset IE < 8 */
+ *word-spacing: -0.43em; /* IE < 8: collapse white-space between units */
+ text-rendering: optimizespeed; /* Webkit: fixes text-rendering:
optimizeLegibility */
+}
+
+.opera-only :-o-prefocus,
+.pure-button-group {
+ word-spacing: -0.43em;
+}
+
+.pure-button-group .pure-button {
+ letter-spacing: normal;
+ word-spacing: normal;
+ vertical-align: top;
+ text-rendering: auto;
+}
+
+/*csslint outline-none:false*/
+
+.pure-button {
+ font-family: inherit;
+ font-size: 100%;
+ padding: 0.5em 1em;
+ color: #444; /* rgba not supported (IE 8) */
+ color: rgba(0, 0, 0, 0.80); /* rgba supported */
+ border: 1px solid #999; /*IE 6/7/8*/
+ border: none rgba(0, 0, 0, 0); /*IE9 + everything else*/
+ background-color: #E6E6E6;
+ text-decoration: none;
+ border-radius: 2px;
+}
+
+.pure-button-hover,
+.pure-button:hover,
+.pure-button:focus {
+ /* csslint ignore:start */
+ filter: alpha(opacity=90);
+ /* csslint ignore:end */
+ background-image: -webkit-linear-gradient(transparent, rgba(0,0,0, 0.05)
40%, rgba(0,0,0, 0.10));
+ background-image: linear-gradient(transparent, rgba(0,0,0, 0.05) 40%,
rgba(0,0,0, 0.10));
+}
+.pure-button:focus {
+ outline: 0;
+}
+.pure-button-active,
+.pure-button:active {
+ box-shadow: 0 0 0 1px rgba(0,0,0, 0.15) inset, 0 0 6px rgba(0,0,0, 0.20)
inset;
+ border-color: #000\9;
+}
+
+.pure-button[disabled],
+.pure-button-disabled,
+.pure-button-disabled:hover,
+.pure-button-disabled:focus,
+.pure-button-disabled:active {
+ border: none;
+ background-image: none;
+ /* csslint ignore:start */
+ filter: alpha(opacity=40);
+ /* csslint ignore:end */
+ opacity: 0.40;
+ cursor: not-allowed;
+ box-shadow: none;
+ pointer-events: none;
+}
+
+.pure-button-hidden {
+ display: none;
+}
+
+.pure-button-primary,
+.pure-button-selected,
+a.pure-button-primary,
+a.pure-button-selected {
+ background-color: rgb(0, 120, 231);
+ color: #fff;
+}
+
+/* Button Groups */
+.pure-button-group .pure-button {
+ margin: 0;
+ border-radius: 0;
+ border-right: 1px solid #111; /* fallback color for rgba() for IE7/8 */
+ border-right: 1px solid rgba(0, 0, 0, 0.2);
+
+}
+
+.pure-button-group .pure-button:first-child {
+ border-top-left-radius: 2px;
+ border-bottom-left-radius: 2px;
+}
+.pure-button-group .pure-button:last-child {
+ border-top-right-radius: 2px;
+ border-bottom-right-radius: 2px;
+ border-right: none;
+}
+
+/*csslint box-model:false*/
+/*
+Box-model set to false because we're setting a height on select elements, which
+also have border and padding. This is done because some browsers don't render
+the padding. We explicitly set the box-model for select elements to border-box,
+so we can ignore the csslint warning.
+*/
+
+.pure-form input[type="text"],
+.pure-form input[type="password"],
+.pure-form input[type="email"],
+.pure-form input[type="url"],
+.pure-form input[type="date"],
+.pure-form input[type="month"],
+.pure-form input[type="time"],
+.pure-form input[type="datetime"],
+.pure-form input[type="datetime-local"],
+.pure-form input[type="week"],
+.pure-form input[type="number"],
+.pure-form input[type="search"],
+.pure-form input[type="tel"],
+.pure-form input[type="color"],
+.pure-form select,
+.pure-form textarea {
+ padding: 0.5em 0.6em;
+ display: inline-block;
+ border: 1px solid #ccc;
+ box-shadow: inset 0 1px 3px #ddd;
+ border-radius: 4px;
+ vertical-align: middle;
+ box-sizing: border-box;
+}
+
+/*
+Need to separate out the :not() selector from the rest of the CSS 2.1 selectors
+since IE8 won't execute CSS that contains a CSS3 selector.
+*/
+.pure-form input:not([type]) {
+ padding: 0.5em 0.6em;
+ display: inline-block;
+ border: 1px solid #ccc;
+ box-shadow: inset 0 1px 3px #ddd;
+ border-radius: 4px;
+ box-sizing: border-box;
+}
+
+
+/* Chrome (as of v.32/34 on OS X) needs additional room for color to display.
*/
+/* May be able to remove this tweak as color inputs become more standardized
across browsers. */
+.pure-form input[type="color"] {
+ padding: 0.2em 0.5em;
+}
+
+
+.pure-form input[type="text"]:focus,
+.pure-form input[type="password"]:focus,
+.pure-form input[type="email"]:focus,
+.pure-form input[type="url"]:focus,
+.pure-form input[type="date"]:focus,
+.pure-form input[type="month"]:focus,
+.pure-form input[type="time"]:focus,
+.pure-form input[type="datetime"]:focus,
+.pure-form input[type="datetime-local"]:focus,
+.pure-form input[type="week"]:focus,
+.pure-form input[type="number"]:focus,
+.pure-form input[type="search"]:focus,
+.pure-form input[type="tel"]:focus,
+.pure-form input[type="color"]:focus,
+.pure-form select:focus,
+.pure-form textarea:focus {
+ outline: 0;
+ border-color: #129FEA;
+}
+
+/*
+Need to separate out the :not() selector from the rest of the CSS 2.1 selectors
+since IE8 won't execute CSS that contains a CSS3 selector.
+*/
+.pure-form input:not([type]):focus {
+ outline: 0;
+ border-color: #129FEA;
+}
+
+.pure-form input[type="file"]:focus,
+.pure-form input[type="radio"]:focus,
+.pure-form input[type="checkbox"]:focus {
+ outline: thin solid #129FEA;
+ outline: 1px auto #129FEA;
+}
+.pure-form .pure-checkbox,
+.pure-form .pure-radio {
+ margin: 0.5em 0;
+ display: block;
+}
+
+.pure-form input[type="text"][disabled],
+.pure-form input[type="password"][disabled],
+.pure-form input[type="email"][disabled],
+.pure-form input[type="url"][disabled],
+.pure-form input[type="date"][disabled],
+.pure-form input[type="month"][disabled],
+.pure-form input[type="time"][disabled],
+.pure-form input[type="datetime"][disabled],
+.pure-form input[type="datetime-local"][disabled],
+.pure-form input[type="week"][disabled],
+.pure-form input[type="number"][disabled],
+.pure-form input[type="search"][disabled],
+.pure-form input[type="tel"][disabled],
+.pure-form input[type="color"][disabled],
+.pure-form select[disabled],
+.pure-form textarea[disabled] {
+ cursor: not-allowed;
+ background-color: #eaeded;
+ color: #cad2d3;
+}
+
+/*
+Need to separate out the :not() selector from the rest of the CSS 2.1 selectors
+since IE8 won't execute CSS that contains a CSS3 selector.
+*/
+.pure-form input:not([type])[disabled] {
+ cursor: not-allowed;
+ background-color: #eaeded;
+ color: #cad2d3;
+}
+.pure-form input[readonly],
+.pure-form select[readonly],
+.pure-form textarea[readonly] {
+ background-color: #eee; /* menu hover bg color */
+ color: #777; /* menu text color */
+ border-color: #ccc;
+}
+
+.pure-form input:focus:invalid,
+.pure-form textarea:focus:invalid,
+.pure-form select:focus:invalid {
+ color: #b94a48;
+ border-color: #e9322d;
+}
+.pure-form input[type="file"]:focus:invalid:focus,
+.pure-form input[type="radio"]:focus:invalid:focus,
+.pure-form input[type="checkbox"]:focus:invalid:focus {
+ outline-color: #e9322d;
+}
+.pure-form select {
+ /* Normalizes the height; padding is not sufficient. */
+ height: 2.25em;
+ border: 1px solid #ccc;
+ background-color: white;
+}
+.pure-form select[multiple] {
+ height: auto;
+}
+.pure-form label {
+ margin: 0.5em 0 0.2em;
+}
+.pure-form fieldset {
+ margin: 0;
+ padding: 0.35em 0 0.75em;
+ border: 0;
+}
+.pure-form legend {
+ display: block;
+ width: 100%;
+ padding: 0.3em 0;
+ margin-bottom: 0.3em;
+ color: #333;
+ border-bottom: 1px solid #e5e5e5;
+}
+
+.pure-form-stacked input[type="text"],
+.pure-form-stacked input[type="password"],
+.pure-form-stacked input[type="email"],
+.pure-form-stacked input[type="url"],
+.pure-form-stacked input[type="date"],
+.pure-form-stacked input[type="month"],
+.pure-form-stacked input[type="time"],
+.pure-form-stacked input[type="datetime"],
+.pure-form-stacked input[type="datetime-local"],
+.pure-form-stacked input[type="week"],
+.pure-form-stacked input[type="number"],
+.pure-form-stacked input[type="search"],
+.pure-form-stacked input[type="tel"],
+.pure-form-stacked input[type="color"],
+.pure-form-stacked input[type="file"],
+.pure-form-stacked select,
+.pure-form-stacked label,
+.pure-form-stacked textarea {
+ display: block;
+ margin: 0.25em 0;
+}
+
+/*
+Need to separate out the :not() selector from the rest of the CSS 2.1 selectors
+since IE8 won't execute CSS that contains a CSS3 selector.
+*/
+.pure-form-stacked input:not([type]) {
+ display: block;
+ margin: 0.25em 0;
+}
+.pure-form-aligned input,
+.pure-form-aligned textarea,
+.pure-form-aligned select,
+/* NOTE: pure-help-inline is deprecated. Use .pure-form-message-inline
instead. */
+.pure-form-aligned .pure-help-inline,
+.pure-form-message-inline {
+ display: inline-block;
+ *display: inline;
+ *zoom: 1;
+ vertical-align: middle;
+}
+.pure-form-aligned textarea {
+ vertical-align: top;
+}
+
+/* Aligned Forms */
+.pure-form-aligned .pure-control-group {
+ margin-bottom: 0.5em;
+}
+.pure-form-aligned .pure-control-group label {
+ text-align: right;
+ display: inline-block;
+ vertical-align: middle;
+ width: 10em;
+ margin: 0 1em 0 0;
+}
+.pure-form-aligned .pure-controls {
+ margin: 1.5em 0 0 11em;
+}
+
+/* Rounded Inputs */
+.pure-form input.pure-input-rounded,
+.pure-form .pure-input-rounded {
+ border-radius: 2em;
+ padding: 0.5em 1em;
+}
+
+/* Grouped Inputs */
+.pure-form .pure-group fieldset {
+ margin-bottom: 10px;
+}
+.pure-form .pure-group input,
+.pure-form .pure-group textarea {
+ display: block;
+ padding: 10px;
+ margin: 0 0 -1px;
+ border-radius: 0;
+ position: relative;
+ top: -1px;
+}
+.pure-form .pure-group input:focus,
+.pure-form .pure-group textarea:focus {
+ z-index: 3;
+}
+.pure-form .pure-group input:first-child,
+.pure-form .pure-group textarea:first-child {
+ top: 1px;
+ border-radius: 4px 4px 0 0;
+ margin: 0;
+}
+.pure-form .pure-group input:first-child:last-child,
+.pure-form .pure-group textarea:first-child:last-child {
+ top: 1px;
+ border-radius: 4px;
+ margin: 0;
+}
+.pure-form .pure-group input:last-child,
+.pure-form .pure-group textarea:last-child {
+ top: -2px;
+ border-radius: 0 0 4px 4px;
+ margin: 0;
+}
+.pure-form .pure-group button {
+ margin: 0.35em 0;
+}
+
+.pure-form .pure-input-1 {
+ width: 100%;
+}
+.pure-form .pure-input-3-4 {
+ width: 75%;
+}
+.pure-form .pure-input-2-3 {
+ width: 66%;
+}
+.pure-form .pure-input-1-2 {
+ width: 50%;
+}
+.pure-form .pure-input-1-3 {
+ width: 33%;
+}
+.pure-form .pure-input-1-4 {
+ width: 25%;
+}
+
+/* Inline help for forms */
+/* NOTE: pure-help-inline is deprecated. Use .pure-form-message-inline
instead. */
+.pure-form .pure-help-inline,
+.pure-form-message-inline {
+ display: inline-block;
+ padding-left: 0.3em;
+ color: #666;
+ vertical-align: middle;
+ font-size: 0.875em;
+}
+
+/* Block help for forms */
+.pure-form-message {
+ display: block;
+ color: #666;
+ font-size: 0.875em;
+}
+
address@hidden only screen and (max-width : 480px) {
+ .pure-form button[type="submit"] {
+ margin: 0.7em 0 0;
+ }
+
+ .pure-form input:not([type]),
+ .pure-form input[type="text"],
+ .pure-form input[type="password"],
+ .pure-form input[type="email"],
+ .pure-form input[type="url"],
+ .pure-form input[type="date"],
+ .pure-form input[type="month"],
+ .pure-form input[type="time"],
+ .pure-form input[type="datetime"],
+ .pure-form input[type="datetime-local"],
+ .pure-form input[type="week"],
+ .pure-form input[type="number"],
+ .pure-form input[type="search"],
+ .pure-form input[type="tel"],
+ .pure-form input[type="color"],
+ .pure-form label {
+ margin-bottom: 0.3em;
+ display: block;
+ }
+
+ .pure-group input:not([type]),
+ .pure-group input[type="text"],
+ .pure-group input[type="password"],
+ .pure-group input[type="email"],
+ .pure-group input[type="url"],
+ .pure-group input[type="date"],
+ .pure-group input[type="month"],
+ .pure-group input[type="time"],
+ .pure-group input[type="datetime"],
+ .pure-group input[type="datetime-local"],
+ .pure-group input[type="week"],
+ .pure-group input[type="number"],
+ .pure-group input[type="search"],
+ .pure-group input[type="tel"],
+ .pure-group input[type="color"] {
+ margin-bottom: 0;
+ }
+
+ .pure-form-aligned .pure-control-group label {
+ margin-bottom: 0.3em;
+ text-align: left;
+ display: block;
+ width: 100%;
+ }
+
+ .pure-form-aligned .pure-controls {
+ margin: 1.5em 0 0 0;
+ }
+
+ /* NOTE: pure-help-inline is deprecated. Use .pure-form-message-inline
instead. */
+ .pure-form .pure-help-inline,
+ .pure-form-message-inline,
+ .pure-form-message {
+ display: block;
+ font-size: 0.75em;
+ /* Increased bottom padding to make it group with its related input
element. */
+ padding: 0.2em 0 0.8em;
+ }
+}
+
+/*csslint adjoining-classes: false, box-model:false*/
+.pure-menu {
+ box-sizing: border-box;
+}
+
+.pure-menu-fixed {
+ position: fixed;
+ left: 0;
+ top: 0;
+ z-index: 3;
+}
+
+.pure-menu-list,
+.pure-menu-item {
+ position: relative;
+}
+
+.pure-menu-list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.pure-menu-item {
+ padding: 0;
+ margin: 0;
+ height: 100%;
+}
+
+.pure-menu-link,
+.pure-menu-heading {
+ display: block;
+ text-decoration: none;
+ white-space: nowrap;
+}
+
+/* HORIZONTAL MENU */
+.pure-menu-horizontal {
+ width: 100%;
+ white-space: nowrap;
+}
+
+.pure-menu-horizontal .pure-menu-list {
+ display: inline-block;
+}
+
+/* Initial menus should be inline-block so that they are horizontal */
+.pure-menu-horizontal .pure-menu-item,
+.pure-menu-horizontal .pure-menu-heading,
+.pure-menu-horizontal .pure-menu-separator {
+ display: inline-block;
+ *display: inline;
+ zoom: 1;
+ vertical-align: middle;
+}
+
+/* Submenus should still be display: block; */
+.pure-menu-item .pure-menu-item {
+ display: block;
+}
+
+.pure-menu-children {
+ display: none;
+ position: absolute;
+ left: 100%;
+ top: 0;
+ margin: 0;
+ padding: 0;
+ z-index: 3;
+}
+
+.pure-menu-horizontal .pure-menu-children {
+ left: 0;
+ top: auto;
+ width: inherit;
+}
+
+.pure-menu-allow-hover:hover > .pure-menu-children,
+.pure-menu-active > .pure-menu-children {
+ display: block;
+ position: absolute;
+}
+
+/* Vertical Menus - show the dropdown arrow */
+.pure-menu-has-children > .pure-menu-link:after {
+ padding-left: 0.5em;
+ content: "\25B8";
+ font-size: small;
+}
+
+/* Horizontal Menus - show the dropdown arrow */
+.pure-menu-horizontal .pure-menu-has-children > .pure-menu-link:after {
+ content: "\25BE";
+}
+
+/* scrollable menus */
+.pure-menu-scrollable {
+ overflow-y: scroll;
+ overflow-x: hidden;
+}
+
+.pure-menu-scrollable .pure-menu-list {
+ display: block;
+}
+
+.pure-menu-horizontal.pure-menu-scrollable .pure-menu-list {
+ display: inline-block;
+}
+
+.pure-menu-horizontal.pure-menu-scrollable {
+ white-space: nowrap;
+ overflow-y: hidden;
+ overflow-x: auto;
+ -ms-overflow-style: none;
+ -webkit-overflow-scrolling: touch;
+ /* a little extra padding for this style to allow for scrollbars */
+ padding: .5em 0;
+}
+
+.pure-menu-horizontal.pure-menu-scrollable::-webkit-scrollbar {
+ display: none;
+}
+
+/* misc default styling */
+
+.pure-menu-separator,
+.pure-menu-horizontal .pure-menu-children .pure-menu-separator {
+ background-color: #ccc;
+ height: 1px;
+ margin: .3em 0;
+}
+
+.pure-menu-horizontal .pure-menu-separator {
+ width: 1px;
+ height: 1.3em;
+ margin: 0 .3em ;
+}
+
+/* Need to reset the separator since submenu is vertical */
+.pure-menu-horizontal .pure-menu-children .pure-menu-separator {
+ display: block;
+ width: auto;
+}
+
+.pure-menu-heading {
+ text-transform: uppercase;
+ color: #565d64;
+}
+
+.pure-menu-link {
+ color: #777;
+}
+
+.pure-menu-children {
+ background-color: #fff;
+}
+
+.pure-menu-link,
+.pure-menu-disabled,
+.pure-menu-heading {
+ padding: .5em 1em;
+}
+
+.pure-menu-disabled {
+ opacity: .5;
+}
+
+.pure-menu-disabled .pure-menu-link:hover {
+ background-color: transparent;
+}
+
+.pure-menu-active > .pure-menu-link,
+.pure-menu-link:hover,
+.pure-menu-link:focus {
+ background-color: #eee;
+}
+
+.pure-menu-selected .pure-menu-link,
+.pure-menu-selected .pure-menu-link:visited {
+ color: #000;
+}
+
+.pure-table {
+ /* Remove spacing between table cells (from Normalize.css) */
+ border-collapse: collapse;
+ border-spacing: 0;
+ empty-cells: show;
+ border: 1px solid #cbcbcb;
+}
+
+.pure-table caption {
+ color: #000;
+ font: italic 85%/1 arial, sans-serif;
+ padding: 1em 0;
+ text-align: center;
+}
+
+.pure-table td,
+.pure-table th {
+ border-left: 1px solid #cbcbcb;/* inner column border */
+ border-width: 0 0 0 1px;
+ font-size: inherit;
+ margin: 0;
+ overflow: visible; /*to make ths where the title is really long work*/
+ padding: 0.5em 1em; /* cell padding */
+}
+
+/* Consider removing this next declaration block, as it causes problems when
+there's a rowspan on the first cell. Case added to the tests. issue#432 */
+.pure-table td:first-child,
+.pure-table th:first-child {
+ border-left-width: 0;
+}
+
+.pure-table thead {
+ background-color: #e0e0e0;
+ color: #000;
+ text-align: left;
+ vertical-align: bottom;
+}
+
+/*
+striping:
+ even - #fff (white)
+ odd - #f2f2f2 (light gray)
+*/
+.pure-table td {
+ background-color: transparent;
+}
+.pure-table-odd td {
+ background-color: #f2f2f2;
+}
+
+/* nth-child selector for modern browsers */
+.pure-table-striped tr:nth-child(2n-1) td {
+ background-color: #f2f2f2;
+}
+
+/* BORDERED TABLES */
+.pure-table-bordered td {
+ border-bottom: 1px solid #cbcbcb;
+}
+.pure-table-bordered tbody > tr:last-child > td {
+ border-bottom-width: 0;
+}
+
+
+/* HORIZONTAL BORDERED TABLES */
+
+.pure-table-horizontal td,
+.pure-table-horizontal th {
+ border-width: 0 0 1px 0;
+ border-bottom: 1px solid #cbcbcb;
+}
+.pure-table-horizontal tbody > tr:last-child > td {
+ border-bottom-width: 0;
+}
diff --git a/talerbank/app/static/style.css b/talerbank/app/static/style.css
deleted file mode 100644
index c720186..0000000
--- a/talerbank/app/static/style.css
+++ /dev/null
@@ -1,158 +0,0 @@
-/*
- This file is part of GNU TALER.
- Copyright (C) 2014, 2015, 2016 INRIA
-
- TALER is free software; you can redistribute it and/or modify it under the
- terms of the GNU Lesser General Public License as published by the Free
Software
- Foundation; either version 2.1, or (at your option) any later version.
-
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
details.
-
- You should have received a copy of the GNU Lesser General Public License
along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-
- @author Marcello Stanisci
- @author Gabor Toth
-*/
-
-body {
- background-color: white;
- margin: 0;
- padding: 0;
- font-family: Verdana, sans;
-}
-
-header {
- width: 100%;
- height: 100px;
- margin: 0;
- padding: 0;
- border-bottom: 1px solid black;
-}
-
-header h1 {
- font-size: 200%;
- margin: 15 0 0 120px;
-/* position: relative;
- top: 50%;
- transform: translateY(-50%);*/
-}
-header #logo {
- float: left;
- width: 100px;
- height: 100px;
- padding: 0;
- margin: 0;
- text-align: center;
- border-right: 1px solid black;
-}
-
-section#menu {
- margin: 0 0 90 0;
- padding: 5px;
- border-right: 1px solid black;
- height: 100%;
- width: 90px;
- float: left;
-}
-
-section#main {
- margin: 0 0 0 100px;
- padding: 20px;
- border-left: 1px solid black;
- height: 100%;
- max-width: 40em;
-}
-
-section#main h1:first-child {
- margin-top: 0;
-}
-
-div.login-form, div.register-form {
- border-radius: 10px;
- background-color: #f2f2f2;
- padding: 11px 27px 44px 27px;
- max-width: 200px;
-}
-
-div.login-form input[type=submit],
-div.register-form input[type=submit] {
- width-max: 30%;
- float: right;
- padding: 6px;
-}
-
-.selected-item {
- border-style: solid;
- border-width: 1px;
-}
-
-.informational {
- border-radius: 8px;
- padding: 8px;
-}
-
-.informational-ok {
- background: #ccffcc;
-}
-
-.informational-fail {
- background: #ff8566;
-}
-
-div.login-form input[type=text],
-div.login-form input[type=password],
-div.register-form input[type=text],
-div.register-form input[type=password] {
- width: 100%;
- padding: 12px 20px;
- margin: 8px 0;
- display: inline-block;
- border: 1px solid #ccc;
- border-radius: 4px;
- box-sizing: border-box;
-}
-
-h1 {
- font-size: 160%;
-}
-
-h2 {
- font-size: 140%;
-}
-
-h3 {
- font-size: 120%;
-}
-
-h4, h5, h6 {
- font-size: 100%;
-}
-
-table.history {
- margin: 30px 0px;
- border-width: 0px;
- border-spacing: 3px;
- border-style: groove;
- border-color: gray;
- border-collapse: separate;
- background-color: white;
-}
-table.history th {
- border-width: 1px;
- padding: 5px;
- border-style: outset;
- border-color: gray;
- background-color: white;
- -moz-border-radius: ;
-}
-table.history td {
- border-width: 1px;
- padding: 5px;
- border-style: outset;
- border-color: gray;
- background-color: white;
- -moz-border-radius: ;
-}
diff --git a/talerbank/app/static/web-common b/talerbank/app/static/web-common
index caf5a98..d7e0135 160000
--- a/talerbank/app/static/web-common
+++ b/talerbank/app/static/web-common
@@ -1 +1 @@
-Subproject commit caf5a98114402d057ba08b14279eb8e46481a02c
+Subproject commit d7e013594d15388b1a7342a44a0e9c8d4ecca82d
diff --git a/talerbank/app/templates/Makefile.am
b/talerbank/app/templates/Makefile.am
index 34751ff..15aa5a8 100644
--- a/talerbank/app/templates/Makefile.am
+++ b/talerbank/app/templates/Makefile.am
@@ -3,10 +3,10 @@ SUBDIRS = .
EXTRA_DIST = \
base.html \
account_disabled.html \
- history.html \
profile_page.html \
public_accounts.html \
- error.html \
- home_page.html \
pin_tan.html \
- register.html
+ register.html \
+ login.html \
+ javascript.html \
+ error_exchange.html
diff --git a/talerbank/app/templates/account_disabled.html
b/talerbank/app/templates/account_disabled.html
index d2ebfb4..c84351c 100644
--- a/talerbank/app/templates/account_disabled.html
+++ b/talerbank/app/templates/account_disabled.html
@@ -1,21 +1,3 @@
-<!DOCTYPE html>
-<!--
- This file is part of GNU TALER.
- Copyright (C) 2014, 2015, 2016 INRIA
-
- TALER is free software; you can redistribute it and/or modify it under the
- terms of the GNU Lesser General Public License as published by the Free
Software
- Foundation; either version 2.1, or (at your option) any later version.
-
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
details.
-
- You should have received a copy of the GNU Lesser General Public License
along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-
- @author Marcello Stanisci
--->
{% extends "base.html" %}
{% block headermsg %}
@@ -27,7 +9,7 @@
<section id="main">
<article>
<h1>Account disabled</h1>
- <p>{{ name }}, your account has been disabled, <a href="{% url
'register' %}">register</a>
+ <p>{{ name }}, your account has been disabled, <a href="{%
url("register") %}">register</a>
a new one!</p>
</article>
</section>
diff --git a/talerbank/app/templates/base.html
b/talerbank/app/templates/base.html
index 3285dd6..c4d0efd 100644
--- a/talerbank/app/templates/base.html
+++ b/talerbank/app/templates/base.html
@@ -1,3 +1,4 @@
+<!doctype html>
<!--
This file is part of GNU TALER.
Copyright (C) 2014, 2015, 2016 INRIA
@@ -14,36 +15,41 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
@author Marcello Stanisci
+ @author Florian Dold
-->
-{% load static from mystatic %}
-
<html data-taler-nojs="true">
<head>
- <title>{{ currency }} Bank - Taler Demo</title>
- <link rel="stylesheet" type="text/css" href="{% static
"web-common/style.css" %}">
- <link rel="stylesheet" type="text/css" href="{% static "style.css" %}">
- <link rel="stylesheet" type="text/css" href="{% static
"web-common/taler-fallback.css" %}" id="taler-presence-stylesheet" />
- <link rel="stylesheet" type="text/css" href="{% static
"web-common/lang.css" %}">
- {% include "lang.html" %}
- <script src="{% static "web-common/lang.js" %}"
type="application/javascript"></script>
- <script src="{% static "web-common/taler-wallet-lib.js" %}"
type="application/javascript"></script>
+ <title>{{ settings_value("TALER_CURRENCY") }} Bank - Taler Demo</title>
+ <link rel="stylesheet" type="text/css" href="{{ static('pure.css') }}" />
+ <link rel="stylesheet" type="text/css" href="{{
static('web-common/demo.css') }}" />
+ <link rel="stylesheet" type="text/css" href="{{
static('web-common/taler-fallback.css') }}" id="taler-presence-stylesheet" />
+ <script src="{{ static('web-common/taler-wallet-lib.js') }}"
type="application/javascript"></script>
{% block head %} {% endblock %}
</head>
- <body class="en">
- <header>
- <a href="{% url "index" %}">
- <div id="logo">
- <svg height="100" width="100">
- <circle cx="50" cy="50" r="40" stroke="darkmagenta"
stroke-width="6" fill="white" />
- <text x="19" y="83" font-family="Verdana" font-size="90"
fill="darkmagenta">
- B
- </text>
- </svg>
- </div>
- </a>
+ <body>
+ <div class="demobar">
+ <h1><span class="tt adorn-brackets">Taler Demo</span></h1>
+ <h1><span class="it"><a href="{{ url('index') }}">Bank</a></span></h1>
+ <p>This part of the demo shows how a bank that supports Taler directly
would work. In addition to
+ using your own bank account, you can also see the transaction history of
some <a href="{{ url('public-accounts') }}">Public Accounts</a>.</p>
+ <p>Other parts of the demo:</p>
+ <ul>
+ <li><a href="{{ env('TALER_ENV_URL_INTRO', '#')
}}">Introduction</a></li>
+ <li><a href="{{ env('TALER_ENV_URL_BANK', '#') }}">Bank</a></li>
+ <li><a href="{{ env('TALER_ENV_URL_MERCHANT_BLOG', '#') }}">Essay
Shop</a></li>
+ <li><a href="{{ env('TALER_ENV_URL_MERCHANT_DONATIONS', '#')
}}">Donations</a></li>
+ </ul>
+ <p>You can learn more about Taler on our main <a
href="https://taler.net">website</a>.</p>
+ </div>
+ <div class="content">
{% block headermsg %} {% endblock %}
- </header>
- {% block content %} {% endblock %}
+ {% block content %} {% endblock %}
+ <div class="copyright">
+ <hr />
+ <p>Copyright © 2014—2017 INRIA</p>
+ <a href="{{ url('javascript') }}" data-jslicense="1"
class="jslicenseinfo">JavaScript license information</a>
+ </div>
+ </div>
</body>
</html>
diff --git a/talerbank/app/templates/login.html
b/talerbank/app/templates/login.html
index b9d6020..c86d4f0 100644
--- a/talerbank/app/templates/login.html
+++ b/talerbank/app/templates/login.html
@@ -1,5 +1,5 @@
-<!DOCTYPE html>
-<!--
+{% extends "base.html" %}
+{#
This file is part of GNU TALER.
Copyright (C) 2014, 2015, 2016 INRIA
@@ -16,22 +16,17 @@
@author Marcello Stanisci
@author Florian Dold
--->
-
-{% extends "base.html" %}
-{% load settings_value from settings %}
+#}
{% block headermsg %}
- <h1 lang="en" class="nav">Welcome to the {% settings_value "TALER_CURRENCY"
%} Bank!</h1>
+ <h1 class="nav">Welcome to the {{ settings_value("TALER_CURRENCY") }}
Bank!</h1>
{% endblock headermsg %}
{% block content %}
- <aside class="sidebar" id="left">
- </aside>
<section id="main">
<article>
<div class="login-form">
- <h1>Please login!</h1>
+ <h2>Please login!</h2>
{% if form.errors %}
<p class="informational informational-fail">
Your username and password didn't match. Please try again.
@@ -45,31 +40,29 @@
{% endif %}
{% if next %}
- {% if user.is_authenticated %}
+ {% if user.is_authenticated() %}
<p class="informational informational-fail">Your account doesn't
have access to this page. To proceed,
please login with an account that has access.</p>
{% else %}
<p>Please login to see this page.</p>
{% endif %}
{% endif %}
- <table>
- <form method="post" action="{% url 'login' %}">
- {% csrf_token %}
- {{ form.username }}
- <input type="password" name="password"
placeholder="password"></input>
- <input type="submit" value="login" />
- <input type="hidden" name="next" value="{{ next }}" />
- </form>
- </table>
+ <form method="post" class="pure-form" action="{{ url('login') }}">
+ <input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token
}}">
+ {{ form.username }}
+ <input type="password" name="password"
placeholder="password"></input>
+ <input type="submit" value="login" class="pure-button
pure-button-primary" />
+ <input type="hidden" name="next" value="{{ next }}" />
+ </form>
</div>
<p>
- If you are a new customer, please <a href="{% url 'register'
%}">register</a>.
+ If you are a new customer, please <a href="{{ url('register')
}}">register</a>.
Registration is fast and gratis, and it gives you a registration bonus
- of 100 {% settings_value "TALER_CURRENCY" %}!
+ of 100 {{ settings_value("TALER_CURRENCY") }}!
</p>
<p>
To view transactions of public accounts,
- please <a href="{% url "public-accounts" %}">click here</a>.
+ please <a href="{{ url('public-accounts') }}">click here</a>.
</p>
</article>
</section>
diff --git a/talerbank/app/templates/pin_tan.html
b/talerbank/app/templates/pin_tan.html
index 244cf9c..166118f 100644
--- a/talerbank/app/templates/pin_tan.html
+++ b/talerbank/app/templates/pin_tan.html
@@ -19,40 +19,32 @@
{% extends "base.html" %}
-{% load settings_value from settings %}
-
{% block headermsg %}
<h1 class="nav">PIN/TAN: Confirm transaction</h1>
{% endblock %}
{% block content %}
- <aside class="sidebar" id="left">
- </aside>
- <section id="main">
- <article>
- {% if previous_failed %}
- <p class="informational informational-fail">
- The captcha wasn't solved correctly. Please try again.
- </p>
- {% endif %}
- <p>
- {% settings_value "TALER_CURRENCY" %} Bank needs to verify that you
- intend to withdraw <b>{{ amount }} {% settings_value "TALER_CURRENCY"
%}</b> from
- <b>{{ exchange }}</b>.
- To prove that you are the account owner, please answer the
- following "security question" (*):
- </p>
- <form method="post" action="{% url "pin-verify" %}">
- {% csrf_token %}
- {{ form.pin }}
- <input type="hidden" name="question_url" value="{{
request.get_full_path }}"></input>
- <input type="submit" value="Ok"></input>
- </form>
- <small style="margin: 40px 0px">(*) A real bank should ask for
- a PIN/TAN instead of a simple calculation. For example by sending
- a one time password to the customer's mobile or providing her a
- random password generator.
- <small>
- </article>
- </section>
+ {% if previous_failed %}
+ <p class="informational informational-fail">
+ The captcha wasn't solved correctly. Please try again.
+ </p>
+ {% endif %}
+ <p>
+ {{ settings_value("TALER_CURRENCY") }} Bank needs to verify that you
+ intend to withdraw <b>{{ amount }} {{ settings_value("TALER_CURRENCY")
}}</b> from
+ <b>{{ exchange }}</b>.
+ To prove that you are the account owner, please answer the
+ following "security question" (*):
+ </p>
+ <form method="post" action="{{ url('pin-verify') }}">
+ <input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
+ {{ form.pin }}
+ <input type="hidden" name="question_url" value="{{ request.get_full_path
}}"></input>
+ <input type="submit" value="Ok"></input>
+ </form>
+ <small style="margin: 40px 0px">(*) A real bank should ask for
+ a PIN/TAN instead of a simple calculation. For example by sending
+ a one time password to the customer's mobile or providing her a
+ random password generator.
+ <small>
{% endblock content %}
diff --git a/talerbank/app/templates/profile_page.html
b/talerbank/app/templates/profile_page.html
index dd2ca67..1a016f1 100644
--- a/talerbank/app/templates/profile_page.html
+++ b/talerbank/app/templates/profile_page.html
@@ -1,5 +1,5 @@
-<!DOCTYPE html>
-<!--
+{% extends "base.html" %}
+{#
This file is part of GNU TALER.
Copyright (C) 2014, 2015, 2016 INRIA
@@ -15,47 +15,38 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
@author Marcello Stanisci
--->
-{% extends "base.html" %}
-{% load static from mystatic %}
+#}
{% block head %}
<meta name="currency" value="{{ currency }}">
<meta name="precision" value="{{ precision }}">
- <meta name="callback-url" value="{% url 'pin-question' %}">
+ <meta name="callback-url" value="{{ url('pin-question') }}">
{% if withdraw and withdraw == "success" %}
<meta name="reserve-pub" value="{{ reserve_pub }}">
{% endif %}
{% if suggested_exchange %}
<meta name="suggested-exchange" value="{{ suggested_exchange }}">
{% endif %}
- <link rel="stylesheet" type="text/css" href="{% static "disabled-button.css"
%}">
- <script src="{% static "chrome-store-link.js" %}"
type="application/javascript"></script>
- {% if use_js %}
- <script src="{% static "profile-page.js" %}"
type="application/javascript"></script>
- {% endif %}
+ <link rel="stylesheet" type="text/css" href="{{
static('disabled-button.css') }}">
+ <script src="{{ static('chrome-store-link.js') }}"
type="application/javascript"></script>
{% endblock head %}
{% block headermsg %}
<h1 class="nav">Welcome <em>{{ name }}</em>!</h1>
{% endblock headermsg %}
{% block content %}
<section id="menu">
- <a href="{% url 'logout' %}">[Logout]</a><br>
- <a href="{% url 'public-accounts' %}">[Public Accounts]</a><br>
- <p>
- Account:<br>
- # {{ account_no }}
- </p>
- <p>Current<br>
- balance:<br>
- <b>
- {{ balance }}</b> <br>
- {{ currency }}
- </p>
+ <a href="{{ url('logout') }}" class="pure-button">[Logout]</a><br>
+ <p>Account: # {{ account_no }}</p>
+ <p>Current balance: {{ balance }} {{ currency }}</p>
</section>
<section id="main">
<article>
<div class="notification">
+ {% if no_initial_bonus %}
+ <p class="informational informational-fail">
+ No initial bonus given, poor bank!
+ </p>
+ {% endif %}
{% if just_withdrawn %}
<p class="informational informational-ok">
Withdrawal approved!
@@ -89,56 +80,45 @@
<h2>Withdraw digital coins using Taler</h2>
<form id="reserve-form"
- {% if js == 'use_js' %}
- action=""
- {% else %}
- action="{% url 'withdraw-nojs' %}"
- method="post"
- {% endif %}
+ class="pure-form"
+ action="{{ url('withdraw-nojs') }}"
+ method="post"
name="tform">
- {% csrf_token %}
+ <input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token
}}">
Amount to withdraw:
<select id="reserve-amount" name="kudos_amount" autofocus>
- <option value="1.00 {{ currency }}">1.00 {{ currency }}</option>
- <option value="10.00 {{ currency }}">10.00 {{ currency
}}</option>
- <option value="15.00 {{ currency }}">15.00 {{ currency
}}</option>
- <option value="20.00 {{ currency }}">20.00 {{ currency
}}</option>
+ <option value="{{ currency }}:1">1.00 {{ currency }}</option>
+ <option value="{{ currency }}:10">10.00 {{ currency }}</option>
+ <option value="{{ currency }}:15">15.00 {{ currency }}</option>
+ <option value="{{ currency }}:20">20.00 {{ currency }}</option>
</select>
<input id="select-exchange"
- class="taler-installed-show"
- {% if use_js %}
- type="button"
- {% else %}
- type="submit"
- {% endif %}
+ class="taler-installed-show pure-button pure-button-primary"
+ type="submit"
value="Select exchange provider"></input>
- <input class="taler-installed-hide"
+ <input class="taler-installed-hide pure-button pure-button-primary"
type="button"
disabled
value="Select exchange provider"></input>
- </div>
</form>
</div>
<p>
- {% if use_js %}
- You're using the JavaScript version of the bank. You can <a href="{%
url 'profile' %}?use_js=false">switch</a> to the JS-free version.
- {% else %}
- You're using the JavaScript-free version of the bank. You can <a
href="{% url 'profile' %}?use_js=true">switch</a> to the JS version.
- {% endif %}
</p>
</article>
<article>
<h2>Transaction history</h2>
<div id="transactions-history">
{% if history %}
- <table class="history">
- <tbody>
+ <table class="pure-table">
+ <thead>
<tr>
<th style="text-align:center">Date</th>
<th style="text-align:center">Amount</th>
<th style="text-align:center">Counterpart</th>
<th style="text-align:center">Subject</th>
</tr>
+ </thead>
+ <tbody>
{% for item in history %}
<tr>
<td style="text-align:right">{{ item.date }}</td>
@@ -160,8 +140,4 @@
</article>
</section>
- <div class="copyright">
- <p>Copyright © 2014—2016 INRIA</p>
- <a href="{% url "javascript" %}" data-jslicense="1"
class="jslicenseinfo">JavaScript license information</a>
- </div>
{% endblock %}
diff --git a/talerbank/app/templates/public_accounts.html
b/talerbank/app/templates/public_accounts.html
index d6c2519..2f38489 100644
--- a/talerbank/app/templates/public_accounts.html
+++ b/talerbank/app/templates/public_accounts.html
@@ -1,5 +1,5 @@
-<!DOCTYPE html>
-<!--
+{% extends "base.html" %}
+{#
This file is part of GNU TALER.
Copyright (C) 2014, 2015, 2016 INRIA
@@ -15,67 +15,58 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
@author Marcello Stanisci
--->
-{% extends "base.html" %}
-
-{% load static from mystatic %}
+#}
{% block headermsg %}
<h1 class="nav">History of public accounts</h1>
{% endblock headermsg %}
{% block content %}
- <aside class="sidebar" id="left">
- </aside>
+ <a href="{{ url('index') }}">Back</a>
<section id="main">
<article>
- <table bgcolor="#E0E0E0" width="100%" width="100%" border="0"
cellpadding="2" cellspacing="1">
- <tr>
- {% for account in public_accounts %}
- <td width="12%" align="center">
- <a id="{{ account.user.username }}"
- href="{% url "public-accounts" name=account.user.username %}"
- {% if account.account_no == selected_account.number %}
- style="font-weight: bold"
- {% endif %}
- >
- {{ account.user.username }}
- </a>
- </td>
- {% endfor %}
- </tr>
- </table>
- <div id="transactions-history">
- {% if selected_account.history %}
- <table class="history">
- <tr>
- <th style="text-align:center">Date</th>
- <th style="text-align:center">Amount</th>
- <th style="text-align:center">Counterpart</th>
- <th style="text-align:center">Subject</th>
- </tr>
- {% for entry in selected_account.history %}
- <tr>
- <td style="text-align:right">{{entry.date}}</td>
- <td style="text-align:right">
- {{ entry.float_amount }} {{ entry.float_currency }}
- </td>
- <td style="text-align:left">{% if entry.counterpart_username %}
{{ entry.counterpart_username }} {% endif %} (account #{{ entry.counterpart
}})</td>
- <td style="text-align:left">
- {% if entry.counterpart_username %}
- <a name="{{ entry.subject }}"></a>
- <a href="public-accounts?account={{ entry.counterpart_username
}}#{{ entry.subject }}">{{ entry.subject }}</a>
- {% else %}
- {{ entry.subject }}
- {% endif %}
- </td>
- </tr>
- {% endfor %}
- {% else %}
- <p>No history for account #{{ selected_account.number }} ({{
selected_account.name}}) yet</p>
- {% endif %}
+ <div name="accountMenu" class="pure-menu pure-menu-horizontal">
+ <ul class="pure-menu-list">
+ {% for account in public_accounts %}
+ {% if account.account_no == selected_account.number %}
+ <li class="pure-menu-item pure-menu-selected">
+ {% else %}
+ <li class="pure-menu-item pure-menu">
+ {% endif %}
+ <a href="{{ url("public-accounts", name=account.user.username) }}"
class="pure-menu-link">
+ {{ account.user.username }}
+ </a>
+ </li>
+ {% endfor %}
+ </ul>
</div>
- </table>
+
+ {% if selected_account.history %}
+ <table class="pure-table pure-table-striped">
+ <thead>
+ <th>Date</th>
+ <th>Amount</th>
+ <th>Counterpart</th>
+ <th>Subject</th>
+ </thead>
+ <tbody>
+ {% for entry in selected_account.history %}
+ <tr>
+ <td>{{entry.date}}</td>
+ <td>
+ {{ entry.float_amount }} {{ entry.float_currency }}
+ </td>
+ <td>{% if entry.counterpart_username %} {{
entry.counterpart_username }} {% endif %} (account #{{ entry.counterpart
}})</td>
+ <td>
+ {{ entry.subject }}
+ </td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ {% else %}
+ <p>No history for account #{{ selected_account.number }} ({{
selected_account.name}}) yet</p>
+ {% endif %}
</article>
</section>
{% endblock content %}
diff --git a/talerbank/app/templates/register.html
b/talerbank/app/templates/register.html
index 20aa8af..509c689 100644
--- a/talerbank/app/templates/register.html
+++ b/talerbank/app/templates/register.html
@@ -1,4 +1,3 @@
-<!DOCTYPE html>
<!--
This file is part of GNU TALER.
Copyright (C) 2014, 2015, 2016 INRIA
@@ -18,10 +17,9 @@
-->
{% extends "base.html" %}
-{% load settings_value from settings %}
{% block headermsg %}
- <h1 class="nav">Register to the {% settings_value "TALER_CURRENCY" %}
bank!</h1>
+ <h1 class="nav">Register to the {{ settings_value('TALER_CURRENCY') }}
bank!</h1>
{% endblock headermsg %}
{% block content %}
@@ -29,6 +27,7 @@
</aside>
<section id="main">
<article>
+ <a href="{{ url('index') }}">Back</a>
<div class="notification">
{% if wrong %}
<p class="informational informational-fail">
@@ -47,13 +46,12 @@
<section id="main">
<article>
<div class="register-form">
- <h1 lang="en">Registration form</h1>
- <h1 lang="it">Form di registrazione</h1>
- <form method="post" action="{% url 'register' %}">
- {% csrf_token %}
+ <h1>Registration form</h1>
+ <form class="pure-form" method="post" action="{{ url('register') }}">
+ <input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token
}}">
<input type="text" name="username" placeholder="username"
autofocus></input>
<input type="password" name="password"
placeholder="password"></input>
- <input type="submit" value="Ok"></input>
+ <input type="submit" value="Ok" class="pure-button
pure-button-primary"></input>
</form>
</div>
</article>
diff --git a/talerbank/app/templatetags/__init__.py
b/talerbank/app/templatetags/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/talerbank/app/templatetags/mystatic.py
b/talerbank/app/templatetags/mystatic.py
deleted file mode 100644
index 2c57841..0000000
--- a/talerbank/app/templatetags/mystatic.py
+++ /dev/null
@@ -1,30 +0,0 @@
-from django import template
-from django.conf import settings
-from urllib.parse import urlparse
-from django.core.urlresolvers import get_script_prefix
-
-def is_absolute(url):
- return bool(urlparse(url).netloc)
-
-def join_urlparts(*parts):
- s = ""
- i = 0
- while i < len(parts):
- n = parts[i]
- i += 1
- if s.endswith("/"):
- n = n.lstrip("/")
- elif s and not n.startswith("/"):
- n = "/" + n
- s += n
- return s
-
-
-register = template.Library()
-
address@hidden(takes_context=True)
-def static(context, url):
- if is_absolute(url):
- return url
- request = context["request"]
- return join_urlparts(get_script_prefix(), settings.STATIC_URL, url)
diff --git a/talerbank/app/templatetags/settings.py
b/talerbank/app/templatetags/settings.py
deleted file mode 100644
index 08fb084..0000000
--- a/talerbank/app/templatetags/settings.py
+++ /dev/null
@@ -1,8 +0,0 @@
-import django.template
-from django.conf import settings
-
-register = django.template.Library()
-
address@hidden
-def settings_value(name):
- return getattr(settings, name, "")
diff --git a/talerbank/app/tests.py b/talerbank/app/tests.py
index 7b437f1..092d21c 100644
--- a/talerbank/app/tests.py
+++ b/talerbank/app/tests.py
@@ -18,8 +18,11 @@ from django.test import TestCase, Client
from django.core.urlresolvers import reverse
from django.conf import settings
from django.contrib.auth.models import User
-from .models import BankAccount
-from . import urlsadmin, urls
+from .models import BankAccount, BankTransaction
+from . import urls
+from . import amounts
+from .views import wire_transfer
+import json
import logging
@@ -28,6 +31,7 @@ logger = logging.getLogger(__name__)
def clearDb():
User.objects.all().delete()
BankAccount.objects.all().delete()
+ BankTransaction.objects.all().delete()
class RegisterTestCase(TestCase):
@@ -63,11 +67,9 @@ class LoginTestCase(TestCase):
currency=settings.TALER_CURRENCY)
user_account.save()
-
def tearDown(self):
clearDb()
-
def test_login(self):
c = Client()
response = c.post(reverse("login", urlconf=urls),
@@ -75,3 +77,192 @@ class LoginTestCase(TestCase):
"password": "test_password"},
follow=True)
self.assertIn(("/profile", 302), response.redirect_chain)
+
+
+class AmountTestCase(TestCase):
+
+ def test_cmp(self):
+ a1 = dict(value=1, fraction=0, currency="X")
+ _a1 = dict(value=1, fraction=0, currency="X")
+ a2 = dict(value=2, fraction=0, currency="X")
+ self.assertEqual(-1, amounts.amount_cmp(a1, a2))
+ self.assertEqual(1, amounts.amount_cmp(a2, a1))
+ self.assertEqual(0, amounts.amount_cmp(a1, _a1))
+
+class AddIncomingTestCase(TestCase):
+ """Test money transfer's API"""
+
+ def setUp(self):
+ bank = User.objects.create_user(username="bank_user",
+ password="bank_password")
+ bank_account = BankAccount(user=bank,
+ currency=settings.TALER_CURRENCY)
+ user = User.objects.create_user(username="user_user",
+ password="user_password")
+ user_account = BankAccount(user=user,
+ currency=settings.TALER_CURRENCY)
+ bank_account.save()
+ user_account.save()
+
+ def tearDown(self):
+ clearDb()
+
+ def test_add_incoming(self):
+ c = Client()
+ data = '{"auth": {"type": "basic"}, \
+ "credit_account": 1, \
+ "wtid": "TESTWTID", \
+ "exchange_url": "https://exchange.test", \
+ "amount": \
+ {"value": 1, \
+ "fraction": 0, \
+ "currency": "%s"}}' \
+ % settings.TALER_CURRENCY
+ response = c.post(reverse("add-incoming", urlconf=urls),
+ data=data,
+ content_type="application/json",
+ follow=True, **{"HTTP_X_TALER_BANK_USERNAME":
"user_user", "HTTP_X_TALER_BANK_PASSWORD": "user_password"})
+ self.assertEqual(200, response.status_code)
+ data = '{"auth": {"type": "basic"}, \
+ "credit_account": 1, \
+ "wtid": "TESTWTID", \
+ "exchange_url": "https://exchange.test", \
+ "amount": \
+ {"value": 1, \
+ "fraction": 0, \
+ "currency": "%s"}}' \
+ % "WRONGCURRENCY"
+ response = c.post(reverse("add-incoming", urlconf=urls),
+ data=data,
+ content_type="application/json",
+ follow=True, **{"HTTP_X_TALER_BANK_USERNAME":
"user_user", "HTTP_X_TALER_BANK_PASSWORD": "user_password"})
+ self.assertEqual(406, response.status_code)
+
+class HistoryTestCase(TestCase):
+
+ def setUp(self):
+ user = User.objects.create_user(username='User', password="Password")
+ ub = BankAccount(user=user, currency=settings.TALER_CURRENCY)
+ ub.account_no = 1
+ ub.balance_obj = dict(value=100, fraction=0,
currency=settings.TALER_CURRENCY)
+ ub.save()
+ user_passive = User.objects.create_user(username='UserP',
password="PasswordP")
+ ub_p = BankAccount(user=user_passive, currency=settings.TALER_CURRENCY)
+ ub_p.account_no = 2
+ ub_p.save()
+ wire_transfer(dict(value=1, fraction=0,
currency=settings.TALER_CURRENCY), ub, ub_p, subject="a")
+ wire_transfer(dict(value=1, fraction=0,
currency=settings.TALER_CURRENCY), ub, ub_p, subject="b")
+ wire_transfer(dict(value=1, fraction=0,
currency=settings.TALER_CURRENCY), ub, ub_p, subject="c")
+ wire_transfer(dict(value=1, fraction=0,
currency=settings.TALER_CURRENCY), ub, ub_p, subject="d")
+ wire_transfer(dict(value=1, fraction=0,
currency=settings.TALER_CURRENCY), ub, ub_p, subject="e")
+ wire_transfer(dict(value=1, fraction=0,
currency=settings.TALER_CURRENCY), ub, ub_p, subject="f")
+ wire_transfer(dict(value=1, fraction=0,
currency=settings.TALER_CURRENCY), ub, ub_p, subject="g")
+ wire_transfer(dict(value=1, fraction=0,
currency=settings.TALER_CURRENCY), ub, ub_p, subject="h")
+
+ def tearDown(self):
+ clearDb()
+
+ def test_history(self):
+ c = Client()
+
+ response = c.get(reverse("history", urlconf=urls), {"auth": "basic",
"delta": "+4"},
+ **{"HTTP_X_TALER_BANK_USERNAME": "User",
"HTTP_X_TALER_BANK_PASSWORD": "Password"})
+ self.assertEqual(200, response.status_code)
+
+ # Get a delta=+1 record in the middle of the list: FAILS
+ response = c.get(reverse("history", urlconf=urls), {"auth": "basic",
"delta": "+1", "start": "5"},
+ **{"HTTP_X_TALER_BANK_USERNAME": "User",
"HTTP_X_TALER_BANK_PASSWORD": "Password"})
+ data = response.content.decode("utf-8")
+ data = json.loads(data)
+ self.assertEqual(data["data"][0]["row_id"], 6)
+ # Get latest record
+ response = c.get(reverse("history", urlconf=urls), {"auth": "basic",
"delta": "-1"},
+ **{"HTTP_X_TALER_BANK_USERNAME": "User",
"HTTP_X_TALER_BANK_PASSWORD": "Password"})
+ data = response.content.decode("utf-8")
+ data = json.loads(data)
+ self.assertEqual(data["data"][0]["wt_subject"], "h")
+ # Get non-existent record: the latest plus one in the future:
transaction "h" takes row_id 11
+ response = c.get(reverse("history", urlconf=urls), {"auth": "basic",
"delta": "1", "start": "11"},
+ **{"HTTP_X_TALER_BANK_USERNAME": "User",
"HTTP_X_TALER_BANK_PASSWORD": "Password"})
+ response_txt = response.content.decode("utf-8")
+ self.assertEqual(204, response.status_code)
+ # Get credit records
+ response = c.get(reverse("history", urlconf=urls), {"auth": "basic",
"delta": "+1", "direction": "credit"},
+ **{"HTTP_X_TALER_BANK_USERNAME": "User",
"HTTP_X_TALER_BANK_PASSWORD": "Password"})
+ self.assertEqual(204, response.status_code)
+ # Get debit records
+ response = c.get(reverse("history", urlconf=urls), {"auth": "basic",
"delta": "+1", "direction": "debit"},
+ **{"HTTP_X_TALER_BANK_USERNAME": "User",
"HTTP_X_TALER_BANK_PASSWORD": "Password"})
+ self.assertNotEqual(204, response.status_code)
+ # Query about non-owned account
+ response = c.get(reverse("history", urlconf=urls), {"auth": "basic",
"delta": "+1", "account_number": 2},
+ **{"HTTP_X_TALER_BANK_USERNAME": "User",
"HTTP_X_TALER_BANK_PASSWORD": "Password"})
+ self.assertEqual(403, response.status_code)
+ # Query about non-existent account
+ response = c.get(reverse("history", urlconf=urls), {"auth": "basic",
"delta": "-1", "account_number": 9},
+ **{"HTTP_X_TALER_BANK_USERNAME": "User",
"HTTP_X_TALER_BANK_PASSWORD": "Password"})
+ self.assertEqual(404, response.status_code)
+
+
+# This tests whether a bank account goes red and then
+## goes green again
+class DebitTestCase(TestCase):
+
+ def setUp(self):
+ u = User.objects.create_user(username='U')
+ u0 = User.objects.create_user(username='U0')
+ ua = BankAccount(user=u, currency=settings.TALER_CURRENCY)
+ u0a = BankAccount(user=u0, currency=settings.TALER_CURRENCY)
+
+ ua.save()
+ u0a.save()
+
+ def test_green(self):
+ u = User.objects.get(username='U')
+ ub = BankAccount.objects.get(user=u)
+ self.assertEqual(False, ub.debit)
+
+ def test_red(self):
+ u = User.objects.get(username='U')
+ u0 = User.objects.get(username='U0')
+
+ ub = BankAccount.objects.get(user=u)
+ ub0 = BankAccount.objects.get(user=u0)
+
+ wire_transfer(dict(value=10, fraction=0,
currency=settings.TALER_CURRENCY),
+ ub0,
+ ub,
+ "Go green")
+ tmp = amounts.get_zero()
+ tmp["value"] = 10
+
+ self.assertEqual(0, amounts.amount_cmp(ub.balance_obj, tmp))
+ self.assertEqual(False, ub.debit)
+ self.assertEqual(True, ub0.debit)
+
+ wire_transfer(dict(value=11, fraction=0,
currency=settings.TALER_CURRENCY),
+ ub,
+ ub0,
+ "Go red")
+
+ self.assertEqual(True, ub.debit)
+ self.assertEqual(False, ub0.debit)
+
+ tmp["value"] = 1
+
+ self.assertEqual(0, amounts.amount_cmp(ub0.balance_obj, tmp))
+
+class TestParseAmount(TestCase):
+ def test_parse_amount(self):
+ ret = amounts.parse_amount("KUDOS:4")
+ self.assertJSONEqual('{"value": 4, "fraction": 0, "currency":
"KUDOS"}', json.dumps(ret))
+ ret = amounts.parse_amount("KUDOS:4.00")
+ self.assertJSONEqual('{"value": 4, "fraction": 0, "currency":
"KUDOS"}', json.dumps(ret))
+ ret = amounts.parse_amount("KUDOS:4.3")
+ self.assertJSONEqual('{"value": 4, "fraction": 30000000, "currency":
"KUDOS"}', json.dumps(ret))
+ try:
+ amounts.parse_amount("Buggy")
+ except amounts.BadFormatAmount:
+ return
+ # make sure the control doesn't get here
+ self.assertEqual(True, False)
diff --git a/talerbank/app/tests_admin.py b/talerbank/app/tests_admin.py
deleted file mode 100644
index 8adbfe1..0000000
--- a/talerbank/app/tests_admin.py
+++ /dev/null
@@ -1,62 +0,0 @@
-# This file is part of TALER
-# (C) 2014, 2015, 2016 INRIA
-#
-# TALER is free software; you can redistribute it and/or modify it under the
-# terms of the GNU Affero General Public License as published by the Free
Software
-# Foundation; either version 3, or (at your option) any later version.
-#
-# TALER is distributed in the hope that it will be useful, but WITHOUT ANY
-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
-# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-#
-# @author Marcello Stanisci
-
-from django.test import TestCase, Client
-from django.core.urlresolvers import reverse
-from django.conf import settings
-from django.contrib.auth.models import User
-from .models import BankAccount
-from . import urlsadmin, urls
-
-import logging
-
-logger = logging.getLogger(__name__)
-
-
-class AddIncomingTestCase(TestCase):
- """Test money transfer's API"""
-
- def setUp(self):
- bank = User.objects.create_user(username="bank_user",
- password="bank_password")
- bank_account = BankAccount(user=bank,
- currency=settings.TALER_CURRENCY)
- user = User.objects.create_user(username="user_user",
- password="user_password")
- user_account = BankAccount(user=user,
- currency=settings.TALER_CURRENCY)
- bank_account.save()
- user_account.save()
-
- def tearDown(self):
- User.objects.all().delete()
- BankAccount.objects.all().delete()
-
- def test_add_incoming(self):
- c = Client()
- data = '{"debit_account":1, \
- "credit_account":2, \
- "wtid":"TESTWTID", \
- "exchange_url":"https://exchange.test", \
- "amount": \
- {"value":1, \
- "fraction":0, \
- "currency":"%s"}}' \
- % settings.TALER_CURRENCY
- response = c.post(reverse("add-incoming", urlconf=urlsadmin),
- data=data,
- content_type="application/json",
- follow=True)
diff --git a/talerbank/app/tests_err.py b/talerbank/app/tests_err.py
new file mode 100644
index 0000000..26b1ae7
--- /dev/null
+++ b/talerbank/app/tests_err.py
@@ -0,0 +1,301 @@
+# This file is part of TALER
+# (C) 2014, 2015, 2016 INRIA
+#
+# TALER is free software; you can redistribute it and/or modify it under the
+# terms of the GNU Affero General Public License as published by the Free
Software
+# Foundation; either version 3, or (at your option) any later version.
+#
+# TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
+# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+#
+# @author Marcello Stanisci
+
+from django.test import TestCase, Client
+from django.core.urlresolvers import reverse
+from django.conf import settings
+from django.contrib.auth.models import User
+from .models import BankAccount, BankTransaction
+from . import urls
+from . import amounts
+from .views import wire_transfer
+import json
+
+import logging
+
+logger = logging.getLogger(__name__)
+
+def clearDb():
+ User.objects.all().delete()
+ BankAccount.objects.all().delete()
+ BankTransaction.objects.all().delete()
+
+
+class RegisterTestCase(TestCase):
+ """User registration"""
+
+ def setUp(self):
+ bank = User.objects.create_user(username='Bank')
+ # Activating this user with a faulty currency.
+ ba = BankAccount(user=bank, currency="XYZ")
+ ba.account_no = 1
+ ba.save()
+
+ def tearDown(self):
+ clearDb()
+
+ def test_register(self):
+ c = Client()
+ response = c.post(reverse("register", urlconf=urls),
+ {"username": "test_register",
+ "password": "test_register"},
+ follow=True)
+ # A currency mismatch is expected when the 100 KUDOS will be
+ # attempted to be given to the new user.
+ # This scenario expects a 500 Internal server error response to
+ # be returned.
+ self.assertEqual(500, response.status_code)
+
+
+class LoginTestCase(TestCase):
+ """User login"""
+
+ def setUp(self):
+ user = User.objects.create_user(username="test_user",
+ password="test_password")
+ user_account = BankAccount(user=user,
+ currency=settings.TALER_CURRENCY)
+ user_account.save()
+
+ def tearDown(self):
+ clearDb()
+
+ def test_login(self):
+ c = Client()
+ # Sending non existent credentials.
+ response = c.post(reverse("login", urlconf=urls),
+ {"username": "test_user",
+ "password": "test_passwordoo"},
+ follow=True)
+ # The current logic -- django's default -- returns 200 OK
+ # even when the login didn't succeed.
+ # So the test here ensures that the bank just doesn't crash.
+ self.assertEqual(200, response.status_code)
+
+
+class AmountTestCase(TestCase):
+
+ # Trying to compare amount of different currencies
+ def test_cmp(self):
+ a1 = dict(value=1, fraction=0, currency="X")
+ a2 = dict(value=2, fraction=0, currency="Y")
+ try:
+ amounts.amount_cmp(a1, a2)
+ except amounts.CurrencyMismatchException:
+ self.assertTrue(True)
+ return
+ # Should never get here
+ self.assertTrue(False)
+
+class AddIncomingTestCase(TestCase):
+ """Test money transfer's API"""
+
+ def setUp(self):
+ bank = User.objects.create_user(username="bank_user",
+ password="bank_password")
+ bank_account = BankAccount(user=bank,
+ currency=settings.TALER_CURRENCY)
+ user = User.objects.create_user(username="user_user",
+ password="user_password")
+ user_account = BankAccount(user=user,
+ currency=settings.TALER_CURRENCY)
+ bank_account.save()
+ user_account.save()
+
+ no1 = BankAccount.objects.get(account_no=1)
+ no2 = BankAccount.objects.get(account_no=2)
+ logger.info("1 username: %s" % no1.user.username)
+ logger.info("2 username: %s" % no2.user.username)
+
+ def tearDown(self):
+ clearDb()
+
+ def test_add_incoming(self):
+ c = Client()
+ data = '{"auth": {"type": "basic"}, \
+ "credit_account": 1, \
+ "wtid": "TESTWTID", \
+ "exchange_url": "https://exchange.test", \
+ "amount": \
+ {"value": 1, \
+ "fraction": 0, \
+ "currency": "%s"}}' \
+ % settings.TALER_CURRENCY
+ # Trying with wrong credentials
+ response = c.post(reverse("add-incoming", urlconf=urls),
+ data=data,
+ content_type="application/json",
+ follow=True, **{"HTTP_X_TALER_BANK_USERNAME":
"user_useroo", "HTTP_X_TALER_BANK_PASSWORD": "user_passwordoo"})
+ self.assertEqual(401, response.status_code)
+ data = '{"auth": {"type": "basic"}, \
+ "credit_account": 1, \
+ "wtid": "TESTWTID", \
+ "exchange_url": "https://exchange.test", \
+ "amount": \
+ {"value": 1, \
+ "fraction": 0, \
+ "currency": "%s"}}' \
+ % "WRONGCURRENCY"
+ # Trying with wrong currency
+ response = c.post(reverse("add-incoming", urlconf=urls),
+ data=data,
+ content_type="application/json",
+ follow=True, **{"HTTP_X_TALER_BANK_USERNAME":
"user_user", "HTTP_X_TALER_BANK_PASSWORD": "user_password"})
+ self.assertEqual(406, response.status_code)
+ # Trying to go debit
+ data = '{"auth": {"type": "basic"}, \
+ "credit_account": 1, \
+ "wtid": "TESTWTID", \
+ "exchange_url": "https://exchange.test", \
+ "amount": \
+ {"value": 60, \
+ "fraction": 0, \
+ "currency": "%s"}}' \
+ % settings.TALER_CURRENCY
+ response = c.post(reverse("add-incoming", urlconf=urls),
+ data=data,
+ content_type="application/json",
+ follow=True, **{"HTTP_X_TALER_BANK_USERNAME":
"user_user", "HTTP_X_TALER_BANK_PASSWORD": "user_password"})
+
+
+class HistoryTestCase(TestCase):
+
+ def setUp(self):
+ user = User.objects.create_user(username='User', password="Password")
+ ub = BankAccount(user=user, currency=settings.TALER_CURRENCY)
+ ub.account_no = 1
+ ub.balance_obj = dict(value=100, fraction=0,
currency=settings.TALER_CURRENCY)
+ ub.save()
+ user_passive = User.objects.create_user(username='UserP',
password="PasswordP")
+ ub_p = BankAccount(user=user_passive, currency=settings.TALER_CURRENCY)
+ ub_p.account_no = 2
+ ub_p.save()
+ wire_transfer(dict(value=1, fraction=0,
currency=settings.TALER_CURRENCY), ub, ub_p, subject="a")
+ wire_transfer(dict(value=1, fraction=0,
currency=settings.TALER_CURRENCY), ub, ub_p, subject="b")
+ wire_transfer(dict(value=1, fraction=0,
currency=settings.TALER_CURRENCY), ub, ub_p, subject="c")
+ wire_transfer(dict(value=1, fraction=0,
currency=settings.TALER_CURRENCY), ub, ub_p, subject="d")
+ wire_transfer(dict(value=1, fraction=0,
currency=settings.TALER_CURRENCY), ub, ub_p, subject="e")
+ wire_transfer(dict(value=1, fraction=0,
currency=settings.TALER_CURRENCY), ub, ub_p, subject="f")
+ wire_transfer(dict(value=1, fraction=0,
currency=settings.TALER_CURRENCY), ub, ub_p, subject="g")
+ wire_transfer(dict(value=1, fraction=0,
currency=settings.TALER_CURRENCY), ub, ub_p, subject="h")
+
+ def tearDown(self):
+ clearDb()
+
+ def test_history(self):
+ c = Client()
+
+ response = c.get(reverse("history", urlconf=urls), {"auth": "basic",
"delta": "+4"},
+ **{"HTTP_X_TALER_BANK_USERNAME": "User",
"HTTP_X_TALER_BANK_PASSWORD": "Password"})
+ self.assertEqual(200, response.status_code)
+
+ # Get a delta=+1 record in the middle of the list: FAILS
+ response = c.get(reverse("history", urlconf=urls), {"auth": "basic",
"delta": "+1", "start": "5"},
+ **{"HTTP_X_TALER_BANK_USERNAME": "User",
"HTTP_X_TALER_BANK_PASSWORD": "Password"})
+ data = response.content.decode("utf-8")
+ data = json.loads(data)
+ self.assertEqual(data["data"][0]["row_id"], 6)
+ # Get latest record
+ response = c.get(reverse("history", urlconf=urls), {"auth": "basic",
"delta": "-1"},
+ **{"HTTP_X_TALER_BANK_USERNAME": "User",
"HTTP_X_TALER_BANK_PASSWORD": "Password"})
+ data = response.content.decode("utf-8")
+ data = json.loads(data)
+ self.assertEqual(data["data"][0]["wt_subject"], "h")
+ # Get non-existent record: the latest plus one in the future:
transaction "h" takes row_id 11
+ response = c.get(reverse("history", urlconf=urls), {"auth": "basic",
"delta": "1", "start": "11"},
+ **{"HTTP_X_TALER_BANK_USERNAME": "User",
"HTTP_X_TALER_BANK_PASSWORD": "Password"})
+ response_txt = response.content.decode("utf-8")
+ self.assertEqual(204, response.status_code)
+ # Get credit records
+ response = c.get(reverse("history", urlconf=urls), {"auth": "basic",
"delta": "+1", "direction": "credit"},
+ **{"HTTP_X_TALER_BANK_USERNAME": "User",
"HTTP_X_TALER_BANK_PASSWORD": "Password"})
+ self.assertEqual(204, response.status_code)
+ # Get debit records
+ response = c.get(reverse("history", urlconf=urls), {"auth": "basic",
"delta": "+1", "direction": "debit"},
+ **{"HTTP_X_TALER_BANK_USERNAME": "User",
"HTTP_X_TALER_BANK_PASSWORD": "Password"})
+ self.assertNotEqual(204, response.status_code)
+ # Query about non-owned account
+ response = c.get(reverse("history", urlconf=urls), {"auth": "basic",
"delta": "+1", "account_number": 2},
+ **{"HTTP_X_TALER_BANK_USERNAME": "User",
"HTTP_X_TALER_BANK_PASSWORD": "Password"})
+ self.assertEqual(403, response.status_code)
+ # Query about non-existent account
+ response = c.get(reverse("history", urlconf=urls), {"auth": "basic",
"delta": "-1", "account_number": 9},
+ **{"HTTP_X_TALER_BANK_USERNAME": "User",
"HTTP_X_TALER_BANK_PASSWORD": "Password"})
+ self.assertEqual(404, response.status_code)
+
+
+# This tests whether a bank account goes red and then
+## goes green again
+class DebitTestCase(TestCase):
+
+ def setUp(self):
+ u = User.objects.create_user(username='U')
+ u0 = User.objects.create_user(username='U0')
+ ua = BankAccount(user=u, currency=settings.TALER_CURRENCY)
+ u0a = BankAccount(user=u0, currency=settings.TALER_CURRENCY)
+
+ ua.save()
+ u0a.save()
+
+ def test_green(self):
+ u = User.objects.get(username='U')
+ ub = BankAccount.objects.get(user=u)
+ self.assertEqual(False, ub.debit)
+
+ def test_red(self):
+ u = User.objects.get(username='U')
+ u0 = User.objects.get(username='U0')
+
+ ub = BankAccount.objects.get(user=u)
+ ub0 = BankAccount.objects.get(user=u0)
+
+ wire_transfer(dict(value=10, fraction=0,
currency=settings.TALER_CURRENCY),
+ ub0,
+ ub,
+ "Go green")
+ tmp = amounts.get_zero()
+ tmp["value"] = 10
+
+ self.assertEqual(0, amounts.amount_cmp(ub.balance_obj, tmp))
+ self.assertEqual(False, ub.debit)
+ self.assertEqual(True, ub0.debit)
+
+ wire_transfer(dict(value=11, fraction=0,
currency=settings.TALER_CURRENCY),
+ ub,
+ ub0,
+ "Go red")
+
+ self.assertEqual(True, ub.debit)
+ self.assertEqual(False, ub0.debit)
+
+ tmp["value"] = 1
+
+ self.assertEqual(0, amounts.amount_cmp(ub0.balance_obj, tmp))
+
+class TestParseAmount(TestCase):
+ def test_parse_amount(self):
+ ret = amounts.parse_amount("KUDOS:4")
+ self.assertJSONEqual('{"value": 4, "fraction": 0, "currency":
"KUDOS"}', json.dumps(ret))
+ ret = amounts.parse_amount("KUDOS:4.00")
+ self.assertJSONEqual('{"value": 4, "fraction": 0, "currency":
"KUDOS"}', json.dumps(ret))
+ ret = amounts.parse_amount("KUDOS:4.3")
+ self.assertJSONEqual('{"value": 4, "fraction": 30000000, "currency":
"KUDOS"}', json.dumps(ret))
+ try:
+ amounts.parse_amount("Buggy")
+ except amounts.BadFormatAmount:
+ return
+ # make sure the control doesn't get here
+ self.assertEqual(True, False)
diff --git a/talerbank/app/urls.py b/talerbank/app/urls.py
index 752dd6a..667366c 100644
--- a/talerbank/app/urls.py
+++ b/talerbank/app/urls.py
@@ -22,11 +22,13 @@ urlpatterns = [
url(r'^', include('talerbank.urls')),
url(r'^$', RedirectView.as_view(pattern_name="profile"), name="index"),
url(r'^favicon\.ico$', views.ignore),
- url(r'^javascript(.html)?/$', views.javascript_licensing,
name="javascript"),
+ url(r'^admin/add/incoming$', views.add_incoming, name="add-incoming"),
+ url(r'^javascript(?:.html)?/$', views.javascript_licensing,
name="javascript"),
url(r'^login/$', views.login_view, name="login"),
url(r'^logout/$', views.logout_view, name="logout"),
url(r'^accounts/register/$', views.register, name="register"),
url(r'^profile$', views.profile_page, name="profile"),
+ url(r'^history$', views.history, name="history"),
url(r'^withdraw$', views.withdraw_nojs, name="withdraw-nojs"),
url(r'^public-accounts$', views.public_accounts, name="public-accounts"),
url(r'^public-accounts/(?P<name>[a-zA-Z0-9 ]+)$', views.public_accounts,
name="public-accounts"),
diff --git a/talerbank/app/urlsadmin.py b/talerbank/app/urlsadmin.py
deleted file mode 100644
index e2e51f8..0000000
--- a/talerbank/app/urlsadmin.py
+++ /dev/null
@@ -1,23 +0,0 @@
-# This file is part of TALER
-# (C) 2014, 2015, 2016 INRIA
-#
-# TALER is free software; you can redistribute it and/or modify it under the
-# terms of the GNU Affero General Public License as published by the Free
Software
-# Foundation; either version 3, or (at your option) any later version.
-#
-# TALER is distributed in the hope that it will be useful, but WITHOUT ANY
-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
-# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-#
-# @author Marcello Stanisci
-
-from django.conf.urls import include, url
-from . import views
-
-urlpatterns = [
- url(r'^', include('talerbank.urls')),
- url(r'^admin/add/incoming$', views.add_incoming, name="add-incoming"),
- ]
diff --git a/talerbank/app/views.py b/talerbank/app/views.py
index d3b8428..3607de7 100644
--- a/talerbank/app/views.py
+++ b/talerbank/app/views.py
@@ -15,6 +15,7 @@
# @author Marcello Stanisci
# @author Florian Dold
+import re
import django.contrib.auth
import django.contrib.auth.views
import django.contrib.auth.forms
@@ -42,9 +43,13 @@ from .models import BankAccount, BankTransaction
logger = logging.getLogger(__name__)
+class DebtLimitExceededException(Exception):
+ pass
+class SameAccountException(Exception):
+ pass
class MyAuthenticationForm(django.contrib.auth.forms.AuthenticationForm):
- def __init__(self, *args, **kwargs):
+ def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["username"].widget.attrs["autofocus"] = True
self.fields["username"].widget.attrs["placeholder"] = "Username"
@@ -60,7 +65,10 @@ def javascript_licensing(request):
def login_view(request):
just_logged_out = get_session_flag(request, "just_logged_out")
response = django.contrib.auth.views.login(
- request, authentication_form=MyAuthenticationForm,
template_name="login.html")
+ request,
+ authentication_form=MyAuthenticationForm,
+ template_name="login.html",
+ extra_context={"user": request.user})
# sometimes the response is a redirect and not a template response
if hasattr(response, "context_data"):
response.context_data["just_logged_out"] = just_logged_out
@@ -83,33 +91,27 @@ def get_session_flag(request, name):
def profile_page(request):
just_withdrawn = get_session_flag(request, "just_withdrawn")
just_registered = get_session_flag(request, "just_registered")
+ no_initial_bonus = get_session_flag(request, "no_initial_bonus")
user_account = BankAccount.objects.get(user=request.user)
history = extract_history(user_account)
reserve_pub = request.session.get("reserve_pub")
- if "use_js" in request.GET:
- print("use_js is in GET as '{}'".format(request.GET["use_js"]))
- if request.GET["use_js"].lower() == "true":
- request.session["use_js"] = True
- else:
- request.session["use_js"] = False
- use_js = request.session.get("use_js", False)
context = dict(
name=user_account.user.username,
- balance=amounts.stringify(user_account.balance),
+ balance=amounts.stringify(amounts.floatify(user_account.balance_obj)),
currency=user_account.currency,
precision=settings.TALER_DIGITS,
account_no=user_account.account_no,
history=history,
just_withdrawn=just_withdrawn,
just_registered=just_registered,
- use_js=use_js,
+ no_initial_bonus=no_initial_bonus,
)
if settings.TALER_SUGGESTED_EXCHANGE:
context["suggested_exchange"] = settings.TALER_SUGGESTED_EXCHANGE
response = render(request, "profile_page.html", context)
- if just_withdrawn and not use_js:
+ if just_withdrawn:
response["X-Taler-Operation"] = "confirm-reserve"
response["X-Taler-Reserve-Pub"] = reserve_pub
response.status_code = 202
@@ -135,11 +137,20 @@ def pin_tan_question(request):
if param not in request.GET:
return HttpResponseBadRequest("parameter {} missing".format(param))
try:
- amount = {"value": int(request.GET["amount_value"]),
- "fraction": int(request.GET["amount_fraction"]),
- "currency": request.GET["amount_currency"]}
+ value = int(request.GET.get("amount_value", None))
+ except ValueError:
+ return HttpResponseBadRequest("invalid parameters: \"amount_value\"
not given or NaN")
+ try:
+ fraction = int(request.GET.get("amount_fraction", None))
except ValueError:
- return HttpResponseBadRequest("invalid parameters")
+ return HttpResponseBadRequest("invalid parameters: \"amount_fraction\"
not given or NaN")
+ try:
+ currency = request.GET.get("amount_currency", None)
+ except ValueError:
+ return HttpResponseBadRequest("invalid parameters: \"amount_currency\"
not given")
+ amount = {"value": value,
+ "fraction": fraction,
+ "currency": currency}
user_account = BankAccount.objects.get(user=request.user)
wiredetails = json.loads(request.GET["wire_details"])
if not isinstance(wiredetails, dict) or "test" not in wiredetails:
@@ -149,8 +160,8 @@ def pin_tan_question(request):
try:
schemas.validate_wiredetails(wiredetails)
schemas.validate_amount(amount)
- except ValueError:
- return HttpResponseBadRequest("invalid parameters")
+ except ValueError as error:
+ return HttpResponseBadRequest("invalid parameters (%s)" % error)
# parameters we store in the session are (more or less) validated
request.session["exchange_account_number"] =
wiredetails["test"]["account_number"]
request.session["amount"] = amount
@@ -200,12 +211,45 @@ def pin_tan_verify(request):
# This is not a withdraw session, we redirect the user to the
# profile page.
return redirect("profile")
- return create_reserve_at_exchange(request,
- amount,
- exchange_url,
- exchange_account_number,
- reserve_pub,
- sender_wiredetails)
+ try:
+ BankAccount.objects.get(account_no=exchange_account_number)
+ except BankAccount.DoesNotExist:
+ raise HttpResponseBadRequest("The bank account #{} of exchange {} does
not exist".format(exchange_account_no, exchange_url))
+ logging.info("asking exchange {} to create reserve
{}".format(exchange_url, reserve_pub))
+ json_body = dict(
+ reserve_pub=reserve_pub,
+ execution_date="/Date(" + str(int(time.time())) + ")/",
+ sender_account_details=sender_wiredetails,
+ # just something unique
+ transfer_details=dict(timestamp=int(time.time() * 1000)),
+ amount=amount,
+ )
+ user_account = BankAccount.objects.get(user=request.user)
+ exchange_account =
BankAccount.objects.get(account_no=exchange_account_number)
+ try:
+ wire_transfer(amount, user_account, exchange_account, reserve_pub)
+ except DebtLimitExceededException:
+ logger.warning("Withdrawal impossible due to debt limit exceeded")
+ request.session["debt_limit"] = True
+ return redirect("profile")
+ except amounts.BadFormatAmount as e:
+ return HttpResponse(e.msg, status=e.status_code)
+ except amounts.CurrencyMismatchException as e:
+ return HttpResponse(e.msg, status=e.status_code)
+ except SameAccountException:
+ logger.error("Odd situation: SameAccountException should NOT occur in
this function")
+ return HttpResponse("internal server error", status=500)
+
+ request_url = urljoin(exchange_url, "admin/add/incoming")
+ res = requests.post(request_url, json=json_body)
+ if res.status_code != 200:
+ return render(request, "error_exchange.html", dict(
+ message="Could not transfer funds to the exchange. The exchange
({}) gave a bad response.".format(exchange_url),
+ response_text=res.text,
+ response_status=res.status_code,
+ ))
+ request.session["just_withdrawn"] = True
+ return redirect("profile")
class UserReg(forms.Form):
@@ -232,7 +276,19 @@ def register(request):
user_account.save()
bank_internal_account = BankAccount.objects.get(account_no=1)
amount = dict(value=100, fraction=0, currency=settings.TALER_CURRENCY)
- wire_transfer(amount, bank_internal_account, user_account, "Joining bonus")
+ try:
+ wire_transfer(amount, bank_internal_account, user_account, "Joining
bonus")
+ except DebtLimitExceededException:
+ logger.info("Debt situation encountered")
+ request.session["no_initial_bonus"] = True
+ except amounts.CurrencyMismatchException as e:
+ return HttpResponse(e.msg, status=e.status_code)
+ except amounts.BadFormatAmount as e:
+ return HttpResponse(e.msg, status=e.status_code)
+ except SameAccountException:
+ logger.error("Odd situation: SameAccountException should NOT occur in
this function")
+ return HttpResponse("internal server error", status=500)
+
request.session["just_registered"] = True
user = django.contrib.auth.authenticate(username=username,
password=password)
django.contrib.auth.login(request, user)
@@ -260,7 +316,7 @@ def extract_history(account):
counterpart = item.credit_account
sign = -1
entry = dict(
- float_amount=amounts.stringify(item.amount * sign),
+ float_amount=amounts.stringify(amounts.floatify(item.amount_obj) *
sign),
float_currency=item.currency,
counterpart=counterpart.account_no,
counterpart_username=counterpart.user.username,
@@ -293,6 +349,108 @@ def public_accounts(request, name=None):
)
return render(request, "public_accounts.html", context)
address@hidden
+def history(request):
+ """
+ This API is used to get a list of transactions related to one user.
+ """
+ # login caller
+ user_account = auth_and_login(request)
+ if not user_account:
+ return JsonResponse(dict(error="authentication failed: bad credentials
OR auth method"),
+ status=401)
+ # delta
+ delta = request.GET.get("delta")
+ if not delta:
+ return HttpResponseBadRequest()
+ #FIXME: make the '+' sign optional
+ parsed_delta = re.search("([\+-])?([0-9]+)", delta)
+ try:
+ parsed_delta.group(0)
+ except AttributeError:
+ return JsonResponse(dict(error="Bad 'delta' parameter"), status=400)
+ delta = int(parsed_delta.group(2))
+ # start
+ start = request.GET.get("start")
+ if start:
+ start = int(start)
+
+ sign = parsed_delta.group(1)
+
+ if ("+" == sign) or (not sign):
+ sign = ""
+ # Assuming Q() means 'true'
+ sign_filter = Q()
+ if "-" == sign and start:
+ sign_filter = Q(id__lt=start)
+ elif "" == sign and start:
+ sign_filter = Q(id__gt=start)
+ # direction (debit/credit)
+ direction = request.GET.get("direction")
+
+ # target account
+ target_account = request.GET.get("account_number")
+ if not target_account:
+ target_account = user_account.bankaccount
+ else:
+ try:
+ target_account = BankAccount.objects.get(account_no=target_account)
+ except BankAccount.DoesNotExist:
+ logger.error("Attempted /history about non existent account")
+ return JsonResponse(dict(error="Queried account does not exist"),
status=404)
+
+ if target_account != user_account.bankaccount:
+ return JsonResponse(dict(error="Querying unowned accounts not
allowed"), status=403)
+
+ query_string = Q(debit_account=target_account) |
Q(credit_account=target_account)
+ history = []
+
+ if "credit" == direction:
+ query_string = Q(credit_account=target_account)
+ if "debit" == direction:
+ query_string = Q(debit_account=target_account)
+
+ qs = BankTransaction.objects.filter(query_string,
sign_filter).order_by("%sid" % sign)[:delta]
+ if 0 == qs.count():
+ return HttpResponse(status=204)
+ for entry in qs:
+ counterpart = entry.credit_account.account_no
+ sign_ = "-"
+ if entry.credit_account.account_no ==
user_account.bankaccount.account_no:
+ counterpart = entry.debit_account.account_no
+ sign_ = "+"
+ history.append(dict(counterpart=counterpart,
+ amount=entry.amount_obj,
+ sign=sign_,
+ wt_subject=entry.subject,
+ row_id=entry.id,
+ date="/Date(" + str(int(entry.date.timestamp())) +
")/"))
+ return JsonResponse(dict(data=history), status=200)
+
+
+def auth_and_login(request):
+ """Return user instance after checking authentication
+ credentials, False if errors occur"""
+
+ auth_type = None
+ if "POST" == request.method:
+ data = json.loads(request.body.decode("utf-8"))
+ auth_type = data["auth"]["type"]
+ if "GET" == request.method:
+ auth_type = request.GET.get("auth")
+
+ if "basic" != auth_type:
+ logger.error("auth method not supported")
+ return False
+
+ username = request.META.get("HTTP_X_TALER_BANK_USERNAME")
+ password = request.META.get("HTTP_X_TALER_BANK_PASSWORD")
+ # logger.info("Trying to log '%s/%s' in" % (username, password))
+ if not username or not password:
+ return False
+ return django.contrib.auth.authenticate(username=username,
+ password=password)
+
@csrf_exempt
@require_POST
@@ -304,32 +462,50 @@ def add_incoming(request):
This view is CSRF exempt, since it is not used from
within the browser, and only over the private admin interface.
"""
- logger.info("Handling /admin/add/incoming.")
data = json.loads(request.body.decode("utf-8"))
subject = "%s %s" % (data["wtid"], data["exchange_url"])
- logger.info("Submitting wire transfer: '%s'", subject)
try:
schemas.validate_incoming_request(data)
- except ValueError:
- return HttpResponseBadRequest()
+ except ValueError as error:
+ logger.error("Bad data POSTed: %s" % error)
+ return JsonResponse(dict(error="invalid data POSTed: %s" % error),
status=400)
+
+ user_account = auth_and_login(request)
+
+ if not user_account:
+ return JsonResponse(dict(error="authentication failed"),
+ status=401)
+
try:
- debit_account = user_account =
BankAccount.objects.get(user=data["debit_account"])
- credit_account = user_account =
BankAccount.objects.get(user=data["credit_account"])
+ credit_account = BankAccount.objects.get(user=data["credit_account"])
except BankAccount.DoesNotExist:
return HttpResponse(status=404)
- wire_transfer(data["amount"],
- debit_account,
- credit_account,
- subject)
- return JsonResponse({"outcome": "ok"}, status=200)
-
+ try:
+ transaction = wire_transfer(data["amount"],
+ user_account.bankaccount,
+ credit_account,
+ subject)
+ return JsonResponse(dict(serial_id=transaction.id,
timestamp="/Date(%s)/" % int(transaction.date.timestamp())))
+ except amounts.BadFormatAmount as e:
+ return JsonResponse(dict(error=e.msg), status=e.status_code)
+ except SameAccountException:
+ return JsonResponse(dict(error="debit and credit account are the
same"), status=422)
+ except DebtLimitExceededException:
+ return JsonResponse(dict(error="debit count has reached its debt
limit", status=403 ),
+ status=403)
+ except amounts.CurrencyMismatchException as e:
+ return JsonResponse(dict(error=e.msg), status=e.status_code)
@login_required
@require_POST
def withdraw_nojs(request):
- amount = amounts.parse_amount(request.POST.get("kudos_amount", ""))
- if amount is None:
+
+ try:
+ amount = amounts.parse_amount(request.POST.get("kudos_amount", ""))
+ except amounts.BadFormatAmount:
+ logger.error("Amount did not pass parsing")
return HttpResponseBadRequest()
+
response = HttpResponse(status=202)
response["X-Taler-Operation"] = "create-reserve"
response["X-Taler-Callback-Url"] = reverse("pin-question")
@@ -340,58 +516,85 @@ def withdraw_nojs(request):
return response
-def create_reserve_at_exchange(request,
- amount,
- exchange_url,
- exchange_account_no,
- reserve_pub,
- sender_account_details):
- try:
- BankAccount.objects.get(account_no=exchange_account_no)
- except BankAccount.DoesNotExist:
- raise HttpResponseBadRequest("The bank account #{} of exchange {} does
not exist".format(exchange_account_no, exchange_url))
- logging.info("asking exchange {} to create reserve
{}".format(exchange_url, reserve_pub))
- json_body = dict(
- reserve_pub=reserve_pub,
- execution_date="/Date(" + str(int(time.time())) + ")/",
- sender_account_details=sender_account_details,
- # just something unique
- transfer_details=dict(timestamp=int(time.time() * 1000)),
- amount=amount,
- )
- request_url = urljoin(exchange_url, "admin/add/incoming")
- res = requests.post(request_url, json=json_body)
- if res.status_code != 200:
- return render(request, "error_exchange.html", dict(
- message="Could not transfer funds to the exchange. The exchange
({}) gave a bad response.".format(exchange_url),
- response_text=res.text,
- response_status=res.status_code,
- ))
- user_account = BankAccount.objects.get(user=request.user)
- exchange_account = BankAccount.objects.get(account_no=exchange_account_no)
- # Build subject including Exchange base URL here.
- logger.info("Reserve data: '%s'" % json.dumps(res.json()))
- wire_transfer(amount, user_account, exchange_account, reserve_pub)
- request.session["just_withdrawn"] = True
- return redirect("profile")
-
-
def wire_transfer(amount,
debit_account,
credit_account,
subject):
if debit_account.pk == credit_account.pk:
- return
- float_amount = amounts.floatify(amount)
- transaction_item = BankTransaction(amount=float_amount,
+ logger.error("Debit and credit account are the same!")
+ raise SameAccountException()
+
+ transaction_item = BankTransaction(amount_value=amount["value"],
+ amount_fraction=amount["fraction"],
currency=amount["currency"],
credit_account=credit_account,
debit_account=debit_account,
subject=subject)
- debit_account.balance -= float_amount
- credit_account.balance += float_amount
+
+ try:
+ if debit_account.debit:
+ debit_account.balance_obj =
amounts.amount_add(debit_account.balance_obj,
+ amount)
+
+ elif -1 == amounts.amount_cmp(debit_account.balance_obj, amount):
+ debit_account.debit = True
+ debit_account.balance_obj = amounts.amount_sub(amount,
+
debit_account.balance_obj)
+ else:
+ debit_account.balance_obj =
amounts.amount_sub(debit_account.balance_obj,
+ amount)
+
+ if False == credit_account.debit:
+ credit_account.balance_obj =
amounts.amount_add(credit_account.balance_obj,
+ amount)
+
+ elif 1 == amounts.amount_cmp(amount, credit_account.balance_obj):
+ credit_account.debit = False
+ credit_account.balance_obj = amounts.amount_sub(amount,
+
credit_account.balance_obj)
+ else:
+ credit_account.balance_obj =
amounts.amount_sub(credit_account.balance_obj,
+ amount)
+ except amounts.CurrencyMismatchException:
+ msg = "The amount to be transferred (%s) doesn't match the bank's
currency (%s)" % (amount["currency"], settings.TALER_CURRENCY)
+ status_code = 406
+ if settings.TALER_CURRENCY != credit_account.balance_obj["currency"]:
+ logger.error("Internal inconsistency: credit account's currency
(%s) differs from bank's (%s)" % (credit_account.balance_obj["currency"],
settings.TALER_CURRENCY))
+ msg = "Internal server error"
+ status_code = 500
+ elif settings.TALER_CURRENCY != debit_account.balance_obj["currency"]:
+ logger.error("Internal inconsistency: debit account's currency
(%s) differs from bank's (%s)" % (debit_account.balance_obj["currency"],
settings.TALER_CURRENCY))
+ msg = "Internal server error"
+ status_code = 500
+ logger.error(msg)
+ raise amounts.CurrencyMismatchException(msg=msg,
status_code=status_code)
+
+ # Check here if any account went beyond the allowed
+ # debit threshold.
+
+ try:
+ threshold = amounts.parse_amount(settings.TALER_MAX_DEBT)
+
+ if debit_account.user.username == "Bank":
+ threshold = amounts.parse_amount(settings.TALER_MAX_DEBT_BANK)
+ except amounts.BadFormatAmount:
+ logger.error("MAX_DEBT|MAX_DEBT_BANK had the wrong format")
+ raise amounts.BadFormatAmount(msg="internal server error",
status_code=500)
+
+ try:
+ if 1 == amounts.amount_cmp(debit_account.balance_obj, threshold) \
+ and 0 != amounts.amount_cmp(amounts.get_zero(), threshold) \
+ and debit_account.debit:
+ logger.error("Negative balance '%s' not allowed." %
json.dumps(debit_account.balance_obj))
+ logger.info("%s's threshold is: '%s'." %
(debit_account.user.username, json.dumps(threshold)))
+ raise DebtLimitExceededException()
+ except amounts.CurrencyMismatchException:
+ logger.error("(Internal) currency mismatch between debt threshold and
debit account")
+ raise amounts.CurrencyMismatchException(msg="internal server error",
status_code=500)
with transaction.atomic():
debit_account.save()
credit_account.save()
transaction_item.save()
+
+ return transaction_item
diff --git a/talerbank/jinja2.py b/talerbank/jinja2.py
new file mode 100644
index 0000000..9169a93
--- /dev/null
+++ b/talerbank/jinja2.py
@@ -0,0 +1,74 @@
+# This file is part of TALER
+# (C) 2017 INRIA
+#
+# TALER is free software; you can redistribute it and/or modify it under the
+# terms of the GNU Affero General Public License as published by the Free
Software
+# Foundation; either version 3, or (at your option) any later version.
+#
+# TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
+# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+#
+# @author Florian Dold
+
+from django.contrib.staticfiles.storage import staticfiles_storage
+from django.core.urlresolvers import reverse
+from django.conf import settings
+from django.core.urlresolvers import get_script_prefix
+from urllib.parse import urlparse
+from jinja2 import Environment
+import os
+
+
+def is_absolute(url):
+ return bool(urlparse(url).netloc)
+
+
+def join_urlparts(*parts):
+ s = ""
+ i = 0
+ while i < len(parts):
+ n = parts[i]
+ i += 1
+ if s.endswith("/"):
+ n = n.lstrip("/")
+ elif s and not n.startswith("/"):
+ n = "/" + n
+ s += n
+ return s
+
+
+def static(url):
+ if is_absolute(url):
+ return url
+ return join_urlparts(get_script_prefix(), settings.STATIC_URL, url)
+
+
+def settings_value(name):
+ return getattr(settings, name, "")
+
+
+def url(*args, **kwargs):
+ # strangely, Django's 'reverse' function
+ # takes a named parameter 'kwargs' instead
+ # of real kwargs.
+ return reverse(*args, kwargs=kwargs)
+
+
+def env_get(name, default=None):
+ return os.environ.get(name, default)
+
+
+def environment(**options):
+ env = Environment(**options)
+ env.globals.update({
+ 'static': static,
+ 'url': url,
+ 'settings_value': settings_value,
+ 'env': env_get,
+ })
+ return env
+
diff --git a/talerbank/settings.py b/talerbank/settings.py
index 6542acf..6bbbc46 100644
--- a/talerbank/settings.py
+++ b/talerbank/settings.py
@@ -1,2 +1,196 @@
-from talerbank.settings_base import *
+"""
+Django settings for talerbank.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/1.9/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/1.9/ref/settings/
+"""
+
+import os
+import logging
+import base64
+from .talerconfig import TalerConfig
+import sys
+import urllib.parse
+import re
+
+logger = logging.getLogger(__name__)
+
+logger.info("DJANGO_SETTINGS_MODULE: %s" %
os.environ.get("DJANGO_SETTINGS_MODULE"))
+
+tc = TalerConfig.from_file(os.environ.get("TALER_CONFIG_FILE"))
+
+# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
+BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/
+
+
+SECRET_KEY = os.environ.get("TALER_BANK_SECRET_KEY", None)
+
+if not SECRET_KEY:
+ logging.info("secret key not configured in TALER_BANK_SECRET_KEY env
variable, generating random secret")
+ SECRET_KEY = base64.b64encode(os.urandom(32)).decode('utf-8')
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+ALLOWED_HOSTS = ["*"]
+
+LOGIN_URL = "login"
+
+LOGIN_REDIRECT_URL = "index"
+
+
+# Application definition
+
+INSTALLED_APPS = [
+ 'django.contrib.admin',
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.messages',
+ 'django.contrib.staticfiles',
+ 'talerbank.app'
+]
+
+MIDDLEWARE_CLASSES = [
+ 'django.middleware.security.SecurityMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.common.CommonMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+]
+
+TEMPLATES = [
+ {
+ 'BACKEND': 'django.template.backends.jinja2.Jinja2',
+ 'DIRS': [os.path.join(BASE_DIR, "talerbank/app/static/web-common/"),
+ os.path.join(BASE_DIR, "talerbank/app/templates")],
+ 'OPTIONS': {
+ 'environment': 'talerbank.jinja2.environment',
+ },
+ },
+]
+
+# Disable those, since they don't work with
+# jinja2 anyways.
+TEMPLATE_CONTEXT_PROCESSORS = []
+
+WSGI_APPLICATION = 'talerbank.wsgi.application'
+
+
+# Database
+# https://docs.djangoproject.com/en/1.9/ref/settings/#databases
+
+DATABASES = {}
+
+dbname = tc.value_string("bank", "database", required=True)
+# db given in cli argument takes precedence over config
+dbname = os.environ.get("TALER_BANK_ALTDB", dbname)
+
+if not dbname:
+ raise Exception("DB not specified (neither in config or as cli argument)")
+
+logger.info("dbname: %s" % dbname)
+
+check_dbstring_format = re.search("[a-z]+:///[a-z]+", dbname)
+if not check_dbstring_format:
+ logger.error("Bad db string given, respect the format 'dbtype:///dbname'")
+ sys.exit(1)
+
+dbconfig = {}
+db_url = urllib.parse.urlparse(dbname)
+
+
+if ((db_url.scheme not in ("postgres")) or ("" == db_url.scheme)):
+ logger.error("DB '%s' is not supported" % db_url.scheme)
+ sys.exit(1)
+if db_url.scheme == "postgres":
+ dbconfig["ENGINE"] = 'django.db.backends.postgresql_psycopg2'
+ dbconfig["NAME"] = db_url.path.lstrip("/")
+
+if not db_url.netloc:
+ p = urllib.parse.parse_qs(db_url.query)
+ if ("host" not in p) or len(p["host"]) == 0:
+ host = None
+ else:
+ host = p["host"][0]
+else:
+ host = db_url.netloc
+
+if host:
+ dbconfig["HOST"] = host
+
+DATABASES["default"] = dbconfig
+
+# Password validation
+# https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+ {
+ 'NAME':
'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+ },
+ {
+ 'NAME':
'django.contrib.auth.password_validation.MinimumLengthValidator',
+ },
+ {
+ 'NAME':
'django.contrib.auth.password_validation.CommonPasswordValidator',
+ },
+ {
+ 'NAME':
'django.contrib.auth.password_validation.NumericPasswordValidator',
+ },
+]
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/1.9/topics/i18n/
+
+LANGUAGE_CODE = 'en-us'
+
+TIME_ZONE = 'UTC'
+
+USE_I18N = True
+
+USE_L10N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/1.9/howto/static-files/
+
+STATIC_URL = '/static/'
+
+STATICFILES_DIRS = [
+ os.path.join(BASE_DIR, "talerbank/app/static"),
+]
+
+# Currently we don't use "collectstatic", so this value isn't used.
+# Instead, we serve static files directly from the installed python package
+# via the "django.contrib.staticfiles" app.
+# We must set it to something valid though, # or django will give us warnings.
+STATIC_ROOT = '/tmp/talerbankstatic/'
+
ROOT_URLCONF = "talerbank.app.urls"
+
+TALER_CURRENCY = tc.value_string("taler", "currency", required=True)
+
+TALER_MAX_DEBT = tc.value_string("bank", "MAX_DEBT",
+ default="%s:50" % TALER_CURRENCY)
+
+TALER_MAX_DEBT_BANK = tc.value_string("bank", "MAX_DEBT_BANK",
+ default="%s:0" % TALER_CURRENCY)
+
+TALER_DIGITS = 2
+TALER_PREDEFINED_ACCOUNTS = ['Tor', 'GNUnet', 'Taler', 'FSF', 'Tutorial']
+TALER_EXPECTS_DONATIONS = ['Tor', 'GNUnet', 'Taler', 'FSF']
+TALER_SUGGESTED_EXCHANGE = tc.value_string("bank", "suggested_exchange")
+
+logging.info("currency: '%s'", TALER_CURRENCY)
diff --git a/talerbank/settings_admin.py b/talerbank/settings_admin.py
deleted file mode 100644
index b2ce6a3..0000000
--- a/talerbank/settings_admin.py
+++ /dev/null
@@ -1,2 +0,0 @@
-from talerbank.settings_base import *
-ROOT_URLCONF = "talerbank.app.urlsadmin"
diff --git a/talerbank/settings_base.py b/talerbank/settings_base.py
deleted file mode 100644
index f92d9f3..0000000
--- a/talerbank/settings_base.py
+++ /dev/null
@@ -1,183 +0,0 @@
-"""
-Django settings for talerbank.
-
-For more information on this file, see
-https://docs.djangoproject.com/en/1.9/topics/settings/
-
-For the full list of settings and their values, see
-https://docs.djangoproject.com/en/1.9/ref/settings/
-"""
-
-import os
-import logging
-import base64
-from .talerconfig import TalerConfig
-import sys
-import urllib.parse
-
-logger = logging.getLogger(__name__)
-
-logger.info("DJANGO_SETTINGS_MODULE: %s" %
os.environ.get("DJANGO_SETTINGS_MODULE"))
-
-tc = TalerConfig.from_file(os.environ.get("TALER_CONFIG_FILE"))
-
-# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
-BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
-
-# Quick-start development settings - unsuitable for production
-# See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/
-
-
-SECRET_KEY = os.environ.get("TALER_BANK_SECRET_KEY", None)
-
-if not SECRET_KEY:
- logging.info("secret key not configured in TALER_BANK_SECRET_KEY env
variable, generating random secret")
- SECRET_KEY = base64.b64encode(os.urandom(32)).decode('utf-8')
-
-# SECURITY WARNING: don't run with debug turned on in production!
-DEBUG = True
-
-ALLOWED_HOSTS = ["*"]
-
-LOGIN_URL = "login"
-
-LOGIN_REDIRECT_URL = "index"
-
-
-# Application definition
-
-INSTALLED_APPS = [
- 'django.contrib.admin',
- 'django.contrib.auth',
- 'django.contrib.contenttypes',
- 'django.contrib.sessions',
- 'django.contrib.messages',
- 'django.contrib.staticfiles',
- 'talerbank.app'
-]
-
-MIDDLEWARE_CLASSES = [
- 'django.middleware.security.SecurityMiddleware',
- 'django.contrib.sessions.middleware.SessionMiddleware',
- 'django.middleware.common.CommonMiddleware',
- 'django.middleware.csrf.CsrfViewMiddleware',
- 'django.contrib.auth.middleware.AuthenticationMiddleware',
- 'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
- 'django.contrib.messages.middleware.MessageMiddleware',
- 'django.middleware.clickjacking.XFrameOptionsMiddleware',
-]
-
-TEMPLATES = [
- {
- 'BACKEND': 'django.template.backends.django.DjangoTemplates',
- 'DIRS': [os.path.join(BASE_DIR, "talerbank/app/static/web-common/")],
- 'APP_DIRS': True,
- 'OPTIONS': {
- 'context_processors': [
- 'django.template.context_processors.debug',
- 'django.template.context_processors.request',
- 'django.contrib.auth.context_processors.auth',
- 'django.contrib.messages.context_processors.messages',
- ],
- },
- },
-]
-
-WSGI_APPLICATION = 'talerbank.wsgi.application'
-
-
-# Database
-# https://docs.djangoproject.com/en/1.9/ref/settings/#databases
-
-DATABASES = {}
-
-# parse a database URL, django can't natively do this!
-dbname = tc.value_string("bank", "database", required=False)
-dbconfig = {}
-if dbname:
- db_url = urllib.parse.urlparse(dbname)
- if db_url.scheme != "postgres":
- raise Exception("only postgres db is supported ('{}' not
understood)".format(dbname))
- dbconfig['ENGINE'] = 'django.db.backends.postgresql_psycopg2'
- dbconfig['NAME'] = db_url.path.lstrip("/")
-
- if not db_url.netloc:
- p = urllib.parse.parse_qs(db_url.query)
- if ("host" not in p) or len(p["host"]) == 0:
- host = None
- else:
- host = p["host"][0]
- else:
- host = db_url.netloc
-
- if host:
- dbconfig["HOST"] = host
-
- logger.info("db string '%s'", dbname)
- logger.info("db info '%s'", dbconfig)
-
- DATABASES["default"] = dbconfig
-else:
- DATABASES["default"] = {
- 'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
- }
-
-
-# Password validation
-# https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators
-
-AUTH_PASSWORD_VALIDATORS = [
- {
- 'NAME':
'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
- },
- {
- 'NAME':
'django.contrib.auth.password_validation.MinimumLengthValidator',
- },
- {
- 'NAME':
'django.contrib.auth.password_validation.CommonPasswordValidator',
- },
- {
- 'NAME':
'django.contrib.auth.password_validation.NumericPasswordValidator',
- },
-]
-
-
-# Internationalization
-# https://docs.djangoproject.com/en/1.9/topics/i18n/
-
-LANGUAGE_CODE = 'en-us'
-
-TIME_ZONE = 'UTC'
-
-USE_I18N = True
-
-USE_L10N = True
-
-USE_TZ = True
-
-
-# Static files (CSS, JavaScript, Images)
-# https://docs.djangoproject.com/en/1.9/howto/static-files/
-
-STATIC_URL = '/static/'
-
-STATICFILES_DIRS = [
- os.path.join(BASE_DIR, "talerbank/app/static"),
-]
-
-# Currently we don't use "collectstatic", so this value isn't used.
-# Instead, we serve static files directly from the installed python package
-# via the "django.contrib.staticfiles" app.
-# We must set it to something valid though, # or django will give us warnings.
-STATIC_ROOT = '/tmp/talerbankstatic/'
-
-
-
-TALER_CURRENCY = tc.value_string("taler", "currency", required=True)
-TALER_DIGITS = 2
-TALER_PREDEFINED_ACCOUNTS = ['Tor', 'GNUnet', 'Taler', 'FSF', 'Tutorial']
-TALER_EXPECTS_DONATIONS = ['Tor', 'GNUnet', 'Taler', 'FSF']
-TALER_SUGGESTED_EXCHANGE = tc.value_string("bank", "suggested_exchange")
-
-logging.info("currency: '%s'", TALER_CURRENCY)
--
To stop receiving notification emails like this one, please contact
address@hidden
[Prev in Thread] |
Current Thread |
[Next in Thread] |
- [GNUnet-SVN] [taler-bank] branch stable updated (1bae352 -> 1238da4),
gnunet <=