Skip to content

Commit

Permalink
Merge pull request #3 from Esekyi/feature/recipe_crud
Browse files Browse the repository at this point in the history
MVP Complete - ready for deployment
  • Loading branch information
Esekyi authored Sep 6, 2024
2 parents 7034b94 + 96ab7c0 commit 3daf4d4
Show file tree
Hide file tree
Showing 38 changed files with 1,470 additions and 305 deletions.
1 change: 1 addition & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
web: gunicorn run:app
2 changes: 2 additions & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
3 changes: 2 additions & 1 deletion app/routes/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ def recipes():
per_page = int(request.args.get('per_page', 3))
try:
paginated_recipes = get_all_recipes(page, per_page)
serialized_recipes = [serialize_recipe(recipe) for recipe in paginated_recipes['items']]
return jsonify({
'recipes': paginated_recipes['items'],
'recipes': serialized_recipes,
'total_items': paginated_recipes['total_items'],
'total_pages': paginated_recipes['total_pages'],
'current_page': paginated_recipes['current_page']
Expand Down
3 changes: 2 additions & 1 deletion app/routes/auth_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@

auth_bp = Blueprint('auth', __name__)

@auth_bp.route('/login', methods=['GET','POST'])

@auth_bp.route('/login', methods=['GET', 'POST'], strict_slashes=False)
def login():
"""Login authentication handler"""
# Check if the user is already authenticated
Expand Down
3 changes: 2 additions & 1 deletion app/routes/main_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

main = Blueprint('main', __name__)

@main.route('/', methods=['GET'])

@main.route('/', methods=['GET'], strict_slashes=False)
def index():
return render_template('index.html')
143 changes: 129 additions & 14 deletions app/routes/recipe_routes.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
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.category import Category
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
Expand Down Expand Up @@ -76,27 +77,141 @@ def list_recipes():
title='Recipes | SpiceShare Inc.')


@bp.route('/recipes/<uuid:recipe_id>', methods=['GET'], strict_slashes=False)
@bp.route('/recipes/<uuid:recipe_id>', 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/<uuid:recipe_id>/edit', methods=['PUT'], strict_slashes=False)
@bp.route('/recipes/<uuid:recipe_id>/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() # Ensure this is a dictionary

form_ingredients = request.form.getlist('ingredients[]')
form_instructions = request.form.getlist('instructions[]')
image = request.files.get('image')

# If they are strings instead of lists
if isinstance(ingredients, str):
ingredients = [ingredients]
if isinstance(instructions, str):
instructions = [instructions]


errors = validate_recipe_data(data)
if errors:
for error in errors:
flash(error, 'error')
return redirect(url_for('recipe_routes.edit_recipe', recipe_id=recipe_id))

image_url = recipe.image_url
if image:
try:
image_url = upload_image_to_s3(image)
flash("Image updated successfully", "success")
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, form_ingredients, form_instructions, image_url) # Ensure data is a dict
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 updating the recipe: {str(e)}", "error")
return redirect(url_for('recipe_routes.edit_recipe', recipe_id=recipe_id))

categories = CategoryService.get_all_categories()
current_category = db.session.query(
Category).filter_by(id=recipe.category_id).first()
return render_template('recipes/createPages/edit.html', recipe=recipe, ingredients=ingredients, current_category=current_category, instructions=instructions, categories=categories, title=f'Edit Recipe {recipe.title} | SpiceShare Inc.')


@bp.route('/recipes/<uuid:recipe_id>/delete', methods=['DELETE'], strict_slashes=False)
@bp.route('/recipes/<uuid:recipe_id>/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')

@bp.route('/most_viewed', methods=['GET'])
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/<uuid:recipe_id>/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/<uuid:comment_id>/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/<uuid:comment_id>/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))
78 changes: 42 additions & 36 deletions app/routes/user_routes.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
"""User services routes"""
from flask import Blueprint, jsonify, request, flash, redirect, url_for, render_template
from app import db
from app.services.user_services import get_all_users, delete_user, get_user_by_id, update_user, create_user, get_user_by_email
from app.services.user_services import get_all_users, delete_user, get_user_by_id, update_user_details, create_user, get_user_by_email, get_user_by_username, is_valid_username
from app.models.recipe import Recipe
from flask_login import login_required, current_user
from werkzeug.security import check_password_hash

# user routes blueprint
bp = Blueprint('user_routes', __name__)

@bp.route('/users', methods=['GET'], strict_slashes=False)
@login_required
def list_users():
"""get all users in db"""
users = get_all_users()
Expand All @@ -22,17 +24,6 @@ def list_users():
return jsonify(user_list)


@bp.route('/user/<uuid:user_id>', methods=["GET"], strict_slashes=False)
@login_required
def get_user(user_id):
"""get a specific user by id"""
user = get_user_by_id(str(user_id))
if not user:
flash('User not found', 'danger')
return redirect(url_for('main.index'))
return jsonify({"username": user.username, "email": user.email})


@bp.route('/register', methods=['GET', 'POST'], strict_slashes=False)
def register():
if request.method == 'POST':
Expand All @@ -48,51 +39,66 @@ def register():
return redirect(url_for('user_routes.register'))

existing_user = get_user_by_email(email)
existing_username = get_user_by_username(username)
if existing_user:
flash('Email already registered', 'error')
return redirect(url_for('user_routes.register'))

if existing_username:
flash("Username already exists.", 'error')
return redirect(url_for('user_routes.register'))

if not is_valid_username(username):
flash(
'Username should be at least 6 characters long must contain only letters, numbers, and underscores, with no spaces.',
'error')
return redirect(url_for('user_routes.register'))

try:
create_user(first_name, last_name, password, email, username)
flash('Registration successful, proceed to login!', 'success')
create_user(first_name, last_name, username, email, password)
flash('Registration successful, proceed to login!', 'info')
return redirect(url_for('auth.login'))
except Exception as e:
db.session.rollback()
flash('An error occured during registeration. Please try again', 'error')
return redirect(url_for('user_routes.register'))

if current_user.is_authenticated:
return redirect(url_for('user_routes.user_profile', user_id=current_user.id))
return render_template('user_auth/register.html')


@bp.route('/user/<uuid:user_id>/edit', methods=["POST"], strict_slashes=False)
@login_required
def edit_user(user_id):
"""Update user details by id"""
"""Update user details by id (only handles POST requests)"""
user = get_user_by_id(str(user_id))
if user and user.id == current_user.id:
data = request.form
new_email = data.get('email')

# run a quick db check to see if email already exist
all_users = get_all_users()
if any(u.email == new_email and u.id != user.id for u in all_users):
flash('Email already in use', 'danger')
return redirect(url_for('user_routes.get_user', user_id=user_id))
if current_user.id != user.id:
print(current_user.id)
flash('You are not authorized to edit this profile', 'error')
return redirect(url_for('user_routes.user_profile', user_id=current_user.id))

if request.method == 'POST':
first_name = request.form.get('first_name')
last_name = request.form.get('last_name')
old_password = request.form.get('old_password')
new_password = request.form.get('new_password')
confirm_password = request.form.get('confirm_password')

if check_password_hash(user.password_hash, old_password):
if new_password == confirm_password:
update_user_details(user.id, first_name, last_name, new_password)
flash('Profile updated successfully', 'success')
else:
flash('New Password and confirm password do not match', 'error')
else:
flash('Old password is incorrect', 'error')

return redirect(url_for('user_routes.user_profile', user_id=user_id))

update_user(
user,
email=new_email,
password=data.get('password'),
first_name=data.get('first_name'),
last_name=data.get('last_name')
)

flash('Profile updated successfully', 'success')
return redirect(url_for('user_routes.get_user', user_id=user_id))

else:
flash('You are not authorized to edit this profile.', 'danger')
return redirect(url_for('main.index'))


@bp.route('/user/<uuid:user_id>/delete', methods=['POST'], strict_slashes=False)
Expand All @@ -109,7 +115,7 @@ def delete_user_profile(user_id):
return redirect(url_for('main.index'))


@bp.route('/user/<uuid:user_id>/profile', methods=['GET'])
@bp.route('/user/<uuid:user_id>/profile', methods=['GET'], strict_slashes=False)
@login_required
def user_profile(user_id):
user = get_user_by_id(user_id)
Expand Down
37 changes: 37 additions & 0 deletions app/services/comment_service.py
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 3daf4d4

Please sign in to comment.