diff --git a/.gitignore b/.gitignore
index 93d2f8b..b7d5fc6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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/
diff --git a/V5.py b/V5.py
index f395e0e..5745db6 100644
--- a/V5.py
+++ b/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
diff --git a/app/forms/account.py b/app/forms/account.py
index 3115ddb..e9bee41 100644
--- a/app/forms/account.py
+++ b/app/forms/account.py
@@ -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 CGU""",
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 !',
)
diff --git a/app/forms/forum.py b/app/forms/forum.py
index c9ab038..4cf4d2d 100644
--- a/app/forms/forum.py
+++ b/app/forms/forum.py
@@ -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
diff --git a/app/forms/login.py b/app/forms/login.py
index 72bec53..498a319 100644
--- a/app/forms/login.py
+++ b/app/forms/login.py
@@ -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(
diff --git a/app/forms/search.py b/app/forms/search.py
index 6ebac62..5ab431f 100644
--- a/app/forms/search.py
+++ b/app/forms/search.py
@@ -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(),
],
)
diff --git a/app/forms/trophies.py b/app/forms/trophy.py
similarity index 91%
rename from app/forms/trophies.py
rename to app/forms/trophy.py
index 62825c9..4a77b02 100644
--- a/app/forms/trophies.py
+++ b/app/forms/trophy.py
@@ -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 !',
)
diff --git a/app/models/forum.py b/app/models/forum.py
index 48aaffc..d661b4b 100644
--- a/app/models/forum.py
+++ b/app/models/forum.py
@@ -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:
# List of topics in this exact forum (of type Topic)
diff --git a/app/models/post.py b/app/models/post.py
index 88daabb..0f6dc2c 100644
--- a/app/models/post.py
+++ b/app/models/post.py
@@ -1,5 +1,4 @@
from app import db
-from app.models.users import User
from datetime import datetime
diff --git a/app/models/privs.py b/app/models/priv.py
similarity index 97%
rename from app/models/privs.py
rename to app/models/priv.py
index 692b3ae..ff96ae4 100644
--- a/app/models/privs.py
+++ b/app/models/priv.py
@@ -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
diff --git a/app/models/program.py b/app/models/program.py
index ef762f2..8ce6095 100644
--- a/app/models/program.py
+++ b/app/models/program.py
@@ -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
diff --git a/app/models/thread.py b/app/models/thread.py
index 6b62341..e623fee 100644
--- a/app/models/thread.py
+++ b/app/models/thread.py
@@ -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:
diff --git a/app/models/topic.py b/app/models/topic.py
index 4afe5db..1dcd8a8 100644
--- a/app/models/topic.py
+++ b/app/models/topic.py
@@ -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
diff --git a/app/models/trophies.py b/app/models/trophy.py
similarity index 100%
rename from app/models/trophies.py
rename to app/models/trophy.py
diff --git a/app/models/users.py b/app/models/user.py
similarity index 96%
rename from app/models/users.py
rename to app/models/user.py
index 63493c2..13c8ee2 100644
--- a/app/models/users.py
+++ b/app/models/user.py
@@ -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",
diff --git a/app/processors/menu.py b/app/processors/menu.py
index eae5737..d7b2007 100644
--- a/app/processors/menu.py
+++ b/app/processors/menu.py
@@ -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():
diff --git a/app/processors/utilities.py b/app/processors/utilities.py
index ad6dbde..7200fb8 100644
--- a/app/processors/utilities.py
+++ b/app/processors/utilities.py
@@ -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,
)
diff --git a/app/routes/account/account.py b/app/routes/account/account.py
index 18fad37..532c472 100644
--- a/app/routes/account/account.py
+++ b/app/routes/account/account.py
@@ -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
diff --git a/app/routes/account/login.py b/app/routes/account/login.py
index 2c4238e..058219f 100644
--- a/app/routes/account/login.py
+++ b/app/routes/account/login.py
@@ -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
diff --git a/app/routes/admin/account.py b/app/routes/admin/account.py
index 1abd8c0..1d4c04c 100644
--- a/app/routes/admin/account.py
+++ b/app/routes/admin/account.py
@@ -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
diff --git a/app/routes/admin/groups.py b/app/routes/admin/groups.py
index 35a03cb..7e667cd 100644
--- a/app/routes/admin/groups.py
+++ b/app/routes/admin/groups.py
@@ -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
diff --git a/app/routes/admin/trophies.py b/app/routes/admin/trophies.py
index 5eb59d0..80265bc 100644
--- a/app/routes/admin/trophies.py
+++ b/app/routes/admin/trophies.py
@@ -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
diff --git a/app/routes/development.py b/app/routes/development.py
index 0cbad14..f4f4cc0 100644
--- a/app/routes/development.py
+++ b/app/routes/development.py
@@ -9,7 +9,7 @@ import os
@app.route('/avatar/')
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)
diff --git a/app/routes/forum/index.py b/app/routes/forum/index.py
index 892ad90..aa0d209 100644
--- a/app/routes/forum/index.py
+++ b/app/routes/forum/index.py
@@ -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')
diff --git a/app/routes/forum/topic.py b/app/routes/forum/topic.py
index f3ab5ec..173e066 100644
--- a/app/routes/forum/topic.py
+++ b/app/routes/forum/topic.py
@@ -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')
diff --git a/app/routes/users.py b/app/routes/users.py
index 7172654..3cb4779 100644
--- a/app/routes/users.py
+++ b/app/routes/users.py
@@ -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
diff --git a/app/utils/converters.py b/app/utils/converters.py
index 7fb7a81..3caae7b 100644
--- a/app/utils/converters.py
+++ b/app/utils/converters.py
@@ -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):
diff --git a/app/utils/filesize.py b/app/utils/filesize.py
index 56e900e..888afff 100644
--- a/app/utils/filesize.py
+++ b/app/utils/filesize.py
@@ -1,5 +1,5 @@
import os
-from random import getrandbits
+
def filesize(file):
"""Return the filesize. Save in /tmp and delete it when done"""
diff --git a/app/utils/filters/is_title.py b/app/utils/filters/is_title.py
index bee2c42..15e3c39 100644
--- a/app/utils/filters/is_title.py
+++ b/app/utils/filters/is_title.py
@@ -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')
diff --git a/app/utils/ldap.py b/app/utils/ldap.py
index c3d21fb..78eaddb 100644
--- a/app/utils/ldap.py
+++ b/app/utils/ldap.py
@@ -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)
diff --git a/app/utils/notify.py b/app/utils/notify.py
index 2a98dc8..245885f 100644
--- a/app/utils/notify.py
+++ b/app/utils/notify.py
@@ -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.
diff --git a/app/utils/render.py b/app/utils/render.py
index f2f9f75..262944f 100644
--- a/app/utils/render.py
+++ b/app/utils/render.py
@@ -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
diff --git a/app/utils/valid_name.py b/app/utils/valid_name.py
index 158cc40..5b94228 100644
--- a/app/utils/valid_name.py
+++ b/app/utils/valid_name.py
@@ -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):
diff --git a/app/utils/validators/__init__.py b/app/utils/validators/__init__.py
index b93b9e6..08899e4 100644
--- a/app/utils/validators/__init__.py
+++ b/app/utils/validators/__init__.py
@@ -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:
diff --git a/app/utils/validators/file.py b/app/utils/validators/file.py
index fd1f743..7c2da7b 100644
--- a/app/utils/validators/file.py
+++ b/app/utils/validators/file.py
@@ -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)")
diff --git a/app/utils/validators/name.py b/app/utils/validators/name.py
new file mode 100644
index 0000000..9ec6b69
--- /dev/null
+++ b/app/utils/validators/name.py
@@ -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.")
diff --git a/app/utils/validators/password.py b/app/utils/validators/password.py
new file mode 100644
index 0000000..7f1b9ab
--- /dev/null
+++ b/app/utils/validators/password.py
@@ -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é.')
diff --git a/config.py b/config.py
index ae5df4d..be5e06f 100644
--- a/config.py
+++ b/config.py
@@ -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
diff --git a/local_config.py.default b/local_config.py.default
index 7885f5f..22986fe 100644
--- a/local_config.py.default
+++ b/local_config.py.default
@@ -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
diff --git a/master.py b/master.py
index 5e617c8..dd588f0 100755
--- a/master.py
+++ b/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")