From 6a8aab0d67e3adf3b28032215e25c67716df9a6e Mon Sep 17 00:00:00 2001 From: Lephe Date: Sat, 10 Jul 2021 21:43:41 +0200 Subject: [PATCH 1/5] forum: replace comment action links with contextual menu The menu works with HTML/CSS only, and JS support will also allow closing it by clicking outside of it (instead of closing allow when clicking on the menu icon again). --- app/data/groups.yaml | 2 +- app/static/css/table.css | 52 +++++++++++++++++----- app/static/css/themes/FK_dark_theme.css | 7 +++ app/static/css/themes/Tituya_v43_theme.css | 9 +++- app/static/css/themes/default_theme.css | 7 +++ app/static/scripts/pc-utils.js | 14 ++++++ app/templates/widgets/thread.html | 30 +++++++++---- 7 files changed, 100 insertions(+), 21 deletions(-) diff --git a/app/data/groups.yaml b/app/data/groups.yaml index 6f5d93d..79fd3b0 100644 --- a/app/data/groups.yaml +++ b/app/data/groups.yaml @@ -20,7 +20,7 @@ # edit.tests # edit.accounts # edit.trophies -# delete.posts +# delete.posts (includes triple XP removal) # delete.tests # delete.accounts # delete.shared-files diff --git a/app/static/css/table.css b/app/static/css/table.css index 092b97a..6683524 100644 --- a/app/static/css/table.css +++ b/app/static/css/table.css @@ -115,6 +115,12 @@ table.thread td.author { table.thread td { vertical-align: top; } +table.thread td.message { + padding-top: 8px; +} +table.thread td.message > *:nth-child(2) { + margin-top: 0; +} table.thread td.message img { max-width: 100%; } @@ -122,21 +128,45 @@ table.thread td.message img { table.thread div.info { float: right; text-align: right; - opacity: 0.7; - padding-top: 8px; - margin-left: 16px; + position: relative; } +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 */ diff --git a/app/static/css/themes/FK_dark_theme.css b/app/static/css/themes/FK_dark_theme.css index de4393d..2769142 100644 --- a/app/static/css/themes/FK_dark_theme.css +++ b/app/static/css/themes/FK_dark_theme.css @@ -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; } diff --git a/app/static/css/themes/Tituya_v43_theme.css b/app/static/css/themes/Tituya_v43_theme.css index 1d6fcb3..de83f15 100644 --- a/app/static/css/themes/Tituya_v43_theme.css +++ b/app/static/css/themes/Tituya_v43_theme.css @@ -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; -} \ No newline at end of file +} diff --git a/app/static/css/themes/default_theme.css b/app/static/css/themes/default_theme.css index e4d0972..5c74f31 100644 --- a/app/static/css/themes/default_theme.css +++ b/app/static/css/themes/default_theme.css @@ -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; diff --git a/app/static/scripts/pc-utils.js b/app/static/scripts/pc-utils.js index 16c7619..98948d4 100644 --- a/app/static/scripts/pc-utils.js +++ b/app/static/scripts/pc-utils.js @@ -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); +})(); diff --git a/app/templates/widgets/thread.html b/app/templates/widgets/thread.html index 9635acf..e27e8d9 100644 --- a/app/templates/widgets/thread.html +++ b/app/templates/widgets/thread.html @@ -9,17 +9,31 @@ {{ widget_user.profile(c.author) }}
-
Posté le {{ c.date_created|dyndate }}
+
Posté le {{ c.date_created | dyndate }}
{% if c.date_created != c.date_modified %} -
Modifié le {{ c.date_modified|dyndate }}
+
Modifié le {{ c.date_modified | dyndate }}
{% endif %} -
Permalien
+ {# TODO: Let guests edit their posts #} - {% if current_user.is_authenticated and current_user.can_edit_post(c) %} -
Modifier
- {% endif %} - {% if current_user.is_authenticated and current_user.can_delete_post(c) %} -
Supprimer
+ {% set can_edit = current_user.is_authenticated and current_user.can_edit_post(c) %} + {% set can_delete = current_user.is_authenticated and current_user.can_delete_post(c) %} + {% set can_punish = current_user.is_authenticated and current_user.priv("delete.posts") %} + + {% if can_edit or can_delete or can_punish %} +
+ +
+ {% if can_edit %} + Modifier + {% endif %} + {% if can_punish %} + Supprimer (normal) + Supprimer (pénalité) + {% elif can_delete %} + Supprimer + {% endif %} +
+
{% endif %}
-- 2.45.0 From ad43262bfbd308221692ad3998a19f24fefb9467 Mon Sep 17 00:00:00 2001 From: Lephe Date: Sun, 11 Jul 2021 11:00:24 +0200 Subject: [PATCH 2/5] forum: decrease XP when deleting posts (+penalty) --- app/routes/posts/edit.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/routes/posts/edit.py b/app/routes/posts/edit.py index 3ee2f2b..c1aee89 100644 --- a/app/routes/posts/edit.py +++ b/app/routes/posts/edit.py @@ -1,4 +1,5 @@ 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.utils.render import render @@ -69,6 +70,11 @@ def delete_post(postid): if current_user.is_anonymous or not current_user.can_delete_post(p): abort(403) + 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(request.referrer) -- 2.45.0 From a4d1e7fdeb781af0d47f074615445386f3e4f6cc Mon Sep 17 00:00:00 2001 From: Lephe Date: Mon, 12 Jul 2021 15:18:54 +0200 Subject: [PATCH 3/5] forum: minor improvements to thread display --- app/static/css/table.css | 10 +++++++++- app/static/css/themes/default_theme.css | 2 +- app/templates/widgets/thread.html | 8 +++++++- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/app/static/css/table.css b/app/static/css/table.css index 6683524..691920f 100644 --- a/app/static/css/table.css +++ b/app/static/css/table.css @@ -118,6 +118,9 @@ table.thread td { 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; } @@ -126,10 +129,15 @@ table.thread td.message img { } table.thread div.info { - float: right; 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; diff --git a/app/static/css/themes/default_theme.css b/app/static/css/themes/default_theme.css index 5c74f31..1c04ac1 100644 --- a/app/static/css/themes/default_theme.css +++ b/app/static/css/themes/default_theme.css @@ -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 { diff --git a/app/templates/widgets/thread.html b/app/templates/widgets/thread.html index e27e8d9..47ec9f0 100644 --- a/app/templates/widgets/thread.html +++ b/app/templates/widgets/thread.html @@ -3,9 +3,12 @@ {% macro thread(comments, top_comment) %} +{% if top_comment == None %} + +{% endif %} {% for c in comments %} - {% if c != top_comment %} + + {% elif loop.index0 != 0 %} +
Ce message est le top comment
+ {% endif %} {% endfor %} -- 2.45.0 From 79289146a277d3e27b4268fc3988adff5e5252cb Mon Sep 17 00:00:00 2001 From: Lephe Date: Mon, 12 Jul 2021 17:49:54 +0200 Subject: [PATCH 4/5] forum: add an action to change the top comment of a topic This also prepares the thread_leader macro for top comments of topics, programs, etc. which have extra stuff to render and more specific actions. --- app/data/groups.yaml | 2 +- app/models/user.py | 7 +++++ app/routes/posts/edit.py | 15 ++++++++++ app/static/css/table.css | 7 +++++ app/templates/forum/topic.html | 5 +++- app/templates/widgets/thread.html | 48 ++++++++++++++++++++++++------- 6 files changed, 71 insertions(+), 13 deletions(-) diff --git a/app/data/groups.yaml b/app/data/groups.yaml index 79fd3b0..f81d603 100644 --- a/app/data/groups.yaml +++ b/app/data/groups.yaml @@ -16,7 +16,7 @@ # publish.shared-files # # Moderation: -# edit.posts +# edit.posts (includes top comment selection) # edit.tests # edit.accounts # edit.trophies diff --git a/app/models/user.py b/app/models/user.py index d0a4390..7b2fe72 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -227,6 +227,13 @@ class Member(User): return self.can_access_post(post) and \ ((post.author == self) or 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 diff --git a/app/routes/posts/edit.py b/app/routes/posts/edit.py index c1aee89..11b0a45 100644 --- a/app/routes/posts/edit.py +++ b/app/routes/posts/edit.py @@ -2,6 +2,8 @@ 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 @@ -78,3 +80,16 @@ def delete_post(postid): p.delete() db.session.commit() return redirect(request.referrer) + +@app.route('/post/entete/', 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) diff --git a/app/static/css/table.css b/app/static/css/table.css index 691920f..2e9a54f 100644 --- a/app/static/css/table.css +++ b/app/static/css/table.css @@ -128,6 +128,12 @@ table.thread td.message img { max-width: 100%; } +table.thread .topcomment-placeholder div { + font-style: italic; + opacity: 0.5; + padding: 8px 0; +} + table.thread div.info { text-align: right; position: relative; @@ -147,6 +153,7 @@ table.thread div.info summary { cursor: pointer; user-select: none; } + table.thread .context-menu { position: absolute; right: 0; diff --git a/app/templates/forum/topic.html b/app/templates/forum/topic.html index 712ac86..468949c 100644 --- a/app/templates/forum/topic.html +++ b/app/templates/forum/topic.html @@ -11,7 +11,10 @@ {% block content %}

{{ t.title }}

- {{ widget_thread.thread([t.thread.top_comment], None) }} + + {% call widget_thread.thread_leader(t.thread.top_comment) %} + {{ t.thread.top_comment.text | md }} + {% endcall %} {{ widget_pagination.paginate(comments, 'forum_topic', t, {'f': t.forum}) }} diff --git a/app/templates/widgets/thread.html b/app/templates/widgets/thread.html index 47ec9f0..486ecea 100644 --- a/app/templates/widgets/thread.html +++ b/app/templates/widgets/thread.html @@ -1,11 +1,14 @@ {% import "widgets/user.html" as widget_user %} {% import "widgets/attachments.html" as widget_attachments %} -{% macro thread(comments, top_comment) %} -
{{ widget_user.profile(c.author) }}
@@ -46,8 +49,11 @@ {{ c.author.signature|md }} {% endif %}
-{% if top_comment == None %} - -{% endif %} +{# 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) %} +
{% for c in comments %} {% if c != top_comment %} @@ -18,9 +21,10 @@ {% endif %} {# TODO: Let guests edit their posts #} - {% set can_edit = current_user.is_authenticated and current_user.can_edit_post(c) %} - {% set can_delete = current_user.is_authenticated and current_user.can_delete_post(c) %} - {% set can_punish = current_user.is_authenticated and current_user.priv("delete.posts") %} + {% set can_edit = current_user.is_authenticated and current_user.can_edit_post(c) %} + {% set can_delete = current_user.is_authenticated and current_user.can_delete_post(c) %} + {% set can_punish = current_user.is_authenticated and current_user.priv("delete.posts") %} + {% set can_topcomm = current_user.is_authenticated and current_user.can_set_topcomment(c) %} {% if can_edit or can_delete or can_punish %}
@@ -29,12 +33,17 @@ {% if can_edit %} Modifier {% endif %} + {% if can_punish %} Supprimer (normal) Supprimer (pénalité) {% elif can_delete %} Supprimer {% endif %} + + {% if can_topcomm %} + Utiliser comme en-tête + {% endif %}
{% endif %} @@ -51,11 +60,28 @@ {% elif loop.index0 != 0 %} - -
Ce message est le top comment
+ + + {% endif %} - {% endfor %}
Le commentaire à cet endroit est actuellement utilisé comme en-tête.
{% 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) %} + + {# Empty line to get normal background (instead of alternate one) #} + + + + + +
{{ widget_user.profile(leader.author) }}{{ caller() }}
+{% endmacro %} -- 2.45.0 From 00813bf71c4bb1b69d2c37bdeb37e11698e5ad2b Mon Sep 17 00:00:00 2001 From: Lephe Date: Mon, 12 Jul 2021 18:30:31 +0200 Subject: [PATCH 5/5] forum: enable topic deletion Topic modification does not work in this snapshot, this is normal. --- app/models/user.py | 4 ++ app/routes/posts/edit.py | 7 +++- app/templates/forum/topic.html | 6 +++ app/templates/widgets/thread.html | 70 ++++++++++++++++++------------- 4 files changed, 57 insertions(+), 30 deletions(-) diff --git a/app/models/user.py b/app/models/user.py index 7b2fe72..1e9de8c 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -227,6 +227,10 @@ 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": diff --git a/app/routes/posts/edit.py b/app/routes/posts/edit.py index 11b0a45..ab6269b 100644 --- a/app/routes/posts/edit.py +++ b/app/routes/posts/edit.py @@ -67,11 +67,16 @@ 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) @@ -79,7 +84,7 @@ def delete_post(postid): p.delete() db.session.commit() - return redirect(request.referrer) + return redirect(next_page) @app.route('/post/entete/', methods=['GET']) @login_required diff --git a/app/templates/forum/topic.html b/app/templates/forum/topic.html index 468949c..aca7f5a 100644 --- a/app/templates/forum/topic.html +++ b/app/templates/forum/topic.html @@ -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 %} Forum de Planète Casio » {{ t.forum.name }} »

{{ t.title }}

@@ -13,7 +14,12 @@

{{ t.title }}

{% call widget_thread.thread_leader(t.thread.top_comment) %} +
+
Posté le {{ t.date_created | dyndate }}
+ {{ widget_thread.post_actions(t) }} +
{{ t.thread.top_comment.text | md }} + {{ widget_attachments.attachments(t.thread.top_comment) }} {% endcall %} {{ widget_pagination.paginate(comments, 'forum_topic', t, {'f': t.forum}) }} diff --git a/app/templates/widgets/thread.html b/app/templates/widgets/thread.html index 486ecea..bcb4534 100644 --- a/app/templates/widgets/thread.html +++ b/app/templates/widgets/thread.html @@ -1,6 +1,46 @@ {% import "widgets/user.html" as widget_user %} {% import "widgets/attachments.html" as widget_attachments %} +{# 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 %} +
+ +
+ {% if can_edit %} + Modifier{{ suffix }} + {% endif %} + + {% if can_punish %} + Supprimer{{ suffix }} (normal) + Supprimer{{ suffix }} (pénalité) + {% elif can_delete %} + Supprimer{{ suffix }} + {% endif %} + + {% if can_topcomm %} + Utiliser comme en-tête + {% endif %} +
+
+ {% endif %} +{% endmacro %} + {# Thread widget: this widget expands to a table that shows a list of comments from a thread, along with message controls. @@ -19,38 +59,10 @@ {% if c.date_created != c.date_modified %} {% endif %} - - {# TODO: Let guests edit their posts #} - {% set can_edit = current_user.is_authenticated and current_user.can_edit_post(c) %} - {% set can_delete = current_user.is_authenticated and current_user.can_delete_post(c) %} - {% set can_punish = current_user.is_authenticated and current_user.priv("delete.posts") %} - {% set can_topcomm = current_user.is_authenticated and current_user.can_set_topcomment(c) %} - - {% if can_edit or can_delete or can_punish %} -
- -
- {% if can_edit %} - Modifier - {% endif %} - - {% if can_punish %} - Supprimer (normal) - Supprimer (pénalité) - {% elif can_delete %} - Supprimer - {% endif %} - - {% if can_topcomm %} - Utiliser comme en-tête - {% endif %} -
-
- {% endif %} + {{ post_actions(c) }} {{ c.text|md }} - {{ widget_attachments.attachments(c) }} {% if c.author.signature %} -- 2.45.0