From 4816fa981f672f302a009af66a77c444c8e4b660 Mon Sep 17 00:00:00 2001 From: MS Date: Wed, 22 Jul 2020 16:33:54 +0200 Subject: Installing Survey --- talermerchantdemos/survey/survey.py | 254 ++++++++++++++++++++++++++++++++++++ 1 file changed, 254 insertions(+) create mode 100644 talermerchantdemos/survey/survey.py (limited to 'talermerchantdemos/survey/survey.py') 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 +# +# @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 + ) -- cgit v1.2.3