From f7829956ff2ee4b517f010a3f23bc62012e881e7 Mon Sep 17 00:00:00 2001 From: Marcello Stanisci Date: Mon, 27 May 2019 18:03:08 +0200 Subject: use latest config logic --- talerdonations/talerconfig.py | 392 ++++++++++++++++++++++++++++++++---------- 1 file changed, 304 insertions(+), 88 deletions(-) (limited to 'talerdonations') diff --git a/talerdonations/talerconfig.py b/talerdonations/talerconfig.py index a7ca065..2d2c78e 100644 --- a/talerdonations/talerconfig.py +++ b/talerdonations/talerconfig.py @@ -1,22 +1,21 @@ -# This file is part of TALER -# (C) 2016 INRIA +## +# This file is part of TALER +# (C) 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 +# You should have received a copy of the GNU General Public License along with +# TALER; see the file COPYING. If not, see # -# @author Florian Dold - -""" -Parse GNUnet-style configurations in pure Python -""" +# @author Florian Dold +# @author Marcello Stanisci +# @brief Parse GNUnet-style configurations in pure Python import logging import collections @@ -24,34 +23,48 @@ import os import weakref import sys import re +from typing import Callable, Any LOGGER = logging.getLogger(__name__) __all__ = ["TalerConfig"] TALER_DATADIR = None + try: - # not clear if this is a good idea ... from talerpaths import TALER_DATADIR as t TALER_DATADIR = t except ImportError: pass +## +# Exception class for a any configuration error. class ConfigurationError(Exception): pass +## +# Exception class for malformed strings having with parameter +# expansion. class ExpansionSyntaxError(Exception): pass - -def expand(var, getter): - """ - Do shell-style parameter expansion. - Supported syntax: - - ${X} - - ${X:-Y} - - $X - """ +## +# Do shell-style parameter expansion. +# Supported syntax: +# - ${X} +# - ${X:-Y} +# - $X +# +# @param var entire config value that might contain a parameter +# to expand. +# @param getter function that is in charge of returning _some_ +# value to be used in place of the parameter to expand. +# Typically, the replacement is searched first under the +# PATHS section of the current configuration, or (if not +# found) in the environment. +# +# @return the expanded config value. +def expand(var: str, getter: Callable[[str], str]) -> str: pos = 0 result = "" while pos != -1: @@ -88,38 +101,25 @@ def expand(var, getter): result = result + replace pos = end - return result + var[pos:] - -class OptionDict(collections.defaultdict): - def __init__(self, config, section_name): - self.config = weakref.ref(config) - self.section_name = section_name - super().__init__() - def __missing__(self, key): - entry = Entry(self.config(), self.section_name, key) - self[key] = entry - return entry - def __getitem__(self, chunk): - return super().__getitem__(chunk.lower()) - def __setitem__(self, chunk, value): - super().__setitem__(chunk.lower(), value) - - -class SectionDict(collections.defaultdict): - def __missing__(self, key): - value = OptionDict(self, key) - self[key] = value - return value - def __getitem__(self, chunk): - return super().__getitem__(chunk.lower()) - def __setitem__(self, chunk, value): - super().__setitem__(chunk.lower(), value) - - +## +# A configuration entry. class Entry: - def __init__(self, config, section, option, **kwargs): + + ## + # Init constructor. + # + # @param self the object itself. + # @param config reference to a configuration object - FIXME + # define "configuration object". + # @param section name of the config section where this entry + # got defined. + # @param option name of the config option associated with this + # entry. + # @param kwargs keyword arguments that hold the value / filename + # / line number of this current option. + def __init__(self, config, section: str, option: str, **kwargs) -> None: self.value = kwargs.get("value") self.filename = kwargs.get("filename") self.lineno = kwargs.get("lineno") @@ -127,14 +127,36 @@ class Entry: self.option = option self.config = weakref.ref(config) - def __repr__(self): + ## + # XML representation of this entry. + # + # @param self the object itself. + # @return XML string holding all the relevant information + # for this entry. + def __repr__(self) -> str: return "" \ % (self.section, self.option, repr(self.value),) - def __str__(self): + ## + # Return the value for this entry, as is. + # + # @param self the object itself. + # @return the config value. + def __str__(self) -> Any: return self.value - def value_string(self, default=None, required=False, warn=False): + ## + # Return entry value, accepting defaults. + # + # @param self the object itself + # @param default default value to return if none was found. + # @param required indicate whether the value was required or not. + # If the value was required, but was not found, an exception + # is found. + # @param warn if True, outputs a warning message if the value was + # not found -- regardless of it being required or not. + # @return the value, or the given @a default, if not found. + def value_string(self, default=None, required=False, warn=False) -> str: if required and self.value is None: raise ConfigurationError("Missing required option '%s' in section '%s'" \ % (self.option.upper(), self.section.upper())) @@ -149,7 +171,16 @@ class Entry: return default return self.value - def value_int(self, default=None, required=False, warn=False): + ## + # Return entry value as a _int_. Raise exception if the + # value is not convertible to a integer. + # + # @param self the object itself + # @param default currently ignored. + # @param required currently ignored. + # @param warn currently ignored. + # @return the value, or the given @a default, if not found. + def value_int(self, default=None, required=False, warn=False) -> int: value = self.value_string(default, warn, required) if value is None: return None @@ -158,8 +189,13 @@ class Entry: except ValueError: raise ConfigurationError("Expected number for option '%s' in section '%s'" \ % (self.option.upper(), self.section.upper())) - - def _getsubst(self, key): + ## + # Fetch value to substitute to expansion variables. + # + # @param self the object itself. + # @param key the value's name to lookup. + # @return the value, if found, None otherwise. + def _getsubst(self, key: str) -> Any: value = self.config()["paths"][key].value if value is not None: return value @@ -168,31 +204,136 @@ class Entry: return value return None - def value_filename(self, default=None, required=False, warn=False): + ## + # Fetch the config value that should be a filename, + # taking care of invoking the variable-expansion logic first. + # + # @param self the object itself. + # @param default currently ignored. + # @param required currently ignored. + # @param warn currently ignored. + # @return the (expanded) filename. + def value_filename(self, default=None, required=False, warn=False) -> str: value = self.value_string(default, required, warn) if value is None: return None return expand(value, self._getsubst) - def location(self): + ## + # Give the filename and line number of this config entry. + # + # @param self this object. + # @return :, or "" if one + # is not known. + def location(self) -> str: if self.filename is None or self.lineno is None: return "" return "%s:%s" % (self.filename, self.lineno) +## +# Represent a section by inheriting from 'defaultdict'. +class OptionDict(collections.defaultdict): + + ## + # Init constructor. + # + # @param self the object itself + # @param config the "config" object -- typically a @a TalerConfig instance. + # @param section_name the section name to assign to this object. + def __init__(self, config, section_name: str) -> None: + self.config = weakref.ref(config) + self.section_name = section_name + super().__init__() + + ## + # Logic to run when a non-existent key is dereferenced. + # Just create and return a empty config @a Entry. Note + # that the freshly created entry will nonetheless put + # under the accessed key (that *does* become existent + # afterwards). + # + # @param self the object itself. + # @param key the key attempted to be accessed. + # @return the no-value entry. + def __missing__(self, key: str) -> Entry: + entry = Entry(self.config(), self.section_name, key) + self[key] = entry + return entry + + ## + # Attempt to fetch one value from the object. + # + # @param self the object itself. + # @param chunk the key (?) that is tried to access. + # @return the object, if it exists, or a freshly created + # (empty) one, if it doesn't exist. + def __getitem__(self, chunk: str) -> Entry: + return super().__getitem__(chunk.lower()) + + ## + # Set one value into the object. + # + # @param self the object itself. + # @param chunk key under which the value is going to be set. + # @param value value to set the @a chunk to. + def __setitem__(self, chunk: str, value: Entry) -> None: + super().__setitem__(chunk.lower(), value) + +## +# Collection of all the (@a OptionDict) sections. +class SectionDict(collections.defaultdict): + ## + # Automatically invoked when a missing section is + # dereferenced. It creates the missing - empty - section. + # + # @param self the object itself. + # @param key the dereferenced section name. + # @return the freshly created section. + def __missing__(self, key): + value = OptionDict(self, key) + self[key] = value + return value + + ## + # Attempt to retrieve a section. + # + # @param self the object itself. + # @param chunk the section name. + def __getitem__(self, chunk: str) -> OptionDict: + return super().__getitem__(chunk.lower()) + + ## + # Set a section. + # + # @param self the object itself. + # @param chunk the section name to set. + # @param value the value to set under that @a chunk. + def __setitem__(self, chunk: str, value: OptionDict) -> None: + super().__setitem__(chunk.lower(), value) + +## +# One loaded taler configuration, including base configuration +# files and included files. class TalerConfig: - """ - One loaded taler configuration, including base configuration - files and included files. - """ - def __init__(self): - """ - Initialize an empty configuration - """ - self.sections = SectionDict() - - # defaults != config file: the first is the 'base' - # whereas the second overrides things from the first. + + ## + # Init constructor.. + # + # @param self the object itself. + def __init__(self) -> None: + self.sections = SectionDict() # just plain dict + + ## + # Load a configuration file, instantiating a config object. + # + # @param filename the filename where to load the configuration + # from. If None, it defaults "taler.conf". + # @param load_defaults if True, then defaults values are loaded + # (from canonical directories like "/share/config.d/taler/") + # before the actual configuration file. This latter then + # can override some/all the defaults. + # @return the config object. @staticmethod def from_file(filename=None, load_defaults=True): cfg = TalerConfig() @@ -204,22 +345,57 @@ class TalerConfig: filename = os.path.expanduser("~/.config/taler.conf") if load_defaults: cfg.load_defaults() - cfg.load_file(filename) + cfg.load_file(os.path.expanduser(filename)) return cfg - def value_string(self, section, option, **kwargs): + ## + # Get a string value from the config. + # + # @param self the config object itself. + # @param section the section to fetch the value from. + # @param option the value's option name. + # @param kwargs dict argument with instructions about + # the value retrieval logic. + # @return the wanted string (or a default / exception if + # a error occurs). + def value_string(self, section, option, **kwargs) -> str: return self.sections[section][option].value_string( kwargs.get("default"), kwargs.get("required"), kwargs.get("warn")) - def value_filename(self, section, option, **kwargs): + ## + # Get a value from the config that should be a filename. + # The variable expansion for the path's components is internally managed. + # + # @param self the config object itself. + # @param section the section to fetch the value from. + # @param option the value's option name. + # @param kwargs dict argument with instructions about + # the value retrieval logic. + # @return the wanted filename (or a default / exception if + # a error occurs). + def value_filename(self, section, option, **kwargs) -> str: return self.sections[section][option].value_filename( kwargs.get("default"), kwargs.get("required"), kwargs.get("warn")) - def value_int(self, section, option, **kwargs): + ## + # Get a integer value from the config. + # + # @param self the config object itself. + # @param section the section to fetch the value from. + # @param option the value's option name. + # @param kwargs dict argument with instructions about + # the value retrieval logic. + # @return the wanted integer (or a default / exception if + # a error occurs). + def value_int(self, section, option, **kwargs) -> int: return self.sections[section][option].value_int( kwargs.get("default"), kwargs.get("required"), kwargs.get("warn")) - def load_defaults(self): + ## + # Load default values from canonical locations. + # + # @param self the object itself. + def load_defaults(self) -> None: base_dir = os.environ.get("TALER_BASE_CONFIG") if base_dir: self.load_dir(base_dir) @@ -236,16 +412,25 @@ class TalerConfig: return LOGGER.warning("no base directory found") + ## + # Load configuration from environment variable + # TALER_CONFIG_FILE or from default location if the + # variable is not set. + # + # @param args currently unused. + # @param kwargs kwargs for subroutine @a from_file. + # @return freshly instantiated config object. @staticmethod def from_env(*args, **kwargs): - """ - Load configuration from environment variable TALER_CONFIG_FILE - or from default location if the variable is not set. - """ filename = os.environ.get("TALER_CONFIG_FILE") return TalerConfig.from_file(filename, *args, **kwargs) - def load_dir(self, dirname): + ## + # Load config values from _each_ file found in a directory. + # + # @param self the object itself. + # @param dirname the directory to crawl in the look for config files. + def load_dir(self, dirname) -> None: try: files = os.listdir(dirname) except FileNotFoundError: @@ -256,7 +441,11 @@ class TalerConfig: continue self.load_file(os.path.join(dirname, file)) - def load_file(self, filename): + ## + # Load config values from a file. + # + # @param filename config file to take the values from. + def load_file(self, filename) -> None: sections = self.sections try: with open(filename, "r") as file: @@ -271,6 +460,16 @@ class TalerConfig: if line.startswith("#"): # comment continue + if line.startswith("@INLINE@"): + pair = line.split() + if 2 != len(pair): + LOGGER.error("invalid inlined config filename given ('%s')" % line) + continue + if pair[1].startswith("/"): + self.load_file(pair[1]) + else: + self.load_file(os.path.join(os.path.dirname(filename), pair[1])) + continue if line.startswith("["): if not line.endswith("]"): LOGGER.error("invalid section header in line %s: %s", @@ -299,8 +498,16 @@ class TalerConfig: LOGGER.error("Configuration file (%s) not found", filename) sys.exit(3) - - def dump(self): + ## + # Dump the textual representation of a config object. + # + # Format: + # + # [section] + # option = value # FIXME (what is location?) + # + # @param self the object itself, that will be dumped. + def dump(self) -> None: for kv_section in self.sections.items(): print("[%s]" % (kv_section[1].section_name,)) for kv_option in kv_section[1].items(): @@ -309,7 +516,16 @@ class TalerConfig: kv_option[1].value, kv_option[1].location())) - def __getitem__(self, chunk): + + ## + # Return a whole section from this object. + # + # @param self the object itself. + # @param chunk name of the section to return. + # @return the section - note that if the section is + # not found, a empty one will created on the fly, + # then set under 'chunk', and returned. + def __getitem__(self, chunk: str) -> OptionDict: if isinstance(chunk, str): return self.sections[chunk] raise TypeError("index must be string") -- cgit v1.2.3 From 5cdf3b0799b848b0b9f547b49b7b57f6c27df372 Mon Sep 17 00:00:00 2001 From: Marcello Stanisci Date: Mon, 27 May 2019 18:08:22 +0200 Subject: ux --- talerdonations/talerconfig.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'talerdonations') diff --git a/talerdonations/talerconfig.py b/talerdonations/talerconfig.py index 2d2c78e..9598be5 100644 --- a/talerdonations/talerconfig.py +++ b/talerdonations/talerconfig.py @@ -32,6 +32,7 @@ __all__ = ["TalerConfig"] TALER_DATADIR = None try: + # not clear if this is a good idea ... from talerpaths import TALER_DATADIR as t TALER_DATADIR = t except ImportError: @@ -343,6 +344,7 @@ class TalerConfig: filename = os.path.join(xdg, "taler.conf") else: filename = os.path.expanduser("~/.config/taler.conf") + logging.info("Loading default config: (%s)" % filename) if load_defaults: cfg.load_defaults() cfg.load_file(os.path.expanduser(filename)) -- cgit v1.2.3 From dd835689654c18353b02f9f5809c7746de222ab9 Mon Sep 17 00:00:00 2001 From: Marcello Stanisci Date: Mon, 27 May 2019 18:17:12 +0200 Subject: talerconfig --- talerdonations/talerconfig.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'talerdonations') diff --git a/talerdonations/talerconfig.py b/talerdonations/talerconfig.py index 9598be5..4a44c97 100644 --- a/talerdonations/talerconfig.py +++ b/talerdonations/talerconfig.py @@ -344,7 +344,7 @@ class TalerConfig: filename = os.path.join(xdg, "taler.conf") else: filename = os.path.expanduser("~/.config/taler.conf") - logging.info("Loading default config: (%s)" % filename) + print("Loading default config: (%s)" % filename) if load_defaults: cfg.load_defaults() cfg.load_file(os.path.expanduser(filename)) @@ -497,7 +497,8 @@ class TalerConfig: value=value, filename=filename, lineno=lineno) sections[current_section][key] = entry except FileNotFoundError: - LOGGER.error("Configuration file (%s) not found", filename) + # not logging here, as this interests the final user mostly. + print("Configuration file (%s) not found" % filename) sys.exit(3) ## -- cgit v1.2.3 From 455755737e6dcb7f938f7e6f9672bfc3e38f0a73 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Thu, 29 Aug 2019 19:01:43 +0200 Subject: yapf style file --- .style.yapf | 5 ++ talerdonations/donations/donations.py | 76 ++++++++++++++++------------ talerdonations/talerconfig.py | 95 ++++++++++++++++++++++++----------- talerdonations/tests.py | 1 + 4 files changed, 116 insertions(+), 61 deletions(-) create mode 100644 .style.yapf (limited to 'talerdonations') diff --git a/.style.yapf b/.style.yapf new file mode 100644 index 0000000..3b39780 --- /dev/null +++ b/.style.yapf @@ -0,0 +1,5 @@ +[style] +based_on_style = pep8 +coalesce_brackets=True +column_limit=80 +dedent_closing_brackets=True diff --git a/talerdonations/donations/donations.py b/talerdonations/donations/donations.py index f0f97c5..5655734 100644 --- a/talerdonations/donations/donations.py +++ b/talerdonations/donations/donations.py @@ -53,6 +53,7 @@ app.config.from_object(__name__) def utility_processor(): def env(name, default=None): return os.environ.get(name, default) + return dict(env=env) @@ -76,7 +77,9 @@ def err_abort(abort_status_code, **params): def backend_get(endpoint, params): headers = {"Authorization": "ApiKey " + APIKEY} try: - resp = requests.get(urljoin(BACKEND_URL, endpoint), params=params, headers=headers) + resp = requests.get(urljoin(BACKEND_URL, endpoint), + params=params, + headers=headers) except requests.ConnectionError: err_abort(500, message="Could not establish connection to backend") try: @@ -84,8 +87,10 @@ def backend_get(endpoint, params): except ValueError: err_abort(500, message="Could not parse response from backend") if resp.status_code != 200: - err_abort(500, message="Backend returned error status", - json=response_json, status_code=resp.status_code) + err_abort(500, + message="Backend returned error status", + json=response_json, + status_code=resp.status_code) return response_json @@ -100,21 +105,25 @@ def backend_get(endpoint, params): def backend_post(endpoint, json): headers = {"Authorization": "ApiKey " + APIKEY} try: - resp = requests.post(urljoin(BACKEND_URL, endpoint), json=json, headers=headers) + resp = requests.post(urljoin(BACKEND_URL, endpoint), + json=json, + headers=headers) except requests.ConnectionError: err_abort(500, message="Could not establish connection to backend") try: response_json = resp.json() except ValueError: - err_abort(500, message="Could not parse response from backend", + err_abort(500, + message="Could not parse response from backend", status_code=resp.status_code) if resp.status_code != 200: - err_abort(500, message="Backend returned error status", - json=response_json, status_code=resp.status_code) + err_abort(500, + message="Backend returned error status", + json=response_json, + status_code=resp.status_code) return response_json - ## # Inspect GET arguments in the look for a parameter. # @@ -126,6 +135,7 @@ def expect_parameter(name): return err_abort(400, message="parameter '{}' required".format(name)) return val + ## # "Fallback" exception handler to capture all the unmanaged errors. # @@ -138,13 +148,16 @@ def internal_error(e): message="Internal error", stack=traceback.format_exc()) + ## # Serve the main index page. # # @return response object of the index page. @app.route("/") def index(): - return flask.render_template("templates/index.html", merchant_currency=CURRENCY) + return flask.render_template("templates/index.html", + merchant_currency=CURRENCY) + ## # Serve the "/javascript" page. @@ -155,7 +168,6 @@ def javascript_licensing(): return flask.render_template("templates/javascript.html") - ## # Serve the "/checkout" page. This page lets the # user pick the payment method they want to use, @@ -167,12 +179,11 @@ def checkout(): amount = expect_parameter("donation_amount") donation_receiver = expect_parameter("donation_receiver") donation_donor = expect_parameter("donation_donor") - return flask.render_template( - "templates/checkout.html", - donation_amount=amount, - donation_receiver=donation_receiver, - donation_donor=donation_donor, - merchant_currency=CURRENCY) + return flask.render_template("templates/checkout.html", + donation_amount=amount, + donation_receiver=donation_receiver, + donation_donor=donation_donor, + merchant_currency=CURRENCY) ## @@ -182,8 +193,7 @@ def checkout(): # @return response object about the mentioned impossibility. @app.route("/provider-not-supported") def provider_not_supported(): - return flask.render_template( "templates/provider-not-supported.html") - + return flask.render_template("templates/provider-not-supported.html") ## @@ -201,18 +211,24 @@ def donate(): payment_system = expect_parameter("payment_system") if payment_system != "taler": return flask.redirect(flask.url_for("provider_not_supported")) - fulfillment_url = flask.url_for("fulfillment", receiver=donation_receiver, _external=True) + fulfillment_url = flask.url_for("fulfillment", + receiver=donation_receiver, + _external=True) order = dict( amount=donation_amount, - extra=dict(donor=donation_donor, receiver=donation_receiver, amount=donation_amount), + extra=dict(donor=donation_donor, + receiver=donation_receiver, + amount=donation_amount), fulfillment_url=fulfillment_url, instance=donation_receiver, summary="Donation to {}".format(donation_receiver), ) order_resp = backend_post("order", dict(order=order)) order_id = order_resp["order_id"] - return flask.redirect(flask.url_for("fulfillment", receiver=donation_receiver, order_id=order_id)) - + return flask.redirect( + flask.url_for("fulfillment", + receiver=donation_receiver, + order_id=order_id)) ## @@ -226,8 +242,7 @@ def donate(): @app.route("/donation/") def fulfillment(receiver): order_id = expect_parameter("order_id") - pay_params = dict(instance=receiver, - order_id=order_id) + pay_params = dict(instance=receiver, order_id=order_id) pay_status = backend_get("check-payment", pay_params) if pay_status.get("payment_redirect_url"): @@ -235,13 +250,12 @@ def fulfillment(receiver): if pay_status.get("paid"): extra = pay_status["contract_terms"]["extra"] - return flask.render_template( - "templates/fulfillment.html", - donation_receiver=extra["receiver"], - donation_amount=extra["amount"], - donation_donor=extra["donor"], - order_id=order_id, - currency=CURRENCY) + return flask.render_template("templates/fulfillment.html", + donation_receiver=extra["receiver"], + donation_amount=extra["amount"], + donation_donor=extra["donor"], + order_id=order_id, + currency=CURRENCY) # no pay_redirect but article not paid, this should never happen! err_abort(500, message="Internal error, invariant failed", json=pay_status) diff --git a/talerdonations/talerconfig.py b/talerdonations/talerconfig.py index 4a44c97..7959ec2 100644 --- a/talerdonations/talerconfig.py +++ b/talerdonations/talerconfig.py @@ -38,17 +38,20 @@ try: except ImportError: pass + ## # Exception class for a any configuration error. class ConfigurationError(Exception): pass + ## # Exception class for malformed strings having with parameter # expansion. class ExpansionSyntaxError(Exception): pass + ## # Do shell-style parameter expansion. # Supported syntax: @@ -80,7 +83,7 @@ def expand(var: str, getter: Callable[[str], str]) -> str: end += 1 if balance != 0: raise ExpansionSyntaxError("unbalanced parentheses") - piece = var[start+2:end-1] + piece = var[start + 2:end - 1] if piece.find(":-") > 0: varname, alt = piece.split(":-", 1) replace = getter(varname) @@ -93,9 +96,9 @@ def expand(var: str, getter: Callable[[str], str]) -> str: replace = var[start:end] else: end = start + 2 - while end < len(var) and var[start+1:end+1].isalnum(): + while end < len(var) and var[start + 1:end + 1].isalnum(): end += 1 - varname = var[start+1:end] + varname = var[start + 1:end] replace = getter(varname) if replace is None: replace = var[start:end] @@ -104,6 +107,7 @@ def expand(var: str, getter: Callable[[str], str]) -> str: return result + var[pos:] + ## # A configuration entry. class Entry: @@ -164,11 +168,14 @@ class Entry: if self.value is None: if warn: if default is not None: - LOGGER.warning("Configuration is missing option '%s' in section '%s',\ - falling back to '%s'", self.option, self.section, default) + LOGGER.warning( + "Configuration is missing option '%s' in section '%s',\ + falling back to '%s'", self.option, + self.section, default) else: - LOGGER.warning("Configuration ** is missing option '%s' in section '%s'", - self.option.upper(), self.section.upper()) + LOGGER.warning( + "Configuration ** is missing option '%s' in section '%s'", + self.option.upper(), self.section.upper()) return default return self.value @@ -190,6 +197,7 @@ class Entry: except ValueError: raise ConfigurationError("Expected number for option '%s' in section '%s'" \ % (self.option.upper(), self.section.upper())) + ## # Fetch value to substitute to expansion variables. # @@ -231,6 +239,7 @@ class Entry: return "" return "%s:%s" % (self.filename, self.lineno) + ## # Represent a section by inheriting from 'defaultdict'. class OptionDict(collections.defaultdict): @@ -280,6 +289,7 @@ class OptionDict(collections.defaultdict): def __setitem__(self, chunk: str, value: Entry) -> None: super().__setitem__(chunk.lower(), value) + ## # Collection of all the (@a OptionDict) sections. class SectionDict(collections.defaultdict): @@ -313,6 +323,7 @@ class SectionDict(collections.defaultdict): def __setitem__(self, chunk: str, value: OptionDict) -> None: super().__setitem__(chunk.lower(), value) + ## # One loaded taler configuration, including base configuration # files and included files. @@ -323,7 +334,7 @@ class TalerConfig: # # @param self the object itself. def __init__(self) -> None: - self.sections = SectionDict() # just plain dict + self.sections = SectionDict() # just plain dict ## # Load a configuration file, instantiating a config object. @@ -465,36 +476,49 @@ class TalerConfig: if line.startswith("@INLINE@"): pair = line.split() if 2 != len(pair): - LOGGER.error("invalid inlined config filename given ('%s')" % line) - continue + LOGGER.error( + "invalid inlined config filename given ('%s')" + % line) + continue if pair[1].startswith("/"): self.load_file(pair[1]) else: - self.load_file(os.path.join(os.path.dirname(filename), pair[1])) + self.load_file( + os.path.join(os.path.dirname(filename), + pair[1])) continue if line.startswith("["): if not line.endswith("]"): - LOGGER.error("invalid section header in line %s: %s", - lineno, repr(line)) + LOGGER.error( + "invalid section header in line %s: %s", + lineno, repr(line)) section_name = line.strip("[]").strip().strip('"') current_section = section_name continue if current_section is None: - LOGGER.error("option outside of section in line %s: %s", lineno, repr(line)) + LOGGER.error( + "option outside of section in line %s: %s", lineno, + repr(line)) continue pair = line.split("=", 1) if len(pair) != 2: - LOGGER.error("invalid option in line %s: %s", lineno, repr(line)) + LOGGER.error("invalid option in line %s: %s", lineno, + repr(line)) key = pair[0].strip() value = pair[1].strip() if value.startswith('"'): value = value[1:] if not value.endswith('"'): - LOGGER.error("mismatched quotes in line %s: %s", lineno, repr(line)) + LOGGER.error("mismatched quotes in line %s: %s", + lineno, repr(line)) else: value = value[:-1] - entry = Entry(self.sections, current_section, key, - value=value, filename=filename, lineno=lineno) + entry = Entry(self.sections, + current_section, + key, + value=value, + filename=filename, + lineno=lineno) sections[current_section][key] = entry except FileNotFoundError: # not logging here, as this interests the final user mostly. @@ -503,23 +527,22 @@ class TalerConfig: ## # Dump the textual representation of a config object. - # + # # Format: - # + # # [section] # option = value # FIXME (what is location?) # # @param self the object itself, that will be dumped. def dump(self) -> None: for kv_section in self.sections.items(): - print("[%s]" % (kv_section[1].section_name,)) + print("[%s]" % (kv_section[1].section_name, )) for kv_option in kv_section[1].items(): print("%s = %s # %s" % \ (kv_option[1].option, kv_option[1].value, kv_option[1].location())) - ## # Return a whole section from this object. # @@ -538,14 +561,26 @@ if __name__ == "__main__": import argparse PARSER = argparse.ArgumentParser() - PARSER.add_argument("--section", "-s", dest="section", - default=None, metavar="SECTION") - PARSER.add_argument("--option", "-o", dest="option", - default=None, metavar="OPTION") - PARSER.add_argument("--config", "-c", dest="config", - default=None, metavar="FILE") - PARSER.add_argument("--filename", "-f", dest="expand_filename", - default=False, action='store_true') + PARSER.add_argument("--section", + "-s", + dest="section", + default=None, + metavar="SECTION") + PARSER.add_argument("--option", + "-o", + dest="option", + default=None, + metavar="OPTION") + PARSER.add_argument("--config", + "-c", + dest="config", + default=None, + metavar="FILE") + PARSER.add_argument("--filename", + "-f", + dest="expand_filename", + default=False, + action='store_true') ARGS = PARSER.parse_args() TC = TalerConfig.from_file(ARGS.config) diff --git a/talerdonations/tests.py b/talerdonations/tests.py index c9918c2..2e95b60 100644 --- a/talerdonations/tests.py +++ b/talerdonations/tests.py @@ -27,6 +27,7 @@ from talerdonations.talerconfig import TalerConfig TC = TalerConfig.from_env() CURRENCY = TC["taler"]["currency"].value_string(required=True) + ## # Main class that gathers all the tests. class DonationsTestCase(unittest.TestCase): -- cgit v1.2.3 From 9c282aa69a4d25f186e591a075b0f2830b5ec45a Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Thu, 29 Aug 2019 19:02:31 +0200 Subject: make pretty --- setup.py | 51 +++++++++-------- talerdonations/donations/donations.py | 100 ++++++++++++++++++++-------------- talerdonations/talerconfig.py | 95 ++++++++++++++++++-------------- 3 files changed, 136 insertions(+), 110 deletions(-) (limited to 'talerdonations') diff --git a/setup.py b/setup.py index 71526a9..906567c 100755 --- a/setup.py +++ b/setup.py @@ -1,28 +1,27 @@ from setuptools import setup, find_packages -setup(name='talerdonations', - version='0.6.0pre1', - description='Example donations site for GNU Taler', - url='git://taler.net/donations', - author=['Marcello Stanisci', 'Florian Dold'], - author_email=['stanisci.m@gmail.com', 'dold@taler.net'], - license='GPL', - packages=find_packages(), - install_requires=["Flask>=0.10", - "requests", - "uwsgi", - "jsmin"], - tests_require=["mock", "nose"], - test_suite="nose.collector", - package_data={ - '':[ - "donations/templates/*.html", - "donations/static/*.svg", - "donations/static/*.css", - "donations/static/web-common/*.png", - "donations/static/web-common/*.css", - "donations/static/web-common/*.html", - ] - }, - scripts=['./bin/taler-merchant-donations'], - zip_safe=False) +setup( + name='talerdonations', + version='0.6.0pre1', + description='Example donations site for GNU Taler', + url='git://taler.net/donations', + author=['Marcello Stanisci', 'Florian Dold'], + author_email=['stanisci.m@gmail.com', 'dold@taler.net'], + license='GPL', + packages=find_packages(), + install_requires=["Flask>=0.10", "requests", "uwsgi", "jsmin"], + tests_require=["mock", "nose"], + test_suite="nose.collector", + package_data={ + '': [ + "donations/templates/*.html", + "donations/static/*.svg", + "donations/static/*.css", + "donations/static/web-common/*.png", + "donations/static/web-common/*.css", + "donations/static/web-common/*.html", + ] + }, + scripts=['./bin/taler-merchant-donations'], + zip_safe=False +) diff --git a/talerdonations/donations/donations.py b/talerdonations/donations/donations.py index 5655734..220fbb7 100644 --- a/talerdonations/donations/donations.py +++ b/talerdonations/donations/donations.py @@ -77,9 +77,9 @@ def err_abort(abort_status_code, **params): def backend_get(endpoint, params): headers = {"Authorization": "ApiKey " + APIKEY} try: - resp = requests.get(urljoin(BACKEND_URL, endpoint), - params=params, - headers=headers) + resp = requests.get( + urljoin(BACKEND_URL, endpoint), params=params, headers=headers + ) except requests.ConnectionError: err_abort(500, message="Could not establish connection to backend") try: @@ -87,10 +87,12 @@ def backend_get(endpoint, params): except ValueError: err_abort(500, message="Could not parse response from backend") if resp.status_code != 200: - err_abort(500, - message="Backend returned error status", - json=response_json, - status_code=resp.status_code) + err_abort( + 500, + message="Backend returned error status", + json=response_json, + status_code=resp.status_code + ) return response_json @@ -105,22 +107,26 @@ def backend_get(endpoint, params): def backend_post(endpoint, json): headers = {"Authorization": "ApiKey " + APIKEY} try: - resp = requests.post(urljoin(BACKEND_URL, endpoint), - json=json, - headers=headers) + resp = requests.post( + urljoin(BACKEND_URL, endpoint), json=json, headers=headers + ) except requests.ConnectionError: err_abort(500, message="Could not establish connection to backend") try: response_json = resp.json() except ValueError: - err_abort(500, - message="Could not parse response from backend", - status_code=resp.status_code) + err_abort( + 500, + message="Could not parse response from backend", + status_code=resp.status_code + ) if resp.status_code != 200: - err_abort(500, - message="Backend returned error status", - json=response_json, - status_code=resp.status_code) + err_abort( + 500, + message="Backend returned error status", + json=response_json, + status_code=resp.status_code + ) return response_json @@ -144,9 +150,11 @@ def expect_parameter(name): # (and execution stack!). @app.errorhandler(Exception) def internal_error(e): - return flask.render_template("templates/error.html", - message="Internal error", - stack=traceback.format_exc()) + return flask.render_template( + "templates/error.html", + message="Internal error", + stack=traceback.format_exc() + ) ## @@ -155,8 +163,9 @@ def internal_error(e): # @return response object of the index page. @app.route("/") def index(): - return flask.render_template("templates/index.html", - merchant_currency=CURRENCY) + return flask.render_template( + "templates/index.html", merchant_currency=CURRENCY + ) ## @@ -179,11 +188,13 @@ def checkout(): amount = expect_parameter("donation_amount") donation_receiver = expect_parameter("donation_receiver") donation_donor = expect_parameter("donation_donor") - return flask.render_template("templates/checkout.html", - donation_amount=amount, - donation_receiver=donation_receiver, - donation_donor=donation_donor, - merchant_currency=CURRENCY) + return flask.render_template( + "templates/checkout.html", + donation_amount=amount, + donation_receiver=donation_receiver, + donation_donor=donation_donor, + merchant_currency=CURRENCY + ) ## @@ -211,14 +222,16 @@ def donate(): payment_system = expect_parameter("payment_system") if payment_system != "taler": return flask.redirect(flask.url_for("provider_not_supported")) - fulfillment_url = flask.url_for("fulfillment", - receiver=donation_receiver, - _external=True) + fulfillment_url = flask.url_for( + "fulfillment", receiver=donation_receiver, _external=True + ) order = dict( amount=donation_amount, - extra=dict(donor=donation_donor, - receiver=donation_receiver, - amount=donation_amount), + extra=dict( + donor=donation_donor, + receiver=donation_receiver, + amount=donation_amount + ), fulfillment_url=fulfillment_url, instance=donation_receiver, summary="Donation to {}".format(donation_receiver), @@ -226,9 +239,10 @@ def donate(): order_resp = backend_post("order", dict(order=order)) order_id = order_resp["order_id"] return flask.redirect( - flask.url_for("fulfillment", - receiver=donation_receiver, - order_id=order_id)) + flask.url_for( + "fulfillment", receiver=donation_receiver, order_id=order_id + ) + ) ## @@ -250,12 +264,14 @@ def fulfillment(receiver): if pay_status.get("paid"): extra = pay_status["contract_terms"]["extra"] - return flask.render_template("templates/fulfillment.html", - donation_receiver=extra["receiver"], - donation_amount=extra["amount"], - donation_donor=extra["donor"], - order_id=order_id, - currency=CURRENCY) + return flask.render_template( + "templates/fulfillment.html", + donation_receiver=extra["receiver"], + donation_amount=extra["amount"], + donation_donor=extra["donor"], + order_id=order_id, + currency=CURRENCY + ) # no pay_redirect but article not paid, this should never happen! err_abort(500, message="Internal error, invariant failed", json=pay_status) diff --git a/talerdonations/talerconfig.py b/talerdonations/talerconfig.py index 7959ec2..1a33294 100644 --- a/talerdonations/talerconfig.py +++ b/talerdonations/talerconfig.py @@ -171,11 +171,13 @@ class Entry: LOGGER.warning( "Configuration is missing option '%s' in section '%s',\ falling back to '%s'", self.option, - self.section, default) + self.section, default + ) else: LOGGER.warning( "Configuration ** is missing option '%s' in section '%s'", - self.option.upper(), self.section.upper()) + self.option.upper(), self.section.upper() + ) return default return self.value @@ -373,7 +375,8 @@ class TalerConfig: # a error occurs). def value_string(self, section, option, **kwargs) -> str: return self.sections[section][option].value_string( - kwargs.get("default"), kwargs.get("required"), kwargs.get("warn")) + kwargs.get("default"), kwargs.get("required"), kwargs.get("warn") + ) ## # Get a value from the config that should be a filename. @@ -388,7 +391,8 @@ class TalerConfig: # a error occurs). def value_filename(self, section, option, **kwargs) -> str: return self.sections[section][option].value_filename( - kwargs.get("default"), kwargs.get("required"), kwargs.get("warn")) + kwargs.get("default"), kwargs.get("required"), kwargs.get("warn") + ) ## # Get a integer value from the config. @@ -402,7 +406,8 @@ class TalerConfig: # a error occurs). def value_int(self, section, option, **kwargs) -> int: return self.sections[section][option].value_int( - kwargs.get("default"), kwargs.get("required"), kwargs.get("warn")) + kwargs.get("default"), kwargs.get("required"), kwargs.get("warn") + ) ## # Load default values from canonical locations. @@ -477,48 +482,58 @@ class TalerConfig: pair = line.split() if 2 != len(pair): LOGGER.error( - "invalid inlined config filename given ('%s')" - % line) + "invalid inlined config filename given ('%s')" % + line + ) continue if pair[1].startswith("/"): self.load_file(pair[1]) else: self.load_file( - os.path.join(os.path.dirname(filename), - pair[1])) + os.path.join( + os.path.dirname(filename), pair[1] + ) + ) continue if line.startswith("["): if not line.endswith("]"): LOGGER.error( - "invalid section header in line %s: %s", - lineno, repr(line)) + "invalid section header in line %s: %s", lineno, + repr(line) + ) section_name = line.strip("[]").strip().strip('"') current_section = section_name continue if current_section is None: LOGGER.error( "option outside of section in line %s: %s", lineno, - repr(line)) + repr(line) + ) continue pair = line.split("=", 1) if len(pair) != 2: - LOGGER.error("invalid option in line %s: %s", lineno, - repr(line)) + LOGGER.error( + "invalid option in line %s: %s", lineno, repr(line) + ) key = pair[0].strip() value = pair[1].strip() if value.startswith('"'): value = value[1:] if not value.endswith('"'): - LOGGER.error("mismatched quotes in line %s: %s", - lineno, repr(line)) + LOGGER.error( + "mismatched quotes in line %s: %s", lineno, + repr(line) + ) else: value = value[:-1] - entry = Entry(self.sections, - current_section, - key, - value=value, - filename=filename, - lineno=lineno) + entry = Entry( + self.sections, + current_section, + key, + value=value, + filename=filename, + lineno=lineno + ) sections[current_section][key] = entry except FileNotFoundError: # not logging here, as this interests the final user mostly. @@ -561,26 +576,22 @@ if __name__ == "__main__": import argparse PARSER = argparse.ArgumentParser() - PARSER.add_argument("--section", - "-s", - dest="section", - default=None, - metavar="SECTION") - PARSER.add_argument("--option", - "-o", - dest="option", - default=None, - metavar="OPTION") - PARSER.add_argument("--config", - "-c", - dest="config", - default=None, - metavar="FILE") - PARSER.add_argument("--filename", - "-f", - dest="expand_filename", - default=False, - action='store_true') + PARSER.add_argument( + "--section", "-s", dest="section", default=None, metavar="SECTION" + ) + PARSER.add_argument( + "--option", "-o", dest="option", default=None, metavar="OPTION" + ) + PARSER.add_argument( + "--config", "-c", dest="config", default=None, metavar="FILE" + ) + PARSER.add_argument( + "--filename", + "-f", + dest="expand_filename", + default=False, + action='store_true' + ) ARGS = PARSER.parse_args() TC = TalerConfig.from_file(ARGS.config) -- cgit v1.2.3 From e7c20a11fac21ab485b17feec81d0887be4cf5eb Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Thu, 29 Aug 2019 19:09:02 +0200 Subject: url-based payments --- talerdonations/donations/donations.py | 26 +++++++- .../donations/templates/request_payment.html | 69 ++++++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 talerdonations/donations/templates/request_payment.html (limited to 'talerdonations') diff --git a/talerdonations/donations/donations.py b/talerdonations/donations/donations.py index 220fbb7..06c6617 100644 --- a/talerdonations/donations/donations.py +++ b/talerdonations/donations/donations.py @@ -245,6 +245,16 @@ def donate(): ) +## +# This endpoint is used by the payment request page +# to check if the payment has been completed via the QR code. +@app.route("/check-status/") +def check_status(order_id, session_id): + pay_params = dict( instance=INSTANCE, order_id=order_id) + pay_status = backend_get("check-payment", pay_params) + return flask.jsonify(paid=pay_status["paid"]) + + ## # Serve the fulfillment page. # @@ -260,7 +270,21 @@ def fulfillment(receiver): pay_status = backend_get("check-payment", pay_params) if pay_status.get("payment_redirect_url"): - return flask.redirect(pay_status["payment_redirect_url"]) + taler_pay_uri = pay_status["taler_pay_uri"] + qrcode_svg = get_qrcode_svg(taler_pay_uri) + check_status_url_enc = urllib.parse.quote( + flask.url_for("check_status", order_id=order_id) + ) + content = flask.render_template( + "templates/request_payment.html", + article_name=article_name, + taler_pay_uri=taler_pay_uri, + qrcode_svg=qrcode_svg, + check_status_url_enc=check_status_url_enc + ) + headers = {"Taler": taler_pay_uri} + resp = flask.Response(content, status=402, headers=headers) + return resp if pay_status.get("paid"): extra = pay_status["contract_terms"]["extra"] diff --git a/talerdonations/donations/templates/request_payment.html b/talerdonations/donations/templates/request_payment.html new file mode 100644 index 0000000..6e050d0 --- /dev/null +++ b/talerdonations/donations/templates/request_payment.html @@ -0,0 +1,69 @@ +{% extends "templates/base.html" %} + + +{% block meta %} + +{% endblock meta %} + + +{% block scripts %} + +{% endblock scripts %} + + +{% block main %} + +

Payment Required

+ +
+

+ Looks like your browser doesn't support GNU Taler payments. You can try + installing a wallet browser extension. +

+
+ +
+ +

+ You can use this QR code to pay with your mobile wallet: +

+ + {{ qrcode_svg | safe }} + +

+ Click this link to open your system's Taler wallet if it exists. +

+ +
+ +{% endblock main %} -- cgit v1.2.3 From 31a05eaab426a6121a93945f1f2e08a686987560 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Thu, 29 Aug 2019 19:10:43 +0200 Subject: make pretty --- talerdonations/donations/donations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'talerdonations') diff --git a/talerdonations/donations/donations.py b/talerdonations/donations/donations.py index 06c6617..886705f 100644 --- a/talerdonations/donations/donations.py +++ b/talerdonations/donations/donations.py @@ -250,7 +250,7 @@ def donate(): # to check if the payment has been completed via the QR code. @app.route("/check-status/") def check_status(order_id, session_id): - pay_params = dict( instance=INSTANCE, order_id=order_id) + pay_params = dict(instance=INSTANCE, order_id=order_id) pay_status = backend_get("check-payment", pay_params) return flask.jsonify(paid=pay_status["paid"]) -- cgit v1.2.3 From c75c8b0dc295a7278b5c6ba26cea9bbdb771789c Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Thu, 29 Aug 2019 19:11:55 +0200 Subject: no more wallet detection --- talerdonations/donations/templates/index.html | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) (limited to 'talerdonations') diff --git a/talerdonations/donations/templates/index.html b/talerdonations/donations/templates/index.html index fc956e5..68a0cb2 100644 --- a/talerdonations/donations/templates/index.html +++ b/talerdonations/donations/templates/index.html @@ -7,22 +7,7 @@ You are paying with an imaginary currency ({{ merchant_currency }}).

-
-

Installing the Taler wallet

- First, you need to install the Taler wallet browser extension. - Install the wallet - - - Wallets for other browsers will be provided in the near future. -
- -
+

Please select a project, the amount (*) of {{ merchant_currency }} you wish to donate, and enter the name that will appear on your receipt:

-- cgit v1.2.3 From 06bf80ca7326e9e1c72b27cb4734f9eefc20f68e Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Thu, 29 Aug 2019 19:13:16 +0200 Subject: no more redirect URL --- talerdonations/donations/donations.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) (limited to 'talerdonations') diff --git a/talerdonations/donations/donations.py b/talerdonations/donations/donations.py index 886705f..d4dd36d 100644 --- a/talerdonations/donations/donations.py +++ b/talerdonations/donations/donations.py @@ -269,7 +269,18 @@ def fulfillment(receiver): pay_params = dict(instance=receiver, order_id=order_id) pay_status = backend_get("check-payment", pay_params) - if pay_status.get("payment_redirect_url"): + + if pay_status.get("paid"): + extra = pay_status["contract_terms"]["extra"] + return flask.render_template( + "templates/fulfillment.html", + donation_receiver=extra["receiver"], + donation_amount=extra["amount"], + donation_donor=extra["donor"], + order_id=order_id, + currency=CURRENCY + ) + else: taler_pay_uri = pay_status["taler_pay_uri"] qrcode_svg = get_qrcode_svg(taler_pay_uri) check_status_url_enc = urllib.parse.quote( @@ -286,16 +297,5 @@ def fulfillment(receiver): resp = flask.Response(content, status=402, headers=headers) return resp - if pay_status.get("paid"): - extra = pay_status["contract_terms"]["extra"] - return flask.render_template( - "templates/fulfillment.html", - donation_receiver=extra["receiver"], - donation_amount=extra["amount"], - donation_donor=extra["donor"], - order_id=order_id, - currency=CURRENCY - ) - # no pay_redirect but article not paid, this should never happen! err_abort(500, message="Internal error, invariant failed", json=pay_status) -- cgit v1.2.3 From 8ef2843d05d57c07115dba494cc913efc3f19014 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Thu, 29 Aug 2019 19:15:09 +0200 Subject: qrcode --- setup.py | 2 +- talerdonations/donations/donations.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) (limited to 'talerdonations') diff --git a/setup.py b/setup.py index 906567c..042635f 100755 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( author_email=['stanisci.m@gmail.com', 'dold@taler.net'], license='GPL', packages=find_packages(), - install_requires=["Flask>=0.10", "requests", "uwsgi", "jsmin"], + install_requires=["Flask>=0.10", "requests", "uwsgi", "jsmin", "qrcode", "lxml"], tests_require=["mock", "nose"], test_suite="nose.collector", package_data={ diff --git a/talerdonations/donations/donations.py b/talerdonations/donations/donations.py index d4dd36d..c00844b 100644 --- a/talerdonations/donations/donations.py +++ b/talerdonations/donations/donations.py @@ -26,6 +26,9 @@ import random import requests import flask import traceback +import qrcode +import qrcode.image.svg +import lxml.etree from ..talerconfig import TalerConfig LOGGER = logging.getLogger(__name__) @@ -255,6 +258,12 @@ def check_status(order_id, session_id): return flask.jsonify(paid=pay_status["paid"]) +def get_qrcode_svg(data): + factory = qrcode.image.svg.SvgImage + img = qrcode.make(data, image_factory=factory) + return lxml.etree.tostring(img.get_image()).decode("utf-8") + + ## # Serve the fulfillment page. # -- cgit v1.2.3 From cdd70817d77265a897e4efb1fed22e66e2c99ce9 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Thu, 29 Aug 2019 19:15:58 +0200 Subject: urllib --- talerdonations/donations/donations.py | 1 + 1 file changed, 1 insertion(+) (limited to 'talerdonations') diff --git a/talerdonations/donations/donations.py b/talerdonations/donations/donations.py index c00844b..21eb76a 100644 --- a/talerdonations/donations/donations.py +++ b/talerdonations/donations/donations.py @@ -26,6 +26,7 @@ import random import requests import flask import traceback +import urllib import qrcode import qrcode.image.svg import lxml.etree -- cgit v1.2.3 From b38365fa582a52592d00f36e8dee2ee9bfca9ee6 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Thu, 29 Aug 2019 19:16:45 +0200 Subject: I want static typing --- talerdonations/donations/donations.py | 1 - 1 file changed, 1 deletion(-) (limited to 'talerdonations') diff --git a/talerdonations/donations/donations.py b/talerdonations/donations/donations.py index 21eb76a..d85c9a9 100644 --- a/talerdonations/donations/donations.py +++ b/talerdonations/donations/donations.py @@ -298,7 +298,6 @@ def fulfillment(receiver): ) content = flask.render_template( "templates/request_payment.html", - article_name=article_name, taler_pay_uri=taler_pay_uri, qrcode_svg=qrcode_svg, check_status_url_enc=check_status_url_enc -- cgit v1.2.3