diff --git a/server/src/app/__init__.py b/server/src/app/__init__.py new file mode 100644 index 0000000..b9357d3 --- /dev/null +++ b/server/src/app/__init__.py @@ -0,0 +1,5 @@ +from flask import Flask + +app = Flask(__name__) + +from app import views \ No newline at end of file diff --git a/server/src/app/database.py b/server/src/app/database.py new file mode 100644 index 0000000..e2e51c3 --- /dev/null +++ b/server/src/app/database.py @@ -0,0 +1,141 @@ +from os.path import dirname +from psycopg2 import connect as psyconn, ProgrammingError, errors +from yaml import safe_load +import logging + + +DIR = dirname(__file__) + "/" + +LOGGER = logging.getLogger(__name__) +LOGGER.setLevel(logging.DEBUG) +logFormatter = logging.Formatter( + "%(asctime)s [%(threadName)s] [%(levelname)s] %(message)s") + +fileHandler = logging.FileHandler(DIR + "../../logs/database.log") +fileHandler.setFormatter(logFormatter) +fileHandler.setLevel(logging.INFO) +LOGGER.addHandler(fileHandler) + +consoleHandler = logging.StreamHandler() +consoleHandler.setLevel(logging.DEBUG) +LOGGER.addHandler(consoleHandler) + + +class Database: + + def __init__(self, **kwargs): + pass + + def connect(self, **kwargs): + with open(DIR + 'config.yaml', 'r') as file: + data = safe_load(file)['database'] + LOGGER.debug('Merging passed arguments with default arguments.') + for key, value in data.items(): + if key not in kwargs or not kwargs[key]: + kwargs[key] = value + LOGGER.info('Connecting to Database.') + self.conn = psyconn(host=kwargs["host"], port=kwargs["port"], dbname=kwargs["database"], + user=kwargs["user"], password=kwargs["password"]) + self.conn.autocommit = True + + def test_connection(self): + if not hasattr(self, "conn"): + LOGGER.info("Connection was not set, setting...") + self.connect() + else: + try: + with self.conn.cursor() as cursor: + try: + cursor.execute("SELECT 1;") + cursor.fetchall() + except: + LOGGER.warn( + 'Connection seem to timed out, reconnecting...') + self.connect() + except: + LOGGER.warn('Connection seem to timed out, reconnecting...') + self.connect() + + def connectionpersistence(func): + def wrapper(*args, **kwargs): + self = args[0] + self.test_connection() + return func(*args, **kwargs) + return wrapper + + @connectionpersistence + def get_user(self, **kwargs): + result = () + if 'login' in kwargs: + query = "SELECT login FROM users WHERE login = %(login)s;" + with self.conn.cursor() as cursor: + cursor.execute(query, kwargs) + try: + result = cursor.fetchall() + except ProgrammingError as e: + LOGGER.exception("") + except Exception as e: + LOGGER.exception("") + return result + + @connectionpersistence + def get_report(self, **kwargs) -> list: + query = "SELECT u.name, bp.date, i.name, bp.amount, bp.price FROM bought_with_prices bp INNER JOIN items i ON bp.item = i.id INNER JOIN users u ON bp.user = u.login" + if kwargs: + query += " WHERE " + tempquery = [] + if "user" in kwargs and kwargs['user']: + tempquery.append(f"bp.user = '{kwargs['user']}'") + if "year" in kwargs and kwargs['year']: + tempstring = "bp.date BETWEEN " + if "month" in kwargs and kwargs['month']: + tempstring += f"'{kwargs['year']}-{kwargs['month']}-01' AND " + tempstring += f"'{kwargs['year']+1}-01-01'" if kwargs['month'] == 12 else f"'{kwargs['year']}-{kwargs['month']+1}-01'" + else: + tempstring += f"'{kwargs['year']}-01-01' AND '{kwargs['year']+1}-01-01'" + tempstring += "::date - INTERVAL '1' DAY" + tempquery.append(tempstring) + query += " AND ".join(tempquery) + query += " ORDER BY u.name, bp.date, i.name ASC;" + LOGGER.debug(f"Executing query: {query}") + result = [] + with self.conn.cursor() as cursor: + cursor.execute(query) + try: + result = cursor.fetchall() + except ProgrammingError as e: + LOGGER.exception("") + except Exception as e: + LOGGER.exception("") + return result + + @connectionpersistence + def insert_bought_items(self, user: str, items: dict, date: str = None): + temp = ['"user", item, amount', "%(user)s, %(item)s, %(amount)s", + "bought.user = %(user)s AND bought.item = %(item)s AND bought.date = " + ("%(date)s" if date else "NOW()::date")] + if date: + temp[0] += ", date" + temp[1] += ", %(date)s" + values = [{'user': user, 'item': int(key), 'amount': value, 'date': date} for key, value in items.items()] + else: + values = [{'user': user, 'item': int(key), 'amount': value} for key, value in items.items()] + query = f"INSERT INTO bought({temp[0]}) VALUES({temp[1]}) ON CONFLICT ON CONSTRAINT bought_user_item_date DO UPDATE SET amount = bought.amount + %(amount)s WHERE {temp[2]};" + with self.conn.cursor() as cursor: + failed = {} + for value in values: + try: + cursor.execute(query, value) + except errors.ForeignKeyViolation as e: + if failed: + failed['items'][value['item']] = value['amount'] + else: + failed = {'user': user, 'items': {value['item']: value['amount']}} + if date: + failed['date'] = date + LOGGER.exception("") + except Exception as e: + LOGGER.exception("") + return failed + + def __delete__(self): + self.conn.close() diff --git a/server/src/app/views.py b/server/src/app/views.py new file mode 100644 index 0000000..2db08b6 --- /dev/null +++ b/server/src/app/views.py @@ -0,0 +1,92 @@ +from app import app +from app.database import Database +from flask import abort, request +from flask.json import jsonify +from os import makedirs +from os.path import dirname, exists +import logging + + +DIR = dirname(__file__) + "/" + +if not exists(DIR + "../logs"): + makedirs(DIR + "../logs") + +LOGGER = logging.getLogger(__name__) +LOGGER.setLevel(logging.DEBUG) +logFormatter = logging.Formatter( + "%(asctime)s [%(threadName)s] [%(levelname)s] %(message)s") + +fileHandler = logging.FileHandler(DIR + "../../logs/server.log") +fileHandler.setFormatter(logFormatter) +fileHandler.setLevel(logging.INFO) +LOGGER.addHandler(fileHandler) + +consoleHandler = logging.StreamHandler() +consoleHandler.setLevel(logging.DEBUG) +LOGGER.addHandler(consoleHandler) + +DATABASE = Database() + +APPNAME = "scan2kasse" + + +@app.route('/') +def index(): + return "

Hello, World!", 200 + + +@app.route(f'/{APPNAME}/login') +def login(): + if not request.json or 'login' not in request.json: + abort(400) + if not DATABASE.get_user(login = request.json['login']): + abort(403) + return jsonify({}), 200 + + +@app.route(f'/{APPNAME}/insert', methods=['POST']) +def insert(): + match request.json: + case {'user': user, 'items': items, 'date': date}: + failed = DATABASE.insert_bought_items(user, items, date) + if failed: + return jsonify(failed), 400 + return jsonify({'inserted': True}), 201 + case {'user': user, 'items': items}: + failed = DATABASE.insert_bought_items(user, items) + if failed: + return jsonify(failed), 400 + return jsonify({'inserted': True}), 201 + case _: + abort(400) + +@app.route(f'/{APPNAME}/', methods=['GET']) +@app.route(f'/{APPNAME}//', methods=['GET']) +@app.route(f'/{APPNAME}//', methods=['GET']) +@app.route(f'/{APPNAME}///', methods=['GET']) +def get_monthly_report_from_user(user: str = None, year: int = None, month: int = None): + if month and (month > 12 or month < 1): + abort(400) + LOGGER.info("Getting results.") + results = DATABASE.get_report(user=user, year=year, month=month) + LOGGER.debug(f"Results received: {results}") + if results: + result_dict = group_results(results) + else: + result_dict = {} + return jsonify(result_dict) + + +def group_results(results: tuple) -> dict: + result_dict = {} + LOGGER.debug("Grouping...") + for result in results: + if result[0] not in result_dict: + result_dict[result[0]] = {} + if str(result[1]) not in result_dict[result[0]]: + result_dict[result[0]][str(result[1])] = {} + result_dict[result[0]][str(result[1])][result[2]] = ( + result[3], result[4]) + LOGGER.debug("Grouped.") + return result_dict \ No newline at end of file diff --git a/server/src/pagebuilder.py b/server/src/pagebuilder.py new file mode 100644 index 0000000..e69de29 diff --git a/server/src/run.py b/server/src/run.py new file mode 100644 index 0000000..858949c --- /dev/null +++ b/server/src/run.py @@ -0,0 +1,6 @@ +from app import app +from gevent.pywsgi import WSGIServer + +if __name__ == '__main__': + http_server = WSGIServer(('', 5000), app) + http_server.serve_forever()