From 37d723c8d6a98b7fbd8a1f5e9497d7f3e3ae840f Mon Sep 17 00:00:00 2001 From: Max Value Date: Fri, 25 Jul 2025 16:30:30 +0100 Subject: [PATCH] Reorganised and compartmentalized --- .gitignore | 3 + LICENCE | 1 + Makefile | 8 + data.db | Bin 12288 -> 12288 bytes main.py | 71 ++++++ setup.py | 2 +- {media => static}/products/0.png | Bin {media => static}/products/1.png | Bin {media => static}/products/10.png | Bin {media => static}/products/11.png | Bin {media => static}/products/12.png | Bin {media => static}/products/13.png | Bin {media => static}/products/14.png | Bin {media => static}/products/15.png | Bin {media => static}/products/16.png | Bin {media => static}/products/17.png | Bin {media => static}/products/18.png | Bin {media => static}/products/19.png | Bin {media => static}/products/2.png | Bin {media => static}/products/20.png | Bin {media => static}/products/3.png | Bin {media => static}/products/4.png | Bin {media => static}/products/5.png | Bin {media => static}/products/6.png | Bin {media => static}/products/7.png | Bin {media => static}/products/8.png | Bin {media => static}/products/9.png | Bin teleshopping.py | 377 ------------------------------ teleshopping/__init__.py | 4 + teleshopping/admin.py | 74 ++++++ teleshopping/api/__init__.py | 3 + teleshopping/api/buy.py | 60 +++++ teleshopping/api/data.py | 43 ++++ teleshopping/api/utils.py | 65 ++++++ teleshopping/pages.py | 55 +++++ teleshopping/utils.py | 66 ++++++ 36 files changed, 454 insertions(+), 378 deletions(-) create mode 100644 LICENCE create mode 100644 Makefile create mode 100755 main.py rename {media => static}/products/0.png (100%) rename {media => static}/products/1.png (100%) rename {media => static}/products/10.png (100%) rename {media => static}/products/11.png (100%) rename {media => static}/products/12.png (100%) rename {media => static}/products/13.png (100%) rename {media => static}/products/14.png (100%) rename {media => static}/products/15.png (100%) rename {media => static}/products/16.png (100%) rename {media => static}/products/17.png (100%) rename {media => static}/products/18.png (100%) rename {media => static}/products/19.png (100%) rename {media => static}/products/2.png (100%) rename {media => static}/products/20.png (100%) rename {media => static}/products/3.png (100%) rename {media => static}/products/4.png (100%) rename {media => static}/products/5.png (100%) rename {media => static}/products/6.png (100%) rename {media => static}/products/7.png (100%) rename {media => static}/products/8.png (100%) rename {media => static}/products/9.png (100%) delete mode 100755 teleshopping.py create mode 100644 teleshopping/__init__.py create mode 100644 teleshopping/admin.py create mode 100644 teleshopping/api/__init__.py create mode 100644 teleshopping/api/buy.py create mode 100644 teleshopping/api/data.py create mode 100644 teleshopping/api/utils.py create mode 100644 teleshopping/pages.py create mode 100644 teleshopping/utils.py diff --git a/.gitignore b/.gitignore index 6620d0b..3012fc5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ *.db *.zip docs/* +teleshopping.egg-info/ +teleshopping/__pycache__/ +teleshopping/api/__pycache__/ diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..a316ec0 --- /dev/null +++ b/LICENCE @@ -0,0 +1 @@ +Not sure yet! Closed source diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..bad50a6 --- /dev/null +++ b/Makefile @@ -0,0 +1,8 @@ +init: .venv/bin/pip + .venv/bin/pip install -r requirements.txt + .venv/bin/pip install -e . + +.venv/bin/pip: + python3 -m venv .venv + +.PHONY: init diff --git a/data.db b/data.db index 48a31ba2c6fa2f5d008cf829a93b406bbf174044..f4195e982a21c1f9818a69e3b7bcbe539efceb37 100755 GIT binary patch delta 55 zcmZojXh@hKE$Gg`z`zW|FtBB!jxkVBuS<{@D8$6W$iTzMzi6|dz(nrNj6DC90sP|$ AxBvhE delta 55 zcmZojXh@hKEy%;bz`zW|Fi")(teleshopping.extensions) + +# authenticated pages +docs = auth.login_required(teleshopping.docs) +app.route("/docs")(docs) +paper_docs = auth.login_required(teleshopping.paper_docs) +app.route("/docs/")(paper_docs) + +# admin pages +admin = auth.login_required(teleshopping.admin) +app.route("/admin")(admin) +admin_page = auth.login_required(teleshopping.admin_page) +app.route("/admin/", methods=['GET', 'POST'])(admin_page) + +# the data api +app.route("/api")(teleshopping.data) +app.route("/api/items")(teleshopping.items) +app.route("/api/clock")(teleshopping.clock) + +# the buy api +app.route("/api/buy")(teleshopping.buy) +# authenticated +buy_data = auth.login_required(teleshopping.recent_data) +app.route("/api/buy/data")(buy_data) +all_data = auth.login_required(teleshopping.all_data) +app.route("/api/buy/all")(all_data) +buy_clear = auth.login_required(teleshopping.clear) +app.route("/api/buy/clear")(buy_clear) + +# utility api +app.route("/api/img/")(teleshopping.img) +# authenticated +docs_generate = auth.login_required(teleshopping.docs_generate) +app.route("/api/generate")(docs_generate) + +# utils +app.before_request(teleshopping.check_database) + +if __name__ == "__main__": + app.run(host='127.0.0.1', port=8000, debug=True) diff --git a/setup.py b/setup.py index 29b2926..0ab3247 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,5 @@ from distutils.core import setup setup(name='teleshopping', - version='1.0', + version='3.0', py_modules=['teleshopping'], ) diff --git a/media/products/0.png b/static/products/0.png similarity index 100% rename from media/products/0.png rename to static/products/0.png diff --git a/media/products/1.png b/static/products/1.png similarity index 100% rename from media/products/1.png rename to static/products/1.png diff --git a/media/products/10.png b/static/products/10.png similarity index 100% rename from media/products/10.png rename to static/products/10.png diff --git a/media/products/11.png b/static/products/11.png similarity index 100% rename from media/products/11.png rename to static/products/11.png diff --git a/media/products/12.png b/static/products/12.png similarity index 100% rename from media/products/12.png rename to static/products/12.png diff --git a/media/products/13.png b/static/products/13.png similarity index 100% rename from media/products/13.png rename to static/products/13.png diff --git a/media/products/14.png b/static/products/14.png similarity index 100% rename from media/products/14.png rename to static/products/14.png diff --git a/media/products/15.png b/static/products/15.png similarity index 100% rename from media/products/15.png rename to static/products/15.png diff --git a/media/products/16.png b/static/products/16.png similarity index 100% rename from media/products/16.png rename to static/products/16.png diff --git a/media/products/17.png b/static/products/17.png similarity index 100% rename from media/products/17.png rename to static/products/17.png diff --git a/media/products/18.png b/static/products/18.png similarity index 100% rename from media/products/18.png rename to static/products/18.png diff --git a/media/products/19.png b/static/products/19.png similarity index 100% rename from media/products/19.png rename to static/products/19.png diff --git a/media/products/2.png b/static/products/2.png similarity index 100% rename from media/products/2.png rename to static/products/2.png diff --git a/media/products/20.png b/static/products/20.png similarity index 100% rename from media/products/20.png rename to static/products/20.png diff --git a/media/products/3.png b/static/products/3.png similarity index 100% rename from media/products/3.png rename to static/products/3.png diff --git a/media/products/4.png b/static/products/4.png similarity index 100% rename from media/products/4.png rename to static/products/4.png diff --git a/media/products/5.png b/static/products/5.png similarity index 100% rename from media/products/5.png rename to static/products/5.png diff --git a/media/products/6.png b/static/products/6.png similarity index 100% rename from media/products/6.png rename to static/products/6.png diff --git a/media/products/7.png b/static/products/7.png similarity index 100% rename from media/products/7.png rename to static/products/7.png diff --git a/media/products/8.png b/static/products/8.png similarity index 100% rename from media/products/8.png rename to static/products/8.png diff --git a/media/products/9.png b/static/products/9.png similarity index 100% rename from media/products/9.png rename to static/products/9.png diff --git a/teleshopping.py b/teleshopping.py deleted file mode 100755 index 07f87c2..0000000 --- a/teleshopping.py +++ /dev/null @@ -1,377 +0,0 @@ -#!.venv/bin/python - -from flask import Flask, Response, request, render_template, jsonify, send_from_directory -from werkzeug.security import generate_password_hash, check_password_hash -from jinja2 import Environment, FileSystemLoader -from flask_httpauth import HTTPBasicAuth -from datetime import datetime, timezone -from os import path, environ, system -from math import radians, cos, sin -from markupsafe import escape -from ast import literal_eval -from flask_cors import CORS -import sqlite3 -import json -import time - -INCREMENT = 18 - -app = Flask(__name__) -CORS(app) - -auth = HTTPBasicAuth() -try: - app.root_path = environ["SHOPPING_PATH"] -except KeyError: - pass - -# get the secrets ready -with open(path.join(app.root_path, "..", "secrets.json"), "r", encoding="utf-8") as f: - users = json.loads(f.read()) -users = {k: generate_password_hash(v) for (k, v) in users.items()} - -# get the static data ready -with open(path.join(app.root_path, "static", "static.json"), "r", encoding="utf-8") as f: - static_data = json.loads(f.read()) - -with open(path.join(app.root_path, "static", "info.json"), "r", encoding="utf-8") as f: - static_info = json.loads(f.read()) - - - - -# make the password verifier function -@auth.verify_password -def verify_password(username, password): - if username in users and \ - check_password_hash(users.get(username), password): - return username - -# database helper function -def database(field: str | list): - query = f"SELECT {','.join(field) if type(field) == list else field} FROM state WHERE id=1;" - - with sqlite3.connect(path.join(app.root_path, "data.db")) as connection: - cursor = connection.cursor() - result = cursor.execute(query).fetchone() - - column_names = [description[0] for description in cursor.description] - - return dict(zip(column_names, result)) # combine column names with data into dict - - - - -@app.route("/") -def gfx_main(): - return Response(render_template("gfx.html"), mimetype="text/html") - -@app.route("/autocue") -def gfx_page(): - request.method = "internal" - data = api() - return Response(render_template("autocue.html", data=data, item=static_data["items"][data["item_id"]]), mimetype="text/html") - -@app.route("/display") -def display_page(): - return Response(render_template("display.html"), mimetype="text/html") - -@app.route("/sounds") -def sounds_page(): - return Response(render_template("sounds.html"), mimetype="text/html") - -@app.route("/chart") -def chart_page(): - return Response(render_template("chart.html"), mimetype="text/html") - - - - -@app.route("/extensions/") -def interface_page(filename): - return send_from_directory("extensions", filename) - - - - -@app.route("/docs") -@auth.login_required -def web_docs(): - return Response(render_template("docs.html", data=static_data, info=static_info, book=False), mimetype="text/html") - -@app.route("/docs/") -@auth.login_required -def paper_docs(filename): - return send_from_directory("docs", filename, as_attachment=True) - -@app.route("/admin") -@auth.login_required -def admin_main(): - return Response(render_template("admin.html", mimetype="text/html")) - -@app.route("/admin/", methods=['GET', 'POST']) -@auth.login_required -def admin_page(page): - if page in ["text", "clock", "timer", "price"]: - if request.method == "POST": # if posting with data update the database then return the new admin page - - post_data = request.form.to_dict() - aggrigate_data = {} - - # perform all data clean up for special cases - aggrigate_data = {} - for key in post_data: - # if a key begins with the word "aggrigate": aggrigate it with all other keys with the same body into a list - if key[:9] == "aggrigate": - new_key = key[10:-5] - if new_key in aggrigate_data: - aggrigate_data[new_key].append(int(post_data[key])) - else: - aggrigate_data.update({new_key: [int(post_data[key])]}) - - # if the key is a timer, make the time fixed to the epoch instead of relative (& account for offset) - if key[:3] == "end": - aggrigate_data.update( - {key: int(datetime.now(timezone.utc).timestamp()) + int(post_data[key])} - ) - - else: aggrigate_data.update({key: post_data[key]}) - - with sqlite3.connect(path.join(app.root_path, "data.db")) as connection: - cursor = connection.cursor() - result = cursor.execute("SELECT * FROM pragma_table_info('state');").fetchall()[1:] - - types = {"TEXT":str, "INTEGER":int, "REAL":float} - type_note = {c[1]:types[c[2]] for c in result} - - valid_data = {k: type_note[k].__call__(aggrigate_data[k]) for k in aggrigate_data if k in type_note} - - data_list = ', '.join([f"{k} = '{escape(v)}'" if type(v) == str else f"{k} = {v}" for (k,v) in valid_data.items()]) - query = f""" - UPDATE state SET - {data_list} - WHERE id = 1; - """ - - print(query) - - cursor.execute(query) - connection.commit() - - request.method = "internal" # set method to internal so that the api call returns only the python object - if page == "clock": - coords = [{ - "i":i, - "x":45*cos(radians(i-90))+50, - "y":45*sin(radians(i-90))+50 - } for i in list(range(0, 360, INCREMENT))] - return Response(render_template("clock.html", data=api(), positions=coords), mimetype="text/html") - - if page == "text": - return Response(render_template("text.html", data=api(), text=static_data['text']), mimetype="text/html") - - if page == "timer": - return Response(render_template("timer.html", data=api()), mimetype="text/html") - - if page == "price": - return Response(render_template("price.html", data=api(), items=static_data['items']), mimetype="text/html") - - else: - return "", 404 - - - - -@app.route("/api") -def api(): - if request.method in ["GET", "internal"]: - data = database("*") - - focus_extra = {} - for key, value in data.items(): - if key[:4] == "bool": data[key] = bool(value) # if the key starts with "bool" make the data a bool - elif key[:4] == "list": data[key] = literal_eval(value) # if the key starts with "list" make the data a literal list - elif key[:5] == "focus": # if the key starts with "focus" make the data into a bool with an additional dict entry for "showing" bool - data[key] = bool((value-1)+abs(value-1)) # if 2 then True, if 1,0 then False - focus_extra[f"bool_{key[6:]}"] = bool(value) # if 1,2 then True, if 0 then False - data |= focus_extra - - if request.method == "internal": return data - return jsonify(data) - else: - return "", 404 - -@app.route("/api/items") -def api_items(): - if request.method == "GET": - return jsonify(static_data) - - else: - return "", 404 - -@app.route("/api/clock") -def api_clock(): - if request.method == "GET": - keys = ["current_position", "movement_speed", "movement_function"] - return jsonify(database(keys)) - - else: - return "", 404 - -@app.route("/api/img/") -def product_image(item_id): - return send_from_directory(path.join("media", "products"), f"{item_id}.png") - -@app.route("/api/generate") -@auth.login_required -def generate_docs(): - # setup the jinja enviroment for use in latex - latex_enviroment = Environment( - loader=FileSystemLoader(path.join(app.root_path, "templates")), - block_start_string = "|%", - block_end_string = "%|", - variable_start_string = "|~", - variable_end_string = "~|", - comment_start_string = "|#", - comment_end_string = "#|", - trim_blocks = True, - lstrip_blocks = True - ) - - # render each latex document - docs_path = path.join(app.root_path, "docs") - for document in ["call-sheet.tex", "manifest-unsafe.tex", "manifest-safe.tex", "gfx-text.tex"]: - template = latex_enviroment.get_template(document) - - with open(path.join(docs_path, document), "w", encoding="utf-8") as f: - f.write(template.render(data=static_data, info=static_info)) - - system(f"pdflatex -interaction='nonstopmode' -output-directory='{docs_path}' '{path.join(docs_path, document)}' > /dev/zero") - system(f"pdflatex -interaction='nonstopmode' -output-directory='{docs_path}' '{path.join(docs_path, document)}' > /dev/zero") - - print(f" - Generated {document}") - - with open(path.join(docs_path, "documentation.html"), "w", encoding="utf-8") as f: - f.write(render_template("docs.html", data=static_data, info=static_info, book=True)) - - floorplan_path = path.join(app.root_path, 'static', 'floorplan.png') - cameras_path = path.join(app.root_path, 'static', 'cameras.png') - system(f"cp '{floorplan_path}' '{docs_path}'") - system(f"cp '{cameras_path}' '{docs_path}'") - - system(f"ebook-convert '{path.join(docs_path, 'documentation.html')}' '{path.join(docs_path, 'documentation.mobi')}' --title='XMDV Teleshopping Documentation' --authors='William Greenwood' --comments='Valid for {static_info['shoot']['date']}' --language=en --change-justification=left --cover='{path.join(app.root_path, 'static', 'cover.jpg')}'") - - system(f"ebook-convert '{path.join(docs_path, 'documentation.html')}' '{path.join(docs_path, 'documentation.epub')}' --title='XMDV Teleshopping Documentation' --authors='William Greenwood' --comments='Valid for {static_info['shoot']['date']}' --language=en --change-justification=left --cover='{path.join(app.root_path, 'static', 'cover.jpg')}'") - - return "", 200 - - - - -@app.route("/api/buy") -def buy(): - with sqlite3.connect(path.join(app.root_path, "data.db")) as connection: - cursor = connection.cursor() - cursor.execute(f""" - INSERT INTO orders VALUES - (NULL, {round(time.time())}) - """) - connection.commit() - - return "", 200 - -@app.route("/api/buy/data") -@auth.login_required -def buy_data(): - data = [] - with sqlite3.connect(path.join(app.root_path, "data.db")) as connection: - cursor = connection.cursor() - - current = round(time.time() / 5) * 5 - - for start in range(current - 600, current, 5): - result = cursor.execute(f""" - SELECT COUNT(*) - FROM orders - WHERE timestamp BETWEEN {start - 1} and {start + 5}; - """) - result = result.fetchone()[0] - data.append(result) - - return jsonify(data) - -@app.route("/api/buy/all") -@auth.login_required -def buy_all(): - data = [] - with sqlite3.connect(path.join(app.root_path, "data.db")) as connection: - cursor = connection.cursor() - - for order in cursor.execute(f""" - SELECT * - FROM orders; - """): - data.append(order[1]) - - return jsonify(data) - -@app.route("/api/buy/clear") -@auth.login_required -def buy_clear(): - with open(path.join(app.root_path, "schema2"), "r", encoding="utf-8") as f: - schema = f.read() - - with sqlite3.connect(path.join(app.root_path, "data.db")) as connection: - cursor = connection.cursor() - cursor.execute("DROP TABLE orders;") - cursor.execute(schema) - connection.commit() - - return "", 200 - - - - -# if this is the first incoming request do a sanity check on the db -# this might happen a few times because of threading, but it doesnt slow requests that much -first_request = True -@app.before_request -def check_database(): - global first_request - if first_request: - with open(path.join(app.root_path, "schema1"), "r", encoding="utf-8") as f: - schema, load = f.read().split("\n\n") - - with sqlite3.connect(path.join(app.root_path, "data.db")) as connection: - cursor = connection.cursor() - try: - cursor.execute(load) - except sqlite3.IntegrityError: - print("Database is setup correctly") - pass - except sqlite3.OperationalError: - print("Table missing or corrupt...") - try: cursor.execute("DROP TABLE state;") # catch if there is no table "state" - except: pass - cursor.execute(schema) - connection.commit() - - with open(path.join(app.root_path, "schema2"), "r", encoding="utf-8") as f: - schema = f.read() - - with sqlite3.connect(path.join(app.root_path, "data.db")) as connection: - cursor = connection.cursor() - try: cursor.execute("DROP TABLE orders;") - except: pass - cursor.execute(schema) - connection.commit() - - # generate_docs() # This might be a bad idea - - first_request = False - - - - -if __name__ == "__main__": - app.run(host='127.0.0.1', port=8000, debug=True) diff --git a/teleshopping/__init__.py b/teleshopping/__init__.py new file mode 100644 index 0000000..513f393 --- /dev/null +++ b/teleshopping/__init__.py @@ -0,0 +1,4 @@ +from .api import * +from .admin import * +from .pages import * +from .utils import * diff --git a/teleshopping/admin.py b/teleshopping/admin.py new file mode 100644 index 0000000..9602368 --- /dev/null +++ b/teleshopping/admin.py @@ -0,0 +1,74 @@ +from math import radians, cos, sin + +INCREMENT = 18 + +def admin(): + return Response(render_template("admin.html", mimetype="text/html")) + +def admin_page(page): + if page in ["text", "clock", "timer", "price"]: + if request.method == "POST": # if posting with data update the database then return the new admin page + + post_data = request.form.to_dict() + aggrigate_data = {} + + # perform all data clean up for special cases + aggrigate_data = {} + for key in post_data: + # if a key begins with the word "aggrigate": aggrigate it with all other keys with the same body into a list + if key[:9] == "aggrigate": + new_key = key[10:-5] + if new_key in aggrigate_data: + aggrigate_data[new_key].append(int(post_data[key])) + else: + aggrigate_data.update({new_key: [int(post_data[key])]}) + + # if the key is a timer, make the time fixed to the epoch instead of relative (& account for offset) + if key[:3] == "end": + aggrigate_data.update( + {key: int(datetime.now(timezone.utc).timestamp()) + int(post_data[key])} + ) + + else: aggrigate_data.update({key: post_data[key]}) + + with sqlite3.connect(path.join(app.root_path, "data.db")) as connection: + cursor = connection.cursor() + result = cursor.execute("SELECT * FROM pragma_table_info('state');").fetchall()[1:] + + types = {"TEXT":str, "INTEGER":int, "REAL":float} + type_note = {c[1]:types[c[2]] for c in result} + + valid_data = {k: type_note[k].__call__(aggrigate_data[k]) for k in aggrigate_data if k in type_note} + + data_list = ', '.join([f"{k} = '{escape(v)}'" if type(v) == str else f"{k} = {v}" for (k,v) in valid_data.items()]) + query = f""" + UPDATE state SET + {data_list} + WHERE id = 1; + """ + + print(query) + + cursor.execute(query) + connection.commit() + + request.method = "internal" # set method to internal so that the api call returns only the python object + if page == "clock": + coords = [{ + "i":i, + "x":45*cos(radians(i-90))+50, + "y":45*sin(radians(i-90))+50 + } for i in list(range(0, 360, INCREMENT))] + return Response(render_template("clock.html", data=api(), positions=coords), mimetype="text/html") + + if page == "text": + return Response(render_template("text.html", data=api(), text=static_data['text']), mimetype="text/html") + + if page == "timer": + return Response(render_template("timer.html", data=api()), mimetype="text/html") + + if page == "price": + return Response(render_template("price.html", data=api(), items=static_data['items']), mimetype="text/html") + + else: + return "", 404 diff --git a/teleshopping/api/__init__.py b/teleshopping/api/__init__.py new file mode 100644 index 0000000..2ad4f57 --- /dev/null +++ b/teleshopping/api/__init__.py @@ -0,0 +1,3 @@ +from .buy import * +from .data import * +from .utils import * diff --git a/teleshopping/api/buy.py b/teleshopping/api/buy.py new file mode 100644 index 0000000..057ba5b --- /dev/null +++ b/teleshopping/api/buy.py @@ -0,0 +1,60 @@ +from flask import jsonify +from os import environ +import sqlite3 +import time + +ROOT = environ["XMDV_PATH"] + +def buy(): + with sqlite3.connect(f"{ROOT}/data.db") as connection: + cursor = connection.cursor() + cursor.execute(f""" + INSERT INTO orders VALUES + (NULL, {round(time.time())}) + """) + connection.commit() + + return "", 200 + +def recent_data(): + data = [] + with sqlite3.connect(f"{ROOT}/data.db") as connection: + cursor = connection.cursor() + + current = round(time.time() / 5) * 5 + + for start in range(current - 600, current, 5): + result = cursor.execute(f""" + SELECT COUNT(*) + FROM orders + WHERE timestamp BETWEEN {start - 1} and {start + 5}; + """) + result = result.fetchone()[0] + data.append(result) + + return jsonify(data) + +def all_data(): + data = [] + with sqlite3.connect(f"{ROOT}/data.db") as connection: + cursor = connection.cursor() + + for order in cursor.execute(f""" + SELECT * + FROM orders; + """): + data.append(order[1]) + + return jsonify(data) + +def clear(): + with open(f"{ROOT}/schema2", "r", encoding="utf-8") as f: + schema = f.read() + + with sqlite3.connect(f"{ROOT}/data.db") as connection: + cursor = connection.cursor() + cursor.execute("DROP TABLE orders;") + cursor.execute(schema) + connection.commit() + + return "", 200 diff --git a/teleshopping/api/data.py b/teleshopping/api/data.py new file mode 100644 index 0000000..d96949a --- /dev/null +++ b/teleshopping/api/data.py @@ -0,0 +1,43 @@ +from flask import request, jsonify +from ..utils import database +from ast import literal_eval +from os import environ +import json + +ROOT = environ["XMDV_PATH"] + +with open(f"{ROOT}/static/static.json", "r", encoding="utf-8") as f: + static_data = json.loads(f.read()) + +def data(): + if request.method in ["GET", "internal"]: + data = database("*") + + focus_extra = {} + for key, value in data.items(): + if key[:4] == "bool": data[key] = bool(value) # if the key starts with "bool" make the data a bool + elif key[:4] == "list": data[key] = literal_eval(value) # if the key starts with "list" make the data a literal list + elif key[:5] == "focus": # if the key starts with "focus" make the data into a bool with an additional dict entry for "showing" bool + data[key] = bool((value-1)+abs(value-1)) # if 2 then True, if 1,0 then False + focus_extra[f"bool_{key[6:]}"] = bool(value) # if 1,2 then True, if 0 then False + data |= focus_extra + + if request.method == "internal": return data + return jsonify(data) + else: + return "", 404 + +def items(): + if request.method == "GET": + return jsonify(static_data) + + else: + return "", 404 + +def clock(): + if request.method == "GET": + keys = ["current_position", "movement_speed", "movement_function"] + return jsonify(database(keys)) + + else: + return "", 404 diff --git a/teleshopping/api/utils.py b/teleshopping/api/utils.py new file mode 100644 index 0000000..b29cf1b --- /dev/null +++ b/teleshopping/api/utils.py @@ -0,0 +1,65 @@ +from flask import send_from_directory, render_template +from jinja2 import Environment, FileSystemLoader +from os import environ, system +import json + +ROOT = environ["XMDV_PATH"] + +# get static info and data +with open(f"{ROOT}/static/static.json", "r", encoding="utf-8") as f: + static_data = json.loads(f.read()) + +with open(f"{ROOT}/static/info.json", "r", encoding="utf-8") as f: + static_info = json.loads(f.read()) + +def img(item_id): + return send_from_directory("static/products", f"{item_id}.png") + +def docs_generate(): + # setup the jinja enviroment for use in latex + latex_enviroment = Environment( + loader=FileSystemLoader(f"{ROOT}/templates"), + block_start_string = "|%", + block_end_string = "%|", + variable_start_string = "|~", + variable_end_string = "~|", + comment_start_string = "|#", + comment_end_string = "#|", + trim_blocks = True, + lstrip_blocks = True + ) + + # render each latex document + docs_path = f"{ROOT}/docs" + for document in ["call-sheet.tex", "manifest-unsafe.tex", "manifest-safe.tex", "gfx-text.tex"]: + template = latex_enviroment.get_template(document) + + with open(f"{docs_path}/{document}", "w", encoding="utf-8") as f: + f.write(template.render(data=static_data, info=static_info)) + + system(f"pdflatex -interaction='nonstopmode' -output-directory='{docs_path}' '{docs_path}/{document}' > /dev/zero") + system(f"pdflatex -interaction='nonstopmode' -output-directory='{docs_path}' '{docs_path}/{document}' > /dev/zero") + + print(f" - Generated {document}") + + with open(f"{docs_path}/documentation.html", "w", encoding="utf-8") as f: + f.write(render_template("docs.html", data=static_data, info=static_info, book=True)) + + floorplan_path = f"{ROOT}/static/floorplan.png" + cameras_path = f"{ROOT}/static/cameras.png" + system(f"cp '{floorplan_path}' '{docs_path}'") + system(f"cp '{cameras_path}' '{docs_path}'") + + for book_type in ["epub", "mobi"]: + system(f""" + ebook-convert '{docs_path}/documentation.html' \\ + '{docs_path}/documentation.{book_type}' \\ + --title='XMDV Teleshopping Documentation' \\ + --authors='William Greenwood' \\ + --comments='Valid for {static_info['shoot']['date']}' \\ + --language=en \\ + --change-justification=left \\ + --cover='{ROOT}/static/cover.jpg' + """) + + return "", 200 diff --git a/teleshopping/pages.py b/teleshopping/pages.py new file mode 100644 index 0000000..cc56076 --- /dev/null +++ b/teleshopping/pages.py @@ -0,0 +1,55 @@ +from flask import Flask, Response, request, render_template, send_from_directory +from .api.data import data as api +from os import environ +import json + +ROOT = environ["XMDV_PATH"] + +with open(f"{ROOT}/static/static.json", "r", encoding="utf-8") as f: + static_data = json.loads(f.read()) + +with open(f"{ROOT}/static/info.json", "r", encoding="utf-8") as f: + static_info = json.loads(f.read()) + +def overlay(): + return Response( + render_template("gfx.html"), + mimetype="text/html" + ) + +def autocue(): + request.method = "internal" + data = api() + return Response( + render_template( + "autocue.html", + data=data, + item=static_data["items"][data["item_id"]] + ), + mimetype="text/html" + ) + +def hud(): + return Response(render_template("display.html"), mimetype="text/html") + +def sounds(): + return Response(render_template("sounds.html"), mimetype="text/html") + +def chart(): + return Response(render_template("chart.html"), mimetype="text/html") + +def extensions(filename): + return send_from_directory("extensions", filename) + +def docs(): + return Response( + render_template( + "docs.html", + data=static_data, + info=static_info, + book=False), + mimetype="text/html" + ) + +def paper_docs(filename): + return send_from_directory("docs", filename, as_attachment=True) diff --git a/teleshopping/utils.py b/teleshopping/utils.py new file mode 100644 index 0000000..b2ee6d9 --- /dev/null +++ b/teleshopping/utils.py @@ -0,0 +1,66 @@ +from werkzeug.security import generate_password_hash, check_password_hash +from os import path, environ +import sqlite3 +import json + +ROOT = environ["XMDV_PATH"] + +# if this is the first incoming request do a sanity check on the db +# this might happen a few times because of threading, but it doesnt slow requests that much +first_request = True +def check_database(): + global first_request + if first_request: + with open(f"{ROOT}/schema1", "r", encoding="utf-8") as f: + schema, load = f.read().split("\n\n") + + with sqlite3.connect(f"{ROOT}/data.db") as connection: + cursor = connection.cursor() + try: + cursor.execute(load) + except sqlite3.IntegrityError: + print("Database is setup correctly") + pass + except sqlite3.OperationalError: + print("Table missing or corrupt...") + try: cursor.execute("DROP TABLE state;") # catch if there is no table "state" + except: pass + cursor.execute(schema) + connection.commit() + + with open(f"{ROOT}/schema2", "r", encoding="utf-8") as f: + schema = f.read() + + with sqlite3.connect(f"{ROOT}/data.db") as connection: + cursor = connection.cursor() + try: cursor.execute("DROP TABLE orders;") + except: pass + cursor.execute(schema) + connection.commit() + + # generate_docs() # This might be a bad idea + + first_request = False + +# helper function to get a dict from the database as kv pairs +def database(field: str | list): + query = f"SELECT {','.join(field) if type(field) == list else field} FROM state WHERE id=1;" + + with sqlite3.connect(f"{ROOT}/data.db") as connection: + cursor = connection.cursor() + result = cursor.execute(query).fetchone() + + column_names = [description[0] for description in cursor.description] + + return dict(zip(column_names, result)) # combine column names with data into dict + +# import password data +with open(f"{ROOT}/../secrets.json", "r", encoding="utf-8") as f: + users = json.loads(f.read()) +users = {k: generate_password_hash(v) for (k, v) in users.items()} + +# make the password verifier function +def verify_password(username, password): + if username in users and \ + check_password_hash(users.get(username), password): + return username -- 2.39.2