From 3397dfbb6400dc9fd8ee76c3d8c47f2923ed1837 Mon Sep 17 00:00:00 2001 From: Typosaurus Date: Sun, 19 Jul 2026 20:22:17 +0000 Subject: [PATCH] ticket #97 attempt 1 --- devplacepy/content.py | 14 +++++- devplacepy/database/schema.py | 6 +++ devplacepy/docs_api/groups/social_actions.py | 22 +++++++++ devplacepy/routers/bookmarks.py | 8 +++- devplacepy/routers/devrant/rants.py | 2 +- .../devii/actions/catalog/engagement.py | 7 +++ devplacepy/templates/saved.html | 34 +++++++++++++- tests/api/bookmarks/saved.py | 47 +++++++++++++++++++ 8 files changed, 134 insertions(+), 6 deletions(-) diff --git a/devplacepy/content.py b/devplacepy/content.py index e6062687..db0fd58e 100644 --- a/devplacepy/content.py +++ b/devplacepy/content.py @@ -1,5 +1,6 @@ # retoor import logging +import re from typing import Any from datetime import datetime, timezone from fastapi.responses import RedirectResponse @@ -55,6 +56,12 @@ CREATE_METADATA_KEYS = ("project_type", "is_private", "language", "topic", "stat BOOKMARKABLE_TYPES = {"post", "gist", "project", "news"} REACTABLE_TYPES = {"post", "comment", "gist", "project"} +NOTE_PATTERN = re.compile(r"(^|\s)@note(\s|$)") + + +def has_note(text: str) -> bool: + return bool(text and NOTE_PATTERN.search(text)) + logger = logging.getLogger(__name__) @@ -198,6 +205,8 @@ def create_content_item( schedule_correction(user, table_name, uid, request) schedule_modification(user, table_name, uid, request) schedule_seo_meta_for_table(table_name, uid) + if request and has_note(mention_text): + set_bookmark(request, user, target_type, uid, saved=True, source="note") return uid, slug @@ -379,6 +388,8 @@ def create_comment_record( summary=f"{user['username']} commented on {target_type} {target_uid}: {content}", links=comment_links, ) + if request and target_type in BOOKMARKABLE_TYPES and has_note(content): + set_bookmark(request, user, target_type, target_uid, saved=True, source="note") return comment_uid, comment_url @@ -435,7 +446,7 @@ def delete_comment_record(request, user: dict, comment: dict) -> tuple[str, str] def set_bookmark( - request, user: dict, target_type: str, target_uid: str, saved: bool + request, user: dict, target_type: str, target_uid: str, saved: bool, source: str = "manual" ) -> bool: bookmarks = get_table("bookmarks") existing = bookmarks.find_one( @@ -458,6 +469,7 @@ def set_bookmark( "created_at": datetime.now(timezone.utc).isoformat(), "deleted_at": None, "deleted_by": None, + "source": source, } ) changed = True diff --git a/devplacepy/database/schema.py b/devplacepy/database/schema.py index c5845de8..eb973587 100644 --- a/devplacepy/database/schema.py +++ b/devplacepy/database/schema.py @@ -243,6 +243,12 @@ def init_db(): ) _index(db, "bookmarks", "idx_bookmarks_user", ["user_uid"]) _index(db, "bookmarks", "idx_bookmarks_target", ["target_type", "target_uid"]) + if "bookmarks" in tables: + bookmarks_table = get_table("bookmarks") + if not bookmarks_table.has_column("source"): + bookmarks_table.create_column_by_example("source", "manual") + _index(db, "bookmarks", "idx_bookmarks_source", ["source"]) + _index(db, "bookmarks", "idx_bookmarks_user_source", ["user_uid", "source"]) _index(db, "polls", "idx_polls_post", ["post_uid"]) _index(db, "poll_options", "idx_poll_options_poll", ["poll_uid"]) _index(db, "poll_votes", "idx_poll_votes_poll", ["poll_uid"]) diff --git a/devplacepy/docs_api/groups/social_actions.py b/devplacepy/docs_api/groups/social_actions.py index ca03a899..6ebf96e7 100644 --- a/devplacepy/docs_api/groups/social_actions.py +++ b/devplacepy/docs_api/groups/social_actions.py @@ -152,6 +152,28 @@ four ways to sign requests. "Bookmarks target posts, projects, gists, and news; see [Posts, Comments, Projects, Gists & News](/docs/content.html)." ], ), + endpoint( + id="notes-listing", + method="GET", + path="/bookmarks/saved?source=note", + title="View personal notes", + summary="Render your personal notes (items tagged with @note). Returns an HTML page.", + auth="user", + interactive=True, + params=[ + field( + "before", + "query", + "string", + False, + "", + "Pagination cursor (created_at of the last item).", + ) + ], + notes=[ + "Notes are auto-created when you include @note in a post or comment body." + ], + ), endpoint( id="polls-vote", method="POST", diff --git a/devplacepy/routers/bookmarks.py b/devplacepy/routers/bookmarks.py index d1d3fdf8..6f52b32c 100644 --- a/devplacepy/routers/bookmarks.py +++ b/devplacepy/routers/bookmarks.py @@ -31,10 +31,13 @@ LABEL_BY_TYPE: dict[str, str] = { @router.get("/saved", response_class=HTMLResponse) -async def saved_page(request: Request, before: str = None): +async def saved_page(request: Request, before: str = None, source: str = None): user = require_user(request) bookmarks = get_table("bookmarks") - rows, next_cursor = paginate(bookmarks, before=before, user_uid=user["uid"]) + filters: dict = {"user_uid": user["uid"]} + if source: + filters["source"] = source + rows, next_cursor = paginate(bookmarks, before=before, **filters) uids_by_type: dict[str, list] = {} for row in rows: @@ -85,6 +88,7 @@ async def saved_page(request: Request, before: str = None): "user": user, "items": items, "next_cursor": next_cursor, + "current_source": source or "", }, model=SavedOut, ) diff --git a/devplacepy/routers/devrant/rants.py b/devplacepy/routers/devrant/rants.py index 0695d474..4abdcab2 100644 --- a/devplacepy/routers/devrant/rants.py +++ b/devplacepy/routers/devrant/rants.py @@ -211,7 +211,7 @@ async def _set_favorite(request: Request, rant_id: str, saved: bool): post = post_by_id(rant_id) if not post: return dr_error("This rant does not exist.") - set_bookmark(request, user, "post", post["uid"], saved) + set_bookmark(request, user, "post", post["uid"], saved, source="manual") return dr_ok() diff --git a/devplacepy/services/devii/actions/catalog/engagement.py b/devplacepy/services/devii/actions/catalog/engagement.py index ba346923..c0121a6a 100644 --- a/devplacepy/services/devii/actions/catalog/engagement.py +++ b/devplacepy/services/devii/actions/catalog/engagement.py @@ -65,6 +65,13 @@ ENGAGEMENT_ACTIONS: tuple[Action, ...] = ( ), ), ), + Action( + name="list_notes", + method="GET", + path="/bookmarks/saved?source=note", + summary="List personal notes (items tagged with @note)", + params=(query("before", "Pagination cursor."),), + ), Action( name="vote_poll", method="POST", diff --git a/devplacepy/templates/saved.html b/devplacepy/templates/saved.html index 2deffd87..94679dd2 100644 --- a/devplacepy/templates/saved.html +++ b/devplacepy/templates/saved.html @@ -2,10 +2,40 @@ {% block extra_head %} + {% endblock %} {% block content %}

Saved

+ +
+ All + Notes +
+
{% for item in items %} @@ -14,13 +44,13 @@ {{ dt_ago(item.created_at) if item.created_at else item.time_ago }} {% else %} -
Nothing saved yet. Use the Save button on posts, gists, projects and news.
+
Nothing saved yet. Use the Save button on posts, gists, projects and news, or add @note to your post or comment to auto-save it.
{% endfor %}
{% if next_cursor %} {% endif %}
diff --git a/tests/api/bookmarks/saved.py b/tests/api/bookmarks/saved.py index d4c3e88a..3620d4fb 100644 --- a/tests/api/bookmarks/saved.py +++ b/tests/api/bookmarks/saved.py @@ -76,3 +76,50 @@ def test_saved_page_lists_post_and_gist(app_server): html = s.get(f"{BASE_URL}/bookmarks/saved").text assert post_title in html assert gist_title in html + + +def test_saved_page_source_filter_notes(app_server): + s, name = _session_bookmarks() + owner_uid = _uid_bookmarks(name) + post_title = f"Note post {int(time.time() * 1000)}" + manual_title = f"Manual bookmark {int(time.time() * 1000)}" + post_uid = _make_post_bookmarks(owner_uid, post_title) + manual_uid = _make_post_bookmarks(owner_uid, manual_title) + + # Create a note bookmark (source=note) directly via DB + bookmarks = get_table("bookmarks") + note_uid = generate_uid() + bookmarks.insert( + { + "uid": note_uid, + "user_uid": owner_uid, + "target_uid": post_uid, + "target_type": "post", + "created_at": datetime.now(timezone.utc).isoformat(), + "deleted_at": None, + "deleted_by": None, + "source": "note", + } + ) + + # Create a manual bookmark (source=manual) + bookmarks.insert( + { + "uid": generate_uid(), + "user_uid": owner_uid, + "target_uid": manual_uid, + "target_type": "post", + "created_at": datetime.now(timezone.utc).isoformat(), + "deleted_at": None, + "deleted_by": None, + "source": "manual", + } + ) + + html = s.get(f"{BASE_URL}/bookmarks/saved?source=note").text + assert post_title in html + assert manual_title not in html + + html_all = s.get(f"{BASE_URL}/bookmarks/saved").text + assert post_title in html_all + assert manual_title in html_all