diff --git a/tildes/tests/test_comment.py b/tildes/tests/test_comment.py index d1f575d..ce79f36 100644 --- a/tildes/tests/test_comment.py +++ b/tildes/tests/test_comment.py @@ -6,7 +6,7 @@ from datetime import timedelta from freezegun import freeze_time from pyramid.security import Authenticated, Everyone, principals_allowed_by_permission -from tildes.enums import CommentSortOption +from tildes.enums import CommentTreeSortOption from tildes.lib.datetime import utc_now from tildes.models.comment import Comment, CommentTree, EDIT_GRACE_PERIOD from tildes.schemas.comment import CommentSchema @@ -134,7 +134,7 @@ def test_comment_tree(db, topic, session_user): """Ensure that building and pruning a comment tree works.""" all_comments = [] - sort = CommentSortOption.POSTED + sort = CommentTreeSortOption.POSTED # add two root comments root = Comment(topic, session_user, "root") diff --git a/tildes/tildes/__init__.py b/tildes/tildes/__init__.py index b23ac20..22b35dc 100644 --- a/tildes/tildes/__init__.py +++ b/tildes/tildes/__init__.py @@ -139,7 +139,7 @@ def current_listing_base_url( "group": ("order", "period", "per_page", "tag", "unfiltered"), "home": ("order", "period", "per_page", "tag", "unfiltered"), "search": ("order", "period", "per_page", "q"), - "user": ("per_page", "type"), + "user": ("order", "per_page", "type"), } try: @@ -172,7 +172,7 @@ def current_listing_normal_url( "home": ("order", "period", "per_page"), "notifications": ("per_page",), "search": ("order", "period", "per_page", "q"), - "user": ("per_page",), + "user": ("order", "per_page"), } try: diff --git a/tildes/tildes/enums.py b/tildes/tildes/enums.py index c686521..a46715e 100644 --- a/tildes/tildes/enums.py +++ b/tildes/tildes/enums.py @@ -17,7 +17,27 @@ class CommentNotificationType(enum.Enum): class CommentSortOption(enum.Enum): - """Enum for the different methods comments can be sorted by.""" + """Enum for the different methods out-of-context comments can be sorted by.""" + + VOTES = enum.auto() + NEW = enum.auto() + + @property + def descending_description(self) -> str: + """Describe this sort option when used in a "descending" order. + + For example, the "votes" sort has a description of "most votes", since using + that sort in descending order means that topics with the most votes will be + listed first. + """ + if self.name == "NEW": + return "newest" + + return "most {}".format(self.name.lower()) + + +class CommentTreeSortOption(enum.Enum): + """Enum for the different methods comment trees can be sorted by.""" VOTES = enum.auto() NEWEST = enum.auto() diff --git a/tildes/tildes/models/comment/comment_query.py b/tildes/tildes/models/comment/comment_query.py index a894bcf..7f7f225 100644 --- a/tildes/tildes/models/comment/comment_query.py +++ b/tildes/tildes/models/comment/comment_query.py @@ -8,6 +8,7 @@ from typing import Any from pyramid.request import Request from sqlalchemy.sql.expression import and_ +from tildes.enums import CommentSortOption from tildes.models.pagination import PaginatedQuery from .comment import Comment from .comment_bookmark import CommentBookmark @@ -78,6 +79,19 @@ class CommentQuery(PaginatedQuery): return comment + def apply_sort_option( + self, sort: CommentSortOption, desc: bool = True + ) -> "CommentQuery": + """Apply a CommentSortOption sorting method (generative).""" + if sort == CommentSortOption.VOTES: + self._sort_column = Comment.num_votes + elif sort == CommentSortOption.NEW: + self._sort_column = Comment.created_time + + self.sort_desc = desc + + return self + def only_bookmarked(self) -> "CommentQuery": """Restrict the comments to ones that the user has bookmarked (generative).""" self._only_bookmarked = True diff --git a/tildes/tildes/models/comment/comment_tree.py b/tildes/tildes/models/comment/comment_tree.py index 772432e..a78b1b4 100644 --- a/tildes/tildes/models/comment/comment_tree.py +++ b/tildes/tildes/models/comment/comment_tree.py @@ -9,7 +9,7 @@ from typing import Iterator, List, Optional, Sequence, Tuple from prometheus_client import Histogram from wrapt import ObjectProxy -from tildes.enums import CommentSortOption +from tildes.enums import CommentTreeSortOption from tildes.metrics import get_histogram from tildes.models.user import User from .comment import Comment @@ -21,7 +21,7 @@ class CommentTree: def __init__( self, comments: Sequence[Comment], - sort: CommentSortOption, + sort: CommentTreeSortOption, viewer: Optional[User] = None, ): """Create a sorted CommentTree from a flat list of Comments.""" @@ -44,7 +44,7 @@ class CommentTree: # no need to sort again if that's the desired sorting. Note also that because # _sort_tree() uses sorted() which is a stable sort, this means that the # "secondary sort" will always be by posting time as well. - if self.tree and sort != CommentSortOption.POSTED: + if self.tree and sort != CommentTreeSortOption.POSTED: with self._sorting_histogram().time(): self.tree = self._sort_tree(self.tree, self.sort) @@ -85,20 +85,20 @@ class CommentTree: self.tree.append(comment) @staticmethod - def _sort_tree(tree: List[Comment], sort: CommentSortOption) -> List[Comment]: + def _sort_tree(tree: List[Comment], sort: CommentTreeSortOption) -> List[Comment]: """Sort the tree by the desired ordering (recursively). Because Python's sorted() function is stable, the ordering of any comments that compare equal on the sorting method will be the same as the order that they were originally in when passed to this function. """ - if sort == CommentSortOption.NEWEST: + if sort == CommentTreeSortOption.NEWEST: tree = sorted(tree, key=lambda c: c.created_time, reverse=True) - elif sort == CommentSortOption.POSTED: + elif sort == CommentTreeSortOption.POSTED: tree = sorted(tree, key=lambda c: c.created_time) - elif sort == CommentSortOption.VOTES: + elif sort == CommentTreeSortOption.VOTES: tree = sorted(tree, key=lambda c: c.num_votes, reverse=True) - elif sort == CommentSortOption.RELEVANCE: + elif sort == CommentTreeSortOption.RELEVANCE: tree = sorted(tree, key=lambda c: c.relevance_sorting_value, reverse=True) for comment in tree: diff --git a/tildes/tildes/templates/user.jinja2 b/tildes/tildes/templates/user.jinja2 index 68e971b..1c30168 100644 --- a/tildes/tildes/templates/user.jinja2 +++ b/tildes/tildes/templates/user.jinja2 @@ -45,6 +45,30 @@ Comments + + {% if order_options %} +
+ +
+ + + {# add a submit button for people with js disabled so this is still usable #} + +
+
+ {% endif %} + {% endif %} diff --git a/tildes/tildes/views/topic.py b/tildes/tildes/views/topic.py index cca528b..c67fcfa 100644 --- a/tildes/tildes/views/topic.py +++ b/tildes/tildes/views/topic.py @@ -20,7 +20,7 @@ from zope.sqlalchemy import mark_changed from tildes.enums import ( CommentLabelOption, CommentNotificationType, - CommentSortOption, + CommentTreeSortOption, LogEventType, TopicSortOption, ) @@ -255,8 +255,8 @@ def get_new_topic_form(request: Request) -> dict: @view_config(route_name="topic", renderer="topic.jinja2") -@use_kwargs({"comment_order": Enum(CommentSortOption, missing="relevance")}) -def get_topic(request: Request, comment_order: CommentSortOption) -> dict: +@use_kwargs({"comment_order": Enum(CommentTreeSortOption, missing="relevance")}) +def get_topic(request: Request, comment_order: CommentTreeSortOption) -> dict: """View a single topic.""" topic = request.context @@ -310,7 +310,7 @@ def get_topic(request: Request, comment_order: CommentSortOption) -> dict: "log": log, "comments": tree, "comment_order": comment_order, - "comment_order_options": CommentSortOption, + "comment_order_options": CommentTreeSortOption, "comment_label_options": CommentLabelOption, } diff --git a/tildes/tildes/views/user.py b/tildes/tildes/views/user.py index 7031969..5e33161 100644 --- a/tildes/tildes/views/user.py +++ b/tildes/tildes/views/user.py @@ -5,12 +5,13 @@ from typing import List, Optional, Type, Union +from marshmallow.fields import String from pyramid.request import Request from pyramid.view import view_config from sqlalchemy.sql.expression import desc from webargs.pyramidparser import use_kwargs -from tildes.enums import CommentLabelOption +from tildes.enums import CommentLabelOption, CommentSortOption, TopicSortOption from tildes.models.comment import Comment from tildes.models.pagination import MixedPaginatedResults from tildes.models.topic import Topic @@ -19,15 +20,21 @@ from tildes.schemas.fields import PostType from tildes.schemas.listing import MixedListingSchema -@view_config(route_name="user", renderer="user.jinja2") +@view_config(route_name="user", renderer="user.jinja2") # noqa @use_kwargs(MixedListingSchema()) -@use_kwargs({"post_type": PostType(load_from="type")}) +@use_kwargs( + { + "post_type": PostType(load_from="type"), + "order_name": String(load_from="order", missing="new"), + } # noqa +) def get_user( request: Request, after: Optional[str], before: Optional[str], per_page: int, anchor_type: Optional[str], + order_name: str, post_type: Optional[str] = None, ) -> dict: # pylint: disable=too-many-arguments @@ -44,14 +51,27 @@ def get_user( per_page = 20 types_to_query: List[Union[Type[Topic], Type[Comment]]] + order_options: Optional[Union[Type[TopicSortOption], Type[CommentSortOption]]] + if post_type == "topic": types_to_query = [Topic] + order_options = TopicSortOption elif post_type == "comment": types_to_query = [Comment] + order_options = CommentSortOption else: # the order here is important so items are in the right order when the results # are merged at the end (we want topics to come first when times match) types_to_query = [Comment, Topic] + order_options = None + + order = None + if order_options: + # try to get the specified order, but fall back to "newest" + try: + order = order_options[order_name.upper()] + except KeyError: + order = order_options["NEW"] result_sets = [] for type_to_query in types_to_query: @@ -66,6 +86,9 @@ def get_user( if after: query = query.after_id36(after) + if order: + query = query.apply_sort_option(order) + query = query.join_all_relationships() # include removed posts if the user's looking at their own page or is an admin @@ -83,6 +106,8 @@ def get_user( "user": user, "posts": posts, "post_type": post_type, + "order": order, + "order_options": order_options, "comment_label_options": CommentLabelOption, }