|
|
@ -3,53 +3,95 @@ |
|
|
|
|
|
|
|
"""JSON API endpoints related to topics.""" |
|
|
|
|
|
|
|
from marshmallow.exceptions import ValidationError |
|
|
|
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 |
|
|
|
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_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 |
|
|
|
|
|
|
|
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(), |
|
|
|
"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_visit_time": ( |
|
|
|
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, |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@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") |
|
|
@ -59,10 +101,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 +139,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(request, topic) |
|
|
|
processed_topics.append(processed_topic) |
|
|
|
|
|
|
|
# Construct the paging next and previous link if there are more topics |
|
|
@ -104,7 +149,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, |
|
|
|
}, |
|
|
@ -113,7 +158,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") |
|
|
@ -161,11 +206,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(request, topic), |
|
|
|
"comments": commentsjson, |
|
|
|
} |
|
|
|
return response |