Merge branch 'dev' into preprod
This commit is contained in:
commit
b6a8302074
|
@ -4,8 +4,12 @@ app/__pycache__/
|
|||
app/static/avatars/
|
||||
app/static/images/trophies/
|
||||
|
||||
## Devlopement files
|
||||
|
||||
# Development files
|
||||
|
||||
# Flask env
|
||||
.env
|
||||
.flaskenv
|
||||
# virtualenv
|
||||
requirements.txt
|
||||
venv/
|
||||
|
@ -14,6 +18,7 @@ venv/
|
|||
Pipfile
|
||||
Pipfile.lock
|
||||
|
||||
|
||||
## Deployment files
|
||||
|
||||
# uWSGI configuration file
|
||||
|
@ -25,10 +30,12 @@ update.sh
|
|||
# Config to set up some server specific config
|
||||
local_config.py
|
||||
|
||||
|
||||
## Wiki
|
||||
|
||||
wiki/
|
||||
|
||||
|
||||
## Personal folder
|
||||
|
||||
exclude/
|
||||
|
|
4
V5.py
4
V5.py
|
@ -1,6 +1,4 @@
|
|||
from app import app, db
|
||||
from app.models.users import User, Guest, Member, Group, GroupPrivilege
|
||||
from app.models.topic import Topic
|
||||
from app import app
|
||||
|
||||
|
||||
@app.shell_context_processor
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, PasswordField, BooleanField, TextAreaField, SubmitField, DecimalField, SelectField
|
||||
from wtforms.fields.html5 import DateField, EmailField
|
||||
from wtforms.validators import DataRequired, InputRequired, Optional, Email, EqualTo
|
||||
from wtforms.validators import InputRequired, Optional, Email, EqualTo
|
||||
from flask_wtf.file import FileField # Cuz' wtforms' FileField is shitty
|
||||
from app.models.trophies import Trophy
|
||||
import app.utils.validators as vd
|
||||
|
||||
|
||||
|
@ -12,15 +11,15 @@ class RegistrationForm(FlaskForm):
|
|||
'Pseudonyme',
|
||||
description='Ce nom est définitif !',
|
||||
validators=[
|
||||
DataRequired(),
|
||||
vd.name_valid,
|
||||
vd.name_available,
|
||||
InputRequired(),
|
||||
vd.name.valid,
|
||||
vd.name.available,
|
||||
],
|
||||
)
|
||||
email = EmailField(
|
||||
'Adresse Email',
|
||||
validators=[
|
||||
DataRequired(),
|
||||
InputRequired(),
|
||||
Email(message="Adresse email invalide."),
|
||||
vd.email,
|
||||
],
|
||||
|
@ -28,21 +27,21 @@ class RegistrationForm(FlaskForm):
|
|||
password = PasswordField(
|
||||
'Mot de passe',
|
||||
validators=[
|
||||
DataRequired(),
|
||||
vd.password,
|
||||
InputRequired(),
|
||||
vd.password.is_strong,
|
||||
],
|
||||
)
|
||||
password2 = PasswordField(
|
||||
'Répéter le mot de passe',
|
||||
validators=[
|
||||
DataRequired(),
|
||||
InputRequired(),
|
||||
EqualTo('password', message="Les mots de passe doivent être identiques."),
|
||||
],
|
||||
)
|
||||
guidelines = BooleanField(
|
||||
"""J'accepte les <a href="#">CGU</a>""",
|
||||
validators=[
|
||||
DataRequired(),
|
||||
InputRequired(),
|
||||
],
|
||||
)
|
||||
newsletter = BooleanField(
|
||||
|
@ -59,7 +58,8 @@ class UpdateAccountForm(FlaskForm):
|
|||
'Avatar',
|
||||
validators=[
|
||||
Optional(),
|
||||
vd.avatar,
|
||||
vd.file.is_image,
|
||||
vd.file.avatar_size,
|
||||
],
|
||||
)
|
||||
email = EmailField(
|
||||
|
@ -68,15 +68,15 @@ class UpdateAccountForm(FlaskForm):
|
|||
Optional(),
|
||||
Email(message="Addresse email invalide."),
|
||||
vd.email,
|
||||
vd.old_password,
|
||||
vd.password.old_password,
|
||||
],
|
||||
)
|
||||
password = PasswordField(
|
||||
'Nouveau mot de passe',
|
||||
validators=[
|
||||
Optional(),
|
||||
vd.password,
|
||||
vd.old_password,
|
||||
vd.password.is_strong,
|
||||
vd.password.old_password,
|
||||
],
|
||||
)
|
||||
password2 = PasswordField(
|
||||
|
@ -129,15 +129,15 @@ class DeleteAccountForm(FlaskForm):
|
|||
delete = BooleanField(
|
||||
'Confirmer la suppression',
|
||||
validators=[
|
||||
DataRequired(),
|
||||
InputRequired(),
|
||||
],
|
||||
description='Attention, cette opération est irréversible !'
|
||||
)
|
||||
old_password = PasswordField(
|
||||
'Mot de passe',
|
||||
validators=[
|
||||
DataRequired(),
|
||||
vd.old_password,
|
||||
InputRequired(),
|
||||
vd.password.old_password,
|
||||
],
|
||||
)
|
||||
submit = SubmitField(
|
||||
|
@ -161,7 +161,7 @@ class ResetPasswordForm(FlaskForm):
|
|||
'Mot de passe',
|
||||
validators=[
|
||||
Optional(),
|
||||
vd.password,
|
||||
vd.password.is_strong,
|
||||
],
|
||||
)
|
||||
password2 = PasswordField(
|
||||
|
@ -179,14 +179,16 @@ class AdminUpdateAccountForm(FlaskForm):
|
|||
'Pseudonyme',
|
||||
validators=[
|
||||
Optional(),
|
||||
vd.name_valid,
|
||||
vd.name.valid,
|
||||
vd.name.available,
|
||||
],
|
||||
)
|
||||
avatar = FileField(
|
||||
'Avatar',
|
||||
validators=[
|
||||
Optional(),
|
||||
vd.avatar,
|
||||
vd.file.is_image,
|
||||
vd.file.avatar_size,
|
||||
],
|
||||
)
|
||||
email = EmailField(
|
||||
|
@ -199,7 +201,7 @@ class AdminUpdateAccountForm(FlaskForm):
|
|||
)
|
||||
email_confirmed = BooleanField(
|
||||
"Confirmer l'email",
|
||||
description="Si décoché, l'utilisateur devra demander explicitement un email "\
|
||||
description="Si décoché, l'utilisateur devra demander explicitement un email "
|
||||
"de validation, ou faire valider son adresse email par un administrateur.",
|
||||
)
|
||||
password = PasswordField(
|
||||
|
@ -207,7 +209,7 @@ class AdminUpdateAccountForm(FlaskForm):
|
|||
description="L'ancien mot de passe ne pourra pas être récupéré !",
|
||||
validators=[
|
||||
Optional(),
|
||||
vd.password,
|
||||
vd.password.is_strong,
|
||||
],
|
||||
)
|
||||
xp = DecimalField(
|
||||
|
@ -269,7 +271,7 @@ class AdminDeleteAccountForm(FlaskForm):
|
|||
delete = BooleanField(
|
||||
'Confirmer la suppression',
|
||||
validators=[
|
||||
DataRequired(),
|
||||
InputRequired(),
|
||||
],
|
||||
description='Attention, cette opération est irréversible !',
|
||||
)
|
||||
|
|
|
@ -1,32 +1,35 @@
|
|||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, FormField, SubmitField, TextAreaField, \
|
||||
MultipleFileField
|
||||
from wtforms.validators import DataRequired, Length, Optional
|
||||
from wtforms import StringField, SubmitField, TextAreaField, MultipleFileField
|
||||
from wtforms.validators import InputRequired, Length
|
||||
import app.utils.validators as vd
|
||||
|
||||
class CommentForm(FlaskForm):
|
||||
message = TextAreaField('Message', validators=[DataRequired()])
|
||||
message = TextAreaField('Message', validators=[InputRequired()])
|
||||
attachments = MultipleFileField('Pièces-jointes',
|
||||
validators=[vd.file.optional, vd.file.count, vd.file.extension,
|
||||
vd.file.size, vd.file.namelength])
|
||||
submit = SubmitField('Commenter')
|
||||
preview = SubmitField('Prévisualiser')
|
||||
|
||||
|
||||
class AnonymousCommentForm(CommentForm):
|
||||
pseudo = StringField('Pseudo',
|
||||
validators=[DataRequired(), vd.name_valid, vd.name_available])
|
||||
validators=[InputRequired(), 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)])
|
||||
validators=[InputRequired(), Length(min=3, max=32)])
|
||||
submit = SubmitField('Créer le sujet')
|
||||
|
||||
|
||||
class AnonymousTopicCreationForm(TopicCreationForm, AnonymousCommentForm):
|
||||
pass
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, PasswordField, BooleanField, SubmitField
|
||||
from wtforms.validators import DataRequired
|
||||
from wtforms.validators import InputRequired
|
||||
|
||||
|
||||
class LoginForm(FlaskForm):
|
||||
username = StringField(
|
||||
'Identifiant',
|
||||
validators=[
|
||||
DataRequired(),
|
||||
InputRequired(),
|
||||
],
|
||||
)
|
||||
password = PasswordField(
|
||||
'Mot de passe',
|
||||
validators=[
|
||||
DataRequired(),
|
||||
InputRequired(),
|
||||
],
|
||||
)
|
||||
remember_me = BooleanField(
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, SubmitField
|
||||
from wtforms.fields.html5 import DateField
|
||||
from wtforms.validators import DataRequired, Optional
|
||||
from wtforms.validators import InputRequired, Optional
|
||||
|
||||
|
||||
# TODO: compléter le formulaire de recherche avancée
|
||||
|
@ -9,7 +9,7 @@ class AdvancedSearchForm(FlaskForm):
|
|||
q = StringField(
|
||||
'Rechercher :',
|
||||
validators=[
|
||||
DataRequired(),
|
||||
InputRequired(),
|
||||
],
|
||||
)
|
||||
date = DateField(
|
||||
|
@ -27,6 +27,6 @@ class SearchForm(FlaskForm):
|
|||
q = StringField(
|
||||
'Rechercher',
|
||||
validators=[
|
||||
DataRequired(),
|
||||
InputRequired(),
|
||||
],
|
||||
)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, SubmitField, BooleanField
|
||||
from wtforms.validators import DataRequired, Optional
|
||||
from wtforms.validators import InputRequired, Optional
|
||||
from flask_wtf.file import FileField # Cuz' wtforms' FileField is shitty
|
||||
|
||||
|
||||
|
@ -8,7 +8,7 @@ class TrophyForm(FlaskForm):
|
|||
name = StringField(
|
||||
'Nom',
|
||||
validators=[
|
||||
DataRequired(),
|
||||
InputRequired(),
|
||||
],
|
||||
)
|
||||
icon = FileField(
|
||||
|
@ -43,7 +43,7 @@ class DeleteTrophyForm(FlaskForm):
|
|||
delete = BooleanField(
|
||||
'Confirmer la suppression',
|
||||
validators=[
|
||||
DataRequired(),
|
||||
InputRequired(),
|
||||
],
|
||||
description='Attention, cette opération est irréversible !',
|
||||
)
|
|
@ -16,8 +16,8 @@ class Forum(db.Model):
|
|||
|
||||
# Relationships
|
||||
parent_id = db.Column(db.Integer, db.ForeignKey('forum.id'), nullable=True)
|
||||
parent = db.relationship('Forum', backref='sub_forums', remote_side=id,
|
||||
lazy=True, foreign_keys=parent_id)
|
||||
parent = db.relationship('Forum', backref='sub_forums', remote_side=id,
|
||||
lazy=True, foreign_keys=parent_id)
|
||||
|
||||
# Other fields populated automatically through relations:
|
||||
# <topics> List of topics in this exact forum (of type Topic)
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
from app import db
|
||||
from app.models.users import User
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# Planète Casio v5
|
||||
# models.privs: Database models for groups and privilege management
|
||||
# models.priv: Database models for groups and privilege management
|
||||
|
||||
from app import db
|
||||
|
|
@ -16,8 +16,8 @@ class Program(Post):
|
|||
# TODO: Compatible calculator models
|
||||
|
||||
thread_id = db.Column(db.Integer,db.ForeignKey('thread.id'),nullable=False)
|
||||
thread = db.relationship('Thread', foreign_keys=thread_id,
|
||||
back_populates='owner_program')
|
||||
thread = db.relationship('Thread', foreign_keys=thread_id,
|
||||
back_populates='owner_program')
|
||||
|
||||
# TODO: Number of views, statistics, attached files, etc
|
||||
|
||||
|
|
|
@ -10,11 +10,11 @@ class Thread(db.Model):
|
|||
|
||||
# Top comment
|
||||
top_comment_id = db.Column(db.Integer, db.ForeignKey('comment.id'))
|
||||
top_comment = db.relationship('Comment', foreign_keys=top_comment_id)
|
||||
top_comment = db.relationship('Comment', foreign_keys=top_comment_id)
|
||||
|
||||
# Post owning the thread, set only by Topic, Program, etc. In general, you
|
||||
# should use [owner_post] which groups them together.
|
||||
owner_topic = db.relationship('Topic')
|
||||
owner_topic = db.relationship('Topic')
|
||||
owner_program = db.relationship('Program')
|
||||
|
||||
# Other fields populated automatically through relations:
|
||||
|
|
|
@ -16,19 +16,19 @@ class Topic(Post):
|
|||
# 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)
|
||||
promotion = db.relationship('Post', foreign_keys=promotion_id)
|
||||
|
||||
# Topic title
|
||||
title = db.Column(db.Unicode(128))
|
||||
|
||||
# Parent forum
|
||||
forum_id = db.Column(db.Integer, db.ForeignKey('forum.id'), nullable=False)
|
||||
forum = db.relationship('Forum',
|
||||
backref=backref('topics', lazy='dynamic'),foreign_keys=forum_id)
|
||||
forum = db.relationship('Forum',
|
||||
backref=backref('topics', lazy='dynamic'), foreign_keys=forum_id)
|
||||
|
||||
# Associated thread
|
||||
thread_id = db.Column(db.Integer,db.ForeignKey('thread.id'),nullable=False)
|
||||
thread = db.relationship('Thread', foreign_keys=thread_id,
|
||||
thread = db.relationship('Thread', foreign_keys=thread_id,
|
||||
back_populates='owner_topic')
|
||||
|
||||
# Number of views in the forum
|
||||
|
|
|
@ -1,21 +1,18 @@
|
|||
from datetime import date
|
||||
from flask import flash
|
||||
from flask_login import UserMixin
|
||||
from sqlalchemy import func as SQLfunc
|
||||
from os.path import isfile
|
||||
from PIL import Image
|
||||
from app import app, db
|
||||
from app.models.privs import SpecialPrivilege, Group, GroupMember, \
|
||||
from app.models.priv import SpecialPrivilege, Group, GroupMember, \
|
||||
GroupPrivilege
|
||||
from app.models.trophies import Trophy, TrophyMember, Title
|
||||
from app.models.trophy import Trophy, TrophyMember, Title
|
||||
from app.models.notification import Notification
|
||||
import app.utils.unicode_names as unicode_names
|
||||
from app.utils.notify import notify
|
||||
import app.utils.ldap as ldap
|
||||
from config import V5Config
|
||||
|
||||
import werkzeug.security
|
||||
import re
|
||||
import math
|
||||
import app
|
||||
import os
|
||||
|
@ -78,8 +75,7 @@ class Member(User):
|
|||
|
||||
# Primary attributes (needed for the system to work)
|
||||
name = db.Column(db.Unicode(User.NAME_MAXLEN), index=True)
|
||||
norm = db.Column(db.Unicode(User.NAME_MAXLEN), index=True,
|
||||
unique=True)
|
||||
norm = db.Column(db.Unicode(User.NAME_MAXLEN), index=True, unique=True)
|
||||
email = db.Column(db.Unicode(120), index=True, unique=True)
|
||||
email_confirmed = db.Column(db.Boolean)
|
||||
password_hash = db.Column(db.String(255))
|
||||
|
@ -107,7 +103,7 @@ class Member(User):
|
|||
|
||||
# Displayed title, if set
|
||||
title_id = db.Column(db.Integer, db.ForeignKey('title.id'), nullable=True)
|
||||
title = db.relationship('Title', foreign_keys=title_id)
|
||||
title = db.relationship('Title', foreign_keys=title_id)
|
||||
|
||||
# Settings
|
||||
newsletter = db.Column(db.Boolean, default=False)
|
||||
|
@ -116,6 +112,7 @@ class Member(User):
|
|||
trophies = db.relationship('Trophy', secondary=TrophyMember,
|
||||
back_populates='owners')
|
||||
topics = db.relationship('Topic')
|
||||
programs = db.relationship('Program')
|
||||
comments = db.relationship('Comment')
|
||||
|
||||
# Displayed title
|
||||
|
@ -158,7 +155,7 @@ class Member(User):
|
|||
if SpecialPrivilege.query.filter_by(mid=self.id, priv=priv).first():
|
||||
return True
|
||||
return db.session.query(Group, GroupPrivilege).filter(
|
||||
Group.id.in_([ g.id for g in self.groups ]),
|
||||
Group.id.in_([g.id for g in self.groups]),
|
||||
GroupPrivilege.gid==Group.id,
|
||||
GroupPrivilege.priv==priv).first() is not None
|
||||
|
||||
|
@ -377,8 +374,7 @@ class Member(User):
|
|||
progress(levels, post_count)
|
||||
|
||||
if context in ["new-program", None]:
|
||||
# TODO: Amount of programs by the user
|
||||
program_count = 0
|
||||
program_count = self.programs.count()
|
||||
|
||||
levels = {
|
||||
5: "Programmeur du dimanche",
|
|
@ -3,9 +3,7 @@ from app.forms.login import LoginForm
|
|||
from app.forms.search import SearchForm
|
||||
from app.models.forum import Forum
|
||||
from app.models.topic import Topic
|
||||
from app.models.thread import Thread
|
||||
from app.models.comment import Comment
|
||||
from app.models.users import Member
|
||||
|
||||
|
||||
@app.context_processor
|
||||
def menu_processor():
|
||||
|
|
|
@ -9,7 +9,7 @@ def utilities_processor():
|
|||
return dict(
|
||||
len=len,
|
||||
# enumerate=enumerate,
|
||||
_url_for = lambda route, args, **other: url_for(route, **args, **other),
|
||||
V5Config = V5Config,
|
||||
_url_for=lambda route, args, **other: url_for(route, **args, **other),
|
||||
V5Config=V5Config,
|
||||
slugify=slugify,
|
||||
)
|
||||
|
|
|
@ -3,8 +3,8 @@ from flask_login import login_required, current_user, logout_user
|
|||
from app import app, db
|
||||
from app.forms.account import UpdateAccountForm, RegistrationForm, \
|
||||
DeleteAccountForm, AskResetPasswordForm, ResetPasswordForm
|
||||
from app.models.users import Member
|
||||
from app.models.trophies import Title
|
||||
from app.models.user import Member
|
||||
from app.models.trophy import Title
|
||||
from app.utils.render import render
|
||||
from app.utils.send_mail import send_validation_mail, send_reset_password_mail
|
||||
from app.utils.priv_required import guest_only
|
||||
|
|
|
@ -3,8 +3,8 @@ from flask_login import login_user, logout_user, login_required, current_user
|
|||
from urllib.parse import urlparse, urljoin
|
||||
from app import app
|
||||
from app.forms.login import LoginForm
|
||||
from app.models.users import Member
|
||||
from app.models.privs import Group
|
||||
from app.models.user import Member
|
||||
from app.models.priv import Group
|
||||
from app.utils.render import render
|
||||
from app.utils.send_mail import send_validation_mail
|
||||
import datetime
|
||||
|
|
|
@ -2,9 +2,9 @@ from flask import flash, redirect, url_for, request
|
|||
from flask_login import current_user
|
||||
from wtforms import BooleanField
|
||||
from app.utils.priv_required import priv_required
|
||||
from app.models.users import Member
|
||||
from app.models.trophies import Trophy, Title
|
||||
from app.models.privs import Group
|
||||
from app.models.user import Member
|
||||
from app.models.trophy import Trophy, Title
|
||||
from app.models.priv import Group
|
||||
from app.forms.account import AdminUpdateAccountForm, AdminDeleteAccountForm, \
|
||||
AdminAccountEditTrophyForm, AdminAccountEditGroupForm
|
||||
from app.utils.render import render
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
from app.utils.priv_required import priv_required
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import SubmitField
|
||||
from app.models.users import Member, Group, GroupPrivilege
|
||||
from app.models.privs import SpecialPrivilege
|
||||
from app.models.user import Member, Group, GroupPrivilege
|
||||
from app.models.priv import SpecialPrivilege
|
||||
from app.utils.render import render
|
||||
from app import app, db
|
||||
import yaml
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from flask import request, flash, redirect, url_for
|
||||
from app.utils.priv_required import priv_required
|
||||
from app.models.trophies import Trophy, Title
|
||||
from app.forms.trophies import TrophyForm, DeleteTrophyForm
|
||||
from app.models.trophy import Trophy, Title
|
||||
from app.forms.trophy import TrophyForm, DeleteTrophyForm
|
||||
from app.utils.render import render
|
||||
from app import app, db
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ import os
|
|||
|
||||
@app.route('/avatar/<filename>')
|
||||
def avatar(filename):
|
||||
filename = secure_filename(filename) # No h4ckers allowed
|
||||
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)
|
||||
|
|
|
@ -9,7 +9,7 @@ from app.models.forum import Forum
|
|||
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.user import Guest
|
||||
from app.models.attachment import Attachment
|
||||
|
||||
|
||||
|
@ -72,7 +72,7 @@ def forum_page(f, page=1):
|
|||
|
||||
# Update member's xp and trophies
|
||||
if current_user.is_authenticated:
|
||||
current_user.add_xp(2) # 2 points for a topic
|
||||
current_user.add_xp(2) # 2 points for a topic
|
||||
current_user.update_trophies('new-post')
|
||||
|
||||
flash('Le sujet a bien été créé', 'ok')
|
||||
|
|
|
@ -9,7 +9,7 @@ from app.models.forum import Forum
|
|||
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.user import Guest
|
||||
from app.models.attachment import Attachment
|
||||
|
||||
|
||||
|
@ -53,7 +53,7 @@ def forum_topic(f, page):
|
|||
|
||||
# Update member's xp and trophies
|
||||
if current_user.is_authenticated:
|
||||
current_user.add_xp(1) # 1 point for a comment
|
||||
current_user.add_xp(1) # 1 point for a comment
|
||||
current_user.update_trophies('new-post')
|
||||
|
||||
flash('Message envoyé', 'ok')
|
||||
|
|
|
@ -2,8 +2,8 @@ from flask import redirect, url_for, send_file
|
|||
from werkzeug.utils import secure_filename
|
||||
import os.path
|
||||
from app import app
|
||||
from app.models.users import Member
|
||||
from app.models.trophies import Trophy
|
||||
from app.models.user import Member
|
||||
from app.models.trophy import Trophy
|
||||
from app.utils import unicode_names
|
||||
from app.utils.render import render
|
||||
from config import V5Config
|
||||
|
|
|
@ -20,8 +20,7 @@ from werkzeug.routing import BaseConverter, ValidationError
|
|||
from app.models.forum import Forum
|
||||
from app.models.topic import Topic
|
||||
from slugify import slugify
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
class ForumConverter(BaseConverter):
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import os
|
||||
from random import getrandbits
|
||||
|
||||
|
||||
def filesize(file):
|
||||
"""Return the filesize. Save in /tmp and delete it when done"""
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from app import app
|
||||
from app.models.trophies import Title
|
||||
from app.models.trophy import Title
|
||||
|
||||
|
||||
@app.template_filter('is_title')
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import ldap
|
||||
from ldap.modlist import addModlist, modifyModlist
|
||||
from app.utils.unicode_names import normalize
|
||||
from config import V5Config
|
||||
|
||||
def get_member(username):
|
||||
""" Get informations about member. Username must be normalized! """
|
||||
""" Get informations about member"""
|
||||
username = normalize(username) # Never safe enough
|
||||
conn = ldap.initialize("ldap://localhost")
|
||||
# Search for user
|
||||
r = conn.search_s(V5Config.LDAP_ORGANIZATION, ldap.SCOPE_SUBTREE,
|
||||
f'(cn={username})')
|
||||
r = conn.search_s(f"{V5Config.LDAP_ENV},{V5Config.LDAP_ROOT}",
|
||||
ldap.SCOPE_SUBTREE, f'(cn={username})')
|
||||
if len(r) > 0:
|
||||
return r[0]
|
||||
else:
|
||||
|
@ -17,13 +19,15 @@ def get_member(username):
|
|||
def edit(user, fields):
|
||||
""" Edit a user. Fields is {'name': ['value'], …} """
|
||||
conn = ldap.initialize("ldap://localhost")
|
||||
# TODO: do this
|
||||
# Connect as root
|
||||
# conn.simple_bind_s(f'cn=ldap-root,{LDAP_ORGANIZATION}', LDAP_PASSWORD)
|
||||
# conn.simple_bind_s(f'cn=ldap-root,{V5Config.LDAP_ENV}',
|
||||
# V5Config.LDAP_PASSWORD)
|
||||
# old_value = {"userPassword": ["my_old_password"]}
|
||||
# new_value = {"userPassword": ["my_new_password"]}
|
||||
|
||||
modlist = ldap.modlist.modifyModlist(old_value, new_value)
|
||||
con.modify_s(dn, modlist)
|
||||
# modlist = modifyModlist(old_value, new_value)
|
||||
# conn.modify_s(dn, modlist)
|
||||
|
||||
|
||||
def set_email(user, email):
|
||||
|
@ -34,9 +38,9 @@ def set_password(user, password):
|
|||
""" Set password for a user. """
|
||||
conn = ldap.initialize("ldap://localhost")
|
||||
# Connect as root
|
||||
conn.simple_bind_s(f'cn=ldap-root,{V5Config.LDAP_ORGANIZATION}',
|
||||
conn.simple_bind_s(f'cn=ldap-root,{V5Config.LDAP_ROOT}',
|
||||
V5Config.LDAP_PASSWORD)
|
||||
conn.passwd_s(f"cn={user.norm},{V5Config.LDAP_ORGANIZATION}",
|
||||
conn.passwd_s(f"cn={user.norm},{V5Config.LDAP_ENV},{V5Config.LDAP_ROOT}",
|
||||
None, password)
|
||||
|
||||
|
||||
|
@ -44,8 +48,8 @@ def check_password(user, password):
|
|||
""" Try to login a user through LDAP register. """
|
||||
conn = ldap.initialize("ldap://localhost")
|
||||
try:
|
||||
conn.simple_bind_s(f"cn={user.norm},{V5Config.LDAP_ORGANIZATION}",
|
||||
password)
|
||||
conn.simple_bind_s(f"cn={user.norm},{V5Config.LDAP_ENV}," \
|
||||
f"{V5Config.LDAP_ROOT}", password)
|
||||
except ldap.INVALID_CREDENTIALS:
|
||||
return False
|
||||
return True
|
||||
|
@ -58,10 +62,10 @@ def add_member(member):
|
|||
return
|
||||
conn = ldap.initialize("ldap://localhost")
|
||||
# Connect as root
|
||||
conn.simple_bind_s(f'cn=ldap-root,{V5Config.LDAP_ORGANIZATION}',
|
||||
conn.simple_bind_s(f'cn=ldap-root,{V5Config.LDAP_ROOT}',
|
||||
V5Config.LDAP_PASSWORD)
|
||||
# Create fields
|
||||
dn = f'cn={member.norm},{V5Config.LDAP_ORGANIZATION}'
|
||||
dn = f'cn={member.norm},{V5Config.LDAP_ENV},{V5Config.LDAP_ROOT}'
|
||||
modlist = addModlist({
|
||||
'objectClass': [bytes('inetOrgPerson', 'UTF8')],
|
||||
'cn': [bytes(member.norm, 'UTF8')],
|
||||
|
@ -79,9 +83,9 @@ def delete_member(member):
|
|||
""" Remove a member from LDAP register """
|
||||
conn = ldap.initialize("ldap://localhost")
|
||||
# Connect as root
|
||||
conn.simple_bind_s(f'cn=ldap-root,{V5Config.LDAP_ORGANIZATION}',
|
||||
conn.simple_bind_s(f'cn=ldap-root,{V5Config.LDAP_ROOT}',
|
||||
V5Config.LDAP_PASSWORD)
|
||||
# Create fields
|
||||
dn = f'cn={member.norm},{V5Config.LDAP_ORGANIZATION}'
|
||||
dn = f'cn={member.norm},{V5Config.LDAP_ENV},{V5Config.LDAP_ROOT}'
|
||||
# Delete the user
|
||||
conn.delete_s(dn)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from app import db
|
||||
from app.models.notification import Notification
|
||||
# from app.models.users import Member
|
||||
from app.models.user import Member
|
||||
|
||||
def notify(user, message, href=None):
|
||||
""" Notify a user (by id, name or object reference) with a message.
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
from flask import render_template
|
||||
from app.forms.login import LoginForm
|
||||
from app.forms.search import SearchForm
|
||||
from app.models.forum import Forum
|
||||
|
||||
def render(*args, styles=[], scripts=[], **kwargs):
|
||||
# TODO: debugguer cette merde : au logout, ça foire
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from app.utils.unicode_names import normalize
|
||||
from app.models.users import User
|
||||
from app.models.user import User
|
||||
import re
|
||||
|
||||
def valid_name(name, msg=False):
|
||||
|
|
|
@ -1,104 +1,19 @@
|
|||
from flask_login import current_user
|
||||
from wtforms.validators import ValidationError
|
||||
from PIL import Image
|
||||
from app.models.users import Member, User
|
||||
from app.models.trophies import Title
|
||||
from app.utils.valid_name import valid_name
|
||||
from app.utils.unicode_names import normalize
|
||||
from math import log
|
||||
from app.models.user import Member
|
||||
from app.models.trophy import Title
|
||||
from werkzeug.exceptions import NotFound
|
||||
import app.utils.ldap as ldap
|
||||
|
||||
from config import V5Config
|
||||
|
||||
from app.utils.validators.file import *
|
||||
from app.utils.validators.name import *
|
||||
from app.utils.validators.password import *
|
||||
|
||||
# TODO: clean this shit into split files
|
||||
|
||||
def name_valid(form, name):
|
||||
valid = valid_name(name.data)
|
||||
default = "Nom d'utilisateur invalide (erreur interne)"
|
||||
msg = {
|
||||
"too-short":
|
||||
"Le nom d'utilisateur doit faire au moins "
|
||||
f"{User.NAME_MINLEN} caractères.",
|
||||
"too-long":
|
||||
"Le nom d'utilisateur doit faire au plus "
|
||||
f"{User.NAME_MAXLEN} caractères.",
|
||||
"cant-normalize":
|
||||
"Ce nom d'utilisateur contient des caractères interdits. Les "
|
||||
"caractères autorisés sont les lettres, lettres accentuées, "
|
||||
'chiffres ainsi que "-" (tiret), "." (point), "~" (tilde) et '
|
||||
'"_" (underscore).',
|
||||
"no-letter":
|
||||
"Le nom d'utilisateur doit contenir au moins une lettre.",
|
||||
"forbidden":
|
||||
"Ce nom d'utilisateur est interdit."
|
||||
}
|
||||
if valid is not True:
|
||||
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:
|
||||
norm = normalize(name.data)
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
member = Member.query.filter_by(norm=norm).first()
|
||||
if member is not None:
|
||||
raise ValidationError("Ce nom d'utilisateur est indisponible.")
|
||||
|
||||
# Double check with LDAP if needed
|
||||
if V5Config.USE_LDAP:
|
||||
member = ldap.get_member(norm)
|
||||
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:
|
||||
return
|
||||
|
||||
def entropy(password):
|
||||
"""Estimate entropy of a password, in bits"""
|
||||
# If you edit this function, please edit accordingly the JS one
|
||||
# in static/script/entropy.js
|
||||
chars = [
|
||||
"abcdefghijklmnopqrstuvwxyz",
|
||||
"ABCDFEGHIJKLMNOPQRSTUVWXYZ",
|
||||
"0123456789",
|
||||
" !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~§", # OWASP special chars
|
||||
"áàâéèêíìîóòôúùûç",
|
||||
]
|
||||
used = set()
|
||||
for c in password:
|
||||
for i in chars:
|
||||
if c in i:
|
||||
used.add(i)
|
||||
return log(len(''.join(used)) ** len(password), 2)
|
||||
|
||||
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:
|
||||
raise ValidationError('Votre ancien mot de passe est requis pour cette modification.')
|
||||
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"""
|
||||
|
@ -112,10 +27,12 @@ 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
|
||||
|
||||
|
||||
def own_title(form, title):
|
||||
# Everyone can use "Member"
|
||||
if title.data == -1:
|
||||
|
|
|
@ -2,29 +2,33 @@ from flask_login import current_user
|
|||
from wtforms.validators import ValidationError, StopValidation
|
||||
from werkzeug.utils import secure_filename
|
||||
from app.utils.filesize import filesize
|
||||
from PIL import Image
|
||||
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
|
||||
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
|
||||
"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 = []
|
||||
|
@ -36,7 +40,9 @@ def extension(form, files):
|
|||
errors.append("." + ext)
|
||||
|
||||
if len(errors) > 0:
|
||||
raise ValidationError(f"Extension(s) invalide(s) ({', '.join(errors)})")
|
||||
raise ValidationError("Extension(s) invalide(s)"
|
||||
f"({', '.join(errors)})")
|
||||
|
||||
|
||||
def size(form, files):
|
||||
"""There is no global limit to file sizes"""
|
||||
|
@ -44,12 +50,13 @@ def size(form, files):
|
|||
if current_user.is_authenticated:
|
||||
if current_user.priv("no-upload-limits"):
|
||||
return
|
||||
if size > 5e6: # 5 Mo per comment for an authenticated user
|
||||
if size > 5e6: # 5 Mo per comment for an authenticated user
|
||||
raise ValidationError("Fichiers trop lourds (max 5 Mo)")
|
||||
else:
|
||||
if size > 500e3: # 500 ko per comment for a guest
|
||||
if size > 500e3: # 500 ko per comment for a guest
|
||||
raise ValidationError("Fichiers trop lourds (max 500 ko)")
|
||||
|
||||
|
||||
def namelength(form, files):
|
||||
errors = []
|
||||
for f in files.data:
|
||||
|
@ -57,5 +64,17 @@ def namelength(form, files):
|
|||
if len(name) > 64:
|
||||
errors.append(f.filename)
|
||||
if len(errors) > 0:
|
||||
raise ValidationError(f"Noms trop longs, 64 caractères max " \
|
||||
raise ValidationError("Noms trop longs, 64 caractères max "
|
||||
f"({', '.join(errors)})")
|
||||
|
||||
|
||||
def is_image(form, avatar):
|
||||
try:
|
||||
Image.open(avatar.data)
|
||||
except IOError:
|
||||
raise ValidationError("Avatar invalide")
|
||||
|
||||
|
||||
def avatar_size(form, file):
|
||||
if filesize(file.data) > 200e3:
|
||||
raise ValidationError("Fichier trop lourd (max 200 ko)")
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
from wtforms.validators import ValidationError
|
||||
from app.utils.valid_name import valid_name
|
||||
from app.models.user import User, Member
|
||||
import app.utils.ldap as ldap
|
||||
from app.utils.unicode_names import normalize
|
||||
from config import V5Config
|
||||
|
||||
|
||||
def valid(form, name):
|
||||
valid = valid_name(name.data)
|
||||
default = "Nom d'utilisateur invalide (erreur interne)"
|
||||
msg = {
|
||||
"too-short":
|
||||
"Le nom d'utilisateur doit faire au moins "
|
||||
f"{User.NAME_MINLEN} caractères.",
|
||||
"too-long":
|
||||
"Le nom d'utilisateur doit faire au plus "
|
||||
f"{User.NAME_MAXLEN} caractères.",
|
||||
"cant-normalize":
|
||||
"Ce nom d'utilisateur contient des caractères interdits. Les "
|
||||
"caractères autorisés sont les lettres, lettres accentuées, "
|
||||
'chiffres ainsi que "-" (tiret), "." (point), "~" (tilde) et '
|
||||
'"_" (underscore).',
|
||||
"no-letter":
|
||||
"Le nom d'utilisateur doit contenir au moins une lettre.",
|
||||
"forbidden":
|
||||
"Ce nom d'utilisateur est interdit."
|
||||
}
|
||||
if valid is not True:
|
||||
err = ' '.join(msg.get(code, default) for code in valid)
|
||||
raise ValidationError(err)
|
||||
|
||||
|
||||
def available(form, name):
|
||||
# If the name is invalid, name_valid() will return a meaningful message
|
||||
try:
|
||||
norm = normalize(name.data)
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
member = Member.query.filter_by(norm=norm).first()
|
||||
if member is not None:
|
||||
raise ValidationError("Ce nom d'utilisateur est indisponible.")
|
||||
|
||||
# Double check with LDAP if needed
|
||||
if V5Config.USE_LDAP:
|
||||
member = ldap.get_member(norm)
|
||||
if member is not None:
|
||||
raise ValidationError("Ce nom d'utilisateur est indisponible.")
|
|
@ -0,0 +1,38 @@
|
|||
from wtforms.validators import ValidationError
|
||||
from flask_login import current_user
|
||||
from math import log
|
||||
|
||||
|
||||
def is_strong(form, password):
|
||||
# To avoid errors in forms where password is optionnal
|
||||
if len(password.data) == 0:
|
||||
return
|
||||
|
||||
def entropy(password):
|
||||
"""Estimate entropy of a password, in bits"""
|
||||
# If you edit this function, please edit accordingly the JS one
|
||||
# in static/script/entropy.js
|
||||
chars = [
|
||||
"abcdefghijklmnopqrstuvwxyz",
|
||||
"ABCDFEGHIJKLMNOPQRSTUVWXYZ",
|
||||
"0123456789",
|
||||
" !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~§", # OWASP special chars
|
||||
"áàâéèêíìîóòôúùûç",
|
||||
]
|
||||
used = set()
|
||||
for c in password:
|
||||
for i in chars:
|
||||
if c in i:
|
||||
used.add(i)
|
||||
return log(len(''.join(used)) ** len(password), 2)
|
||||
|
||||
if entropy(password.data) < 60:
|
||||
raise ValidationError("Mot de passe pas assez complexe")
|
||||
|
||||
|
||||
def old_password(form, field):
|
||||
if field.data:
|
||||
if not form.old_password.data:
|
||||
raise ValidationError('Votre ancien mot de passe est requis pour cette modification.')
|
||||
if not current_user.check_password(form.old_password.data):
|
||||
raise ValidationError('Mot de passe actuel erroné.')
|
|
@ -29,7 +29,8 @@ class DefaultConfig(object):
|
|||
USE_LDAP = False
|
||||
# LDAP configuration
|
||||
LDAP_PASSWORD = "openldap"
|
||||
LDAP_ORGANIZATION = "o=planet-casio"
|
||||
LDAP_ROOT = "o=planet-casio"
|
||||
LDAP_ENV = "o=prod"
|
||||
# Secret key used to authenticate tokens. **USE YOURS!**
|
||||
SECRET_KEY = "a-random-secret-key"
|
||||
# Uploaded data folder
|
||||
|
|
|
@ -5,7 +5,7 @@ class LocalConfig(object):
|
|||
DB_NAME = "pcv5"
|
||||
USE_LDAP = True
|
||||
LDAP_PASSWORD = "openldap"
|
||||
LDAP_ORGANIZATION = "o=planet-casio"
|
||||
LDAP_ENV = "o=prod"
|
||||
SECRET_KEY = "a-random-secret-key" # CHANGE THIS VALUE *NOW*
|
||||
AVATARS_FOLDER = '/home/pc/data/avatars/'
|
||||
ENABLE_GUEST_POST = True
|
||||
|
|
10
master.py
10
master.py
|
@ -1,18 +1,18 @@
|
|||
#! /usr/bin/python3
|
||||
|
||||
from app import app, db
|
||||
from app.models.users import Member, Group, GroupPrivilege
|
||||
from app.models.privs import SpecialPrivilege
|
||||
from app.models.trophies import Trophy, Title, TrophyMember
|
||||
from app.models.user import Member, Group, GroupPrivilege
|
||||
from app.models.priv import SpecialPrivilege
|
||||
from app.models.trophy import Trophy, Title
|
||||
from app.models.forum import Forum
|
||||
from app.utils import unicode_names
|
||||
import os
|
||||
import sys
|
||||
import yaml
|
||||
import readline
|
||||
import slugify
|
||||
from PIL import Image
|
||||
|
||||
|
||||
help_msg = """
|
||||
This is the Planète Casio master shell. Type 'exit' or C-D to leave.
|
||||
|
||||
|
@ -186,7 +186,7 @@ def create_trophies():
|
|||
print(f"Created {len(tr)} trophies.")
|
||||
|
||||
# Create their icons in /app/static/images/trophies
|
||||
names = [ slugify.slugify(t["name"]) for t in tr ]
|
||||
names = [slugify.slugify(t["name"]) for t in tr]
|
||||
src = os.path.join(app.root_path, "data", "trophies.png")
|
||||
dst = os.path.join(app.root_path, "static", "images", "trophies")
|
||||
|
||||
|
|
Loading…
Reference in New Issue