diff --git a/__pycache__/webserver.cpython-312.pyc b/__pycache__/webserver.cpython-312.pyc index fc5a7e2..eed7273 100644 Binary files a/__pycache__/webserver.cpython-312.pyc and b/__pycache__/webserver.cpython-312.pyc differ diff --git a/database.log b/database.log index f9616cb..d6dabb4 100644 --- a/database.log +++ b/database.log @@ -1838,4 +1838,7 @@ 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 + 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-26 18:29:30.532818 --- ERROR --- DatabaseError(message=''module' object is not callable. Did you mean: 'config.config(...)'?', + payload=(10, 0), + sql='SELECT *, (SELECT COALESCE(array_agg(row_to_json(g)), '{}') FROM main_recipe_items g WHERE rp_id = main_recipes.id) AS rp_items FROM main_recipes LIMIT %s OFFSET %s;') \ No newline at end of file diff --git a/scripts/recipes/__pycache__/database_recipes.cpython-312.pyc b/scripts/recipes/__pycache__/database_recipes.cpython-312.pyc index 5e28a33..7046928 100644 Binary files a/scripts/recipes/__pycache__/database_recipes.cpython-312.pyc 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 index b70a979..3277892 100644 Binary files a/scripts/recipes/__pycache__/recipes_api.cpython-312.pyc 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 index 8ea6ed2..7b3f7bc 100644 --- a/scripts/recipes/database_recipes.py +++ b/scripts/recipes/database_recipes.py @@ -22,4 +22,45 @@ def getModalSKUs(site, payload, convert=True): if rows and count: return rows, count - return [], 0 \ No newline at end of file + return [], 0 + +def getRecipes(site:str, payload:tuple, convert=True): + recordset = [] + count = 0 + with open("scripts/recipes/sql/getRecipes.sql", "r+") as file: + sql = file.read().replace("%%site_name%%", site) + with open(f"scripts/recipes/sql/getRecipesCount.sql", "r+") as file: + sqlcount = file.read().replace("%%site_name%%", site) + try: + database_config = config.config() + with psycopg2.connect(**database_config) as conn: + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchall() + if rows and convert: + recordset = [postsqldb.tupleDictionaryFactory(cur.description, row) for row in rows] + if rows and not convert: + recordset = rows + cur.execute(sqlcount) + count = cur.fetchone()[0] + except (Exception, psycopg2.DatabaseError) as error: + raise postsqldb.DatabaseError(error, payload, sql) + return recordset, count + +def postRecipeUpdate(site, payload, convert=True): + database_config = config.config() + updated = () + with psycopg2.connect(**database_config) as conn: + with conn.cursor() as cur: + set_clause, values = postsqldb.updateStringFactory(payload['update']) + with open("scripts/recipes/sql/postRecipeUpdate.sql") as file: + sql = file.read().replace("%%site_name%%", site).replace("%%set_clause%%", set_clause) + values.append(payload['id']) + cur.execute(sql, values) + + rows = cur.fetchone() + if rows and convert: + updated = postsqldb.tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + updated = rows + return updated \ No newline at end of file diff --git a/scripts/recipes/recipes_api.py b/scripts/recipes/recipes_api.py index 7b55f3a..2629590 100644 --- a/scripts/recipes/recipes_api.py +++ b/scripts/recipes/recipes_api.py @@ -5,13 +5,22 @@ from main import unfoldCostLayers from user_api import login_required import os import postsqldb, webpush +from flasgger import swag_from from scripts.recipes import database_recipes +from flask_restx import Api, fields recipes_api = Blueprint('recipes_api', __name__) +model_api = Api(recipes_api) @recipes_api.route("/recipes") @login_required def recipes(): + """This is the main endpoint to reach the webpage for a list of all recipes + --- + responses: + 200: + description: returns recipes/index.html with sites, current_site. + """ sites = [site[1] for site in main.get_sites(session['user']['sites'])] return render_template("recipes/index.html", current_site=session['selected_site'], @@ -19,7 +28,24 @@ def recipes(): @recipes_api.route("/recipe//") @login_required -def recipe(mode, id): +def recipe(id, mode='view'): + """This is the main endpoint to reach the webpage for a recipe's view or edit mode. + --- + parameters: + - name: mode + in: path + type: string + required: true + default: view + - name: id + in: path + type: integer + required: true + default: all + responses: + 200: + description: Respondes with either the Edit or View webpage for the recipe. + """ database_config = config() with psycopg2.connect(**database_config) as conn: units = postsqldb.UnitsTable.getAll(conn) @@ -32,16 +58,22 @@ def recipe(mode, id): @recipes_api.route('/recipes/getRecipes', methods=["GET"]) def getRecipes(): + """ Get a subquery of recipes from the database by passing a page, limit + --- + responses: + 200: + description: limit of rows passed returned to requester + """ recipes = [] + count=0 if request.method == "GET": page = int(request.args.get('page', 1)) limit = int(request.args.get('limit', 1)) offset = (page-1)*limit - database_config = config() site_name = session['selected_site'] - with psycopg2.connect(**database_config) as conn: - recipes, count = postsqldb.RecipesTable.getRecipes(conn, site_name, (limit, offset), convert=True) - return jsonify({'recipes': recipes, 'end': math.ceil(count/limit), 'error': False, 'message': 'bleh'}) + recipes, count = database_recipes.getRecipes(site_name, (limit, offset)) + return jsonify({'recipes': recipes, 'end': math.ceil(count/limit), 'error': False, 'message': 'fetch was successful!'}) + return jsonify({'recipes': recipes, 'end': math.ceil(count/limit), 'error': True, 'message': f'method is not allowed: {request.method}'}) @recipes_api.route('/recipe/getRecipe', methods=["GET"]) def getRecipe(): @@ -75,6 +107,12 @@ def addRecipe(): @recipes_api.route('/recipe/getItems', methods=["GET"]) def getItems(): + """ Pass along a page, limit, and search strings to get a pagination of items from the system + --- + responses: + 200: + description: Items were returned successfully! + """ recordset = [] count = {'count': 0} if request.method == "GET": @@ -83,22 +121,35 @@ def getItems(): search_string = request.args.get('search_string', 10) site_name = session['selected_site'] offset = (page - 1) * limit - recordset, count = database_recipes.getModalSKUs(site_name, (limit, offset)) + recordset, count = database_recipes.getModalSKUs(site_name, (search_string, 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"}) +update_model = model_api.model('model', { + 'id': fields.Integer(min=1), + 'update': fields.Raw(required=True, description="all the data to be updated!") +}) + @recipes_api.route('/recipe/postUpdate', methods=["POST"]) +@model_api.expect(update_model) def postUpdate(): + """ This is an endpoint for updating an RecipeTuple in the sites recipes table + --- + responses: + 200: + description: The time was updated successfully! + + Returns: + dict: returns a dictionary containing the updated recipe object, error status, and a message to post for notifications + """ recipe = {} if request.method == "POST": recipe_id = int(request.get_json()['recipe_id']) update = request.get_json()['update'] - database_config = config() site_name = session['selected_site'] - with psycopg2.connect(**database_config) as conn: - recipe = postsqldb.RecipesTable.updateRecipe(conn, site_name, {'id': recipe_id, 'update': update}, convert=True) + recipe = database_recipes.postRecipeUpdate(site_name, {'id': recipe_id, 'update': update}) return jsonify({'recipe': recipe, 'error': False, 'message': 'Update of Recipe successful!'}) return jsonify({'recipe': recipe, 'error': True, 'message': 'Update of Recipe unsuccessful!'}) diff --git a/scripts/recipes/sql/getRecipes.sql b/scripts/recipes/sql/getRecipes.sql new file mode 100644 index 0000000..1703d1e --- /dev/null +++ b/scripts/recipes/sql/getRecipes.sql @@ -0,0 +1,3 @@ +SELECT *, + (SELECT COALESCE(array_agg(row_to_json(g)), '{}') FROM %%site_name%%_recipe_items g WHERE rp_id = %%site_name%%_recipes.id) AS rp_items + FROM %%site_name%%_recipes LIMIT %s OFFSET %s; \ No newline at end of file diff --git a/scripts/recipes/sql/getRecipesCount.sql b/scripts/recipes/sql/getRecipesCount.sql new file mode 100644 index 0000000..a9f5ad7 --- /dev/null +++ b/scripts/recipes/sql/getRecipesCount.sql @@ -0,0 +1 @@ +SELECT COUNT(*) FROM %%site_name%%_recipes; \ No newline at end of file diff --git a/scripts/recipes/sql/itemsModal.sql b/scripts/recipes/sql/itemsModal.sql index e304973..7af323e 100644 --- a/scripts/recipes/sql/itemsModal.sql +++ b/scripts/recipes/sql/itemsModal.sql @@ -1,2 +1,3 @@ SELECT item.id, item.barcode, item.item_name FROM %%site_name%%_items item +WHERE item.search_string LIKE '%%' || %s || '%%' LIMIT %s OFFSET %s; \ No newline at end of file diff --git a/scripts/recipes/sql/postRecipeUpdate.sql b/scripts/recipes/sql/postRecipeUpdate.sql new file mode 100644 index 0000000..79c3c61 --- /dev/null +++ b/scripts/recipes/sql/postRecipeUpdate.sql @@ -0,0 +1 @@ +UPDATE %%site_name%%_recipes SET %%set_clause%% WHERE id=%s RETURNING *; \ No newline at end of file diff --git a/webserver.py b/webserver.py index a323205..e07c9cc 100644 --- a/webserver.py +++ b/webserver.py @@ -9,8 +9,11 @@ import database import postsqldb from webpush import trigger_push_notifications_for_subscriptions from scripts.recipes import recipes_api +from flasgger import Swagger + app = Flask(__name__, instance_relative_config=True) +swagger = Swagger(app) UPLOAD_FOLDER = 'static/pictures' FILES_FOLDER = 'static/files' app.config.from_pyfile('application.cfg.py') @@ -66,6 +69,18 @@ def inject_user(): @app.route("/transactions/") @login_required def transactions(id): + """This is the main endpoint to reach the webpage for an items transaction history + --- + parameters: + - name: id + in: path + type: integer + required: true + default: all + responses: + 200: + description: Returns the transactions.html webpage for the item with passed ID + """ sites = [site[1] for site in main.get_sites(session['user']['sites'])] return render_template("items/transactions.html", id=id, current_site=session['selected_site'], sites=sites)