|
|
@ -4,18 +4,107 @@ |
|
|
|
"""JSON API endpoints related to topics.""" |
|
|
|
|
|
|
|
from marshmallow.exceptions import ValidationError |
|
|
|
from pyramid.exceptions import HTTPBadRequest |
|
|
|
from pyramid.request import Request |
|
|
|
from pyramid.response import Response |
|
|
|
from pyramid.view import view_config |
|
|
|
from tildes.enums import TopicSortOption |
|
|
|
from tildes.enums import CommentTreeSortOption, TopicSortOption |
|
|
|
from tildes.models.comment import CommentTree, Comment |
|
|
|
from tildes.models.comment.comment_tree import CommentInTree |
|
|
|
from tildes.models.topic import Topic |
|
|
|
from tildes.schemas.fields import ShortTimePeriod |
|
|
|
from tildes.lib.id import id36_to_id |
|
|
|
|
|
|
|
|
|
|
|
def _topic_to_dict(topic: Topic) -> dict: |
|
|
|
"""Convert a Topic object to a dictionary for JSON serialization.""" |
|
|
|
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(), |
|
|
|
"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_visit_time": ( |
|
|
|
topic.last_visit_time.isoformat() if topic.last_visit_time else None |
|
|
|
), |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
def _comment_to_dict(request: Request, comment: Comment) -> dict: |
|
|
|
"""Convert a Comment object to a dictionary for JSON serialization.""" |
|
|
|
|
|
|
|
# Check permissions for viewing comment details (and set safe defaults) |
|
|
|
author = None |
|
|
|
rendered_html = None |
|
|
|
exemplary = None |
|
|
|
is_op = None |
|
|
|
is_me = None |
|
|
|
is_new = None |
|
|
|
if request.has_permission("view", comment): |
|
|
|
author = comment.user.username |
|
|
|
rendered_html = comment.rendered_html |
|
|
|
exemplary = comment.is_label_active("exemplary") |
|
|
|
is_me = request.user == comment.user if request.user else False |
|
|
|
if request.has_permission("view_author", comment.topic): |
|
|
|
is_op = comment.user == comment.topic.user |
|
|
|
is_new = ( |
|
|
|
(comment.created_time > comment.topic.last_visit_time) |
|
|
|
if (comment.topic.last_visit_time and not is_me) |
|
|
|
else False |
|
|
|
) |
|
|
|
|
|
|
|
return { |
|
|
|
"id": comment.comment_id36, |
|
|
|
"topic_id": comment.topic.topic_id36, |
|
|
|
"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, |
|
|
|
"is_removed": comment.is_removed, |
|
|
|
"is_deleted": comment.is_deleted, |
|
|
|
"exemplary": exemplary, |
|
|
|
"collapsed": (comment.collapsed_state == "full"), |
|
|
|
"collapsed_individual": (comment.collapsed_state == "individual"), |
|
|
|
"is_op": is_op, |
|
|
|
"is_me": is_me, |
|
|
|
"is_new": is_new, |
|
|
|
"voted": comment.user_voted, |
|
|
|
"bookmarked": comment.user_bookmarked, |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
def _comment_subtree_to_dict(request: Request, comments: list[CommentInTree]) -> list: |
|
|
|
"""Convert a comment subtree to a list of dictionaries for JSON serialization.""" |
|
|
|
comments_list = [] |
|
|
|
for comment in comments: |
|
|
|
comment_dict = _comment_to_dict(request, comment) |
|
|
|
comment_dict["depth"] = comment.depth |
|
|
|
comment_dict["children"] = ( |
|
|
|
_comment_subtree_to_dict(request, comment.replies) |
|
|
|
if comment.replies |
|
|
|
else [] |
|
|
|
) |
|
|
|
comments_list.append(comment_dict) |
|
|
|
return comments_list |
|
|
|
|
|
|
|
|
|
|
|
@view_config(route_name="apibeta.topics", openapi=True, renderer="json") |
|
|
|
def get_topics(request: Request) -> dict: |
|
|
|
"""Get topics""" |
|
|
|
def get_topics(request: Request) -> dict: # noqa |
|
|
|
"""Get a list of topics (without comments).""" |
|
|
|
limit = request.openapi_validated.parameters.query.get("limit") |
|
|
|
period_raw = request.openapi_validated.parameters.query.get("period") |
|
|
|
tag = request.openapi_validated.parameters.query.get("tag", None) |
|
|
@ -91,25 +180,7 @@ def get_topics(request: Request) -> dict: |
|
|
|
|
|
|
|
# Build the JSON topic data |
|
|
|
for topic in topics: |
|
|
|
processed_topic = { |
|
|
|
"id": topic.topic_id, |
|
|
|
"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(), |
|
|
|
"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, |
|
|
|
} |
|
|
|
processed_topic = _topic_to_dict(topic) |
|
|
|
processed_topics.append(processed_topic) |
|
|
|
|
|
|
|
# Construct the paging next and previous link if there are more topics |
|
|
@ -139,3 +210,78 @@ def get_topics(request: Request) -> dict: |
|
|
|
}, |
|
|
|
} |
|
|
|
return response |
|
|
|
|
|
|
|
|
|
|
|
@view_config(route_name="apibeta.topic", openapi=True, renderer="json") |
|
|
|
def get_topic(request: Request) -> dict: |
|
|
|
"""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") |
|
|
|
|
|
|
|
comment_order = CommentTreeSortOption.RELEVANCE |
|
|
|
if comment_order_raw is not None: |
|
|
|
try: |
|
|
|
comment_order = CommentTreeSortOption[comment_order_raw.upper()] |
|
|
|
except KeyError: |
|
|
|
return Response( |
|
|
|
status=400, |
|
|
|
content_type="application/json", |
|
|
|
json=[ |
|
|
|
{ |
|
|
|
"message": f"Invalid order value: {comment_order_raw}", |
|
|
|
"field": "order", |
|
|
|
"exception": "ValidationError", |
|
|
|
} |
|
|
|
], |
|
|
|
) |
|
|
|
else: |
|
|
|
if request.user and request.user.comment_sort_order_default: |
|
|
|
comment_order = request.user.comment_sort_order_default |
|
|
|
else: |
|
|
|
comment_order = CommentTreeSortOption.RELEVANCE |
|
|
|
|
|
|
|
try: |
|
|
|
topic_id = id36_to_id(topic_id36) |
|
|
|
query = request.query(Topic).filter(Topic.topic_id == topic_id) |
|
|
|
topic = query.one_or_none() |
|
|
|
if not topic: |
|
|
|
raise ValueError(f"Topic with ID {topic_id36} not found") |
|
|
|
except ValueError as exc: |
|
|
|
return Response( |
|
|
|
status=400, |
|
|
|
content_type="application/json", |
|
|
|
json=[ |
|
|
|
{ |
|
|
|
"message": str(exc), |
|
|
|
"field": "topic_id36", |
|
|
|
"exception": "ValidationError", |
|
|
|
} |
|
|
|
], |
|
|
|
) |
|
|
|
|
|
|
|
# deleted and removed comments need to be included since they're necessary for |
|
|
|
# building the tree if they have replies |
|
|
|
comments = ( |
|
|
|
request.query(Comment) |
|
|
|
.include_deleted() |
|
|
|
.include_removed() |
|
|
|
.filter(Comment.topic == topic) |
|
|
|
.order_by(Comment.created_time) |
|
|
|
.all() |
|
|
|
) |
|
|
|
|
|
|
|
tree = CommentTree(comments, comment_order, request.user) |
|
|
|
tree.collapse_from_labels() |
|
|
|
|
|
|
|
if request.user: |
|
|
|
# collapse old comments if the user has a previous visit to the topic |
|
|
|
# (and doesn't have that behavior disabled) |
|
|
|
if topic.last_visit_time and request.user.collapse_old_comments: |
|
|
|
tree.uncollapse_new_comments(topic.last_visit_time) |
|
|
|
tree.finalize_collapsing_maximized() |
|
|
|
|
|
|
|
commentsjson = _comment_subtree_to_dict(request, tree.tree) |
|
|
|
|
|
|
|
# Construct the final response JSON object |
|
|
|
response = {"topic": _topic_to_dict(topic), "comments": commentsjson} |
|
|
|
return response |