Compare commits
6 Commits
53afccf2a3
...
6f98cba65e
Author | SHA1 | Date |
---|---|---|
Lephe | 6f98cba65e | |
Lephe | d50b58cd24 | |
Lephe | 3ee3794818 | |
Darks | 0edc996287 | |
Darks | 41d1411f86 | |
Darks | f722d700c2 |
|
@ -1,10 +1,8 @@
|
|||
# This file is a list of forums to create when setting up Planète Casio.
|
||||
#
|
||||
# * Keys are used as URLs paths and for unique identification.
|
||||
# * Prefixes represent the privilege category for a forum. Owning privileges
|
||||
# with this prefix allows the user to post in this forum and all its
|
||||
# sub-forum regardless of their settings ("forum-root-*" are hyper powerful).
|
||||
# * For open forums, use the prefix "open".
|
||||
# * Prefixes are used to identify privileges for each forum, see groups.yaml
|
||||
# for details.
|
||||
|
||||
/:
|
||||
name: Forum de Planète Casio
|
||||
|
@ -104,8 +102,8 @@
|
|||
prefix: discussion
|
||||
descr: Sujets hors-sujet et discussion libre.
|
||||
|
||||
# Limited-access board
|
||||
# Prefixes "admin" and "assoc" are reserved for this and require special
|
||||
# Limited-access boards
|
||||
# Prefixes "admin" and "creativecalc" are reserved for this and require special
|
||||
# privileges to list, read and edit topics and messages.
|
||||
|
||||
/admin:
|
||||
|
@ -116,5 +114,5 @@
|
|||
|
||||
/creativecalc:
|
||||
name: CreativeCalc
|
||||
prefix: assoc
|
||||
prefix: creativecalc
|
||||
descr: Forum privé de l'association CreativeCalc, réservé aux membres.
|
||||
|
|
|
@ -1,63 +1,109 @@
|
|||
# LIST OF PRIVILEGES:
|
||||
#
|
||||
# Access to specific forums (see forums.yaml for prefix values):
|
||||
# forum.access.<prefix>
|
||||
# forum.post.<prefix>
|
||||
# forum.post-news
|
||||
# forum.post-anywhere
|
||||
# -> All forums are readable by default except <admin> and <creativecalc>
|
||||
# -> All forums are writable by default except <admin>, <creativecalc>,
|
||||
# children of <news>, and forums with children ("categories")
|
||||
# -> Use member.can_access_forum(forum) and member.can_post_in_forum(forum)
|
||||
#
|
||||
# Access to extended publication methods:
|
||||
# publish.schedule-posts
|
||||
# publish.pin-posts
|
||||
# publish.shared-files
|
||||
#
|
||||
# Moderation:
|
||||
# edit.posts
|
||||
# edit.tests
|
||||
# edit.accounts
|
||||
# edit.trophies
|
||||
# delete.posts
|
||||
# delete.tests
|
||||
# delete.accounts
|
||||
# delete.shared-files
|
||||
# move.posts
|
||||
#
|
||||
# Shoutbox:
|
||||
# shoutbox.kick
|
||||
# shoutbox.ban
|
||||
#
|
||||
# Miscellaneous:
|
||||
# misc.unlimited-pms
|
||||
# misc.dev-infos
|
||||
# misc.community-login
|
||||
# misc.admin-panel
|
||||
# misc.no-upload-limits
|
||||
#
|
||||
# TODO: PRIVILEGES NOT YET IMPLEMENTED:
|
||||
# The features that these privileges control are not implemented yet, or the
|
||||
# privilege checks are missing.
|
||||
#
|
||||
# publish.*
|
||||
# edit.tests
|
||||
# delete.tests delete.shared-files
|
||||
# move.posts
|
||||
# shoutbox.*
|
||||
# misc.unlimited-pms
|
||||
# misc.community-login
|
||||
|
||||
-
|
||||
name: Administrateur
|
||||
css: "color: #ee0000;"
|
||||
descr: "Vous voyez Chuck Norris ? Pareil."
|
||||
privs: access-admin-board access-assoc-board write-news write-anywhere
|
||||
upload-shared-files delete-shared-files
|
||||
edit-posts delete-posts scheduled-posting
|
||||
delete-content move-public-content move-private-content showcase-content
|
||||
edit-static-content extract-posts
|
||||
delete-notes delete-tests
|
||||
shoutbox-kick shoutbox-ban
|
||||
unlimited-pms footer-statistics community-login
|
||||
access-admin-panel edit-account delete-account edit-trophies
|
||||
delete_notification no-upload-limits
|
||||
privs: forum.access.admin forum.access.creativecalc forum.post-news
|
||||
forum.post-anywhere
|
||||
publish.schedule-posts publish.pin-posts publish.shared-files
|
||||
edit.posts edit.tests edit.accounts edit.trophies
|
||||
delete.posts delete.tests delete.accounts delete.shared-files
|
||||
move.posts
|
||||
shoutbox.kick shoutbox.ban
|
||||
misc.unlimited-pms misc.dev-infos misc.community-login misc.admin-panel
|
||||
misc.no-upload-limits
|
||||
-
|
||||
name: Modérateur
|
||||
css: "color: green;"
|
||||
descr: "Maîtres du kick, ils sont là pour faire respecter un semblant d'ordre."
|
||||
privs: access-admin-board
|
||||
edit-posts delete-posts
|
||||
move-public-content extract-posts
|
||||
delete-notes delete-tests
|
||||
shoutbox-kick shoutbox-ban
|
||||
unlimited-pms no-upload-limits
|
||||
privs: forum.access.admin
|
||||
edit.posts edit.tests
|
||||
delete.posts delete.tests
|
||||
move.posts
|
||||
shoutbox.kick shoutbox.ban
|
||||
misc.unlimited-pms misc.no-upload-limits
|
||||
-
|
||||
name: Développeur
|
||||
css: "color: #4169e1;"
|
||||
descr: "Les développeurs maintiennent et améliorent le code du site."
|
||||
privs: access-admin-board
|
||||
upload-shared-files delete-shared-files
|
||||
scheduled-posting
|
||||
edit-static-content
|
||||
unlimited-pms footer-statistics community-login
|
||||
access-admin-panel no-upload-limits
|
||||
privs: forum.access.admin forum.post-anywhere
|
||||
publish.schedule-posts publish.shared-files
|
||||
delete.shared-files
|
||||
misc.unlimited-pms misc.dev-infos misc.community-login misc.admin-panel
|
||||
-
|
||||
name: Rédacteur
|
||||
css: "color: blue;"
|
||||
descr: "Rédigent les meilleurs articles de la page d'accueil, rien que pour
|
||||
vous <3"
|
||||
privs: access-admin-board write-news
|
||||
upload-shared-files delete-shared-files
|
||||
scheduled-posting
|
||||
showcase-content edit-static-content
|
||||
no-upload-limits
|
||||
privs: forum.access.admin forum.post-news
|
||||
publish.schedule-posts publish.pin-posts publish.shared-files
|
||||
delete.shared-files
|
||||
misc.no-upload-limits
|
||||
-
|
||||
name: Responsable communauté
|
||||
css: "color: DarkOrange;"
|
||||
descr: "Anime les pages Twitter et Facebook de Planète Casio et surveille
|
||||
l'évolution du monde autour de nous !"
|
||||
privs: access-admin-board write-news
|
||||
upload-shared-files delete-shared-files
|
||||
scheduled-posting
|
||||
showcase-content
|
||||
privs: forum.access.admin forum.post-news
|
||||
publish.schedule-posts publish.pin-posts publish.shared-files
|
||||
delete.shared-files
|
||||
-
|
||||
name: Partenaire
|
||||
css: "color: purple;"
|
||||
descr: "Membres de l'équipe d'administration des sites partenaires."
|
||||
privs: write-news
|
||||
upload-shared-files delete-shared-files
|
||||
scheduled-posting
|
||||
privs: forum.post-news
|
||||
publish.schedule-posts publish.shared-files
|
||||
delete.shared-files
|
||||
-
|
||||
name: Compte communautaire
|
||||
css: "background:#d8d8d8; border-radius:4px; color:#303030; padding:1px 2px;"
|
||||
|
@ -66,13 +112,13 @@
|
|||
name: Robot
|
||||
css: "color: #cf25d0;"
|
||||
descr: "♫ Je suis Nono, le petit robot, l'ami d'Ulysse ♫"
|
||||
privs: shoutbox-post shoutbox-kick shoutbox-ban
|
||||
privs: shoutbox.kick shoutbox.ban
|
||||
-
|
||||
name: Membre de CreativeCalc
|
||||
css: "color: #222222;"
|
||||
descr: "CreativeCalc est l'association qui gère Planète Casio."
|
||||
privs: access-assoc-board
|
||||
privs: forum.access.creativecalc
|
||||
-
|
||||
name: No login
|
||||
css: "color: #888888;"
|
||||
descr: "Compte dont l'accès au site est désactivé."
|
||||
name: No login
|
||||
css: "color: #888888;"
|
||||
descr: "Compte dont l'accès au site est désactivé."
|
||||
|
|
|
@ -20,7 +20,8 @@ class Forum(db.Model):
|
|||
lazy=True, foreign_keys=parent_id)
|
||||
|
||||
# Other fields populated automatically through relations:
|
||||
# <topics> List of topics in this exact forum (of type Topic)
|
||||
# <sub_forums> Children forums
|
||||
# <topics> List of topics in this exact forum (of type Topic)
|
||||
|
||||
# Some configuration
|
||||
TOPICS_PER_PAGE = 30
|
||||
|
@ -36,6 +37,19 @@ class Forum(db.Model):
|
|||
else:
|
||||
self.parent = parent
|
||||
|
||||
def is_news(self):
|
||||
"""Whether this forum is a news board."""
|
||||
return (self.parent is not None) and (self.parent.prefix == "news")
|
||||
|
||||
def is_default_accessible(self):
|
||||
"""Whether this forum can be read without privileges."""
|
||||
return (self.prefix != "admin") and (self.prefix != "creativecalc")
|
||||
|
||||
def is_default_postable(self):
|
||||
"""Whether this forum can be posted to without privileges."""
|
||||
return self.is_default_accessible() and (not self.is_news()) and \
|
||||
(self.sub_forums == [])
|
||||
|
||||
def post_count(self):
|
||||
"""Number of posts in every topic of the forum, without subforums."""
|
||||
# TODO: optimize this with real ORM
|
||||
|
|
|
@ -9,6 +9,8 @@ class Topic(Post):
|
|||
|
||||
__mapper_args__ = {
|
||||
'polymorphic_identity': __tablename__,
|
||||
# Because there is an extra relation to Post (promotion), SQLAlchemy
|
||||
# cannot guess which Post we inherit from; specify here.
|
||||
'inherit_condition': id == Post.id
|
||||
}
|
||||
|
||||
|
|
|
@ -20,11 +20,11 @@ import os
|
|||
|
||||
|
||||
class User(UserMixin, db.Model):
|
||||
""" Website user that performs actions on the post """
|
||||
""" Any website user, logged in (Member) or not (Guest) """
|
||||
|
||||
__tablename__ = 'user'
|
||||
|
||||
# User ID, should be used to refer to any user. Thea actual user can either
|
||||
# User ID, should be used to refer to any user. The actual user can either
|
||||
# be a guest (with IP as key) or a member (with this ID as key).
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
# User type (polymorphic discriminator)
|
||||
|
@ -55,7 +55,7 @@ class Guest(User):
|
|||
# ID of the [User] entry
|
||||
id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True)
|
||||
# Reusable username, cannot be chosen as the name of a member
|
||||
# but will be distinguished at rendering time if a member take it later
|
||||
# but will be distinguished at rendering time if a member takes it later
|
||||
name = db.Column(db.Unicode(User.NAME_MAXLEN))
|
||||
|
||||
def __init__(self, name):
|
||||
|
@ -66,7 +66,7 @@ class Guest(User):
|
|||
|
||||
|
||||
class Member(User):
|
||||
""" Registered user with full access to the website's services """
|
||||
""" Registered user with full access to the site's features """
|
||||
|
||||
__tablename__ = 'member'
|
||||
__mapper_args__ = {'polymorphic_identity': __tablename__}
|
||||
|
@ -116,10 +116,6 @@ class Member(User):
|
|||
programs = db.relationship('Program')
|
||||
comments = db.relationship('Comment')
|
||||
|
||||
# Displayed title
|
||||
# title_id = db.Column(db.Integer, db.ForeignKey('title.id'))
|
||||
# title = db.relationship('Title', foreign_keys=title_id)
|
||||
|
||||
# Other fields populated automatically through relations:
|
||||
# <notifications> List of unseen notifications (of type Notification)
|
||||
# <polls> Polls created by the member (of class Poll)
|
||||
|
@ -152,6 +148,8 @@ class Member(User):
|
|||
db.session.delete(self)
|
||||
db.session.commit()
|
||||
|
||||
# Privilege checks
|
||||
|
||||
def priv(self, priv):
|
||||
"""Check whether the member has the specified privilege."""
|
||||
if SpecialPrivilege.query.filter_by(mid=self.id, priv=priv).first():
|
||||
|
@ -166,6 +164,35 @@ class Member(User):
|
|||
sp = SpecialPrivilege.query.filter_by(mid=self.id).all()
|
||||
return sorted(row.priv for row in sp)
|
||||
|
||||
def can_access_forum(self, forum):
|
||||
"""Whether this member can read the forum's contents."""
|
||||
return forum.is_default_accessible() or \
|
||||
self.priv(f"forum.access.{forum.prefix}")
|
||||
|
||||
def can_post_in_forum(self, forum):
|
||||
"""Whether this member can post in the forum."""
|
||||
return forum.is_default_postable() or \
|
||||
(forum.is_news() and self.priv("forum.post-news")) or \
|
||||
self.priv("forum.post.{forum.prefix}") or \
|
||||
self.priv("forum.post-anywhere")
|
||||
|
||||
def can_access_post(self, post):
|
||||
"""Whether this member can access the post's forum (if any)."""
|
||||
if post.type == "comment" and post.thread.owner_topic:
|
||||
return self.can_access_forum(post.thread.owner_post.forum)
|
||||
# Posts from other types of content are all public
|
||||
return True
|
||||
|
||||
def can_edit_post(self, post):
|
||||
"""Whether this member can edit the post."""
|
||||
return self.can_access_post(post) and \
|
||||
((post.author == self) or self.priv("edit.posts"))
|
||||
|
||||
def can_delete_post(self, post):
|
||||
"""Whether this member can delete the post."""
|
||||
return self.can_access_post(post) and \
|
||||
((post.author == self) or self.priv("delete.posts"))
|
||||
|
||||
def update(self, **data):
|
||||
"""
|
||||
Update all or part of the user's metadata. The [data] dictionary
|
||||
|
|
|
@ -14,7 +14,7 @@ from config import V5Config
|
|||
|
||||
|
||||
@app.route('/admin/compte/<user_id>/editer', methods=['GET', 'POST'])
|
||||
@priv_required('access-admin-panel', 'edit-account')
|
||||
@priv_required('misc.admin-panel', 'edit.accounts')
|
||||
def adm_edit_account(user_id):
|
||||
user = Member.query.filter_by(id=user_id).first_or_404()
|
||||
|
||||
|
@ -119,7 +119,7 @@ def adm_edit_account(user_id):
|
|||
|
||||
|
||||
@app.route('/admin/compte/<user_id>/supprimer', methods=['GET', 'POST'])
|
||||
@priv_required('access-admin-panel', 'delete-account')
|
||||
@priv_required('misc.admin-panel', 'delete.accounts')
|
||||
def adm_delete_account(user_id):
|
||||
user = Member.query.filter_by(id=user_id).first_or_404()
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ from app.utils.render import render
|
|||
# TODO: add pagination & moderation tools (deletion)
|
||||
|
||||
@app.route('/admin/fichiers', methods=['GET'])
|
||||
@priv_required('access-admin-panel')
|
||||
@priv_required('misc.admin-panel')
|
||||
def adm_attachments():
|
||||
attachments = Attachment.query.all()
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ from app import app
|
|||
from config import V5Config
|
||||
|
||||
@app.route('/admin/config', methods=['GET'])
|
||||
@priv_required('access-admin-panel')
|
||||
@priv_required('misc.admin-panel')
|
||||
def adm_config():
|
||||
config = {k: getattr(V5Config, k) for k in [
|
||||
"DOMAIN", "DB_NAME", "USE_LDAP", "LDAP_ROOT", "LDAP_ENV",
|
||||
|
|
|
@ -4,7 +4,7 @@ from app.models.forum import Forum
|
|||
from app import app, db
|
||||
|
||||
@app.route('/admin/forums', methods=['GET'])
|
||||
@priv_required('access-admin-panel')
|
||||
@priv_required('misc.admin-panel')
|
||||
def adm_forums():
|
||||
main_forum = Forum.query.filter_by(parent=None).first()
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import os
|
|||
|
||||
|
||||
@app.route('/admin/groupes', methods=['GET', 'POST'])
|
||||
@priv_required('access-admin-panel')
|
||||
@priv_required('misc.admin-panel')
|
||||
def adm_groups():
|
||||
# Users with either groups or special privileges
|
||||
users_groups = Member.query.join(GroupMember)
|
||||
|
|
|
@ -4,6 +4,6 @@ from app import app
|
|||
|
||||
|
||||
@app.route('/admin', methods=['GET'])
|
||||
@priv_required('access-admin-panel')
|
||||
@priv_required('misc.admin-panel')
|
||||
def adm():
|
||||
return render('admin/index.html')
|
||||
|
|
|
@ -6,7 +6,7 @@ from app import app, db
|
|||
|
||||
|
||||
@app.route('/admin/membres', methods=['GET', 'POST'])
|
||||
@priv_required('access-admin-panel')
|
||||
@priv_required('misc.admin-panel')
|
||||
def adm_members():
|
||||
users = Member.query.all()
|
||||
users = sorted(users, key = lambda x: x.name)
|
||||
|
|
|
@ -7,7 +7,7 @@ from app import app, db
|
|||
|
||||
|
||||
@app.route('/admin/trophees', methods=['GET', 'POST'])
|
||||
@priv_required('access-admin-panel', 'edit-trophies')
|
||||
@priv_required('misc.admin-panel', 'edit.trophies')
|
||||
def adm_trophies():
|
||||
form = TrophyForm()
|
||||
if request.method == "POST":
|
||||
|
@ -31,7 +31,7 @@ def adm_trophies():
|
|||
|
||||
|
||||
@app.route('/admin/trophees/<trophy_id>/editer', methods=['GET', 'POST'])
|
||||
@priv_required('access-admin-panel', 'edit-trophies')
|
||||
@priv_required('misc.admin-panel', 'edit.trophies')
|
||||
def adm_edit_trophy(trophy_id):
|
||||
trophy = Trophy.query.filter_by(id=trophy_id).first_or_404()
|
||||
|
||||
|
@ -59,7 +59,7 @@ def adm_edit_trophy(trophy_id):
|
|||
|
||||
|
||||
@app.route('/admin/trophees/<trophy_id>/supprimer', methods=['GET', 'POST'])
|
||||
@priv_required('access-admin-panel', 'edit-trophies')
|
||||
@priv_required('misc.admin-panel', 'edit.trophies')
|
||||
def adm_delete_trophy(trophy_id):
|
||||
trophy = Trophy.query.filter_by(id=trophy_id).first_or_404()
|
||||
|
||||
|
|
|
@ -20,21 +20,18 @@ def forum_index():
|
|||
@app.route('/forum/<forum:f>/', methods=['GET', 'POST'])
|
||||
@app.route('/forum/<forum:f>/p/<int:page>', methods=['GET', 'POST'])
|
||||
def forum_page(f, page=1):
|
||||
if not f.is_default_accessible() and not (
|
||||
current_user.is_authenticated and current_user.can_access_forum(f)):
|
||||
abort(403)
|
||||
|
||||
if current_user.is_authenticated:
|
||||
form = TopicCreationForm()
|
||||
else:
|
||||
form = AnonymousTopicCreationForm()
|
||||
|
||||
# TODO: do not hardcode name of news forums
|
||||
if form.validate_on_submit() and (
|
||||
# User can write anywhere
|
||||
(current_user.is_authenticated and current_user.priv('write-anywhere'))
|
||||
# Forum is news forum TODO: add good condition to check if it's news
|
||||
or ("/actus" in f.url and current_user.is_authenticated
|
||||
and current_user.priv('write-news'))
|
||||
# Forum is not news and is a leaf:
|
||||
or ("/actus" not in f.url and not f.sub_forums)) and (
|
||||
V5Config.ENABLE_GUEST_POST or current_user.is_authenticated):
|
||||
(V5Config.ENABLE_GUEST_POST and f.is_default_postable()) or \
|
||||
(current_user.is_authenticated and current_user.can_post_in_forum(f))):
|
||||
|
||||
# Manage author
|
||||
if current_user.is_authenticated:
|
||||
|
|
|
@ -18,6 +18,10 @@ from datetime import datetime
|
|||
def forum_topic(f, page):
|
||||
t, page = page
|
||||
|
||||
if not f.is_default_accessible() and not (
|
||||
current_user.is_authenticated and current_user.can_access_forum(f)):
|
||||
abort(403)
|
||||
|
||||
# Quick n' dirty workaround to converters
|
||||
if f != t.forum:
|
||||
abort(404)
|
||||
|
@ -27,8 +31,10 @@ def forum_topic(f, page):
|
|||
else:
|
||||
form = AnonymousCommentForm()
|
||||
|
||||
if form.validate_on_submit() and \
|
||||
(V5Config.ENABLE_GUEST_POST or current_user.is_authenticated):
|
||||
if form.validate_on_submit() and (
|
||||
V5Config.ENABLE_GUEST_POST or \
|
||||
(current_user.is_authenticated and current_user.can_post_in_forum(f))):
|
||||
|
||||
# Manage author
|
||||
if current_user.is_authenticated:
|
||||
author = current_user
|
||||
|
@ -70,8 +76,8 @@ def forum_topic(f, page):
|
|||
if page == -1:
|
||||
page = (t.thread.comments.count() - 1) // Thread.COMMENTS_PER_PAGE + 1
|
||||
|
||||
comments = t.thread.comments.paginate(page,
|
||||
Thread.COMMENTS_PER_PAGE, True)
|
||||
comments = t.thread.comments.order_by(Comment.date_created.asc()) \
|
||||
.paginate(page, Thread.COMMENTS_PER_PAGE, True)
|
||||
|
||||
# Anti-necropost
|
||||
last_com = t.thread.comments.order_by(desc(Comment.date_modified)).first()
|
||||
|
|
|
@ -8,7 +8,6 @@ from flask import redirect, url_for, abort, request
|
|||
from flask_login import login_required, current_user
|
||||
|
||||
@app.route('/post/<int:postid>', methods=['GET','POST'])
|
||||
# TODO: Allow guest edit of posts
|
||||
@login_required
|
||||
def edit_post(postid):
|
||||
# TODO: Maybe not safe
|
||||
|
@ -17,8 +16,8 @@ def edit_post(postid):
|
|||
|
||||
p = Post.query.filter_by(id=postid).first_or_404()
|
||||
|
||||
# TODO: Check whether privileged user has access to board
|
||||
if p.author != current_user and not current_user.priv("edit-posts"):
|
||||
# Check permissions. TODO: Allow guests to edit their posts
|
||||
if current_user.is_anonymous or not current_user.can_edit_post(p):
|
||||
abort(403)
|
||||
|
||||
if p.type == "comment":
|
||||
|
@ -44,8 +43,7 @@ def edit_post(postid):
|
|||
def delete_post(postid):
|
||||
p = Post.query.filter_by(id=postid).first_or_404()
|
||||
|
||||
# TODO: Check whether privileged user has access to board
|
||||
if p.author != current_user and not current_user.priv("delete-posts"):
|
||||
if current_user.is_anonymous or not current_user.can_delete_post(p):
|
||||
abort(403)
|
||||
|
||||
for a in p.attachments:
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
{% if current_user.is_authenticated %}
|
||||
{% if current_user == member %}
|
||||
<div><a href="{{ url_for('edit_account') }}">Modifier le compte</a></div>
|
||||
{% elif current_user.priv('access-admin-panel') %}
|
||||
{% elif current_user.priv('edit.accounts') %}
|
||||
<div><a href="{{ url_for('adm_edit_account', user_id=member.id) }}">Modifier le compte</a></div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<footer>
|
||||
<p>Planète Casio est un site communautaire non affilié à CASIO. Toute reproduction de Planète Casio, même partielle, est interdite.</p>
|
||||
<p>Les programmes et autres publications présentes sur Planète Casio restent la propriété de leurs auteurs et peuvent être soumis à des licences ou des copyrights.</p>
|
||||
{% if current_user.is_authenticated and current_user.priv('footer-statistics') %}
|
||||
{% if current_user.is_authenticated and current_user.priv('misc.dev-infos') %}
|
||||
<p>Page générée en {{ g.request_time() }}</p>
|
||||
{% endif %}
|
||||
<p>Ceci est un environnement de test. Tout contenu peut être supprimé sans avertissement préalable.</p>
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
<path fill="#ffffff" d="M12,15.39L8.24,17.66L9.23,13.38L5.91,10.5L10.29,10.13L12,6.09L13.71,10.13L18.09,10.5L14.77,13.38L15.76,17.66M22,9.24L14.81,8.63L12,2L9.19,8.63L2,9.24L7.45,13.97L5.82,21L12,17.27L18.18,21L16.54,13.97L22,9.24Z"></path>
|
||||
</svg>Topics favoris
|
||||
</a>
|
||||
{% if current_user.priv('access-admin-panel') %}
|
||||
{% if current_user.priv('misc.admin-panel') %}
|
||||
<a href="{{ url_for('adm') }}">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path fill="#ffffff" d="M3,1H19A1,1 0 0,1 20,2V6A1,1 0 0,1 19,7H3A1,1 0 0,1 2,6V2A1,1 0 0,1 3,1M3,9H19A1,1 0 0,1 20,10V10.67L17.5,9.56L11,12.44V15H3A1,1 0 0,1 2,14V10A1,1 0 0,1 3,9M3,17H11C11.06,19.25 12,21.4 13.46,23H3A1,1 0 0,1 2,22V18A1,1 0 0,1 3,17M8,5H9V3H8V5M8,13H9V11H8V13M8,21H9V19H8V21M4,3V5H6V3H4M4,11V13H6V11H4M4,19V21H6V19H4M17.5,12L22,14V17C22,19.78 20.08,22.37 17.5,23C14.92,22.37 13,19.78 13,17V14L17.5,12M17.5,13.94L15,15.06V17.72C15,19.26 16.07,20.7 17.5,21.06V13.94Z"></path>
|
||||
|
|
|
@ -10,7 +10,10 @@
|
|||
<hr>
|
||||
|
||||
{% for f in main_forum.sub_forums %}
|
||||
<a href="{{ url_for('forum_page', f=f) }}">{{ f.name }}</a>
|
||||
{% if f.is_default_accessible() or
|
||||
(current_user.is_authenticated and current_user.can_access_forum(f)) %}
|
||||
<a href="{{ url_for('forum_page', f=f) }}">{{ f.name }}</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
<hr>
|
||||
|
|
|
@ -48,10 +48,8 @@
|
|||
</table>
|
||||
{% endif %}
|
||||
|
||||
{% if (current_user.is_authenticated and current_user.priv('write-anywhere'))
|
||||
or ("/actus" in f.url and current_user.is_authenticated and current_user.priv('write-news'))
|
||||
or ("/actus" not in f.url and not f.sub_forums)
|
||||
and (current_user.is_authenticated or V5Config.ENABLE_GUEST_POST) %}
|
||||
{% if (V5Config.ENABLE_GUEST_POST and f.is_default_postable())
|
||||
or (current_user.is_authenticated and current_user.can_post_in_forum(f)) %}
|
||||
<div class=form>
|
||||
<h2>Créer un nouveau sujet</h2>
|
||||
<form action="" method="post" enctype="multipart/form-data">
|
||||
|
|
|
@ -9,9 +9,9 @@
|
|||
<p>
|
||||
Bienvenue sur le forum de Planète Casio ! Vous pouvez créer des
|
||||
nouveaux sujets ou poster des réponses avec un compte
|
||||
{% if not current_user.is_authenticated %}
|
||||
{%- if not current_user.is_authenticated %}
|
||||
ou en postant en tant qu'invité
|
||||
{% endif %}
|
||||
{%- endif -%}
|
||||
.
|
||||
</p>
|
||||
|
||||
|
@ -20,22 +20,28 @@
|
|||
{% else %}
|
||||
|
||||
{% for l1 in main_forum.sub_forums %}
|
||||
<table class=forumlist>
|
||||
<tr><th>{{ l1.name }}</th><th>Nombre de sujets</th></tr>
|
||||
{% if l1.is_default_accessible() or
|
||||
(current_user.is_authenticated and current_user.can_access_forum(l1)) %}
|
||||
<table class=forumlist>
|
||||
<tr><th>{{ l1.name }}</th><th>Nombre de sujets</th></tr>
|
||||
|
||||
{% if l1.sub_forums == [] %}
|
||||
<tr><td><a href='/forum{{ l1.url }}'>{{ l1.name }}</a></td>
|
||||
<td>{{ l1.topics.count() }}</td></tr>
|
||||
<tr><td>{{ l1.descr }}</td><td></td></tr>
|
||||
{% endif %}
|
||||
{% if l1.sub_forums == [] %}
|
||||
<tr><td><a href='/forum{{ l1.url }}'>{{ l1.name }}</a></td>
|
||||
<td>{{ l1.topics.count() }}</td></tr>
|
||||
<tr><td>{{ l1.descr }}</td><td></td></tr>
|
||||
{% endif %}
|
||||
|
||||
{% for l2 in l1.sub_forums %}
|
||||
<tr><td><a href='/forum{{ l2.url }}'>{{ l2.name }}</td>
|
||||
<td>{{ l2.topics.count() }}</td></tr>
|
||||
<tr><td>{{ l2.descr }}</td><td></td></tr>
|
||||
{% endfor %}
|
||||
{% for l2 in l1.sub_forums %}
|
||||
{% if l2.is_default_accessible() or
|
||||
(current_user.is_authenticated and current_user.can_access_forum(l2)) %}
|
||||
<tr><td><a href='/forum{{ l2.url }}'>{{ l2.name }}</td>
|
||||
<td>{{ l2.topics.count() }}</td></tr>
|
||||
<tr><td>{{ l2.descr }}</td><td></td></tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
</table>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% endif %}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{% extends "base/base.html" %}
|
||||
{% import "widgets/editor.html" as widget_editor %}
|
||||
{% import "widgets/thread.html" as widget_thread %}
|
||||
{% 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 %}
|
||||
|
||||
|
@ -25,7 +25,8 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if current_user.is_authenticated or V5Config.ENABLE_GUEST_POST %}
|
||||
{% if V5Config.ENABLE_GUEST_POST
|
||||
or (current_user.is_authenticated and current_user.can_post_in_forum(t.forum)) %}
|
||||
<div class=form>
|
||||
<h3>Commenter le sujet</h3>
|
||||
<form action="" method="post" enctype="multipart/form-data">
|
||||
|
@ -48,7 +49,7 @@
|
|||
|
||||
<div>{{ form.submit(class_='bg-ok') }}</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
|
@ -13,9 +13,14 @@
|
|||
{% if c.date_created != c.date_modified %}
|
||||
<div>Modifié le {{ c.date_modified|date }}</div>
|
||||
{% endif %}
|
||||
<div><a href="{{ request.path }}#{{ c.id }}">Permalink</a></div>
|
||||
<div><a href="{{ url_for('edit_post', postid=c.id, r=request.path) }}">Modifier</a></div>
|
||||
<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><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>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{{ c.text|md }}
|
||||
|
|
|
@ -8,7 +8,7 @@ markdown_tags = [
|
|||
"img",
|
||||
"a",
|
||||
"sub", "sup",
|
||||
"table", "tbody", "tr", "th", "td",
|
||||
"table", "thead", "tbody", "tr", "th", "td",
|
||||
"form", "fieldset", "input", "textarea",
|
||||
"label", "progress"
|
||||
]
|
||||
|
@ -16,8 +16,8 @@ markdown_tags = [
|
|||
markdown_attrs = {
|
||||
"*": ["id", "class"],
|
||||
"img": ["src", "alt", "title"],
|
||||
"a": ["href", "alt", "title"],
|
||||
"form": ["action", "method"],
|
||||
"a": ["href", "alt", "title", "rel"],
|
||||
"form": ["action", "method", "enctype"],
|
||||
"input": ["id", "name", "type", "value"],
|
||||
"label": ["for"],
|
||||
"progress": ["value", "min", "max"],
|
||||
|
|
|
@ -8,7 +8,9 @@ from bleach import clean
|
|||
from app.utils.bleach_allowlist import markdown_tags, markdown_attrs
|
||||
|
||||
from app.utils.markdown_extensions.pclinks import PCLinkExtension
|
||||
from app.utils.markdown_extensions.escape_html import EscapeHtml
|
||||
from app.utils.markdown_extensions.hardbreaks import HardBreakExtension
|
||||
from app.utils.markdown_extensions.escape_html import EscapeHtmlExtension
|
||||
from app.utils.markdown_extensions.linkify import LinkifyExtension
|
||||
|
||||
|
||||
@app.template_filter('md')
|
||||
|
@ -25,8 +27,10 @@ def md(text):
|
|||
'sane_lists',
|
||||
'tables',
|
||||
CodeHiliteExtension(linenums=True, use_pygments=True),
|
||||
EscapeHtml(),
|
||||
EscapeHtmlExtension(),
|
||||
FootnoteExtension(UNIQUE_IDS=True),
|
||||
HardBreakExtension(),
|
||||
LinkifyExtension(),
|
||||
TocExtension(baselevel=2),
|
||||
PCLinkExtension(),
|
||||
]
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from markdown.extensions import Extension
|
||||
|
||||
|
||||
class EscapeHtml(Extension):
|
||||
class EscapeHtmlExtension(Extension):
|
||||
def extendMarkdown(self, md):
|
||||
md.preprocessors.deregister('html_block')
|
||||
md.inlinePatterns.deregister('html')
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
from markdown.extensions import Extension
|
||||
from markdown.inlinepatterns import SubstituteTagPattern
|
||||
|
||||
|
||||
class HardBreakExtension(Extension):
|
||||
def extendMarkdown(self, md):
|
||||
BREAK_RE = r' *\\\\\n'
|
||||
breakPattern = SubstituteTagPattern(BREAK_RE, 'br')
|
||||
md.inlinePatterns.register(breakPattern, 'hardbreak', 185)
|
|
@ -0,0 +1,56 @@
|
|||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2013 Raitis Stengrevics
|
||||
# https://github.com/daGrevis/mdx_linkify
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from bleach.linkifier import Linker
|
||||
|
||||
from markdown.postprocessors import Postprocessor
|
||||
from markdown.extensions import Extension
|
||||
|
||||
|
||||
class LinkifyExtension(Extension):
|
||||
def __init__(self, **kwargs):
|
||||
self.config = {
|
||||
'linker_options': [{}, 'Options for bleach.linkifier.Linker'],
|
||||
}
|
||||
super(LinkifyExtension, self).__init__(**kwargs)
|
||||
|
||||
def extendMarkdown(self, md):
|
||||
md.postprocessors.register(
|
||||
LinkifyPostprocessor(
|
||||
md,
|
||||
self.getConfig('linker_options'),
|
||||
),
|
||||
"linkify",
|
||||
50,
|
||||
)
|
||||
|
||||
|
||||
class LinkifyPostprocessor(Postprocessor):
|
||||
def __init__(self, md, linker_options):
|
||||
super(LinkifyPostprocessor, self).__init__(md)
|
||||
linker_options.setdefault("skip_tags", ["code"])
|
||||
self._linker_options = linker_options
|
||||
|
||||
def run(self, text):
|
||||
linker = Linker(**self._linker_options)
|
||||
return linker.linkify(text)
|
|
@ -20,6 +20,7 @@ from app.models.poll import Poll
|
|||
from app.models.topic import Topic
|
||||
from app.models.user import Member
|
||||
|
||||
|
||||
class PCLinkExtension(Extension):
|
||||
def __init__(self, **kwargs):
|
||||
self.config = {
|
||||
|
@ -36,7 +37,7 @@ class PCLinkExtension(Extension):
|
|||
PCLINK_RE = r'\[\[([a-z]+): ?(\w+)\]\]'
|
||||
pclinkPattern = PCLinksInlineProcessor(PCLINK_RE, self.getConfigs())
|
||||
pclinkPattern.md = md
|
||||
md.inlinePatterns.register(pclinkPattern, 'pclink', 75)
|
||||
md.inlinePatterns.register(pclinkPattern, 'pclink', 135)
|
||||
|
||||
|
||||
class PCLinksInlineProcessor(InlineProcessor):
|
||||
|
|
|
@ -13,7 +13,7 @@ def priv_required(*perms):
|
|||
|
||||
Example:
|
||||
@app.route('/admin')
|
||||
@priv_required('access-admin-board')
|
||||
@priv_required('forum.access.admin')
|
||||
def admin_board():
|
||||
pass
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ def optional(form, files):
|
|||
|
||||
def count(form, files):
|
||||
if current_user.is_authenticated:
|
||||
if current_user.priv("no-upload-limits"):
|
||||
if current_user.priv("misc.no-upload-limits"):
|
||||
return
|
||||
if len(files.data) > 100: # 100 files for a authenticated user
|
||||
raise ValidationError("100 fichiers maximum autorisés")
|
||||
|
@ -48,7 +48,7 @@ def size(form, files):
|
|||
"""There is no global limit to file sizes"""
|
||||
size = sum([filesize(f) for f in files.data])
|
||||
if current_user.is_authenticated:
|
||||
if current_user.priv("no-upload-limits"):
|
||||
if current_user.priv("misc.no-upload-limits"):
|
||||
return
|
||||
if size > 5e6: # 5 Mo per comment for an authenticated user
|
||||
raise ValidationError("Fichiers trop lourds (max 5 Mo)")
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
# Privileges
|
||||
|
||||
Read/write access to forum boards:
|
||||
access-admin-board Administration board of the forum
|
||||
access-assoc-board CreativeCalc discussion board
|
||||
write-news Post articles on the news board
|
||||
|
||||
Shared file upload (like /Fr/adpc/img.php for any file):
|
||||
upload-shared-files Upload files and images on the website server
|
||||
delete-shared-files Delete files uploaded with upload-shared-files
|
||||
|
||||
Post management:
|
||||
edit-posts Edit any post on the website
|
||||
delete-posts Remove any post from the website
|
||||
scheduled-posting Schedule a post or content creation in the future
|
||||
|
||||
Content (topic, progs, tutos, etc) management:
|
||||
delete-content Delete whole topics, program pages, or tutorials
|
||||
move-public-content Change the section of a page in a public section
|
||||
move-private-content Change the section of a page in a private section
|
||||
showcase-content Manage stocky content (post-its)
|
||||
edit-static-content Edit static content pages
|
||||
extract-posts Move out-of-topic posts to a new places
|
||||
|
||||
Program evaluation:
|
||||
delete-notes Delete program notes
|
||||
delete-tests Delete program tests
|
||||
|
||||
Shoutbox:
|
||||
shoutbox-post Write messages in the shoutbox
|
||||
shoutbox-kick Kick people using the shoutbox
|
||||
shoutbox-ban Ban people using the shoutbox
|
||||
|
||||
Miscellaenous:
|
||||
unlimited-pms Removes the limit on the number of private messages
|
||||
footer-statistics View performance statistics in the page footer
|
||||
community-login Automatically login as a community account
|
||||
|
||||
Administration panel:
|
||||
access-admin-panel Administration panel of website
|
||||
edit-account Edit details of any account
|
||||
delete-account Remove member accounts
|
Loading…
Reference in New Issue