From 9953c5763cb286d268311404cc43191d2015b1bf Mon Sep 17 00:00:00 2001 From: TLSM Date: Mon, 28 Nov 2022 12:36:04 -0500 Subject: [PATCH 1/4] Port get.py improvements from upstream. Generally standardizes the get_* helpers: - Adds type hinting. - Deduplicates block property addition. - Respects `graceful` in more contexts. - More resilient to invalid user input / less boilerplate necessary at call-sites. --- files/helpers/get.py | 335 ++++++++++++++++++++++----------------- files/helpers/strings.py | 3 +- files/routes/users.py | 15 +- 3 files changed, 193 insertions(+), 160 deletions(-) diff --git a/files/helpers/get.py b/files/helpers/get.py index 0a4fd8d8b..57f7079b2 100644 --- a/files/helpers/get.py +++ b/files/helpers/get.py @@ -1,15 +1,19 @@ -from files.classes import * -from files.helpers.strings import sql_ilike_clean +from typing import Iterable, List, Optional, Type, Union + from flask import g +from sqlalchemy import and_, any_, or_ + +from files.classes import * +from files.helpers.const import AUTOJANNY_ID +from files.helpers.strings import sql_ilike_clean -def get_id(username, v=None, graceful=False): - +def get_id( + username:str, + graceful:bool=False) -> Optional[int]: username = sql_ilike_clean(username) - user = g.db.query( - User.id - ).filter( + user = g.db.query(User.id).filter( or_( User.username.ilike(username), User.original_username.ilike(username) @@ -17,25 +21,23 @@ def get_id(username, v=None, graceful=False): ).one_or_none() if not user: - if not graceful: - abort(404) - else: - return None + if graceful: return None + abort(404) return user[0] -def get_user(username, v=None, graceful=False): - - if not username: - if not graceful: abort(404) - else: return None - +def get_user( + username:Optional[str], + v:Optional[User]=None, + graceful:bool=False, + include_blocks:bool=False) -> Optional[User]: username = sql_ilike_clean(username) + if not username: + if graceful: return None + abort(404) - user = g.db.query( - User - ).filter( + user = g.db.query(User).filter( or_( User.username.ilike(username), User.original_username.ilike(username) @@ -43,34 +45,23 @@ def get_user(username, v=None, graceful=False): ).one_or_none() if not user: - if not graceful: abort(404) - else: return None + if graceful: return None + abort(404) - if v: - block = g.db.query(UserBlock).filter( - or_( - and_( - UserBlock.user_id == v.id, - UserBlock.target_id == user.id - ), - and_(UserBlock.user_id == user.id, - UserBlock.target_id == v.id - ) - ) - ).first() - - user.is_blocking = block and block.user_id == v.id - user.is_blocked = block and block.target_id == v.id + if v and include_blocks: + user = _add_block_props(user, v) return user -def get_users(usernames, v=None, graceful=False): - if not usernames: - if not graceful: abort(404) - else: return [] +def get_users( + usernames:Iterable[str], + graceful:bool=False) -> List[User]: + if not usernames: return [] usernames = [ sql_ilike_clean(n) for n in usernames ] - + if not any(usernames): + if graceful and len(usernames) == 0: return [] + abort(404) users = g.db.query(User).filter( or_( User.username == any_(usernames), @@ -78,96 +69,90 @@ def get_users(usernames, v=None, graceful=False): ) ).all() - if not users: - if not graceful: abort(404) - else: return [] + if len(users) != len(usernames) and not graceful: + abort(404) return users -def get_account(id, v=None): - try: id = int(id) - except: abort(404) +def get_account( + id:Union[str,int], + v:Optional[User]=None, + graceful:bool=False, + include_blocks:bool=False) -> Optional[User]: + try: + id = int(id) + except: + if graceful: return None + abort(404) - user = g.db.query(User).filter_by(id = id).one_or_none() - - if not user: abort(404) + user = g.db.get(User, id) + if not user: + if graceful: return None + abort(404) - if v: - block = g.db.query(UserBlock).filter( - or_( - and_( - UserBlock.user_id == v.id, - UserBlock.target_id == user.id - ), - and_(UserBlock.user_id == user.id, - UserBlock.target_id == v.id - ) - ) - ).first() - - user.is_blocking = block and block.user_id == v.id - user.is_blocked = block and block.target_id == v.id + if v and include_blocks: + user = _add_block_props(user, v) return user -def get_post(i, v=None, graceful=False): +def get_post( + i:Union[str,int], + v:Optional[User]=None, + graceful:bool=False) -> Optional[Submission]: try: i = int(i) except: if graceful: return None - else: abort(404) + abort(404) if v: vt = g.db.query(Vote).filter_by( user_id=v.id, submission_id=i).subquery() blocking = v.blocking.subquery() - items = g.db.query( + post = g.db.query( Submission, vt.c.vote_type, blocking.c.target_id, ) - items=items.filter(Submission.id == i + post = post.filter(Submission.id == i ).join( - vt, - vt.c.submission_id == Submission.id, + vt, + vt.c.submission_id == Submission.id, isouter=True ).join( - blocking, - blocking.c.target_id == Submission.author_id, + blocking, + blocking.c.target_id == Submission.author_id, isouter=True ) - - items=items.one_or_none() + post = post.one_or_none() - if not items: + if not post: if graceful: return None else: abort(404) - x = items[0] - x.voted = items[1] or 0 - x.is_blocking = items[2] or 0 + x = post[0] + x.voted = post[1] or 0 + x.is_blocking = post[2] or 0 else: - items = g.db.query( - Submission - ).filter(Submission.id == i).one_or_none() - if not items: + post = g.db.get(Submission, i) + if not post: if graceful: return None else: abort(404) - x=items + x = post return x -def get_posts(pids, v=None): - - if not pids: - return [] +def get_posts( + pids:Iterable[int], + v:Optional[User]=None) -> List[Submission]: + if not pids: return [] if v: - vt = g.db.query(Vote).filter( + vt = g.db.query(Vote.vote_type, Vote.submission_id).filter( Vote.submission_id.in_(pids), Vote.user_id==v.id ).subquery() @@ -183,67 +168,52 @@ def get_posts(pids, v=None): ).filter( Submission.id.in_(pids) ).join( - vt, vt.c.submission_id==Submission.id, isouter=True + vt, vt.c.submission_id == Submission.id, isouter=True ).join( - blocking, - blocking.c.target_id == Submission.author_id, - isouter=True + blocking, blocking.c.target_id == Submission.author_id, isouter=True ).join( - blocked, - blocked.c.user_id == Submission.author_id, - isouter=True - ).all() - - output = [p[0] for p in query] - for i in range(len(output)): - output[i].voted = query[i][1] or 0 - output[i].is_blocking = query[i][2] or 0 - output[i].is_blocked = query[i][3] or 0 + blocked, blocked.c.user_id == Submission.author_id, isouter=True + ) else: - output = g.db.query(Submission,).filter(Submission.id.in_(pids)).all() + query = g.db.query(Submission).filter(Submission.id.in_(pids)) + + results = query.all() + + if v: + output = [p[0] for p in results] + for i in range(len(output)): + output[i].voted = results[i][1] or 0 + output[i].is_blocking = results[i][2] or 0 + output[i].is_blocked = results[i][3] or 0 + else: + output = results return sorted(output, key=lambda x: pids.index(x.id)) -def get_comment(i, v=None, graceful=False): + +def get_comment( + i:Union[str,int], + v:Optional[User]=None, + graceful:bool=False) -> Optional[Comment]: try: i = int(i) except: if graceful: return None abort(404) + if not i: + if graceful: return None + else: abort(404) - if v: + comment = g.db.get(Comment, i) + if not comment: + if graceful: return None + else: abort(404) - comment=g.db.query(Comment).filter(Comment.id == i).one_or_none() - - if not comment and not graceful: abort(404) - - block = g.db.query(UserBlock).filter( - or_( - and_( - UserBlock.user_id == v.id, - UserBlock.target_id == comment.author_id - ), - and_( - UserBlock.user_id == comment.author_id, - UserBlock.target_id == v.id - ) - ) - ).first() - - vts = g.db.query(CommentVote).filter_by(user_id=v.id, comment_id=comment.id) - vt = g.db.query(CommentVote).filter_by(user_id=v.id, comment_id=comment.id).one_or_none() - comment.is_blocking = block and block.user_id == v.id - comment.is_blocked = block and block.target_id == v.id - comment.voted = vt.vote_type if vt else 0 - - else: - comment = g.db.query(Comment).filter(Comment.id == i).one_or_none() - if not comment and not graceful:abort(404) - - return comment + return _add_vote_and_block_props(comment, v, CommentVote) -def get_comments(cids, v=None, load_parent=False): - +def get_comments( + cids:Iterable[int], + v:Optional[User]=None) -> List[Comment]: if not cids: return [] if v: @@ -261,7 +231,8 @@ def get_comments(cids, v=None, load_parent=False): ).filter(Comment.id.in_(cids)) if not (v and (v.shadowbanned or v.admin_level > 1)): - comments = comments.join(User, User.id == Comment.author_id).filter(User.shadowbanned == None) + comments = comments.join(User, User.id == Comment.author_id) \ + .filter(User.shadowbanned == None) comments = comments.join( votes, @@ -284,21 +255,18 @@ def get_comments(cids, v=None, load_parent=False): comment.is_blocking = c[2] or 0 comment.is_blocked = c[3] or 0 output.append(comment) - else: - output = g.db.query(Comment).join(User, User.id == Comment.author_id).filter(User.shadowbanned == None, Comment.id.in_(cids)).all() - - if load_parent: - parents = [x.parent_comment_id for x in output if x.parent_comment_id] - parents = get_comments(parents, v=v) - parents = {x.id: x for x in parents} - for c in output: c.sex = parents.get(c.parent_comment_id) + output = g.db.query(Comment) \ + .join(User, User.id == Comment.author_id) \ + .filter(User.shadowbanned == None, Comment.id.in_(cids)) \ + .all() return sorted(output, key=lambda x: cids.index(x.id)) -def get_domain(s): - +# TODO: This function was concisely inlined into posts.py in upstream. +# Think it involved adding `tldextract` as a dependency. +def get_domain(s:str) -> Optional[BannedDomain]: parts = s.split(".") domain_list = set() for i in range(len(parts)): @@ -308,7 +276,9 @@ def get_domain(s): domain_list.add(new_domain) - doms = [x for x in g.db.query(BannedDomain).filter(BannedDomain.domain.in_(domain_list)).all()] + doms = g.db.query(BannedDomain) \ + .filter(BannedDomain.domain.in_(domain_list)).all() + doms = [x for x in doms] if not doms: return None @@ -316,3 +286,70 @@ def get_domain(s): doms = sorted(doms, key=lambda x: len(x.domain), reverse=True) return doms[0] + + +def _add_block_props( + target:Union[Submission, Comment, User], + v:Optional[User]): + if not v: return target + id = None + + if any(isinstance(target, cls) for cls in [Submission, Comment]): + id = target.author_id + elif isinstance(target, User): + id = target.id + else: + raise TypeError("add_block_props only supports non-None " + "submissions, comments, and users") + + if hasattr(target, 'is_blocking') and hasattr(target, 'is_blocked'): + return target + + # users can't block or be blocked by themselves or AutoJanny + if v.id == id or id == AUTOJANNY_ID: + target.is_blocking = False + target.is_blocked = False + return target + + block = g.db.query(UserBlock).filter( + or_( + and_( + UserBlock.user_id == v.id, + UserBlock.target_id == id + ), + and_( + UserBlock.user_id == id, + UserBlock.target_id == v.id + ) + ) + ).first() + target.is_blocking = block and block.user_id == v.id + target.is_blocked = block and block.target_id == v.id + return target + + +def _add_vote_props( + target:Union[Submission, Comment], + v:Optional[User], + vote_cls:Union[Type[Vote], Type[CommentVote], None]): + if hasattr(target, 'voted'): return target + + vt = g.db.query(vote_cls.vote_type).filter_by(user_id=v.id) + if vote_cls is Vote: + vt = vt.filter_by(submission_id=target.id) + elif vote_cls is CommentVote: + vt = vt.filter_by(comment_id=target.id) + else: + vt = None + if vt: vt = vt.one_or_none() + target.voted = vt.vote_type if vt else 0 + return target + + +def _add_vote_and_block_props( + target:Union[Submission, Comment], + v:Optional[User], + vote_cls:Union[Type[Vote], Type[CommentVote], None]): + if not v: return target + target = _add_block_props(target, v) + return _add_vote_props(target, v, vote_cls) diff --git a/files/helpers/strings.py b/files/helpers/strings.py index 693265f87..21e4bf5b3 100644 --- a/files/helpers/strings.py +++ b/files/helpers/strings.py @@ -1,8 +1,9 @@ - import typing # clean strings for searching def sql_ilike_clean(my_str): + if my_str is None: + return None return my_str.replace(r'\\', '').replace('_', r'\_').replace('%', '').strip() # this will also just return a bool verbatim diff --git a/files/routes/users.py b/files/routes/users.py index 58f240a9a..ceffb4942 100644 --- a/files/routes/users.py +++ b/files/routes/users.py @@ -540,7 +540,7 @@ def message2(v, username): return {"error": "You have been permabanned and cannot send messages; " + \ "contact modmail if you think this decision was incorrect."}, 403 - user = get_user(username, v=v) + user = get_user(username, v=v, include_blocks=True) if hasattr(user, 'is_blocking') and user.is_blocking: return {"error": "You're blocking this user."}, 403 if v.admin_level <= 1 and hasattr(user, 'is_blocked') and user.is_blocked: @@ -772,9 +772,7 @@ def visitors(v): @app.get("/@") @auth_desired def u_username(username, v=None): - - - u = get_user(username, v=v) + u = get_user(username, v=v, include_blocks=True) if username != u.username: @@ -858,8 +856,7 @@ def u_username(username, v=None): @app.get("/@/comments") @auth_desired def u_username_comments(username, v=None): - - user = get_user(username, v=v) + user = get_user(username, v=v, include_blocks=True) if username != user.username: return redirect(f'/@{user.username}/comments') @@ -945,8 +942,7 @@ def u_username_comments(username, v=None): @app.get("/@/info") @auth_required def u_username_info(username, v=None): - - user=get_user(username, v=v) + user = get_user(username, v=v, include_blocks=True) if hasattr(user, 'is_blocking') and user.is_blocking: return {"error": "You're blocking this user."}, 401 @@ -958,8 +954,7 @@ def u_username_info(username, v=None): @app.get("//info") @auth_required def u_user_id_info(id, v=None): - - user=get_account(id, v=v) + user = get_account(id, v=v, include_blocks=True) if hasattr(user, 'is_blocking') and user.is_blocking: return {"error": "You're blocking this user."}, 401 From 4d22d9bce2a3db60cf8aac8ff7eb09ce808f35fb Mon Sep 17 00:00:00 2001 From: TLSM Date: Mon, 28 Nov 2022 12:55:31 -0500 Subject: [PATCH 2/4] Eager load get_posts for submission_listings. Ported in logic from upstream to use SQLAlchemy eager loading instead of repeated queries when building a submission_listing. Adjusted loaded relationships to include only those used on TheMotte. Using test data from seed_db, before and after: GET / |----------|--------|--------|--------|--------|--------|------------| | Database | SELECT | INSERT | UPDATE | DELETE | Totals | Duplicates | |----------|--------|--------|--------|--------|--------|------------| | default | 83 | 0 | 0 | 0 | 83 | 72 | |----------|--------|--------|--------|--------|--------|------------| Total queries: 83 in 0.031s GET / |----------|--------|--------|--------|--------|--------|------------| | Database | SELECT | INSERT | UPDATE | DELETE | Totals | Duplicates | |----------|--------|--------|--------|--------|--------|------------| | default | 14 | 0 | 0 | 0 | 14 | 0 | |----------|--------|--------|--------|--------|--------|------------| Total queries: 14 in 0.00718s --- files/classes/badges.py | 4 +++- files/helpers/get.py | 14 +++++++++++++- files/routes/front.py | 2 +- files/routes/search.py | 2 +- files/routes/users.py | 12 ++++++------ 5 files changed, 24 insertions(+), 10 deletions(-) diff --git a/files/classes/badges.py b/files/classes/badges.py index 0e79829f2..16479d799 100644 --- a/files/classes/badges.py +++ b/files/classes/badges.py @@ -33,7 +33,9 @@ class Badge(Base): Index('badges_badge_id_idx', badge_id) user = relationship("User", viewonly=True) - badge = relationship("BadgeDef", primaryjoin="foreign(Badge.badge_id) == remote(BadgeDef.id)", viewonly=True) + badge = relationship("BadgeDef", + primaryjoin="foreign(Badge.badge_id) == remote(BadgeDef.id)", + lazy="joined", innerjoin=True, viewonly=True) def __repr__(self): return f"" diff --git a/files/helpers/get.py b/files/helpers/get.py index 57f7079b2..c68679d3f 100644 --- a/files/helpers/get.py +++ b/files/helpers/get.py @@ -2,6 +2,7 @@ from typing import Iterable, List, Optional, Type, Union from flask import g from sqlalchemy import and_, any_, or_ +from sqlalchemy.orm import selectinload from files.classes import * from files.helpers.const import AUTOJANNY_ID @@ -148,7 +149,8 @@ def get_post( def get_posts( pids:Iterable[int], - v:Optional[User]=None) -> List[Submission]: + v:Optional[User]=None, + eager:bool=False) -> List[Submission]: if not pids: return [] if v: @@ -177,6 +179,16 @@ def get_posts( else: query = g.db.query(Submission).filter(Submission.id.in_(pids)) + if eager: + query = query.options( + selectinload(Submission.author).options( + selectinload(User.badges), + selectinload(User.notes), + ), + selectinload(Submission.reports), + selectinload(Submission.awards), + ) + results = query.all() if v: diff --git a/files/routes/front.py b/files/routes/front.py index 850c9b29f..8a14c3126 100644 --- a/files/routes/front.py +++ b/files/routes/front.py @@ -204,7 +204,7 @@ def front_all(v, sub=None, subdomain=None): site=SITE ) - posts = get_posts(ids, v=v) + posts = get_posts(ids, v=v, eager=True) if v: if v.hidevotedon: posts = [x for x in posts if not hasattr(x, 'voted') or not x.voted] diff --git a/files/routes/search.py b/files/routes/search.py index 62e1c478a..d67227084 100644 --- a/files/routes/search.py +++ b/files/routes/search.py @@ -160,7 +160,7 @@ def searchposts(v): next_exists = (len(ids) > 25) ids = ids[:25] - posts = get_posts(ids, v=v) + posts = get_posts(ids, v=v, eager=True) if request.headers.get("Authorization"): return {"total":total, "data":[x.json for x in posts]} diff --git a/files/routes/users.py b/files/routes/users.py index ceffb4942..e550f9ffc 100644 --- a/files/routes/users.py +++ b/files/routes/users.py @@ -90,7 +90,7 @@ def upvoters_posts(v, username, uid): next_exists = len(listing) > 25 listing = listing[:25] - listing = get_posts(listing, v=v) + listing = get_posts(listing, v=v, eager=True) return render_template("voted_posts.html", next_exists=next_exists, listing=listing, page=page, v=v) @@ -132,7 +132,7 @@ def downvoters_posts(v, username, uid): next_exists = len(listing) > 25 listing = listing[:25] - listing = get_posts(listing, v=v) + listing = get_posts(listing, v=v, eager=True) return render_template("voted_posts.html", next_exists=next_exists, listing=listing, page=page, v=v) @@ -173,7 +173,7 @@ def upvoting_posts(v, username, uid): next_exists = len(listing) > 25 listing = listing[:25] - listing = get_posts(listing, v=v) + listing = get_posts(listing, v=v, eager=True) return render_template("voted_posts.html", next_exists=next_exists, listing=listing, page=page, v=v) @@ -215,7 +215,7 @@ def downvoting_posts(v, username, uid): next_exists = len(listing) > 25 listing = listing[:25] - listing = get_posts(listing, v=v) + listing = get_posts(listing, v=v, eager=True) return render_template("voted_posts.html", next_exists=next_exists, listing=listing, page=page, v=v) @@ -824,7 +824,7 @@ def u_username(username, v=None): for p in sticky: ids = [p.id] + ids - listing = get_posts(ids, v=v) + listing = get_posts(ids, v=v, eager=True) if u.unban_utc: if request.headers.get("Authorization"): {"data": [x.json for x in listing]} @@ -1105,7 +1105,7 @@ def saved_posts(v, username): ids=ids[:25] - listing = get_posts(ids, v=v) + listing = get_posts(ids, v=v, eager=True) if request.headers.get("Authorization"): return {"data": [x.json for x in listing]} return render_template("userpage.html", From 5aaef144cf751a0668a2e746f63840132ce20912 Mon Sep 17 00:00:00 2001 From: TLSM Date: Mon, 28 Nov 2022 14:33:24 -0500 Subject: [PATCH 3/4] Deduplicate post/comment sorting & time filtering. Ported in from upstream with adjustments for TheMotte, most notably universal default to 'new' and fixes to 'hot'. Lumped into this PR because eager comment loading uses it. --- files/classes/user.py | 30 ++--------- files/helpers/contentsorting.py | 53 ++++++++++++++++++++ files/routes/front.py | 88 +++------------------------------ files/routes/posts.py | 46 ++--------------- files/routes/search.py | 82 +++--------------------------- files/routes/users.py | 28 ++--------- 6 files changed, 77 insertions(+), 250 deletions(-) create mode 100644 files/helpers/contentsorting.py diff --git a/files/classes/user.py b/files/classes/user.py index 4efbf44ee..ee65b14ed 100644 --- a/files/classes/user.py +++ b/files/classes/user.py @@ -19,6 +19,7 @@ from .sub_block import * from files.__main__ import app, Base, cache from files.helpers.security import * from files.helpers.assetcache import assetcache_path +from files.helpers.contentsorting import apply_time_filter, sort_objects import random from datetime import datetime from os import environ, remove, path @@ -292,33 +293,8 @@ class User(Base): if not (v and (v.admin_level > 1 or v.id == self.id)): posts = posts.filter_by(deleted_utc=0, is_banned=False, private=False, ghost=False) - now = int(time.time()) - if t == 'hour': - cutoff = now - 3600 - elif t == 'day': - cutoff = now - 86400 - elif t == 'week': - cutoff = now - 604800 - elif t == 'month': - cutoff = now - 2592000 - elif t == 'year': - cutoff = now - 31536000 - else: - cutoff = 0 - posts = posts.filter(Submission.created_utc >= cutoff) - - if sort == "new": - posts = posts.order_by(Submission.created_utc.desc()) - elif sort == "old": - posts = posts.order_by(Submission.created_utc) - elif sort == "controversial": - posts = posts.order_by((Submission.upvotes+1)/(Submission.downvotes+1) + (Submission.downvotes+1)/(Submission.upvotes+1), Submission.downvotes.desc()) - elif sort == "top": - posts = posts.order_by(Submission.downvotes - Submission.upvotes) - elif sort == "bottom": - posts = posts.order_by(Submission.upvotes - Submission.downvotes) - elif sort == "comments": - posts = posts.order_by(Submission.comment_count.desc()) + posts = apply_time_filter(posts, t, Submission) + posts = sort_objects(posts, sort, Submission) posts = posts.offset(25 * (page - 1)).limit(26).all() diff --git a/files/helpers/contentsorting.py b/files/helpers/contentsorting.py new file mode 100644 index 000000000..ec3bfb513 --- /dev/null +++ b/files/helpers/contentsorting.py @@ -0,0 +1,53 @@ +import time + +from sqlalchemy.sql import func + +from files.helpers.const import * + + +def apply_time_filter(objects, t, cls): + now = int(time.time()) + if t == 'hour': + cutoff = now - (60 * 60) + elif t == 'day': + cutoff = now - (24 * 60 * 60) + elif t == 'week': + cutoff = now - (7 * 24 * 60 * 60) + elif t == 'month': + cutoff = now - (30 * 24 * 60 * 60) + elif t == 'year': + cutoff = now - (365 * 24 * 60 * 60) + else: + cutoff = 0 + return objects.filter(cls.created_utc >= cutoff) + + +def sort_objects(objects, sort, cls): + if sort == 'hot': + ti = int(time.time()) + 3600 + return objects.order_by( + -100000 + * (cls.upvotes + 1) + / (func.power((ti - cls.created_utc) / 1000, 1.23)), + cls.created_utc.desc()) + elif sort == 'bump' and cls.__name__ == 'Submission': + return objects.filter(cls.comment_count > 1).order_by( + cls.bump_utc.desc(), cls.created_utc.desc()) + elif sort == 'comments' and cls.__name__ == 'Submission': + return objects.order_by( + cls.comment_count.desc(), cls.created_utc.desc()) + elif sort == 'controversial': + return objects.order_by( + (cls.upvotes + 1) / (cls.downvotes + 1) + + (cls.downvotes + 1) / (cls.upvotes + 1), + cls.downvotes.desc(), cls.created_utc.desc()) + elif sort == 'top': + return objects.order_by( + cls.downvotes - cls.upvotes, cls.created_utc.desc()) + elif sort == 'bottom': + return objects.order_by( + cls.upvotes - cls.downvotes, cls.created_utc.desc()) + elif sort == 'old': + return objects.order_by(cls.created_utc) + else: # default, or sort == 'new' + return objects.order_by(cls.created_utc.desc()) diff --git a/files/routes/front.py b/files/routes/front.py index 8a14c3126..57632a66a 100644 --- a/files/routes/front.py +++ b/files/routes/front.py @@ -2,6 +2,7 @@ from files.helpers.wrappers import * from files.helpers.get import * from files.__main__ import app, cache, limiter from files.classes.submission import Submission +from files.helpers.contentsorting import apply_time_filter, sort_objects defaulttimefilter = environ.get("DEFAULT_TIME_FILTER", "all").strip() @@ -250,15 +251,7 @@ def frontlist(v=None, sort='new', page=1, t="all", ids_only=True, ccmode="false" if lt: posts = posts.filter(Submission.created_utc < lt) if not gt and not lt: - if t == 'all': cutoff = 0 - else: - now = int(time.time()) - if t == 'hour': cutoff = now - 3600 - elif t == 'week': cutoff = now - 604800 - elif t == 'month': cutoff = now - 2592000 - elif t == 'year': cutoff = now - 31536000 - else: cutoff = now - 86400 - posts = posts.filter(Submission.created_utc >= cutoff) + posts = apply_time_filter(posts, t, Submission) if (ccmode == "true"): posts = posts.filter(Submission.club == True) @@ -282,27 +275,7 @@ def frontlist(v=None, sort='new', page=1, t="all", ids_only=True, ccmode="false" if not (v and v.shadowbanned): posts = posts.join(User, User.id == Submission.author_id).filter(User.shadowbanned == None) - if sort == "hot": - ti = int(time.time()) + 3600 - posts = posts.order_by( - -100000 - * (Submission.realupvotes + 1 + Submission.comment_count / 10) - / (func.power((ti - Submission.created_utc) / 1000, 1.23)), - Submission.created_utc.desc()) - elif sort == "bump": - posts = posts.filter(Submission.comment_count > 1).order_by(Submission.bump_utc.desc(), Submission.created_utc.desc()) - elif sort == "new": - posts = posts.order_by(Submission.created_utc.desc()) - elif sort == "old": - posts = posts.order_by(Submission.created_utc) - elif sort == "controversial": - posts = posts.order_by((Submission.upvotes+1)/(Submission.downvotes+1) + (Submission.downvotes+1)/(Submission.upvotes+1), Submission.downvotes.desc(), Submission.created_utc.desc()) - elif sort == "top": - posts = posts.order_by(Submission.downvotes - Submission.upvotes, Submission.created_utc.desc()) - elif sort == "bottom": - posts = posts.order_by(Submission.upvotes - Submission.downvotes, Submission.created_utc.desc()) - elif sort == "comments": - posts = posts.order_by(Submission.comment_count.desc(), Submission.created_utc.desc()) + posts = sort_objects(posts, sort, Submission) if v: size = v.frontsize or 0 else: size = 25 @@ -376,32 +349,8 @@ def changeloglist(v=None, sort="new", page=1, t="all", site=None): posts = posts.filter(Submission.title.ilike('_changelog%'), Submission.author_id.in_(admins)) if t != 'all': - cutoff = 0 - now = int(time.time()) - if t == 'hour': - cutoff = now - 3600 - elif t == 'day': - cutoff = now - 86400 - elif t == 'week': - cutoff = now - 604800 - elif t == 'month': - cutoff = now - 2592000 - elif t == 'year': - cutoff = now - 31536000 - posts = posts.filter(Submission.created_utc >= cutoff) - - if sort == "new": - posts = posts.order_by(Submission.created_utc.desc()) - elif sort == "old": - posts = posts.order_by(Submission.created_utc) - elif sort == "controversial": - posts = posts.order_by((Submission.upvotes+1)/(Submission.downvotes+1) + (Submission.downvotes+1)/(Submission.upvotes+1), Submission.downvotes.desc()) - elif sort == "top": - posts = posts.order_by(Submission.downvotes - Submission.upvotes) - elif sort == "bottom": - posts = posts.order_by(Submission.upvotes - Submission.downvotes) - elif sort == "comments": - posts = posts.order_by(Submission.comment_count.desc()) + posts = apply_time_filter(posts, t, Submission) + posts = sort_objects(posts, sort, Submission) posts = posts.offset(25 * (page - 1)).limit(26).all() @@ -477,31 +426,8 @@ def get_comments_idlist(page=1, v=None, sort="new", t="all", gt=0, lt=0): if lt: comments = comments.filter(Comment.created_utc < lt) if not gt and not lt: - now = int(time.time()) - if t == 'hour': - cutoff = now - 3600 - elif t == 'day': - cutoff = now - 86400 - elif t == 'week': - cutoff = now - 604800 - elif t == 'month': - cutoff = now - 2592000 - elif t == 'year': - cutoff = now - 31536000 - else: - cutoff = 0 - comments = comments.filter(Comment.created_utc >= cutoff) - - if sort == "new": - comments = comments.order_by(Comment.created_utc.desc()) - elif sort == "old": - comments = comments.order_by(Comment.created_utc) - elif sort == "controversial": - comments = comments.order_by((Comment.upvotes+1)/(Comment.downvotes+1) + (Comment.downvotes+1)/(Comment.upvotes+1), Comment.downvotes.desc()) - elif sort == "top": - comments = comments.order_by(Comment.downvotes - Comment.upvotes) - elif sort == "bottom": - comments = comments.order_by(Comment.upvotes - Comment.downvotes) + comments = apply_time_filter(comments, t, Comment) + comments = sort_objects(comments, sort, Comment) comments = comments.offset(25 * (page - 1)).limit(26).all() return [x[0] for x in comments] diff --git a/files/routes/posts.py b/files/routes/posts.py index 13d5ebd1c..c066383fa 100644 --- a/files/routes/posts.py +++ b/files/routes/posts.py @@ -4,6 +4,7 @@ from files.helpers.wrappers import * from files.helpers.sanitize import * from files.helpers.strings import sql_ilike_clean from files.helpers.alerts import * +from files.helpers.contentsorting import sort_objects from files.helpers.const import * from files.classes import * from flask import * @@ -185,34 +186,13 @@ def post_id(pid, anything=None, v=None, sub=None): pinned = [c[0] for c in comments.filter(Comment.is_pinned != None).all()] comments = comments.filter(Comment.level == 1, Comment.is_pinned == None) - - if sort == "new": - comments = comments.order_by(Comment.created_utc.desc()) - elif sort == "old": - comments = comments.order_by(Comment.created_utc) - elif sort == "controversial": - comments = comments.order_by((Comment.upvotes+1)/(Comment.downvotes+1) + (Comment.downvotes+1)/(Comment.upvotes+1), Comment.downvotes.desc()) - elif sort == "top": - comments = comments.order_by(Comment.realupvotes.desc()) - elif sort == "bottom": - comments = comments.order_by(Comment.upvotes - Comment.downvotes) - + comments = sort_objects(comments, sort, Comment) comments = [c[0] for c in comments.all()] else: pinned = g.db.query(Comment).filter(Comment.parent_submission == post.id, Comment.is_pinned != None).all() comments = g.db.query(Comment).join(User, User.id == Comment.author_id).filter(User.shadowbanned == None, Comment.parent_submission == post.id, Comment.level == 1, Comment.is_pinned == None) - - if sort == "new": - comments = comments.order_by(Comment.created_utc.desc()) - elif sort == "old": - comments = comments.order_by(Comment.created_utc) - elif sort == "controversial": - comments = comments.order_by((Comment.upvotes+1)/(Comment.downvotes+1) + (Comment.downvotes+1)/(Comment.upvotes+1), Comment.downvotes.desc()) - elif sort == "top": - comments = comments.order_by(Comment.realupvotes.desc()) - elif sort == "bottom": - comments = comments.order_by(Comment.upvotes - Comment.downvotes) + comments = sort_objects(comments, sort, Comment) filter_clause = (Comment.filter_state != 'filtered') & (Comment.filter_state != 'removed') comments = comments.filter(filter_clause) @@ -325,15 +305,7 @@ def viewmore(v, pid, sort, offset): if sort == "new": comments = comments.filter(Comment.created_utc < newest.created_utc) - comments = comments.order_by(Comment.created_utc.desc()) - elif sort == "old": - comments = comments.order_by(Comment.created_utc) - elif sort == "controversial": - comments = comments.order_by((Comment.upvotes+1)/(Comment.downvotes+1) + (Comment.downvotes+1)/(Comment.upvotes+1), Comment.downvotes.desc()) - elif sort == "top": - comments = comments.order_by(Comment.realupvotes.desc()) - elif sort == "bottom": - comments = comments.order_by(Comment.upvotes - Comment.downvotes) + comments = sort_objects(comments, sort, Comment) comments = [c[0] for c in comments.all()] else: @@ -341,15 +313,7 @@ def viewmore(v, pid, sort, offset): if sort == "new": comments = comments.filter(Comment.created_utc < newest.created_utc) - comments = comments.order_by(Comment.created_utc.desc()) - elif sort == "old": - comments = comments.order_by(Comment.created_utc) - elif sort == "controversial": - comments = comments.order_by((Comment.upvotes+1)/(Comment.downvotes+1) + (Comment.downvotes+1)/(Comment.upvotes+1), Comment.downvotes.desc()) - elif sort == "top": - comments = comments.order_by(Comment.realupvotes.desc()) - elif sort == "bottom": - comments = comments.order_by(Comment.upvotes - Comment.downvotes) + comments = sort_objects(comments, sort, Comment) comments = comments.all() comments = comments[offset:] diff --git a/files/routes/search.py b/files/routes/search.py index d67227084..850e81589 100644 --- a/files/routes/search.py +++ b/files/routes/search.py @@ -3,6 +3,7 @@ import re from sqlalchemy import * from flask import * from files.__main__ import app +from files.helpers.contentsorting import apply_time_filter, sort_objects from files.helpers.strings import sql_ilike_clean @@ -13,8 +14,6 @@ valid_params=[ ] def searchparse(text): - - criteria = {x[0]:x[1] for x in query_regex.findall(text)} for x in criteria: @@ -29,13 +28,9 @@ def searchparse(text): return criteria - - - @app.get("/search/posts") @auth_desired def searchposts(v): - query = request.values.get("q", '').strip() page = max(1, int(request.values.get("page", 1))) @@ -45,17 +40,6 @@ def searchposts(v): criteria=searchparse(query) - - - - - - - - - - - posts = g.db.query(Submission.id) if not (v and v.paid_dues): posts = posts.filter_by(club=False) @@ -118,35 +102,9 @@ def searchposts(v): ) ) - if t: - now = int(time.time()) - if t == 'hour': - cutoff = now - 3600 - elif t == 'day': - cutoff = now - 86400 - elif t == 'week': - cutoff = now - 604800 - elif t == 'month': - cutoff = now - 2592000 - elif t == 'year': - cutoff = now - 31536000 - else: - cutoff = 0 - posts = posts.filter(Submission.created_utc >= cutoff) - - if sort == "new": - posts = posts.order_by(Submission.created_utc.desc()) - elif sort == "old": - posts = posts.order_by(Submission.created_utc) - elif sort == "controversial": - posts = posts.order_by((Submission.upvotes+1)/(Submission.downvotes+1) + (Submission.downvotes+1)/(Submission.upvotes+1), Submission.downvotes.desc()) - elif sort == "top": - posts = posts.order_by(Submission.downvotes - Submission.upvotes) - elif sort == "bottom": - posts = posts.order_by(Submission.upvotes - Submission.downvotes) - elif sort == "comments": - posts = posts.order_by(Submission.comment_count.desc()) + posts = apply_time_filter(posts, t, Submission) + posts = sort_objects(posts, sort, Submission) total = posts.count() @@ -155,8 +113,6 @@ def searchposts(v): ids = [x[0] for x in posts] - - next_exists = (len(ids) > 25) ids = ids[:25] @@ -175,11 +131,10 @@ def searchposts(v): next_exists=next_exists ) + @app.get("/search/comments") @auth_desired def searchcomments(v): - - query = request.values.get("q", '').strip() try: page = max(1, int(request.values.get("page", 1))) @@ -213,21 +168,7 @@ def searchcomments(v): if 'over18' in criteria: comments = comments.filter(Comment.over_18 == True) if t: - now = int(time.time()) - if t == 'hour': - cutoff = now - 3600 - elif t == 'day': - cutoff = now - 86400 - elif t == 'week': - cutoff = now - 604800 - elif t == 'month': - cutoff = now - 2592000 - elif t == 'year': - cutoff = now - 31536000 - else: - cutoff = 0 - comments = comments.filter(Comment.created_utc >= cutoff) - + comments = apply_time_filter(comments, t, Comment) if v and v.admin_level < 2: private = [x[0] for x in g.db.query(Submission.id).filter(Submission.private == True).all()] @@ -241,17 +182,7 @@ def searchcomments(v): club = [x[0] for x in g.db.query(Submission.id).filter(Submission.club == True).all()] comments = comments.filter(Comment.parent_submission.notin_(club)) - - if sort == "new": - comments = comments.order_by(Comment.created_utc.desc()) - elif sort == "old": - comments = comments.order_by(Comment.created_utc) - elif sort == "controversial": - comments = comments.order_by((Comment.upvotes+1)/(Comment.downvotes+1) + (Comment.downvotes+1)/(Comment.upvotes+1), Comment.downvotes.desc()) - elif sort == "top": - comments = comments.order_by(Comment.downvotes - Comment.upvotes) - elif sort == "bottom": - comments = comments.order_by(Comment.upvotes - Comment.downvotes) + comments = sort_objects(comments, sort, Comment) total = comments.count() @@ -271,7 +202,6 @@ def searchcomments(v): @app.get("/search/users") @auth_desired def searchusers(v): - query = request.values.get("q", '').strip() page = max(1, int(request.values.get("page", 1))) diff --git a/files/routes/users.py b/files/routes/users.py index e550f9ffc..9303d26a2 100644 --- a/files/routes/users.py +++ b/files/routes/users.py @@ -8,6 +8,7 @@ from files.helpers.sanitize import * from files.helpers.strings import sql_ilike_clean from files.helpers.const import * from files.helpers.assetcache import assetcache_path +from files.helpers.contentsorting import apply_time_filter, sort_objects from files.mail import * from flask import * from files.__main__ import app, limiter, db_session @@ -899,31 +900,8 @@ def u_username_comments(username, v=None): (Comment.filter_state != 'filtered') & (Comment.filter_state != 'removed') ) - now = int(time.time()) - if t == 'hour': - cutoff = now - 3600 - elif t == 'day': - cutoff = now - 86400 - elif t == 'week': - cutoff = now - 604800 - elif t == 'month': - cutoff = now - 2592000 - elif t == 'year': - cutoff = now - 31536000 - else: - cutoff = 0 - comments = comments.filter(Comment.created_utc >= cutoff) - - if sort == "new": - comments = comments.order_by(Comment.created_utc.desc()) - elif sort == "old": - comments = comments.order_by(Comment.created_utc) - elif sort == "controversial": - comments = comments.order_by((Comment.upvotes+1)/(Comment.downvotes+1) + (Comment.downvotes+1)/(Comment.upvotes+1), Comment.downvotes.desc()) - elif sort == "top": - comments = comments.order_by(Comment.downvotes - Comment.upvotes) - elif sort == "bottom": - comments = comments.order_by(Comment.upvotes - Comment.downvotes) + comments = apply_time_filter(comments, t, Comment) + comments = sort_objects(comments, sort, Comment) comments = comments.offset(25 * (page - 1)).limit(26).all() ids = [x.id for x in comments] From afe209d5d8034d19ea85b33c9133ab81e649f716 Mon Sep 17 00:00:00 2001 From: TLSM Date: Mon, 28 Nov 2022 17:47:54 -0500 Subject: [PATCH 4/4] Eager load comments for post rendering. GET /post/1/clever-unique-post-title-number-0 |----------|--------|--------|--------|--------|--------|------------| | Database | SELECT | INSERT | UPDATE | DELETE | Totals | Duplicates | |----------|--------|--------|--------|--------|--------|------------| | default | 942 | 0 | 1 | 0 | 943 | 921 | |----------|--------|--------|--------|--------|--------|------------| Total queries: 943 in 0.377s # request time in browser 17249ms GET /post/1/clever-unique-post-title-number-0 |----------|--------|--------|--------|--------|--------|------------| | Database | SELECT | INSERT | UPDATE | DELETE | Totals | Duplicates | |----------|--------|--------|--------|--------|--------|------------| | default | 58 | 0 | 1 | 0 | 59 | 35 | |----------|--------|--------|--------|--------|--------|------------| Total queries: 59 in 0.0423s # request time in browser 544ms Also, fixes seed_db not populating top_comment_id on generated comments. If you want to test locally with seed_db test data, you need to reseed. --- files/classes/comment.py | 12 ++++-- files/commands/seed_db.py | 8 +++- files/helpers/contentsorting.py | 34 ++++++++++++++++ files/helpers/get.py | 72 +++++++++++++++++++++++++++++++++ files/routes/posts.py | 7 +++- 5 files changed, 125 insertions(+), 8 deletions(-) diff --git a/files/classes/comment.py b/files/classes/comment.py index 8e3a899cf..81049e244 100644 --- a/files/classes/comment.py +++ b/files/classes/comment.py @@ -58,8 +58,13 @@ class Comment(Base): senttouser = relationship("User", primaryjoin="User.id==Comment.sentto", viewonly=True) parent_comment = relationship("Comment", remote_side=[id], viewonly=True) child_comments = relationship("Comment", lazy="dynamic", remote_side=[parent_comment_id], viewonly=True) - awards = relationship("AwardRelationship", viewonly=True) - reports = relationship("CommentFlag", viewonly=True) + awards = relationship("AwardRelationship", + primaryjoin="AwardRelationship.comment_id == Comment.id", + viewonly=True) + reports = relationship("CommentFlag", + primaryjoin="CommentFlag.comment_id == Comment.id", + order_by="CommentFlag.created_utc", + viewonly=True) notes = relationship("UserNote", back_populates="comment") def __init__(self, *args, **kwargs): @@ -70,7 +75,6 @@ class Comment(Base): super().__init__(*args, **kwargs) def __repr__(self): - return f"" @property @@ -87,7 +91,7 @@ class Comment(Base): @lazy def flags(self, v): - flags = g.db.query(CommentFlag).filter_by(comment_id=self.id).order_by(CommentFlag.created_utc).all() + flags = self.reports if not (v and (v.shadowbanned or v.admin_level > 2)): for flag in flags: if flag.user.shadowbanned: diff --git a/files/commands/seed_db.py b/files/commands/seed_db.py index 00b41cf73..6ea27008b 100644 --- a/files/commands/seed_db.py +++ b/files/commands/seed_db.py @@ -130,8 +130,12 @@ def seed_db(): db.session.add(comment) comments.append(comment) - db.session.commit() db.session.flush() + for c in comments: + c.top_comment_id = c.id + db.session.add(c) + + db.session.commit() print(f"Creating {NUM_REPLY_COMMENTS} reply comments") for i in range(NUM_REPLY_COMMENTS): @@ -143,6 +147,7 @@ def seed_db(): author_id=user.id, parent_submission=str(parent.post.id), parent_comment_id=parent.id, + top_comment_id=parent.top_comment_id, level=parent.level + 1, over_18=False, is_bot=False, @@ -155,7 +160,6 @@ def seed_db(): comments.append(comment) db.session.commit() - db.session.flush() print("Updating comment counts for all posts") for post in posts: diff --git a/files/helpers/contentsorting.py b/files/helpers/contentsorting.py index ec3bfb513..bdb4fb1db 100644 --- a/files/helpers/contentsorting.py +++ b/files/helpers/contentsorting.py @@ -51,3 +51,37 @@ def sort_objects(objects, sort, cls): return objects.order_by(cls.created_utc) else: # default, or sort == 'new' return objects.order_by(cls.created_utc.desc()) + + +# Presently designed around files.helpers.get.get_comment_trees_eager +# Behavior should parallel that of sort_objects above. TODO: Unify someday? +def sort_comment_results(comments, sort): + DESC = (2 << 30) - 1 # descending sorts, Y2038 problem, change before then + if sort == 'hot': + ti = int(time.time()) + 3600 + key_func = lambda c: ( + -100000 + * (c.upvotes + 1) + / (pow(((ti - c.created_utc) / 1000), 1.23)), + DESC - c.created_utc + ) + elif sort == 'controversial': + key_func = lambda c: ( + (c.upvotes + 1) / (c.downvotes + 1) + + (c.downvotes + 1) / (c.upvotes + 1), + DESC - c.downvotes, + DESC - c.created_utc + ) + elif sort == 'top': + key_func = lambda c: (c.downvotes - c.upvotes, DESC - c.created_utc) + elif sort == 'bottom': + key_func = lambda c: (c.upvotes - c.downvotes, DESC - c.created_utc) + elif sort == 'old': + key_func = lambda c: c.created_utc + else: # default, or sort == 'new' + key_func = lambda c: DESC - c.created_utc + + key_func_pinned = lambda c: ( + (c.is_pinned is None, c.is_pinned == '', c.is_pinned), # sort None last + key_func(c)) + return sorted(comments, key=key_func_pinned) diff --git a/files/helpers/get.py b/files/helpers/get.py index c68679d3f..886b31677 100644 --- a/files/helpers/get.py +++ b/files/helpers/get.py @@ -1,3 +1,4 @@ +from collections import defaultdict from typing import Iterable, List, Optional, Type, Union from flask import g @@ -6,6 +7,7 @@ from sqlalchemy.orm import selectinload from files.classes import * from files.helpers.const import AUTOJANNY_ID +from files.helpers.contentsorting import sort_comment_results from files.helpers.strings import sql_ilike_clean @@ -276,6 +278,76 @@ def get_comments( return sorted(output, key=lambda x: cids.index(x.id)) +# TODO: There is probably some way to unify this with get_comments. However, in +# the interim, it's a hot path and benefits from having tailored code. +def get_comment_trees_eager( + top_comment_ids:Iterable[int], + sort:str="old", + v:Optional[User]=None) -> List[Comment]: + + if v: + votes = g.db.query(CommentVote).filter_by(user_id=v.id).subquery() + blocking = v.blocking.subquery() + blocked = v.blocked.subquery() + + query = g.db.query( + Comment, + votes.c.vote_type, + blocking.c.target_id, + blocked.c.target_id, + ).join( + votes, votes.c.comment_id==Comment.id, isouter=True + ).join( + blocking, + blocking.c.target_id == Comment.author_id, + isouter=True + ).join( + blocked, + blocked.c.user_id == Comment.author_id, + isouter=True + ) + else: + query = g.db.query(Comment) + + query = query.filter(Comment.top_comment_id.in_(top_comment_ids)) + query = query.options( + selectinload(Comment.author).options( + selectinload(User.badges), + selectinload(User.notes), + ), + selectinload(Comment.reports).options( + selectinload(CommentFlag.user), + ), + selectinload(Comment.awards), + ) + results = query.all() + + if v: + comments = [c[0] for c in results] + for i in range(len(comments)): + comments[i].voted = results[i][1] or 0 + comments[i].is_blocking = results[i][2] or 0 + comments[i].is_blocked = results[i][3] or 0 + else: + comments = results + + comments_map = {} + comments_map_parent = defaultdict(lambda: list()) + for c in comments: + c.replies2 = [] + comments_map[c.id] = c + comments_map_parent[c.parent_comment_id].append(c) + + for parent_id in comments_map_parent: + if parent_id is None: continue + + comments_map_parent[parent_id] = sort_comment_results( + comments_map_parent[parent_id], sort) + comments_map[parent_id].replies2 = comments_map_parent[parent_id] + + return [comments_map[tcid] for tcid in top_comment_ids] + + # TODO: This function was concisely inlined into posts.py in upstream. # Think it involved adding `tldextract` as a dependency. def get_domain(s:str) -> Optional[BannedDomain]: diff --git a/files/routes/posts.py b/files/routes/posts.py index c066383fa..de2c55300 100644 --- a/files/routes/posts.py +++ b/files/routes/posts.py @@ -231,11 +231,14 @@ def post_id(pid, anything=None, v=None, sub=None): g.db.add(pin) pinned.remove(pin) - post.replies = pinned + comments + top_comments = pinned + comments + top_comment_ids = [c.id for c in top_comments] + post.replies = get_comment_trees_eager(top_comment_ids, sort, v) post.views += 1 g.db.add(post) - g.db.commit() + g.db.flush() + if request.headers.get("Authorization"): return post.json else: if post.is_banned and not (v and (v.admin_level > 1 or post.author_id == v.id)): template = "submission_banned.html"