Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@
from routes.verify_user import (
verify_user_blueprint,
)
from routes.bookmark import (
bookmark_blueprint,
)
from routes.my_bookmarks import (
my_bookmarks_blueprint,
)
from settings import Settings
from utils.after_request import after_request_logger
from utils.before_request.browser_language import browser_language
Expand Down Expand Up @@ -256,6 +262,8 @@ def after_request(response):
app.register_blueprint(return_post_banner_blueprint)
app.register_blueprint(admin_panel_comments_blueprint)
app.register_blueprint(change_profile_picture_blueprint)
app.register_blueprint(bookmark_blueprint)
app.register_blueprint(my_bookmarks_blueprint)


if __name__ == "__main__":
Expand Down
Binary file modified app/instance/flaskblog.db
Binary file not shown.
15 changes: 15 additions & 0 deletions app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,18 @@ class Comment(db.Model):

def __repr__(self):
return f"<Comment {self.id} on Post {self.post_id}>"


class Bookmark(db.Model):
__tablename__ = "bookmarks"

id = db.Column(db.Integer, primary_key=True, autoincrement=True)
user_id = db.Column(db.Integer, db.ForeignKey("users.user_id", ondelete="CASCADE"))
post_id = db.Column(db.Integer, db.ForeignKey("posts.id", ondelete="CASCADE"))
time_stamp = db.Column(db.Integer, default=current_time_stamp)
Comment on lines +80 to +83
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd / && find . -name "models.py" -type f 2>/dev/null | head -20

Repository: DogukanUrker/FlaskBlog

Length of output: 479


🏁 Script executed:

# First, locate the file structure
git ls-files | grep -E "(models|app)" | head -20

Repository: DogukanUrker/FlaskBlog

Length of output: 574


🏁 Script executed:

# Read the models.py file, specifically around lines 80-83
cat -n app/models.py | sed -n '70,95p'

Repository: DogukanUrker/FlaskBlog

Length of output: 973


🏁 Script executed:

# Check the entire Bookmark model definition
cat -n app/models.py | grep -A 20 "class Bookmark"

Repository: DogukanUrker/FlaskBlog

Length of output: 735


Enforce NOT NULL for bookmark foreign keys.
The user_id and post_id columns are nullable by default. In SQL, multiple NULL values satisfy unique constraints (NULL ≠ NULL), allowing multiple bookmarks with missing user or post references. This defeats the uniqueness constraint intent (ensuring each user can bookmark each post only once). Add nullable=False to both foreign key columns.

Suggested fix
-    user_id = db.Column(db.Integer, db.ForeignKey("users.user_id", ondelete="CASCADE"))
-    post_id = db.Column(db.Integer, db.ForeignKey("posts.id", ondelete="CASCADE"))
+    user_id = db.Column(db.Integer, db.ForeignKey("users.user_id", ondelete="CASCADE"), nullable=False)
+    post_id = db.Column(db.Integer, db.ForeignKey("posts.id", ondelete="CASCADE"), nullable=False)
🤖 Prompt for AI Agents
In `@app/models.py` around lines 80 - 83, The user_id and post_id foreign key
columns are currently nullable which breaks the uniqueness intent; update the
bookmark model's user_id and post_id Column definitions to include
nullable=False (i.e., set db.Column(..., db.ForeignKey("users.user_id",
ondelete="CASCADE"), nullable=False) and similarly for post_id) so NULLs cannot
be inserted and any existing UniqueConstraint on this model will behave
correctly; locate the columns named user_id and post_id in the bookmark model in
app/models.py and add nullable=False to each db.Column call.


# 确保每个用户对每篇文章只能收藏一次
__table_args__ = (db.UniqueConstraint('user_id', 'post_id', name='_user_post_uc'),)

def __repr__(self):
return f"<Bookmark user_id={self.user_id} post_id={self.post_id}>"
134 changes: 134 additions & 0 deletions app/routes/bookmark.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
"""
Bookmark routes for handling post bookmarks
"""

from flask import Blueprint, jsonify, session, request
from database import db
from models import Bookmark, Post, User
from utils.log import Log

bookmark_blueprint = Blueprint("bookmark", __name__)


@bookmark_blueprint.route("/api/bookmark/<int:post_id>", methods=["POST"])
def toggle_bookmark(post_id):
"""Toggle bookmark status for a post"""
try:
# Check if user is logged in
if "username" not in session:
return jsonify({"error": "User not logged in"}), 401

# Get user ID
user = User.query.filter_by(username=session["username"]).first()
if not user:
return jsonify({"error": "User not found"}), 404

# Check if post exists
post = Post.query.get(post_id)
if not post:
return jsonify({"error": "Post not found"}), 404

# Check if bookmark already exists
existing_bookmark = Bookmark.query.filter_by(
user_id=user.user_id,
post_id=post_id
).first()

if existing_bookmark:
# Remove bookmark
db.session.delete(existing_bookmark)
db.session.commit()
Log.info(f"Bookmark removed for user {user.username} on post {post_id}")
return jsonify({"bookmarked": False, "message": "Bookmark removed"}), 200
else:
# Add bookmark
new_bookmark = Bookmark(
user_id=user.user_id,
post_id=post_id
)
db.session.add(new_bookmark)
db.session.commit()
Log.info(f"Bookmark added for user {user.username} on post {post_id}")
return jsonify({"bookmarked": True, "message": "Bookmark added"}), 201

except Exception as e:
db.session.rollback()
Log.error(f"Error toggling bookmark: {e}")
return jsonify({"error": "Internal server error"}), 500


@bookmark_blueprint.route("/api/bookmark/status/<int:post_id>", methods=["GET"])
def get_bookmark_status(post_id):
"""Get bookmark status for a specific post"""
try:
# Check if user is logged in
if "username" not in session:
return jsonify({"bookmarked": False}), 200

# Get user ID
user = User.query.filter_by(username=session["username"]).first()
if not user:
return jsonify({"bookmarked": False}), 200

# Check if bookmark exists
bookmark = Bookmark.query.filter_by(
user_id=user.user_id,
post_id=post_id
).first()

return jsonify({"bookmarked": bookmark is not None}), 200

except Exception as e:
Log.error(f"Error getting bookmark status: {e}")
return jsonify({"error": "Internal server error"}), 500


@bookmark_blueprint.route("/my-bookmarks")
def my_bookmarks():
"""Display user's bookmarked posts"""
try:
# Check if user is logged in
if "username" not in session:
return jsonify({"error": "User not logged in"}), 401

# Get user ID
user = User.query.filter_by(username=session["username"]).first()
if not user:
return jsonify({"error": "User not found"}), 404

# Get user's bookmarks with post details
bookmarks = db.session.query(Bookmark, Post).join(
Post, Bookmark.post_id == Post.id
).filter(
Bookmark.user_id == user.user_id
).order_by(
Bookmark.time_stamp.desc()
).all()

# Format data for template
bookmarked_posts = []
for bookmark, post in bookmarks:
bookmarked_posts.append({
'id': post.id,
'title': post.title,
'tags': post.tags,
'content': post.content,
'banner': post.banner,
'author': post.author,
'views': post.views,
'time_stamp': post.time_stamp,
'last_edit_time_stamp': post.last_edit_time_stamp,
'category': post.category,
'url_id': post.url_id,
'abstract': post.abstract,
'bookmark_time': bookmark.time_stamp
})

return jsonify({
"bookmarked_posts": bookmarked_posts,
"username": user.username
}), 200

except Exception as e:
Log.error(f"Error getting user bookmarks: {e}")
return jsonify({"error": "Internal server error"}), 500
Comment on lines +86 to +134
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Resolve /my-bookmarks route conflict and JSON serialization risk.
There’s already an HTML page at /my-bookmarks in app/routes/my_bookmarks.py. This JSON endpoint will collide. Also, post.banner is LargeBinary and will likely fail JSON serialization.

✅ Suggested fix (example)
-@bookmark_blueprint.route("/my-bookmarks")
+@bookmark_blueprint.route("/api/my-bookmarks")
 def my_bookmarks():
@@
-                'banner': post.banner,
-                'content': post.content,
+                # Consider returning banner_url instead of raw binary,
+                # and omit large content if not needed by the client.
                 'author': post.author,
@@
-                'abstract': post.abstract,
+                'abstract': post.abstract,
🤖 Prompt for AI Agents
In `@app/routes/bookmark.py` around lines 86 - 134, The /my-bookmarks view
(function my_bookmarks on the bookmark_blueprint) conflicts with the existing
HTML route and returns JSON containing Post.banner (LargeBinary) which isn't
JSON-safe; rename the endpoint (e.g., "/api/my-bookmarks" or "/bookmarks/json")
to avoid the collision with app/routes/my_bookmarks.py and update any clients,
and change the response for Post.banner so it is not raw LargeBinary — either
omit banner from the bookmarked_posts payload or convert post.banner to a
JSON-safe string (e.g., base64-encode) before adding it to the dict; keep the
rest of the logic and error handling the same.

63 changes: 63 additions & 0 deletions app/routes/my_bookmarks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""
My Bookmarks page route
"""

from flask import Blueprint, render_template, session, redirect, url_for
from database import db
from models import Bookmark, Post, User
from utils.log import Log
from utils.get_profile_picture import get_profile_picture

my_bookmarks_blueprint = Blueprint("my_bookmarks", __name__)


@my_bookmarks_blueprint.route("/my-bookmarks")
def my_bookmarks():
"""Display user's bookmarked posts page"""
try:
# Check if user is logged in
if "username" not in session:
return redirect("/login/redirect=&my-bookmarks")

# Get user ID
user = User.query.filter_by(username=session["username"]).first()
if not user:
return redirect("/login/redirect=&my-bookmarks")

# Get user's bookmarks with post details
bookmarks = db.session.query(Bookmark, Post).join(
Post, Bookmark.post_id == Post.id
).filter(
Bookmark.user_id == user.user_id
).order_by(
Bookmark.time_stamp.desc()
).all()

# Format data for template (match the expected tuple format)
bookmarked_posts = []
for bookmark, post in bookmarks:
bookmarked_posts.append([
post.id,
post.title,
post.tags,
post.content,
post.banner,
post.author,
post.views,
post.time_stamp,
post.last_edit_time_stamp,
post.category,
post.url_id,
post.abstract,
])

return render_template(
"my_bookmarks.html",
bookmarked_posts=bookmarked_posts,
username=user.username,
get_profile_picture=get_profile_picture
)

except Exception as e:
Log.error(f"Error loading my bookmarks page: {e}")
return redirect(url_for("index.index"))
31 changes: 31 additions & 0 deletions app/scripts/migrate_bookmarks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""
Database migration script for adding bookmarks table
Run this script to create the bookmarks table in the database
"""

import os
import sys

# Add the parent directory to the path so we can import our modules
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from database import db, init_db
from models import Bookmark
from app import create_app
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

create_app function does not exist in app.py.

Same issue as in test_bookmarks.py - the app module doesn't export a create_app factory function.

🐛 Proposed fix
-from app import create_app
+from app import app

Then update line 20:

-        app = create_app()
-        with app.app_context():
+        with app.app_context():
🤖 Prompt for AI Agents
In `@app/scripts/migrate_bookmarks.py` at line 14, The file imports a non-existent
factory create_app; either add a create_app() factory to app.py or change the
import/usage in migrate_bookmarks.py to use the exported Flask instance (e.g.,
import the app object from app) and update any call sites that call create_app()
(see the invocation around line 20) to use the imported app instance instead;
reference symbols: create_app (remove or implement) and app (use the Flask app
object) so the module imports and invocation match what app.py actually exports.

from utils.log import Log

def migrate_bookmarks():
"""Create the bookmarks table"""
try:
app = create_app()
with app.app_context():
# Create all tables (this will only create the new bookmarks table)
db.create_all()
Log.success("Bookmarks table created successfully!")
return True
except Exception as e:
Log.error(f"Error creating bookmarks table: {e}")
return False

if __name__ == "__main__":
migrate_bookmarks()
73 changes: 73 additions & 0 deletions app/scripts/test_bookmark_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""
Test script for bookmark functionality
Tests both logged in and logged out scenarios
"""

import requests
import json

# Test configuration
BASE_URL = "http://localhost:1283"

def test_bookmark_functionality():
"""Test bookmark functionality"""
print("🧪 Testing Bookmark Functionality")
print("=" * 50)

# Test 1: Check if server is running
print("1. Testing server connection...")
try:
response = requests.get(BASE_URL)
if response.status_code == 200:
print("✅ Server is running")
else:
print(f"❌ Server returned status {response.status_code}")
return False
except Exception as e:
print(f"❌ Server connection failed: {e}")
return False

# Test 2: Test bookmark API without login
print("\n2. Testing bookmark API without login...")
try:
response = requests.post(f"{BASE_URL}/api/bookmark/1")
if response.status_code == 401:
print("✅ Correctly rejected unauthenticated request")
else:
print(f"❌ Unexpected status code: {response.status_code}")
print(f"Response: {response.text}")
except Exception as e:
print(f"❌ API test failed: {e}")

# Test 3: Test bookmark status API without login
print("\n3. Testing bookmark status API without login...")
try:
response = requests.get(f"{BASE_URL}/api/bookmark/status/1")
if response.status_code == 200:
data = response.json()
if data.get("bookmarked") == False:
print("✅ Correctly returned False for unauthenticated user")
else:
print(f"❌ Unexpected response: {data}")
else:
print(f"❌ Unexpected status code: {response.status_code}")
except Exception as e:
print(f"❌ API test failed: {e}")

# Test 4: Test my-bookmarks page without login
print("\n4. Testing my-bookmarks page without login...")
try:
response = requests.get(f"{BASE_URL}/my-bookmarks", allow_redirects=False)
if response.status_code == 302:
print("✅ Correctly redirected to login page")
print(f"Redirect location: {response.headers.get('Location')}")
else:
print(f"❌ Unexpected status code: {response.status_code}")
except Exception as e:
print(f"❌ Page test failed: {e}")

print("\n🎯 Bookmark functionality tests completed!")
return True
Comment on lines +12 to +70
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fail the script when any test step fails.
Currently, unexpected status codes only print errors and the function still returns True, which can mask regressions.

✅ Suggested fix
 def test_bookmark_functionality():
     """Test bookmark functionality"""
+    success = True
@@
-        else:
+        else:
             print(f"❌ Unexpected status code: {response.status_code}")
             print(f"Response: {response.text}")
+            success = False
@@
-        else:
+        else:
             print(f"❌ Unexpected status code: {response.status_code}")
+            success = False
@@
-        else:
+        else:
             print(f"❌ Unexpected status code: {response.status_code}")
+            success = False
@@
-    return True
+    return success
🤖 Prompt for AI Agents
In `@app/scripts/test_bookmark_api.py` around lines 12 - 70, The
test_bookmark_functionality currently prints failures but still returns True;
update each failure branch in functions like test_bookmark_functionality
(including server connection, unauthenticated POST to /api/bookmark/1, GET
/api/bookmark/status/1 checks, and /my-bookmarks redirect check) to immediately
fail the test by either returning False or raising an exception (choose one
consistent behavior) instead of merely printing; ensure the success path only
returns True at the end when no failure branch was taken, and keep the error
output (response.status_code, response.text, exception message) in the failure
return/exception to aid debugging.


if __name__ == "__main__":
test_bookmark_functionality()
Loading
Loading