Merge remote-tracking branch 'upstream/frost' into 601_convert_created_utc_to_timestamp_for_commentvotes

This commit is contained in:
Viet Than 2023-07-21 23:00:16 -04:00
commit 9c24f3957a
74 changed files with 1409 additions and 2254 deletions

View file

@ -178,7 +178,7 @@ div.deleted.removed {
background-color: var(--gray) !important;
}
.comment-anchor:target, .unread {
background: #2280B310 !important;
background: #2280B310;
}
#frontpage .posts .card, #userpage .posts .card, #search .posts .card {
border: none;

View file

@ -13,6 +13,7 @@
--gray-800: #1e293b;
--gray-900: #0f172a;
--background: #2d3c50;
--white: #fff;
}
body, .container.transparent, .card {
@ -141,3 +142,11 @@ color: var(--gray-700);
#frontpage .post-title a:visited, .visited {
color: #6e6e6e !important;
}
.custom-switch .custom-control-label::after {
background-color: var(--white);
}
.bg-white {
background-color: var(--background) !important;
}

View file

@ -2785,6 +2785,15 @@ ol > li::before {
#logo {
margin-left: 0.5rem;
}
.logo-text {
font-size: 1.5rem;
font-weight: 700;
color: var(--black)
}
.logo-text:hover {
text-decoration: none;
color: var(--black)
}
.navbar-brand, .navbar-light .navbar-brand {
color: var(--primary);
font-weight: 600;
@ -4924,7 +4933,7 @@ html {
display: block;
}
.comment-anchor:target, .unread {
background: #ffffff22 !important;
background: #ffffff22;
padding: 12px;
padding-bottom: 4px;
}
@ -5368,4 +5377,4 @@ div[id^="reply-edit-"] li > p:first-child {
pre {
line-height: .5rem;
}
}

View file

@ -1,16 +1,16 @@
import math
from typing import TYPE_CHECKING, Literal, Optional
from urllib.parse import parse_qs, urlencode, urlparse
from flask import g
import math
from sqlalchemy import *
from sqlalchemy.orm import relationship
from files.classes.base import CreatedBase
from files.classes.visstate import StateMod, StateReport
from files.classes.visstate import StateMod, StateReport, VisibilityState
from files.helpers.config.const import *
from files.helpers.config.environment import SCORE_HIDING_TIME_HOURS, SITE_FULL
from files.helpers.content import (ModerationState, body_displayed,
from files.helpers.content import (body_displayed,
execute_shadowbanned_fake_votes)
from files.helpers.lazy import lazy
from files.helpers.math import clamp
@ -139,7 +139,7 @@ class Comment(CreatedBase):
@property
@lazy
def fullname(self):
return f"t3_{self.id}"
return f"comment_{self.id}"
@property
@lazy
@ -151,8 +151,8 @@ class Comment(CreatedBase):
@property
@lazy
def parent_fullname(self):
if self.parent_comment_id: return f"t3_{self.parent_comment_id}"
elif self.parent_submission: return f"t2_{self.parent_submission}"
if self.parent_comment_id: return f"comment_{self.parent_comment_id}"
elif self.parent_submission: return f"post_{self.parent_submission}"
def replies(self, user):
if self.replies2 != None: return [x for x in self.replies2 if not x.author.shadowbanned]
@ -359,7 +359,7 @@ class Comment(CreatedBase):
return self.is_message and not self.sentto
@lazy
def header_msg(self, v, is_notification_page:bool, reply_count:int) -> str:
def header_msg(self, v, is_notification_page: bool) -> str:
'''
Returns a message that is in the header for a comment, usually for
display on a notification page.
@ -367,9 +367,9 @@ class Comment(CreatedBase):
if self.post:
post_html:str = f"<a href=\"{self.post.permalink}\">{self.post.realtitle(v)}</a>"
if v:
if v.id == self.author_id and reply_count:
text = f"Comment {'Replies' if reply_count != 1 else 'Reply'}"
elif v.id == self.post.author_id and self.level == 1:
if self.level > 1 and v.id == self.parent_comment.author_id:
text = "Comment Reply"
elif self.level == 1 and v.id == self.post.author_id:
text = "Post Reply"
elif self.parent_submission in v.subscribed_idlist():
text = "Subscribed Thread"
@ -420,22 +420,23 @@ class Comment(CreatedBase):
@lazy
def show_descendants(self, v:"User | None") -> bool:
if self.moderation_state.is_visible_to(v, getattr(self, 'is_blocking', False)):
if self.visibility_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]:
def visibility_and_message(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))
return self.visibility_state.visibility_and_message(
v, getattr(self, 'is_blocking', False))
@property
def moderation_state(self) -> ModerationState:
return ModerationState.from_submittable(self)
def visibility_state(self) -> VisibilityState:
return VisibilityState.from_submittable(self)
def volunteer_janitor_is_unknown(self):
return self.volunteer_janitor_badness > 0.4 and self.volunteer_janitor_badness < 0.6

View file

@ -8,9 +8,9 @@ from sqlalchemy.sql.sqltypes import Boolean, Integer, String, Text
from files.classes.cron.tasks import (RepeatableTask, ScheduledTaskType,
TaskRunContext)
from files.classes.submission import Submission
from files.classes.visstate import StateMod
from files.classes.visstate import StateMod, StateReport, VisibilityState
from files.helpers.config.const import SUBMISSION_TITLE_LENGTH_MAXIMUM
from files.helpers.content import ModerationState, body_displayed
from files.helpers.content import body_displayed
from files.helpers.lazy import lazy
from files.helpers.sanitize import filter_emojis_only
@ -173,13 +173,12 @@ class ScheduledSubmissionTask(RepeatableTask):
return f"/tasks/scheduled_posts/{self.id}/content"
@property
def moderation_state(self) -> ModerationState:
return ModerationState(
removed=False,
removed_by_name=None,
def visibility_state(self) -> VisibilityState:
return VisibilityState(
state_mod=StateMod.VISIBLE,
state_mod_set_by=None,
state_report=StateReport.UNREPORTED,
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

View file

@ -6,13 +6,13 @@ from sqlalchemy.orm import Session, declared_attr, deferred, relationship
from files.classes.base import CreatedBase
from files.classes.flags import Flag
from files.classes.visstate import StateMod, StateReport
from files.classes.visstate import StateMod, StateReport, VisibilityState
from files.classes.votes import Vote
from files.helpers.assetcache import assetcache_path
from files.helpers.config.const import *
from files.helpers.config.environment import (SCORE_HIDING_TIME_HOURS, SITE,
SITE_FULL, SITE_ID)
from files.helpers.content import ModerationState, body_displayed
from files.helpers.content import body_displayed
from files.helpers.lazy import lazy
from files.helpers.time import format_age, format_datetime
@ -164,7 +164,7 @@ class Submission(CreatedBase):
@property
@lazy
def fullname(self):
return f"t2_{self.id}"
return f"post_{self.id}"
@property
@lazy
@ -357,5 +357,5 @@ class Submission(CreatedBase):
return f"/edit_post/{self.id}"
@property
def moderation_state(self) -> ModerationState:
return ModerationState.from_submittable(self)
def visibility_state(self) -> VisibilityState:
return VisibilityState.from_submittable(self)

View file

@ -243,7 +243,7 @@ class User(CreatedBase):
@property
@lazy
def fullname(self):
return f"t1_{self.id}"
return f"user_{self.id}"
@property
@lazy
@ -348,25 +348,15 @@ class User(CreatedBase):
def post_notifications_count(self):
return g.db.query(Notification.user_id).join(Comment).filter(Notification.user_id == self.id, Notification.read == False, Comment.author_id == AUTOJANNY_ID).count()
@property
@lazy
def reddit_notifications_count(self):
return g.db.query(Notification.user_id).join(Comment).filter(Notification.user_id == self.id, Notification.read == False, Comment.state_mod == StateMod.VISIBLE, Comment.state_user_deleted_utc == None, Comment.body_html.like('%<p>New site mention: <a href="https://old.reddit.com/r/%'), Comment.parent_submission == None, Comment.author_id == NOTIFICATIONS_ID).count()
@property
@lazy
def normal_count(self):
return self.notifications_count - self.post_notifications_count - self.reddit_notifications_count
return self.notifications_count - self.post_notifications_count
@property
@lazy
def do_posts(self):
return self.post_notifications_count and self.notifications_count-self.reddit_notifications_count == self.post_notifications_count
@property
@lazy
def do_reddit(self):
return self.notifications_count == self.reddit_notifications_count
return self.post_notifications_count and self.notifications_count == self.post_notifications_count
@property
@lazy

View file

@ -1,5 +1,15 @@
from __future__ import annotations
import enum
from dataclasses import dataclass
from typing import TYPE_CHECKING
from files.helpers.config.const import PERMS
if TYPE_CHECKING:
from files.classes.user import User
from files.helpers.content import Submittable
class StateMod(enum.Enum):
VISIBLE = 0
@ -11,3 +21,107 @@ class StateReport(enum.Enum):
RESOLVED = 1
REPORTED = 2
IGNORED = 3
@dataclass(frozen=True, kw_only=True, slots=True)
class VisibilityState:
'''
The full moderation state machine. It holds the moderation state, report
state, deleted information, and shadowban information. A decision to show
or hide a post or comment should be able to be done with information from
this alone.
'''
state_mod: StateMod
state_mod_set_by: str | None
state_report: StateReport
state_mod_set_by: str | None
deleted: bool
op_shadowbanned: bool
op_id: int
op_name_safe: str
@property
def removed(self) -> bool:
return self.state_mod == StateMod.REMOVED
@property
def filtered(self) -> bool:
return self.state_mod == StateMod.FILTERED
@property
def reports_ignored(self) -> bool:
return self.state_report == StateReport.IGNORED
@classmethod
def from_submittable(cls, target: Submittable) -> "VisibilityState":
return cls(
state_mod=target.state_mod,
state_mod_set_by=target.state_mod_set_by, # type: ignore
state_report=target.state_report,
deleted=bool(target.state_user_deleted_utc != None),
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
def visibility_and_message(self, v: User | None, is_blocking: bool) -> 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.
'''
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.state_mod_set_by:
msg = f'Removed by @{self.state_mod_set_by}'
return False, msg
if self.filtered and not can_moderate:
return False, 'Filtered'
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_and_message(v, is_blocking)[0]
def replacement_message(self, v: User | None, is_blocking: bool) -> str:
return self.visibility_and_message(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

View file

@ -2,14 +2,10 @@ from __future__ import annotations
import random
import urllib.parse
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Optional
from sqlalchemy.orm import Session
from files.helpers.config.const import PERMS
from files.classes.visstate import StateMod, StateReport
if TYPE_CHECKING:
from files.classes import Comment, Submission, User
Submittable = Comment | Submission
@ -90,100 +86,8 @@ def canonicalize_url2(url:str, *, httpsify:bool=False) -> urllib.parse.ParseResu
return url_parsed
@dataclass(frozen=True, kw_only=True, slots=True)
class ModerationState:
'''
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.state_mod != StateMod.VISIBLE),
removed_by_name=target.state_mod_set_by, # type: ignore
deleted=bool(target.state_user_deleted_utc != None),
reports_ignored=bool(target.state_report == StateReport.IGNORED),
filtered=bool(target.state_mod == StateMod.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
def visibility_state(self, v: User | None, is_blocking: bool) -> 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.
'''
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'
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:
moderated:Optional[str] = target.moderation_state.moderated_body(v)
moderated:Optional[str] = target.visibility_state.moderated_body(v)
if moderated: return moderated
body = target.body_html if is_html else target.body

View file

@ -45,7 +45,7 @@ def frontlist(v=None, sort='new', page=1, t="all", ids_only=True, ccmode="false"
if (ccmode == "true"):
posts = posts.filter(Submission.club == True)
posts = posts.filter_by(state_mod=StateMod.VISIBLE, private=False, state_user_deleted_utc=None)
posts = posts.filter_by(private=False, state_user_deleted_utc=None)
if ccmode == "false" and not gt and not lt:
posts = posts.filter_by(stickied=None)

View file

@ -184,9 +184,6 @@ def sanitize_raw(sanitized:Optional[str], allow_newlines:bool, length_limit:Opti
@with_gevent_timeout(2)
def sanitize(sanitized, alert=False, comment=False, edit=False):
# double newlines, eg. hello\nworld becomes hello\n\nworld, which later becomes <p>hello</p><p>world</p>
sanitized = linefeeds_regex.sub(r'\1\n\n\2', sanitized)
if MULTIMEDIA_EMBEDDING_ENABLED:
# turn eg. https://wikipedia.org/someimage.jpg into ![](https://wikipedia.org/someimage.jpg)
sanitized = image_regex.sub(r'\1![](\2)\4', sanitized)

View file

@ -26,7 +26,7 @@ def pusher_thread2(interests, notifbody, username):
'notification': {
'title': f'New message from @{username}',
'body': notifbody,
'deep_link': f'{SITE_FULL}/notifications?messages=true',
'deep_link': f'{SITE_FULL}/notifications/messages',
'icon': SITE_FULL + assetcache_path(f'images/{SITE_ID}/icon.webp'),
}
},
@ -36,7 +36,7 @@ def pusher_thread2(interests, notifbody, username):
'body': notifbody,
},
'data': {
'url': '/notifications?messages=true',
'url': '/notifications/messages',
}
}
},

View file

@ -111,16 +111,15 @@ def api_comment(v):
parent_fullname = request.values.get("parent_fullname", "").strip()
if len(parent_fullname) < 4: abort(400)
id = parent_fullname[3:]
parent = None
parent_post = None
parent_comment_id = None
if parent_fullname.startswith("t2_"):
parent = get_post(id, v=v)
if parent_fullname.startswith("post_"):
parent = get_post(parent_fullname.split("post_")[1], v=v)
parent_post = parent
elif parent_fullname.startswith("t3_"):
parent = get_comment(id, v=v)
elif parent_fullname.startswith("comment_"):
parent = get_comment(parent_fullname.split("comment_")[1], v=v)
parent_post = get_post(parent.parent_submission, v=v) if parent.parent_submission else None
parent_comment_id = parent.id
else: abort(400)

View file

@ -42,124 +42,149 @@ def unread(v):
@app.get("/notifications")
@auth_required
def notifications(v):
try: page = max(int(request.values.get("page", 1)), 1)
except: page = 1
def notifications_main(v: User):
page: int = max(request.values.get("page", 1, int) or 1, 1)
messages = request.values.get('messages')
modmail = request.values.get('modmail')
posts = request.values.get('posts')
reddit = request.values.get('reddit')
if modmail and v.admin_level >= 2:
comments = g.db.query(Comment).filter(Comment.sentto == MODMAIL_ID).order_by(Comment.id.desc()).offset(25*(page-1)).limit(26).all()
next_exists = (len(comments) > 25)
listing = comments[:25]
elif messages:
if v and (v.shadowbanned or v.admin_level >= 3):
comments = g.db.query(Comment).filter(Comment.sentto != None, or_(Comment.author_id==v.id, Comment.sentto==v.id), Comment.parent_submission == None, Comment.level == 1).order_by(Comment.id.desc()).offset(25*(page-1)).limit(26).all()
else:
comments = g.db.query(Comment).join(User, User.id == Comment.author_id).filter(User.shadowbanned == None, Comment.sentto != None, or_(Comment.author_id==v.id, Comment.sentto==v.id), Comment.parent_submission == None, Comment.level == 1).order_by(Comment.id.desc()).offset(25*(page-1)).limit(26).all()
next_exists = (len(comments) > 25)
listing = comments[:25]
elif posts:
notifications = g.db.query(Notification, Comment).join(Comment, Notification.comment_id == Comment.id).filter(Notification.user_id == v.id, Comment.author_id == AUTOJANNY_ID).order_by(Notification.created_utc.desc()).offset(25 * (page - 1)).limit(101).all()
listing = []
for index, x in enumerate(notifications[:100]):
n, c = x
if n.read and index > 24: break
elif not n.read:
n.read = True
c.unread = True
g.db.add(n)
if n.created_utc > 1620391248: c.notif_utc = n.created_utc
listing.append(c)
g.db.commit()
next_exists = (len(notifications) > len(listing))
elif reddit:
notifications = g.db.query(Notification, Comment).join(Comment, Notification.comment_id == Comment.id).filter(Notification.user_id == v.id, Comment.body_html.like('%<p>New site mention: <a href="https://old.reddit.com/r/%'), Comment.parent_submission == None, Comment.author_id == NOTIFICATIONS_ID).order_by(Notification.created_utc.desc()).offset(25 * (page - 1)).limit(101).all()
listing = []
for index, x in enumerate(notifications[:100]):
n, c = x
if n.read and index > 24: break
elif not n.read:
n.read = True
c.unread = True
g.db.add(n)
if n.created_utc > 1620391248: c.notif_utc = n.created_utc
listing.append(c)
g.db.commit()
next_exists = (len(notifications) > len(listing))
else:
comments = g.db.query(Comment, Notification).join(Notification, Notification.comment_id == Comment.id).filter(
comments = (g.db.query(Comment, Notification)
.join(Notification.comment)
.filter(
Notification.user_id == v.id,
Comment.state_mod == StateMod.VISIBLE,
Comment.state_user_deleted_utc == None,
Comment.author_id != AUTOJANNY_ID,
Comment.body_html.notlike('%<p>New site mention: <a href="https://old.reddit.com/r/%')
).order_by(Notification.created_utc.desc())
).order_by(Notification.created_utc.desc()))
if not (v and (v.shadowbanned or v.admin_level >= 3)):
comments = comments.join(User, User.id == Comment.author_id).filter(User.shadowbanned == None)
if not v.shadowbanned and v.admin_level < 3:
comments = comments.join(Comment.author).filter(User.shadowbanned == None)
comments = comments.offset(25 * (page - 1)).limit(26).all()
comments = comments.offset(25 * (page - 1)).limit(26).all()
next_exists = (len(comments) > 25)
comments = comments[:25]
next_exists = (len(comments) > 25)
comments = comments[:25]
cids = [x[0].id for x in comments]
for c, n in comments:
c.notif_utc = n.created_utc
c.unread = not n.read
n.read = True
comms = get_comments(cids, v=v)
listing: list[Comment] = [c for c, _ in comments]
listing = []
for c, n in comments:
if n.created_utc > 1620391248: c.notif_utc = n.created_utc
if not n.read:
n.read = True
c.unread = True
g.db.add(n)
# TODO: commit after request rendered, then default session expiry is fine
g.db.expire_on_commit = False
g.db.commit()
g.db.expire_on_commit = True
if c.parent_submission:
if c.replies2 == None:
c.replies2 = c.child_comments.filter(or_(Comment.author_id == v.id, Comment.id.in_(cids))).all()
for x in c.replies2:
if x.replies2 == None: x.replies2 = []
count = 0
while count < 50 and c.parent_comment and (c.parent_comment.author_id == v.id or c.parent_comment.id in cids):
count += 1
c = c.parent_comment
if c.replies2 == None:
c.replies2 = c.child_comments.filter(or_(Comment.author_id == v.id, Comment.id.in_(cids))).all()
for x in c.replies2:
if x.replies2 == None:
x.replies2 = x.child_comments.filter(or_(Comment.author_id == v.id, Comment.id.in_(cids))).all()
else:
while c.parent_comment:
c = c.parent_comment
c.replies2 = g.db.query(Comment).filter_by(parent_comment_id=c.id).order_by(Comment.id).all()
if request.headers.get("Authorization"):
return {"data": [x.json for x in listing]}
if c not in listing: listing.append(c)
return render_template("notifications.html",
v=v,
notifications=listing,
next_exists=next_exists,
page=page,
standalone=True,
render_replies=False,
is_notification_page=True,
)
@app.get("/notifications/posts")
@auth_required
def notifications_posts(v: User):
page: int = max(request.values.get("page", 1, int) or 1, 1)
notifications = (g.db.query(Notification, Comment)
.join(Comment, Notification.comment_id == Comment.id)
.filter(Notification.user_id == v.id, Comment.author_id == AUTOJANNY_ID)
.order_by(Notification.created_utc.desc()).offset(25 * (page - 1)).limit(101).all())
listing = []
for index, x in enumerate(notifications[:100]):
n, c = x
if n.read and index > 24: break
elif not n.read:
n.read = True
c.unread = True
g.db.add(n)
if n.created_utc > 1620391248: c.notif_utc = n.created_utc
listing.append(c)
next_exists = (len(notifications) > len(listing))
g.db.commit()
if request.headers.get("Authorization"): return {"data":[x.json for x in listing]}
if request.headers.get("Authorization"):
return {"data": [x.json for x in listing]}
return render_template("notifications.html",
v=v,
notifications=listing,
next_exists=next_exists,
page=page,
standalone=True,
render_replies=True
)
v=v,
notifications=listing,
next_exists=next_exists,
page=page,
standalone=True,
render_replies=True,
is_notification_page=True,
)
@app.get("/notifications/modmail")
@admin_level_required(2)
def notifications_modmail(v: User):
page: int = max(request.values.get("page", 1, int) or 1, 1)
comments = (g.db.query(Comment)
.filter(Comment.sentto == MODMAIL_ID)
.order_by(Comment.id.desc()).offset(25 * (page - 1)).limit(26).all())
next_exists = (len(comments) > 25)
listing = comments[:25]
if request.headers.get("Authorization"):
return {"data": [x.json for x in listing]}
return render_template("notifications.html",
v=v,
notifications=listing,
next_exists=next_exists,
page=page,
standalone=True,
render_replies=True,
is_notification_page=True,
)
@app.get("/notifications/messages")
@auth_required
def notifications_messages(v: User):
page: int = max(request.values.get("page", 1, int) or 1, 1)
comments = g.db.query(Comment).filter(
Comment.sentto != None,
or_(Comment.author_id==v.id, Comment.sentto==v.id),
Comment.parent_submission == None,
Comment.level == 1,
)
if not v.shadowbanned and v.admin_level < 3:
comments = comments.join(Comment.author).filter(User.shadowbanned == None)
comments = comments.order_by(Comment.id.desc()).offset(25 * (page - 1)).limit(26).all()
next_exists = (len(comments) > 25)
listing = comments[:25]
if request.headers.get("Authorization"):
return {"data": [x.json for x in listing]}
return render_template("notifications.html",
v=v,
notifications=listing,
next_exists=next_exists,
page=page,
standalone=True,
render_replies=True,
is_notification_page=True,
)
@app.get("/")

View file

@ -17,7 +17,7 @@ def login_get(v):
if redir.startswith(f'{SITE_FULL}/'): return redirect(redir)
elif redir.startswith('/'): return redirect(f'{SITE_FULL}{redir}')
return render_template("login.html", failed=False, redirect=redir)
return render_template("login/login.html", failed=False, redirect=redir)
def check_for_alts(current_id):
@ -95,18 +95,18 @@ def login_post():
if not account:
time.sleep(random.uniform(0, 2))
return render_template("login.html", failed=True)
return render_template("login/login.html", failed=True)
if request.values.get("password"):
if not account.verifyPass(request.values.get("password")):
time.sleep(random.uniform(0, 2))
return render_template("login.html", failed=True)
return render_template("login/login.html", failed=True)
if account.mfa_secret:
now = int(time.time())
hash = generate_hash(f"{account.id}+{now}+2fachallenge")
return render_template("login_2fa.html",
return render_template("login/login_2fa.html",
v=account,
time=now,
hash=hash,
@ -124,7 +124,7 @@ def login_post():
if not account.validate_2fa(request.values.get("2fa_token", "").strip()):
hash = generate_hash(f"{account.id}+{time}+2fachallenge")
return render_template("login_2fa.html",
return render_template("login/login_2fa.html",
v=account,
time=now,
hash=hash,
@ -193,7 +193,7 @@ def sign_up_get(v):
ref_user = None
if ref_user and (ref_user.id in session.get("history", [])):
return render_template("sign_up_failed_ref.html")
return render_template("login/sign_up_failed_ref.html")
now = int(time.time())
token = token_hex(16)
@ -209,7 +209,7 @@ def sign_up_get(v):
error = request.values.get("error")
return render_template(
"sign_up.html",
"login/sign_up.html",
formkey=formkey,
now=now,
ref_user=ref_user,
@ -358,7 +358,7 @@ def sign_up_post(v):
@app.get("/forgot")
def get_forgot():
return render_template("forgot_password.html")
return render_template("login/forgot_password.html")
@app.post("/forgot")
@ -370,7 +370,7 @@ def post_forgot():
email = request.values.get("email",'').strip().lower()
if not email_regex.fullmatch(email):
return render_template("forgot_password.html", error="Invalid email.")
return render_template("login/forgot_password.html", error="Invalid email.")
username = username.lstrip('@')
@ -390,7 +390,7 @@ def post_forgot():
v=user)
)
return render_template("forgot_password.html",
return render_template("login/forgot_password.html",
msg="If the username and email matches an account, you will be sent a password reset email. You have ten minutes to complete the password reset process.")
@ -420,7 +420,7 @@ def get_reset():
reset_token = generate_hash(f"{user.id}+{timestamp}+reset+{user.login_nonce}")
return render_template("reset_password.html",
return render_template("login/reset_password.html",
v=user,
token=reset_token,
time=timestamp,
@ -456,7 +456,7 @@ def post_reset(v):
abort(404)
if password != confirm_password:
return render_template("reset_password.html",
return render_template("login/reset_password.html",
v=user,
token=token,
time=timestamp,
@ -475,7 +475,7 @@ def post_reset(v):
@auth_desired
def lost_2fa(v):
return render_template(
"lost_2fa.html",
"login/lost_2fa.html",
v=v
)

View file

@ -78,13 +78,13 @@ def participation_stats(v):
"signups last 24h": users.filter(User.created_utc > day).count(),
"total posts": submissions.count(),
"posting users": g.db.query(Submission.author_id).distinct().count(),
"listed posts": submissions.filter_by(Submission.state_mod == StateMod.VISIBLE).filter(Submission.state_user_deleted_utc == None).count(),
"removed posts (by admins)": submissions.filter_by(Submission.state_mod != StateMod.VISIBLE).count(),
"listed posts": submissions.filter(Submission.state_mod == StateMod.VISIBLE).filter(Submission.state_user_deleted_utc == None).count(),
"removed posts (by admins)": submissions.filter(Submission.state_mod != StateMod.VISIBLE).count(),
"deleted posts (by author)": submissions.filter(Submission.state_user_deleted_utc != None).count(),
"posts last 24h": submissions.filter(Submission.created_utc > day).count(),
"total comments": comments.filter(Comment.author_id.notin_((AUTOJANNY_ID,NOTIFICATIONS_ID))).count(),
"commenting users": g.db.query(Comment.author_id).distinct().count(),
"removed comments (by admins)": comments.filter_by(Comment.state_mod != StateMod.VISIBLE).count(),
"removed comments (by admins)": comments.filter(Comment.state_mod != StateMod.VISIBLE).count(),
"deleted comments (by author)": comments.filter(Comment.state_user_deleted_utc != None).count(),
"comments last_24h": comments.filter(Comment.created_utc > day, Comment.author_id.notin_((AUTOJANNY_ID,NOTIFICATIONS_ID))).count(),
"post votes": g.db.query(Vote.submission_id).count(),

View file

@ -540,7 +540,7 @@ def messagereply(v):
'notification': {
'title': f'New message from @{v.username}',
'body': notifbody,
'deep_link': f'{SITE_FULL}/notifications?messages=true',
'deep_link': f'{SITE_FULL}/notifications/messages',
'icon': SITE_FULL + assetcache_path(f'images/{SITE_ID}/icon.webp'),
}
},
@ -550,7 +550,7 @@ def messagereply(v):
'body': notifbody,
},
'data': {
'url': '/notifications?messages=true',
'url': '/notifications/messages',
}
}
},

View file

@ -17,8 +17,8 @@ def admin_vote_info_get(v):
if not link: return render_template("votes.html", v=v)
try:
if "t2_" in link: thing = get_post(link.split("t2_")[1], v=v)
elif "t3_" in link: thing = get_comment(link.split("t3_")[1], v=v)
if "post_" in link: thing = get_post(link.split("post_")[1], v=v)
elif "comment_" in link: thing = get_comment(link.split("comment_")[1], v=v)
else: abort(400)
except: abort(400)

View file

@ -1,109 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
{% include "analytics.html" %}
<meta name="description" content="{{config('DESCRIPTION')}}">
{% include "csp.html" %}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="author" content="">
<title>{% block pagetitle %}{{SITE_TITLE}}{% endblock %}</title>
{% if v %}
<style>:root{--primary:#{{v.themecolor}}}</style>
<link rel="stylesheet" href="{{ 'css/main.css' | asset }}">
<link rel="stylesheet" href="{{ ('css/'~v.theme~'.css') | asset }}">
{% if v.css %}
<style>{{v.css | safe}}</style>
{% endif %}
{% else %}
<style>:root{--primary:#{{config('DEFAULT_COLOR')}}</style>
<link rel="stylesheet" href="{{ 'css/main.css' | asset }}">
<link rel="stylesheet" href="{{ ('css/'~config('DEFAULT_THEME')~'.css') | asset }}">
{% endif %}
</head>
<body id="login">
<nav class="navbar navbar-expand-lg navbar-dark bg-transparent fixed-top border-0">
<div class="container-fluid">
<button class="navbar-toggler d-none" role="button" data-bs-toggle="collapse" data-bs-target="#navbarResponsive"
aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</nav>
<div class="container-fluid position-absolute h-100 p-0">
<div class="row no-gutters h-100">
<div class="col-12 col-md-6 my-auto p-3">
<div class="row justify-content-center">
<div class="col-10 col-md-7">
<div class="mb-5">
<a href="/" class="text-decoration-none"><span class="h3 text-primary"></span></a>
</div>
<h1 class="h2">{% block authtitle %}{% endblock %}</h1>
<p class="text-muted mb-md-5">{% block authtext %}{% endblock %}</p>
{% if error %}
<div class="alert alert-danger alert-dismissible fade show d-flex my-3" role="alert">
<i class="fas fa-exclamation-circle my-auto"></i>
<span>
{{error}}
</span>
<button class="close" data-bs-dismiss="alert" aria-label="Close">
<span aria-hidden="true"><i class="far fa-times"></i></span>
</button>
</div>
{% endif %}
{% if msg %}
<div class="alert alert-success alert-dismissible fade show d-flex my-3" role="alert">
<i class="fas fa-info-circle my-auto" aria-hidden="true"></i>
<span>
{{msg}}
</span>
<button class="close" data-bs-dismiss="alert" aria-label="Close">
<span aria-hidden="true"><i class="far fa-times"></i></span>
</button>
</div>
{% endif %}
{% block content %}
{% endblock %}
</div>
</div>
</div>
<div class="col-12 col-md-6 d-none d-md-block">
<div class="splash-wrapper">
<div class="splash-overlay"></div>
<img alt="cover" loading="lazy" class="splash-img" src="{{ ('images/'~SITE_ID~'/cover.webp') | asset }}"></img>
</div>
</div>
</div>
</div>
</body>
</html>

View file

@ -10,9 +10,8 @@
{%- set score = c.score_str(render_ctx) -%}
{%- set downs = c.downvotes_str(render_ctx) -%}
{% set replies = c.replies(v) %}
{%- set is_notification_page = request.path.startswith('/notifications') -%}
{% if not c.visibility_state(v)[0] %}
{% if not c.visibility_and_message(v)[0] %}
{% if c.show_descendants(v) %}
<div id="comment-{{c.id}}" class="comment">
<div class="comment-collapse-icon" onclick="collapse_comment('{{c.id}}', this.parentElement)"></div>
@ -22,7 +21,7 @@
<div class="comment-user-info">
{% if standalone and c.over_18 %}<span class="badge badge-danger">+18</span>{% endif %}
{{c.visibility_state(v)[1]}}
{{c.visibility_and_message(v)[1]}}
</div>
<div class="comment-body">
@ -61,10 +60,10 @@
{%- set voted = c.voted_display(v) -%}
{% if standalone and level==1 %}
<div class="post-info post-row-cid-{{c.id}} mb-1 mr-2 {% if is_notification_page %}mt-5{% else %}mt-3{% endif %}">
<div class="post-info post-row-cid-{{c.id}} mb-1 mr-2 mt-3">
{% if c.post and c.post.over_18 %}<span class="badge badge-danger text-small-extra mr-1">+18</span>{% endif %}
<span class="align-top">
<span class="font-weight-bold">{{c.header_msg(v, is_notification_page, replies | length) | safe}}</span>
<span class="font-weight-bold">{{c.header_msg(v, is_notification_page) | safe}}</span>
</span>
</div>
{% endif %}
@ -83,7 +82,7 @@
<div class="comment-body">
<div id="{% if comment_info and comment_info.id == c.id %}context{%else%}comment-{{c.id}}-only{% endif %}" class="{% if c.unread %}unread{% endif %} comment-{{c.id}}-only comment-anchor {% if comment_info and comment_info.id == c.id %}context{%endif%}{% if c.state_mod == StateMod.REMOVED %} removed{% endif %}{% if c.state_mod == StateMod.FILTERED %} filtered{% endif %}{% if c.state_user_deleted_utc %} deleted{% endif %}">
{%- include 'component/comment/reports.html'-%}
{% if c.state_mod == StateMod.REMOVED and c.state_mod_set_by and v and v.admin_level >= 2 %}
{% if c.state_mod == StateMod.REMOVED and c.state_mod_set_by and v and v.admin_level >= PERMS['POST_COMMENT_MODERATION'] %}
<div id="comment-banned-warning" class="comment-text text-removed mb-0">removed by @{{c.state_mod_set_by}}</div>
{% endif %}

View file

@ -1,10 +1,10 @@
<div id="reply-to-{{c.id}}" class="d-none">
<div id="comment-form-space-{{c.fullname}}" class="comment-write collapsed child">
<form id="reply-to-t3_{{c.id}}" action="/comment" method="post" enctype="multipart/form-data">
<form id="reply-to-{{c.fullname}}" action="/comment" method="post" enctype="multipart/form-data">
<input type="hidden" name="formkey" value="{{v.formkey}}">
<input type="hidden" name="parent_fullname" value="{{c.fullname}}">
<input autocomplete="off" id="reply-form-submission-{{c.fullname}}" type="hidden" name="submission" value="{{c.post.id}}">
<textarea required autocomplete="off" minlength="1" maxlength="{{COMMENT_BODY_LENGTH_MAXIMUM}}" oninput="markdown('reply-form-body-{{c.fullname}}', 'reply-edit-{{c.id}}');charLimit('reply-form-body-{{c.fullname}}','charcount-{{c.id}}')" id="reply-form-body-{{c.fullname}}" data-fullname="{{c.fullname}}" name="body" form="reply-to-t3_{{c.id}}" class="comment-box form-control rounded" aria-label="With textarea" placeholder="Add your comment..." rows="3"></textarea>
<textarea required autocomplete="off" minlength="1" maxlength="{{COMMENT_BODY_LENGTH_MAXIMUM}}" oninput="markdown('reply-form-body-{{c.fullname}}', 'reply-edit-{{c.id}}');charLimit('reply-form-body-{{c.fullname}}','charcount-{{c.id}}')" id="reply-form-body-{{c.fullname}}" data-fullname="{{c.fullname}}" name="body" form="reply-to-{{c.fullname}}" class="comment-box form-control rounded" aria-label="With textarea" placeholder="Add your comment..." rows="3"></textarea>
<div class="text-small font-weight-bold mt-1" id="charcount-{{c.id}}" style="right: 1rem; bottom: 0.5rem; z-index: 3;"></div>

View file

@ -3,7 +3,7 @@
<div id="comment-form-space-{{c.id}}" class="comment-write collapsed child">
<form id="reply-to-message-{{c.id}}" action="/reply" method="post" class="input-group" enctype="multipart/form-data">
<input type="hidden" name="formkey" value="{{v.formkey}}">
<textarea required autocomplete="off" minlength="1" maxlength="{{MESSAGE_BODY_LENGTH_MAXIMUM}}" name="body" form="reply-to-t3_{{c.id}}" data-id="{{c.id}}" class="comment-box form-control rounded" id="reply-form-body-{{c.id}}" aria-label="With textarea" rows="3" oninput="markdown('reply-form-body-{{c.id}}', 'message-reply-{{c.id}}')"></textarea>
<textarea required autocomplete="off" minlength="1" maxlength="{{MESSAGE_BODY_LENGTH_MAXIMUM}}" name="body" form="reply-to-message-{{c.id}}" data-id="{{c.id}}" class="comment-box form-control rounded" id="reply-form-body-{{c.id}}" aria-label="With textarea" rows="3" oninput="markdown('reply-form-body-{{c.id}}', 'message-reply-{{c.id}}')"></textarea>
<div class="comment-format" id="comment-format-bar-{{c.id}}">
{% if c.sentto == MODMAIL_ID %}
<label class="btn btn-secondary m-0 mt-3" for="file-upload">

View file

@ -1,32 +1,15 @@
{% extends "email/default.html" %}
{% block title %}Remove Two-Factor Authentication{% endblock %}</h1>
{% block preheader %}Remove Two-Factor Authentication.{% endblock %}
{% block title %}Remove Two-Factor Authentication{% endblock %}
{% block content %}
<p>We received a request to remove two-factor authentication from your account. In 72 hours, click the link below.</p>
<p>If you didn't make this request, change your password and use the Log Out Everywhere feature in your <a href="/settings/security">Security Settings</a> to permanently invalidate the link.</p>
<div class="overflow-x-auto"><table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<div class="overflow-x-auto><table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
<tr>
<td align="center">
<a href="{{action_url}}" class="f-fallback button" target="_blank">Remove 2FA</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<p>Please note that {{SITE_TITLE}} will never ask you for your email, password, or two-factor token via email, text, or phone.</p>
<div class="overflow-x-auto"><table class="body-sub" role="presentation">
<tr>
<td>
<p class="f-fallback sub">If youre having trouble with the button above, copy and paste the URL below into your web browser.</p>
<p class="f-fallback sub">{{action_url}}</p>
</td>
</tr>
</table>
<div class="button-container">
<a href="{{action_url}}" class="button" target="_blank" rel="noopener">Remove 2FA</a>
</div>
<p class="user-info">Please note that {{SITE_TITLE}} will never ask you for your email, password, or two-factor token via email, text, or phone.</p>
<hr />
<p class="raw-link">If the button doesn't work, you can copy and paste this link into your browser: <br>{{action_url}}</p>
{% endblock %}

View file

@ -1,410 +1,112 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "https://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<meta name="description" content="{{config('DESCRIPTION')}}">
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline'; connect-src 'self'; object-src 'none';">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="x-apple-disable-message-reformatting" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title></title>
<style type="text/css" rel="stylesheet" media="all">
html {
font-size: 14px;
}
body {
width: 100% !important;
height: 100%;
margin: 0;
-webkit-text-size-adjust: none;
}
a {
color: #FF66AC!important;
}
a img {
border: none;
}
td {
word-break: break-word;
}
.preheader {
display: none !important;
visibility: hidden;
mso-hide: all;
font-size: 1px;
line-height: 1px;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
}
body,
td,
th {
font-family: Helvetica, Arial, sans-serif;
}
h1 {
margin-top: 0;
color: #121213;
font-size: 22px;
font-weight: bold;
text-align: left;
}
h2 {
margin-top: 0;
color: #121213;
font-size: 1rem;
font-weight: bold;
text-align: left;
}
h3 {
margin-top: 0;
color: #121213;
font-size: 14px;
font-weight: bold;
text-align: left;
}
td,
th {
font-size: 1rem;
}
p,
ul,
ol,
blockquote {
margin: .4em 0 1.1875em;
font-size: 1rem;
line-height: 1.625;
}
p.sub {
font-size: 13px;
}
.align-right {
text-align: right;
}
.align-left {
text-align: left;
}
.align-center {
text-align: center;
}
.button {
background-color: #FF66AC;
border-top: 10px solid #FF66AC;
border-right: 18px solid #FF66AC;
border-bottom: 10px solid #FF66AC;
border-left: 18px solid #FF66AC;
display: inline-block;
color: #FFF!important;
text-decoration: none;
border-radius: .25rem;
-webkit-text-size-adjust: none;
box-sizing: border-box;
}
.button--green {
background-color: #23CE6B;
border-top: 10px solid #23CE6B;
border-right: 18px solid #23CE6B;
border-bottom: 10px solid #23CE6B;
border-left: 18px solid #23CE6B;
}
.button--red {
background-color: #F05D5E;
border-top: 10px solid #F05D5E;
border-right: 18px solid #F05D5E;
border-bottom: 10px solid #F05D5E;
border-left: 18px solid #F05D5E;
}
@media only screen and (max-width: 500px) {
.button {
width: 100% !important;
text-align: center !important;
}
}
.attributes {
margin: 0 0 21px;
}
.attributes_content {
background-color: #EDF2F7;
padding: 1rem;
border-radius: 0.35rem;
}
.attributes_item {
padding: 0;
}
.related {
width: 100%;
margin: 0;
padding: 25px 0 0 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
.related_item {
padding: 10px 0;
color: #CBCCCF;
font-size: 15px;
line-height: 18px;
}
.related_item-title {
display: block;
margin: .5em 0 0;
}
.related_item-thumb {
display: block;
padding-bottom: 10px;
}
.related_heading {
border-top: 1px solid #CBCCCF;
text-align: center;
padding: 25px 0 10px;
}
.discount {
width: 100%;
margin: 0;
padding: 24px;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
background-color: #EDF2F7;
border: 2px dashed #CBCCCF;
}
.discount_heading {
text-align: center;
}
.discount_body {
text-align: center;
font-size: 15px;
}
.social {
width: auto;
}
.social td {
padding: 0;
width: auto;
}
.social_icon {
height: 20px;
margin: 0 8px 10px 8px;
padding: 0;
}
.purchase {
width: 100%;
margin: 0;
padding: 35px 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
.purchase_content {
width: 100%;
margin: 0;
padding: 25px 0 0 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
.purchase_item {
padding: 10px 0;
color: #121213;
font-size: 15px;
line-height: 18px;
}
.purchase_heading {
padding-bottom: 8px;
border-bottom: 1px solid #E6E6E6;
}
.purchase_heading p {
margin: 0;
color: #85878E;
font-size: 12px;
}
.purchase_footer {
padding-top: 15px;
border-top: 1px solid #E6E6E6;
}
.purchase_total {
margin: 0;
text-align: right;
font-weight: bold;
color: #121213;
}
.purchase_total--label {
padding: 0 15px 0 0;
}
body {
background-color: #EDF2F7;
color: #121213;
}
p {
color: #121213;
}
p.sub {
color: #6B6E76;
}
.email-wrapper {
width: 100%;
margin: 0;
padding: 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
background-color: #EDF2F7;
}
.email-content {
width: 100%;
margin: 0;
padding: 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
.email-masthead {
display: none;
}
.email-masthead_logo {
width: 94px;
}
.email-masthead_name {
font-size: 1.25rem;
font-weight: bold;
color: #121213;
text-decoration: none;
}
.email-body {
width: 100%;
margin: 0;
padding: 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
background-color: #cfcfcf;
}
.email-body_inner {
width: 570px;
margin: 0 auto;
padding: 0;
-premailer-width: 570px;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
background-color: #cfcfcf;
}
.email-footer {
width: 570px;
margin: 0 auto;
padding: 0;
-premailer-width: 570px;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
text-align: center;
}
.email-footer p {
color: #6B6E76;
}
.body-action {
width: 100%;
margin: 30px auto;
padding: 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
text-align: center;
}
.body-sub {
margin-top: 25px;
padding-top: 25px;
border-top: 1px solid #E6E6E6;
}
.content-cell {
padding: 35px;
}
@media only screen and (max-width: 600px) {
.email-body_inner,
.email-footer {
width: 100% !important;
}
}
@media (prefers-color-scheme: dark) {
body,
.email-body,
.email-body_inner,
.email-content,
.email-wrapper,
.email-masthead,
.email-footer {
background-color: #121213 !important;
color: #FFF !important;
}
p,
ul,
ol,
blockquote,
h1,
h2,
h3 {
color: #FFF !important;
}
.attributes_content,
.discount {
background-color: #222 !important;
}
.email-masthead_name {
text-shadow: none !important;
}
}
</style>
</head>
<body>
<span class="preheader">{% block preheader %}Thanks for joining {{SITE_TITLE}}! Please take a sec to verify the email you used to sign up.{% endblock %}</span>
<div class="overflow-x-auto"><table class="email-wrapper" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<div class="overflow-x-auto"><table class="email-content" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="email-masthead">
<a href="/" class="f-fallback email-masthead_name">
{{SITE_TITLE}}
</a>
</td>
</tr>
<tr>
<td class="email-body" width="100%" cellpadding="0" cellspacing="0">
<div class="overflow-x-auto"><table class="email-body_inner" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="content-cell">
<div class="f-fallback">
<h1>{% block title %}Title Goes Here{% endblock %}</h1>
{% block content %}
{% for entry in data %}
<h3>{{entry[0]}}</h3>
<p>{{entry[1]}}</p>
{% endfor %}
{% endblock %}
</div>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
<head>
<meta name="description" content="{{config('DESCRIPTION')}}">
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline'; connect-src 'self'; object-src 'none';">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title></title>
<style type="text/css" rel="stylesheet" media="all">
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f5f5f5;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #ffffff;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
h1 {
text-align: center;
color: #333333;
}
p {
margin-bottom: 10px;
color: #333333;
line-height: 1.25rem;
}
.button-container {
text-align: center;
margin-top: 30px;
margin-bottom: 40px;
}
.button {
display: inline-block;
background-color: #1a6187;
color: #ffffff;
padding: 10px 20px;
text-decoration: none;
border-radius: 5px;
}
.raw-link {
display: block;
margin-top: 10px;
font-size: 14px;
text-align: left;
color: #777777;
word-wrap: break-word;
}
.reference-text {
margin-top: 20px;
font-size: 14px;
line-height: 1rem;
}
.user-info {
font-size: 14px;
color: #333333;
}
.user-info p {
margin-left: 20px;
line-height: 1rem;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #222222;
color: #ffffff;
}
h1 {
color: #ffffff;
}
p {
color: #cccccc;
}
.container {
background-color: #1d323d;
}
.button {
background-color: #2280B3;
}
.raw-link {
color: #cccccc;
}
.reference-text {
color: #cccccc;
}
.user-info {
color: #cccccc;
}
}
@media screen and (max-width: 600px) {
.container {
padding: 10px;
}
h1 {
font-size: 24px;
}
}
</style>
</head>
<body>
<div class="container">
<h1>{% block title %}{% endblock %}</h1>
{% block content %}
{% endblock %}
</div>
</body>
</html>

View file

@ -1,54 +1,18 @@
{% extends "email/default.html" %}
{% block title %}Verify Your Email{% endblock %}</h1>
{% block preheader %}Verify your new {{SITE_TITLE}} email.{% endblock %}
{% block title %}Verify your new {{SITE_TITLE}} email.{% endblock %}</h1>
{% block content %}
<p>You told us you wanted to change your {{SITE_TITLE}} account email. To finish this process, please verify your new email address:</p>
<div class="overflow-x-auto"><table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<div class="overflow-x-auto><table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
<tr>
<td align="center">
<a href="{{action_url}}" class="f-fallback button" target="_blank">Verify email</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<p>For reference, here's your current information:</p>
<div class="overflow-x-auto"><table class="attributes" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="attributes_content">
<div class="overflow-x-auto><table width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="attributes_item">
<span class="f-fallback">
<strong>Email:</strong> {{v.email}}
</span>
</td>
</tr>
<tr>
<td class="attributes_item">
<span class="f-fallback">
<strong>Username:</strong> {{v.username}}
</span>
</td>
</tr>
</table>
</td>
</tr>
</table>
<p>Please note that {{SITE_TITLE}} will never ask you for your email, password, or two-factor token via email, text, or phone.</p>
<div class="overflow-x-auto"><table class="body-sub" role="presentation">
<tr>
<td>
<p class="f-fallback sub">If youre having trouble with the button above, copy and paste the URL below into your web browser.</p>
<p class="f-fallback sub">{{action_url}}</p>
</td>
</tr>
</table>
<p>You told us you wanted to change your {{SITE_TITLE}} account email. To finish this process, please verify your new email address:</p>
<div class="button-container">
<a href="{{action_url}}" class="button" target="_blank" rel="noopener">Verify Email</a>
</div>
<p class="reference-text">For reference, here's your current information:</p>
<div class="user-info">
<p><strong>Username:</strong> {{v.username}}</p>
<p><strong>Email:</strong> {{v.email}}</p>
</div>
<p class="user-info">Please note that {{SITE_TITLE}} will never ask you for your email, password, or two-factor token via email, text, or phone.</p>
<hr />
<p class="raw-link">If the button doesn't work, you can copy and paste this link into your browser: <br>{{action_url}}</p>
{% endblock %}

View file

@ -1,52 +1,18 @@
{% extends "email/default.html" %}
{% block title %}Welcome to {{SITE_TITLE}}!{% endblock %}</h1>
{% block title %}Welcome to {{SITE_TITLE}}!{% endblock %}
{% block content %}
<p>Thanks for joining {{SITE_TITLE}}. Were happy to have you on board. To get the most out of {{SITE_TITLE}}, please verify your account email:</p>
<div class="overflow-x-auto"><table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<div class="overflow-x-auto><table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
<tr>
<td align="center">
<a href="{{action_url}}" class="f-fallback button" target="_blank">Verify email</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<p>For reference, here's your username.</p>
<div class="overflow-x-auto"><table class="attributes" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="attributes_content">
<div class="overflow-x-auto><table width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="attributes_item">
<span class="f-fallback">
<strong>Email:</strong> {{v.email}}
</span>
</td>
</tr>
<tr>
<td class="attributes_item">
<span class="f-fallback">
<strong>Username:</strong> {{v.username}}
</span>
</td>
</tr>
</table>
</td>
</tr>
</table>
<p>Please note that {{SITE_TITLE}} will never ask you for your email, password, or two-factor token via email, text, or phone.</p>
<div class="overflow-x-auto"><table class="body-sub" role="presentation">
<tr>
<td>
<p class="f-fallback sub">If youre having trouble with the button above, copy and paste the URL below into your web browser.</p>
<p class="f-fallback sub">{{action_url}}</p>
</td>
</tr>
</table>
<p>Thanks for joining {{SITE_TITLE}}. Were happy to have you on board. Please click the button below to verify your email address:</p>
<div class="button-container">
<a href="{{action_url}}" class="button" target="_blank" rel="noopener">Verify Email</a>
</div>
<p class="reference-text">For reference, here's your username:</p>
<div class="user-info">
<p><strong>Username:</strong> {{v.username}}</p>
<p><strong>Email:</strong> {{v.email}}</p>
</div>
<p class="user-info">Please note that {{SITE_TITLE}} will never ask you for your email, password, or two-factor token via email, text, or phone.</p>
<hr />
<p class="raw-link">If the button doesn't work, you can copy and paste this link into your browser: <br>{{action_url}}</p>
{% endblock %}

View file

@ -1,52 +1,18 @@
{% extends "email/default.html" %}
{% block title %}Reset Your Password{% endblock %}
{% block preheader %}Reset your {{SITE_TITLE}} password.{% endblock %}
{% block title %}Reset your {{SITE_TITLE}} password.{% endblock %}
{% block content %}
<p>To reset your password, click the button below:</p>
<div class="overflow-x-auto"><table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<div class="overflow-x-auto><table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
<tr>
<td align="center">
<a href="{{action_url}}" class="f-fallback button" target="_blank">Reset password</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<p>For reference, here's your login information:</p>
<div class="overflow-x-auto"><table class="attributes" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="attributes_content">
<div class="overflow-x-auto><table width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="attributes_item">
<span class="f-fallback">
<strong>Email:</strong> {{v.email}}
</span>
</td>
</tr>
<tr>
<td class="attributes_item">
<span class="f-fallback">
<strong>Username:</strong> {{v.username}}
</span>
</td>
</tr>
</table>
</td>
</tr>
</table>
<div class="overflow-x-auto"><table class="body-sub" role="presentation">
<tr>
<td>
<p class="f-fallback sub">If youre having trouble with the button above, copy and paste the URL below into your web browser.</p>
<p class="f-fallback sub">{{action_url}}</p>
</td>
</tr>
</table>
<p>To reset your password, click the button below:</p>
<div class="button-container">
<a href="{{action_url}}" class="button" target="_blank" rel="noopener">Reset password</a>
</div>
<p class="reference-text">For reference, here's your login information:</p>
<div class="user-info">
<p><strong>Username:</strong> {{v.username}}</p>
<p><strong>Email:</strong> {{v.email}}</p>
</div>
<p class="user-info">Please note that {{SITE_TITLE}} will never ask you for your email, password, or two-factor token via email, text, or phone.</p>
<hr />
<p class="raw-link">If the button doesn't work, you can copy and paste this link into your browser: <br>{{action_url}}</p>
{% endblock %}

View file

@ -1,30 +0,0 @@
{% extends "authforms.html" %}
{% block pagetitle %}{{SITE_TITLE}} Password Reset{% endblock %}
{% block authtitle %}Reset your password.{% endblock %}
{% block authtext %}If there's an email address associated with your account, you can use it to recover your {{SITE_TITLE}} account and change your password.{% endblock %}
{% block content %}
<div id="login-form" class="">
<form action="/forgot" method="post" class="mt-3">
<label for="username" class="mt-3">Username</label>
<input autocomplete="off" class="form-control" id="username" aria-describedby="usernameHelp"
type="text" name="username" required="">
<label for="email" class="mt-3">Email</label>
<input type="email" pattern='[^@]+@[^@]+\.[^@]+' autocomplete="off" class="form-control" id="password" aria-describedby="passwordHelp" name="email" required>
<input autocomplete="off" class="btn btn-primary login w-100 mt-3" type="submit" value="Send recovery link">
</form>
</div>
{% endblock %}

View file

@ -6,8 +6,8 @@
<img alt="header icon" height=33 width=80 src="{{ ('images/'~SITE_ID~'/headericon.webp') | asset }}">
</a>
<a href="/" class="flex-grow-1">
<img id="logo" alt="logo" src="{{ ('images/'~SITE_ID~'/logo.webp') | asset }}" width="100" height="20">
<a href="/" id="logo" class="flex-grow-1 logo-text">
{{SITE_TITLE}}
</a>
<div class="flex-grow-1 d-fl d-none d-md-block {% if not v %}pad{% endif %}">
@ -27,7 +27,7 @@
{% if v %}
{% if v.notifications_count %}
<a class="mobile-nav-icon d-md-none pl-0" href="/notifications{% if v.do_posts %}?posts=true{% elif v.do_reddit %}?reddit=true{% endif %}" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Notifications"><i class="fas fa-bell align-middle text-danger" {% if v.do_posts %}style="color:blue!important"{% elif v.do_reddit %}style="color:#805ad5!important"{% endif %}></i><span class="notif-count ml-1" style="padding-left: 4.5px;{% if v.do_posts %}background:blue{% elif v.do_reddit %}background:#805ad5{% endif %}">{{v.notifications_count}}</span></a>
<a class="mobile-nav-icon d-md-none pl-0" href="/notifications{% if v.do_posts %}/posts{% endif %}" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Notifications"><i class="fas fa-bell align-middle text-danger" {% if v.do_posts %}style="color:blue!important"{% endif %}></i><span class="notif-count ml-1" style="padding-left: 4.5px;{% if v.do_posts %}background:blue{% endif %}">{{v.notifications_count}}</span></a>
{% else %}
<a class="mobile-nav-icon d-md-none" href="/notifications" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Notifications"><i class="fas fa-bell align-middle text-gray-500 black"></i></a>
{% endif %}
@ -57,7 +57,7 @@
{% if v.notifications_count %}
<li class="nav-item d-flex align-items-center text-center justify-content-center mx-1">
<a class="nav-link position-relative" href="/notifications{% if v.do_posts %}?posts=true{% elif v.do_reddit %}?reddit=true{% endif %}" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Notifications"><i class="fas fa-bell text-danger" {% if v.do_posts %}style="color:blue!important"{% elif v.do_reddit %}style="color:#805ad5!important"{% endif %}></i><span class="notif-count ml-1" style="padding-left: 4.5px;{% if v.do_posts %}background:blue{% elif v.do_reddit %}background:#805ad5{% endif %}">{{v.notifications_count}}</span></a>
<a class="nav-link position-relative" href="/notifications{% if v.do_posts %}/posts{% endif %}" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Notifications"><i class="fas fa-bell text-danger" {% if v.do_posts %}style="color:blue!important"{% endif %}></i><span class="notif-count ml-1" style="padding-left: 4.5px;{% if v.do_posts %}background:blue{% endif %}">{{v.notifications_count}}</span></a>
</li>
{% else %}

View file

@ -1,124 +0,0 @@
{%- import "util/forms.html" as forms -%}
<!DOCTYPE html>
<html lang="en">
<head>
{% include "analytics.html" %}
<meta name="description" content="{{config('DESCRIPTION')}}">
{% include "csp.html" %}
<script src="{{ 'js/bootstrap.js' | asset }}"></script>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="author" content="">
{% block title %}
<title>Login - {{SITE_TITLE}}</title>
{% endblock %}
<style>:root{--primary:#{{config('DEFAULT_COLOR')}}</style>
<link rel="stylesheet" href="{{ 'css/main.css' | asset }}">
<link rel="stylesheet" href="{{ ('css/'~config('DEFAULT_THEME')~'.css') | asset }}">
</head>
<body id="login">
<nav class="navbar navbar-expand-lg navbar-dark bg-transparent fixed-top border-0">
<div class="container-fluid d-none">
<button class="navbar-toggler d-none" role="button" data-bs-toggle="collapse" data-bs-target="#navbarResponsive"
aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</nav>
<div class="container-fluid position-absolute h-100 p-0 overflow-auto">
<div class="row no-gutters h-100">
<div class="col-12 col-md-6 my-auto p-3">
<div class="row justify-content-center">
<div class="col-10 col-md-7">
<div class="mb-5">
<a href="/" class="text-decoration-none"><span class="h3 text-primary"></span></a>
</div>
{% block content %}
<div id="login-form" class="">
<h1 class="h2">Welcome back.</h1>
<p class="text-muted mb-md-5">Glad to have you back!</p>
{% if failed %}
<div class="alert alert-danger alert-dismissible fade show d-flex my-3" role="alert">
<i class="fas fa-exclamation-circle my-auto"></i>
<div>
Incorrect username, email address, or password.
<br>
<a href="/forgot" class="alert-link">Forgot password?</a>
</div>
<button class="close" data-bs-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
{% endif %}
<form action="/login" method="post" class="mt-md-3" id="login">
<label for="username" class="mt-3">Username or Email Address</label>
<input autocomplete="off" class="form-control" id="username" aria-describedby="usernameHelp"
type="text" name="username" required="">
<input type="hidden" name="redirect" value="{{redirect}}">
<label for="password" class="mt-3">Password</label>
<input autocomplete="off" class="form-control" id="password" aria-describedby="passwordHelp"
type="password" name="password" required="">
<small><a href="/forgot">Forgot password?</a></small>
<button class="btn btn-primary login w-100 mt-3" id="login_button">Sign in</button>
<div class="text-center text-muted text-small mt-5 mb-3">
Don't have an account? <a href="/signup{{'?redirect='+redirect if redirect else ''}}" class="font-weight-bold toggle-login">Sign up</a>
</div>
</form>
</div>
{% endblock %}
</div>
</div>
</div>
<div class="col-12 col-md-6 d-none d-md-block">
<div class="splash-wrapper">
<div class="splash-overlay"></div>
<img alt="cover" loading="lazy" class="splash-img" src="{{ ('images/'~SITE_ID~'/cover.webp') | asset }}"></img>
</div>
</div>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,63 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
{% include "analytics.html" %}
{% include "csp.html" %}
<meta name="description" content="{{config('DESCRIPTION')}}">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>{% block pagetitle %}{{SITE_TITLE}}{% endblock %}</title>
{% if v %}
<style>:root{--primary:#{{v.themecolor}}}</style>
<link rel="stylesheet" href="{{ 'css/main.css' | asset }}">
<link rel="stylesheet" href="{{ ('css/'~v.theme~'.css') | asset }}">
{% if v.css %}
<style>{{v.css | safe}}</style>
{% endif %}
{% else %}
<style>:root{--primary:#{{config('DEFAULT_COLOR')}}</style>
<link rel="stylesheet" href="{{ 'css/main.css' | asset }}">
<link rel="stylesheet" href="{{ ('css/'~config('DEFAULT_THEME')~'.css') | asset }}">
{% endif %}
</head>
<body id="login">
<div class="container-fluid position-absolute h-100 p-0">
<div class="row no-gutters h-100">
<div class="col-12 col-md-6 my-auto p-3">
<div class="row justify-content-center">
<div class="col-10 col-md-7">
<h2>{% block authtitle %}{% endblock %}</h2>
<p class="text-muted mb-md-5">{% block authtext %}{% endblock %}</p>
{% if error %}
<div class="alert alert-danger alert-dismissible fade show d-flex my-3" role="alert">
<i class="fas fa-exclamation-circle my-auto"></i>
<span>{{error}}</span>
<button class="close" data-bs-dismiss="alert" aria-label="Close">
<span aria-hidden="true"><i class="far fa-times"></i></span>
</button>
</div>
{% endif %}
{% if msg %}
<div class="alert alert-success alert-dismissible fade show d-flex my-3" role="alert">
<i class="fas fa-info-circle my-auto" aria-hidden="true"></i>
<span>{{msg}}</span>
<button class="close" data-bs-dismiss="alert" aria-label="Close">
<span aria-hidden="true"><i class="far fa-times"></i></span>
</button>
</div>
{% endif %}
{% block content %}{% endblock %}
</div>
</div>
</div>
<div class="col-12 col-md-6 d-mob-none h-100">
<div class="splash-wrapper">
<div class="splash-overlay"></div>
<img alt="cover" loading="lazy" class="splash-img" src="{{ ('images/'~SITE_ID~'/cover.webp') | asset }}">
</div>
</div>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,15 @@
{% extends "login/authforms.html" %}
{% block pagetitle %}{{SITE_TITLE}} Password Reset{% endblock %}
{% block authtitle %}Reset your password.{% endblock %}
{% block authtext %}If there's an email address associated with your account, you can use it to recover your {{SITE_TITLE}} account and change your password.{% endblock %}
{% block content %}
<div id="login-form" class="">
<form action="/forgot" method="post" class="mt-3">
<label for="username" class="mt-3">Username</label>
<input autocomplete="off" class="form-control" id="username" aria-describedby="usernameHelp" type="text" name="username" required="">
<label for="email" class="mt-3">Email</label>
<input type="email" pattern='[^@]+@[^@]+\.[^@]+' autocomplete="off" class="form-control" id="password" aria-describedby="passwordHelp" name="email" required>
<input autocomplete="off" class="btn btn-primary login w-100 mt-3" type="submit" value="Send recovery link">
</form>
</div>
{% endblock %}

View file

@ -0,0 +1,33 @@
{%- extends 'login/authforms.html' -%}
{% block authtitle %}Welcome back.{% endblock %}
{% block authtext %}Glad to have you back!{% endblock %}
{% block content %}
<div id="login-form" class="">
{% if failed %}
<div class="alert alert-danger alert-dismissible fade show d-flex my-3" role="alert">
<i class="fas fa-exclamation-circle my-auto"></i>
<div>
Incorrect username, email address, or password.
<br>
<a href="/forgot" class="alert-link">Forgot password?</a>
</div>
<button class="close" data-bs-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
{% endif %}
<form action="/login" method="post" class="mt-md-3" id="login">
<label for="username" class="mt-3">Username or Email Address</label>
<input autocomplete="off" class="form-control" id="username" aria-describedby="usernameHelp" type="text" name="username" required="">
<input type="hidden" name="redirect" value="{{redirect}}">
<label for="password" class="mt-3">Password</label>
<input autocomplete="off" class="form-control" id="password" aria-describedby="passwordHelp" type="password" name="password" required="">
<small><a href="/forgot">Forgot password?</a></small>
<button class="btn btn-primary login w-100 mt-3" id="login_button">Sign in</button>
<div class="text-center text-muted text-small mt-5 mb-3">
Don't have an account? <a href="/signup{{'?redirect='+redirect if redirect else ''}}" class="font-weight-bold toggle-login">Sign up</a>
</div>
</form>
</div>
{% endblock %}

View file

@ -0,0 +1,29 @@
{%- extends "login/authforms.html" -%}
{% block authtitle %}Two-step login{% endblock %}
{% block authtext %}To login, please enter the 6-digit verification code generated in your authenticator app.{% endblock %}
{%- block content -%}
<div id="login-form" class="">
{% if failed %}
<div class="alert alert-danger alert-dismissible fade show d-flex my-3" role="alert">
<i class="fas fa-exclamation-circle my-auto"></i>
<div>
Invalid verification code. Please try again.
</div>
<button class="close" data-bs-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
{% endif %}
<form action="/login" method="post" class="mt-md-3" id="login">
<input type="hidden" name="username" value="{{v.username}}">
<input type="hidden" name="redirect" value="{{redirect}}">
<input type="hidden" name="time" value="{{time}}">
<input type="hidden" name="hash" value="{{hash}}">
<label for="2fa_token" class="mt-3">Your verification code</label>
<input autocomplete="off" class="form-control" id="2fa_token" name="2fa_token" type="text" placeholder="6-digit code">
<small><a href="/lost_2fa">Lost your 2FA device?</a></small>
<button class="btn btn-primary login w-100 mt-3" id="login_button">Sign in</button>
</form>
</div>
{%- endblock -%}

View file

@ -0,0 +1,19 @@
{% extends "login/authforms.html" %}
{% block pagetitle %}{{SITE_TITLE}} Two-Factor Removal{% endblock %}
{% block authtitle %}Remove the two-factor authentication from your account.{% endblock %}
{% block authtext %}If all information is correct, you will be able to remove 2-factor authentication from your account in 24 hours.{% endblock %}
{% block content %}
<div id="login-form" class="">
<form action="/request_2fa_disable" method="post" class="mt-3">
<label for="username" class="mt-3">Username</label>
<input autocomplete="off" class="form-control" id="username" aria-describedby="usernameHelp"
type="text" name="username" required=""{% if v %} value="{{v.username}}" disabled{% endif %}>
<label for="email" class="mt-3">Password</label>
<input autocomplete="off" class="form-control" id="password" aria-describedby="passwordHelp"
type="password" name="password" required="">
<label for="email" class="mt-3">Email</label>
<input autocomplete="off" class="form-control" id="password" type="email" pattern='[^@]+@[^@]+\.[^@]+' name="email" required=""{% if v %} value="{{v.email}}" disabled{% endif %}>
<input autocomplete="off" class="btn btn-primary login w-100 mt-3" type="submit" value="Send recovery link">
</form>
</div>
{% endblock %}

View file

@ -0,0 +1,20 @@
{%- extends 'login/authforms.html' -%}
{% block pagetitle %}{{SITE_TITLE}} Password Reset{% endblock %}
{% block authtitle %}Change your password.{% endblock %}
{% block content %}
<div id="login-form" class="">
<form action="/reset" method="post" class="mt-3">
<input type="hidden" name="time" value="{{time}}">
<input type="hidden" name="user_id" value="{{v.id}}">
<input type="hidden" name="token" value="{{token}}">
<label for="passentry" class="mt-3">New Password</label>
<input autocomplete="off" class="form-control" id="passentry" aria-describedby="usernameHelp" type="password" name="password" required="">
<label for="confentry" class="mt-3">Confirm New Password</label>
<input autocomplete="off" class="form-control" id="confentry" aria-describedby="passwordHelp" type="password" name="confirm_password" required="">
<input autocomplete="off" class="btn btn-primary login w-100 mt-3" type="submit" value="Change password">
</form>
</div>
{% endblock %}

View file

@ -0,0 +1,61 @@
{%- extends 'login/authforms.html' -%}
{% set login_namespace = namespace() %}
{% if ref_user %}
{% set login_namespace.authtitle = '@' ~ ref_user.username ~ ' has invited you!' %}
{% set login_namespace.authtext = 'Looks like someone wants you to join ' ~ SITE_NAME ~ '.' %}
{% else %}
{% set login_namespace.authtitle = "Create your account." %}
{% set login_namespace.authtext = "No email address required." %}
{% endif %}
{% block authtitle %}{{login_namespace.authtitle}}{% endblock %}
{% block authtext %}{{login_namespace.authtext}}{% endblock %}
{%- block content -%}
<div id="register-form" class="">
<form action="/signup" method="post" class="mt-md-3" id="signup">
{% if error %}<span class="text-danger">{{error}}</span><br>{% endif %}
<input type="hidden" name="formkey" value="{{formkey}}">
<input type="hidden" name="now" value="{{now}}">
{% if redirect %}<input type="hidden" name="redirect" value="{{redirect}}">{% endif %}
{% if ref_user %}<input type="hidden" name="referred_by" value="{{ref_user.id}}">{% endif %}
<label for="username-register" class="mt-3">Username</label>
<input autocomplete="off" class="form-control" id="username-register" aria-describedby="usernameHelpRegister" type="text" name="username" pattern="[a-zA-Z0-9_\-]{3,25}" min="3" max="25" required autofocus tabindex="1">
<small id="usernameHelpRegister"></small>
<label for="email-register" class="mt-3">Email Address</label> <small class="d-inline-block text-muted ml-1">(optional)</small>
<input style="background-color: var(--gray-800)" autocomplete="off" class="form-control" id="email-register" aria-describedby="emailHelpRegister" type="email" pattern='[^@]+@[^@]+\.[^@]+' name="email" tabindex="2">
<label for="password-register" class="mt-3">Password</label>
<input autocomplete="off" class="form-control" id="password-register" aria-describedby="passwordHelpReigster" type="password" name="password" required tabindex="4">
<small id="passwordHelpRegister" class="form-text font-weight-bold text-muted d-none mt-1">Minimum of 8 characters required.</small>
<small id="passwordHelpSuccess" class="form-text font-weight-bold text-success d-none mt-1">Your password meets the requirements.</small>
<label for="password_confirm" class="mt-3">Confirm Password</label>
<input autocomplete="off" class="form-control" id="password_confirm" aria-describedby="passwordConfirmHelp" type="password" name="password_confirm" required tabindex="5">
<div class="custom-control custom-checkbox mt-4">
<input autocomplete="off" type="checkbox" class="custom-control-input" id="termsCheck" required tabindex="6">
<label class="custom-control-label terms" for="termsCheck">I accept the <a href="/rules" tabindex="8">rules</a></label>
</div>
{% if hcaptcha %}
<div class="h-captcha" data-sitekey="{{hcaptcha}}"></div>
{% endif %}
<button class="btn btn-primary login w-100 mt-3" id="register_button" tabindex="7">Register</button>
<div class="text-center text-muted text-small mt-2 mb-0">
Already have an account? <a href="/login{{'?redirect='+redirect if redirect else ''}}" class="font-weight-bold toggle-login" tabindex="9">Log in</a>
</div>
</form>
</div>
{%- endblock -%}
<script src="{{ 'js/signup.js' | asset }}"></script>
{% if hcaptcha %}
<script src="{{ 'js/hcaptcha.js' | asset }}"></script>
{% endif %}

View file

@ -0,0 +1,17 @@
{%- extends "login/authforms.html" -%}
{%- block authtitle -%}Whoops! You can't refer yourself!{%- endblock -%}
{%- block authtext -%}Send this link to a friend instead :){%- endblock -%}
{%- block content -%}
<div id="register-form" class="">
<label>Referral code</label>
<input autocomplete="off" type="text" class="form-control copy-link" readonly value="{{SITE_FULL}}/signup?ref={{request.values.get('ref')}}" data-clipboard-text="{{SITE_FULL}}/signup?ref={{request.values.get('ref')}}">
<div class="text-center mt-5 mb-3">
Already have an account? <a href="/login" class="font-weight-bold text-small toggle-login">Log in.</a>
</div>
</div>
<div class="toast clipboard" id="toast-success" role="alert" aria-live="assertive" aria-atomic="true" data-bs-animation="true" data-bs-autohide="true" data-bs-delay="5000">
<div class="toast-body text-center">
<i class="fas fa-check-circle text-success mr-2"></i>Link copied to clipboard
</div>
{%- endblock -%}

View file

@ -1,108 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
{% include "analytics.html" %}
<meta name="description" content="{{config('DESCRIPTION')}}">
{% include "csp.html" %}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="author" content="">
<title>2-Step Login - {{SITE_TITLE}}</title>
<style>:root{--primary:#{{config('DEFAULT_COLOR')}}</style>
<link rel="stylesheet" href="{{ 'css/main.css' | asset }}">
<link rel="stylesheet" href="{{ ('css/'~config('DEFAULT_THEME')~'.css') | asset }}">
</head>
<body id="login">
<nav class="navbar navbar-expand-lg navbar-dark bg-transparent fixed-top border-0">
<div class="container-fluid d-none">
<button class="navbar-toggler d-none" role="button" data-bs-toggle="collapse" data-bs-target="#navbarResponsive"
aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</nav>
<div class="container-fluid position-absolute h-100 p-0 overflow-auto">
<div class="row no-gutters h-100">
<div class="col-12 col-md-6 my-auto p-3">
<div class="row justify-content-center">
<div class="col-10 col-md-7">
<div class="mb-5">
<a href="/" class="text-decoration-none"><span class="h3 text-primary"></span></a>
</div>
<div id="login-form" class="">
<h1 class="h2">Two-step login</h1>
<p class="text-muted mb-md-5">To login, please enter the 6-digit verification code generated in your authenticator app.</p>
{% if failed %}
<div class="alert alert-danger alert-dismissible fade show d-flex my-3" role="alert">
<i class="fas fa-exclamation-circle my-auto"></i>
<div>
Invalid verification code. Please try again.
</div>
<button class="close" data-bs-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
{% endif %}
<form action="/login" method="post" class="mt-md-3" id="login">
<input type="hidden" name="username" value="{{v.username}}">
<input type="hidden" name="redirect" value="{{redirect}}">
<input type="hidden" name="time" value="{{time}}">
<input type="hidden" name="hash" value="{{hash}}">
<label for="2fa_token" class="mt-3">Your verification code</label>
<input autocomplete="off" class="form-control" id="2fa_token" name="2fa_token" type="text" placeholder="6-digit code">
<small><a href="/lost_2fa">Lost your 2FA device?</a></small>
<button class="btn btn-primary login w-100 mt-3" id="login_button">Sign in</button>
</form>
</div>
</div>
</div>
</div>
<div class="col-12 col-md-6 d-none d-md-block">
<div class="splash-wrapper">
<div class="splash-overlay"></div>
<img alt="cover" loading="lazy" class="splash-img" src="{{ ('images/'~SITE_ID~'/cover.webp') | asset }}"></img>
</div>
</div>
</div>
</div>
</body>
</html>

View file

@ -1,35 +0,0 @@
{% extends "authforms.html" %}
{% block pagetitle %}{{SITE_TITLE}} Two-Factor Removal{% endblock %}
{% block authtitle %}Remove the two-factor authentication from your account.{% endblock %}
{% block authtext %}If all information is correct, you will be able to remove 2-factor authentication from your account in 24 hours.{% endblock %}
{% block content %}
<div id="login-form" class="">
<form action="/request_2fa_disable" method="post" class="mt-3">
<label for="username" class="mt-3">Username</label>
<input autocomplete="off" class="form-control" id="username" aria-describedby="usernameHelp"
type="text" name="username" required=""{% if v %} value="{{v.username}}" disabled{% endif %}>
<label for="email" class="mt-3">Password</label>
<input autocomplete="off" class="form-control" id="password" aria-describedby="passwordHelp"
type="password" name="password" required="">
<label for="email" class="mt-3">Email</label>
<input autocomplete="off" class="form-control" id="password" type="email" pattern='[^@]+@[^@]+\.[^@]+' name="email" required=""{% if v %} value="{{v.email}}" disabled{% endif %}>
<input autocomplete="off" class="btn btn-primary login w-100 mt-3" type="submit" value="Send recovery link">
</form>
</div>
{% endblock %}

View file

@ -19,35 +19,28 @@
</a>
</li>
<li class="nav-item">
<a class="nav-link py-3{% if '/notifications?posts=true' in request.full_path %} active{% endif %}" href="/notifications?posts=true">
<a class="nav-link py-3{% if '/notifications/posts' in request.full_path %} active{% endif %}" href="/notifications/posts">
Posts {% if v.post_notifications_count %}<span class="font-weight-bold" style="color:blue">({{v.post_notifications_count}})</span>{% endif %}
</a>
</li>
<li class="nav-item">
<a class="nav-link py-3{% if '/notifications?messages=true' in request.full_path %} active{% endif %}" href="/notifications?messages=true">
<a class="nav-link py-3{% if '/notifications/messages' in request.full_path %} active{% endif %}" href="/notifications/messages">
Messages
</a>
</li>
{% if v.admin_level >= 2 %}
<li class="nav-item">
<a class="nav-link py-3{% if '/notifications?modmail=true' in request.full_path %} active{% endif %}" href="/notifications?modmail=true">
<a class="nav-link py-3{% if '/notifications/modmail' in request.full_path %} active{% endif %}" href="/notifications/modmail">
Modmail
</a>
</li>
{% endif %}
{% if v.admin_level %}
<li class="nav-item">
<a class="nav-link py-3{% if '/notifications?reddit=true' in request.full_path %} active{% endif %}" href="/notifications?reddit=true">
Reddit {% if v.reddit_notifications_count %}<span class="font-weight-bold" style="color:#805ad5">({{v.reddit_notifications_count}})</span>{% endif %}
</a>
</li>
{% endif %}
</ul>
</div>
</div>
<a class="btn btn-primary mt-3 ml-3" role="button" onclick="post_toast(this,'/clear', '1')">Clear all notifications</a>
<a class="btn btn-primary mt-3 ml-3" role="button" onclick="post_toast(this, '/clear', '1')">Mark All Read</a>
<div class="notifs px-3 p-md-0">

View file

@ -1,33 +0,0 @@
{% extends "authforms.html" %}
{% block pagetitle %}{{SITE_TITLE}} Password Reset{% endblock %}
{% block authtitle %}Change your password.{% endblock %}
{% block content %}
<div id="login-form" class="">
<form action="/reset" method="post" class="mt-3">
<input type="hidden" name="time" value="{{time}}">
<input type="hidden" name="user_id" value="{{v.id}}">
<input type="hidden" name="token" value="{{token}}">
<label for="passentry" class="mt-3">New Password</label>
<input autocomplete="off" class="form-control" id="passentry" aria-describedby="usernameHelp"
type="password" name="password" required="">
<label for="confentry" class="mt-3">Confirm New Password</label>
<input autocomplete="off" class="form-control" id="confentry" aria-describedby="passwordHelp"
type="password" name="confirm_password" required="">
<input autocomplete="off" class="btn btn-primary login w-100 mt-3" type="submit" value="Change password">
</form>
</div>
{% endblock %}

View file

@ -51,8 +51,10 @@
{% endfor %}
</select>
</div>
<span class="text-small-extra text-muted">Themes are not officially supported. If you run into an issue, please report it or submit a pull request.</span>
</div>
</div>
</div>
<h2 class="h5">Profile Picture</h2>
<div class="settings-section rounded">

View file

@ -1,161 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
{% include "analytics.html" %}
<meta name="description" content="{{config('DESCRIPTION')}}">
{% include "csp.html" %}
<script src="{{ 'js/bootstrap.js' | asset }}"></script>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="author" content="">
<meta property="og:type" content="article">
<meta property="og:title" content="{{SITE_TITLE}}">
<meta property="og:site_name" content="{{request.host}}">
<meta property="og:image" content="{{ ('images/'~SITE_ID~'/site_preview.webp') | asset }}">
<meta property="og:url" content="{{request.host}}">
<meta property="og:description" name="description" content="{{config('DESCRIPTION')}}">
<meta property="og:author" name="author" content="{{SITE_FULL}}">
<meta property="og:site_name" content="{{request.host}}">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="{{SITE_FULL}}">
<meta name="twitter:title" content="{{SITE_TITLE}}">
<meta name="twitter:creator" content="{{SITE_FULL}}">
<meta name="twitter:description" content="{{config('DESCRIPTION')}}">
<meta name="twitter:image" content="{{ ('images/'~SITE_ID~'/site_preview.webp') | asset }}">
<meta name="twitter:url" content="{{request.host}}">
<title>{% if ref_user %}{{ref_user.username}} invites you to {{SITE_TITLE}}{% else %}Sign up - {{SITE_TITLE}}{% endif %}</title>
<style>:root{--primary:#{{config('DEFAULT_COLOR')}}</style>
<link rel="stylesheet" href="{{ 'css/main.css' | asset }}">
<link rel="stylesheet" href="{{ ('css/'~config('DEFAULT_THEME')~'.css') | asset }}">
</head>
<body id="login">
<nav class="navbar navbar-expand-lg navbar-dark bg-transparent fixed-top border-0">
<div class="container-fluid d-none">
<button class="navbar-toggler d-none" role="button" data-bs-toggle="collapse" data-bs-target="#navbarResponsive"
aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</nav>
<div class="container-fluid position-absolute h-100 p-0 overflow-auto">
<div class="row no-gutters h-100">
<div class="col-12 col-md-6 my-auto p-3">
<div class="row justify-content-center">
<div class="col-10 col-md-7">
<div class="mb-3">
<a href="/" class="text-decoration-none"><span class="h3 text-primary"></span></a>
</div>
<div id="register-form" class="">
{% if ref_user %}
<h1 class="h2">@{{ref_user.username}} has invited you!</h1>
<p class="text-muted mb-md-2">Looks like someone wants you to join {{SITE_TITLE}}.</p>
{% else %}
<h1 class="h2">Create your account.</h1>
<p class="text-muted mb-md-2">No email address required.</p>
{% endif %}
<form action="/signup" method="post" class="mt-md-3" id="signup">
{% if error %}<span class="text-danger">{{error}}</span><br>{% endif %}
<input type="hidden" name="formkey" value="{{formkey}}">
<input type="hidden" name="now" value="{{now}}">
{% if redirect %}<input type="hidden" name="redirect" value="{{redirect}}">{% endif %}
{% if ref_user %}
<input type="hidden" name="referred_by" value="{{ref_user.id}}">{% endif %}
<label for="username-register" class="mt-3">Username</label>
<input autocomplete="off" class="form-control" id="username-register"
aria-describedby="usernameHelpRegister" type="text" name="username" pattern="[a-zA-Z0-9_\-]{3,25}" min="3" max="25" required autofocus tabindex="1">
<small id="usernameHelpRegister"></small>
<label for="email-register" class="mt-3">Email Address</label>
<small class="d-inline-block text-muted ml-1">(optional)</small>
<input style="background-color: var(--gray-800)" autocomplete="off" class="form-control" id="email-register"
aria-describedby="emailHelpRegister" type="email" pattern='[^@]+@[^@]+\.[^@]+' name="email" tabindex="2">
<label for="password-register" class="mt-3">Password</label>
<input autocomplete="off" class="form-control" id="password-register"
aria-describedby="passwordHelpReigster" type="password" name="password" required tabindex="4">
<small id="passwordHelpRegister" class="form-text font-weight-bold text-muted d-none mt-1">Minimum of 8
characters
required.</small>
<small id="passwordHelpSuccess" class="form-text font-weight-bold text-success d-none mt-1">Your password meets the requirements.
</small>
<label for="password_confirm" class="mt-3">Confirm Password</label>
<input autocomplete="off" class="form-control" id="password_confirm"
aria-describedby="passwordConfirmHelp" type="password" name="password_confirm"
required tabindex="5">
<div class="custom-control custom-checkbox mt-4">
<input autocomplete="off" type="checkbox" class="custom-control-input" id="termsCheck" required tabindex="6">
<label class="custom-control-label terms" for="termsCheck">I accept the <a href="/rules" tabindex="8">rules</a></label>
</div>
{% if hcaptcha %}
<div class="h-captcha" data-sitekey="{{hcaptcha}}"></div>
{% endif %}
<button class="btn btn-primary login w-100 mt-3" id="register_button" tabindex="7">Register</button>
<div class="text-center text-muted text-small mt-2 mb-0">
Already have an account? <a href="/login{{'?redirect='+redirect if redirect else ''}}" class="font-weight-bold toggle-login" tabindex="9">Log in</a>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="col-12 col-md-6 d-none d-md-block">
<div class="splash-wrapper">
<div class="splash-overlay"></div>
<img alt="cover" loading="lazy" class="splash-img" src="{{ ('images/'~SITE_ID~'/cover.webp') | asset }}"></img>
</div>
</div>
</div>
</div>
<script src="{{ 'js/signup.js' | asset }}"></script>
{% if hcaptcha %}
<script src="{{ 'js/hcaptcha.js' | asset }}"></script>
{% endif %}
</body>
</html>

View file

@ -1,106 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
{% include "analytics.html" %}
<meta name="description" content="{{config('DESCRIPTION')}}">
{% include "csp.html" %}
<script src="{{ 'js/bootstrap.js' | asset }}"></script>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="author" content="">
<meta property="og:type" content="article">
<meta property="og:title" content="{{SITE_TITLE}}">
<meta property="og:site_name" content="{{request.host}}">
<meta property="og:image" content="{{ ('images/'~SITE_ID~'/site_preview.webp') | asset }}">
<meta property="og:url" content="{{request.host}}">
<meta property="og:description" name="description" content="{{config('DESCRIPTION')}}">
<meta property="og:author" name="author" content="{{SITE_FULL}}">
<meta property="og:site_name" content="{{request.host}}">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="{{SITE_FULL}}">
<meta name="twitter:title" content="{{SITE_TITLE}}">
<meta name="twitter:creator" content="{{SITE_FULL}}">
<meta name="twitter:description" content="{{config('DESCRIPTION')}}">
<meta name="twitter:image" content="{{ ('images/'~SITE_ID~'/site_preview.webp') | asset }}">
<meta name="twitter:url" content="{{request.host}}">
<title>{% if ref_user %}{{ref_user.username}} invites you to {{SITE_TITLE}}{% else %}{{SITE_TITLE}}{% endif %}</title>
<style>:root{--primary:#{{config('DEFAULT_COLOR')}}</style>
<link rel="stylesheet" href="{{ 'css/main.css' | asset }}">
<link rel="stylesheet" href="{{ ('css/'~config('DEFAULT_THEME')~'.css') | asset }}">
</head>
<body id="login">
<nav class="navbar navbar-expand-lg navbar-dark bg-transparent fixed-top border-0">
<div class="container-fluid">
<button class="navbar-toggler d-none" role="button" data-bs-toggle="collapse" data-bs-target="#navbarResponsive"
aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</nav>
<div class="container-fluid position-absolute h-100 p-0">
<div class="row no-gutters h-100">
<div class="col-12 col-md-6 my-auto p-3">
<div class="row justify-content-center">
<div class="col-10 col-md-7">
<div class="text-center mb-5">
<a href="/" class="text-decoration-none"><span class="h3 text-primary"></span></a>
</div>
<div id="register-form" class="">
<h1 class="h4 font-weight-normal text-center">Whoops! You can't refer yourself!</h1>
<p class="text-center text-muted mb-md-5">Send this link to a friend instead :)</p>
<label>Referral code</label>
<input autocomplete="off" type="text" class="form-control copy-link" readonly value="{{SITE_FULL}}/signup?ref={{request.values.get('ref')}}" data-clipboard-text="{{SITE_FULL}}/signup?ref={{request.values.get('ref')}}">
<div class="text-center mt-5 mb-3">
Already have an account? <a href="/login" class="font-weight-bold text-small toggle-login">Log in.</a>
</div>
</div>
</div>
</div>
</div>
<div class="col-12 col-md-6 d-none d-md-block">
<div class="splash-wrapper">
<div class="splash-overlay"></div>
<img alt="cover" loading="lazy" class="splash-img" src="{{ ('images/'~SITE_ID~'/cover.webp') | asset }}"></img>
</div>
</div>
</div>
</div>
<div class="toast clipboard" id="toast-success" role="alert" aria-live="assertive" aria-atomic="true" data-bs-animation="true" data-bs-autohide="true" data-bs-delay="5000">
<div class="toast-body text-center">
<i class="fas fa-check-circle text-success mr-2"></i>Link copied to clipboard
</div>
</div>
</body>
</html>

View file

@ -421,7 +421,7 @@
<div id="comment-form-space-{{p.fullname}}" class="comment-write mb-3">
<form id="reply-to-{{p.fullname}}" action="/comment" method="post">
{{forms.formkey(v)}}
<input type="hidden" name="parent_fullname" value="t2_{{p.id}}">
<input type="hidden" name="parent_fullname" value="{{p.fullname}}">
<input autocomplete="off" id="reply-form-submission-{{p.fullname}}" type="hidden" name="submission" value="{{p.id}}">
<textarea required autocomplete="off" minlength="1" maxlength="{{COMMENT_BODY_LENGTH_MAXIMUM}}" oninput="markdown('reply-form-body-{{p.fullname}}', 'form-preview-{{p.id}}');charLimit('reply-form-body-{{p.fullname}}','charcount-reply')" id="reply-form-body-{{p.fullname}}" data-fullname="{{p.fullname}}" class="comment-box form-control rounded" id="comment-form" name="body" form="reply-to-{{p.fullname}}" aria-label="With textarea" placeholder="Add your comment..." rows="3"></textarea>

View file

@ -14,91 +14,73 @@
{% endblock %}
{% block content %}
<div class="mb-2 p-3">
<div class="col-12">
<div id="post-{{p.id}}" class="card d-flex flex-row-reverse flex-nowrap justify-content-end border-0 p-0 {% if voted==1 %} upvoted{% elif voted==-1 %} downvoted{% endif %}">
<div class="card-block my-md-auto{% if p.state_mod == StateMod.REMOVED %} banned{% endif %}">
<div class="post-meta text-left d-md-none mb-1">{% if p.over_18 %}<span class="badge badge-danger">+18</span> {% endif %}{% if p.state_mod == StateMod.REMOVED %}removed by @{{p.state_mod_set_by}}{% else %}[Deleted by user]{% endif %}</div>
<h5 class="card-title post-title text-left mb-0 mb-md-1">{{p.plaintitle(v)}}</h5>
<div class="post-meta text-left d-none d-md-block">{% if p.over_18 %}<span class="badge badge-danger">+18</span> {% endif %}{% if p.state_mod == StateMod.REMOVED %}removed by @{{p.state_mod_set_by}}{% else %}[Deleted by user]{% endif %}</div>
</div>
<div id="voting" class="d-md-block my-auto mr-3 text-center">
<div class="post-{{p.id}}-up arrow-up mx-auto">
</div>
<span class="post-{{p.id}}-score-up score-up text-muted{% if voted!=1 %} d-none{% endif %}"></span>
<span class="post-{{p.id}}-score-none score text-muted{% if voted!=0 and voted!=-2 %} d-none{% endif %}"></span>
<span class="post-{{p.id}}-score-down score-down text-muted{% if voted!=-1 %} d-none{% endif %}"></span>
<div class="post-{{p.id}}-down arrow-down mx-auto">
</div>
</div>
</div>
{% if v and v.admin_level >= 2 and p.body_html %}
<div class="post-body mt-4 mb-2">
{{p.body_html | safe}}
<div class="mb-2 p-3">
<div class="col-12">
<div id="post-{{p.id}}" class="card d-flex flex-row-reverse flex-nowrap justify-content-end border-0 p-0 {% if voted==1 %} upvoted{% elif voted==-1 %} downvoted{% endif %}">
<div class="card-block my-md-auto{% if p.state_mod == StateMod.REMOVED %} banned{% endif %}">
<div class="post-meta text-left d-md-none mb-1">
{% if p.over_18 %}<span class="badge badge-danger">+18</span>{% endif %}
{% if p.state_mod == StateMod.REMOVED %}removed by @{{p.state_mod_set_by}}{% else %}[Deleted by user]{% endif %}</div>
<h5 class="card-title post-title text-left mb-0 mb-md-1">{{p.plaintitle(v)}}</h5>
<div class="post-meta text-left d-none d-md-block">
{% if p.over_18 %}<span class="badge badge-danger">+18</span>{% endif %}
{% if p.state_mod == StateMod.REMOVED %}removed by @{{p.state_mod_set_by}}{% else %}[Deleted by user]{% endif %}
</div>
</div>
<div id="voting" class="d-md-block my-auto mr-3 text-center">
<div class="post-{{p.id}}-up arrow-up mx-auto">
</div>
<span class="post-{{p.id}}-score-up score-up text-muted{% if voted!=1 %} d-none{% endif %}"></span>
<span class="post-{{p.id}}-score-none score text-muted{% if voted!=0 and voted!=-2 %} d-none{% endif %}"></span>
<span class="post-{{p.id}}-score-down score-down text-muted{% if voted!=-1 %} d-none{% endif %}"></span>
<div class="post-{{p.id}}-down arrow-down mx-auto">
</div>
</div>
</div>
{% if v and v.admin_level >= 2 and p.body_html %}
<div class="post-body mt-4 mb-2">
{{p.realbody(v) | safe}}
</div>
{% endif %}
</div>
</div>
<div class="row mb-2 p-3">
<div class="col-12">
<div class="post-actions d-none d-md-block">
<ul class="list-inline text-left mb-2">
<li class="list-inline-item"><a href="{{p.permalink}}"><i
class="fas fa-comment-dots"></i>{{p.comment_count}} Comment{{'s' if p.comment_count != 1 else ''}}</a>
</li>
</ul>
</div>
</div>
</div>
{% include "volunteer_teaser.html" %}
<div class="comment-section">
</div>
</div>
<div class="row mb-2 p-3">
<div class="col-12">
<div class="post-actions d-none d-md-block">
<ul class="list-inline text-left mb-2">
<li class="list-inline-item"><a href="{{p.permalink}}">
<i class="fas fa-comment-dots"></i>{{p.comment_count}} Comment{{'s' if p.comment_count != 1 else ''}}</a>
</li>
</ul>
</div>
</div>
</div>
{% include "volunteer_teaser.html" %}
<div class="comment-section">
{% with comments=p.replies %}
{% include "comments.html" %}
{% endwith %}
</div>
{% endblock %}
{% block mobileactions %}
<div class="row fixed-top bg-white shadow d-inline-flex d-md-none p-3" id="footer-actions" style="z-index: 3; top: 48px; transition: top cubic-bezier(0, 0, 0.2, 1) 220ms;">
<div class="col text-center">
<div class="post-actions mx-auto">
<ul class="list-inline">
<li id="voting-mobile" class="voting list-inline-item{% if voted==1 %} upvoted{% elif voted==-1 %} downvoted{% endif %}">
<span class="arrow-mobile-up mr-2 arrow-mobile-up">
<i class="fas fa-arrow-alt-up mx-0"></i>
</span>
<span class="post-{{p.id}}-score-mobile-up score-up text-muted{% if voted!=1 %} d-none{% endif %}"></span>
<span class="post-{{p.id}}-score-mobile-none score text-muted{% if voted!=0 and voted!=-2 %} d-none{% endif %}"></span>
<span class="post-{{p.id}}-score-mobile-down score-down text-muted{% if voted!=-1 %} d-none{% endif %}"></span>
<span class="arrow-mobile-down arrow-mobile-down ml-2 my-0">
<i class="fas fa-arrow-alt-down mx-0"></i>
</span>
</li>
</ul>
</div>
</div>
</div>
<div class="row fixed-top bg-white shadow d-inline-flex d-md-none p-3" id="footer-actions" style="z-index: 3; top: 48px; transition: top cubic-bezier(0, 0, 0.2, 1) 220ms;">
<div class="col text-center">
<div class="post-actions mx-auto">
<ul class="list-inline">
<li id="voting-mobile" class="voting list-inline-item{% if voted==1 %} upvoted{% elif voted==-1 %} downvoted{% endif %}">
<span class="arrow-mobile-up mr-2 arrow-mobile-up">
<i class="fas fa-arrow-alt-up mx-0"></i>
</span>
<span class="post-{{p.id}}-score-mobile-up score-up text-muted{% if voted!=1 %} d-none{% endif %}"></span>
<span class="post-{{p.id}}-score-mobile-none score text-muted{% if voted!=0 and voted!=-2 %} d-none{% endif %}"></span>
<span class="post-{{p.id}}-score-mobile-down score-down text-muted{% if voted!=-1 %} d-none{% endif %}"></span>
<span class="arrow-mobile-down arrow-mobile-down ml-2 my-0">
<i class="fas fa-arrow-alt-down mx-0"></i>
</span>
</li>
</ul>
</div>
</div>
</div>
{% endblock %}

View file

@ -15,7 +15,7 @@ class CommentsFixture:
assert submit_get_response.status_code == 200
comment_body = data.get('body', util.generate_text())
submit_comment_response = client.post("/comment", data={
"parent_fullname": f't2_{post_id}',
"parent_fullname": f'post_{post_id}',
'parent_level': 1,
'submission': post_id,
"body": comment_body,

View file

@ -24,9 +24,9 @@ class SubmissionsFixture:
assert post_body in submit_post_response.text
post_info = util.ItemData.from_html(submit_post_response.text)
post_id_full = post_info.id_full
assert post_id_full.startswith('t2_')
assert post_id_full.startswith('post_')
post_id = int(post_id_full[3:])
post_id = int(post_id_full.split('_')[1])
db = db_session()
submission = db.query(Submission).filter_by(id=post_id).first()

View file

@ -115,19 +115,19 @@ def test_comment_descendant_count(accounts, submissions, comments):
reply1 = comments.comment_for_client(alice_client, post.id, {
'body': 'You\'re wrong, this isn\'t contentious',
'parent_fullname': f't3_{root.id}',
'parent_fullname': f'comment_{root.id}',
'parent_level': root.level,
})
rereply1 = comments.comment_for_client(alice_client, post.id, {
'body': 'no u',
'parent_fullname': f't3_{reply1.id}',
'parent_fullname': f'comment_{reply1.id}',
'parent_level': reply1.level,
})
reply2 = comments.comment_for_client(alice_client, post.id, {
'body': 'Good poast',
'parent_fullname': f't3_{root.id}',
'parent_fullname': f'comment_{root.id}',
'parent_level': root.level,
})
@ -153,7 +153,7 @@ def test_more_button_label_in_deep_threads(accounts, submissions, comments):
for i in range(1, 25 + 1):
c = comments.comment_for_client(alice_client, post.id, {
'body': str(i),
'parent_fullname': f't3_{c.id}',
'parent_fullname': f'comment_{c.id}',
'parent_level': c.level,
})
if i % 5 == 0:

View file

@ -37,9 +37,9 @@ def no_rate_limit(test_function):
# this is meant to be a utility class that stores post and comment references so you can use them in other calls
# it's pretty barebones and will probably be fleshed out
class ItemData:
id = None
id_full = None
url = None
id: str | None = None
id_full: str | None = None
url: str | None = None
@staticmethod
def from_html(text):
@ -52,7 +52,7 @@ class ItemData:
result = ItemData()
result.id = match.group(1) # this really should get yanked out of the JS, not the URL
result.id_full = f"t2_{result.id}"
result.id_full = f"post_{result.id}"
result.url = url
return result
@ -69,6 +69,6 @@ class ItemData:
result = ItemData()
result.id = match.group(1) # this really should get yanked out of the JS, not the HTML
result.id_full = f"t3_{result.id}"
result.id_full = f"comment_{result.id}"
result.url = f"/comment/{result.id}"
return result

View file

@ -17,8 +17,8 @@ depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}
${downgrades if downgrades else "pass"}

View file

@ -17,22 +17,22 @@ depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('submissions', sa.Column('bump_utc', sa.Integer(), server_default=sa.schema.FetchedValue(), nullable=True))
op.create_foreign_key('comments_app_id_fkey', 'comments', 'oauth_apps', ['app_id'], ['id'])
op.create_foreign_key('commentvotes_app_id_fkey', 'commentvotes', 'oauth_apps', ['app_id'], ['id'])
op.create_foreign_key('modactions_user_id_fkey', 'modactions', 'users', ['user_id'], ['id'])
op.create_foreign_key('submissions_app_id_fkey', 'submissions', 'oauth_apps', ['app_id'], ['id'])
op.create_foreign_key('votes_app_id_fkey', 'votes', 'oauth_apps', ['app_id'], ['id'])
# ### end Alembic commands ###
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('submissions', sa.Column('bump_utc', sa.Integer(), server_default=sa.schema.FetchedValue(), nullable=True))
op.create_foreign_key('comments_app_id_fkey', 'comments', 'oauth_apps', ['app_id'], ['id'])
op.create_foreign_key('commentvotes_app_id_fkey', 'commentvotes', 'oauth_apps', ['app_id'], ['id'])
op.create_foreign_key('modactions_user_id_fkey', 'modactions', 'users', ['user_id'], ['id'])
op.create_foreign_key('submissions_app_id_fkey', 'submissions', 'oauth_apps', ['app_id'], ['id'])
op.create_foreign_key('votes_app_id_fkey', 'votes', 'oauth_apps', ['app_id'], ['id'])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint('votes_app_id_fkey', 'votes', type_='foreignkey')
op.drop_constraint('submissions_app_id_fkey', 'submissions', type_='foreignkey')
op.drop_constraint('modactions_user_id_fkey', 'modactions', type_='foreignkey')
op.drop_constraint('commentvotes_app_id_fkey', 'commentvotes', type_='foreignkey')
op.drop_constraint('comments_app_id_fkey', 'comments', type_='foreignkey')
op.drop_column('submissions', 'bump_utc')
# ### end Alembic commands ###
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint('votes_app_id_fkey', 'votes', type_='foreignkey')
op.drop_constraint('submissions_app_id_fkey', 'submissions', type_='foreignkey')
op.drop_constraint('modactions_user_id_fkey', 'modactions', type_='foreignkey')
op.drop_constraint('commentvotes_app_id_fkey', 'commentvotes', type_='foreignkey')
op.drop_constraint('comments_app_id_fkey', 'comments', type_='foreignkey')
op.drop_column('submissions', 'bump_utc')
# ### end Alembic commands ###

View file

@ -17,30 +17,30 @@ depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('usernotes', 'note',
existing_type=sa.VARCHAR(length=10000),
nullable=False)
op.alter_column('usernotes', 'tag',
existing_type=sa.VARCHAR(length=10),
nullable=False)
op.create_foreign_key('usernotes_author_id_fkey', 'usernotes', 'users', ['author_id'], ['id'])
op.create_foreign_key('usernotes_reference_comment_fkey', 'usernotes', 'comments', ['reference_comment'], ['id'], ondelete='SET NULL')
op.create_foreign_key('usernotes_reference_post_fkey', 'usernotes', 'submissions', ['reference_post'], ['id'], ondelete='SET NULL')
op.create_foreign_key('usernotes_reference_user_fkey', 'usernotes', 'users', ['reference_user'], ['id'], ondelete='CASCADE')
# ### end Alembic commands ###
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('usernotes', 'note',
existing_type=sa.VARCHAR(length=10000),
nullable=False)
op.alter_column('usernotes', 'tag',
existing_type=sa.VARCHAR(length=10),
nullable=False)
op.create_foreign_key('usernotes_author_id_fkey', 'usernotes', 'users', ['author_id'], ['id'])
op.create_foreign_key('usernotes_reference_comment_fkey', 'usernotes', 'comments', ['reference_comment'], ['id'], ondelete='SET NULL')
op.create_foreign_key('usernotes_reference_post_fkey', 'usernotes', 'submissions', ['reference_post'], ['id'], ondelete='SET NULL')
op.create_foreign_key('usernotes_reference_user_fkey', 'usernotes', 'users', ['reference_user'], ['id'], ondelete='CASCADE')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint('usernotes_reference_user_fkey', 'usernotes', type_='foreignkey')
op.drop_constraint('usernotes_reference_post_fkey', 'usernotes', type_='foreignkey')
op.drop_constraint('usernotes_reference_comment_fkey', 'usernotes', type_='foreignkey')
op.drop_constraint('usernotes_author_id_fkey', 'usernotes', type_='foreignkey')
op.alter_column('usernotes', 'tag',
existing_type=sa.VARCHAR(length=10),
nullable=True)
op.alter_column('usernotes', 'note',
existing_type=sa.VARCHAR(length=10000),
nullable=True)
# ### end Alembic commands ###
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint('usernotes_reference_user_fkey', 'usernotes', type_='foreignkey')
op.drop_constraint('usernotes_reference_post_fkey', 'usernotes', type_='foreignkey')
op.drop_constraint('usernotes_reference_comment_fkey', 'usernotes', type_='foreignkey')
op.drop_constraint('usernotes_author_id_fkey', 'usernotes', type_='foreignkey')
op.alter_column('usernotes', 'tag',
existing_type=sa.VARCHAR(length=10),
nullable=True)
op.alter_column('usernotes', 'note',
existing_type=sa.VARCHAR(length=10000),
nullable=True)
# ### end Alembic commands ###

View file

@ -17,12 +17,12 @@ depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('comments', 'treasure_amount')
# ### end Alembic commands ###
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('comments', 'treasure_amount')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('comments', sa.Column('treasure_amount', sa.VARCHAR(length=10), autoincrement=False, nullable=True))
# ### end Alembic commands ###
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('comments', sa.Column('treasure_amount', sa.VARCHAR(length=10), autoincrement=False, nullable=True))
# ### end Alembic commands ###

View file

@ -17,16 +17,16 @@ depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('comments', 'slots_result')
op.drop_column('comments', 'blackjack_result')
op.drop_column('comments', 'wordle_result')
# ### end Alembic commands ###
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('comments', 'slots_result')
op.drop_column('comments', 'blackjack_result')
op.drop_column('comments', 'wordle_result')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('comments', sa.Column('wordle_result', sa.VARCHAR(length=115), autoincrement=False, nullable=True))
op.add_column('comments', sa.Column('blackjack_result', sa.VARCHAR(length=860), autoincrement=False, nullable=True))
op.add_column('comments', sa.Column('slots_result', sa.VARCHAR(length=32), autoincrement=False, nullable=True))
# ### end Alembic commands ###
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('comments', sa.Column('wordle_result', sa.VARCHAR(length=115), autoincrement=False, nullable=True))
op.add_column('comments', sa.Column('blackjack_result', sa.VARCHAR(length=860), autoincrement=False, nullable=True))
op.add_column('comments', sa.Column('slots_result', sa.VARCHAR(length=32), autoincrement=False, nullable=True))
# ### end Alembic commands ###

View file

@ -17,16 +17,16 @@ depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('users', 'sigs_disabled')
op.drop_column('users', 'sig_html')
op.drop_column('users', 'sig')
# ### end Alembic commands ###
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('users', 'sigs_disabled')
op.drop_column('users', 'sig_html')
op.drop_column('users', 'sig')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('users', sa.Column('sig', sa.VARCHAR(length=200), autoincrement=False, nullable=True))
op.add_column('users', sa.Column('sig_html', sa.VARCHAR(length=1000), autoincrement=False, nullable=True))
op.add_column('users', sa.Column('sigs_disabled', sa.BOOLEAN(), autoincrement=False, nullable=True))
# ### end Alembic commands ###
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('users', sa.Column('sig', sa.VARCHAR(length=200), autoincrement=False, nullable=True))
op.add_column('users', sa.Column('sig_html', sa.VARCHAR(length=1000), autoincrement=False, nullable=True))
op.add_column('users', sa.Column('sigs_disabled', sa.BOOLEAN(), autoincrement=False, nullable=True))
# ### end Alembic commands ###

View file

@ -17,34 +17,34 @@ depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('users', 'alt')
op.drop_column('users', 'unblockable')
op.drop_column('users', 'deflector')
op.drop_column('users', 'bird')
op.drop_column('users', 'rehab')
op.drop_column('users', 'fish')
op.drop_column('users', 'mute')
op.drop_column('users', 'eye')
op.drop_column('users', 'longpost')
op.drop_column('users', 'marseyawarded')
op.drop_column('users', 'progressivestack')
op.drop_column('users', 'unmutable')
# ### end Alembic commands ###
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('users', 'alt')
op.drop_column('users', 'unblockable')
op.drop_column('users', 'deflector')
op.drop_column('users', 'bird')
op.drop_column('users', 'rehab')
op.drop_column('users', 'fish')
op.drop_column('users', 'mute')
op.drop_column('users', 'eye')
op.drop_column('users', 'longpost')
op.drop_column('users', 'marseyawarded')
op.drop_column('users', 'progressivestack')
op.drop_column('users', 'unmutable')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('users', sa.Column('unmutable', sa.BOOLEAN(), autoincrement=False, nullable=True))
op.add_column('users', sa.Column('progressivestack', sa.INTEGER(), autoincrement=False, nullable=True))
op.add_column('users', sa.Column('marseyawarded', sa.INTEGER(), autoincrement=False, nullable=True))
op.add_column('users', sa.Column('longpost', sa.INTEGER(), autoincrement=False, nullable=True))
op.add_column('users', sa.Column('eye', sa.BOOLEAN(), autoincrement=False, nullable=True))
op.add_column('users', sa.Column('mute', sa.BOOLEAN(), autoincrement=False, nullable=True))
op.add_column('users', sa.Column('fish', sa.BOOLEAN(), autoincrement=False, nullable=True))
op.add_column('users', sa.Column('rehab', sa.INTEGER(), autoincrement=False, nullable=True))
op.add_column('users', sa.Column('bird', sa.INTEGER(), autoincrement=False, nullable=True))
op.add_column('users', sa.Column('deflector', sa.INTEGER(), autoincrement=False, nullable=True))
op.add_column('users', sa.Column('unblockable', sa.BOOLEAN(), autoincrement=False, nullable=True))
op.add_column('users', sa.Column('alt', sa.BOOLEAN(), autoincrement=False, nullable=True))
# ### end Alembic commands ###
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('users', sa.Column('unmutable', sa.BOOLEAN(), autoincrement=False, nullable=True))
op.add_column('users', sa.Column('progressivestack', sa.INTEGER(), autoincrement=False, nullable=True))
op.add_column('users', sa.Column('marseyawarded', sa.INTEGER(), autoincrement=False, nullable=True))
op.add_column('users', sa.Column('longpost', sa.INTEGER(), autoincrement=False, nullable=True))
op.add_column('users', sa.Column('eye', sa.BOOLEAN(), autoincrement=False, nullable=True))
op.add_column('users', sa.Column('mute', sa.BOOLEAN(), autoincrement=False, nullable=True))
op.add_column('users', sa.Column('fish', sa.BOOLEAN(), autoincrement=False, nullable=True))
op.add_column('users', sa.Column('rehab', sa.INTEGER(), autoincrement=False, nullable=True))
op.add_column('users', sa.Column('bird', sa.INTEGER(), autoincrement=False, nullable=True))
op.add_column('users', sa.Column('deflector', sa.INTEGER(), autoincrement=False, nullable=True))
op.add_column('users', sa.Column('unblockable', sa.BOOLEAN(), autoincrement=False, nullable=True))
op.add_column('users', sa.Column('alt', sa.BOOLEAN(), autoincrement=False, nullable=True))
# ### end Alembic commands ###

View file

@ -17,12 +17,12 @@ depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('users', sa.Column('volunteer_last_started_utc', sa.DateTime(), nullable=True))
# ### end Alembic commands ###
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('users', sa.Column('volunteer_last_started_utc', sa.DateTime(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('users', 'volunteer_last_started_utc')
# ### end Alembic commands ###
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('users', 'volunteer_last_started_utc')
# ### end Alembic commands ###

View file

@ -17,23 +17,23 @@ depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('volunteer_janitor',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('comment_id', sa.Integer(), nullable=False),
sa.Column('edited_utc', sa.DateTime(), nullable=False),
sa.Column('result', sa.Enum('Pending', 'TopQuality', 'Good', 'Neutral', 'Bad', 'Warning', 'Ban', name='volunteerjanitorresult'), nullable=False),
sa.ForeignKeyConstraint(['comment_id'], ['comments.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index('volunteer_comment_index', 'volunteer_janitor', ['user_id', 'comment_id'], unique=False)
# ### end Alembic commands ###
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('volunteer_janitor',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('comment_id', sa.Integer(), nullable=False),
sa.Column('edited_utc', sa.DateTime(), nullable=False),
sa.Column('result', sa.Enum('Pending', 'TopQuality', 'Good', 'Neutral', 'Bad', 'Warning', 'Ban', name='volunteerjanitorresult'), nullable=False),
sa.ForeignKeyConstraint(['comment_id'], ['comments.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index('volunteer_comment_index', 'volunteer_janitor', ['user_id', 'comment_id'], unique=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index('volunteer_comment_index', table_name='volunteer_janitor')
op.drop_table('volunteer_janitor')
# ### end Alembic commands ###
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index('volunteer_comment_index', table_name='volunteer_janitor')
op.drop_table('volunteer_janitor')
# ### end Alembic commands ###

View file

@ -17,14 +17,14 @@ depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('volunteer_janitor', sa.Column('recorded_utc', sa.DateTime(), nullable=False))
op.drop_column('volunteer_janitor', 'edited_utc')
# ### end Alembic commands ###
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('volunteer_janitor', sa.Column('recorded_utc', sa.DateTime(), nullable=False))
op.drop_column('volunteer_janitor', 'edited_utc')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('volunteer_janitor', sa.Column('edited_utc', postgresql.TIMESTAMP(), autoincrement=False, nullable=False))
op.drop_column('volunteer_janitor', 'recorded_utc')
# ### end Alembic commands ###
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('volunteer_janitor', sa.Column('edited_utc', postgresql.TIMESTAMP(), autoincrement=False, nullable=False))
op.drop_column('volunteer_janitor', 'recorded_utc')
# ### end Alembic commands ###

View file

@ -17,16 +17,16 @@ depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index('discord_id_idx', table_name='users')
op.drop_constraint('one_discord_account', 'users', type_='unique')
op.drop_column('users', 'discord_id')
# ### end Alembic commands ###
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index('discord_id_idx', table_name='users')
op.drop_constraint('one_discord_account', 'users', type_='unique')
op.drop_column('users', 'discord_id')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('users', sa.Column('discord_id', sa.VARCHAR(length=64), autoincrement=False, nullable=True))
op.create_unique_constraint('one_discord_account', 'users', ['discord_id'])
op.create_index('discord_id_idx', 'users', ['discord_id'], unique=False)
# ### end Alembic commands ###
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('users', sa.Column('discord_id', sa.VARCHAR(length=64), autoincrement=False, nullable=True))
op.create_unique_constraint('one_discord_account', 'users', ['discord_id'])
op.create_index('discord_id_idx', 'users', ['discord_id'], unique=False)
# ### end Alembic commands ###

View file

@ -17,17 +17,17 @@ depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('comments', sa.Column(
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('comments', sa.Column(
'descendant_count',
sa.Integer(),
nullable=False,
server_default=sa.text('0')
))
# ### end Alembic commands ###
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('comments', 'descendant_count')
# ### end Alembic commands ###
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('comments', 'descendant_count')
# ### end Alembic commands ###

View file

@ -22,9 +22,9 @@ branch_labels = None
depends_on = None
def upgrade():
db =Session(bind=op.get_bind())
db: Session = Session(bind=op.get_bind())
bulk_recompute_descendant_counts(lambda q: q, db)
def downgrade():
db =Session(bind=op.get_bind())
db: Session = Session(bind=op.get_bind())
db.execute(update(Comment).values(descendant_count=0))

View file

@ -17,12 +17,12 @@ depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('users', 'song')
# ### end Alembic commands ###
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('users', 'song')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('users', sa.Column('song', sa.VARCHAR(length=50), autoincrement=False, nullable=True))
# ### end Alembic commands ###
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('users', sa.Column('song', sa.VARCHAR(length=50), autoincrement=False, nullable=True))
# ### end Alembic commands ###

View file

@ -17,50 +17,50 @@ depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('comments', 'body',
existing_type=sa.VARCHAR(length=40000),
type_=sa.Text(),
existing_nullable=True)
op.alter_column('comments', 'body_html',
existing_type=sa.VARCHAR(length=40000),
type_=sa.Text(),
nullable=False)
op.alter_column('submissions', 'body',
existing_type=sa.VARCHAR(length=40000),
type_=sa.Text(),
existing_nullable=True)
op.alter_column('submissions', 'body_html',
existing_type=sa.VARCHAR(length=40000),
type_=sa.Text(),
existing_nullable=True)
# ### end Alembic commands ###
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('comments', 'body',
existing_type=sa.VARCHAR(length=40000),
type_=sa.Text(),
existing_nullable=True)
op.alter_column('comments', 'body_html',
existing_type=sa.VARCHAR(length=40000),
type_=sa.Text(),
nullable=False)
op.alter_column('submissions', 'body',
existing_type=sa.VARCHAR(length=40000),
type_=sa.Text(),
existing_nullable=True)
op.alter_column('submissions', 'body_html',
existing_type=sa.VARCHAR(length=40000),
type_=sa.Text(),
existing_nullable=True)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('submissions', 'body_html',
existing_type=sa.Text(),
type_=sa.VARCHAR(length=40000),
existing_nullable=True)
op.alter_column('submissions', 'body',
existing_type=sa.Text(),
type_=sa.VARCHAR(length=40000),
existing_nullable=True)
op.alter_column('oauth_apps', 'redirect_uri',
existing_type=sa.String(length=50),
type_=sa.VARCHAR(length=4096),
existing_nullable=False)
op.alter_column('oauth_apps', 'client_id',
existing_type=sa.String(),
type_=sa.CHAR(length=64),
existing_nullable=True)
op.alter_column('comments', 'body_html',
existing_type=sa.Text(),
type_=sa.VARCHAR(length=40000),
nullable=True)
op.alter_column('comments', 'body',
existing_type=sa.Text(),
type_=sa.VARCHAR(length=40000),
existing_nullable=True)
# ### end Alembic commands ###
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('submissions', 'body_html',
existing_type=sa.Text(),
type_=sa.VARCHAR(length=40000),
existing_nullable=True)
op.alter_column('submissions', 'body',
existing_type=sa.Text(),
type_=sa.VARCHAR(length=40000),
existing_nullable=True)
op.alter_column('oauth_apps', 'redirect_uri',
existing_type=sa.String(length=50),
type_=sa.VARCHAR(length=4096),
existing_nullable=False)
op.alter_column('oauth_apps', 'client_id',
existing_type=sa.String(),
type_=sa.CHAR(length=64),
existing_nullable=True)
op.alter_column('comments', 'body_html',
existing_type=sa.Text(),
type_=sa.VARCHAR(length=40000),
nullable=True)
op.alter_column('comments', 'body',
existing_type=sa.Text(),
type_=sa.VARCHAR(length=40000),
existing_nullable=True)
# ### end Alembic commands ###

View file

@ -17,65 +17,65 @@ depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('tasks_repeatable',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('author_id', sa.Integer(), nullable=False),
sa.Column('type_id', sa.SmallInteger(), nullable=False),
sa.Column('enabled', sa.Boolean(), nullable=False),
sa.Column('run_state', sa.SmallInteger(), nullable=False),
sa.Column('run_time_last', sa.DateTime(), nullable=True),
sa.Column('frequency_day', sa.SmallInteger(), nullable=False),
sa.Column('time_of_day_utc', sa.Time(), nullable=False),
sa.Column('created_utc', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['author_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('tasks_repeatable_python',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('import_path', sa.String(), nullable=False),
sa.Column('callable', sa.String(), nullable=False),
sa.ForeignKeyConstraint(['id'], ['tasks_repeatable.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('tasks_repeatable_runs',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('task_id', sa.Integer(), nullable=False),
sa.Column('manual', sa.Boolean(), nullable=False),
sa.Column('traceback_str', sa.Text(), nullable=True),
sa.Column('completed_utc', sa.DateTime(), nullable=True),
sa.Column('created_utc', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['task_id'], ['tasks_repeatable.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('tasks_repeatable_scheduled_submissions',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('author_id_submission', sa.Integer(), nullable=False),
sa.Column('ghost', sa.Boolean(), nullable=False),
sa.Column('private', sa.Boolean(), nullable=False),
sa.Column('over_18', sa.Boolean(), nullable=False),
sa.Column('is_bot', sa.Boolean(), nullable=False),
sa.Column('title', sa.String(length=500), nullable=False),
sa.Column('url', sa.String(), nullable=True),
sa.Column('body', sa.Text(), nullable=True),
sa.Column('body_html', sa.Text(), nullable=True),
sa.Column('flair', sa.String(), nullable=True),
sa.Column('embed_url', sa.String(), nullable=True),
sa.ForeignKeyConstraint(['author_id_submission'], ['users.id'], ),
sa.ForeignKeyConstraint(['id'], ['tasks_repeatable.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.add_column('submissions', sa.Column('task_id', sa.Integer(), nullable=True))
op.create_foreign_key(None, 'submissions', 'tasks_repeatable_scheduled_submissions', ['task_id'], ['id'])
# ### end Alembic commands ###
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('tasks_repeatable',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('author_id', sa.Integer(), nullable=False),
sa.Column('type_id', sa.SmallInteger(), nullable=False),
sa.Column('enabled', sa.Boolean(), nullable=False),
sa.Column('run_state', sa.SmallInteger(), nullable=False),
sa.Column('run_time_last', sa.DateTime(), nullable=True),
sa.Column('frequency_day', sa.SmallInteger(), nullable=False),
sa.Column('time_of_day_utc', sa.Time(), nullable=False),
sa.Column('created_utc', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['author_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('tasks_repeatable_python',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('import_path', sa.String(), nullable=False),
sa.Column('callable', sa.String(), nullable=False),
sa.ForeignKeyConstraint(['id'], ['tasks_repeatable.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('tasks_repeatable_runs',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('task_id', sa.Integer(), nullable=False),
sa.Column('manual', sa.Boolean(), nullable=False),
sa.Column('traceback_str', sa.Text(), nullable=True),
sa.Column('completed_utc', sa.DateTime(), nullable=True),
sa.Column('created_utc', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['task_id'], ['tasks_repeatable.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('tasks_repeatable_scheduled_submissions',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('author_id_submission', sa.Integer(), nullable=False),
sa.Column('ghost', sa.Boolean(), nullable=False),
sa.Column('private', sa.Boolean(), nullable=False),
sa.Column('over_18', sa.Boolean(), nullable=False),
sa.Column('is_bot', sa.Boolean(), nullable=False),
sa.Column('title', sa.String(length=500), nullable=False),
sa.Column('url', sa.String(), nullable=True),
sa.Column('body', sa.Text(), nullable=True),
sa.Column('body_html', sa.Text(), nullable=True),
sa.Column('flair', sa.String(), nullable=True),
sa.Column('embed_url', sa.String(), nullable=True),
sa.ForeignKeyConstraint(['author_id_submission'], ['users.id'], ),
sa.ForeignKeyConstraint(['id'], ['tasks_repeatable.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.add_column('submissions', sa.Column('task_id', sa.Integer(), nullable=True))
op.create_foreign_key(None, 'submissions', 'tasks_repeatable_scheduled_submissions', ['task_id'], ['id'])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'submissions', type_='foreignkey')
op.drop_column('submissions', 'task_id')
op.drop_table('tasks_repeatable_scheduled_submissions')
op.drop_table('tasks_repeatable_runs')
op.drop_table('tasks_repeatable_python')
op.drop_table('tasks_repeatable')
# ### end Alembic commands ###
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'submissions', type_='foreignkey')
op.drop_column('submissions', 'task_id')
op.drop_table('tasks_repeatable_scheduled_submissions')
op.drop_table('tasks_repeatable_runs')
op.drop_table('tasks_repeatable_python')
op.drop_table('tasks_repeatable')
# ### end Alembic commands ###

View file

@ -19,71 +19,71 @@ depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! (adjusted -@TLSM) ###
op.drop_index('fki_exile_exiler_fkey', table_name='exiles')
op.drop_index('fki_exile_sub_fkey', table_name='exiles')
op.drop_table('exiles')
op.drop_index('fki_exile_exiler_fkey', table_name='exiles')
op.drop_index('fki_exile_sub_fkey', table_name='exiles')
op.drop_table('exiles')
op.drop_index('fki_mod_sub_fkey', table_name='mods')
op.drop_table('mods')
op.drop_index('fki_mod_sub_fkey', table_name='mods')
op.drop_table('mods')
op.drop_index('fki_sub_blocks_sub_fkey', table_name='sub_blocks')
op.drop_table('sub_blocks')
op.drop_index('fki_sub_blocks_sub_fkey', table_name='sub_blocks')
op.drop_table('sub_blocks')
op.drop_constraint('sub_fkey', 'submissions', type_='foreignkey')
op.drop_column('submissions', 'sub')
op.drop_column('users', 'subs_created')
op.drop_constraint('sub_fkey', 'submissions', type_='foreignkey')
op.drop_column('submissions', 'sub')
op.drop_column('users', 'subs_created')
op.drop_index('subs_idx', table_name='subs')
op.drop_table('subs')
op.drop_index('subs_idx', table_name='subs')
op.drop_table('subs')
op.execute("DELETE FROM modactions WHERE kind = 'move_hole'")
op.execute("DELETE FROM modactions WHERE kind = 'move_hole'")
def downgrade():
# ### commands auto generated by Alembic - please adjust! (adjusted -@TLSM) ###
op.create_table('subs',
sa.Column('name', sa.VARCHAR(length=20), autoincrement=False, nullable=False),
sa.Column('sidebar', sa.VARCHAR(length=500), autoincrement=False, nullable=True),
sa.Column('sidebar_html', sa.VARCHAR(length=1000), autoincrement=False, nullable=True),
sa.Column('sidebarurl', sa.VARCHAR(length=60), autoincrement=False, nullable=True),
sa.Column('bannerurl', sa.VARCHAR(length=60), autoincrement=False, nullable=True),
sa.Column('css', sa.VARCHAR(length=4000), autoincrement=False, nullable=True),
sa.PrimaryKeyConstraint('name', name='subs_pkey'),
postgresql_ignore_search_path=False
)
op.create_index('subs_idx', 'subs', ['name'], unique=False)
# ### commands auto generated by Alembic - please adjust! (adjusted -@TLSM) ###
op.create_table('subs',
sa.Column('name', sa.VARCHAR(length=20), autoincrement=False, nullable=False),
sa.Column('sidebar', sa.VARCHAR(length=500), autoincrement=False, nullable=True),
sa.Column('sidebar_html', sa.VARCHAR(length=1000), autoincrement=False, nullable=True),
sa.Column('sidebarurl', sa.VARCHAR(length=60), autoincrement=False, nullable=True),
sa.Column('bannerurl', sa.VARCHAR(length=60), autoincrement=False, nullable=True),
sa.Column('css', sa.VARCHAR(length=4000), autoincrement=False, nullable=True),
sa.PrimaryKeyConstraint('name', name='subs_pkey'),
postgresql_ignore_search_path=False
)
op.create_index('subs_idx', 'subs', ['name'], unique=False)
op.add_column('users', sa.Column('subs_created', sa.INTEGER(), server_default=sa.text('0'), autoincrement=False, nullable=False))
op.add_column('submissions', sa.Column('sub', sa.VARCHAR(length=20), autoincrement=False, nullable=True))
op.create_foreign_key('sub_fkey', 'submissions', 'subs', ['sub'], ['name'])
op.add_column('users', sa.Column('subs_created', sa.INTEGER(), server_default=sa.text('0'), autoincrement=False, nullable=False))
op.add_column('submissions', sa.Column('sub', sa.VARCHAR(length=20), autoincrement=False, nullable=True))
op.create_foreign_key('sub_fkey', 'submissions', 'subs', ['sub'], ['name'])
op.create_table('sub_blocks',
sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column('sub', sa.VARCHAR(length=20), autoincrement=False, nullable=False),
sa.ForeignKeyConstraint(['sub'], ['subs.name'], name='sub_blocks_sub_fkey'),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], name='sub_blocks_user_fkey'),
sa.PrimaryKeyConstraint('user_id', 'sub', name='sub_blocks_pkey')
)
op.create_index('fki_sub_blocks_sub_fkey', 'sub_blocks', ['sub'], unique=False)
op.create_table('sub_blocks',
sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column('sub', sa.VARCHAR(length=20), autoincrement=False, nullable=False),
sa.ForeignKeyConstraint(['sub'], ['subs.name'], name='sub_blocks_sub_fkey'),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], name='sub_blocks_user_fkey'),
sa.PrimaryKeyConstraint('user_id', 'sub', name='sub_blocks_pkey')
)
op.create_index('fki_sub_blocks_sub_fkey', 'sub_blocks', ['sub'], unique=False)
op.create_table('mods',
sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column('sub', sa.VARCHAR(length=20), autoincrement=False, nullable=False),
sa.Column('created_utc', sa.INTEGER(), autoincrement=False, nullable=False),
sa.ForeignKeyConstraint(['sub'], ['subs.name'], name='mod_sub_fkey'),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], name='user_mod_fkey'),
sa.PrimaryKeyConstraint('user_id', 'sub', name='mods_pkey')
)
op.create_index('fki_mod_sub_fkey', 'mods', ['sub'], unique=False)
op.create_table('mods',
sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column('sub', sa.VARCHAR(length=20), autoincrement=False, nullable=False),
sa.Column('created_utc', sa.INTEGER(), autoincrement=False, nullable=False),
sa.ForeignKeyConstraint(['sub'], ['subs.name'], name='mod_sub_fkey'),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], name='user_mod_fkey'),
sa.PrimaryKeyConstraint('user_id', 'sub', name='mods_pkey')
)
op.create_index('fki_mod_sub_fkey', 'mods', ['sub'], unique=False)
op.create_table('exiles',
sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column('sub', sa.VARCHAR(length=20), autoincrement=False, nullable=False),
sa.Column('exiler_id', sa.INTEGER(), autoincrement=False, nullable=False),
sa.ForeignKeyConstraint(['exiler_id'], ['users.id'], name='exile_exiler_fkey'),
sa.ForeignKeyConstraint(['sub'], ['subs.name'], name='exile_sub_fkey'),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], name='exile_user_fkey'),
sa.PrimaryKeyConstraint('user_id', 'sub', name='exiles_pkey')
)
op.create_index('fki_exile_sub_fkey', 'exiles', ['sub'], unique=False)
op.create_index('fki_exile_exiler_fkey', 'exiles', ['exiler_id'], unique=False)
op.create_table('exiles',
sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column('sub', sa.VARCHAR(length=20), autoincrement=False, nullable=False),
sa.Column('exiler_id', sa.INTEGER(), autoincrement=False, nullable=False),
sa.ForeignKeyConstraint(['exiler_id'], ['users.id'], name='exile_exiler_fkey'),
sa.ForeignKeyConstraint(['sub'], ['subs.name'], name='exile_sub_fkey'),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], name='exile_user_fkey'),
sa.PrimaryKeyConstraint('user_id', 'sub', name='exiles_pkey')
)
op.create_index('fki_exile_sub_fkey', 'exiles', ['sub'], unique=False)
op.create_index('fki_exile_exiler_fkey', 'exiles', ['exiler_id'], unique=False)

View file

@ -17,14 +17,14 @@ depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('tasks_repeatable', sa.Column('label', sa.String(), nullable=True))
op.create_unique_constraint(None, 'tasks_repeatable', ['label'])
# ### end Alembic commands ###
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('tasks_repeatable', sa.Column('label', sa.String(), nullable=True))
op.create_unique_constraint(None, 'tasks_repeatable', ['label'])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'tasks_repeatable', type_='unique')
op.drop_column('tasks_repeatable', 'label')
# ### end Alembic commands ###
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'tasks_repeatable', type_='unique')
op.drop_column('tasks_repeatable', 'label')
# ### end Alembic commands ###

View file

@ -17,22 +17,22 @@ depends_on = None
def upgrade():
# manually adjusted to introduce our intended defaults
op.add_column('comments', sa.Column('volunteer_janitor_badness', sa.Float()))
op.execute("UPDATE comments SET volunteer_janitor_badness = 0.5")
op.alter_column('comments', 'volunteer_janitor_badness', nullable=False)
# manually adjusted to introduce our intended defaults
op.add_column('comments', sa.Column('volunteer_janitor_badness', sa.Float()))
op.execute("UPDATE comments SET volunteer_janitor_badness = 0.5")
op.alter_column('comments', 'volunteer_janitor_badness', nullable=False)
op.add_column('users', sa.Column('volunteer_janitor_correctness', sa.Float()))
op.execute("UPDATE users SET volunteer_janitor_correctness = 0")
op.alter_column('users', 'volunteer_janitor_correctness', nullable=False)
op.add_column('users', sa.Column('volunteer_janitor_correctness', sa.Float()))
op.execute("UPDATE users SET volunteer_janitor_correctness = 0")
op.alter_column('users', 'volunteer_janitor_correctness', nullable=False)
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('users', schema=None) as batch_op:
batch_op.drop_column('volunteer_janitor_correctness')
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('users', schema=None) as batch_op:
batch_op.drop_column('volunteer_janitor_correctness')
with op.batch_alter_table('comments', schema=None) as batch_op:
batch_op.drop_column('volunteer_janitor_badness')
with op.batch_alter_table('comments', schema=None) as batch_op:
batch_op.drop_column('volunteer_janitor_badness')
# ### end Alembic commands ###
# ### end Alembic commands ###

View file

@ -19,10 +19,10 @@ depends_on = None
def upgrade():
# this is now disabled because this code is no longer compatible with this version of the DB
#volunteer_janitor_recalc_all_comments(Session(bind=op.get_bind()))
pass
# this is now disabled because this code is no longer compatible with this version of the DB
#volunteer_janitor_recalc_all_comments(Session(bind=op.get_bind()))
pass
def downgrade():
pass
pass

View file

@ -17,76 +17,76 @@ depends_on = None
def upgrade():
op.add_column('comments', sa.Column('state_user_deleted_utc', sa.DateTime(timezone=True), nullable=True))
op.add_column('submissions', sa.Column('state_user_deleted_utc', sa.DateTime(timezone=True), nullable=True))
op.add_column('comments', sa.Column('state_user_deleted_utc', sa.DateTime(timezone=True), nullable=True))
op.add_column('submissions', sa.Column('state_user_deleted_utc', sa.DateTime(timezone=True), nullable=True))
op.drop_index('subimssion_binary_group_idx', table_name='submissions')
op.drop_index('submission_isdeleted_idx', table_name='submissions')
op.drop_index('submission_new_sort_idx', table_name='submissions')
op.drop_index('subimssion_binary_group_idx', table_name='submissions')
op.drop_index('submission_isdeleted_idx', table_name='submissions')
op.drop_index('submission_new_sort_idx', table_name='submissions')
op.execute("""
UPDATE comments
SET state_user_deleted_utc =
CASE
WHEN deleted_utc > 0 THEN
(timestamp 'epoch' + deleted_utc * interval '1 second') at time zone 'utc'
ELSE NULL
END
""")
op.execute("""
UPDATE comments
SET state_user_deleted_utc =
CASE
WHEN deleted_utc > 0 THEN
(timestamp 'epoch' + deleted_utc * interval '1 second') at time zone 'utc'
ELSE NULL
END
""")
op.execute("""
UPDATE submissions
SET state_user_deleted_utc =
CASE
WHEN deleted_utc > 0 THEN
(timestamp 'epoch' + deleted_utc * interval '1 second') at time zone 'utc'
ELSE NULL
END
""")
op.execute("""
UPDATE submissions
SET state_user_deleted_utc =
CASE
WHEN deleted_utc > 0 THEN
(timestamp 'epoch' + deleted_utc * interval '1 second') at time zone 'utc'
ELSE NULL
END
""")
op.drop_column('comments', 'deleted_utc')
op.drop_column('submissions', 'deleted_utc')
op.drop_column('comments', 'deleted_utc')
op.drop_column('submissions', 'deleted_utc')
op.create_index('subimssion_binary_group_idx', 'submissions', ['is_banned', 'state_user_deleted_utc', 'over_18'], unique=False)
op.create_index('submission_isdeleted_idx', 'submissions', ['state_user_deleted_utc'], unique=False)
op.create_index('submission_new_sort_idx', 'submissions', ['is_banned', 'state_user_deleted_utc', sa.text('created_utc DESC'), 'over_18'], unique=False)
op.create_index('subimssion_binary_group_idx', 'submissions', ['is_banned', 'state_user_deleted_utc', 'over_18'], unique=False)
op.create_index('submission_isdeleted_idx', 'submissions', ['state_user_deleted_utc'], unique=False)
op.create_index('submission_new_sort_idx', 'submissions', ['is_banned', 'state_user_deleted_utc', sa.text('created_utc DESC'), 'over_18'], unique=False)
def downgrade():
op.add_column('comments', sa.Column('deleted_utc', sa.INTEGER(), server_default=sa.text('0'), autoincrement=False, nullable=True))
op.add_column('submissions', sa.Column('deleted_utc', sa.INTEGER(), server_default=sa.text('0'), autoincrement=False, nullable=True))
op.add_column('comments', sa.Column('deleted_utc', sa.INTEGER(), server_default=sa.text('0'), autoincrement=False, nullable=True))
op.add_column('submissions', sa.Column('deleted_utc', sa.INTEGER(), server_default=sa.text('0'), autoincrement=False, nullable=True))
op.drop_index('submission_new_sort_idx', table_name='submissions')
op.drop_index('submission_isdeleted_idx', table_name='submissions')
op.drop_index('subimssion_binary_group_idx', table_name='submissions')
op.drop_index('submission_new_sort_idx', table_name='submissions')
op.drop_index('submission_isdeleted_idx', table_name='submissions')
op.drop_index('subimssion_binary_group_idx', table_name='submissions')
op.execute("""
UPDATE comments
SET deleted_utc =
COALESCE(
EXTRACT(EPOCH FROM state_user_deleted_utc)::integer,
0
)
""")
op.execute("""
UPDATE submissions
SET deleted_utc =
COALESCE(
EXTRACT(EPOCH FROM state_user_deleted_utc)::integer,
0
)
""")
op.execute("""
UPDATE comments
SET deleted_utc =
COALESCE(
EXTRACT(EPOCH FROM state_user_deleted_utc)::integer,
0
)
""")
op.execute("""
UPDATE submissions
SET deleted_utc =
COALESCE(
EXTRACT(EPOCH FROM state_user_deleted_utc)::integer,
0
)
""")
op.alter_column('comments', 'deleted_utc', nullable=False)
op.alter_column('submissions', 'deleted_utc', nullable=False)
op.alter_column('comments', 'deleted_utc', nullable=False)
op.alter_column('submissions', 'deleted_utc', nullable=False)
op.drop_column('comments', 'state_user_deleted_utc')
op.drop_column('submissions', 'state_user_deleted_utc')
op.drop_column('comments', 'state_user_deleted_utc')
op.drop_column('submissions', 'state_user_deleted_utc')
op.create_index('submission_new_sort_idx', 'submissions', ['is_banned', 'deleted_utc', 'created_utc', 'over_18'], unique=False)
op.create_index('submission_isdeleted_idx', 'submissions', ['deleted_utc'], unique=False)
op.create_index('subimssion_binary_group_idx', 'submissions', ['is_banned', 'deleted_utc', 'over_18'], unique=False)
op.create_index('submission_new_sort_idx', 'submissions', ['is_banned', 'deleted_utc', 'created_utc', 'over_18'], unique=False)
op.create_index('submission_isdeleted_idx', 'submissions', ['deleted_utc'], unique=False)
op.create_index('subimssion_binary_group_idx', 'submissions', ['is_banned', 'deleted_utc', 'over_18'], unique=False)

View file

@ -17,17 +17,17 @@ depends_on = None
def upgrade():
# update for comments
op.execute(sa.text("""UPDATE modactions SET kind = 'remove_comment' WHERE kind = 'ban_comment'"""))
op.execute(sa.text("""UPDATE modactions SET kind = 'unremove_comment' WHERE kind = 'unban_comment'"""))
# update for posts
op.execute(sa.text("""UPDATE modactions SET kind = 'remove_post' WHERE kind = 'ban_post'"""))
op.execute(sa.text("""UPDATE modactions SET kind = 'unremove_post' WHERE kind = 'unban_post'"""))
# update for comments
op.execute(sa.text("""UPDATE modactions SET kind = 'remove_comment' WHERE kind = 'ban_comment'"""))
op.execute(sa.text("""UPDATE modactions SET kind = 'unremove_comment' WHERE kind = 'unban_comment'"""))
# update for posts
op.execute(sa.text("""UPDATE modactions SET kind = 'remove_post' WHERE kind = 'ban_post'"""))
op.execute(sa.text("""UPDATE modactions SET kind = 'unremove_post' WHERE kind = 'unban_post'"""))
def downgrade():
# rollback for comments
op.execute(sa.text("""UPDATE modactions SET kind = 'ban_comment' WHERE kind = 'remove_comment'"""))
op.execute(sa.text("""UPDATE modactions SET kind = 'unban_comment' WHERE kind = 'unremove_comment'"""))
# rollback for posts
op.execute(sa.text("""UPDATE modactions SET kind = 'ban_post' WHERE kind = 'remove_post'"""))
op.execute(sa.text("""UPDATE modactions SET kind = 'unban_post' WHERE kind = 'unremove_post'"""))
# rollback for comments
op.execute(sa.text("""UPDATE modactions SET kind = 'ban_comment' WHERE kind = 'remove_comment'"""))
op.execute(sa.text("""UPDATE modactions SET kind = 'unban_comment' WHERE kind = 'unremove_comment'"""))
# rollback for posts
op.execute(sa.text("""UPDATE modactions SET kind = 'ban_post' WHERE kind = 'remove_post'"""))
op.execute(sa.text("""UPDATE modactions SET kind = 'unban_post' WHERE kind = 'unremove_post'"""))

View file

@ -17,217 +17,217 @@ depends_on = None
def upgrade():
state_mod_enum = sa.Enum('VISIBLE', 'FILTERED', 'REMOVED', name='statemod', create_type=False)
state_report_enum = sa.Enum('UNREPORTED', 'RESOLVED', 'REPORTED', 'IGNORED', name='statereport', create_type=False)
state_mod_enum = sa.Enum('VISIBLE', 'FILTERED', 'REMOVED', name='statemod', create_type=False)
state_report_enum = sa.Enum('UNREPORTED', 'RESOLVED', 'REPORTED', 'IGNORED', name='statereport', create_type=False)
state_mod_enum.create(op.get_bind(), checkfirst=True)
state_report_enum.create(op.get_bind(), checkfirst=True)
state_mod_enum.create(op.get_bind(), checkfirst=True)
state_report_enum.create(op.get_bind(), checkfirst=True)
op.add_column('comments', sa.Column('state_mod', state_mod_enum, nullable=True))
op.add_column('comments', sa.Column('state_mod_set_by', sa.String(), nullable=True))
op.add_column('comments', sa.Column('state_report', state_report_enum, nullable=True))
op.drop_index('fki_comment_approver_fkey', table_name='comments')
op.drop_constraint('comment_approver_fkey', 'comments', type_='foreignkey')
op.add_column('comments', sa.Column('state_mod', state_mod_enum, nullable=True))
op.add_column('comments', sa.Column('state_mod_set_by', sa.String(), nullable=True))
op.add_column('comments', sa.Column('state_report', state_report_enum, nullable=True))
op.drop_index('fki_comment_approver_fkey', table_name='comments')
op.drop_constraint('comment_approver_fkey', 'comments', type_='foreignkey')
# If `is_banned`, set `state_mod` to the `REMOVED` enum
# otherwise, if `filter_state` is `FILTERED`, set `state_mod` to the `FILTERED` enum
# otherwise, set `state_mod` to the `VISIBLE` enum
op.execute("""
UPDATE comments
SET state_mod = CASE
WHEN is_banned THEN 'REMOVED'::statemod
WHEN filter_state = 'FILTERED' THEN 'FILTERED'::statemod
ELSE 'VISIBLE'::statemod
END
""")
# If `is_banned`, set `state_mod` to the `REMOVED` enum
# otherwise, if `filter_state` is `FILTERED`, set `state_mod` to the `FILTERED` enum
# otherwise, set `state_mod` to the `VISIBLE` enum
op.execute("""
UPDATE comments
SET state_mod = CASE
WHEN is_banned THEN 'REMOVED'::statemod
WHEN filter_state = 'FILTERED' THEN 'FILTERED'::statemod
ELSE 'VISIBLE'::statemod
END
""")
# if `state_mod` is `REMOVED`, set `state_mod_set_by` to `ban_reason`
# otherwise, if `state_mod` is `VISIBLE`, set `state_mod_set_by` to the `username` of the `users` table, indexed by `is_approved == users.id`
op.execute("""
UPDATE comments
SET state_mod_set_by = CASE
WHEN state_mod = 'REMOVED' THEN ban_reason
WHEN state_mod = 'VISIBLE' THEN (SELECT username FROM users WHERE id = is_approved)
END
""")
# if `state_mod` is `REMOVED`, set `state_mod_set_by` to `ban_reason`
# otherwise, if `state_mod` is `VISIBLE`, set `state_mod_set_by` to the `username` of the `users` table, indexed by `is_approved == users.id`
op.execute("""
UPDATE comments
SET state_mod_set_by = CASE
WHEN state_mod = 'REMOVED' THEN ban_reason
WHEN state_mod = 'VISIBLE' THEN (SELECT username FROM users WHERE id = is_approved)
END
""")
# if `filter_state` is `IGNORED`, set `state_report` to the `IGNORED` enum
# otherwise, if `filter_state` is `REPORTED`, set `state_report` to the `REPORTED` enum
# otherwise, if `state_mod_set_by` is non-NULL, set `state_report` to the `RESOLVED` enum
# otherwise, set `state_report` to the `UNREPORTED` enum
op.execute("""
UPDATE comments
SET state_report = CASE
WHEN filter_state = 'IGNORED' THEN 'IGNORED'::statereport
WHEN filter_state = 'REPORTED' THEN 'REPORTED'::statereport
WHEN state_mod_set_by IS NOT NULL THEN 'RESOLVED'::statereport
ELSE 'UNREPORTED'::statereport
END
""")
# if `filter_state` is `IGNORED`, set `state_report` to the `IGNORED` enum
# otherwise, if `filter_state` is `REPORTED`, set `state_report` to the `REPORTED` enum
# otherwise, if `state_mod_set_by` is non-NULL, set `state_report` to the `RESOLVED` enum
# otherwise, set `state_report` to the `UNREPORTED` enum
op.execute("""
UPDATE comments
SET state_report = CASE
WHEN filter_state = 'IGNORED' THEN 'IGNORED'::statereport
WHEN filter_state = 'REPORTED' THEN 'REPORTED'::statereport
WHEN state_mod_set_by IS NOT NULL THEN 'RESOLVED'::statereport
ELSE 'UNREPORTED'::statereport
END
""")
op.alter_column('comments', 'state_mod', nullable=False)
op.alter_column('comments', 'state_report', nullable=False)
op.alter_column('comments', 'state_mod', nullable=False)
op.alter_column('comments', 'state_report', nullable=False)
op.drop_column('comments', 'is_banned')
op.drop_column('comments', 'ban_reason')
op.drop_column('comments', 'is_approved')
op.drop_column('comments', 'filter_state')
op.drop_column('comments', 'is_banned')
op.drop_column('comments', 'ban_reason')
op.drop_column('comments', 'is_approved')
op.drop_column('comments', 'filter_state')
op.add_column('submissions', sa.Column('state_mod', state_mod_enum, nullable=True))
op.add_column('submissions', sa.Column('state_mod_set_by', sa.String(), nullable=True))
op.add_column('submissions', sa.Column('state_report', state_report_enum, nullable=True))
op.drop_index('fki_submissions_approver_fkey', table_name='submissions')
op.drop_index('submission_isbanned_idx', table_name='submissions')
op.drop_index('subimssion_binary_group_idx', table_name='submissions')
op.create_index('subimssion_binary_group_idx', 'submissions', ['state_mod', 'state_user_deleted_utc', 'over_18'], unique=False)
op.drop_index('submission_new_sort_idx', table_name='submissions')
op.create_index('submission_new_sort_idx', 'submissions', ['state_mod', 'state_user_deleted_utc', sa.text('created_utc DESC'), 'over_18'], unique=False)
op.create_index('submission_state_mod_idx', 'submissions', ['state_mod'], unique=False)
op.drop_constraint('submissions_approver_fkey', 'submissions', type_='foreignkey')
op.add_column('submissions', sa.Column('state_mod', state_mod_enum, nullable=True))
op.add_column('submissions', sa.Column('state_mod_set_by', sa.String(), nullable=True))
op.add_column('submissions', sa.Column('state_report', state_report_enum, nullable=True))
op.drop_index('fki_submissions_approver_fkey', table_name='submissions')
op.drop_index('submission_isbanned_idx', table_name='submissions')
op.drop_index('subimssion_binary_group_idx', table_name='submissions')
op.create_index('subimssion_binary_group_idx', 'submissions', ['state_mod', 'state_user_deleted_utc', 'over_18'], unique=False)
op.drop_index('submission_new_sort_idx', table_name='submissions')
op.create_index('submission_new_sort_idx', 'submissions', ['state_mod', 'state_user_deleted_utc', sa.text('created_utc DESC'), 'over_18'], unique=False)
op.create_index('submission_state_mod_idx', 'submissions', ['state_mod'], unique=False)
op.drop_constraint('submissions_approver_fkey', 'submissions', type_='foreignkey')
# If `is_banned`, set `state_mod` to the `REMOVED` enum
# otherwise, if `filter_state` is `FILTERED`, set `state_mod` to the `FILTERED` enum
# otherwise, set `state_mod` to the `VISIBLE` enum
op.execute("""
UPDATE submissions
SET state_mod = CASE
WHEN is_banned THEN 'REMOVED'::statemod
WHEN filter_state = 'FILTERED' THEN 'FILTERED'::statemod
ELSE 'VISIBLE'::statemod
END
""")
# If `is_banned`, set `state_mod` to the `REMOVED` enum
# otherwise, if `filter_state` is `FILTERED`, set `state_mod` to the `FILTERED` enum
# otherwise, set `state_mod` to the `VISIBLE` enum
op.execute("""
UPDATE submissions
SET state_mod = CASE
WHEN is_banned THEN 'REMOVED'::statemod
WHEN filter_state = 'FILTERED' THEN 'FILTERED'::statemod
ELSE 'VISIBLE'::statemod
END
""")
# if `state_mod` is `REMOVED`, set `state_mod_set_by` to `ban_reason`
# otherwise, if `state_mod` is `VISIBLE`, set `state_mod_set_by` to the `username` of the `users` table, indexed by `is_approved == users.id`
op.execute("""
UPDATE submissions
SET state_mod_set_by = CASE
WHEN state_mod = 'REMOVED' THEN ban_reason
WHEN state_mod = 'VISIBLE' THEN (SELECT username FROM users WHERE id = is_approved)
END
""")
# if `state_mod` is `REMOVED`, set `state_mod_set_by` to `ban_reason`
# otherwise, if `state_mod` is `VISIBLE`, set `state_mod_set_by` to the `username` of the `users` table, indexed by `is_approved == users.id`
op.execute("""
UPDATE submissions
SET state_mod_set_by = CASE
WHEN state_mod = 'REMOVED' THEN ban_reason
WHEN state_mod = 'VISIBLE' THEN (SELECT username FROM users WHERE id = is_approved)
END
""")
# if `filter_state` is `IGNORED`, set `state_report` to the `IGNORED` enum
# otherwise, if `filter_state` is `REPORTED`, set `state_report` to the `REPORTED` enum
# otherwise, if `state_mod_set_by` is non-NULL, set `state_report` to the `RESOLVED` enum
# otherwise, set `state_report` to the `UNREPORTED` enum
op.execute("""
UPDATE submissions
SET state_report = CASE
WHEN filter_state = 'IGNORED' THEN 'IGNORED'::statereport
WHEN filter_state = 'REPORTED' THEN 'REPORTED'::statereport
WHEN state_mod_set_by IS NOT NULL THEN 'RESOLVED'::statereport
ELSE 'UNREPORTED'::statereport
END
""")
# if `filter_state` is `IGNORED`, set `state_report` to the `IGNORED` enum
# otherwise, if `filter_state` is `REPORTED`, set `state_report` to the `REPORTED` enum
# otherwise, if `state_mod_set_by` is non-NULL, set `state_report` to the `RESOLVED` enum
# otherwise, set `state_report` to the `UNREPORTED` enum
op.execute("""
UPDATE submissions
SET state_report = CASE
WHEN filter_state = 'IGNORED' THEN 'IGNORED'::statereport
WHEN filter_state = 'REPORTED' THEN 'REPORTED'::statereport
WHEN state_mod_set_by IS NOT NULL THEN 'RESOLVED'::statereport
ELSE 'UNREPORTED'::statereport
END
""")
op.alter_column('submissions', 'state_mod', nullable=False)
op.alter_column('submissions', 'state_report', nullable=False)
op.alter_column('submissions', 'state_mod', nullable=False)
op.alter_column('submissions', 'state_report', nullable=False)
op.drop_column('submissions', 'is_banned')
op.drop_column('submissions', 'ban_reason')
op.drop_column('submissions', 'is_approved')
op.drop_column('submissions', 'filter_state')
op.drop_column('submissions', 'is_banned')
op.drop_column('submissions', 'ban_reason')
op.drop_column('submissions', 'is_approved')
op.drop_column('submissions', 'filter_state')
def downgrade():
op.add_column('comments', sa.Column('filter_state', sa.VARCHAR(length=40), autoincrement=False, nullable=True))
op.add_column('comments', sa.Column('is_approved', sa.INTEGER(), autoincrement=False, nullable=True))
op.add_column('comments', sa.Column('ban_reason', sa.VARCHAR(length=25), autoincrement=False, nullable=True))
op.add_column('comments', sa.Column('is_banned', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=True))
op.create_foreign_key('comment_approver_fkey', 'comments', 'users', ['is_approved'], ['id'])
op.create_index('fki_comment_approver_fkey', 'comments', ['is_approved'], unique=False)
op.add_column('comments', sa.Column('filter_state', sa.VARCHAR(length=40), autoincrement=False, nullable=True))
op.add_column('comments', sa.Column('is_approved', sa.INTEGER(), autoincrement=False, nullable=True))
op.add_column('comments', sa.Column('ban_reason', sa.VARCHAR(length=25), autoincrement=False, nullable=True))
op.add_column('comments', sa.Column('is_banned', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=True))
op.create_foreign_key('comment_approver_fkey', 'comments', 'users', ['is_approved'], ['id'])
op.create_index('fki_comment_approver_fkey', 'comments', ['is_approved'], unique=False)
op.execute("""
UPDATE comments
SET is_banned = CASE
WHEN state_mod = 'REMOVED' THEN TRUE
ELSE FALSE
END
""")
op.execute("""
UPDATE comments
SET is_banned = CASE
WHEN state_mod = 'REMOVED' THEN TRUE
ELSE FALSE
END
""")
op.execute("""
UPDATE comments
SET is_approved = (SELECT id FROM users WHERE username = state_mod_set_by)
WHERE state_mod = 'VISIBLE' AND (state_report = 'RESOLVED' OR state_report = 'IGNORED')
""")
op.execute("""
UPDATE comments
SET is_approved = (SELECT id FROM users WHERE username = state_mod_set_by)
WHERE state_mod = 'VISIBLE' AND (state_report = 'RESOLVED' OR state_report = 'IGNORED')
""")
op.execute("""
UPDATE comments
SET ban_reason = CASE
WHEN state_mod = 'REMOVED' THEN state_mod_set_by
ELSE NULL
END
""")
op.execute("""
UPDATE comments
SET ban_reason = CASE
WHEN state_mod = 'REMOVED' THEN state_mod_set_by
ELSE NULL
END
""")
op.execute("""
UPDATE comments
SET filter_state = CASE
WHEN state_report = 'IGNORED' THEN 'IGNORED'
WHEN state_report = 'REPORTED' THEN 'REPORTED'
WHEN state_mod = 'FILTERED' THEN 'FILTERED'
ELSE NULL
END
""")
op.execute("""
UPDATE comments
SET filter_state = CASE
WHEN state_report = 'IGNORED' THEN 'IGNORED'
WHEN state_report = 'REPORTED' THEN 'REPORTED'
WHEN state_mod = 'FILTERED' THEN 'FILTERED'
ELSE NULL
END
""")
op.alter_column('comments', sa.Column('filter_state', sa.VARCHAR(length=40), autoincrement=False, nullable=True))
op.alter_column('comments', sa.Column('is_banned', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=True))
op.alter_column('comments', sa.Column('filter_state', sa.VARCHAR(length=40), autoincrement=False, nullable=True))
op.alter_column('comments', sa.Column('is_banned', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=True))
op.drop_column('comments', 'state_report')
op.drop_column('comments', 'state_mod_set_by')
op.drop_column('comments', 'state_mod')
op.add_column('submissions', sa.Column('filter_state', sa.VARCHAR(length=40), autoincrement=False, nullable=True))
op.add_column('submissions', sa.Column('is_approved', sa.INTEGER(), autoincrement=False, nullable=True))
op.add_column('submissions', sa.Column('ban_reason', sa.VARCHAR(length=25), autoincrement=False, nullable=True))
op.add_column('submissions', sa.Column('is_banned', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=True))
op.create_foreign_key('submissions_approver_fkey', 'submissions', 'users', ['is_approved'], ['id'])
op.drop_index('submission_state_mod_idx', table_name='submissions')
op.drop_index('submission_new_sort_idx', table_name='submissions')
op.create_index('submission_new_sort_idx', 'submissions', ['is_banned', 'state_user_deleted_utc', 'created_utc', 'over_18'], unique=False)
op.drop_index('subimssion_binary_group_idx', table_name='submissions')
op.create_index('subimssion_binary_group_idx', 'submissions', ['is_banned', 'state_user_deleted_utc', 'over_18'], unique=False)
op.create_index('submission_isbanned_idx', 'submissions', ['is_banned'], unique=False)
op.create_index('fki_submissions_approver_fkey', 'submissions', ['is_approved'], unique=False)
op.drop_column('comments', 'state_report')
op.drop_column('comments', 'state_mod_set_by')
op.drop_column('comments', 'state_mod')
op.add_column('submissions', sa.Column('filter_state', sa.VARCHAR(length=40), autoincrement=False, nullable=True))
op.add_column('submissions', sa.Column('is_approved', sa.INTEGER(), autoincrement=False, nullable=True))
op.add_column('submissions', sa.Column('ban_reason', sa.VARCHAR(length=25), autoincrement=False, nullable=True))
op.add_column('submissions', sa.Column('is_banned', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=True))
op.create_foreign_key('submissions_approver_fkey', 'submissions', 'users', ['is_approved'], ['id'])
op.drop_index('submission_state_mod_idx', table_name='submissions')
op.drop_index('submission_new_sort_idx', table_name='submissions')
op.create_index('submission_new_sort_idx', 'submissions', ['is_banned', 'state_user_deleted_utc', 'created_utc', 'over_18'], unique=False)
op.drop_index('subimssion_binary_group_idx', table_name='submissions')
op.create_index('subimssion_binary_group_idx', 'submissions', ['is_banned', 'state_user_deleted_utc', 'over_18'], unique=False)
op.create_index('submission_isbanned_idx', 'submissions', ['is_banned'], unique=False)
op.create_index('fki_submissions_approver_fkey', 'submissions', ['is_approved'], unique=False)
op.execute("""
UPDATE submissions
SET is_banned = CASE
WHEN state_mod = 'REMOVED' THEN TRUE
ELSE FALSE
END
""")
op.execute("""
UPDATE submissions
SET is_banned = CASE
WHEN state_mod = 'REMOVED' THEN TRUE
ELSE FALSE
END
""")
op.execute("""
UPDATE submissions
SET is_approved = (SELECT id FROM users WHERE username = state_mod_set_by)
WHERE state_mod = 'VISIBLE' AND (state_report = 'RESOLVED' OR state_report = 'IGNORED')
""")
op.execute("""
UPDATE submissions
SET is_approved = (SELECT id FROM users WHERE username = state_mod_set_by)
WHERE state_mod = 'VISIBLE' AND (state_report = 'RESOLVED' OR state_report = 'IGNORED')
""")
op.execute("""
UPDATE submissions
SET ban_reason = CASE
WHEN state_mod = 'REMOVED' THEN state_mod_set_by
ELSE NULL
END
""")
op.execute("""
UPDATE submissions
SET ban_reason = CASE
WHEN state_mod = 'REMOVED' THEN state_mod_set_by
ELSE NULL
END
""")
op.execute("""
UPDATE submissions
SET filter_state = CASE
WHEN state_report = 'IGNORED' THEN 'IGNORED'
WHEN state_report = 'REPORTED' THEN 'REPORTED'
WHEN state_mod = 'FILTERED' THEN 'FILTERED'
ELSE NULL
END
""")
op.execute("""
UPDATE submissions
SET filter_state = CASE
WHEN state_report = 'IGNORED' THEN 'IGNORED'
WHEN state_report = 'REPORTED' THEN 'REPORTED'
WHEN state_mod = 'FILTERED' THEN 'FILTERED'
ELSE NULL
END
""")
op.alter_column('submissions', sa.Column('filter_state', sa.VARCHAR(length=40), autoincrement=False, nullable=True))
op.alter_column('submissions', sa.Column('is_banned', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=True))
op.alter_column('submissions', sa.Column('filter_state', sa.VARCHAR(length=40), autoincrement=False, nullable=True))
op.alter_column('submissions', sa.Column('is_banned', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=True))
op.drop_column('submissions', 'state_report')
op.drop_column('submissions', 'state_mod_set_by')
op.drop_column('submissions', 'state_mod')
op.drop_column('submissions', 'state_report')
op.drop_column('submissions', 'state_mod_set_by')
op.drop_column('submissions', 'state_mod')