Merge branch 'dev' into preprod

This commit is contained in:
Darks 2020-08-01 21:54:44 +02:00
commit 9d9aee6565
Signed by untrusted user: Darks
GPG key ID: F61F10FA138E797C
39 changed files with 613 additions and 80 deletions

View file

@ -28,8 +28,8 @@ app.url_map.converters['topicpage'] = TopicPageConverter
# Register routes
from app import routes
# Register utils
from app import utils
# Register filters
from app.utils import filters
# Register processors
from app import processors

View file

@ -11,7 +11,7 @@
shoutbox-kick shoutbox-ban
unlimited-pms footer-statistics community-login
access-admin-panel edit-account delete-account edit-trophies
delete_notification
delete_notification no-upload-limits
-
name: Modérateur
css: "color: green;"
@ -21,7 +21,7 @@
move-public-content extract-posts
delete-notes delete-tests
shoutbox-kick shoutbox-ban
unlimited-pms
unlimited-pms no-upload-limits
-
name: Développeur
css: "color: #4169e1;"
@ -31,7 +31,7 @@
scheduled-posting
edit-static-content
unlimited-pms footer-statistics community-login
access-admin-panel
access-admin-panel no-upload-limits
-
name: Rédacteur
css: "color: blue;"
@ -41,6 +41,7 @@
upload-shared-files delete-shared-files
scheduled-posting
showcase-content edit-static-content
no-upload-limits
-
name: Responsable communauté
css: "color: DarkOrange;"

View file

@ -1,23 +1,32 @@
from flask_wtf import FlaskForm
from wtforms import StringField, FormField, SubmitField, TextAreaField
from wtforms.validators import DataRequired, Length
from wtforms import StringField, FormField, SubmitField, TextAreaField, \
MultipleFileField
from wtforms.validators import DataRequired, Length, Optional
import app.utils.validators as vd
class TopicCreationForm(FlaskForm):
title = StringField('Nom du sujet',
validators=[DataRequired(), Length(min=3, max=32)])
message = TextAreaField('Message principal', validators=[DataRequired()])
submit = SubmitField('Créer le sujet')
class AnonymousTopicCreationForm(TopicCreationForm):
pseudo = StringField('Pseudo',
validators=[DataRequired(), vd.name_valid, vd.name_available])
class CommentForm(FlaskForm):
message = TextAreaField('Commentaire', validators=[DataRequired()])
message = TextAreaField('Message', validators=[DataRequired()])
attachments = MultipleFileField('Pièces-jointes',
validators=[vd.file.optional, vd.file.count, vd.file.extension,
vd.file.size])
submit = SubmitField('Commenter')
preview = SubmitField('Prévisualiser')
class AnonymousCommentForm(CommentForm):
pseudo = StringField('Pseudo',
validators=[DataRequired(), vd.name_valid, vd.name_available])
class CommentEditForm(CommentForm):
submit = SubmitField('Modifier')
class AnonymousCommentEditForm(CommentEditForm, AnonymousCommentForm):
pass
class TopicCreationForm(CommentForm):
title = StringField('Nom du sujet',
validators=[DataRequired(), Length(min=3, max=32)])
submit = SubmitField('Créer le sujet')
class AnonymousTopicCreationForm(TopicCreationForm, AnonymousCommentForm):
pass

View file

@ -3,3 +3,4 @@ from app.models.thread import Thread
from app.models.forum import Forum
from app.models.topic import Topic
from app.models.notification import Notification
from app.models.program import Program

50
app/models/attachment.py Normal file
View file

@ -0,0 +1,50 @@
from werkzeug.utils import secure_filename
from sqlalchemy.orm import backref
from app import db
from app.utils.filesize import filesize
from config import V5Config
import os
class Attachment(db.Model):
__tablename__ = 'attachment'
id = db.Column(db.Integer, primary_key=True)
# Original name of the file
name = db.Column(db.Unicode(64))
# The comment linked with
comment_id = db.Column(db.Integer, db.ForeignKey('comment.id'), nullable=False)
comment = db.relationship('Comment', backref=backref('attachments'))
# The size of the file
size = db.Column(db.Integer)
# Storage file path
@property
def path(self):
return os.path.join(V5Config.DATA_FOLDER, "attachments",
f"{self.id:05}", self.name)
@property
def url(self):
return f"/fichiers/{self.id:05}/{self.name}"
def __init__(self, file, comment):
self.name = secure_filename(file.filename)
self.size = filesize(file)
self.comment = comment
def set_file(self, file):
os.mkdir(os.path.dirname(self.path))
file.save(self.path)
def edit_file(self, file):
file.name = secure_filename(file.filename)
self.set_file(file)
def delete_file(self):
try:
os.delete(self.path)
except FileNotFoundError:
pass

43
app/models/program.py Normal file
View file

@ -0,0 +1,43 @@
from app import db
from app.models.post import Post
class Program(Post):
__tablename__ = 'program'
__mapper_args__ = {'polymorphic_identity': __tablename__}
# ID of underlying Post object
id = db.Column(db.Integer, db.ForeignKey('post.id'), primary_key=True)
# Program name
title = db.Column(db.Unicode(128))
# TODO: Category (games/utilities/lessons)
# TODO: Tags
# TODO: Compatible calculator models
thread_id = db.Column(db.Integer,db.ForeignKey('thread.id'),nullable=False)
thread = db.relationship('Thread', foreign_keys=thread_id)
# TODO: Number of views, statistics, attached files, etc
def __init__(self, author, title, thread):
"""
Create a Program.
Arguments:
author -- post author (User, though only Members can post)
title -- program title (unicode string)
thread -- discussion thread attached to the topic
"""
Post.__init__(self, author)
self.title = title
self.thread = thread
@staticmethod
def from_topic(topic):
p = Program(topic.author, topic.title, topic.thread)
topic.promotion = p
def __repr__(self):
return f'<Program: #{self.id} "{self.title}">'

View file

@ -3,13 +3,22 @@ from app.models.post import Post
class Topic(Post):
__tablename__ = 'topic'
__mapper_args__ = {'polymorphic_identity': __tablename__}
# ID of the underlying [Post] object
id = db.Column(db.Integer, db.ForeignKey('post.id'), primary_key=True)
__mapper_args__ = {
'polymorphic_identity': __tablename__,
'primary_key': id,
'inherit_condition': id == Post.id
}
# Post that the topic was promoted into. If this is not None, then the
# topic was published into a project and a redirection should be emitted
promotion_id = db.Column(db.Integer,db.ForeignKey('post.id'),nullable=True)
promotion = db.relationship('Post', foreign_keys=promotion_id)
# Topic title
title = db.Column(db.Unicode(32))
title = db.Column(db.Unicode(128))
# Parent forum
forum_id = db.Column(db.Integer, db.ForeignKey('forum.id'), nullable=False)

View file

@ -1,6 +1,8 @@
# Register routes here
from app.routes import index, search, users, tools
from app.routes import index, search, users, tools, development
from app.routes.account import login, account, notification
from app.routes.admin import index, groups, account, trophies, forums
from app.routes.admin import index, groups, account, trophies, forums, attachments
from app.routes.forum import index, topic
from app.routes.programs import index
from app.routes.posts import edit

View file

@ -130,7 +130,7 @@ def validation():
try:
mail = request.args['email']
except Exception as e:
print("Error: {e}")
print(f"Error: {e}")
abort(404)
if current_user.is_authenticated:

View file

@ -0,0 +1,13 @@
from app import app
from app.models.attachment import Attachment
from app.utils.priv_required import priv_required
from app.utils.render import render
# TODO: add pagination & moderation tools (deletion)
@app.route('/admin/fichiers', methods=['GET'])
@priv_required('access-admin-panel')
def adm_attachments():
attachments = Attachment.query.all()
return render('admin/attachments.html', attachments=attachments)

25
app/routes/development.py Normal file
View file

@ -0,0 +1,25 @@
from flask import send_file, redirect, url_for, abort
from werkzeug.utils import secure_filename
from app import app
from config import V5Config
import os
# These routes are used in development
# In production, those files should be served by the web server (nginx)
@app.route('/avatar/<filename>')
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'))
@app.route('/fichiers/<path>/<name>')
def attachment(path, name):
file = os.path.join(V5Config.DATA_FOLDER, "attachments",
secure_filename(path), secure_filename(name))
if os.path.isfile(file):
return send_file(file)
else:
abort(404)

View file

@ -10,6 +10,7 @@ from app.models.topic import Topic
from app.models.thread import Thread
from app.models.comment import Comment
from app.models.users import Guest
from app.models.attachment import Attachment
@app.route('/forum/')
@ -34,17 +35,18 @@ def forum_page(f):
or ("/actus" not in f.url and not f.sub_forums)) and (
V5Config.ENABLE_GUEST_POST or current_user.is_authenticated):
# First create the thread, then the comment, then the topic
th = Thread()
db.session.add(th)
db.session.commit()
# Manage author
if current_user.is_authenticated:
author = current_user
else:
author = Guest(form.pseudo.data)
db.session.add(author)
# First create the thread, then the comment, then the topic
th = Thread()
db.session.add(th)
db.session.commit()
c = Comment(author, form.message.data, th)
db.session.add(c)
db.session.commit()
@ -56,6 +58,17 @@ def forum_page(f):
db.session.add(t)
db.session.commit()
# Manage files
attachments = []
for file in form.attachments.data:
if file.filename != "":
a = Attachment(file, c)
attachments.append((a, file))
db.session.add(a)
db.session.commit()
for a, file in attachments:
a.set_file(file)
# Update member's xp and trophies
if current_user.is_authenticated:
current_user.add_xp(2) # 2 points for a topic

View file

@ -10,6 +10,7 @@ from app.models.topic import Topic
from app.models.thread import Thread
from app.models.comment import Comment
from app.models.users import Guest
from app.models.attachment import Attachment
@app.route('/forum/<forum:f>/<topicpage:page>', methods=['GET', 'POST'])
@ -27,16 +28,29 @@ def forum_topic(f, page):
if form.validate_on_submit() and \
(V5Config.ENABLE_GUEST_POST or current_user.is_authenticated):
# Manage author
if current_user.is_authenticated:
author = current_user
else:
author = Guest(form.pseudo.data)
db.session.add(author)
# Create comment
c = Comment(author, form.message.data, t.thread)
db.session.add(c)
db.session.commit()
# Manage files
attachments = []
for file in form.attachments.data:
if file.filename != "":
a = Attachment(file, c)
attachments.append((a, file))
db.session.add(a)
db.session.commit()
for a, file in attachments:
a.set_file(file)
# Update member's xp and trophies
if current_user.is_authenticated:
current_user.add_xp(1) # 1 point for a comment

34
app/routes/posts/edit.py Normal file
View file

@ -0,0 +1,34 @@
from app import app, db
from app.models.post import Post
from app.utils.render import render
from app.forms.forum import CommentEditForm, AnonymousCommentEditForm
from flask import redirect, url_for, abort
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):
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"):
abort(403)
if p.type == "comment":
form = CommentEditForm()
if form.validate_on_submit():
p.text = form.message.data
if form.submit.data:
db.session.add(p)
db.session.commit()
# TODO: Proper redirection
return redirect(url_for('index'))
form.message.data = p.text
return render('forum/edit_comment.html', comment=p, form=form)
else:
abort(404)

View file

@ -0,0 +1,8 @@
from app import app, db
from app.models.program import Program
from app.utils.render import render
@app.route('/programmes')
def program_index():
programs = Program.query.all()
return render('/programs/index.html')

View file

@ -21,11 +21,3 @@ def user(username):
def user_by_id(user_id):
member = Member.query.filter_by(id=user_id).first_or_404()
return redirect(url_for('user', username=member.name))
@app.route('/avatar/<filename>')
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'))

View file

@ -105,5 +105,6 @@ footer {
--background: #e0e0e0;
--border: 1px solid #c0c0c0;
--background-xp: #f85555;
--background-xp-100: #d03333;
--border-xp: 1px solid #d03333;
}

View file

@ -31,12 +31,19 @@
background: var(--background);
border: var(--border);
}
.profile-xp-100 {
background: var(--background-xp);
border: var(--border-xp);
}
.profile-xp div {
height: 10px;
background: var(--background-xp);
border: var(--border-xp);
margin: -1px;
}
.profile-xp-100 div {
background: var(--background-xp-100);
}
.profile.guest {
flex-direction: column;

View file

@ -0,0 +1,45 @@
{% extends "base/base.html" %}
{% block title %}
<a href="{{ url_for('adm') }}">Panneau d'administration</a> » <h1>Pièces jointes</h1>
{% endblock %}
{% block content %}
<section>
<p>Cette page présente une vue d'ensemble des pièces-jointes postées sur le site.</p>
<h2>Pièces jointes</h2>
<table style="width:95%; margin: auto;">
<tr><th>ID</th><th>Nom</th><th>Auteur</th><th>Taille</th></tr>
{% for a in attachments %}
<tr>
<td>{{ a.id }}</td>
<td><a href="{{ a.url }}">{{ a.name }}</a></td>
<td><a href="{{ url_for('user', username=a.comment.author.name) }}">{{ a.comment.author.name }}</a></td>
<td>{{ a.size }}</td>
</tr>
{% endfor %}
</table>
<h2>Liste des groupes</h2>
<table style="width:90%; margin: auto;">
<tr><th>Groupe</th><th>Membres</th><th>Privilèges</th></tr>
{% for group in groups %}
<tr><td><span style="{{ group.css }}">{{ group.name }}</span></td><td>
{% for user in group.members %}
{{ user.name }}
{% endfor %}
</td><td>
{% for priv in group.privs() %}
<code>{{ priv }}</code>
{{- ', ' if not loop.last }}
{% endfor %}
</td></tr>
{% endfor %}
</table>
</section>
{% endblock %}

View file

@ -8,9 +8,10 @@
<section>
<p>Pages générales du panneau d'administration :</p>
<ul>
<li><a href="{{ url_for('adm_groups') }}">Groupes et privilèges</a></li>
<li><a href="{{ url_for('adm_groups') }}">Groupes et privilèges</a></li>
<li><a href="{{ url_for('adm_trophies') }}">Titres et trophées</a></li>
<li><a href="{{ url_for('adm_forums') }}">Arbre des forums</a></li>
<li><a href="{{ url_for('adm_forums') }}">Arbre des forums</a></li>
<li><a href="{{ url_for('adm_attachments') }}">Pièces-jointes</a></li>
</ul>
</section>
{% endblock %}

View file

@ -0,0 +1,41 @@
{% extends "base/base.html" %}
{% import "widgets/editor.html" as widget_editor %}
{% import "widgets/user.html" as widget_user %}
{% block title %}
<a href='/forum'>Forum de Planète Casio</a> » Édition de commentaire</h1>
{% endblock %}
{% block content %}
<section>
<h1>Édition de commentaire</h1>
<h3>Commentaire actuel</h3>
<table class="thread">
<tr>
<td class="author">{{ widget_user.profile(comment.author) }}</td>
<td><div>{{ comment.text }}</div></td>
</tr>
</table>
<div class="form">
<h3>Nouveau commentaire</h3>
<form action="" method="post" enctype="multipart/form-data">
{{ form.hidden_tag() }}
{% if form.pseudo %}
{{ form.pseudo.label }}
{{ form.pseudo }}
{% for error in form.pseudo.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
{% endif %}
{{ widget_editor.text_editor(form.message, label=False) }}
<div>{{ form.preview(class_='bg-ok') }}</div>
<div>{{ form.submit(class_='bg-ok') }}</div>
</form>
</div>
</section>
{% endblock %}

View file

@ -69,6 +69,11 @@
{{ widget_editor.text_editor(form.message) }}
{{ form.attachments }}
{% for error in form.attachments.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
<div>{{ form.submit(class_='bg-ok') }}</div>
</form>
</div>

View file

@ -15,25 +15,25 @@
.
</p>
{% if main_forum == None %}
{% if main_forum == None %}
<p>Il n'y a aucun forum.</p>
{% else %}
{% else %}
{% for l1 in main_forum.sub_forums %}
{% for l1 in main_forum.sub_forums %}
<table class=forumlist>
<tr><th>{{ l1.name }}</th><th>Nombre de sujets</th></tr>
<tr><th>{{ l1.name }}</th><th>Nombre de sujets</th></tr>
{% if l1.sub_forums == [] %}
<tr><td><a href='/forum{{ l1.url }}'>{{ l1.name }}</td>
<td>{{ l1.topics | length }}</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 | length }}</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 | length }}</td></tr>
<tr><td>{{ l2.descr }}</td><td></td></tr>
{% endfor %}
{% for l2 in l1.sub_forums %}
<tr><td><a href='/forum{{ l2.url }}'>{{ l2.name }}</td>
<td>{{ l2.topics | length }}</td></tr>
<tr><td>{{ l2.descr }}</td><td></td></tr>
{% endfor %}
</table>
{% endfor %}

View file

@ -2,6 +2,7 @@
{% import "widgets/editor.html" as widget_editor %}
{% import "widgets/user.html" as widget_user %}
{% import "widgets/pagination.html" as widget_pagination with context %}
{% import "widgets/attachments.html" as widget_attachments %}
{% block title %}
<a href='/forum'>Forum de Planète Casio</a> » <a href="{{ url_for('forum_page', f=t.forum) }}">{{ t.forum.name }}</a> » <h1>{{ t.title }}</h1>
@ -26,18 +27,19 @@
<div>{% if c.date_created != c.date_modified %}
Posté le {{ c.date_created|date }} (Modifié le {{ c.date_modified|date }})
{% else %}
Posté le {{ c.date_created|date }}
Posté le {{ c.date_created|dyndate }}
{% endif %}
| <a href="{{ url_for('forum_topic', f=t.forum, page=(t,comments.page), _anchor=c.id) }}">#</a>
| <a href="#">Modifier</a>
| <a href="{{ url_for('edit_post', postid=c.id) }}">Modifier</a>
| <a href="#">Supprimer</a>
</div>
<!--<hr>-->
<p>{{ c.text }}</p>
{{ widget_attachments.attachments(c) }}
{% elif loop.index0 != 0 %}
<div>Ce message est le top comment</div>
{% endif %}
</td>
</td>
</tr>
{% endfor %}
</table>
@ -60,6 +62,11 @@
{{ widget_editor.text_editor(form.message, label=False) }}
{{ form.attachments }}
{% for error in form.attachments.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
<div>{{ form.submit(class_='bg-ok') }}</div>
</form>
{% endif %}

View file

@ -0,0 +1,21 @@
{% extends "base/base.html" %}
{% block title %}
<h1>Programmes de Planète Casio</h1>
{% endblock %}
{% block content %}
<section>
<h2>Tous les programmes</h2>
<table class=programlist>
<tr><th>ID</th><th>Nom</th><th>Auteur</th><th>Publié le</th></tr>
{% for p in programs %}
<tr><td>{{ p.id }}</td>
<td><a href='#'>{{ p.name }}</a></td>
<td>{{ p.author.name }}</td>
<td>{{ p.date_created }}</td></tr>
{% endfor %}
</table>
</section>
{% endblock %}

View file

@ -0,0 +1,16 @@
{% macro attachments(comment) %}
{% if comment.attachments %}
<summary>Pièces-jointes</summary>
<details>
<table>
<tr><th>Nom</th><th>Taille</th></tr>
{% for a in comment.attachments %}
<tr>
<td><a href="{{ a.url }}">{{ a.name }}</a></td>
<td>{{ a.size }}</td>
</tr>
{% endfor %}
</table>
</details>
{% endif %}
{% endmacro %}

View file

@ -11,7 +11,11 @@
{% endif %}
<div class="profile-points">Niveau {{ user.level[0] }} <span>({{ user.xp }})</span></div>
<div class="profile-points-small">N{{ user.level[0] }} <span>({{ user.xp }})</span></div>
<div class="profile-xp"><div style='width: {{ user.level[1] }}%;'></div></div>
{% if user.level[0] <= 100 %}
<div class="profile-xp"><div style='width: {{ user.level[0] }}%;'></div></div>
{% else %}
<div class="profile-xp profile-xp-100"><div style='width: {{ user.level[0] - 100 }}%;'></div></div>
{% endif %}
</div>
</div>
{% else %}

View file

@ -1,5 +0,0 @@
# Register utils here
from app.utils import pluralize
from app.utils import date
from app.utils import is_title

View file

@ -1,9 +0,0 @@
from app import app
@app.template_filter('date')
def filter_date(date, format="%Y-%m-%d à %H:%M"):
"""
Print a date in a human-readable format.
"""
return date.strftime(format)

9
app/utils/filesize.py Normal file
View file

@ -0,0 +1,9 @@
import os
from random import getrandbits
def filesize(file):
"""Return the filesize. Save in /tmp and delete it when done"""
file.seek(0, os.SEEK_END)
size = file.tell()
file.seek(0)
return size

View file

@ -0,0 +1,3 @@
# Register filters here
from app.utils.filters import date, is_title, pluralize

26
app/utils/filters/date.py Normal file
View file

@ -0,0 +1,26 @@
from app import app
from datetime import datetime
@app.template_filter('date')
def filter_date(date, format="%Y-%m-%d à %H:%M"):
"""
Print a date in a human-readable format.
"""
if format == "dynamic":
d = "1er" if date.day == 1 else int(date.day)
m = ["Janvier", "Février", "Mars", "Avril", "Mai", "Juin", "Juillet",
"Août", "Septembre", "Octobre", "Novembre","Décembre"] \
[date.month - 1]
# Omit current year in the dynamic format
if date.year == datetime.now().year:
format = f"{d} {m} à %H:%M"
else:
format = f"{d} {m} %Y à %H:%M"
return date.strftime(format)
@app.template_filter('dyndate')
def filter_dyndate(date):
return filter_date(date, format="dynamic")

View file

@ -8,8 +8,12 @@ from app.utils.unicode_names import normalize
from math import log
from werkzeug.exceptions import NotFound
import app.utils.ldap as ldap
from config import V5Config
from app.utils.validators.file import *
# TODO: clean this shit into split files
def name_valid(form, name):
valid = valid_name(name.data)
@ -35,7 +39,6 @@ def name_valid(form, name):
err = ' '.join(msg.get(code, default) for code in valid)
raise ValidationError(err)
def name_available(form, name):
# If the name is invalid, name_valid() will return a meaningful message
try:
@ -53,14 +56,11 @@ def name_available(form, name):
if member is not None:
raise ValidationError("Ce nom d'utilisateur est indisponible.")
def email(form, email):
member = Member.query.filter_by(email=email.data).first()
if member is not None:
raise ValidationError('Adresse email déjà utilisée.')
def password(form, password):
# To avoid errors in forms where password is optionnal
if len(password.data) == 0:
@ -87,14 +87,12 @@ def password(form, password):
if entropy(password.data) < 60:
raise ValidationError("Mot de passe pas assez complexe")
def avatar(form, avatar):
try:
Image.open(avatar.data)
except IOError:
raise ValidationError("Avatar invalide")
def old_password(form, field):
if field.data:
if not form.old_password.data:
@ -102,7 +100,6 @@ def old_password(form, field):
if not current_user.check_password(form.old_password.data):
raise ValidationError('Mot de passe actuel erroné.')
def id_exists(object):
"""Check if an id exists in a table"""
def _id_exists(form, id):
@ -115,7 +112,6 @@ def id_exists(object):
raise ValidationError('L\'id n\'existe pas dans la BDD')
return _id_exists
def css(form, css):
"""Check if input is valid and sane CSS"""
pass

View file

@ -0,0 +1,51 @@
from flask_login import current_user
from wtforms.validators import ValidationError, StopValidation
from werkzeug.utils import secure_filename
from app.utils.filesize import filesize
import re
def optional(form, files):
if(len(files.data) == 0 or files.data[0].filename == ""):
raise StopValidation()
def count(form, files):
if current_user.is_authenticated:
if current_user.priv("no-upload-limits"):
return
if len(files.data) > 100: # 100 files for a authenticated user
raise ValidationError("100 fichiers maximum autorisés")
else:
if len(files.data) > 3:
raise ValidationError("3 fichiers maximum autorisés")
def extension(form, files):
valid_extensions = [
"g[123][a-z]|cpa|c1a|fxi|cat|mcs|xcp|fls", # Casio files
"png|jpg|jpeg|bmp|tiff|gif|xcf", # Images
"[ch](pp|\+\+|xx)?|s|py|bide|lua|lc", # Source code
"txt|md|tex|pdf|odt|ods|docx|xlsx", # Office files
"zip|7z|tar|bz2?|t?gz|xz|zst", # Archives
]
r = re.compile("|".join(valid_extensions), re.IGNORECASE)
errors = []
for f in files.data:
name = secure_filename(f.filename)
ext = name.split(".")[-1]
if not r.fullmatch(ext):
errors.append("." + ext)
if len(errors) > 0:
raise ValidationError(f"Extension(s) invalide(s) ({', '.join(errors)})")
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"):
return
if size > 5e6: # 5 Mo per comment for an authenticated user
raise ValidationError("Fichiers trop lourds (max 5Mo)")
else:
if size > 500e3: # 500 ko per comment for a guest
raise ValidationError("Fichiers trop lourds (max 500ko)")

View file

@ -0,0 +1,35 @@
"""add a base model for programs
Revision ID: c5561fa6af4e
Revises: c7779a558510
Create Date: 2020-08-01 14:52:52.878440
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'c5561fa6af4e'
down_revision = 'c7779a558510'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('program',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('title', sa.Unicode(length=128), nullable=True),
sa.Column('thread_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['id'], ['post.id'], ),
sa.ForeignKeyConstraint(['thread_id'], ['thread.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('program')
# ### end Alembic commands ###

View file

@ -0,0 +1,30 @@
"""add promotion information to topics
Revision ID: c7779a558510
Revises: 001d2eaf0413
Create Date: 2020-08-01 11:27:23.298821
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'c7779a558510'
down_revision = '001d2eaf0413'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('topic', sa.Column('promotion_id', sa.Integer(), nullable=True))
op.create_foreign_key(None, 'topic', 'post', ['promotion_id'], ['id'])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'topic', type_='foreignkey')
op.drop_column('topic', 'promotion_id')
# ### end Alembic commands ###

View file

@ -0,0 +1,35 @@
"""Added attachments
Revision ID: cd4868f312c5
Revises: 001d2eaf0413
Create Date: 2020-08-01 19:22:12.405038
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'cd4868f312c5'
down_revision = 'c5561fa6af4e'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('attachment',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.Unicode(length=64), nullable=True),
sa.Column('comment_id', sa.Integer(), nullable=False),
sa.Column('size', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['comment_id'], ['comment.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('attachment')
# ### end Alembic commands ###