from app import app, db, V5Config from app.models.attachment import Attachment from app.models.comment import Comment from app.models.forum import Forum from app.models.post import Post from app.models.program import Program from app.models.thread import Thread from app.models.topic import Topic from app.models.user import Member from app.utils.render import render from app.utils.check_csrf import check_csrf from app.utils.priv_required import priv_required from app.forms.forum import CommentEditForm, AnonymousCommentEditForm, TopicEditForm from app.forms.post import MovePost, SearchThread, MergePost from wtforms import BooleanField from urllib.parse import urlparse from flask import redirect, url_for, abort, request, flash from flask_login import login_required, current_user from sqlalchemy import text, and_ from datetime import timedelta @app.route('/post/editer/', methods=['GET','POST']) @login_required def edit_post(postid): # FIXME: Maybe not safe referrer = urlparse(request.args.get('r', default = '/', type = str)).path print(referrer) p = Post.query.get_or_404(postid) # Check permissions if not current_user.can_edit_post(p): abort(403) if isinstance(p, Comment): base = CommentEditForm comment = p elif isinstance(p, Topic): base = TopicEditForm comment = p.thread.top_comment else: abort(404) class TheForm(base): pass for a in comment.attachments: setattr(TheForm, f'a{a.id}', BooleanField(f'a{a.id}')) setattr(TheForm, 'attachment_list', { f'a{a.id}': a for a in comment.attachments }) form = TheForm() if isinstance(p, Topic): forums = sorted(Forum.query.all(), key=lambda f: f.url) forums = [f for f in forums if current_user.can_post_in_forum(f)] form.forum.choices = [(f.url, f"{f.url}: {f.name}") for f in forums] if form.validate_on_submit(): comment.text = form.message.data # Remove attachments for id, a in form.attachment_list.items(): if form[id].data: a.delete() # Add new attachments attachments = [] for file in form.attachments.data: if file.filename != "": a = Attachment(file, comment) attachments.append((a, file)) db.session.add(a) comment.touch() db.session.add(comment) if isinstance(p, Topic): p.title = form.title.data p.summary = form.summary.data # If there's a thumbnail, set it if form.thumbnail.data: p.thumbnail = comment.attachments[int(form.thumbnail.data)-1] else: p.thumbnail = None f = Forum.query.filter_by(url=form.forum.data).first_or_404() if current_user.can_post_in_forum(f): p.forum = f db.session.merge(p) db.session.commit() for a, file in attachments: a.set_file(file) flash('Modifications enregistrées', 'ok') admin_msg = "[admin] " if current_user != p.author else "" app.v5logger.info(f"{admin_msg}<{current_user.name}> has edited the post #{p.id}") # Determine topic URL now, in case forum was changed if isinstance(p, Topic): return redirect(url_for('forum_topic', f=p.forum, page=(p,1))) else: return redirect(referrer) # Non-submitted form if isinstance(p, Comment): form.message.data = p.text return render('forum/edit_comment.html', comment=p, form=form) elif isinstance(p, Topic): form.message.data = p.thread.top_comment.text form.title.data = p.title form.forum.data = p.forum.url form.summary.data = p.summary return render('forum/edit_topic.html', t=p, form=form) @app.route('/post/supprimer/', methods=['GET','POST']) @login_required @check_csrf def delete_post(postid): next_page = request.referrer p = Post.query.get_or_404(postid) xp = -1 if not current_user.can_delete_post(p): abort(403) # Is a penalty deletion is_penalty = request.args.get('penalty') == 'True' \ and current_user.priv('delete.posts') # Users who need to have their trophies updated authors = set() # When deleting topics, return to forum page if isinstance(p, Topic): next_page = url_for('forum_page', f=p.forum) xp = -2 for comment in p.thread.comments: if isinstance(comment.author, Member): comment.author.add_xp(-1) db.session.merge(comment.author) authors.add(comment.author) if isinstance(p.author, Member): factor = 3 if is_penalty else 1 p.author.add_xp(xp * factor) db.session.merge(p.author) authors.add(p.author) admin_msg = "[admin] " if current_user != p.author else "" p.delete() db.session.commit() for author in authors: author.update_trophies("new-post") flash("Le contenu a été supprimé", 'ok') penalty_msg = " (with penalty)" if is_penalty else "" app.v5logger.info(f"{admin_msg}<{current_user.name}> has deleted the post #{p.id}{penalty_msg}") return redirect(next_page) @app.route('/post/entete/', methods=['GET']) @login_required @check_csrf def set_post_topcomment(postid): comment = Post.query.get_or_404(postid) if current_user.can_set_topcomment(comment): comment.thread.top_comment = comment db.session.add(comment.thread) db.session.commit() flash("Le post a été défini comme nouvel en-tête", 'ok') admin_msg = "[admin] " if current_user != comment.author else "" app.v5logger.info(f"{admin_msg}<{current_user.name}> has set a new top comment on thread #{comment.thread.id}") return redirect(request.referrer) @app.route('/post/deplacer/', methods=['GET', 'POST']) @login_required def move_post(postid): comment = Post.query.get_or_404(postid) if not current_user.can_edit_post(comment): abort(403) if not isinstance(comment, Comment): flash("Vous ne pouvez pas déplacer un message principal", 'error') abort(403) move_form = MovePost(prefix="move_") search_form = SearchThread(prefix="thread_") # There is a bug with validate_on_submit keyword = search_form.name.data if search_form.search.data else "" # Get 10 last corresponding threads # TODO: add support for every MainPost req = text("""SELECT thread.id, topic.title FROM thread INNER JOIN topic ON topic.thread_id = thread.id WHERE lower(topic.title) LIKE lower(:keyword) ORDER BY thread.id DESC LIMIT 10""") threads = list(db.session.execute(req, {'keyword': '%'+keyword+'%'})) move_form.thread.choices = [(t[0], f"{t[1]}") for t in threads] if move_form.validate_on_submit(): thread = Thread.query.get_or_404(move_form.thread.data) owner_post = thread.owner_post if isinstance(owner_post, Topic): t = owner_post if not current_user.can_access_forum(t.forum): abort(403) comment.thread = thread db.session.add(comment) db.session.commit() flash("Le topic a été déplacé", 'ok') admin_msg = "[admin] " if current_user != comment.author else "" app.v5logger.info(f"{admin_msg}<{current_user.name}> has moved the comment #{comment.id} to thread #{thread.id}") return redirect(url_for('forum_topic', f=t.forum, page=(t,1))) return render('post/move_post.html', comment=comment, search_form=search_form, move_form=move_form) @app.route('/post/verrouiller/', methods=['GET']) @priv_required("lock.threads") @check_csrf def lock_thread(postid): post = Post.query.get_or_404(postid) if not post.is_metacontent: flash("Vous ne pouvez pas verrouiller ce contenu (n'est pas de type metacontenu)", 'error') abort(403) post.thread.locked = not post.thread.locked db.session.add(post.thread) db.session.commit() if post.thread.locked: flash(f"Le thread a été verrouillé", 'ok') app.v5logger.info(f"[admin] <{current_user.name}> has locked the thread #{post.thread.id}") else: flash(f"Le thread a été déverrouillé", 'ok') app.v5logger.info(f"[admin] <{current_user.name}> has unlocked the thread #{post.thread.id}") return redirect(request.referrer) @app.route('/post/fusionner/', methods=['GET', 'POST']) @login_required def merge_post(postid): comment = Comment.query.get_or_404(postid) # Get the posts from the same user in the same topic that are separated # by less than V5Config.MERGE_AGE_THRESHOLD compatible_comments = Comment.query.filter(and_( Comment.thread_id == comment.thread_id, and_( Post.author_id == comment.author_id, and_( Post.date_created > comment.date_created, Post.date_created <= comment.date_created + timedelta(0, V5Config.MERGE_AGE_THRESHOLD) ) ) )) merge_form = MergePost() merge_form.post.choices = [(t.id, f"{t.text[:30]}[…]") for t in list(compatible_comments)] if merge_form.validate_on_submit(): merge_comment = Comment.query.filter_by(id=merge_form.post.data).first_or_404() comment.text += f"\n\n---\n\n{merge_comment.text}" # Change the modification date only if the other post is more recent if merge_comment.date_created > comment.date_modified: comment.date_modified = merge_comment.date_created if merge_comment.is_top_comment: comment.thread.set_top_comment(comment) db.session.add(comment) merge_comment.delete() db.session.commit() if isinstance(comment.thread.owner_post, Topic): return redirect(url_for('forum_topic', f=comment.thread.owner_post.forum, page=[comment.thread.owner_post, -1])) elif isinstance(comment.thread.owner_post, Program): return redirect(url_for('program_view', page=[comment.thread.owner_post, -1])) return render('post/merge_post.html', comment=comment, merge_form=merge_form)