summaryrefslogtreecommitdiff
path: root/talermerchantdemos/survey/survey.py
diff options
context:
space:
mode:
Diffstat (limited to 'talermerchantdemos/survey/survey.py')
-rw-r--r--talermerchantdemos/survey/survey.py254
1 files changed, 254 insertions, 0 deletions
diff --git a/talermerchantdemos/survey/survey.py b/talermerchantdemos/survey/survey.py
new file mode 100644
index 0000000..7a71ded
--- /dev/null
+++ b/talermerchantdemos/survey/survey.py
@@ -0,0 +1,254 @@
+##
+# This file is part of GNU TALER.
+# Copyright (C) 2017 Taler Systems SA
+#
+# TALER is free software; you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free Software
+# Foundation; either version 2.1, 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 Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License along with
+# GNU TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+#
+# @author Marcello Stanisci
+# @brief Minimal Website to tip users who fill the survey.
+
+import os
+import re
+import datetime
+import base64
+import logging
+import json
+from urllib.parse import urljoin
+import flask
+import requests
+import traceback
+import qrcode
+import qrcode.image.svg
+import lxml.etree
+from taler.util.talerconfig import TalerConfig
+
+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')
+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)
+app.config.from_object(__name__)
+LOGGER = logging.getLogger(__name__)
+
+
+##
+# Helper function that returns a HTTP response with
+# a prettified version of a backend error response.
+#
+# @param requests_response the error response coming from
+# the merchant backend.
+# @return a flask-native response object.
+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
+
+
+##
+# Make the environment available into templates.
+#
+# @return the environment-reading function.
+@app.context_processor
+def utility_processor():
+ def env(name, default=None):
+ return os.environ.get(name, default)
+
+ def prettydate(talerdate):
+ parsed_time = re.search(r"/Date\(([0-9]+)\)/", talerdate)
+ if not parsed_time:
+ return "malformed date given"
+ parsed_time = int(parsed_time.group(1))
+ timestamp = datetime.datetime.fromtimestamp(parsed_time)
+ # returns the YYYY-MM-DD date format.
+ return timestamp.strftime("%Y-%b-%d")
+
+ return dict(env=env, prettydate=prettydate)
+
+
+##
+# 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))
+
+
+##
+# 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:
+ 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
+
+
+##
+# 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=None):
+ headers = {"Authorization": "ApiKey " + APIKEY}
+ if params is None:
+ params = dict()
+ 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
+
+
+##
+# 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 /favicon.ico requests.
+#
+# @return the favicon.ico file.
+@app.route("/favicon.ico")
+def favicon():
+ print("will look into: " + os.path.join(app.root_path, 'static'))
+ return flask.send_from_directory(
+ os.path.join(app.root_path, 'static'),
+ "favicon.ico",
+ mimetype="image/vnd.microsoft.ico"
+ )
+
+
+##
+# Give information about the tip reserve status.
+#
+# @return the backend response to a /tip-query request,
+# in a prettified format.
+@app.route("/survey-stats", methods=["GET"])
+def survey_stats():
+ stats = backend_get("tip-query")
+ return flask.render_template("templates/survey_stats.html", stats=stats)
+
+
+def get_qrcode_svg(data):
+ factory = qrcode.image.svg.SvgImage
+ img = qrcode.make(data, image_factory=factory)
+ return lxml.etree.tostring(img.get_image()).decode("utf-8")
+
+
+##
+# Tell the backend to 'authorize' a tip; this means that
+# the backend will allocate a certain amount to be later
+# picked up by the wallet.
+#
+# @return the URL where to redirect the browser, in order
+# for the wallet to pick the tip up, or a error page
+# otherwise.
+@app.route("/submit-survey", methods=["POST"])
+def submit_survey():
+ tip_spec = dict(
+ amount=CURRENCY + ":1.0",
+ next_url=os.environ.get("TALER_ENV_URL_INTRO", "https://taler.net/"),
+ justification="Payment methods survey"
+ )
+ backend_resp = backend_post("tip-authorize", tip_spec)
+
+ taler_tip_uri = backend_resp.get("taler_tip_uri")
+ if taler_tip_uri:
+ qrcode_svg = get_qrcode_svg(taler_tip_uri)
+ content = flask.render_template(
+ "templates/show_tip.html",
+ qrcode_svg=qrcode_svg,
+ taler_tip_uri=taler_tip_uri,
+ )
+ headers = {"Taler": taler_tip_uri}
+ return flask.Response(content, status=402, headers=headers)
+
+ err_abort(
+ 500, message="Tipping failed, unexpected backend response", json=resp
+ )
+
+
+##
+# Serve the main index page.
+#
+# @return response object of the index page.
+@app.route("/", methods=["GET"])
+def index():
+ return flask.render_template(
+ "templates/index.html", merchant_currency=CURRENCY
+ )