Browse Source

Make default comment sort method affected by tags

Adds a new comment sort named "relevance" (may change the name) and
makes it the default. This sort takes into account the tags that have
been applied to comments and sorts them differently based on them. This
will certainly require a lot more work and adjustments, but for now it
will help push down noise, offtopic, and joke comments.

In the future, this can probably also be used to apply some more
interesting sorting effects, like potentially placing new comments
temporarily near the top of the comment section to help them get a bit
of initial exposure in large threads.
merge-requests/37/head
Deimos 6 years ago
parent
commit
902d0fcd0c
  1. 3
      tildes/tildes/enums.py
  2. 15
      tildes/tildes/models/comment/comment.py
  3. 33
      tildes/tildes/models/comment/comment_tree.py
  4. 2
      tildes/tildes/views/topic.py

3
tildes/tildes/enums.py

@ -20,6 +20,7 @@ class CommentSortOption(enum.Enum):
VOTES = enum.auto() VOTES = enum.auto()
NEWEST = enum.auto() NEWEST = enum.auto()
POSTED = enum.auto() POSTED = enum.auto()
RELEVANCE = enum.auto()
@property @property
def description(self) -> str: def description(self) -> str:
@ -28,6 +29,8 @@ class CommentSortOption(enum.Enum):
return "newest first" return "newest first"
elif self.name == "POSTED": elif self.name == "POSTED":
return "order posted" return "order posted"
elif self.name == "RELEVANCE":
return "relevance"
return "most {}".format(self.name.lower()) return "most {}".format(self.name.lower())

15
tildes/tildes/models/comment/comment.py

@ -250,3 +250,18 @@ class Comment(DatabaseModel):
def tags_by_user(self, user: User) -> Sequence[str]: def tags_by_user(self, user: User) -> Sequence[str]:
"""Return list of tag names that a user has applied to this comment.""" """Return list of tag names that a user has applied to this comment."""
return [tag.name for tag in self.tags if tag.user_id == user.user_id] return [tag.name for tag in self.tags if tag.user_id == user.user_id]
def is_tag_active(self, tag_name: str) -> bool:
"""Return whether a tag has been applied enough to be considered "active"."""
tag_weight = self.tag_weights[tag_name]
# all tags must have at least 1.0 weight
if tag_weight < 1.0:
return False
# for "noise", weight must be more than 1/5 of the vote count (5 votes
# effectively override 1.0 of tag weight)
if tag_name == "noise" and self.num_votes >= tag_weight * 5:
return False
return True

33
tildes/tildes/models/comment/comment_tree.py

@ -4,7 +4,7 @@
"""Contains the CommentTree and CommentInTree classes.""" """Contains the CommentTree and CommentInTree classes."""
from datetime import datetime from datetime import datetime
from typing import Iterator, List, Optional, Sequence
from typing import Iterator, List, Optional, Sequence, Tuple
from prometheus_client import Histogram from prometheus_client import Histogram
from wrapt import ObjectProxy from wrapt import ObjectProxy
@ -98,6 +98,8 @@ class CommentTree:
tree = sorted(tree, key=lambda c: c.created_time) tree = sorted(tree, key=lambda c: c.created_time)
elif sort == CommentSortOption.VOTES: elif sort == CommentSortOption.VOTES:
tree = sorted(tree, key=lambda c: c.num_votes, reverse=True) tree = sorted(tree, key=lambda c: c.num_votes, reverse=True)
elif sort == CommentSortOption.RELEVANCE:
tree = sorted(tree, key=lambda c: c.relevance_sorting_value, reverse=True)
for comment in tree: for comment in tree:
if not comment.has_visible_descendant: if not comment.has_visible_descendant:
@ -177,11 +179,7 @@ class CommentTree:
if comment.user == self.viewer: if comment.user == self.viewer:
continue continue
# Collapse a comment if it has weight from noise tags of at least
# 1.0 and the vote count is less than 5x the weight (so 5 votes are
# "stronger" than each 1.0 of noise and will prevent collapsing)
noise_weight = comment.tag_weights["noise"]
if noise_weight >= 1.0 and comment.num_votes < noise_weight * 5:
if comment.is_tag_active("noise"):
comment.collapsed_state = "full" comment.collapsed_state = "full"
def uncollapse_new_comments(self, threshold: datetime) -> None: def uncollapse_new_comments(self, threshold: datetime) -> None:
@ -274,3 +272,26 @@ class CommentInTree(ObjectProxy):
for reply in self.replies: for reply in self.replies:
reply.recursively_collapse() reply.recursively_collapse()
@property
def relevance_sorting_value(self) -> Tuple[int, ...]:
"""Value to use for the comment with the "relevance" comment sorting method.
Returns a tuple, which allows sorting the comments into "tiers" and then still
supporting further sorting inside those tiers when it's useful. For example,
comments tagged as offtopic can be sorted below all non-offtopic comments, but
then still sorted by votes relative to other offtopic comments.
"""
if self.is_removed:
return (-100,)
if self.is_tag_active("noise"):
return (-2, self.num_votes)
if self.is_tag_active("offtopic"):
return (-1, self.num_votes)
if self.is_tag_active("joke"):
return (self.num_votes // 2,)
return (self.num_votes,)

2
tildes/tildes/views/topic.py

@ -243,7 +243,7 @@ def get_new_topic_form(request: Request) -> dict:
@view_config(route_name="topic", renderer="topic.jinja2") @view_config(route_name="topic", renderer="topic.jinja2")
@use_kwargs({"comment_order": Enum(CommentSortOption, missing="votes")})
@use_kwargs({"comment_order": Enum(CommentSortOption, missing="relevance")})
def get_topic(request: Request, comment_order: CommentSortOption) -> dict: def get_topic(request: Request, comment_order: CommentSortOption) -> dict:
"""View a single topic.""" """View a single topic."""
topic = request.context topic = request.context

Loading…
Cancel
Save