PCv5/app/routes/posts/edit.py

293 lines
10 KiB
Python

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/<int:postid>', 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/<int:postid>', 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/<int:postid>', 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/<int:postid>', 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/<int:postid>', 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/<int:postid>', 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)