diff options
Diffstat (limited to 'talermerchantdemos/blog/blog.py')
-rw-r--r-- | talermerchantdemos/blog/blog.py | 190 |
1 files changed, 157 insertions, 33 deletions
diff --git a/talermerchantdemos/blog/blog.py b/talermerchantdemos/blog/blog.py index 865605b..c8315e5 100644 --- a/talermerchantdemos/blog/blog.py +++ b/talermerchantdemos/blog/blog.py @@ -1,6 +1,6 @@ ## -# This file is part of GNU taler. -# Copyright (C) 2014-2017 INRIA +# This file is part of GNU Taler. +# Copyright (C) 2014-2020 Taler Systems SA # # TALER is free software; you can redistribute it and/or modify it under the # terms of the GNU Lesser General Public License as published by the Free Software @@ -24,21 +24,57 @@ import traceback import uuid import base64 import flask +from flask import request +from flask_babel import Babel +from flask_babel import refresh +from flask_babel import force_locale +from flask_babel import gettext import time import sys from urllib.parse import urljoin, urlencode, urlparse from taler.util.talerconfig import TalerConfig, ConfigurationError from ..blog.content import ARTICLES, get_article_file, get_image_file -from talermerchantdemos.httpcommon import backend_get, backend_post +from talermerchantdemos.httpcommon import backend_get, backend_post, fallback_404 +from datetime import datetime + +class Deadline: + def __init__(self, value): + self.value = value + def isExpired(self): + if self.value == "never": + return False + now = int(round(time.time()) * 1000) + now_dt = datetime.fromtimestamp(now / 1000) + deadline_dt = datetime.fromtimestamp(self.value / 1000) + print("debug: checking refund expiration, now: {}, deadline: {}".format( + now_dt.strftime("%c"), deadline_dt.strftime("%c") + )) + return now > self.value + +def refundable(pay_status): + refunded = pay_status.get("refunded") + refund_deadline = pay_status.get("contract_terms", {}).get("refund_deadline") + assert(refunded != None and refund_deadline) + t_ms = refund_deadline.get("t_ms") + assert(t_ms) + rd = Deadline(t_ms) + if not refunded and not rd.isExpired(): + 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)) + print( + "You are using Python {}.{}.".format( + sys.version_info.major, sys.version_info.minor + ) + ) sys.exit(1) - BASE_DIR = os.path.dirname(os.path.abspath(__file__)) -app = flask.Flask(__name__, template_folder=BASE_DIR, static_folder=BASE_DIR + '/../static/') +app = flask.Flask( + __name__, template_folder=BASE_DIR, static_folder=BASE_DIR + "/../static/" +) app.secret_key = base64.b64encode(os.urandom(64)).decode("utf-8") LOGGER = logging.getLogger(__name__) @@ -54,6 +90,34 @@ ARTICLE_AMOUNT = CURRENCY + ":0.5" BACKEND_URL = urljoin(BACKEND_BASE_URL, "instances/blog/") app.config.from_object(__name__) +babel = Babel(app) + +print("Using translations from:") +print(list(babel.translation_directories)) +translations = [str(translation) for translation in babel.list_translations()] +translations.append('en') +print("Operating with the following translations available:") +print(translations) + + +## +# Helper function used inside Jinja2 logic to create a links +# to the current page but in a different language. Used to +# implement the "Language" menu. +# +def self_localized(lang): + """ + Return URL for the current page in another locale. + """ + path = request.path + # path must have the form "/$LANG/$STUFF" + parts = path.split('/', 2) + if (2 >= len(parts)): + # Totally unexpected path format, do not localize + return path + return "/" + lang + "/" + parts[2] + +app.jinja_env.globals.update(self_localized=self_localized) ## @@ -76,7 +140,7 @@ def utility_processor(): # @param abort_status_code status code to return along the response. # @param params _kw_ arguments to passed verbatim to the templating engine. def err_abort(abort_status_code, **params): - t = flask.render_template("templates/error.html", **params) + t = flask.render_template("templates/error.html.j2", **params) flask.abort(flask.make_response(t, abort_status_code)) @@ -89,7 +153,7 @@ def err_abort(abort_status_code, **params): @app.errorhandler(Exception) def internal_error(e): return flask.render_template( - "templates/error.html", message="Internal error", stack=traceback.format_exc() + "templates/error.html.j2", message=gettext("Internal error"), stack=traceback.format_exc() ) @@ -99,11 +163,21 @@ def internal_error(e): # @return response object of the index page. @app.route("/") def index(): - supported = ['en', 'de' ] default = 'en' - target = flask.request.accept_languages.best_match(supported, default) + target = flask.request.accept_languages.best_match(translations, default) return flask.redirect("/" + target + "/", code=302) +@babel.localeselector +def get_locale(): + parts = request.path.split('/', 2) + if (2 >= len(parts)): + # Totally unexpected path format, do not localize + return "en" + lang = parts[1] + if lang in translations: + return lang + return "en" + ## # Serve the main index page for a particular language. # @@ -111,7 +185,7 @@ def index(): @app.route("/<lang>/") def start(lang): return flask.render_template( - "templates/index.html", lang=lang, merchant_currency=CURRENCY, articles=ARTICLES.values() + "templates/index.html.j2", lang=lang, merchant_currency=CURRENCY, articles=ARTICLES.values() ) @@ -127,8 +201,13 @@ def confirm_refund(lang, order_id): 400, message="Cannot refund unpaid article", ) article_name = pay_status["contract_terms"]["extra"]["article_name"] + + if not refundable(pay_status): + return flask.render_template( + "templates/error.html.j2", message=gettext("Article is not anymore refundable") + ) return flask.render_template( - "templates/confirm_refund.html", article_name=article_name, order_id=order_id + "templates/confirm_refund.html.j2", article_name=article_name, order_id=order_id ) @@ -152,10 +231,15 @@ def refund(lang, order_id): BACKEND_URL, f"private/orders/{order_id}", params=dict(session_id=session_id) ) order_status = pay_status.get("order_status") + if order_status != "paid": err_abort( 402, message="You did not pay for this article (nice try!)", json=pay_status ) + if not refundable(pay_status): + err_abort( + 403, message="Item not refundable (anymore)", json=pay_status + ) refund_spec = dict(reason="Demo reimbursement", refund=ARTICLE_AMOUNT) resp = backend_post(BACKEND_URL, f"private/orders/{order_id}/refund", refund_spec) return flask.redirect(pay_status["order_status_url"]) @@ -174,7 +258,7 @@ def refund(lang, order_id): # - 404: supplemental @a data not found. # In the successful case, a response object carrying the # article in it will be returned. -def render_article(article_name, data, order_id): +def render_article(article_name, data, order_id, refundable): article_info = ARTICLES.get(article_name) if article_info is None: m = "Internal error: Files for article ({}) not found.".format(article_name) @@ -188,10 +272,11 @@ def render_article(article_name, data, order_id): err_abort(404, message=m) # the order_id is needed for refunds return flask.render_template( - "templates/article_frame.html", + "templates/article_frame.html.j2", article_file=get_article_file(article_info), article_name=article_name, order_id=order_id, + refundable=refundable ) ## @@ -207,6 +292,27 @@ def post_order(article_name,lang): fulfillment_url=flask.request.base_url, summary="Essay: " + article_name.replace("_", " "), # 10 minutes time for a refund + wire_transfer_deadline=dict(t_ms=1000 * int(time.time() + 15 * 30)), + ) + order_resp = backend_post( + BACKEND_URL, + "private/orders", + dict(order=order, refund_delay=dict(d_ms=1000 * 120))) + return order_resp + +## +# Setup a fresh order with the backend. +# +# @param article_name which article the order is for +# @param lang which language to use +# +def post_order(article_name,lang): + order = dict( + amount=ARTICLE_AMOUNT, + extra=dict(article_name=article_name,lang=lang), + fulfillment_url=flask.request.base_url, + summary="Essay: " + article_name.replace("_", " "), + # 10 minutes time for a refund refund_deadline=dict(t_ms=1000 * int(time.time() + 10 * 30)), wire_transfer_deadline=dict(t_ms=1000 * int(time.time() + 15 * 30)), ) @@ -258,7 +364,6 @@ def article(article_name, lang=None, data=None): pay_status = backend_get( BACKEND_URL, f"private/orders/{order_id}", params=dict(session_id=session_id) ) - order_status = pay_status.get("order_status") if order_status == "claimed": if not lang: @@ -274,30 +379,49 @@ def article(article_name, lang=None, data=None): if order_status == "paid": refunded = pay_status["refunded"] - if refunded: + if refunded: return flask.render_template( - "templates/article_refunded.html", + "templates/article_refunded.html.j2", article_name=article_name, order_id=order_id, ) - else: - response = render_article(article_name, data, order_id) - response.set_cookie("order_id", order_id, path=urllib.parse.quote(f"/essay/{article_name}")) - response.set_cookie("order_id", order_id, path=urllib.parse.quote(f"/{lang}/essay/{article_name}")) - return response - else: - # Check if the customer came on this page via the - # re-purchase detection mechanism - ai = pay_status.get("already_paid_order_id") - au = pay_status.get("already_paid_fulfillment_url") - if ai is not None and au is not None: - response = flask.redirect(au) - response.set_cookie("order_id", ai, path=urllib.parse.quote(f"/essay/{article_name}")) - return response + response = render_article(article_name, data, order_id, refundable(pay_status)) + response.set_cookie( + "order_id", order_id, path=urllib.parse.quote(f"/essay/{article_name}") + ) + response.set_cookie( + "order_id", order_id, path=urllib.parse.quote(f"/{lang}/essay/{article_name}") + ) + return response + + # Check if the customer came on this page via the + # re-purchase detection mechanism + ai = pay_status.get("already_paid_order_id") + au = pay_status.get("already_paid_fulfillment_url") + if ai is not None and au is not None: + response = flask.redirect(au) + response.set_cookie( + "order_id", ai, path=urllib.parse.quote(f"/essay/{article_name}") + ) + return response # Redirect the browser to a page where the wallet can # run the payment protocol. response = flask.redirect(pay_status["order_status_url"]) - response.set_cookie("order_id", order_id, path=urllib.parse.quote(f"/essay/{article_name}")) - response.set_cookie("order_id", order_id, path=urllib.parse.quote(f"/{lang}/essay/{article_name}")) + response.set_cookie( + "order_id", order_id, path=urllib.parse.quote(f"/essay/{article_name}") + ) + response.set_cookie( + "order_id", order_id, path=urllib.parse.quote(f"/{lang}/essay/{article_name}") + ) return response + +@app.errorhandler(500) +def handler(e): + return flask.render_template( + "templates/error.html.j2", message=gettext("Internal server error")) + +@app.errorhandler(404) +def handler(e): + return flask.render_template( + "templates/error.html.j2", message=gettext("Page not found")) |