taler-merchant-demos

Python-based Frontends for the Demonstration Web site
Log | Files | Refs | Submodules | README | LICENSE

commit 769ea2778ee20d84dd5c3acc98434f4939d6d461
parent 3403b8ce5342bd8f26fe2edd7cebe64cf532c2a1
Author: Florian Dold <florian@dold.me>
Date:   Tue,  3 Sep 2024 16:09:33 +0200

don't use deprecated Python APIs, read config from file

Diffstat:
Mtalermerchantdemos/blog/blog.py | 300++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mtalermerchantdemos/blog/content.py | 107++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Mtalermerchantdemos/cli.py | 21+++++----------------
Mtalermerchantdemos/donations/donations.py | 65+++++++++++++++++++++++++++++++++++++++--------------------------
Mtalermerchantdemos/httpcommon/__init__.py | 33+++++++++++++++++++--------------
Mtalermerchantdemos/landing/landing.py | 28++++++++++++++++++----------
Mtalermerchantdemos/util/talerconfig.py | 8++++++--
7 files changed, 301 insertions(+), 261 deletions(-)

diff --git a/talermerchantdemos/blog/blog.py b/talermerchantdemos/blog/blog.py @@ -35,7 +35,7 @@ import time import sys from urllib.parse import urljoin, urlencode, urlparse from ..util.talerconfig import TalerConfig, ConfigurationError -from ..blog.content import ARTICLES, get_article_file, get_image_file +from ..blog.content import ARTICLES, get_article_contents from talermerchantdemos.httpcommon import ( backend_get, backend_get_with_status, @@ -48,14 +48,64 @@ from talermerchantdemos.httpcommon import ( get_locale, ) +if not sys.version_info.major == 3 and sys.version_info.minor >= 6: + print("Python 3.6 or higher is required.") + print( + "You are using Python {}.{}.".format( + sys.version_info.major, sys.version_info.minor + ) + ) + sys.exit(1) + +app = flask.Flask(__name__, template_folder="../templates", static_folder="../static") +app.wsgi_app = ProxyFix(app.wsgi_app, x_host=1, x_prefix=1) +app.debug = True +app.secret_key = base64.b64encode(os.urandom(64)).decode("utf-8") + +logging.basicConfig() +LOGGER = logging.getLogger(__name__) + +config_filename = uwsgi.opt["config_filename"].decode("utf-8") +if config_filename == "": + config_filename = None + +config = TalerConfig.from_file(config_filename) + +CURRENCY = config["taler"]["currency"].value_string(required=True) + +ARTICLE_AMOUNT = CURRENCY + ":0.5" +BACKEND_URL = config["frontend-demo-blog"]["backend_url"].value_string(required=True) +APIKEY = config["frontend-demo-blog"]["backend_apikey"].value_string(required=True) + +BABEL_TRANSLATION_DIRECTORIES = "../translations" + +app.config.from_object(__name__) +babel = Babel(app) +babel.localeselector(get_locale) + +LOGGER.info("Using translations from:" + ":".join(list(babel.translation_directories))) +translations = [str(translation) for translation in babel.list_translations()] +if not "en" in translations: + translations.append("en") +LOGGER.info( + "Operating with the following translations available: " + " ".join(translations) +) + +# Add context processor that will make additional variables +# and functions available in the template. +app.context_processor( + make_utility_processor("blog", os.environ.get("TALER_ENV_URL_MERCHANT_BLOG")) +) + + def req_add_cookie_check(): current_url = list(urllib.parse.urlparse(flask.request.base_url)) args_writable = flask.request.args.copy() # Adding the used param. args_writable.update(dict(expect_state="yes")) - current_url[4] = urllib.parse.urlencode(args_writable) + current_url[4] = urllib.parse.urlencode(args_writable) # Stringify the result. - return urllib.parse.urlunparse(current_url) + return urllib.parse.urlunparse(current_url) def req_rm_cookie_check(): @@ -63,9 +113,9 @@ def req_rm_cookie_check(): args_writable = flask.request.args.copy() # Stripping the used param. args_writable.pop("expect_state") - current_url[4] = urllib.parse.urlencode(args_writable) + current_url[4] = urllib.parse.urlencode(args_writable) # Stringify the result. - return urllib.parse.urlunparse(current_url) + return urllib.parse.urlunparse(current_url) def err_abort(abort_status_code, **params): @@ -91,52 +141,6 @@ def refundable(pay_status): return True return False -if not sys.version_info.major == 3 and sys.version_info.minor >= 6: - print("Python 3.6 or higher is required.") - print( - "You are using Python {}.{}.".format( - sys.version_info.major, sys.version_info.minor - ) - ) - sys.exit(1) - -app = flask.Flask(__name__, template_folder="../templates", static_folder="../static") -app.wsgi_app = ProxyFix(app.wsgi_app, x_host=1, x_prefix=1) -app.debug = True -app.secret_key = base64.b64encode(os.urandom(64)).decode("utf-8") - -logging.basicConfig() -LOGGER = logging.getLogger(__name__) -try: - - BACKEND_BASE_URL = uwsgi.opt["backend_url"].decode("utf-8") - CURRENCY = uwsgi.opt["currency"].decode("utf-8") - APIKEY = uwsgi.opt["apikey"].decode("utf-8") -except ConfigurationError as ce: - print(ce) - exit(1) - -ARTICLE_AMOUNT = CURRENCY + ":0.5" -BACKEND_URL = urljoin(BACKEND_BASE_URL, "instances/blog/") - -BABEL_TRANSLATION_DIRECTORIES = "../translations" - -app.config.from_object(__name__) -babel = Babel(app) -babel.localeselector(get_locale) - -LOGGER.info("Using translations from:" + ":".join(list(babel.translation_directories))) -translations = [str(translation) for translation in babel.list_translations()] -if not "en" in translations: - translations.append("en") -LOGGER.info( - "Operating with the following translations available: " + " ".join(translations) -) - -# Add context processor that will make additional variables -# and functions available in the template. -app.context_processor(make_utility_processor("blog", os.environ.get ("TALER_ENV_URL_MERCHANT_BLOG"))) - ## # "Fallback" exception handler to capture all the unmanaged errors. @@ -162,7 +166,7 @@ def internal_error(e): def index(): default = "en" target = flask.request.accept_languages.best_match(translations, default) - return flask.redirect(url_for ('index') + target + "/", code=302) + return flask.redirect(url_for("index") + target + "/", code=302) ## @@ -301,14 +305,14 @@ def render_article(article_name, lang, data, order_id, refundable): ) err_abort(500, message=m) if data is not None: - if data in article_info.extra_files: - return flask.send_file(get_image_file(data)) - m = gettext("Supplemental file ({}) for article ({}) not found.").format( - data, article_name - ) + #if data in article_info.extra_files: + # return flask.send_file(get_image_file(data)) + #m = gettext("Supplemental file ({}) for article ({}) not found.").format( + # data, article_name + #) err_abort(404, message=m) # the order_id is needed for refunds - article_contents = open(get_article_file(article_info)).read() + article_contents = get_article_contents(article_info) return flask.render_template( "blog-article-frame.html.j2", page_title=gettext("GNU Taler Demo: Article"), @@ -387,25 +391,25 @@ def article(article_name, lang=None, data=None): article_url = flask.request.base_url if not session_id: - # If expect_state = yes then session_id should be set already - # this is a way to check that the client supports cookies - if maybe_expect_state == "yes": - error_page = flask.render_template( - "blog-error.html.j2", - page_title=gettext("GNU Taler Demo: Error"), - message=gettext("Please enable cookies."), - ) - return flask.make_response(error_page, 412) - - # first time setting session_id - # check if browser support cookies with a flag - session_id = flask.session["session_id"] = str(uuid.uuid4()) - return flask.redirect(req_add_cookie_check(), code=302) - + # If expect_state = yes then session_id should be set already + # this is a way to check that the client supports cookies + if maybe_expect_state == "yes": + error_page = flask.render_template( + "blog-error.html.j2", + page_title=gettext("GNU Taler Demo: Error"), + message=gettext("Please enable cookies."), + ) + return flask.make_response(error_page, 412) + + # first time setting session_id + # check if browser support cookies with a flag + session_id = flask.session["session_id"] = str(uuid.uuid4()) + return flask.redirect(req_add_cookie_check(), code=302) + # If session is present then we know that cookies are enabled # remove the flag if present if maybe_expect_state == "yes": - return flask.redirect(req_rm_cookie_check(), code=302) + return flask.redirect(req_rm_cookie_check(), code=302) ############################ # user has a session and cookie works @@ -415,66 +419,69 @@ def article(article_name, lang=None, data=None): # if an order_id is present then render if paid or refunded if current_order_id is not None: - status, current_order = backend_get_with_status( - BACKEND_URL, - f"private/orders/{current_order_id}", - params=dict(session_id=session_id), - auth_token=APIKEY, - ) - - if status == 200: - if current_order.get("order_status") == "paid" and not current_order.get("refunded"): - return render_article( - article_name, - lang, - data, - current_order_id, - refundable(current_order) - ) - - # Checking repurchase case. That happens when the client - # visits this page in the same session where the article - # was paid already. - ai = current_order.get("already_paid_order_id") - au = current_order.get("already_paid_fulfillment_url") - - if ai is not None: - print("== Merchant says 'see other': ", ai, au) - response = flask.redirect(article_url) - response.set_cookie( - "order_id", - ai, - path=urllib.parse.quote(url_for ('index') + f"essay/{article_name}") - ) - response.set_cookie( - "order_id", - ai, - path=urllib.parse.quote(url_for ('index') + f"{lang}/essay/{article_name}") - ) - return response - - # If new_if_refunded == "yes" the user already acknowledge the - # state of the current order and is asking for a new one. - if current_order.get("refunded") and new_if_refunded != "yes": - return flask.render_template( - "blog-article-refunded.html.j2", - page_title=gettext("GNU Taler Demo: Refunded"), - article_name=article_name, - article_lang=lang, - order_id=current_order_id, - ) - - elif status != 404: - # not found may be normal, could means that - # merchant forgot about the order becuase - # it was a long time without being paid - raise BackendException( - message=gettext("Backend returned error status"), - backend_status=status, - backend_json=current_order, + status, current_order = backend_get_with_status( + BACKEND_URL, + f"private/orders/{current_order_id}", + params=dict(session_id=session_id), + auth_token=APIKEY, ) - + if status == 200: + if current_order.get("order_status") == "paid" and not current_order.get( + "refunded" + ): + return render_article( + article_name, + lang, + data, + current_order_id, + refundable(current_order), + ) + + # Checking repurchase case. That happens when the client + # visits this page in the same session where the article + # was paid already. + ai = current_order.get("already_paid_order_id") + au = current_order.get("already_paid_fulfillment_url") + + if ai is not None: + print("== Merchant says 'see other': ", ai, au) + response = flask.redirect(article_url) + response.set_cookie( + "order_id", + ai, + path=urllib.parse.quote(url_for("index") + f"essay/{article_name}"), + ) + response.set_cookie( + "order_id", + ai, + path=urllib.parse.quote( + url_for("index") + f"{lang}/essay/{article_name}" + ), + ) + return response + + # If new_if_refunded == "yes" the user already acknowledge the + # state of the current order and is asking for a new one. + if current_order.get("refunded") and new_if_refunded != "yes": + return flask.render_template( + "blog-article-refunded.html.j2", + page_title=gettext("GNU Taler Demo: Refunded"), + article_name=article_name, + article_lang=lang, + order_id=current_order_id, + ) + + elif status != 404: + # not found may be normal, could means that + # merchant forgot about the order becuase + # it was a long time without being paid + raise BackendException( + message=gettext("Backend returned error status"), + backend_status=status, + backend_json=current_order, + ) + # Current order is not present or unpaid # Check if there is a paid but not refunded order in this session list_resp = backend_get( @@ -483,12 +490,12 @@ def article(article_name, lang=None, data=None): params=dict(session_id=session_id, fulfillment_url=article_url, refunded="no"), auth_token=APIKEY, ) - + already_paid_order = None for order in list_resp.get("orders"): - if order.get("paid"): - already_paid_order = order - break + if order.get("paid"): + already_paid_order = order + break if already_paid_order is not None: # Found one, now this is the current order. @@ -498,49 +505,46 @@ def article(article_name, lang=None, data=None): response.set_cookie( "order_id", order_id, - path=urllib.parse.quote(url_for ('index') + f"essay/{article_name}") + path=urllib.parse.quote(url_for("index") + f"essay/{article_name}"), ) response.set_cookie( "order_id", order_id, - path=urllib.parse.quote(url_for ('index') + f"{lang}/essay/{article_name}") + path=urllib.parse.quote(url_for("index") + f"{lang}/essay/{article_name}"), ) return response - + ############################ # We couln't find a paid order # # Note that it could be the case that the user is still paying # an order with another device, in other browser on the same - # session or claimed in the same brower. - # Still, creating an order is cheap and we can safely redirect + # session or claimed in the same brower. + # Still, creating an order is cheap and we can safely redirect # to a payment page and relay on repurchase detection to avoid # double payments. # # create a new order and ask for payment ############################ - + order_resp = post_order(article_name, article_url, session_id, lang) order_id = order_resp["order_id"] token = order_resp["token"] redirect_url = backend_payment_url( - BACKEND_URL, - f"orders/{order_id}", - session_id, - token + BACKEND_URL, f"orders/{order_id}", session_id, token ) print("new order URL", redirect_url) response = flask.redirect(redirect_url) response.set_cookie( "order_id", order_id, - path=urllib.parse.quote(url_for ('index') + f"essay/{article_name}") + path=urllib.parse.quote(url_for("index") + f"essay/{article_name}"), ) response.set_cookie( "order_id", order_id, - path=urllib.parse.quote(url_for ('index') + f"{lang}/essay/{article_name}") + path=urllib.parse.quote(url_for("index") + f"{lang}/essay/{article_name}"), ) return response diff --git a/talermerchantdemos/blog/content.py b/talermerchantdemos/blog/content.py @@ -21,11 +21,12 @@ import logging import os import re from bs4 import BeautifulSoup -from pkg_resources import resource_stream, resource_filename from os import listdir from os.path import isfile, join from urllib.parse import quote +import importlib.resources + LOGGER = logging.getLogger(__name__) NOISY_LOGGER = logging.getLogger("chardet.charsetprober") @@ -39,6 +40,8 @@ Article = namedtuple("Article", "slug title teaser main_file extra_files lang") # articles available in that language. ARTICLES = {} +articles_per_lang = {} + ## # Add article to the list of the available articles. @@ -54,28 +57,17 @@ def add_article(slug, title, teaser, main_file, extra_files, lang="en"): if not (lang in ARTICLES): ARTICLES[lang] = OrderedDict() ARTICLES[lang][slug] = Article(slug, title, teaser, main_file, extra_files, lang) + articles_per_lang.setdefault(lang, 0) + articles_per_lang[lang] += 1 ## -# Build the file path of a image. -# -# @param image the image filename. -# @return the path to the image file. -def get_image_file(image): - filex = resource_filename("talermerchantdemos", os.path.join("blog/data/", image)) - return os.path.abspath(filex) - - -## -# Build the file path of a article. +# Return contents of an article. # # @param article the article filename. -# @return the path to the article HTML file. -def get_article_file(article): - filex = resource_filename( - "talermerchantdemos", article.main_file, - ) - return os.path.abspath(filex) +# @return text contents of the article +def get_article_contents(article): + return article.main_file.read_text() ## @@ -90,16 +82,14 @@ def get_article_file(article): # HTML itself, so give it here if a explicit title needs to be # specified. def add_from_html(resource_name, lang): - res = resource_stream("talermerchantdemos", resource_name) - soup = BeautifulSoup(res, "html.parser") - res.close() + soup = BeautifulSoup(resource_name.read_bytes(), "html.parser") title_el = soup.find("h2") if title_el is None: LOGGER.warning("Cannot extract title from '%s'", resource_name) return title = title_el.get_text().strip() slug = title.replace(" ", "_") - slug = re.sub(r'[^a-zA-Z0-9_]+', "-", slug) + slug = re.sub(r"[^a-zA-Z0-9_]+", "-", slug) teaser = soup.find("p", attrs={"id": ["teaser"]}) if teaser is None: @@ -107,7 +97,9 @@ def add_from_html(resource_name, lang): if len(paragraphs) > 0: teaser = paragraphs[0].prettify() if len(teaser) < 100: - LOGGER.warning("Cannot extract adequate teaser from '%s'", resource_name) + LOGGER.warning( + "Cannot extract adequate teaser from '%s'", resource_name + ) return else: LOGGER.warning("Cannot extract teaser from '%s'", resource_name) @@ -117,28 +109,53 @@ def add_from_html(resource_name, lang): re_proc = re.compile("^/[^/][^/]/essay/[^/]+/data/[^/]+$") imgs = soup.find_all("img") extra_files = [] - for img in imgs: - # We require that any image whose access is regulated is src'd - # as "<slug>/data/img.png". We also need to check if the <slug> - # component actually matches the article's slug - if re_proc.match(img["src"]): - if img["src"].split(os.sep)[2] == slug: - LOGGER.info( - "extra file for %s is %s" % (slug, os.path.basename(img["src"])) - ) - extra_files.append(os.path.basename(img["src"])) - else: - LOGGER.warning( - "Image src and slug don't match: '%s' != '%s'" - % (img["src"].split(os.sep)[2], slug) - ) + #for img in imgs: + # # We require that any image whose access is regulated is src'd + # # as "<slug>/data/img.png". We also need to check if the <slug> + # # component actually matches the article's slug + # if re_proc.match(img["src"]): + # if img["src"].split(os.sep)[2] == slug: + # LOGGER.info( + # "extra file for %s is %s" % (slug, os.path.basename(img["src"])) + # ) + # extra_files.append(os.path.basename(img["src"])) + # else: + # LOGGER.warning( + # "Image src and slug don't match: '%s' != '%s'" + # % (img["src"].split(os.sep)[2], slug) + # ) add_article(slug, title, teaser, resource_name, extra_files, lang) -for l in listdir(resource_filename("talermerchantdemos", "blog/articles/")): - # Filter by active languages, otherwise this takes quite a while to load... - if l in {"en", "ar", "zh", "fr", "hi", "it", "ja", "ko", "pt", "pt_BR", "ru", "tr", "de", "sv", "es"}: - LOGGER.info("importing %s" % l) - for a in listdir(resource_filename("talermerchantdemos", "blog/articles/" + l)): - if os.path.isfile(resource_filename("talermerchantdemos", "blog/articles/" + l + "/" + a)): - add_from_html("blog/articles/" + l + "/" + a, l) +pkgfiles = importlib.resources.files("talermerchantdemos") + +supported_langs = { + "en", + "ar", + "zh", + "fr", + "hi", + "it", + "ja", + "ko", + "pt", + "pt_BR", + "ru", + "tr", + "de", + "sv", + "es", +} + +for l in pkgfiles.joinpath("blog/articles/").iterdir(): + lang = l.name + if lang not in supported_langs: + continue + LOGGER.info("importing %s" % l) + for a in l.iterdir(): + if not a.is_file(): + continue + # Max 50 articles per language + if articles_per_lang.get(lang, 0) > 50: + break + add_from_html(a, lang) diff --git a/talermerchantdemos/cli.py b/talermerchantdemos/cli.py @@ -44,9 +44,11 @@ arg_load_python = "--if-not-plugin python --plugins python --endif".split(" ") # The effect it to launch the blog HTTP service. # # @param args command line options. -def handle_serve_http(config, which_shop, port=None): +def handle_serve_http(config_filename, which_shop, port=None): + config = TalerConfig.from_file(config_filename) confsection = f"frontend-demo-{which_shop}" currency = config["taler"]["currency"].value_string(required=True) + cfg_arg = "" if config_filename is None else config_filename params = [ "uwsgi", "uwsgi", @@ -57,23 +59,11 @@ def handle_serve_http(config, which_shop, port=None): "--log-format", UWSGI_LOGFMT, "--set-ph", - f"currency={currency}", + f"config_filename={cfg_arg}", "--module", f"talermerchantdemos.{which_shop}:app", ] - # Only read backend config if necessary - has_backend = which_shop in ("blog", "donations") - if has_backend: - backend_url = config[confsection]["backend"].value_string(required=True) - apikey = config[confsection]["backend_apikey"].value_string(required=True) - extra = [ - "--set-ph", - f"backend_url={backend_url}" "--set-ph", - f"apikey={apikey}", - ] - params.extend(extra) - # Takes precedence. if port: http_serve = "tcp" @@ -126,8 +116,7 @@ def demos(config, http_port, which_shop): if which_shop not in ["blog", "donations", "landing"]: print("Please use a valid shop name: blog, donations, landing.") sys.exit(1) - config_obj = TalerConfig.from_file(config) - handle_serve_http(config_obj, which_shop, http_port) + handle_serve_http(config, which_shop, http_port) demos() diff --git a/talermerchantdemos/donations/donations.py b/talermerchantdemos/donations/donations.py @@ -54,24 +54,32 @@ app = flask.Flask(__name__, template_folder="../templates", static_folder="../st app.wsgi_app = ProxyFix(app.wsgi_app, x_host=1, x_prefix=1) app.debug = True app.secret_key = base64.b64encode(os.urandom(64)).decode("utf-8") - -try: - BACKEND_BASE_URL = uwsgi.opt["backend_url"].decode("utf-8") - CURRENCY = uwsgi.opt["currency"].decode("utf-8") - APIKEY = uwsgi.opt["apikey"].decode("utf-8") -except ConfigurationError as ce: - print(ce) - exit(1) +app.config.from_object(__name__) BABEL_TRANSLATION_DIRECTORIES = "../translations" - -app.config.from_object(__name__) babel = Babel(app) babel.localeselector(get_locale) +config_filename = uwsgi.opt["config_filename"].decode("utf-8") +if config_filename == "": + config_filename = None +config = TalerConfig.from_file(config_filename) + +CURRENCY = config["taler"]["currency"].value_string(required=True) + +backend_urls = {} +backend_apikeys = {} + +def add_backend(name): + backend_urls[name] = config["frontend-demo-donations"][f"backend_url_{name}"].value_string(required=True) + backend_apikeys[name] = config["frontend-demo-donations"][f"backend_apikey_{name}"].value_string(required=True) + +add_backend("tor") +add_backend("taler") +add_backend("gnunet") + LOGGER.info("Using translations from:" + ":".join(list(babel.translation_directories))) -LOGGER.info("backend: " + BACKEND_BASE_URL) LOGGER.info("currency: " + CURRENCY) translations = [str(translation) for translation in babel.list_translations()] if not "en" in translations: @@ -82,7 +90,12 @@ LOGGER.info( # Add context processor that will make additional variables # and functions available in the template. -app.context_processor(make_utility_processor("donations", os.environ.get ("TALER_ENV_URL_MERCHANT_DONATIONS"))) +app.context_processor( + make_utility_processor( + "donations", os.environ.get("TALER_ENV_URL_MERCHANT_DONATIONS") + ) +) + ## # Return a error response to the client. @@ -102,8 +115,7 @@ def err_abort(abort_status_code, **params): # @return the JSON response from the backend, or a error response # if something unexpected happens. def backend_instanced_get(instance, endpoint, params): - backend_url = urljoin(BACKEND_BASE_URL, f"instances/{instance}/") - return backend_get(backend_url, endpoint, params, auth_token=APIKEY) + return backend_get(backend_urls[instance], endpoint, params, auth_token=backend_apikeys[instance]) ## @@ -115,8 +127,7 @@ def backend_instanced_get(instance, endpoint, params): # @param json the POST's body. # @return the backend response (JSON format). def backend_instanced_post(instance, endpoint, json): - backend_url = urljoin(BACKEND_BASE_URL, f"instances/{instance}/") - return backend_post(backend_url, endpoint, json, auth_token=APIKEY) + return backend_post(backend_urls[instance], endpoint, json, auth_token=backend_apikeys[instance]) ## @@ -142,7 +153,9 @@ def internal_error(e): return flask.render_template( "donations-error.html.j2", page_title=gettext("GNU Taler Demo: Error"), - message=str(e)) + message=str(e), + ) + ## # Serve the /favicon.ico requests. @@ -166,7 +179,7 @@ def favicon(): def index(): default = "en" target = flask.request.accept_languages.best_match(translations, default) - return flask.redirect(url_for ('index') + target + "/", code=302) + return flask.redirect(url_for("index") + target + "/", code=302) ## @@ -186,9 +199,9 @@ def start(lang): ) return flask.render_template( - "donations-index.html.j2", + "donations-index.html.j2", page_title=gettext("GNU Taler Demo: Donations"), - merchant_currency=CURRENCY + merchant_currency=CURRENCY, ) @@ -221,9 +234,9 @@ def checkout(lang): @app.route("/<lang>/provider-not-supported") def provider_not_supported(lang): return flask.render_template( - "donations-provider-not-supported.html.j2", + "donations-provider-not-supported.html.j2", page_title=gettext("GNU Taler Demo: Donations"), -) + ) ## @@ -257,7 +270,7 @@ def donate(lang): fulfillment_url=fulfillment_url, summary="Donation to {}".format(donation_receiver), wire_transfer_deadline=dict(t_s=int(time.time() + 10)), - minimum_age = 16 + minimum_age=16, ) order_resp = backend_instanced_post( donation_receiver, "private/orders", dict(order=order) @@ -265,8 +278,8 @@ def donate(lang): if not order_resp: return err_abort( - 500, # FIXME: status code got lost in the httpcommon module. - message=gettext("Backend could not create the order") + 500, # FIXME: status code got lost in the httpcommon module. + message=gettext("Backend could not create the order"), ) order_id = order_resp["order_id"] @@ -311,5 +324,5 @@ def handler(e): return flask.render_template( "donations-error.html.j2", page_title=gettext("GNU Taler Demo: Error"), - message=gettext("Page not found") + message=gettext("Page not found"), ) diff --git a/talermerchantdemos/httpcommon/__init__.py b/talermerchantdemos/httpcommon/__init__.py @@ -5,13 +5,14 @@ from flask import request, url_for from datetime import datetime import time from flask_babel import gettext -import babel # used for lang sanity check +import babel # used for lang sanity check import os import re import logging LOGGER = logging.getLogger(__name__) + class BackendException(Exception): """Exception for failed communication with the Taler merchant backend""" @@ -20,6 +21,7 @@ class BackendException(Exception): self.backend_status = backend_status self.backend_json = backend_json + ## # POST a request to the backend, and return a error # response if any error occurs. @@ -53,11 +55,11 @@ def backend_post(backend_url, endpoint, json, auth_token=None): backend_status=resp.status_code, backend_json=response_json, ) - print("Backend responds to {}: {}/{}".format( - final_url, - str(response_json), - resp.status_code - )) + print( + "Backend responds to {}: {}/{}".format( + final_url, str(response_json), resp.status_code + ) + ) return response_json @@ -78,6 +80,7 @@ def backend_get(backend_url, endpoint, params, auth_token=None): ) return json + def backend_get_with_status(backend_url, endpoint, params, auth_token=None): headers = dict() if auth_token is not None: @@ -97,6 +100,7 @@ def backend_get_with_status(backend_url, endpoint, params, auth_token=None): print("Backend responds to {}: {}".format(final_url, str(response_json))) return resp.status_code, response_json + def get_locale(): parts = request.path.split("/", 2) if 2 >= len(parts): @@ -117,21 +121,22 @@ def get_locale(): return "en" return lang + ## # Construct the payment URL where customer can pay the order # # @param backend_url where the backend is located # @param order_id id of the order already created # @param session_id session in which the order is going to be paid -# @param token if the order requires a token +# @param token if the order requires a token # @return the JSON response from the backend, or a error response # if something unexpected happens. def backend_payment_url(backend_url, endpoint, session_id, token): - final_url = urljoin(backend_url, endpoint) - query = urlencode({"token":token, "session_id":session_id}) - redirect_url = urlparse(final_url)._replace(query=query) - return urlunparse(redirect_url) - + final_url = urljoin(backend_url, endpoint) + query = urlencode({"token": token, "session_id": session_id}) + redirect_url = urlparse(final_url)._replace(query=query) + return urlunparse(redirect_url) + ## # Helper function used inside Jinja2 logic to create a links @@ -192,7 +197,7 @@ all_languages = { # Make the environment available into templates. # # @return the environment-reading function -def make_utility_processor(pagename,base_url): +def make_utility_processor(pagename, base_url): def utility_processor(): def getactive(): return pagename @@ -221,7 +226,7 @@ def make_utility_processor(pagename,base_url): getactive=getactive, getlang=getlang, all_languages=all_languages, - static=static + static=static, ) return utility_processor diff --git a/talermerchantdemos/landing/landing.py b/talermerchantdemos/landing/landing.py @@ -57,11 +57,15 @@ app.secret_key = base64.b64encode(os.urandom(64)).decode("utf-8") logging.basicConfig() LOGGER = logging.getLogger(__name__) -try: - CURRENCY = uwsgi.opt["currency"].decode("utf-8") -except ConfigurationError as ce: - print(ce) - exit(1) + +config_filename = uwsgi.opt["config_filename"].decode("utf-8") +if config_filename == "": + config_filename = None + +config = TalerConfig.from_file(config_filename) + +CURRENCY = config["taler"]["currency"].value_string(required=True) + BABEL_TRANSLATION_DIRECTORIES = "../translations" @@ -80,7 +84,10 @@ LOGGER.info( # Add context processor that will make additional variables # and functions available in the template. -app.context_processor(make_utility_processor("landing", os.environ.get ("TALER_ENV_URL_INTRO"))) +app.context_processor( + make_utility_processor("landing", os.environ.get("TALER_ENV_URL_INTRO")) +) + ## # Exception handler to capture all the unmanaged errors. @@ -120,7 +127,8 @@ def favicon(): def index(): default = "en" target = flask.request.accept_languages.best_match(translations, default) - return flask.redirect(url_for ('index') + target + "/", code=302) + return flask.redirect(url_for("index") + target + "/", code=302) + ## # Serve the internationalized main index page. @@ -166,13 +174,13 @@ def start(lang): ) -@app.errorhandler(404) # How to trigger this? +@app.errorhandler(404) # How to trigger this? @app.errorhandler(werkzeug.exceptions.NotFound) def handler_404(e): return flask.render_template( - "landing-error.html.j2", + "landing-error.html.j2", page_title=gettext("GNU Taler Demo: Error"), - message=gettext("Page not found") + message=gettext("Page not found"), ) diff --git a/talermerchantdemos/util/talerconfig.py b/talermerchantdemos/util/talerconfig.py @@ -32,6 +32,7 @@ LOGGER = logging.getLogger(__name__) __all__ = ["TalerConfig"] + ## # Exception class for a any configuration error. class ConfigurationError(Exception): @@ -100,6 +101,7 @@ def expand(var: str, getter: Callable[[str], str]) -> str: return result + var[pos:] + ## # A configuration entry. class Entry: @@ -158,8 +160,10 @@ class Entry: # @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: - print("Missing required option '%s' in section '%s'" - % (self.option.upper(), self.section.upper())) + print( + "Missing required option '%s' in section '%s'" + % (self.option.upper(), self.section.upper()) + ) sys.exit(1) if self.value is None: