Remplacer les liens d'actions des commentaires par un menu contextuel #100
|
@ -16,11 +16,11 @@
|
|||
# publish.shared-files
|
||||
#
|
||||
# Moderation:
|
||||
# edit.posts
|
||||
# edit.posts (includes top comment selection)
|
||||
# edit.tests
|
||||
# edit.accounts
|
||||
# edit.trophies
|
||||
# delete.posts
|
||||
# delete.posts (includes triple XP removal)
|
||||
# delete.tests
|
||||
# delete.accounts
|
||||
# delete.shared-files
|
||||
|
|
|
@ -227,6 +227,17 @@ class Member(User):
|
|||
return self.can_access_post(post) and \
|
||||
((post.author == self) or self.priv("delete.posts"))
|
||||
|
||||
def can_punish_post(self, post):
|
||||
"""Whether this member can delete the post with penalty."""
|
||||
return self.can_access_post(post) and self.priv("delete.posts")
|
||||
|
||||
def can_set_topcomment(self, comment):
|
||||
"""Whether this member can designate the comment as top comment."""
|
||||
if comment.type != "comment":
|
||||
return False
|
||||
post = comment.thread.owner_post
|
||||
return self.can_edit_post(post) and (comment.author == post.author)
|
||||
|
||||
def update(self, **data):
|
||||
"""
|
||||
Update all or part of the user's metadata. The [data] dictionary
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
from app import app, db
|
||||
from app.models.user import Member
|
||||
from app.models.post import Post
|
||||
from app.models.attachment import Attachment
|
||||
from app.models.topic import Topic
|
||||
from app.models.program import Program
|
||||
from app.utils.render import render
|
||||
from app.utils.check_csrf import check_csrf
|
||||
from app.forms.forum import CommentEditForm, AnonymousCommentEditForm
|
||||
|
@ -64,11 +67,34 @@ def edit_post(postid):
|
|||
@login_required
|
||||
@check_csrf
|
||||
def delete_post(postid):
|
||||
next_page = request.referrer
|
||||
p = Post.query.filter_by(id=postid).first_or_404()
|
||||
|
||||
if current_user.is_anonymous or not current_user.can_delete_post(p):
|
||||
abort(403)
|
||||
|
||||
# When deleting topics, return to forum page
|
||||
if isinstance(p, Topic):
|
||||
next_page = url_for('forum_page', f=p.forum)
|
||||
|
||||
if isinstance(p.author, Member):
|
||||
amount = -3 if request.args.get('penalty') == 'True' else -1
|
||||
p.author.add_xp(amount)
|
||||
db.session.add(p.author)
|
||||
|
||||
p.delete()
|
||||
db.session.commit()
|
||||
return redirect(next_page)
|
||||
|
||||
@app.route('/post/entete/<int:postid>', methods=['GET'])
|
||||
@login_required
|
||||
@check_csrf
|
||||
def set_post_topcomment(postid):
|
||||
comment = Post.query.filter_by(id=postid).first_or_404()
|
||||
|
||||
if current_user.can_set_topcomment(comment):
|
||||
comment.thread.top_comment = comment
|
||||
db.session.add(comment.thread)
|
||||
db.session.commit()
|
||||
|
||||
return redirect(request.referrer)
|
||||
|
|
|
@ -115,28 +115,73 @@ table.thread td.author {
|
|||
table.thread td {
|
||||
vertical-align: top;
|
||||
}
|
||||
table.thread td.message {
|
||||
padding-top: 8px;
|
||||
}
|
||||
table.thread.topcomment td.message {
|
||||
padding-top: 0;
|
||||
}
|
||||
table.thread td.message > *:nth-child(2) {
|
||||
margin-top: 0;
|
||||
}
|
||||
table.thread td.message img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
table.thread div.info {
|
||||
float: right;
|
||||
text-align: right;
|
||||
opacity: 0.7;
|
||||
padding-top: 8px;
|
||||
margin-left: 16px;
|
||||
table.thread .topcomment-placeholder div {
|
||||
font-style: italic;
|
||||
opacity: 0.5;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
table.thread div.info {
|
||||
text-align: right;
|
||||
position: relative;
|
||||
}
|
||||
table.thread.topcomment div.info {
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
table.thread:not(.topcomment) div.info {
|
||||
float: right;
|
||||
}
|
||||
table.thread div.info > * {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
table.thread div.info summary {
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
table.thread .context-menu {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 100%;
|
||||
margin: 0;
|
||||
padding: 4px 0;
|
||||
box-shadow: var(--shadow);
|
||||
border-radius: 4px;
|
||||
transition: none;
|
||||
|
||||
background: var(--background);
|
||||
z-index: 2;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
table.thread .context-menu a {
|
||||
display: block;
|
||||
padding: 4px 8px;
|
||||
text-align: center;
|
||||
color: inherit;
|
||||
}
|
||||
table.thread .context-menu a:hover {
|
||||
background: var(--background-light);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1199px) {
|
||||
table.thread div.info {
|
||||
float: none;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
margin-left: 0;
|
||||
}
|
||||
table.thread div.info > *:not(:last-child):after {
|
||||
content: '·';
|
||||
margin: 0 4px;
|
||||
}
|
||||
table.thread td.author {
|
||||
/* Includes padding */
|
||||
|
|
|
@ -108,6 +108,13 @@ footer {
|
|||
--border-xp: 1px solid #d03333;
|
||||
}
|
||||
|
||||
.context-menu {
|
||||
--background: #1d2326;
|
||||
--shadow: 0 0 12px -9px #000000;
|
||||
--border: #404040;
|
||||
--background-light: #262c2f;
|
||||
}
|
||||
|
||||
table {
|
||||
--border: #404040;
|
||||
}
|
||||
|
|
|
@ -102,6 +102,13 @@ footer {
|
|||
--border-xp: 1px solid #be1818;
|
||||
}
|
||||
|
||||
.context-menu {
|
||||
--background: #ffffff;
|
||||
--shadow: 0 0 12px -9px #000000;
|
||||
--border: #d0d0d0;
|
||||
--background-light: #f0f0f0;
|
||||
}
|
||||
|
||||
div.pagination {
|
||||
font-size: 14px;
|
||||
margin: 13px;
|
||||
|
@ -129,4 +136,4 @@ div.editor-toolbar {
|
|||
--separator: #aaa2a2;
|
||||
--text-disabled: #c0c0c0;
|
||||
--text: #515151;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
table {
|
||||
--border: #d8d8d8;
|
||||
}
|
||||
table tr:nth-child(even) {
|
||||
table tr:nth-child(odd) {
|
||||
--background: rgba(0, 0, 0, .1);
|
||||
}
|
||||
table th {
|
||||
|
@ -110,6 +110,13 @@ footer {
|
|||
--border-xp: 1px solid #d03333;
|
||||
}
|
||||
|
||||
.context-menu {
|
||||
--background: #ffffff;
|
||||
--shadow: 0 0 12px -9px #000000;
|
||||
--border: #d0d0d0;
|
||||
--background-light: #f0f0f0;
|
||||
}
|
||||
|
||||
div.editor-toolbar, div.CodeMirror {
|
||||
--border: #c0c0c0;
|
||||
--background-light: #d9d9d9;
|
||||
|
|
|
@ -11,3 +11,17 @@ function getCookie(name) {
|
|||
if( end == -1 ) end = document.cookie.length;
|
||||
return unescape( document.cookie.substring( debut+name.length+1, end ) );
|
||||
}
|
||||
|
||||
/* Automatically close context menus when clicking out of them */
|
||||
function closeContextMenus(e) {
|
||||
document.querySelectorAll('details[open]>.context-menu').forEach(menu => {
|
||||
if(!menu.contains(event.target)) {
|
||||
menu.parentElement.open = false;
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
(function(){
|
||||
window.addEventListener("click", closeContextMenus);
|
||||
})();
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
{% import "widgets/thread.html" as widget_thread with context %}
|
||||
{% import "widgets/user.html" as widget_user %}
|
||||
{% import "widgets/pagination.html" as widget_pagination with context %}
|
||||
{% import "widgets/attachments.html" as widget_attachments %}
|
||||
|
||||
{% block title %}
|
||||
<a href='/forum'>Forum de Planète Casio</a> » <a href="{{ url_for('forum_page', f=t.forum) }}">{{ t.forum.name }}</a> » <h1>{{ t.title }}</h1>
|
||||
|
@ -11,7 +12,15 @@
|
|||
{% block content %}
|
||||
<section>
|
||||
<h1>{{ t.title }}</h1>
|
||||
{{ widget_thread.thread([t.thread.top_comment], None) }}
|
||||
|
||||
{% call widget_thread.thread_leader(t.thread.top_comment) %}
|
||||
<div class="info">
|
||||
<div>Posté le {{ t.date_created | dyndate }}</div>
|
||||
{{ widget_thread.post_actions(t) }}
|
||||
</div>
|
||||
{{ t.thread.top_comment.text | md }}
|
||||
{{ widget_attachments.attachments(t.thread.top_comment) }}
|
||||
{% endcall %}
|
||||
|
||||
{{ widget_pagination.paginate(comments, 'forum_topic', t, {'f': t.forum}) }}
|
||||
|
||||
|
|
|
@ -1,30 +1,68 @@
|
|||
{% import "widgets/user.html" as widget_user %}
|
||||
{% import "widgets/attachments.html" as widget_attachments %}
|
||||
|
||||
{% macro thread(comments, top_comment) %}
|
||||
<table class="thread {{ 'topcomment' if top_comment == None else ''}} ">
|
||||
{# Post actions: this widget expands to a context menu with actions controlling
|
||||
a post, supporting different types of posts. #}
|
||||
{% macro post_actions(post) %}
|
||||
{# TODO (Guest edit): determine permissions in post_actions widget #}
|
||||
|
||||
{% set auth = current_user.is_authenticated %}
|
||||
{% set can_edit = auth and current_user.can_edit_post(post) %}
|
||||
{% set can_delete = auth and current_user.can_delete_post(post) %}
|
||||
{% set can_punish = auth and current_user.can_punish_post(post) %}
|
||||
{% set can_topcomm = auth and current_user.can_set_topcomment(post) %}
|
||||
|
||||
{% if post.type == "topic" %}
|
||||
{% set suffix = " le topic" %}
|
||||
{% elif post.type == "program" %}
|
||||
{% set suffix = " le programme" %}
|
||||
{% endif %}
|
||||
|
||||
{% if can_edit or can_delete or can_punish or can_topcomm %}
|
||||
<details>
|
||||
<summary><b>⋮</b></summary>
|
||||
<div class='context-menu'>
|
||||
{% if can_edit %}
|
||||
<a href="{{ url_for('edit_post', postid=post.id, r=request.path) }}">Modifier{{ suffix }}</a>
|
||||
{% endif %}
|
||||
|
||||
{% if can_punish %}
|
||||
<a href="{{ url_for('delete_post', postid=post.id, penalty=False, csrf_token=csrf_token()) }}" onclick="return confirm('Le post sera supprimé.')">Supprimer{{ suffix }} (normal)</a>
|
||||
<a href="{{ url_for('delete_post', postid=post.id, penalty=True, csrf_token=csrf_token()) }}" onclick="return confirm('Le post sera supprimé avec pénalité d\'XP.')">Supprimer{{ suffix }} (pénalité)</a>
|
||||
{% elif can_delete %}
|
||||
<a href="{{ url_for('delete_post', postid=post.id, penalty=False, csrf_token=csrf_token()) }}" onclick="return confirm('Le post sera supprimé !')">Supprimer{{ suffix }}</a>
|
||||
{% endif %}
|
||||
|
||||
{% if can_topcomm %}
|
||||
<a href="{{ url_for('set_post_topcomment', postid=post.id, csrf_token=csrf_token()) }}">Utiliser comme en-tête</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</details>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{# Thread widget: this widget expands to a table that shows a list of comments
|
||||
from a thread, along with message controls.
|
||||
|
||||
comments: List of comments to render
|
||||
top_comment: Thread's top comment (will be elided if encountered) #}
|
||||
|
||||
{% macro thread(comments, top_comment, owner=None) %}
|
||||
<table class="thread">
|
||||
{% for c in comments %}
|
||||
<tr id="{{ c.id }}">
|
||||
{% if c != top_comment %}
|
||||
<tr id="{{ c.id }}">
|
||||
<td class="author">{{ widget_user.profile(c.author) }}</td>
|
||||
<td class="message">
|
||||
<div class="info">
|
||||
<div>Posté le {{ c.date_created|dyndate }}</div>
|
||||
<div>Posté le <a href="{{ request.path }}#{{ c.id }}">{{ c.date_created | dyndate }}</a></div>
|
||||
{% if c.date_created != c.date_modified %}
|
||||
<div>Modifié le {{ c.date_modified|dyndate }}</div>
|
||||
{% endif %}
|
||||
<div><a href="{{ request.path }}#{{ c.id }}">Permalien</a></div>
|
||||
{# TODO: Let guests edit their posts #}
|
||||
{% if current_user.is_authenticated and current_user.can_edit_post(c) %}
|
||||
<div><a href="{{ url_for('edit_post', postid=c.id, r=request.path) }}">Modifier</a></div>
|
||||
{% endif %}
|
||||
{% if current_user.is_authenticated and current_user.can_delete_post(c) %}
|
||||
<div><a href="{{ url_for('delete_post', postid=c.id, csrf_token=csrf_token()) }}" onclick="return confirm('Le message sera supprimé')">Supprimer</a></div>
|
||||
<div>Modifié le <a href="{{ request.path }}#{{ c.id }}">{{ c.date_modified | dyndate }}</a></div>
|
||||
{% endif %}
|
||||
{{ post_actions(c) }}
|
||||
</div>
|
||||
|
||||
{{ c.text|md }}
|
||||
|
||||
{{ widget_attachments.attachments(c) }}
|
||||
|
||||
{% if c.author.signature %}
|
||||
|
@ -32,10 +70,30 @@
|
|||
{{ c.author.signature|md }}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% elif loop.index0 != 0 %}
|
||||
<div>Ce message est le top comment</div>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% elif loop.index0 != 0 %}
|
||||
<tr class="topcomment-placeholder">
|
||||
<td></td>
|
||||
<td><div>Le commentaire à cet endroit est actuellement utilisé comme en-tête.</div></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endmacro %}
|
||||
|
||||
{# Thread leader widget: this widget expands to a single-message thread which
|
||||
can show more text when called. This is intended for programs and similar
|
||||
objects which display metadata before description and comments.
|
||||
|
||||
leader: Posts's top comment (actual rendering is delegated to caller) #}
|
||||
|
||||
{% macro thread_leader(leader) %}
|
||||
<table class="thread topcomment">
|
||||
{# Empty line to get normal background (instead of alternate one) #}
|
||||
<tr></tr>
|
||||
<tr id="{{ leader.id }}">
|
||||
<td class="author">{{ widget_user.profile(leader.author) }}</td>
|
||||
<td class="message">{{ caller() }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endmacro %}
|
||||
|
|
Loading…
Reference in New Issue