-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2 from Esekyi/feature/recipe_crud
Recipe crud operations
- Loading branch information
Showing
38 changed files
with
1,440 additions
and
58 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,26 @@ | ||
import os | ||
from werkzeug.utils import secure_filename | ||
from dotenv import load_dotenv | ||
from datetime import timedelta | ||
|
||
|
||
# loading environment variables from .env file | ||
load_dotenv() | ||
|
||
class Config: | ||
SECRET_KEY = os.environ.get('SECRET_KEY') or 'with_this_you-will-never-guess' | ||
SQLALCHEMY_DATABASE_URI = os.environ.get( | ||
SECRET_KEY = os.getenv('SECRET_KEY') or 'with_this_you-will-never-guess' | ||
SQLALCHEMY_DATABASE_URI = os.getenv( | ||
'DATABASE_URL') or 'postgresql://spiceshare:fudf2024@localhost/spiceshare_db' | ||
SQLALCHEMY_TRACK_MODIFICATIONS = False | ||
SESSION_COOKIE_HTTPONLY = True | ||
SESSION_COOKIE_SECURE = True | ||
REMEMBER_COOKIE_DURATION = timedelta(days=14) | ||
SERVER_NAME = '127.0.0.1:5000' | ||
|
||
|
||
UPLOAD_FOLDER = 'app/static/uploads' | ||
ALLOWED_EXTENSIONS = {'jpg', 'jpeg', 'png', 'gif'} | ||
# AWS S3 configuration | ||
S3_ACCESS_KEY_ID = os.getenv('AWS_ACCESS_KEY_ID') | ||
S3_SECRET_ACCESS_KEY = os.getenv('AWS_SECRET_ACCESS_KEY') | ||
S3_REGION = os.getenv('AWS_REGION') | ||
S3_BUCKET_NAME = os.getenv('S3_BUCKET_NAME') | ||
S3_URL = f"https://{S3_BUCKET_NAME}.s3.{S3_REGION}.amazonaws.com/" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
from app import db | ||
from datetime import datetime | ||
import uuid | ||
from sqlalchemy.dialects.postgresql import UUID | ||
|
||
|
||
class Instruction(db.Model): | ||
id = db.Column(UUID(as_uuid=True), primary_key=True, | ||
default=uuid.uuid4) | ||
step_number = db.Column(db.Integer, nullable=False) | ||
name = db.Column(db.Text, nullable=False) | ||
recipe_id = db.Column(UUID(as_uuid=True), db.ForeignKey( | ||
'recipe.id'), nullable=False) | ||
created_at = db.Column(db.DateTime, default=datetime.utcnow) | ||
updated_at = db.Column( | ||
db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) | ||
|
||
def __repr__(self) -> str: | ||
return f'<Instruction {self.step_number}: {self.name[:20]}>' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
from flask import Blueprint, jsonify, request | ||
from app.services.recipe_service import get_all_recipes | ||
|
||
api = Blueprint('api', __name__) | ||
|
||
def serialize_recipe(recipe): | ||
"""Convert a Recipe object to a dictionary.""" | ||
return { | ||
'id': recipe.id, | ||
'title': recipe.title, | ||
'description': recipe.description, | ||
'image_url': recipe.image_url, | ||
'created_at': recipe.created_at.isoformat(), # Convert datetime to string if needed | ||
'view_count': recipe.view_count | ||
} | ||
|
||
@api.route('/recipes', methods=['GET'], strict_slashes=False) | ||
def recipes(): | ||
page = int(request.args.get('page', 1)) | ||
per_page = int(request.args.get('per_page', 3)) | ||
try: | ||
paginated_recipes = get_all_recipes(page, per_page) | ||
return jsonify({ | ||
'recipes': paginated_recipes['items'], | ||
'total_items': paginated_recipes['total_items'], | ||
'total_pages': paginated_recipes['total_pages'], | ||
'current_page': paginated_recipes['current_page'] | ||
}) | ||
except ValueError as e: | ||
return jsonify({'error': str(e)}), 400 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
from flask import Blueprint, request, url_for, render_template, jsonify, flash, redirect | ||
from app.services.category_service import CategoryService | ||
|
||
cat_bp = Blueprint('category_routes', __name__) | ||
|
||
|
||
@cat_bp.route('/categories', methods=['POST'], strict_slashes=False) | ||
def create_category(): | ||
data = request.json | ||
name = data.get('name') | ||
description = data.get('description') | ||
|
||
if not name: | ||
return jsonify({'error': 'Category name is required'}), 400 | ||
|
||
try: | ||
new_category = CategoryService.create_category(name, description) | ||
return jsonify({ | ||
'message': 'Category created successfully', | ||
'category': { | ||
'id': new_category.id, | ||
'name': new_category.name, | ||
'description': new_category.description | ||
} | ||
}), 201 | ||
except ValueError as e: | ||
return jsonify({'error': str(e)}), 400 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
from flask import Blueprint, render_template, flash | ||
|
||
main = Blueprint('main', __name__) | ||
|
||
@main.route('/', methods=['GET']) | ||
def index(): | ||
return render_template('index.html') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,102 @@ | ||
from flask import Blueprint, jsonify, request | ||
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.validation_service import validate_recipe_data | ||
from app.services.category_service import CategoryService | ||
from app.services.image_service import upload_image_to_s3 | ||
from flask_login import login_required, current_user | ||
from app.models.user import User | ||
import os | ||
|
||
bp = Blueprint('recipe_route', __name__) | ||
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 | ||
def add_recipe(): | ||
if request.method == 'POST': | ||
data = request.form.to_dict() # ensuring form data comes as to dictionary. | ||
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')) | ||
|
||
|
||
image_url = None | ||
if image: | ||
try: | ||
image_url = upload_image_to_s3(image) | ||
flash("Image was uploaded", "success") | ||
except ValueError as e: | ||
flash(str(e), 'error') | ||
return redirect(url_for('recipe_routes.add_recipe')) | ||
|
||
|
||
try: | ||
send_url = image_url if image_url is not None else '' | ||
user_id = current_user.id # Ensure user_id is set correctly | ||
create_recipe(data, user_id, ingredients, instructions, send_url) # Pass user_id correctly | ||
flash("Recipe created successfully!", 'success') | ||
return redirect(url_for('recipe_routes.list_recipes')) | ||
|
||
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.add_recipe')) | ||
|
||
|
||
categories = CategoryService.get_all_categories() | ||
return render_template('recipes/createPages/create.html', categories=categories, title='Create Recipe | SpiceShare Inc.') | ||
|
||
|
||
@bp.route('/recipes', methods=['GET'], strict_slashes=False) | ||
def list_recipes(): | ||
page = int(request.args.get('page', 1)) | ||
per_page = int(request.args.get('per_page', 9)) | ||
paginated_recipes = get_all_recipes(page, per_page) | ||
|
||
# Fetch user details for each recipe | ||
for recipe in paginated_recipes['items']: | ||
recipe.user = db.session.query(User).filter_by(id=recipe.user_id).first() | ||
return render_template('recipes/readPages/allRecipes.html', | ||
recipes=paginated_recipes['items'], | ||
total_items=paginated_recipes['total_items'], | ||
total_pages=paginated_recipes['total_pages'], | ||
current_page=paginated_recipes['current_page'], | ||
per_page=per_page, | ||
title='Recipes | SpiceShare Inc.') | ||
|
||
|
||
@bp.route('/recipes/<uuid:recipe_id>', methods=['GET'], strict_slashes=False) | ||
def view_recipe(recipe_id): | ||
recipe = get_recipe_by_id(recipe_id) | ||
recipe.view_count += 1 | ||
db.session.commit() | ||
|
||
return render_template('recipes/readPages/recipe_detail.html', recipe=recipe, title=f'{recipe.title} - SpiceShare Inc.') | ||
|
||
|
||
@bp.route('/recipes/<uuid:recipe_id>/edit', methods=['PUT'], strict_slashes=False) | ||
@login_required | ||
def edit_recipe(recipe_id): | ||
pass | ||
|
||
|
||
@bp.route('/recipes/<uuid:recipe_id>/delete', methods=['DELETE'], strict_slashes=False) | ||
@login_required | ||
def remove_recipe(recipe_id): | ||
pass | ||
|
||
@bp.route('/most_viewed', methods=['GET']) | ||
def most_viewed(): | ||
recipes = get_most_viewed_recipes(limit=5) # adjust with preferred limit default 5 | ||
return recipes |
Oops, something went wrong.