Compare commits

...

13 Commits

Author SHA1 Message Date
Eragon f5573c0900
editor: Add preview 2022-05-12 23:02:40 +02:00
Eragon e6ae16c43d
Merge remote-tracking branch 'origin/dev' into new_editor 2022-05-12 22:02:33 +02:00
Lephe 8393cf1933
perf: eagerly load auxiliary data 2022-05-12 20:45:31 +01:00
Darks b7dc2ebbf2
hook: add reminder to build css before commiting when needed 2022-05-12 21:39:10 +02:00
Lephe 38c4f274a0
perf: optimize away more privilege requests
[MIGRATION] This commit contains a new version of the schema.
2022-05-12 20:00:24 +01:00
Lephe 7d9e897ae9
perf: optimize away special privilege requests by lazy loading 2022-05-12 19:24:17 +01:00
Lephe 1040d57506
css: fully recompile LESS files 2022-05-12 19:24:17 +01:00
Darks f64e3a2c39
debugger: add some style to enhance it 2022-05-12 20:07:28 +02:00
Lephe 8f620c6150
meta: add optional setting for flask-debug-toolbar
It provides profiling information and an overview of SQL requests while
in development.
2022-05-05 20:33:46 +01:00
Lephe 5a87d29c7f
account: make default avatar selection less hacky 2022-05-05 20:33:45 +01:00
Lephe a3ed633791
config: update and slight improvements
* Rename Config → FlaskApplicationSettings so we know exactly what we're
  talking about
* Clarify that LocalConfig overrides both V5Config and Flask settings
* Only give defaults that are needed in LocalConfig and remove old
  settings that are no longer used
2022-05-05 20:33:45 +01:00
Darks eb5ce1bd5c
attachement: switch to uuid + check permission in dl widget (#109)
Also added is_default_accessible() to Thread class as its owner may be a 
Topic with forum access restrictions or public main content (like 
Program)

[MIGRATION] This commit contains a new version of the schema. /!\ This 
migration breaks all attachments
2022-04-26 23:29:11 +02:00
Darks faf5bd184d
navbar: properly generate links to recent topics 2022-04-26 20:40:56 +02:00
31 changed files with 332 additions and 99 deletions

View File

@ -24,3 +24,8 @@ python-pillow
python-pyyaml
python-slugify
```
Optionnel:
```
python-flask-debugtoolbar
```

View File

@ -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)

View File

@ -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):

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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")

View File

@ -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)

View File

@ -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)

View File

@ -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']

View File

@ -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):

View File

@ -0,0 +1,3 @@
#flDebug * {
overflow: auto !important;
}

View File

@ -0,0 +1,4 @@
/* Some styles to enhance debugger */
#flDebug * {
overflow: auto !important;
}

View File

@ -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;
}
);
});
}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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)

View File

@ -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',

View File

@ -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

8
hooks/pre-commit Executable file
View File

@ -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

View File

@ -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

View File

@ -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))

View File

@ -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 ###

View File

@ -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 ###

View File

@ -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')