summaryrefslogtreecommitdiff
path: root/talermerchantdemos/blog/blog.py
diff options
context:
space:
mode:
authorChristian Grothoff <christian@grothoff.org>2020-10-10 22:55:31 +0200
committerChristian Grothoff <christian@grothoff.org>2020-10-10 22:55:31 +0200
commit2e665813a44988bfd906c0fab773f82652047841 (patch)
treeabb09bb226ec904d23592edbc8372634f5b7a31b /talermerchantdemos/blog/blog.py
parentad45c1d0d6152d6eddde39881461a3472f799ab7 (diff)
parent58af97724593e6d9cf423035f46da8f88c29526c (diff)
downloadtaler-merchant-demos-2e665813a44988bfd906c0fab773f82652047841.tar.gz
taler-merchant-demos-2e665813a44988bfd906c0fab773f82652047841.tar.bz2
taler-merchant-demos-2e665813a44988bfd906c0fab773f82652047841.zip
merge torsten-redesign branch, implement i18n support
Diffstat (limited to 'talermerchantdemos/blog/blog.py')
-rw-r--r--talermerchantdemos/blog/blog.py139
1 files changed, 110 insertions, 29 deletions
diff --git a/talermerchantdemos/blog/blog.py b/talermerchantdemos/blog/blog.py
index d6cd3fc..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,6 +24,11 @@ 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
@@ -85,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)
##
@@ -107,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))
@@ -120,23 +153,44 @@ 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()
)
##
-# Serve the main index page.
+# Serve the main index page, redirecting to /<lang>/
#
# @return response object of the index page.
@app.route("/")
def index():
+ default = 'en'
+ 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.
+#
+# @return response object of the index page.
+@app.route("/<lang>/")
+def start(lang):
return flask.render_template(
- "templates/index.html", merchant_currency=CURRENCY, articles=ARTICLES.values()
+ "templates/index.html.j2", lang=lang, merchant_currency=CURRENCY, articles=ARTICLES.values()
)
-@app.route("/confirm-refund/<order_id>", methods=["GET"])
-def confirm_refund(order_id):
+@app.route("/<lang>/confirm-refund/<order_id>", methods=["GET"])
+def confirm_refund(lang, order_id):
session_id = flask.session.get("session_id", "")
pay_status = backend_get(
BACKEND_URL, f"private/orders/{order_id}", params=dict(session_id=session_id)
@@ -144,16 +198,16 @@ def confirm_refund(order_id):
order_status = pay_status.get("order_status")
if order_status != "paid":
err_abort(
- 400, message="can't refund unpaid article",
+ 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", message="Item not refundable (anymore)"
+ "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
)
@@ -163,16 +217,15 @@ def confirm_refund(order_id):
#
# @param order_id the order ID of the transaction to refund.
# @return the following errors (named by HTTP response code):
-# - 400: no article was asked to be refunded!
-# - 401: the refund was asked on a non-payed article.
-# - 500: the backend was unable to give response.
-# Or, in the successful case, a redirection to the
-# "refund URL" is returned; then the wallet will run
-# the refund protocol in a transparent way.
+# - 400: order unknown
+# - 402: the refund was asked on an unpaid article.
+# - 302: in the successful case, a redirection to the
+# "refund URL" is returned; then the wallet will run
+# the refund protocol in a transparent way.
@app.route("/refund/<order_id>", methods=["POST"])
-def refund(order_id):
+def refund(lang, order_id):
if not order_id:
- return flask.jsonify(dict(error="Aborting refund: article not payed")), 401
+ return flask.jsonify(dict(error="Aborting refund: order unknown")), 400
session_id = flask.session.get("session_id", "")
pay_status = backend_get(
BACKEND_URL, f"private/orders/{order_id}", params=dict(session_id=session_id)
@@ -219,7 +272,7 @@ def render_article(article_name, data, order_id, refundable):
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,
@@ -247,6 +300,25 @@ def post_order(article_name,lang):
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)),
+ )
+ order_resp = backend_post(BACKEND_URL, "private/orders", dict(order=order))
+ return order_resp
+
##
# Trigger a article purchase. The logic follows the main steps:
@@ -267,9 +339,10 @@ def post_order(article_name,lang):
# In the successful case, either the article is returned, or
# the browser gets redirected to a page where the wallet can
# send the payment.
-@app.route("/essay/<article_name>")
+@app.route("/<lang>/essay/<article_name>")
+@app.route("/<lang>/essay/<article_name>/data/<data>")
@app.route("/essay/<article_name>/data/<data>")
-def article(article_name, data=None):
+def article(article_name, lang=None, data=None):
# We use an explicit session ID so that each payment (or payment replay) is
# bound to a browser. This forces re-play and prevents sharing the article
# by just sharing the URL.
@@ -279,8 +352,6 @@ def article(article_name, data=None):
if not session_id:
session_id = flask.session["session_id"] = str(uuid.uuid4())
order_id = None
- # Temporary workaround...
- lang="en"
##
# First-timer; generate order first.
if not order_id:
@@ -309,12 +380,19 @@ def article(article_name, data=None):
if order_status == "paid":
refunded = pay_status["refunded"]
if refunded:
- return flask.render_template(
- "templates/article_refunded.html",
+ return flask.render_template(
+ "templates/article_refunded.html.j2",
article_name=article_name,
order_id=order_id,
)
- return render_article(article_name, data, order_id, refundable(pay_status))
+ 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
@@ -333,14 +411,17 @@ def article(article_name, data=None):
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", message="Internal server error")
+ "templates/error.html.j2", message=gettext("Internal server error"))
@app.errorhandler(404)
def handler(e):
return flask.render_template(
- "templates/error.html", message="Page not found")
+ "templates/error.html.j2", message=gettext("Page not found"))