Browse Source

Limit Exemplary comment label to one/day/user

This is a bit of a hacky way of doing it in a few aspects and definitely
still needs some work, but it's a reasonable start. Specifically, a
major miss is that there's no way to remove an Exemplary label unless
you currently have one available. This should definitely be fixed, but
I'm not sure what might be the best approach yet.
merge-requests/40/head
Deimos 6 years ago
parent
commit
7cf785e51c
  1. 56
      tildes/alembic/versions/8e54f422541c_user_track_last_usage_of_exemplary_label.py
  2. 3
      tildes/scss/modules/_btn.scss
  3. 7
      tildes/scss/modules/_comment.scss
  4. 19
      tildes/sql/init/triggers/comment_labels/users.sql
  5. 12
      tildes/static/js/behaviors/comment-label-button.js
  6. 8
      tildes/tildes/models/comment/comment_label.py
  7. 15
      tildes/tildes/models/user/user.py
  8. 5
      tildes/tildes/templates/macros/comments.jinja2
  9. 4
      tildes/tildes/views/api/web/comment.py

56
tildes/alembic/versions/8e54f422541c_user_track_last_usage_of_exemplary_label.py

@ -0,0 +1,56 @@
"""User: track last usage of exemplary label
Revision ID: 8e54f422541c
Revises: 5cd2db18b722
Create Date: 2018-09-26 00:22:02.728425
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = "8e54f422541c"
down_revision = "5cd2db18b722"
branch_labels = None
depends_on = None
def upgrade():
op.add_column(
"users",
sa.Column(
"last_exemplary_label_time", sa.TIMESTAMP(timezone=True), nullable=True
),
)
op.execute(
"""
CREATE OR REPLACE FUNCTION update_user_last_exemplary_label_time() RETURNS TRIGGER AS $$
BEGIN
UPDATE users
SET last_exemplary_label_time = NOW()
WHERE user_id = NEW.user_id;
RETURN NULL;
END
$$ LANGUAGE plpgsql;
"""
)
op.execute(
"""
CREATE TRIGGER update_user_last_exemplary_label_time
AFTER INSERT ON comment_labels
FOR EACH ROW
WHEN (NEW.label = 'EXEMPLARY')
EXECUTE PROCEDURE update_user_last_exemplary_label_time();
"""
)
def downgrade():
op.execute("DROP TRIGGER update_user_last_exemplary_label_time ON comment_labels")
op.execute("DROP FUNCTION update_user_last_exemplary_label_time()")
op.drop_column("users", "last_exemplary_label_time")

3
tildes/scss/modules/_btn.scss

@ -69,7 +69,8 @@
.btn-comment-label { .btn-comment-label {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
margin: 0.4rem;
margin: 0 0.4rem;
padding: 0.2rem;
font-size: 0.6rem; font-size: 0.6rem;
font-weight: bold; font-weight: bold;

7
tildes/scss/modules/_comment.scss

@ -102,7 +102,6 @@
margin: 0; margin: 0;
padding: 0 1rem; padding: 0 1rem;
justify-content: space-between; justify-content: space-between;
align-items: center;
@media (min-width: $size-md) { @media (min-width: $size-md) {
justify-content: left; justify-content: left;
@ -113,6 +112,12 @@
font-size: 0.6rem; font-size: 0.6rem;
} }
.comment-label-note {
font-size: 0.5rem;
font-style: italic;
text-align: center;
}
.comment-text { .comment-text {
padding: 0.2rem; padding: 0.2rem;
padding-left: 0.4rem; padding-left: 0.4rem;

19
tildes/sql/init/triggers/comment_labels/users.sql

@ -0,0 +1,19 @@
-- Copyright (c) 2018 Tildes contributors <code@tildes.net>
-- SPDX-License-Identifier: AGPL-3.0-or-later
CREATE OR REPLACE FUNCTION update_user_last_exemplary_label_time() RETURNS TRIGGER AS $$
BEGIN
UPDATE users
SET last_exemplary_label_time = NOW()
WHERE user_id = NEW.user_id;
RETURN NULL;
END
$$ LANGUAGE plpgsql;
CREATE TRIGGER update_user_last_exemplary_label_time
AFTER INSERT ON comment_labels
FOR EACH ROW
WHEN (NEW.label = 'EXEMPLARY')
EXECUTE PROCEDURE update_user_last_exemplary_label_time();

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

@ -55,7 +55,17 @@ $.onmount('[data-js-comment-label-button]', function() {
} }
$(label).on('after.success.ic', function(evt) { $(label).on('after.success.ic', function(evt) {
Tildes.addUserLabel(commentID, evt.target.textContent);
var labelName = evt.target.textContent;
Tildes.addUserLabel(commentID, labelName);
// if the applied label was Exemplary, remove the button from the
// template since they can't use it again anyway
if (labelName === "exemplary") {
var exemplaryButton = labeltemplate.content.querySelector('.btn-comment-label-exemplary');
if (exemplaryButton) {
exemplaryButton.parentElement.remove();
}
}
}); });
} }

8
tildes/tildes/models/comment/comment_label.py

@ -18,7 +18,13 @@ from .comment import Comment
class CommentLabel(DatabaseModel): class CommentLabel(DatabaseModel):
"""Model for the labels attached to comments by users."""
"""Model for the labels attached to comments by users.
Trigger behavior:
Outgoing:
- Inserting a row for an exemplary label will set last_exemplary_label_time for
the relevant user.
"""
__tablename__ = "comment_labels" __tablename__ = "comment_labels"

15
tildes/tildes/models/user/user.py

@ -32,7 +32,7 @@ from sqlalchemy.orm import deferred
from sqlalchemy.sql.expression import text from sqlalchemy.sql.expression import text
from sqlalchemy_utils import Ltree from sqlalchemy_utils import Ltree
from tildes.enums import TopicSortOption
from tildes.enums import CommentLabelOption, TopicSortOption
from tildes.lib.database import ArrayOfLtree, CIText from tildes.lib.database import ArrayOfLtree, CIText
from tildes.lib.datetime import utc_now from tildes.lib.datetime import utc_now
from tildes.lib.hash import hash_string, is_match_for_hash from tildes.lib.hash import hash_string, is_match_for_hash
@ -49,6 +49,8 @@ class User(DatabaseModel):
deletions, and updates to is_unread in comment_notifications. deletions, and updates to is_unread in comment_notifications.
- num_unread_messages will be incremented and decremented by insertions, - num_unread_messages will be incremented and decremented by insertions,
deletions, and updates to unread_user_ids in message_conversations. deletions, and updates to unread_user_ids in message_conversations.
- last_exemplary_label_time will be set when a row for an exemplary label is
inserted into comment_labels.
""" """
schema_class = UserSchema schema_class = UserSchema
@ -102,6 +104,7 @@ class User(DatabaseModel):
"filtered_topic_tags", ArrayOfLtree, nullable=False, server_default="{}" "filtered_topic_tags", ArrayOfLtree, nullable=False, server_default="{}"
) )
comment_label_weight: Optional[float] = Column(REAL) comment_label_weight: Optional[float] = Column(REAL)
last_exemplary_label_time: Optional[datetime] = Column(TIMESTAMP(timezone=True))
@hybrid_property @hybrid_property
def filtered_topic_tags(self) -> List[str]: def filtered_topic_tags(self) -> List[str]:
@ -238,3 +241,13 @@ class User(DatabaseModel):
def is_admin(self) -> bool: def is_admin(self) -> bool:
"""Return whether the user has admin permissions.""" """Return whether the user has admin permissions."""
return "admin" in self.auth_principals return "admin" in self.auth_principals
def is_label_available(self, label: CommentLabelOption) -> bool:
"""Return whether the user has a particular label available."""
if label == CommentLabelOption.EXEMPLARY:
if not self.last_exemplary_label_time:
return True
return utc_now() - self.last_exemplary_label_time > timedelta(hours=24)
return True

5
tildes/tildes/templates/macros/comments.jinja2

@ -254,13 +254,16 @@
{% macro comment_label_options_template(options) %} {% macro comment_label_options_template(options) %}
<template id="comment-label-options"> <template id="comment-label-options">
<menu class="comment-label-buttons"> <menu class="comment-label-buttons">
{% for label in options %}
{% for label in options if request.user.is_label_available(label) %}
<li> <li>
<a class="btn-comment-label btn-comment-label-{{ label.name|lower }}" <a class="btn-comment-label btn-comment-label-{{ label.name|lower }}"
{% if label.reason_prompt %} {% if label.reason_prompt %}
data-js-reason-prompt="{{ label.reason_prompt }}" data-js-reason-prompt="{{ label.reason_prompt }}"
{% endif %} {% endif %}
>{{ label.name|lower }}</a> >{{ label.name|lower }}</a>
{% if label.name == "EXEMPLARY" %}
<div class="comment-label-note">One per day</div>
{% endif %}
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>

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

@ -4,6 +4,7 @@
"""Web API endpoints related to comments.""" """Web API endpoints related to comments."""
from marshmallow.fields import Boolean from marshmallow.fields import Boolean
from pyramid.httpexceptions import HTTPUnprocessableEntity
from pyramid.request import Request from pyramid.request import Request
from pyramid.response import Response from pyramid.response import Response
from sqlalchemy.dialects.postgresql import insert from sqlalchemy.dialects.postgresql import insert
@ -265,6 +266,9 @@ def put_label_comment(
"""Add a label to a comment.""" """Add a label to a comment."""
comment = request.context comment = request.context
if not request.user.is_label_available(name):
raise HTTPUnprocessableEntity("That label is not available.")
savepoint = request.tm.savepoint() savepoint = request.tm.savepoint()
weight = request.user.comment_label_weight weight = request.user.comment_label_weight

Loading…
Cancel
Save