make it possible to position poll options

pull/136/head
Aevann 2023-03-01 00:09:16 +02:00
parent 0ff2a35736
commit 55c8a51a1d
11 changed files with 122 additions and 91 deletions

View File

@ -276,20 +276,29 @@ class Comment(Base):
for o in self.options: for o in self.options:
input_type = 'radio' if o.exclusive else 'checkbox' input_type = 'radio' if o.exclusive else 'checkbox'
body += f'<div class="custom-control"><input type="{input_type}" class="custom-control-input" id="comment-{o.id}" name="option-{self.id}"' option_body = f'<div class="custom-control"><input type="{input_type}" class="custom-control-input" id="comment-{o.id}" name="option-{self.id}"'
if o.voted(v): body += " checked" if o.voted(v): option_body += " checked"
if v: if v:
if self.parent_submission: if self.parent_submission:
sub = self.post.sub sub = self.post.sub
if sub in {'furry','vampire','racist','femboy'} and not v.house.lower().startswith(sub): body += ' disabled ' if sub in {'furry','vampire','racist','femboy'} and not v.house.lower().startswith(sub): option_body += ' disabled '
body += f''' data-nonce="{g.nonce}" data-onclick="poll_vote_{o.exclusive}('{o.id}', '{self.id}', 'comment')"''' option_body += f''' data-nonce="{g.nonce}" data-onclick="poll_vote_{o.exclusive}('{o.id}', '{self.id}', 'comment')"'''
else: else:
body += f''' data-nonce="{g.nonce}" data-onclick="poll_vote_no_v()"''' option_body += f''' data-nonce="{g.nonce}" data-onclick="poll_vote_no_v()"'''
body += f'''><label class="custom-control-label" for="comment-{o.id}">{o.body_html}<span class="presult-{self.id}''' option_body += f'''><label class="custom-control-label" for="comment-{o.id}">{o.body_html}<span class="presult-{self.id}'''
if not self.total_poll_voted(v): body += ' d-none' if not self.total_poll_voted(v): option_body += ' d-none'
body += f'"> - <a href="/votes/comment/option/{o.id}"><span id="score-comment-{o.id}">{o.upvotes}</span> votes</a></label></div>''' option_body += f'"> - <a href="/votes/comment/option/{o.id}"><span id="score-comment-{o.id}">{o.upvotes}</span> votes</a></label></div>'''
if o.exclusive > 1: s = '!!'
elif o.exclusive: s = '&amp;&amp;'
else: s = '$$'
if f'{s}{o.body}{s}' in body:
body = body.replace(f'{s}{o.body}{s}', option_body)
else:
body += option_body
if not self.ghost and self.author.show_sig(v): if not self.ghost and self.author.show_sig(v):
body += f'<section id="signature-{self.author.id}" class="user-signature"><hr>{self.author.sig_html}</section>' body += f'<section id="signature-{self.author.id}" class="user-signature"><hr>{self.author.sig_html}</section>'

View File

@ -11,7 +11,8 @@ class SubmissionOption(Base):
__tablename__ = "submission_options" __tablename__ = "submission_options"
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
submission_id = Column(Integer, ForeignKey("submissions.id")) parent_id = Column(Integer, ForeignKey("submissions.id"))
body = Column(Text)
body_html = Column(Text) body_html = Column(Text)
exclusive = Column(Integer) exclusive = Column(Integer)
created_utc = Column(Integer) created_utc = Column(Integer)
@ -66,7 +67,8 @@ class CommentOption(Base):
__tablename__ = "comment_options" __tablename__ = "comment_options"
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
comment_id = Column(Integer, ForeignKey("comments.id")) parent_id = Column(Integer, ForeignKey("comments.id"))
body = Column(Text)
body_html = Column(Text) body_html = Column(Text)
exclusive = Column(Integer) exclusive = Column(Integer)
created_utc = Column(Integer) created_utc = Column(Integer)

View File

@ -285,39 +285,49 @@ class Submission(Base):
winner = [x for x in self.options if x.exclusive == 3] winner = [x for x in self.options if x.exclusive == 3]
for o in self.options: for o in self.options:
if o.exclusive > 1: option_body = ''
body += f'''<div class="custom-control mt-2"><input name="option-{self.id}" autocomplete="off" class="custom-control-input bet" type="radio" id="{o.id}" data-nonce="{g.nonce}" data-onclick="bet_vote(this,'{o.id}')"'''
if o.voted(v): body += " checked "
if not (v and v.coins >= POLL_BET_COINS) or self.total_bet_voted(v): body += " disabled "
body += f'''><label class="custom-control-label" for="{o.id}">{o.body_html}<span class="presult-{self.id}''' if o.exclusive > 1:
body += f'"> - <a href="/votes/post/option/{o.id}"><span id="option-{o.id}">{o.upvotes}</span> bets</a>' option_body += f'''<div class="custom-control mt-2"><input name="option-{self.id}" autocomplete="off" class="custom-control-input bet" type="radio" id="{o.id}" data-nonce="{g.nonce}" data-onclick="bet_vote(this,'{o.id}')"'''
if o.voted(v): option_body += " checked "
if not (v and v.coins >= POLL_BET_COINS) or self.total_bet_voted(v): option_body += " disabled "
option_body += f'''><label class="custom-control-label" for="{o.id}">{o.body_html}<span class="presult-{self.id}'''
option_body += f'"> - <a href="/votes/post/option/{o.id}"><span id="option-{o.id}">{o.upvotes}</span> bets</a>'
if not self.total_bet_voted(v): if not self.total_bet_voted(v):
body += f'''<span class="cost"> (cost of entry: {POLL_BET_COINS} coins)</span>''' option_body += f'''<span class="cost"> (cost of entry: {POLL_BET_COINS} coins)</span>'''
body += "</label>" option_body += "</label>"
if o.exclusive == 3: if o.exclusive == 3:
body += " - <b>WINNER!</b>" option_body += " - <b>WINNER!</b>"
if not winner and v and v.admin_level >= PERMS['POST_BETS_DISTRIBUTE']: if not winner and v and v.admin_level >= PERMS['POST_BETS_DISTRIBUTE']:
body += f'''<button class="btn btn-primary distribute" data-areyousure="postToastReload(this,'/distribute/{o.id}')" data-nonce="{g.nonce}" data-onclick="areyousure(this)">Declare winner</button>''' option_body += f'''<button class="btn btn-primary distribute" data-areyousure="postToastReload(this,'/distribute/{o.id}')" data-nonce="{g.nonce}" data-onclick="areyousure(this)">Declare winner</button>'''
body += "</div>" option_body += "</div>"
else: else:
input_type = 'radio' if o.exclusive else 'checkbox' input_type = 'radio' if o.exclusive else 'checkbox'
body += f'<div class="custom-control mt-2"><input type="{input_type}" class="custom-control-input" id="post-{o.id}" name="option-{self.id}"' option_body += f'<div class="custom-control mt-2"><input type="{input_type}" class="custom-control-input" id="post-{o.id}" name="option-{self.id}"'
if o.voted(v): body += " checked" if o.voted(v): option_body += " checked"
if v: if v:
sub = self.sub sub = self.sub
if sub in {'furry','vampire','racist','femboy'} and not v.house.lower().startswith(sub): body += ' disabled ' if sub in {'furry','vampire','racist','femboy'} and not v.house.lower().startswith(sub): option_body += ' disabled '
body += f''' data-nonce="{g.nonce}" data-onclick="poll_vote_{o.exclusive}('{o.id}', '{self.id}', 'post')"''' option_body += f''' data-nonce="{g.nonce}" data-onclick="poll_vote_{o.exclusive}('{o.id}', '{self.id}', 'post')"'''
else: else:
body += f''' data-nonce="{g.nonce}" data-onclick="poll_vote_no_v()"''' option_body += f''' data-nonce="{g.nonce}" data-onclick="poll_vote_no_v()"'''
body += f'''><label class="custom-control-label" for="post-{o.id}">{o.body_html}<span class="presult-{self.id}''' option_body += f'''><label class="custom-control-label" for="post-{o.id}">{o.body_html}<span class="presult-{self.id}'''
if not self.total_poll_voted(v): body += ' d-none' if not self.total_poll_voted(v): option_body += ' d-none'
body += f'"> - <a href="/votes/post/option/{o.id}"><span id="score-post-{o.id}">{o.upvotes}</span> votes</a></label></div>''' option_body += f'"> - <a href="/votes/post/option/{o.id}"><span id="score-post-{o.id}">{o.upvotes}</span> votes</a></label></div>'''
if o.exclusive > 1: s = '!!'
elif o.exclusive: s = '&amp;&amp;'
else: s = '$$'
if f'{s}{o.body}{s}' in body:
body = body.replace(f'{s}{o.body}{s}', option_body)
else:
body += option_body
if not listing and not self.ghost and self.author.show_sig(v): if not listing and not self.ghost and self.author.show_sig(v):
body += f'<section id="signature-{self.author.id}" class="user-signature"><hr>{self.author.sig_html}</section>' body += f'<section id="signature-{self.author.id}" class="user-signature"><hr>{self.author.sig_html}</section>'

View File

@ -466,28 +466,52 @@ def execute_lawlz_actions(v:User, p:Submission):
g.db.add(ma_2) g.db.add(ma_2)
g.db.add(ma_3) g.db.add(ma_3)
def process_poll_options(target:Union[Submission, Comment],
cls:Union[Type[SubmissionOption], Type[CommentOption]],
options:Iterable[str], exclusive:int, friendly_name:str,
db:scoped_session) -> None:
for option in options:
if len(option) > 500: abort(400, f"{friendly_name} option too long!")
if cls is SubmissionOption:
option = cls(
submission_id=target.id,
body_html=option,
exclusive=exclusive,
)
else:
option = cls(
comment_id=target.id,
body_html=option,
exclusive=exclusive,
)
db.add(option)
def execute_wordle(c:Comment, body:str): def execute_wordle(c:Comment, body:str):
if not FEATURES['WORDLE']: return if not FEATURES['WORDLE']: return
if not "!wordle" in body: return if not "!wordle" in body: return
answer = random.choice(WORDLE_LIST) answer = random.choice(WORDLE_LIST)
c.wordle_result = f'_active_{answer}' c.wordle_result = f'_active_{answer}'
def process_poll_options(v:User, target:Union[Submission, Comment]):
patterns = [(poll_regex, 0), (choice_regex, 1)]
if isinstance(target, Submission) and v and v.admin_level >= PERMS['POST_BETS']:
patterns.append((bet_regex, 2))
option_count = 0
for pattern, exclusive in patterns:
for i in pattern.finditer(target.body):
option_count += 1
if option_count > POLL_MAX_OPTIONS:
abort(400, f"Max number of poll options is {POLL_MAX_OPTIONS}")
body = i.group(1)
if len(body) > 500:
abort(400, f"Poll option body too long! (Max 500 characters)")
if isinstance(target, Submission):
cls = SubmissionOption
else:
cls = CommentOption
g.db.flush()
existing = g.db.query(cls).filter_by(
parent_id=target.id,
body=body,
exclusive=exclusive,
).one_or_none()
if not existing:
option = cls(
parent_id=target.id,
body=body,
body_html=filter_emojis_only(body),
exclusive=exclusive,
)
g.db.add(option)

View File

@ -30,7 +30,7 @@ valid_sub_regex = re.compile("^[a-zA-Z0-9_\-]{3,25}$", flags=re.A)
query_regex = re.compile("(\w+):(\S+)", flags=re.A) query_regex = re.compile("(\w+):(\S+)", flags=re.A)
poll_regex = re.compile("\s*\$\$([^\$\n]+)\$\$\s*(?!([^<]*<\/(code|pre|a)>|[^`]*`))", flags=re.A) poll_regex = re.compile("\s*\$\$([^\$\n]+)\$\$\s*(?!([^<]*<\/(code|pre|a)>|[^`]*`))", flags=re.A)
bet_regex = re.compile("\s*\$\$\$([^\$\n]+)\$\$\$\s*(?!([^<]*<\/(code|pre|a)>|[^`]*`))", flags=re.A) bet_regex = re.compile("\s*!!([^\$\n]+)!!\s*(?!([^<]*<\/(code|pre|a)>|[^`]*`))", flags=re.A)
choice_regex = re.compile("\s*&&([^\$\n]+)&&\s*(?!([^<]*<\/(code|pre|a)>|[^`]*`))", flags=re.A) choice_regex = re.compile("\s*&&([^\$\n]+)&&\s*(?!([^<]*<\/(code|pre|a)>|[^`]*`))", flags=re.A)
html_comment_regex = re.compile("<!--.*-->", flags=re.A) html_comment_regex = re.compile("<!--.*-->", flags=re.A)

View File

@ -647,18 +647,3 @@ def validate_css(css):
return False, f"The domain '{domain}' is not allowed, please use one of these domains\n\n{approved_embed_hosts}." return False, f"The domain '{domain}' is not allowed, please use one of these domains\n\n{approved_embed_hosts}."
return True, "" return True, ""
def sanitize_poll_options(v:User, body:str, allow_bets:bool) -> tuple[str, List[Any], List[Any], List[Any]]:
def sanitize_poll_type(body:str, re:re.Pattern) -> tuple[str, List[str]]:
opts = []
for i in list(re.finditer(body))[:POLL_MAX_OPTIONS] if POLL_MAX_OPTIONS else list(re.finditer(body)):
opts.append(filter_emojis_only(i.group(1)))
body = body.replace(i.group(0), "")
return (body, opts)
bets = []
if allow_bets and v and v.admin_level >= PERMS['POST_BETS']:
body, bets = sanitize_poll_type(body, bet_regex)
body, options = sanitize_poll_type(body, poll_regex)
body, choices = sanitize_poll_type(body, choice_regex)
return (body, bets, options, choices)

View File

@ -150,8 +150,6 @@ def comment(v:User):
if not v.admin_level >= PERMS['POST_COMMENT_MODERATION'] and parent_user.any_block_exists(v): if not v.admin_level >= PERMS['POST_COMMENT_MODERATION'] and parent_user.any_block_exists(v):
abort(403, "You can't reply to users who have blocked you or users that you have blocked!") abort(403, "You can't reply to users who have blocked you or users that you have blocked!")
body, _, options, choices = sanitize_poll_options(v, body, False)
if request.files.get("file") and not g.is_tor: if request.files.get("file") and not g.is_tor:
files = request.files.getlist('file')[:20] files = request.files.getlist('file')[:20]
@ -264,15 +262,14 @@ def comment(v:User):
g.db.add(c) g.db.add(c)
g.db.flush() g.db.flush()
process_poll_options(v, c)
execute_blackjack(v, c, c.body, "comment") execute_blackjack(v, c, c.body, "comment")
execute_under_siege(v, c, c.body, "comment") execute_under_siege(v, c, c.body, "comment")
if c.level == 1: c.top_comment_id = c.id if c.level == 1: c.top_comment_id = c.id
else: c.top_comment_id = parent.top_comment_id else: c.top_comment_id = parent.top_comment_id
process_poll_options(c, CommentOption, options, 0, "Poll", g.db)
process_poll_options(c, CommentOption, choices, 1, "Poll", g.db)
if post_target.id not in ADMIGGER_THREADS and v.agendaposter and not v.marseyawarded and AGENDAPOSTER_PHRASE not in c.body.lower() and not (posting_to_submission and post_target.sub == 'chudrama'): if post_target.id not in ADMIGGER_THREADS and v.agendaposter and not v.marseyawarded and AGENDAPOSTER_PHRASE not in c.body.lower() and not (posting_to_submission and post_target.sub == 'chudrama'):
c.is_banned = True c.is_banned = True
c.ban_reason = "AutoJanny" c.ban_reason = "AutoJanny"
@ -403,10 +400,6 @@ def edit_comment(cid, v):
elif v.bird and len(body) > 140: elif v.bird and len(body) > 140:
abort(403, "You have to type less than 140 characters!") abort(403, "You have to type less than 140 characters!")
body, _, options, choices = sanitize_poll_options(v, body, False)
process_poll_options(c, CommentOption, options, 0, "Poll", g.db)
process_poll_options(c, CommentOption, choices, 1, "Poll", g.db)
execute_antispam_comment_check(body, v) execute_antispam_comment_check(body, v)
body = process_files(request.files, v, body) body = process_files(request.files, v, body)
@ -428,6 +421,9 @@ def edit_comment(cid, v):
abort(403, "You can only type marseys!") abort(403, "You can only type marseys!")
c.body = body c.body = body
process_poll_options(v, c)
c.body_html = body_html c.body_html = body_html
execute_blackjack(v, c, c.body, "comment") execute_blackjack(v, c, c.body, "comment")

View File

@ -35,7 +35,7 @@ def vote_option(option_id, v):
if option.exclusive: if option.exclusive:
vote = g.db.query(SubmissionOptionVote).join(SubmissionOption).filter( vote = g.db.query(SubmissionOptionVote).join(SubmissionOption).filter(
SubmissionOptionVote.user_id==v.id, SubmissionOptionVote.user_id==v.id,
SubmissionOptionVote.submission_id==option.submission_id, SubmissionOptionVote.submission_id==option.parent_id,
SubmissionOption.exclusive==option.exclusive).all() SubmissionOption.exclusive==option.exclusive).all()
if vote: if vote:
if option.exclusive == 2: abort(400, "You already voted on this bet!") if option.exclusive == 2: abort(400, "You already voted on this bet!")
@ -47,7 +47,7 @@ def vote_option(option_id, v):
vote = SubmissionOptionVote( vote = SubmissionOptionVote(
option_id=option_id, option_id=option_id,
user_id=v.id, user_id=v.id,
submission_id=option.submission_id, submission_id=option.parent_id,
) )
g.db.add(vote) g.db.add(vote)
elif existing and not option.exclusive: elif existing and not option.exclusive:
@ -107,7 +107,7 @@ def vote_option_comment(option_id, v):
if option.exclusive: if option.exclusive:
vote = g.db.query(CommentOptionVote).join(CommentOption).filter( vote = g.db.query(CommentOptionVote).join(CommentOption).filter(
CommentOptionVote.user_id==v.id, CommentOptionVote.user_id==v.id,
CommentOptionVote.comment_id==option.comment_id, CommentOptionVote.comment_id==option.parent_id,
CommentOption.exclusive==1).one_or_none() CommentOption.exclusive==1).one_or_none()
if vote: if vote:
g.db.delete(vote) g.db.delete(vote)
@ -117,7 +117,7 @@ def vote_option_comment(option_id, v):
vote = CommentOptionVote( vote = CommentOptionVote(
option_id=option_id, option_id=option_id,
user_id=v.id, user_id=v.id,
comment_id=option.comment_id, comment_id=option.parent_id,
) )
g.db.add(vote) g.db.add(vote)
elif existing: elif existing:

View File

@ -294,11 +294,6 @@ def edit_post(pid, v):
body = body.strip()[:POST_BODY_LENGTH_LIMIT(v)] # process_files() may be adding stuff to the body body = body.strip()[:POST_BODY_LENGTH_LIMIT(v)] # process_files() may be adding stuff to the body
if body != p.body: if body != p.body:
body, bets, options, choices = sanitize_poll_options(v, body, False)
process_poll_options(p, SubmissionOption, bets, 2, "Bet", g.db)
process_poll_options(p, SubmissionOption, options, 0, "Poll", g.db)
process_poll_options(p, SubmissionOption, choices, 1, "Poll", g.db)
torture = (v.agendaposter and not v.marseyawarded and p.sub != 'chudrama' and v.id == p.author_id) torture = (v.agendaposter and not v.marseyawarded and p.sub != 'chudrama' and v.id == p.author_id)
body_html = sanitize(body, golden=False, limit_pings=100, showmore=False, torture=torture) body_html = sanitize(body, golden=False, limit_pings=100, showmore=False, torture=torture)
@ -309,6 +304,8 @@ def edit_post(pid, v):
p.body = body p.body = body
process_poll_options(v, p)
execute_under_siege(v, p, p.body, 'submission') execute_under_siege(v, p, p.body, 'submission')
for text in [p.body, p.title, p.url]: for text in [p.body, p.title, p.url]:
@ -649,8 +646,6 @@ def submit_post(v:User, sub=None):
if len(url) > 2048: if len(url) > 2048:
abort(400, "There's a 2048 character limit for URLs!") abort(400, "There's a 2048 character limit for URLs!")
body, bets, options, choices = sanitize_poll_options(v, body, True)
body = process_files(request.files, v, body) body = process_files(request.files, v, body)
body = body.strip()[:POST_BODY_LENGTH_LIMIT(v)] # process_files() adds content to the body, so we need to re-strip body = body.strip()[:POST_BODY_LENGTH_LIMIT(v)] # process_files() adds content to the body, so we need to re-strip
@ -699,13 +694,11 @@ def submit_post(v:User, sub=None):
g.db.add(post) g.db.add(post)
g.db.flush() g.db.flush()
process_poll_options(v, post)
for text in {post.body, post.title, post.url}: for text in {post.body, post.title, post.url}:
if execute_blackjack(v, post, text, 'submission'): break if execute_blackjack(v, post, text, 'submission'): break
process_poll_options(post, SubmissionOption, bets, 2, "Bet", g.db)
process_poll_options(post, SubmissionOption, options, 0, "Poll", g.db)
process_poll_options(post, SubmissionOption, choices, 1, "Poll", g.db)
vote = Vote(user_id=v.id, vote = Vote(user_id=v.id,
vote_type=1, vote_type=1,
submission_id=post.id submission_id=post.id

View File

@ -0,0 +1,11 @@
alter table submission_options add column body character varying(500);
update submission_options set body=body_html;
alter table submission_options alter column body set not null;
alter table comment_options add column body character varying(500);
update comment_options set body=body_html;
alter table comment_options alter column body set not null;
alter table submission_options rename column submission_id to parent_id;
alter table comment_options rename column comment_id to parent_id;

View File

@ -327,7 +327,8 @@ CREATE TABLE public.comment_option_votes (
CREATE TABLE public.comment_options ( CREATE TABLE public.comment_options (
id integer DEFAULT nextval('public.comment_option_id_seq'::regclass) NOT NULL, id integer DEFAULT nextval('public.comment_option_id_seq'::regclass) NOT NULL,
comment_id integer NOT NULL, parent_id integer NOT NULL,
body character varying(500) NOT NULL,
body_html character varying(500) NOT NULL, body_html character varying(500) NOT NULL,
exclusive integer NOT NULL, exclusive integer NOT NULL,
created_utc integer created_utc integer
@ -870,7 +871,8 @@ CREATE TABLE public.submission_option_votes (
CREATE TABLE public.submission_options ( CREATE TABLE public.submission_options (
id integer DEFAULT nextval('public.submission_option_id_seq'::regclass) NOT NULL, id integer DEFAULT nextval('public.submission_option_id_seq'::regclass) NOT NULL,
submission_id integer NOT NULL, parent_id integer NOT NULL,
body character varying(500) NOT NULL,
body_html character varying(500) NOT NULL, body_html character varying(500) NOT NULL,
exclusive integer NOT NULL, exclusive integer NOT NULL,
created_utc integer created_utc integer
@ -2872,4 +2874,3 @@ ALTER TABLE ONLY public.comments
-- --
-- PostgreSQL database dump complete -- PostgreSQL database dump complete
-- --