gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[GNUnet-SVN] [taler-blog] branch stable updated (8cbacb5 -> 0e9953d)


From: gnunet
Subject: [GNUnet-SVN] [taler-blog] branch stable updated (8cbacb5 -> 0e9953d)
Date: Mon, 05 Feb 2018 11:51:24 +0100

This is an automated email from the git hooks/post-receive script.

marcello pushed a change to branch stable
in repository blog.

    from 8cbacb5  fix demobar
     add 8ce17eb  mock and nose for testing
     add 24d0c1c  repo description
     add 3744683  installing beautifulsoup4 automatically
     add 95f928c  providing 'make pylint'
     add 3ce3aeb  some linting
     add 078cac0  suppressing output from noisy logger
     add 8d8aade  done with /refund test case
     add d105795  remove backoffice things (moved to backoffice.git)
     add 58be785  use upcoming backend API
     add b879ff9  temporarily disable refund test case
     add 2a17c7f  typo / missing namespace
     add 96992ce  remove unnecessary param
     add d6b66a9  fix unmatching variable name
     add 41c1a7b  further simplify blog
     add 88cdca2  no more /generate-contract in test
     add aab81fb  fix var name
     add 47c2829  fix var name in template
     add ba2879c  include instance in order
     add f78a9b9  proper backend error handling
     add a9ffe29  fix syntax error
     add 249e5c9  typo
     add d581e32  fix errors
     add 73a7298  typo/syntax
     add 119e1f7  payment logic
     add eeb3919  typo
     add bde9272  include resource url
     add 27414f7  no need for custom URL helpers
     add 9e5e665  use url_for properly
     add 8833ad7  spelling
     add d7e776f  specify currency as decimal number
     add a50b35a  fix refund
     add 7677796  fix refund
     add fb4390a  fix var name clash
     add 6588044  add paid article cache
     add 491e171  explicitly convert uuid to string
     add 90c9391  fix article rendering for articles with images
     add bc47af7  fix variable name
     add b65bffe  use cached order_id
     add 6d14250  include error message
     add 08d6da0  more readable errors
     add 76e5021  remove unused JS
     add dccc33d  delete article from cache after refund
     add 15544eb  actually include extra information in contract terms
     add 4dfb6a9  fix typo, output debugging info on error
     add cceb36a  remove some wall of text, remove review quotes
     add ebc03f9  experiment with styling
     add 3ca2453  style fixes
     add c9cba96  fix markup
     add d317904  style
     add e056437  bar styling
     add 0cf8d6e  fix teaser
     add 1cb24be  add viewport
     add 3aac29e  class
     add 35c6325  typo
     add 2d2c1a7  copy
     add d897ba6  match tags
     add ef55ff9  quotes
     add b95ad65  teaser to text
     add eb8c5b2  use uwsgi cache
     add 930140a  try importing uwsgi to trigger the correct exception
     add 6980278  simpler terminology: talk only about order, not proposal
     add 36db0c0  apikey
     add d83dbc3  fix test config
     add 0e9953d  check that requested article name actually matches order

No new revisions were added by this update.

Summary of changes:
 .gitignore                                  |  15 ++
 Makefile.in                                 |   6 +-
 setup.py                                    |   5 +-
 taler-merchant-blog.in                      |   3 +-
 talerblog/blog/blog.py                      | 349 +++++++++++++---------------
 talerblog/blog/content.py                   |   9 +-
 talerblog/blog/templates/article_frame.html |   4 +-
 talerblog/blog/templates/backoffice.html    |  80 -------
 talerblog/blog/templates/base.html          |  52 +++--
 talerblog/blog/templates/error.html         |  22 ++
 talerblog/blog/templates/index.html         | 116 +++------
 talerblog/blog/templates/purchase.html      |  41 ----
 talerblog/helpers.py                        | 101 --------
 talerblog/tests.conf                        |   1 +
 talerblog/tests.py                          |  59 ++---
 15 files changed, 319 insertions(+), 544 deletions(-)
 create mode 100644 .gitignore
 delete mode 100644 talerblog/blog/templates/backoffice.html
 create mode 100644 talerblog/blog/templates/error.html
 delete mode 100644 talerblog/blog/templates/purchase.html
 delete mode 100644 talerblog/helpers.py

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..409c488
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,15 @@
+.eggs/
+Makefile
+aclocal.m4
+autom4te.cache/
+compile
+config.log
+config.status
+configure
+frontend-blog.wsgi
+install-sh
+missing
+taler-merchant-blog
+talerblog.egg-info/
+talerblog/__pycache__/
+talerblog/blog/__pycache__/
diff --git a/Makefile.in b/Makefile.in
index 508f141..c42a688 100644
--- a/Makefile.in
+++ b/Makefile.in
@@ -49,5 +49,7 @@ install: $(templates) install-data
 .PHONY: check
 check:
        @export address@hidden@/talerblog/tests.conf; \
-        export address@hidden@/lib/address@hidden@/site-packages; \
-        python3 talerblog/tests.py
+        python3 setup.py test
+
+pylint:
+       @pylint talerblog/
diff --git a/setup.py b/setup.py
index 4c76f73..08c7ee5 100755
--- a/setup.py
+++ b/setup.py
@@ -8,7 +8,10 @@ setup(name='talerblog',
       author_email='address@hidden',
       license='GPL',
       packages=find_packages(),
-      install_requires=["Flask>=0.10"],
+      install_requires=["Flask>=0.10",
+                        "beautifulsoup4"],
+      tests_require=["nose", "mock"],
+      test_suite="nose.collector",
       package_data={
           '':[
               "blog/templates/*.html",
diff --git a/taler-merchant-blog.in b/taler-merchant-blog.in
index ca1b7c7..0b25879 100644
--- a/taler-merchant-blog.in
+++ b/taler-merchant-blog.in
@@ -43,7 +43,8 @@ def handle_serve_uwsgi(args):
               "--master",
               "--die-on-term",
               "--log-format", UWSGI_LOGFMT,
-              "--wsgi-file", "@prefix@/share/taler/frontend-blog.wsgi"]
+              "--wsgi-file", "@prefix@/share/taler/frontend-blog.wsgi",
+              "--cache2", "name=paid_articles,items=500"]
     if serve_uwsgi == "tcp":
         port = TC["blog"]["uwsgi_port"].value_int(required=True)
         spec = ":%d" % (port,)
diff --git a/talerblog/blog/blog.py b/talerblog/blog/blog.py
index 650edd0..02d10d0 100644
--- a/talerblog/blog/blog.py
+++ b/talerblog/blog/blog.py
@@ -20,45 +20,85 @@
 Implement URL handlers and payment logic for the blog merchant.
 """
 
-from urllib.parse import urljoin, quote, parse_qsl
+from urllib.parse import urljoin, quote
 import logging
 import os
+import traceback
+import uuid
 import base64
 import requests
 import flask
+from werkzeug.contrib.cache import UWSGICache, SimpleCache
 from talerblog.talerconfig import TalerConfig
-from talerblog.helpers import (make_url, \
-    expect_parameter, join_urlparts, \
-    get_query_string, backend_error)
-from talerblog.blog.content import (ARTICLES, \
-    get_article_file, get_image_file)
+from ..blog.content import ARTICLES, get_article_file, get_image_file
 
-LOGGER = logging.getLogger(__name__)
 
 BASE_DIR = os.path.dirname(os.path.abspath(__file__))
-
 app = flask.Flask(__name__, template_folder=BASE_DIR)
-app.debug = True
 app.secret_key = base64.b64encode(os.urandom(64)).decode('utf-8')
 
+LOGGER = logging.getLogger(__name__)
 TC = TalerConfig.from_env()
-
 BACKEND_URL = TC["frontends"]["backend"].value_string(required=True)
 CURRENCY = TC["taler"]["currency"].value_string(required=True)
+APIKEY = TC["frontends"]["backend_apikey"].value_string(required=True)
 INSTANCE = TC["blog"]["instance"].value_string(required=True)
-
-ARTICLE_AMOUNT = dict(value=0, fraction=50000000, currency=CURRENCY)
+ARTICLE_AMOUNT = CURRENCY + ":0.5"
 
 app.config.from_object(__name__)
 
 
 @app.context_processor
 def utility_processor():
-    def url(my_url):
-        return join_urlparts(flask.request.script_root, my_url)
+    # These helpers will be available in templates
     def env(name, default=None):
         return os.environ.get(name, default)
-    return dict(url=url, env=env)
+    return dict(env=env)
+
+
+def err_abort(abort_status_code, **params):
+    t = flask.render_template("templates/error.html", **params)
+    flask.abort(flask.make_response(t, abort_status_code))
+
+
+def backend_get(endpoint, params):
+    headers = {"Authorization": "ApiKey " + APIKEY}
+    try:
+        resp = requests.get(urljoin(BACKEND_URL, endpoint), params=params, 
headers=headers)
+    except requests.ConnectionError:
+        err_abort(500, message="Could not establish connection to backend")
+    try:
+        response_json = resp.json()
+    except ValueError:
+        err_abort(500, message="Could not parse response from backend")
+    if resp.status_code != 200:
+        err_abort(500, message="Backend returned error status",
+                  json=response_json, status_code=resp.status_code)
+    return response_json
+
+
+def backend_post(endpoint, json):
+    headers = {"Authorization": "ApiKey " + APIKEY}
+    try:
+        resp = requests.post(urljoin(BACKEND_URL, endpoint), json=json, 
headers=headers)
+    except requests.ConnectionError:
+        err_abort(500, message="Could not establish connection to backend")
+    try:
+        response_json = resp.json()
+    except ValueError:
+        err_abort(500, message="Could not parse response from backend",
+                  status_code=resp.status_code)
+    if resp.status_code != 200:
+        err_abort(500, message="Backend returned error status",
+                  json=response_json, status_code=resp.status_code)
+    return response_json
+
+
address@hidden(Exception)
+def internal_error(e):
+    return flask.render_template("templates/error.html",
+                                 message="Internal error",
+                                 stack=traceback.format_exc())
 
 
 @app.route("/")
@@ -72,180 +112,119 @@ def index():
 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.
+try:
+    import uwsgi
+    paid_articles_cache = UWSGICache(0, "paid_articles")
+except ImportError:
+    paid_articles_cache = SimpleCache()
+
+
 # Triggers the refund by serving /refund/test?order_id=XY.
 # Will be triggered by a "refund button".
address@hidden("/refund", methods=["GET", "POST"])
-def refund():
-    if flask.request.method == "POST":
-        payed_articles = flask.session["payed_articles"] = 
flask.session.get("payed_articles", {})
-        article_name = flask.request.form.get("article_name")
-        if not article_name:
-            return flask.jsonify(dict(error="No article_name found in form")), 
400
-        LOGGER.info("Looking for %s to refund" % article_name)
-        order_id = payed_articles.get(article_name)
-        if not order_id:
-            return flask.jsonify(dict(error="Aborting refund: article not 
payed")), 401
-        resp = requests.post(urljoin(BACKEND_URL, "refund"),
-                             json=dict(order_id=order_id,
-                                       refund=ARTICLE_AMOUNT,
-                                       reason="Demo reimbursement",
-                                       instance=INSTANCE))
-        if resp.status_code != 200:
-            return backend_error(resp)
-        payed_articles[article_name] = "__refunded"
-        response = flask.make_response()
-        response.headers["X-Taler-Refund-Url"] = make_url("/refund", 
("order_id", order_id))
-        return response, 402
-
-    else:
-        order_id = expect_parameter("order_id", False)
-        if not order_id:
-            LOGGER.error("Missing parameter 'order_id'")
-            return flask.jsonify(dict(error="Missing parameter 'order_id'")), 
400
-        resp = requests.get(urljoin(BACKEND_URL, "refund"),
-                            params=dict(order_id=order_id, instance=INSTANCE))
-        if resp.status_code != 200:
-            return backend_error(resp)
-        return flask.jsonify(resp.json()), resp.status_code
-
-
address@hidden("/generate-contract", methods=["GET"])
-def generate_contract():
-    article_name = expect_parameter("article_name")
-    pretty_name = article_name.replace("_", " ")
-    order = dict(
-        summary=pretty_name,
-        nonce=flask.request.args.get("nonce"),
-        amount=ARTICLE_AMOUNT,
-        max_fee=dict(value=1, fraction=0, currency=CURRENCY),
-        products=[
-            dict(
-                description="Essay: " + pretty_name,
-                quantity=1,
-                product_id=0,
-                price=ARTICLE_AMOUNT,
-            ),
-        ],
-        fulfillment_url=make_url("/essay/" + quote(article_name)),
-        pay_url=make_url("/pay"),
-        merchant=dict(
-            instance=INSTANCE,
-            address="nowhere",
-            name="Kudos Inc.",
-            jurisdiction="none",
-        ),
-        extra=dict(article_name=article_name),
address@hidden("/refund/<order_id>", methods=["POST"])
+def refund(order_id):
+    article_name = flask.request.form.get("article_name")
+    if not article_name:
+        return flask.jsonify(dict(error="No article_name found in form")), 400
+    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,
     )
-    resp = requests.post(urljoin(BACKEND_URL, "proposal"),
-                         json=dict(order=order))
-    if resp.status_code != 200:
-        return backend_error(resp)
-    proposal_resp = resp.json()
-    return flask.jsonify(**proposal_resp)
-
-
address@hidden("/cc-payment/<name>")
-def cc_payment(name):
-    return flask.render_template("templates/cc-payment.html",
-                                 article_name=name)
-
-
address@hidden("/essay/<name>")
address@hidden("/essay/<name>/data/<data>")
-def article(name, data=None):
-    LOGGER.info("processing %s" % name)
-    payed_articles = flask.session.get("payed_articles", {})
-
-    if payed_articles.get(name, "") == "__refunded":
-        return flask.render_template("templates/article_refunded.html", 
article_name=name)
-
-    if name in payed_articles:
-        articleInfo = ARTICLES[name]
-        if articleInfo is None:
-            flask.abort(500)
-        if data is not None:
-            if data in articleInfo.extra_files:
-                return flask.send_file(get_image_file(data))
-            return "permission denied", 400
-        return flask.render_template("templates/article_frame.html",
-                                     
article_file=get_article_file(articleInfo),
-                                     article_name=name)
-
-    contract_url = make_url("/generate-contract",
-                            ("article_name", name))
-    response = flask.make_response(
-        flask.render_template("templates/fallback.html"), 402)
-    response.headers["X-Taler-Contract-Url"] = contract_url
-    response.headers["X-Taler-Contract-Query"] = "fulfillment_url"
-    # Useless (?) header, as X-Taler-Contract-Url takes always (?) precedence
-    # over X-Offer-Url.  This one might only be useful if the contract 
retrieval
-    # goes wrong.
-    response.headers["X-Taler-Offer-Url"] = make_url("/essay/" + quote(name))
-    return response
-
-
address@hidden("/pay", methods=["POST"])
-def pay():
-    deposit_permission = flask.request.get_json()
-    if deposit_permission is None:
-        return flask.jsonify(error="no json in body"), 400
-    resp = requests.post(urljoin(BACKEND_URL, "pay"),
-                         json=deposit_permission)
-    if resp.status_code != 200:
-        return backend_error(resp)
-    proposal_data = resp.json()["contract_terms"]
-    article_name = proposal_data["extra"]["article_name"]
-    payed_articles = flask.session["payed_articles"] = 
flask.session.get("payed_articles", {})
-
+    resp = backend_post("refund", refund_spec)
     try:
-        resp.json()["refund_permissions"].pop()
-        # we had some refunds on the article purchase already!
-        LOGGER.info("Article %s was refunded, before /pay" % article_name)
-        payed_articles[article_name] = "__refunded"
-        return flask.jsonify(resp.json()), 200
-    except IndexError:
-        pass
-
-    if not deposit_permission["order_id"]:
-        LOGGER.error("order_id missing from deposit_permission!")
-        return flask.jsonify(dict(error="internal error: ask for refund!")), 
500
-    if article_name not in payed_articles:
-        LOGGER.info("Article %s goes in state" % article_name)
-        payed_articles[article_name] = deposit_permission["order_id"]
-    return flask.jsonify(resp.json()), 200
-
-
address@hidden("/history")
-def history():
-    qs = get_query_string().decode("utf-8")
-    url = urljoin(BACKEND_URL, "history")
-    resp = requests.get(url, params=dict(parse_qsl(qs)))
-    if resp.status_code != 200:
-        return backend_error(resp)
-    return flask.jsonify(resp.json()), resp.status_code
-
-
address@hidden("/backoffice")
-def track():
-    response = 
flask.make_response(flask.render_template("templates/backoffice.html"))
-    return response
-
+        # delete from paid article cache
+        article_name = resp["contract_terms"]["extra"]["article_name"]
+        session_id = flask.session.get("session_id", "")
+        paid_articles_cache.delete(session_id + "-" + article_name)
+        return flask.redirect(resp["refund_redirect_url"])
+    except KeyError:
+        err_abort(500, message="Response from backend incomplete",
+                json=resp, stack=traceback.format_exc())
+
+
+def render_article(article_name, data, order_id):
+    article_info = ARTICLES.get(article_name)
+    if article_info is None:
+        m = "Internal error: Files for article ({}) not 
found.".format(article_name)
+        err_abort(500, message=m)
+    if data is not None:
+        if data in article_info.extra_files:
+            return flask.send_file(get_image_file(data))
+        m = "Supplemental file ({}) for article ({}) not found.".format(
+                data, article_name)
+        err_abort(404, message=m)
+    # the order_id is needed for refunds
+    return flask.render_template("templates/article_frame.html",
+                                 article_file=get_article_file(article_info),
+                                 article_name=article_name,
+                                 order_id=order_id)
+
+
address@hidden("/essay/<article_name>")
address@hidden("/essay/<article_name>/data/<data>")
+def article(article_name, 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.
+    session_id = flask.session.get("session_id")
+    order_id = flask.request.args.get("order_id")
+    session_sig = flask.request.args.get("session_sig")
+
+    if not session_id:
+        session_id = flask.session["session_id"] = str(uuid.uuid4())
+
+    cached_order_id = paid_articles_cache.get(session_id + "-" + article_name)
+    if cached_order_id:
+        return render_article(article_name, data, cached_order_id)
+
+    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"))
+
+    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("_", " "),
+        )
+        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,
+    )
 
address@hidden("/track/transfer")
-def track_transfer():
-    qs = get_query_string().decode("utf-8")
-    url = urljoin(BACKEND_URL, "track/transfer")
-    resp = requests.get(url, params=dict(parse_qsl(qs)))
-    if resp.status_code != 200:
-        return backend_error(resp)
-    return flask.jsonify(resp.json()), resp.status_code
+    pay_status = backend_get("check-payment", pay_params)
 
+    if pay_status.get("paid"):
+        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)
+        if pay_status.get("refunded"):
+            return flask.render_template("templates/article_refunded.html",
+                                         article_name=article_name)
+        paid_articles_cache.set(session_id + "-" + article_name, order_id)
+        return render_article(article_name, data, order_id)
+    else:
+        if pay_status.get("payment_redirect_url"):
+            return flask.redirect(pay_status["payment_redirect_url"])
 
address@hidden("/track/order")
-def track_order():
-    qs = get_query_string().decode("utf-8")
-    url = urljoin(BACKEND_URL, "track/transaction")
-    resp = requests.get(url, params=dict(parse_qsl(qs)))
-    if resp.status_code != 200:
-        return backend_error(resp)
-    return flask.jsonify(resp.json()), resp.status_code
+    # no pay_redirect but article not paid, this should never happen!
+    err_abort(500, message="Internal error, invariant failed", json=pay_status)
diff --git a/talerblog/blog/content.py b/talerblog/blog/content.py
index 3202a50..4aeb865 100644
--- a/talerblog/blog/content.py
+++ b/talerblog/blog/content.py
@@ -26,9 +26,9 @@ from bs4 import BeautifulSoup
 from pkg_resources import resource_stream, resource_filename
 
 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()
 
 
@@ -52,6 +52,7 @@ def add_from_html(resource_name, teaser_paragraph=0, 
title=None):
     """
     res = resource_stream("talerblog", resource_name)
     soup = BeautifulSoup(res, 'html.parser')
+    res.close()
     if title is None:
         title_el = soup.find("h1", attrs={"class":["chapter", "unnumbered"]})
         if title_el is None:
@@ -64,7 +65,9 @@ def add_from_html(resource_name, teaser_paragraph=0, 
title=None):
 
     teaser = soup.find("p", attrs={"id":["teaser"]})
     if teaser is None:
-        teaser = str(paragraphs[teaser_paragraph])
+        teaser = paragraphs[teaser_paragraph].get_text()
+    else:
+        teaser = teaser.get_text()
     re_proc = re.compile("^/essay/[^/]+/data/[^/]+$")
     imgs = soup.find_all("img")
     extra_files = []
diff --git a/talerblog/blog/templates/article_frame.html 
b/talerblog/blog/templates/article_frame.html
index 50d58e2..015c9d8 100644
--- a/talerblog/blog/templates/article_frame.html
+++ b/talerblog/blog/templates/article_frame.html
@@ -1,8 +1,8 @@
 {% extends "templates/base.html" %}
 {% block main %}
 {% include "articles/" + article_file %}
-  <form action="/refund" method="POST">
+  <form action="{{ url_for('refund', order_id=order_id) }}" method="POST">
     <input type="text" name="article_name" value={{ article_name}} hidden>
-    <input type="submit" value="Ask refund!">
+    <input type="submit" value="Request refund">
   </form>
 {% endblock main %}
diff --git a/talerblog/blog/templates/backoffice.html 
b/talerblog/blog/templates/backoffice.html
deleted file mode 100644
index b87495b..0000000
--- a/talerblog/blog/templates/backoffice.html
+++ /dev/null
@@ -1,80 +0,0 @@
-{% extends "templates/base.html" %}
-{% block main %}
-  <h1>Backoffice</h1>
-  <p>This page simulates a backoffice facility.  Through it,
-  the user can see the money flow from Taler transactions to
-  wire transfers and viceversa.</p>
-  <div>
-    <form action="">
-      <input type="text"
-             placeholder="Order ID"
-             class="order"></input><br>
-      <input type="text"
-             placeholder="WTID"
-             class="transfer"
-             style="visibility: hidden;"></input><br>
-      <input type="text"
-             placeholder="Exchange URI"
-             class="transfer"
-             style="visibility: hidden;"></input><br>
-      <input type="radio"
-             name="track-type"
-             value="order"
-             onclick="cherry_pick_form_order(this.parentNode)"
-             checked>Track order id</input><br>
-      <input type="radio"
-             name="track-type"
-             value="wtid"
-             onclick="cherry_pick_form_transfer(this.parentNode)">Track wire 
transfer</input><br>
-      <input type="button"
-             value="submit"
-             onclick='track_cherry_pick(this.parentNode)'></input>
-    </form>
-  </div>
-  <div id="history-container">
-    <table id="history" width="50%" style="visibility: hidden;">
-      <col width="40">
-      <col width="40">
-      <col width="40">
-      <tbody>
-        <tr class="no-records">
-          <th colspan="3">No records found</th>
-        </tr>
-        <tr class="headers" style="visibility: hidden">
-          <th class="order-id">Order ID</th>
-          <th class="amount">Amount</th>
-          <th class="date">Date</th>
-        </tr>
-      </tbody>
-    </table>
-    <br/>
-    <div class="loader"></div>
-  </div>
-
-  <div id="popup1" class="overlay">
-    <div class="popup">
-      <h2>
-        <a class="close" href="#" onclick="close_popup();">&times;</a>
-      </h2>
-      <div class="track-content">
-        <table>
-          <tbody>
-            <tr>
-              <th class="wtid">WTID</th>
-              <th class="amount">Amount</th>
-              <th class="date">Date</th>
-            </tr>
-          </tbody>
-        </table>
-      </div>
-    </div>
-  </div>
-{% endblock main %}
-
-{% block styles %}
-  <link rel="stylesheet" type="text/css" href="{{ 
url("/static/backoffice.css") }}">
-{% endblock styles %}
-
-{% block scripts %}
-  <script src="{{ url('/static/backoffice.js') }}" 
type="application/javascript"></script>
-{% endblock scripts %}
diff --git a/talerblog/blog/templates/base.html 
b/talerblog/blog/templates/base.html
index 755e72f..98ce2e3 100644
--- a/talerblog/blog/templates/base.html
+++ b/talerblog/blog/templates/base.html
@@ -17,21 +17,45 @@
 
 <html data-taler-nojs="true">
 <head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Taler Donation Demo</title>
-  <link rel="stylesheet" type="text/css" href="{{ 
url('/static/web-common/pure.css') }}" />
-  <link rel="stylesheet" type="text/css" href="{{ 
url('/static/web-common/demo.css') }}" />
-  <link rel="stylesheet" type="text/css" href="{{ 
url('/static/web-common/taler-fallback.css') }}" id="taler-presence-stylesheet" 
/>
-  <script src="{{ url("/static/web-common/taler-wallet-lib.js") }}" 
type="application/javascript"></script>
-  <script src="{{ url("/static/web-common/lang.js") }}" 
type="application/javascript"></script>
+  <link rel="stylesheet" type="text/css" href="{{ url_for('static', 
filename='web-common/pure.css') }}" />
+  <link rel="stylesheet" type="text/css" href="{{ url_for('static', 
filename='web-common/demo.css') }}" />
+  <link rel="stylesheet" type="text/css" href="{{ url_for('static', 
filename='web-common/taler-fallback.css') }}" id="taler-presence-stylesheet" />
+  <style>
+    .warn {
+      background-color: #aa393977;
+      padding: 1em;
+    }
+    .notice {
+      border-radius: 1em;
+      background: #0333;
+      border-left: 0.3em solid #033;
+      padding-left: 1em;
+      padding-top: 0.5em;
+      padding-bottom: 0.5em;
+      margin-top: 2em;
+      margin-bottom: 2em;
+    }
+    #main a:link, #main a:visited, #main a:hover, #main a:active {
+        color: black;
+    }
+  </style>
   {% block styles %}{% endblock %}
   {% block scripts %}{% endblock %}
 </head>
 
 <body>
-  <div class="demobar">
+  <div class="demobar" style="display: flex; flex-direction: column;">
     <h1><span class="tt adorn-brackets">Taler Demo</span></h1>
     <h1><span class="it"><a href="{{ env('TALER_ENV_URL_MERCHANT_BLOG') 
}}">Shop</a></span></h1>
-    <p>This is the Essay shop, you can buy articles using an imaginary 
currency (for now).</p>
+    <p>On this page you can buy articles using an imaginary currency (for now).
+       The articles are chapters from Richard Stallman's book &quot;Free 
Software, Free Society&quot;,
+       which is also
+      <a 
href="http://shop.fsf.org/product/free-software-free-society-2/";>published by 
the FSF</a>
+      and available gratis at <a href="http://www.gnu.org/";>gnu.org</a>.
+    </p>
     <ul>
       <li><a href="{{ env('TALER_ENV_URL_INTRO', '#') }}">Introduction</a></li>
       <li><a href="{{ env('TALER_ENV_URL_BANK', '#') }}">Bank</a></li>
@@ -40,25 +64,15 @@
       <li><a href="{{ env('TALER_ENV_URL_MERCHANT_SURVEY', '#') 
}}">Tipping/Survey</a></li>
     </ul>
     <p>You can learn more about Taler on our main <a 
href="https://taler.net";>website</a>.</p>
+    <div style="flex-grow:1"></div>
+    <p>Copyright &copy; 2014&mdash;2018 Inria</p>
   </div>
 
   <section id="main" class="content">
-    <a href="{{ url("/") }}">
-      <div id="logo">
-        <svg height="100" width="100">
-          <circle cx="50" cy="50" r="40" stroke="darkcyan" stroke-width="6" 
fill="white" />
-          <text x="19" y="82" font-family="Verdana" font-size="90" 
fill="darkcyan">S</text>
-        </svg>
-      </div>
-    </a>
     {% block main %}
       This is the main content of the page.
     {% endblock %}
     <hr />
-    <div class="copyright">
-      <p>Copyright &copy; 2014&mdash;2017 INRIA</p>
-      <a href="/javascript" data-jslicense="1" 
class="jslicenseinfo">JavaScript license information</a>
-    </div>
   </section>
 </body>
 </html>
diff --git a/talerblog/blog/templates/error.html 
b/talerblog/blog/templates/error.html
new file mode 100644
index 0000000..0d4bd02
--- /dev/null
+++ b/talerblog/blog/templates/error.html
@@ -0,0 +1,22 @@
+{% extends "templates/base.html" %}
+{% block main %}
+  <h1>An Error Occurred</h1>
+
+  <p>{{ message }}</p>
+
+  {% if status_code %}
+  <p>The backend returned status code {{ status_code }}.</p>
+  {% endif %}
+
+  {% if json %}
+  <p>Backend Response:</p>
+  <pre>{{ json }}</pre>
+  {% endif %}
+
+  {% if stack %}
+  <p>Stack trace:</p>
+  <pre>
+    {{ stack }}
+  </pre>
+  {% endif %}
+{% endblock main %}
diff --git a/talerblog/blog/templates/index.html 
b/talerblog/blog/templates/index.html
index 5d767ee..40cd7ea 100644
--- a/talerblog/blog/templates/index.html
+++ b/talerblog/blog/templates/index.html
@@ -1,94 +1,46 @@
 {% extends "templates/base.html" %}
 {% block main %}
-      <h1>About</h1>
-      <p>This &quot;blog&quot; simulates how a website selling articles using
-         Taler should work.
-         We illustrate the use of Taler using articles from
-         Richard Stallman's book &quot;Free Software, Free Society&quot;,
-         which is also
-        <a 
href="http://shop.fsf.org/product/free-software-free-society-2/";>published by 
the FSF</a>
-        and available gratis at <a href="http://www.gnu.org/";>gnu.org</a>.
+    <h1>Essay Shop: Free Software, Free Society</h1>
+    <div style="font-size: smaller;">
+      <p>This is the second edition of <cite>Free Software, Free Society: 
Selected Essays of Richard M. Stallman.</cite><br>
+      Free Software Foundation<br>
+      51 Franklin Street, Fifth Floor<br>
+      Boston, MA 02110-1335
+      <br>
+      Copyright &copy; 2002, 2010 Free Software Foundation, Inc.
       </p>
 
-    <h2 class="taler-installed-hide">Taler wallet required for payment</h2>
+      <p>Verbatim copying and distribution of this entire book are permitted
+      worldwide, without royalty, in any medium, provided this notice is
+      preserved. Permission is granted to copy and distribute translations
+      of this book from the original English into another language provided
+      the translation has been approved by the Free Software Foundation and
+      the copyright notice and this permission notice are preserved on all
+      copies.
+      </p>
+      <p>ISBN 978-0-9831592-0-9</p>
+    </div>
 
-    <p class="taler-installed-hide">
-    This site requires a Taler wallet to pay for articles.
-    Please visit our <a href="/landing">landing page</a>
-    to install a wallet (and to withdraw digital coins).
-    </p>
-    <h2>Back-office interface</h2>
-    <p>
-    If you are a merchant and want to track your deposits, try the
-    <a href="/backoffice">back-office</a>!
-    </p>
+    <h2>Chapters</h2>
+    <div class="taler-installed-hide warn">
+      <p>This site requires a Taler wallet to pay for articles.
+      You can install it from our <a href="https://taler.net/en/wallet.html"; 
rel="noopener noreferrer" target="_blank">installation page</a>.
+    </div>
 
-    <h2>Free Software, Free Society</h2>
+    <div class="taler-installed-show">
+      Click on an individual chapter to to purchase it.  You can
+      get free, virtual money to buy articles on this page at the <a href="{{ 
env('TALER_ENV_URL_BANK', '#') }}">bank</a>.
+    </div>
 
-    <p>This is the second edition of <cite>Free Software, Free Society: 
Selected Essays of Richard M. Stallman.</cite><br>
-Free Software Foundation<br>
-51 Franklin Street, Fifth Floor<br>
-Boston, MA 02110-1335
-<br>
-Copyright &copy; 2002, 2010 Free Software Foundation, Inc.
-</p><blockquote><p>Verbatim copying and distribution of this entire book are 
permitted
-worldwide, without royalty, in any medium, provided this notice is
-preserved. Permission is granted to copy and distribute translations
-of this book from the original English into another language provided
-the translation has been approved by the Free Software Foundation and
-the copyright notice and this permission notice are preserved on all
-copies.
-</p></blockquote>
-<p>ISBN 978-0-9831592-0-9
-<br>
-<br>
-</p>
-<p>
-<em>Richard Stallman is the prophet of the free software movement.
-He understood the dangers of software patents years ago. Now that
-this has become a crucial issue in the world, buy this book and read
-what he said.</em><br> &mdash;<strong>Tim Berners-Lee,</strong> inventor of 
the World
-Wide Web
-<br>
-<br>
-<em>Richard Stallman is the philosopher king of software. He
-single-handedly ignited what has become a world-wide movement to
-create software that is Free, with a capital F. He has toiled for
-years at a project that many once considered a fool&rsquo;s errand, and now
-that is widely seen as &ldquo;inevitable.&rdquo;</em><br> 
&mdash;<strong>Simon&nbsp;L.
-Garfinkel,</strong> computer science author and columnist
-<br>
-<br>
-<em>By his hugely successful efforts to establish the idea of &ldquo;Free
-Software,&rdquo; Stallman has made a massive contribution to the human
-condition. His contribution combines elements that have technical,
-social, political, and economic consequences.</em><br> &mdash;<strong>Gerald 
Jay
-Sussman,</strong> Matsushita Professor of Electrical Engineering, MIT
-<br>
-<br>
-<em>RMS is the leading philosopher of software. You may dislike
-some of his attitudes, but you cannot avoid his ideas. This slim
-volume will make those ideas readily accessible to those who are
-confused by the buzzwords of rampant commercialism. This book needs
-to be widely circulated and widely read.</em><br> &mdash;<strong>Peter 
Salus,</strong>
-computer science writer, book reviewer, and UNIX historian
-<br>
-<br>
-<em>Richard is the leading force of the free software movement.
-This book is very important to spread the key concepts of free
-software world-wide, so everyone can understand it. Free software
-gives people freedom to use their creativity.</em><br> &mdash;<strong>Masayuki
-Ida,</strong> professor, Graduate School of International Management, Aoyama
-Gakuin University
-</p>
-    <h2>Chapters</h2>
-    <!-- TODO: show this section ONLY if Taler wallet is present! -->
-    <ul style="list-style-type:none">
+    <div>
       {% for article in articles %}
-      <h3><a href="{{ url_for("article", name=article.slug) 
}}">{{article.title}}</a></h3>
-      {{ article.teaser|safe }}
+      <div class="notice">
+      <h3 class="taler-installed-show"><a href="{{ url_for('article', 
article_name=article.slug) }}">{{article.title}}</a></h3>
+      <h3 class="taler-installed-hide">{{article.title}} <span 
style="font-size: small;">(install wallet to buy/read)</span></h3>
+      <p>{{ article.teaser|safe }} <a class="taler-installed-show" href="{{ 
url_for('article', article_name=article.slug) }}">(Read more...)</a></p>
+      </div>
       {% else %}
       <em>(No articles available)</em>
       {% endfor %}
-    </ul>
+    </div>
 {% endblock main %}
diff --git a/talerblog/blog/templates/purchase.html 
b/talerblog/blog/templates/purchase.html
deleted file mode 100644
index d1baf38..0000000
--- a/talerblog/blog/templates/purchase.html
+++ /dev/null
@@ -1,41 +0,0 @@
-{% extends "templates/base.html" %}
-
-{% block main %}
-<meta name="hc" value="{{ hc }}">
-<meta name="pay_url" value="{{ pay_url|safe }}">
-<meta name="offering_url" value="{{ offering_url|safe }}">
-<meta name="contract_url" value="{{ contract_url|safe }}">
-<meta name="no_contract" value="{{ no_contract }}">
-<div id="ccfakeform" class="fade">
-  <p>
-  Oops, it looks like you don't have a Taler wallet installed.  Why don't you 
enter
-  all your credit card details before reading the article? <em>You can also
-  use GNU Taler to complete the purchase at any time.</em>
-  </p>
-
-  <form>
-    First name<br> <input type="text"></input><br>
-    Family name<br> <input type="text"></input><br>
-    Age<br> <input type="text"></input><br>
-    Nationality<br> <input type="text"></input><br>
-    Gender<br> <input type="radio" name"gender">Male</input>
-    CC number<br> <input type="text"></input><br>
-    <input type="radio" name="gender">Female</input><br>
-  </form>
-  <form method="get" action="/cc-payment/{{ article_name }}">
-    <input type="submit"></input>
-  </form>
-</div>
-
-<div id="talerwait">
-  <em>Processing payment with GNU Taler, please wait <span 
id="action-indicator"></span></em>
-</div>
-{% endblock main %}
-
-{% block body_prelude %}
-  <script src="{{ url('/static/body-prelude.js') }}" 
type="application/javascript"></script>
-{% endblock body_prelude %}
-
-{% block scripts %}
-  <script src="{{ url('/static/purchase.js') }}" 
type="application/javascript"></script>
-{% endblock scripts %}
diff --git a/talerblog/helpers.py b/talerblog/helpers.py
deleted file mode 100644
index 614e463..0000000
--- a/talerblog/helpers.py
+++ /dev/null
@@ -1,101 +0,0 @@
-#  This file is part of TALER
-#  (C) 2016 INRIA
-#
-#  TALER is free software; you can redistribute it and/or modify it under the
-#  terms of the GNU Affero General Public License as published by the Free 
Software
-#  Foundation; either version 3, or (at your option) any later version.
-#
-#  TALER is distributed in the hope that it will be useful, but WITHOUT ANY
-#  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
FOR
-#  A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
-#
-#  You should have received a copy of the GNU General Public License along with
-#  TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
-#
-#  @author Florian Dold
-#  @author Marcello Stanisci
-
-from urllib.parse import urljoin, urlencode
-import logging
-import json
-import flask
-from .talerconfig import TalerConfig
-
-LOGGER = logging.getLogger(__name__)
-
-TC = TalerConfig.from_env()
-BACKEND_URL = TC["frontends"]["backend"].value_string(required=True)
-NDIGITS = TC["frontends"]["NDIGITS"].value_int()
-CURRENCY = TC["taler"]["CURRENCY"].value_string()
-
-FRACTION_BASE = 1e8
-
-if not NDIGITS:
-    NDIGITS = 2
-
-class MissingParameterException(Exception):
-    def __init__(self, param):
-        self.param = param
-        super().__init__()
-
-def amount_to_float(amount):
-    return amount['value'] + (float(amount['fraction']) / float(FRACTION_BASE))
-
-
-def amount_from_float(floatx):
-    value = int(floatx)
-    fraction = int((floatx - value) * FRACTION_BASE)
-    return dict(currency=CURRENCY, value=value, fraction=fraction)
-
-
-def join_urlparts(*parts):
-    ret = ""
-    part = 0
-    while part < len(parts):
-        buf = parts[part]
-        part += 1
-        if ret.endswith("/"):
-            buf = buf.lstrip("/")
-        elif ret and not buf.startswith("/"):
-            buf = "/" + buf
-        ret += buf
-    return ret
-
-
-def make_url(page, *query_params):
-    """
-    Return a URL to a page in the current Flask application with the given
-    query parameters (sequence of key/value pairs).
-    """
-    query = urlencode(query_params)
-    if page.startswith("/"):
-        root = flask.request.url_root
-        page = page.lstrip("/")
-    else:
-        root = flask.request.base_url
-    url = urljoin(root, "%s?%s" % (page, query))
-    # urlencode is overly eager with quoting, the wallet right now
-    # needs some characters unquoted.
-    return url.replace("%24", "$").replace("%7B", "{").replace("%7D", "}")
-
-
-def expect_parameter(name, alt=None):
-    value = flask.request.args.get(name, None)
-    if value is None and alt is None:
-        LOGGER.error("Missing parameter '%s' from '%s'." % (name, 
flask.request.args))
-        raise MissingParameterException(name)
-    return value if value else alt
-
-
-def get_query_string():
-    return flask.request.query_string
-
-def backend_error(requests_response):
-    LOGGER.error("Backend error: status code: "
-                 + str(requests_response.status_code))
-    try:
-        return flask.jsonify(requests_response.json()), 
requests_response.status_code
-    except json.decoder.JSONDecodeError:
-        LOGGER.error("Backend error (NO JSON returned): status code: "
-                     + str(requests_response.status_code))
-        return flask.jsonify(dict(error="Backend died, no JSON got from it")), 
502
diff --git a/talerblog/tests.conf b/talerblog/tests.conf
index 05d3310..0d62bd7 100644
--- a/talerblog/tests.conf
+++ b/talerblog/tests.conf
@@ -3,6 +3,7 @@ CURRENCY = TESTKUDOS
 
 [frontends]
 BACKEND = http://backend.test.taler.net/
+BACKEND_APIKEY = sandbox
 
 [blog]
 INSTANCE = FSF
diff --git a/talerblog/tests.py b/talerblog/tests.py
index 050d852..8ad3556 100644
--- a/talerblog/tests.py
+++ b/talerblog/tests.py
@@ -1,49 +1,54 @@
 #!/usr/bin/env python3
 
 import unittest
+import logging
 from mock import patch, MagicMock
-from talerblog.blog import blog
-from talerblog.talerconfig import TalerConfig
+from .blog import blog
+from .talerconfig import TalerConfig
 
 TC = TalerConfig.from_env()
 CURRENCY = TC["taler"]["currency"].value_string(required=True)
+LOGGER = logging.getLogger(__name__)
 
 class BlogTestCase(unittest.TestCase):
     def setUp(self):
         blog.app.testing = True
         self.app = blog.app.test_client()
+        self.instance = TC["blog"]["instance"].value_string(required=True)
 
+    @patch("requests.get")
     @patch("requests.post")
-    def test_proposal_creation(self, mocked_post):
+    @patch("flask.session")
+    @unittest.skip("API changed")
+    def test_refund(self, mocked_session, mocked_post, mocked_get):
+
+        # Test GET
+        ret_get = MagicMock()
+        ret_get.status_code = 200
+        ret_get.json.return_value = {"error": "mocckky error"}
+        mocked_get.return_value = ret_get
+        response = self.app.get("/refund?order_id=99")
+        mocked_get.assert_called_with(
+            "http://backend.test.taler.net/refund";,
+            params={"order_id": "99", "instance": self.instance})
+
+        # Test POST
+        mocked_session.get.return_value = {"mocckky": 99}
         ret_post = MagicMock()
         ret_post.status_code = 200
-        ret_post.json.return_value = {}
         mocked_post.return_value = ret_post
-        self.app.get("/generate-contract?nonce=55&article_name=Check_Me")
+        response = self.app.post("/refund", data={"article_name": "mocckky"})
         mocked_post.assert_called_with(
-            "http://backend.test.taler.net/proposal";,
+            "http://backend.test.taler.net/refund";,
             json={
-                "order": {
-                    "summary": "Check Me",
-                    "nonce": "55",
-                    "amount": blog.ARTICLE_AMOUNT,
-                    "max_fee": {
-                        "value": 1,
-                        "fraction": 0,
-                        "currency": CURRENCY},
-                    "products": [{
-                        "description": "Essay: Check Me",
-                        "quantity": 1,
-                        "product_id": 0,
-                        "price": blog.ARTICLE_AMOUNT}],
-                    "fulfillment_url": "http://localhost/essay/Check_Me";,
-                    "pay_url": "http://localhost/pay";,
-                    "merchant": {
-                        "instance": 
TC["blog"]["instance"].value_string(required=True),
-                        "address": "nowhere",
-                        "name": "Kudos Inc.",
-                        "jurisdiction": "none"},
-                    "extra": {"article_name": "Check_Me"}}})
+                "order_id": 99,
+                "refund": {
+                    "value": 0,
+                    "fraction": 50000000,
+                    "currency": CURRENCY},
+                "reason": "Demo reimbursement",
+                "instance": self.instance})
+
 
 if __name__ == "__main__":
     unittest.main()

-- 
To stop receiving notification emails like this one, please contact
address@hidden



reply via email to

[Prev in Thread] Current Thread [Next in Thread]