[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();">×</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 "Free
Software, Free Society",
+ 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 © 2014—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 © 2014—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 "blog" simulates how a website selling articles using
- Taler should work.
- We illustrate the use of Taler using articles from
- Richard Stallman's book "Free Software, Free Society",
- 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 © 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 © 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> —<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’s errand, and now
-that is widely seen as “inevitable.”</em><br>
—<strong>Simon L.
-Garfinkel,</strong> computer science author and columnist
-<br>
-<br>
-<em>By his hugely successful efforts to establish the idea of “Free
-Software,” Stallman has made a massive contribution to the human
-condition. His contribution combines elements that have technical,
-social, political, and economic consequences.</em><br> —<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> —<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> —<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
[Prev in Thread] |
Current Thread |
[Next in Thread] |
- [GNUnet-SVN] [taler-blog] branch stable updated (8cbacb5 -> 0e9953d),
gnunet <=