[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[GNUnet-SVN] [taler-bank] branch stable updated (e3d5117 -> 860d826)
From: |
gnunet |
Subject: |
[GNUnet-SVN] [taler-bank] branch stable updated (e3d5117 -> 860d826) |
Date: |
Mon, 05 Feb 2018 10:56:06 +0100 |
This is an automated email from the git hooks/post-receive script.
marcello pushed a change to branch stable
in repository bank.
from e3d5117 don't autocomplete captcha
add a7ec665 tolerating fraction-less amount strings
add 7795cbb add fields to indicate whether a transaction is cancelled,
and if it reimburses another one.
add 2cf4730 Done with /reject logic.
add 1eef242 new migrations
add 8e1bd9c striking cancelled wire transfers' subject lines through.
add 77b7a1d striking canceled transactions in public history too
add 5475349 pretty amount
add 3a53b2d being nice with small screens
add 98d4ef4 adding /history's 'cancelled' argument
add 2045df5 nullifying rejected transactions without generating a
reimbursing transaction.
add f1e7799 remove obsolete instructions from README
add 5ae7269 remove obsolete information from README
add 79a16b1 remove obsolete information from README
add 52e913b fix install-dev
add 6b9df03 remove leftover from previous (wrong) /reject semantics
add ec0d72c do not trigger /admin/add/incoming at the exchange upon
withdrawal.
add a58a34b be nice with small screens; add Survey account; make account
generating logic shorter.
add 82c2e5b migration
add 071f971 returning error codes
add f54e7ad adjust wire transfer form
add 97bea4a adjust test case to last change
add ecb3a19 autocompleting input field
add 9db3307 remove artificial line striker
add 4e1f605 Implementing #5222.
add cb0ec0a UI fixes due to #5222.
add 64d9a87 Catching "db not found" error from the launcher.
add 88bef48 test launcher against db not found, plus being nice with
small screens.
add a7f0006 also accepting database connection strings of the form
postgres:///dbname?host=/path/to/sockets/dir/.
add d12ec88 exporting PYTHONPATH to run tests
add 3fd2bd8 set PYTHONPATH only for tests
add 8f51ca5 error codes are not taken from settings
add b630970 script installation path directive
add b228b4c fix path retrieval per-user-compliant
add c85ea8f typo
add e3c2420 lint tests
add af81a32 linting everything but (almost) unavoidable things
add 5e612a3 fix typo in just_withdrawn
add febbea7 typo / accidental paste
add b61c342 better class name
add acdef0d Closing #5149.
add 544b1f0 history extracting logic goes in one point, and /history
calls it now.
add 3562cf0 set the state to implement the "see next page" feature useful
when an account's history is too long.
add 1a22709 make /<page_number> available
add 9d4586a implement page numbers
add beb6279 fixing the back-and-forth arrows to navigate multiple pages
/public-history results.
add 8d9993d fix 1-page long histories
add 91abe5e fix off-by-one /history error
add ce5cc25 handle 0-lenght histories as of pagination
add c794683 syntax
add 1e1e187 better formatting "dump db" command output
add 8035958 Adding tool to perform wire transfers manually - the Web
server isn't required to run in order to have wire transfers effective.
add d129b85 rename uri to url
add 860d826 adapting to new amount format <curr>:x.y
No new revisions were added by this update.
Summary of changes:
Makefile.am | 87 ++-
README | 23 +-
bank-check-alt-baddb.conf | 17 -
configure.ac | 3 +
install-dev.py.in | 4 +-
setup.py | 3 +-
taler-bank-manage.in | 15 +-
talerbank/app/amount.py | 23 +-
talerbank/app/management/commands/dump_talerdb.py | 32 +-
talerbank/app/management/commands/helpers.py | 24 -
.../app/management/commands/provide_accounts.py | 53 +-
talerbank/app/management/commands/wire_transfer.py | 80 +++
talerbank/app/middleware.py | 65 ++
.../migrations/0011_banktransaction_reimburses.py | 19 +
.../app/migrations/0012_auto_20171212_1540.py | 19 +
...y => 0013_remove_banktransaction_reimburses.py} | 8 +-
talerbank/app/models.py | 63 +-
talerbank/app/schemas.py | 121 +++-
talerbank/app/static/bank.css | 41 ++
talerbank/app/templates/base.html | 2 +-
talerbank/app/templates/login.html | 9 +-
talerbank/app/templates/pin_tan.html | 4 +-
talerbank/app/templates/profile_page.html | 67 +-
talerbank/app/templates/public_accounts.html | 89 ++-
talerbank/app/templates/register.html | 1 +
talerbank/app/tests.py | 455 +++++++++----
talerbank/app/tests_alt.py | 4 +-
talerbank/app/urls.py | 36 +-
talerbank/app/views.py | 720 +++++++++++----------
talerbank/settings.py | 96 +--
30 files changed, 1362 insertions(+), 821 deletions(-)
delete mode 100644 bank-check-alt-baddb.conf
delete mode 100644 talerbank/app/management/commands/helpers.py
create mode 100644 talerbank/app/management/commands/wire_transfer.py
create mode 100644 talerbank/app/middleware.py
create mode 100644 talerbank/app/migrations/0011_banktransaction_reimburses.py
create mode 100644 talerbank/app/migrations/0012_auto_20171212_1540.py
copy talerbank/app/migrations/{0005_remove_banktransaction_currency.py =>
0013_remove_banktransaction_reimburses.py} (53%)
diff --git a/Makefile.am b/Makefile.am
index fdea99d..012f7b2 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -30,25 +30,88 @@ pkgdata_DATA = \
install-dev:
@$(PYTHON) ./install-dev.py
+env:
+ @export DJANGO_SETTINGS_MODULE="talerbank.settings" \
+ TALER_PREFIX="@prefix@" \
+ TALER_CONFIG_FILE="bank-check.conf" && bash
+FMT="\n\n%s\n%s\n"
+SEPARATOR=`printf "%s%s" \
+ "----------------------------------------------" \
+ "------------------------"`
+PYTHONPATH_APPEND=`printf "%s" \
+ "@prefix@/lib/address@hidden@/site-packages"`
+
check:
- @export DJANGO_SETTINGS_MODULE="talerbank.settings"
TALER_PREFIX="@prefix@" TALER_CONFIG_FILE="bank-check.conf" && python3 -m
django test --no-input talerbank.app.tests
- @printf --
"\n\n----------------------------------------------------------------------\nTesting
against non existent config file\n\n"
- @export DJANGO_SETTINGS_MODULE="talerbank.settings"
TALER_PREFIX="@prefix@" TALER_CONFIG_FILE="non-existent.conf" && python3 -m
django test --no-input talerbank.app.tests ; test 3 = $$?
- @printf --
"\n\n----------------------------------------------------------------------\nTesting
against bad db string\n\n"
- @export DJANGO_SETTINGS_MODULE="talerbank.settings"
TALER_PREFIX="@prefix@" TALER_CONFIG_FILE="bank-check-alt-baddb.conf" &&
python3 -m django test --no-input
talerbank.app.tests_alt.BadDatabaseStringTestCase ; test 2 = $$?
- @printf --
"\n\n----------------------------------------------------------------------\nTesting
against bad amount\n\n"
- @export DJANGO_SETTINGS_MODULE="talerbank.settings"
TALER_PREFIX="@prefix@" TALER_CONFIG_FILE="bank-check-alt-badamount.conf" &&
python3 -m django test --no-input
talerbank.app.tests_alt.BadMaxDebtOptionTestCase
- @printf --
"\n\n----------------------------------------------------------------------\nTesting
against no currency in config\n\n"
- @export TALER_BASE_CONFIG="/tmp"
DJANGO_SETTINGS_MODULE="talerbank.settings" TALER_PREFIX="@prefix@"
TALER_CONFIG_FILE="bank-check-alt-nocurrency.conf" && python3 -m django test
--no-input talerbank.app.tests_alt.NoCurrencyOptionTestCase ; test 3 = $$?
+ @export DJANGO_SETTINGS_MODULE="talerbank.settings" \
+ TALER_PREFIX="@prefix@" \
+ TALER_CONFIG_FILE="bank-check.conf" \
+ PYTHONPATH="${PYTHONPATH_APPEND}:${PYTHONPATH}" \
+ && python3 -m django test --no-input talerbank.app.tests
+ @printf ${FMT} ${SEPARATOR} \
+ "Testing against non existent config file"
+ @export DJANGO_SETTINGS_MODULE="talerbank.settings" \
+ TALER_PREFIX="@prefix@" \
+ TALER_CONFIG_FILE="non-existent.conf" \
+ PYTHONPATH="${PYTHONPATH_APPEND}:${PYTHONPATH}" \
+ && python3 \
+ -m django test \
+ --no-input talerbank.app.tests ; \
+ test 3 = $$?
+ @printf ${FMT} ${SEPARATOR} \
+ "Testing against bad db string"
+ @export DJANGO_SETTINGS_MODULE="talerbank.settings" \
+ TALER_PREFIX="@prefix@" \
+ TALER_BANK_ALTDB="bad db string" \
+ PYTHONPATH="${PYTHONPATH_APPEND}:${PYTHONPATH}" \
+ && python3 \
+ -m django test \
+ --no-input \
+ talerbank.app.tests_alt.BadDatabaseStringTestCase ; \
+ test 2 = $$?
+ @printf ${FMT} ${SEPARATOR} \
+ "Testing against bad amount"
+ @export DJANGO_SETTINGS_MODULE="talerbank.settings" \
+ TALER_PREFIX="@prefix@" \
+ TALER_CONFIG_FILE="bank-check-alt-badamount.conf" \
+ PYTHONPATH="${PYTHONPATH_APPEND}:${PYTHONPATH}" \
+ && python3 \
+ -m django test \
+ --no-input \
+ talerbank.app.tests_alt.BadMaxDebitOptionTestCase
+ @printf ${FMT} ${SEPARATOR} \
+ "Testing against no currency in config"
+ @export TALER_BASE_CONFIG="/tmp" \
+ DJANGO_SETTINGS_MODULE="talerbank.settings" \
+ TALER_PREFIX="@prefix@" \
+ TALER_CONFIG_FILE="bank-check-alt-nocurrency.conf" \
+ PYTHONPATH="${PYTHONPATH_APPEND}:${PYTHONPATH}" \
+ && python3 \
+ -m django test \
+ --no-input \
+ talerbank.app.tests_alt.NoCurrencyOptionTestCase ; \
+ test 3 = $$?
+ @printf ${FMT} ${SEPARATOR} \
+ "Testing against db not found"
+ @export DJANGO_SETTINGS_MODULE="talerbank.settings" \
+ TALER_PREFIX="@prefix@" \
+ TALER_BANK_ALTDB="postgres:///idontexist" \
+ PYTHONPATH="${PYTHONPATH_APPEND}:${PYTHONPATH}" \
+ && python3 ./taler-bank-manage serve-uwsgi ; \
+ test 4 = $$?
# install into prefix
install-exec-hook:
- @pip3 install . --install-option="address@hidden@" @DEBIAN_PIP3_SYSTEM@
+ @pip3 install . @DEBIAN_PIP3_SYSTEM@ \
+ --target="${PYTHONPATH_APPEND}" \
+ --install-option="address@hidden@/bin"
@# force update when sources changed
- @pip3 install . --install-option="address@hidden@" @DEBIAN_PIP3_SYSTEM@
--upgrade --no-deps
+ @pip3 install . --target="${PYTHONPATH_APPEND}" \
+ @DEBIAN_PIP3_SYSTEM@ --upgrade --no-deps \
+ --install-option="address@hidden@/bin"
pylint:
@pylint --load-plugins pylint_django talerbank/
app:
- @tar czf taler-bank-$(PACKAGE_VERSION)-app.tgz `cat INCLUDE.APP`
+ @tar czf taler-bank-$(PACKAGE_VERSION)-app.tgz \
+ `cat INCLUDE.APP`
diff --git a/README b/README
index 8fedf6a..0c82d67 100644
--- a/README
+++ b/README
@@ -3,25 +3,6 @@ This code implements a bank Web portal that tightly integrates
with
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.
-
-You may also have to install certain Python packages. Try:
-
-$ pip3 install validictory
-$ pip3 install django-simple-math-captcha
-
================== HOW TO INSTALL THE BANK =================
From the repository's top directory, run
@@ -36,11 +17,9 @@ The next step is to specify the install prefix, run
$ export PREFIX=$HOME/local # Adapt to your needs.
$ ./configure --prefix=$PREFIX
-On some Debian systems, the additional flag --enable-debian-system
-might be useful to let the --prefix option be correctly executed.
-
Then the usual GNU-compatible commands, that are
+# this will download all dependencies
$ make install
and optionally
diff --git a/bank-check-alt-baddb.conf b/bank-check-alt-baddb.conf
deleted file mode 100644
index 6861623..0000000
--- a/bank-check-alt-baddb.conf
+++ /dev/null
@@ -1,17 +0,0 @@
-# Config file containing intentional errors, used
-# to test how the bank reacts.
-
-[taler]
-
-CURRENCY = KUDOS
-
-[bank]
-
-# Which database should we use?
-DATABASE = bad db string
-
-# FIXME
-MAX_DEBT = KUDOS:50
-
-# FIXME
-MAX_DEBT_BANK = KUDOS:0
diff --git a/configure.ac b/configure.ac
index a22226f..4904365 100644
--- a/configure.ac
+++ b/configure.ac
@@ -24,6 +24,9 @@ PC_INIT([3.4])
pyheaders=0
PC_PYTHON_CHECK_HEADERS([pyheaders=1])
+# Get Python version
+PC_PYTHON_CHECK_VERSION()
+
#
# Check for pip3
#
diff --git a/install-dev.py.in b/install-dev.py.in
index b4cfc6e..214b243 100644
--- a/install-dev.py.in
+++ b/install-dev.py.in
@@ -19,7 +19,7 @@ prefix_path = "%s/lib/python%d.%d/site-packages" % (
current_paths = os.environ.get("PYTHONPATH", "").split(":")
current_paths.append(prefix_path)
-current_paths.remove("")
+current_paths = [x for x in current_paths if x != ""]
os.environ["PYTHONPATH"] = ":".join(current_paths)
args = ["pip3", "install", '--install-option=--prefix=%s' % "@prefix@", "-e",
"."]
@@ -27,5 +27,3 @@ if "@DEBIAN_PIP3_SYSTEM@":
args.push("@DEBIAN_PIP3_SYSTEM@")
os.execvp("pip3", args)
-
-
diff --git a/setup.py b/setup.py
index 4bfdc5c..b2f2988 100755
--- a/setup.py
+++ b/setup.py
@@ -12,7 +12,8 @@ setup(name='talerbank',
"psycopg2",
"requests",
"uWSGI",
- "validictory"
+ "validictory",
+ "mock"
],
package_data={
diff --git a/taler-bank-manage.in b/taler-bank-manage.in
index df83042..935c8d5 100644
--- a/taler-bank-manage.in
+++ b/taler-bank-manage.in
@@ -6,11 +6,13 @@ the GNU Taler bank.
"""
import argparse
+import django
import sys
import os
import site
import logging
from talerbank.talerconfig import TalerConfig
+from django.core.management import call_command
os.environ.setdefault("TALER_PREFIX", "@prefix@")
site.addsitedir("%s/lib/python%d.%d/site-packages" % (
@@ -25,9 +27,7 @@ TC =
TalerConfig.from_file(os.environ.get("TALER_CONFIG_FILE"))
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
@@ -37,7 +37,6 @@ def handle_django(args):
def handle_serve_http(args):
import django
django.setup()
- from django.core.management import call_command
call_command('migrate')
call_command('provide_accounts')
call_command('check')
@@ -57,9 +56,7 @@ def handle_serve_http(args):
def handle_serve_uwsgi(args):
del args # pacify PEP checkers
- import django
django.setup()
- from django.core.management import call_command
call_command('migrate')
call_command('provide_accounts')
call_command('check')
@@ -83,9 +80,7 @@ def handle_serve_uwsgi(args):
os.execlp(*params)
def handle_sampledata():
- import django
django.setup()
- from django.core.management import call_command
call_command('sample_donations')
def handle_config(args):
@@ -132,4 +127,8 @@ if getattr(ARGS, 'func', None) is None:
if ARGS.config is not None:
os.environ["TALER_CONFIG_FILE"] = ARGS.config
-ARGS.func(ARGS)
+try:
+ ARGS.func(ARGS)
+except django.db.utils.OperationalError:
+ LOGGER.error("Your database has serious problems. Does it exist?")
+ sys.exit(4)
diff --git a/talerbank/app/amount.py b/talerbank/app/amount.py
index c3e2b93..d535586 100644
--- a/talerbank/app/amount.py
+++ b/talerbank/app/amount.py
@@ -22,13 +22,14 @@
# mentioned above, and it is meant to be manually copied into any project
# which might need it.
-from typing import Type
-
class CurrencyMismatch(Exception):
+ hint = "Internal logic error (currency mismatch)"
+ http_status_code = 500
def __init__(self, curr1, curr2) -> None:
super(CurrencyMismatch, self).__init__(
"%s vs %s" % (curr1, curr2))
+
class BadFormatAmount(Exception):
def __init__(self, faulty_str) -> None:
super(BadFormatAmount, self).__init__(
@@ -63,14 +64,14 @@ class Amount:
# instantiating an amount object.
@classmethod
def parse(cls, amount_str: str):
- exp = r'^\s*([-_*A-Za-z0-9]+):([0-9]+)\.([0-9]+)\s*$'
+ exp = r'^\s*([-_*A-Za-z0-9]+):([0-9]+)\.?([0-9]+)?\s*$'
import re
parsed = re.search(exp, amount_str)
if not parsed:
raise BadFormatAmount(amount_str)
value = int(parsed.group(2))
fraction = 0
- for i, digit in enumerate(parsed.group(3)):
+ for i, digit in enumerate(parsed.group(3) or "0"):
fraction += int(int(digit) * (Amount._fraction() / 10 ** (i+1)))
return cls(parsed.group(1), value, fraction)
@@ -119,15 +120,17 @@ class Amount:
# Dump string from this amount, will put 'ndigits' numbers
# after the dot.
- def stringify(self, ndigits: int) -> str:
+ def stringify(self, ndigits: int, pretty=False) -> str:
assert ndigits > 0
- ret = '%s:%s.' % (self.currency, str(self.value))
- fraction = self.fraction
+ tmp = self.fraction
+ fraction_str = ""
while ndigits > 0:
- ret += str(int(fraction / (Amount._fraction() / 10)))
- fraction = (fraction * 10) % (Amount._fraction())
+ fraction_str += str(int(tmp / (Amount._fraction() / 10)))
+ tmp = (tmp * 10) % (Amount._fraction())
ndigits -= 1
- return ret
+ if not pretty:
+ return "%s:%d.%s" % (self.currency, self.value, fraction_str)
+ return "%d.%s %s" % (self.value, fraction_str, self.currency)
# Dump the Taler-compliant 'dict' amount
def dump(self) -> dict:
diff --git a/talerbank/app/management/commands/dump_talerdb.py
b/talerbank/app/management/commands/dump_talerdb.py
index ba81444..956c07e 100644
--- a/talerbank/app/management/commands/dump_talerdb.py
+++ b/talerbank/app/management/commands/dump_talerdb.py
@@ -1,16 +1,19 @@
# This file is part of TALER
# (C) 2014, 2015, 2106 INRIA
#
-# TALER is free software; you can redistribute it and/or modify it under the
-# terms of the GNU General Public License as published by the Free Software
-# Foundation; either version 3, or (at your option) any later version.
+# TALER is free software; you can redistribute it and/or modify
+# it under the terms of the GNU 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.
+# 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/>
+# 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
@@ -19,7 +22,6 @@ import logging
from django.core.management.base import BaseCommand
from django.db.utils import OperationalError, ProgrammingError
from ...models import BankAccount, BankTransaction
-from .helpers import hard_db_error_log
LOGGER = logging.getLogger(__name__)
@@ -30,9 +32,11 @@ def dump_accounts():
print("No accounts created yet..")
return
for acc in accounts:
- print(acc.user.username + " has account number " +
str(acc.account_no))
+ print(acc.user.username + \
+ " has account number " + \
+ str(acc.account_no))
except (OperationalError, ProgrammingError):
- hard_db_error_log()
+ LOGGER.error("Hard database error, does it exist?")
sys.exit(1)
@@ -41,15 +45,13 @@ def dump_history():
history = BankTransaction.objects.all()
for item in history:
msg = []
- # concatenating via 'append' because the + operator put
- # 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(item.amount.stringify(2))
- msg.append(item.subject)
+ msg.append(" '" + item.subject + "'")
print(''.join(msg))
except (OperationalError, ProgrammingError):
- hard_db_error_log()
+ LOGGER.error("Hard database error, does it exist?")
sys.exit(1)
diff --git a/talerbank/app/management/commands/helpers.py
b/talerbank/app/management/commands/helpers.py
deleted file mode 100644
index 62137e9..0000000
--- a/talerbank/app/management/commands/helpers.py
+++ /dev/null
@@ -1,24 +0,0 @@
-# This file is part of TALER
-# (C) 2017 Taler Systems SA
-#
-# TALER is free software; you can redistribute it and/or modify it under the
-# terms of the GNU 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
-
-import logging
-LOGGER = logging.getLogger(__name__)
-
-def hard_db_error_log():
- LOGGER.error("likely causes: non existent DB or unmigrated project\n"
- "(try 'taler-bank-manage django migrate' in the latter case)",
- stack_info=False,
- exc_info=True)
diff --git a/talerbank/app/management/commands/provide_accounts.py
b/talerbank/app/management/commands/provide_accounts.py
index c719296..6bf8c53 100644
--- a/talerbank/app/management/commands/provide_accounts.py
+++ b/talerbank/app/management/commands/provide_accounts.py
@@ -22,51 +22,32 @@ from django.db.utils import ProgrammingError,
OperationalError
from django.core.management.base import BaseCommand
from django.conf import settings
from ...models import BankAccount
-from .helpers import hard_db_error_log
LOGGER = logging.getLogger(__name__)
+LOGGER.setLevel(logging.INFO)
-def demo_accounts():
- for name in settings.TALER_PREDEFINED_ACCOUNTS:
- try:
- User.objects.get(username=name)
- except User.DoesNotExist:
- BankAccount(user=User.objects.create_user(username=name,
password='x'),
- is_public=True).save()
- LOGGER.info("Creating account for '%s'", name)
-
-
-def ensure_account(name):
- LOGGER.info("ensuring account '%s'", name)
- user = None
+def make_account(username):
try:
- user = User.objects.get(username=name)
- except (OperationalError, ProgrammingError):
- hard_db_error_log()
- sys.exit(1)
+ User.objects.get(username=username)
except User.DoesNotExist:
- LOGGER.info("Creating *user* account '%s'", name)
- user = User.objects.create_user(username=name, password='x')
-
- try:
- BankAccount.objects.get(user=user)
-
- except BankAccount.DoesNotExist:
- acc = BankAccount(user=user, is_public=True)
- acc.save()
- LOGGER.info("Creating *bank* account number \
- '%s' for user '%s'", acc.account_no, name)
-
-
-def basic_accounts():
- ensure_account("Bank")
- ensure_account("Exchange")
+ LOGGER.info("Creating account for '%s'", username)
+ BankAccount(
+ user=User.objects.create_user(
+ username=username, password='x'),
+ is_public=True).save()
+ except (OperationalError, ProgrammingError):
+ LOGGER.error("db does not exist, or the project"
+ " is not migrated. Try 'taler-bank-manage"
+ " django migrate' in the latter case.",
+ stack_info=False,
+ exc_info=True)
+ sys.exit(1)
class Command(BaseCommand):
help = "Provide initial user accounts"
def handle(self, *args, **options):
- basic_accounts()
- demo_accounts()
+ for username in settings.TALER_PREDEFINED_ACCOUNTS:
+ make_account(username)
diff --git a/talerbank/app/management/commands/wire_transfer.py
b/talerbank/app/management/commands/wire_transfer.py
new file mode 100644
index 0000000..82f18d0
--- /dev/null
+++ b/talerbank/app/management/commands/wire_transfer.py
@@ -0,0 +1,80 @@
+# This file is part of TALER
+# (C) 2014, 2015, 2106 INRIA
+#
+# TALER is free software; you can redistribute it and/or modify
+# it under the terms of the GNU 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
+
+import sys
+import logging
+import json
+from django.core.management.base import BaseCommand
+from django.contrib.auth import authenticate
+from ...amount import Amount, BadFormatAmount
+from ...views import wire_transfer
+from ...models import BankAccount, BankTransaction
+
+LOGGER = logging.getLogger(__name__)
+
+class Command(BaseCommand):
+ help = "Wire transfer money and return the transaction id."
+
+ def add_arguments(self, parser):
+ parser.add_argument(
+ "user", type=str, metavar="USERNAME",
+ help="Which user is performing the wire transfer")
+ parser.add_argument(
+ "password", type=str, metavar="PASSWORD",
+ help="Performing user's password.")
+ parser.add_argument(
+ "credit-account", type=int, metavar="CREDIT-ACCOUNT",
+ help="Which account number will *receive* money.")
+ parser.add_argument(
+ "subject", type=str, metavar="SUBJECT",
+ help="SUBJECT will be the wire transfer subject.")
+ parser.add_argument(
+ "amount", type=str, metavar="AMOUNT",
+ help="Wire transfer's amount, given in the " \
+ "CURRENCY:X.Y form.")
+
+
+ def handle(self, *args, **options):
+
+ user = authenticate(
+ username=options["user"], password=options["password"])
+ if not user:
+ LOGGER.error("Wrong user/password.")
+ sys.exit(1)
+ try:
+ amount = Amount.parse(options["amount"])
+ except BadFormatAmount:
+ LOGGER.error("Amount's format is wrong: respect C:X.Y.")
+ sys.exit(1)
+
+ try:
+ credit_account = BankAccount.objects.get(
+ account_no=options["credit-account"])
+ except BankAccount.DoesNotExist:
+ LOGGER.error("Credit account does not exist.")
+ sys.exit(1)
+
+ try:
+ transaction = wire_transfer(
+ amount, user.bankaccount,
+ credit_account, options["subject"])
+ print("Transaction id: " + str(transaction.id))
+ except Exception as exc:
+ LOGGER.error(exc)
+ sys.exit(1)
diff --git a/talerbank/app/middleware.py b/talerbank/app/middleware.py
new file mode 100644
index 0000000..88e3b1b
--- /dev/null
+++ b/talerbank/app/middleware.py
@@ -0,0 +1,65 @@
+import logging
+from django.http import JsonResponse
+from django.shortcuts import redirect
+from .models import BankAccount, BankTransaction
+from .views import \
+ (DebitLimitException, SameAccountException,
+ LoginFailed, RejectNoRightsException)
+from .schemas import \
+ (URLParameterMissing, URLParameterMalformed,
+ JSONFieldException, UnknownCurrencyException)
+from .amount import CurrencyMismatch, BadFormatAmount
+
+LOGGER = logging.getLogger()
+
+class ExceptionMiddleware:
+
+ def __init__(self, get_response):
+ self.get_response = get_response
+
+ self.excs = {
+ BankAccount.DoesNotExist: 0,
+ BankTransaction.DoesNotExist: 1,
+ SameAccountException: 2,
+ DebitLimitException: 3,
+ URLParameterMissing: 8,
+ URLParameterMalformed: 9,
+ JSONFieldException: 6,
+ CurrencyMismatch: 11,
+ BadFormatAmount: 11,
+ LoginFailed: 12,
+ RejectNoRightsException: 13,
+ UnknownCurrencyException: 14}
+
+ self.apis = {
+ "/reject": 5300,
+ "/history": 5200,
+ "/admin/add/incoming": 5100}
+
+ self.render = {
+ "/profile": "profile",
+ "/register": "index",
+ "/public-accounts": "index",
+ "/pin/verify": "profile"}
+
+
+ def __call__(self, request):
+ return self.get_response(request)
+
+ def process_exception(self, request, exception):
+ if not self.excs.get(exception.__class__):
+ return None
+ taler_ec = self.excs.get(exception.__class__)
+ # The way error codes compose matches definitions found
+ # at [1].
+ taler_ec += self.apis.get(request.path, 1000)
+ render_to = self.render.get(request.path)
+ if not render_to:
+ return JsonResponse({"ec": taler_ec,
+ "error": exception.hint},
+ status=exception.http_status_code)
+ request.session["profile_hint"] = \
+ True, False, exception.hint
+ return redirect(render_to)
+
+# [1]
https://git.taler.net/exchange.git/tree/src/include/taler_error_codes.h#n1502
diff --git a/talerbank/app/migrations/0011_banktransaction_reimburses.py
b/talerbank/app/migrations/0011_banktransaction_reimburses.py
new file mode 100644
index 0000000..6ea385d
--- /dev/null
+++ b/talerbank/app/migrations/0011_banktransaction_reimburses.py
@@ -0,0 +1,19 @@
+# Generated by Django 2.0 on 2017-12-12 15:31
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('app', '0010_banktransaction_cancelled'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='banktransaction',
+ name='reimburses',
+ field=models.ForeignKey(default=False,
on_delete=django.db.models.deletion.CASCADE, related_name='reimburser',
to='app.BankTransaction'),
+ ),
+ ]
diff --git a/talerbank/app/migrations/0012_auto_20171212_1540.py
b/talerbank/app/migrations/0012_auto_20171212_1540.py
new file mode 100644
index 0000000..21af37a
--- /dev/null
+++ b/talerbank/app/migrations/0012_auto_20171212_1540.py
@@ -0,0 +1,19 @@
+# Generated by Django 2.0 on 2017-12-12 15:40
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('app', '0011_banktransaction_reimburses'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='banktransaction',
+ name='reimburses',
+ field=models.ForeignKey(default=None, null=True,
on_delete=django.db.models.deletion.CASCADE, related_name='reimburser',
to='app.BankTransaction'),
+ ),
+ ]
diff --git a/talerbank/app/migrations/0005_remove_banktransaction_currency.py
b/talerbank/app/migrations/0013_remove_banktransaction_reimburses.py
similarity index 53%
copy from talerbank/app/migrations/0005_remove_banktransaction_currency.py
copy to talerbank/app/migrations/0013_remove_banktransaction_reimburses.py
index 9cd781f..5b88248 100644
--- a/talerbank/app/migrations/0005_remove_banktransaction_currency.py
+++ b/talerbank/app/migrations/0013_remove_banktransaction_reimburses.py
@@ -1,6 +1,4 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.10.3 on 2017-10-30 14:37
-from __future__ import unicode_literals
+# Generated by Django 2.0 on 2017-12-14 10:19
from django.db import migrations
@@ -8,12 +6,12 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
- ('app', '0004_auto_20171030_1428'),
+ ('app', '0012_auto_20171212_1540'),
]
operations = [
migrations.RemoveField(
model_name='banktransaction',
- name='currency',
+ name='reimburses',
),
]
diff --git a/talerbank/app/models.py b/talerbank/app/models.py
index f8c5c47..a584912 100644
--- a/talerbank/app/models.py
+++ b/talerbank/app/models.py
@@ -1,16 +1,18 @@
# 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 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.
#
-# 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/>
+# 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
# @author Florian Dold
@@ -20,7 +22,9 @@ from typing import Any, Tuple
from django.contrib.auth.models import User
from django.db import models
from django.conf import settings
-from django.core.exceptions import ValidationError
+from django.core.exceptions import \
+ ValidationError, \
+ ObjectDoesNotExist
from .amount import Amount, BadFormatAmount
class AmountField(models.Field):
@@ -28,7 +32,8 @@ class AmountField(models.Field):
description = 'Amount object in Taler style'
def deconstruct(self) -> Tuple[str, str, list, dict]:
- name, path, args, kwargs = super(AmountField, self).deconstruct()
+ name, path, args, kwargs = super(
+ AmountField, self).deconstruct()
return name, path, args, kwargs
def db_type(self, connection: Any) -> str:
@@ -55,28 +60,42 @@ class AmountField(models.Field):
return Amount.parse(settings.TALER_CURRENCY)
return Amount.parse(value)
except BadFormatAmount:
- raise ValidationError("Invalid input for an amount string: %s" %
value)
+ raise ValidationError(
+ "Invalid input for an amount string: %s" % value)
def get_zero_amount() -> Amount:
return Amount(settings.TALER_CURRENCY)
+class BankAccountDoesNotExist(ObjectDoesNotExist):
+ hint = "Specified bank account does not exist"
+ http_status_code = 404
+
+class BankTransactionDoesNotExist(ObjectDoesNotExist):
+ hint = "Specified bank transaction does not exist"
+ http_status_code = 404
+
class BankAccount(models.Model):
is_public = models.BooleanField(default=False)
debit = models.BooleanField(default=False)
account_no = models.AutoField(primary_key=True)
user = models.OneToOneField(User, on_delete=models.CASCADE)
amount = AmountField(default=get_zero_amount)
+ DoesNotExist = BankAccountDoesNotExist
class BankTransaction(models.Model):
amount = AmountField(default=False)
- debit_account = models.ForeignKey(BankAccount,
- on_delete=models.CASCADE,
- db_index=True,
- related_name="debit_account")
- credit_account = models.ForeignKey(BankAccount,
- on_delete=models.CASCADE,
- db_index=True,
- related_name="credit_account")
- subject = models.CharField(default="(no subject given)", max_length=200)
- date = models.DateTimeField(auto_now=True, db_index=True)
+ debit_account = models.ForeignKey(
+ BankAccount,
+ on_delete=models.CASCADE,
+ db_index=True,
+ related_name="debit_account")
+ credit_account = models.ForeignKey(
+ BankAccount,
+ on_delete=models.CASCADE,
+ db_index=True,
+ related_name="credit_account")
+ subject = models.CharField(
+ default="(no subject given)", max_length=200)
+ date = models.DateTimeField(
+ auto_now=True, db_index=True)
cancelled = models.BooleanField(default=False)
diff --git a/talerbank/app/schemas.py b/talerbank/app/schemas.py
index 810f33f..1ddb684 100644
--- a/talerbank/app/schemas.py
+++ b/talerbank/app/schemas.py
@@ -1,16 +1,19 @@
# 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 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.
+# 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/>
+# 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
@@ -20,9 +23,38 @@ definitions of JSON schemas for validating data
"""
import json
-import validictory
+from validictory import validate
+from validictory.validator import \
+ (RequiredFieldValidationError,
+ FieldValidationError)
+
from django.conf import settings
+class UnknownCurrencyException(ValueError):
+ def __init__(self, hint, http_status_code):
+ self.hint = hint
+ self.http_status_code = http_status_code
+ super().__init__()
+
+class URLParameterMissing(ValueError):
+ def __init__(self, param, http_status_code):
+ self.hint = "URL parameter '%s' is missing" % param
+ self.http_status_code = http_status_code
+ super().__init__()
+
+class URLParameterMalformed(ValueError):
+ def __init__(self, param, http_status_code):
+ self.hint = "URL parameter '%s' is malformed" % param
+ self.http_status_code = http_status_code
+ super().__init__()
+
+class JSONFieldException(ValueError):
+ def __init__(self, hint, http_status_code):
+ self.hint = hint
+ self.http_status_code = http_status_code
+ super().__init__()
+
+'''
AMOUNT_SCHEMA = {
"type": "object",
"properties": {
@@ -31,20 +63,23 @@ AMOUNT_SCHEMA = {
"currency": {"type": "string",
"pattern": "^"+settings.TALER_CURRENCY+"$"}
}
-}
+}'''
+
+AMOUNT_SCHEMA = {
+ "type": "string",
+ "pattern": "^"+settings.TALER_CURRENCY+":([0-9]+)\.?([0-9]+)?$"}
WITHDRAW_SESSION_SCHEMA = {
"type": "object",
"properties": {
"amount": {"type": AMOUNT_SCHEMA},
- "exchange_url": {"type": "string"},
"reserve_pub": {"type": "string"},
"exchange_account_number": {"type": "integer"},
"sender_wiredetails": {
"type": "object",
"properties": {
"type": {"type": "string"},
- "bank_uri": {"type": "string"},
+ "bank_url": {"type": "string"},
"account_number": {"type": "integer"}
}
}
@@ -59,7 +94,7 @@ WIREDETAILS_SCHEMA = {
"properties": {
"type": {"type": "string"},
"account_number": {"type": "integer"},
- "bank_uri": {"type": "string"},
+ "bank_url": {"type": "string"},
"name": {"type": "string", "required": False},
}
}
@@ -88,13 +123,16 @@ HISTORY_REQUEST_SCHEMA = {
"type": "object",
"properties": {
"auth": {"type": "string", "pattern": "^basic$"},
+ "cancelled": {"type": "string",
+ "pattern": "^(omit|show)$",
+ "required": False},
"delta": {"type": "string",
"pattern": r"^([\+-])?([0-9])+$"},
"start": {"type": "string",
"pattern": "^([0-9]+)$",
"required": False},
"direction": {"type": "string",
- "pattern": "^(debit|credit|both|cancel\+|cancel-)$"},
+ "pattern": r"^(debit|credit|both|cancel\+|cancel-)$"},
"account_number": {"type": "string",
"pattern": "^([0-9]+)$",
"required": False}
@@ -106,7 +144,6 @@ INCOMING_REQUEST_SCHEMA = {
"properties": {
"amount": {"type": AMOUNT_SCHEMA},
"subject": {"type": "string"},
- "exchange_url": {"type": "string"},
"credit_account": {"type": "integer"},
"auth": AUTH_SCHEMA
}
@@ -120,7 +157,7 @@ PIN_TAN_ARGS = {
"amount_currency": {"type": "string"},
"exchange": {"type": "string"},
"reserve_pub": {"type": "string"},
- "wire_details": {"format": "wiredetails_string"}
+ "exchange_wire_details": {"format": "wiredetails_string"}
}
}
@@ -133,29 +170,49 @@ def validate_pintan_types(validator, fieldname, value,
format_option):
data = json.loads(value)
validate_wiredetails(data)
except Exception:
- raise validictory.FieldValidationError(
- "Missing/malformed '%s'" % fieldname, fieldname, value)
+ raise FieldValidationError(
+ "Malformed '%s'" % fieldname, fieldname, value)
-def validate_pin_tan_args(pin_tan_args):
+def validate_pin_tan(data):
format_dict = {
"str_to_int": validate_pintan_types,
"wiredetails_string": validate_pintan_types}
- validictory.validate(pin_tan_args, PIN_TAN_ARGS,
format_validators=format_dict)
+ validate(data, PIN_TAN_ARGS, format_validators=format_dict)
-def validate_reject_request(reject_request):
- validictory.validate(reject_request, REJECT_REQUEST_SCHEMA)
+def validate_reject(data):
+ validate(data, REJECT_REQUEST_SCHEMA)
-def validate_history_request(history_request):
- validictory.validate(history_request, HISTORY_REQUEST_SCHEMA)
-
-def validate_amount(amount):
- validictory.validate(amount, AMOUNT_SCHEMA)
+def validate_history(data):
+ validate(data, HISTORY_REQUEST_SCHEMA)
def validate_wiredetails(wiredetails):
- validictory.validate(wiredetails, WIREDETAILS_SCHEMA)
+ validate(wiredetails, WIREDETAILS_SCHEMA)
+
+def validate_add_incoming(data):
+ validate(data, INCOMING_REQUEST_SCHEMA)
-def validate_incoming_request(incoming_request):
- validictory.validate(incoming_request, INCOMING_REQUEST_SCHEMA)
+def check_withdraw_session(data):
+ validate(data, WITHDRAW_SESSION_SCHEMA)
-def check_withdraw_session(session):
- validictory.validate(session, WITHDRAW_SESSION_SCHEMA)
+def validate_data(request, data):
+ switch = {
+ "/reject": validate_reject,
+ "/history": validate_history,
+ "/admin/add/incoming": validate_add_incoming,
+ "/pin/verify": check_withdraw_session,
+ "/pin/question": validate_pin_tan
+ }
+ try:
+ switch.get(request.path_info)(data)
+ except RequiredFieldValidationError as exc:
+ if request.method == "GET":
+ raise URLParameterMissing(exc.fieldname, 400)
+ raise JSONFieldException(
+ "Field '%s' is missing" % exc.fieldname, 400)
+ except FieldValidationError as exc:
+ if exc.fieldname == "currency":
+ raise UnknownCurrencyException("Unknown currency", 406)
+ if request.method == "GET":
+ raise URLParameterMalformed(exc.fieldname, 400)
+ raise JSONFieldException(
+ "Malformed '%s' field" % exc.fieldname, 400)
diff --git a/talerbank/app/static/bank.css b/talerbank/app/static/bank.css
index c01fc13..4c16e94 100644
--- a/talerbank/app/static/bank.css
+++ b/talerbank/app/static/bank.css
@@ -3,8 +3,49 @@ h1.nav {
display: inline-block;
}
+div.pages-list {
+ margin-top: 15px;
+}
+
+a.page-number {
+ color: blue;
+}
+
+a.current-page-number {
+ color: inherit;
+}
+
a.pure-button {
position: absolute;
right: 20px;
top: 23px;
}
+
+.cancelled {
+ text-decoration: line-through;
+}
+
+/**
+ * NOTE: could not set input width with "normal"
+ * 'size' and 'maxlength' HTML attributes because
+ * they are ignored for "number"-typed inputs. */
+input#id_amount {
+ width: 230px;
+ padding-right: 160px;
+}
+
+/* Styling the wrapper */
+span.currency-symbol {
+ position: absolute;
+ margin-top: 13px;
+ margin-left: 90px;
+}
+
+input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+}
+
+input[type="number"] {
+ -moz-appearance: textfield;
+}
diff --git a/talerbank/app/templates/base.html
b/talerbank/app/templates/base.html
index a604561..56d736f 100644
--- a/talerbank/app/templates/base.html
+++ b/talerbank/app/templates/base.html
@@ -21,8 +21,8 @@
<html data-taler-nojs="true">
<head>
<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('bank.css') }}" />
+ <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" />
{% block head %} {% endblock %}
diff --git a/talerbank/app/templates/login.html
b/talerbank/app/templates/login.html
index 5a45aad..40a5a5b 100644
--- a/talerbank/app/templates/login.html
+++ b/talerbank/app/templates/login.html
@@ -27,15 +27,14 @@
<article>
<div class="login-form">
<h2>Please login!</h2>
- {% if form.errors %}
+ {% if fail_message %}
<p class="informational informational-fail">
- Your username and password didn't match. Please try again.
+ {{ hint }}
</p>
{% endif %}
-
- {% if just_logged_out %}
+ {% if success_message %}
<p class="informational informational-ok">
- You were logged out successfully.
+ {{ hint }}
</p>
{% endif %}
diff --git a/talerbank/app/templates/pin_tan.html
b/talerbank/app/templates/pin_tan.html
index f8f8188..de836c0 100644
--- a/talerbank/app/templates/pin_tan.html
+++ b/talerbank/app/templates/pin_tan.html
@@ -24,9 +24,9 @@
{% endblock %}
{% block content %}
- {% if previous_failed %}
+ {% if fail_message %}
<p class="informational informational-fail">
- The captcha wasn't solved correctly. Please try again.
+ {{ hint }}
</p>
{% endif %}
<p>
diff --git a/talerbank/app/templates/profile_page.html
b/talerbank/app/templates/profile_page.html
index ef53437..4a03e52 100644
--- a/talerbank/app/templates/profile_page.html
+++ b/talerbank/app/templates/profile_page.html
@@ -42,38 +42,20 @@
</section>
<section id="main">
<article>
- <div class="notification">
- {% if wire_transfer_error %}
+ {% if fail_message %}
+ <div class="notification">
<p class="informational informational-fail">
- {% if info_bar %}
- {{ info_bar }}
- {% else %}
- Could not perform wire transfer, check all fields are correctly
- entered.
- {% endif %}
+ {{ hint }}
</p>
- {% endif %}
- {% if just_wire_transferred %}
- <p class="informational informational-ok">
- Wire transfer done!
- </p>
- {% endif %}
- {% 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!
- </p>
- {% endif %}
- {% if just_registered %}
+ </div>
+ {% endif %}
+ {% if success_message %}
+ <div class="notification">
<p class="informational informational-ok">
- Registration successful!
+ {{ hint }}
</p>
- {% endif %}
</div>
+ {% endif %}
</article>
<article>
<div class="taler-installed-hide">
@@ -112,6 +94,8 @@
class="taler-installed-show pure-button pure-button-primary"
type="submit"
value="Select exchange provider" />
+ <br />
+ <br />
<input class="taler-installed-hide pure-button pure-button-primary"
type="button"
disabled
@@ -125,10 +109,23 @@
name="tform">
<input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token
}}" />
- {{ wt_form }}
+ <span class="currency-widget">
+ <span class="currency-symbol">{{ currency }}</span>
+ {{ wt_form.amount }}
+ </span>
+ <label for="id_receiver">
+ to
+ {{ wt_form.receiver }}
+ </label>
+ <br />
+ <br />
+ <label for="id_subject">
+ subject
+ {{ wt_form.subject }}
+ </label>
<input class="pure-button pure-button-primary"
type="submit"
- value="Submit" />
+ value="Transfer!" />
</form>
</div>
<p>
@@ -154,8 +151,16 @@
<td style="text-align:right">
{{ item.sign }} {{ item.amount }}
</td>
- <td class="text-align:left">{% if item.counterpart_username %} {{
item.counterpart_username }} {% endif %} (account #{{ item.counterpart }})</td>
- <td class="text-align:left">{{ item.subject }}</td>
+ <td class="text-align:left">
+ {% if item.counterpart_username %}
+ {{ item.counterpart_username }}
+ {% endif %} (account #{{ item.counterpart }})
+ </td>
+ <td class="text-align:left
+ {% if item.cancelled %}
+ cancelled
+ {% endif %}">{{ item.subject }}
+ </td>
</tr>
{% endfor %}
</tbody>
diff --git a/talerbank/app/templates/public_accounts.html
b/talerbank/app/templates/public_accounts.html
index 90d5e59..3ebe6ad 100644
--- a/talerbank/app/templates/public_accounts.html
+++ b/talerbank/app/templates/public_accounts.html
@@ -25,6 +25,20 @@
<a href="{{ url('index') }}">Back</a>
<section id="main">
<article>
+ {% if fail_message %}
+ <div class="notification">
+ <p class="informational informational-fail">
+ {{ hint }}
+ </p>
+ </div>
+ {% endif %}
+ {% if success_message %}
+ <div class="notification">
+ <p class="informational informational-ok">
+ {{ hint }}
+ </p>
+ </div>
+ {% endif %}
<div name="accountMenu" class="pure-menu pure-menu-horizontal">
<ul class="pure-menu-list">
{% for account in public_accounts %}
@@ -41,32 +55,55 @@
</ul>
</div>
- {% 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.sign }} {{ entry.amount }}
- </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 %}
+ <div class="results">
+ {% 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.sign }} {{ entry.amount }}
+ </td>
+ <td>{% if entry.counterpart_username %} {{
entry.counterpart_username }} {% endif %} (account #{{ entry.counterpart
}})</td>
+ <td {% if entry.cancelled %} class="cancelled" {% endif %}>
+ {{ entry.subject }}
+ </td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ <div class="pages-list">
+ {% if back %}
+ <a
+ class="page-number"
+ href="{{ url("public-accounts", name=selected_account.name,
page=back) }}">‹...</a>
+ {% endif %}
+ {% for pagej in pages %}
+ <a
+ {% if pagej == current_page%}
+ class="current-page-number"
+ {% else %}
+ class="page-number"
+ {% endif %}
+ href="{{ url("public-accounts", name=selected_account.name,
page=pagej) }}">{{ pagej }}</a>
+ {% endfor %}
+ {% if forth %}
+ <a
+ class="page-number"
+ href="{{ url("public-accounts", name=selected_account.name,
page=forth) }}">...›</a>
+ {% endif %}
+ </div>
+ {% else %}
+ <p>No history for account #{{ selected_account.number }} ({{
selected_account.name}}) yet</p>
+ {% endif %}
+ </div>
</article>
</section>
{% endblock content %}
diff --git a/talerbank/app/templates/register.html
b/talerbank/app/templates/register.html
index b7423e7..f01b5d9 100644
--- a/talerbank/app/templates/register.html
+++ b/talerbank/app/templates/register.html
@@ -29,6 +29,7 @@
<article>
<a href="{{ url('index') }}">Back</a>
<div class="notification">
+ <!-- To be flag-ified -->
{% if wrong %}
<p class="informational informational-fail">
Some fields were either not filled or filled incorrectly.
diff --git a/talerbank/app/tests.py b/talerbank/app/tests.py
index 35374c6..f6ccb4b 100644
--- a/talerbank/app/tests.py
+++ b/talerbank/app/tests.py
@@ -17,6 +17,8 @@
import json
import timeit
import logging
+from urllib.parse import unquote
+from django.db import connection
from django.test import TestCase, Client
from django.urls import reverse
from django.conf import settings
@@ -31,81 +33,81 @@ LOGGER = logging.getLogger()
LOGGER.setLevel(logging.WARNING)
def clear_db():
- # FIXME: this way we do not reset autoincrement
- # fields.
User.objects.all().delete()
BankAccount.objects.all().delete()
BankTransaction.objects.all().delete()
+ with connection.cursor() as cursor:
+ cursor.execute(
+ "ALTER SEQUENCE app_bankaccount_account_no_seq" \
+ " RESTART")
+ cursor.execute(
+ "ALTER SEQUENCE app_banktransaction_id_seq RESTART")
class WithdrawTestCase(TestCase):
def setUp(self):
- BankAccount(
+ self.user_bank_account = BankAccount(
user=User.objects.create_user(
username="test_user",
password="test_password"),
- account_no=100).save()
+ account_no=100)
+ self.user_bank_account.save()
- BankAccount(
+ self.exchange_bank_account = BankAccount(
user=User.objects.create_user(
username="test_exchange",
password=""),
- account_no=99).save()
+ account_no=99)
+ self.exchange_bank_account.save()
self.client = Client()
- def tearDown(self):
- clear_db()
-
+ @patch('talerbank.app.views.wire_transfer')
@patch('hashlib.new')
- @patch('requests.post')
@patch('time.time')
- def test_withdraw(self, mocked_time, mocked_post, mocked_hashlib):
+ def test_withdraw(self, mocked_time,
+ mocked_hashlib, mocked_wire_transfer):
wire_details = '''{
"test": {
"type":"test",
"account_number":99,
- "bank_uri":"http://bank.example/",
+ "bank_url":"http://bank.example/",
"name":"example"
}
}'''
+ amount = Amount(settings.TALER_CURRENCY, 0, 1)
params = {
- "amount_value": "0",
- "amount_fraction": "1",
- "amount_currency": settings.TALER_CURRENCY,
- "exchange": "http://exchange.example/",
+ "amount_value": str(amount.value),
+ "amount_fraction": str(amount.fraction),
+ "amount_currency": amount.currency,
"reserve_pub": "UVZ789",
- "wire_details": wire_details.replace("\n", "").replace(" ", "")
+ "exchange": "https://exchange.example.com/",
+ "exchange_wire_details":
+ wire_details.replace("\n", "").replace(" ", "")
}
- self.client.login(username="test_user", password="test_password")
+ self.client.login(username="test_user",
+ password="test_password")
- self.client.get(reverse("pin-question", urlconf=urls),
- params)
+ response = self.client.get(
+ reverse("pin-question", urlconf=urls),
+ params)
+ self.assertEqual(response.status_code, 200)
# We mock hashlib in order to fake the CAPTCHA.
hasher = MagicMock()
hasher.hexdigest = MagicMock()
hasher.hexdigest.return_value = "0"
mocked_hashlib.return_value = hasher
- post = MagicMock()
- post.status_code = 200
- mocked_post.return_value = post
mocked_time.return_value = 0
- self.client.post(reverse("pin-verify", urlconf=urls),
- {"pin_1": "0"})
- expected_json = {
- "reserve_pub": "UVZ789",
- "execution_date": "/Date(0)/",
- "sender_account_details": {
- "type": "test",
- "bank_uri": "http://testserver/",
- "account_number": 100
- },
- "transfer_details": {"timestamp": 0},
- "amount": {
- "value": 0,
- "fraction": 1,
- "currency": settings.TALER_CURRENCY}
- }
-
mocked_post.assert_called_with("http://exchange.example/admin/add/incoming",
- json=expected_json)
+
+ response = self.client.post(
+ reverse("pin-verify", urlconf=urls),
+ {"pin_1": "0"})
+
+ args, kwargs = mocked_wire_transfer.call_args
+ del kwargs
+ self.assertTrue(
+ args[0].dump() == amount.dump() \
+ and self.user_bank_account in args \
+ and "UVZ789" in args \
+ and self.exchange_bank_account in args)
def tearDown(self):
clear_db()
@@ -113,10 +115,13 @@ class WithdrawTestCase(TestCase):
class InternalWireTransferTestCase(TestCase):
def setUp(self):
- BankAccount(user=User.objects.create_user(username='give_money',
- password="gm")).save()
- BankAccount(user=User.objects.create_user(username='take_money'),
- account_no=88).save()
+ BankAccount(user=User.objects.create_user(
+ username='give_money',
+ password="gm")).save()
+ self.take_money = BankAccount(
+ user=User.objects.create_user(
+ username='take_money'), account_no=4)
+ self.take_money.save()
def tearDown(self):
clear_db()
@@ -124,13 +129,16 @@ class InternalWireTransferTestCase(TestCase):
def test_internal_wire_transfer(self):
client = Client()
client.login(username="give_money", password="gm")
- response = client.post(reverse("profile", urlconf=urls),
- {"amount": 3.0,
- "counterpart": 88,
- "subject": "charity"})
- self.assertEqual(0, Amount.cmp(Amount(settings.TALER_CURRENCY, 3),
-
BankAccount.objects.get(account_no=88).amount))
- self.assertEqual(200, response.status_code)
+ response = client.post(
+ reverse("profile", urlconf=urls),
+ {"amount": 3.0,
+ "receiver": self.take_money.account_no,
+ "subject": "charity"})
+ take_money = BankAccount.objects.get(account_no=4)
+ self.assertEqual(0, Amount.cmp(
+ Amount(settings.TALER_CURRENCY, 3),
+ take_money.amount))
+ self.assertEqual(302, response.status_code)
class RegisterTestCase(TestCase):
@@ -138,8 +146,8 @@ class RegisterTestCase(TestCase):
def setUp(self):
BankAccount(
- user=User.objects.create_user(username='Bank'),
- account_no=1).save()
+ user=User.objects.create_user(
+ username='Bank')).save()
def tearDown(self):
clear_db()
@@ -162,8 +170,8 @@ class RegisterWrongCurrencyTestCase(TestCase):
# Note, config has KUDOS as currency.
BankAccount(
user=User.objects.create_user(username='Bank'),
- amount=Amount('WRONGCURRENCY'),
- account_no=1).save()
+ amount=Amount('WRONGCURRENCY')).save()
+ # Takes account_no = 1, as the first one.
def tearDown(self):
clear_db()
@@ -184,16 +192,30 @@ class LoginTestCase(TestCase):
user=User.objects.create_user(
username="test_user",
password="test_password")).save()
+ self.client = Client()
def tearDown(self):
clear_db()
def test_login(self):
- client = Client()
- self.assertTrue(client.login(username="test_user",
- password="test_password"))
- self.assertFalse(client.login(username="test_user",
- password="test_passwordii"))
+ self.assertTrue(self.client.login(
+ username="test_user",
+ password="test_password"))
+ self.assertFalse(self.client.login(
+ username="test_user",
+ password="test_passwordii"))
+
+ def test_failing_login(self):
+ response = self.client.get(
+ reverse("history", urlconf=urls), {"auth": "basic"},
+ **{"HTTP_X_TALER_BANK_USERNAME": "Wrong",
+ "HTTP_X_TALER_BANK_PASSWORD": "Credentials"})
+ data = response.content.decode("utf-8")
+ self.assertJSONEqual(
+ '{"error": "Wrong username/password", "ec": 5212}',
+ json.loads(data))
+ self.assertEqual(401, response.status_code)
+
class AmountTestCase(TestCase):
@@ -235,10 +257,7 @@ class RejectTestCase(TestCase):
"credit_account": %d, \
"subject": "TESTWTID", \
"exchange_url": "https://exchange.test", \
- "amount": \
- {"value": 5, \
- "fraction": 0, \
- "currency": "%s"}}' \
+ "amount": "%s:5.0"}' \
% (rejecting.bankaccount.account_no,
settings.TALER_CURRENCY)
response = client.post(
@@ -248,7 +267,7 @@ class RejectTestCase(TestCase):
follow=True, **{
"HTTP_X_TALER_BANK_USERNAME": "rejected_user",
"HTTP_X_TALER_BANK_PASSWORD": "rejected_password"})
-
+ self.assertEqual(response.status_code, 200)
data = response.content.decode("utf-8")
jdata = json.loads(data)
rejected = User.objects.get(username="rejected_user")
@@ -257,7 +276,8 @@ class RejectTestCase(TestCase):
data='{"row_id": %d, \
"auth": {"type": "basic"}, \
"account_number": %d}' \
- % (jdata["row_id"], rejected.bankaccount.account_no),
+ % (jdata["row_id"],
+ rejected.bankaccount.account_no),
content_type="application/json",
**{"HTTP_X_TALER_BANK_USERNAME": "rejecting_user",
"HTTP_X_TALER_BANK_PASSWORD": "rejecting_password"})
@@ -284,47 +304,43 @@ class AddIncomingTestCase(TestCase):
"credit_account": 1, \
"subject": "TESTWTID", \
"exchange_url": "https://exchange.test", \
- "amount": \
- {"value": 1, \
- "fraction": 0, \
- "currency": "%s"}}' \
+ "amount": "%s:1.0"}' \
% settings.TALER_CURRENCY
- response = client.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"})
+ response = client.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, \
"subject": "TESTWTID", \
"exchange_url": "https://exchange.test", \
- "amount": \
- {"value": 1, \
- "fraction": 0, \
- "currency": "%s"}}' \
- % "WRONGCURRENCY"
- response = client.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)
+ "amount": "WRONGCURRENCY:1.0"}'
+ response = client.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"})
+ # note: a bad currency request gets 400.
+ self.assertEqual(400, response.status_code)
# Try to go debit
data = '{"auth": {"type": "basic"}, \
"credit_account": 1, \
"subject": "TESTWTID", \
"exchange_url": "https://exchange.test", \
- "amount": \
- {"value": 50, \
- "fraction": 1, \
- "currency": "%s"}}' \
- % settings.TALER_CURRENCY
- response = client.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"})
+ "amount": "%s:50.1"}' % settings.TALER_CURRENCY
+ response = client.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(403, response.status_code)
class HistoryContext:
@@ -342,54 +358,96 @@ class HistoryTestCase(TestCase):
user=User.objects.create_user(
username='User',
password="Password"),
- amount=Amount(settings.TALER_CURRENCY, 100),
- account_no=1)
+ amount=Amount(settings.TALER_CURRENCY, 100))
debit_account.save()
credit_account = BankAccount(
user=User.objects.create_user(
username='User0',
- password="Password0"),
- account_no=2)
+ password="Password0"))
credit_account.save()
- for subject in ("a", "b", "c", "d", "e", "f", "g", "h"):
+ for subject in (
+ "a", "b", "c", "d", "e", "f", "g", "h", "i"):
wire_transfer(Amount(settings.TALER_CURRENCY, 1),
debit_account,
credit_account, subject)
+ # reject transaction 'i'.
+ trans_i = BankTransaction.objects.get(subject="i")
+ self.client = Client()
+ self.client.post(
+ reverse("reject", urlconf=urls),
+ data='{"auth": {"type": "basic"}, \
+ "row_id": %d, \
+ "account_number": 44}' % trans_i.id, # Ignored
+ content_type="application/json",
+ follow=True,
+ **{"HTTP_X_TALER_BANK_USERNAME": "User0",
+ "HTTP_X_TALER_BANK_PASSWORD": "Password0"})
+
def tearDown(self):
clear_db()
def test_history(self):
- client = Client()
- for ctx in (HistoryContext(expected_resp={"status": 200},
- delta="4", direction="both"),
- HistoryContext(expected_resp={
- "field": "row_id", "value": 6,
- "status": 200}, delta="+1", start="5",
direction="both"),
- HistoryContext(expected_resp={
- "field": "wt_subject", "value": "h",
- "status": 200}, delta="-1", direction="both"),
- HistoryContext(expected_resp={"status": 204},
- delta="1", start="11", direction="both"),
- HistoryContext(expected_resp={"status": 204},
- delta="+1", direction="cancel+"),
- HistoryContext(expected_resp={"status": 204},
- delta="+1", direction="credit"),
- HistoryContext(expected_resp={"status": 200},
- delta="+1", direction="debit")):
- response = client.get(reverse("history", urlconf=urls),
ctx.urlargs,
- **{"HTTP_X_TALER_BANK_USERNAME": "User",
- "HTTP_X_TALER_BANK_PASSWORD": "Password"})
+ for ctx in (
+ HistoryContext(
+ expected_resp={"status": 200},
+ delta="4", direction="both"),
+ HistoryContext(
+ expected_resp={
+ "fields": [("row_id", 6)],
+ "status": 200},
+ delta="+1", start="5", direction="both"),
+ HistoryContext(
+ expected_resp={
+ "fields": [("wt_subject", "h")],
+ "status": 200},
+ delta="-1", start=9, direction="both"),
+ HistoryContext(
+ expected_resp={"status": 204},
+ delta="1", start="11", direction="both"),
+ HistoryContext(
+ expected_resp={
+ "status": 200,
+ "fields": [("wt_subject", "i"),
+ ("sign", "cancel-")]},
+ start=8, delta="+1", direction="cancel-"),
+ HistoryContext(
+ expected_resp={"status": 204},
+ start=8, delta="+1",
+ direction="cancel-", cancelled="omit"),
+ HistoryContext(
+ expected_resp={"status": 204},
+ start=8, delta="-1", direction="cancel-"),
+ HistoryContext(
+ expected_resp={"status": 204},
+ delta="+1", direction="cancel+"),
+ HistoryContext(expected_resp={"status": 200},
+ delta="+1", direction="debit")):
+ response = self.client.get(
+ reverse("history", urlconf=urls), ctx.urlargs,
+ **{"HTTP_X_TALER_BANK_USERNAME": "User",
+ "HTTP_X_TALER_BANK_PASSWORD": "Password"})
data = response.content.decode("utf-8")
try:
data = json.loads(data)["data"][0]
except (json.JSONDecodeError, KeyError):
data = {}
+ self.assertEqual(
+ ctx.expected_resp.get("status"),
+ response.status_code,
+ "Failing request: %s?%s" % \
+ (response.request["PATH_INFO"],
+ unquote(response.request["QUERY_STRING"])))
+
+ # extract expected data from response
+ expected_data = {}
+ response_data = {}
+ for key, value in ctx.expected_resp.get("fields", []):
+ response_data.update({key: data.get(key)})
+ expected_data.update({key: value})
+
+ self.assertEqual(expected_data, response_data)
- self.assertEqual(data.get(ctx.expected_resp.get("field")),
- ctx.expected_resp.get("value"))
- self.assertEqual(ctx.expected_resp.get("status"),
- response.status_code)
class DBAmountSubtraction(TestCase):
def setUp(self):
@@ -423,9 +481,11 @@ class DBCustomColumnTestCase(TestCase):
def test_exists(self):
user_bankaccount = BankAccount.objects.get(
user=User.objects.get(username='U'))
- self.assertTrue(isinstance(user_bankaccount.amount, Amount))
+ self.assertTrue(
+ isinstance(user_bankaccount.amount, Amount))
-# This tests whether a bank account goes debit and then goes >=0 again
+# This tests whether a bank account goes debit and then goes >=0
+# again
class DebitTestCase(TestCase):
def setUp(self):
@@ -453,8 +513,10 @@ class DebitTestCase(TestCase):
user_bankaccount,
"Go green")
tmp = Amount(settings.TALER_CURRENCY, 10)
- self.assertEqual(0, Amount.cmp(user_bankaccount.amount, tmp))
- self.assertEqual(0, Amount.cmp(user_bankaccount0.amount, tmp))
+ self.assertEqual(
+ 0, Amount.cmp(user_bankaccount.amount, tmp))
+ self.assertEqual(
+ 0, Amount.cmp(user_bankaccount0.amount, tmp))
self.assertFalse(user_bankaccount.debit)
self.assertTrue(user_bankaccount0.debit)
@@ -467,15 +529,33 @@ class DebitTestCase(TestCase):
tmp.value = 1
self.assertTrue(user_bankaccount.debit)
self.assertFalse(user_bankaccount0.debit)
- self.assertEqual(0, Amount.cmp(user_bankaccount.amount, tmp))
- self.assertEqual(0, Amount.cmp(user_bankaccount0.amount, tmp))
+ self.assertEqual(
+ 0, Amount.cmp(user_bankaccount.amount, tmp))
+ self.assertEqual(
+ 0, Amount.cmp(user_bankaccount0.amount, tmp))
class ParseAmountTestCase(TestCase):
def test_parse_amount(self):
ret = Amount.parse("KUDOS:4.0")
- self.assertJSONEqual('{"value": 4, "fraction": 0, "currency":
"KUDOS"}', ret.dump())
+ self.assertJSONEqual(
+ '{"value": 4, \
+ "fraction": 0, \
+ "currency": "KUDOS"}',
+ ret.dump())
ret = Amount.parse("KUDOS:4.3")
- self.assertJSONEqual('{"value": 4, "fraction": 30000000, "currency":
"KUDOS"}', ret.dump())
+ self.assertJSONEqual(
+ '{"value": 4, \
+ "fraction": 30000000, \
+ "currency": "KUDOS"}',
+ ret.dump())
+ ret = Amount.parse("KUDOS:4")
+ self.assertJSONEqual(
+ '{"value": 4, "fraction": 0, "currency": "KUDOS"}',
+ ret.dump())
+ ret = Amount.parse("KUDOS:4.") # forbid?
+ self.assertJSONEqual(
+ '{"value": 4, "fraction": 0, "currency": "KUDOS"}',
+ ret.dump())
try:
Amount.parse("Buggy")
except BadFormatAmount:
@@ -512,9 +592,108 @@ class MeasureHistory(TestCase):
# Measure the time extract_history() needs to retrieve
# ~ntransfers records.
- timer = timeit.Timer(stmt="extract_history(self.user_bankaccount0)",
- setup="from talerbank.app.views import
extract_history",
- globals=locals())
+ timer = timeit.Timer(
+ stmt="extract_history(self.user_bankaccount0)",
+ setup="from talerbank.app.views import extract_history",
+ globals=locals())
total_time = timer.timeit(number=1)
allowed_time_per_record = 0.003
- self.assertLess(total_time, self.ntransfers*allowed_time_per_record)
+ self.assertLess(
+ total_time, self.ntransfers*allowed_time_per_record)
+
+class BalanceTestCase(TestCase):
+
+ def setUp(self):
+ self.the_bank = BankAccount(
+ user=User.objects.create_user(
+ username='U0', password='U0PASS'),
+ amount=Amount(settings.TALER_CURRENCY, 3))
+ self.the_bank.save()
+
+ user = BankAccount(
+ user=User.objects.create_user(username='U'),
+ amount=Amount(settings.TALER_CURRENCY, 10))
+ user.save()
+
+ # bank: 3, user: 10 (START).
+
+ # bank: 2, user: 11
+ wire_transfer(Amount(settings.TALER_CURRENCY, 1),
+ self.the_bank,
+ user,
+ "mock")
+
+ # bank: 4, user: 9
+ wire_transfer(Amount(settings.TALER_CURRENCY, 2),
+ user,
+ self.the_bank,
+ "mock")
+
+ # bank: -1, user: 14
+ wire_transfer(Amount(settings.TALER_CURRENCY, 5),
+ self.the_bank,
+ user,
+ "mock")
+
+ # bank: 7, user: 6 (END)
+ wire_transfer(Amount(settings.TALER_CURRENCY, 8),
+ user,
+ self.the_bank,
+ "mock")
+
+ # bank: -3, user: 16 (END)
+ wire_transfer(Amount(settings.TALER_CURRENCY, 10),
+ user,
+ self.the_bank,
+ "mock")
+
+
+ self.client = Client()
+
+ def tearDown(self):
+ clear_db()
+
+ def test_balance(self):
+ self.client.login(username="U0",
+ password="U0PASS")
+ response = self.client.get(
+ reverse("history", urlconf=urls),
+ {"auth": "basic",
+ "delta": 30,
+ "direction": "both",
+ "account_number": 55}, # unused
+ **{"HTTP_X_TALER_BANK_USERNAME": "U0",
+ "HTTP_X_TALER_BANK_PASSWORD": "U0PASS"})
+ data = response.content.decode("utf-8")
+ self.assertEqual(response.status_code, 200)
+ entries = json.loads(data)
+
+
+ acc_in = Amount(settings.TALER_CURRENCY)
+ acc_out = Amount(settings.TALER_CURRENCY)
+
+ for entry in entries["data"]:
+ if entry["sign"] == "+":
+ acc_in.add(Amount(**entry["amount"]))
+ if entry["sign"] == "-":
+ acc_out.add(Amount(**entry["amount"]))
+
+ expected_amount = Amount(settings.TALER_CURRENCY, 3)
+ try:
+ debit = False
+ acc_in.subtract(acc_out)
+ expected_amount.add(acc_in)
+ except ValueError:
+ # "out" is bigger than "in"
+ LOGGER.info("out > in")
+ acc_out.subtract(acc_in)
+ try:
+ expected_amount.subtract(acc_out)
+ except ValueError:
+ # initial amount wasn't enough to cover expenses
+ debit = True
+ acc_out.subtract(expected_amount)
+ expected_amount = acc_out
+
+ self.assertEqual(
+ Amount.cmp(expected_amount, self.the_bank.amount), 0)
diff --git a/talerbank/app/tests_alt.py b/talerbank/app/tests_alt.py
index 423ebb4..9f54aa1 100644
--- a/talerbank/app/tests_alt.py
+++ b/talerbank/app/tests_alt.py
@@ -22,8 +22,8 @@ from .amount import Amount, BadFormatAmount
LOGGER = logging.getLogger()
LOGGER.setLevel(logging.WARNING)
-class BadMaxDebtOptionTestCase(TestCase):
- def test_badmaxdebtoption(self):
+class BadMaxDebitOptionTestCase(TestCase):
+ def test_badmaxdebitoption(self):
with self.assertRaises(BadFormatAmount):
Amount.parse(settings.TALER_MAX_DEBT)
Amount.parse(settings.TALER_MAX_DEBT_BANK)
diff --git a/talerbank/app/urls.py b/talerbank/app/urls.py
index c44c7c3..19c0169 100644
--- a/talerbank/app/urls.py
+++ b/talerbank/app/urls.py
@@ -1,16 +1,19 @@
# 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 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.
+# 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/>
+# 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
@@ -20,9 +23,11 @@ from . import views
urlpatterns = [
url(r'^', include('talerbank.urls')),
- url(r'^$', RedirectView.as_view(pattern_name="profile"), name="index"),
+ url(r'^$', RedirectView.as_view(pattern_name="profile"),
+ name="index"),
url(r'^favicon\.ico$', views.ignore),
- url(r'^admin/add/incoming$', views.add_incoming, name="add-incoming"),
+ url(r'^admin/add/incoming$', views.add_incoming,
+ name="add-incoming"),
url(r'^login/$', views.login_view, name="login"),
url(r'^logout/$', views.logout_view, name="logout"),
url(r'^accounts/register/$', views.register, name="register"),
@@ -30,10 +35,15 @@ urlpatterns = [
url(r'^history$', views.serve_history, name="history"),
url(r'^reject$', views.reject, name="reject"),
url(r'^withdraw$', views.withdraw_nojs, name="withdraw-nojs"),
- url(r'^public-accounts$', views.serve_public_accounts,
name="public-accounts"),
- url(r'^public-accounts/(?P<name>[a-zA-Z0-9 ]+)$',
+ url(r'^public-accounts$', views.serve_public_accounts,
+ name="public-accounts"),
+ url(r'^public-accounts/(?P<name>[a-zA-Z0-9]+)$',
+ views.serve_public_accounts,
+ name="public-accounts"),
+ url(r'^public-accounts/(?P<name>[a-zA-Z0-9]+)/(?P<page>[0-9]+)$',
views.serve_public_accounts,
name="public-accounts"),
- url(r'^pin/question$', views.pin_tan_question, name="pin-question"),
+ url(r'^pin/question$', views.pin_tan_question,
+ name="pin-question"),
url(r'^pin/verify$', views.pin_tan_verify, name="pin-verify"),
]
diff --git a/talerbank/app/views.py b/talerbank/app/views.py
index 39d35cf..55686ce 100644
--- a/talerbank/app/views.py
+++ b/talerbank/app/views.py
@@ -1,29 +1,29 @@
# 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 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.
#
-# 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/>
+# 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
# @author Florian Dold
-from urllib.parse import urljoin
from functools import wraps
+import math
import json
import logging
-import time
import hashlib
import random
import re
-import requests
import django.contrib.auth
import django.contrib.auth.views
import django.contrib.auth.forms
@@ -37,27 +37,32 @@ from django.views.decorators.http import
require_http_methods
from django.urls import reverse
from django.contrib.auth.models import User
from django.db.models import Q
-from django.http import (JsonResponse, HttpResponse,
- HttpResponseBadRequest as HRBR)
+from django.http import JsonResponse, HttpResponse
from django.shortcuts import render, redirect
-from validictory.validator import (RequiredFieldValidationError as RFVE,
- FieldValidationError as FVE)
from .models import BankAccount, BankTransaction
-from .amount import Amount, CurrencyMismatch, BadFormatAmount
-from .schemas import (validate_pin_tan_args, check_withdraw_session,
- validate_history_request, validate_incoming_request,
- validate_reject_request)
-
+from .amount import Amount
+from .schemas import validate_data
LOGGER = logging.getLogger(__name__)
-class DebtLimitExceededException(Exception):
- def __init__(self) -> None:
- super().__init__("Debt limit exceeded")
+class LoginFailed(Exception):
+ hint = "Wrong username/password"
+ http_status_code = 401
+
+class DebitLimitException(Exception):
+ hint = "Debit too high, operation forbidden."
+ http_status_code = 403
class SameAccountException(Exception):
- pass
+ hint = "Debit and credit account are the same."
+ http_status_code = 403
-class MyAuthenticationForm(django.contrib.auth.forms.AuthenticationForm):
+class RejectNoRightsException(Exception):
+ hint = "You weren't the transaction credit account, " \
+ "no rights to reject."
+ http_status_code = 403
+
+class TalerAuthenticationForm(
+ django.contrib.auth.forms.AuthenticationForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["username"].widget.attrs["autofocus"] = True
@@ -69,15 +74,16 @@ def ignore(request):
return HttpResponse()
def login_view(request):
- just_logged_out = get_session_flag(request, "just_logged_out")
+ fail_message, success_message, hint = get_session_hint(request,
"login_hint")
response = django.contrib.auth.views.login(
request,
- authentication_form=MyAuthenticationForm,
+ authentication_form=TalerAuthenticationForm,
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
+ response.context_data["fail_message"] = fail_message
+ response.context_data["success_message"] = success_message
+ response.context_data["hint"] = hint
return response
def get_session_flag(request, name):
@@ -85,15 +91,61 @@ def get_session_flag(request, name):
Get a flag from the session and clear it.
"""
if name in request.session:
+ ret = request.session[name]
del request.session[name]
- return True
+ return ret
return False
+def get_session_hint(request, name):
+ """
+ Get a hint from the session and clear it.
+ """
+ if name in request.session:
+ ret = request.session[name]
+ del request.session[name]
+ return ret
+ # Fail message, success message, hint.
+ return False, False, None
+
+
+def predefined_accounts_list():
+ account = 2
+ ret = []
+ for i in settings.TALER_PREDEFINED_ACCOUNTS[1:]:
+ ret.append((account, "%s (#%d)" % (i, account)))
+ account += 1
+ return ret
+
+# Thanks to [1]
+class InputDatalist(forms.TextInput):
+
+ def __init__(self, datalist, name, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self._name = name
+ self._datalist = datalist()
+ self.attrs.update(
+ {"list": "%slist" % name,
+ "pattern": "[1-9]+"})
+
+ def render(self, name, value, attrs=None, renderer=None):
+ html = super().render(
+ name, value, attrs=attrs, renderer=renderer)
+ datalist = '<datalist id="%slist">' % self._name
+ for dl_value, dl_text in self._datalist:
+ datalist += '<option value="%s">%s</option>' \
+ % (dl_value, dl_text)
+ datalist += "</datalist>"
+ return html + datalist
+
class WTForm(forms.Form):
- '''Form used to wire transfer funds internally in the bank.'''
- amount = forms.FloatField(label=settings.TALER_CURRENCY, min_value=0.1)
- counterpart = forms.IntegerField()
+ '''Form used to wire transfer money internally in the bank.'''
+ amount = forms.FloatField(
+ min_value=0.1,
+ widget=forms.NumberInput(attrs={"class": "currency-input"}))
+ receiver = forms.IntegerField(
+ min_value=1,
+ widget=InputDatalist(predefined_accounts_list, "receiver"))
subject = forms.CharField()
# Check if user's logged in. Check if he/she has withdrawn or
@@ -101,62 +153,43 @@ class WTForm(forms.Form):
@login_required
def profile_page(request):
- info_bar = None
if request.method == "POST":
wtf = WTForm(request.POST)
if wtf.is_valid():
amount_parts = (settings.TALER_CURRENCY,
wtf.cleaned_data.get("amount") + 0.0)
- try:
- wire_transfer(Amount.parse("%s:%s" % amount_parts),
- BankAccount.objects.get(
- user=request.user),
- BankAccount.objects.get(
-
account_no=wtf.cleaned_data.get("counterpart")),
- wtf.cleaned_data.get("subject"))
- request.session["just_wire_transferred"] = True
- except BankAccount.DoesNotExist:
- request.session["wire_transfer_error"] = True
- info_bar = "Specified account for receiver does not exist"
- except WireTransferException as exc:
- request.session["wire_transfer_error"] = True
- info_bar = "Internal server error, sorry!"
- if isinstance(exc.exc, SameAccountException):
- info_bar = "Operation not possible: debit and credit
account are the same!"
+ wire_transfer(
+ Amount.parse("%s:%s" % amount_parts),
+ BankAccount.objects.get(user=request.user),
+
BankAccount.objects.get(account_no=wtf.cleaned_data.get("receiver")),
+ wtf.cleaned_data.get("subject"))
+ request.session["profile_hint"] = False, True, "Wire transfer
successful!"
+ return redirect("profile")
wtf = WTForm()
-
- just_wire_transferred = get_session_flag(request, "just_wire_transferred")
- wire_transfer_error = get_session_flag(request, "wire_transfer_error")
- 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")
-
+ fail_message, success_message, hint = get_session_hint(request,
"profile_hint")
context = dict(
- name=user_account.user.username,
- balance=user_account.amount.stringify(settings.TALER_DIGITS),
- sign="-" if user_account.debit else "",
+ name=request.user.username,
+ balance=request.user.bankaccount.amount.stringify(
+ settings.TALER_DIGITS, pretty=True),
+ sign="-" if request.user.bankaccount.debit else "",
+ fail_message=fail_message,
+ success_message=success_message,
+ hint=hint,
precision=settings.TALER_DIGITS,
- currency=user_account.amount.currency,
- account_no=user_account.account_no,
+ currency=request.user.bankaccount.amount.currency,
+ account_no=request.user.bankaccount.account_no,
wt_form=wtf,
- history=history,
- just_withdrawn=just_withdrawn,
- just_registered=just_registered,
- no_initial_bonus=no_initial_bonus,
- just_wire_transferred=just_wire_transferred,
- wire_transfer_error=wire_transfer_error,
- info_bar=info_bar
+ history=extract_history(request.user.bankaccount),
)
if settings.TALER_SUGGESTED_EXCHANGE:
context["suggested_exchange"] = settings.TALER_SUGGESTED_EXCHANGE
response = render(request, "profile_page.html", context)
- if just_withdrawn:
+ if "just_withdrawn" in request.session:
+ del request.session["just_withdrawn"]
response["X-Taler-Operation"] = "confirm-reserve"
- response["X-Taler-Reserve-Pub"] = reserve_pub
+ response["X-Taler-Reserve-Pub"] = request.session.get(
+ "reserve_pub")
response.status_code = 202
return response
@@ -169,51 +202,47 @@ def hash_answer(ans):
def make_question():
num1 = random.randint(1, 10)
- op = random.choice(("*", "+", "-"))
+ operand = random.choice(("*", "+", "-"))
num2 = random.randint(1, 10)
- if op == "*":
+ if operand == "*":
answer = str(num1 * num2)
- elif op == "-":
+ elif operand == "-":
# ensure result is positive
num1, num2 = max(num1, num2), min(num1, num2)
answer = str(num1 - num2)
else:
answer = str(num1 + num2)
- question = "{} {} {}".format(num1, op, num2)
+ question = "{} {} {}".format(num1, operand, num2)
return question, hash_answer(answer)
@require_GET
@login_required
def pin_tan_question(request):
- try:
- validate_pin_tan_args(request.GET.dict())
- # Currency is not checked, as any mismatches will be
- # detected afterwards
- except (FVE, RFVE) as err:
- return HRBR("invalid '%s'" % err.fieldname)
+ validate_data(request, request.GET.dict())
user_account = BankAccount.objects.get(user=request.user)
+ wire_details = json.loads(request.GET["exchange_wire_details"])
request.session["exchange_account_number"] = \
- json.loads(request.GET["wire_details"])["test"]["account_number"]
+ wire_details["test"]["account_number"]
amount = Amount(request.GET["amount_currency"],
int(request.GET["amount_value"]),
int(request.GET["amount_fraction"]))
request.session["amount"] = amount.dump()
- request.session["exchange_url"] = request.GET["exchange"]
request.session["reserve_pub"] = request.GET["reserve_pub"]
- request.session["sender_wiredetails"] = dict(
- type="test",
- bank_uri=request.build_absolute_uri(reverse("index")),
- account_number=user_account.account_no
- )
- previous_failed = get_session_flag(request, "captcha_failed")
+ request.session["sender_wiredetails"] = {
+ "type": "test",
+ "bank_url": request.build_absolute_uri(reverse("index")),
+ "account_number": user_account.account_no}
+ fail_message, success_message, hint = get_session_hint(request,
"captcha_failed")
question, hashed_answer = make_question()
context = dict(
question=question,
hashed_answer=hashed_answer,
amount=amount.stringify(settings.TALER_DIGITS),
- previous_failed=previous_failed,
- exchange=request.GET["exchange"])
+ exchange=request.GET["exchange"],
+ fail_message=fail_message,
+ success_message=success_message,
+ hint=hint)
return render(request, "pin_tan.html", context)
@@ -226,48 +255,18 @@ def pin_tan_verify(request):
LOGGER.warning("Wrong CAPTCHA answer: %s vs %s",
type(hashed_attempt),
type(request.POST.get("pin_1")))
- request.session["captcha_failed"] = True
+ request.session["captcha_failed"] = True, False, "Wrong CAPTCHA
answer."
return redirect(request.POST.get("question_url", "profile"))
# Check the session is a "pin tan" one
- try:
- check_withdraw_session(request.session)
- amount = Amount(**request.session["amount"])
- exchange_bank_account = BankAccount.objects.get(
- account_no=request.session["exchange_account_number"])
- wire_transfer(amount,
- BankAccount.objects.get(user=request.user),
- exchange_bank_account,
- request.session["reserve_pub"],
- request=request,
- session_expand=dict(debt_limit=True))
- except (FVE, RFVE) as exc:
- LOGGER.warning("Not a withdrawing session")
- return redirect("profile")
-
- except BankAccount.DoesNotExist as exc:
- return JsonResponse({"error": "That exchange is unknown to this bank"},
- status=404)
- except WireTransferException as exc:
- return exc.response
- res = requests.post(
- urljoin(request.session["exchange_url"],
- "admin/add/incoming"),
- json={"reserve_pub": request.session["reserve_pub"],
- "execution_date":
- "/Date(" + str(int(time.time())) + ")/",
- "sender_account_details":
- request.session["sender_wiredetails"],
- "transfer_details":
- {"timestamp": int(time.time() * 1000)},
- "amount": amount.dump()})
- if res.status_code != 200:
- return render(request,
- "error_exchange.html",
- {"message": "Could not transfer funds to the exchange. \
- The exchange (%s) gave a bad response.\
- " % request.session["exchange_url"],
- "response_text": res.text,
- "response_status": res.status_code})
+ validate_data(request, request.session)
+ amount = Amount(**request.session["amount"])
+ exchange_bank_account = BankAccount.objects.get(
+ account_no=request.session["exchange_account_number"])
+ wire_transfer(amount,
+ BankAccount.objects.get(user=request.user),
+ exchange_bank_account,
+ request.session["reserve_pub"])
+ request.session["profile_hint"] = False, True, "Withdrawal successful!"
request.session["just_withdrawn"] = True
return redirect("profile")
@@ -284,27 +283,31 @@ def register(request):
return render(request, "register.html")
form = UserReg(request.POST)
if not form.is_valid():
- return render(request, "register.html", dict(wrong_field=True))
+ return render(request, "register.html",
+ {"fail_message": True,
+ "success_message": False,
+ "hint": "Wrong field(s): %s." % ",
".join(form.errors.keys())})
username = form.cleaned_data["username"]
password = form.cleaned_data["password"]
if User.objects.filter(username=username).exists():
- return render(request, "register.html", dict(not_available=True))
+ return render(request, "register.html",
+ {"fail_message": True,
+ "success_message": False,
+ "hint": "Username not available."})
with transaction.atomic():
- user = User.objects.create_user(username=username, password=password)
+ user = User.objects.create_user(
+ username=username,
+ password=password)
user_account = BankAccount(user=user)
user_account.save()
bank_internal_account = BankAccount.objects.get(account_no=1)
- try:
- wire_transfer(Amount(settings.TALER_CURRENCY, 100, 0),
- bank_internal_account,
- user_account,
- "Joining bonus",
- request=request,
- session_expand=dict(no_initial_bobus=True))
- except WireTransferException as exc:
- return exc.response
- request.session["just_registered"] = True
- user = django.contrib.auth.authenticate(username=username,
password=password)
+ wire_transfer(Amount(settings.TALER_CURRENCY, 100, 0),
+ bank_internal_account,
+ user_account,
+ "Joining bonus")
+ request.session["profile_hint"] = False, True, "Registration successful!"
+ user = django.contrib.auth.authenticate(
+ username=username, password=password)
django.contrib.auth.login(request, user)
return redirect("profile")
@@ -314,15 +317,14 @@ def logout_view(request):
Log out the user and redirect to index page.
"""
django.contrib.auth.logout(request)
- request.session["just_logged_out"] = True
+ request.session["login_hint"] = False, True, "Logged out!"
return redirect("index")
-def extract_history(account):
+def extract_history(account, delta=None, start=-1, sign="+"):
history = []
- related_transactions = BankTransaction.objects.filter(
- Q(debit_account=account) | Q(credit_account=account))
- for item in related_transactions:
+ qs = query_history(account, "both", delta, start, sign)
+ for item in qs:
if item.credit_account == account:
counterpart = item.debit_account
sign = ""
@@ -330,34 +332,82 @@ def extract_history(account):
counterpart = item.credit_account
sign = "-"
entry = dict(
+ row_id=item.id,
+ cancelled=item.cancelled,
sign=sign,
- amount=item.amount.stringify(settings.TALER_DIGITS),
+ amount=item.amount.stringify(
+ settings.TALER_DIGITS, pretty=True),
counterpart=counterpart.account_no,
counterpart_username=counterpart.user.username,
subject=item.subject,
- date=item.date.strftime("%d/%m/%y %H:%M"),
- )
+ date=item.date.strftime("%d/%m/%y %H:%M"))
history.append(entry)
return history
-def serve_public_accounts(request, name=None):
+def serve_public_accounts(request, name=None, page=None):
+
+ try:
+ page = int(page)
+ except Exception:
+ page = 1
+
if not name:
name = settings.TALER_PREDEFINED_ACCOUNTS[0]
- try:
- user = User.objects.get(username=name)
- account = BankAccount.objects.get(user=user, is_public=True)
- except (User.DoesNotExist, BankAccount.DoesNotExist):
- return HttpResponse("account '{}' not found".format(name), status=404)
+ user = User.objects.get(username=name)
+
+ if "public_history_count" not in request.session:
+ qs = extract_history(user.bankaccount, sign="-")
+ youngest = -1
+ if qs:
+ youngest = qs[0]["row_id"]
+ request.session["public_history_account"] = \
+ len(qs), youngest
+
+ DELTA = 30
+ youngest = request.session["public_history_account"][1]
+ # normalize page
+ if not page or page in [0, 1]:
+ page = 1
+ # page 0 and 1 give both the youngest 100 records.
+ if page > 1:
+ youngest = youngest - (DELTA * (page - 1)) # goes backwards.
+ if not user.bankaccount.is_public:
+ request.session["public_accounts_hint"] = \
+ True, False, "Could not query private accounts!"
+ fail_message, success_message, hint = \
+ get_session_hint(request, "public_accounts_hint")
public_accounts = BankAccount.objects.filter(is_public=True)
- history = extract_history(account)
+
+ # Retrieve DELTA records older than 'start'.
+ history = extract_history(
+ user.bankaccount, DELTA,
+ -1 if youngest < 2 else youngest, "-")
+
+ num_pages = max(
+ request.session["public_history_account"][0] / DELTA,
+ 1) # makes sure pages[0] exists, below.
+
+ pages = list(
+ range(max(1, page - 3),
+ # need +1 because the range is not inclusive for
+ # the upper limit.
+ min(page + 4, (math.ceil(num_pages) + 1))))
+
context = dict(
+ current_page=page,
+ back = page - 1 if pages[0] > 1 else None,
+ forth = page + 1 if pages[-1] < num_pages else None,
public_accounts=public_accounts,
selected_account=dict(
+ fail_message=fail_message,
+ success_message=success_message,
+ hint=hint,
name=name,
- number=account.account_no,
+ number=user.bankaccount.account_no,
history=history,
- )
+ ),
+ pages=pages
)
return render(request, "public_accounts.html", context)
@@ -366,75 +416,94 @@ def login_via_headers(view_func):
user_account = auth_and_login(request)
if not user_account:
LOGGER.error("authentication failed")
- return JsonResponse(dict(error="authentication failed"),
- status=401)
+ raise LoginFailed("authentication failed")
return view_func(request, user_account, *args, **kwargs)
return wraps(view_func)(_decorator)
+# Internal function used by /history and /public-accounts. It
+# offers abstraction against the query string definition and DB
+# querying.
+#
+# 'bank_account': whose history is going to be retrieved.
+# 'direction': (both|credit|debit|cancel+|cancel-).
+# 'delta': how many results are going to be extracted. If 'None'
+# is given, no filter of this kind will be applied.
+# 'start': a "id" indicating the first record to be returned.
+# If -1 is given, then the first record will be the youngest
+# and 'delta' records will be returned, _regardless_ of the
+# 'sign' being passed.
+# 'sign': (+|-) indicating that we want records younger|older
+# than 'start'.
+
+def query_history(bank_account, direction, delta, start, sign):
+ direction_switch = {
+ "both": Q(debit_account=bank_account) \
+ | Q(credit_account=bank_account),
+ "credit": Q(credit_account=bank_account),
+ "debit": Q(debit_account=bank_account),
+ "cancel+": Q(credit_account=bank_account) \
+ & Q(cancelled=True),
+ "cancel-": Q(debit_account=bank_account) \
+ & Q(cancelled=True)}
+
+ sign_filter = {
+ "+": Q(id__gt=start),
+ "-": Q(id__lt=start),
+ }
+
+ # Handle special case.
+ if start == -1: # return 'delta' youngest records.
+ sign = "+"
+
+ return BankTransaction.objects.filter(
+ direction_switch.get(direction),
+ sign_filter.get(sign)).order_by(
+ # '-id' does descending ordering.
+ "-id" if sign in ["-", "*"] else "id")[:delta]
+
@require_GET
@login_via_headers
def serve_history(request, user_account):
"""
- This API is used to get a list of transactions related to one user.
+ This API is used to get a list of transactions related to one
+ user.
"""
- try:
- # Note, this does check the currency.
- validate_history_request(request.GET.dict())
- except (FVE, RFVE) as exc:
- LOGGER.error("/history, bad '%s' arg" % exc.fieldname)
- return JsonResponse({"error": "invalid '%s'" % exc.fieldname},
- status=400)
-
+ validate_data(request, request.GET.dict())
# delta
parsed_delta = re.search(r"([\+-])?([0-9]+)",
request.GET.get("delta"))
- # start
- start = int(request.GET.get("start", -1))
-
sign = parsed_delta.group(1)
- # Assuming Q() means 'true'
- sign_filter = Q()
- if start >= 0:
- sign_filter = Q(id__gt=start)
- if sign == "-":
- sign_filter = Q(id__lt=start)
+ qs = query_history(user_account.bankaccount,
+ request.GET.get("direction"),
+ int(parsed_delta.group(2)),
+ int(request.GET.get("start", -1)),
+ sign if sign else "+")
- direction_switch = {
- "both": Q(debit_account=user_account.bankaccount) \
- | Q(credit_account=user_account.bankaccount),
- "credit": Q(credit_account=user_account.bankaccount),
- "debit": Q(debit_account=user_account.bankaccount),
- "cancel+": Q(credit_account=user_account.bankaccount) \
- & Q(cancelled=True),
- "cancel-": Q(debit_account=user_account.bankaccount) \
- & Q(cancelled=True)
- }
- # Sanity checks are done at the beginning, so 'direction' key
- # (and its value as switch's key) does exist here.
- query_string = direction_switch[request.GET["direction"]]
history = []
-
- qs = BankTransaction.objects.filter(
- query_string, sign_filter).order_by(
- "-id" if sign == "-" else "id")[:int(parsed_delta.group(2))]
- if qs.count() == 0:
- return HttpResponse(status=204)
+ cancelled = request.GET.get("cancelled", "show")
for entry in qs:
counterpart = entry.credit_account.account_no
sign_ = "-"
- if entry.credit_account.account_no ==
user_account.bankaccount.account_no:
+ if entry.cancelled and cancelled == "omit":
+ continue
+ 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.dump(),
- sign=sign_,
- wt_subject=entry.subject,
- row_id=entry.id,
- date="/Date(" + str(int(entry.date.timestamp())) +
")/"))
+ cancel = "cancel" if entry.cancelled else ""
+ sign_ = cancel + sign_
+ history.append(dict(
+ counterpart=counterpart,
+ amount=entry.amount.dump(),
+ sign=sign_,
+ wt_subject=entry.subject,
+ row_id=entry.id,
+ date="/Date("+str(int(entry.date.timestamp()))+")/"))
+ if not history:
+ return HttpResponse(status=204)
return JsonResponse(dict(data=history), status=200)
-
def auth_and_login(request):
"""Return user instance after checking authentication
credentials, False if errors occur"""
@@ -447,44 +516,32 @@ def auth_and_login(request):
auth_type = request.GET.get("auth")
if auth_type != "basic":
LOGGER.error("auth method not supported")
- return False
+ raise LoginFailed("auth method not supported")
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:
LOGGER.error("user or password not given")
- return False
- return django.contrib.auth.authenticate(username=username,
- password=password)
+ raise LoginFailed("missing user/password")
+ return django.contrib.auth.authenticate(
+ username=username,
+ password=password)
address@hidden
@csrf_exempt
@require_http_methods(["PUT", "POST"])
@login_via_headers
def reject(request, user_account):
data = json.loads(request.body.decode("utf-8"))
- try:
- validate_reject_request(data)
- except (FVE, RFVE) as exc:
- LOGGER.error("invalid %s" % exc.fieldname)
- return JsonResponse({"error": "invalid '%s'" % exc.fieldname},
status=400)
- try:
- trans = BankTransaction.objects.get(id=data["row_id"])
- except BankTransaction.DoesNotExist:
- return JsonResponse({"error": "unknown transaction"}, status=404)
-
- if trans.credit_account.account_no != user_account.bankaccount.account_no:
- LOGGER.error("you can only reject a transaction where you _got_ money")
- return JsonResponse({"error": "you can only reject a transaction where
you _got_ money"},
- status=401) # Unauthorized
- try:
- wire_transfer(trans.amount, user_account.bankaccount,
- trans.debit_account, "/reject: reimbursement",
- cancelled=True)
- except WireTransferException as exc:
- # Logging the error is taken care of wire_transfer()
- return exc.response
-
+ validate_data(request, data)
+ trans = BankTransaction.objects.get(id=data["row_id"])
+ if trans.credit_account.account_no != \
+ user_account.bankaccount.account_no:
+ raise RejectNoRightsException()
+ trans.cancelled = True
+ trans.debit_account.amount.add(trans.amount)
+ trans.credit_account.amount.subtract(trans.amount)
+ trans.save()
return HttpResponse(status=204)
@@ -500,43 +557,26 @@ def add_incoming(request, user_account):
within the browser, and only over the private admin interface.
"""
data = json.loads(request.body.decode("utf-8"))
- try:
- # Note, this does check the currency.
- validate_incoming_request(data)
- except (FVE, RFVE) as exc:
- return JsonResponse({"error": "invalid '%s'" % exc.fieldname},
- status=406 if exc.fieldname == "currency" else 400)
-
-
+ validate_data(request, data)
subject = "%s %s" % (data["subject"], data["exchange_url"])
- try:
- credit_account =
BankAccount.objects.get(account_no=data["credit_account"])
- wtrans = wire_transfer(Amount(**data["amount"]),
- user_account.bankaccount,
- credit_account,
- subject)
- except BankAccount.DoesNotExist:
- return JsonResponse({"error": "credit_account (%d) not found" %
data["credit_account"]},
- status=404)
- except WireTransferException as exc:
- return exc.response
- return JsonResponse({"row_id": wtrans.id,
- "timestamp":
- "/Date(%s)/" % int(wtrans.date.timestamp())})
+ credit_account = BankAccount.objects.get(
+ account_no=data["credit_account"])
+ wtrans = wire_transfer(Amount.parse(data["amount"]),
+ user_account.bankaccount,
+ credit_account,
+ subject)
+ return JsonResponse(
+ {"row_id": wtrans.id,
+ "timestamp": "/Date(%s)/" % int(wtrans.date.timestamp())})
@login_required
@require_POST
def withdraw_nojs(request):
- try:
- amount = Amount.parse(request.POST.get("kudos_amount", ""))
- except BadFormatAmount:
- LOGGER.error("Amount did not pass parsing")
- return HRBR()
-
+ amount = Amount.parse(
+ request.POST.get("kudos_amount", "not-given"))
user_account = BankAccount.objects.get(user=request.user)
-
response = HttpResponse(status=202)
response["X-Taler-Operation"] = "create-reserve"
response["X-Taler-Callback-Url"] = reverse("pin-question")
@@ -544,97 +584,73 @@ def withdraw_nojs(request):
response["X-Taler-Amount"] = json.dumps(amount.dump())
response["X-Taler-Sender-Wire"] = json.dumps(dict(
type="test",
- bank_uri=request.build_absolute_uri(reverse("index")),
+ bank_url=request.build_absolute_uri(reverse("index")),
account_number=user_account.account_no
))
if settings.TALER_SUGGESTED_EXCHANGE:
- response["X-Taler-Suggested-Exchange"] =
settings.TALER_SUGGESTED_EXCHANGE
+ response["X-Taler-Suggested-Exchange"] = \
+ settings.TALER_SUGGESTED_EXCHANGE
return response
-class WireTransferException(Exception):
- def __init__(self, exc, response):
- self.exc = exc
- self.response = response
- super().__init__()
-
-def wire_transfer(amount, debit_account, credit_account, subject, **kwargs):
-
- def err_cb(exc, resp):
- LOGGER.error(str(exc))
- raise WireTransferException(exc, resp)
-
- def wire_transfer_internal(amount, debit_account, credit_account, subject):
- LOGGER.info("%s => %s, %s, %s" %
- (debit_account.account_no,
- credit_account.account_no,
- amount.stringify(2),
- subject))
- if debit_account.pk == credit_account.pk:
- LOGGER.error("Debit and credit account are the same!")
- raise SameAccountException()
-
- transaction_item = BankTransaction(amount=amount,
- credit_account=credit_account,
- debit_account=debit_account,
- subject=subject,
- cancelled=kwargs.get("cancelled",
False))
- if debit_account.debit:
- debit_account.amount.add(amount)
-
- elif -1 == Amount.cmp(debit_account.amount, amount):
- debit_account.debit = True
- tmp = Amount(**amount.dump())
- tmp.subtract(debit_account.amount)
- debit_account.amount.set(**tmp.dump())
- else:
- debit_account.amount.subtract(amount)
-
- if not credit_account.debit:
- credit_account.amount.add(amount)
- elif Amount.cmp(amount, credit_account.amount) == 1:
- credit_account.debit = False
- tmp = Amount(**amount.dump())
- tmp.subtract(credit_account.amount)
- credit_account.amount.set(**tmp.dump())
- else:
- credit_account.amount.subtract(amount)
+def wire_transfer(amount, debit_account, credit_account,
+ subject):
+ LOGGER.info("%s => %s, %s, %s" %
+ (debit_account.account_no,
+ credit_account.account_no,
+ amount.stringify(2),
+ subject))
+ if debit_account.pk == credit_account.pk:
+ LOGGER.error("Debit and credit account are the same!")
+ raise SameAccountException()
+
+ transaction_item = BankTransaction(
+ amount=amount,
+ credit_account=credit_account,
+ debit_account=debit_account,
+ subject=subject)
+ if debit_account.debit:
+ debit_account.amount.add(amount)
+
+ elif -1 == Amount.cmp(debit_account.amount, amount):
+ debit_account.debit = True
+ tmp = Amount(**amount.dump())
+ tmp.subtract(debit_account.amount)
+ debit_account.amount.set(**tmp.dump())
+ else:
+ debit_account.amount.subtract(amount)
+
+ if not credit_account.debit:
+ credit_account.amount.add(amount)
+ elif Amount.cmp(amount, credit_account.amount) == 1:
+ credit_account.debit = False
+ tmp = Amount(**amount.dump())
+ tmp.subtract(credit_account.amount)
+ credit_account.amount.set(**tmp.dump())
+ else:
+ credit_account.amount.subtract(amount)
- # Check here if any account went beyond the allowed
- # debit threshold.
+ # Check here if any account went beyond the allowed
+ # debit threshold.
- threshold = Amount.parse(settings.TALER_MAX_DEBT)
- if debit_account.user.username == "Bank":
- threshold = Amount.parse(settings.TALER_MAX_DEBT_BANK)
- if Amount.cmp(debit_account.amount, threshold) == 1 \
- and Amount.cmp(Amount(settings.TALER_CURRENCY), threshold) != 0 \
+ threshold = Amount.parse(settings.TALER_MAX_DEBT)
+ if debit_account.user.username == "Bank":
+ threshold = Amount.parse(settings.TALER_MAX_DEBT_BANK)
+ if Amount.cmp(debit_account.amount, threshold) == 1 \
+ and Amount.cmp(Amount(settings.TALER_CURRENCY),
+ threshold) != 0 \
and debit_account.debit:
- LOGGER.info("Negative balance '%s' not allowed.\
- " % json.dumps(debit_account.amount.dump()))
- LOGGER.info("%s's threshold is: '%s'.\
- " % (debit_account.user.username,
json.dumps(threshold.dump())))
- raise DebtLimitExceededException()
+ LOGGER.info("Negative balance '%s' not allowed.\
+ " % json.dumps(debit_account.amount.dump()))
+ LOGGER.info("%s's threshold is: '%s'." \
+ % (debit_account.user.username,
+ json.dumps(threshold.dump())))
+ raise DebitLimitException()
- with transaction.atomic():
- debit_account.save()
- credit_account.save()
- transaction_item.save()
+ with transaction.atomic():
+ debit_account.save()
+ credit_account.save()
+ transaction_item.save()
- return transaction_item
+ return transaction_item
- try:
- return wire_transfer_internal(amount, debit_account, credit_account,
subject)
- except (CurrencyMismatch, BadFormatAmount) as exc:
- err_cb(exc, JsonResponse({"error": "internal server error"},
- status=500))
- except DebtLimitExceededException as exc:
- if kwargs.get("request"):
- if kwargs.get("session_expand"):
- kwargs["request"].session.update(kwargs["session_expand"])
- if kwargs["request"].request.path == "/pin/verify":
- err_cb(exc, redirect("profile"))
- else:
- err_cb(exc, JsonResponse({"error": "Unallowed debit"},
- status=403))
- except SameAccountException as exc:
- err_cb(exc, JsonResponse({"error": "sender account == receiver
account"},
- status=422))
+# [1]
https://stackoverflow.com/questions/24783275/django-form-with-choices-but-also-with-freetext-option
diff --git a/talerbank/settings.py b/talerbank/settings.py
index 6380937..e8f226d 100644
--- a/talerbank/settings.py
+++ b/talerbank/settings.py
@@ -18,12 +18,15 @@ from .talerconfig import TalerConfig, ConfigurationError
LOGGER = logging.getLogger(__name__)
-LOGGER.info("DJANGO_SETTINGS_MODULE: %s" %
os.environ.get("DJANGO_SETTINGS_MODULE"))
+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__)))
+# 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/
@@ -32,8 +35,9 @@ BASE_DIR =
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
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")
+ LOGGER.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!
@@ -67,18 +71,17 @@ MIDDLEWARE = [
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
+ 'talerbank.app.middleware.ExceptionMiddleware',
]
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',
- },
- },
-]
+ {'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.
@@ -96,16 +99,20 @@ DBNAME = TC.value_string("bank", "database", required=True)
DBNAME = os.environ.get("TALER_BANK_ALTDB", DBNAME)
if not DBNAME:
- raise Exception("DB not specified (neither in config or as cli argument)")
+ 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)
+CHECK_DBSTRING_FORMAT = re.search(
+ r"[a-z]+:///[a-z]+([\?][a-z]+=[a-z/]+)?", DBNAME)
if not CHECK_DBSTRING_FORMAT:
- LOGGER.error("Bad db string given, respect the format 'dbtype:///dbname'")
+ LOGGER.error("Bad db string given '%s', respect the format" \
+ "'dbtype:///dbname'" % DBNAME)
sys.exit(2)
DBCONFIG = {}
+# Maybe trust the parsing from urlparse?
DB_URL = urllib.parse.urlparse(DBNAME)
if DB_URL.scheme not in ("postgres") or DB_URL.scheme == "":
@@ -125,7 +132,7 @@ else:
HOST = DB_URL.netloc
if HOST:
- DBCONFIG["HOST"] = HOST
+ DBCONFIG["HOST"] = HOST # Sockets directory.
DATABASES["default"] = DBCONFIG
@@ -133,19 +140,14 @@ DATABASES["default"] = DBCONFIG
# 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',
- },
-]
+ {'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
@@ -172,24 +174,30 @@ STATICFILES_DIRS = [
os.path.join(BASE_DIR, "talerbank/app/static/web-common"),
]
-# 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"
try:
- TALER_CURRENCY = TC.value_string("taler", "currency", required=True)
+ TALER_CURRENCY = TC.value_string(
+ "taler", "currency", required=True)
except ConfigurationError as exc:
LOGGER.error(exc)
sys.exit(3)
-TALER_MAX_DEBT = TC.value_string("bank", "MAX_DEBT", default="%s:50.0" %
TALER_CURRENCY)
-TALER_MAX_DEBT_BANK = TC.value_string("bank", "MAX_DEBT_BANK",
default="%s:0.0" % TALER_CURRENCY)
-
-TALER_DIGITS = TC.value_int("bank", "NDIGITS", default=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")
+TALER_MAX_DEBT = TC.value_string(
+ "bank", "MAX_DEBT",
+ default="%s:50.0" % TALER_CURRENCY)
+TALER_MAX_DEBT_BANK = TC.value_string(
+ "bank", "MAX_DEBT_BANK",
+ default="%s:0.0" % TALER_CURRENCY)
+
+TALER_DIGITS = TC.value_int(
+ "bank", "NDIGITS", default=2)
+# Order matters
+TALER_PREDEFINED_ACCOUNTS = [
+ 'Bank', 'Exchange', 'Tor', 'GNUnet',
+ 'Taler', 'FSF', 'Tutorial', 'Survey']
+TALER_EXPECTS_DONATIONS = [
+ 'Tor', 'GNUnet', 'Taler', 'FSF']
+TALER_SUGGESTED_EXCHANGE = TC.value_string(
+ "bank", "suggested_exchange")
--
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 (e3d5117 -> 860d826),
gnunet <=