Skip to content

Commit

Permalink
Merge pull request #2 from Esekyi/feature/recipe_crud
Browse files Browse the repository at this point in the history
Recipe crud operations
  • Loading branch information
Esekyi authored Aug 31, 2024
2 parents e183fca + 90ff6cd commit 80048bd
Show file tree
Hide file tree
Showing 38 changed files with 1,440 additions and 58 deletions.
8 changes: 7 additions & 1 deletion app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_wtf import CSRFProtect
from app.config import Config
from flask_login import LoginManager
from uuid import UUID
Expand All @@ -13,6 +14,7 @@
# initializing db extensions
db = SQLAlchemy()
migrate = Migrate()
csrf = CSRFProtect()

def create_app():
if not os.environ.get('FLASK_DEBUG'):
Expand All @@ -25,12 +27,16 @@ def create_app():
db.init_app(app)
migrate.init_app(app,db)
login_manager.init_app(app)
csrf.init_app(app)

# register blueprint
from app.routes import user_routes, recipe_routes, auth_routes
from app.routes import user_routes, recipe_routes, auth_routes, category_routes, main_routes, api
app.register_blueprint(user_routes.bp)
app.register_blueprint(recipe_routes.bp)
app.register_blueprint(auth_routes.auth_bp)
app.register_blueprint(category_routes.cat_bp, url_prefix='/api')
app.register_blueprint(main_routes.main)
app.register_blueprint(api.api, url_prefix='/api/v1')


return app
Expand Down
23 changes: 19 additions & 4 deletions app/config.py
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/"
1 change: 1 addition & 0 deletions app/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
from app.models.recipe import Recipe
from app.models.comment import Comment
from app.models.ingredient import Ingredient
from app.models.instruction import Instruction
1 change: 0 additions & 1 deletion app/models/ingredient.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ class Ingredient(db.Model):
id = db.Column(UUID(as_uuid=True), primary_key=True,
default=uuid.uuid4)
name = db.Column(db.String(120), nullable=False)
quantity = db.Column(db.String(50), 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)
Expand Down
19 changes: 19 additions & 0 deletions app/models/instruction.py
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]}>'
6 changes: 5 additions & 1 deletion app/models/recipe.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ class Recipe(db.Model):
default=uuid.uuid4)
title = db.Column(db.String(120), nullable=False)
description = db.Column(db.Text, nullable=False)
instructions = db.Column(db.Text, nullable=False)
# default value in Fahrenheit # in °F
oven_temp = db.Column(db.Integer, default=60)
prep_time = db.Column(db.Integer) # in minutes
cook_time = db.Column(db.Integer) # in minutes
servings = db.Column(db.Integer)
image_url = db.Column(db.String(255))
view_count = db.Column(db.Integer, default=0)
category_id = db.Column(UUID(as_uuid=True), db.ForeignKey(
'category.id'), nullable=False)
user_id = db.Column(UUID(as_uuid=True), db.ForeignKey(
Expand All @@ -25,6 +27,8 @@ class Recipe(db.Model):
comments = db.relationship('Comment', backref='recipe', lazy=True)
ingredients = db.relationship(
'Ingredient', backref='recipe', lazy='dynamic')
instructions = db.relationship(
'Instruction', backref='recipes', lazy='dynamic')

def __repr__(self):
return f'<Recipe {self.title}>'
31 changes: 31 additions & 0 deletions app/routes/api.py
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

14 changes: 12 additions & 2 deletions app/routes/auth_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,33 @@
@auth_bp.route('/login', methods=['GET','POST'])
def login():
"""Login authentication handler"""
# Check if the user is already authenticated
if current_user.is_authenticated:
return redirect(url_for('user_routes.user_profile', user_id=current_user.id))

if request.method == 'POST':
email = request.form["email"]
password = request.form["password"]
user = User.query.filter_by(email=email).first()

if user and check_password_hash(user.password_hash, password):
login_user(user)
return jsonify({"success": "you're logged in"})
next_page = request.args.get('next')
if next_page:
return redirect(next_page)
else:
flash('logged in', 'success')
return redirect(url_for('main.index'))

flash('Invalid Credentials', 'warning')

return render_template('login.html')
return render_template('user_auth/login.html')

@auth_bp.route('/logout')
@login_required
def logout():
"""logout handler"""
logout_user()
flash("You've been logged out succesfully!",'success')
return redirect(url_for('auth.login'))

27 changes: 27 additions & 0 deletions app/routes/category_routes.py
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
7 changes: 7 additions & 0 deletions app/routes/main_routes.py
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')
102 changes: 100 additions & 2 deletions app/routes/recipe_routes.py
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
Loading

0 comments on commit 80048bd

Please sign in to comment.