gnunet-svn
[Top][All Lists]
Advanced

[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) }}">&lsaquo;...</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) }}">...&rsaquo;</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



reply via email to

[Prev in Thread] Current Thread [Next in Thread]