diff --git a/app/__init__.py b/app/__init__.py index 7bde02a..9805d2d 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -10,6 +10,8 @@ login_manager = LoginManager() login_manager.login_view = 'auth.login' +login_manager.login_message = 'You Must Login to Access This Page!' +login_manager.login_message_category = 'error' # initializing db extensions db = SQLAlchemy() diff --git a/app/routes/recipe_routes.py b/app/routes/recipe_routes.py index f77f55f..351507b 100644 --- a/app/routes/recipe_routes.py +++ b/app/routes/recipe_routes.py @@ -1,17 +1,17 @@ from flask import Blueprint, request, url_for, render_template, flash, redirect from app import db -from app.services.recipe_service import get_all_recipes, get_recipe_by_id, create_recipe, get_most_viewed_recipes +from app.services.recipe_service import get_all_recipes, get_recipe_by_id, create_recipe, get_most_viewed_recipes, update_recipe, delete_recipe, get_recipe_with_details from app.services.validation_service import validate_recipe_data from app.services.category_service import CategoryService -from app.services.image_service import upload_image_to_s3 +from app.services.image_service import upload_image_to_s3, delete_image_from_s3 +from app.services.comment_service import CommentService from flask_login import login_required, current_user from app.models.user import User +from app.models.comment import Comment import os bp = Blueprint('recipe_routes', __name__) -# s3_url = f"https://{os.getenv('S3_BUCKET_NAME')}.s3{os.getenv('AWS_REGION')}.amazonaws.com/" - @bp.route('/recipes/create', methods=['GET', 'POST'], strict_slashes=False) @login_required @@ -76,28 +76,134 @@ def list_recipes(): title='Recipes | SpiceShare Inc.') -@bp.route('/recipes/', methods=['GET'], strict_slashes=False) +@bp.route('/recipes/', methods=['GET', 'POST'], strict_slashes=False) def view_recipe(recipe_id): - recipe = get_recipe_by_id(recipe_id) - recipe.view_count += 1 - db.session.commit() + recipe, ingredients, instructions = get_recipe_with_details(recipe_id) + comments = CommentService.get_comment_by_recipe(recipe_id) # Fetch comments for the recipe - return render_template('recipes/readPages/recipe_detail.html', recipe=recipe, title=f'{recipe.title} - SpiceShare Inc.') + # Fetch user details for each comment + for comment in comments: + comment.user = db.session.query(User).filter_by(id=comment.user_id).first() # Get the user who made the comment + + if recipe: + recipe.view_count += 1 + db.session.commit() + recipe.user = db.session.query(User).filter_by(id=recipe.user_id).first() + return render_template('recipes/readPages/recipe_detail.html', recipe=recipe, ingredients=ingredients, instructions=instructions, comments=comments, title=f'{recipe.title} - SpiceShare Inc.') + else: + flash("Recipe not found.", "error") + return redirect(url_for('recipe_routes.list_recipes')) -@bp.route('/recipes//edit', methods=['PUT'], strict_slashes=False) +@bp.route('/recipes//edit', methods=['GET', 'POST'], strict_slashes=False) @login_required def edit_recipe(recipe_id): - pass + recipe, ingredients, instructions = get_recipe_with_details(recipe_id) + if request.method == 'POST': + if recipe and recipe.user_id == current_user.id: + data = request.form.to_dict() + ingredients = request.form.getlist('ingredients[]') + instructions = request.form.getlist('instructions[]') + image = request.files.get('image') + + errors = validate_recipe_data(data) + if errors: + for error in errors: + flash(error, 'error') + return redirect(url_for('recipe_routes.add_recipe', recipe_id=recipe_id)) + + image_url = recipe.image_url + if image: + try: + image_url = upload_image_to_s3(image) + flash("Image updated successfully", "succes") + except ValueError as e: + flash(str(e), 'error') + return redirect(url_for('recipe_routes.edit_recipe', recipe_id=recipe_id)) + + try: + update_recipe(recipe, data, ingredients, + instructions, image_url) + flash( + f"Recipe {recipe.title} updated successfully!", 'success') + return redirect(url_for('recipe_routes.view_recipe', recipe_id=recipe_id)) + + except Exception as e: + # Rollback db session in case of an error + db.session.rollback() + flash( + f"An error occurred while creating the recipe: {str(e)}", "error") + return redirect(url_for('recipe_routes.edit_recipe', recipe_id=recipe_id)) -@bp.route('/recipes//delete', methods=['DELETE'], strict_slashes=False) + categories = CategoryService.get_all_categories() + return render_template('recipes/createPages/edit1.html', recipe=recipe, ingredients=ingredients, instructions=instructions, categories=categories, title=f'Edit Recipe {recipe.title} | SpiceShare Inc.') + + +@bp.route('/recipes//delete', methods=['POST'], strict_slashes=False) @login_required def remove_recipe(recipe_id): - pass + recipe = get_recipe_by_id(recipe_id) + + if recipe and recipe.user_id == current_user.id: + try: + if recipe.image_url: + if delete_image_from_s3(recipe.image_url): + flash("Image deleted", 'success') + else: + flash("Failed to delet Image", 'info') + + delete_recipe(recipe) + flash("Recipe deleted successfully!", 'info') + return redirect(url_for('recipe_routes.recipe_list')) + + except Exception as e: + flash( + f"An error occured while deleting the recipe: {str(e)}", "error") + else: + + flash("Unauthorized action.", 'error') + return redirect(url_for('recipe_routes.list_recipes')) @bp.route('/most_viewed', methods=['GET'], strict_slashes=False) def most_viewed(): recipes = get_most_viewed_recipes(limit=5) # adjust with preferred limit default 5 return recipes + + +@bp.route('/recipes//comments', methods=['POST']) +@login_required +def add_comment(recipe_id): + text = request.form.get('text') + if text: + comment = CommentService.add_comment(recipe_id, current_user.id, text) + flash("Comment added successfully!", "success") + else: + flash("Comment cannot be empty.", "info") + return redirect(url_for('recipe_routes.view_recipe', recipe_id=recipe_id)) + + +@bp.route('/comments//update', methods=['POST']) +@login_required +def update_comment(comment_id): + text = request.form.get('text') + comment = CommentService.update_comment( + comment_id, current_user.id, text) # Pass current_user.id + if comment: + flash("Comment updated successfully!", "success") + else: + flash("Comment not found or you are not authorized to update it.", "error") + return redirect(url_for('recipe_routes.view_recipe', recipe_id=comment.recipe_id)) + + +@bp.route('/comments//delete', methods=['POST']) +@login_required +def delete_comment(comment_id): + # Pass current_user.id + comment = Comment.query.get_or_404(comment_id) + if CommentService.delete_comment(comment_id, current_user.id): + flash("Comment deleted successfully!", "info") + else: + flash("Comment not found or you are not authorized to delete it.", "error") + return redirect(url_for('recipe_routes.view_recipe', recipe_id=comment.recipe_id)) diff --git a/app/services/comment_service.py b/app/services/comment_service.py new file mode 100644 index 0000000..2970747 --- /dev/null +++ b/app/services/comment_service.py @@ -0,0 +1,37 @@ +from app import db +from app.models.comment import Comment + +class CommentService: + @staticmethod + def add_comment(recipe_id, user_id, text): + """Add a comment to a recipe.""" + new_comment = Comment(recipe_id=recipe_id, user_id=user_id, text=text) + db.session.add(new_comment) + db.session.commit() + return new_comment + + + @staticmethod + def get_comment_by_recipe(recipe_id): + """Retrieve all comments for a given recipe.""" + return Comment.query.filter_by(recipe_id=recipe_id).order_by(Comment.created_at.desc()).all() + + + @staticmethod + def update_comment(comment_id, user_id, text): + """Updates a comments.""" + comment = Comment.query.get_or_404(comment_id) + if comment and comment.user_id == user_id: + comment.text = text + db.session.commit() + return comment + return None + + @staticmethod + def delete_comment(comment_id, user_id): + comment = Comment.query.get_or_404(comment_id) + if comment and comment.user_id == user_id: + db.session.delete(comment) + db.session.commit() + return True + return False diff --git a/app/services/image_service.py b/app/services/image_service.py index 18817b6..78a923b 100644 --- a/app/services/image_service.py +++ b/app/services/image_service.py @@ -3,10 +3,10 @@ from flask import current_app import uuid from werkzeug.utils import secure_filename -import logging # Add this import at the top +import logging # Configure logging -logging.basicConfig(level=logging.DEBUG) # Set the logging level to DEBUG +logging.basicConfig(level=logging.DEBUG) # logging level to DEBUG @@ -17,7 +17,6 @@ def upload_image_to_s3(image, folder='recipes'): aws_secret_access_key = current_app.config['S3_SECRET_ACCESS_KEY'] region_name = current_app.config['S3_REGION'] bucket_name = current_app.config['S3_BUCKET_NAME'] - try: # Generating a random and unique filename for image @@ -57,3 +56,45 @@ def upload_image_to_s3(image, folder='recipes'): except Exception as e: logging.error(f"An unexpected error occurred: {e}") raise ValueError(f"An unexpected error occurred: {e}") + + +def delete_image_from_s3(image_filename, folder='recipes'): + """ + Deletes an image from the S3 bucket. + + :param image_filename: The filename of the image to be deleted. + :return: True if the file was deleted, False if the file was not found or deletion failed. + """ + + aws_access_key_id = current_app.config['S3_ACCESS_KEY_ID'] + aws_secret_access_key = current_app.config['S3_SECRET_ACCESS_KEY'] + region_name = current_app.config['S3_REGION'] + bucket_name = current_app.config['S3_BUCKET_NAME'] + + try: + s3_client = boto3( + 's3', + aws_access_key_id=aws_access_key_id, + aws_secret_access_key=aws_secret_access_key, + region_name=region_name + ) + + + # Delete the file from S3 + response = s3_client.delete_object( + Bucket=bucket_name, + Key=f"{folder}/{image_filename}" + ) + + if response.get('ResponseMetadata', {}).get('HTTPStatusCode') == 204: + return True + else: + return False + + + except ClientError as e: + logging.error(f"Failed to delete image: {e}") + return False + except Exception as e: + logging.error(f"An unexpected error occurred: {e}") + return False diff --git a/app/services/recipe_service.py b/app/services/recipe_service.py index db6a974..2f6869c 100644 --- a/app/services/recipe_service.py +++ b/app/services/recipe_service.py @@ -1,3 +1,5 @@ +# Adjust the import based on your project structure +from app.models.recipe import Recipe from app.models.recipe import Recipe from app.models.ingredient import Ingredient from app.models.comment import Comment @@ -9,65 +11,65 @@ def create_recipe(data, user_id, ingredients, instructions, send_url): - """ - Create a new recipe and save it to the database. - - :param data: A dictionary containing recipe data. - :param ingredients: A list of ingredients to associate with the recipe. + """ + Create a new recipe and save it to the database. + + :param data: A dictionary containing recipe data. + :param ingredients: A list of ingredients to associate with the recipe. :param comments: A list of comments to associate with the recipe. - :param user_id: The ID of the user creating the recipe. - :return: The newly created Recipe object. - """ - try: - oven_temp = int(data.get('oven_temp')) if data.get('oven_temp') else 60, - prep_time = int(data.get('prep_time')) if data.get('prep_time') else 0 - cook_time = int(data.get('cook_time')) if data.get('cook_time') else 0 - servings = int(data.get('servings')) if data.get('servings') else 0 - - new_recipe = Recipe( - title=data['title'], - description=data['description'], - oven_temp=oven_temp, - prep_time=prep_time, # Default to 0 if not provided - cook_time=cook_time, # Default to 0 if not provided - servings=servings, # Default to 0 if not provided - category_id=data['category_id'], + :param user_id: The ID of the user creating the recipe. + :return: The newly created Recipe object. + """ + try: + oven_temp = int(data.get('oven_temp')) if data.get( + 'oven_temp') else 60, + prep_time = int(data.get('prep_time')) if data.get('prep_time') else 0 + cook_time = int(data.get('cook_time')) if data.get('cook_time') else 0 + servings = int(data.get('servings')) if data.get('servings') else 0 + + new_recipe = Recipe( + title=data['title'], + description=data['description'], + oven_temp=oven_temp, + prep_time=prep_time, # Default to 0 if not provided + cook_time=cook_time, # Default to 0 if not provided + servings=servings, # Default to 0 if not provided + category_id=data['category_id'], user_id=user_id, - image_url=send_url - ) - db.session.add(new_recipe) - db.session.flush() # Flush pending transaction to get the recipe ID before committing - - # Handle ingredients - for ingredient_name in ingredients: - if ingredient_name: # not tolerating any empty ingredient - ingredient = Ingredient( - name=ingredient_name, - recipe_id=new_recipe.id - ) - db.session.add(ingredient) - - # Handle each instruction - for i, instruction in enumerate(instructions): - if instruction: - new_instruction = Instruction( - step_number=i + 1, - name=instruction, - recipe_id=new_recipe.id - ) - db.session.add(new_instruction) - - - db.session.commit() # comit all changes to different tables - return new_recipe - - except Exception as e: - db.session.rollback() - raise Exception(f"Failed to create recipe: {str(e)}") + image_url=send_url + ) + db.session.add(new_recipe) + db.session.flush() # Flush pending transaction to get the recipe ID before committing + + # Handle ingredients + for ingredient_name in ingredients: + if ingredient_name: # not tolerating any empty ingredient + ingredient = Ingredient( + name=ingredient_name, + recipe_id=new_recipe.id + ) + db.session.add(ingredient) + + # Handle each instruction + for i, instruction in enumerate(instructions): + if instruction: + new_instruction = Instruction( + step_number=i + 1, + name=instruction, + recipe_id=new_recipe.id + ) + db.session.add(new_instruction) + + db.session.commit() # comit all changes to different tables + return new_recipe + + except Exception as e: + db.session.rollback() + raise Exception(f"Failed to create recipe: {str(e)}") def get_all_recipes(page=1, per_page=3): - """Get all recipes with pagination. + """Get all recipes with pagination. Args: page (int): The current page number. @@ -77,29 +79,102 @@ def get_all_recipes(page=1, per_page=3): dict: A dictionary containing paginated recipes and pagination info. """ - recipes = Recipe.query.order_by(Recipe.created_at.desc()).all() - return paginate(recipes, page, per_page) + recipes = Recipe.query.order_by(Recipe.created_at.desc()).all() + return paginate(recipes, page, per_page) + + +def update_recipe(recipe, data, ingredients, instructions, image_url): + """Update an existing recipe with new data.""" + try: + recipe.title = data.get('title', recipe.title) + recipe.description = data.get('description', recipe.description) + recipe.category_id = data.grt('category_id', recipe.category_id) + recipe.oven_temp = int(data.get('oven_temp', recipe.oven_temp)) if data.get( + 'oven_temp') else recipe.oven_temp + recipe.prep_time = int(data.get('prep_time', recipe.prep_time)) if data.get( + 'prep_time') else recipe.prep_time + recipe.cook_time = int(data.get('cook_time', recipe.cook_time)) if data.get( + 'cook_time') else recipe.cook_time + recipe.servings = int(data.get('servings', recipe.servings)) if data.get( + 'servings') else recipe.servings + recipe.image_url = image_url + + # Update ingredients + existing_ingredients = { + ing.id: ing for ing in Ingredient.query.filter_by(recipe_id=recipe.id).all()} + for ingredient_data in ingredients: + if ingredient_data: + ingredient_id = ingredient_data.get('id') + if ingredient_id and ingredient_id in existing_ingredients: + existing_ingredients[ingredient_id].name = ingredient_data['name'] + else: + # add new ingredient + new_ingredient = Ingredient( + name=ingredient_data['name'], + recipe_id=recipe.id + ) + deb.session.add(new_ingredient) + + # Update instructions + existing_instructions = { + instr.step_number: instr for instr in Instruction.query.filter_by(recipe_id=recipe.id).all()} + for i, instruction in enumerate(instructions): + if instruction: + if i + 1 in existing_instructions: + # update existing instructions + existing_instructions[i + 1].name = instruction + else: + new_instruction = Instruction( + step_number=i + 1, + name=instruction, + recipe_id=recipe.id + ) + db.session.add(new_instruction) + + db.session.commit() + + except Exception as e: + db.session.rollback() + raise Exception(f"Failed to create recipe: {str(e)}") def get_recipe_by_id(recipe_id): - return Recipe.query.get_or_404(recipe_id) + return Recipe.query.get_or_404(recipe_id) + + +def get_recipe_with_details(recipe_id): + """Fetch a recipe along with its ingredients and instructions.""" + recipe = Recipe.query.get(recipe_id) + if recipe: + ingredients = Ingredient.query.filter_by(recipe_id=recipe.id).all() + instructions = Instruction.query.filter_by( + recipe_id=recipe.id).order_by(Instruction.step_number).all() + return recipe, ingredients, instructions + return None, None, None def delete_recipe(recipe): + """Delete a recipe from the database.""" db.session.delete(recipe) + db.session.commit() def get_most_viewed_recipes(limit=5): - """ - Check through Recipe table and get the most viewed recipes - - sorted from descending on the view count field. limit of recipes needed - :param int limit, 0""" - return Recipe.query.order_by(Recipe.view_count.desc()).limit(limit).all() + """ + Check through Recipe table and get the most viewed recipes - + sorted from descending on the view count field. limit of recipes needed + :param int limit, 0""" + return Recipe.query.order_by(Recipe.view_count.desc()).limit(limit).all() def get_random_recipes_with_images(limit=3): - recipes_with_image = Recipe.query.filter( - Recipe.image_url != None, Recipe.image_url != '').all() - if len(recipes_with_image) > limit: - return random.sample(recipes_with_image, limit) - return recipes_with_image + recipes_with_image = Recipe.query.filter( + Recipe.image_url != None, Recipe.image_url != '').all() + if len(recipes_with_image) > limit: + return random.sample(recipes_with_image, limit) + return recipes_with_image + + +def get_recipes_by_user(user_id): + """Fetch recipes created by a specific user.""" + return Recipe.query.filter_by(user_id=user_id)