summaryrefslogtreecommitdiff
path: root/talerblog
diff options
context:
space:
mode:
authorMarcello Stanisci <stanisci.m@gmail.com>2019-03-12 17:36:29 +0100
committerMarcello Stanisci <stanisci.m@gmail.com>2019-03-12 17:36:29 +0100
commit9a02cbe1785b72e08b5d7dd5b3a08a766f0c3d84 (patch)
tree8a893b54beaafb700fec0b1137f8c5fda65f1a01 /talerblog
parentb9821604dab511eb9b3b2f3eeb700cb685631eba (diff)
downloadblog-9a02cbe1785b72e08b5d7dd5b3a08a766f0c3d84.tar.gz
blog-9a02cbe1785b72e08b5d7dd5b3a08a766f0c3d84.tar.bz2
blog-9a02cbe1785b72e08b5d7dd5b3a08a766f0c3d84.zip
Doxygen-commenting blog.py, and content.py.
Diffstat (limited to 'talerblog')
-rw-r--r--talerblog/blog/blog.py152
-rw-r--r--talerblog/blog/content.py45
2 files changed, 158 insertions, 39 deletions
diff --git a/talerblog/blog/blog.py b/talerblog/blog/blog.py
index 02d10d0..860a3a9 100644
--- a/talerblog/blog/blog.py
+++ b/talerblog/blog/blog.py
@@ -1,3 +1,4 @@
+##
# This file is part of GNU TALER.
# Copyright (C) 2014-2017 INRIA
#
@@ -14,11 +15,7 @@
#
# @author Florian Dold
# @author Marcello Stanisci
-
-
-"""
-Implement URL handlers and payment logic for the blog merchant.
-"""
+# @brief Implementation of a Taler-compatible blog.
from urllib.parse import urljoin, quote
import logging
@@ -48,6 +45,11 @@ ARTICLE_AMOUNT = CURRENCY + ":0.5"
app.config.from_object(__name__)
+##
+# Extends the templating language with a function (@c env)
+# that fetches values from the environment.
+#
+# @return a @a dict containing the extension.
@app.context_processor
def utility_processor():
# These helpers will be available in templates
@@ -56,11 +58,22 @@ def utility_processor():
return dict(env=env)
+##
+# Return a error response to the client.
+#
+# @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)
flask.abort(flask.make_response(t, abort_status_code))
-
+##
+# Issue a GET request to the backend.
+#
+# @param endpoint the backend endpoint where to issue the request.
+# @param params (dict type of) URL parameters to append to the request.
+# @return the JSON response from the backend, or a error response
+# if something unexpected happens.
def backend_get(endpoint, params):
headers = {"Authorization": "ApiKey " + APIKEY}
try:
@@ -76,7 +89,14 @@ def backend_get(endpoint, params):
json=response_json, status_code=resp.status_code)
return response_json
-
+##
+# POST a request to the backend, and return a error
+# response if any error occurs.
+#
+# @param endpoint the backend endpoint where to POST
+# this request.
+# @param json the POST's body.
+# @return the backend response (JSON format).
def backend_post(endpoint, json):
headers = {"Authorization": "ApiKey " + APIKEY}
try:
@@ -94,28 +114,42 @@ def backend_post(endpoint, json):
return response_json
+
+##
+# "Fallback" exception handler to capture all the unmanaged errors.
+#
+# @param e the Exception object, currently unused.
+# @return flask-native response object carrying the error message
+# (and execution stack!).
@app.errorhandler(Exception)
def internal_error(e):
return flask.render_template("templates/error.html",
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,
articles=ARTICLES.values())
-
+##
+# Serve the "/javascript" page.
+#
+# @return response object for the /javascript page.
@app.route("/javascript")
def javascript_licensing():
return flask.render_template("templates/javascript.html")
-# Cache for paid articles (in the form <session_id>-<article_name>), so we
-# don't always have to ask the backend / DB, and so we don't have to store
-# variable-size cookies on the client.
+##
+# @brief Cache for paid articles (in the form <session_id>-<article_name>),
+# so we don't always have to ask the backend / DB, and so we don't
+# have to store variable-size cookies on the client.
try:
import uwsgi
paid_articles_cache = UWSGICache(0, "paid_articles")
@@ -123,8 +157,18 @@ except ImportError:
paid_articles_cache = SimpleCache()
+##
# Triggers the refund by serving /refund/test?order_id=XY.
# Will be triggered by a "refund button".
+#
+# @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.
@app.route("/refund/<order_id>", methods=["POST"])
def refund(order_id):
article_name = flask.request.form.get("article_name")
@@ -133,12 +177,10 @@ def refund(order_id):
LOGGER.info("Looking for %s to refund" % article_name)
if not order_id:
return flask.jsonify(dict(error="Aborting refund: article not payed")), 401
- refund_spec = dict(
- instance=INSTANCE,
- order_id=order_id,
- reason="Demo reimbursement",
- refund=ARTICLE_AMOUNT,
- )
+ refund_spec = dict(instance=INSTANCE,
+ order_id=order_id,
+ reason="Demo reimbursement",
+ refund=ARTICLE_AMOUNT)
resp = backend_post("refund", refund_spec)
try:
# delete from paid article cache
@@ -151,6 +193,20 @@ def refund(order_id):
json=resp, stack=traceback.format_exc())
+
+##
+# Render the article after a successful purchase.
+#
+# @param article_name _slugged_ (= spaces converted to underscores) article title.
+# @param data image filename to return along the article.
+# @param order_id the ID of the order where this article got purchased.
+# (Will be put in the refund-request form action, since any article
+# will also have a "refund button" aside.)
+# @return the following errors (named by HTTP return code):
+# - 500: file for article not found.
+# - 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):
article_info = ARTICLES.get(article_name)
if article_info is None:
@@ -169,6 +225,25 @@ def render_article(article_name, data, order_id):
order_id=order_id)
+##
+# Trigger a article purchase. The logic follows the main steps:
+#
+# 1. Always check if the article was paid already, via the
+# "/check-payment" API from the backend.
+# 2. If so, return the article.
+# 3. If not, redirect the browser to a page where the
+# wallet will initiate the payment protocol.
+#
+# @param article_name (slugged) article title.
+# @param data filename of a supplement data (image/sound/..)
+# @return the following errors might occur (named by HTTP response code):
+# - 402: @a article_name does not correspond to the @a order_id
+# of a PAYED article.
+# - 500: neither the article was paid, nor a payment was triggered.
+# - 400: a invalid order_id was passed along the GET parameters.
+# 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("/essay/<article_name>/data/<data>")
def article(article_name, data=None):
@@ -187,42 +262,59 @@ def article(article_name, data=None):
if cached_order_id:
return render_article(article_name, data, cached_order_id)
+ ##
+ # If there was an order_id but no session_sig, either the user played
+ # around with the URL or the wallet is old/broken.
if order_id and not session_sig:
- # If there was an order_id but no session_sig, either the user played
- # around with the URL or the wallet is old/broken.
err_abort(400, message=("Bad request (session_sig missing). "
"Your wallet might be broken or outdated"))
-
+ ##
+ # First-timer; generate order first.
if not order_id:
order = dict(
amount=ARTICLE_AMOUNT,
extra=dict(article_name=article_name),
fulfillment_url=flask.request.base_url,
instance=INSTANCE,
- summary="Essay: " + article_name.replace("_", " "),
- )
+ summary="Essay: " + article_name.replace("_", " "))
order_resp = backend_post("order", dict(order=order))
order_id = order_resp["order_id"]
- pay_params = dict(
- instance=INSTANCE,
- order_id=order_id,
- resource_url=flask.request.base_url,
- session_id=session_id,
- session_sig=session_sig,
- )
+ ##
+ # Prepare data for the upcoming payment check.
+ pay_params = dict(instance=INSTANCE,
+ order_id=order_id,
+ resource_url=flask.request.base_url,
+ session_id=session_id,
+ session_sig=session_sig)
pay_status = backend_get("check-payment", pay_params)
if pay_status.get("paid"):
+
+ ##
+ # Somehow, a session with a payed article which _differs_ from
+ # the article requested in the URL existed; trigger the pay protocol!
if pay_status["contract_terms"]["extra"]["article_name"] != article_name:
err_abort(402, message="You did not pay for this article (nice try!)", json=pay_status)
+
+ ##
+ # Show a "article refunded" page, in that case.
if pay_status.get("refunded"):
return flask.render_template("templates/article_refunded.html",
article_name=article_name)
+ ##
+ # Put the article in the cache.
paid_articles_cache.set(session_id + "-" + article_name, order_id)
+
+ ##
+ # Finally return the article.
return render_article(article_name, data, order_id)
+
else:
+ ##
+ # Redirect the browser to a page where the wallet can
+ # run the payment protocol.
if pay_status.get("payment_redirect_url"):
return flask.redirect(pay_status["payment_redirect_url"])
diff --git a/talerblog/blog/content.py b/talerblog/blog/content.py
index 4aeb865..8dddd1f 100644
--- a/talerblog/blog/content.py
+++ b/talerblog/blog/content.py
@@ -1,3 +1,4 @@
+##
# This file is part of GNU TALER.
# Copyright (C) 2014-2016 INRIA
#
@@ -13,10 +14,7 @@
# GNU TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
#
# @author Florian Dold
-
-"""
-Define content and associated metadata that is served on the blog.
-"""
+# @brief Define content and associated metadata that is served on the blog.
from collections import OrderedDict, namedtuple
import logging
@@ -29,27 +27,56 @@ LOGGER = logging.getLogger(__name__)
NOISY_LOGGER = logging.getLogger("chardet.charsetprober")
NOISY_LOGGER.setLevel(logging.INFO)
Article = namedtuple("Article", "slug title teaser main_file extra_files")
-ARTICLES = OrderedDict()
+##
+# @var if a article is added to this list, then it will
+# be made available in the blog.
+ARTICLES = OrderedDict()
+##
+# Add article to the list of the available articles.
+#
+# @param slug article's title with all the spaces converted to underscores.
+# @param title article's title.
+# @param teaser a short description of the main article's content.
+# @param main_file path to the article's HTML file.
+# @param extra_file collection of extra files associated with the
+# article, like images and sounds.
def add_article(slug, title, teaser, main_file, extra_files):
ARTICLES[slug] = Article(slug, title, teaser, main_file, extra_files)
+##
+# 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("talerblog", os.path.join("blog/data/", image))
return os.path.abspath(filex)
-
+##
+# Build the file path of a article.
+#
+# @param article the article filename.
+# @return the path to the article HTML file.
def get_article_file(article):
filex = resource_filename("talerblog", article.main_file)
return os.path.basename(filex)
+##
+# Extract information from HTML file, and use these informations
+# to make the article available in the blog.
+#
+# @param resource_name path to the (HTML) article.
+# @param teaser_paragraph position of the teaser paragraph in the
+# article's list of all the P tags. Defaults to zero, as normally
+# this information is found under the very first P tag.
+# @param title article's title; normally, this bit is extracted from the
+# HTML itself, so give it here if a explicit title needs to be
+# specified.
def add_from_html(resource_name, teaser_paragraph=0, title=None):
- """
- Extract information from article html.
- """
res = resource_stream("talerblog", resource_name)
soup = BeautifulSoup(res, 'html.parser')
res.close()