Compare commits
13 Commits
fc1a14eeb2
...
f5573c0900
Author | SHA1 | Date |
---|---|---|
Eragon | f5573c0900 | |
Eragon | e6ae16c43d | |
Lephe | 8393cf1933 | |
Darks | b7dc2ebbf2 | |
Lephe | 38c4f274a0 | |
Lephe | 7d9e897ae9 | |
Lephe | 1040d57506 | |
Darks | f64e3a2c39 | |
Lephe | 8f620c6150 | |
Lephe | 5a87d29c7f | |
Lephe | a3ed633791 | |
Darks | eb5ce1bd5c | |
Darks | faf5bd184d |
|
@ -24,3 +24,8 @@ python-pillow
|
|||
python-pyyaml
|
||||
python-slugify
|
||||
```
|
||||
|
||||
Optionnel:
|
||||
```
|
||||
python-flask-debugtoolbar
|
||||
```
|
||||
|
|
|
@ -4,13 +4,13 @@ from flask_migrate import Migrate
|
|||
from flask_login import LoginManager
|
||||
from flask_mail import Mail
|
||||
from flask_wtf.csrf import CSRFProtect
|
||||
from config import Config
|
||||
from config import FlaskApplicationSettings, V5Config
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(Config)
|
||||
app.config.from_object(FlaskApplicationSettings)
|
||||
|
||||
# Check security of secret
|
||||
if Config.SECRET_KEY == "a-random-secret-key":
|
||||
if FlaskApplicationSettings.SECRET_KEY == "a-random-secret-key":
|
||||
raise Exception("Please use a strong secret key!")
|
||||
|
||||
db = SQLAlchemy(app)
|
||||
|
@ -36,3 +36,9 @@ from app.utils import filters
|
|||
|
||||
# Register processors
|
||||
from app import processors
|
||||
|
||||
# Enable flask-debug-toolbar if requested
|
||||
if V5Config.ENABLE_FLASK_DEBUG_TOOLBAR:
|
||||
from flask_debugtoolbar import DebugToolbarExtension
|
||||
app.config['DEBUG_TB_PROFILER_ENABLED'] = True
|
||||
toolbar = DebugToolbarExtension(app)
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
from werkzeug.utils import secure_filename
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import backref
|
||||
from app import db
|
||||
from app.utils.filesize import filesize
|
||||
from config import V5Config
|
||||
import os
|
||||
import uuid
|
||||
|
||||
|
||||
class Attachment(db.Model):
|
||||
__tablename__ = 'attachment'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
|
||||
# Original name of the file
|
||||
name = db.Column(db.Unicode(64))
|
||||
|
@ -15,7 +18,8 @@ class Attachment(db.Model):
|
|||
# The comment linked with
|
||||
comment_id = db.Column(db.Integer, db.ForeignKey('comment.id'),
|
||||
nullable=False, index=True)
|
||||
comment = db.relationship('Comment', backref=backref('attachments'))
|
||||
comment = db.relationship('Comment', back_populates='attachments',
|
||||
foreign_keys=comment_id)
|
||||
|
||||
# The size of the file
|
||||
size = db.Column(db.Integer)
|
||||
|
@ -24,11 +28,11 @@ class Attachment(db.Model):
|
|||
@property
|
||||
def path(self):
|
||||
return os.path.join(V5Config.DATA_FOLDER, "attachments",
|
||||
f"{self.id:05}", self.name)
|
||||
f"{self.id}", self.name)
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return f"/fichiers/{self.id:05}/{self.name}"
|
||||
return f"/fichiers/{self.id}/{self.name}"
|
||||
|
||||
|
||||
def __init__(self, file, comment):
|
||||
|
|
|
@ -20,7 +20,8 @@ class Comment(Post):
|
|||
backref=backref('comments', lazy='dynamic'),
|
||||
foreign_keys=thread_id)
|
||||
|
||||
# attachments (relation from Attachment)
|
||||
attachments = db.relationship('Attachment', back_populates='comment',
|
||||
lazy='joined')
|
||||
|
||||
@property
|
||||
def is_top_comment(self):
|
||||
|
|
|
@ -16,16 +16,18 @@ class SpecialPrivilege(db.Model):
|
|||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
# Member that is granted the privilege
|
||||
mid = db.Column(db.Integer, db.ForeignKey('member.id'), index=True)
|
||||
member_id = db.Column(db.Integer, db.ForeignKey('member.id'), index=True)
|
||||
member = db.relationship('Member', back_populates="special_privs",
|
||||
foreign_keys=member_id)
|
||||
# Privilege name
|
||||
priv = db.Column(db.String(64))
|
||||
|
||||
def __init__(self, member, priv):
|
||||
self.mid = member.id
|
||||
self.member = member
|
||||
self.priv = priv
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Privilege: {self.priv} of member #{self.mid}>'
|
||||
return f'<Privilege: {self.priv} of member #{self.member_id}>'
|
||||
|
||||
|
||||
# Group: User group, corresponds to a community role and a set of privileges
|
||||
|
@ -43,7 +45,9 @@ class Group(db.Model):
|
|||
description = db.Column(db.UnicodeText)
|
||||
# List of members (lambda delays evaluation)
|
||||
members = db.relationship('Member', secondary=lambda: GroupMember,
|
||||
back_populates='groups')
|
||||
back_populates='groups', lazy='joined')
|
||||
# List of privileges
|
||||
privileges = db.relationship('GroupPrivilege', back_populates='group')
|
||||
|
||||
def __init__(self, name, css, descr):
|
||||
self.name = name
|
||||
|
@ -57,7 +61,7 @@ class Group(db.Model):
|
|||
* Group privileges
|
||||
"""
|
||||
|
||||
for gp in GroupPrivilege.query.filter_by(gid=self.id).all():
|
||||
for gp in self.privileges:
|
||||
db.session.delete(gp)
|
||||
db.session.commit()
|
||||
|
||||
|
@ -65,8 +69,7 @@ class Group(db.Model):
|
|||
db.session.commit()
|
||||
|
||||
def privs(self):
|
||||
gps = GroupPrivilege.query.filter_by(gid=self.id).all()
|
||||
return sorted(gp.priv for gp in gps)
|
||||
return sorted(gp.priv for gp in self.privileges)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Group: {self.name}>'
|
||||
|
@ -77,15 +80,17 @@ GroupMember = db.Table('group_member', db.Model.metadata,
|
|||
db.Column('gid', db.Integer, db.ForeignKey('group.id')),
|
||||
db.Column('uid', db.Integer, db.ForeignKey('member.id')))
|
||||
|
||||
|
||||
# Many-to-many relationship for privileges granted to groups
|
||||
# GroupPrivilege: A list of privileges for groups, materialized as a table
|
||||
class GroupPrivilege(db.Model):
|
||||
__tablename__ = 'group_privilege'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
gid = db.Column(db.Integer, db.ForeignKey('group.id'))
|
||||
group_id = db.Column(db.Integer, db.ForeignKey('group.id'))
|
||||
group = db.relationship('Group', back_populates='privileges',
|
||||
foreign_keys=group_id)
|
||||
|
||||
priv = db.Column(db.String(64))
|
||||
|
||||
def __init__(self, group, priv):
|
||||
self.gid = group.id
|
||||
self.group = group
|
||||
self.priv = priv
|
||||
|
|
|
@ -53,6 +53,13 @@ class Thread(db.Model):
|
|||
return self.owner_program[0]
|
||||
return None
|
||||
|
||||
def is_default_accessible(self):
|
||||
if self.owner_program != []:
|
||||
return True
|
||||
if self.owner_topic != []:
|
||||
return self.owner_topic[0].forum.is_default_accessible()
|
||||
return False
|
||||
|
||||
def delete(self):
|
||||
"""Recursively delete thread and all associated contents."""
|
||||
# Remove reference to top comment
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
from datetime import date
|
||||
from flask import url_for
|
||||
from flask_login import UserMixin
|
||||
from sqlalchemy import func as SQLfunc
|
||||
from os.path import isfile
|
||||
from PIL import Image
|
||||
import werkzeug.security
|
||||
|
||||
from app import app, db
|
||||
from app.models.priv import SpecialPrivilege, Group, GroupMember, \
|
||||
|
@ -19,7 +18,9 @@ import app.utils.ldap as ldap
|
|||
from app.utils.unicode_names import normalize
|
||||
from config import V5Config
|
||||
|
||||
import werkzeug.security
|
||||
from os.path import isfile
|
||||
from datetime import date
|
||||
from PIL import Image
|
||||
import math
|
||||
import app
|
||||
import os
|
||||
|
@ -89,19 +90,9 @@ class Member(User):
|
|||
xp = db.Column(db.Integer)
|
||||
register_date = db.Column(db.Date, default=date.today)
|
||||
|
||||
avatar_id = db.Column(db.Integer, default=0)
|
||||
@property
|
||||
def avatar(self):
|
||||
return f'{self.id}_{self.avatar_id}.png'
|
||||
|
||||
@property
|
||||
def level(self):
|
||||
level = math.asinh(self.xp / 1000) * (100 / math.asinh(10))
|
||||
return int(level), int(level * 100) % 100
|
||||
|
||||
# Groups and related privileges
|
||||
groups = db.relationship('Group', secondary=GroupMember,
|
||||
back_populates='members')
|
||||
back_populates='members', lazy='joined')
|
||||
|
||||
# Personal information, all optional
|
||||
bio = db.Column(db.UnicodeText)
|
||||
|
@ -115,11 +106,21 @@ class Member(User):
|
|||
# Settings
|
||||
newsletter = db.Column(db.Boolean, default=False)
|
||||
theme = db.Column(db.Unicode(32))
|
||||
avatar_id = db.Column(db.Integer, default=0)
|
||||
|
||||
# Relations
|
||||
trophies = db.relationship('Trophy', secondary=TrophyMember,
|
||||
back_populates='owners')
|
||||
|
||||
# Specially-offered privileges (use self.special_privileges())
|
||||
special_privs = db.relationship('SpecialPrivilege',
|
||||
back_populates='member', lazy='joined')
|
||||
|
||||
|
||||
# Other fields populated automatically through relations:
|
||||
# <notifications> List of unseen notifications (of type Notification)
|
||||
# <polls> Polls created by the member (of class Poll)
|
||||
|
||||
# Access to polymorphic posts
|
||||
# TODO: Check that the query uses the double index on Post.{author_id,type}
|
||||
def comments(self):
|
||||
|
@ -129,9 +130,22 @@ class Member(User):
|
|||
def programs(self):
|
||||
return db.session.query(Program).filter(Post.author_id==self.id).all()
|
||||
|
||||
# Other fields populated automatically through relations:
|
||||
# <notifications> List of unseen notifications (of type Notification)
|
||||
# <polls> Polls created by the member (of class Poll)
|
||||
@property
|
||||
def avatar_filename(self):
|
||||
return f'{self.id}_{self.avatar_id}.png'
|
||||
|
||||
@property
|
||||
def avatar_url(self):
|
||||
if self.avatar_id == 0:
|
||||
return url_for('static', filename='images/default_avatar.png')
|
||||
else:
|
||||
return url_for('avatar',filename=self.avatar_filename)
|
||||
|
||||
@property
|
||||
def level(self):
|
||||
level = math.asinh(self.xp / 1000) * (100 / math.asinh(10))
|
||||
return int(level), int(level * 100) % 100
|
||||
|
||||
|
||||
def __init__(self, name, email, password):
|
||||
"""Register a new user."""
|
||||
|
@ -185,7 +199,7 @@ class Member(User):
|
|||
Deletes the user, but not the posts; use either transfer_posts() or
|
||||
delete_posts() before calling this.
|
||||
"""
|
||||
for sp in SpecialPrivilege.query.filter_by(mid=self.id).all():
|
||||
for sp in self.special_privs:
|
||||
db.session.delete(sp)
|
||||
|
||||
self.trophies = []
|
||||
|
@ -198,17 +212,16 @@ class Member(User):
|
|||
|
||||
def priv(self, priv):
|
||||
"""Check whether the member has the specified privilege."""
|
||||
if SpecialPrivilege.query.filter_by(mid=self.id, priv=priv).first():
|
||||
if priv in self.special_privs:
|
||||
return True
|
||||
return db.session.query(Group, GroupPrivilege).filter(
|
||||
Group.id.in_([g.id for g in self.groups]),
|
||||
GroupPrivilege.gid==Group.id,
|
||||
GroupPrivilege.priv==priv).first() is not None
|
||||
for g in self.groups:
|
||||
if priv in g.privs():
|
||||
return True
|
||||
return False
|
||||
|
||||
def special_privileges(self):
|
||||
"""List member's special privileges."""
|
||||
sp = SpecialPrivilege.query.filter_by(mid=self.id).all()
|
||||
return sorted(row.priv for row in sp)
|
||||
return sorted(self.special_privs)
|
||||
|
||||
def can_access_forum(self, forum):
|
||||
"""Whether this member can read the forum's contents."""
|
||||
|
@ -250,6 +263,10 @@ class Member(User):
|
|||
post = comment.thread.owner_post
|
||||
return self.can_edit_post(post) and (comment.author == post.author)
|
||||
|
||||
def can_access_file(self, file):
|
||||
"""Whether this member can access the file."""
|
||||
return self.can_access_post(file.comment)
|
||||
|
||||
def update(self, **data):
|
||||
"""
|
||||
Update all or part of the user's metadata. The [data] dictionary
|
||||
|
@ -308,20 +325,22 @@ class Member(User):
|
|||
|
||||
def set_avatar(self, avatar):
|
||||
# Save old avatar filepath
|
||||
old_avatar = os.path.join(V5Config.DATA_FOLDER, "avatars", self.avatar)
|
||||
old_avatar = os.path.join(V5Config.DATA_FOLDER, "avatars",
|
||||
self.avatar_filename)
|
||||
# Resize & convert image
|
||||
size = 128, 128
|
||||
im = Image.open(avatar)
|
||||
im.thumbnail(size, Image.ANTIALIAS)
|
||||
im.thumbnail((128, 128), Image.ANTIALIAS)
|
||||
|
||||
# Change avatar id
|
||||
# TODO: verify concurrency behavior
|
||||
current_id = db.session.query(SQLfunc.max(Member.avatar_id)).first()[0]
|
||||
self.avatar_id = current_id + 1
|
||||
db.session.merge(self)
|
||||
db.session.commit()
|
||||
|
||||
# Save the new avatar
|
||||
im.save(os.path.join(V5Config.DATA_FOLDER, "avatars", self.avatar),
|
||||
'PNG')
|
||||
im.save(os.path.join(V5Config.DATA_FOLDER, "avatars",
|
||||
self.avatar_filename), 'PNG')
|
||||
# If nothing has failed, remove old one (allow failure to regularize
|
||||
# exceptional situations like missing avatar or folder migration)
|
||||
try:
|
||||
|
@ -549,8 +568,7 @@ class Member(User):
|
|||
# TODO: Trophy "actif"
|
||||
|
||||
if context in ["on-profile-update", None]:
|
||||
if isfile(os.path.join(
|
||||
V5Config.DATA_FOLDER, "avatars", self.avatar)):
|
||||
if self.avatar_id != 0:
|
||||
self.add_trophy("Artiste")
|
||||
else:
|
||||
self.del_trophy("Artiste")
|
||||
|
|
|
@ -32,5 +32,18 @@ def menu_processor():
|
|||
|
||||
last_active_topics = list(filter(f, last_active_topics))[:10]
|
||||
|
||||
# Constructing last news
|
||||
raw = db.session.execute( """SELECT topic.id FROM topic
|
||||
INNER JOIN forum ON topic.forum_id = forum.id
|
||||
INNER JOIN comment ON topic.thread_id = comment.thread_id
|
||||
INNER JOIN post ON post.id = comment.id
|
||||
WHERE forum.url LIKE '/actus%'
|
||||
GROUP BY topic.id
|
||||
ORDER BY MIN(post.date_created) DESC
|
||||
LIMIT 10;
|
||||
""")
|
||||
last_news = [Topic.query.get(id) for id in raw]
|
||||
|
||||
return dict(login_form=login_form, search_form=search_form,
|
||||
main_forum=main_forum, last_active_topics=last_active_topics)
|
||||
main_forum=main_forum, last_active_topics=last_active_topics,
|
||||
last_news=last_news)
|
||||
|
|
|
@ -15,7 +15,7 @@ def adm_groups():
|
|||
# Users with either groups or special privileges
|
||||
users_groups = Member.query.join(GroupMember)
|
||||
users_special = Member.query \
|
||||
.join(SpecialPrivilege, Member.id == SpecialPrivilege.mid)
|
||||
.join(SpecialPrivilege, Member.id == SpecialPrivilege.member_id)
|
||||
users = users_groups.union(users_special)
|
||||
users = sorted(users, key = lambda x: x.name)
|
||||
|
||||
|
|
|
@ -2,9 +2,11 @@ from app import app
|
|||
from app.utils.filters.markdown import md
|
||||
from flask import request, abort
|
||||
from werkzeug.exceptions import BadRequestKeyError
|
||||
from app import csrf
|
||||
|
||||
class API():
|
||||
@app.route("/api/markdown", methods=["POST"])
|
||||
@csrf.exempt
|
||||
def api_markdown():
|
||||
try:
|
||||
markdown = request.get_json()['text']
|
||||
|
|
|
@ -11,9 +11,7 @@ import os
|
|||
def avatar(filename):
|
||||
filename = secure_filename(filename) # No h4ckers allowed
|
||||
filepath = os.path.join(V5Config.DATA_FOLDER, "avatars", filename)
|
||||
if os.path.isfile(filepath):
|
||||
return send_file(filepath)
|
||||
return redirect(url_for('static', filename='images/default_avatar.png'))
|
||||
return send_file(filepath)
|
||||
|
||||
@app.route('/fichiers/<path>/<name>')
|
||||
def attachment(path, name):
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
#flDebug * {
|
||||
overflow: auto !important;
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
/* Some styles to enhance debugger */
|
||||
#flDebug * {
|
||||
overflow: auto !important;
|
||||
}
|
|
@ -12,8 +12,9 @@ function editor_event_source(event)
|
|||
usually reports the SVG in the button as the source */
|
||||
let node = event.target || event.srcElement;
|
||||
while(node != document.body) {
|
||||
if(node.tagName == "BUTTON" && !button)
|
||||
if(node.tagName == "BUTTON" && !button) {
|
||||
button = node;
|
||||
}
|
||||
if(node.classList.contains("editor") && !editor) {
|
||||
editor = node;
|
||||
break;
|
||||
|
@ -123,10 +124,11 @@ function editor_set_title(event, level, diff)
|
|||
});
|
||||
}
|
||||
|
||||
// Tab insert some spaces
|
||||
// Ctrl+Enter send the form
|
||||
previewTimeout = null;
|
||||
ta = document.querySelector(".editor textarea");
|
||||
ta.addEventListener('keydown', function(e) {
|
||||
// Tab insert some spaces
|
||||
// Ctrl+Enter send the form
|
||||
let keyCode = e.keyCode || e.which;
|
||||
if (keyCode == 9) {
|
||||
// TODO Add one tab to selected text without replacing it
|
||||
|
@ -149,4 +151,39 @@ ta.addEventListener('keydown', function(e) {
|
|||
t.submit.click();
|
||||
}
|
||||
}
|
||||
|
||||
// Set a timeout for refreshing the preview
|
||||
if (previewTimeout != null) {
|
||||
clearTimeout(previewTimeout);
|
||||
}
|
||||
previewTimeout = setTimeout(preview, 1000);
|
||||
});
|
||||
|
||||
|
||||
function preview() {
|
||||
const previewArea = document.querySelector("#editor_content_preview");
|
||||
|
||||
const textarea = document.querySelector(".editor textarea");
|
||||
const payload = {text: ta.value};
|
||||
|
||||
const headers = new Headers();
|
||||
headers.append("Content-Type", "application/json");
|
||||
|
||||
const params = {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
headers
|
||||
};
|
||||
|
||||
console.log(payload);
|
||||
|
||||
fetch("/api/markdown", params).then(
|
||||
(response) => {
|
||||
response.text().then(
|
||||
(text) => {
|
||||
previewArea.innerHTML = text;
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<div>
|
||||
{{ form.avatar.label }}
|
||||
<div>
|
||||
<img class="avatar" src="{{ url_for('avatar', filename=current_user.avatar) }}" meta="{{ current_user.avatar }}" />
|
||||
<img class="avatar" src="{{ current_user.avatar_url }}" />
|
||||
{{ form.avatar }}
|
||||
</div>
|
||||
{% for error in form.avatar.errors %}
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
<div>
|
||||
{{ form.avatar.label }}
|
||||
<div>
|
||||
<img class="avatar" src="{{ url_for('avatar', filename=user.avatar) }}" meta="{{ user.avatar }}" />
|
||||
<img class="avatar" src="{{ user.avatar_url }}" />
|
||||
{{ form.avatar }}
|
||||
</div>
|
||||
{% for error in form.avatar.errors %}
|
||||
|
|
|
@ -61,7 +61,7 @@
|
|||
<li>
|
||||
{% if current_user.is_authenticated %}
|
||||
<a href="{{ url_for('user', username=current_user.name) }}" role="button" label="Compte" tabindex="0">
|
||||
<img src="{{ url_for('avatar', filename=current_user.avatar) }}" alt="Avatar de {{ current_user.name }}">
|
||||
<img src="{{ current_user.avatar_url }}" alt="Avatar de {{ current_user.name }}">
|
||||
<div>Compte</div>
|
||||
</a>
|
||||
{% else %}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div>
|
||||
<h2>
|
||||
<a href="{{ url_for('user', username=current_user.name) }}">
|
||||
<img src="{{ url_for('avatar', filename=current_user.avatar) }}" alt="Avatar de {{ current_user.name }}"></a>
|
||||
<img src="{{ current_user.avatar_url }}" alt="Avatar de {{ current_user.name }}"></a>
|
||||
<a href="{{ url_for('user', username=current_user.name) }}">
|
||||
{{ current_user.name }}</a>
|
||||
</h2>
|
||||
|
|
|
@ -20,10 +20,8 @@
|
|||
|
||||
<h3>Derniers topics actifs</h3>
|
||||
<ul>
|
||||
{% for t in last_active_topics %}
|
||||
<li>
|
||||
<a href="{{ url_for('forum_topic', f=t.forum, page=(t,'fin'))}}">{{ t.title }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% for t in last_active_topics %}
|
||||
<li><a href="{{ url_for('forum_topic', f=t.forum, page=(t,'fin'))}}">{{ t.title }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -18,10 +18,8 @@
|
|||
|
||||
<h3>Derniers articles</h3>
|
||||
<ul>
|
||||
<li><a href="#">Un nouvel OS pour les Graph 75</a></li>
|
||||
<li><a href="#">Les 7 Days CPC arrivent bientôt</a></li>
|
||||
<li><a href="#">Résultats de jeu du mois de Février 2017</a></li>
|
||||
<li><a href="#">Test du shield relai Sainsmart pour Arduino</a></li>
|
||||
<li><a href="#">Un nouveau tutoriel sur le C-engine</a></li>
|
||||
{% for t in last_news %}
|
||||
<li><a href="{{ url_for('forum_topic', f=t.forum, page=(t,'fin'))}}">{{ t.title }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -121,14 +121,17 @@
|
|||
{{ field.label if label }}
|
||||
{{ field() }}
|
||||
|
||||
<!-- Load the script -->
|
||||
<script>
|
||||
window.addEventListener("load", function(){});
|
||||
</script>
|
||||
<div id="editor_content_preview">
|
||||
</div>
|
||||
|
||||
<!-- Display errors -->
|
||||
{% for error in field.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
|
||||
<!-- Load the script -->
|
||||
<script>
|
||||
window.addEventListener("load", function(){});
|
||||
</script>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{% macro profile(user) %}
|
||||
{% if user.type == "member" %}
|
||||
<div class="profile">
|
||||
<img class="profile-avatar" src="{{ url_for('avatar', filename=user.avatar) }}" alt="Avatar de {{ user.name }}">
|
||||
<img class="profile-avatar" src="{{ user.avatar_url }}" alt="Avatar de {{ user.name }}">
|
||||
<div>
|
||||
<div class="profile-name"><a href="{{ url_for('user', username=user.name) }}">{{ user.name }}</a></div>
|
||||
{% if user.title %}
|
||||
|
|
|
@ -13,8 +13,10 @@ License: [BSD](https://opensource.org/licenses/bsd-license.php)
|
|||
|
||||
from markdown.extensions import Extension
|
||||
from markdown.inlinepatterns import InlineProcessor
|
||||
from uuid import UUID
|
||||
import xml.etree.ElementTree as etree
|
||||
from flask import url_for, render_template
|
||||
from flask_login import current_user
|
||||
from app.utils.unicode_names import normalize
|
||||
from app.utils.filters.humanize import humanize
|
||||
from app.models.poll import Poll
|
||||
|
@ -36,7 +38,7 @@ class PCLinkExtension(Extension):
|
|||
self.md = md
|
||||
|
||||
# append to end of inline patterns
|
||||
PCLINK_RE = r'<([a-z]+): ?(\w+)>'
|
||||
PCLINK_RE = r'<([a-z]+): ?([\w-]+)>'
|
||||
pclinkPattern = PCLinksInlineProcessor(PCLINK_RE, self.getConfigs())
|
||||
pclinkPattern.md = md
|
||||
md.inlinePatterns.register(pclinkPattern, 'pclink', 135)
|
||||
|
@ -125,7 +127,7 @@ def handleUser(content_id, context):
|
|||
|
||||
def handleFile(content_id, context):
|
||||
try:
|
||||
content_id = int(content_id)
|
||||
UUID(content_id)
|
||||
except ValueError:
|
||||
return "[ID de fichier invalide]"
|
||||
|
||||
|
@ -134,6 +136,16 @@ def handleFile(content_id, context):
|
|||
if file is None:
|
||||
return "[Fichier non trouvé]"
|
||||
|
||||
# Manage permissions
|
||||
# TODO: use Guest methods when implemented
|
||||
if current_user.is_authenticated:
|
||||
if not current_user.can_access_file(file):
|
||||
return "[Accès au fichier refusé]"
|
||||
else:
|
||||
if not file.comment.thread.is_default_accessible():
|
||||
return "[Accès au fichier refusé]"
|
||||
|
||||
|
||||
html = render_template('widgets/download_button.html', file=file)
|
||||
html = html.replace('\n', '') # Needed to avoid lots of <br> due to etree
|
||||
return etree.fromstring(html)
|
||||
|
|
|
@ -17,6 +17,7 @@ def render(*args, styles=[], scripts=[], **kwargs):
|
|||
'css/table.css',
|
||||
'css/pagination.css',
|
||||
'css/editor.css',
|
||||
'css/debugger.css',
|
||||
]
|
||||
scripts_ = [
|
||||
'scripts/trigger_menu.js',
|
||||
|
|
33
config.py
33
config.py
|
@ -6,11 +6,28 @@ try:
|
|||
except ImportError:
|
||||
print(" \033[92mWARNING: Local config not found\033[0m")
|
||||
|
||||
# The LocalConfig class serves a dual purpose and overrides settings in
|
||||
# both V5Config and some fields of FlaskApplicationSettings. The first has
|
||||
# defaults (DefaultConfig), but not the second, so we provide them here.
|
||||
class LocalConfig():
|
||||
pass
|
||||
FLASK_DEBUG = False
|
||||
DB_NAME = "pcv5"
|
||||
SECRET_KEY = "a-random-secret-key"
|
||||
|
||||
#---
|
||||
# Flask configuration
|
||||
#---
|
||||
|
||||
class FlaskApplicationSettings(object):
|
||||
"""
|
||||
This object specifies the settings for the Flask application. All the keys
|
||||
and values are predefined by Flask.
|
||||
|
||||
See: https://flask.palletsprojects.com/en/2.1.x/config/
|
||||
"""
|
||||
|
||||
DEBUG = os.environ.get("FLASK_DEBUG") or LocalConfig.FLASK_DEBUG
|
||||
|
||||
class Config(object):
|
||||
SECRET_KEY = os.environ.get('SECRET_KEY') or LocalConfig.SECRET_KEY
|
||||
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
|
||||
'postgresql+psycopg2://' + os.environ.get('USER') + ':@/' \
|
||||
|
@ -27,9 +44,11 @@ class Config(object):
|
|||
# Do not attach cookies to cross-origin requests
|
||||
SESSION_COOKIE_SAMESITE = "Lax"
|
||||
|
||||
#---
|
||||
# v5 configuration
|
||||
#---
|
||||
|
||||
class DefaultConfig(object):
|
||||
"""Every value here can be overrided in the local_config.py class"""
|
||||
# Domain
|
||||
DOMAIN = "v5.planet-casio.com"
|
||||
# Database name
|
||||
|
@ -59,8 +78,14 @@ class DefaultConfig(object):
|
|||
# is computed in the page header, so it doesn't account for most of the
|
||||
# template generation.
|
||||
SLOW_REQUEST_THRESHOLD = 0.400 # s
|
||||
# Whether to enable flask-debug-toolbar
|
||||
ENABLE_FLASK_DEBUG_TOOLBAR = False
|
||||
|
||||
|
||||
class V5Config(LocalConfig, DefaultConfig):
|
||||
# Values put here cannot be overidden with local_config
|
||||
"""
|
||||
This object holds the settings for the v5 code. Each parameter has the
|
||||
value specified in LocalConfig (if any), and defaults to the value set in
|
||||
DefaultConfig.
|
||||
"""
|
||||
pass
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
#!/bin/bash
|
||||
|
||||
modifiedless=$(make -n css | grep less)
|
||||
if [[ -n "$modifiedless" ]]
|
||||
then
|
||||
echo -e "\e[33;1m[error] remember to run 'make css' before commiting\e[0m"
|
||||
exit 1
|
||||
fi
|
|
@ -1,12 +0,0 @@
|
|||
# This is a sample file
|
||||
# You can override every value available in the class DefaultConfig
|
||||
|
||||
class LocalConfig(object):
|
||||
DB_NAME = "pcv5"
|
||||
USE_LDAP = True
|
||||
LDAP_PASSWORD = "openldap"
|
||||
LDAP_ENV = "o=prod"
|
||||
SECRET_KEY = "a-random-secret-key" # CHANGE THIS VALUE *NOW*
|
||||
AVATARS_FOLDER = '/home/pc/data/avatars/'
|
||||
ENABLE_GUEST_POST = True
|
||||
SEND_MAILS = True
|
|
@ -78,11 +78,11 @@ def update_groups():
|
|||
# Update an existing group
|
||||
if g is not None:
|
||||
changes = (g.css != css) or (g.description != descr) or \
|
||||
(set(g.privs()) != set(privs))
|
||||
(set(g.privs) != set(privs))
|
||||
g.css = css
|
||||
g.description = descr
|
||||
|
||||
for gpriv in GroupPrivilege.query.filter_by(gid=g.id):
|
||||
for gpriv in g.privs:
|
||||
db.session.delete(gpriv)
|
||||
for priv in privs:
|
||||
db.session.add(GroupPrivilege(g, priv))
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
"""special privileges as a relationship
|
||||
|
||||
Revision ID: 44fbbb1fd537
|
||||
Revises: 72df33816b21
|
||||
Create Date: 2022-05-12 19:15:15.592896
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '44fbbb1fd537'
|
||||
down_revision = '72df33816b21'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('special_privilege', sa.Column('member_id', sa.Integer(), nullable=True))
|
||||
op.drop_index('ix_special_privilege_mid', table_name='special_privilege')
|
||||
op.create_index(op.f('ix_special_privilege_member_id'), 'special_privilege', ['member_id'], unique=False)
|
||||
op.drop_constraint('special_privilege_mid_fkey', 'special_privilege', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'special_privilege', 'member', ['member_id'], ['id'])
|
||||
op.drop_column('special_privilege', 'mid')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('special_privilege', sa.Column('mid', sa.INTEGER(), autoincrement=False, nullable=True))
|
||||
op.drop_constraint(None, 'special_privilege', type_='foreignkey')
|
||||
op.create_foreign_key('special_privilege_mid_fkey', 'special_privilege', 'member', ['mid'], ['id'])
|
||||
op.drop_index(op.f('ix_special_privilege_member_id'), table_name='special_privilege')
|
||||
op.create_index('ix_special_privilege_mid', 'special_privilege', ['mid'], unique=False)
|
||||
op.drop_column('special_privilege', 'member_id')
|
||||
# ### end Alembic commands ###
|
|
@ -0,0 +1,34 @@
|
|||
"""Switched attachment id to UUID
|
||||
|
||||
Revision ID: 72df33816b21
|
||||
Revises: d2227d2479e2
|
||||
Create Date: 2022-04-26 21:50:05.466388
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '72df33816b21'
|
||||
down_revision = 'd2227d2479e2'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# Create uuid-ossp extension, required to use uuid_generate_v4()
|
||||
op.execute('CREATE EXTENSION IF NOT EXISTS "uuid-ossp";')
|
||||
# /!\ This operation will break all attachments /!\
|
||||
op.execute("""ALTER TABLE attachment ALTER COLUMN id DROP DEFAULT,
|
||||
ALTER COLUMN id SET DATA TYPE UUID USING (uuid_generate_v4()),
|
||||
ALTER COLUMN id SET DEFAULT uuid_generate_v4();""")
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# /!\ This operation will break all attachments /!\
|
||||
op.execute("""ALTER TABLE attachment ALTER COLUMN id DROP DEFAULT,
|
||||
ALTER COLUMN id SET DATA TYPE integer USING nextval('attachment_id_seq'::regclass),
|
||||
ALTER COLUMN id SET DEFAULT nextval('attachment_id_seq'::regclass);""")
|
||||
# ### end Alembic commands ###
|
|
@ -0,0 +1,25 @@
|
|||
"""relate groups with their privileges through a relationship
|
||||
|
||||
Revision ID: fcf53f1a14e3
|
||||
Revises: 44fbbb1fd537
|
||||
Create Date: 2022-05-12 19:43:04.436448
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'fcf53f1a14e3'
|
||||
down_revision = '44fbbb1fd537'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# Actually modified by hand for once - Lephe'
|
||||
op.alter_column('group_privilege', 'gid', new_column_name='group_id')
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.alter_column('group_privilege', 'group_id', new_column_name='gid')
|
Loading…
Reference in New Issue