summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2020-01-16 18:11:35 +0100
committerFlorian Dold <florian.dold@gmail.com>2020-01-16 18:11:40 +0100
commit47ff439abf85ecea69d7f281ca30a00143c8a868 (patch)
treef6bde35d3dd4fbe962c726b3dc2e032adfe7f223
parent55addfc68f26f50425e3af84972f4d342a99939f (diff)
downloadbank-47ff439abf85ecea69d7f281ca30a00143c8a868.tar.gz
bank-47ff439abf85ecea69d7f281ca30a00143c8a868.tar.bz2
bank-47ff439abf85ecea69d7f281ca30a00143c8a868.zip
preparations towards the new bank API
-rw-r--r--Makefile2
-rwxr-xr-xbin/taler-bank-manage3
-rwxr-xr-xrun-tests.sh2
-rwxr-xr-xsetup.py2
-rw-r--r--talerbank/__init__.py2
-rw-r--r--talerbank/app/management/commands/add_bank_account.py12
-rw-r--r--talerbank/app/management/commands/dump_talerdb.py6
-rw-r--r--talerbank/app/management/commands/provide_accounts.py7
-rw-r--r--talerbank/app/management/commands/top_up.py14
-rw-r--r--talerbank/app/management/commands/wire_transfer.py23
-rw-r--r--talerbank/app/middleware.py104
-rw-r--r--talerbank/app/migrations/0001_initial.py94
-rw-r--r--talerbank/app/models.py242
-rw-r--r--talerbank/app/schemas.py52
-rw-r--r--talerbank/app/templates/profile_page.html46
-rw-r--r--talerbank/app/tests.py691
-rw-r--r--talerbank/app/urls.py29
-rw-r--r--talerbank/app/views.py273
-rw-r--r--talerbank/jinja2.py20
-rw-r--r--talerbank/settings.py110
20 files changed, 644 insertions, 1090 deletions
diff --git a/Makefile b/Makefile
index b416c64..21caedf 100644
--- a/Makefile
+++ b/Makefile
@@ -36,4 +36,4 @@ dist:
.PHONY: pretty
pretty:
- yapf -r -i talerbank/
+ black talerbank/
diff --git a/bin/taler-bank-manage b/bin/taler-bank-manage
index 4397bae..45047a4 100755
--- a/bin/taler-bank-manage
+++ b/bin/taler-bank-manage
@@ -65,8 +65,11 @@ def handle_django(args):
def handle_serve_http(args):
import django
django.setup()
+ print("migrating")
call_command('migrate')
+ print("providing accounts")
call_command('provide_accounts')
+ print("checking")
call_command('check')
port = args.port
TC = TalerConfig.from_file(os.environ.get("TALER_CONFIG_FILE", args.config))
diff --git a/run-tests.sh b/run-tests.sh
index 62ccabf..d30faea 100755
--- a/run-tests.sh
+++ b/run-tests.sh
@@ -3,4 +3,4 @@
# See Bug #5850 for some tests that are currently skipped
export TALER_CONFIG_FILE="talerbank/app/testconfigs/bank-check.conf"
-./manage.py test --no-input talerbank.app.tests
+exec ./manage.py test --no-input talerbank.app.tests "$@"
diff --git a/setup.py b/setup.py
index 24a7193..18cbbf8 100755
--- a/setup.py
+++ b/setup.py
@@ -10,7 +10,7 @@ setup(name='talerbank',
author_email='stanisci.m@gmail.com, flo@dold.me',
license='GPL',
packages=find_packages(),
- install_requires=["django>=2",
+ install_requires=["django>=3",
"psycopg2",
"requests",
"uWSGI",
diff --git a/talerbank/__init__.py b/talerbank/__init__.py
index 6c4125c..80c8cd8 100644
--- a/talerbank/__init__.py
+++ b/talerbank/__init__.py
@@ -1,4 +1,4 @@
import logging
-FMT = '%(asctime)-15s %(module)s %(levelname)s %(message)s'
+FMT = "%(asctime)-15s %(module)s %(levelname)s %(message)s"
logging.basicConfig(format=FMT, level=logging.WARNING)
diff --git a/talerbank/app/management/commands/add_bank_account.py b/talerbank/app/management/commands/add_bank_account.py
index 4378e44..4c4aacf 100644
--- a/talerbank/app/management/commands/add_bank_account.py
+++ b/talerbank/app/management/commands/add_bank_account.py
@@ -40,8 +40,12 @@ class Command(BaseCommand):
help = "Add bank accounts."
def add_arguments(self, parser):
- parser.add_argument("accountname", type=str, help="Login name of the new bank account")
- parser.add_argument("--public", action="store_true", help="Make the bank account public")
+ parser.add_argument(
+ "accountname", type=str, help="Login name of the new bank account"
+ )
+ parser.add_argument(
+ "--public", action="store_true", help="Make the bank account public"
+ )
##
# Django-specific definition to invoke the account creator
@@ -56,4 +60,6 @@ class Command(BaseCommand):
),
is_public=False,
).save()
- print(f"Created new bank account {accountname} (password set to random password)")
+ print(
+ f"Created new bank account {accountname} (password set to random password)"
+ )
diff --git a/talerbank/app/management/commands/dump_talerdb.py b/talerbank/app/management/commands/dump_talerdb.py
index 92849eb..bd40064 100644
--- a/talerbank/app/management/commands/dump_talerdb.py
+++ b/talerbank/app/management/commands/dump_talerdb.py
@@ -37,9 +37,7 @@ 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):
LOGGER.error("Hard database error, does it exist?")
sys.exit(1)
@@ -56,7 +54,7 @@ def dump_history():
msg.append("-%s, " % item.debit_account.account_no)
msg.append(item.amount.stringify(2))
msg.append(" '" + item.subject + "'")
- print(''.join(msg))
+ print("".join(msg))
except (OperationalError, ProgrammingError):
LOGGER.error("Hard database error, does it exist?")
sys.exit(1)
diff --git a/talerbank/app/management/commands/provide_accounts.py b/talerbank/app/management/commands/provide_accounts.py
index ce84835..8e729e3 100644
--- a/talerbank/app/management/commands/provide_accounts.py
+++ b/talerbank/app/management/commands/provide_accounts.py
@@ -46,10 +46,9 @@ def make_account(username):
LOGGER.info("Creating account for '%s'", username)
BankAccount(
user=User.objects.create_user(
- username=username,
- password=str(uuid.uuid4())
+ username=username, password=str(uuid.uuid4())
),
- is_public=True
+ is_public=True,
).save()
except (OperationalError, ProgrammingError):
@@ -58,7 +57,7 @@ def make_account(username):
" is not migrated. Try 'taler-bank-manage"
" django migrate' in the latter case.",
stack_info=False,
- exc_info=True
+ exc_info=True,
)
sys.exit(1)
diff --git a/talerbank/app/management/commands/top_up.py b/talerbank/app/management/commands/top_up.py
index c0da7e7..88cca0a 100644
--- a/talerbank/app/management/commands/top_up.py
+++ b/talerbank/app/management/commands/top_up.py
@@ -44,12 +44,14 @@ class Command(BaseCommand):
"user",
type=str,
metavar="USERNAME",
- help="User that is getting credited with the top-up"
+ help="User that is getting credited with the top-up",
)
parser.add_argument(
- "amount", type=str, metavar="AMOUNT",
- help="Wire transfer's amount, given in the " \
- "CURRENCY:X.Y form.")
+ "amount",
+ type=str,
+ metavar="AMOUNT",
+ help="Wire transfer's amount, given in the " "CURRENCY:X.Y form.",
+ )
##
# Django-specific definition to invoke the account creator
@@ -60,9 +62,7 @@ class Command(BaseCommand):
user = User.objects.get(username=options["user"])
# Take the money from the bank's account
try:
- debit_account = BankAccount.objects.get(
- account_no=1,
- )
+ debit_account = BankAccount.objects.get(account_no=1,)
except BankAccount.DoesNotExist:
LOGGER.error("Debit account (bank's own account) does not exist.")
sys.exit(1)
diff --git a/talerbank/app/management/commands/wire_transfer.py b/talerbank/app/management/commands/wire_transfer.py
index bf6bef1..08372ac 100644
--- a/talerbank/app/management/commands/wire_transfer.py
+++ b/talerbank/app/management/commands/wire_transfer.py
@@ -46,30 +46,29 @@ class Command(BaseCommand):
"user",
type=str,
metavar="USERNAME",
- help="Which user is performing the wire transfer"
+ help="Which user is performing the wire transfer",
)
parser.add_argument(
- "password",
- type=str,
- metavar="PASSWORD",
- help="Performing user's password."
+ "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."
+ help="Which account number will *receive* money.",
)
parser.add_argument(
"subject",
type=str,
metavar="SUBJECT",
- help="SUBJECT will be the wire transfer 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.")
+ "amount",
+ type=str,
+ metavar="AMOUNT",
+ help="Wire transfer's amount, given in the " "CURRENCY:X.Y form.",
+ )
##
# This callable gets invoked when the user invokes the
@@ -80,9 +79,7 @@ class Command(BaseCommand):
# @param args arguments list -- currently unused.
# @param options options given by the user at the command line.
def handle(self, *args, **options):
- user = authenticate(
- username=options["user"], password=options["password"]
- )
+ user = authenticate(username=options["user"], password=options["password"])
if not user:
LOGGER.error("Wrong user/password.")
sys.exit(1)
diff --git a/talerbank/app/middleware.py b/talerbank/app/middleware.py
index eb34b96..e335a1a 100644
--- a/talerbank/app/middleware.py
+++ b/talerbank/app/middleware.py
@@ -5,21 +5,18 @@ from . import urls
from django.http import JsonResponse
from django.urls import reverse
from django.shortcuts import redirect
-from .models import BankAccount, BankTransaction, \
- BankAccountDoesNotExist, BankTransactionDoesNotExist
-from .views import \
- (DebitLimitException, SameAccountException,
- LoginFailed, RejectNoRightsException, UnhandledException,
- set_profile_hint)
-
-from .schemas import \
- (JSONFieldException,
- URLParamValidationError,
- InvalidSession)
-
-from taler.util.amount import \
- (CurrencyMismatch, BadFormatAmount,
- NumberTooBig, NegativeNumber)
+from .models import BankAccount, BankTransaction
+from .views import (
+ DebitLimitException,
+ SameAccountException,
+ LoginFailed,
+ UnhandledException,
+ set_profile_hint,
+)
+
+from .schemas import JSONFieldException, URLParamValidationError, InvalidSession
+
+from taler.util.amount import CurrencyMismatchError, AmountFormatError
LOGGER = logging.getLogger()
@@ -56,65 +53,56 @@ class DecompressionMiddleware:
return self.get_response(request)
-##
-# Class holding data needed by the handling logic.
+
class ExceptionMiddleware:
+ """
+ Middleware for handling exceptions not caught directly
+ by the application logic.
+ """
- ##
- # Init constructor.
- #
- # @param self the object itself.
- # @param get_response a Django-provided callable that calls
- # whatever comes next in the chain: a further middleware
- # or the view itself (please refer to the official
- # documentation for more details).
def __init__(self, get_response):
+ """
+ # Init constructor.
+ #
+ # @param self the object itself.
+ # @param get_response a Django-provided callable that calls
+ # whatever comes next in the chain: a further middleware
+ # or the view itself (please refer to the official
+ # documentation for more details).
+ """
self.get_response = get_response
# Map between endpoints and Web pages to render
# after the exception gets managed.
self.render = {
reverse("profile", urlconf=urls): "profile",
- reverse("register", urlconf=urls): "index",
+ reverse("register", urlconf=urls): "index",
reverse("public-accounts", urlconf=urls): "index",
}
- ##
- # This function is transparently invoked by Django when
- # a request traverses the chain made of middleware classes
- # and the view itself as the last element in the chain.
- #
- # @param self this class.
- # @param request Django-specific request object (of the same
- # type that is handed to views).
- # @return Django-specific response object.
def __call__(self, request):
+ """
+ This function is transparently invoked by Django when
+ a request traverses the chain made of middleware classes
+ and the view itself as the last element in the chain.
+ """
return self.get_response(request)
- ##
- # Main logic for processing the exception. It checks
- # if the exception captured can be managed, and does it
- # if so. Otherwise, it lets the native handler operate.
- #
- # @param self a @a ExceptionMiddleware object.
- # @param request Django-specific HTTP request.
- # @param exception the exception raised from the bank.
def process_exception(self, request, exception):
+ """
+ Main logic for processing the exception. It checks
+ if the exception captured can be managed, and does it
+ if so. Otherwise, it lets the native handler operate.
+ """
LOGGER.error(f"Error: {exception}, while serving {request.get_full_path()}")
- if not hasattr(exception, "taler_error_code"):
- print("####### Exception without Taler Error Code ########")
- traceback.print_exc()
- print("###################################################")
- exception = UnhandledException()
-
- render_to = self.render.get(request.path)
-
- if not render_to:
- return JsonResponse({"ec": exception.taler_error_code,
- "error": exception.hint},
- status=exception.http_status_code)
- set_profile_hint(request, failure=True, success=False, hint=exception.hint)
- return redirect(render_to)
+ if hasattr(exception, "taler_error_code"):
+ render_to = self.render.get(request.path)
-# [1] https://git.taler.net/exchange.git/tree/src/include/taler_error_codes.h
+ if not render_to:
+ return JsonResponse(
+ {"ec": exception.taler_error_code, "error": exception.hint},
+ status=exception.http_status_code,
+ )
+ set_profile_hint(request, failure=True, success=False, hint=exception.hint)
+ return redirect(render_to)
diff --git a/talerbank/app/migrations/0001_initial.py b/talerbank/app/migrations/0001_initial.py
index e6ca081..91c0e98 100644
--- a/talerbank/app/migrations/0001_initial.py
+++ b/talerbank/app/migrations/0001_initial.py
@@ -1,4 +1,4 @@
-# Generated by Django 2.2.4 on 2019-08-27 18:55
+# Generated by Django 3.0.2 on 2020-01-13 12:57
from django.conf import settings
from django.db import migrations, models
@@ -17,100 +17,94 @@ class Migration(migrations.Migration):
operations = [
migrations.CreateModel(
- name='BankAccount',
+ name="BankAccount",
fields=[
- ('is_public', models.BooleanField(default=False)),
- ('debit', models.BooleanField(default=False)),
+ ("is_public", models.BooleanField(default=False)),
+ ("account_no", models.AutoField(primary_key=True, serialize=False)),
(
- 'account_no',
- models.AutoField(primary_key=True, serialize=False)
+ "balance",
+ talerbank.app.models.SignedAmountField(
+ default=talerbank.app.models.get_zero_signed_amount
+ ),
),
(
- 'amount',
- talerbank.app.models.AmountField(
- default=talerbank.app.models.get_zero_amount
- )
- ),
- (
- 'user',
+ "user",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
- to=settings.AUTH_USER_MODEL
- )
+ to=settings.AUTH_USER_MODEL,
+ ),
),
],
),
migrations.CreateModel(
- name='TalerWithdrawOperation',
+ name="TalerWithdrawOperation",
fields=[
(
- 'withdraw_id',
+ "withdraw_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
- serialize=False
- )
+ serialize=False,
+ ),
),
- ('amount', talerbank.app.models.AmountField(default=False)),
- ('selection_done', models.BooleanField(default=False)),
- ('withdraw_done', models.BooleanField(default=False)),
- ('selected_reserve_pub', models.TextField(null=True)),
+ ("amount", talerbank.app.models.AmountField(default=False)),
+ ("selection_done", models.BooleanField(default=False)),
+ ("withdraw_done", models.BooleanField(default=False)),
+ ("selected_reserve_pub", models.TextField(null=True)),
(
- 'selected_exchange_account',
+ "selected_exchange_account",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
- related_name='selected_exchange_account',
- to='app.BankAccount'
- )
+ related_name="selected_exchange_account",
+ to="app.BankAccount",
+ ),
),
(
- 'withdraw_account',
+ "withdraw_account",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
- related_name='withdraw_account',
- to='app.BankAccount'
- )
+ related_name="withdraw_account",
+ to="app.BankAccount",
+ ),
),
],
),
migrations.CreateModel(
- name='BankTransaction',
+ name="BankTransaction",
fields=[
(
- 'id',
+ "id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
- verbose_name='ID'
- )
+ verbose_name="ID",
+ ),
),
- ('amount', talerbank.app.models.AmountField(default=False)),
+ ("amount", talerbank.app.models.AmountField(default=False)),
(
- 'subject',
- models.CharField(
- default='(no subject given)', max_length=200
- )
+ "subject",
+ models.CharField(default="(no subject given)", max_length=200),
),
- ('date', models.DateTimeField(auto_now=True, db_index=True)),
- ('cancelled', models.BooleanField(default=False)),
+ ("date", models.DateTimeField(auto_now=True, db_index=True)),
+ ("cancelled", models.BooleanField(default=False)),
(
- 'credit_account',
+ "credit_account",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
- related_name='credit_account',
- to='app.BankAccount'
- )
+ related_name="credit_account",
+ to="app.BankAccount",
+ ),
),
(
- 'debit_account',
+ "debit_account",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
- related_name='debit_account',
- to='app.BankAccount'
- )
+ related_name="debit_account",
+ to="app.BankAccount",
+ ),
),
],
),
diff --git a/talerbank/app/models.py b/talerbank/app/models.py
index f869ea3..08da111 100644
--- a/talerbank/app/models.py
+++ b/talerbank/app/models.py
@@ -23,179 +23,185 @@ 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, \
- ObjectDoesNotExist
-from taler.util.amount import Amount, BadFormatAmount, NumberTooBig, CurrencyMismatch
+from django.core.exceptions import ValidationError, ObjectDoesNotExist
+from taler.util.amount import Amount, SignedAmount, CurrencyMismatchError
-class InvalidAmount(Amount):
- def __init__(self, currency):
- super(InvalidAmount, self
- ).__init__(currency, value=float('nan'), fraction=float('nan'))
- def stringify(self, ndigits, pretty):
- return "Invalid Amount, please report"
+def get_zero_amount() -> Amount:
+ """
+ Helper function that instantiates a zero-valued Amount
+ object in the currency that the bank runs on.
+ """
+ return Amount(settings.TALER_CURRENCY, 0, 0)
- def dump(self):
- return "Invalid Amount, please report"
+def get_zero_signed_amount() -> SignedAmount:
+ """
+ Helper function that instantiates a zero-valued SignedAmount
+ object in the currency that the bank runs on.
+ """
+ return SignedAmount(True, get_zero_amount())
-##
-# Helper function that instantiates a zero-valued @a Amount
-# object.
-def get_zero_amount() -> Amount:
- return Amount(settings.TALER_CURRENCY)
+
+class SignedAmountField(models.Field):
+ """Custom implementation of the SignedAmount class as a database type."""
+
+ description = "Signed amount object in Taler style"
+
+ def db_type(self, connection: Any) -> str:
+ """
+ Return the database type of the serialized amount.
+ """
+ return "varchar"
+
+ def get_prep_value(self, value: SignedAmount) -> str:
+ """
+ Stringifies the Amount object to feed the DB connector.
+ """
+ c = value.amount.currency
+ if settings.TALER_CURRENCY != c:
+ raise CurrencyMismatchError(settings.TALER_CURRENCY, c)
+ return value.stringify()
+
+ @staticmethod
+ def from_db_value(value: str, *args) -> Amount:
+ """
+ Parse the stringified Amount back to Python.
+
+ Parameters
+ ----------
+ value : str
+ Serialized amount coming from the database.
+ (String in the usual CURRENCY:X.Y format)
+ args : any
+ Unused
+ """
+ del args # pacify PEP checkers
+ return SignedAmount.parse(value)
+
+ def to_python(self, value: Any) -> Amount:
+ """
+ Parse the stringified Amount back to Python. FIXME:
+ why this serializer consider _more_ cases respect to the
+ one above ('from_db_value')?
+
+ Parameters
+ ----------
+ value: serialized amount coming from the database
+
+ """
+
+ if isinstance(value, SignedAmount):
+ return value
+ try:
+ return SignedAmount.parse(value)
+ except BadFormatAmount:
+ raise ValidationError(
+ "Invalid input for a signed amount string: %s" % value
+ )
-##
-# Custom implementation of the @a Amount class as a database type.
class AmountField(models.Field):
- description = 'Amount object in Taler style'
-
- ##
- # Return the database type of the serialized amount.
- #
- # @param self the object itself.
- # @param connection the database connection.
- # @return type of the serialized amount: varchar.
+ """Custom implementation of the Amount class as a database type."""
+
+ description = "Amount object in Taler style"
+
def db_type(self, connection: Any) -> str:
+ """
+ Return the database type of the serialized amount.
+ """
return "varchar"
- ##
- # Stringifies the Amount object to feed the DB connector.
- #
- # @param self the object itself.
- # @para value the @a Amount object to be serialized.
def get_prep_value(self, value: Amount) -> str:
- if not value:
- return "%s:0.0" % settings.TALER_CURRENCY
+ """
+ Stringifies the Amount object to feed the DB connector.
+ """
if settings.TALER_CURRENCY != value.currency:
- raise CurrencyMismatch(settings.TALER_CURRENCY, value.currency)
- return value.stringify(settings.TALER_DIGITS)
-
- ##
- # Parse the stringified Amount back to Python.
- #
- # @param value serialized amount coming from the database.
- # (It is just a string in the usual CURRENCY:X.Y form)
- # @param args currently unused.
- # @return the @a Amount object.
+ raise CurrencyMismatchError(settings.TALER_CURRENCY, value.currency)
+ return value.stringify()
+
@staticmethod
def from_db_value(value: str, *args) -> Amount:
+ """
+ Parse the stringified Amount back to Python.
+
+ Parameters
+ ----------
+ value : str
+ Serialized amount coming from the database.
+ (String in the usual CURRENCY:X.Y format)
+ args : any
+ Unused
+ """
del args # pacify PEP checkers
- if value is None:
- return Amount.parse(settings.TALER_CURRENCY)
- try:
- return Amount.parse(value)
- except NumberTooBig:
- # Keep the currency right to avoid causing
- # exceptions if some operation is attempted
- # against this invalid amount. NOTE that the
- # value is defined as NaN, so no actual/useful
- # amount will ever be generated using this one.
- # And more, the NaN value will make it easier
- # to scan the database to find these faulty
- # amounts.
- # We also decide to not raise exception here
- # because they would propagate in too many places
- # in the code, and it would be too verbose to
- # just try-cactch any possible exception situation.
- return InvalidAmount(settings.TALER_CURRENCY)
-
- ##
- # Parse the stringified Amount back to Python. FIXME:
- # why this serializer consider _more_ cases respect to the
- # one above ('from_db_value')?
- #
- # @param value serialized amount coming from the database.
- # (It is just a string in the usual CURRENCY:X.Y form)
- # @param args currently unused.
- # @return the @a Amount object.
+ return Amount.parse(value)
+
def to_python(self, value: Any) -> Amount:
+ """
+ Parse the stringified Amount back to Python. FIXME:
+ why this serializer consider _more_ cases respect to the
+ one above ('from_db_value')?
+
+ Parameters
+ ----------
+ value: serialized amount coming from the database
+
+ """
+
if isinstance(value, Amount):
return value
try:
- if value is None:
- return Amount.parse(settings.TALER_CURRENCY)
return Amount.parse(value)
except BadFormatAmount:
raise ValidationError("Invalid input for an amount string: %s" % value)
-class BankAccountDoesNotExist(Exception):
- def __init__(self, msg):
- super(BankAccountDoesNotExist, self).__init__(msg)
- self.hint = msg
- self.http_status_code = 404
- self.taler_error_code = 5110
-
-class BankTransactionDoesNotExist(Exception):
- def __init__(self, msg):
- super(BankTransactionDoesNotExist, self).__init__(msg)
- self.hint = msg
- self.http_status_code = 404
- self.taler_error_code = 5111
def join_dict(**inputDict):
- return ", ".join(['%s==%s' % (key, value) for (key, value) in inputDict.items()])
+ return ", ".join(["%s==%s" % (key, value) for (key, value) in inputDict.items()])
-class CustomManager(models.Manager):
- def __init__(self):
- super(CustomManager, self).__init__()
-
- def get_queryset(self):
- return models.QuerySet(self.model, using=self._db)
-
- def get(self, *args, **kwargs):
- try:
- return super(CustomManager, self).get(*args, **kwargs)
- except BankAccount.DoesNotExist:
- raise BankAccountDoesNotExist(f"Bank account not found for {join_dict(**kwargs)}")
- except BankTransaction.DoesNotExist:
- raise BankTransactionDoesNotExist(f"Bank transaction not found for {join_dict(**kwargs)}")
-
-##
-# The class representing a bank account.
class BankAccount(models.Model):
+ """
+ The class representing a bank account.
+ """
+
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)
- objects = CustomManager()
+ balance = SignedAmountField(default=get_zero_signed_amount)
+
-##
-# The class representing a bank transaction.
class BankTransaction(models.Model):
+ """
+ The class representing a bank transaction.
+ """
+
amount = AmountField(default=False)
debit_account = models.ForeignKey(
BankAccount,
on_delete=models.CASCADE,
db_index=True,
- related_name="debit_account"
+ related_name="debit_account",
)
credit_account = models.ForeignKey(
BankAccount,
on_delete=models.CASCADE,
db_index=True,
- related_name="credit_account"
+ 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)
- objects = CustomManager()
class TalerWithdrawOperation(models.Model):
- withdraw_id = models.UUIDField(
- primary_key=True, default=uuid.uuid4, editable=False
- )
+ withdraw_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
amount = AmountField(default=False)
withdraw_account = models.ForeignKey(
BankAccount,
on_delete=models.CASCADE,
db_index=True,
- related_name="withdraw_account"
+ related_name="withdraw_account",
)
selection_done = models.BooleanField(default=False)
withdraw_done = models.BooleanField(default=False)
@@ -203,6 +209,6 @@ class TalerWithdrawOperation(models.Model):
BankAccount,
null=True,
on_delete=models.CASCADE,
- related_name="selected_exchange_account"
+ related_name="selected_exchange_account",
)
selected_reserve_pub = models.TextField(null=True)
diff --git a/talerbank/app/schemas.py b/talerbank/app/schemas.py
index 2825241..91d6c0a 100644
--- a/talerbank/app/schemas.py
+++ b/talerbank/app/schemas.py
@@ -32,7 +32,7 @@ from urllib.parse import urlparse
# can handle (because of the wallet).
# FIXME: also defined in views.py. Need a common.py to contain
# such definitions ?
-UINT64_MAX = (2**64) - 1
+UINT64_MAX = (2 ** 64) - 1
##
# Pattern for amounts, plain RegEx.
@@ -54,11 +54,13 @@ class InvalidSession(ValueError):
self.http_status_code = http_status_code
super().__init__()
+
class InternalServerError(Exception):
def __init__(self, hint):
self.hint = hint
self.http_status_code = 500
- self.taler_error_code = 1011 # TALER_EC_INTERNAL_LOGIC_ERROR
+ self.taler_error_code = 1011 # TALER_EC_INTERNAL_LOGIC_ERROR
+
##
# Exception class to be raised when a JSON
@@ -82,6 +84,7 @@ class JSONFieldException(ValueError):
self.http_status_code = http_status_code
self.taler_error_code = 5106
+
##
# Exception class to be raised when at least one expected URL
# parameter is either not found or malformed.
@@ -103,9 +106,7 @@ class URLParamValidationError(ValueError):
class AuthForm(forms.Form):
type = forms.CharField(
validators=[
- RegexValidator(
- "^basic$", message="Only 'basic' method provided for now"
- )
+ RegexValidator("^basic$", message="Only 'basic' method provided for now")
]
)
@@ -129,36 +130,24 @@ class AuthField(forms.Field):
##
# Common logic to inherit from all the other validators
-class BankValidator():
+class BankValidator:
def __init__(self, validator, data):
self.validation_result = validator(data)
if not self.validation_result.is_valid():
raise JSONFieldException(self.validation_result.errors, 400)
def get(self, name, default=None):
- ret = self.validation_result.cleaned_data.get(name)
+ ret = self.validation_result.cleaned_data.get(name)
if not ret:
return default
return ret
-class RejectData(BankValidator):
- def __init__(self, data):
- super(RejectData, self).__init__(self.InnerValidator, data)
-
- class InnerValidator(forms.Form):
- auth = AuthField()
- # FIXME: adjust min/max values.
- row_id = forms.IntegerField()
- account_number = forms.IntegerField()
-
-
class AddIncomingData(BankValidator):
def __init__(self, data):
super(AddIncomingData, self).__init__(self.InnerValidator, data)
class InnerValidator(forms.Form):
- auth = AuthField()
amount = forms.CharField(
validators=[
RegexValidator(
@@ -174,20 +163,12 @@ class AddIncomingData(BankValidator):
##
# Subset of /history and /history-range input.
class HistoryParamsBase(forms.Form):
- auth = forms.CharField(
- validators=[
- RegexValidator("^basic$", message="Only 'basic' is allowed")
- ]
- )
-
cancelled = forms.CharField(
required=False,
empty_value="show",
validators=[
- RegexValidator(
- "^(omit|show)$", message="Only 'omit' or 'show' are valid"
- )
- ]
+ RegexValidator("^(omit|show)$", message="Only 'omit' or 'show' are valid")
+ ],
)
ordering = forms.CharField(
@@ -196,16 +177,16 @@ class HistoryParamsBase(forms.Form):
validators=[
RegexValidator(
"^(ascending|descending)$",
- message="Only 'ascending' or 'descending' are valid"
+ message="Only 'ascending' or 'descending' are valid",
)
- ]
+ ],
)
direction = forms.CharField(
validators=[
RegexValidator(
"^(debit|credit|both|cancel\+|cancel-)$",
- message="Only: debit/credit/both/cancel+/cancel-"
+ message="Only: debit/credit/both/cancel+/cancel-",
)
]
)
@@ -217,7 +198,7 @@ class HistoryParamsBase(forms.Form):
class HistoryParams(BankValidator):
def __init__(self, data):
super(HistoryParams, self).__init__(self.InnerValidator, data)
-
+
class InnerValidator(HistoryParamsBase):
# FIXME: adjust min/max values.
delta = forms.IntegerField()
@@ -225,7 +206,6 @@ class HistoryParams(BankValidator):
class HistoryRangeParams(BankValidator):
-
def __init__(self, data):
super(HistoryRangeParams, self).__init__(self.InnerValidator, data)
@@ -256,12 +236,10 @@ class PaytoField(forms.Field):
class WithdrawHeadless(BankValidator):
-
def __init__(self, data):
super(WithdrawHeadless, self).__init__(self.InnerValidator, data)
class InnerValidator(forms.Form):
- auth = AuthField()
amount = forms.CharField(
validators=[
RegexValidator(
@@ -272,8 +250,8 @@ class WithdrawHeadless(BankValidator):
reserve_pub = forms.CharField(required=True)
exchange_wire_details = PaytoField(required=False)
-class WithdrawHeadlessUri(BankValidator):
+class WithdrawHeadlessUri(BankValidator):
def __init__(self, data):
super(WithdrawHeadlessUri, self).__init__(self.InnerValidator, data)
diff --git a/talerbank/app/templates/profile_page.html b/talerbank/app/templates/profile_page.html
index bb6ce49..cea85ce 100644
--- a/talerbank/app/templates/profile_page.html
+++ b/talerbank/app/templates/profile_page.html
@@ -15,6 +15,7 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
@author Marcello Stanisci
+ @author Florian Dold <dold@taler.net>
#}
{% block headermsg %}
@@ -25,13 +26,7 @@
{% endblock headermsg %}
{% block content %}
<section id="menu">
- <p>Account: # {{ account_no }}</p>
- {% if is_valid_amount(balance) %}
- <p>Current balance: <b>{{ sign }} {{ amount_stringify(balance) }}</b></p>
- {% else %}
- <p class="informational-fail">Current balance: <b>INVALID AMOUNT, PLEASE REPORT</b></p>
- {% endif%}
-
+ <p>Bank account balance: <br/> <b>{{ amount_stringify(balance) }}</b></p>
</section>
<section id="main">
<article>
@@ -73,47 +68,12 @@
class="pure-button pure-button-primary"
type="submit"
value="Start withdrawal" />
- </form>
- <h2>Wire transfer</h2>
- <form id="wt-form"
- class="pure-form"
- action="{{ url('profile') }}"
- method="post"
- name="tform">
-
- <input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}" />
-
- <div id="transfer-fields">
-
- <div class="fieldbox">
- <label class="fieldlabel" for="id_amount">Amount to transfer:</label>
- {{ wt_form.amount }}<!--
- --><input type="text" readonly class="currency-indicator" size="{{ currency|length }}" tabindex="-1" value="{{ currency }}">
- </div>
-
- <div class="fieldbox">
- <label class="fieldlabel" for="id_receiver">Receiver:</label>
- {{ wt_form.receiver }}
- </div>
-
- <div class="fieldbox">
- <label class="fieldlabel" for="id_receiver">Subject:</label>
- {{ wt_form.subject }}
- </div>
-
- </div>
-
-
- <input class="pure-button pure-button-primary"
- type="submit"
- value="Transfer!" />
- </form>
</div>
<p>
</p>
</article>
<article>
- <h2>Transaction history</h2>
+ <h2>Transactions for {{ name }}</h2>
<div id="transactions-history">
{% if history %}
<table class="pure-table">
diff --git a/talerbank/app/tests.py b/talerbank/app/tests.py
index 87d7283..61aec9e 100644
--- a/talerbank/app/tests.py
+++ b/talerbank/app/tests.py
@@ -31,17 +31,23 @@ from django.urls import reverse
from django.conf import settings
from django.contrib.auth.models import User
from mock import patch, MagicMock
-from .models import BankAccount, BankTransaction, \
- BankAccountDoesNotExist, BankTransactionDoesNotExist
+from .models import BankAccount, BankTransaction
from . import urls
from .views import wire_transfer
-from taler.util.amount import Amount, CurrencyMismatch, BadFormatAmount
+from taler.util.amount import (
+ Amount,
+ SignedAmount,
+ CurrencyMismatchError,
+ AmountFormatError,
+)
LOGGER = logging.getLogger()
-LOGGER.setLevel(logging.INFO)
+LOGGER.setLevel(logging.DEBUG)
-logging.disable(logging.CRITICAL)
+# logging.disable(logging.CRITICAL)
# reenable: logging.disable(logging.NOTSET)
+logging.disable(logging.NOTSET)
+
def make_auth_line(username, password):
credentials = "%s:%s" % (username, password)
@@ -49,14 +55,13 @@ def make_auth_line(username, password):
header_line = "Basic %s" % b64enc.decode()
return header_line
+
def clear_db():
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_bankaccount_account_no_seq" " RESTART")
cursor.execute("ALTER SEQUENCE app_banktransaction_id_seq RESTART")
@@ -64,13 +69,9 @@ def clear_db():
# to some endpoint that needs to authenticate the
# user.
class MalformedLoginTestCase(TestCase):
-
def test_malformed_login(self):
- self.client.generic(
- "POST",
- reverse("add-incoming", urlconf=urls),
- "malformed"
- )
+ self.client.generic("POST", reverse("add-incoming", urlconf=urls), "malformed")
+
class PublicAccountsTestCase(TestCase):
def setUp(self):
@@ -96,22 +97,20 @@ class WithdrawTestCase(TestCase):
user=User.objects.create_user(
username="test_user", password="test_password"
),
- account_no=100
+ account_no=100,
)
self.user_bank_account.save()
self.exchange_bank_account = BankAccount(
- user=User.objects.create_user(
- username="test_exchange", password=""
- ),
- account_no=99
+ user=User.objects.create_user(username="test_exchange", password=""),
+ account_no=99,
)
self.exchange_bank_account.save()
self.client = Client()
- @patch('talerbank.app.views.wire_transfer')
- @patch('hashlib.new')
- @patch('time.time')
+ @patch("talerbank.app.views.wire_transfer")
+ @patch("hashlib.new")
+ @patch("time.time")
@unittest.skip("skip outdated test case")
def test_withdraw(self, mocked_time, mocked_hashlib, mocked_wire_transfer):
amount = Amount(settings.TALER_CURRENCY, 0, 1)
@@ -121,13 +120,11 @@ class WithdrawTestCase(TestCase):
"amount_currency": amount.currency,
"reserve_pub": "UVZ789",
"exchange": "https://exchange.example.com/",
- "exchange_wire_details": "payto://x-taler-bank/bank.example/99"
+ "exchange_wire_details": "payto://x-taler-bank/bank.example/99",
}
self.client.login(username="test_user", password="test_password")
- response = 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()
@@ -143,10 +140,11 @@ class WithdrawTestCase(TestCase):
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)
+ 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()
@@ -155,10 +153,10 @@ class WithdrawTestCase(TestCase):
class InternalWireTransferTestCase(TestCase):
def setUp(self):
BankAccount(
- user=User.objects.create_user(username='give_money', password="gm")
+ 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
+ user=User.objects.create_user(username="take_money"), account_no=4
)
self.take_money.save()
@@ -169,25 +167,25 @@ class InternalWireTransferTestCase(TestCase):
client = Client()
client.login(username="give_money", password="gm")
response = client.post(
- reverse("profile", urlconf=urls), {
+ reverse("profile", urlconf=urls),
+ {
"amount": 3.0,
"receiver": self.take_money.account_no,
- "subject": "charity"
- }
+ "subject": "charity",
+ },
)
take_money = BankAccount.objects.get(account_no=4)
- self.assertEqual(
- 0,
- Amount.cmp(Amount(settings.TALER_CURRENCY, 3), take_money.amount)
- )
+ r = SignedAmount.parse(f"{settings.TALER_CURRENCY}:3.0")
+ self.assertEqual(take_money.balance, r)
self.assertEqual(302, response.status_code)
class RegisterTestCase(TestCase):
"""User registration"""
+
def setUp(self):
clear_db()
- BankAccount(user=User.objects.create_user(username='Bank')).save()
+ BankAccount(user=User.objects.create_user(username="Bank")).save()
def tearDown(self):
clear_db()
@@ -195,11 +193,9 @@ class RegisterTestCase(TestCase):
def test_register(self):
client = Client()
response = client.post(
- reverse("register", urlconf=urls), {
- "username": "test_register",
- "password": "test_register"
- },
- follow=True
+ reverse("register", urlconf=urls),
+ {"username": "test_register", "password": "test_register"},
+ follow=True,
)
self.assertIn(("/profile", 302), response.redirect_chain)
# this assertion tests "/profile""s view
@@ -210,10 +206,8 @@ class RegisterTestCase(TestCase):
# Normal case.
response = client.post(
- reverse("register-headless", urlconf=urls), {
- "username": "test_register_headless",
- "password": "password*+#@"
- }
+ reverse("register-headless", urlconf=urls),
+ {"username": "test_register_headless", "password": "password*+#@"},
)
self.assertEqual(200, response.status_code)
@@ -226,29 +220,27 @@ class RegisterTestCase(TestCase):
# Try registering unavailable username.
response = client.post(
- reverse("register-headless", urlconf=urls), {
- "username": "test_register_headless",
- "password": "password"
- }
+ reverse("register-headless", urlconf=urls),
+ {"username": "test_register_headless", "password": "password"},
)
self.assertEqual(409, response.status_code)
# NOTE: Django 2.2.2 allows ANY character! Is this normal?
response = client.post(
- reverse("register-headless", urlconf=urls), {
- "username": "'''+++;;;'''",
- "password": "password2"
- }
+ reverse("register-headless", urlconf=urls),
+ {"username": "'''+++;;;'''", "password": "password2"},
)
self.assertEqual(200, response.status_code)
class LoginTestCase(TestCase):
"""User login"""
+
def setUp(self):
BankAccount(
- user=User.objects.
- create_user(username="test_user", password="test_password")
+ user=User.objects.create_user(
+ username="test_user", password="test_password"
+ )
).save()
self.client = Client()
@@ -265,178 +257,112 @@ class LoginTestCase(TestCase):
def test_failing_login(self):
response = self.client.get(
- reverse("history", urlconf=urls), {"auth": "basic"}, **{
- "HTTP_AUTHORIZATION": make_auth_line("Wrong", "Credentials")
- }
+ reverse("history", urlconf=urls),
+ {"auth": "basic"},
+ HTTP_AUTHORIZATION=make_auth_line("Wrong", "Credentials"),
)
data = response.content.decode("utf-8")
self.assertEqual(401, response.status_code)
-class AmountTestCase(TestCase):
- def test_cmp(self):
- amount1 = Amount("X", 1)
- _amount1 = Amount("X", 1)
- amount2 = Amount("X", 2)
-
- self.assertEqual(-1, Amount.cmp(amount1, amount2))
- self.assertEqual(1, Amount.cmp(amount2, amount1))
- self.assertEqual(0, Amount.cmp(amount1, _amount1))
-
- # Trying to compare amount of different currencies
- def test_cmp_diff_curr(self):
- amount1 = Amount("X", 1)
- amount2 = Amount("Y", 2)
- with self.assertRaises(CurrencyMismatch):
- Amount.cmp(amount1, amount2)
-
-
-class RejectTestCase(TestCase):
- def setUp(self):
- BankAccount(
- user=User.objects.
- create_user(username="rejected_user", password="rejected_password")
- ).save()
- BankAccount(
- user=User.objects.create_user(
- username="rejecting_user", password="rejecting_password"
- )
- ).save()
-
- def tearDown(self):
- clear_db()
-
- def test_reject(self):
- client = Client()
- rejecting = User.objects.get(username="rejecting_user")
- data = '{"auth": {"type": "basic"}, \
- "credit_account": %d, \
- "subject": "TESTWTID", \
- "exchange_url": "https://exchange.test", \
- "amount": "%s:5.0"}' \
- % (rejecting.bankaccount.account_no,
- settings.TALER_CURRENCY)
- response = client.post(
- reverse("add-incoming", urlconf=urls),
- data=data,
- content_type="application/json",
- follow=True,
- **{
- "HTTP_AUTHORIZATION": make_auth_line("rejected_user", "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")
- response = client.put(
- reverse("reject", urlconf=urls),
- data='{"row_id": %d, \
- "auth": {"type": "basic"}, \
- "account_number": %d}' \
- % (jdata["row_id"],
- rejected.bankaccount.account_no),
- content_type="application/json",
- **{
- "HTTP_AUTHORIZATION": make_auth_line("rejecting_user", "rejecting_password"),
- })
- self.assertEqual(response.status_code, 204)
-
-
class WithdrawHeadlessTestCase(TestCase):
def setUp(self):
BankAccount(
user=User.objects.create_user(
username="headless_wallet", password="headless_password"
),
- amount=Amount(settings.TALER_CURRENCY, 10)
+ balance=SignedAmount(True, Amount(settings.TALER_CURRENCY, 10, 0)),
).save()
# Gets account #2, in line with config.
BankAccount(
user=User.objects.create_user(
username="normal_exchange", password="normal_password"
),
- account_no=2
+ account_no=2,
).save()
def test_withdraw_headless(self):
client = Client()
# Use default exchange.
- data = '{"auth": {"type": "basic"}, \
+ data = (
+ '{"auth": {"type": "basic"}, \
"reserve_pub": "RESERVEPUB", \
- "amount": "%s:10"}' % settings.TALER_CURRENCY
+ "amount": "%s:10"}'
+ % settings.TALER_CURRENCY
+ )
response = client.post(
reverse("withdraw-headless", urlconf=urls),
data=data,
content_type="application/json",
follow=True,
- **{
- "HTTP_AUTHORIZATION": make_auth_line("headless_wallet", "headless_password")
-
- }
+ HTTP_AUTHORIZATION=make_auth_line("headless_wallet", "headless_password"),
)
self.assertEqual(200, response.status_code)
# Try withdrawing more than owning.
- data = '{"auth": {"type": "basic"}, \
+ data = (
+ '{"auth": {"type": "basic"}, \
"reserve_pub": "RESERVEPUB", \
- "amount": "%s:100"}' % settings.TALER_CURRENCY
+ "amount": "%s:100"}'
+ % settings.TALER_CURRENCY
+ )
response = client.post(
reverse("withdraw-headless", urlconf=urls),
data=data,
content_type="application/json",
follow=True,
- **{
- "HTTP_AUTHORIZATION": make_auth_line("headless_wallet", "headless_password")
- }
+ HTTP_AUTHORIZATION=make_auth_line("headless_wallet", "headless_password"),
)
self.assertEqual(406, response.status_code)
# Try withdrawing giving exchange field.
- data = '{"auth": {"type": "basic"}, \
+ data = (
+ '{"auth": {"type": "basic"}, \
"exchange_wire_details": "payto://x-taler-bank/bank.example.com/2", \
"reserve_pub": "RESERVEPUB", \
- "amount": "%s:0.4"}' % settings.TALER_CURRENCY
+ "amount": "%s:0.4"}'
+ % settings.TALER_CURRENCY
+ )
response = client.post(
reverse("withdraw-headless", urlconf=urls),
data=data,
content_type="application/json",
follow=True,
- **{
- "HTTP_AUTHORIZATION": make_auth_line("headless_wallet", "headless_password")
- }
+ HTTP_AUTHORIZATION=make_auth_line("headless_wallet", "headless_password"),
)
self.assertEqual(200, response.status_code)
# Try withdrawing giving non-existent recipient.
- data = '{"auth": {"type": "basic"}, \
+ data = (
+ '{"auth": {"type": "basic"}, \
"exchange_wire_details": "payto://x-taler-bank/bank.example.com/2222", \
"reserve_pub": "RESERVEPUB", \
- "amount": "%s:0.4"}' % settings.TALER_CURRENCY
+ "amount": "%s:0.4"}'
+ % settings.TALER_CURRENCY
+ )
response = client.post(
reverse("withdraw-headless", urlconf=urls),
data=data,
content_type="application/json",
follow=True,
- **{
- "HTTP_AUTHORIZATION": make_auth_line("headless_wallet", "headless_password")
- }
+ HTTP_AUTHORIZATION=make_auth_line("headless_wallet", "headless_password"),
)
self.assertEqual(404, response.status_code)
# Try withdrawing giving invalid JSON.
- data = '{"auth": {"type": "basic"}, \
+ data = (
+ '{"auth": {"type": "basic"}, \
"XXX": "YYY", \
- "amount": "%s:0.4"}' % settings.TALER_CURRENCY
+ "amount": "%s:0.4"}'
+ % settings.TALER_CURRENCY
+ )
response = client.post(
reverse("withdraw-headless", urlconf=urls),
data=data,
content_type="application/json",
follow=True,
- **{
- "HTTP_AUTHORIZATION": make_auth_line("headless_wallet", "headless_password")
- }
+ HTTP_AUTHORIZATION=make_auth_line("headless_wallet", "headless_password"),
)
self.assertEqual(400, response.status_code)
@@ -446,14 +372,17 @@ class WithdrawHeadlessTestCase(TestCase):
class AddIncomingTestCase(TestCase):
"""Test money transfer's API"""
+
def setUp(self):
BankAccount(
- user=User.objects.
- create_user(username="bank_user", password="bank_password")
+ user=User.objects.create_user(
+ username="bank_user", password="bank_password"
+ )
).save()
BankAccount(
- user=User.objects.
- create_user(username="user_user", password="user_password")
+ user=User.objects.create_user(
+ username="user_user", password="user_password"
+ )
).save()
def tearDown(self):
@@ -461,20 +390,20 @@ class AddIncomingTestCase(TestCase):
def test_add_incoming(self):
client = Client()
- data = '{"auth": {"type": "basic"}, \
+ data = (
+ '{"auth": {"type": "basic"}, \
"credit_account": 1, \
"subject": "TESTWTID", \
"exchange_url": "https://exchange.test", \
- "amount": "%s:1.0"}' \
- % settings.TALER_CURRENCY
+ "amount": "%s:1.0"}'
+ % settings.TALER_CURRENCY
+ )
response = client.post(
reverse("add-incoming", urlconf=urls),
data=data,
content_type="application/json",
follow=True,
- **{
- "HTTP_AUTHORIZATION": make_auth_line("user_user", "user_password")
- }
+ HTTP_AUTHORIZATION=make_auth_line("user_user", "user_password"),
)
self.assertEqual(200, response.status_code)
@@ -485,10 +414,8 @@ class AddIncomingTestCase(TestCase):
data=zdata,
content_type="application/json",
follow=True,
- **{
- "HTTP_AUTHORIZATION": make_auth_line("user_user", "user_password"),
- "HTTP_CONTENT_ENCODING": "deflate"
- }
+ HTTP_AUTHORIZATION=make_auth_line("user_user", "user_password"),
+ HTTP_CONTENT_ENCODING="deflate",
)
self.assertEqual(200, response.status_code)
@@ -503,360 +430,181 @@ class AddIncomingTestCase(TestCase):
data=data,
content_type="application/json",
follow=True,
- **{
- "HTTP_AUTHORIZATION": make_auth_line("user_user", "user_password")
- }
+ HTTP_AUTHORIZATION=make_auth_line("user_user", "user_password"),
)
# note: a bad currency request gets 400.
- self.assertRaises(CurrencyMismatch)
- self.assertEqual(406, response.status_code)
+ self.assertEqual(400, response.status_code)
LOGGER.info(response.content.decode("utf-8"))
# Try to go debit
- data = '{"auth": {"type": "basic"}, \
+ data = (
+ '{"auth": {"type": "basic"}, \
"credit_account": 1, \
"subject": "TESTWTID", \
"exchange_url": "https://exchange.test", \
- "amount": "%s:50.1"}' % settings.TALER_CURRENCY
+ "amount": "%s:50.1"}'
+ % settings.TALER_CURRENCY
+ )
response = client.post(
reverse("add-incoming", urlconf=urls),
data=data,
content_type="application/json",
follow=True,
- **{
- "HTTP_AUTHORIZATION": make_auth_line("user_user", "user_password")
- }
+ HTTP_AUTHORIZATION=make_auth_line("user_user", "user_password"),
)
self.assertEqual(406, response.status_code)
# Try use a non-existent recipient.
- data = '{"auth": {"type": "basic"}, \
+ data = (
+ '{"auth": {"type": "basic"}, \
"credit_account": 1987, \
"subject": "TESTWTID", \
"exchange_url": "https://exchange.test", \
- "amount": "%s:1"}' % settings.TALER_CURRENCY
+ "amount": "%s:1"}'
+ % settings.TALER_CURRENCY
+ )
response = client.post(
reverse("add-incoming", urlconf=urls),
data=data,
content_type="application/json",
follow=True,
- **{
- "HTTP_AUTHORIZATION": make_auth_line("user_user", "user_password")
- }
+ HTTP_AUTHORIZATION=make_auth_line("user_user", "user_password"),
)
self.assertEqual(404, response.status_code)
-class HistoryContext:
- def __init__(self, expected_resp, **kwargs):
- self.expected_resp = expected_resp
- self.urlargs = kwargs
- self.urlargs.update({"auth": "basic"})
-
- def dump(self):
- return self.urlargs
-
class CustomDoesNotExistTestCase(TestCase):
-
def test_bankaccount_doesnotexist(self):
- with self.assertRaises(BankAccountDoesNotExist):
+ with self.assertRaises(BankAccount.DoesNotExist):
BankAccount.objects.get(account_no=1000)
- with self.assertRaises(BankTransactionDoesNotExist):
+ with self.assertRaises(BankTransaction.DoesNotExist):
BankTransaction.objects.get(subject="1000")
+
class HistoryTestCase(TestCase):
def setUp(self):
clear_db()
debit_account = BankAccount(
- user=User.objects.create_user(username='User', password="Password"),
- amount=Amount(settings.TALER_CURRENCY, 100)
+ user=User.objects.create_user(username="User", password="Password"),
+ balance=SignedAmount(True, Amount(settings.TALER_CURRENCY, 100, 0)),
)
debit_account.save()
credit_account = BankAccount(
- user=User.objects.
- create_user(username='User0', password="Password0")
+ user=User.objects.create_user(username="User0", password="Password0")
)
credit_account.save()
for subject in ("a", "b", "c", "d", "e", "f", "g", "h", "i"):
wire_transfer(
- Amount(settings.TALER_CURRENCY, 1), debit_account,
- credit_account, subject
+ Amount(settings.TALER_CURRENCY, 1, 0),
+ 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_AUTHORIZATION": make_auth_line("User0", "Password0")
- }
- )
def tearDown(self):
clear_db()
- def assert_result(self, response, ctx):
-
- data = response.content.decode("utf-8")
- try:
- # FIXME, not always data is found this way.
- 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 => raw body: %s" % \
- (response.request["PATH_INFO"],
- unquote(response.request["QUERY_STRING"]),
- response.content.decode("utf-8")))
-
- # 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)
-
- def test_history_range(self):
- now = int(time.time())
-
- for ctx in (
-
- # Expect empty results, range too ancient.
- HistoryContext(
- expected_resp={"status": 204}, start=1, end=2, direction="both"
- ),
-
- # Expect empty results, range too ahead.
- HistoryContext(
- expected_resp={"status": 200},
- start=now + 40,
- end=now + 50,
- direction="both"
- ),
-
- # Expect non empty results.
- HistoryContext(
- expected_resp={"status": 200},
- start=now - 30,
- end=now + 30,
- direction="both"
- )
- ):
-
- response = self.client.get(
- reverse("history-range", urlconf=urls), ctx.urlargs, **{
- "HTTP_AUTHORIZATION": make_auth_line("User", "Password")
- }
- )
-
- self.assert_result(response, ctx)
-
def test_history(self):
- for ctx in (
- HistoryContext(
- expected_resp={"status": 200}, delta="-4", direction="both"
- ),
- HistoryContext(
- expected_resp={
- "fields": [("row_id", 9)],
- "status": 200
- },
- delta="+1",
- start="5",
- direction="both"
- ),
- HistoryContext(
- expected_resp={
- "fields": [("wt_subject", "c")],
- "status": 200
- },
- delta="1",
- start=2,
- direction="both",
- ordering="ascending"
- ),
- HistoryContext(
- expected_resp={
- "fields": [("wt_subject", "a")],
- "status": 200
- },
- delta="-1",
- start=2,
- 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"
- )
- ):
+ def histquery(**urlargs):
response = self.client.get(
- reverse("history", urlconf=urls), ctx.urlargs, **{
- "HTTP_AUTHORIZATION": make_auth_line("User", "Password")
- }
+ reverse("history", urlconf=urls),
+ urlargs,
+ HTTP_AUTHORIZATION=make_auth_line("User", "Password"),
)
- self.assert_result(response, ctx)
-
-
-class DBAmountSubtraction(TestCase):
- def setUp(self):
- BankAccount(
- user=User.objects.create_user(username='U'),
- amount=Amount(settings.TALER_CURRENCY, 3)
- ).save()
-
- def tearDown(self):
- clear_db()
+ return response
+
+ # test query #1
+ r = histquery(delta="-4", direction="both")
+ rd = json.loads(r.content)
+ self.assertEqual(r.status_code, 200)
+
+ # test query #2
+ r = histquery(delta="+1", start="5", direction="both")
+ self.assertEqual(r.status_code, 200)
+ rd = json.loads(r.content)
+ self.assertEqual(r.status_code, 200)
+ self.assertEqual(rd["data"][0]["row_id"], 6)
+
+ # test query #3
+ r = histquery(delta="+1", start="2", direction="both")
+ self.assertEqual(r.status_code, 200)
+ rd = json.loads(r.content)
+ self.assertEqual(rd["data"][0]["wt_subject"], "c")
+
+ # test query #4
+ r = histquery(delta="-1", start="2", direction="both")
+ self.assertEqual(r.status_code, 200)
+ rd = json.loads(r.content)
+ self.assertEqual(rd["data"][0]["wt_subject"], "a")
+
+ # test query #5
+ r = histquery(delta="1", start="11", direction="both")
+ self.assertEqual(r.status_code, 200)
+ rd = json.loads(r.content)
+ self.assertEqual(len(rd["data"]), 0)
- def test_subtraction(self):
- user_bankaccount = BankAccount.objects.get(
- user=User.objects.get(username='U')
- )
- user_bankaccount.amount.subtract(Amount(settings.TALER_CURRENCY, 2))
- self.assertEqual(
- Amount.cmp(Amount(settings.TALER_CURRENCY, 1), user_bankaccount.amount), 0
- )
class DBCustomColumnTestCase(TestCase):
def setUp(self):
- BankAccount(user=User.objects.create_user(username='U')).save()
+ BankAccount(user=User.objects.create_user(username="U")).save()
def tearDown(self):
clear_db()
def test_exists(self):
- user_bankaccount = BankAccount.objects.get(
- user=User.objects.get(username='U')
- )
- self.assertTrue(isinstance(user_bankaccount.amount, Amount))
+ user_bankaccount = BankAccount.objects.get(user=User.objects.get(username="U"))
+ self.assertTrue(isinstance(user_bankaccount.balance, SignedAmount))
## This tests whether a bank account goes debit and then goes >=0
## again
class DebitTestCase(TestCase):
def setUp(self):
- BankAccount(user=User.objects.create_user(username='U')).save()
- BankAccount(user=User.objects.create_user(username='U0')).save()
+ BankAccount(user=User.objects.create_user(username="U")).save()
+ BankAccount(user=User.objects.create_user(username="U0")).save()
def tearDown(self):
clear_db()
def test_green(self):
- user_bankaccount = BankAccount.objects.get(
- user=User.objects.get(username='U')
- )
- self.assertEqual(False, user_bankaccount.debit)
+ user_bankaccount = BankAccount.objects.get(user=User.objects.get(username="U"))
+ self.assertTrue(user_bankaccount.balance.is_zero())
def test_red(self):
- user_bankaccount = BankAccount.objects.get(
- user=User.objects.get(username='U')
- )
+ user_bankaccount = BankAccount.objects.get(user=User.objects.get(username="U"))
user_bankaccount0 = BankAccount.objects.get(
- user=User.objects.get(username='U0')
+ user=User.objects.get(username="U0")
)
wire_transfer(
- Amount(settings.TALER_CURRENCY, 10, 0), user_bankaccount0,
- user_bankaccount, "Go green"
+ Amount(settings.TALER_CURRENCY, 10, 0),
+ user_bankaccount0,
+ 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.assertFalse(user_bankaccount.debit)
-
- self.assertTrue(user_bankaccount0.debit)
wire_transfer(
- Amount(settings.TALER_CURRENCY, 11), user_bankaccount,
- user_bankaccount0, "Go red"
+ Amount(settings.TALER_CURRENCY, 11, 0),
+ user_bankaccount,
+ user_bankaccount0,
+ "Go red",
)
- 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))
+ amt_one = SignedAmount.parse(f"{settings.TALER_CURRENCY}:1")
-
-class ParseAmountTestCase(TestCase):
- def test_parse_amount(self):
- ret = Amount.parse("KUDOS:4.0")
- 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()
- )
- 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 as err:
- return
- # make sure the control doesn't get here
- self.assertEqual(True, False)
+ self.assertEqual(user_bankaccount.balance, -amt_one)
+ self.assertEqual(user_bankaccount0.balance, amt_one)
class MeasureHistory(TestCase):
def setUp(self):
self.user_bankaccount0 = BankAccount(
- user=User.objects.create_user(username='U0'),
- amount=Amount(settings.TALER_CURRENCY, 3000)
+ user=User.objects.create_user(username="U0"),
+ balance=SignedAmount(True, Amount(settings.TALER_CURRENCY, 3000, 0)),
)
self.user_bankaccount0.save()
- user_bankaccount = BankAccount(
- user=User.objects.create_user(username='U')
- )
+ user_bankaccount = BankAccount(user=User.objects.create_user(username="U"))
user_bankaccount.save()
self.ntransfers = 1000
@@ -866,8 +614,10 @@ class MeasureHistory(TestCase):
for i in range(self.ntransfers):
del i # to pacify PEP checkers
wire_transfer(
- Amount(settings.TALER_CURRENCY, 1), self.user_bankaccount0,
- user_bankaccount, "bulk"
+ Amount(settings.TALER_CURRENCY, 1, 0),
+ self.user_bankaccount0,
+ user_bankaccount,
+ "bulk",
)
def tearDown(self):
@@ -880,7 +630,7 @@ class MeasureHistory(TestCase):
timer = timeit.Timer(
stmt="extract_history(self.user_bankaccount0, False)",
setup="from talerbank.app.views import extract_history",
- globals=locals()
+ globals=locals(),
)
total_time = timer.timeit(number=1)
allowed_time_per_record = 0.003
@@ -890,14 +640,14 @@ class MeasureHistory(TestCase):
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)
+ user=User.objects.create_user(username="U0", password="U0PASS"),
+ balance=SignedAmount(True, Amount(settings.TALER_CURRENCY, 3, 0)),
)
self.the_bank.save()
user = BankAccount(
- user=User.objects.create_user(username='U'),
- amount=Amount(settings.TALER_CURRENCY, 10)
+ user=User.objects.create_user(username="U"),
+ balance=SignedAmount(True, Amount(settings.TALER_CURRENCY, 10, 0)),
)
user.save()
@@ -905,27 +655,27 @@ class BalanceTestCase(TestCase):
# bank: 2, user: 11
wire_transfer(
- Amount(settings.TALER_CURRENCY, 1), self.the_bank, user, "mock"
+ Amount(settings.TALER_CURRENCY, 1, 0), self.the_bank, user, "mock"
)
# bank: 4, user: 9
wire_transfer(
- Amount(settings.TALER_CURRENCY, 2), user, self.the_bank, "mock"
+ Amount(settings.TALER_CURRENCY, 2, 0), user, self.the_bank, "mock"
)
# bank: -1, user: 14
wire_transfer(
- Amount(settings.TALER_CURRENCY, 5), self.the_bank, user, "mock"
+ Amount(settings.TALER_CURRENCY, 5, 0), self.the_bank, user, "mock"
)
# bank: 7, user: 6 (END)
wire_transfer(
- Amount(settings.TALER_CURRENCY, 8), user, self.the_bank, "mock"
+ Amount(settings.TALER_CURRENCY, 8, 0), user, self.the_bank, "mock"
)
# bank: -3, user: 16 (END)
wire_transfer(
- Amount(settings.TALER_CURRENCY, 10), user, self.the_bank, "mock"
+ Amount(settings.TALER_CURRENCY, 10, 0), self.the_bank, user, "mock"
)
self.client = Client()
@@ -937,44 +687,23 @@ class BalanceTestCase(TestCase):
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_AUTHORIZATION": make_auth_line("U0", "U0PASS")
- }
+ {"delta": -30, "direction": "both", "account_number": 55},
+ HTTP_AUTHORIZATION=make_auth_line("U0", "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)
+ acc_bal = SignedAmount(True, Amount(settings.TALER_CURRENCY, 10, 0))
+ print("acc_bal start", acc_bal)
- for entry in entries["data"]:
- if entry["sign"] == "+":
- acc_in.add(Amount(**entry["amount"]))
+ for entry in reversed(entries["data"]):
+ print("entry", entry)
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)
+ acc_bal += SignedAmount.parse(entry["amount"])
+ if entry["sign"] == "+":
+ acc_bal -= SignedAmount.parse(entry["amount"])
+ print("acc_bal after", acc_bal)
+
+ expected_amount = SignedAmount.parse(f"{settings.TALER_CURRENCY}:16.0")
+ self.assertEqual(acc_bal, expected_amount)
diff --git a/talerbank/app/urls.py b/talerbank/app/urls.py
index 28230ed..cabdc22 100644
--- a/talerbank/app/urls.py
+++ b/talerbank/app/urls.py
@@ -31,54 +31,45 @@ urlpatterns = [
"login/",
auth_views.LoginView.as_view(
template_name="login.html",
- authentication_form=views.TalerAuthenticationForm
+ authentication_form=views.TalerAuthenticationForm,
),
- name="login"
+ name="login",
),
path("logout/", views.logout_view, name="logout"),
path("accounts/register", views.register, name="register"),
path("profile", views.profile_page, name="profile"),
path("history", views.serve_history, name="history"),
path("history-range", views.serve_history_range, name="history-range"),
- path("reject", views.reject, name="reject"),
path(
"api/withdraw-operation/<str:withdraw_id>",
views.api_withdraw_operation,
- name="api-withdraw-operation"
- ),
- path(
- "api/withdraw-headless",
- views.withdraw_headless,
- name="withdraw-headless"
+ name="api-withdraw-operation",
),
+ path("api/withdraw-headless", views.withdraw_headless, name="withdraw-headless"),
path(
"api/withdraw-headless-uri",
views.withdraw_headless_uri,
- name="withdraw-headless-uri"
+ name="withdraw-headless-uri",
),
path("api/register", views.register_headless, name="register-headless"),
path("start-withdrawal", views.start_withdrawal, name="start-withdrawal"),
path(
- "show-withdrawal/<str:withdraw_id>",
- views.show_withdrawal,
- name="withdraw-show"
+ "show-withdrawal/<str:withdraw_id>", views.show_withdrawal, name="withdraw-show"
),
path(
"confirm-withdrawal/<str:withdraw_id>",
views.confirm_withdrawal,
- name="withdraw-confirm"
- ),
- path(
- "public-accounts", views.serve_public_accounts, name="public-accounts"
+ name="withdraw-confirm",
),
+ path("public-accounts", views.serve_public_accounts, name="public-accounts"),
path(
"public-accounts/<str:name>",
views.serve_public_accounts,
- name="public-accounts"
+ name="public-accounts",
),
path(
"public-accounts/<str:name>/<int:page>",
views.serve_public_accounts,
- name="public-accounts"
+ name="public-accounts",
),
]
diff --git a/talerbank/app/views.py b/talerbank/app/views.py
index a6902be..3df8505 100644
--- a/talerbank/app/views.py
+++ b/talerbank/app/views.py
@@ -45,7 +45,7 @@ from django.shortcuts import render, redirect
from django.core.exceptions import ObjectDoesNotExist
from datetime import datetime
from .models import BankAccount, BankTransaction, TalerWithdrawOperation
-from taler.util.amount import Amount
+from taler.util.amount import Amount, SignedAmount
import qrcode
import qrcode.image.svg
import lxml
@@ -53,7 +53,6 @@ from .schemas import (
HistoryParams,
HistoryRangeParams,
URLParamValidationError,
- RejectData,
AddIncomingData,
JSONFieldException,
InvalidSession,
@@ -129,18 +128,6 @@ class SameAccountException(Exception):
self.taler_error_code = 5102
-##
-# Exception raised when someone tries to reject a
-# transaction, but they have no rights to accomplish
-# such operation.
-class RejectNoRightsException(Exception):
- def __init__(self, msg):
- super(RejectNoRightsException, self).__init__(msg)
- self.hint = "Only original payer can reject."
- self.http_status_code = 403
- self.taler_error_code = 5200
-
-
class UnhandledException(Exception):
def __init__(self, msg="Unhandled exception happened!"):
super(UnhandledException, self).__init__(msg)
@@ -278,11 +265,12 @@ class InputDatalist(forms.TextInput):
return html + datalist
-##
-# Form for sending wire transfers. It usually appears in the
-# user profile page.
-#
class WTForm(forms.Form):
+ """
+ Form for sending wire transfers. It usually appears in the
+ user profile page.
+ """
+
amount = forms.FloatField(
min_value=0.1, widget=forms.NumberInput(attrs={"class": "currency-input"})
)
@@ -333,13 +321,12 @@ def profile_page(request):
is_success, is_failure, hint = get_session_hint(request, "profile_hint")
context = dict(
name=request.user.username,
- balance=request.user.bankaccount.amount,
- sign="-" if request.user.bankaccount.debit else "",
+ balance=request.user.bankaccount.balance,
fail_message=is_failure,
success_message=is_success,
hint=hint,
precision=settings.TALER_DIGITS,
- currency=request.user.bankaccount.amount.currency,
+ currency=request.user.bankaccount.balance.amount.currency,
account_no=request.user.bankaccount.account_no,
wt_form=wtf,
history=extract_history(request.user.bankaccount, -1 * (UINT64_MAX / 2 / 2)),
@@ -423,16 +410,6 @@ def internal_register(request):
user_account.save()
bank_internal_account = BankAccount.objects.get(account_no=1)
- # Raise:
- #
- # SameAccountException
- # DebitLimitException
- # CurrencyMismatch
- #
- # Amount group:
- # BadFormatAmount
- # NumberTooBig
- # NegativeNumber
wire_transfer(
Amount(settings.TALER_CURRENCY, 100, 0),
bank_internal_account,
@@ -443,15 +420,13 @@ def internal_register(request):
return user
-##
-# This method serves the request for programmatically
-# registering a user.
-#
-# @param request Django-specific HTTP request object.
-# @return Django-specific HTTP response object.
@require_POST
@csrf_exempt
def register_headless(request):
+ """
+ This method serves the request for programmatically
+ registering a user.
+ """
try:
user = internal_register(request)
@@ -465,15 +440,13 @@ def register_headless(request):
return HttpResponse(status=200)
-##
-# This method serves the request for registering a user.
-# If successful, it redirects the user to their profile page;
-# otherwise it will show again the same form (currently, without
-# displaying _any_ error/warning message.)
-#
-# @param request Django-specific HTTP request object.
-# @return Django-specific HTTP response object.
def register(request):
+ """
+ This method serves the request for registering a user.
+ If successful, it redirects the user to their profile page;
+ otherwise it will show again the same form (currently, without
+ displaying _any_ error/warning message.)
+ """
if request.method != "POST":
return render(request, "register.html")
@@ -535,7 +508,7 @@ def logout_view(request):
# @return the history array.
def extract_history(account, delta, start=None):
history = []
- qs = query_history(account, "both", delta, start, "descending")
+ qs = query_history(account, "both", delta, start)
for item in qs:
if item.credit_account == account:
counterpart = item.debit_account
@@ -589,7 +562,6 @@ def serve_public_accounts(request, name=None, page=None):
# and django/python is not allowing slicing with big numbers.
UINT64_MAX / 2 / 2,
0,
- "descending",
).count()
DELTA = 30
# '//' operator is NO floating point.
@@ -708,7 +680,7 @@ def query_history_range(bank_account, direction, start, end, descending):
# @param sign this value ("+"/"-") determines whether the history
# entries will be younger / older than @a start.
# @param ordering "descending" or anything else (for "ascending").
-def query_history(bank_account, direction, delta, start, ordering):
+def query_history(bank_account, direction, delta, start):
if start is None:
if delta > 0:
start = -1
@@ -722,7 +694,7 @@ def query_history(bank_account, direction, delta, start, ordering):
qs = BankTransaction.objects.filter(
direction_switch(bank_account, direction), sign_filter
)
- order = "-id" if "descending" == ordering else "id"
+ order = "id" if (delta > 0) else "-id"
return qs.order_by(order)[: abs(delta)]
@@ -751,11 +723,11 @@ def build_history_response(qs, cancelled, user_account):
history.append(
dict(
counterpart=counterpart,
- amount=entry.amount.dump(),
+ amount=entry.amount.stringify(),
sign=sign_,
wt_subject=entry.subject,
row_id=entry.id,
- date=dict(t_ms=int(entry.date.timestamp())*1000)
+ date=dict(t_ms=int(entry.date.timestamp()) * 1000),
)
)
return history
@@ -786,8 +758,6 @@ def serve_history_range(request, user_account):
history = build_history_response(qs, args.get("cancelled", "show"), user_account)
- if not history:
- return HttpResponse(status=204)
return JsonResponse(dict(data=history), status=200)
@@ -806,15 +776,13 @@ def serve_history(request, user_account):
args.get("direction"),
args.get("delta"),
args.get("start", None),
- args.get("ordering", "descending"),
)
history = build_history_response(qs, args.get("cancelled", "show"), user_account)
- if not history:
- return HttpResponse(status=204)
return JsonResponse(dict(data=history), status=200)
+
##
# Implements the HTTP basic auth schema.
#
@@ -830,64 +798,13 @@ def basic_auth(request):
if len(tokens) != 2:
raise LoginFailed("invalid Authorization header")
- # decode the base64 content.
+ # decode the base64 content.
if tokens[0] != "Basic":
raise LoginFailed("Not supporting '%s' authorization method" % tokens[0])
username, password = base64.b64decode(tokens[1]).decode("utf-8").split(":")
return django.contrib.auth.authenticate(username=username, password=password)
-##
-# Serve a request of /reject (for rejecting wire transfers).
-#
-# @param request Django-specific HTTP request object.
-# @param user_account the account that is going to reject the
-# transfer. Used to check whether they have this right
-# or not (only accounts which _got_ payed can cancel the
-# transaction.)
-@transaction.atomic
-@csrf_exempt
-@require_http_methods(["PUT", "POST"])
-@login_via_headers
-def reject(request, user_account):
-
- data = RejectData(json.loads(decode_body(request)))
-
- trans = BankTransaction.objects.get(id=data.get("row_id"))
- if trans.credit_account.account_no != user_account.bankaccount.account_no:
- raise RejectNoRightsException()
- trans.cancelled = True
- if trans.debit_account.debit:
- # balance is negative
- if 1 > Amount.cmp(trans.debit_account.amount, trans.amount):
- # debit_account.amount <= trans.amount
- trans.debit_account.debit = False
- tmp = Amount(**trans.amount.dump())
- tmp.subtract(trans.debit_account.amount)
- trans.debit_account.amount.set(**tmp.dump())
- else:
- # debit_account > trans.amount
- trans.debit_account.amount.subtract(trans.amount)
- else:
- # balance is positive, simply add
- trans.debit_account.amount.add(trans.amount)
- if trans.credit_account.debit:
- # credit account balance is already negative
- trans.credit_account.amount.add(trans.amount)
- else:
- if -1 == Amount.cmp(trans.credit_account.amount, trans.amount):
- # credit_account.amount < trans.amount
- trans.credit_account.debit = True
- tmp = Amount(**trans.amount.dump())
- tmp.subtract(trans.credit_account.amount)
- trans.credit_account.amount.set(**tmp.dump())
- else:
- # credit_account.amount >= trans.amount
- trans.credit_account.amount.subtract(trans.amount)
-
- trans.save()
- return HttpResponse(status=204)
-
##
# Serve a request to make a wire transfer. Allows fintech
@@ -906,17 +823,40 @@ def add_incoming(request, user_account):
subject = "%s %s" % (data.get("subject"), data.get("exchange_url"))
- credit_account = BankAccount.objects.get(account_no=data.get("credit_account"))
+ try:
+ credit_account = BankAccount.objects.get(account_no=data.get("credit_account"))
+ except BankAccount.DoesNotExist:
+ return JsonResponse(
+ {
+ "error": "Bank account not found"
+ },
+ status=404,
+ )
+
+
+ amount = Amount.parse(data.get("amount"))
+
+ if amount.currency != settings.TALER_CURRENCY:
+ return JsonResponse(
+ {
+ "error": "Incorrect currency"
+ },
+ status=400,
+ )
+
wtrans = wire_transfer(
- Amount.parse(data.get("amount")),
+ amount,
user_account.bankaccount,
credit_account,
subject,
)
return JsonResponse(
- {"row_id": wtrans.id, "timestamp": dict(t_ms=(int(wtrans.date.timestamp())*1000))}
+ {
+ "row_id": wtrans.id,
+ "timestamp": dict(t_ms=(int(wtrans.date.timestamp()) * 1000)),
+ }
)
@@ -927,10 +867,10 @@ def withdraw_headless_uri(request, user):
data = WithdrawHeadlessUri(json.loads(decode_body(request)))
amount = Amount.parse(data.get("amount"))
user_account = BankAccount.objects.get(user=user)
- debt_threshold = Amount.parse(settings.TALER_MAX_DEBT)
- if not check_transfer_allowed(
- user_account.amount, user_account.debit, debt_threshold, amount
- ):
+ withdraw_amount = SignedAmount(True, amount)
+ debt_threshold = SignedAmount.parse(settings.TALER_MAX_DEBT)
+ user_balance = SignedAmount(not user_account.debit, user_account.amount)
+ if user_balance - amount < -debt_threshold:
raise DebitLimitException(
f"Aborting payment initiated by '{user_account.user.username}', debit limit crossed."
)
@@ -941,15 +881,13 @@ def withdraw_headless_uri(request, user):
return JsonResponse({"taler_withdraw_uri": taler_withdraw_uri,})
-##
-# Serves a headless withdrawal request for the Taler protocol.
-#
-# @param request Django-specific HTTP request.
-# @return Django-specific HTTP response object.
@login_via_headers
@csrf_exempt
@require_POST
def withdraw_headless(request, user):
+ """
+ Serves a headless withdrawal request for the Taler protocol.
+ """
data = WithdrawHeadless(json.loads(decode_body(request)))
sender_payto = "payto://x-taler-bank/%s/%d" % (
@@ -965,7 +903,11 @@ def withdraw_headless(request, user):
else:
exchange_accno = get_acct_from_payto(exchange_payto)
- exchange_bankaccount = BankAccount.objects.get(account_no=exchange_accno)
+ try:
+ exchange_bankaccount = BankAccount.objects.get(account_no=exchange_accno)
+ except ObjectDoesNotExist:
+ err = dict(hint="Bank account not found")
+ return JsonResponse(err, status=404)
wire_transfer(
Amount.parse(data.get("amount")),
@@ -977,10 +919,12 @@ def withdraw_headless(request, user):
return JsonResponse(ret_obj)
-# Endpoint used by the browser and wallet to check withdraw status and
-# put in the exchange info.
@csrf_exempt
def api_withdraw_operation(request, withdraw_id):
+ """
+ Endpoint used by the browser and wallet to check withdraw status and
+ put in the exchange info.
+ """
try:
op = TalerWithdrawOperation.objects.get(withdraw_id=withdraw_id)
except ObjectDoesNotExist:
@@ -1038,32 +982,20 @@ def api_withdraw_operation(request, withdraw_id):
return JsonResponse(dict(error="only GET and POST are allowed"), status=305)
-def check_transfer_allowed(balance, balance_is_debit, debt_limit, transfer_amount):
- if balance_is_debit:
- total_debt = Amount(**transfer_amount.dump())
- total_debt.add(balance)
- return Amount.cmp(total_debt, debt_limit) <= 0
- max_transfer = Amount(**balance.dump())
- max_transfer.add(debt_limit)
- return Amount.cmp(transfer_amount, max_transfer) <= 0
-
-
-##
-# Serve a Taler withdrawal request; takes the amount chosen
-# by the user, and builds a response to trigger the wallet into
-# the withdrawal protocol
-#
-# @param request Django-specific HTTP request.
-# @return Django-specific HTTP response object.
@login_required
@require_POST
def start_withdrawal(request):
+ """
+ Serve a Taler withdrawal request; takes the amount chosen
+ by the user, and builds a response to trigger the wallet into
+ the withdrawal protocol
+ """
user_account = BankAccount.objects.get(user=request.user)
amount = Amount.parse(request.POST.get("kudos_amount", "not-given"))
- debt_threshold = Amount.parse(settings.TALER_MAX_DEBT)
- if not check_transfer_allowed(
- user_account.amount, user_account.debit, debt_threshold, amount
- ):
+ withdraw_amount = SignedAmount(True, amount)
+ debt_threshold = SignedAmount.parse(settings.TALER_MAX_DEBT)
+ user_balance = user_account.balance
+ if user_balance - withdraw_amount < -debt_threshold:
raise DebitLimitException(
f"Aborting payment initiated by '{user_account.user.username}', debit limit crossed."
)
@@ -1147,17 +1079,12 @@ def confirm_withdrawal(request, withdraw_id):
raise Exception("not reached")
-##
-# Make a wire transfer between two accounts (internal to the bank)
-#
-# @param amount (object type) how much money the wire transfer is worth.
-# FIXME: a check about whether this value is zero is missing
-# @param debit_account the account that gives money.
-# @param credit_account the account that receives money.
-# @return a @a BankTransaction object.
def wire_transfer(amount, debit_account, credit_account, subject):
+ """
+ Make a wire transfer between two accounts (internal to the bank)
+ """
LOGGER.debug(
- "%s => %s, %s, %s"
+ "transfering %s => %s, %s, %s"
% (
debit_account.account_no,
credit_account.account_no,
@@ -1176,45 +1103,19 @@ def wire_transfer(amount, debit_account, credit_account, subject):
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.
-
- threshold = Amount.parse(settings.TALER_MAX_DEBT)
if debit_account.user.username == "Bank":
- threshold = Amount.parse(settings.TALER_MAX_DEBT_BANK)
-
- LOGGER.info(
- f"Account {debit_account.user.username} "
- + f"(bal {debit_account.amount.stringify()}, thr {threshold.stringify()} requested transfer of"
- + f"{threshold.stringify()} to account {credit_account.user.username}"
- )
+ threshold = -SignedAmount.parse(settings.TALER_MAX_DEBT_BANK)
+ else:
+ threshold = -SignedAmount.parse(settings.TALER_MAX_DEBT)
- if Amount.cmp(debit_account.amount, threshold) > 0 and debit_account.debit:
+ if debit_account.balance - SignedAmount(True, amount) < threshold:
raise DebitLimitException(
f"Aborting payment initiated by '{debit_account.user.username}', debit limit crossed."
)
+ debit_account.balance -= SignedAmount(True, amount)
+ credit_account.balance += SignedAmount(True, amount)
+
with transaction.atomic():
debit_account.save()
credit_account.save()
diff --git a/talerbank/jinja2.py b/talerbank/jinja2.py
index 7287e1b..4decadd 100644
--- a/talerbank/jinja2.py
+++ b/talerbank/jinja2.py
@@ -131,13 +131,15 @@ def amount_stringify(amount):
def environment(**options):
env = Environment(**options)
- env.globals.update({
- 'static': static,
- 'url': url,
- 'settings_value': settings_value,
- 'env': env_get,
- 'is_valid_amount': is_valid_amount,
- 'amount_stringify': amount_stringify,
- 'tojson': tojson,
- })
+ env.globals.update(
+ {
+ "static": static,
+ "url": url,
+ "settings_value": settings_value,
+ "env": env_get,
+ "is_valid_amount": is_valid_amount,
+ "amount_stringify": amount_stringify,
+ "tojson": tojson,
+ }
+ )
return env
diff --git a/talerbank/settings.py b/talerbank/settings.py
index d50808b..6b39226 100644
--- a/talerbank/settings.py
+++ b/talerbank/settings.py
@@ -18,8 +18,7 @@ from taler.util.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"))
@@ -38,7 +37,7 @@ if not SECRET_KEY:
" TALER_BANK_SECRET_KEY env variable,"
" generating random secret"
)
- SECRET_KEY = base64.b64encode(os.urandom(32)).decode('utf-8')
+ SECRET_KEY = base64.b64encode(os.urandom(32)).decode("utf-8")
# SECURITY WARNING: don't run with debug turned on in production!
@@ -58,41 +57,43 @@ LOGIN_REDIRECT_URL = "index"
# Application definition
INSTALLED_APPS = [
- 'django.contrib.auth', 'django.contrib.contenttypes',
- 'django.contrib.sessions', 'django.contrib.messages',
- 'django.contrib.staticfiles', 'talerbank.app'
+ "django.contrib.auth",
+ "django.contrib.contenttypes",
+ "django.contrib.sessions",
+ "django.contrib.messages",
+ "django.contrib.staticfiles",
+ "talerbank.app",
]
MIDDLEWARE = [
- 'django.middleware.security.SecurityMiddleware',
- 'django.contrib.sessions.middleware.SessionMiddleware',
- 'django.middleware.common.CommonMiddleware',
- 'django.middleware.csrf.CsrfViewMiddleware',
- 'django.contrib.auth.middleware.AuthenticationMiddleware',
- 'django.contrib.sessions.middleware.SessionMiddleware',
- 'django.contrib.messages.middleware.MessageMiddleware',
- 'django.middleware.clickjacking.XFrameOptionsMiddleware',
- 'talerbank.app.middleware.ExceptionMiddleware',
- 'talerbank.app.middleware.DecompressionMiddleware'
+ "django.middleware.security.SecurityMiddleware",
+ "django.contrib.sessions.middleware.SessionMiddleware",
+ "django.middleware.common.CommonMiddleware",
+ "django.middleware.csrf.CsrfViewMiddleware",
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
+ "django.contrib.sessions.middleware.SessionMiddleware",
+ "django.contrib.messages.middleware.MessageMiddleware",
+ "django.middleware.clickjacking.XFrameOptionsMiddleware",
+ "talerbank.app.middleware.ExceptionMiddleware",
+ "talerbank.app.middleware.DecompressionMiddleware",
]
-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'
+TEMPLATES = [
+ {
+ "BACKEND": "django.template.backends.jinja2.Jinja2",
+ "DIRS": [
+ os.path.join(BASE_DIR, "talerbank/app/static/web-common/"),
+ os.path.join(BASE_DIR, "talerbank/app/templates"),
+ ],
+ "OPTIONS": {"environment": "talerbank.jinja2.environment"},
}
-}]
+]
# Disable those, since they don't work with
# jinja2 anyways.
TEMPLATE_CONTEXT_PROCESSORS = []
-WSGI_APPLICATION = 'talerbank.wsgi.application'
+WSGI_APPLICATION = "talerbank.wsgi.application"
# Database
# https://docs.djangoproject.com/en/1.9/ref/settings/#databases
@@ -103,17 +104,15 @@ 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(
- r"[a-z]+:///[a-z]+([\?][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 '%s', respect the format" \
- "'dbtype:///dbname'" % DBNAME)
+ LOGGER.error(
+ "Bad db string given '%s', respect the format" "'dbtype:///dbname'" % DBNAME
+ )
sys.exit(2)
DBCONFIG = {}
@@ -124,7 +123,7 @@ if DB_URL.scheme not in ("postgres") or DB_URL.scheme == "":
LOGGER.error("DB '%s' is not supported" % DB_URL.scheme)
sys.exit(1)
if DB_URL.scheme == "postgres":
- DBCONFIG["ENGINE"] = 'django.db.backends.postgresql_psycopg2'
+ DBCONFIG["ENGINE"] = "django.db.backends.postgresql_psycopg2"
DBCONFIG["NAME"] = DB_URL.path.lstrip("/")
if not DB_URL.netloc:
@@ -145,21 +144,21 @@ 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
# https://docs.djangoproject.com/en/1.9/topics/i18n/
-LANGUAGE_CODE = 'en-us'
+LANGUAGE_CODE = "en-us"
-TIME_ZONE = 'UTC'
+TIME_ZONE = "UTC"
USE_I18N = True
@@ -173,7 +172,7 @@ USE_TZ = False
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.9/howto/static-files/
-STATIC_URL = '/static/'
+STATIC_URL = "/static/"
STATICFILES_DIRS = [
os.path.join(BASE_DIR, "talerbank/app/static"),
@@ -189,9 +188,7 @@ 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 = 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
)
@@ -199,10 +196,15 @@ TALER_MAX_DEBT_BANK = TC.value_string(
TALER_DIGITS = TC.value_int("bank", "NDIGITS", default=2)
# Order matters
TALER_PREDEFINED_ACCOUNTS = [
- 'Bank', 'Exchange', 'Tor', 'GNUnet', 'Taler', 'FSF', 'Tutorial', 'Survey'
+ "Bank",
+ "Exchange",
+ "Tor",
+ "GNUnet",
+ "Taler",
+ "FSF",
+ "Tutorial",
+ "Survey",
]
-TALER_EXPECTS_DONATIONS = ['Tor', 'GNUnet', 'Taler', 'FSF']
+TALER_EXPECTS_DONATIONS = ["Tor", "GNUnet", "Taler", "FSF"]
TALER_SUGGESTED_EXCHANGE = TC.value_string("bank", "suggested_exchange")
-TALER_SUGGESTED_EXCHANGE_PAYTO = TC.value_string(
- "bank", "suggested_exchange_payto"
-)
+TALER_SUGGESTED_EXCHANGE_PAYTO = TC.value_string("bank", "suggested_exchange_payto")