* invisibleify completely removed trees only (fixes #431) * fix visibility state for shadowbanned users. this also ends up moving some of the complexity out of the templates. * comments: remove unused variable * moderation state machine * no seriously this really should check for v not being None * fix shadowban state * fix visibility state * update stateful counters * don't use bespoke function for show_descendants * properly mock ModerationState for cron submissions * fix approval discrepency * remove treenukes for removed comments * show shadowbans as removed
This commit is contained in:
parent
77af24a5b1
commit
39ce6a4ee9
9 changed files with 167 additions and 62 deletions
|
@ -1,5 +1,4 @@
|
||||||
import time
|
from typing import TYPE_CHECKING, Literal, Optional
|
||||||
from typing import Literal, Optional
|
|
||||||
from urllib.parse import parse_qs, urlencode, urlparse
|
from urllib.parse import parse_qs, urlencode, urlparse
|
||||||
|
|
||||||
from flask import g
|
from flask import g
|
||||||
|
@ -9,11 +8,14 @@ from sqlalchemy.orm import relationship
|
||||||
from files.classes.base import CreatedBase
|
from files.classes.base import CreatedBase
|
||||||
from files.helpers.config.const import *
|
from files.helpers.config.const import *
|
||||||
from files.helpers.config.environment import SCORE_HIDING_TIME_HOURS, SITE_FULL
|
from files.helpers.config.environment import SCORE_HIDING_TIME_HOURS, SITE_FULL
|
||||||
from files.helpers.content import (body_displayed,
|
from files.helpers.content import (ModerationState, body_displayed,
|
||||||
execute_shadowbanned_fake_votes)
|
execute_shadowbanned_fake_votes)
|
||||||
from files.helpers.lazy import lazy
|
from files.helpers.lazy import lazy
|
||||||
from files.helpers.time import format_age
|
from files.helpers.time import format_age
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from files.classes.user import User
|
||||||
|
|
||||||
CommentRenderContext = Literal['comments', 'volunteer']
|
CommentRenderContext = Literal['comments', 'volunteer']
|
||||||
|
|
||||||
class Comment(CreatedBase):
|
class Comment(CreatedBase):
|
||||||
|
@ -402,7 +404,6 @@ class Comment(CreatedBase):
|
||||||
if v.id == self.author_id: return 1
|
if v.id == self.author_id: return 1
|
||||||
return getattr(self, 'voted', 0)
|
return getattr(self, 'voted', 0)
|
||||||
|
|
||||||
|
|
||||||
def sticky_api_url(self, v) -> str | None:
|
def sticky_api_url(self, v) -> str | None:
|
||||||
'''
|
'''
|
||||||
Returns the API URL used to sticky this comment.
|
Returns the API URL used to sticky this comment.
|
||||||
|
@ -418,7 +419,25 @@ class Comment(CreatedBase):
|
||||||
return 'pin_comment'
|
return 'pin_comment'
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@lazy
|
@lazy
|
||||||
def active_flags(self, v):
|
def active_flags(self, v):
|
||||||
return len(self.flags(v))
|
return len(self.flags(v))
|
||||||
|
|
||||||
|
@lazy
|
||||||
|
def show_descendants(self, v:"User | None") -> bool:
|
||||||
|
if self.moderation_state.is_visible_to(v, getattr(self, 'is_blocking', False)):
|
||||||
|
return True
|
||||||
|
return bool(self.descendant_count)
|
||||||
|
|
||||||
|
@lazy
|
||||||
|
def visibility_state(self, v:"User | None") -> tuple[bool, str]:
|
||||||
|
'''
|
||||||
|
Returns a tuple of whether this content is visible and a publicly
|
||||||
|
visible message to accompany it. The visibility state machine is
|
||||||
|
a slight mess but... this should at least unify the state checks.
|
||||||
|
'''
|
||||||
|
return self.moderation_state.visibility_state(v, getattr(self, 'is_blocking', False))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def moderation_state(self) -> ModerationState:
|
||||||
|
return ModerationState.from_submittable(self)
|
||||||
|
|
|
@ -8,10 +8,8 @@ from sqlalchemy.sql.sqltypes import Boolean, Integer, String, Text
|
||||||
from files.classes.cron.tasks import (RepeatableTask, ScheduledTaskType,
|
from files.classes.cron.tasks import (RepeatableTask, ScheduledTaskType,
|
||||||
TaskRunContext)
|
TaskRunContext)
|
||||||
from files.classes.submission import Submission
|
from files.classes.submission import Submission
|
||||||
from files.helpers.config.const import (RENDER_DEPTH_LIMIT,
|
from files.helpers.config.const import SUBMISSION_TITLE_LENGTH_MAXIMUM
|
||||||
SUBMISSION_TITLE_LENGTH_MAXIMUM)
|
from files.helpers.content import ModerationState, body_displayed
|
||||||
from files.helpers.config.environment import SITE_FULL
|
|
||||||
from files.helpers.content import body_displayed
|
|
||||||
from files.helpers.lazy import lazy
|
from files.helpers.lazy import lazy
|
||||||
from files.helpers.sanitize import filter_emojis_only
|
from files.helpers.sanitize import filter_emojis_only
|
||||||
|
|
||||||
|
@ -172,3 +170,16 @@ class ScheduledSubmissionTask(RepeatableTask):
|
||||||
@property
|
@property
|
||||||
def edit_url(self) -> str:
|
def edit_url(self) -> str:
|
||||||
return f"/tasks/scheduled_posts/{self.id}/content"
|
return f"/tasks/scheduled_posts/{self.id}/content"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def moderation_state(self) -> ModerationState:
|
||||||
|
return ModerationState(
|
||||||
|
removed=False,
|
||||||
|
removed_by_name=None,
|
||||||
|
deleted=False, # we only want to show deleted UI color if disabled
|
||||||
|
reports_ignored=False,
|
||||||
|
filtered=False,
|
||||||
|
op_shadowbanned=False,
|
||||||
|
op_id=self.author_id_submission,
|
||||||
|
op_name_safe=self.author_name
|
||||||
|
)
|
||||||
|
|
|
@ -11,7 +11,7 @@ from files.helpers.assetcache import assetcache_path
|
||||||
from files.helpers.config.const import *
|
from files.helpers.config.const import *
|
||||||
from files.helpers.config.environment import (SCORE_HIDING_TIME_HOURS, SITE,
|
from files.helpers.config.environment import (SCORE_HIDING_TIME_HOURS, SITE,
|
||||||
SITE_FULL, SITE_ID)
|
SITE_FULL, SITE_ID)
|
||||||
from files.helpers.content import body_displayed
|
from files.helpers.content import ModerationState, body_displayed
|
||||||
from files.helpers.lazy import lazy
|
from files.helpers.lazy import lazy
|
||||||
from files.helpers.time import format_age, format_datetime
|
from files.helpers.time import format_age, format_datetime
|
||||||
|
|
||||||
|
@ -360,3 +360,7 @@ class Submission(CreatedBase):
|
||||||
@property
|
@property
|
||||||
def edit_url(self) -> str:
|
def edit_url(self) -> str:
|
||||||
return f"/edit_post/{self.id}"
|
return f"/edit_post/{self.id}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def moderation_state(self) -> ModerationState:
|
||||||
|
return ModerationState.from_submittable(self)
|
||||||
|
|
|
@ -2,7 +2,8 @@ from __future__ import annotations
|
||||||
|
|
||||||
import random
|
import random
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
from typing import TYPE_CHECKING, Optional, Union
|
from dataclasses import dataclass
|
||||||
|
from typing import TYPE_CHECKING, Any, Optional
|
||||||
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
@ -10,7 +11,9 @@ from files.helpers.config.const import PERMS
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from files.classes import Comment, Submission, User
|
from files.classes import Comment, Submission, User
|
||||||
Submittable = Union[Submission, Comment]
|
Submittable = Comment | Submission
|
||||||
|
else:
|
||||||
|
Submittable = Any
|
||||||
|
|
||||||
|
|
||||||
def _replace_urls(url:str) -> str:
|
def _replace_urls(url:str) -> str:
|
||||||
|
@ -86,18 +89,100 @@ def canonicalize_url2(url:str, *, httpsify:bool=False) -> urllib.parse.ParseResu
|
||||||
return url_parsed
|
return url_parsed
|
||||||
|
|
||||||
|
|
||||||
def moderated_body(target:Submittable, v:Optional[User]) -> Optional[str]:
|
@dataclass(frozen=True, kw_only=True, slots=True)
|
||||||
if v and (v.admin_level >= PERMS['POST_COMMENT_MODERATION'] \
|
class ModerationState:
|
||||||
or v.id == target.author_id):
|
'''
|
||||||
|
The moderation state machine. This holds moderation state information,
|
||||||
|
including whether this was removed, deleted, filtered, whether OP was
|
||||||
|
shadowbanned, etc
|
||||||
|
'''
|
||||||
|
removed: bool
|
||||||
|
removed_by_name: str | None
|
||||||
|
deleted: bool
|
||||||
|
reports_ignored: bool
|
||||||
|
filtered: bool
|
||||||
|
op_shadowbanned: bool
|
||||||
|
op_id: int
|
||||||
|
op_name_safe: str
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_submittable(cls, target: Submittable) -> "ModerationState":
|
||||||
|
return cls(
|
||||||
|
removed=bool(target.is_banned or target.filter_state == 'removed'),
|
||||||
|
removed_by_name=target.ban_reason, # type: ignore
|
||||||
|
deleted=bool(target.deleted_utc != 0),
|
||||||
|
reports_ignored=bool(target.filter_state == 'ignored'),
|
||||||
|
filtered=bool(target.filter_state == 'filtered'),
|
||||||
|
op_shadowbanned=bool(target.author.shadowbanned),
|
||||||
|
op_id=target.author_id, # type: ignore
|
||||||
|
op_name_safe=target.author_name
|
||||||
|
)
|
||||||
|
|
||||||
|
def moderated_body(self, v: User | None) -> str | None:
|
||||||
|
if v and (v.admin_level >= PERMS['POST_COMMENT_MODERATION'] \
|
||||||
|
or v.id == self.op_id):
|
||||||
|
return None
|
||||||
|
if self.deleted: return 'Deleted'
|
||||||
|
if self.appear_removed(v): return 'Removed'
|
||||||
|
if self.filtered: return 'Filtered'
|
||||||
return None
|
return None
|
||||||
if target.deleted_utc: return 'Deleted by author'
|
|
||||||
if target.is_banned or target.filter_state == 'removed': return 'Removed'
|
def visibility_state(self, v: User | None, is_blocking: bool) -> tuple[bool, str]:
|
||||||
if target.filter_state == 'filtered': return 'Filtered'
|
'''
|
||||||
return None
|
Returns a tuple of whether this content is visible and a publicly
|
||||||
|
visible message to accompany it. The visibility state machine is
|
||||||
|
a slight mess but... this should at least unify the state checks.
|
||||||
|
'''
|
||||||
|
def can(v: User | None, perm_level: int) -> bool:
|
||||||
|
return v and v.admin_level >= perm_level
|
||||||
|
|
||||||
|
can_moderate: bool = can(v, PERMS['POST_COMMENT_MODERATION'])
|
||||||
|
can_shadowban: bool = can(v, PERMS['USER_SHADOWBAN'])
|
||||||
|
|
||||||
|
if v and v.id == self.op_id:
|
||||||
|
return True, "This shouldn't be here, please report it!"
|
||||||
|
if (self.removed and not can_moderate) or \
|
||||||
|
(self.op_shadowbanned and not can_shadowban):
|
||||||
|
msg: str = 'Removed'
|
||||||
|
if self.removed_by_name:
|
||||||
|
msg = f'Removed by @{self.removed_by_name}'
|
||||||
|
return False, msg
|
||||||
|
if self.filtered and not can_moderate:
|
||||||
|
return False, 'Filtered, please go kick a mod in the ass to fix this'
|
||||||
|
if self.deleted and not can_moderate:
|
||||||
|
return False, 'Deleted by author'
|
||||||
|
if is_blocking:
|
||||||
|
return False, f'You are blocking @{self.op_name_safe}'
|
||||||
|
return True, "This shouldn't be here, please report it!"
|
||||||
|
|
||||||
|
def is_visible_to(self, v: User | None, is_blocking: bool) -> bool:
|
||||||
|
return self.visibility_state(v, is_blocking)[0]
|
||||||
|
|
||||||
|
def replacement_message(self, v: User | None, is_blocking: bool) -> str:
|
||||||
|
return self.visibility_state(v, is_blocking)[1]
|
||||||
|
|
||||||
|
def appear_removed(self, v: User | None) -> bool:
|
||||||
|
if self.removed: return True
|
||||||
|
if not self.op_shadowbanned: return False
|
||||||
|
return (not v) or bool(v.admin_level < PERMS['USER_SHADOWBAN'])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def publicly_visible(self) -> bool:
|
||||||
|
return all(
|
||||||
|
not state for state in
|
||||||
|
[self.deleted, self.removed, self.filtered, self.op_shadowbanned]
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def explicitly_moderated(self) -> bool:
|
||||||
|
'''
|
||||||
|
Whether this was removed or filtered and not as the result of a shadowban
|
||||||
|
'''
|
||||||
|
return self.removed or self.filtered
|
||||||
|
|
||||||
|
|
||||||
def body_displayed(target:Submittable, v:Optional[User], is_html:bool) -> str:
|
def body_displayed(target:Submittable, v:Optional[User], is_html:bool) -> str:
|
||||||
moderated:Optional[str] = moderated_body(target, v)
|
moderated:Optional[str] = target.moderation_state.moderated_body(v)
|
||||||
if moderated: return moderated
|
if moderated: return moderated
|
||||||
|
|
||||||
body = target.body_html if is_html else target.body
|
body = target.body_html if is_html else target.body
|
||||||
|
|
|
@ -276,13 +276,13 @@ def update_filter_status(v):
|
||||||
return { 'result': f'Status of {new_status} is not permitted' }
|
return { 'result': f'Status of {new_status} is not permitted' }
|
||||||
|
|
||||||
if post_id:
|
if post_id:
|
||||||
p = g.db.get(Submission, post_id)
|
target = g.db.get(Submission, post_id)
|
||||||
old_status = p.filter_state
|
old_status = target.filter_state
|
||||||
rows_updated = g.db.query(Submission).where(Submission.id == post_id) \
|
rows_updated = g.db.query(Submission).where(Submission.id == post_id) \
|
||||||
.update({Submission.filter_state: new_status})
|
.update({Submission.filter_state: new_status})
|
||||||
elif comment_id:
|
elif comment_id:
|
||||||
c = g.db.get(Comment, comment_id)
|
target = g.db.get(Comment, comment_id)
|
||||||
old_status = c.filter_state
|
old_status = target.filter_state
|
||||||
rows_updated = g.db.query(Comment).where(Comment.id == comment_id) \
|
rows_updated = g.db.query(Comment).where(Comment.id == comment_id) \
|
||||||
.update({Comment.filter_state: new_status})
|
.update({Comment.filter_state: new_status})
|
||||||
else:
|
else:
|
||||||
|
@ -290,15 +290,15 @@ def update_filter_status(v):
|
||||||
|
|
||||||
if rows_updated == 1:
|
if rows_updated == 1:
|
||||||
# If comment now visible, update state to reflect publication.
|
# If comment now visible, update state to reflect publication.
|
||||||
if (comment_id
|
if (isinstance(target, Comment)
|
||||||
and old_status in ['filtered', 'removed']
|
and old_status in ['filtered', 'removed']
|
||||||
and new_status in ['normal', 'ignored']):
|
and new_status in ['normal', 'ignored']):
|
||||||
comment_on_publish(c)
|
comment_on_publish(target) # XXX: can cause discrepancies if removal state ≠ filter state
|
||||||
|
|
||||||
if (comment_id
|
if (isinstance(target, Comment)
|
||||||
and old_status in ['normal', 'ignored']
|
and old_status in ['normal', 'ignored']
|
||||||
and new_status in ['filtered', 'removed']):
|
and new_status in ['filtered', 'removed']):
|
||||||
comment_on_unpublish(c)
|
comment_on_unpublish(target) # XXX: can cause discrepancies if removal state ≠ filter state
|
||||||
|
|
||||||
g.db.commit()
|
g.db.commit()
|
||||||
return { 'result': 'Update successful' }
|
return { 'result': 'Update successful' }
|
||||||
|
@ -1302,7 +1302,6 @@ def unsticky_comment(cid, v):
|
||||||
@limiter.exempt
|
@limiter.exempt
|
||||||
@admin_level_required(2)
|
@admin_level_required(2)
|
||||||
def api_ban_comment(c_id, v):
|
def api_ban_comment(c_id, v):
|
||||||
|
|
||||||
comment = g.db.query(Comment).filter_by(id=c_id).one_or_none()
|
comment = g.db.query(Comment).filter_by(id=c_id).one_or_none()
|
||||||
if not comment:
|
if not comment:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
@ -1310,7 +1309,7 @@ def api_ban_comment(c_id, v):
|
||||||
comment.is_banned = True
|
comment.is_banned = True
|
||||||
comment.is_approved = None
|
comment.is_approved = None
|
||||||
comment.ban_reason = v.username
|
comment.ban_reason = v.username
|
||||||
g.db.add(comment)
|
comment_on_unpublish(comment) # XXX: can cause discrepancies if removal state ≠ filter state
|
||||||
ma=ModAction(
|
ma=ModAction(
|
||||||
kind="ban_comment",
|
kind="ban_comment",
|
||||||
user_id=v.id,
|
user_id=v.id,
|
||||||
|
@ -1325,7 +1324,6 @@ def api_ban_comment(c_id, v):
|
||||||
@limiter.exempt
|
@limiter.exempt
|
||||||
@admin_level_required(2)
|
@admin_level_required(2)
|
||||||
def api_unban_comment(c_id, v):
|
def api_unban_comment(c_id, v):
|
||||||
|
|
||||||
comment = g.db.query(Comment).filter_by(id=c_id).one_or_none()
|
comment = g.db.query(Comment).filter_by(id=c_id).one_or_none()
|
||||||
if not comment: abort(404)
|
if not comment: abort(404)
|
||||||
|
|
||||||
|
@ -1340,6 +1338,7 @@ def api_unban_comment(c_id, v):
|
||||||
comment.is_banned = False
|
comment.is_banned = False
|
||||||
comment.ban_reason = None
|
comment.ban_reason = None
|
||||||
comment.is_approved = v.id
|
comment.is_approved = v.id
|
||||||
|
comment_on_publish(comment) # XXX: can cause discrepancies if removal state ≠ filter state
|
||||||
|
|
||||||
g.db.add(comment)
|
g.db.add(comment)
|
||||||
|
|
||||||
|
|
|
@ -331,17 +331,13 @@ def edit_comment(cid, v):
|
||||||
@limiter.limit("1/second;30/minute;200/hour;1000/day")
|
@limiter.limit("1/second;30/minute;200/hour;1000/day")
|
||||||
@auth_required
|
@auth_required
|
||||||
def delete_comment(cid, v):
|
def delete_comment(cid, v):
|
||||||
|
|
||||||
c = get_comment(cid, v=v)
|
c = get_comment(cid, v=v)
|
||||||
|
if c.deleted_utc: abort(409)
|
||||||
if not c.deleted_utc:
|
if c.author_id != v.id: abort(403)
|
||||||
|
c.deleted_utc = int(time.time())
|
||||||
if c.author_id != v.id: abort(403)
|
# TODO: update stateful counters
|
||||||
|
g.db.add(c)
|
||||||
c.deleted_utc = int(time.time())
|
g.db.commit()
|
||||||
|
|
||||||
g.db.add(c)
|
|
||||||
g.db.commit()
|
|
||||||
|
|
||||||
return {"message": "Comment deleted!"}
|
return {"message": "Comment deleted!"}
|
||||||
|
|
||||||
|
@ -349,16 +345,13 @@ def delete_comment(cid, v):
|
||||||
@limiter.limit("1/second;30/minute;200/hour;1000/day")
|
@limiter.limit("1/second;30/minute;200/hour;1000/day")
|
||||||
@auth_required
|
@auth_required
|
||||||
def undelete_comment(cid, v):
|
def undelete_comment(cid, v):
|
||||||
|
|
||||||
c = get_comment(cid, v=v)
|
c = get_comment(cid, v=v)
|
||||||
|
if not c.deleted_utc: abort(409)
|
||||||
if c.deleted_utc:
|
if c.author_id != v.id: abort(403)
|
||||||
if c.author_id != v.id: abort(403)
|
c.deleted_utc = 0
|
||||||
|
# TODO: update stateful counters
|
||||||
c.deleted_utc = 0
|
g.db.add(c)
|
||||||
|
g.db.commit()
|
||||||
g.db.add(c)
|
|
||||||
g.db.commit()
|
|
||||||
|
|
||||||
return {"message": "Comment undeleted!"}
|
return {"message": "Comment undeleted!"}
|
||||||
|
|
||||||
|
@ -366,7 +359,6 @@ def undelete_comment(cid, v):
|
||||||
@app.post("/pin_comment/<cid>")
|
@app.post("/pin_comment/<cid>")
|
||||||
@auth_required
|
@auth_required
|
||||||
def pin_comment(cid, v):
|
def pin_comment(cid, v):
|
||||||
|
|
||||||
comment = get_comment(cid, v=v)
|
comment = get_comment(cid, v=v)
|
||||||
|
|
||||||
if not comment.is_pinned:
|
if not comment.is_pinned:
|
||||||
|
|
|
@ -15,7 +15,6 @@ from files.__main__ import app, db_session, limiter
|
||||||
from files.classes import *
|
from files.classes import *
|
||||||
from files.helpers.alerts import *
|
from files.helpers.alerts import *
|
||||||
from files.helpers.caching import invalidate_cache
|
from files.helpers.caching import invalidate_cache
|
||||||
from files.helpers.comments import comment_filter_moderated
|
|
||||||
from files.helpers.config.const import *
|
from files.helpers.config.const import *
|
||||||
from files.helpers.content import canonicalize_url2
|
from files.helpers.content import canonicalize_url2
|
||||||
from files.helpers.contentsorting import sort_objects
|
from files.helpers.contentsorting import sort_objects
|
||||||
|
@ -94,7 +93,6 @@ def post_id(pid, anything=None, v=None):
|
||||||
Comment.parent_submission == post.id,
|
Comment.parent_submission == post.id,
|
||||||
Comment.level == 1,
|
Comment.level == 1,
|
||||||
).order_by(Comment.is_pinned.desc().nulls_last())
|
).order_by(Comment.is_pinned.desc().nulls_last())
|
||||||
top_comments = comment_filter_moderated(top_comments, v)
|
|
||||||
top_comments = sort_objects(top_comments, sort, Comment)
|
top_comments = sort_objects(top_comments, sort, Comment)
|
||||||
|
|
||||||
pg_top_comment_ids = []
|
pg_top_comment_ids = []
|
||||||
|
@ -108,7 +106,6 @@ def post_id(pid, anything=None, v=None):
|
||||||
|
|
||||||
def comment_tree_filter(q: Query) -> Query:
|
def comment_tree_filter(q: Query) -> Query:
|
||||||
q = q.filter(Comment.top_comment_id.in_(pg_top_comment_ids))
|
q = q.filter(Comment.top_comment_id.in_(pg_top_comment_ids))
|
||||||
q = comment_filter_moderated(q, v)
|
|
||||||
return q
|
return q
|
||||||
|
|
||||||
comments, comment_tree = get_comment_trees_eager(comment_tree_filter, sort, v)
|
comments, comment_tree = get_comment_trees_eager(comment_tree_filter, sort, v)
|
||||||
|
@ -160,7 +157,6 @@ def viewmore(v, pid, sort, offset):
|
||||||
# `NOT IN :ids` in top_comments.
|
# `NOT IN :ids` in top_comments.
|
||||||
top_comments = top_comments.filter(Comment.created_utc <= newest_created_utc)
|
top_comments = top_comments.filter(Comment.created_utc <= newest_created_utc)
|
||||||
|
|
||||||
top_comments = comment_filter_moderated(top_comments, v)
|
|
||||||
top_comments = sort_objects(top_comments, sort, Comment)
|
top_comments = sort_objects(top_comments, sort, Comment)
|
||||||
|
|
||||||
pg_top_comment_ids = []
|
pg_top_comment_ids = []
|
||||||
|
@ -174,7 +170,6 @@ def viewmore(v, pid, sort, offset):
|
||||||
|
|
||||||
def comment_tree_filter(q: Query) -> Query:
|
def comment_tree_filter(q: Query) -> Query:
|
||||||
q = q.filter(Comment.top_comment_id.in_(pg_top_comment_ids))
|
q = q.filter(Comment.top_comment_id.in_(pg_top_comment_ids))
|
||||||
q = comment_filter_moderated(q, v)
|
|
||||||
return q
|
return q
|
||||||
|
|
||||||
_, comment_tree = get_comment_trees_eager(comment_tree_filter, sort, v)
|
_, comment_tree = get_comment_trees_eager(comment_tree_filter, sort, v)
|
||||||
|
|
|
@ -517,7 +517,7 @@ def messagereply(v):
|
||||||
g.db.add(c)
|
g.db.add(c)
|
||||||
g.db.flush()
|
g.db.flush()
|
||||||
|
|
||||||
if user_id and user_id != v.id and user_id != 2:
|
if user_id and user_id != v.id and user_id != MODMAIL_ID:
|
||||||
notif = g.db.query(Notification).filter_by(comment_id=c.id, user_id=user_id).one_or_none()
|
notif = g.db.query(Notification).filter_by(comment_id=c.id, user_id=user_id).one_or_none()
|
||||||
if not notif:
|
if not notif:
|
||||||
notif = Notification(comment_id=c.id, user_id=user_id)
|
notif = Notification(comment_id=c.id, user_id=user_id)
|
||||||
|
|
|
@ -10,11 +10,10 @@
|
||||||
{%- set score = c.score_str(render_ctx) -%}
|
{%- set score = c.score_str(render_ctx) -%}
|
||||||
{%- set downs = c.downvotes_str(render_ctx) -%}
|
{%- set downs = c.downvotes_str(render_ctx) -%}
|
||||||
|
|
||||||
{%- set replies = c.replies(v) -%}
|
{% set replies = c.replies(v) %}
|
||||||
{%- set is_notification_page = request.path.startswith('/notifications') -%}
|
|
||||||
|
|
||||||
{% if (c.is_banned or c.deleted_utc or c.is_blocking) and not (v and v.admin_level >= 2) and not (v and v.id==c.author_id) %}
|
|
||||||
|
|
||||||
|
{% if not c.visibility_state(v)[0] %}
|
||||||
|
{% if c.show_descendants(v) %}
|
||||||
<div id="comment-{{c.id}}" class="comment">
|
<div id="comment-{{c.id}}" class="comment">
|
||||||
<div class="comment-collapse-icon" onclick="collapse_comment('{{c.id}}', this.parentElement)"></div>
|
<div class="comment-collapse-icon" onclick="collapse_comment('{{c.id}}', this.parentElement)"></div>
|
||||||
<div class="comment-collapse-bar-click" onclick="collapse_comment('{{c.id}}', this.parentElement)">
|
<div class="comment-collapse-bar-click" onclick="collapse_comment('{{c.id}}', this.parentElement)">
|
||||||
|
@ -23,7 +22,7 @@
|
||||||
|
|
||||||
<div class="comment-user-info">
|
<div class="comment-user-info">
|
||||||
{% if standalone and c.over_18 %}<span class="badge badge-danger">+18</span>{% endif %}
|
{% if standalone and c.over_18 %}<span class="badge badge-danger">+18</span>{% endif %}
|
||||||
{% if c.is_banned %}removed by @{{c.ban_reason}}{% elif c.deleted_utc %}Deleted by author{% elif c.is_blocking %}You are blocking @{{c.author_name}}{% endif %}
|
{{c.visibility_state(v)[1]}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="comment-body">
|
<div class="comment-body">
|
||||||
|
@ -54,6 +53,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue