From 36628b31e86cfa102e142cae63fb43306ca83c16 Mon Sep 17 00:00:00 2001 From: Andrew Shu Date: Sun, 31 Aug 2025 20:31:01 -0700 Subject: [PATCH 1/8] Refactor to clarify API does not use marshmallow * Get SimpleHoursPeriod directly, not via marshmallow schema Mentioning marshmallow in imports is misleading here, since we are constructing the result dict by hand and not using marshmallow to generate the OpenAPI response. * Rename API serialize methods to mention API This differentiates from the Marshmallow Schema dump method which is used in the JSON renderer in json.py --- tildes/tildes/views/api/beta/comment.py | 22 ++++++++++++++----- tildes/tildes/views/api/beta/topic.py | 29 +++++++++++++++---------- tildes/tildes/views/api/beta/user.py | 21 ++++++++++-------- 3 files changed, 45 insertions(+), 27 deletions(-) diff --git a/tildes/tildes/views/api/beta/comment.py b/tildes/tildes/views/api/beta/comment.py index e57b355..7d37aa2 100644 --- a/tildes/tildes/views/api/beta/comment.py +++ b/tildes/tildes/views/api/beta/comment.py @@ -8,8 +8,11 @@ from tildes.models.comment import Comment from tildes.models.comment.comment_tree import CommentInTree -def comment_to_dict(request: Request, comment: Comment) -> dict: - """Convert a Comment object to a dictionary for JSON serialization.""" +def comment_to_api_dict(request: Request, comment: Comment) -> dict: + """Convert a Comment object to a dictionary for JSON serialization. + + The schema is defined in our OpenAPI YAML file. + """ # Check permissions for viewing comment details (and set safe defaults) author = None @@ -66,14 +69,21 @@ def comment_to_dict(request: Request, comment: Comment) -> dict: } -def comment_subtree_to_dict(request: Request, comments: list[CommentInTree]) -> list: - """Convert a comment subtree to a list of dictionaries for JSON serialization.""" +def comment_subtree_to_api_dict( + request: Request, comments: list[CommentInTree] +) -> list: + """Convert a comment subtree to a list of dictionaries for JSON serialization. + + The schema is defined in our OpenAPI YAML file. + """ comments_list = [] for comment in comments: - comment_dict = comment_to_dict(request, comment) + comment_dict = comment_to_api_dict(request, comment) comment_dict["depth"] = comment.depth comment_dict["children"] = ( - comment_subtree_to_dict(request, comment.replies) if comment.replies else [] + comment_subtree_to_api_dict(request, comment.replies) + if comment.replies + else [] ) comments_list.append(comment_dict) return comments_list diff --git a/tildes/tildes/views/api/beta/topic.py b/tildes/tildes/views/api/beta/topic.py index 02a40f7..6b04bc8 100644 --- a/tildes/tildes/views/api/beta/topic.py +++ b/tildes/tildes/views/api/beta/topic.py @@ -3,24 +3,26 @@ """JSON API endpoints related to topics.""" -from marshmallow.exceptions import ValidationError from pyramid.request import Request from pyramid.view import view_config from tildes.enums import CommentTreeSortOption, TopicSortOption from tildes.models.comment import CommentTree, Comment from tildes.models.topic import Topic -from tildes.schemas.fields import ShortTimePeriod +from tildes.lib.datetime import SimpleHoursPeriod from tildes.lib.id import id36_to_id from tildes.views.api.beta.api_utils import ( build_error_response, get_next_and_prev_link, query_apply_pagination, ) -from tildes.views.api.beta.comment import comment_subtree_to_dict +from tildes.views.api.beta.comment import comment_subtree_to_api_dict -def topic_to_dict(topic: Topic) -> dict: - """Convert a Topic object to a dictionary for JSON serialization.""" +def topic_to_api_dict(topic: Topic) -> dict: + """Convert a Topic object to a dictionary for JSON serialization. + + The schema is defined in our OpenAPI YAML file. + """ return { "id": topic.topic_id36, "title": topic.title, @@ -59,10 +61,13 @@ def get_topics(request: Request) -> dict: # noqa after = request.openapi_validated.parameters.query.get("after", None) # Parse parameters where necessary - try: - period = ShortTimePeriod(allow_none=True).deserialize(period_raw) - except ValidationError as exc: - return build_error_response(str(exc), field="period") + if not period_raw or period_raw == "all": + period = None + else: + try: + period = SimpleHoursPeriod.from_short_form(period_raw) + except ValueError as exc: + return build_error_response(str(exc), field="period") try: if order_raw: @@ -94,7 +99,7 @@ def get_topics(request: Request) -> dict: # noqa # Build the JSON topic data for topic in topics: - processed_topic = topic_to_dict(topic) + processed_topic = topic_to_api_dict(topic) processed_topics.append(processed_topic) # Construct the paging next and previous link if there are more topics @@ -161,11 +166,11 @@ def get_topic(request: Request) -> dict: tree.uncollapse_new_comments(topic.last_visit_time) tree.finalize_collapsing_maximized() - commentsjson = comment_subtree_to_dict(request, tree.tree) + commentsjson = comment_subtree_to_api_dict(request, tree.tree) # Construct the final response JSON object response = { - "topic": topic_to_dict(topic), + "topic": topic_to_api_dict(topic), "comments": commentsjson, } return response diff --git a/tildes/tildes/views/api/beta/user.py b/tildes/tildes/views/api/beta/user.py index 3b5b865..0f97ea1 100644 --- a/tildes/tildes/views/api/beta/user.py +++ b/tildes/tildes/views/api/beta/user.py @@ -15,12 +15,15 @@ from tildes.views.api.beta.api_utils import ( get_next_and_prev_link, query_apply_pagination, ) -from tildes.views.api.beta.comment import comment_to_dict -from tildes.views.api.beta.topic import topic_to_dict +from tildes.views.api.beta.comment import comment_to_api_dict +from tildes.views.api.beta.topic import topic_to_api_dict -def _user_to_dict(user: User) -> dict: - """Convert a User object to a dictionary for JSON serialization.""" +def _user_to_api_dict(user: User) -> dict: + """Convert a User object to a dictionary for JSON serialization. + + The schema is defined in our OpenAPI YAML file. + """ return { "username": user.username, "joined_at": user.created_time.isoformat(), @@ -85,16 +88,16 @@ def get_user(request: Request) -> dict: # noqa processed_results = [] for item in combined_results.results: if isinstance(item, Topic): - processed_results.append(topic_to_dict(item)) + processed_results.append(topic_to_api_dict(item)) elif isinstance(item, Comment): - processed_results.append(comment_to_dict(request, item)) + processed_results.append(comment_to_api_dict(request, item)) # Construct the paging next and previous link if there are more topics (next_link, prev_link) = get_next_and_prev_link(request, combined_results) # Construct the final response JSON object response = { - "user": _user_to_dict(user), + "user": _user_to_api_dict(user), "history": processed_results, "pagination": { "num_items": len(processed_results), @@ -144,7 +147,7 @@ def get_user_comments(request: Request) -> dict: # Build the JSON history data processed_comments = [] for comment in query_result.results: - processed_comments.append(comment_to_dict(request, comment)) + processed_comments.append(comment_to_api_dict(request, comment)) # Construct the paging next and previous link if there are more comments (next_link, prev_link) = get_next_and_prev_link(request, query_result) @@ -200,7 +203,7 @@ def get_user_topics(request: Request) -> dict: # Build the JSON history data processed_topics = [] for topic in query_result.results: - processed_topics.append(topic_to_dict(topic)) + processed_topics.append(topic_to_api_dict(topic)) # Construct the paging next and previous link if there are more topics (next_link, prev_link) = get_next_and_prev_link(request, query_result) From 88815a7d6b2850c02eb7409537474248b9afab7c Mon Sep 17 00:00:00 2001 From: Andrew Shu Date: Sun, 31 Aug 2025 23:32:56 -0700 Subject: [PATCH 2/8] API: Cut off comment subtree if no permission to view --- tildes/tildes/views/api/beta/comment.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/tildes/tildes/views/api/beta/comment.py b/tildes/tildes/views/api/beta/comment.py index 7d37aa2..a07c914 100644 --- a/tildes/tildes/views/api/beta/comment.py +++ b/tildes/tildes/views/api/beta/comment.py @@ -80,10 +80,16 @@ def comment_subtree_to_api_dict( for comment in comments: comment_dict = comment_to_api_dict(request, comment) comment_dict["depth"] = comment.depth - comment_dict["children"] = ( - comment_subtree_to_api_dict(request, comment.replies) - if comment.replies - else [] - ) + + if request.has_permission("view", comment) or not comment.removed_marker: + # Recursively display reply comments, unless we hit a "removed marker" + comment_dict["children"] = ( + comment_subtree_to_api_dict(request, comment.replies) + if comment.replies + else [] + ) + else: + comment_dict["children"] = [] + comments_list.append(comment_dict) return comments_list From 711692227ab470fa56389893a2851227cdd151e2 Mon Sep 17 00:00:00 2001 From: Andrew Shu Date: Mon, 1 Sep 2025 00:07:30 -0700 Subject: [PATCH 3/8] Check permissions for all Comment fields in API --- tildes/openapi_beta.yaml | 7 ++- tildes/tildes/views/api/beta/comment.py | 62 ++++++++++++++++--------- 2 files changed, 46 insertions(+), 23 deletions(-) diff --git a/tildes/openapi_beta.yaml b/tildes/openapi_beta.yaml index 436b571..3c18065 100644 --- a/tildes/openapi_beta.yaml +++ b/tildes/openapi_beta.yaml @@ -343,8 +343,12 @@ components: - removed - deleted - exemplary + - collapsed + - collapsed_individual - voted - bookmarked + - depth + - children properties: id: type: string @@ -358,6 +362,7 @@ components: nullable: true created_at: type: string + nullable: true edited_at: type: string nullable: true @@ -372,10 +377,8 @@ components: nullable: true collapsed: type: boolean - nullable: true collapsed_individual: type: boolean - nullable: true by_op: type: boolean nullable: true diff --git a/tildes/tildes/views/api/beta/comment.py b/tildes/tildes/views/api/beta/comment.py index a07c914..54507f1 100644 --- a/tildes/tildes/views/api/beta/comment.py +++ b/tildes/tildes/views/api/beta/comment.py @@ -14,20 +14,45 @@ def comment_to_api_dict(request: Request, comment: Comment) -> dict: The schema is defined in our OpenAPI YAML file. """ + # Some fields do not require permissions + comment_id = comment.comment_id36 + topic_id = comment.topic.topic_id36 + is_removed = comment.is_removed + is_deleted = comment.is_deleted + collapsed = ( + hasattr(comment, "collapsed_state") and comment.collapsed_state == "full" + ) + collapsed_individual = ( + hasattr(comment, "collapsed_state") and comment.collapsed_state == "individual" + ) + # Check permissions for viewing comment details (and set safe defaults) author = None + created_time = None + edited_time = None rendered_html = None + votes = 0 exemplary = None by_op = None by_me = None is_new_comment = None + voted = False + bookmarked = False + if request.has_permission("view", comment): author = comment.user.username + created_time = comment.created_time.isoformat() + edited_time = ( + comment.last_edited_time.isoformat() if comment.last_edited_time else None + ) rendered_html = comment.rendered_html + votes = comment.num_votes exemplary = comment.is_label_active("exemplary") by_me = request.user == comment.user if request.user else False + if request.has_permission("view_author", comment.topic): by_op = comment.user == comment.topic.user + is_new_comment = ( (comment.created_time > comment.topic.last_visit_time) if ( @@ -38,34 +63,29 @@ def comment_to_api_dict(request: Request, comment: Comment) -> dict: else False ) + if request.has_permission("vote", comment): + voted = comment.user_voted + if request.has_permission("bookmark", comment): + bookmarked = comment.user_bookmarked + return { - "id": comment.comment_id36, - "topic_id": comment.topic.topic_id36, + "id": comment_id, + "topic_id": topic_id, "author": author, "rendered_html": rendered_html, - "created_at": comment.created_time.isoformat(), - "edited_at": ( - comment.last_edited_time.isoformat() if comment.last_edited_time else None - ), - "votes": comment.num_votes, - "removed": comment.is_removed, - "deleted": comment.is_deleted, + "created_at": created_time, + "edited_at": edited_time, + "votes": votes, + "removed": is_removed, + "deleted": is_deleted, "exemplary": exemplary, - "collapsed": ( - (comment.collapsed_state == "full") - if hasattr(comment, "collapsed_state") - else None - ), - "collapsed_individual": ( - (comment.collapsed_state == "individual") - if hasattr(comment, "collapsed_state") - else None - ), + "collapsed": collapsed, + "collapsed_individual": collapsed_individual, "by_op": by_op, "by_me": by_me, "new_comment": is_new_comment, - "voted": comment.user_voted, - "bookmarked": comment.user_bookmarked, + "voted": voted, + "bookmarked": bookmarked, } From a66696c3374229b39ae4e4f374e6b5d3847af573 Mon Sep 17 00:00:00 2001 From: Andrew Shu Date: Mon, 1 Sep 2025 00:13:00 -0700 Subject: [PATCH 4/8] API: Rename properties for consistency * Rename Topic last_visit_time to last_visited_at For consistency with other DateTime properties. * Rename Comment votes to vote_count For consistency with Topic property names. * Rename pagination num_items to item_count For consistency with other "count" properties. --- tildes/openapi_beta.yaml | 12 ++++++------ tildes/tildes/views/api/beta/comment.py | 6 +++--- tildes/tildes/views/api/beta/topic.py | 4 ++-- tildes/tildes/views/api/beta/user.py | 6 +++--- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/tildes/openapi_beta.yaml b/tildes/openapi_beta.yaml index 3c18065..27116c4 100644 --- a/tildes/openapi_beta.yaml +++ b/tildes/openapi_beta.yaml @@ -274,7 +274,7 @@ components: - ignored - official - tags - - last_visit_time + - last_visited_at properties: id: type: string @@ -326,7 +326,7 @@ components: type: array items: type: string - last_visit_time: + last_visited_at: type: string nullable: true @@ -339,7 +339,7 @@ components: - rendered_html - created_at - edited_at - - votes + - vote_count - removed - deleted - exemplary @@ -366,7 +366,7 @@ components: edited_at: type: string nullable: true - votes: + vote_count: type: integer removed: type: boolean @@ -410,11 +410,11 @@ components: Pagination: type: object required: - - num_items + - item_count - next_link - prev_link properties: - num_items: + item_count: type: integer description: The number of items returned in this response. next_link: diff --git a/tildes/tildes/views/api/beta/comment.py b/tildes/tildes/views/api/beta/comment.py index 54507f1..c3c1773 100644 --- a/tildes/tildes/views/api/beta/comment.py +++ b/tildes/tildes/views/api/beta/comment.py @@ -31,7 +31,7 @@ def comment_to_api_dict(request: Request, comment: Comment) -> dict: created_time = None edited_time = None rendered_html = None - votes = 0 + vote_count = 0 exemplary = None by_op = None by_me = None @@ -46,7 +46,7 @@ def comment_to_api_dict(request: Request, comment: Comment) -> dict: comment.last_edited_time.isoformat() if comment.last_edited_time else None ) rendered_html = comment.rendered_html - votes = comment.num_votes + vote_count = comment.num_votes exemplary = comment.is_label_active("exemplary") by_me = request.user == comment.user if request.user else False @@ -75,7 +75,7 @@ def comment_to_api_dict(request: Request, comment: Comment) -> dict: "rendered_html": rendered_html, "created_at": created_time, "edited_at": edited_time, - "votes": votes, + "vote_count": vote_count, "removed": is_removed, "deleted": is_deleted, "exemplary": exemplary, diff --git a/tildes/tildes/views/api/beta/topic.py b/tildes/tildes/views/api/beta/topic.py index 6b04bc8..0da2e87 100644 --- a/tildes/tildes/views/api/beta/topic.py +++ b/tildes/tildes/views/api/beta/topic.py @@ -44,7 +44,7 @@ def topic_to_api_dict(topic: Topic) -> dict: "ignored": topic.user_ignored, "official": topic.is_official, "tags": topic.tags, - "last_visit_time": ( + "last_visited_at": ( topic.last_visit_time.isoformat() if topic.last_visit_time else None ), } @@ -109,7 +109,7 @@ def get_topics(request: Request) -> dict: # noqa response = { "topics": processed_topics, "pagination": { - "num_items": len(processed_topics), + "item_count": len(processed_topics), "next_link": next_link, "prev_link": prev_link, }, diff --git a/tildes/tildes/views/api/beta/user.py b/tildes/tildes/views/api/beta/user.py index 0f97ea1..23b0ffa 100644 --- a/tildes/tildes/views/api/beta/user.py +++ b/tildes/tildes/views/api/beta/user.py @@ -100,7 +100,7 @@ def get_user(request: Request) -> dict: # noqa "user": _user_to_api_dict(user), "history": processed_results, "pagination": { - "num_items": len(processed_results), + "item_count": len(processed_results), "next_link": next_link, "prev_link": prev_link, }, @@ -156,7 +156,7 @@ def get_user_comments(request: Request) -> dict: response = { "comments": processed_comments, "pagination": { - "num_items": len(processed_comments), + "item_count": len(processed_comments), "next_link": next_link, "prev_link": prev_link, }, @@ -212,7 +212,7 @@ def get_user_topics(request: Request) -> dict: response = { "topics": processed_topics, "pagination": { - "num_items": len(processed_topics), + "item_count": len(processed_topics), "next_link": next_link, "prev_link": prev_link, }, From 351d43bf228c894a3ecf421398a477891267b089 Mon Sep 17 00:00:00 2001 From: Andrew Shu Date: Mon, 1 Sep 2025 00:19:46 -0700 Subject: [PATCH 5/8] API: Update schema "required" properties To ensure API responses are consistent. Also most of these properties are expected from a consumer point of view, even if null. Mark Error schema properties nullable where not in required list. --- tildes/openapi_beta.yaml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tildes/openapi_beta.yaml b/tildes/openapi_beta.yaml index 27116c4..48ec7e7 100644 --- a/tildes/openapi_beta.yaml +++ b/tildes/openapi_beta.yaml @@ -345,6 +345,9 @@ components: - exemplary - collapsed - collapsed_individual + - by_op + - by_me + - new_comment - voted - bookmarked - depth @@ -429,9 +432,11 @@ components: required: - message properties: - field: - type: string message: type: string + field: + type: string + nullable: true exception: type: string + nullable: true From 389f1abd0640d2ebc3da9e9318c9aefecdb6f494 Mon Sep 17 00:00:00 2001 From: Andrew Shu Date: Sat, 6 Sep 2025 21:00:10 -0700 Subject: [PATCH 6/8] API: Fix permission checks for User info --- tildes/openapi_beta.yaml | 6 ++++++ tildes/tildes/templates/user.jinja2 | 24 ++++++++++++------------ tildes/tildes/views/api/beta/user.py | 22 +++++++++++++++++----- 3 files changed, 35 insertions(+), 17 deletions(-) diff --git a/tildes/openapi_beta.yaml b/tildes/openapi_beta.yaml index 48ec7e7..3f27904 100644 --- a/tildes/openapi_beta.yaml +++ b/tildes/openapi_beta.yaml @@ -409,6 +409,12 @@ components: properties: username: type: string + joined_at: + type: string + nullable: true + bio_rendered_html: + type: string + nullable: true Pagination: type: object diff --git a/tildes/tildes/templates/user.jinja2 b/tildes/tildes/templates/user.jinja2 index 6d19357..f57043b 100644 --- a/tildes/tildes/templates/user.jinja2 +++ b/tildes/tildes/templates/user.jinja2 @@ -171,18 +171,18 @@ {% endif %} {% if request.has_permission("view_info", user) %} -

User info

-
-
Registered
-
{{ user.created_time.strftime('%B %-d, %Y') }}
- - {% if user.bio_rendered_html %} -
-
Bio
-
{{ user.bio_rendered_html|safe }}
-
- {% endif %} -
+

User info

+
+
Registered
+
{{ user.created_time.strftime('%B %-d, %Y') }}
+ + {% if user.bio_rendered_html %} +
+
Bio
+
{{ user.bio_rendered_html|safe }}
+
+ {% endif %} +
{% endif %} {% if request.has_permission('message', user) %} diff --git a/tildes/tildes/views/api/beta/user.py b/tildes/tildes/views/api/beta/user.py index 23b0ffa..9428833 100644 --- a/tildes/tildes/views/api/beta/user.py +++ b/tildes/tildes/views/api/beta/user.py @@ -19,15 +19,27 @@ from tildes.views.api.beta.comment import comment_to_api_dict from tildes.views.api.beta.topic import topic_to_api_dict -def _user_to_api_dict(user: User) -> dict: +def _user_to_api_dict(request: Request, user: User) -> dict: """Convert a User object to a dictionary for JSON serialization. The schema is defined in our OpenAPI YAML file. """ + + # Some fields do not require permissions + username = user.username + + # Check permissions for viewing user details (and set safe defaults) + joined_at = None + bio_rendered_html = None + + if request.has_permission("view_info", user): + joined_at = user.created_time.isoformat() + bio_rendered_html = user.bio_rendered_html + return { - "username": user.username, - "joined_at": user.created_time.isoformat(), - "bio_rendered_html": user.bio_rendered_html, + "username": username, + "joined_at": joined_at, + "bio_rendered_html": bio_rendered_html, } @@ -97,7 +109,7 @@ def get_user(request: Request) -> dict: # noqa # Construct the final response JSON object response = { - "user": _user_to_api_dict(user), + "user": _user_to_api_dict(request, user), "history": processed_results, "pagination": { "item_count": len(processed_results), From badbbe161518994583d9ca7c1928056eee65258b Mon Sep 17 00:00:00 2001 From: Andrew Shu Date: Sat, 6 Sep 2025 23:54:51 -0700 Subject: [PATCH 7/8] API: Fix function return type hints --- tildes/tildes/views/api/beta/topic.py | 5 +++-- tildes/tildes/views/api/beta/user.py | 7 ++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/tildes/tildes/views/api/beta/topic.py b/tildes/tildes/views/api/beta/topic.py index 0da2e87..75ae289 100644 --- a/tildes/tildes/views/api/beta/topic.py +++ b/tildes/tildes/views/api/beta/topic.py @@ -4,6 +4,7 @@ """JSON API endpoints related to topics.""" from pyramid.request import Request +from pyramid.response import Response from pyramid.view import view_config from tildes.enums import CommentTreeSortOption, TopicSortOption from tildes.models.comment import CommentTree, Comment @@ -51,7 +52,7 @@ def topic_to_api_dict(topic: Topic) -> dict: @view_config(route_name="apibeta.topics", openapi=True, renderer="json") -def get_topics(request: Request) -> dict: # noqa +def get_topics(request: Request) -> dict | Response: # noqa: MC0001 """Get a list of topics (without comments).""" limit = request.openapi_validated.parameters.query.get("limit", 50) period_raw = request.openapi_validated.parameters.query.get("period") @@ -118,7 +119,7 @@ def get_topics(request: Request) -> dict: # noqa @view_config(route_name="apibeta.topic", openapi=True, renderer="json") -def get_topic(request: Request) -> dict: +def get_topic(request: Request) -> dict | Response: """Get a single topic (with comments).""" topic_id36 = request.openapi_validated.parameters.path.get("topic_id36") comment_order_raw = request.openapi_validated.parameters.query.get("order") diff --git a/tildes/tildes/views/api/beta/user.py b/tildes/tildes/views/api/beta/user.py index 9428833..36ce682 100644 --- a/tildes/tildes/views/api/beta/user.py +++ b/tildes/tildes/views/api/beta/user.py @@ -5,6 +5,7 @@ from typing import Union from pyramid.request import Request +from pyramid.response import Response from pyramid.view import view_config from tildes.models.user.user import User from tildes.models.comment import Comment @@ -44,7 +45,7 @@ def _user_to_api_dict(request: Request, user: User) -> dict: @view_config(route_name="apibeta.user", openapi=True, renderer="json") -def get_user(request: Request) -> dict: # noqa +def get_user(request: Request) -> dict | Response: # noqa: MC0001 """Get a single user with their comment and post history.""" username = request.openapi_validated.parameters.path.get("username") limit = request.openapi_validated.parameters.query.get("limit", 20) @@ -121,7 +122,7 @@ def get_user(request: Request) -> dict: # noqa @view_config(route_name="apibeta.user_comments", openapi=True, renderer="json") -def get_user_comments(request: Request) -> dict: +def get_user_comments(request: Request) -> dict | Response: """Get comments made by a user.""" username = request.openapi_validated.parameters.path.get("username") limit = request.openapi_validated.parameters.query.get("limit", 50) @@ -177,7 +178,7 @@ def get_user_comments(request: Request) -> dict: @view_config(route_name="apibeta.user_topics", openapi=True, renderer="json") -def get_user_topics(request: Request) -> dict: +def get_user_topics(request: Request) -> dict | Response: """Get topics made by a user.""" username = request.openapi_validated.parameters.path.get("username") limit = request.openapi_validated.parameters.query.get("limit", 50) From 64758fc1f31f0257585ac0c16ee29dca00c86051 Mon Sep 17 00:00:00 2001 From: Andrew Shu Date: Sun, 7 Sep 2025 01:26:50 -0700 Subject: [PATCH 8/8] API: Fix permission checks for Topic --- tildes/tildes/views/api/beta/topic.py | 91 +++++++++++++++++++-------- tildes/tildes/views/api/beta/user.py | 4 +- 2 files changed, 67 insertions(+), 28 deletions(-) diff --git a/tildes/tildes/views/api/beta/topic.py b/tildes/tildes/views/api/beta/topic.py index 75ae289..5016686 100644 --- a/tildes/tildes/views/api/beta/topic.py +++ b/tildes/tildes/views/api/beta/topic.py @@ -19,35 +19,74 @@ from tildes.views.api.beta.api_utils import ( from tildes.views.api.beta.comment import comment_subtree_to_api_dict -def topic_to_api_dict(topic: Topic) -> dict: +def topic_to_api_dict(request: Request, topic: Topic) -> dict: """Convert a Topic object to a dictionary for JSON serialization. The schema is defined in our OpenAPI YAML file. """ + + # Some fields do not require permissions + topic_id = topic.topic_id36 + title = topic.title + comments_url = topic.permalink + group = str(topic.group.path) + created_at = topic.created_time.isoformat() + edited_at = topic.last_edited_time.isoformat() if topic.last_edited_time else None + vote_count = topic.num_votes + comment_count = topic.num_comments + new_comment_count = topic.comments_since_last_visit + official = topic.is_official + tags = topic.tags + last_visited_at = ( + topic.last_visit_time.isoformat() if topic.last_visit_time else None + ) + + # Check permissions for viewing topic details (and set safe defaults) + text_html = None + url = None + content_metadata = None + posted_by_user = "unknown user" + voted = False + bookmarked = False + ignored = False + + if request.has_permission("view_content", topic): + text_html = topic.rendered_html + url = topic.link + content_metadata = topic.content_metadata_for_display + + if request.has_permission("view_author", topic): + posted_by_user = topic.user.username + + if request.has_permission("vote", topic): + voted = topic.user_voted + + if request.has_permission("bookmark", topic): + bookmarked = topic.user_bookmarked + + if request.has_permission("ignore", topic): + ignored = topic.user_ignored + return { - "id": topic.topic_id36, - "title": topic.title, - "text_html": topic.rendered_html, - "url": topic.link, - "comments_url": topic.permalink, - "group": str(topic.group.path), - "content_metadata": topic.content_metadata_for_display, - "created_at": topic.created_time.isoformat(), - "edited_at": ( - topic.last_edited_time.isoformat() if topic.last_edited_time else None - ), - "posted_by_user": topic.user.username, - "vote_count": topic.num_votes, - "comment_count": topic.num_comments, - "new_comment_count": topic.comments_since_last_visit, - "voted": topic.user_voted, - "bookmarked": topic.user_bookmarked, - "ignored": topic.user_ignored, - "official": topic.is_official, - "tags": topic.tags, - "last_visited_at": ( - topic.last_visit_time.isoformat() if topic.last_visit_time else None - ), + "id": topic_id, + "title": title, + "text_html": text_html, + "url": url, + "comments_url": comments_url, + "group": group, + "content_metadata": content_metadata, + "created_at": created_at, + "edited_at": edited_at, + "posted_by_user": posted_by_user, + "vote_count": vote_count, + "comment_count": comment_count, + "new_comment_count": new_comment_count, + "voted": voted, + "bookmarked": bookmarked, + "ignored": ignored, + "official": official, + "tags": tags, + "last_visited_at": last_visited_at, } @@ -100,7 +139,7 @@ def get_topics(request: Request) -> dict | Response: # noqa: MC0001 # Build the JSON topic data for topic in topics: - processed_topic = topic_to_api_dict(topic) + processed_topic = topic_to_api_dict(request, topic) processed_topics.append(processed_topic) # Construct the paging next and previous link if there are more topics @@ -171,7 +210,7 @@ def get_topic(request: Request) -> dict | Response: # Construct the final response JSON object response = { - "topic": topic_to_api_dict(topic), + "topic": topic_to_api_dict(request, topic), "comments": commentsjson, } return response diff --git a/tildes/tildes/views/api/beta/user.py b/tildes/tildes/views/api/beta/user.py index 36ce682..3b65bf7 100644 --- a/tildes/tildes/views/api/beta/user.py +++ b/tildes/tildes/views/api/beta/user.py @@ -101,7 +101,7 @@ def get_user(request: Request) -> dict | Response: # noqa: MC0001 processed_results = [] for item in combined_results.results: if isinstance(item, Topic): - processed_results.append(topic_to_api_dict(item)) + processed_results.append(topic_to_api_dict(request, item)) elif isinstance(item, Comment): processed_results.append(comment_to_api_dict(request, item)) @@ -216,7 +216,7 @@ def get_user_topics(request: Request) -> dict | Response: # Build the JSON history data processed_topics = [] for topic in query_result.results: - processed_topics.append(topic_to_api_dict(topic)) + processed_topics.append(topic_to_api_dict(request, topic)) # Construct the paging next and previous link if there are more topics (next_link, prev_link) = get_next_and_prev_link(request, query_result)