mirror of https://gitlab.com/tildes/tildes.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
214 lines
7.2 KiB
214 lines
7.2 KiB
"""Contains the Comment class."""
|
|
|
|
from collections import Counter
|
|
from datetime import datetime, timedelta
|
|
from typing import Any, Optional, Sequence, Tuple
|
|
|
|
from pyramid.security import Allow, Authenticated, Deny, DENY_ALL, Everyone
|
|
from sqlalchemy import (
|
|
Boolean,
|
|
Column,
|
|
ForeignKey,
|
|
Integer,
|
|
Text,
|
|
TIMESTAMP,
|
|
)
|
|
from sqlalchemy.ext.hybrid import hybrid_property
|
|
from sqlalchemy.orm import deferred, relationship
|
|
from sqlalchemy.sql.expression import text
|
|
|
|
from tildes.lib.datetime import utc_now
|
|
from tildes.lib.id import id_to_id36
|
|
from tildes.lib.markdown import convert_markdown_to_safe_html
|
|
from tildes.metrics import incr_counter
|
|
from tildes.models import DatabaseModel
|
|
from tildes.models.topic import Topic
|
|
from tildes.models.user import User
|
|
from tildes.schemas.comment import CommentSchema
|
|
|
|
|
|
# edits inside this period after creation will not mark the comment as edited
|
|
EDIT_GRACE_PERIOD = timedelta(minutes=5)
|
|
|
|
|
|
class Comment(DatabaseModel):
|
|
"""Model for a comment on the site.
|
|
|
|
Trigger behavior:
|
|
Incoming:
|
|
- num_votes will be incremented and decremented by insertions and
|
|
deletions in comment_votes.
|
|
Outgoing:
|
|
- Inserting, deleting, or updating is_deleted will increment or
|
|
decrement num_comments on the relevant topic.
|
|
- Inserting a row will increment num_comments on any topic_visit rows
|
|
for the comment's author and the relevant topic.
|
|
- Inserting or updating is_deleted will update last_activity_time on
|
|
the relevant topic.
|
|
- Setting is_deleted to true will delete any rows in
|
|
comment_notifications related to the comment.
|
|
- Setting is_deleted to true will decrement num_comments on all
|
|
topic_visit rows for the relevant topic, where the visit_time was
|
|
after the time the comment was originally posted.
|
|
- Inserting a row or updating markdown will send a rabbitmq message
|
|
for "comment.created" or "comment.edited" respectively.
|
|
Internal:
|
|
- deleted_time will be set when is_deleted is set to true
|
|
"""
|
|
|
|
schema_class = CommentSchema
|
|
|
|
__tablename__ = 'comments'
|
|
|
|
comment_id: int = Column(Integer, primary_key=True)
|
|
topic_id: int = Column(
|
|
Integer,
|
|
ForeignKey('topics.topic_id'),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
user_id: int = Column(
|
|
Integer,
|
|
ForeignKey('users.user_id'),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
parent_comment_id: Optional[int] = Column(
|
|
Integer,
|
|
ForeignKey('comments.comment_id'),
|
|
index=True,
|
|
)
|
|
created_time: datetime = Column(
|
|
TIMESTAMP(timezone=True),
|
|
nullable=False,
|
|
index=True,
|
|
server_default=text('NOW()'),
|
|
)
|
|
is_deleted: bool = Column(
|
|
Boolean, nullable=False, server_default='false', index=True)
|
|
deleted_time: Optional[datetime] = Column(TIMESTAMP(timezone=True))
|
|
is_removed: bool = Column(Boolean, nullable=False, server_default='false')
|
|
removed_time: Optional[datetime] = Column(TIMESTAMP(timezone=True))
|
|
last_edited_time: Optional[datetime] = Column(TIMESTAMP(timezone=True))
|
|
_markdown: str = deferred(Column('markdown', Text, nullable=False))
|
|
rendered_html: str = Column(Text, nullable=False)
|
|
num_votes: int = Column(
|
|
Integer, nullable=False, server_default='0', index=True)
|
|
|
|
user: User = relationship('User', lazy=False, innerjoin=True)
|
|
topic: Topic = relationship('Topic', innerjoin=True)
|
|
parent_comment: Optional['Comment'] = relationship(
|
|
'Comment', uselist=False, remote_side=[comment_id])
|
|
|
|
@hybrid_property
|
|
def markdown(self) -> str:
|
|
"""Return the comment's markdown."""
|
|
# pylint: disable=method-hidden
|
|
return self._markdown
|
|
|
|
@markdown.setter # type: ignore
|
|
def markdown(self, new_markdown: str) -> None:
|
|
"""Set the comment's markdown and render its HTML."""
|
|
if new_markdown == self.markdown:
|
|
return
|
|
|
|
self._markdown = new_markdown
|
|
self.rendered_html = convert_markdown_to_safe_html(new_markdown)
|
|
|
|
if (self.created_time and
|
|
utc_now() - self.created_time > EDIT_GRACE_PERIOD):
|
|
self.last_edited_time = utc_now()
|
|
|
|
def __repr__(self) -> str:
|
|
"""Display the comment's ID as its repr format."""
|
|
return f'<Comment ({self.comment_id})>'
|
|
|
|
def __init__(
|
|
self,
|
|
topic: Topic,
|
|
author: User,
|
|
markdown: str,
|
|
parent_comment: Optional['Comment'] = None,
|
|
) -> None:
|
|
"""Create a new comment."""
|
|
self.topic = topic
|
|
self.user_id = author.user_id
|
|
if parent_comment:
|
|
self.parent_comment_id = parent_comment.comment_id
|
|
else:
|
|
self.parent_comment_id = None
|
|
|
|
self.markdown = markdown
|
|
|
|
incr_counter('comments')
|
|
|
|
def __acl__(self) -> Sequence[Tuple[str, Any, str]]:
|
|
"""Pyramid security ACL."""
|
|
acl = []
|
|
|
|
if not (self.is_deleted or self.is_removed):
|
|
acl.append((Allow, Everyone, 'view'))
|
|
|
|
if not self.topic.is_locked:
|
|
acl.append((Allow, Authenticated, 'reply'))
|
|
else:
|
|
acl.append((Allow, 'admin', 'reply'))
|
|
|
|
acl.append((Allow, Authenticated, 'mark_read'))
|
|
|
|
acl.append((Allow, self.user_id, 'edit'))
|
|
acl.append((Allow, self.user_id, 'delete'))
|
|
|
|
# everyone except the comment's author can vote on it
|
|
acl.append((Deny, self.user_id, 'vote'))
|
|
acl.append((Allow, Authenticated, 'vote'))
|
|
|
|
# temporary - nobody can tag comments
|
|
acl.append((Deny, Everyone, 'tag'))
|
|
|
|
if not self.is_deleted:
|
|
acl.append((Allow, 'admin', 'view'))
|
|
|
|
acl.append((Allow, self.user_id, 'view'))
|
|
acl.append((Allow, self.user_id, 'reply'))
|
|
acl.append((Allow, self.user_id, 'edit'))
|
|
acl.append((Allow, self.user_id, 'delete'))
|
|
|
|
acl.append(DENY_ALL)
|
|
|
|
return acl
|
|
|
|
@property
|
|
def comment_id36(self) -> str:
|
|
"""Return the comment's ID in ID36 format."""
|
|
return id_to_id36(self.comment_id)
|
|
|
|
@property
|
|
def parent_comment_id36(self) -> str:
|
|
"""Return the parent comment's ID in ID36 format."""
|
|
if not self.parent_comment_id:
|
|
raise AttributeError
|
|
|
|
return id_to_id36(self.parent_comment_id)
|
|
|
|
@property
|
|
def permalink(self) -> str:
|
|
"""Return the permalink for this comment."""
|
|
return f'{self.topic.permalink}#comment-{self.comment_id36}'
|
|
|
|
@property
|
|
def parent_comment_permalink(self) -> str:
|
|
"""Return the permalink for this comment's parent."""
|
|
if not self.parent_comment_id:
|
|
raise AttributeError
|
|
|
|
return f'{self.topic.permalink}#comment-{self.parent_comment_id36}'
|
|
|
|
@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 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]
|