a fresh start :)
This commit is contained in:
@@ -1,228 +0,0 @@
|
||||
from flask import Blueprint, request, url_for, make_response
|
||||
from ..lib.babycode import babycode_to_html
|
||||
from ..constants import REACTION_EMOJI
|
||||
from .users import is_logged_in, get_active_user
|
||||
from ..models import APIRateLimits, Threads, Reactions, Users, BookmarkCollections, BookmarkedThreads, BookmarkedPosts, BadgeUploads
|
||||
from ..db import db
|
||||
|
||||
bp = Blueprint("api", __name__, url_prefix="/api/")
|
||||
|
||||
|
||||
@bp.post('/thread-updates/<thread_id>')
|
||||
def thread_updates(thread_id):
|
||||
thread = Threads.find({'id': thread_id})
|
||||
if not thread:
|
||||
return {'error': 'no such thread'}, 404
|
||||
target_time = request.json.get('since')
|
||||
if not target_time:
|
||||
return {'error': 'missing parameter "since"'}, 400
|
||||
try:
|
||||
target_time = int(target_time)
|
||||
except:
|
||||
return {'error': 'parameter "since" is not/cannot be converted to a number'}, 400
|
||||
|
||||
q = 'SELECT id FROM posts WHERE thread_id = ? AND posts.created_at > ? ORDER BY posts.created_at ASC LIMIT 1'
|
||||
new_post = db.fetch_one(q, thread_id, target_time)
|
||||
if not new_post:
|
||||
return {'status': 'none'}
|
||||
|
||||
url = url_for('threads.thread', slug=thread.slug, after=new_post['id'], _anchor=f"post-{new_post['id']}")
|
||||
return {'status': 'new_post', 'url': url}
|
||||
|
||||
|
||||
@bp.post('/babycode-preview')
|
||||
def babycode_preview():
|
||||
if not is_logged_in():
|
||||
return {'error': 'not authorized'}, 401
|
||||
user = get_active_user()
|
||||
if not APIRateLimits.is_allowed(user.id, 'babycode_preview', 5):
|
||||
return {'error': 'too many requests'}, 429
|
||||
markup = request.json.get('markup')
|
||||
if not markup or not isinstance(markup, str):
|
||||
return {'error': 'markup field missing or invalid type'}, 400
|
||||
banned_tags = request.json.get('banned_tags', [])
|
||||
rendered = babycode_to_html(markup, banned_tags).result
|
||||
return {'html': rendered}
|
||||
|
||||
|
||||
@bp.post('/add-reaction/<post_id>')
|
||||
def add_reaction(post_id):
|
||||
if not is_logged_in():
|
||||
return {'error': 'not authorized', 'error_code': 401}, 401
|
||||
user = get_active_user()
|
||||
reaction_text = request.json.get('emoji')
|
||||
if not reaction_text or not isinstance(reaction_text, str):
|
||||
return {'error': 'emoji field missing or invalid type', 'error_code': 400}, 400
|
||||
if reaction_text not in REACTION_EMOJI:
|
||||
return {'error': 'unsupported reaction', 'error_code': 400}, 400
|
||||
|
||||
reaction = Reactions.find({
|
||||
'user_id': user.id,
|
||||
'post_id': int(post_id),
|
||||
'reaction_text': reaction_text,
|
||||
})
|
||||
|
||||
if reaction:
|
||||
return {'error': 'reaction already exists', 'error_code': 409}, 409
|
||||
|
||||
reaction = Reactions.create({
|
||||
'user_id': user.id,
|
||||
'post_id': int(post_id),
|
||||
'reaction_text': reaction_text,
|
||||
})
|
||||
|
||||
return {'status': 'added'}
|
||||
|
||||
|
||||
@bp.post('/remove-reaction/<post_id>')
|
||||
def remove_reaction(post_id):
|
||||
if not is_logged_in():
|
||||
return {'error': 'not authorized'}, 401
|
||||
user = get_active_user()
|
||||
reaction_text = request.json.get('emoji')
|
||||
if not reaction_text or not isinstance(reaction_text, str):
|
||||
return {'error': 'emoji field missing or invalid type'}, 400
|
||||
if reaction_text not in REACTION_EMOJI:
|
||||
return {'error': 'unsupported reaction'}, 400
|
||||
|
||||
reaction = Reactions.find({
|
||||
'user_id': user.id,
|
||||
'post_id': int(post_id),
|
||||
'reaction_text': reaction_text,
|
||||
})
|
||||
|
||||
if not reaction:
|
||||
return {'error': 'reaction does not exist'}, 404
|
||||
|
||||
reaction.delete()
|
||||
|
||||
return {'status': 'removed'}
|
||||
|
||||
@bp.post('/manage-bookmark-collections/<user_id>')
|
||||
def manage_bookmark_collections(user_id):
|
||||
if not is_logged_in():
|
||||
return {'error': 'not authorized', 'error_code': 401}, 401
|
||||
|
||||
target_user = Users.find({'id': user_id})
|
||||
if target_user.id != get_active_user().id:
|
||||
return {'error': 'forbidden', 'error_code': 403}, 403
|
||||
|
||||
if target_user.is_guest():
|
||||
return {'error': 'forbidden', 'error_code': 403}, 403
|
||||
|
||||
collections_data = request.json
|
||||
for idx, coll_data in enumerate(collections_data.get('collections')):
|
||||
if coll_data['is_new']:
|
||||
collection = BookmarkCollections.create({
|
||||
'name': coll_data['name'],
|
||||
'user_id': target_user.id,
|
||||
'sort_order': idx,
|
||||
})
|
||||
else:
|
||||
collection = BookmarkCollections.find({'id': coll_data['id']})
|
||||
if not collection:
|
||||
continue
|
||||
|
||||
update = {'name': coll_data['name']}
|
||||
if not collection.is_default:
|
||||
update['sort_order'] = idx
|
||||
collection.update(update)
|
||||
|
||||
for removed_id in collections_data.get('removed_collections'):
|
||||
collection = BookmarkCollections.find({'id': removed_id})
|
||||
if not collection:
|
||||
continue
|
||||
|
||||
if collection.is_default:
|
||||
continue
|
||||
|
||||
collection.delete()
|
||||
|
||||
|
||||
return {'status': 'ok'}, 200
|
||||
|
||||
|
||||
@bp.post('/bookmark-post/<post_id>')
|
||||
def bookmark_post(post_id):
|
||||
if not is_logged_in():
|
||||
return {'error': 'not authorized', 'error_code': 401}, 401
|
||||
|
||||
operation = request.json.get('operation')
|
||||
if operation == 'remove' and request.json.get('collection_id', '') == '':
|
||||
return {'status': 'not modified'}, 304
|
||||
collection_id = int(request.json.get('collection_id'))
|
||||
post_id = int(post_id)
|
||||
memo = request.json.get('memo', '')
|
||||
|
||||
if operation == 'move':
|
||||
bm = BookmarkedPosts.find({'post_id': post_id})
|
||||
if not bm:
|
||||
BookmarkedPosts.create({
|
||||
'post_id': post_id,
|
||||
'collection_id': collection_id,
|
||||
'note': memo,
|
||||
})
|
||||
else:
|
||||
bm.update({
|
||||
'collection_id': collection_id,
|
||||
'note': memo,
|
||||
})
|
||||
elif operation == 'remove':
|
||||
bm = BookmarkedPosts.find({'post_id': post_id})
|
||||
if bm:
|
||||
bm.delete()
|
||||
else:
|
||||
return {'error': 'bad request'}, 400
|
||||
|
||||
return {'status': 'ok'}, 200
|
||||
|
||||
|
||||
@bp.post('/bookmark-thread/<thread_id>')
|
||||
def bookmark_thread(thread_id):
|
||||
if not is_logged_in():
|
||||
return {'error': 'not authorized', 'error_code': 401}, 401
|
||||
|
||||
operation = request.json.get('operation')
|
||||
if operation == 'remove' and request.json.get('collection_id', '') == '':
|
||||
return {'status': 'not modified'}, 304
|
||||
collection_id = int(request.json.get('collection_id'))
|
||||
thread_id = int(thread_id)
|
||||
memo = request.json.get('memo', '')
|
||||
|
||||
if operation == 'move':
|
||||
bm = BookmarkedThreads.find({'thread_id': thread_id})
|
||||
if not bm:
|
||||
BookmarkedThreads.create({
|
||||
'thread_id': thread_id,
|
||||
'collection_id': collection_id,
|
||||
'note': memo,
|
||||
})
|
||||
else:
|
||||
bm.update({
|
||||
'collection_id': collection_id,
|
||||
'note': memo,
|
||||
})
|
||||
elif operation == 'remove':
|
||||
bm = BookmarkedThreads.find({
|
||||
'thread_id': thread_id,
|
||||
'note': memo,
|
||||
})
|
||||
if bm:
|
||||
bm.delete()
|
||||
else:
|
||||
return {'error': 'bad request'}, 400
|
||||
|
||||
return {'status': 'ok'}, 200
|
||||
|
||||
@bp.get('/current-user')
|
||||
def get_current_user_info():
|
||||
if not is_logged_in():
|
||||
return {'user': None}
|
||||
|
||||
user = get_active_user()
|
||||
return {
|
||||
'user': {
|
||||
'username': user.username,
|
||||
'display_name': user.display_name,
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
from flask import Blueprint, redirect, url_for, render_template
|
||||
from datetime import datetime
|
||||
|
||||
bp = Blueprint("app", __name__, url_prefix = "/")
|
||||
|
||||
@bp.route("/")
|
||||
def index():
|
||||
return redirect(url_for("topics.all_topics"))
|
||||
@@ -1,111 +0,0 @@
|
||||
from flask import Blueprint, render_template, render_template_string, current_app, abort
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
bp = Blueprint('guides', __name__, url_prefix='/guides/')
|
||||
|
||||
|
||||
def parse_guide_title(content: str):
|
||||
lines = content.strip().split('\n', 1)
|
||||
fline = lines[0].strip()
|
||||
if fline.startswith('#'):
|
||||
title = fline[2:].strip()
|
||||
content = lines[1] if len(lines) > 1 else ''
|
||||
return title, content.strip()
|
||||
|
||||
return None, content.strip()
|
||||
|
||||
|
||||
def get_guides_by_category():
|
||||
guides_dir = Path(current_app.root_path) / 'templates' / 'guides'
|
||||
categories = {}
|
||||
|
||||
for item in guides_dir.iterdir():
|
||||
if item.is_dir() and not item.name.startswith('_'):
|
||||
category = item.name
|
||||
categories[category] = []
|
||||
for guide_file in sorted(item.glob('*.html')):
|
||||
if guide_file.name.startswith('_'):
|
||||
continue
|
||||
|
||||
m = re.match(r'(\d+)-(.+)\.html', guide_file.name)
|
||||
if not m:
|
||||
continue
|
||||
|
||||
sort_num = int(m.group(1))
|
||||
slug = m.group(2)
|
||||
|
||||
try:
|
||||
with open(guide_file, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
title, template = parse_guide_title(content)
|
||||
if not title:
|
||||
title = slug.replace('-', ' ').title()
|
||||
|
||||
categories[category].append(({
|
||||
'sort': sort_num,
|
||||
'slug': slug,
|
||||
'filename': guide_file.name,
|
||||
'title': title,
|
||||
'path': f'{category}/{guide_file.name}',
|
||||
'url': f'/guides/{category}/{slug}',
|
||||
'template': template,
|
||||
}))
|
||||
except Exception as e:
|
||||
current_app.logger.warning(f'failed to read {guide_file}: {e}')
|
||||
continue
|
||||
|
||||
categories[category].sort(key=lambda x: x['sort'])
|
||||
|
||||
return categories
|
||||
|
||||
|
||||
@bp.get('/babycode')
|
||||
def babycode():
|
||||
# print(get_guides_by_category())
|
||||
return '<h1>no</h1>'
|
||||
|
||||
|
||||
@bp.get('/<category>/<slug>')
|
||||
def guide_page(category, slug):
|
||||
categories = get_guides_by_category()
|
||||
if category not in categories:
|
||||
abort(404)
|
||||
return
|
||||
|
||||
for i, guide in enumerate(categories[category]):
|
||||
if guide['slug'] != slug:
|
||||
continue
|
||||
|
||||
next_guide = None
|
||||
prev_guide = None
|
||||
if i != 0:
|
||||
prev_guide = categories[category][i - 1]
|
||||
|
||||
if i + 1 < len(categories[category]):
|
||||
next_guide = categories[category][i + 1]
|
||||
|
||||
return render_template_string(guide['template'], next_guide=next_guide, prev_guide=prev_guide, category=category, guide=guide)
|
||||
|
||||
abort(404)
|
||||
|
||||
|
||||
@bp.get('/<category>/')
|
||||
def category_index(category):
|
||||
categories = get_guides_by_category()
|
||||
if category not in categories:
|
||||
abort(404)
|
||||
return
|
||||
|
||||
return render_template('guides/category_index.html', category=category, pages=categories[category])
|
||||
|
||||
|
||||
@bp.get('/')
|
||||
def guides_index():
|
||||
return render_template('guides/guides_index.html', categories=get_guides_by_category())
|
||||
|
||||
|
||||
@bp.get('/contact')
|
||||
def contact():
|
||||
return render_template('guides/contact.html')
|
||||
@@ -1,62 +0,0 @@
|
||||
from flask import Blueprint, render_template, abort, request
|
||||
from .users import get_active_user, is_logged_in
|
||||
from ..models import BookmarkCollections, BookmarkedPosts, BookmarkedThreads, BadgeUploads, Badges
|
||||
from functools import wraps
|
||||
|
||||
bp = Blueprint('hyperapi', __name__, url_prefix='/hyperapi/')
|
||||
|
||||
def login_required(view_func):
|
||||
@wraps(view_func)
|
||||
def dec(*args, **kwargs):
|
||||
if not is_logged_in():
|
||||
abort(403)
|
||||
return view_func(*args, **kwargs)
|
||||
return dec
|
||||
|
||||
def account_required(view_func):
|
||||
@wraps(view_func)
|
||||
def dec(*args, **kwargs):
|
||||
if get_active_user().is_guest():
|
||||
abort(403)
|
||||
return view_func(*args, **kwargs)
|
||||
return dec
|
||||
|
||||
@bp.errorhandler(403)
|
||||
def handle_403(e):
|
||||
return "<h1>forbidden</h1>", 403
|
||||
|
||||
|
||||
@bp.get('/bookmarks-dropdown/<bookmark_type>')
|
||||
@login_required
|
||||
@account_required
|
||||
def bookmarks_dropdown(bookmark_type):
|
||||
collections = BookmarkCollections.findall({'user_id': get_active_user().id})
|
||||
concept_id = request.args.get('id')
|
||||
require_reload = bool(int(request.args.get('require_reload', default=0)))
|
||||
if bookmark_type.lower() == 'thread':
|
||||
selected = next(filter(lambda bc: bc.has_thread(concept_id), collections), None)
|
||||
elif bookmark_type.lower() == 'post':
|
||||
selected = next(filter(lambda bc: bc.has_post(concept_id), collections), None)
|
||||
else:
|
||||
abort(400)
|
||||
return
|
||||
|
||||
if selected:
|
||||
if bookmark_type.lower() == 'thread':
|
||||
memo = BookmarkedThreads.find({'collection_id': selected.id, 'thread_id': int(concept_id)}).note
|
||||
else:
|
||||
memo = BookmarkedPosts.find({'collection_id': selected.id, 'post_id': int(concept_id)}).note
|
||||
else:
|
||||
memo = ''
|
||||
|
||||
|
||||
return render_template('components/bookmarks_dropdown.html', collections=collections, id=concept_id, selected=selected, type=bookmark_type, memo=memo, require_reload=require_reload)
|
||||
|
||||
|
||||
@bp.get('/badge-editor')
|
||||
@login_required
|
||||
@account_required
|
||||
def get_badges():
|
||||
uploads = BadgeUploads.get_for_user(get_active_user().id)
|
||||
badges = sorted(Badges.findall({'user_id': int(get_active_user().id)}), key=lambda x: x['sort_order'])
|
||||
return render_template('components/badge_editor_badges.html', uploads=uploads, badges=badges)
|
||||
@@ -1,106 +0,0 @@
|
||||
from flask import (
|
||||
Blueprint, render_template, request, redirect, url_for,
|
||||
flash
|
||||
)
|
||||
from .users import get_active_user, is_logged_in
|
||||
from ..models import Users, PasswordResetLinks, MOTD
|
||||
from ..constants import InfoboxKind, MOTD_BANNED_TAGS
|
||||
from ..lib.babycode import babycode_to_html, BABYCODE_VERSION
|
||||
from ..db import db
|
||||
import secrets
|
||||
import time
|
||||
|
||||
bp = Blueprint("mod", __name__, url_prefix = "/mod/")
|
||||
|
||||
@bp.before_request
|
||||
def _before_request():
|
||||
if not is_logged_in():
|
||||
return redirect(url_for("users.log_in"))
|
||||
|
||||
if not get_active_user().is_mod():
|
||||
return redirect(url_for("topics.all_topics"))
|
||||
|
||||
|
||||
@bp.get("/sort-topics")
|
||||
def sort_topics():
|
||||
topics = db.query("SELECT * FROM topics ORDER BY sort_order ASC")
|
||||
return render_template("mod/sort-topics.html", topics = topics)
|
||||
|
||||
|
||||
@bp.post("/sort-topics")
|
||||
def sort_topics_post():
|
||||
topics_list = request.form.getlist('topics[]')
|
||||
print(topics_list)
|
||||
with db.transaction():
|
||||
for new_order, topic_id in enumerate(topics_list):
|
||||
db.execute("UPDATE topics SET sort_order = ? WHERE id = ?", new_order, topic_id)
|
||||
|
||||
return redirect(url_for(".sort_topics"))
|
||||
|
||||
|
||||
@bp.get("/user-list")
|
||||
def user_list():
|
||||
users = Users.select()
|
||||
return render_template("mod/user-list.html", users = users)
|
||||
|
||||
|
||||
@bp.post("/reset-pass/<user_id>")
|
||||
def create_reset_pass(user_id):
|
||||
now = int(time.time())
|
||||
key = secrets.token_urlsafe(20)
|
||||
reset_link = PasswordResetLinks.create({
|
||||
'user_id': int(user_id),
|
||||
'expires_at': now + 24 * 60 * 60,
|
||||
'key': key,
|
||||
})
|
||||
|
||||
return redirect(url_for('users.reset_link_login', key=key))
|
||||
|
||||
|
||||
@bp.get('/panel')
|
||||
def panel():
|
||||
return render_template('mod/panel.html')
|
||||
|
||||
|
||||
@bp.get('/motd')
|
||||
def motd_editor():
|
||||
current = MOTD.get_all()[0] if MOTD.has_motd() else None
|
||||
return render_template('mod/motd.html', current=current)
|
||||
|
||||
|
||||
@bp.post('/motd')
|
||||
def motd_editor_form():
|
||||
orig_body = request.form.get('body', default='')
|
||||
title = request.form.get('title', default='')
|
||||
data = {
|
||||
'title': title,
|
||||
'body_original_markup': orig_body,
|
||||
'body_rendered': babycode_to_html(orig_body, banned_tags=MOTD_BANNED_TAGS).result,
|
||||
'format_version': BABYCODE_VERSION,
|
||||
'edited_at': int(time.time()),
|
||||
}
|
||||
|
||||
if MOTD.has_motd():
|
||||
motd = MOTD.get_all()[0]
|
||||
motd.update(data)
|
||||
message = 'MOTD updated.'
|
||||
else:
|
||||
data['created_at'] = int(time.time())
|
||||
data['user_id'] = get_active_user().id
|
||||
motd = MOTD.create(data)
|
||||
message = 'MOTD created.'
|
||||
|
||||
flash(message, InfoboxKind.INFO)
|
||||
return redirect(url_for('.motd_editor'))
|
||||
|
||||
|
||||
@bp.post('/motd/delete')
|
||||
def motd_delete():
|
||||
if not MOTD.has_motd():
|
||||
flash('No MOTD to delete.', InfoboxKind.WARN)
|
||||
return redirect(url_for('.motd_editor'))
|
||||
|
||||
current = MOTD.get_all()[0]
|
||||
current.delete()
|
||||
flash('MOTD deleted.', InfoboxKind.INFO)
|
||||
return redirect(url_for('.motd_editor'))
|
||||
@@ -1,153 +0,0 @@
|
||||
from flask import (
|
||||
Blueprint, redirect, url_for, flash, render_template, request
|
||||
)
|
||||
from .users import login_required, get_active_user
|
||||
from ..lib.babycode import babycode_to_html, babycode_to_rssxml, BABYCODE_VERSION
|
||||
from ..constants import InfoboxKind
|
||||
from ..db import db
|
||||
from ..models import Posts, PostHistory, Threads, Topics, Mentions
|
||||
|
||||
bp = Blueprint("posts", __name__, url_prefix = "/post")
|
||||
|
||||
|
||||
def create_post(thread_id, user_id, content, markup_language="babycode"):
|
||||
parsed_content = babycode_to_html(content)
|
||||
parsed_rss = babycode_to_rssxml(content)
|
||||
with db.transaction():
|
||||
post = Posts.create({
|
||||
"thread_id": thread_id,
|
||||
"user_id": user_id,
|
||||
"current_revision_id": None,
|
||||
})
|
||||
|
||||
revision = PostHistory.create({
|
||||
"post_id": post.id,
|
||||
"content": parsed_content.result,
|
||||
"content_rss": parsed_rss,
|
||||
"is_initial_revision": True,
|
||||
"original_markup": content,
|
||||
"markup_language": markup_language,
|
||||
"format_version": BABYCODE_VERSION,
|
||||
})
|
||||
|
||||
for mention in parsed_content.mentions:
|
||||
Mentions.create({
|
||||
'revision_id': revision.id,
|
||||
'mentioned_user_id': mention['mentioned_user_id'],
|
||||
'original_mention_text': mention['mention_text'],
|
||||
'start_index': mention['start'],
|
||||
'end_index': mention['end'],
|
||||
})
|
||||
|
||||
post.update({"current_revision_id": revision.id})
|
||||
return post
|
||||
|
||||
|
||||
def update_post(post_id, new_content, markup_language='babycode'):
|
||||
parsed_content = babycode_to_html(new_content)
|
||||
parsed_rss = babycode_to_rssxml(new_content)
|
||||
with db.transaction():
|
||||
post = Posts.find({'id': post_id})
|
||||
new_revision = PostHistory.create({
|
||||
'post_id': post.id,
|
||||
'content': parsed_content.result,
|
||||
"content_rss": parsed_rss,
|
||||
'is_initial_revision': False,
|
||||
'original_markup': new_content,
|
||||
'markup_language': markup_language,
|
||||
'format_version': BABYCODE_VERSION,
|
||||
})
|
||||
|
||||
for mention in parsed_content.mentions:
|
||||
Mentions.create({
|
||||
'revision_id': new_revision.id,
|
||||
'mentioned_user_id': mention['mentioned_user_id'],
|
||||
'original_mention_text': mention['mention_text'],
|
||||
'start_index': mention['start'],
|
||||
'end_index': mention['end'],
|
||||
})
|
||||
|
||||
post.update({'current_revision_id': new_revision.id})
|
||||
|
||||
|
||||
@bp.post("/<post_id>/delete")
|
||||
@login_required
|
||||
def delete(post_id):
|
||||
post = Posts.find({'id': post_id})
|
||||
if not post:
|
||||
abort(404)
|
||||
return
|
||||
|
||||
thread = Threads.find({'id': post.thread_id})
|
||||
user = get_active_user()
|
||||
if not user:
|
||||
return redirect(url_for('threads.thread', slug=thread.slug))
|
||||
|
||||
if user.is_mod() or post.user_id == user.id:
|
||||
post.delete()
|
||||
|
||||
post_count = Posts.count({
|
||||
'thread_id': thread.id,
|
||||
})
|
||||
|
||||
if post_count == 0:
|
||||
topic = Topics.find({
|
||||
'id': thread.topic_id,
|
||||
})
|
||||
thread.delete()
|
||||
flash('Thread deleted.', InfoboxKind.INFO)
|
||||
return redirect(url_for('topics.topic', slug=topic.slug))
|
||||
|
||||
flash('Post deleted.', InfoboxKind.INFO)
|
||||
|
||||
return redirect(url_for('threads.thread', slug=thread.slug))
|
||||
|
||||
|
||||
@bp.get("/<post_id>/edit")
|
||||
@login_required
|
||||
def edit(post_id):
|
||||
post = Posts.find({'id': post_id})
|
||||
if not post:
|
||||
abort(404)
|
||||
return
|
||||
|
||||
user = get_active_user()
|
||||
q = f"{Posts.FULL_POSTS_QUERY} WHERE posts.id = ?"
|
||||
editing_post = db.fetch_one(q, post_id)
|
||||
if not editing_post:
|
||||
abort(404)
|
||||
return
|
||||
if editing_post['user_id'] != user.id:
|
||||
return redirect(url_for('topics.all_topics'))
|
||||
|
||||
thread = Threads.find({'id': editing_post['thread_id']})
|
||||
|
||||
thread_predicate = f'{Posts.FULL_POSTS_QUERY} WHERE posts.thread_id = ?'
|
||||
|
||||
context_prev_q = f'{thread_predicate} AND posts.created_at < ? ORDER BY posts.created_at DESC LIMIT 2'
|
||||
context_next_q = f'{thread_predicate} AND posts.created_at > ? ORDER BY posts.created_at ASC LIMIT 2'
|
||||
prev_context = db.query(context_prev_q, thread.id, editing_post['created_at'])
|
||||
next_context = db.query(context_next_q, thread.id, editing_post['created_at'])
|
||||
|
||||
return render_template('posts/edit.html',
|
||||
editing_post = editing_post,
|
||||
thread = thread,
|
||||
prev_context = prev_context,
|
||||
next_context = next_context,
|
||||
)
|
||||
|
||||
|
||||
@bp.post("/<post_id>/edit")
|
||||
@login_required
|
||||
def edit_form(post_id):
|
||||
user = get_active_user()
|
||||
post = Posts.find({'id': post_id})
|
||||
if not post:
|
||||
abort(404)
|
||||
return
|
||||
if post.user_id != user.id:
|
||||
return redirect(url_for('topics.all_topics'))
|
||||
|
||||
update_post(post.id, request.form.get('new_content', default=''))
|
||||
thread = Threads.find({'id': post.thread_id})
|
||||
return redirect(url_for('threads.thread', slug=thread.slug, after=post.id, _anchor=f'post-{post.id}'))
|
||||
@@ -1,267 +0,0 @@
|
||||
from flask import (
|
||||
Blueprint, render_template, request, redirect, url_for, flash,
|
||||
abort, current_app,
|
||||
)
|
||||
from .users import login_required, mod_only, get_active_user, is_logged_in
|
||||
from ..db import db
|
||||
from ..models import Threads, Topics, Posts, Subscriptions, Reactions
|
||||
from ..constants import InfoboxKind
|
||||
from ..lib.render_atom import render_atom_template
|
||||
from .posts import create_post
|
||||
from slugify import slugify
|
||||
from app import cache
|
||||
import math
|
||||
import time
|
||||
|
||||
bp = Blueprint("threads", __name__, url_prefix = "/threads/")
|
||||
|
||||
|
||||
def get_post_url(post_id, _anchor=False, external=False):
|
||||
post = Posts.find({'id': post_id})
|
||||
if not post:
|
||||
return ""
|
||||
|
||||
thread = Threads.find({'id': post.thread_id})
|
||||
|
||||
anchor = None if not _anchor else f'post-{post_id}'
|
||||
|
||||
return url_for('threads.thread', slug=thread.slug, after=post_id, _external=external, _anchor=anchor)
|
||||
# if not _anchor:
|
||||
# return res
|
||||
|
||||
# return f"{res}#post-{post_id}"
|
||||
|
||||
|
||||
@bp.get("/<slug>")
|
||||
def thread(slug):
|
||||
POSTS_PER_PAGE = 10
|
||||
thread = Threads.find({"slug": slug})
|
||||
if not thread:
|
||||
abort(404)
|
||||
return
|
||||
|
||||
post_count = Posts.count({"thread_id": thread.id})
|
||||
page_count = max(math.ceil(post_count / POSTS_PER_PAGE), 1)
|
||||
|
||||
page = 1
|
||||
after = request.args.get("after", default=None)
|
||||
if after is not None:
|
||||
after_id = int(after)
|
||||
post_position = Posts.count([
|
||||
("thread_id", "=", thread.id),
|
||||
("id", "<=", after_id),
|
||||
])
|
||||
page = math.ceil((post_position) / POSTS_PER_PAGE)
|
||||
else:
|
||||
page = max(1, min(page_count, int(request.args.get("page", default = 1))))
|
||||
posts = thread.get_posts(POSTS_PER_PAGE, (page - 1) * POSTS_PER_PAGE)
|
||||
topic = Topics.find({"id": thread.topic_id})
|
||||
other_topics = Topics.select()
|
||||
|
||||
is_subscribed = False
|
||||
unread_count = None
|
||||
if is_logged_in():
|
||||
subscription = Subscriptions.find({
|
||||
'thread_id': thread.id,
|
||||
'user_id': get_active_user().id,
|
||||
})
|
||||
if subscription:
|
||||
unread_count = subscription.get_unread_count()
|
||||
if int(posts[-1]['created_at']) > int(subscription.last_seen):
|
||||
subscription.update({
|
||||
'last_seen': int(posts[-1]['created_at'])
|
||||
})
|
||||
is_subscribed = True
|
||||
|
||||
return render_template(
|
||||
"threads/thread.html",
|
||||
thread = thread,
|
||||
current_page = page,
|
||||
page_count = page_count,
|
||||
posts = posts,
|
||||
topic = topic,
|
||||
topics = other_topics,
|
||||
is_subscribed = is_subscribed,
|
||||
Reactions = Reactions,
|
||||
unread_count = unread_count,
|
||||
__feedlink = url_for('.thread_atom', slug=slug, _external=True),
|
||||
__feedtitle = f'replies to {thread.title}',
|
||||
)
|
||||
|
||||
|
||||
@bp.get("/<slug>/feed.atom")
|
||||
@cache.cached(timeout=5 * 60, unless=lambda: current_app.config.get('DEBUG', False))
|
||||
def thread_atom(slug):
|
||||
thread = Threads.find({"slug": slug})
|
||||
if not thread:
|
||||
abort(404) # TODO throw an atom friendly 404
|
||||
return
|
||||
|
||||
topic = Topics.find({'id': thread.topic_id})
|
||||
posts = thread.get_posts_rss()
|
||||
|
||||
return render_atom_template('threads/thread.atom', thread=thread, topic=topic, posts=posts, get_post_url=get_post_url)
|
||||
|
||||
|
||||
@bp.post("/<slug>")
|
||||
@login_required
|
||||
def reply(slug):
|
||||
thread = Threads.find({"slug": slug})
|
||||
if not thread:
|
||||
abort(404)
|
||||
return
|
||||
user = get_active_user()
|
||||
if user.is_guest():
|
||||
return redirect(url_for('.thread', slug=slug))
|
||||
if thread.locked() and not user.is_mod():
|
||||
return redirect(url_for('.thread', slug=slug))
|
||||
|
||||
post_content = request.form['post_content']
|
||||
post = create_post(thread.id, user.id, post_content)
|
||||
|
||||
subscription = Subscriptions.find({'user_id': user.id, 'thread_id': thread.id})
|
||||
|
||||
if subscription:
|
||||
subscription.update({'last_seen': int(time.time())})
|
||||
elif request.form.get('subscribe', default=None) == 'on':
|
||||
Subscriptions.create({'user_id': user.id, 'thread_id': thread.id, 'last_seen': int(time.time())})
|
||||
|
||||
return redirect(url_for(".thread", slug=slug, after=post.id, _anchor="latest-post"))
|
||||
|
||||
|
||||
@bp.get("/create")
|
||||
@login_required
|
||||
def create():
|
||||
all_topics = Topics.select()
|
||||
return render_template("threads/create.html", all_topics = all_topics)
|
||||
|
||||
|
||||
@bp.post("/create")
|
||||
@login_required
|
||||
def create_form():
|
||||
topic = Topics.find({"id": request.form['topic_id']})
|
||||
user = get_active_user()
|
||||
if not topic:
|
||||
flash('Invalid topic', InfoboxKind.ERROR)
|
||||
return redirect(url_for('.create'))
|
||||
|
||||
if topic.is_locked and not get_active_user().is_mod():
|
||||
flash(f'Topic "{topic.name}" is locked', InfoboxKind.ERROR)
|
||||
return redirect(url_for('.create'))
|
||||
|
||||
title = request.form['title'].strip()
|
||||
now = int(time.time())
|
||||
slug = f"{slugify(title)}-{now}"
|
||||
|
||||
post_content = request.form['initial_post']
|
||||
thread = Threads.create({
|
||||
"topic_id": topic.id,
|
||||
"user_id": user.id,
|
||||
"title": title,
|
||||
"slug": slug,
|
||||
"created_at": now,
|
||||
})
|
||||
post = create_post(thread.id, user.id, post_content)
|
||||
return redirect(url_for(".thread", slug = thread.slug))
|
||||
|
||||
|
||||
@bp.post("/<slug>/lock")
|
||||
@login_required
|
||||
def lock(slug):
|
||||
user = get_active_user()
|
||||
thread = Threads.find({'slug': slug})
|
||||
if not thread:
|
||||
abort(404)
|
||||
return
|
||||
if not ((thread.user_id == user.id) or user.is_mod()):
|
||||
return redirect(url_for('.thread', slug=slug))
|
||||
target_op = request.form.get('target_op')
|
||||
thread.update({
|
||||
'is_locked': target_op
|
||||
})
|
||||
return redirect(url_for('.thread', slug=slug))
|
||||
|
||||
|
||||
@bp.post("/<slug>/sticky")
|
||||
@login_required
|
||||
@mod_only(".thread", slug = lambda slug: slug)
|
||||
def sticky(slug):
|
||||
user = get_active_user()
|
||||
thread = Threads.find({'slug': slug})
|
||||
if not thread:
|
||||
abort(404)
|
||||
return
|
||||
if not ((thread.user_id == user.id) or user.is_mod()):
|
||||
return redirect(url_for('.thread', slug=slug))
|
||||
target_op = request.form.get('target_op')
|
||||
thread.update({
|
||||
'is_stickied': target_op
|
||||
})
|
||||
return redirect(url_for('.thread', slug=slug))
|
||||
|
||||
|
||||
@bp.post("/<slug>/move")
|
||||
@login_required
|
||||
@mod_only(".thread", slug = lambda slug: slug)
|
||||
def move(slug):
|
||||
user = get_active_user()
|
||||
|
||||
new_topic_id = request.form.get('new_topic_id', default=None)
|
||||
if new_topic_id is None:
|
||||
flash('Thread is already in this topic.', InfoboxKind.ERROR)
|
||||
return redirect(url_for('.thread', slug=slug))
|
||||
|
||||
new_topic = Topics.find({
|
||||
'id': new_topic_id
|
||||
})
|
||||
if not new_topic:
|
||||
return redirect(url_for('topics.all_topics'))
|
||||
thread = Threads.find({
|
||||
'slug': slug
|
||||
})
|
||||
if not thread:
|
||||
return redirect(url_for('topics.all_topics'))
|
||||
if new_topic.id == thread.topic_id:
|
||||
flash('Thread is already in this topic.', InfoboxKind.ERROR)
|
||||
return redirect(url_for('.thread', slug=slug))
|
||||
|
||||
old_topic = Topics.find({'id': thread.topic_id})
|
||||
thread.update({'topic_id': new_topic_id})
|
||||
flash(f'Topic moved from "{old_topic.name}" to "{new_topic.name}".', InfoboxKind.INFO)
|
||||
return redirect(url_for('.thread', slug=slug))
|
||||
|
||||
|
||||
@bp.post("/<slug>/subscribe")
|
||||
@login_required
|
||||
def subscribe(slug):
|
||||
user = get_active_user()
|
||||
thread = Threads.find({'slug': slug})
|
||||
if not thread:
|
||||
return redirect(url_for('topics.all_topics'))
|
||||
subscription = Subscriptions.find({
|
||||
'user_id': user.id,
|
||||
'thread_id': thread.id,
|
||||
})
|
||||
if request.form['subscribe'] == 'subscribe':
|
||||
if subscription:
|
||||
subscription.delete()
|
||||
Subscriptions.create({
|
||||
'user_id': user.id,
|
||||
'thread_id': thread.id,
|
||||
'last_seen': int(time.time()),
|
||||
})
|
||||
elif request.form['subscribe'] == 'unsubscribe':
|
||||
if not subscription:
|
||||
return redirect(url_for('.thread', slug=slug))
|
||||
subscription.delete()
|
||||
elif request.form['subscribe'] == 'read':
|
||||
if not subscription:
|
||||
return redirect(url_for('.thread', slug=slug))
|
||||
subscription.update({
|
||||
'last_seen': int(time.time())
|
||||
})
|
||||
last_visible_post = request.form.get('last_visible_post', default=None)
|
||||
if last_visible_post is not None:
|
||||
return redirect(url_for('.thread', slug=thread.slug, after=last_visible_post))
|
||||
else:
|
||||
return redirect(url_for('users.inbox', username=user.username))
|
||||
@@ -1,147 +0,0 @@
|
||||
from flask import (
|
||||
Blueprint, render_template, request, redirect, url_for, flash, session,
|
||||
abort, current_app
|
||||
)
|
||||
from .users import login_required, mod_only, get_active_user, is_logged_in
|
||||
from ..models import Users, Topics, Threads, Subscriptions
|
||||
from ..constants import InfoboxKind
|
||||
from ..lib.render_atom import render_atom_template
|
||||
from slugify import slugify
|
||||
from app import cache
|
||||
import time
|
||||
import math
|
||||
|
||||
bp = Blueprint("topics", __name__, url_prefix = "/topics/")
|
||||
|
||||
|
||||
@bp.get("/")
|
||||
def all_topics():
|
||||
return render_template("topics/topics.html", topic_list = Topics.get_list(), active_threads = Topics.get_active_threads())
|
||||
|
||||
|
||||
@bp.get("/create")
|
||||
@login_required
|
||||
@mod_only(".all_topics")
|
||||
def create():
|
||||
return render_template("topics/create.html")
|
||||
|
||||
|
||||
@bp.post("/create")
|
||||
@login_required
|
||||
@mod_only(".all_topics")
|
||||
def create_post():
|
||||
topic_name = request.form['name'].strip()
|
||||
now = int(time.time())
|
||||
slug = f"{slugify(topic_name)}-{now}"
|
||||
|
||||
topic_count = Topics.count()
|
||||
topic = Topics.create({
|
||||
"name": topic_name,
|
||||
"description": request.form['description'],
|
||||
"slug": slug,
|
||||
"sort_order": topic_count + 1,
|
||||
})
|
||||
|
||||
flash("Topic created.", InfoboxKind.INFO)
|
||||
return redirect(url_for("topics.topic", slug = slug))
|
||||
|
||||
|
||||
@bp.get("/<slug>")
|
||||
def topic(slug):
|
||||
THREADS_PER_PAGE = 10
|
||||
target_topic = Topics.find({
|
||||
"slug": slug
|
||||
})
|
||||
if not target_topic:
|
||||
abort(404)
|
||||
return
|
||||
|
||||
threads_count = Threads.count({
|
||||
"topic_id": target_topic.id
|
||||
})
|
||||
|
||||
sort_by = session.get('sort_by', default="activity")
|
||||
|
||||
page_count = max(math.ceil(threads_count / THREADS_PER_PAGE), 1)
|
||||
page = max(1, min(int(request.args.get('page', default=1)), page_count))
|
||||
|
||||
threads_list = target_topic.get_threads(THREADS_PER_PAGE, page, sort_by)
|
||||
subscriptions = {}
|
||||
if is_logged_in():
|
||||
for thread in threads_list:
|
||||
subscription = Subscriptions.find({
|
||||
'user_id': get_active_user().id,
|
||||
'thread_id': thread['id'],
|
||||
})
|
||||
if subscription:
|
||||
subscriptions[thread['id']] = subscription.get_unread_count()
|
||||
|
||||
return render_template(
|
||||
"topics/topic.html",
|
||||
threads_list = threads_list,
|
||||
subscriptions = subscriptions,
|
||||
topic = target_topic,
|
||||
current_page = page,
|
||||
page_count = page_count,
|
||||
__feedlink = url_for('.topic_atom', slug=slug, _external=True),
|
||||
__feedtitle = f'latest threads in {target_topic.name}',
|
||||
)
|
||||
|
||||
|
||||
@bp.get('/<slug>/feed.atom')
|
||||
@cache.cached(timeout=10 * 60, unless=lambda: current_app.config.get('DEBUG', False))
|
||||
def topic_atom(slug):
|
||||
target_topic = Topics.find({
|
||||
"slug": slug
|
||||
})
|
||||
if not target_topic:
|
||||
abort(404) # TODO throw an atom friendly 404
|
||||
return
|
||||
|
||||
threads_list = target_topic.get_threads_with_op_rss()
|
||||
|
||||
return render_atom_template('topics/topic.atom', threads_list=threads_list, target_topic=target_topic)
|
||||
|
||||
|
||||
@bp.get("/<slug>/edit")
|
||||
@login_required
|
||||
@mod_only(".topic", slug = lambda slug: slug)
|
||||
def edit(slug):
|
||||
topic = Topics.find({"slug": slug})
|
||||
if not topic:
|
||||
abort(404)
|
||||
return
|
||||
return render_template("topics/edit.html", topic=topic)
|
||||
|
||||
|
||||
@bp.post("/<slug>/edit")
|
||||
@login_required
|
||||
@mod_only(".topic", slug = lambda slug: slug)
|
||||
def edit_post(slug):
|
||||
topic = Topics.find({"slug": slug})
|
||||
if not topic:
|
||||
abort(404)
|
||||
return
|
||||
|
||||
topic.update({
|
||||
"name": request.form.get('name', default = topic.name).strip(),
|
||||
"description": request.form.get('description', default = topic.description),
|
||||
"is_locked": int(request.form.get("is_locked", default = topic.is_locked)),
|
||||
})
|
||||
|
||||
return redirect(url_for("topics.topic", slug=slug))
|
||||
|
||||
|
||||
@bp.post("/<slug>/delete")
|
||||
@login_required
|
||||
@mod_only(".topic", slug = lambda slug: slug)
|
||||
def delete(slug):
|
||||
topic = Topics.find({"slug": slug})
|
||||
if not topic:
|
||||
abort(404)
|
||||
return
|
||||
|
||||
topic.delete()
|
||||
|
||||
flash("Topic deleted.", InfoboxKind.INFO)
|
||||
return redirect(url_for("topics.all_topics"))
|
||||
1003
app/routes/users.py
1003
app/routes/users.py
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user