summaryrefslogtreecommitdiff
path: root/talermerchantdemos/blog/blog.py
diff options
context:
space:
mode:
Diffstat (limited to 'talermerchantdemos/blog/blog.py')
-rw-r--r--talermerchantdemos/blog/blog.py190
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"))