diff --git a/src/hxbooks/main.py b/src/hxbooks/main.py index 53e7833..76f4b21 100644 --- a/src/hxbooks/main.py +++ b/src/hxbooks/main.py @@ -29,21 +29,19 @@ bp = Blueprint("main", __name__) def save_search(user: User, search_name: str, query_params: str) -> bool: """Save a search for a user. Mock implementation.""" # Initialize saved_searches if None - if not hasattr(user, "saved_searches") or user.saved_searches is None: - user.saved_searches = {} - user.saved_searches[search_name] = query_params + user.saved_searches = user.saved_searches | {search_name: query_params} # noqa: PLR6104 + print(f"{user.saved_searches=}") db.session.commit() return True def delete_saved_search(user: User, search_name: str) -> bool: """Delete a saved search for a user. Mock implementation.""" - if not user or not hasattr(user, "saved_searches") or not user.saved_searches: - return False - if search_name in user.saved_searches: - del user.saved_searches[search_name] + user.saved_searches = { + k: v for k, v in user.saved_searches.items() if k != search_name + } # needs to be a new object to trigger SQLAlchemy change detection db.session.commit() return True return False @@ -103,20 +101,12 @@ def book_detail(book_id: int) -> ResponseReturnValue: flash("Book not found", "error") return redirect(url_for("main.index")) - # Get user-specific data - viewing_user = session.get("viewing_as_user") - user_data = {} + for reading in book.readings: + print( + f"Reading: {reading}, user: {reading.user.username if reading.user else 'N/A'}, dropped: {reading.dropped}, finished: {reading.finished}, end_date: {reading.end_date}" + ) - if viewing_user: - # TODO: Get reading status, wishlist status, etc. - # This will need additional library functions - user_data = { - "is_wishlisted": False, - "current_reading": None, - "reading_history": [], - } - - return render_template("book/detail.html.j2", book=book, user_data=user_data) + return render_template("book/detail.html.j2", book=book) @bp.route("/book/new", methods=["GET", "POST"]) @@ -130,14 +120,26 @@ def create_book() -> ResponseReturnValue: try: # Get current viewing user as owner - viewing_user = g.get("viewing_as_user") + viewing_user = g.get("viewing_user") + + # Process textarea inputs for authors and genres + authors = [ + author.strip() + for author in request.form.get("authors", "").split("\n") + if author.strip() + ] + genres = [ + genre.strip() + for genre in request.form.get("genres", "").split("\n") + if genre.strip() + ] # Create book with submitted data book = library.create_book( title=title, owner_id=viewing_user.id if viewing_user else None, - authors=request.form.getlist("authors"), - genres=request.form.getlist("genres"), + authors=authors, + genres=genres, isbn=request.form.get("isbn"), publisher=request.form.get("publisher"), edition=request.form.get("edition"), @@ -167,12 +169,24 @@ def update_book(book_id: int) -> ResponseReturnValue: return redirect(url_for("main.index")) try: + # Process textarea inputs for authors and genres + authors = [ + author.strip() + for author in request.form.get("authors", "").split("\n") + if author.strip() + ] + genres = [ + genre.strip() + for genre in request.form.get("genres", "").split("\n") + if genre.strip() + ] + # Update book with form data library.update_book( book_id=book_id, title=request.form.get("title"), - authors=request.form.getlist("authors"), - genres=request.form.getlist("genres"), + authors=authors, + genres=genres, isbn=request.form.get("isbn"), publisher=request.form.get("publisher"), edition=request.form.get("edition"), @@ -192,23 +206,26 @@ def update_book(book_id: int) -> ResponseReturnValue: return redirect(url_for("main.book_detail", book_id=book_id)) -@bp.route("/book//delete", methods=["POST"]) +@bp.route("/book//delete", methods=["GET", "POST"]) def delete_book(book_id: int) -> ResponseReturnValue: - """Delete a book.""" + """Delete a book (GET shows confirmation, POST performs deletion).""" book = library.get_book(book_id) if not book: flash("Book not found", "error") return redirect(url_for("main.index")) - try: - title = book.title - library.delete_book(book_id) - flash(f"Book '{title}' deleted successfully!", "success") + if request.method == "POST": + # Perform the actual deletion + try: + title = book.title + library.delete_book(book_id) + flash(f"Book '{title}' deleted successfully!", "success") + except Exception as e: + flash(f"Error deleting book: {e}", "error") + return redirect(url_for("main.index")) - except Exception as e: - flash(f"Error deleting book: {e}", "error") - - return redirect(url_for("main.index")) + # Show confirmation page + return render_template("book/delete_confirm.html.j2", book=book) @bp.route("/import", methods=["GET", "POST"]) @@ -222,7 +239,7 @@ def import_book() -> ResponseReturnValue: try: # Get current viewing user as owner - viewing_user = g.get("viewing_as_user") + viewing_user = g.get("viewing_user") # Import book from ISBN book = library.import_book_from_isbn( @@ -259,17 +276,20 @@ def set_viewing_user(username: str = "") -> ResponseReturnValue: @bp.route("/saved-search", methods=["POST"]) def save_search_route() -> ResponseReturnValue: """Save a search for the current user.""" - viewing_user = session.get("viewing_as_user") + viewing_user = g.get("viewing_user") if not viewing_user: flash("You must select a user to save searches", "error") return redirect(url_for("main.index")) search_name = request.form.get("name", "").strip() query_params = request.form.get("query_params", "") + print( + f"Saving search for user {viewing_user.username}: {search_name} -> {query_params}" + ) if not search_name: flash("Search name is required", "error") - return redirect(url_for("main.index")) + return redirect(url_for("main.index", q=query_params)) try: success = save_search(viewing_user, search_name, query_params) @@ -280,27 +300,146 @@ def save_search_route() -> ResponseReturnValue: except Exception as e: flash(f"Error saving search: {e}", "error") - return redirect(url_for("main.index")) + return redirect(url_for("main.index", q=query_params)) -@bp.route("/saved-search", methods=["DELETE"]) -def delete_saved_search_route() -> ResponseReturnValue: - """Delete a saved search.""" - viewing_user = session.get("viewing_as_user") +@bp.route("/book//reading/start", methods=["POST"]) +def start_reading_route(book_id: int) -> ResponseReturnValue: + """Start reading a book.""" + viewing_user = g.get("viewing_user") if not viewing_user: - return {"error": "No user selected"}, 400 - - data = request.get_json() - search_name = data.get("name") if data else None - - if not search_name: - return {"error": "Search name required"}, 400 + flash("You must select a user to start reading", "error") + return redirect(url_for("main.book_detail", book_id=book_id)) try: - success = delete_saved_search(viewing_user, search_name) - if success: - return {"success": True} - else: - return {"error": "Search not found"}, 404 + library.start_reading(book_id=book_id, user_id=viewing_user.id) + flash("Started reading!", "success") except Exception as e: - return {"error": str(e)}, 500 + flash(f"Error starting reading: {e}", "error") + + return redirect(url_for("main.book_detail", book_id=book_id)) + + +@bp.route("/book//reading/finish", methods=["POST"]) +def finish_reading_route(book_id: int) -> ResponseReturnValue: + """Finish reading a book.""" + viewing_user = g.get("viewing_user") + if not viewing_user: + flash("You must select a user to finish reading", "error") + return redirect(url_for("main.book_detail", book_id=book_id)) + + try: + # Find current reading for this user and book + current_readings = library.get_current_readings(user_id=viewing_user.id) + current_reading = next( + (r for r in current_readings if r.book_id == book_id), None + ) + + if not current_reading: + flash("No active reading session found", "error") + return redirect(url_for("main.book_detail", book_id=book_id)) + + library.finish_reading(reading_id=current_reading.id) + flash("Finished reading!", "success") + except Exception as e: + flash(f"Error finishing reading: {e}", "error") + + return redirect(url_for("main.book_detail", book_id=book_id)) + + +@bp.route("/book//reading/drop", methods=["POST"]) +def drop_reading_route(book_id: int) -> ResponseReturnValue: + """Drop reading a book.""" + viewing_user = g.get("viewing_user") + if not viewing_user: + flash("You must select a user to drop reading", "error") + return redirect(url_for("main.book_detail", book_id=book_id)) + + try: + # Find current reading for this user and book + current_readings = library.get_current_readings(user_id=viewing_user.id) + current_reading = next( + (r for r in current_readings if r.book_id == book_id), None + ) + + if not current_reading: + flash("No active reading session found", "error") + return redirect(url_for("main.book_detail", book_id=book_id)) + + library.drop_reading(reading_id=current_reading.id) + flash("Dropped reading", "info") + except Exception as e: + flash(f"Error dropping reading: {e}", "error") + + return redirect(url_for("main.book_detail", book_id=book_id)) + + +@bp.route("/book//wishlist/add", methods=["POST"]) +def add_to_wishlist_route(book_id: int) -> ResponseReturnValue: + """Add book to wishlist.""" + viewing_user = g.get("viewing_user") + if not viewing_user: + flash("You must select a user to add to wishlist", "error") + return redirect(url_for("main.book_detail", book_id=book_id)) + + try: + library.add_to_wishlist(book_id=book_id, user_id=viewing_user.id) + flash("Added to wishlist!", "success") + except Exception as e: + flash(f"Error adding to wishlist: {e}", "error") + + return redirect(url_for("main.book_detail", book_id=book_id)) + + +@bp.route("/book//wishlist/remove", methods=["POST"]) +def remove_from_wishlist_route(book_id: int) -> ResponseReturnValue: + """Remove book from wishlist.""" + viewing_user = g.get("viewing_user") + if not viewing_user: + flash("You must select a user to remove from wishlist", "error") + return redirect(url_for("main.book_detail", book_id=book_id)) + + try: + removed = library.remove_from_wishlist(book_id=book_id, user_id=viewing_user.id) + if removed: + flash("Removed from wishlist", "info") + else: + flash("Book was not in wishlist", "warning") + except Exception as e: + flash(f"Error removing from wishlist: {e}", "error") + + return redirect(url_for("main.book_detail", book_id=book_id)) + + +@bp.route("/saved-search//delete", methods=["GET", "POST"]) +def delete_saved_search_route(search_name: str) -> ResponseReturnValue: + """Delete a saved search (GET shows confirmation, POST performs deletion).""" + viewing_user = g.get("viewing_user") + if not viewing_user: + flash("You must select a user to manage saved searches", "error") + return redirect(url_for("main.index")) + + # Check if search exists + saved_searches = viewing_user.saved_searches or {} + if search_name not in saved_searches: + flash(f"Saved search '{search_name}' not found", "error") + return redirect(url_for("main.index")) + + if request.method == "POST": + # Perform the actual deletion + try: + success = delete_saved_search(viewing_user, search_name) + if success: + flash(f"Saved search '{search_name}' deleted successfully!", "success") + else: + flash("Error deleting saved search", "error") + except Exception as e: + flash(f"Error deleting saved search: {e}", "error") + return redirect(url_for("main.index")) + + # Show confirmation page + return render_template( + "components/delete_search_confirm.html.j2", + search_name=search_name, + search_params=saved_searches[search_name], + ) diff --git a/src/hxbooks/templates/base.html.j2 b/src/hxbooks/templates/base.html.j2 index 98913ed..6ebf75d 100644 --- a/src/hxbooks/templates/base.html.j2 +++ b/src/hxbooks/templates/base.html.j2 @@ -1,5 +1,5 @@ - @@ -46,7 +46,8 @@
{% for category, message in get_flashed_messages(with_categories=true) %} -
+ {% include 'components/import_modal.html.j2' %} + {% include 'components/save_search_modal.html.j2' %} + - - {% include 'components/import_modal.html.j2' %} \ No newline at end of file diff --git a/src/hxbooks/templates/book/delete_confirm.html.j2 b/src/hxbooks/templates/book/delete_confirm.html.j2 new file mode 100644 index 0000000..12fd735 --- /dev/null +++ b/src/hxbooks/templates/book/delete_confirm.html.j2 @@ -0,0 +1,47 @@ +{% extends "base.html.j2" %} + +{% block title %}Delete {{ book.title }}{% endblock %} + +{% block header %} +
+ ← Back +

🗑️ Delete Book

+
+{% endblock %} + +{% block content %} +
+
+
+
+
⚠️ Confirm Deletion
+
+
+

Are you sure you want to delete this book?

+ +
+
{{ book.title }}
+ {% if book.authors %} +

by {{ book.authors | join(', ') }}

+ {% endif %} + {% if book.isbn %} +

ISBN: {{ book.isbn }}

+ {% endif %} +
+ + + +
+
+ +
+ Cancel +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/src/hxbooks/templates/book/detail.html.j2 b/src/hxbooks/templates/book/detail.html.j2 index 0efb7a4..5a62687 100644 --- a/src/hxbooks/templates/book/detail.html.j2 +++ b/src/hxbooks/templates/book/detail.html.j2 @@ -9,9 +9,9 @@

{{ book.title }}

{% endblock %} @@ -22,7 +22,7 @@
{% include 'components/book_form.html.j2' %} - +
@@ -31,7 +31,7 @@
- +
{% if session.get('viewing_as_user') %} @@ -55,29 +55,24 @@
{% endblock %} \ No newline at end of file diff --git a/src/hxbooks/templates/components/book_form.html.j2 b/src/hxbooks/templates/components/book_form.html.j2 index 15b74ec..89b8c8d 100644 --- a/src/hxbooks/templates/components/book_form.html.j2 +++ b/src/hxbooks/templates/components/book_form.html.j2 @@ -2,13 +2,11 @@
- +
- +
@@ -16,14 +14,14 @@
- +
Enter one author per line
- +
Enter one genre per line
@@ -32,86 +30,49 @@
- +
- +
- +
- +
- +
- +
- +
- -
- - - \ No newline at end of file + +
\ No newline at end of file diff --git a/src/hxbooks/templates/components/delete_search_confirm.html.j2 b/src/hxbooks/templates/components/delete_search_confirm.html.j2 new file mode 100644 index 0000000..0d136c9 --- /dev/null +++ b/src/hxbooks/templates/components/delete_search_confirm.html.j2 @@ -0,0 +1,37 @@ +{% extends "base.html.j2" %} + +{% block title %}Delete Saved Search{% endblock %} + +{% block header %} +
+ ← Back +

🗑️ Delete Saved Search

+
+{% endblock %} + +{% block content %} +
+
+
+
+
⚠️ Confirm Deletion
+
+
+

Are you sure you want to delete this saved search?

+ +
+
🔍 {{ search_name }}
+

Search: {{ search_params }}

+
+ +
+
+ +
+ Cancel +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/src/hxbooks/templates/components/reading_status.html.j2 b/src/hxbooks/templates/components/reading_status.html.j2 index 53259f8..1f305ac 100644 --- a/src/hxbooks/templates/components/reading_status.html.j2 +++ b/src/hxbooks/templates/components/reading_status.html.j2 @@ -1,43 +1,53 @@ - + +{% if g.viewing_user %} +{% set user_readings = book.readings | selectattr('user_id', 'equalto', g.viewing_user.id) | list %} +{% set current_reading = user_readings | selectattr('end_date', 'none') | selectattr('dropped', 'false') | first %} +{% set reading_history = user_readings | selectattr('end_date') | sort(attribute='end_date', reverse=true) %} +
📖 Reading Status
- + - {% if user_data.get('current_reading') %} + {% if current_reading %}
Currently Reading
- Started: {{ user_data.current_reading.start_date.strftime('%B %d, %Y') }} + Started: {{ current_reading.start_date.strftime('%B %d, %Y') }}
- - +
+ +
+
+ +
{% else %} - {% if user_data.get('reading_history') %} + {% if reading_history %}

Previously read

{% else %}

Not read yet

{% endif %} - +
+ +
{% endif %} - + - {% if user_data.get('reading_history') %} + {% if reading_history %}
Reading History: - {% for reading in user_data.reading_history[:3] %} + {% for reading in reading_history[:3] %}
- {% if reading.finished %} - ✓ Finished {{ reading.end_date.strftime('%m/%d/%Y') }} - {% if reading.rating %} - ⭐{{ reading.rating }}/5{% endif %} - {% elif reading.dropped %} - ⏸ Dropped {{ reading.end_date.strftime('%m/%d/%Y') }} - {% else %} - 📖 Started {{ reading.start_date.strftime('%m/%d/%Y') }} - {% endif %} + {% if not reading.dropped %} ✓ Finished {% else %} ⏸ Dropped {% endif %} + {{ reading.start_date.strftime('%m/%d/%Y') }} - {{ reading.end_date.strftime('%m/%d/%Y') }} + - ⭐{{ reading.rating or "-" }}/5
{% endfor %} + {% if reading_history | length > 3 %} + ... and {{ reading_history | length - 3 }} more + {% endif %}
{% endif %} -
\ No newline at end of file + +{% endif %} \ No newline at end of file diff --git a/src/hxbooks/templates/components/save_search_modal.html.j2 b/src/hxbooks/templates/components/save_search_modal.html.j2 new file mode 100644 index 0000000..01c4f45 --- /dev/null +++ b/src/hxbooks/templates/components/save_search_modal.html.j2 @@ -0,0 +1,24 @@ + + \ No newline at end of file diff --git a/src/hxbooks/templates/components/sidebar_content.html.j2 b/src/hxbooks/templates/components/sidebar_content.html.j2 index 0c82864..d1d0e73 100644 --- a/src/hxbooks/templates/components/sidebar_content.html.j2 +++ b/src/hxbooks/templates/components/sidebar_content.html.j2 @@ -48,63 +48,22 @@
Saved Searches
{% for search_name, search_params in saved_searches.items() %} {% endfor %}
{% endif %} - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/src/hxbooks/templates/components/wishlist_status.html.j2 b/src/hxbooks/templates/components/wishlist_status.html.j2 index 78bb35a..d6d2c1e 100644 --- a/src/hxbooks/templates/components/wishlist_status.html.j2 +++ b/src/hxbooks/templates/components/wishlist_status.html.j2 @@ -1,14 +1,22 @@ - + +{% if g.viewing_user %} +{% set user_wishlist = book.wished_by | selectattr('user_id', 'equalto', g.viewing_user.id) | first %} +
💝 Wishlist
- - {% if user_data.get('is_wishlisted') %} + + {% if user_wishlist %}
- Added to wishlist: {{ user_data.wishlist_date.strftime('%B %d, %Y') if user_data.get('wishlist_date') else 'Unknown date' }} + Added to wishlist: {{ user_wishlist.wishlisted_date.strftime('%B %d, %Y') }}
- +
+ +
{% else %}

Not in wishlist

- +
+ +
{% endif %} -
\ No newline at end of file + +{% endif %} \ No newline at end of file