diff --git a/application/recipes/__pycache__/database_recipes.cpython-312.pyc b/application/recipes/__pycache__/database_recipes.cpython-312.pyc index bc37820..1b66db5 100644 Binary files a/application/recipes/__pycache__/database_recipes.cpython-312.pyc and b/application/recipes/__pycache__/database_recipes.cpython-312.pyc differ diff --git a/application/recipes/__pycache__/recipes_api.cpython-312.pyc b/application/recipes/__pycache__/recipes_api.cpython-312.pyc index a38ed2c..7319074 100644 Binary files a/application/recipes/__pycache__/recipes_api.cpython-312.pyc and b/application/recipes/__pycache__/recipes_api.cpython-312.pyc differ diff --git a/application/recipes/database_recipes.py b/application/recipes/database_recipes.py index e4886dd..90ab78c 100644 --- a/application/recipes/database_recipes.py +++ b/application/recipes/database_recipes.py @@ -1,6 +1,12 @@ from application import postsqldb import config -import psycopg2 +import psycopg2 +import random +import string + +def getUUID(n): + random_string = ''.join(random.choices(string.ascii_letters + string.digits, k=n)) + return random_string def getModalSKUs(site, payload, convert=True): database_config = config.config() @@ -42,6 +48,23 @@ def getItemData(site:str, payload:tuple, convert:bool=True): except (Exception, psycopg2.DatabaseError) as error: raise postsqldb.DatabaseError(error, payload, sql) +def getUnits(convert:bool=True): + database_config = config.config() + recordset = () + sql = f"SELECT id, fullname FROM units;" + try: + with psycopg2.connect(**database_config) as conn: + with conn.cursor() as cur: + cur.execute(sql) + rows = cur.fetchall() + if rows and convert: + recordset = [postsqldb.tupleDictionaryFactory(cur.description, row) for row in rows] + if rows and not convert: + recordset = rows + return recordset + except (Exception, psycopg2.DatabaseError) as error: + raise postsqldb.DatabaseError(error, (), sql) + def getRecipes(site:str, payload:tuple, convert=True): recordset = [] count = 0 diff --git a/application/recipes/recipes_api.py b/application/recipes/recipes_api.py index 1c40883..9719ce9 100644 --- a/application/recipes/recipes_api.py +++ b/application/recipes/recipes_api.py @@ -1,16 +1,19 @@ -from flask import Blueprint, request, render_template, redirect, session, url_for, send_file, jsonify, Response, current_app, send_from_directory -import psycopg2, math, json, datetime, main, copy, requests, process, database, pprint, MyDataclasses -from config import config, sites_config -from main import unfoldCostLayers +# 3rd party imports +from flask import ( + Blueprint, request, render_template, session, jsonify, current_app, send_from_directory + ) +import math + +# application imports +import main from user_api import login_required -import os -import postsqldb, webpush +import webpush from application.recipes import database_recipes from application import postsqldb as db -recipes_api = Blueprint('recipes_api', __name__) +recipes_api = Blueprint('recipes_api', __name__, template_folder="templates", static_folder="static") -@recipes_api.route("/recipes") +@recipes_api.route("/") @login_required def recipes(): """This is the main endpoint to reach the webpage for a list of all recipes @@ -20,11 +23,11 @@ def recipes(): 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", + return render_template("index.html", current_site=session['selected_site'], sites=sites) -@recipes_api.route("/recipe//") +@recipes_api.route("//") @login_required def recipe(id, mode='view'): """This is the main endpoint to reach the webpage for a recipe's view or edit mode. @@ -44,16 +47,15 @@ def recipe(id, mode='view'): 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) - + + units = database_recipes.getUnits() + print("woot") if mode == "edit": - return render_template("recipes/recipe_edit.html", recipe_id=id, current_site=session['selected_site'], units=units) + return render_template("recipe_edit.html", recipe_id=id, current_site=session['selected_site'], units=units) if mode == "view": - return render_template("recipes/recipe_view.html", recipe_id=id, current_site=session['selected_site']) + return render_template("recipe_view.html", recipe_id=id, current_site=session['selected_site']) -@recipes_api.route('/recipes/getRecipes', methods=["GET"]) +@recipes_api.route('/getRecipes', methods=["GET"]) @login_required def getRecipes(): """ Get a subquery of recipes from the database by passing a page, limit @@ -73,7 +75,7 @@ def getRecipes(): 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"]) +@recipes_api.route('/getRecipe', methods=["GET"]) @login_required def getRecipe(): """ Get a query for recipe id from database by passing an id @@ -91,7 +93,7 @@ def getRecipe(): return jsonify({'recipe': recipe, 'error': False, 'message': 'Recipe returned successfully!'}) return jsonify({'recipe': recipe, 'error': True, 'message': f'method {request.method} not allowed'}) -@recipes_api.route('/recipes/addRecipe', methods=["POST"]) +@recipes_api.route('/addRecipe', methods=["POST"]) @login_required def addRecipe(): """ post a new recipe into the database by passing a recipe_name and recipe description @@ -103,21 +105,19 @@ def addRecipe(): if request.method == "POST": recipe_name = request.get_json()['recipe_name'] recipe_description = request.get_json()['recipe_description'] - database_config = config() site_name = session['selected_site'] user_id = session['user_id'] - with psycopg2.connect(**database_config) as conn: - recipe = db.RecipesTable.Payload( - name=recipe_name, - author=user_id, - description=recipe_description - ) - recipe = database_recipes.postAddRecipe(site_name, recipe.payload()) - webpush.push_ntfy('New Recipe', f"New Recipe added to {site_name}; {recipe_name}! {recipe_description} \n http://test.treehousefullofstars.com/recipe/view/{recipe['id']} \n http://test.treehousefullofstars.com/recipe/edit/{recipe['id']}") + recipe = db.RecipesTable.Payload( + name=recipe_name, + author=user_id, + description=recipe_description + ) + recipe = database_recipes.postAddRecipe(site_name, recipe.payload()) + webpush.push_ntfy('New Recipe', f"New Recipe added to {site_name}; {recipe_name}! {recipe_description} \n http://test.treehousefullofstars.com/recipe/view/{recipe['id']} \n http://test.treehousefullofstars.com/recipe/edit/{recipe['id']}") return jsonify({'recipe': recipe, 'error': False, 'message': 'Recipe added successful!'}) return jsonify({'recipe': recipe, 'error': True, 'message': f'method {request.method}'}) -@recipes_api.route('/recipe/getItems', methods=["GET"]) +@recipes_api.route('/getItems', methods=["GET"]) @login_required def getItems(): """ Pass along a page, limit, and search strings to get a pagination of items from the system @@ -138,7 +138,7 @@ def getItems(): 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"]) +@recipes_api.route('/postUpdate', methods=["POST"]) @login_required def postUpdate(): """ This is an endpoint for updating an RecipeTuple in the sites recipes table @@ -159,7 +159,7 @@ def postUpdate(): return jsonify({'recipe': recipe, 'error': False, 'message': 'Update of Recipe successful!'}) return jsonify({'recipe': recipe, 'error': True, 'message': 'Update of Recipe unsuccessful!'}) -@recipes_api.route('/recipe/postCustomItem', methods=["POST"]) +@recipes_api.route('/postCustomItem', methods=["POST"]) @login_required def postCustomItem(): """ post a recipe item to the database by passing a uuid, recipe_id, item_type, item_name, uom, qty, and link @@ -173,7 +173,7 @@ def postCustomItem(): site_name = session['selected_site'] rp_id = int(request.get_json()['rp_id']) recipe_item = db.RecipesTable.ItemPayload( - uuid=f"%{int(request.get_json()['rp_id'])}{database.getUUID(6)}%", + uuid=f"%{int(request.get_json()['rp_id'])}{database_recipes.getUUID(6)}%", rp_id=rp_id, item_type=request.get_json()['item_type'], item_name=request.get_json()['item_name'], @@ -186,7 +186,7 @@ def postCustomItem(): return jsonify({'recipe': recipe, 'error': False, 'message': 'Recipe Item was added successful!'}) return jsonify({'recipe': recipe, 'error': True, 'message': f'method {request.method} not allowed!'}) -@recipes_api.route('/recipe/postSKUItem', methods=["POST"]) +@recipes_api.route('/postSKUItem', methods=["POST"]) @login_required def postSKUItem(): """ post a recipe item to the database by passing a recipe_id and item_id @@ -201,7 +201,6 @@ def postSKUItem(): item_id = int(request.get_json()['item_id']) site_name = session['selected_site'] item = database_recipes.getItemData(site_name, (item_id, )) - print(item) recipe_item = db.RecipesTable.ItemPayload( uuid=item['barcode'], rp_id=recipe_id, @@ -217,7 +216,7 @@ def postSKUItem(): return jsonify({'recipe': recipe, 'error': False, 'message': 'Recipe Item was added successful!'}) return jsonify({'recipe': recipe, 'error': True, 'message': f'method {request.method} is not allowed!'}) -@recipes_api.route('/recipe/postImage/', methods=["POST"]) +@recipes_api.route('/postImage/', methods=["POST"]) @login_required def uploadImage(recipe_id): """ post an image for a recipe into the database and files by passing the recipe_id and picture_path @@ -239,7 +238,7 @@ def uploadImage(recipe_id): database_recipes.postUpdateRecipe(site_name, {'id': recipe_id, 'update': {'picture_path': file.filename.replace(" ", "_")}}) return jsonify({'error': False, 'message': 'Recipe was updated successfully!'}) -@recipes_api.route('/recipe/getImage/') +@recipes_api.route('/getImage/') @login_required def get_image(recipe_id): """ get the picture path for a recipe by passing teh recipe id in the path @@ -258,7 +257,7 @@ def get_image(recipe_id): picture_path = database_recipes.getPicturePath(site_name, (recipe_id,)) return send_from_directory('static/pictures/recipes', picture_path) -@recipes_api.route('/recipe/deleteRecipeItem', methods=["POST"]) +@recipes_api.route('/deleteRecipeItem', methods=["POST"]) @login_required def deleteRecipeItem(): """ delete recipe item from database by passing the recipe item ID @@ -276,7 +275,7 @@ def deleteRecipeItem(): return jsonify({'recipe': recipe, 'error': False, 'message': f'Recipe Item {deleted_item['item_name']} was deleted successful!'}) return jsonify({'recipe': recipe, 'error': True, 'message': f'method {request.method} is not allowed!'}) -@recipes_api.route('/recipe/saveRecipeItem', methods=["POST"]) +@recipes_api.route('/saveRecipeItem', methods=["POST"]) @login_required def saveRecipeItem(): """ post an update to a recipe item in the database by passing the recipe item ID and an update diff --git a/static/handlers/recipeEditHandler.js b/application/recipes/static/js/recipeEditHandler.js similarity index 97% rename from static/handlers/recipeEditHandler.js rename to application/recipes/static/js/recipeEditHandler.js index 7bca145..804e270 100644 --- a/static/handlers/recipeEditHandler.js +++ b/application/recipes/static/js/recipeEditHandler.js @@ -195,7 +195,7 @@ async function saveLineItem(item){ } async function getRecipe() { - const url = new URL('/recipe/getRecipe', window.location.origin) + const url = new URL('/recipes/getRecipe', window.location.origin) url.searchParams.append('id', recipe_id); const response = await fetch(url) data = await response.json() @@ -203,7 +203,7 @@ async function getRecipe() { } async function getImage(){ - await fetch(`/recipe/getImage/${recipe.id}`) + await fetch(`/recipes/getImage/${recipe.id}`) .then(response => response.blob()) .then(imageBlob => { const imageURL = URL.createObjectURL(imageBlob); @@ -220,7 +220,7 @@ async function addCustomItem() { uom: document.getElementById('customUOM').value, links: {main: document.getElementById('customWeblink').value} } - const response = await fetch(`/recipe/postCustomItem`, { + const response = await fetch(`/recipes/postCustomItem`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -248,7 +248,7 @@ async function addCustomItem() { } async function addSKUItem(item_id) { - const response = await fetch(`/recipe/postSKUItem`, { + const response = await fetch(`/recipes/postSKUItem`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -277,13 +277,9 @@ async function addSKUItem(item_id) { let updated = {} async function postUpdate() { let description = document.getElementById('recipeDescription').value - updated.description = description - - - console.log(updated) - + updated.description = description - const response = await fetch(`/recipe/postUpdate`, { + const response = await fetch(`/recipes/postUpdate`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -313,7 +309,7 @@ async function updateImage() { const formData = new FormData(); formData.append('file', fileInput.files[0]); - await fetch(`/recipe/postImage/${recipe.id}`, { + await fetch(`/recipes/postImage/${recipe.id}`, { method: 'POST', body: formData }) @@ -458,7 +454,7 @@ async function updateItemsPaginationElement() { } async function fetchItems() { - const url = new URL('/recipe/getItems', window.location.origin); + const url = new URL('/recipes/getItems', window.location.origin); url.searchParams.append('page', pagination_current); url.searchParams.append('limit', items_limit); url.searchParams.append('search_string', search_string); diff --git a/static/handlers/recipeViewHandler.js b/application/recipes/static/js/recipeViewHandler.js similarity index 96% rename from static/handlers/recipeViewHandler.js rename to application/recipes/static/js/recipeViewHandler.js index b577fee..287aa59 100644 --- a/static/handlers/recipeViewHandler.js +++ b/application/recipes/static/js/recipeViewHandler.js @@ -84,7 +84,7 @@ async function replenishInstructions() { } async function getRecipe() { - const url = new URL('/recipe/getRecipe', window.location.origin) + const url = new URL('/recipes/getRecipe', window.location.origin) url.searchParams.append('id', recipe_id); const response = await fetch(url) data = await response.json() @@ -93,7 +93,7 @@ async function getRecipe() { async function getImage(){ console.log('fetching image!') - await fetch(`/recipe/getImage/${recipe.id}`) + await fetch(`/recipes/getImage/${recipe.id}`) .then(response => response.blob()) .then(imageBlob => { const imageURL = URL.createObjectURL(imageBlob); diff --git a/static/handlers/recipesListHandler.js b/application/recipes/static/js/recipesListHandler.js similarity index 99% rename from static/handlers/recipesListHandler.js rename to application/recipes/static/js/recipesListHandler.js index 5b846bb..d5372c7 100644 --- a/static/handlers/recipesListHandler.js +++ b/application/recipes/static/js/recipesListHandler.js @@ -107,12 +107,12 @@ async function replenishRecipesTable() { let viewOp = document.createElement('a') viewOp.innerHTML = `view ` viewOp.setAttribute('class', 'uk-button uk-button-default uk-button-small') - viewOp.href = `/recipe/view/${recipes[i].id}` + viewOp.href = `/recipes/view/${recipes[i].id}` let editOp = document.createElement('a') editOp.innerHTML = `edit ` editOp.setAttribute('class', 'uk-button uk-button-default uk-button-small') - editOp.href = `/recipe/edit/${recipes[i].id}` + editOp.href = `/recipes/edit/${recipes[i].id}` buttonGroup.append(viewOp, editOp) opsCell.append(buttonGroup) diff --git a/templates/recipes/index.html b/application/recipes/templates/index.html similarity index 98% rename from templates/recipes/index.html rename to application/recipes/templates/index.html index 0d4fe8b..d1bf1ff 100644 --- a/templates/recipes/index.html +++ b/application/recipes/templates/index.html @@ -159,5 +159,5 @@ - + \ No newline at end of file diff --git a/templates/recipes/recipe_edit.html b/application/recipes/templates/recipe_edit.html similarity index 99% rename from templates/recipes/recipe_edit.html rename to application/recipes/templates/recipe_edit.html index d79ac9e..f49ed61 100644 --- a/templates/recipes/recipe_edit.html +++ b/application/recipes/templates/recipe_edit.html @@ -328,5 +328,5 @@ const recipe_id = {{recipe_id|tojson}} const units = {{units|tojson}} - + \ No newline at end of file diff --git a/templates/recipes/recipe_view.html b/application/recipes/templates/recipe_view.html similarity index 98% rename from templates/recipes/recipe_view.html rename to application/recipes/templates/recipe_view.html index 1d99e71..e049802 100644 --- a/templates/recipes/recipe_view.html +++ b/application/recipes/templates/recipe_view.html @@ -124,5 +124,5 @@ const session = {{session|tojson}} const recipe_id = {{recipe_id|tojson}} - + \ No newline at end of file diff --git a/database.log b/database.log index d6dabb4..1d1e97e 100644 --- a/database.log +++ b/database.log @@ -1841,4 +1841,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-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 + 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;') +2025-04-26 21:18:52.710178 --- ERROR --- DatabaseError(message='duplicate key value violates unique constraint "test_recipe_items_uuid_key"DETAIL: Key (uuid)=(%X0031BMH6V%) already exists.', + payload=('%X0031BMH6V%', 3, 'sku', 'Torani Peppermint syrup', 1, 1.0, 2020, '{}'), + sql='INSERT INTO test_recipe_items(uuid, rp_id, item_type, item_name, uom, qty, item_id, links) VALUES (%s, %s, %s, %s, %s, %s, %s, %s) RETURNING *;') \ No newline at end of file diff --git a/static/pictures/recipes/20250425_163102.jpg b/static/pictures/recipes/20250425_163102.jpg new file mode 100644 index 0000000..cdcba4d Binary files /dev/null and b/static/pictures/recipes/20250425_163102.jpg differ diff --git a/webserver.py b/webserver.py index 1675ae2..a29e91c 100644 --- a/webserver.py +++ b/webserver.py @@ -32,7 +32,7 @@ app.register_blueprint(workshop_api) app.register_blueprint(receipts_API.receipt_api) app.register_blueprint(shopping_list_API.shopping_list_api) app.register_blueprint(group_api.groups_api) -app.register_blueprint(recipes_api.recipes_api) +app.register_blueprint(recipes_api.recipes_api, url_prefix='/recipes')