From ff7d6d4f9c9391578b3d6d39d4c234f41f8ace94 Mon Sep 17 00:00:00 2001 From: TIO NEON <15000986919@163.com> Date: Sat, 31 Jan 2026 01:13:22 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=9B=E5=BB=BA=E4=B9=A6=E7=AD=BE?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/app.py | 8 ++ app/instance/flaskblog.db | Bin 10997760 -> 11005952 bytes app/models.py | 15 ++ app/routes/bookmark.py | 134 ++++++++++++++++++ app/routes/my_bookmarks.py | 63 ++++++++ app/scripts/migrate_bookmarks.py | 31 ++++ app/scripts/test_bookmark_api.py | 73 ++++++++++ app/scripts/test_bookmarks.py | 76 ++++++++++ app/static/js/bookmark.js | 80 +++++++++++ app/templates/components/navbar.html | 3 + app/templates/components/post_card_macro.html | 24 +++- app/templates/layout.html | 1 + app/templates/my_bookmarks.html | 60 ++++++++ app/templates/post.html | 18 ++- 14 files changed, 577 insertions(+), 9 deletions(-) create mode 100644 app/routes/bookmark.py create mode 100644 app/routes/my_bookmarks.py create mode 100644 app/scripts/migrate_bookmarks.py create mode 100644 app/scripts/test_bookmark_api.py create mode 100644 app/scripts/test_bookmarks.py create mode 100644 app/static/js/bookmark.js create mode 100644 app/templates/my_bookmarks.html diff --git a/app/app.py b/app/app.py index 812574f8d..58495f013 100755 --- a/app/app.py +++ b/app/app.py @@ -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 @@ -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__": diff --git a/app/instance/flaskblog.db b/app/instance/flaskblog.db index 1c4d91aea4aa970ce8d35069d66b6980492d7e8e..261bdcd4b6fba523022dcd80fce780e32871ae7f 100644 GIT binary patch delta 1300 zcmYMw&r@4f6u|Kp2qEtgi2Q2QAUtcVM3E+trc%Wwgck^e5b^^GRnyRfCLts=FQKSy zA+5D)Mcc;Tzi8`^t~*(@OSdeXapTHGb>lex1&-s4e#2NiGoP7z&pqeNy?67*cEjc` z+YMLEt*wF}Z3zE|_FCVdxmD=j`m5Rar_d;E2`yRU55`-~lA&ALYWm|&xm_B*u%K%+ z_4L$T(iG#1+2mqAzq}GJEUSA9>AWYTxFd=jagX>Fc`q;bN@C?^i+NuxosfNjh%%;x z`eaF5&8tOHm44&?$~{BJhG#kuj)dG)$gz@|EW~yTN+;!Lz&8_BI<^ zsp{|6E}br=bqQKmRc~`@4Ndjt+L3Gb)bF@=r9-b@6pUg^TeHa^H}y$}MVEf@PI*GV zQ9hnB2%YBgxw6=)(VA)liCU+{X({AOxr93GJmj?5Y?isD@ySyqf5zr;hipsZQA;j0 zwK}5Cj^x({=H~+5(8`(I?0hgfa4a7k%eV)Xxx7E8E)@oX&UI&|;0RA-4*HfRY z$k5QrSTQ@FY|9QhW`b*zk?49N<&7k5nRMmXyY{ZyB@PtgiF8RFw%cmo+mJr{^KEZD zt0phHb}qYiG^j&8v}k}1jnG4cgeDl!j20O25L(fOc66W<`(VPucm$8aj4nKeZuCIL zIDt48aS{n6v4m4dA&m@{ zk;Mvf$YT|!QGkjfN?5}gtm9R@hSzZx=Wrey*hCo@a1n3d65hmRyoI;%4zA!`Y~ek; zkE^(b>!{!Ze296WXda__t9b KRW-qXjQ;`)2>99n delta 797 zcmYMo$6L+;0KoAdy_NUv9V Y$b_Ow%1-Ed+(8vy?2R1M%lY$UOn%DqiY98cPCGO zf}_vP_jx{-Z^Q47^oGuk^tSx$NDxG>1piBGFRjRn1ZB;|vBcjXJz5yV>k~f{U9m{C zFqK{VB`Z}J{gWH23-<)6NYSrwdu>*#r??;^R2Qmo%Bw={weeI<<@eT{%y2SDgyT7} zyl7EYX{0)w{MuL&Zc28yB)fajn>0eClR+kZ2ooWTC^6zB=u0*^lp z<}sfIEMyUjS;A75QNwapu##FEpq z2RX!H>N&zuj&Yn5oa7XzIm21bah?lY_;s!TqriEMF<_@jg" + + +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) + + # 确保每个用户对每篇文章只能收藏一次 + __table_args__ = (db.UniqueConstraint('user_id', 'post_id', name='_user_post_uc'),) + + def __repr__(self): + return f"" diff --git a/app/routes/bookmark.py b/app/routes/bookmark.py new file mode 100644 index 000000000..e771b6353 --- /dev/null +++ b/app/routes/bookmark.py @@ -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/", 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/", 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 \ No newline at end of file diff --git a/app/routes/my_bookmarks.py b/app/routes/my_bookmarks.py new file mode 100644 index 000000000..91f7c58e4 --- /dev/null +++ b/app/routes/my_bookmarks.py @@ -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")) \ No newline at end of file diff --git a/app/scripts/migrate_bookmarks.py b/app/scripts/migrate_bookmarks.py new file mode 100644 index 000000000..42d80dc1a --- /dev/null +++ b/app/scripts/migrate_bookmarks.py @@ -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 +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() \ No newline at end of file diff --git a/app/scripts/test_bookmark_api.py b/app/scripts/test_bookmark_api.py new file mode 100644 index 000000000..c3fab4686 --- /dev/null +++ b/app/scripts/test_bookmark_api.py @@ -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 + +if __name__ == "__main__": + test_bookmark_functionality() \ No newline at end of file diff --git a/app/scripts/test_bookmarks.py b/app/scripts/test_bookmarks.py new file mode 100644 index 000000000..0e96a781c --- /dev/null +++ b/app/scripts/test_bookmarks.py @@ -0,0 +1,76 @@ +""" +Test script for bookmark functionality +Run this script to test the bookmark feature +""" + +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 User, Post, Bookmark +from app import create_app +from utils.log import Log + +def test_bookmark_functionality(): + """Test the bookmark functionality""" + try: + app = create_app() + with app.app_context(): + Log.info("Starting bookmark functionality test...") + + # Test 1: Check if bookmarks table exists + Log.info("Test 1: Checking if bookmarks table exists...") + try: + # Try to query the bookmarks table + Bookmark.query.first() + Log.success("✓ Bookmarks table exists") + except Exception as e: + Log.error(f"✗ Bookmarks table does not exist: {e}") + return False + + # Test 2: Check if we can create a bookmark + Log.info("Test 2: Testing bookmark creation...") + try: + # Get a test user and post + user = User.query.first() + post = Post.query.first() + + if not user or not post: + Log.warning("No users or posts found in database for testing") + return True + + # Create a test bookmark + bookmark = Bookmark( + user_id=user.user_id, + post_id=post.id + ) + db.session.add(bookmark) + db.session.commit() + Log.success(f"✓ Bookmark created successfully for user {user.username} on post {post.title}") + + # Clean up test bookmark + db.session.delete(bookmark) + db.session.commit() + Log.success("✓ Test bookmark cleaned up") + + except Exception as e: + Log.error(f"✗ Error creating bookmark: {e}") + db.session.rollback() + return False + + Log.success("All bookmark functionality tests passed!") + return True + + except Exception as e: + Log.error(f"Error during bookmark testing: {e}") + return False + +if __name__ == "__main__": + success = test_bookmark_functionality() + if success: + Log.success("Bookmark functionality is ready!") + else: + Log.error("Bookmark functionality test failed!") \ No newline at end of file diff --git a/app/static/js/bookmark.js b/app/static/js/bookmark.js new file mode 100644 index 000000000..fb8a54a39 --- /dev/null +++ b/app/static/js/bookmark.js @@ -0,0 +1,80 @@ +// Bookmark functionality +async function toggleBookmark(postId, button) { + try { + const response = await fetch(`/api/bookmark/${postId}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken || '' + } + }); + + if (response.ok) { + const data = await response.json(); + + // Update button appearance + if (data.bookmarked) { + button.classList.remove('btn-ghost'); + button.classList.add('btn-primary'); + button.innerHTML = ''; + button.title = 'Remove bookmark'; + } else { + button.classList.remove('btn-primary'); + button.classList.add('btn-ghost'); + button.innerHTML = ''; + button.title = 'Add bookmark'; + } + + // Show success message (optional) + console.log(data.message); + } else { + console.error('Failed to toggle bookmark'); + alert('Please login to bookmark posts'); + } + } catch (error) { + console.error('Error toggling bookmark:', error); + alert('An error occurred. Please try again.'); + } +} + +// Check bookmark status for a post +async function checkBookmarkStatus(postId, button) { + try { + const response = await fetch(`/api/bookmark/status/${postId}`, { + headers: { + 'X-CSRFToken': csrfToken || '' + } + }); + + if (response.ok) { + const data = await response.json(); + + // Update button appearance based on status + if (data.bookmarked) { + button.classList.remove('btn-ghost'); + button.classList.add('btn-primary'); + button.innerHTML = ''; + button.title = 'Remove bookmark'; + } else { + button.classList.remove('btn-primary'); + button.classList.add('btn-ghost'); + button.innerHTML = ''; + button.title = 'Add bookmark'; + } + } + } catch (error) { + console.error('Error checking bookmark status:', error); + } +} + +// Initialize bookmark buttons on page load +document.addEventListener('DOMContentLoaded', function() { + // Check all bookmark buttons on the page + const bookmarkButtons = document.querySelectorAll('.bookmark-btn'); + bookmarkButtons.forEach(button => { + const postId = button.getAttribute('data-post-id'); + if (postId) { + checkBookmarkStatus(postId, button); + } + }); +}); \ No newline at end of file diff --git a/app/templates/components/navbar.html b/app/templates/components/navbar.html index bc5c582cb..5c957bf2a 100755 --- a/app/templates/components/navbar.html +++ b/app/templates/components/navbar.html @@ -30,6 +30,9 @@ {% if session["username"] %} + + +
diff --git a/app/templates/components/post_card_macro.html b/app/templates/components/post_card_macro.html index 27bf2e512..8de3f0f29 100755 --- a/app/templates/components/post_card_macro.html +++ b/app/templates/components/post_card_macro.html @@ -9,12 +9,24 @@
{{ post[9] }} - - {{ post[1] }} - +
+ + {{ post[1] }} + + {% if session['username'] %} + + {% endif %} +

{{ post[11] }}