diff --git a/MyDataclasses.py b/MyDataclasses.py index 54d85a2..d196664 100644 --- a/MyDataclasses.py +++ b/MyDataclasses.py @@ -331,6 +331,8 @@ class ShoppingListPayload: self.type ) + +# DONE @dataclass class SitePayload: site_name: str @@ -357,6 +359,7 @@ class SitePayload: self.default_primary_location ) +#DONE @dataclass class RolePayload: role_name:str @@ -408,7 +411,8 @@ class SiteManager: "shopping_lists", "shopping_list_items", "item_locations", - "conversions" + "conversions", + "sku_prefix" ] self.drop_order = [ "item_info", @@ -431,5 +435,6 @@ class SiteManager: "shopping_list_items", "shopping_lists", "item_locations", - "conversions" + "conversions", + "sku_prefix" ] \ No newline at end of file diff --git a/__pycache__/MyDataclasses.cpython-312.pyc b/__pycache__/MyDataclasses.cpython-312.pyc index 1c402a2..34e4cd5 100644 Binary files a/__pycache__/MyDataclasses.cpython-312.pyc and b/__pycache__/MyDataclasses.cpython-312.pyc differ diff --git a/__pycache__/admin_api.cpython-312.pyc b/__pycache__/admin_api.cpython-312.pyc new file mode 100644 index 0000000..051956d Binary files /dev/null and b/__pycache__/admin_api.cpython-312.pyc differ diff --git a/__pycache__/api_admin.cpython-312.pyc b/__pycache__/api_admin.cpython-312.pyc new file mode 100644 index 0000000..c3af222 Binary files /dev/null and b/__pycache__/api_admin.cpython-312.pyc differ diff --git a/__pycache__/database_admin.cpython-312.pyc b/__pycache__/database_admin.cpython-312.pyc new file mode 100644 index 0000000..0b0b227 Binary files /dev/null and b/__pycache__/database_admin.cpython-312.pyc differ diff --git a/__pycache__/main.cpython-312.pyc b/__pycache__/main.cpython-312.pyc index 6d0c743..534163c 100644 Binary files a/__pycache__/main.cpython-312.pyc and b/__pycache__/main.cpython-312.pyc differ diff --git a/__pycache__/postsqldb.cpython-312.pyc b/__pycache__/postsqldb.cpython-312.pyc index c94c452..94f16ee 100644 Binary files a/__pycache__/postsqldb.cpython-312.pyc and b/__pycache__/postsqldb.cpython-312.pyc differ diff --git a/__pycache__/process.cpython-312.pyc b/__pycache__/process.cpython-312.pyc index 7a232e0..b46f177 100644 Binary files a/__pycache__/process.cpython-312.pyc and b/__pycache__/process.cpython-312.pyc differ diff --git a/__pycache__/user_api.cpython-312.pyc b/__pycache__/user_api.cpython-312.pyc index 4fbfaad..6e65e3d 100644 Binary files a/__pycache__/user_api.cpython-312.pyc and b/__pycache__/user_api.cpython-312.pyc differ diff --git a/__pycache__/workshop_api.cpython-312.pyc b/__pycache__/workshop_api.cpython-312.pyc index 6bcea1e..431faa6 100644 Binary files a/__pycache__/workshop_api.cpython-312.pyc and b/__pycache__/workshop_api.cpython-312.pyc differ diff --git a/admin.py b/admin.py deleted file mode 100644 index dda93b3..0000000 --- a/admin.py +++ /dev/null @@ -1,122 +0,0 @@ -from flask import Blueprint, request, render_template, redirect, session, url_for, send_file, jsonify, Response -import psycopg2, math, json, datetime, main, copy, requests -from config import config, sites_config -from main import unfoldCostLayers, get_sites, get_roles, create_site_secondary, getUser -from manage import create - -admin = Blueprint('admin_api', __name__) - -@admin.route("/admin/getSites") -def getSites(): - sites = get_sites(session.get('user')[13]) - return jsonify(sites=sites) - -@admin.route("/getRoles") -def getRoles(): - sites_roles = {} - sites = get_sites(session.get('user')[13]) - for site in sites: - site_roles = get_roles(site_id=site[0]) - sites_roles[site[1]] = site_roles - return jsonify(sites=sites_roles) - -@admin.route("/admin/getUsers", methods=["POST"]) -def getUsers(): - if request.method == "POST": - page = request.get_json()['page'] - limit = request.get_json()['limit'] - offset = (page - 1) * limit - - database_config = config() - with psycopg2.connect(**database_config) as conn: - try: - with conn.cursor() as cur: - sql = f"SELECT * FROM logins LIMIT %s OFFSET %s;" - cur.execute(sql, (limit, offset)) - users = cur.fetchall() - cur.execute("SELECT COUNT(*) FROM main_items;") - count = cur.fetchone()[0] - return jsonify(users=users, endpage=math.ceil(count/limit)) - except (Exception, psycopg2.DatabaseError) as error: - print(error) - conn.rollback() - return jsonify(message="FAILED") - return jsonify(message="FAILED") - - -@admin.route("/admin/editRole/") -def getRole(id): - database_config = config() - with psycopg2.connect(**database_config) as conn: - try: - with conn.cursor() as cur: - sql = f"SELECT * FROM roles LEFT JOIN sites ON sites.id = roles.site_id WHERE roles.id = %s;" - cur.execute(sql, (id, )) - role = cur.fetchone() - return render_template("admin/role.html", role=role, proto={'referrer': request.referrer}) - except (Exception, psycopg2.DatabaseError) as error: - print(error) - conn.rollback() - return jsonify(message="FAILED") - -@admin.route("/addRole", methods=["POST"]) -def addRole(): - if request.method == "POST": - role_name = request.get_json()['role_name'] - role_description = request.get_json()['role_description'] - site_id = request.get_json()['site_id'] - - - sql = f"INSERT INTO roles (role_name, role_description, site_id) VALUES (%s, %s, %s);" - print(role_name, role_description, site_id) - - - database_config = config() - with psycopg2.connect(**database_config) as conn: - try: - with conn.cursor() as cur: - data = (role_name, role_description, site_id) - cur.execute(sql, data) - except (Exception, psycopg2.DatabaseError) as error: - print(error) - conn.rollback() - return jsonify(message="FAILED") - - return jsonify(message="SUCCESS") - - return jsonify(message="FAILED") - -@admin.route("/deleteRole", methods=["POST"]) -def deleteRole(): - if request.method == "POST": - role_id = request.get_json()['role_id'] - database_config = config() - with psycopg2.connect(**database_config) as conn: - try: - with conn.cursor() as cur: - sql = f"DELETE FROM roles WHERE roles.id = %s;" - cur.execute(sql, (role_id, )) - return jsonify(message="Role Deleted!") - except (Exception, psycopg2.DatabaseError) as error: - print(error) - conn.rollback() - return jsonify(message=error) - return jsonify(message="FAILED") - -@admin.route("/addSite", methods=["POST"]) -async def addSite(): - if request.method == "POST": - site_name = request.get_json()['site_name'] - site_description = request.get_json()['site_description'] - default_zone = request.get_json()["default_zone"] - default_location = request.get_json()['default_location'] - username = session.get('user')[1] - user_id = session.get('user')[0] - - create(site_name, username, default_zone, default_location) - result = await create_site_secondary(site_name, user_id, default_zone, default_location, default_location, site_description) - - if result: - return jsonify(message="Success!") - - return jsonify(message="Failed!") \ No newline at end of file diff --git a/api_admin.py b/api_admin.py new file mode 100644 index 0000000..cbad775 --- /dev/null +++ b/api_admin.py @@ -0,0 +1,263 @@ +from flask import Blueprint, request, render_template, redirect, session, url_for, send_file, jsonify, Response +import psycopg2, math, json, datetime, main, copy, requests +from config import config, sites_config +from main import unfoldCostLayers, get_sites, get_roles, create_site_secondary, getUser +from manage import create +from user_api import login_required +import postsqldb, process, hashlib, database_admin + + +admin_api = Blueprint('admin_api', __name__) + +@admin_api.route('/admin') +def admin_index(): + sites = [site[1] for site in main.get_sites(session['user']['sites'])] + return render_template("admin/index.html", + current_site=session['selected_site'], + sites=sites) + +@admin_api.route('/admin/site/') +@login_required +def adminSites(id): + if id == "new": + new_site = postsqldb.SitesTable.Payload( + "", + "", + session['user_id'] + ) + return render_template("admin/site.html", site=new_site.get_dictionary()) + else: + database_config = config() + with psycopg2.connect(**database_config) as conn: + site = postsqldb.SitesTable.select_tuple(conn, (id,)) + return render_template('admin/site.html', site=site) + +@admin_api.route('/admin/role/') +@login_required +def adminRoles(id): + database_config = config() + with psycopg2.connect(**database_config) as conn: + sites = postsqldb.SitesTable.selectTuples(conn) + if id == "new": + new_role = postsqldb.RolesTable.Payload( + "", + "", + 0 + ) + return render_template("admin/role.html", role=new_role.get_dictionary(), sites=sites) + else: + role = postsqldb.RolesTable.select_tuple(conn, (id,)) + return render_template('admin/role.html', role=role, sites=sites) + +@admin_api.route('/admin/user/') +@login_required +def adminUser(id): + database_config = config() + with psycopg2.connect(**database_config) as conn: + if id == "new": + new_user = postsqldb.LoginsTable.Payload("", "", "", "") + return render_template("admin/user.html", user=new_user.get_dictionary()) + else: + user = database_admin.selectLoginsUser(int(id)) + return render_template('admin/user.html', user=user) + +@admin_api.route('/admin/getSites', methods=['GET']) +@login_required +def getSites(): + if request.method == "GET": + records = [] + count = 0 + page = int(request.args.get('page', 1)) + limit = int(request.args.get('limit', 10)) + offset = (page - 1) * limit + database_config = config() + with psycopg2.connect(**database_config) as conn: + records, count = postsqldb.SitesTable.paginateTuples(conn, (limit, offset)) + return jsonify({'sites': records, "end": math.ceil(count/limit), 'error':False, 'message': 'Sites Loaded Successfully!'}) + return jsonify({'sites': records, "end": math.ceil(count/limit), 'error':True, 'message': 'There was a problem loading Sites!'}) + +@admin_api.route('/admin/getRoles', methods=['GET']) +@login_required +def getRoles(): + if request.method == "GET": + records = [] + count = 0 + page = int(request.args.get('page', 1)) + limit = int(request.args.get('limit', 10)) + offset = (page - 1) * limit + database_config = config() + with psycopg2.connect(**database_config) as conn: + records, count = postsqldb.RolesTable.paginate_tuples(conn, (limit, offset)) + return jsonify({'roles': records, "end": math.ceil(count/limit), 'error':False, 'message': 'Roles Loaded Successfully!'}) + return jsonify({'roles': records, "end": math.ceil(count/limit), 'error':True, 'message': 'There was a problem loading Roles!'}) + +@admin_api.route('/admin/getLogins', methods=['GET']) +@login_required +def getLogins(): + if request.method == "GET": + records = [] + count = 0 + page = int(request.args.get('page', 1)) + limit = int(request.args.get('limit', 10)) + offset = (page - 1) * limit + database_config = config() + with psycopg2.connect(**database_config) as conn: + records, count = postsqldb.LoginsTable.paginate_tuples(conn, (limit, offset)) + return jsonify({'logins': records, "end": math.ceil(count/limit), 'error':False, 'message': 'logins Loaded Successfully!'}) + return jsonify({'logins': records, "end": math.ceil(count/limit), 'error':True, 'message': 'There was a problem loading logins!'}) + +@admin_api.route('/admin/site/postDeleteSite', methods=["POST"]) +def postDeleteSite(): + if request.method == "POST": + site_id = request.get_json()['site_id'] + database_config = config() + user_id = session['user_id'] + try: + with psycopg2.connect(**database_config) as conn: + user = postsqldb.LoginsTable.select_tuple(conn, (user_id,)) + admin_user = (user['username'], user['password'], user['email'], user['row_type']) + site = postsqldb.SitesTable.select_tuple(conn, (site_id,)) + site = postsqldb.SitesTable.Manager( + site['site_name'], + admin_user, + site['default_zone'], + site['default_primary_location'], + site['site_description'] + ) + process.deleteSite(site_manager=site) + except Exception as error: + conn.rollback() + return jsonify({'error': True, 'message': error}) + return jsonify({'error': False, 'message': f""}) + return jsonify({'error': True, 'message': f""}) + +@admin_api.route('/admin/site/postAddSite', methods=["POST"]) +def postAddSite(): + if request.method == "POST": + payload = request.get_json()['payload'] + database_config = config() + site_name = session['selected_site'] + user_id = session['user_id'] + print(payload) + try: + with psycopg2.connect(**database_config) as conn: + user = postsqldb.LoginsTable.select_tuple(conn, (user_id,)) + admin_user = (user['username'], user['password'], user['email'], user['row_type']) + site = postsqldb.SitesTable.Manager( + payload['site_name'], + admin_user, + payload['default_zone'], + payload['default_primary_location'], + payload['site_description'] + ) + process.addSite(site_manager=site) + + except Exception as error: + conn.rollback() + return jsonify({'error': True, 'message': error}) + return jsonify({'error': False, 'message': f"Zone added to {site_name}."}) + return jsonify({'error': True, 'message': f"These was an error with adding this Zone to {site_name}."}) + +@admin_api.route('/admin/site/postEditSite', methods=["POST"]) +def postEditSite(): + if request.method == "POST": + payload = request.get_json()['payload'] + database_config = config() + try: + with psycopg2.connect(**database_config) as conn: + postsqldb.SitesTable.update_tuple(conn, payload) + except Exception as error: + conn.rollback() + return jsonify({'error': True, 'message': error}) + return jsonify({'error': False, 'message': f"Site updated."}) + return jsonify({'error': True, 'message': f"These was an error with updating Site."}) + +@admin_api.route('/admin/role/postAddRole', methods=["POST"]) +def postAddRole(): + if request.method == "POST": + payload = request.get_json()['payload'] + database_config = config() + print(payload) + try: + with psycopg2.connect(**database_config) as conn: + role = postsqldb.RolesTable.Payload( + payload['role_name'], + payload['role_description'], + payload['site_id'] + ) + postsqldb.RolesTable.insert_tuple(conn, role.payload()) + + except Exception as error: + conn.rollback() + return jsonify({'error': True, 'message': error}) + return jsonify({'error': False, 'message': f"Role added."}) + return jsonify({'error': True, 'message': f"These was an error with adding this Role."}) + +@admin_api.route('/admin/role/postEditRole', methods=["POST"]) +def postEditRole(): + if request.method == "POST": + payload = request.get_json()['payload'] + database_config = config() + print(payload) + try: + with psycopg2.connect(**database_config) as conn: + postsqldb.RolesTable.update_tuple(conn, payload) + + except Exception as error: + conn.rollback() + return jsonify({'error': True, 'message': error}) + return jsonify({'error': False, 'message': f"Role updated."}) + return jsonify({'error': True, 'message': f"These was an error with updating this Role."}) + +@admin_api.route('/admin/user/postAddLogin', methods=["POST"]) +def postAddLogin(): + if request.method == "POST": + payload = request.get_json()['payload'] + database_config = config() + user = [] + try: + with psycopg2.connect(**database_config) as conn: + user = postsqldb.LoginsTable.Payload( + payload['username'], + hashlib.sha256(payload['password'].encode()).hexdigest(), + payload['email'], + payload['row_type'] + ) + user = postsqldb.LoginsTable.insert_tuple(conn, user.payload()) + except postsqldb.DatabaseError as error: + conn.rollback() + return jsonify({'user': user, 'error': True, 'message': error}) + return jsonify({'user': user, 'error': False, 'message': f"User added."}) + return jsonify({'user': user, 'error': True, 'message': f"These was an error with adding this User."}) + +@admin_api.route('/admin/user/postEditLogin', methods=["POST"]) +def postEditLogin(): + if request.method == "POST": + payload = request.get_json()['payload'] + database_config = config() + try: + with psycopg2.connect(**database_config) as conn: + postsqldb.LoginsTable.update_tuple(conn, payload) + except Exception as error: + conn.rollback() + return jsonify({'error': True, 'message': error}) + return jsonify({'error': False, 'message': f"User was Added Successfully."}) + return jsonify({'error': True, 'message': f"These was an error with adding this user."}) + +@admin_api.route('/admin/user/postEditLoginPassword', methods=["POST"]) +def postEditLoginPassword(): + if request.method == "POST": + payload = request.get_json()['payload'] + database_config = config() + try: + with psycopg2.connect(**database_config) as conn: + user = postsqldb.LoginsTable.select_tuple(conn, (payload['id'],)) + if hashlib.sha256(payload['current_password'].encode()).hexdigest() != user['password']: + return jsonify({'error': True, 'message': "The provided current password is incorrect"}) + payload['update']['password'] = hashlib.sha256(payload['update']['password'].encode()).hexdigest() + postsqldb.LoginsTable.update_tuple(conn, payload) + except Exception as error: + conn.rollback() + return jsonify({'error': True, 'message': error}) + return jsonify({'error': False, 'message': f"Password was changed successfully."}) + return jsonify({'error': True, 'message': f"These was an error with updating this Users password."}) diff --git a/database.log b/database.log index c0ee631..f9616cb 100644 --- a/database.log +++ b/database.log @@ -1808,4 +1808,34 @@ sql='WITH sum_cte AS ( SELECT mi.id, SUM(mil.quantity_on_hand)::FLOAT8 AS total_sum FROM main_item_locations mil JOIN main_items mi ON mil.part_id = mi.id GROUP BY mi.id )SELECT main_items.*, row_to_json(main_item_info.*) as item_info, sum_cte.total_sum as total_qoh, (SELECT COALESCE(row_to_json(u), '{}') FROM units as u WHERE u.id=main_item_info.uom) as uomFROM main_itemsLEFT JOIN sum_cte ON main_items.id = sum_cte.idLEFT JOIN main_item_info ON main_items.item_info_id = main_item_info.idWHERE main_items.search_string LIKE '%%' || %s || '%%'ORDER BY main_items.id item_nameLIMIT %s OFFSET %s;') 2025-04-20 09:54:33.948670 --- ERROR --- DatabaseError(message=''int' object is not iterable', payload=(), - sql='SELECT * FROM sites') \ No newline at end of file + sql='SELECT * FROM sites') +2025-04-20 19:52:54.986069 --- ERROR --- DatabaseError(message='table "testsite_zones" does not exist', + payload=DROP TABLE TestSite_zones CASCADE;, + sql='zones') +2025-04-20 20:11:14.230809 --- ERROR --- DatabaseError(message='tuple index out of range', + payload=('main', ''), + sql='INSERT INTO testsite_zones(name, description, site_id) VALUES (%s, %s, %s) RETURNING *;') +2025-04-20 20:32:54.096676 --- ERROR --- DatabaseError(message='relation "testsite_sku_prefix" does not existLINE 1: SELECT * FROM TestSite_sku_prefix LIMIT 25 OFFSET 0; ^', + payload=(25, 0), + sql='SELECT * FROM TestSite_sku_prefix LIMIT %s OFFSET %s;') +2025-04-20 20:33:30.514244 --- ERROR --- DatabaseError(message='relation "testsite_item_locations" does not existLINE 3: FROM TestSite_item_locations mil ^', + payload=['', 50, 0], + sql='WITH sum_cte AS ( SELECT mi.id, SUM(mil.quantity_on_hand)::FLOAT8 AS total_sum FROM TestSite_item_locations mil JOIN TestSite_items mi ON mil.part_id = mi.id GROUP BY mi.id )SELECT TestSite_items.*, row_to_json(TestSite_item_info.*) as item_info, sum_cte.total_sum as total_qoh, (SELECT COALESCE(row_to_json(u), '{}') FROM units as u WHERE u.id=TestSite_item_info.uom) as uomFROM TestSite_itemsLEFT JOIN sum_cte ON TestSite_items.id = sum_cte.idLEFT JOIN TestSite_item_info ON TestSite_items.item_info_id = TestSite_item_info.idWHERE TestSite_items.search_string LIKE '%%' || %s || '%%'ORDER BY TestSite_items.id ASCLIMIT %s OFFSET %s;') +2025-04-20 20:34:46.464320 --- ERROR --- DatabaseError(message='relation "testsite_item_locations" does not existLINE 3: FROM TestSite_item_locations mil ^', + payload=['', 50, 0], + sql='WITH sum_cte AS ( SELECT mi.id, SUM(mil.quantity_on_hand)::FLOAT8 AS total_sum FROM TestSite_item_locations mil JOIN TestSite_items mi ON mil.part_id = mi.id GROUP BY mi.id )SELECT TestSite_items.*, row_to_json(TestSite_item_info.*) as item_info, sum_cte.total_sum as total_qoh, (SELECT COALESCE(row_to_json(u), '{}') FROM units as u WHERE u.id=TestSite_item_info.uom) as uomFROM TestSite_itemsLEFT JOIN sum_cte ON TestSite_items.id = sum_cte.idLEFT JOIN TestSite_item_info ON TestSite_items.item_info_id = TestSite_item_info.idWHERE TestSite_items.search_string LIKE '%%' || %s || '%%'ORDER BY TestSite_items.id ASCLIMIT %s OFFSET %s;') +2025-04-20 21:10:59.263962 --- ERROR --- DatabaseError(message='relation "testsite_item_locations" does not existLINE 3: FROM TestSite_item_locations mil ^', + payload=['', 50, 0], + sql='WITH sum_cte AS ( SELECT mi.id, SUM(mil.quantity_on_hand)::FLOAT8 AS total_sum FROM TestSite_item_locations mil JOIN TestSite_items mi ON mil.part_id = mi.id GROUP BY mi.id )SELECT TestSite_items.*, row_to_json(TestSite_item_info.*) as item_info, sum_cte.total_sum as total_qoh, (SELECT COALESCE(row_to_json(u), '{}') FROM units as u WHERE u.id=TestSite_item_info.uom) as uomFROM TestSite_itemsLEFT JOIN sum_cte ON TestSite_items.id = sum_cte.idLEFT JOIN TestSite_item_info ON TestSite_items.item_info_id = TestSite_item_info.idWHERE TestSite_items.search_string LIKE '%%' || %s || '%%'ORDER BY TestSite_items.id ASCLIMIT %s OFFSET %s;') +2025-04-21 14:42:32.973758 --- ERROR --- DatabaseError(message='new row for relation "logins" violates check constraint "logins_email_check"DETAIL: Failing row contains (29, ScannerDeviceB, 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08, test, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, f, {}, device).', + payload=('ScannerDeviceB', '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08', 'test', '{}', '{}', '{}', '{}', '{}', '{}', '{}', '{}', '{}', '{}', '{}', False, '{}', 'device'), + sql='INSERT INTO logins(username, password, email, favorites, unseen_pantry_items, unseen_groups, unseen_shopping_lists, unseen_recipes, seen_pantry_items, seen_groups, seen_shopping_lists, seen_recipes, sites, site_roles, system_admin, flags, row_type) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING *;') +2025-04-21 14:43:23.959085 --- ERROR --- DatabaseError(message='new row for relation "logins" violates check constraint "logins_email_check"DETAIL: Failing row contains (30, ScannerDeviceB, 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08, test, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, f, {}, device).', + payload=('ScannerDeviceB', '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08', 'test', '{}', '{}', '{}', '{}', '{}', '{}', '{}', '{}', '{}', '{}', '{}', False, '{}', 'device'), + sql='INSERT INTO logins(username, password, email, favorites, unseen_pantry_items, unseen_groups, unseen_shopping_lists, unseen_recipes, seen_pantry_items, seen_groups, seen_shopping_lists, seen_recipes, sites, site_roles, system_admin, flags, row_type) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING *;') +2025-04-21 14:44:16.605030 --- ERROR --- DatabaseError(message='new row for relation "logins" violates check constraint "logins_email_check"DETAIL: Failing row contains (31, ScannerDeviceB, 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08, test, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, f, {}, device).', + payload=('ScannerDeviceB', '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08', 'test', '{}', '{}', '{}', '{}', '{}', '{}', '{}', '{}', '{}', '{}', '{}', False, '{}', 'device'), + sql='INSERT INTO logins(username, password, email, favorites, unseen_pantry_items, unseen_groups, unseen_shopping_lists, unseen_recipes, seen_pantry_items, seen_groups, seen_shopping_lists, seen_recipes, sites, site_roles, system_admin, flags, row_type) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING *;') +2025-04-21 14:45:18.122865 --- ERROR --- DatabaseError(message='new row for relation "logins" violates check constraint "logins_email_check"DETAIL: Failing row contains (32, ScannerDeviceB, 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08, test, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, f, {}, device).', + payload=('ScannerDeviceB', '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08', 'test', '{}', '{}', '{}', '{}', '{}', '{}', '{}', '{}', '{}', '{}', '{}', False, '{}', 'device'), + sql='INSERT INTO logins(username, password, email, favorites, unseen_pantry_items, unseen_groups, unseen_shopping_lists, unseen_recipes, seen_pantry_items, seen_groups, seen_shopping_lists, seen_recipes, sites, site_roles, system_admin, flags, row_type) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING *;') \ No newline at end of file diff --git a/database_admin.py b/database_admin.py new file mode 100644 index 0000000..2b6f3bc --- /dev/null +++ b/database_admin.py @@ -0,0 +1,18 @@ +import postsqldb +import psycopg2 +import config + +def selectLoginsUser(login_id): + database_config = config.config() + try: + with psycopg2.connect(**database_config) as conn: + with open("sql/SELECT/admin/selectLoginsUser.sql", "r") as file: + sql = file.read() + with conn.cursor() as cur: + cur.execute(sql, (login_id,)) + user = cur.fetchone() + if user: + user = postsqldb.tupleDictionaryFactory(cur.description, user) + return user + except Exception as error: + raise postsqldb.DatabaseError(error, login_id, sql) \ No newline at end of file diff --git a/postsqldb.py b/postsqldb.py index fc76581..81ee6bf 100644 --- a/postsqldb.py +++ b/postsqldb.py @@ -1137,7 +1137,6 @@ class ZonesTable: @dataclass class Payload: name: str - site_id: int description: str = "" def __post_init__(self): @@ -1148,7 +1147,6 @@ class ZonesTable: return ( self.name, self.description, - self.site_id ) @classmethod @@ -1798,6 +1796,163 @@ class CycleCountsTable: pass class SitesTable: + + @dataclass + class Manager: + site_name: str + admin_user: tuple + default_zone: int + default_location: int + description: str + create_order: list = field(init=False) + drop_order: list = field(init=False) + + def __post_init__(self): + self.create_order = [ + "logins", + "sites", + "roles", + "units", + "cost_layers", + "linked_items", + "brands", + "food_info", + "item_info", + "zones", + "locations", + "logistics_info", + "transactions", + "item", + "vendors", + "groups", + "group_items", + "receipts", + "receipt_items", + "recipes", + "recipe_items", + "shopping_lists", + "shopping_list_items", + "item_locations", + "conversions" + ] + self.drop_order = [ + "item_info", + "items", + "cost_layers", + "linked_items", + "transactions", + "brands", + "food_info", + "logistics_info", + "zones", + "locations", + "vendors", + "group_items", + "groups", + "receipt_items", + "receipts", + "recipe_items", + "recipes", + "shopping_list_items", + "shopping_lists", + "item_locations", + "conversions" + ] + + @dataclass + class Payload: + site_name: str + site_description: str + site_owner_id: int + default_zone: str = None + default_auto_issue_location: str = None + default_primary_location: str = None + creation_date: datetime.datetime = field(init=False) + flags: dict = field(default_factory=dict) + + def __post_init__(self): + self.creation_date = datetime.datetime.now() + + def payload(self): + return ( + self.site_name, + self.site_description, + self.creation_date, + self.site_owner_id, + json.dumps(self.flags), + self.default_zone, + self.default_auto_issue_location, + self.default_primary_location + ) + + def get_dictionary(self): + return self.__dict__ + + @classmethod + def insert_tuple(self, conn, payload:tuple, convert=True): + """inserts payload into sites table + + Args: + conn (_T_connector@connect): Postgresql Connector + payload (tuple): (site_name[str], site_description[str], creation_date[timestamp], site_owner_id[int], + flags[dict], default_zone[str], default_auto_issue_location[str], default_primary_location[str]) + convert (bool, optional): Determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: inserted tuple + """ + site_tuple = () + with open(f"sql/INSERT/insertSitesTuple.sql", "r+") as file: + sql = file.read() + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + site_tuple = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + site_tuple = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return site_tuple + + + @classmethod + def paginateTuples(self, conn, payload: tuple, convert=True): + """_summary_ + + Args: + conn (_type_): _description_ + payload (tuple): (limit, offset) + convert (bool, optional): _description_. Defaults to True. + + Raises: + DatabaseError: _description_ + + Returns: + _type_: _description_ + """ + recordsets = [] + count = 0 + sql = f"SELECT * FROM sites LIMIT %s OFFSET %s;" + sql_count = f"SELECT COUNT(*) FROM sites;" + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchall() + if rows and convert: + recordsets = [tupleDictionaryFactory(cur.description, row) for row in rows] + elif rows and not convert: + recordsets = rows + cur.execute(sql_count) + count = cur.fetchone()[0] + except Exception as error: + raise DatabaseError(error, (), sql) + return recordsets, count + @classmethod def selectTuples(self, conn, convert=True): recordsets = [] @@ -1812,4 +1967,380 @@ class SitesTable: recordsets = rows except Exception as error: raise DatabaseError(error, (), sql) - return recordsets \ No newline at end of file + return recordsets + + @classmethod + def select_tuple(self, conn, payload:tuple, convert=True): + record = [] + sql = f"SELECT * FROM sites WHERE id=%s;" + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + record = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + record = rows + except Exception as error: + raise DatabaseError(error, (), sql) + return record + + @classmethod + def update_tuple(self, conn, payload, convert=True): + """_summary_ + + Args: + conn (_T_connector@connect): Postgresql Connector + site (str): + table (str): + payload (dict): {'id': row_id, 'update': {... column_to_update: value_to_update_to...}} + convert (bool, optional): determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: updated tuple + """ + updated = () + + set_clause, values = updateStringFactory(payload['update']) + values.append(payload['id']) + sql = f"UPDATE sites SET {set_clause} WHERE id=%s RETURNING *;" + try: + with conn.cursor() as cur: + cur.execute(sql, values) + rows = cur.fetchone() + if rows and convert: + updated = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + updated = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return updated + +class RolesTable: + @dataclass + class Payload: + role_name:str + role_description:str + site_id: int + flags: dict = field(default_factory=dict) + + def payload(self): + return ( + self.role_name, + self.role_description, + self.site_id, + json.dumps(self.flags) + ) + + def get_dictionary(self): + return self.__dict__ + + @classmethod + def insert_tuple(self, conn, payload:tuple, convert=True): + """inserts payload into roles table + + Args: + conn (_T_connector@connect): Postgresql Connector + payload (tuple): (role_name[str], role_description[str], site_id[int], flags[jsonb]) + convert (bool, optional): Determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: inserted tuple + """ + role_tuple = () + with open(f"sql/INSERT/insertRolesTuple.sql", "r+") as file: + sql = file.read() + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + role_tuple = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + role_tuple = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return role_tuple + + @classmethod + def select_tuple(self, conn, payload:tuple, convert=True): + record = [] + sql = f"SELECT roles.*, row_to_json(sites.*) as site FROM roles LEFT JOIN sites ON sites.id = roles.site_id WHERE roles.id=%s;" + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + record = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + record = rows + except Exception as error: + raise DatabaseError(error, (), sql) + return record + + @classmethod + def paginate_tuples(self, conn, payload:tuple, convert=True): + """ + Args: + conn (_T_connector@connect): Postgresql Connector + payload (tuple): (limit, offset) + convert (bool, optional): Determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: inserted tuple + """ + recordset = [] + + sql = f"SELECT roles.*, row_to_json(sites.*) as site FROM roles LEFT JOIN sites ON sites.id = roles.site_id LIMIT %s OFFSET %s;" + sql_count = f"SELECT COUNT(*) FROM roles;" + + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchall() + if rows and convert: + recordset = [tupleDictionaryFactory(cur.description, row) for row in rows] + elif rows and not convert: + recordset = rows + + + cur.execute(sql_count) + count = cur.fetchone()[0] + except Exception as error: + raise DatabaseError(error, payload, sql) + return recordset, count + + @classmethod + def update_tuple(self, conn, payload, convert=True): + """_summary_ + + Args: + conn (_T_connector@connect): Postgresql Connector + site (str): + table (str): + payload (dict): {'id': row_id, 'update': {... column_to_update: value_to_update_to...}} + convert (bool, optional): determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: updated tuple + """ + updated = () + + set_clause, values = updateStringFactory(payload['update']) + values.append(payload['id']) + sql = f"UPDATE roles SET {set_clause} WHERE id=%s RETURNING *;" + try: + with conn.cursor() as cur: + cur.execute(sql, values) + rows = cur.fetchone() + if rows and convert: + updated = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + updated = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return updated + +class LoginsTable: + @dataclass + class Payload: + username:str + password:str + email: str + row_type: str + system_admin: bool = False + flags: dict = field(default_factory=dict) + favorites: dict = field(default_factory=dict) + unseen_pantry_items: list = field(default_factory=list) + unseen_groups: list = field(default_factory=list) + unseen_shopping_lists: list = field(default_factory=list) + unseen_recipes: list = field(default_factory=list) + seen_pantry_items: list = field(default_factory=list) + seen_groups: list = field(default_factory=list) + seen_shopping_lists: list = field(default_factory=list) + seen_recipes: list = field(default_factory=list) + sites: list = field(default_factory=list) + site_roles: list = field(default_factory=list) + + def payload(self): + return ( + self.username, + self.password, + self.email, + json.dumps(self.favorites), + lst2pgarr(self.unseen_pantry_items), + lst2pgarr(self.unseen_groups), + lst2pgarr(self.unseen_shopping_lists), + lst2pgarr(self.unseen_recipes), + lst2pgarr(self.seen_pantry_items), + lst2pgarr(self.seen_groups), + lst2pgarr(self.seen_shopping_lists), + lst2pgarr(self.seen_recipes), + lst2pgarr(self.sites), + lst2pgarr(self.site_roles), + self.system_admin, + json.dumps(self.flags), + self.row_type + ) + + def get_dictionary(self): + return self.__dict__ + + + @classmethod + def insert_tuple(self, conn, payload:tuple, convert=True): + """inserts payload into roles table + + Args: + conn (_T_connector@connect): Postgresql Connector + payload (tuple): (username, password, email, favorites, unseen_pantry_items, unseen_groups, unseen_shopping_lists, + unseen_recipes, seen_pantry_items, seen_groups, seen_shopping_lists, seen_recipes, + sites, site_roles, system_admin, flags, row_type) + convert (bool, optional): Determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: inserted tuple + """ + login = () + with open(f"sql/INSERT/insertLoginsTupleTwo.sql", "r+") as file: + sql = file.read() + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + login = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + login = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return login + + + @classmethod + def select_tuple(self, conn, payload:tuple, convert=True): + """_summary_ + + Args: + conn (_type_): _description_ + payload (tuple): (user_id,) + convert (bool, optional): _description_. Defaults to False. + + Raises: + DatabaseError: _description_ + + Returns: + _type_: _description_ + """ + user = () + try: + with conn.cursor() as cur: + sql = f"SELECT * FROM logins WHERE id=%s;" + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + user = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + user = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return user + + @classmethod + def paginate_tuples(self, conn, payload:tuple, convert=True): + """ + Args: + conn (_T_connector@connect): Postgresql Connector + payload (tuple): (limit, offset) + convert (bool, optional): Determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: inserted tuple + """ + recordset = [] + + sql = f"SELECT * FROM logins LIMIT %s OFFSET %s;" + sql_count = f"SELECT COUNT(*) FROM logins;" + + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchall() + if rows and convert: + recordset = [tupleDictionaryFactory(cur.description, row) for row in rows] + elif rows and not convert: + recordset = rows + + cur.execute(sql_count) + count = cur.fetchone()[0] + except Exception as error: + raise DatabaseError(error, payload, sql) + return recordset, count + + @classmethod + def get_washed_tuple(self, conn, payload:tuple, convert=True): + user = () + try: + with conn.cursor() as cur: + sql = f"SELECT id, username, sites, site_roles, system_admin, flags FROM logins WHERE id=%s;" + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + user = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + user = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return user + + @classmethod + def update_tuple(self, conn, payload, convert=True): + """_summary_ + + Args: + conn (_T_connector@connect): Postgresql Connector + site (str): + table (str): + payload (dict): {'id': row_id, 'update': {... column_to_update: value_to_update_to...}} + convert (bool, optional): determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: updated tuple + """ + updated = () + + set_clause, values = updateStringFactory(payload['update']) + values.append(payload['id']) + sql = f"UPDATE logins SET {set_clause} WHERE id=%s RETURNING *;" + try: + with conn.cursor() as cur: + cur.execute(sql, values) + rows = cur.fetchone() + if rows and convert: + updated = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + updated = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return updated diff --git a/process.log b/process.log index 97904b9..b4bf39d 100644 --- a/process.log +++ b/process.log @@ -418,4 +418,571 @@ 2025-04-07 18:13:10.570396 --- CAUTION --- DatabaseError(message='current transaction is aborted, commands ignored until end of transaction block', payload=('loaves', ' loaf', ' Loaf', ' A single loaf of a unit.'), sql='INSERT INTO units(plural, single, fullname, description) VALUES (%s, %s, %s, %s) RETURNING *;') ["loaves", " loaf", " Loaf", " A single loaf of a unit."] 2025-04-07 18:13:10.579705 --- CAUTION --- DatabaseError(message='current transaction is aborted, commands ignored until end of transaction block', payload=('packs', ' pack', ' Pack', ' A Single Pack of a unit.'), sql='INSERT INTO units(plural, single, fullname, description) VALUES (%s, %s, %s, %s) RETURNING *;') - ["packs", " pack", " Pack", " A Single Pack of a unit."] \ No newline at end of file + ["packs", " pack", " Pack", " A Single Pack of a unit."]2025-04-20 19:12:42.901904 --- INFO --- logins Created! +2025-04-20 19:12:42.907042 --- INFO --- sites Created! +2025-04-20 19:12:42.916090 --- INFO --- roles Created! +2025-04-20 19:12:42.924299 --- INFO --- units Created! +2025-04-20 19:12:42.936431 --- INFO --- cost_layers Created! +2025-04-20 19:12:42.948063 --- INFO --- linked_items Created! +2025-04-20 19:12:42.956357 --- INFO --- brands Created! +2025-04-20 19:12:42.965966 --- INFO --- food_info Created! +2025-04-20 19:12:42.978005 --- INFO --- item_info Created! +2025-04-20 19:12:42.989756 --- INFO --- zones Created! +2025-04-20 19:12:42.999753 --- INFO --- locations Created! +2025-04-20 19:12:43.010766 --- INFO --- logistics_info Created! +2025-04-20 19:12:43.020793 --- INFO --- transactions Created! +2025-04-20 19:12:43.032889 --- INFO --- item Created! +2025-04-20 19:12:43.042155 --- INFO --- vendors Created! +2025-04-20 19:12:43.052984 --- INFO --- groups Created! +2025-04-20 19:12:43.065257 --- INFO --- group_items Created! +2025-04-20 19:12:43.075250 --- INFO --- receipts Created! +2025-04-20 19:12:43.085279 --- INFO --- receipt_items Created! +2025-04-20 19:12:43.095708 --- INFO --- recipes Created! +2025-04-20 19:12:43.108432 --- INFO --- recipe_items Created! +2025-04-20 19:12:43.118248 --- INFO --- shopping_lists Created! +2025-04-20 19:12:43.130093 --- INFO --- shopping_list_items Created! +2025-04-20 19:12:43.142255 --- INFO --- item_locations Created! +2025-04-20 19:12:43.152766 --- INFO --- conversions Created! +2025-04-20 19:12:43.157756 --- INFO --- Admin User Created! +2025-04-20 19:12:43.173869 --- ERROR --- module 'MyDataclasses' has no attribute 'ZonePayload' +2025-04-20 19:13:09.179803 --- INFO --- logins Created! +2025-04-20 19:13:09.186834 --- INFO --- sites Created! +2025-04-20 19:13:09.191988 --- INFO --- roles Created! +2025-04-20 19:13:09.196031 --- INFO --- units Created! +2025-04-20 19:13:09.203142 --- INFO --- cost_layers Created! +2025-04-20 19:13:09.211122 --- INFO --- linked_items Created! +2025-04-20 19:13:09.217381 --- INFO --- brands Created! +2025-04-20 19:13:09.223367 --- INFO --- food_info Created! +2025-04-20 19:13:09.231980 --- INFO --- item_info Created! +2025-04-20 19:13:09.239962 --- INFO --- zones Created! +2025-04-20 19:13:09.247043 --- INFO --- locations Created! +2025-04-20 19:13:09.254389 --- INFO --- logistics_info Created! +2025-04-20 19:13:09.261528 --- INFO --- transactions Created! +2025-04-20 19:13:09.270171 --- INFO --- item Created! +2025-04-20 19:13:09.276674 --- INFO --- vendors Created! +2025-04-20 19:13:09.283569 --- INFO --- groups Created! +2025-04-20 19:13:09.291142 --- INFO --- group_items Created! +2025-04-20 19:13:09.300047 --- INFO --- receipts Created! +2025-04-20 19:13:09.307082 --- INFO --- receipt_items Created! +2025-04-20 19:13:09.314189 --- INFO --- recipes Created! +2025-04-20 19:13:09.323320 --- INFO --- recipe_items Created! +2025-04-20 19:13:09.331286 --- INFO --- shopping_lists Created! +2025-04-20 19:13:09.338671 --- INFO --- shopping_list_items Created! +2025-04-20 19:13:09.347666 --- INFO --- item_locations Created! +2025-04-20 19:13:09.354290 --- INFO --- conversions Created! +2025-04-20 19:13:09.358352 --- INFO --- Admin User Created! +2025-04-20 19:13:09.373201 --- ERROR --- module 'MyDataclasses' has no attribute 'ZonePayload' +2025-04-20 19:14:30.040093 --- INFO --- logins Created! +2025-04-20 19:14:30.047388 --- INFO --- sites Created! +2025-04-20 19:14:30.053137 --- INFO --- roles Created! +2025-04-20 19:14:30.058339 --- INFO --- units Created! +2025-04-20 19:14:30.066445 --- INFO --- cost_layers Created! +2025-04-20 19:14:30.074589 --- INFO --- linked_items Created! +2025-04-20 19:14:30.080372 --- INFO --- brands Created! +2025-04-20 19:14:30.088112 --- INFO --- food_info Created! +2025-04-20 19:14:30.095435 --- INFO --- item_info Created! +2025-04-20 19:14:30.104268 --- INFO --- zones Created! +2025-04-20 19:14:30.111070 --- INFO --- locations Created! +2025-04-20 19:14:30.118747 --- INFO --- logistics_info Created! +2025-04-20 19:14:30.126464 --- INFO --- transactions Created! +2025-04-20 19:14:30.136123 --- INFO --- item Created! +2025-04-20 19:14:30.142326 --- INFO --- vendors Created! +2025-04-20 19:14:30.150179 --- INFO --- groups Created! +2025-04-20 19:14:30.159002 --- INFO --- group_items Created! +2025-04-20 19:14:30.165728 --- INFO --- receipts Created! +2025-04-20 19:14:30.173195 --- INFO --- receipt_items Created! +2025-04-20 19:14:30.180307 --- INFO --- recipes Created! +2025-04-20 19:14:30.188377 --- INFO --- recipe_items Created! +2025-04-20 19:14:30.195275 --- INFO --- shopping_lists Created! +2025-04-20 19:14:30.204333 --- INFO --- shopping_list_items Created! +2025-04-20 19:14:30.212189 --- INFO --- item_locations Created! +2025-04-20 19:14:30.218980 --- INFO --- conversions Created! +2025-04-20 19:14:30.223144 --- INFO --- Admin User Created! +2025-04-20 19:14:30.237347 --- ERROR --- module 'MyDataclasses' has no attribute 'ZonePayload' +2025-04-20 19:15:37.294592 --- INFO --- logins Created! +2025-04-20 19:15:37.300684 --- INFO --- sites Created! +2025-04-20 19:15:37.306214 --- INFO --- roles Created! +2025-04-20 19:15:37.310743 --- INFO --- units Created! +2025-04-20 19:15:37.320253 --- INFO --- cost_layers Created! +2025-04-20 19:15:37.328204 --- INFO --- linked_items Created! +2025-04-20 19:15:37.334340 --- INFO --- brands Created! +2025-04-20 19:15:37.341236 --- INFO --- food_info Created! +2025-04-20 19:15:37.348720 --- INFO --- item_info Created! +2025-04-20 19:15:37.358766 --- INFO --- zones Created! +2025-04-20 19:15:37.365313 --- INFO --- locations Created! +2025-04-20 19:15:37.373227 --- INFO --- logistics_info Created! +2025-04-20 19:15:37.379296 --- INFO --- transactions Created! +2025-04-20 19:15:37.387641 --- INFO --- item Created! +2025-04-20 19:15:37.395209 --- INFO --- vendors Created! +2025-04-20 19:15:37.402427 --- INFO --- groups Created! +2025-04-20 19:15:37.410391 --- INFO --- group_items Created! +2025-04-20 19:15:37.418421 --- INFO --- receipts Created! +2025-04-20 19:15:37.425524 --- INFO --- receipt_items Created! +2025-04-20 19:15:37.432240 --- INFO --- recipes Created! +2025-04-20 19:15:37.441206 --- INFO --- recipe_items Created! +2025-04-20 19:15:37.448373 --- INFO --- shopping_lists Created! +2025-04-20 19:15:37.456919 --- INFO --- shopping_list_items Created! +2025-04-20 19:15:37.465857 --- INFO --- item_locations Created! +2025-04-20 19:15:37.471974 --- INFO --- conversions Created! +2025-04-20 19:15:37.477332 --- INFO --- Admin User Created! +2025-04-20 19:15:37.488490 --- ERROR --- module 'MyDataclasses' has no attribute 'ZonePayload' +2025-04-20 19:16:04.245426 --- INFO --- logins Created! +2025-04-20 19:16:04.252369 --- INFO --- sites Created! +2025-04-20 19:16:04.257457 --- INFO --- roles Created! +2025-04-20 19:16:04.262528 --- INFO --- units Created! +2025-04-20 19:16:04.268813 --- INFO --- cost_layers Created! +2025-04-20 19:16:04.277403 --- INFO --- linked_items Created! +2025-04-20 19:16:04.282454 --- INFO --- brands Created! +2025-04-20 19:16:04.289349 --- INFO --- food_info Created! +2025-04-20 19:16:04.297588 --- INFO --- item_info Created! +2025-04-20 19:16:04.304872 --- INFO --- zones Created! +2025-04-20 19:16:04.312242 --- INFO --- locations Created! +2025-04-20 19:16:04.320032 --- INFO --- logistics_info Created! +2025-04-20 19:16:04.327526 --- INFO --- transactions Created! +2025-04-20 19:16:04.336107 --- INFO --- item Created! +2025-04-20 19:16:04.342583 --- INFO --- vendors Created! +2025-04-20 19:16:04.350295 --- INFO --- groups Created! +2025-04-20 19:16:04.357531 --- INFO --- group_items Created! +2025-04-20 19:16:04.365626 --- INFO --- receipts Created! +2025-04-20 19:16:04.372230 --- INFO --- receipt_items Created! +2025-04-20 19:16:04.379414 --- INFO --- recipes Created! +2025-04-20 19:16:04.387527 --- INFO --- recipe_items Created! +2025-04-20 19:16:04.394398 --- INFO --- shopping_lists Created! +2025-04-20 19:16:04.403440 --- INFO --- shopping_list_items Created! +2025-04-20 19:16:04.410510 --- INFO --- item_locations Created! +2025-04-20 19:16:04.418073 --- INFO --- conversions Created! +2025-04-20 19:16:04.423218 --- INFO --- Admin User Created! +2025-04-20 19:16:04.436197 --- ERROR --- module 'MyDataclasses' has no attribute 'ZonePayload' +2025-04-20 19:16:58.466588 --- INFO --- logins Created! +2025-04-20 19:16:58.472688 --- INFO --- sites Created! +2025-04-20 19:16:58.477737 --- INFO --- roles Created! +2025-04-20 19:16:58.481251 --- INFO --- units Created! +2025-04-20 19:16:58.489772 --- INFO --- cost_layers Created! +2025-04-20 19:16:58.497672 --- INFO --- linked_items Created! +2025-04-20 19:16:58.502626 --- INFO --- brands Created! +2025-04-20 19:16:58.509339 --- INFO --- food_info Created! +2025-04-20 19:16:58.516796 --- INFO --- item_info Created! +2025-04-20 19:16:58.524536 --- INFO --- zones Created! +2025-04-20 19:16:58.532376 --- INFO --- locations Created! +2025-04-20 19:16:58.540104 --- INFO --- logistics_info Created! +2025-04-20 19:16:58.547122 --- INFO --- transactions Created! +2025-04-20 19:16:58.555955 --- INFO --- item Created! +2025-04-20 19:16:58.562475 --- INFO --- vendors Created! +2025-04-20 19:16:58.570565 --- INFO --- groups Created! +2025-04-20 19:16:58.578991 --- INFO --- group_items Created! +2025-04-20 19:16:58.586669 --- INFO --- receipts Created! +2025-04-20 19:16:58.594838 --- INFO --- receipt_items Created! +2025-04-20 19:16:58.601902 --- INFO --- recipes Created! +2025-04-20 19:16:58.608915 --- INFO --- recipe_items Created! +2025-04-20 19:16:58.617684 --- INFO --- shopping_lists Created! +2025-04-20 19:16:58.625427 --- INFO --- shopping_list_items Created! +2025-04-20 19:16:58.635479 --- INFO --- item_locations Created! +2025-04-20 19:16:58.642372 --- INFO --- conversions Created! +2025-04-20 19:16:58.646415 --- INFO --- Admin User Created! +2025-04-20 19:16:58.668418 --- ERROR --- module 'MyDataclasses' has no attribute 'VendorPayload' +2025-04-20 19:17:15.674844 --- INFO --- logins Created! +2025-04-20 19:17:15.682368 --- INFO --- sites Created! +2025-04-20 19:17:15.687761 --- INFO --- roles Created! +2025-04-20 19:17:15.692281 --- INFO --- units Created! +2025-04-20 19:17:15.700205 --- INFO --- cost_layers Created! +2025-04-20 19:17:15.707736 --- INFO --- linked_items Created! +2025-04-20 19:17:15.714280 --- INFO --- brands Created! +2025-04-20 19:17:15.720500 --- INFO --- food_info Created! +2025-04-20 19:17:15.728902 --- INFO --- item_info Created! +2025-04-20 19:17:15.735575 --- INFO --- zones Created! +2025-04-20 19:17:15.743577 --- INFO --- locations Created! +2025-04-20 19:17:15.750537 --- INFO --- logistics_info Created! +2025-04-20 19:17:15.757739 --- INFO --- transactions Created! +2025-04-20 19:17:15.767727 --- INFO --- item Created! +2025-04-20 19:17:15.773470 --- INFO --- vendors Created! +2025-04-20 19:17:15.781078 --- INFO --- groups Created! +2025-04-20 19:17:15.791354 --- INFO --- group_items Created! +2025-04-20 19:17:15.799134 --- INFO --- receipts Created! +2025-04-20 19:17:15.806841 --- INFO --- receipt_items Created! +2025-04-20 19:17:15.813370 --- INFO --- recipes Created! +2025-04-20 19:17:15.821296 --- INFO --- recipe_items Created! +2025-04-20 19:17:15.828999 --- INFO --- shopping_lists Created! +2025-04-20 19:17:15.837757 --- INFO --- shopping_list_items Created! +2025-04-20 19:17:15.846351 --- INFO --- item_locations Created! +2025-04-20 19:17:15.853372 --- INFO --- conversions Created! +2025-04-20 19:17:15.856625 --- INFO --- Admin User Created! +2025-04-20 19:17:15.871202 --- ERROR --- module 'MyDataclasses' has no attribute 'VendorPayload' +2025-04-20 19:17:59.229086 --- INFO --- logins Created! +2025-04-20 19:17:59.236178 --- INFO --- sites Created! +2025-04-20 19:17:59.241179 --- INFO --- roles Created! +2025-04-20 19:17:59.244639 --- INFO --- units Created! +2025-04-20 19:17:59.252556 --- INFO --- cost_layers Created! +2025-04-20 19:17:59.258931 --- INFO --- linked_items Created! +2025-04-20 19:17:59.265640 --- INFO --- brands Created! +2025-04-20 19:17:59.271635 --- INFO --- food_info Created! +2025-04-20 19:17:59.281402 --- INFO --- item_info Created! +2025-04-20 19:17:59.289153 --- INFO --- zones Created! +2025-04-20 19:17:59.295595 --- INFO --- locations Created! +2025-04-20 19:17:59.303510 --- INFO --- logistics_info Created! +2025-04-20 19:17:59.310620 --- INFO --- transactions Created! +2025-04-20 19:17:59.318745 --- INFO --- item Created! +2025-04-20 19:17:59.325400 --- INFO --- vendors Created! +2025-04-20 19:17:59.333182 --- INFO --- groups Created! +2025-04-20 19:17:59.341367 --- INFO --- group_items Created! +2025-04-20 19:17:59.348914 --- INFO --- receipts Created! +2025-04-20 19:17:59.355674 --- INFO --- receipt_items Created! +2025-04-20 19:17:59.363428 --- INFO --- recipes Created! +2025-04-20 19:17:59.371858 --- INFO --- recipe_items Created! +2025-04-20 19:17:59.378939 --- INFO --- shopping_lists Created! +2025-04-20 19:17:59.388138 --- INFO --- shopping_list_items Created! +2025-04-20 19:17:59.395999 --- INFO --- item_locations Created! +2025-04-20 19:17:59.402528 --- INFO --- conversions Created! +2025-04-20 19:17:59.406967 --- INFO --- Admin User Created! +2025-04-20 19:43:36.914652 --- INFO --- logins Created! +2025-04-20 19:43:36.922436 --- INFO --- sites Created! +2025-04-20 19:43:36.927458 --- INFO --- roles Created! +2025-04-20 19:43:36.931555 --- INFO --- units Created! +2025-04-20 19:43:36.939094 --- INFO --- cost_layers Created! +2025-04-20 19:43:36.947628 --- INFO --- linked_items Created! +2025-04-20 19:43:36.953631 --- INFO --- brands Created! +2025-04-20 19:43:36.959852 --- INFO --- food_info Created! +2025-04-20 19:43:36.968778 --- INFO --- item_info Created! +2025-04-20 19:43:36.976558 --- INFO --- zones Created! +2025-04-20 19:43:36.983310 --- INFO --- locations Created! +2025-04-20 19:43:36.990780 --- INFO --- logistics_info Created! +2025-04-20 19:43:36.998082 --- INFO --- transactions Created! +2025-04-20 19:43:37.005780 --- INFO --- item Created! +2025-04-20 19:43:37.013460 --- INFO --- vendors Created! +2025-04-20 19:43:37.020215 --- INFO --- groups Created! +2025-04-20 19:43:37.028782 --- INFO --- group_items Created! +2025-04-20 19:43:37.036257 --- INFO --- receipts Created! +2025-04-20 19:43:37.043565 --- INFO --- receipt_items Created! +2025-04-20 19:43:37.049814 --- INFO --- recipes Created! +2025-04-20 19:43:37.057702 --- INFO --- recipe_items Created! +2025-04-20 19:43:37.065761 --- INFO --- shopping_lists Created! +2025-04-20 19:43:37.073370 --- INFO --- shopping_list_items Created! +2025-04-20 19:43:37.081053 --- INFO --- item_locations Created! +2025-04-20 19:43:37.088088 --- INFO --- conversions Created! +2025-04-20 19:43:37.093156 --- INFO --- Admin User Created! +2025-04-20 19:45:44.395265 --- INFO --- item_info DROPPED! +2025-04-20 19:45:44.405900 --- INFO --- items DROPPED! +2025-04-20 19:45:44.414804 --- INFO --- cost_layers DROPPED! +2025-04-20 19:45:44.422703 --- INFO --- linked_items DROPPED! +2025-04-20 19:45:44.430346 --- INFO --- transactions DROPPED! +2025-04-20 19:45:44.437350 --- INFO --- brands DROPPED! +2025-04-20 19:45:44.444569 --- INFO --- food_info DROPPED! +2025-04-20 19:45:44.452885 --- INFO --- logistics_info DROPPED! +2025-04-20 19:48:31.583108 --- INFO --- item_info DROPPED! +2025-04-20 19:48:31.591460 --- INFO --- items DROPPED! +2025-04-20 19:48:31.596408 --- INFO --- cost_layers DROPPED! +2025-04-20 19:48:31.601566 --- INFO --- linked_items DROPPED! +2025-04-20 19:48:31.607329 --- INFO --- transactions DROPPED! +2025-04-20 19:48:31.611822 --- INFO --- brands DROPPED! +2025-04-20 19:48:31.615984 --- INFO --- food_info DROPPED! +2025-04-20 19:48:31.621444 --- INFO --- logistics_info DROPPED! +2025-04-20 19:51:08.211394 --- INFO --- item_info DROPPED! +2025-04-20 19:51:08.219628 --- INFO --- items DROPPED! +2025-04-20 19:51:08.225163 --- INFO --- cost_layers DROPPED! +2025-04-20 19:51:08.231236 --- INFO --- linked_items DROPPED! +2025-04-20 19:51:08.236599 --- INFO --- transactions DROPPED! +2025-04-20 19:51:08.241802 --- INFO --- brands DROPPED! +2025-04-20 19:51:08.247341 --- INFO --- food_info DROPPED! +2025-04-20 19:51:08.251883 --- INFO --- logistics_info DROPPED! +2025-04-20 19:52:54.948592 --- INFO --- item_info DROPPED! +2025-04-20 19:52:54.956447 --- INFO --- items DROPPED! +2025-04-20 19:52:54.962023 --- INFO --- cost_layers DROPPED! +2025-04-20 19:52:54.967556 --- INFO --- linked_items DROPPED! +2025-04-20 19:52:54.973165 --- INFO --- transactions DROPPED! +2025-04-20 19:52:54.976632 --- INFO --- brands DROPPED! +2025-04-20 19:52:54.981398 --- INFO --- food_info DROPPED! +2025-04-20 19:52:54.985072 --- INFO --- logistics_info DROPPED! +2025-04-20 19:52:54.989456 --- ERROR --- DatabaseError(message='table "testsite_zones" does not exist', payload=DROP TABLE TestSite_zones CASCADE;, sql='zones') +2025-04-20 19:56:25.272595 --- INFO --- item_info DROPPED! +2025-04-20 19:56:25.282064 --- INFO --- items DROPPED! +2025-04-20 19:56:25.287581 --- INFO --- cost_layers DROPPED! +2025-04-20 19:56:25.293563 --- INFO --- linked_items DROPPED! +2025-04-20 19:56:25.298578 --- INFO --- transactions DROPPED! +2025-04-20 19:56:25.303670 --- INFO --- brands DROPPED! +2025-04-20 19:56:25.307742 --- INFO --- food_info DROPPED! +2025-04-20 19:56:25.313677 --- INFO --- logistics_info DROPPED! +2025-04-20 19:56:25.317716 --- INFO --- zones DROPPED! +2025-04-20 19:56:25.325855 --- INFO --- locations DROPPED! +2025-04-20 19:56:25.330990 --- INFO --- vendors DROPPED! +2025-04-20 19:56:25.337850 --- INFO --- group_items DROPPED! +2025-04-20 19:56:25.346723 --- INFO --- groups DROPPED! +2025-04-20 19:56:25.354480 --- INFO --- receipt_items DROPPED! +2025-04-20 19:56:25.362108 --- INFO --- receipts DROPPED! +2025-04-20 19:56:25.369684 --- INFO --- recipe_items DROPPED! +2025-04-20 19:56:25.377807 --- INFO --- recipes DROPPED! +2025-04-20 19:56:25.385620 --- INFO --- shopping_list_items DROPPED! +2025-04-20 19:56:25.393541 --- INFO --- shopping_lists DROPPED! +2025-04-20 19:56:25.401384 --- INFO --- item_locations DROPPED! +2025-04-20 19:56:25.405523 --- INFO --- conversions DROPPED! +2025-04-20 19:58:10.901757 --- INFO --- item_info DROPPED! +2025-04-20 19:58:10.911845 --- INFO --- items DROPPED! +2025-04-20 19:58:10.917872 --- INFO --- cost_layers DROPPED! +2025-04-20 19:58:10.922065 --- INFO --- linked_items DROPPED! +2025-04-20 19:58:10.928478 --- INFO --- transactions DROPPED! +2025-04-20 19:58:10.932582 --- INFO --- brands DROPPED! +2025-04-20 19:58:10.936689 --- INFO --- food_info DROPPED! +2025-04-20 19:58:10.941754 --- INFO --- logistics_info DROPPED! +2025-04-20 19:58:36.985976 --- INFO --- item_info DROPPED! +2025-04-20 19:58:36.996149 --- INFO --- items DROPPED! +2025-04-20 19:58:37.001166 --- INFO --- cost_layers DROPPED! +2025-04-20 19:58:37.006862 --- INFO --- linked_items DROPPED! +2025-04-20 19:58:37.012179 --- INFO --- transactions DROPPED! +2025-04-20 19:58:37.016694 --- INFO --- brands DROPPED! +2025-04-20 19:58:37.020758 --- INFO --- food_info DROPPED! +2025-04-20 19:58:37.026061 --- INFO --- logistics_info DROPPED! +2025-04-20 20:07:37.732426 --- INFO --- item_info DROPPED! +2025-04-20 20:07:37.740467 --- INFO --- items DROPPED! +2025-04-20 20:07:37.745042 --- INFO --- cost_layers DROPPED! +2025-04-20 20:07:37.750893 --- INFO --- linked_items DROPPED! +2025-04-20 20:07:37.754429 --- INFO --- transactions DROPPED! +2025-04-20 20:07:37.758729 --- INFO --- brands DROPPED! +2025-04-20 20:07:37.763189 --- INFO --- food_info DROPPED! +2025-04-20 20:07:37.767020 --- INFO --- logistics_info DROPPED! +2025-04-20 20:07:37.771055 --- INFO --- zones DROPPED! +2025-04-20 20:07:37.775628 --- INFO --- locations DROPPED! +2025-04-20 20:07:37.779735 --- INFO --- vendors DROPPED! +2025-04-20 20:07:37.784456 --- INFO --- group_items DROPPED! +2025-04-20 20:07:37.788915 --- INFO --- groups DROPPED! +2025-04-20 20:07:37.793395 --- INFO --- receipt_items DROPPED! +2025-04-20 20:07:37.798563 --- INFO --- receipts DROPPED! +2025-04-20 20:07:37.803691 --- INFO --- recipe_items DROPPED! +2025-04-20 20:07:37.808903 --- INFO --- recipes DROPPED! +2025-04-20 20:07:37.813005 --- INFO --- shopping_list_items DROPPED! +2025-04-20 20:07:37.818899 --- INFO --- shopping_lists DROPPED! +2025-04-20 20:07:37.823062 --- INFO --- item_locations DROPPED! +2025-04-20 20:07:37.827577 --- INFO --- conversions DROPPED! +2025-04-20 20:10:51.725827 --- INFO --- item_info DROPPED! +2025-04-20 20:10:51.735709 --- INFO --- items DROPPED! +2025-04-20 20:10:51.741311 --- INFO --- cost_layers DROPPED! +2025-04-20 20:10:51.745890 --- INFO --- linked_items DROPPED! +2025-04-20 20:10:51.750915 --- INFO --- transactions DROPPED! +2025-04-20 20:10:51.755599 --- INFO --- brands DROPPED! +2025-04-20 20:10:51.761188 --- INFO --- food_info DROPPED! +2025-04-20 20:10:51.765063 --- INFO --- logistics_info DROPPED! +2025-04-20 20:10:51.769585 --- INFO --- zones DROPPED! +2025-04-20 20:10:51.774893 --- INFO --- locations DROPPED! +2025-04-20 20:10:51.779100 --- INFO --- vendors DROPPED! +2025-04-20 20:10:51.784704 --- INFO --- group_items DROPPED! +2025-04-20 20:10:51.789211 --- INFO --- groups DROPPED! +2025-04-20 20:10:51.793357 --- INFO --- receipt_items DROPPED! +2025-04-20 20:10:51.799194 --- INFO --- receipts DROPPED! +2025-04-20 20:10:51.803289 --- INFO --- recipe_items DROPPED! +2025-04-20 20:10:51.808076 --- INFO --- recipes DROPPED! +2025-04-20 20:10:51.813010 --- INFO --- shopping_list_items DROPPED! +2025-04-20 20:10:51.817596 --- INFO --- shopping_lists DROPPED! +2025-04-20 20:10:51.821643 --- INFO --- item_locations DROPPED! +2025-04-20 20:10:51.826587 --- INFO --- conversions DROPPED! +2025-04-20 20:11:14.056575 --- INFO --- logins Created! +2025-04-20 20:11:14.063971 --- INFO --- sites Created! +2025-04-20 20:11:14.069365 --- INFO --- roles Created! +2025-04-20 20:11:14.073411 --- INFO --- units Created! +2025-04-20 20:11:14.082962 --- INFO --- cost_layers Created! +2025-04-20 20:11:14.091859 --- INFO --- linked_items Created! +2025-04-20 20:11:14.096907 --- INFO --- brands Created! +2025-04-20 20:11:14.104713 --- INFO --- food_info Created! +2025-04-20 20:11:14.110843 --- INFO --- item_info Created! +2025-04-20 20:11:14.119202 --- INFO --- zones Created! +2025-04-20 20:11:14.125756 --- INFO --- locations Created! +2025-04-20 20:11:14.131825 --- INFO --- logistics_info Created! +2025-04-20 20:11:14.138848 --- INFO --- transactions Created! +2025-04-20 20:11:14.146927 --- INFO --- item Created! +2025-04-20 20:11:14.152932 --- INFO --- vendors Created! +2025-04-20 20:11:14.159374 --- INFO --- groups Created! +2025-04-20 20:11:14.166932 --- INFO --- group_items Created! +2025-04-20 20:11:14.174750 --- INFO --- receipts Created! +2025-04-20 20:11:14.181847 --- INFO --- receipt_items Created! +2025-04-20 20:11:14.186633 --- INFO --- recipes Created! +2025-04-20 20:11:14.194852 --- INFO --- recipe_items Created! +2025-04-20 20:11:14.201475 --- INFO --- shopping_lists Created! +2025-04-20 20:11:14.209506 --- INFO --- shopping_list_items Created! +2025-04-20 20:11:14.217085 --- INFO --- item_locations Created! +2025-04-20 20:11:14.224012 --- INFO --- conversions Created! +2025-04-20 20:11:14.227800 --- INFO --- Admin User Created! +2025-04-20 20:11:14.240274 --- ERROR --- DatabaseError(message='tuple index out of range', payload=('main', ''), sql='INSERT INTO testsite_zones(name, description, site_id) VALUES (%s, %s, %s) RETURNING *;') +2025-04-20 20:12:29.007584 --- INFO --- logins Created! +2025-04-20 20:12:29.015493 --- INFO --- sites Created! +2025-04-20 20:12:29.021142 --- INFO --- roles Created! +2025-04-20 20:12:29.025026 --- INFO --- units Created! +2025-04-20 20:12:29.032140 --- INFO --- cost_layers Created! +2025-04-20 20:12:29.039496 --- INFO --- linked_items Created! +2025-04-20 20:12:29.046115 --- INFO --- brands Created! +2025-04-20 20:12:29.052237 --- INFO --- food_info Created! +2025-04-20 20:12:29.060538 --- INFO --- item_info Created! +2025-04-20 20:12:29.066956 --- INFO --- zones Created! +2025-04-20 20:12:29.073431 --- INFO --- locations Created! +2025-04-20 20:12:29.082564 --- INFO --- logistics_info Created! +2025-04-20 20:12:29.090000 --- INFO --- transactions Created! +2025-04-20 20:12:29.098688 --- INFO --- item Created! +2025-04-20 20:12:29.104873 --- INFO --- vendors Created! +2025-04-20 20:12:29.112717 --- INFO --- groups Created! +2025-04-20 20:12:29.120101 --- INFO --- group_items Created! +2025-04-20 20:12:29.128686 --- INFO --- receipts Created! +2025-04-20 20:12:29.135147 --- INFO --- receipt_items Created! +2025-04-20 20:12:29.141797 --- INFO --- recipes Created! +2025-04-20 20:12:29.151352 --- INFO --- recipe_items Created! +2025-04-20 20:12:29.158714 --- INFO --- shopping_lists Created! +2025-04-20 20:12:29.166933 --- INFO --- shopping_list_items Created! +2025-04-20 20:12:29.175124 --- INFO --- item_locations Created! +2025-04-20 20:12:29.181717 --- INFO --- conversions Created! +2025-04-20 20:12:29.186271 --- INFO --- Admin User Created! +2025-04-20 20:12:38.923616 --- INFO --- item_info DROPPED! +2025-04-20 20:12:38.932843 --- INFO --- items DROPPED! +2025-04-20 20:12:38.937372 --- INFO --- cost_layers DROPPED! +2025-04-20 20:12:38.942503 --- INFO --- linked_items DROPPED! +2025-04-20 20:12:38.946721 --- INFO --- transactions DROPPED! +2025-04-20 20:12:38.951727 --- INFO --- brands DROPPED! +2025-04-20 20:12:38.955969 --- INFO --- food_info DROPPED! +2025-04-20 20:12:38.961360 --- INFO --- logistics_info DROPPED! +2025-04-20 20:12:38.966848 --- INFO --- zones DROPPED! +2025-04-20 20:12:38.970884 --- INFO --- locations DROPPED! +2025-04-20 20:12:38.975885 --- INFO --- vendors DROPPED! +2025-04-20 20:12:38.980966 --- INFO --- group_items DROPPED! +2025-04-20 20:12:38.985746 --- INFO --- groups DROPPED! +2025-04-20 20:12:38.989709 --- INFO --- receipt_items DROPPED! +2025-04-20 20:12:38.994949 --- INFO --- receipts DROPPED! +2025-04-20 20:12:38.998971 --- INFO --- recipe_items DROPPED! +2025-04-20 20:12:39.004057 --- INFO --- recipes DROPPED! +2025-04-20 20:12:39.009013 --- INFO --- shopping_list_items DROPPED! +2025-04-20 20:12:39.013298 --- INFO --- shopping_lists DROPPED! +2025-04-20 20:12:39.017813 --- INFO --- item_locations DROPPED! +2025-04-20 20:12:39.022064 --- INFO --- conversions DROPPED! +2025-04-20 20:32:27.356365 --- INFO --- logins Created! +2025-04-20 20:32:27.364095 --- INFO --- sites Created! +2025-04-20 20:32:27.368637 --- INFO --- roles Created! +2025-04-20 20:32:27.374196 --- INFO --- units Created! +2025-04-20 20:32:27.381877 --- INFO --- cost_layers Created! +2025-04-20 20:32:27.389138 --- INFO --- linked_items Created! +2025-04-20 20:32:27.395362 --- INFO --- brands Created! +2025-04-20 20:32:27.402835 --- INFO --- food_info Created! +2025-04-20 20:32:27.409510 --- INFO --- item_info Created! +2025-04-20 20:32:27.417471 --- INFO --- zones Created! +2025-04-20 20:32:27.424663 --- INFO --- locations Created! +2025-04-20 20:32:27.433133 --- INFO --- logistics_info Created! +2025-04-20 20:32:27.439780 --- INFO --- transactions Created! +2025-04-20 20:32:27.448585 --- INFO --- item Created! +2025-04-20 20:32:27.455303 --- INFO --- vendors Created! +2025-04-20 20:32:27.462869 --- INFO --- groups Created! +2025-04-20 20:32:27.470653 --- INFO --- group_items Created! +2025-04-20 20:32:27.478653 --- INFO --- receipts Created! +2025-04-20 20:32:27.486804 --- INFO --- receipt_items Created! +2025-04-20 20:32:27.493141 --- INFO --- recipes Created! +2025-04-20 20:32:27.500870 --- INFO --- recipe_items Created! +2025-04-20 20:32:27.507862 --- INFO --- shopping_lists Created! +2025-04-20 20:32:27.515340 --- INFO --- shopping_list_items Created! +2025-04-20 20:32:27.524287 --- INFO --- item_locations Created! +2025-04-20 20:32:27.532196 --- INFO --- conversions Created! +2025-04-20 20:32:27.535331 --- INFO --- Admin User Created! +2025-04-20 20:33:25.173233 --- INFO --- item_info DROPPED! +2025-04-20 20:33:25.182788 --- INFO --- items DROPPED! +2025-04-20 20:33:25.187318 --- INFO --- cost_layers DROPPED! +2025-04-20 20:33:25.193746 --- INFO --- linked_items DROPPED! +2025-04-20 20:33:25.197263 --- INFO --- transactions DROPPED! +2025-04-20 20:33:25.202565 --- INFO --- brands DROPPED! +2025-04-20 20:33:25.207124 --- INFO --- food_info DROPPED! +2025-04-20 20:33:25.213012 --- INFO --- logistics_info DROPPED! +2025-04-20 20:33:25.218554 --- INFO --- zones DROPPED! +2025-04-20 20:33:25.222613 --- INFO --- locations DROPPED! +2025-04-20 20:33:25.227136 --- INFO --- vendors DROPPED! +2025-04-20 20:33:25.232695 --- INFO --- group_items DROPPED! +2025-04-20 20:33:25.237264 --- INFO --- groups DROPPED! +2025-04-20 20:33:25.241580 --- INFO --- receipt_items DROPPED! +2025-04-20 20:33:25.246657 --- INFO --- receipts DROPPED! +2025-04-20 20:33:25.251050 --- INFO --- recipe_items DROPPED! +2025-04-20 20:33:25.255068 --- INFO --- recipes DROPPED! +2025-04-20 20:33:25.260690 --- INFO --- shopping_list_items DROPPED! +2025-04-20 20:33:25.265769 --- INFO --- shopping_lists DROPPED! +2025-04-20 20:33:25.269793 --- INFO --- item_locations DROPPED! +2025-04-20 20:33:25.273824 --- INFO --- conversions DROPPED! +2025-04-20 20:34:25.344816 --- INFO --- logins Created! +2025-04-20 20:34:25.352155 --- INFO --- sites Created! +2025-04-20 20:34:25.357696 --- INFO --- roles Created! +2025-04-20 20:34:25.363010 --- INFO --- units Created! +2025-04-20 20:34:25.370030 --- INFO --- cost_layers Created! +2025-04-20 20:34:25.377554 --- INFO --- linked_items Created! +2025-04-20 20:34:25.383668 --- INFO --- brands Created! +2025-04-20 20:34:25.390875 --- INFO --- food_info Created! +2025-04-20 20:34:25.397424 --- INFO --- item_info Created! +2025-04-20 20:34:25.405761 --- INFO --- zones Created! +2025-04-20 20:34:25.412801 --- INFO --- locations Created! +2025-04-20 20:34:25.420564 --- INFO --- logistics_info Created! +2025-04-20 20:34:25.427949 --- INFO --- transactions Created! +2025-04-20 20:34:25.435344 --- INFO --- item Created! +2025-04-20 20:34:25.442389 --- INFO --- vendors Created! +2025-04-20 20:34:25.449534 --- INFO --- groups Created! +2025-04-20 20:34:25.457550 --- INFO --- group_items Created! +2025-04-20 20:34:25.465405 --- INFO --- receipts Created! +2025-04-20 20:34:25.471947 --- INFO --- receipt_items Created! +2025-04-20 20:34:25.478815 --- INFO --- recipes Created! +2025-04-20 20:34:25.486803 --- INFO --- recipe_items Created! +2025-04-20 20:34:25.495032 --- INFO --- shopping_lists Created! +2025-04-20 20:34:25.503567 --- INFO --- shopping_list_items Created! +2025-04-20 20:34:25.511863 --- INFO --- item_locations Created! +2025-04-20 20:34:25.517088 --- INFO --- conversions Created! +2025-04-20 20:34:25.522653 --- INFO --- Admin User Created! +2025-04-20 20:34:40.207702 --- INFO --- item_info DROPPED! +2025-04-20 20:34:40.217297 --- INFO --- items DROPPED! +2025-04-20 20:34:40.223866 --- INFO --- cost_layers DROPPED! +2025-04-20 20:34:40.228928 --- INFO --- linked_items DROPPED! +2025-04-20 20:34:40.233942 --- INFO --- transactions DROPPED! +2025-04-20 20:34:40.238435 --- INFO --- brands DROPPED! +2025-04-20 20:34:40.243588 --- INFO --- food_info DROPPED! +2025-04-20 20:34:40.248615 --- INFO --- logistics_info DROPPED! +2025-04-20 20:34:40.252808 --- INFO --- zones DROPPED! +2025-04-20 20:34:40.258575 --- INFO --- locations DROPPED! +2025-04-20 20:34:40.262926 --- INFO --- vendors DROPPED! +2025-04-20 20:34:40.267004 --- INFO --- group_items DROPPED! +2025-04-20 20:34:40.272103 --- INFO --- groups DROPPED! +2025-04-20 20:34:40.276648 --- INFO --- receipt_items DROPPED! +2025-04-20 20:34:40.281217 --- INFO --- receipts DROPPED! +2025-04-20 20:34:40.287034 --- INFO --- recipe_items DROPPED! +2025-04-20 20:34:40.291009 --- INFO --- recipes DROPPED! +2025-04-20 20:34:40.295365 --- INFO --- shopping_list_items DROPPED! +2025-04-20 20:34:40.301096 --- INFO --- shopping_lists DROPPED! +2025-04-20 20:34:40.305141 --- INFO --- item_locations DROPPED! +2025-04-20 20:34:40.310973 --- INFO --- conversions DROPPED! +2025-04-20 21:03:22.361206 --- INFO --- logins Created! +2025-04-20 21:03:22.368210 --- INFO --- sites Created! +2025-04-20 21:03:22.372737 --- INFO --- roles Created! +2025-04-20 21:03:22.376787 --- INFO --- units Created! +2025-04-20 21:03:22.384390 --- INFO --- cost_layers Created! +2025-04-20 21:03:22.392461 --- INFO --- linked_items Created! +2025-04-20 21:03:22.397727 --- INFO --- brands Created! +2025-04-20 21:03:22.403723 --- INFO --- food_info Created! +2025-04-20 21:03:22.411420 --- INFO --- item_info Created! +2025-04-20 21:03:22.418983 --- INFO --- zones Created! +2025-04-20 21:03:22.426220 --- INFO --- locations Created! +2025-04-20 21:03:22.433434 --- INFO --- logistics_info Created! +2025-04-20 21:03:22.440189 --- INFO --- transactions Created! +2025-04-20 21:03:22.448293 --- INFO --- item Created! +2025-04-20 21:03:22.455434 --- INFO --- vendors Created! +2025-04-20 21:03:22.462439 --- INFO --- groups Created! +2025-04-20 21:03:22.469950 --- INFO --- group_items Created! +2025-04-20 21:03:22.477434 --- INFO --- receipts Created! +2025-04-20 21:03:22.484434 --- INFO --- receipt_items Created! +2025-04-20 21:03:22.491212 --- INFO --- recipes Created! +2025-04-20 21:03:22.499338 --- INFO --- recipe_items Created! +2025-04-20 21:03:22.506939 --- INFO --- shopping_lists Created! +2025-04-20 21:03:22.514674 --- INFO --- shopping_list_items Created! +2025-04-20 21:03:22.522661 --- INFO --- item_locations Created! +2025-04-20 21:03:22.529320 --- INFO --- conversions Created! +2025-04-20 21:03:22.533343 --- INFO --- Admin User Created! +2025-04-20 21:07:46.103441 --- INFO --- item_info DROPPED! +2025-04-20 21:07:46.113647 --- INFO --- items DROPPED! +2025-04-20 21:07:46.119615 --- INFO --- cost_layers DROPPED! +2025-04-20 21:07:46.125899 --- INFO --- linked_items DROPPED! +2025-04-20 21:07:46.130623 --- INFO --- transactions DROPPED! +2025-04-20 21:07:46.134817 --- INFO --- brands DROPPED! +2025-04-20 21:07:46.139474 --- INFO --- food_info DROPPED! +2025-04-20 21:07:46.145048 --- INFO --- logistics_info DROPPED! +2025-04-20 21:07:46.150355 --- INFO --- zones DROPPED! +2025-04-20 21:07:46.155777 --- INFO --- locations DROPPED! +2025-04-20 21:07:46.160313 --- INFO --- vendors DROPPED! +2025-04-20 21:07:46.165662 --- INFO --- group_items DROPPED! +2025-04-20 21:07:46.169670 --- INFO --- groups DROPPED! +2025-04-20 21:07:46.174114 --- INFO --- receipt_items DROPPED! +2025-04-20 21:07:46.179634 --- INFO --- receipts DROPPED! +2025-04-20 21:07:46.184753 --- INFO --- recipe_items DROPPED! +2025-04-20 21:07:46.189291 --- INFO --- recipes DROPPED! +2025-04-20 21:07:46.193588 --- INFO --- shopping_list_items DROPPED! +2025-04-20 21:07:46.197995 --- INFO --- shopping_lists DROPPED! +2025-04-20 21:07:46.203577 --- INFO --- item_locations DROPPED! +2025-04-20 21:07:46.208195 --- INFO --- conversions DROPPED! diff --git a/process.py b/process.py index e876eda..2eee402 100644 --- a/process.py +++ b/process.py @@ -5,10 +5,12 @@ import postsqldb def dropSiteTables(conn, site_manager: MyDataclasses.SiteManager): try: for table in site_manager.drop_order: + print(table) database.__dropTable(conn, site_manager.site_name, table) with open("process.log", "a+") as file: file.write(f"{datetime.datetime.now()} --- INFO --- {table} DROPPED!\n") except Exception as error: + print(error) raise error def setupSiteTables(conn, site_manager: MyDataclasses.SiteManager): @@ -72,6 +74,7 @@ def deleteSite(site_manager: MyDataclasses.SiteManager): site = database.deleteSitesTuple(conn, site_manager.site_name, (site['id'], ), convert=True) + conn.commit() except Exception as error: with open("process.log", "a+") as file: file.write(f"{datetime.datetime.now()} --- ERROR --- {error}\n") @@ -97,19 +100,19 @@ def addSite(site_manager: MyDataclasses.SiteManager): site_owner_id=admin_user['id'] ) site = database.insertSitesTuple(conn, site.payload(), convert=True) - + print("site", site) role = MyDataclasses.RolePayload("Admin", f"Admin for {site['site_name']}", site['id']) role = database.insertRolesTuple(conn, role.payload(), convert=True) - + print("role", role) admin_user = database.updateAddLoginSitesRoles(conn, (site["id"], role["id"], admin_user["id"]), convert=True) - - default_zone = MyDataclasses.ZonePayload(site_manager.default_zone, site['id']) + print('admin_user', admin_user) + default_zone = postsqldb.ZonesTable.Payload(site_manager.default_zone) default_zone = database.insertZonesTuple(conn, site["site_name"], default_zone.payload(), convert=True) - + print('default_zone', default_zone) uuid = f"{site_manager.default_zone}@{site_manager.default_location}" - default_location = MyDataclasses.LocationPayload(uuid, site_manager.default_location, default_zone['id']) + default_location = postsqldb.LocationsTable.Payload(uuid, site_manager.default_location, default_zone['id']) default_location = database.insertLocationsTuple(conn, site['site_name'], default_location.payload(), convert=True) - + print('default_location', default_location) # need to update the default zones/locations for site. payload = { 'id': site['id'], @@ -120,8 +123,8 @@ def addSite(site_manager: MyDataclasses.SiteManager): database.__updateTuple(conn, site_manager.site_name, f"sites", payload) - blank_vendor = MyDataclasses.VendorPayload("None", admin_user['id']) - blank_brand = MyDataclasses.BrandsPayload("None") + blank_vendor = postsqldb.VendorsTable.Payload("None", admin_user['id']) + blank_brand = postsqldb.BrandsTable.Payload("None") blank_vendor = database.insertVendorsTuple(conn, site['site_name'], blank_vendor.payload(), convert=True) blank_brand = database.insertBrandsTuple(conn, site['site_name'], blank_brand.payload(), convert=True) diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scripts/__pycache__/__init__.cpython-312.pyc b/scripts/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..d3088b4 Binary files /dev/null and b/scripts/__pycache__/__init__.cpython-312.pyc differ diff --git a/scripts/__pycache__/postsqldb.cpython-312.pyc b/scripts/__pycache__/postsqldb.cpython-312.pyc new file mode 100644 index 0000000..8875f31 Binary files /dev/null and b/scripts/__pycache__/postsqldb.cpython-312.pyc differ diff --git a/scripts/postsqldb.py b/scripts/postsqldb.py new file mode 100644 index 0000000..81ee6bf --- /dev/null +++ b/scripts/postsqldb.py @@ -0,0 +1,2346 @@ +import datetime +import psycopg2, json +import psycopg2.extras +from dataclasses import dataclass, field +import random +import string + +class DatabaseError(Exception): + def __init__(self, message, payload=[], sql=""): + super().__init__(message) + self.payload = payload + self.message = str(message).replace("\n", "") + self.sql = sql.replace("\n", "") + self.log_error() + + def log_error(self): + with open("database.log", "a+") as file: + file.write("\n") + file.write(f"{datetime.datetime.now()} --- ERROR --- DatabaseError(message='{self.message}',\n") + file.write(f"{" "*41}payload={self.payload},\n") + file.write(f"{" "*41}sql='{self.sql}')") + + def __str__(self): + return f"DatabaseError(message='{self.message}', payload={self.payload}, sql='{self.sql}')" + +def tupleDictionaryFactory(columns, row): + columns = [desc[0] for desc in columns] + return dict(zip(columns, row)) + +def lst2pgarr(alist): + return '{' + ','.join(alist) + '}' + +def updateStringFactory(updated_values: dict): + set_clause = ', '.join([f"{key} = %s" for key in updated_values.keys()]) + values = [] + for value in updated_values.values(): + if isinstance(value, dict): + value = json.dumps(value) + values.append(value) + + return set_clause, values + +def getUUID(n): + random_string = ''.join(random.choices(string.ascii_letters + string.digits, k=n)) + return random_string + +class ConversionsTable: + @dataclass + class Payload: + item_id: int + uom_id: int + conv_factor: float + + def payload(self): + return ( + self.item_id, + self.uom_id, + self.conv_factor + ) + + @classmethod + def create_table(self, conn, site): + with open(f"sql/CREATE/conversions.sql", 'r') as file: + sql = file.read().replace("%%site_name%%", site) + try: + with conn.cursor() as cur: + cur.execute(sql) + except Exception as error: + raise DatabaseError(error, sql, "PrefixTable") + + @classmethod + def delete_table(self, conn, site): + with open(f"sql/DROP/conversions.sql", 'r') as file: + sql = file.read().replace("%%site_name%%", site) + try: + with conn.cursor() as cur: + cur.execute(sql) + except Exception as error: + raise DatabaseError(error, 'ConversionsTable', sql) + + @classmethod + def insert_tuple(self, conn, site: str, payload: list, convert=True): + """insert into recipes table for site + + Args: + conn (_T_connector@connect): Postgresql Connector + site (stre): + payload (tuple): (item_id, uom_id, conversion_factor) + convert (bool, optional): Determines if to return tuple as a dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: inserted tuple + """ + record = () + with open(f"sql/INSERT/insertConversionsTuple.sql", "r+") as file: + sql = file.read().replace("%%site_name%%", site) + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + record = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + record = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return record + + + @classmethod + def delete_item_tuple(self, conn, site_name, payload, convert=True): + """This is a basic funtion to delete a tuple from a table in site with an id. All + tables in this database has id's associated with them. + + Args: + conn (_T_connector@connect): Postgresql Connector + site_name (str): + payload (tuple): (tuple_id,...) + convert (bool, optional): Determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: deleted tuple + """ + deleted = () + sql = f"WITH deleted_rows AS (DELETE FROM {site_name}_conversions WHERE id IN ({','.join(['%s'] * len(payload))}) RETURNING *) SELECT * FROM deleted_rows;" + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + deleted = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + deleted = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return deleted + + @classmethod + def update_item_tuple(self, conn, site, payload, convert=False): + """_summary_ + + Args: + conn (_T_connector@connect): Postgresql Connector + site (str): + table (str): + payload (dict): {'id': row_id, 'update': {... column_to_update: value_to_update_to...}} + convert (bool, optional): determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: updated tuple + """ + updated = () + + set_clause, values = updateStringFactory(payload['update']) + values.append(payload['id']) + sql = f"UPDATE {site}_conversions SET {set_clause} WHERE id=%s RETURNING *;" + try: + with conn.cursor() as cur: + cur.execute(sql, values) + rows = cur.fetchone() + if rows and convert: + updated = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + updated = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return updated + +class ShoppingListsTable: + @dataclass + class Payload: + name: str + description: str + author: int + type: str = "plain" + creation_date: datetime.datetime = field(init=False) + + def __post_init__(self): + self.creation_date = datetime.datetime.now() + + def payload(self): + return ( + self.name, + self.description, + self.author, + self.creation_date, + self.type + ) + + @dataclass + class ItemPayload: + uuid: str + sl_id: int + item_type: str + item_name: str + uom: str + qty: float + item_id: int = None + links: dict = field(default_factory=dict) + + def payload(self): + return ( + self.uuid, + self.sl_id, + self.item_type, + self.item_name, + self.uom, + self.qty, + self.item_id, + json.dumps(self.links) + ) + + @classmethod + def getItem(self, conn, site, payload, convert=True): + """_summary_ + + Args: + conn (_type_): _description_ + site (_type_): _description_ + payload (_type_): (id, ) + convert (bool, optional): _description_. Defaults to True. + + Raises: + DatabaseError: _description_ + + Returns: + _type_: _description_ + """ + record = () + with open('sql/SELECT/selectShoppingListItem.sql', 'r') as file: + sql = file.read().replace("%%site_name%%", site) + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + record = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + record = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return record + +class UnitsTable: + @dataclass + class Payload: + __slots__ = ('plural', 'single', 'fullname', 'description') + + plural: str + single: str + fullname: str + description: str + + def payload(self): + return ( + self.plural, + self.single, + self.fullname, + self.description + ) + + @classmethod + def create_table(self, conn): + with open(f"sql/CREATE/units.sql", 'r') as file: + sql = file.read() + try: + with conn.cursor() as cur: + cur.execute(sql) + except Exception as error: + raise DatabaseError(error, sql, "UnitsTable") + + @classmethod + def delete_table(self, conn): + with open(f"sql/DROP/units.sql", 'r') as file: + sql = file.read() + try: + with conn.cursor() as cur: + cur.execute(sql) + except Exception as error: + raise DatabaseError(error, 'PrefixTable', sql) + + @classmethod + def insert_tuple(self, conn, payload: list, convert=True): + """_summary_ + + Args: + conn (_type_): _description_ + payload (list): (plural, single, fullname, description) + convert (bool, optional): _description_. Defaults to True. + + Raises: + DatabaseError: _description_ + + Returns: + _type_: _description_ + """ + record = () + with open(f"sql/INSERT/insertUnitsTuple.sql", "r+") as file: + sql = file.read() + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + record = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + record = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return record + + @classmethod + def getAll(self, conn, convert=True): + """_summary_ + + Args: + conn (_type_): _description_ + convert (bool, optional): _description_. Defaults to True. + + Raises: + DatabaseError: _description_ + + Returns: + _type_: _description_ + """ + records = () + sql = f"SELECT * FROM units;" + try: + with conn.cursor() as cur: + cur.execute(sql) + rows = cur.fetchall() + if rows and convert: + records = [tupleDictionaryFactory(cur.description, row) for row in rows] + elif rows and not convert: + records = rows + except Exception as error: + raise DatabaseError(error, "", sql) + return records + +class SKUPrefixTable: + @dataclass + class Payload: + __slots__ = ('uuid', 'name', 'description') + + uuid: str + name: str + description: str + + def payload(self): + return ( + self.uuid, + self.name, + self.description + ) + + @classmethod + def create_table(self, conn, site): + with open(f"sql/CREATE/sku_prefix.sql", 'r') as file: + sql = file.read().replace("%%site_name%%", site) + try: + with conn.cursor() as cur: + cur.execute(sql) + except Exception as error: + raise DatabaseError(error, sql, "PrefixTable") + + @classmethod + def delete_table(self, conn, site): + with open(f"sql/DROP/sku_prefix.sql", 'r') as file: + sql = file.read().replace("%%site_name%%", site) + try: + with conn.cursor() as cur: + cur.execute(sql) + except Exception as error: + raise DatabaseError(error, 'PrefixTable', sql) + + @classmethod + def paginatePrefixes(self, conn, site: str, payload: tuple, convert=True): + """_summary_ + + Args: + conn (_type_): _description_ + site (_type_): _description_ + payload (_type_): (limit, offset) + convert (bool, optional): _description_. Defaults to True. + + Raises: + DatabaseError: _description_ + + Returns: + _type_: _description_ + """ + recordset = [] + count = 0 + with open(f"sql/SELECT/getSkuPrefixes.sql", "r+") as file: + sql = file.read().replace("%%site_name%%", site) + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchall() + if rows and convert: + recordset = [tupleDictionaryFactory(cur.description, row) for row in rows] + if rows and not convert: + recordset = rows + + cur.execute(f"SELECT COUNT(*) FROM {site}_sku_prefix;") + count = cur.fetchone()[0] + except (Exception, psycopg2.DatabaseError) as error: + raise DatabaseError(error, payload, sql) + return recordset, count + + @classmethod + def insert_tuple(self, conn, site, payload, convert=True): + """insert payload into zones table of site + + Args: + conn (_T_connector@connect): Postgresql Connector + site (str): + payload (tuple): (name[str],) + convert (bool, optional): Determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: inserted tuple + """ + prefix = () + with open(f"sql/INSERT/insertSKUPrefixTuple.sql", "r+") as file: + sql = file.read().replace("%%site_name%%", site) + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + prefix = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + prefix = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return prefix + + @classmethod + def update_tuple(self, conn, site, payload, convert=True): + """_summary_ + + Args: + conn (_T_connector@connect): Postgresql Connector + site (str): + table (str): + payload (dict): {'id': row_id, 'update': {... column_to_update: value_to_update_to...}} + convert (bool, optional): determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: updated tuple + """ + updated = () + + set_clause, values = updateStringFactory(payload['update']) + values.append(payload['id']) + sql = f"UPDATE {site}_sku_prefix SET {set_clause} WHERE id=%s RETURNING *;" + try: + with conn.cursor() as cur: + cur.execute(sql, values) + rows = cur.fetchone() + if rows and convert: + updated = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + updated = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return updated + +class RecipesTable: + @dataclass + class Payload: + #__slots__ = ('name', 'author', 'description', 'created_date', 'instructions', 'picture_path') + + name: str + author: int + description: str + created_date: datetime = field(init=False) + instructions: list = field(default_factory=list) + picture_path: str = "" + + def __post_init__(self): + self.created_date = datetime.datetime.now() + + def payload(self): + return ( + self.name, + self.author, + self.description, + self.created_date, + lst2pgarr(self.instructions), + self.picture_path + ) + + @dataclass + class ItemPayload: + uuid: str + rp_id: int + item_type: str + item_name:str + uom: int + qty: float = 0.0 + item_id: int = None + links: dict = field(default_factory=dict) + + def payload(self): + return ( + self.uuid, + self.rp_id, + self.item_type, + self.item_name, + self.uom, + self.qty, + self.item_id, + json.dumps(self.links) + ) + + @classmethod + def create_table(self, conn, site): + with open(f"sql/CREATE/recipes.sql", 'r') as file: + sql = file.read().replace("%%site_name%%", site) + try: + with conn.cursor() as cur: + cur.execute(sql) + except Exception as error: + raise DatabaseError(error, sql, "PrefixTable") + + @classmethod + def delete_table(self, conn, site): + with open(f"sql/DROP/recipes.sql", 'r') as file: + sql = file.read().replace("%%site_name%%", site) + try: + with conn.cursor() as cur: + cur.execute(sql) + except Exception as error: + raise DatabaseError(error, 'PrefixTable', sql) + + @classmethod + def insert_tuple(self, conn, site: str, payload: list, convert=True): + """insert into recipes table for site + + Args: + conn (_T_connector@connect): Postgresql Connector + site (stre): + payload (tuple): (name[str], author[int], description[str], creation_date[timestamp], instructions[list], picture_path[str]) + convert (bool, optional): Determines if to return tuple as a dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: inserted tuple + """ + recipe = () + with open(f"sql/INSERT/insertRecipesTuple.sql", "r+") as file: + sql = file.read().replace("%%site_name%%", site) + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + recipe = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + recipe = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return recipe + + @classmethod + def insert_item_tuple(self, conn, site, payload, convert=True): + """insert into recipe_items table for site + + Args: + conn (_T_connector@connect): Postgresql Connector + site (stre): + payload (tuple): (uuid[str], rp_id[int], item_type[str], item_name[str], uom[str], qty[float], item_id[int], links[jsonb]) + convert (bool, optional): Determines if to return tuple as a dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: inserted tuple + """ + recipe_item = () + with open(f"sql/INSERT/insertRecipeItemsTuple.sql", "r+") as file: + sql = file.read().replace("%%site_name%%", site) + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + recipe_item = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + recipe_item = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return recipe_item + + @classmethod + def delete_item_tuple(self, conn, site_name, payload, convert=True): + """This is a basic funtion to delete a tuple from a table in site with an id. All + tables in this database has id's associated with them. + + Args: + conn (_T_connector@connect): Postgresql Connector + site_name (str): + payload (tuple): (tuple_id,...) + convert (bool, optional): Determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: deleted tuple + """ + deleted = () + sql = f"WITH deleted_rows AS (DELETE FROM {site_name}_recipe_items WHERE id IN ({','.join(['%s'] * len(payload))}) RETURNING *) SELECT * FROM deleted_rows;" + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + deleted = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + deleted = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return deleted + + @classmethod + def update_item_tuple(self, conn, site, payload, convert=False): + """_summary_ + + Args: + conn (_T_connector@connect): Postgresql Connector + site (str): + table (str): + payload (dict): {'id': row_id, 'update': {... column_to_update: value_to_update_to...}} + convert (bool, optional): determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: updated tuple + """ + updated = () + + set_clause, values = updateStringFactory(payload['update']) + values.append(payload['id']) + sql = f"UPDATE {site}_recipe_items SET {set_clause} WHERE id=%s RETURNING *;" + try: + with conn.cursor() as cur: + cur.execute(sql, values) + rows = cur.fetchone() + if rows and convert: + updated = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + updated = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return updated + + @classmethod + def getRecipes(self, conn, site: str, payload: tuple, convert=True): + """_summary_ + + Args: + conn (_type_): _description_ + site (_type_): _description_ + payload (_type_): (limit, offset) + convert (bool, optional): _description_. Defaults to True. + + Raises: + DatabaseError: _description_ + + Returns: + _type_: _description_ + """ + recordset = [] + count = 0 + with open(f"sql/SELECT/getRecipes.sql", "r+") as file: + sql = file.read().replace("%%site_name%%", site) + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchall() + if rows and convert: + recordset = [tupleDictionaryFactory(cur.description, row) for row in rows] + if rows and not convert: + recordset = rows + + cur.execute(f"SELECT COUNT(*) FROM {site}_recipes;") + count = cur.fetchone()[0] + except (Exception, psycopg2.DatabaseError) as error: + raise DatabaseError(error, payload, sql) + return recordset, count + + @classmethod + def getRecipe(self, conn, site: str, payload: tuple, convert=True): + """_summary_ + + Args: + conn (_type_): _description_ + site (_type_): _description_ + payload (_type_): (id, ) + convert (bool, optional): _description_. Defaults to True. + + Raises: + DatabaseError: _description_ + + Returns: + _type_: _description_ + """ + record = () + with open(f"sql/SELECT/getRecipeByID.sql", "r+") as file: + sql = file.read().replace("%%site_name%%", site) + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + record = tupleDictionaryFactory(cur.description, rows) + if rows and not convert: + record = rows + except (Exception, psycopg2.DatabaseError) as error: + raise DatabaseError(error, payload, sql) + return record + + @classmethod + def updateRecipe(self, conn, site, payload, convert=True): + """_summary_ + + Args: + conn (_T_connector@connect): Postgresql Connector + site (str): + table (str): + payload (dict): {'id': row_id, 'update': {... column_to_update: value_to_update_to...}} + convert (bool, optional): determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: updated tuple + """ + updated = () + + set_clause, values = updateStringFactory(payload['update']) + values.append(payload['id']) + sql = f"UPDATE {site}_recipes SET {set_clause} WHERE id=%s RETURNING *;" + try: + with conn.cursor() as cur: + cur.execute(sql, values) + rows = cur.fetchone() + if rows and convert: + updated = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + updated = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return updated + +class ItemInfoTable: + @dataclass + class Payload: + barcode: str + packaging: str = "" + uom_quantity: float = 1.0 + uom: int = 1 + cost: float = 0.0 + safety_stock: float = 0.0 + lead_time_days: float = 0.0 + ai_pick: bool = False + prefixes: list = field(default_factory=list) + + def __post_init__(self): + if not isinstance(self.barcode, str): + raise TypeError(f"barcode must be of type str; not {type(self.barcode)}") + + def payload(self): + return ( + self.barcode, + self.packaging, + self.uom_quantity, + self.uom, + self.cost, + self.safety_stock, + self.lead_time_days, + self.ai_pick, + lst2pgarr(self.prefixes) + ) + @classmethod + def select_tuple(self, conn, site:str, payload:tuple, convert=True): + """_summary_ + + Args: + conn (_type_): _description_ + site (_type_): _description_ + payload (_type_): (item_info_id,) + convert (bool, optional): _description_. Defaults to True. + + Raises: + DatabaseError: _description_ + + Returns: + _type_: _description_ + """ + selected = () + sql = f"SELECT * FROM {site}_item_info WHERE id=%s;" + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + selected = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + selected = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return selected + + @classmethod + def update_tuple(self, conn, site:str, payload: dict, convert=True): + """_summary_ + + Args: + conn (_T_connector@connect): Postgresql Connector + site (str): + table (str): + payload (dict): {'id': row_id, 'update': {... column_to_update: value_to_update_to...}} + convert (bool, optional): determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: updated tuple + """ + updated = () + + set_clause, values = updateStringFactory(payload['update']) + values.append(payload['id']) + sql = f"UPDATE {site}_item_info SET {set_clause} WHERE id=%s RETURNING *;" + try: + with conn.cursor() as cur: + cur.execute(sql, values) + rows = cur.fetchone() + if rows and convert: + updated = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + updated = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return updated + +class ItemTable: + + @classmethod + def paginateLinkedLists(self, conn, site:str, payload:tuple, convert=True): + records = [] + count = 0 + + sql = f"SELECT * FROM {site}_items WHERE row_type = 'list' LIMIT %s OFFSET %s;" + sql_count = f"SELECT COUNT(*) FROM {site}_items WHERE row_type = 'list' LIMIT %s OFFSET %s;" + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchall() + if rows and convert: + records = [tupleDictionaryFactory(cur.description, row) for row in rows] + if rows and not convert: + records = rows + + cur.execute(sql_count, payload) + count = cur.fetchone()[0] + + except (Exception, psycopg2.DatabaseError) as error: + raise DatabaseError(error, payload, sql) + return records, count + + @classmethod + def getItemAllByID(self, conn, site, payload, convert=True): + """_summary_ + + Args: + conn (_type_): _description_ + site (_type_): _description_ + payload (_type_): (item_id, ) + convert (bool, optional): _description_. Defaults to False. + + Raises: + DatabaseError: _description_ + + Returns: + _type_: _description_ + """ + item = () + with open(f"sql/SELECT/getItemAllByID.sql", "r+") as file: + getItemAllByID_sql = file.read().replace("%%site_name%%", site) + try: + with conn.cursor() as cur: + cur.execute(getItemAllByID_sql, payload) + rows = cur.fetchone() + if rows and convert: + item = tupleDictionaryFactory(cur.description, rows) + if rows and not convert: + item = rows + except (Exception, psycopg2.DatabaseError) as error: + raise DatabaseError(error, payload, getItemAllByID_sql) + return item + + @classmethod + def getLinkedItemByBarcode(self, conn, site, payload, convert=True): + item = () + sql = f"SELECT * FROM {site}_itemlinks WHERE barcode=%s;" + if convert: + item = {} + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + item = tupleDictionaryFactory(cur.description, rows) + if rows and not convert: + item = rows + except (Exception, psycopg2.DatabaseError) as error: + raise DatabaseError(error, payload, sql) + return item + + @classmethod + def getItemAllByBarcode(self, conn, site, payload, convert=True): + item = () + if convert: + item = {} + linked_item = self.getLinkedItemByBarcode(conn, site, (payload[0],)) + + if len(linked_item) > 1: + item = self.getItemAllByID(conn, site, payload=(linked_item['link'], ), convert=convert) + item['item_info']['uom_quantity'] = linked_item['conv_factor'] + else: + with open(f"sql/SELECT/getItemAllByBarcode.sql", "r+") as file: + getItemAllByBarcode_sql = file.read().replace("%%site_name%%", site) + try: + with conn.cursor() as cur: + cur.execute(getItemAllByBarcode_sql, payload) + rows = cur.fetchone() + if rows and convert: + item = tupleDictionaryFactory(cur.description, rows) + if rows and not convert: + item = rows + except (Exception, psycopg2.DatabaseError) as error: + raise DatabaseError(error, payload, getItemAllByBarcode_sql) + return item + + @classmethod + def update_tuple(self, conn, site, payload, convert=True): + """_summary_ + + Args: + conn (_T_connector@connect): Postgresql Connector + site (str): + table (str): + payload (dict): {'id': row_id, 'update': {... column_to_update: value_to_update_to...}} + convert (bool, optional): determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: updated tuple + """ + updated = () + + set_clause, values = updateStringFactory(payload['update']) + values.append(payload['id']) + sql = f"UPDATE {site}_items SET {set_clause} WHERE id=%s RETURNING *;" + try: + with conn.cursor() as cur: + cur.execute(sql, values) + rows = cur.fetchone() + if rows and convert: + updated = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + updated = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return updated + +class ReceiptTable: + + @classmethod + def update_receipt(self, conn, site:str, payload:dict, convert=True): + """_summary_ + + Args: + conn (_T_connector@connect): Postgresql Connector + site (str): + table (str): + payload (dict): {'id': row_id, 'update': {... column_to_update: value_to_update_to...}} + convert (bool, optional): determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: updated tuple + """ + updated = () + + set_clause, values = updateStringFactory(payload['update']) + values.append(payload['id']) + sql = f"UPDATE {site}_receipts SET {set_clause} WHERE id=%s RETURNING *;" + try: + with conn.cursor() as cur: + cur.execute(sql, values) + rows = cur.fetchone() + if rows and convert: + updated = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + updated = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return updated + + @classmethod + def update_receipt_item(self, conn, site:str, payload:dict, convert=True): + """_summary_ + + Args: + conn (_T_connector@connect): Postgresql Connector + site (str): + table (str): + payload (dict): {'id': row_id, 'update': {... column_to_update: value_to_update_to...}} + convert (bool, optional): determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: updated tuple + """ + updated = () + + set_clause, values = updateStringFactory(payload['update']) + values.append(payload['id']) + sql = f"UPDATE {site}_receipt_items SET {set_clause} WHERE id=%s RETURNING *;" + try: + with conn.cursor() as cur: + cur.execute(sql, values) + rows = cur.fetchone() + if rows and convert: + updated = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + updated = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return updated + + @classmethod + def select_tuple(self, conn, site:str, payload:tuple, convert=True): + """_summary_ + + Args: + conn (_type_): _description_ + site (_type_): _description_ + payload (_type_): (receipt_id,) + convert (bool, optional): _description_. Defaults to True. + + Raises: + DatabaseError: _description_ + + Returns: + _type_: _description_ + """ + selected = () + sql = f"SELECT * FROM {site}_receipts WHERE id=%s;" + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + selected = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + selected = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return selected + + @classmethod + def select_item_tuple(self, conn, site:str, payload:tuple, convert=True): + """_summary_ + + Args: + conn (_type_): _description_ + site (_type_): _description_ + payload (_type_): (receipt_id,) + convert (bool, optional): _description_. Defaults to True. + + Raises: + DatabaseError: _description_ + + Returns: + _type_: _description_ + """ + selected = () + sql = f"SELECT * FROM {site}_receipt_items WHERE id=%s;" + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + selected = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + selected = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return selected + +class ZonesTable: + @dataclass + class Payload: + name: str + description: str = "" + + def __post_init__(self): + if not isinstance(self.name, str): + raise TypeError(f"Zone name should be of type str; not {type(self.name)}") + + def payload(self): + return ( + self.name, + self.description, + ) + + @classmethod + def insert_tuple(self, conn, site, payload, convert=True): + """insert payload into zones table of site + + Args: + conn (_T_connector@connect): Postgresql Connector + site (str): + payload (tuple): (name[str],) + convert (bool, optional): Determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: inserted tuple + """ + zone = () + with open(f"sql/INSERT/insertZonesTuple.sql", "r+") as file: + sql = file.read().replace("%%site_name%%", site) + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + zone = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + zone = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return zone + + @classmethod + def update_tuple(self, conn, site, payload, convert=True): + """_summary_ + + Args: + conn (_T_connector@connect): Postgresql Connector + site (str): + table (str): + payload (dict): {'id': row_id, 'update': {... column_to_update: value_to_update_to...}} + convert (bool, optional): determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: updated tuple + """ + updated = () + + set_clause, values = updateStringFactory(payload['update']) + values.append(payload['id']) + sql = f"UPDATE {site}_zones SET {set_clause} WHERE id=%s RETURNING *;" + try: + with conn.cursor() as cur: + cur.execute(sql, values) + rows = cur.fetchone() + if rows and convert: + updated = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + updated = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return updated + + @classmethod + def paginateZones(self, conn, site:str, payload:tuple, convert=True): + """_summary_ + + Args: + conn (_type_): _description_ + site (str): _description_ + payload (tuple): (limit, offset) + convert (bool, optional): _description_. Defaults to True. + + Raises: + DatabaseError: _description_ + + Returns: + _type_: _description_ + """ + recordset = () + count = 0 + sql = f"SELECT * FROM {site}_zones LIMIT %s OFFSET %s;" + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchall() + if rows and convert: + recordset = [tupleDictionaryFactory(cur.description, row) for row in rows] + elif rows and not convert: + recordset = rows + + cur.execute(f"SELECT COUNT(*) FROM {site}_zones;") + count = cur.fetchone()[0] + except Exception as error: + raise DatabaseError(error, (), sql) + return recordset, count + + @classmethod + def paginateZonesBySku(self, conn, site: str, payload: tuple, convert=True): + zones = () + count = 0 + with open(f"sql/SELECT/zones/paginateZonesBySku.sql", "r+") as file: + sql = file.read().replace("%%site_name%%", site) + with open(f"sql/SELECT/zones/paginateZonesBySkuCount.sql", "r+") as file: + sql_count = file.read().replace("%%site_name%%", site) + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchall() + if rows and convert: + zones = [tupleDictionaryFactory(cur.description, row) for row in rows] + elif rows and not convert: + zones = rows + + cur.execute(sql_count, payload) + count = cur.fetchone()[0] + + except Exception as error: + raise DatabaseError(error, payload, sql) + return zones, count + +class LocationsTable: + @dataclass + class Payload: + uuid: str + name: str + zone_id: int + + def __post_init__(self): + if not isinstance(self.uuid, str): + raise TypeError(f"uuid must be of type str; not {type(self.uuid)}") + if not isinstance(self.name, str): + raise TypeError(f"Location name must be of type str; not {type(self.name)}") + if not isinstance(self.zone_id, int): + raise TypeError(f"zone_id must be of type str; not {type(self.zone_id)}") + + def payload(self): + return ( + self.uuid, + self.name, + self.zone_id + ) + + @classmethod + def paginateLocations(self, conn, site:str, payload:tuple, convert=True): + """_summary_ + + Args: + conn (_type_): _description_ + site (str): _description_ + payload (tuple): (limit, offset) + convert (bool, optional): _description_. Defaults to True. + + Raises: + DatabaseError: _description_ + + Returns: + _type_: _description_ + """ + recordset = () + count = 0 + sql = f"SELECT * FROM {site}_locations LIMIT %s OFFSET %s;" + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchall() + if rows and convert: + recordset = [tupleDictionaryFactory(cur.description, row) for row in rows] + elif rows and not convert: + recordset = rows + + cur.execute(f"SELECT COUNT(*) FROM {site}_locations;") + count = cur.fetchone()[0] + except Exception as error: + raise DatabaseError(error, (), sql) + return recordset, count + + @classmethod + def paginateLocationsWithZone(self, conn, site:str, payload:tuple, convert=True): + """_summary_ + + Args: + conn (_type_): _description_ + site (str): _description_ + payload (tuple): (limit, offset) + convert (bool, optional): _description_. Defaults to True. + + Raises: + DatabaseError: _description_ + + Returns: + _type_: _description_ + """ + recordset = () + count = 0 + with open(f"sql/SELECT/getLocationsWithZone.sql", "r+") as file: + sql = file.read().replace("%%site_name%%", site) + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchall() + if rows and convert: + recordset = [tupleDictionaryFactory(cur.description, row) for row in rows] + elif rows and not convert: + recordset = rows + cur.execute(f"SELECT COUNT(*) FROM {site}_locations;") + count = cur.fetchone()[0] + except Exception as error: + raise DatabaseError(error, (), sql) + return recordset, count + + @classmethod + def paginateLocationsBySkuZone(self, conn, site: str, payload: tuple, convert=True): + """_summary_ + + Args: + conn (_type_): _description_ + site (str): _description_ + payload (tuple): (item_id, zone_id, limit, offset) + convert (bool, optional): _description_. Defaults to True. + + Raises: + DatabaseError: _description_ + + Returns: + _type_: _description_ + """ + locations = () + count = 0 + with open(f"sql/SELECT/locations/paginateLocationsBySkuZone.sql", "r+") as file: + sql = file.read().replace("%%site_name%%", site) + with open(f"sql/SELECT/locations/paginateLocationsBySkuZoneCount.sql", "r+") as file: + sql_count = file.read().replace("%%site_name%%", site) + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchall() + if rows and convert: + locations = [tupleDictionaryFactory(cur.description, row) for row in rows] + elif rows and not convert: + locations = rows + + cur.execute(sql_count, payload) + count = cur.fetchone()[0] + + except Exception as error: + raise DatabaseError(error, payload, sql) + return locations, count + + @classmethod + def insert_tuple(self, conn, site, payload, convert=True): + """insert payload into zones table of site + + Args: + conn (_T_connector@connect): Postgresql Connector + site (str): + payload (tuple): (name[str],) + convert (bool, optional): Determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: inserted tuple + """ + zone = () + with open(f"sql/INSERT/insertLocationsTuple.sql", "r+") as file: + sql = file.read().replace("%%site_name%%", site) + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + zone = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + zone = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return zone + +class VendorsTable: + @dataclass + class Payload: + vendor_name: str + created_by: int + vendor_address: str = "" + creation_date: datetime.datetime = field(init=False) + phone_number: str = "" + + def __post_init__(self): + if not isinstance(self.vendor_name, str): + raise TypeError(f"vendor_name should be of type str; not {type(self.vendor_name)}") + self.creation_date = datetime.datetime.now() + + + def payload(self): + return ( + self.vendor_name, + self.vendor_address, + self.creation_date, + self.created_by, + self.phone_number + ) + + @classmethod + def paginateVendors(self, conn, site:str, payload:tuple, convert=True): + """_summary_ + + Args: + conn (_type_): _description_ + site (str): _description_ + payload (tuple): (limit, offset) + convert (bool, optional): _description_. Defaults to True. + + Raises: + DatabaseError: _description_ + + Returns: + _type_: _description_ + """ + recordset = () + count = 0 + sql = f"SELECT * FROM {site}_vendors LIMIT %s OFFSET %s;" + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchall() + if rows and convert: + recordset = [tupleDictionaryFactory(cur.description, row) for row in rows] + elif rows and not convert: + recordset = rows + + cur.execute(f"SELECT COUNT(*) FROM {site}_vendors;") + count = cur.fetchone()[0] + except Exception as error: + raise DatabaseError(error, (), sql) + return recordset, count + + @classmethod + def insert_tuple(self, conn, site, payload, convert=True): + """insert payload into zones table of site + + Args: + conn (_T_connector@connect): Postgresql Connector + site (str): + payload (tuple): (name[str],) + convert (bool, optional): Determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: inserted tuple + """ + zone = () + with open(f"sql/INSERT/insertVendorsTuple.sql", "r+") as file: + sql = file.read().replace("%%site_name%%", site) + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + zone = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + zone = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return zone + + @classmethod + def update_tuple(self, conn, site, payload, convert=True): + """_summary_ + + Args: + conn (_T_connector@connect): Postgresql Connector + site (str): + table (str): + payload (dict): {'id': row_id, 'update': {... column_to_update: value_to_update_to...}} + convert (bool, optional): determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: updated tuple + """ + updated = () + + set_clause, values = updateStringFactory(payload['update']) + values.append(payload['id']) + sql = f"UPDATE {site}_vendors SET {set_clause} WHERE id=%s RETURNING *;" + try: + with conn.cursor() as cur: + cur.execute(sql, values) + rows = cur.fetchone() + if rows and convert: + updated = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + updated = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return updated + +class BrandsTable: + @dataclass + class Payload: + name: str + + def __post_init__(self): + if not isinstance(self.name, str): + return TypeError(f"brand name should be of type str; not {type(self.name)}") + + def payload(self): + return ( + self.name, + ) + + @classmethod + def paginateBrands(self, conn, site:str, payload:tuple, convert=True): + """_summary_ + + Args: + conn (_type_): _description_ + site (str): _description_ + payload (tuple): (limit, offset) + convert (bool, optional): _description_. Defaults to True. + + Raises: + DatabaseError: _description_ + + Returns: + _type_: _description_ + """ + recordset = () + count = 0 + sql = f"SELECT * FROM {site}_brands LIMIT %s OFFSET %s;" + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchall() + if rows and convert: + recordset = [tupleDictionaryFactory(cur.description, row) for row in rows] + elif rows and not convert: + recordset = rows + + cur.execute(f"SELECT COUNT(*) FROM {site}_brands;") + count = cur.fetchone()[0] + except Exception as error: + raise DatabaseError(error, (), sql) + return recordset, count + + @classmethod + def insert_tuple(self, conn, site, payload, convert=True): + """insert payload into zones table of site + + Args: + conn (_T_connector@connect): Postgresql Connector + site (str): + payload (tuple): (name[str],) + convert (bool, optional): Determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: inserted tuple + """ + brand = () + with open(f"sql/INSERT/insertBrandsTuple.sql", "r+") as file: + sql = file.read().replace("%%site_name%%", site) + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + brand = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + brand = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return brand + + @classmethod + def update_tuple(self, conn, site, payload, convert=True): + """_summary_ + + Args: + conn (_T_connector@connect): Postgresql Connector + site (str): + table (str): + payload (dict): {'id': row_id, 'update': {... column_to_update: value_to_update_to...}} + convert (bool, optional): determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: updated tuple + """ + updated = () + + set_clause, values = updateStringFactory(payload['update']) + values.append(payload['id']) + sql = f"UPDATE {site}_brands SET {set_clause} WHERE id=%s RETURNING *;" + try: + with conn.cursor() as cur: + cur.execute(sql, values) + rows = cur.fetchone() + if rows and convert: + updated = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + updated = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return updated + +class ItemLocationsTable: + @dataclass + class Payload: + part_id: int + location_id: int + quantity_on_hand: float = 0.0 + cost_layers: list = field(default_factory=list) + + def __post_init__(self): + if not isinstance(self.part_id, int): + raise TypeError(f"part_id must be of type int; not {type(self.part_id)}") + if not isinstance(self.location_id, int): + raise TypeError(f"part_id must be of type int; not {type(self.part_id)}") + + def payload(self): + return ( + self.part_id, + self.location_id, + self.quantity_on_hand, + lst2pgarr(self.cost_layers) + ) + + @classmethod + def insert_tuple(self, conn, site, payload, convert=True): + """insert payload into zones table of site + + Args: + conn (_T_connector@connect): Postgresql Connector + site (str): + payload (tuple): (name[str],) + convert (bool, optional): Determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: inserted tuple + """ + item_location = () + with open(f"sql/INSERT/insertItemLocationsTuple.sql", "r+") as file: + sql = file.read().replace("%%site_name%%", site) + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + item_location = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + item_location = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return item_location + + @classmethod + def select_by_id(self, conn, site: str, payload: tuple, convert=True): + item_locations = () + with open(f"sql/SELECT/selectItemLocations", "r+") as file: + sql = file.read().replace("%%site_name%%", site) + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchall() + if rows and convert: + item_locations = [tupleDictionaryFactory(cur.description, row) for row in rows] + elif rows and not convert: + item_locations = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return item_locations + +class ItemLinksTable: + @dataclass + class Payload: + barcode: str + link: int + data: dict = field(default_factory=dict) + conv_factor: float = 1 + + def __post_init__(self): + if not isinstance(self.barcode, str): + raise TypeError(f"barcode must be of type str; not {type(self.barocde)}") + if not isinstance(self.link, int): + raise TypeError(f"link must be of type str; not {type(self.link)}") + + def payload(self): + return ( + self.barcode, + self.link, + json.dumps(self.data), + self.conv_factor + ) + + @classmethod + def insert_tuple(self, conn, site:str, payload:tuple, convert=True): + """insert payload into itemlinks table of site + + Args: + conn (_T_connector@connect): Postgresql Connector + site (str): + payload (tuple): (barcode[str], link[int], data[jsonb], conv_factor[float]) + convert (bool, optional): Determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: inserted tuple + """ + link = () + with open(f"sql/INSERT/insertItemLinksTuple.sql", "r+") as file: + sql = file.read().replace("%%site_name%%", site) + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + link = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + link = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return link + +class CycleCountsTable: + @dataclass + class Payload: + pass + +class SitesTable: + + @dataclass + class Manager: + site_name: str + admin_user: tuple + default_zone: int + default_location: int + description: str + create_order: list = field(init=False) + drop_order: list = field(init=False) + + def __post_init__(self): + self.create_order = [ + "logins", + "sites", + "roles", + "units", + "cost_layers", + "linked_items", + "brands", + "food_info", + "item_info", + "zones", + "locations", + "logistics_info", + "transactions", + "item", + "vendors", + "groups", + "group_items", + "receipts", + "receipt_items", + "recipes", + "recipe_items", + "shopping_lists", + "shopping_list_items", + "item_locations", + "conversions" + ] + self.drop_order = [ + "item_info", + "items", + "cost_layers", + "linked_items", + "transactions", + "brands", + "food_info", + "logistics_info", + "zones", + "locations", + "vendors", + "group_items", + "groups", + "receipt_items", + "receipts", + "recipe_items", + "recipes", + "shopping_list_items", + "shopping_lists", + "item_locations", + "conversions" + ] + + @dataclass + class Payload: + site_name: str + site_description: str + site_owner_id: int + default_zone: str = None + default_auto_issue_location: str = None + default_primary_location: str = None + creation_date: datetime.datetime = field(init=False) + flags: dict = field(default_factory=dict) + + def __post_init__(self): + self.creation_date = datetime.datetime.now() + + def payload(self): + return ( + self.site_name, + self.site_description, + self.creation_date, + self.site_owner_id, + json.dumps(self.flags), + self.default_zone, + self.default_auto_issue_location, + self.default_primary_location + ) + + def get_dictionary(self): + return self.__dict__ + + @classmethod + def insert_tuple(self, conn, payload:tuple, convert=True): + """inserts payload into sites table + + Args: + conn (_T_connector@connect): Postgresql Connector + payload (tuple): (site_name[str], site_description[str], creation_date[timestamp], site_owner_id[int], + flags[dict], default_zone[str], default_auto_issue_location[str], default_primary_location[str]) + convert (bool, optional): Determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: inserted tuple + """ + site_tuple = () + with open(f"sql/INSERT/insertSitesTuple.sql", "r+") as file: + sql = file.read() + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + site_tuple = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + site_tuple = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return site_tuple + + + @classmethod + def paginateTuples(self, conn, payload: tuple, convert=True): + """_summary_ + + Args: + conn (_type_): _description_ + payload (tuple): (limit, offset) + convert (bool, optional): _description_. Defaults to True. + + Raises: + DatabaseError: _description_ + + Returns: + _type_: _description_ + """ + recordsets = [] + count = 0 + sql = f"SELECT * FROM sites LIMIT %s OFFSET %s;" + sql_count = f"SELECT COUNT(*) FROM sites;" + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchall() + if rows and convert: + recordsets = [tupleDictionaryFactory(cur.description, row) for row in rows] + elif rows and not convert: + recordsets = rows + cur.execute(sql_count) + count = cur.fetchone()[0] + except Exception as error: + raise DatabaseError(error, (), sql) + return recordsets, count + + @classmethod + def selectTuples(self, conn, convert=True): + recordsets = [] + sql = f"SELECT * FROM sites" + try: + with conn.cursor() as cur: + cur.execute(sql) + rows = cur.fetchall() + if rows and convert: + recordsets = [tupleDictionaryFactory(cur.description, row) for row in rows] + elif rows and not convert: + recordsets = rows + except Exception as error: + raise DatabaseError(error, (), sql) + return recordsets + + @classmethod + def select_tuple(self, conn, payload:tuple, convert=True): + record = [] + sql = f"SELECT * FROM sites WHERE id=%s;" + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + record = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + record = rows + except Exception as error: + raise DatabaseError(error, (), sql) + return record + + @classmethod + def update_tuple(self, conn, payload, convert=True): + """_summary_ + + Args: + conn (_T_connector@connect): Postgresql Connector + site (str): + table (str): + payload (dict): {'id': row_id, 'update': {... column_to_update: value_to_update_to...}} + convert (bool, optional): determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: updated tuple + """ + updated = () + + set_clause, values = updateStringFactory(payload['update']) + values.append(payload['id']) + sql = f"UPDATE sites SET {set_clause} WHERE id=%s RETURNING *;" + try: + with conn.cursor() as cur: + cur.execute(sql, values) + rows = cur.fetchone() + if rows and convert: + updated = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + updated = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return updated + +class RolesTable: + @dataclass + class Payload: + role_name:str + role_description:str + site_id: int + flags: dict = field(default_factory=dict) + + def payload(self): + return ( + self.role_name, + self.role_description, + self.site_id, + json.dumps(self.flags) + ) + + def get_dictionary(self): + return self.__dict__ + + @classmethod + def insert_tuple(self, conn, payload:tuple, convert=True): + """inserts payload into roles table + + Args: + conn (_T_connector@connect): Postgresql Connector + payload (tuple): (role_name[str], role_description[str], site_id[int], flags[jsonb]) + convert (bool, optional): Determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: inserted tuple + """ + role_tuple = () + with open(f"sql/INSERT/insertRolesTuple.sql", "r+") as file: + sql = file.read() + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + role_tuple = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + role_tuple = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return role_tuple + + @classmethod + def select_tuple(self, conn, payload:tuple, convert=True): + record = [] + sql = f"SELECT roles.*, row_to_json(sites.*) as site FROM roles LEFT JOIN sites ON sites.id = roles.site_id WHERE roles.id=%s;" + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + record = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + record = rows + except Exception as error: + raise DatabaseError(error, (), sql) + return record + + @classmethod + def paginate_tuples(self, conn, payload:tuple, convert=True): + """ + Args: + conn (_T_connector@connect): Postgresql Connector + payload (tuple): (limit, offset) + convert (bool, optional): Determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: inserted tuple + """ + recordset = [] + + sql = f"SELECT roles.*, row_to_json(sites.*) as site FROM roles LEFT JOIN sites ON sites.id = roles.site_id LIMIT %s OFFSET %s;" + sql_count = f"SELECT COUNT(*) FROM roles;" + + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchall() + if rows and convert: + recordset = [tupleDictionaryFactory(cur.description, row) for row in rows] + elif rows and not convert: + recordset = rows + + + cur.execute(sql_count) + count = cur.fetchone()[0] + except Exception as error: + raise DatabaseError(error, payload, sql) + return recordset, count + + @classmethod + def update_tuple(self, conn, payload, convert=True): + """_summary_ + + Args: + conn (_T_connector@connect): Postgresql Connector + site (str): + table (str): + payload (dict): {'id': row_id, 'update': {... column_to_update: value_to_update_to...}} + convert (bool, optional): determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: updated tuple + """ + updated = () + + set_clause, values = updateStringFactory(payload['update']) + values.append(payload['id']) + sql = f"UPDATE roles SET {set_clause} WHERE id=%s RETURNING *;" + try: + with conn.cursor() as cur: + cur.execute(sql, values) + rows = cur.fetchone() + if rows and convert: + updated = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + updated = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return updated + +class LoginsTable: + @dataclass + class Payload: + username:str + password:str + email: str + row_type: str + system_admin: bool = False + flags: dict = field(default_factory=dict) + favorites: dict = field(default_factory=dict) + unseen_pantry_items: list = field(default_factory=list) + unseen_groups: list = field(default_factory=list) + unseen_shopping_lists: list = field(default_factory=list) + unseen_recipes: list = field(default_factory=list) + seen_pantry_items: list = field(default_factory=list) + seen_groups: list = field(default_factory=list) + seen_shopping_lists: list = field(default_factory=list) + seen_recipes: list = field(default_factory=list) + sites: list = field(default_factory=list) + site_roles: list = field(default_factory=list) + + def payload(self): + return ( + self.username, + self.password, + self.email, + json.dumps(self.favorites), + lst2pgarr(self.unseen_pantry_items), + lst2pgarr(self.unseen_groups), + lst2pgarr(self.unseen_shopping_lists), + lst2pgarr(self.unseen_recipes), + lst2pgarr(self.seen_pantry_items), + lst2pgarr(self.seen_groups), + lst2pgarr(self.seen_shopping_lists), + lst2pgarr(self.seen_recipes), + lst2pgarr(self.sites), + lst2pgarr(self.site_roles), + self.system_admin, + json.dumps(self.flags), + self.row_type + ) + + def get_dictionary(self): + return self.__dict__ + + + @classmethod + def insert_tuple(self, conn, payload:tuple, convert=True): + """inserts payload into roles table + + Args: + conn (_T_connector@connect): Postgresql Connector + payload (tuple): (username, password, email, favorites, unseen_pantry_items, unseen_groups, unseen_shopping_lists, + unseen_recipes, seen_pantry_items, seen_groups, seen_shopping_lists, seen_recipes, + sites, site_roles, system_admin, flags, row_type) + convert (bool, optional): Determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: inserted tuple + """ + login = () + with open(f"sql/INSERT/insertLoginsTupleTwo.sql", "r+") as file: + sql = file.read() + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + login = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + login = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return login + + + @classmethod + def select_tuple(self, conn, payload:tuple, convert=True): + """_summary_ + + Args: + conn (_type_): _description_ + payload (tuple): (user_id,) + convert (bool, optional): _description_. Defaults to False. + + Raises: + DatabaseError: _description_ + + Returns: + _type_: _description_ + """ + user = () + try: + with conn.cursor() as cur: + sql = f"SELECT * FROM logins WHERE id=%s;" + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + user = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + user = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return user + + @classmethod + def paginate_tuples(self, conn, payload:tuple, convert=True): + """ + Args: + conn (_T_connector@connect): Postgresql Connector + payload (tuple): (limit, offset) + convert (bool, optional): Determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: inserted tuple + """ + recordset = [] + + sql = f"SELECT * FROM logins LIMIT %s OFFSET %s;" + sql_count = f"SELECT COUNT(*) FROM logins;" + + try: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchall() + if rows and convert: + recordset = [tupleDictionaryFactory(cur.description, row) for row in rows] + elif rows and not convert: + recordset = rows + + cur.execute(sql_count) + count = cur.fetchone()[0] + except Exception as error: + raise DatabaseError(error, payload, sql) + return recordset, count + + @classmethod + def get_washed_tuple(self, conn, payload:tuple, convert=True): + user = () + try: + with conn.cursor() as cur: + sql = f"SELECT id, username, sites, site_roles, system_admin, flags FROM logins WHERE id=%s;" + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + user = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + user = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return user + + @classmethod + def update_tuple(self, conn, payload, convert=True): + """_summary_ + + Args: + conn (_T_connector@connect): Postgresql Connector + site (str): + table (str): + payload (dict): {'id': row_id, 'update': {... column_to_update: value_to_update_to...}} + convert (bool, optional): determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: updated tuple + """ + updated = () + + set_clause, values = updateStringFactory(payload['update']) + values.append(payload['id']) + sql = f"UPDATE logins SET {set_clause} WHERE id=%s RETURNING *;" + try: + with conn.cursor() as cur: + cur.execute(sql, values) + rows = cur.fetchone() + if rows and convert: + updated = tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + updated = rows + except Exception as error: + raise DatabaseError(error, payload, sql) + return updated diff --git a/scripts/recipes/__init__.py b/scripts/recipes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scripts/recipes/__pycache__/__init__.cpython-312.pyc b/scripts/recipes/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..553789b Binary files /dev/null and b/scripts/recipes/__pycache__/__init__.cpython-312.pyc differ diff --git a/scripts/recipes/__pycache__/database_recipes.cpython-312.pyc b/scripts/recipes/__pycache__/database_recipes.cpython-312.pyc new file mode 100644 index 0000000..c5e6242 Binary files /dev/null and b/scripts/recipes/__pycache__/database_recipes.cpython-312.pyc differ diff --git a/scripts/recipes/__pycache__/recipes_api.cpython-312.pyc b/scripts/recipes/__pycache__/recipes_api.cpython-312.pyc new file mode 100644 index 0000000..b70a979 Binary files /dev/null and b/scripts/recipes/__pycache__/recipes_api.cpython-312.pyc differ diff --git a/scripts/recipes/database_recipes.py b/scripts/recipes/database_recipes.py new file mode 100644 index 0000000..8ea6ed2 --- /dev/null +++ b/scripts/recipes/database_recipes.py @@ -0,0 +1,25 @@ +from scripts import postsqldb +import config +import psycopg2 + +def getModalSKUs(site, payload, convert=True): + database_config = config.config() + with psycopg2.connect(**database_config) as conn: + with conn.cursor() as cur: + with open("scripts/recipes/sql/itemsModal.sql") as file: + sql = file.read().replace("%%site_name%%", site) + cur.execute(sql, payload) + rows = cur.fetchall() + + if rows and convert: + rows = [postsqldb.tupleDictionaryFactory(cur.description, row) for row in rows] + + with open("scripts/recipes/sql/itemsModalCount.sql") as file: + sql = file.read().replace("%%site_name%%", site) + + cur.execute(sql) + count = cur.fetchone()[0] + + if rows and count: + return rows, count + return [], 0 \ No newline at end of file diff --git a/recipes_api.py b/scripts/recipes/recipes_api.py similarity index 95% rename from recipes_api.py rename to scripts/recipes/recipes_api.py index 064344e..7b55f3a 100644 --- a/recipes_api.py +++ b/scripts/recipes/recipes_api.py @@ -5,6 +5,7 @@ from main import unfoldCostLayers from user_api import login_required import os import postsqldb, webpush +from scripts.recipes import database_recipes recipes_api = Blueprint('recipes_api', __name__) @@ -82,12 +83,10 @@ def getItems(): search_string = request.args.get('search_string', 10) site_name = session['selected_site'] offset = (page - 1) * limit - database_config = config() - with psycopg2.connect(**database_config) as conn: - payload = (search_string, limit, offset) - recordset, count = database.getItemsWithQOH(conn, site_name, payload, convert=True) - return jsonify({"items":recordset, "end":math.ceil(count['count']/limit), "error":False, "message":"items fetched succesfully!"}) - return jsonify({"items":recordset, "end":math.ceil(count['count']/limit), "error":True, "message":"There was an error with this GET statement"}) + recordset, count = database_recipes.getModalSKUs(site_name, (limit, offset)) + print(recordset) + return jsonify({"items":recordset, "end":math.ceil(count/limit), "error":False, "message":"items fetched succesfully!"}) + return jsonify({"items":recordset, "end":math.ceil(count/limit), "error":True, "message":"There was an error with this GET statement"}) @recipes_api.route('/recipe/postUpdate', methods=["POST"]) diff --git a/scripts/recipes/sql/itemsModal.sql b/scripts/recipes/sql/itemsModal.sql new file mode 100644 index 0000000..e304973 --- /dev/null +++ b/scripts/recipes/sql/itemsModal.sql @@ -0,0 +1,2 @@ +SELECT item.id, item.barcode, item.item_name FROM %%site_name%%_items item +LIMIT %s OFFSET %s; \ No newline at end of file diff --git a/scripts/recipes/sql/itemsModalCount.sql b/scripts/recipes/sql/itemsModalCount.sql new file mode 100644 index 0000000..0a44948 --- /dev/null +++ b/scripts/recipes/sql/itemsModalCount.sql @@ -0,0 +1 @@ +SELECT COUNT(item.*) FROM %%site_name%%_items item; \ No newline at end of file diff --git a/sql/CREATE/zones.sql b/sql/CREATE/zones.sql index 5da4e93..5a60c54 100644 --- a/sql/CREATE/zones.sql +++ b/sql/CREATE/zones.sql @@ -2,9 +2,5 @@ CREATE TABLE IF NOT EXISTS %%site_name%%_zones( id SERIAL PRIMARY KEY, name VARCHAR(32) NOT NULL, description TEXT, - site_id INTEGER NOT NULL, - UNIQUE(name), - CONSTRAINT fk_site - FOREIGN KEY(site_id) - REFERENCES sites(id) + UNIQUE(name) ); diff --git a/sql/INSERT/insertLoginsTupleTwo.sql b/sql/INSERT/insertLoginsTupleTwo.sql new file mode 100644 index 0000000..8103c19 --- /dev/null +++ b/sql/INSERT/insertLoginsTupleTwo.sql @@ -0,0 +1,6 @@ +INSERT INTO logins +(username, password, email, favorites, unseen_pantry_items, unseen_groups, unseen_shopping_lists, + unseen_recipes, seen_pantry_items, seen_groups, seen_shopping_lists, seen_recipes, + sites, site_roles, system_admin, flags, row_type) +VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) +RETURNING *; \ No newline at end of file diff --git a/sql/INSERT/insertZonesTuple.sql b/sql/INSERT/insertZonesTuple.sql index d109e77..5175752 100644 --- a/sql/INSERT/insertZonesTuple.sql +++ b/sql/INSERT/insertZonesTuple.sql @@ -1,4 +1,4 @@ INSERT INTO %%site_name%%_zones -(name, description, site_id) -VALUES (%s, %s, %s) +(name, description) +VALUES (%s, %s) RETURNING *; \ No newline at end of file diff --git a/sql/admin/SELECT/selectLoginsUser.sql b/sql/admin/SELECT/selectLoginsUser.sql new file mode 100644 index 0000000..83ee988 --- /dev/null +++ b/sql/admin/SELECT/selectLoginsUser.sql @@ -0,0 +1,16 @@ +WITH passed_id AS (SELECT %s AS passed_id), + cte_login AS ( + SELECT logins.* FROM logins + WHERE logins.id = (SELECT passed_id FROM passed_id) + ), + cte_roles AS ( + SELECT roles.*, + row_to_json(sites.*) AS site + FROM roles + LEFT JOIN sites ON sites.id = roles.site_id + WHERE roles.id = ANY(SELECT unnest(site_roles) FROM cte_login) + ) + +SELECT login.*, + (SELECT COALESCE(array_agg(row_to_json(r)), '{}') FROM cte_roles r) AS site_roles +FROM cte_login login; \ No newline at end of file diff --git a/static/adminHandler.js b/static/adminHandler.js deleted file mode 100644 index 8dca71a..0000000 --- a/static/adminHandler.js +++ /dev/null @@ -1,28 +0,0 @@ -async function clickRoleRow(role_id){ - const roleurl = new URL(`/admin/editRole/${role_id}`, window.location.origin); - window.location.href = roleurl.toString(); -} - -async function fetchSites() { - const url = new URL('/admin/getSites', window.location.origin); - const response = await fetch(url); - const data = await response.json(); - return data.sites; -} - -async function fetchUsers(limit, page) { - const url = new URL('/admin/getUsers', window.location.origin); - const response = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - limit: limit, - page: page - }), - }); - const data = await response.json(); - return data.users; -} - diff --git a/static/files/receipts/20250425_163102.jpg b/static/files/receipts/20250425_163102.jpg new file mode 100644 index 0000000..cdcba4d Binary files /dev/null and b/static/files/receipts/20250425_163102.jpg differ diff --git a/static/handlers/adminHandler.js b/static/handlers/adminHandler.js new file mode 100644 index 0000000..985f4ce --- /dev/null +++ b/static/handlers/adminHandler.js @@ -0,0 +1,489 @@ +var mode = false +async function toggleDarkMode() { + let darkMode = document.getElementById("dark-mode"); + darkMode.disabled = !darkMode.disabled; + mode = !mode; + if(mode){ + document.getElementById('modeToggle').innerHTML = "light_mode" + document.getElementById('main_html').classList.add('uk-light') + } else { + document.getElementById('modeToggle').innerHTML = "dark_mode" + document.getElementById('main_html').classList.remove('uk-light') + } +} + +if(session.user.flags.darkmode){ + toggleDarkMode() +} + +document.addEventListener('DOMContentLoaded', async function() { + let sites = await fetchSites() + await updateSitesPagination() + await replenishSitesTable(sites) + + let roles = await fetchRoles() + await updateRolesPagination() + await replenishRolesTable(roles) + + let logins = await fetchLogins() + console.log(logins) + await updateLoginsPagination() + await replenishLoginsTable(logins) +}) + +async function openDeleteModal(item_name, item_type, site_id) { + document.getElementById('delete_item_name').innerHTML = item_name + + if(item_type == "site"){ + document.getElementById("deleteSubmitButton").onclick = async function() { + await postDeleteSite(site_id, item_name) + } + } + UIkit.modal(document.getElementById('deleteConfirmation')).show(); +} + +// Site functions +var sites_current_page = 1 +var sites_end_page = 10 +var sites_limit = 25 +async function fetchSites(){ + const url = new URL('/admin/getSites', window.location.origin) + url.searchParams.append('page', sites_current_page) + url.searchParams.append('limit', sites_limit) + const response = await fetch(url) + data = await response.json() + sites_end_page = data.end + return data.sites +} + +async function replenishSitesTable(sites){ + let sitesTableBody = document.getElementById('sitesTableBody') + sitesTableBody.innerHTML = "" + + for(let i=0; i < sites.length; i++){ + let tableRow = document.createElement('tr') + + + let idCell = document.createElement('td') + idCell.innerHTML = `${sites[i].id}` + let nameCell = document.createElement('td') + nameCell.innerHTML = `${sites[i].site_name}` + let descriptionCell = document.createElement('td') + descriptionCell.innerHTML = `${sites[i].site_description}` + let opCell = document.createElement('td') + opCell.innerHTML = `` + + let editOp = document.createElement('a') + editOp.setAttribute('class', 'uk-button uk-button-small uk-button-default') + editOp.innerHTML = "edit" + editOp.href = `/admin/site/${sites[i].id}` + + let deleteOp = document.createElement('a') + deleteOp.setAttribute('class', 'uk-button uk-button-small uk-button-default') + deleteOp.innerHTML = "delete" + deleteOp.onclick = async function() { + await openDeleteModal(sites[i].site_name, "site", sites[i].id) + } + + opCell.append(editOp, deleteOp) + tableRow.append(idCell, nameCell, descriptionCell, opCell) + sitesTableBody.append(tableRow) + } +} + +async function updateSitesPagination() { + let paginationElement = document.getElementById("sitesPagination"); + paginationElement.innerHTML = ""; + // previous + let previousElement = document.createElement('li') + if(sites_current_page<=1){ + previousElement.innerHTML = ``; + previousElement.classList.add('uk-disabled'); + }else { + previousElement.innerHTML = ``; + } + paginationElement.append(previousElement) + + //first + let firstElement = document.createElement('li') + if(sites_current_page<=1){ + firstElement.innerHTML = `1`; + firstElement.classList.add('uk-disabled'); + }else { + firstElement.innerHTML = `1`; + } + paginationElement.append(firstElement) + + // ... + if(sites_current_page-2>1){ + let firstDotElement = document.createElement('li') + firstDotElement.classList.add('uk-disabled') + firstDotElement.innerHTML = ``; + paginationElement.append(firstDotElement) + } + // last + if(sites_current_page-2>0){ + let lastElement = document.createElement('li') + lastElement.innerHTML = `${sites_current_page-1}` + paginationElement.append(lastElement) + } + // current + if(sites_current_page!=1 && sites_current_page != sites_end_page){ + let currentElement = document.createElement('li') + currentElement.innerHTML = `
  • ${sites_current_page}
  • ` + paginationElement.append(currentElement) + } + // next + if(sites_current_page+2${sites_current_page+1}` + paginationElement.append(nextElement) + } + // ... + if(sites_current_page+2<=sites_end_page){ + let secondDotElement = document.createElement('li') + secondDotElement.classList.add('uk-disabled') + secondDotElement.innerHTML = ``; + paginationElement.append(secondDotElement) + } + //end + let endElement = document.createElement('li') + if(sites_current_page>=sites_end_page){ + endElement.innerHTML = `${sites_end_page}`; + endElement.classList.add('uk-disabled'); + }else { + endElement.innerHTML = `${sites_end_page}`; + } + paginationElement.append(endElement) + //next button + let nextElement = document.createElement('li') + if(sites_current_page>=sites_end_page){ + nextElement.innerHTML = ``; + nextElement.classList.add('uk-disabled'); + }else { + nextElement.innerHTML = ``; + } + paginationElement.append(nextElement) +} + +async function setSitesPage(pageNumber){ + sites_current_page = pageNumber; + let sites = await fetchSites() + await updateSitesPagination() + await replenishSitesTable(sites) +} + +async function postDeleteSite(site_id, item_name){ + let valid = document.getElementById('delete_input') + if(valid.value==item_name){ + valid.classList.remove('uk-form-danger') + const response = await fetch(`/admin/site/postDeleteSite`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + site_id: site_id + }), + }); + data = await response.json(); + transaction_status = "success" + if (data.error){ + transaction_status = "danger" + } + } else { + valid.classList.add('uk-form-danger') + data = {'message': 'You did not confirm the item correctly!!'} + transaction_status = "danger" + } + + UIkit.notification({ + message: data.message, + status: transaction_status, + pos: 'top-right', + timeout: 5000 + }); + let sites = await fetchSites() + await updateSitesPagination() + await replenishSitesTable(sites) + UIkit.modal(document.getElementById('deleteConfirmation')).hide(); + +} + +// Roles functions +var roles_current_page = 1 +var roles_end_page = 10 +var roles_limit = 25 +async function fetchRoles(){ + const url = new URL('/admin/getRoles', window.location.origin) + url.searchParams.append('page', roles_current_page) + url.searchParams.append('limit', roles_limit) + const response = await fetch(url) + data = await response.json() + roles_end_page = data.end + return data.roles +} + +async function replenishRolesTable(roles){ + let rolesTableBody = document.getElementById('rolesTableBody') + rolesTableBody.innerHTML = "" + + for(let i=0; i < roles.length; i++){ + let tableRow = document.createElement('tr') + + + let idCell = document.createElement('td') + idCell.innerHTML = `${roles[i].id}` + let nameCell = document.createElement('td') + nameCell.innerHTML = `${roles[i].role_name}` + let descriptionCell = document.createElement('td') + descriptionCell.innerHTML = `${roles[i].role_description}` + let siteCell = document.createElement('td') + siteCell.innerHTML = `${roles[i].site.site_name}` + let opCell = document.createElement('td') + opCell.innerHTML = `` + + let editOp = document.createElement('a') + editOp.setAttribute('class', 'uk-button uk-button-small uk-button-default') + editOp.innerHTML = "edit" + editOp.href = `/admin/role/${roles[i].id}` + + let deleteOp = document.createElement('a') + deleteOp.setAttribute('class', 'uk-button uk-button-small uk-button-default') + deleteOp.innerHTML = "delete" + deleteOp.onclick = async function() { + await openDeleteModal(roles[i].role_name, "role", roles[i].id) + } + + opCell.append(editOp, deleteOp) + tableRow.append(idCell, nameCell, descriptionCell, siteCell, opCell) + rolesTableBody.append(tableRow) + } +} + +async function updateRolesPagination() { + let paginationElement = document.getElementById("rolesPagination"); + paginationElement.innerHTML = ""; + // previous + let previousElement = document.createElement('li') + if(roles_current_page<=1){ + previousElement.innerHTML = ``; + previousElement.classList.add('uk-disabled'); + }else { + previousElement.innerHTML = ``; + } + paginationElement.append(previousElement) + + //first + let firstElement = document.createElement('li') + if(roles_current_page<=1){ + firstElement.innerHTML = `1`; + firstElement.classList.add('uk-disabled'); + }else { + firstElement.innerHTML = `1`; + } + paginationElement.append(firstElement) + + // ... + if(roles_current_page-2>1){ + let firstDotElement = document.createElement('li') + firstDotElement.classList.add('uk-disabled') + firstDotElement.innerHTML = ``; + paginationElement.append(firstDotElement) + } + // last + if(roles_current_page-2>0){ + let lastElement = document.createElement('li') + lastElement.innerHTML = `${roles_current_page-1}` + paginationElement.append(lastElement) + } + // current + if(roles_current_page!=1 && roles_current_page != roles_end_page){ + let currentElement = document.createElement('li') + currentElement.innerHTML = `
  • ${roles_current_page}
  • ` + paginationElement.append(currentElement) + } + // next + if(roles_current_page+2${roles_current_page+1}` + paginationElement.append(nextElement) + } + // ... + if(roles_current_page+2<=roles_end_page){ + let secondDotElement = document.createElement('li') + secondDotElement.classList.add('uk-disabled') + secondDotElement.innerHTML = ``; + paginationElement.append(secondDotElement) + } + //end + let endElement = document.createElement('li') + if(roles_current_page>=roles_end_page){ + endElement.innerHTML = `${roles_end_page}`; + endElement.classList.add('uk-disabled'); + }else { + endElement.innerHTML = `${roles_end_page}`; + } + paginationElement.append(endElement) + //next button + let nextElement = document.createElement('li') + if(roles_current_page>=roles_end_page){ + nextElement.innerHTML = ``; + nextElement.classList.add('uk-disabled'); + }else { + nextElement.innerHTML = ``; + } + paginationElement.append(nextElement) +} + +async function setRolesPage(pageNumber){ + roles_current_page = pageNumber; + let roles = await fetchRoles() + await updateRolesPagination() + await replenishRolesTable(roles) +} + + +// users/devices functions +var logins_current_page = 1 +var logins_end_page = 10 +var logins_limit = 25 +async function fetchLogins(){ + const url = new URL('/admin/getLogins', window.location.origin) + url.searchParams.append('page', logins_current_page) + url.searchParams.append('limit', logins_limit) + const response = await fetch(url) + data = await response.json() + logins_end_page = data.end + return data.logins +} + +async function replenishLoginsTable(logins){ + let usersTableBody = document.getElementById('usersTableBody') + usersTableBody.innerHTML = "" + + for(let i=0; i < logins.length; i++){ + let tableRow = document.createElement('tr') + + + let idCell = document.createElement('td') + idCell.innerHTML = `${logins[i].id}` + let nameCell = document.createElement('td') + nameCell.innerHTML = `${logins[i].username}` + + let emailCell = document.createElement('td') + emailCell.innerHTML = `${logins[i].email}` + + let typeCell = document.createElement('td') + typeCell.innerHTML = `${logins[i].row_type}` + + + let opCell = document.createElement('td') + opCell.innerHTML = `` + + let editOp = document.createElement('a') + editOp.setAttribute('class', 'uk-button uk-button-small uk-button-default') + editOp.innerHTML = "edit" + editOp.href = `/admin/user/${logins[i].id}` + + let deleteOp = document.createElement('a') + deleteOp.setAttribute('class', 'uk-button uk-button-small uk-button-default') + deleteOp.innerHTML = "delete" + deleteOp.onclick = async function() { + await openDeleteModal(logins[i].username, "login", logins[i].id) + } + + opCell.append(editOp, deleteOp) + tableRow.append(idCell, nameCell, emailCell, typeCell, opCell) + usersTableBody.append(tableRow) + } +} + +async function updateLoginsPagination() { + let paginationElement = document.getElementById("usersPagination"); + paginationElement.innerHTML = ""; + // previous + let previousElement = document.createElement('li') + if(logins_current_page<=1){ + previousElement.innerHTML = ``; + previousElement.classList.add('uk-disabled'); + }else { + previousElement.innerHTML = ``; + } + paginationElement.append(previousElement) + + //first + let firstElement = document.createElement('li') + if(logins_current_page<=1){ + firstElement.innerHTML = `1`; + firstElement.classList.add('uk-disabled'); + }else { + firstElement.innerHTML = `1`; + } + paginationElement.append(firstElement) + + // ... + if(logins_current_page-2>1){ + let firstDotElement = document.createElement('li') + firstDotElement.classList.add('uk-disabled') + firstDotElement.innerHTML = ``; + paginationElement.append(firstDotElement) + } + // last + if(logins_current_page-2>0){ + let lastElement = document.createElement('li') + lastElement.innerHTML = `${logins_current_page-1}` + paginationElement.append(lastElement) + } + // current + if(logins_current_page!=1 && logins_current_page != logins_end_page){ + let currentElement = document.createElement('li') + currentElement.innerHTML = `
  • ${logins_current_page}
  • ` + paginationElement.append(currentElement) + } + // next + if(logins_current_page+2${logins_current_page+1}` + paginationElement.append(nextElement) + } + // ... + if(logins_current_page+2<=logins_end_page){ + let secondDotElement = document.createElement('li') + secondDotElement.classList.add('uk-disabled') + secondDotElement.innerHTML = ``; + paginationElement.append(secondDotElement) + } + //end + let endElement = document.createElement('li') + if(logins_current_page>=logins_end_page){ + endElement.innerHTML = `${logins_end_page}`; + endElement.classList.add('uk-disabled'); + }else { + endElement.innerHTML = `${logins_end_page}`; + } + paginationElement.append(endElement) + //next button + let nextElement = document.createElement('li') + if(logins_current_page>=logins_end_page){ + nextElement.innerHTML = ``; + nextElement.classList.add('uk-disabled'); + }else { + nextElement.innerHTML = ``; + } + paginationElement.append(nextElement) +} + +async function setLoginsPage(pageNumber){ + logins_current_page = pageNumber; + let logins = await fetchLogins() + await updateLoginsPagination() + await replenishLoginsTable(logins) +} + + + +// uom functions +async function test() { + console.log('test') +} \ No newline at end of file diff --git a/templates/admin/index.html b/templates/admin/index.html new file mode 100644 index 0000000..e2ba58d --- /dev/null +++ b/templates/admin/index.html @@ -0,0 +1,281 @@ + + + + + + + + + + + + + + + + + + +
    + + +
    +
    +
    +
    +
    + +
    +
    + +
    +
    +
    + +
    +
    + + + + + + + + + + + + +
    IDSite NameSite DescriptionOperations
    + add_circle +
    +
    +
    + +
    +
    +
    + +
    +
    + + + + + + + + + + + + + +
    IDRoles NameRole DescriptionSiteOperations
    + add_circle +
    +
    +
    + +
    +
    +
    + +
    +
    + + + + + + + + + + + + + +
    IDUsernameEmailTypeOperations
    + add_circle +
    +
    +
    + +
    +
    +
    + +
    +
    + + + + + + + + + + + + +
    IDFullnameDescriptionOperations
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    + + +
    +
    + +
    +

    DELETE

    +
    +
    +
    +
    +

    You are attempting to delete something important, in order to ensure this is your intent, please type in the name of the + item you were going to delete

    +
    +
    +

    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    + + + {% assets "js_all" %} + + {% endassets %} + + + \ No newline at end of file diff --git a/templates/admin/role.html b/templates/admin/role.html index eaaa84a..c316e87 100644 --- a/templates/admin/role.html +++ b/templates/admin/role.html @@ -1,130 +1,164 @@ - + - Edit Role - + + - - + + + + + + + + + - -
    -
    -
    -
    - arrow_back - Home - Profile +
    +
    + +
    +

    Role Form

    +
    +
    +

    Roles exist to harness a users/devices access to a sites specific apps and inputs. You use roles to better define a groups permissions + within the app in order to control who has access to what. Specific User/Device permissions will overwrite any of their roles, permissions.

    +
    +
    +
    +
    +
    +
    + +
    + +
    -
    - - +
    +
    + +
    +
    -
    - - -
    -
    - - -
    -
    -

    Permissions

    -
    -
    -
    All
    -
    -
    -

    - -

    -

    - -

    -
    -
    -

    - -

    -

    - -

    -
    -
    - - +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    - + {% assets "js_all" %} + + {% endassets %} diff --git a/templates/admin/site.html b/templates/admin/site.html new file mode 100644 index 0000000..54b47c0 --- /dev/null +++ b/templates/admin/site.html @@ -0,0 +1,240 @@ + + + + + + + + + + + + + + + + + + +
    +
    + +
    +

    Site Form

    +
    +
    +

    Sites are the main driving force of the system. They are essentially larger pools of items and apps that are segregated and only meet in cross-sites applications. Think of + sites as your house, your shed, or your car. When designing a site think about who has access and who will be using it the most often, what permissions might be needed, and + do you need all the apps active on the site. The more sites you have the more bloated the system can become so beware making tons of sites. Thats why Zones and Locations + exist.

    +
    +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    + + +
    +
    + +
    +
    +
    +
    + + +
    +
    + +
    +
    +
    +
    + + +
    +
    + +
    +
    +
    +
    +
    + These are how the system will create the default locations and zones that get assigned to all new items by default. +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + + {% assets "js_all" %} + + {% endassets %} + + + \ No newline at end of file diff --git a/templates/admin/user.html b/templates/admin/user.html new file mode 100644 index 0000000..383b730 --- /dev/null +++ b/templates/admin/user.html @@ -0,0 +1,367 @@ + + + + + User + + + + + + + + + + + + + +
    +
    +
    + +
    +

    User/Device Form

    +
    +
    +

    Users and Devices, better known as access points, are credentials to acccess the system from specific areas. Users are intended to be + you basic login for actual people. Devices are created when you know a the credentials are to be used in one place and no where else. + Differentiating between the two is helpful to see who by/where in your setup transactions are happening.

    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    + +
    +
    +
    +
    +
    + These are the Sites this access point has permission to see + + + + + + + + + + + + + + + + + +
    Site NameOperations
    Test
    Test
    +
    +
    + These are the roles that the access point has in each permissable site + + + + + + + + + + + + + + + + + +
    Site RoleOperations
    Test
    Test
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    + + {% assets "js_all" %} + + {% endassets %} + + + diff --git a/templates/admin/users.html b/templates/admin/users.html deleted file mode 100644 index 7d8582c..0000000 --- a/templates/admin/users.html +++ /dev/null @@ -1,17 +0,0 @@ -
    -
    -

    Your Users

    -
    -
    -

    This is where all the users who have access to this instance!

    -
    -
    - %%userstable%% -
    -
    - -
    -
    - %%pagination%% -
    -
    diff --git a/test.py b/test.py index 5cb70b4..1add264 100644 --- a/test.py +++ b/test.py @@ -4,4 +4,15 @@ import random, uuid, csv, postsqldb import pdf2image, os, pymupdf, PIL -from pywebpush import webpush, WebPushException \ No newline at end of file +from pywebpush import webpush, WebPushException + + +site = MyDataclasses.SitePayload( + "testA", + "Test site A", + 1 +) + +print("payload", site) +x = site.__dict__ +print("dict", x) \ No newline at end of file diff --git a/user_api.py b/user_api.py index 8275e16..967b17a 100644 --- a/user_api.py +++ b/user_api.py @@ -4,9 +4,16 @@ from config import config, sites_config, setFirstSetupDone from functools import wraps from manage import create from main import create_site, getUser, setSystemAdmin +import postsqldb login_app = Blueprint('login', __name__) +def update_session_user(): + database_config = config() + with psycopg2.connect(**database_config) as conn: + user = postsqldb.LoginsTable.get_washed_tuple(conn, (session['user_id'],)) + session['user'] = user + def login_required(func): @wraps(func) def wrapper(*args, **kwargs): diff --git a/webserver.py b/webserver.py index e6dc9f1..a323205 100644 --- a/webserver.py +++ b/webserver.py @@ -1,13 +1,14 @@ import celery.schedules from flask import Flask, render_template, session, request, redirect, jsonify from flask_assets import Environment, Bundle -import api, config, user_api, psycopg2, main, admin, item_API, receipts_API, shopping_list_API, group_api, recipes_api -from user_api import login_required +import api, config, user_api, psycopg2, main, api_admin, item_API, receipts_API, shopping_list_API, group_api +from user_api import login_required, update_session_user from external_API import external_api from workshop_api import workshop_api import database import postsqldb from webpush import trigger_push_notifications_for_subscriptions +from scripts.recipes import recipes_api app = Flask(__name__, instance_relative_config=True) UPLOAD_FOLDER = 'static/pictures' @@ -21,7 +22,7 @@ assets = Environment(app) app.secret_key = '11gs22h2h1a4h6ah8e413a45' app.register_blueprint(api.database_api) app.register_blueprint(user_api.login_app) -app.register_blueprint(admin.admin) +app.register_blueprint(api_admin.admin_api) app.register_blueprint(item_API.items_api) app.register_blueprint(external_api) app.register_blueprint(workshop_api) @@ -90,6 +91,7 @@ def transaction(): @app.route("/items") @login_required def items(): + update_session_user() sites = [site[1] for site in main.get_sites(session['user']['sites'])] return render_template("items/index.html", current_site=session['selected_site'], @@ -116,6 +118,7 @@ def subscribe(): @app.route("/") @login_required def home(): + update_session_user() sites = [site[1] for site in main.get_sites(session['user']['sites'])] session['selected_site'] = sites[0] return redirect("/items") diff --git a/workshop_api.py b/workshop_api.py index da6e357..15b7df3 100644 --- a/workshop_api.py +++ b/workshop_api.py @@ -10,6 +10,7 @@ workshop_api = Blueprint('workshop_api', __name__) @workshop_api.route("/workshop") @login_required def workshop(): + print(session['user']) sites = [site[1] for site in main.get_sites(session['user']['sites'])] print(session.get('user')['system_admin']) if not session.get('user')['system_admin']: @@ -117,7 +118,6 @@ def postAddZone(): site_id = cur.fetchone()[0] zone = postsqldb.ZonesTable.Payload( request.get_json()['name'], - site_id, request.get_json()['description'] ) postsqldb.ZonesTable.insert_tuple(conn, site_name, zone.payload())