diff --git a/tildes/openapi_beta.yaml b/tildes/openapi_beta.yaml
index 436b571..3f27904 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,12 +339,19 @@ components:
- rendered_html
- created_at
- edited_at
- - votes
+ - vote_count
- removed
- deleted
- exemplary
+ - collapsed
+ - collapsed_individual
+ - by_op
+ - by_me
+ - new_comment
- voted
- bookmarked
+ - depth
+ - children
properties:
id:
type: string
@@ -358,10 +365,11 @@ components:
nullable: true
created_at:
type: string
+ nullable: true
edited_at:
type: string
nullable: true
- votes:
+ vote_count:
type: integer
removed:
type: boolean
@@ -372,10 +380,8 @@ components:
nullable: true
collapsed:
type: boolean
- nullable: true
collapsed_individual:
type: boolean
- nullable: true
by_op:
type: boolean
nullable: true
@@ -403,15 +409,21 @@ components:
properties:
username:
type: string
+ joined_at:
+ type: string
+ nullable: true
+ bio_rendered_html:
+ type: string
+ nullable: true
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:
@@ -426,9 +438,11 @@ components:
required:
- message
properties:
- field:
- type: string
message:
type: string
+ field:
+ type: string
+ nullable: true
exception:
type: string
+ nullable: true
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/comment.py b/tildes/tildes/views/api/beta/comment.py
index e57b355..c3c1773 100644
--- a/tildes/tildes/views/api/beta/comment.py
+++ b/tildes/tildes/views/api/beta/comment.py
@@ -8,23 +8,51 @@ 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.
+ """
+
+ # 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
+ vote_count = 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
+ vote_count = 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 (
@@ -35,45 +63,53 @@ def comment_to_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,
+ "vote_count": vote_count,
+ "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,
}
-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 []
- )
+
+ 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
diff --git a/tildes/tildes/views/api/beta/topic.py b/tildes/tildes/views/api/beta/topic.py
index 02a40f7..5016686 100644
--- a/tildes/tildes/views/api/beta/topic.py
+++ b/tildes/tildes/views/api/beta/topic.py
@@ -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
diff --git a/tildes/tildes/views/api/beta/user.py b/tildes/tildes/views/api/beta/user.py
index 3b5b865..3b65bf7 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
@@ -15,21 +16,36 @@ 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(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,
}
@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)
@@ -85,19 +101,19 @@ 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(request, 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(request, user),
"history": processed_results,
"pagination": {
- "num_items": len(processed_results),
+ "item_count": len(processed_results),
"next_link": next_link,
"prev_link": prev_link,
},
@@ -106,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)
@@ -144,7 +160,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)
@@ -153,7 +169,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,
},
@@ -162,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)
@@ -200,7 +216,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(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)
@@ -209,7 +225,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,
},