diff options
author | Marcello Stanisci <stanisci.m@gmail.com> | 2019-03-12 17:36:29 +0100 |
---|---|---|
committer | Marcello Stanisci <stanisci.m@gmail.com> | 2019-03-12 17:36:29 +0100 |
commit | 9a02cbe1785b72e08b5d7dd5b3a08a766f0c3d84 (patch) | |
tree | 8a893b54beaafb700fec0b1137f8c5fda65f1a01 /talerblog | |
parent | b9821604dab511eb9b3b2f3eeb700cb685631eba (diff) | |
download | blog-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.py | 152 | ||||
-rw-r--r-- | talerblog/blog/content.py | 45 |
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() |