Browse Source

Rename comment tags to labels

merge-requests/40/head
Deimos 6 years ago
parent
commit
1bf64a04d7
  1. 7
      tildes/alembic/env.py
  2. 38
      tildes/alembic/versions/5cd2db18b722_rename_comment_tags_to_labels.py
  3. 2
      tildes/development.ini
  4. 2
      tildes/production.ini.example
  5. 2
      tildes/scripts/clean_private_data.py
  6. 10
      tildes/scss/_themes.scss
  7. 12
      tildes/scss/_variables.scss
  8. 24
      tildes/scss/modules/_btn.scss
  9. 12
      tildes/scss/modules/_comment.scss
  10. 2
      tildes/scss/modules/_label.scss
  11. 100
      tildes/static/js/behaviors/comment-label-button.js
  12. 100
      tildes/static/js/behaviors/comment-tag-button.js
  13. 6
      tildes/tildes/enums.py
  14. 2
      tildes/tildes/models/comment/__init__.py
  15. 50
      tildes/tildes/models/comment/comment.py
  16. 26
      tildes/tildes/models/comment/comment_label.py
  17. 22
      tildes/tildes/models/comment/comment_tree.py
  18. 6
      tildes/tildes/models/user/user.py
  19. 4
      tildes/tildes/routes.py
  20. 8
      tildes/tildes/schemas/comment.py
  21. 50
      tildes/tildes/templates/macros/comments.jinja2
  22. 4
      tildes/tildes/templates/notifications_unread.jinja2
  23. 4
      tildes/tildes/templates/topic.jinja2
  24. 4
      tildes/tildes/templates/user.jinja2
  25. 51
      tildes/tildes/views/api/web/comment.py
  26. 6
      tildes/tildes/views/notifications.py
  27. 6
      tildes/tildes/views/topic.py
  28. 4
      tildes/tildes/views/user.py

7
tildes/alembic/env.py

@ -12,7 +12,12 @@ config = context.config
fileConfig(config.config_file_name) fileConfig(config.config_file_name)
# import all DatabaseModel subclasses here for autogenerate support # import all DatabaseModel subclasses here for autogenerate support
from tildes.models.comment import Comment, CommentNotification, CommentTag, CommentVote
from tildes.models.comment import (
Comment,
CommentLabel,
CommentNotification,
CommentVote,
)
from tildes.models.group import Group, GroupSubscription from tildes.models.group import Group, GroupSubscription
from tildes.models.log import Log from tildes.models.log import Log
from tildes.models.message import MessageConversation, MessageReply from tildes.models.message import MessageConversation, MessageReply

38
tildes/alembic/versions/5cd2db18b722_rename_comment_tags_to_labels.py

@ -0,0 +1,38 @@
"""Rename comment tags to labels
Revision ID: 5cd2db18b722
Revises: afa3128a9b54
Create Date: 2018-09-25 01:05:55.606680
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = "5cd2db18b722"
down_revision = "afa3128a9b54"
branch_labels = None
depends_on = None
def upgrade():
op.execute("ALTER TYPE commenttagoption RENAME TO commentlabeloption")
op.rename_table("comment_tags", "comment_labels")
op.alter_column("comment_labels", "tag", new_column_name="label")
op.alter_column(
"users", "comment_tag_weight", new_column_name="comment_label_weight"
)
def downgrade():
op.alter_column(
"users", "comment_label_weight", new_column_name="comment_tag_weight"
)
op.alter_column("comment_labels", "label", new_column_name="tag")
op.rename_table("comment_labels", "comment_tags")
op.execute("ALTER TYPE commentlabeloption RENAME TO commenttagoption")

2
tildes/development.ini

@ -36,7 +36,7 @@ redis.sessions.timeout = 600
sqlalchemy.url = postgresql+psycopg2://tildes:@:6432/tildes sqlalchemy.url = postgresql+psycopg2://tildes:@:6432/tildes
tildes.default_user_comment_tag_weight = 1.0
tildes.default_user_comment_label_weight = 1.0
webassets.auto_build = false webassets.auto_build = false
webassets.base_dir = %(here)s/static webassets.base_dir = %(here)s/static

2
tildes/production.ini.example

@ -20,7 +20,7 @@ redis.sessions.timeout = 3600
sqlalchemy.url = postgresql+psycopg2://tildes:@:6432/tildes sqlalchemy.url = postgresql+psycopg2://tildes:@:6432/tildes
tildes.default_user_comment_tag_weight = 1.0
tildes.default_user_comment_label_weight = 1.0
tildes.welcome_message_sender = Deimos tildes.welcome_message_sender = Deimos
webassets.auto_build = false webassets.auto_build = false

2
tildes/scripts/clean_private_data.py

@ -5,7 +5,7 @@
Other things that should probably be added here eventually: Other things that should probably be added here eventually:
- Delete individual votes on comments/topics after voting has been closed - Delete individual votes on comments/topics after voting has been closed
- Delete which users tagged comments after tagging has been closed
- Delete which users labeled comments after labeling has been closed
- Delete old used invite codes (30 days after used?) - Delete old used invite codes (30 days after used?)
""" """

10
tildes/scss/_themes.scss

@ -117,11 +117,11 @@
color: $text-secondary-color; color: $text-secondary-color;
} }
.label-comment-tag-exemplary { @include specialtag($comment-tag-exemplary-color, $is-light); }
.label-comment-tag-joke { @include specialtag($comment-tag-joke-color, $is-light); }
.label-comment-tag-noise { @include specialtag($comment-tag-noise-color, $is-light); }
.label-comment-tag-offtopic { @include specialtag($comment-tag-offtopic-color, $is-light); }
.label-comment-tag-malice { @include specialtag($comment-tag-malice-color, $is-light); }
.label-comment-exemplary { @include specialtag($comment-label-exemplary-color, $is-light); }
.label-comment-joke { @include specialtag($comment-label-joke-color, $is-light); }
.label-comment-noise { @include specialtag($comment-label-noise-color, $is-light); }
.label-comment-offtopic { @include specialtag($comment-label-offtopic-color, $is-light); }
.label-comment-malice { @include specialtag($comment-label-malice-color, $is-light); }
%collapsed-theme { %collapsed-theme {
header { header {

12
tildes/scss/_variables.scss

@ -35,12 +35,12 @@ $fg-lightest: $base1;
$topic-tag-nsfw-color: $red; $topic-tag-nsfw-color: $red;
$topic-tag-spoiler-color: $yellow; $topic-tag-spoiler-color: $yellow;
// Colors for comment tags
$comment-tag-exemplary-color: $green;
$comment-tag-joke-color: $cyan;
$comment-tag-noise-color: $yellow;
$comment-tag-offtopic-color: $blue;
$comment-tag-malice-color: $red;
// Colors for comment labels
$comment-label-exemplary-color: $green;
$comment-label-joke-color: $cyan;
$comment-label-noise-color: $yellow;
$comment-label-offtopic-color: $blue;
$comment-label-malice-color: $red;
$sidebar-width: 300px; $sidebar-width: 300px;

24
tildes/scss/modules/_btn.scss

@ -66,7 +66,7 @@
} }
} }
.btn-comment-tag {
.btn-comment-label {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
margin: 0.4rem; margin: 0.4rem;
@ -87,7 +87,7 @@
} }
} }
@mixin tagbutton($color) {
@mixin labelbutton($color) {
color: $color; color: $color;
border-color: $color; border-color: $color;
@ -101,22 +101,22 @@
} }
} }
.btn-comment-tag-exemplary {
@include tagbutton($comment-tag-exemplary-color);
.btn-comment-label-exemplary {
@include labelbutton($comment-label-exemplary-color);
} }
.btn-comment-tag-joke {
@include tagbutton($comment-tag-joke-color);
.btn-comment-label-joke {
@include labelbutton($comment-label-joke-color);
} }
.btn-comment-tag-noise {
@include tagbutton($comment-tag-noise-color);
.btn-comment-label-noise {
@include labelbutton($comment-label-noise-color);
} }
.btn-comment-tag-offtopic {
@include tagbutton($comment-tag-offtopic-color);
.btn-comment-label-offtopic {
@include labelbutton($comment-label-offtopic-color);
} }
.btn-comment-tag-malice {
@include tagbutton($comment-tag-malice-color);
.btn-comment-label-malice {
@include labelbutton($comment-label-malice-color);
} }

12
tildes/scss/modules/_comment.scss

@ -87,7 +87,7 @@
} }
} }
.comment-tags {
.comment-labels {
margin: 0 0 0 0.4rem; margin: 0 0 0 0.4rem;
list-style-type: none; list-style-type: none;
@ -97,7 +97,7 @@
} }
} }
.comment-tag-buttons {
.comment-label-buttons {
display: flex; display: flex;
margin: 0; margin: 0;
padding: 0 1rem; padding: 0 1rem;
@ -109,7 +109,7 @@
} }
} }
.comment-tag-count {
.comment-label-count {
font-size: 0.6rem; font-size: 0.6rem;
} }
@ -144,11 +144,11 @@
%collapsed { %collapsed {
.comment-edited-time, .comment-edited-time,
.comment-label-buttons,
.comment-labels,
.comment-nav-link, .comment-nav-link,
.comment-posted-time, .comment-posted-time,
.comment-replies, .comment-replies,
.comment-tag-buttons,
.comment-tags,
.comment-text, .comment-text,
.comment-votes, .comment-votes,
.post-buttons { .post-buttons {
@ -211,6 +211,6 @@
.is-comment-exemplary { .is-comment-exemplary {
& > .comment-itself { & > .comment-itself {
margin-left: -2px; margin-left: -2px;
border-left: 3px solid $comment-tag-exemplary-color !important;
border-left: 3px solid $comment-label-exemplary-color !important;
} }
} }

2
tildes/scss/modules/_label.scss

@ -5,7 +5,7 @@
font-size: 0.6rem; font-size: 0.6rem;
} }
.label-comment-tag {
.label-comment {
font-size: 0.5rem; font-size: 0.5rem;
font-weight: bold; font-weight: bold;
color: #fff; color: #fff;

100
tildes/static/js/behaviors/comment-label-button.js

@ -0,0 +1,100 @@
// Copyright (c) 2018 Tildes contributors <code@tildes.net>
// SPDX-License-Identifier: AGPL-3.0-or-later
$.onmount('[data-js-comment-label-button]', function() {
$(this).click(function(event) {
event.preventDefault();
var $comment = $(this).parents('.comment').first();
var userLabels = $comment.attr('data-comment-user-labels');
// check if the label button div already exists and just remove it if so
$labelButtons = $comment.find('.comment-itself:first').find('.comment-label-buttons');
if ($labelButtons.length > 0) {
$labelButtons.remove();
return;
}
var commentID = $comment.attr('data-comment-id36');
var labelURL = '/api/web/comments/' + commentID + '/labels/';
var labeltemplate = document.querySelector('#comment-label-options');
var clone = document.importNode(labeltemplate.content, true);
var options = clone.querySelectorAll('a');
for (i = 0; i < options.length; i++) {
var label = options[i];
var labelName = label.textContent;
var labelOptionActive = false;
if (userLabels.indexOf(labelName) !== -1) {
labelOptionActive = true;
}
var labelPrompt = label.getAttribute("data-js-reason-prompt");
if (labelOptionActive) {
label.className += " btn btn-used";
label.setAttribute('data-ic-delete-from', labelURL + labelName);
// if the label requires a prompt, confirm that they want to remove it
// (since we don't want to accidentally lose the reason they typed in)
if (labelPrompt) {
label.setAttribute("data-ic-confirm", "Remove your "+labelName+" label?");
}
$(label).on('after.success.ic', function(evt) {
Tildes.removeUserLabel(commentID, evt.target.textContent);
});
} else {
label.setAttribute('data-ic-put-to', labelURL + labelName);
if (labelPrompt) {
label.setAttribute("data-ic-prompt", labelPrompt);
label.setAttribute("data-ic-prompt-name", "reason");
}
$(label).on('after.success.ic', function(evt) {
Tildes.addUserLabel(commentID, evt.target.textContent);
});
}
label.setAttribute('data-ic-target', '#comment-' + commentID + ' .comment-itself:first');
}
// update Intercooler so it knows about these new elements
Intercooler.processNodes(clone);
$comment.find(".post-buttons").first().after(clone);
});
});
Tildes.removeUserLabel = function(commentID, labelName) {
$comment = $("#comment-" + commentID);
var userLabels = $comment.attr('data-comment-user-labels').split(" ");
// if the label isn't there, don't need to do anything
labelIndex = userLabels.indexOf(labelName);
if (labelIndex === -1) {
return;
}
// remove the label from the list and update the data attr
userLabels.splice(labelIndex, 1);
$comment.attr('data-comment-user-labels', userLabels.join(" "));
}
Tildes.addUserLabel = function(commentID, labelName) {
$comment = $("#comment-" + commentID);
var userLabels = $comment.attr('data-comment-user-labels').split(" ");
// don't add the label again if it's already there
labelIndex = userLabels.indexOf(labelName);
if (labelIndex !== -1) {
return;
}
// add the label to the list and update the data attr
userLabels.push(labelName);
$comment.attr('data-comment-user-labels', userLabels.join(" "));
}

100
tildes/static/js/behaviors/comment-tag-button.js

@ -1,100 +0,0 @@
// Copyright (c) 2018 Tildes contributors <code@tildes.net>
// SPDX-License-Identifier: AGPL-3.0-or-later
$.onmount('[data-js-comment-tag-button]', function() {
$(this).click(function(event) {
event.preventDefault();
var $comment = $(this).parents('.comment').first();
var user_tags = $comment.attr('data-comment-user-tags');
// check if the tagging button div already exists and just remove it if so
$tagButtons = $comment.find('.comment-itself:first').find('.comment-tag-buttons');
if ($tagButtons.length > 0) {
$tagButtons.remove();
return;
}
var commentID = $comment.attr('data-comment-id36');
var tagURL = '/api/web/comments/' + commentID + '/tags/';
var tagtemplate = document.querySelector('#comment-tag-options');
var clone = document.importNode(tagtemplate.content, true);
var options = clone.querySelectorAll('a');
for (i = 0; i < options.length; i++) {
var tag = options[i];
var tagName = tag.textContent;
var tagOptionActive = false;
if (user_tags.indexOf(tagName) !== -1) {
tagOptionActive = true;
}
var tagPrompt = tag.getAttribute("data-js-reason-prompt");
if (tagOptionActive) {
tag.className += " btn btn-used";
tag.setAttribute('data-ic-delete-from', tagURL + tagName);
// if the tag requires a prompt, confirm that they want to remove it
// (since we don't want to accidentally lose the reason they typed in)
if (tagPrompt) {
tag.setAttribute("data-ic-confirm", "Remove your "+tagName+" tag?");
}
$(tag).on('after.success.ic', function(evt) {
Tildes.removeUserTag(commentID, evt.target.textContent);
});
} else {
tag.setAttribute('data-ic-put-to', tagURL + tagName);
if (tagPrompt) {
tag.setAttribute("data-ic-prompt", tagPrompt);
tag.setAttribute("data-ic-prompt-name", "reason");
}
$(tag).on('after.success.ic', function(evt) {
Tildes.addUserTag(commentID, evt.target.textContent);
});
}
tag.setAttribute('data-ic-target', '#comment-' + commentID + ' .comment-itself:first');
}
// update Intercooler so it knows about these new elements
Intercooler.processNodes(clone);
$comment.find(".post-buttons").first().after(clone);
});
});
Tildes.removeUserTag = function(commentID, tagName) {
$comment = $("#comment-" + commentID);
var user_tags = $comment.attr('data-comment-user-tags').split(" ");
// if the tag isn't there, don't need to do anything
tagIndex = user_tags.indexOf(tagName);
if (tagIndex === -1) {
return;
}
// remove the tag from the list and update the data attr
user_tags.splice(tagIndex, 1);
$comment.attr('data-comment-user-tags', user_tags.join(" "));
}
Tildes.addUserTag = function(commentID, tagName) {
$comment = $("#comment-" + commentID);
var user_tags = $comment.attr('data-comment-user-tags').split(" ");
// don't add the tag again if it's already there
tagIndex = user_tags.indexOf(tagName);
if (tagIndex !== -1) {
return;
}
// add the tag to the list and update the data attr
user_tags.push(tagName);
$comment.attr('data-comment-user-tags', user_tags.join(" "));
}

6
tildes/tildes/enums.py

@ -37,8 +37,8 @@ class CommentSortOption(enum.Enum):
return "most {}".format(self.name.lower()) return "most {}".format(self.name.lower())
class CommentTagOption(enum.Enum):
"""Enum for the (site-wide) comment tag options."""
class CommentLabelOption(enum.Enum):
"""Enum for the (site-wide) comment label options."""
EXEMPLARY = enum.auto() EXEMPLARY = enum.auto()
JOKE = enum.auto() JOKE = enum.auto()
@ -48,7 +48,7 @@ class CommentTagOption(enum.Enum):
@property @property
def reason_prompt(self) -> Optional[str]: def reason_prompt(self) -> Optional[str]:
"""Return the reason prompt for this tag, if any."""
"""Return the reason prompt for this label, if any."""
if self.name == "EXEMPLARY": if self.name == "EXEMPLARY":
return ( return (
"What makes this comment exemplary? " "What makes this comment exemplary? "

2
tildes/tildes/models/comment/__init__.py

@ -4,6 +4,6 @@ from .comment import Comment, EDIT_GRACE_PERIOD
from .comment_notification import CommentNotification from .comment_notification import CommentNotification
from .comment_notification_query import CommentNotificationQuery from .comment_notification_query import CommentNotificationQuery
from .comment_query import CommentQuery from .comment_query import CommentQuery
from .comment_tag import CommentTag
from .comment_label import CommentLabel
from .comment_tree import CommentTree from .comment_tree import CommentTree
from .comment_vote import CommentVote from .comment_vote import CommentVote

50
tildes/tildes/models/comment/comment.py

@ -157,7 +157,7 @@ class Comment(DatabaseModel):
acl.append((Allow, Everyone, "view")) acl.append((Allow, Everyone, "view"))
# view exemplary reasons: # view exemplary reasons:
# - only author gets shown the reasons (admins can see as well with all tags)
# - only author gets shown the reasons (admins can see as well with all labels)
acl.append((Allow, self.user_id, "view_exemplary_reasons")) acl.append((Allow, self.user_id, "view_exemplary_reasons"))
# vote: # vote:
@ -169,14 +169,14 @@ class Comment(DatabaseModel):
acl.append((Deny, self.user_id, "vote")) acl.append((Deny, self.user_id, "vote"))
acl.append((Allow, Authenticated, "vote")) acl.append((Allow, Authenticated, "vote"))
# tag:
# - removed comments can't be tagged by anyone
# - otherwise, people with the "comment.tag" permission other than the author
# label:
# - removed comments can't be labeled by anyone
# - otherwise, people with the "comment.label" permission other than the author
if self.is_removed: if self.is_removed:
acl.append((Deny, Everyone, "tag"))
acl.append((Deny, Everyone, "label"))
acl.append((Deny, self.user_id, "tag"))
acl.append((Allow, "comment.tag", "tag"))
acl.append((Deny, self.user_id, "label"))
acl.append((Allow, "comment.label", "label"))
# reply: # reply:
# - removed comments can't be replied to by anyone # - removed comments can't be replied to by anyone
@ -205,7 +205,7 @@ class Comment(DatabaseModel):
# tools that require specifically granted permissions # tools that require specifically granted permissions
acl.append((Allow, "admin", "remove")) acl.append((Allow, "admin", "remove"))
acl.append((Allow, "admin", "view_tags"))
acl.append((Allow, "admin", "view_labels"))
acl.append(DENY_ALL) acl.append(DENY_ALL)
@ -238,34 +238,34 @@ class Comment(DatabaseModel):
return f"{self.topic.permalink}#comment-{self.parent_comment_id36}" return f"{self.topic.permalink}#comment-{self.parent_comment_id36}"
@property @property
def tag_counts(self) -> Counter:
"""Counter for number of times each tag is on this comment."""
return Counter([tag.name for tag in self.tags])
def label_counts(self) -> Counter:
"""Counter for number of times each label is on this comment."""
return Counter([label.name for label in self.labels])
@property @property
def tag_weights(self) -> Counter:
"""Counter with cumulative weights of each tag on this comment."""
def label_weights(self) -> Counter:
"""Counter with cumulative weights of each label on this comment."""
weights: Counter = Counter() weights: Counter = Counter()
for tag in self.tags:
weights[tag.name] += tag.weight
for label in self.labels:
weights[label.name] += label.weight
return weights return weights
def tags_by_user(self, user: User) -> Sequence[str]:
"""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]
def labels_by_user(self, user: User) -> Sequence[str]:
"""Return list of label names that a user has applied to this comment."""
return [label.name for label in self.labels if label.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]
def is_label_active(self, label_name: str) -> bool:
"""Return whether a label has been applied enough to be considered "active"."""
label_weight = self.label_weights[label_name]
# all tags must have at least 1.0 weight
if tag_weight < 1.0:
# all labels must have at least 1.0 weight
if label_weight < 1.0:
return False return False
# for "noise", weight must be more than 1/5 of the vote count (5 votes # 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:
# effectively override 1.0 of label weight)
if label_name == "noise" and self.num_votes >= label_weight * 5:
return False return False
return True return True

26
tildes/tildes/models/comment/comment_tag.py → tildes/tildes/models/comment/comment_label.py

@ -1,7 +1,7 @@
# Copyright (c) 2018 Tildes contributors <code@tildes.net> # Copyright (c) 2018 Tildes contributors <code@tildes.net>
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
"""Contains the CommentTag class."""
"""Contains the CommentLabel class."""
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional
@ -11,22 +11,22 @@ from sqlalchemy.dialects.postgresql import ENUM
from sqlalchemy.orm import backref, relationship from sqlalchemy.orm import backref, relationship
from sqlalchemy.sql.expression import text from sqlalchemy.sql.expression import text
from tildes.enums import CommentTagOption
from tildes.enums import CommentLabelOption
from tildes.models import DatabaseModel from tildes.models import DatabaseModel
from tildes.models.user import User from tildes.models.user import User
from .comment import Comment from .comment import Comment
class CommentTag(DatabaseModel):
"""Model for the tags attached to comments by users."""
class CommentLabel(DatabaseModel):
"""Model for the labels attached to comments by users."""
__tablename__ = "comment_tags"
__tablename__ = "comment_labels"
comment_id: int = Column( comment_id: int = Column(
Integer, ForeignKey("comments.comment_id"), nullable=False, primary_key=True Integer, ForeignKey("comments.comment_id"), nullable=False, primary_key=True
) )
tag: CommentTagOption = Column(
ENUM(CommentTagOption), nullable=False, primary_key=True
label: CommentLabelOption = Column(
ENUM(CommentLabelOption), nullable=False, primary_key=True
) )
user_id: int = Column( user_id: int = Column(
Integer, ForeignKey("users.user_id"), nullable=False, primary_key=True Integer, ForeignKey("users.user_id"), nullable=False, primary_key=True
@ -37,26 +37,26 @@ class CommentTag(DatabaseModel):
weight: float = Column(REAL, nullable=False, server_default=text("1.0")) weight: float = Column(REAL, nullable=False, server_default=text("1.0"))
reason: Optional[str] = Column(Text) reason: Optional[str] = Column(Text)
comment: Comment = relationship(Comment, backref=backref("tags", lazy=False))
comment: Comment = relationship(Comment, backref=backref("labels", lazy=False))
user: User = relationship(User, lazy=False, innerjoin=True) user: User = relationship(User, lazy=False, innerjoin=True)
def __init__( def __init__(
self, self,
comment: Comment, comment: Comment,
user: User, user: User,
tag: CommentTagOption,
label: CommentLabelOption,
weight: float, weight: float,
reason: Optional[str] = None, reason: Optional[str] = None,
) -> None: ) -> None:
"""Add a new tag to a comment."""
"""Add a new label to a comment."""
# pylint: disable=too-many-arguments # pylint: disable=too-many-arguments
self.comment_id = comment.comment_id self.comment_id = comment.comment_id
self.user_id = user.user_id self.user_id = user.user_id
self.tag = tag
self.label = label
self.weight = weight self.weight = weight
self.reason = reason self.reason = reason
@property @property
def name(self) -> str: def name(self) -> str:
"""Return the name of the tag (to avoid needing to do .tag.name)."""
return self.tag.name.lower()
"""Return the name of the label (to avoid needing to do .label.name)."""
return self.label.name.lower()

22
tildes/tildes/models/comment/comment_tree.py

@ -172,14 +172,14 @@ class CommentTree:
order=self.sort.name, order=self.sort.name,
) )
def collapse_from_tags(self) -> None:
"""Collapse comments based on how they've been tagged."""
def collapse_from_labels(self) -> None:
"""Collapse comments based on how they've been labeled."""
for comment in self.comments: for comment in self.comments:
# never affect the viewer's own comments # never affect the viewer's own comments
if comment.user == self.viewer: if comment.user == self.viewer:
continue continue
if comment.is_tag_active("noise"):
if comment.is_label_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:
@ -276,25 +276,25 @@ class CommentInTree(ObjectProxy):
Returns a tuple, which allows sorting the comments into "tiers" and then still 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, 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
comments labeled as offtopic can be sorted below all non-offtopic comments, but
then still sorted by votes relative to other offtopic comments. then still sorted by votes relative to other offtopic comments.
""" """
if self.is_removed: if self.is_removed:
return (-100,) return (-100,)
if self.is_tag_active("noise"):
if self.is_label_active("noise"):
return (-2, self.num_votes) return (-2, self.num_votes)
if self.is_tag_active("offtopic"):
if self.is_label_active("offtopic"):
return (-1, self.num_votes) return (-1, self.num_votes)
if self.is_tag_active("joke"):
if self.is_label_active("joke"):
return (self.num_votes // 2,) return (self.num_votes // 2,)
# Exemplary comments add 1.0 to the the total weight of the exemplary tags, and
# multiply the vote count by that. At minimum (weight 1.0), votes are doubled.
if self.is_tag_active("exemplary"):
multiplier = self.tag_weights["exemplary"] + 1.0
# Exemplary comments add 1.0 to the the total weight of the exemplary labels,
# and multiply the vote count by that. Minimum (weight 1.0), votes are doubled.
if self.is_label_active("exemplary"):
multiplier = self.label_weights["exemplary"] + 1.0
return (round(multiplier * self.num_votes),) return (round(multiplier * self.num_votes),)
return (self.num_votes,) return (self.num_votes,)

6
tildes/tildes/models/user/user.py

@ -101,7 +101,7 @@ class User(DatabaseModel):
_filtered_topic_tags: List[Ltree] = Column( _filtered_topic_tags: List[Ltree] = Column(
"filtered_topic_tags", ArrayOfLtree, nullable=False, server_default="{}" "filtered_topic_tags", ArrayOfLtree, nullable=False, server_default="{}"
) )
comment_tag_weight: Optional[float] = Column(REAL)
comment_label_weight: Optional[float] = Column(REAL)
@hybrid_property @hybrid_property
def filtered_topic_tags(self) -> List[str]: def filtered_topic_tags(self) -> List[str]:
@ -228,9 +228,9 @@ class User(DatabaseModel):
else: else:
raise ValueError("Unknown permissions format") raise ValueError("Unknown permissions format")
# give the user the "comment.tag" permission if they're over a week old
# give the user the "comment.label" permission if they're over a week old
if utc_now() - self.created_time > timedelta(days=7): if utc_now() - self.created_time > timedelta(days=7):
principals.append("comment.tag")
principals.append("comment.label")
return principals return principals

4
tildes/tildes/routes.py

@ -134,7 +134,9 @@ def add_intercooler_routes(config: Configurator) -> None:
"comment_vote", "/comments/{comment_id36}/vote", factory=comment_by_id36 "comment_vote", "/comments/{comment_id36}/vote", factory=comment_by_id36
) )
add_ic_route( add_ic_route(
"comment_tag", "/comments/{comment_id36}/tags/{name}", factory=comment_by_id36
"comment_label",
"/comments/{comment_id36}/labels/{name}",
factory=comment_by_id36,
) )
add_ic_route( add_ic_route(
"comment_mark_read", "comment_mark_read",

8
tildes/tildes/schemas/comment.py

@ -5,7 +5,7 @@
from marshmallow import Schema from marshmallow import Schema
from tildes.enums import CommentTagOption
from tildes.enums import CommentLabelOption
from tildes.schemas.fields import Enum, ID36, Markdown, SimpleString from tildes.schemas.fields import Enum, ID36, Markdown, SimpleString
@ -21,10 +21,10 @@ class CommentSchema(Schema):
strict = True strict = True
class CommentTagSchema(Schema):
"""Marshmallow schema for comment tags."""
class CommentLabelSchema(Schema):
"""Marshmallow schema for comment labels."""
name = Enum(CommentTagOption)
name = Enum(CommentLabelOption)
reason = SimpleString(missing=None) reason = SimpleString(missing=None)
class Meta: class Meta:

50
tildes/tildes/templates/macros/comments.jinja2

@ -19,8 +19,8 @@
data-comment-depth="{{ loop.depth0 }}" data-comment-depth="{{ loop.depth0 }}"
{% endif %} {% endif %}
{% if request.has_permission("tag", comment) %}
data-comment-user-tags="{{ comment.tags_by_user(request.user)|join(' ') }}"
{% if request.has_permission("label", comment) %}
data-comment-user-labels="{{ comment.labels_by_user(request.user)|join(' ') }}"
{% endif %} {% endif %}
> >
{{ render_comment_contents(comment, is_individual_comment) }} {{ render_comment_contents(comment, is_individual_comment) }}
@ -97,28 +97,28 @@
{% endif %} {% endif %}
{% if (request.has_permission("view_exemplary_reasons", comment) {% if (request.has_permission("view_exemplary_reasons", comment)
and comment.is_tag_active("exemplary")) %}
and comment.is_label_active("exemplary")) %}
<details class="comment-exemplary-reasons"> <details class="comment-exemplary-reasons">
<summary><span class="label label-comment-tag label-comment-tag-exemplary">Exemplary</span>
<span class="comment-tag-count">x{{ comment.tag_counts["exemplary"] }}</span>
<summary><span class="label label-comment label-comment-exemplary">Exemplary</span>
<span class="comment-label-count">x{{ comment.label_counts["exemplary"] }}</span>
</summary> </summary>
<ul> <ul>
{% for tag in comment.tags if tag.name == "exemplary" %}
<li>"{{ tag.reason }}"</li>
{% for label in comment.labels if label.name == "exemplary" %}
<li>"{{ label.reason }}"</li>
{% endfor %} {% endfor %}
</ul> </ul>
</details> </details>
{% endif %} {% endif %}
{% if comment.tag_counts and request.has_permission("view_tags", comment) %}
<ul class="comment-tags">
{% for tag_name, weight in comment.tag_weights.most_common() %}
{% if comment.label_counts and request.has_permission("view_labels", comment) %}
<ul class="comment-labels">
{% for label_name, weight in comment.label_weights.most_common() %}
<li> <li>
<span class="label label-comment-tag label-comment-tag-{{ tag_name|lower }}">{{ tag_name }}</span>
<span class="comment-tag-count">
<span class="label label-comment label-comment-{{ label_name|lower }}">{{ label_name }}</span>
<span class="comment-label-count">
{{ weight }}: {{ weight }}:
{% for tag in comment.tags if tag.name == tag_name %}
{{ username_linked(tag.user.username) }} ({{ tag.weight }}{{ ' "%s"' % tag.reason if tag.reason else '' }})
{% for label in comment.labels if label.name == label_name %}
{{ username_linked(label.user.username) }} ({{ label.weight }}{{ ' "%s"' % label.reason if label.reason else '' }})
{% endfor %} {% endfor %}
</span> </span>
</li> </li>
@ -165,8 +165,8 @@
</a></li> </a></li>
{% endif %} {% endif %}
{% if request.has_permission('tag', comment) %}
<li><a class="post-button" name="tag" data-js-comment-tag-button>Tag</a></li>
{% if request.has_permission('label', comment) %}
<li><a class="post-button" name="label" data-js-comment-label-button>Label</a></li>
{% endif %} {% endif %}
{% if request.has_permission('edit', comment) %} {% if request.has_permission('edit', comment) %}
@ -237,7 +237,7 @@
{% do classes.append('is-comment-by-op') %} {% do classes.append('is-comment-by-op') %}
{% endif %} {% endif %}
{% if request.has_permission('view', comment) and comment.is_tag_active("exemplary") %}
{% if request.has_permission('view', comment) and comment.is_label_active("exemplary") %}
{% do classes.append("is-comment-exemplary") %} {% do classes.append("is-comment-exemplary") %}
{% endif %} {% endif %}
@ -251,16 +251,16 @@
{{ classes|join(' ') }} {{ classes|join(' ') }}
{% endmacro %} {% endmacro %}
{% macro comment_tag_options_template(options) %}
<template id="comment-tag-options">
<menu class="comment-tag-buttons">
{% for tag in options %}
{% macro comment_label_options_template(options) %}
<template id="comment-label-options">
<menu class="comment-label-buttons">
{% for label in options %}
<li> <li>
<a class="btn-comment-tag btn-comment-tag-{{ tag.name|lower }}"
{% if tag.reason_prompt %}
data-js-reason-prompt="{{ tag.reason_prompt }}"
<a class="btn-comment-label btn-comment-label-{{ label.name|lower }}"
{% if label.reason_prompt %}
data-js-reason-prompt="{{ label.reason_prompt }}"
{% endif %} {% endif %}
>{{ tag.name|lower }}</a>
>{{ label.name|lower }}</a>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>

4
tildes/tildes/templates/notifications_unread.jinja2

@ -3,7 +3,7 @@
{% extends 'base_user_menu.jinja2' %} {% extends 'base_user_menu.jinja2' %}
{% from 'macros/comments.jinja2' import comment_tag_options_template, render_single_comment with context %}
{% from 'macros/comments.jinja2' import comment_label_options_template, render_single_comment with context %}
{% from 'macros/links.jinja2' import group_linked, username_linked %} {% from 'macros/links.jinja2' import group_linked, username_linked %}
{% block title %}Unread notifications{% endblock %} {% block title %}Unread notifications{% endblock %}
@ -57,5 +57,5 @@
<p><a href="/notifications">Go to previously read notifications</a></p> <p><a href="/notifications">Go to previously read notifications</a></p>
{% endif %} {% endif %}
{{ comment_tag_options_template(comment_tag_options) }}
{{ comment_label_options_template(comment_label_options) }}
{% endblock %} {% endblock %}

4
tildes/tildes/templates/topic.jinja2

@ -3,7 +3,7 @@
{% extends 'base.jinja2' %} {% extends 'base.jinja2' %}
{% from 'macros/comments.jinja2' import comment_tag_options_template, render_comment_tree with context %}
{% from 'macros/comments.jinja2' import comment_label_options_template, render_comment_tree with context %}
{% from 'macros/datetime.jinja2' import time_ago, time_ago_abbreviated, time_ago_responsive %} {% from 'macros/datetime.jinja2' import time_ago, time_ago_abbreviated, time_ago_responsive %}
{% from 'macros/forms.jinja2' import markdown_textarea %} {% from 'macros/forms.jinja2' import markdown_textarea %}
{% from 'macros/links.jinja2' import group_linked, username_linked %} {% from 'macros/links.jinja2' import group_linked, username_linked %}
@ -236,7 +236,7 @@
</article> </article>
{{ comment_tag_options_template(comment_tag_options) }}
{{ comment_label_options_template(comment_label_options) }}
{% endblock content %} {% endblock content %}
{% block sidebar %} {% block sidebar %}

4
tildes/tildes/templates/user.jinja2

@ -3,7 +3,7 @@
{% extends 'base_user_menu.jinja2' %} {% extends 'base_user_menu.jinja2' %}
{% from 'macros/comments.jinja2' import comment_tag_options_template, render_single_comment with context %}
{% from 'macros/comments.jinja2' import comment_label_options_template, render_single_comment with context %}
{% from 'macros/links.jinja2' import group_linked, username_linked %} {% from 'macros/links.jinja2' import group_linked, username_linked %}
{% from 'macros/topics.jinja2' import render_topic_for_listing with context %} {% from 'macros/topics.jinja2' import render_topic_for_listing with context %}
@ -72,7 +72,7 @@
</div> </div>
{% endif %} {% endif %}
{{ comment_tag_options_template(comment_tag_options) }}
{{ comment_label_options_template(comment_label_options) }}
{% endblock %} {% endblock %}

51
tildes/tildes/views/api/web/comment.py

@ -13,12 +13,17 @@ from sqlalchemy.orm.exc import FlushError
from webargs.pyramidparser import use_kwargs from webargs.pyramidparser import use_kwargs
from zope.sqlalchemy import mark_changed from zope.sqlalchemy import mark_changed
from tildes.enums import CommentNotificationType, CommentTagOption, LogEventType
from tildes.enums import CommentNotificationType, CommentLabelOption, LogEventType
from tildes.lib.datetime import utc_now from tildes.lib.datetime import utc_now
from tildes.models.comment import Comment, CommentNotification, CommentTag, CommentVote
from tildes.models.comment import (
Comment,
CommentLabel,
CommentNotification,
CommentVote,
)
from tildes.models.log import LogComment from tildes.models.log import LogComment
from tildes.models.topic import TopicVisit from tildes.models.topic import TopicVisit
from tildes.schemas.comment import CommentSchema, CommentTagSchema
from tildes.schemas.comment import CommentSchema, CommentLabelSchema
from tildes.views import IC_NOOP from tildes.views import IC_NOOP
from tildes.views.decorators import ic_view_config, rate_limit_view from tildes.views.decorators import ic_view_config, rate_limit_view
@ -247,25 +252,27 @@ def delete_vote_comment(request: Request) -> dict:
@ic_view_config( @ic_view_config(
route_name="comment_tag",
route_name="comment_label",
request_method="PUT", request_method="PUT",
permission="tag",
permission="label",
renderer="comment_contents.jinja2", renderer="comment_contents.jinja2",
) )
@use_kwargs(CommentTagSchema(only=("name",)), locations=("matchdict",))
@use_kwargs(CommentTagSchema(only=("reason",)))
def put_tag_comment(request: Request, name: CommentTagOption, reason: str) -> Response:
"""Add a tag to a comment."""
@use_kwargs(CommentLabelSchema(only=("name",)), locations=("matchdict",))
@use_kwargs(CommentLabelSchema(only=("reason",)))
def put_label_comment(
request: Request, name: CommentLabelOption, reason: str
) -> Response:
"""Add a label to a comment."""
comment = request.context comment = request.context
savepoint = request.tm.savepoint() savepoint = request.tm.savepoint()
weight = request.user.comment_tag_weight
weight = request.user.comment_label_weight
if weight is None: if weight is None:
weight = request.registry.settings["tildes.default_user_comment_tag_weight"]
weight = request.registry.settings["tildes.default_user_comment_label_weight"]
tag = CommentTag(comment, request.user, name, weight, reason)
request.db_session.add(tag)
label = CommentLabel(comment, request.user, name, weight, reason)
request.db_session.add(label)
try: try:
# manually flush before attempting to commit, to avoid having all objects # manually flush before attempting to commit, to avoid having all objects
@ -287,20 +294,20 @@ def put_tag_comment(request: Request, name: CommentTagOption, reason: str) -> Re
@ic_view_config( @ic_view_config(
route_name="comment_tag",
route_name="comment_label",
request_method="DELETE", request_method="DELETE",
permission="tag",
permission="label",
renderer="comment_contents.jinja2", renderer="comment_contents.jinja2",
) )
@use_kwargs(CommentTagSchema(only=("name",)), locations=("matchdict",))
def delete_tag_comment(request: Request, name: CommentTagOption) -> Response:
"""Remove a tag (that the user previously added) from a comment."""
@use_kwargs(CommentLabelSchema(only=("name",)), locations=("matchdict",))
def delete_label_comment(request: Request, name: CommentLabelOption) -> Response:
"""Remove a label (that the user previously added) from a comment."""
comment = request.context comment = request.context
request.query(CommentTag).filter(
CommentTag.comment_id == comment.comment_id,
CommentTag.user_id == request.user.user_id,
CommentTag.tag == name,
request.query(CommentLabel).filter(
CommentLabel.comment_id == comment.comment_id,
CommentLabel.user_id == request.user.user_id,
CommentLabel.label == name,
).delete(synchronize_session=False) ).delete(synchronize_session=False)
# commit and then re-query the comment to get complete data # commit and then re-query the comment to get complete data

6
tildes/tildes/views/notifications.py

@ -7,7 +7,7 @@ from pyramid.request import Request
from pyramid.view import view_config from pyramid.view import view_config
from sqlalchemy.sql.expression import desc from sqlalchemy.sql.expression import desc
from tildes.enums import CommentTagOption
from tildes.enums import CommentLabelOption
from tildes.models.comment import CommentNotification from tildes.models.comment import CommentNotification
@ -31,7 +31,7 @@ def get_user_unread_notifications(request: Request) -> dict:
for notification in notifications: for notification in notifications:
notification.is_unread = False notification.is_unread = False
return {"notifications": notifications, "comment_tag_options": CommentTagOption}
return {"notifications": notifications, "comment_label_options": CommentLabelOption}
@view_config(route_name="notifications", renderer="notifications.jinja2") @view_config(route_name="notifications", renderer="notifications.jinja2")
@ -49,4 +49,4 @@ def get_user_notifications(request: Request) -> dict:
.all() .all()
) )
return {"notifications": notifications, "comment_tag_options": CommentTagOption}
return {"notifications": notifications, "comment_label_options": CommentLabelOption}

6
tildes/tildes/views/topic.py

@ -18,9 +18,9 @@ from webargs.pyramidparser import use_kwargs
from zope.sqlalchemy import mark_changed from zope.sqlalchemy import mark_changed
from tildes.enums import ( from tildes.enums import (
CommentLabelOption,
CommentNotificationType, CommentNotificationType,
CommentSortOption, CommentSortOption,
CommentTagOption,
LogEventType, LogEventType,
TopicSortOption, TopicSortOption,
) )
@ -277,7 +277,7 @@ def get_topic(request: Request, comment_order: CommentSortOption) -> dict:
.all() .all()
) )
tree.collapse_from_tags()
tree.collapse_from_labels()
# if the user has the "mark new comments" feature enabled # if the user has the "mark new comments" feature enabled
if request.user and request.user.track_comment_visits: if request.user and request.user.track_comment_visits:
@ -298,7 +298,7 @@ def get_topic(request: Request, comment_order: CommentSortOption) -> dict:
"comments": tree, "comments": tree,
"comment_order": comment_order, "comment_order": comment_order,
"comment_order_options": CommentSortOption, "comment_order_options": CommentSortOption,
"comment_tag_options": CommentTagOption,
"comment_label_options": CommentLabelOption,
} }

4
tildes/tildes/views/user.py

@ -12,7 +12,7 @@ from pyramid.view import view_config
from sqlalchemy.sql.expression import desc from sqlalchemy.sql.expression import desc
from webargs.pyramidparser import use_kwargs from webargs.pyramidparser import use_kwargs
from tildes.enums import CommentTagOption
from tildes.enums import CommentLabelOption
from tildes.models.comment import Comment from tildes.models.comment import Comment
from tildes.models.topic import Topic from tildes.models.topic import Topic
from tildes.models.user import User, UserInviteCode from tildes.models.user import User, UserInviteCode
@ -109,7 +109,7 @@ def get_user(
"user": user, "user": user,
"posts": posts, "posts": posts,
"post_type": post_type, "post_type": post_type,
"comment_tag_options": CommentTagOption,
"comment_label_options": CommentLabelOption,
} }

Loading…
Cancel
Save