mirror of https://git.skyjake.fi/gemini/bubble.git
634 lines
24 KiB
Python
634 lines
24 KiB
Python
import re
|
|
from utils import *
|
|
from model import Post, Segment, User, Subspace
|
|
|
|
|
|
def edit_segment(session):
|
|
if not session.user:
|
|
return 60, 'Must be signed in to edit posts'
|
|
|
|
db = session.db
|
|
req = session.req
|
|
user = session.user
|
|
|
|
try:
|
|
found = re.match(r'^(edit|move|raw)-segment/(\d+)$', req.path[len(session.path):])
|
|
seg_action = found.group(1)
|
|
seg_id = int(found.group(2))
|
|
except:
|
|
return 59, 'Bad request'
|
|
segment = db.get_segment(seg_id)
|
|
if not segment:
|
|
return 51, 'Not found'
|
|
|
|
if session.is_gemini and is_empty_query(req) and seg_action != 'raw':
|
|
if seg_action == 'edit':
|
|
if segment.type == Segment.LINK:
|
|
return 10, 'Edit link: (URL followed by label, separated with space)'
|
|
if segment.type == Segment.POLL:
|
|
return 10, 'Edit poll option:'
|
|
else:
|
|
if segment.type == Segment.POLL:
|
|
return 10, 'Move to which position in the poll: (X to remove)'
|
|
|
|
return 10, 'Edited text: (see Help for special commands)' if seg_action == 'edit' else \
|
|
'Move to which § number? (Other actions: "X" to remove; "." to insert text; ":" to insert long text; "S" to split text; "M" to merge text; "/" to insert a link; "P" to insert poll)'
|
|
|
|
post = db.get_post(segment.post)
|
|
|
|
if not session.is_editable(post):
|
|
return 61, 'Cannot edit posts by other users'
|
|
|
|
if seg_action == 'raw':
|
|
if segment.type != Segment.TEXT:
|
|
return 50, 'Only text segments can be viewed in raw mode'
|
|
if session.is_gemini:
|
|
return segment.content
|
|
else:
|
|
if not req.content_mime.startswith('text/'):
|
|
return 50, 'Bad content format (must be text)'
|
|
seg_text = req.content.decode('utf-8')
|
|
db.update_segment(segment, content=seg_text)
|
|
|
|
elif seg_action == 'edit':
|
|
if segment.type == Segment.LINK:
|
|
seg_url, seg_text = parse_link_segment_query(req)
|
|
db.update_segment(segment, url=seg_url, content=seg_text)
|
|
|
|
elif segment.type == Segment.TEXT:
|
|
if session.is_gemini:
|
|
seg_text = clean_query(req)
|
|
if seg_text == ':':
|
|
return 30, f'{session.server_root("titan")}{session.path}edit-segment/{segment.id}'
|
|
if seg_text == '\\' or seg_text == '/':
|
|
return 30, f'{session.server_root()}{session.path}raw-segment/{segment.id}'
|
|
else:
|
|
seg_text = clean_text(req.content.decode('utf-8'))
|
|
|
|
seg_text = seg_text.rstrip()
|
|
if user.flags & User.COMPOSER_SPLIT_FLAG:
|
|
parts = split_paragraphs(seg_text)
|
|
else:
|
|
parts = [seg_text]
|
|
|
|
db.update_segment(segment, content=parts[0].strip())
|
|
|
|
# Multiple segments will be added if the paragraphs were split.
|
|
if len(parts) > 1:
|
|
pos_idx = db.get_segment_position_as_index(post, segment)
|
|
for new_part in parts[1:]:
|
|
added = db.get_segment(id=db.create_segment(post, Segment.TEXT,
|
|
content=new_part))
|
|
pos_idx += 1
|
|
db.move_segment(post, added, pos_idx)
|
|
|
|
elif segment.type == Segment.POLL:
|
|
opt_text = clean_title(clean_query(req))
|
|
db.update_segment(segment, content=opt_text)
|
|
|
|
elif segment.type == Segment.IMAGE or segment.type == Segment.ATTACHMENT:
|
|
text = clean_query(req).strip()
|
|
text = remove_prefix(text, segment.url)
|
|
text = remove_prefix(text, '=> ' + segment.url)
|
|
seg_text = clean_title(text.strip())
|
|
db.update_segment(segment, content=seg_text)
|
|
else:
|
|
# Moving and other actions.
|
|
arg = clean_query(req)
|
|
cmd = arg.upper()
|
|
pos_idx = db.get_segment_position_as_index(post, segment)
|
|
|
|
if cmd == 'X':
|
|
# TODO: Need a token here?
|
|
db.destroy_segment(segment)
|
|
|
|
elif cmd == '.':
|
|
inserted = db.get_segment(id=db.create_segment(post, Segment.TEXT, ""))
|
|
db.move_segment(post, inserted, pos_idx + 1)
|
|
return 30, f'{session.path}edit-segment/{inserted.id}'
|
|
|
|
elif cmd == ':':
|
|
inserted = db.get_segment(id=db.create_segment(post, Segment.TEXT, ""))
|
|
db.move_segment(post, inserted, pos_idx + 1)
|
|
return 30, f'{session.server_root("titan")}{session.path}edit-segment/{inserted.id}'
|
|
|
|
elif cmd == 'S':
|
|
if segment.type != Segment.TEXT:
|
|
return 50, 'Only text segments can be split'
|
|
new_segments = split_paragraphs(segment.content)
|
|
if len(new_segments) > 1:
|
|
# Update the first segment and insert additional new segments as needed.
|
|
db.update_segment(segment, content=new_segments[0])
|
|
for new_content in new_segments[1:]:
|
|
inserted = db.get_segment(id=db.create_segment(post, Segment.TEXT,
|
|
content=new_content))
|
|
pos_idx += 1
|
|
db.move_segment(post, inserted, pos_idx)
|
|
return 30, f'{session.path}edit/{post.id}'
|
|
|
|
elif cmd == 'M':
|
|
all_segments = db.get_segments(post, poll=False)
|
|
try:
|
|
next_segment = all_segments[pos_idx + 1]
|
|
except:
|
|
return 50, 'Last segment cannot be merged'
|
|
if segment.type != Segment.TEXT or next_segment.type != Segment.TEXT:
|
|
return 50, 'Only text segments can be merged'
|
|
db.update_segment(segment, content=segment.content + '\n\n' + next_segment.content)
|
|
db.destroy_segment(next_segment)
|
|
return 30, f'{session.path}edit/{post.id}'
|
|
|
|
elif cmd == '/':
|
|
inserted = db.get_segment(id=db.create_segment(post, Segment.LINK))
|
|
db.move_segment(post, inserted, pos_idx + 1)
|
|
return 30, f'{session.path}edit-segment/{inserted.id}'
|
|
|
|
elif cmd == 'P':
|
|
inserted_id = db.create_segment(post, Segment.POLL)
|
|
return 30, f'{session.path}edit-segment/{inserted_id}'
|
|
|
|
else:
|
|
try:
|
|
db.move_segment(post, segment, max(1, int(arg)) - 1)
|
|
except:
|
|
return 50, 'Expected a position number or X'
|
|
|
|
return 30, f'{session.server_root()}{session.path}edit/{segment.post}'
|
|
|
|
|
|
def make_composer_page(session):
|
|
"""Post composer."""
|
|
|
|
db = session.db
|
|
req = session.req
|
|
CHECKS = session.CHECKS
|
|
|
|
found = re.match(r'^(\d+)(/([\w-]+)(/([0-9a-zA-Z]{10}))?)?$',
|
|
req.path[len(session.path + 'edit/'):])
|
|
try:
|
|
post_id = int(found.group(1))
|
|
except:
|
|
return 30, session.server_root()
|
|
post_action = found.group(3)
|
|
req_token = found.group(5)
|
|
post = db.get_post(post_id)
|
|
if not post:
|
|
return 51, 'Not found'
|
|
subspace = db.get_subspace(id=post.subspace)
|
|
is_orphan_comment = post.parent and not db.get_post(id=post.parent)
|
|
|
|
if not session.user:
|
|
return 60, 'Must be signed in to edit posts'
|
|
|
|
if is_orphan_comment and post_action != 'delete':
|
|
return 61, 'Locked comment'
|
|
|
|
if post_action == 'delete':
|
|
if not session.is_deletable(post):
|
|
return 61, 'Cannot delete post'
|
|
elif post_action == 'move':
|
|
if not session.is_movable(post):
|
|
return 61, 'Cannot move post'
|
|
elif post_action == 'mod-title':
|
|
if not session.is_title_editable(post):
|
|
return 61, 'Cannot edit post title'
|
|
elif not session.is_editable(post):
|
|
return 61, 'Cannot edit posts by other users'
|
|
|
|
user_token = db.get_token(session.user)
|
|
is_issue_tracker = (subspace.flags & Subspace.ISSUE_TRACKER) != 0
|
|
post_type = 'Issue' if is_issue_tracker else 'Post' if not post.parent else 'Comment'
|
|
|
|
link = f'{session.path}edit/{post_id}'
|
|
gemini_link = f'{session.server_root()}' + link
|
|
seg_link = session.path + 'edit-segment'
|
|
titan_seg_link = f'titan://{session.bubble.hostname}:{session.bubble.port}' + seg_link
|
|
|
|
if post_action == 'add-text':
|
|
if not is_empty_query(req) or (session.is_titan and len(req.content)):
|
|
seg_text = clean_query(req) if session.is_gemini \
|
|
else clean_text(req.content.decode('utf-8'))
|
|
if session.user.flags & User.COMPOSER_SPLIT_FLAG:
|
|
parts = split_paragraphs(seg_text)
|
|
else:
|
|
parts = [seg_text.rstrip()]
|
|
for part in parts:
|
|
db.create_segment(post, Segment.TEXT, content=part)
|
|
return 30, gemini_link
|
|
return 10, 'Add text segment:'
|
|
|
|
if post_action == 'add-link':
|
|
if not is_empty_query(req):
|
|
seg_url, seg_text = parse_link_segment_query(req)
|
|
db.create_segment(post, Segment.LINK, url=seg_url, content=seg_text)
|
|
return 30, link
|
|
return 10, 'Add link: (URL followed by label, separated with space)'
|
|
|
|
if post_action == 'add-poll':
|
|
if not is_empty_query(req):
|
|
db.create_segment(post, Segment.POLL, content=clean_title(clean_query(req)))
|
|
return 30, link
|
|
return 10, 'Add poll option:'
|
|
|
|
if post_action == 'add-file' and session.is_titan:
|
|
if len(req.content) > session.bubble.max_file_size:
|
|
return 50, f'File attachments must be less than {int(session.bubble.max_file_size / 1024)} KB'
|
|
if req.content_token:
|
|
fn = req.content_token.strip()
|
|
else:
|
|
fn = ''
|
|
mime = 'application/octet-stream'
|
|
if req.content_mime:
|
|
mime = req.content_mime.lower().split(';')[0]
|
|
file_id = db.create_file(session.user, fn, mime, req.content)
|
|
is_image = mime.startswith('image/')
|
|
|
|
url_path = '/u/' + session.user.name + '/'
|
|
url_path += 'image' if is_image else 'file'
|
|
url_path += f'/{file_id}'
|
|
if len(fn):
|
|
# TODO: Clean up the filename.
|
|
url_path += '/' + fn
|
|
EXTENSIONS = {
|
|
'image/jpeg': '.jpeg',
|
|
'image/jpg': '.jpg',
|
|
'image/png': '.png',
|
|
'image/gif': '.gif'
|
|
}
|
|
if len(fn) == 0 and mime in EXTENSIONS:
|
|
url_path += EXTENSIONS[mime]
|
|
segment_id = db.create_segment(post,
|
|
Segment.IMAGE if is_image else Segment.ATTACHMENT,
|
|
url=url_path, content=fn)
|
|
db.set_file_segment(file_id, segment_id)
|
|
return 30, gemini_link
|
|
|
|
if post_action == 'title' or post_action == 'mod-title':
|
|
if req.query == None:
|
|
return 10, f'Enter {post_type.lower()} title:'
|
|
title_text = clean_title(clean_query(req))
|
|
db.update_post(post, title=title_text)
|
|
return 30, link if post_action == 'title' else post.page_url()
|
|
|
|
if post_action == 'omit-feed':
|
|
if session.user.role == User.LIMITED:
|
|
return 61, "Not authorized"
|
|
db.update_post(post, flags=post.flags ^ Post.OMIT_FROM_FEED_FLAG)
|
|
return 30, link
|
|
|
|
if post_action == 'omit-all':
|
|
if session.user.role == User.LIMITED:
|
|
return 61, "Not authorized"
|
|
db.update_post(post, flags=post.flags ^ Post.OMIT_FROM_ALL_FLAG)
|
|
return 30, link
|
|
|
|
if post_action == 'featured-link':
|
|
db.update_post(post, flags=post.flags ^ Post.DISABLE_FEATURED_LINK_FLAG)
|
|
db.update_post_summary(post)
|
|
return 30, link
|
|
|
|
if post_action == 'preview':
|
|
db.update_post_summary(post)
|
|
page = f'=> {link}/publish 📤 Publish {post_type.lower()} (in {subspace.title()})\n'
|
|
page += f'=> {link} Edit draft\n'
|
|
session.context = subspace
|
|
session.is_context_tracker = is_issue_tracker
|
|
if not post.parent:
|
|
page += '\n# Feed Preview\n'
|
|
page += session.feed_entry(post, session.context)
|
|
if not post.title:
|
|
page += f'\n# {post_type} Preview\n'
|
|
else:
|
|
page += '\n\n⁂\n\n\n'
|
|
page += session.render_post(post)
|
|
poll = session.render_poll(post)
|
|
if poll:
|
|
if not page.endswith('\n\n'): page += '\n'
|
|
page += poll
|
|
return page
|
|
|
|
if post_action == 'publish':
|
|
if session.user.role == User.LIMITED and subspace.owner != session.user.id:
|
|
return 61, "Not authorized"
|
|
if is_issue_tracker:
|
|
if len(post.title) == 0 and post.parent == 0:
|
|
return 50, "Issues must have a title"
|
|
if len(post.summary.strip()) == 0:
|
|
return 50, "Cannot publish empty post"
|
|
db.publish_post(post)
|
|
if session.user.role == User.LIMITED:
|
|
db.update_post(post, flags=Post.OMIT_FROM_ALL_FLAG | Post.OMIT_FROM_FEED_FLAG)
|
|
return 30, post.page_url()
|
|
|
|
if post_action == 'unpublish':
|
|
db.unpublish_post(post)
|
|
return 30, f'/edit/{post.id}'
|
|
|
|
if post_action == 'move':
|
|
if not db.verify_token(session.user, req_token):
|
|
return 61, "Not authorized"
|
|
if session.user.role == User.LIMITED:
|
|
return 61, "Not authorized"
|
|
|
|
if is_empty_query(req):
|
|
return 10, f'Move post {post.id} to which subspace?'
|
|
|
|
subname = clean_query(req)
|
|
if subname.startswith('s/'):
|
|
subname = subname[2:]
|
|
dst_sub = db.get_subspace(name=subname)
|
|
if not dst_sub or dst_sub.id == post.subspace:
|
|
return 10, f'Move post {post.id} to which subspace?'
|
|
if dst_sub.flags & Subspace.ISSUE_TRACKER:
|
|
return 50, "Cannot move to an issue tracker subspace"
|
|
if dst_sub.owner and dst_sub.owner != post.user:
|
|
return 50, "Cannot move to another user's subspace"
|
|
|
|
oldsub_id = post.subspace
|
|
db.update_post(post, subspace_id=dst_sub.id)
|
|
db.move_comments(post, oldsub_id, dst_sub.id)
|
|
|
|
# Notify as if the post was new.
|
|
db.notify_followed_about_post(post)
|
|
|
|
post.sub_name = dst_sub.name
|
|
return 30, post.page_url()
|
|
|
|
if post_action == 'delete':
|
|
if not db.verify_token(session.user, req_token):
|
|
return 61, "Not authorized"
|
|
|
|
if is_empty_query(req):
|
|
return 10, f'Delete {post_type.lower()} "{post.ymd_date()} {post.title_text()}" in {"u" if post.sub_owner else "s"}/{post.sub_name}? (Enter YES to confirm)'
|
|
|
|
if req.query.upper() == 'YES':
|
|
dst = '/dashboard' if post.is_draft else '/u/' + session.user.name
|
|
try:
|
|
if post.parent:
|
|
dst = db.get_post(post.parent).page_url()
|
|
except:
|
|
pass
|
|
db.destroy_post(post)
|
|
return 30, dst
|
|
else:
|
|
return 30, f'/edit/{post.id}'
|
|
|
|
is_draft = post.is_draft
|
|
is_comment = post.parent != 0
|
|
|
|
if is_comment:
|
|
kind = 'Comment'
|
|
elif is_draft:
|
|
kind = 'Draft'
|
|
else:
|
|
kind = post_type
|
|
|
|
if not is_comment:
|
|
page = f'# Edit {kind}\n'
|
|
page += session.dashboard_link()
|
|
if is_draft:
|
|
page += f'=> {link}/preview 👁️ Preview {post_type.lower()}\n'
|
|
|
|
# Options and metadata:
|
|
page += f'\n## {post.title_text()}\n'
|
|
page += f'=> {link}/title ✏️ Edit {post_type.lower()} title\n'
|
|
if session.user.role != User.LIMITED:
|
|
if not subspace.flags & Subspace.OMIT_FROM_ALL_FLAG:
|
|
page += f'=> {link}/omit-all {CHECKS[nonzero(post.flags & Post.OMIT_FROM_ALL_FLAG)]} Omit {post_type.lower()} from All Posts\n'
|
|
page += f'=> {link}/omit-feed {CHECKS[nonzero(post.flags & Post.OMIT_FROM_FEED_FLAG)]} Omit {post_type.lower()} from Gemini/Atom feed\n'
|
|
page += f'=> {link}/featured-link {CHECKS[is_zero(post.flags & Post.DISABLE_FEATURED_LINK_FLAG)]} Feature first link\n'
|
|
else:
|
|
page += f"=> /{subspace.title()} Limited account: post is visible only in {subspace.title()}\n"
|
|
if is_issue_tracker:
|
|
page += f'=> /{subspace.title()} 🐞 Issue in: {subspace.title()}\n'
|
|
if not is_draft:
|
|
page += f'\nThis {post_type.lower()} is published as:\n'
|
|
page += session.gemini_feed_entry(post, None)
|
|
else:
|
|
page = '# Edit Comment\n'
|
|
page += session.dashboard_link()
|
|
if is_draft:
|
|
page += f'=> {link}/preview 👁️ Preview comment\n'
|
|
try:
|
|
page += '\nThis is a comment on:\n'
|
|
parent_post = db.get_post(id=post.parent)
|
|
page += session.gemini_feed_entry(parent_post, None)
|
|
except:
|
|
pass
|
|
|
|
segments = db.get_segments(post)
|
|
sid = 0
|
|
|
|
# Split out the poll options.
|
|
poll_options = list(filter(lambda s: s.type == Segment.POLL, segments))
|
|
segments = list(filter(lambda s: s.type != Segment.POLL, segments))
|
|
|
|
if len(poll_options):
|
|
page += '\n## Poll\n'
|
|
opt_num = 1
|
|
for opt in poll_options:
|
|
page += f'\n=> {seg_link}/{opt.id} ✏️ OPTION {opt_num}: {opt.content}\n'
|
|
page += f"=> {session.path}move-segment/{opt.id} ↕︎ Move/actions\n"
|
|
opt_num += 1
|
|
page += f'\n=> {link}/add-poll Add poll option\n'
|
|
|
|
if len(segments) == 1:
|
|
page += '\n## Contents\n'
|
|
|
|
for segment in segments:
|
|
sid += 1
|
|
|
|
if len(segments) > 1:
|
|
page += f'\n## — § {sid} —\n\n'
|
|
else:
|
|
page += '\n'
|
|
|
|
if segment.type == Segment.TEXT:
|
|
page += segment.content + \
|
|
f"\n=> {seg_link}/{segment.id} ✏️ Edit text\n"
|
|
|
|
elif segment.type == Segment.LINK:
|
|
page += f'=> {segment.url} {segment.content}\n'
|
|
page += f"=> {seg_link}/{segment.id} ✏️ Edit link\n"
|
|
|
|
elif segment.type == Segment.IMAGE:
|
|
page += f'=> {segment.url} {segment.content}\n'
|
|
page += f'=> {seg_link}/{segment.id} ✏️ Edit caption\n'
|
|
|
|
elif segment.type == Segment.ATTACHMENT:
|
|
page += f'=> {segment.url} {segment.content}\n'
|
|
page += f'=> {seg_link}/{segment.id} ✏️ Edit label\n'
|
|
|
|
page += f"=> {session.path}move-segment/{segment.id} ↕︎ Move/actions\n"
|
|
|
|
page += f'\n'
|
|
|
|
page += '## Actions\n'
|
|
page += f'=> {link}/add-text Add text\n'
|
|
page += f'=> titan://{session.bubble.hostname}/edit/{post.id}/add-text Add long text\n'
|
|
page += f'=> {link}/add-link Add link\n'
|
|
page += f'=> titan://{session.bubble.hostname}/edit/{post.id}/add-file Add image or file attachment ({int(session.bubble.max_file_size/1024)} KB)\nOptionally, you can set a filename with the token field.\n'
|
|
if not is_comment and not poll_options:
|
|
page += f'=> {link}/add-poll Add poll\n'
|
|
|
|
actions = []
|
|
if not post.parent:
|
|
cur_tags = ': ' + post.tags if post.tags else ''
|
|
actions.append(f'=> /edit-tags/{post.id} 🏷️ Add/remove tags{cur_tags}\n')
|
|
if not is_draft and session.is_unpublishable(post):
|
|
actions.append(f'=> {link}/unpublish Unpublish {kind.lower()}\nThe {kind.lower()} is converted into a draft.\n')
|
|
actions.append(f'=> {link}/delete/{user_token} ❌ Delete {kind.lower()}\n')
|
|
page += '\n' + ''.join(actions)
|
|
return page
|
|
|
|
|
|
def make_comment(session):
|
|
if not session.user:
|
|
return 60, 'Login required'
|
|
if session.user.role == User.LIMITED:
|
|
return 61, 'Not authorized'
|
|
|
|
db = session.db
|
|
req = session.req
|
|
|
|
found = re.match(r'^(\d+)(/([\w-]+))?$', req.path[len(session.path + 'comment/'):])
|
|
post_id = int(found.group(1))
|
|
post = db.get_post(post_id)
|
|
if not post:
|
|
return 51, 'Not found'
|
|
if post.flags & Post.LOCKED_FLAG and session.user.role != User.ADMIN:
|
|
return 61, 'Post is locked'
|
|
|
|
special = None
|
|
if session.is_gemini:
|
|
com_text = clean_query(req)
|
|
if len(com_text) == 0:
|
|
return 10, 'New comment: (draft a long comment by ending with a backslash)'
|
|
|
|
if com_text == '.':
|
|
special = 'draft'
|
|
com_text = ''
|
|
elif com_text == ':':
|
|
return 30, session.server_root('titan') + req.path
|
|
elif com_text.endswith('\\'):
|
|
com_text = com_text[:-1].strip()
|
|
special = 'draft'
|
|
else:
|
|
if not req.content_mime.startswith('text/'):
|
|
return 50, 'Content must be text'
|
|
com_text = req.content.decode('utf-8')
|
|
|
|
com_id = db.create_post(session.user, post.subspace, parent=post.id)
|
|
com = db.get_post(com_id, draft=True)
|
|
if com_text:
|
|
db.create_segment(com, Segment.TEXT, content=com_text)
|
|
db.update_post_summary(com)
|
|
if not special:
|
|
db.publish_post(com)
|
|
return 30, session.server_root() + post.page_url()
|
|
# Keep as a draft.
|
|
return 30, session.server_root() + session.path + f'edit/{com_id}'
|
|
|
|
|
|
def make_tags_page(session):
|
|
# Check the rights for changing tags.
|
|
if not session.user:
|
|
return 60, 'Login required'
|
|
|
|
db = session.db
|
|
req = session.req
|
|
|
|
found = re.search(r'/(\d+)(/(add|remove|open|close))?$', req.path)
|
|
if not found:
|
|
return 59, 'Bad request'
|
|
post_id = int(found[1])
|
|
action = found[3]
|
|
post = db.get_post(id=post_id)
|
|
if not post:
|
|
return 51, 'Not found'
|
|
if post.parent:
|
|
return 59, 'Comments cannot have tags'
|
|
|
|
if not session.is_deletable(post):
|
|
return 61, 'Not authorized to edit tags'
|
|
|
|
subspace = db.get_subspace(id=post.subspace)
|
|
is_issue_tracker = (subspace.flags & Subspace.ISSUE_TRACKER) != 0
|
|
session.is_context_tracker = is_issue_tracker
|
|
|
|
edit_link = f'/edit-tags/{post.id}'
|
|
|
|
if is_issue_tracker:
|
|
if action == 'open':
|
|
action = 'remove'
|
|
req.query = Post.TAG_CLOSED
|
|
edit_link = post.page_url()
|
|
elif action == 'close':
|
|
action = 'add'
|
|
req.query = Post.TAG_CLOSED
|
|
edit_link = post.page_url()
|
|
|
|
if action == 'add':
|
|
if req.query is None:
|
|
return 10, "Tag to add: " + session.NAME_HINT
|
|
tag = clean_query(req)
|
|
if not is_valid_name(tag):
|
|
return 59, 'Invalid tag name'
|
|
if tag == Post.TAG_ANNOUNCEMENT and (session.user.role != User.ADMIN or
|
|
is_issue_tracker):
|
|
return 61, 'Not authorized'
|
|
db.modify_tags(post, tag, session.user, add=True)
|
|
|
|
# Only pinned or announcement can be used at a time.
|
|
if tag == Post.TAG_ANNOUNCEMENT:
|
|
db.modify_tags(post, Post.TAG_PINNED, session.user, add=False)
|
|
elif tag == Post.TAG_PINNED:
|
|
db.modify_tags(post, Post.TAG_ANNOUNCEMENT, session.user, add=False)
|
|
|
|
return 30, edit_link
|
|
|
|
if action == 'remove':
|
|
if req.query is None:
|
|
return 10, "Tag to remove:"
|
|
tag = clean_query(req)
|
|
if tag == Post.TAG_ANNOUNCEMENT and session.user.role != User.ADMIN:
|
|
return 61, 'Not authorized'
|
|
db.modify_tags(post, tag, session.user, add=False)
|
|
return 30, edit_link
|
|
|
|
if not req.query:
|
|
Kind = "Issue" if is_issue_tracker else "Post"
|
|
kind = Kind.lower()
|
|
page = f'# {Kind} Tags\n\n'
|
|
page += 'Editing tags on:\n'
|
|
page += session.gemini_feed_entry(post, subspace)
|
|
tags = list(filter(lambda tag: tag != Post.TAG_POLL, db.get_tags(post)))
|
|
popular_tags = db.get_popular_tags(subspace)
|
|
#if len(tags):
|
|
# page += '### ' + ' '.join(map(lambda t: '#' + t, tags)) + '\n'
|
|
if tags:
|
|
page += f'\nCurrent tags on the {kind} (click to remove):\n'
|
|
for tag in tags:
|
|
page += f'=> {edit_link}/remove?{tag} ❌ {tag}\n'
|
|
|
|
page += '\n## Add Tag\n'
|
|
page += f'=> {edit_link}/add New tag\n'
|
|
for tag in sorted(popular_tags[:15]):
|
|
if tag in (Post.TAG_PINNED, Post.TAG_ANNOUNCEMENT, Post.TAG_POLL, Post.TAG_CLOSED):
|
|
continue
|
|
if not tag in tags:
|
|
page += f'=> {edit_link}/add?{tag} 🏷️ {tag}\n'
|
|
|
|
if session.user.role == User.ADMIN or post.is_pinned == 0:
|
|
page += '\n'
|
|
|
|
if post.is_pinned != 1:
|
|
page += f'=> {edit_link}/add?{Post.TAG_PINNED} 📌 {Post.TAG_PINNED}\n'
|
|
if not is_issue_tracker and post.is_pinned != 2 and session.user.role == User.ADMIN:
|
|
page += f'=> {edit_link}/add?{Post.TAG_ANNOUNCEMENT} 📣 {Post.TAG_ANNOUNCEMENT}\n'
|
|
if is_issue_tracker and Post.TAG_CLOSED not in tags:
|
|
page += f'=> {edit_link}/add?{Post.TAG_CLOSED} ✔︎ {Post.TAG_CLOSED}\n'
|
|
return page
|
|
|
|
return 30, post.page_url()
|