Skip to content

Commit

Permalink
Merge pull request #4 from Esekyi/feature/search
Browse files Browse the repository at this point in the history
Feature/search - Implemented Search feature ✅
  • Loading branch information
Esekyi authored Sep 7, 2024
2 parents d2c7d06 + 1e25451 commit 5ba770b
Show file tree
Hide file tree
Showing 16 changed files with 189 additions and 34 deletions.
5 changes: 3 additions & 2 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,13 @@ def create_app():
csrf.init_app(app)

# register blueprint
from app.routes import user_routes, recipe_routes, auth_routes, category_routes, main_routes, api
from app.routes import user_routes, recipe_routes, auth_routes, category_routes, main_routes,search_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(search_routes.search_bp)
app.register_blueprint(category_routes.cat_bp, url_prefix='/api')
app.register_blueprint(api.api, url_prefix='/api/v1')


Expand Down
2 changes: 1 addition & 1 deletion app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
class Config:
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'
'DATABASE_URL') or 'postgresql://spiceshare:o@localhost/spiceshare_db'
SQLALCHEMY_TRACK_MODIFICATIONS = False
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SECURE = True
Expand Down
5 changes: 5 additions & 0 deletions app/models/recipe.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,8 @@ class Recipe(db.Model):

def __repr__(self):
return f'<Recipe {self.title}>'

def increment_view_count(self):
"""Increment the view count without updating the updated_at timestamp."""
self.view_count += 1
# No commit or onupdate manipulation here
15 changes: 13 additions & 2 deletions app/routes/auth_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@
auth_bp = Blueprint('auth', __name__)


def is_safe_url(target):
"""Check if the target URL is safe for redirection."""
from urllib.parse import urlparse, urljoin
# Allow relative URLs
if target.startswith('/'):
return True
safe = urlparse(target).scheme in ['http', 'https'] and \
urljoin(request.host_url, target) == target
return safe

@auth_bp.route('/login', methods=['GET', 'POST'], strict_slashes=False)
def login():
"""Login authentication handler"""
Expand All @@ -19,12 +29,13 @@ def login():
if request.method == 'POST':
email = request.form["email"]
password = request.form["password"]
next_page = request.form.get("next")
user = User.query.filter_by(email=email).first()

if user and check_password_hash(user.password_hash, password):
login_user(user)
next_page = request.args.get('next')
if next_page:

if next_page and is_safe_url(next_page):
return redirect(next_page)
else:
flash('logged in', 'success')
Expand Down
15 changes: 11 additions & 4 deletions app/routes/recipe_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,10 @@ def add_recipe():
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")
flash(f"An error occurred while creating the recipe: {str(e)}", "error")
# If recipe creation fails, delete the uploaded image if it was uploaded
if image_url:
delete_image_from_s3(image_url) # delete the image if it was uploaded
return redirect(url_for('recipe_routes.add_recipe'))


Expand Down Expand Up @@ -87,9 +89,11 @@ def view_recipe(recipe_id):
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.increment_view_count() # Increment view count without committing
recipe.user = db.session.query(User).filter_by(id=recipe.user_id).first()

# Commit all changes here
db.session.commit() # Commit after all changes are made
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")
Expand Down Expand Up @@ -140,6 +144,9 @@ def edit_recipe(recipe_id):
# Rollback db session in case of an error
db.session.rollback()
flash(f"An error occurred while updating the recipe: {str(e)}", "error")
# delete the image if it was uploaded
if image_url:
delete_image_from_s3(image_url)
return redirect(url_for('recipe_routes.edit_recipe', recipe_id=recipe_id))

categories = CategoryService.get_all_categories()
Expand Down
25 changes: 25 additions & 0 deletions app/routes/search_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from flask import Blueprint, request, render_template
from app.services.search_service import search_recipes

search_bp = Blueprint('search', __name__)


@search_bp.route('/search', methods=['GET'], strict_slashes=False)
def search():
query = request.args.get('q', '').strip()
page = int(request.args.get('page', 1))
per_page = int(request.args.get('per_page', 10))

if not query:
return render_template('search_results.html', query=query, recipes=[])

results = search_recipes(query, page, per_page)

return render_template(
'search_results.html',
query=query,
recipes=results['items'],
total_pages=results['total_pages'],
current_page=results['current_page'],
per_page=results['per_page'],
)
11 changes: 9 additions & 2 deletions app/routes/user_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
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_details, create_user, get_user_by_email, get_user_by_username, is_valid_username
from app.services.recipe_service import get_recipes_by_user
from app.models.recipe import Recipe
from flask_login import login_required, current_user
from werkzeug.security import check_password_hash
Expand Down Expand Up @@ -119,10 +120,16 @@ def delete_user_profile(user_id):
@login_required
def user_profile(user_id):
user = get_user_by_id(user_id)
page = int(request.args.get('page', 1))
per_page = int(request.args.get('per_page', 9))

if user != current_user:
flash("You are not authorized to view page", 'danger')
return redirect(url_for('index'))

recipes = Recipe.query.filter_by(user_id=user.id).all()
return render_template('user_auth/user_profile.html',recipes=recipes, user=user)
paginated_user_recipes = get_recipes_by_user(user.id, page, per_page)
return render_template('user_auth/user_profile.html', recipes=paginated_user_recipes['items'], user=user, total_items=paginated_user_recipes['total_items'],
total_pages=paginated_user_recipes['total_pages'],
current_page=paginated_user_recipes['current_page'],
per_page=per_page
)
8 changes: 5 additions & 3 deletions app/services/recipe_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ def update_recipe(recipe, data, ingredients, instructions, image_url):


def get_recipe_by_id(recipe_id):
"""Fetch a recipe by its ID without updating the updated_at timestamp."""
return Recipe.query.get_or_404(recipe_id)


Expand Down Expand Up @@ -186,6 +187,7 @@ def get_random_recipes_with_images(limit=3):
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)
def get_recipes_by_user(user_id, page=1, per_page=9):
"""Fetch recipes created by a specific user with pagination."""
recipes = Recipe.query.filter_by(user_id=user_id).all()
return paginate(recipes, page, per_page)
32 changes: 32 additions & 0 deletions app/services/search_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from app import db
from app.models.recipe import Recipe
from app.models.category import Category
from app.models.ingredient import Ingredient
from app.models.instruction import Instruction
from app.models.user import User
from app.services.pagination_service import paginate
from sqlalchemy import or_


def search_recipes(query, page=1, per_page=10):
"""
Search for recipes based on a query string.
The query can match recipe names, ingredients, author or categories.
"""
query = f"%{query.lower()}%"


recipes = Recipe.query.join(Ingredient).join(Category).join(User).filter(
or_(
Recipe.title.ilike(query),
Recipe.description.ilike(query),
Ingredient.name.ilike(query),
Category.name.ilike(query),
User.first_name.ilike(query),
User.last_name.ilike(query),
User.username.ilike(query)
)
).distinct()


return paginate(recipes.all(), page, per_page)
4 changes: 0 additions & 4 deletions app/static/css/output.css

This file was deleted.

17 changes: 12 additions & 5 deletions app/templates/recipes/readPages/allRecipes.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ <h1 class="text-3xl font-bold">Recipes</h1>
<button type="button" class="bg-blue-600 text-white py-2 px-4 rounded"
onclick="window.location.href='{{ url_for('recipe_routes.add_recipe') }}';">+ Add Recipe</button>
</div>
<div class="mt-6">
<input type="text" name="search" placeholder="Search by recipe name, category, ingredients..."
class="w-full border border-gray-300 rounded-lg py-2 px-4 mb-6">
<div class="mt-6 mb-6">
<form action="{{ url_for('search.search') }}" method="get" class="flex">
<input type="text" name="q" placeholder="Search by recipe name, category, ingredients..."
class="w-full border border-gray-300 rounded-l-lg py-3 px-4 focus:outline-none focus:border-indigo-600">
<button type="submit" class="bg-indigo-600 text-white px-6 py-3 rounded-r-lg hover:bg-indigo-700 transition duration-300">Search</button>
</form>
</div>
<div class="grid grid-cols-3 gap-6">
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">

{% for recipe in recipes %}
<div class="bg-white shadow-lg rounded-lg overflow-hidden">
Expand Down Expand Up @@ -77,4 +80,8 @@ <h3 class="text-lg font-bold">Savory Porridge</h3>
</div>
</section>

{% endblock %}
{% endblock %}

{% block scripts %}
<script src="{{ url_for('static', filename='js/scripts.js') }}"></script>
{% endblock scripts %}
7 changes: 5 additions & 2 deletions app/templates/recipes/readPages/read_layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,16 @@
<!-- Include SortableJS for drag-and-drop functionality -->
</head>

<body class="bg-gray-50">
<body class="bg-gray-50 flex flex-col min-h-screen">
<!-- Header -->
<header class="p-4 border-b border-gray-200 shadow-md">
<div class="max-w-6xl mx-auto flex justify-between items-center">
<div class="text-xl font-bold text-blue-800">SpiceShare</div>

<nav>
<a href="{{ url_for('recipe_routes.list_recipes')}}" class="text-blue-800 mx-4">Recipes</a>


{% if current_user.is_authenticated %}
<a href="{{ url_for('user_routes.user_profile', user_id=current_user.id)}}" class="text-blue-800 mx-4">My Profile</a>
<a href="{{ url_for('auth.logout')}}" class="text-blue-800">Logout</a>
Expand All @@ -44,7 +47,7 @@
</div>
</header>
<!--- Container for page -->
<main class="container mx-auto px-4 mt-8">
<main class="container mx-auto px-4 mt-8 flex-grow">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class=" mb-4 flash-messages">
Expand Down
4 changes: 2 additions & 2 deletions app/templates/recipes/readPages/recipe_detail.html
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{% extends "recipes/readPages/read_layout.html" %}
{% block content %}

<body class="bg-white text-gray-900 font-sans">
<body class="bg-white text-gray-900 font-sans flex flex-col min-h-screen">

<!-- Main Content -->
<main class="max-w-4xl mx-auto p-4">
<main class="max-w-4xl mx-auto p-4 flex-grow">
<!-- Recipe Title -->
{% if recipe %}
<section class="text-center mb-8">
Expand Down
43 changes: 43 additions & 0 deletions app/templates/search_results.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{% extends "recipes/readPages/read_layout.html" %}
{% block content %}
<div class="container mx-auto mt-8">
<h1 class="text-3xl font-semibold mb-6 text-indigo-600">Search Results</h1>

{% if query %}
<p class="mb-4">Results for "<strong>{{ query }}</strong>":</p>
{% else %}
<p class="mb-4">No search query provided.</p>
{% endif %}

{% if recipes %}
<ul class="space-y-4">
{% for recipe in recipes %}
<li class="border-b pb-4">
<a href="{{ url_for('recipe_routes.view_recipe', recipe_id=recipe.id) }}"
class="text-xl text-blue-600 hover:underline">{{ recipe.title }}</a>
<p class="text-gray-700">{{ recipe.description[:150] }}...</p>
<p class="text-gray-500 text-sm">By {{ recipe.author.username }} | Category: {{ recipe.category.name }}</p>
</li>
{% endfor %}
</ul>

<!-- Pagination -->
<div class="flex justify-center mt-6">
<div class="inline-flex items-center space-x-4">
{% if current_page > 1 %}
<a href="?q={{query}}&page={{ current_page - 1 }}&per_page={{ per_page }}"
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">Previous</a>
{% endif %}
<span class="text-gray-600">Page {{ current_page }} of {{ total_pages }}</span>
{% if current_page < total_pages %}
<a href="?q={{ query }}&page={{ current_page + 1 }}&per_page={{ per_page }}"
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">Next</a>
{% endif %}
</div>
</div>

{% else %}
<p class="text-gray-500">No recipes found matching your search criteria.</p>
{% endif %}
</div>
{% endblock %}
1 change: 1 addition & 0 deletions app/templates/user_auth/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ <h2 class="text-2xl font-bold text-gray-800 mb-6 text-center">Sign In</h2>
</button>
</div>
</div>
<input type="hidden" name="next" value="{{ request.args.get('next') }}">
<div class="flex items-center mb-6">
<label class="inline-flex items-center text-gray-700">
<input id="remember_me" name="remember_me" type="checkbox" class="form-checkbox text-blue-600">
Expand Down
29 changes: 22 additions & 7 deletions app/templates/user_auth/user_profile.html
Original file line number Diff line number Diff line change
Expand Up @@ -111,14 +111,14 @@ <h2 class="text-3xl font-bold text-gray-800">Profile</h2>
<button type="submit" class="w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-md
transition duration-300">Save Changes</button>
</div>
</form>
</form>

<!-- Posted Recipes Section -->
<div class="mt-8">
<h3 class="text-2xl font-bold text-gray-800 mb-4">Your Recipes</h3>
{% if recipes%}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{% for recipe in recipes %}
<!-- Posted Recipes Section -->
<div class="mt-8">
<h3 class="text-2xl font-bold text-gray-800 mb-4">Your Recipes</h3>
{% if recipes%}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{% for recipe in recipes %}
<div class="bg-white p-4 rounded-lg shadow-md">
{% if recipe.image_url %}
<img src="https://recipe-files.s3.eu-north-1.amazonaws.com/recipes/{{ recipe.image_url }}" alt="{{ recipe.title }}"
Expand All @@ -136,6 +136,21 @@ <h4 class="text-xl font-semibold">{{ recipe.title }}</h4>
</div>
{% endfor %}
</div>

<!-- Pagination -->
<div class="flex justify-center mt-6">
<div class="inline-flex items-center space-x-4">
{% if current_page > 1 %}
<a href="?page={{ current_page - 1 }}&per_page={{ per_page }}"
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">Previous</a>
{% endif %}
<span class="text-gray-600">Page {{ current_page }} of {{ total_pages }}</span>
{% if current_page < total_pages %} <a href="?page={{ current_page + 1 }}&per_page={{ per_page }}"
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">Next</a>
{% endif %}
</div>
</div>

{% else %}
<p class="text-gray-700">You haven't posted any recipes yet.</p>
{% endif %}
Expand Down

0 comments on commit 5ba770b

Please sign in to comment.