Merge branch 'master-fork'
This commit is contained in:
commit
b6d758c197
|
@ -15,3 +15,4 @@ login.login_message = "Veuillez vous authentifier avant de continuer."
|
|||
|
||||
from app import models
|
||||
from app.routes import index, login, search, account, admin, users
|
||||
from app.utils import pluralize
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
-
|
||||
name: Administrateur
|
||||
css: "color: #ee0000"
|
||||
descr: "Vous voyez Chuck Norris ? Pareil."
|
||||
privs: access-admin-board access-assoc-board write-news
|
||||
upload-shared-files delete-shared-files
|
||||
edit-posts delete-posts scheduled-posting
|
||||
delete-content move-public-content move-private-content showcase-content
|
||||
edit-static-content extract-posts
|
||||
delete-notes delete-tests
|
||||
shoutbox-kick shoutbox-ban
|
||||
unlimited-pms footer-statistics community-login
|
||||
access-admin-panel edit-account delete-account
|
||||
-
|
||||
name: Modérateur
|
||||
css: "color: green"
|
||||
descr: "Maîtres du kick, ils sont là pour faire respecter un semblant d'ordre."
|
||||
privs: access-admin-board
|
||||
edit-posts delete-posts
|
||||
move-public-content extract-posts
|
||||
delete-notes delete-tests
|
||||
shoutbox-kick shoutbox-ban
|
||||
unlimited-pms
|
||||
-
|
||||
name: Développeur
|
||||
css: "color: #4169e1"
|
||||
descr: "Les développeurs maintiennent et améliorent le code du site."
|
||||
privs: access-admin-board
|
||||
upload-shared-files delete-shared-files
|
||||
scheduled-posting
|
||||
edit-static-content
|
||||
unlimited-pms footer-statistics community-login
|
||||
access-admin-panel
|
||||
-
|
||||
name: Rédacteur
|
||||
css: "color: blue"
|
||||
descr: "Rédigent les meilleurs articles de la page d'accueil, rien que pour
|
||||
vous <3"
|
||||
privs: access-admin-board write-news
|
||||
upload-shared-files delete-shared-files
|
||||
scheduled-posting
|
||||
showcase-content edit-static-content
|
||||
-
|
||||
name: Responsable communauté
|
||||
css: "color: DarkOrange"
|
||||
descr: "Anime les pages Twitter et Facebook de Planète Casio et surveille
|
||||
l'évolution du monde autour de nous !"
|
||||
privs: access-admin-board write-news
|
||||
upload-shared-files delete-shared-files
|
||||
scheduled-posting
|
||||
showcase-content
|
||||
-
|
||||
name: Partenaire
|
||||
css: "color: purple"
|
||||
descr: "Membres de l'équipe d'administration des sites partenaires."
|
||||
privs: write-news
|
||||
upload-shared-files delete-shared-files
|
||||
scheduled-posting
|
||||
-
|
||||
name: Compte communautaire
|
||||
css: "background:#d8d8d8; border-radius:4px; color:#303030; padding:1px 2px"
|
||||
descr: "Compte à usage général de l'équipe de Planète Casio."
|
||||
-
|
||||
name: Robot
|
||||
css: "color: #cf25d0"
|
||||
descr: "♫ Je suis Nono, le petit robot, l'ami d'Ulysse ♫"
|
||||
privs: shoutbox-post shoutbox-kick shoutbox-ban
|
||||
-
|
||||
name: Membre de CreativeCalc
|
||||
css: "color: #222222"
|
||||
descr: "CreativeCalc est l'association qui gère Planète Casio."
|
||||
privs: access-assoc-board
|
|
@ -1,5 +1,5 @@
|
|||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, PasswordField, BooleanField, TextAreaField, SubmitField
|
||||
from wtforms import StringField, PasswordField, BooleanField, TextAreaField, SubmitField, DecimalField
|
||||
from wtforms.fields.html5 import DateField
|
||||
from wtforms.validators import DataRequired, Optional, Email, EqualTo
|
||||
from flask_wtf.file import FileField # Cuz' wtforms' FileField is shitty
|
||||
|
@ -29,4 +29,22 @@ class UpdateAccountForm(FlaskForm):
|
|||
class DeleteAccountForm(FlaskForm):
|
||||
delete = BooleanField('Confirmer la suppression', validators=[DataRequired()], description='Attention, cette opération est irréversible !')
|
||||
old_password = PasswordField('Mot de passe', validators=[DataRequired(), vd.old_password])
|
||||
submit = SubmitField('Supprimer le compte')
|
||||
|
||||
|
||||
class AdminUpdateAccountForm(FlaskForm):
|
||||
username = StringField('Pseudonyme', validators=[DataRequired(), vd.name])
|
||||
avatar = FileField('Avatar', validators=[Optional(), vd.avatar])
|
||||
email = StringField('Adresse Email', validators=[Optional(), Email(), vd.email])
|
||||
password = PasswordField('Mot de passe :', validators=[Optional(), vd.password])
|
||||
xp = DecimalField('XP', validators=[Optional()])
|
||||
innovation = DecimalField('Innovation', validators=[Optional()])
|
||||
birthday = DateField('Anniversaire', validators=[Optional()])
|
||||
signature = TextAreaField('Signature', validators=[Optional()])
|
||||
biography = TextAreaField('Présentation', validators=[Optional()])
|
||||
newsletter = BooleanField('Inscription à la newsletter', description='Un mail par trimestre environ, pour être prévenu des concours, évènements et nouveautés.')
|
||||
submit = SubmitField('Mettre à jour')
|
||||
|
||||
class AdminDeleteAccountForm(FlaskForm):
|
||||
delete = BooleanField('Confirmer la suppression', validators=[DataRequired()], description='Attention, cette opération est irréversible !')
|
||||
submit = SubmitField('Supprimer le compte')
|
|
@ -27,12 +27,6 @@ class SpecialPrivilege(db.Model):
|
|||
def __repr__(self):
|
||||
return f'<Privilege "{self.priv}" of member #{mid}>'
|
||||
|
||||
# TODO: clean this. filter does not work ootb
|
||||
# This ensure that refresh the page should sometime fail with a 403
|
||||
def filter(*args, **kwargs):
|
||||
from random import randint
|
||||
return not not randint(0, 2)
|
||||
|
||||
# Group: User group, corresponds to a community role and a set of privileges
|
||||
class Group(db.Model):
|
||||
__tablename__ = 'group'
|
||||
|
@ -50,11 +44,29 @@ class Group(db.Model):
|
|||
members = db.relationship('Member', secondary=lambda:GroupMember,
|
||||
back_populates='groups')
|
||||
|
||||
def __init__(self, name, css):
|
||||
def __init__(self, name, css, descr):
|
||||
self.name = name
|
||||
self.css = css
|
||||
self.description = descr
|
||||
self.members = []
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
Deletes the group and the associated information:
|
||||
* Group privileges
|
||||
"""
|
||||
|
||||
for gp in GroupPrivilege.query.filter_by(gid=self.id).all():
|
||||
db.session.delete(gp)
|
||||
db.session.commit()
|
||||
|
||||
db.session.delete(self)
|
||||
db.session.commit()
|
||||
|
||||
def privs(self):
|
||||
gps = GroupPrivilege.query.filter_by(gid=self.id).all()
|
||||
return sorted(gp.priv for gp in gps)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Group "{self.name}">'
|
||||
|
||||
|
|
|
@ -35,25 +35,34 @@ class User(UserMixin, db.Model):
|
|||
def valid_name(name):
|
||||
"""
|
||||
Checks whether a string is a valid user name. The criteria are:
|
||||
1. No whitespace-class character
|
||||
2. At least one letter
|
||||
3. At least 3 characters and no longer than 32 characters
|
||||
1. At least 3 characters and no longer than 32 characters
|
||||
2. No whitespace-class character
|
||||
3. No special chars
|
||||
4. At least one letter
|
||||
5. Not in forbidden usernames
|
||||
|
||||
Possibily other intresting criteria:
|
||||
4. Unicode restriction
|
||||
6. Unicode restriction
|
||||
"""
|
||||
|
||||
# Rule 1
|
||||
if type(name) != str or len(name) < 3 or len(name) > 32:
|
||||
return False
|
||||
if name in V5Config.FORBIDDEN_USERNAMES:
|
||||
return False
|
||||
# Rule 2
|
||||
# Reject all Unicode whitespaces. This is important to avoid the most
|
||||
# common Unicode tricks!
|
||||
if re.search(r'\s', name) is not None:
|
||||
return False
|
||||
# Rule 3
|
||||
if re.search(V5Config.FORBIDDEN_CHARS_USERNAMES, name) is not None:
|
||||
return False
|
||||
# Rule 4
|
||||
# There must be at least one letter (avoid complete garbage)
|
||||
if re.search(r'\w', name) is None:
|
||||
return False
|
||||
# Rule 5
|
||||
if name in V5Config.FORBIDDEN_USERNAMES:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
@ -125,15 +134,33 @@ class Member(User, db.Model):
|
|||
self.signature = ""
|
||||
self.birthday = None
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
Deletes the user and the associated information:
|
||||
* Special privileges
|
||||
"""
|
||||
|
||||
for sp in SpecialPrivilege.query.filter_by(mid=self.id).all():
|
||||
db.session.delete(sp)
|
||||
db.session.commit()
|
||||
|
||||
db.session.delete(self)
|
||||
db.session.commit()
|
||||
|
||||
def priv(self, priv):
|
||||
"""Check whether the member has the specified privilege."""
|
||||
if SpecialPrivilege.filter(uid=self.id, priv=priv):
|
||||
if SpecialPrivilege.query.filter_by(mid=self.id, priv=priv).first():
|
||||
return True
|
||||
return False
|
||||
# return db.session.query(User, Group, GroupPrivilege).filter(
|
||||
# Group.id.in_(User.groups), GroupPrivilege.gid==Group.id,
|
||||
# GroupPrivilege.priv==priv).first() is not None
|
||||
|
||||
def special_privileges(self):
|
||||
"""List member's special privileges."""
|
||||
sp = SpecialPrivilege.query.filter_by(mid=self.id).all()
|
||||
return sorted(row.priv for row in sp)
|
||||
|
||||
def update(self, **data):
|
||||
"""
|
||||
Update all or part of the user's metadata. The [data] dictionary
|
||||
|
@ -173,6 +200,11 @@ class Member(User, db.Model):
|
|||
self.birthday = data["birthday"]
|
||||
if "newsletter" in data:
|
||||
self.newsletter = data["newsletter"]
|
||||
# For admins only
|
||||
if "xp" in data:
|
||||
self.xp = data["xp"]
|
||||
if "innovation" in data:
|
||||
self.innovation = data["innovation"]
|
||||
|
||||
def get_public_data(self):
|
||||
"""Returns the public information of the member."""
|
||||
|
|
|
@ -7,7 +7,7 @@ from app.utils.render import render
|
|||
|
||||
@app.route('/account', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def account():
|
||||
def edit_account():
|
||||
form = UpdateAccountForm()
|
||||
if request.method == "POST":
|
||||
if form.validate_on_submit():
|
||||
|
|
|
@ -1,53 +1,71 @@
|
|||
from flask import request, flash, redirect, url_for, abort
|
||||
from flask_login import login_required
|
||||
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.forms.account import AdminUpdateAccountForm, AdminDeleteAccountForm
|
||||
from app.utils.render import render
|
||||
from app.utils.priv_required import priv_required
|
||||
from app import app, db
|
||||
import yaml
|
||||
import os
|
||||
|
||||
@app.route('/admin', methods=['GET', 'POST'])
|
||||
@priv_required('panel-admin')
|
||||
def admin():
|
||||
class AdminForm(FlaskForm):
|
||||
submit = SubmitField('Régénérer les groupes, les privilèges, et les ' +
|
||||
'membres de test "PlanèteCasio" et "GLaDOS" (mdp "v5-forever")')
|
||||
@priv_required('access-admin-panel')
|
||||
def adm():
|
||||
return render('admin/index.html')
|
||||
|
||||
form = AdminForm()
|
||||
@app.route('/admin/groups', methods=['GET', 'POST'])
|
||||
@priv_required('access-admin-panel')
|
||||
def adm_groups():
|
||||
class GroupRegenerationForm(FlaskForm):
|
||||
submit = SubmitField(
|
||||
'Régénérer les groupes, privilèges, et comptes communs')
|
||||
|
||||
form = GroupRegenerationForm()
|
||||
if form.validate_on_submit():
|
||||
# Clean up groups
|
||||
for g in Group.query.all():
|
||||
db.session.delete(g)
|
||||
db.session.commit( )
|
||||
g.delete()
|
||||
|
||||
# Create base groups
|
||||
g_admins = Group('Administrateur', 'color: red')
|
||||
g_modos = Group('Modérateur', 'color: green')
|
||||
g_redacs = Group('Rédacteur', 'color: blue')
|
||||
g_community = Group('Compte communautaire', 'background: #c8c8c8;' +
|
||||
'border-radius: 4px; color: #303030; padding: 1px 2px')
|
||||
db.session.add(g_admins)
|
||||
db.session.add(g_modos)
|
||||
db.session.add(g_redacs)
|
||||
db.session.add(g_community)
|
||||
groups = []
|
||||
with open(os.path.join(app.root_path, "data", "groups.yaml")) as fp:
|
||||
groups = yaml.load(fp.read())
|
||||
|
||||
for g in groups:
|
||||
g["obj"] = Group(g["name"], g["css"], g["descr"])
|
||||
db.session.add(g["obj"])
|
||||
db.session.commit()
|
||||
|
||||
for g in groups:
|
||||
for priv in g.get("privs", "").split():
|
||||
db.session.add(GroupPrivilege(g["obj"], priv))
|
||||
db.session.commit()
|
||||
|
||||
# Clean up test members
|
||||
for name in "PlanèteCasio GLaDOS".split():
|
||||
m = Member.query.filter_by(name=name).first()
|
||||
if m is not None:
|
||||
db.session.delete(m)
|
||||
db.session.commit()
|
||||
m.delete()
|
||||
|
||||
# Create template members
|
||||
|
||||
def addgroup(member, group):
|
||||
g = Group.query.filter_by(name=group).first()
|
||||
if g is not None:
|
||||
member.groups.append(g)
|
||||
|
||||
m = Member('PlanèteCasio','contact@planet-casio.com','v5-forever')
|
||||
m.groups.append(g_community)
|
||||
addgroup(m, "Compte communautaire")
|
||||
db.session.add(m)
|
||||
|
||||
m = Member('GLaDOS', 'glados@aperture.science', 'v5-forever')
|
||||
m.groups.append(g_modos)
|
||||
m.groups.append(g_redacs)
|
||||
addgroup(m, "Robot")
|
||||
db.session.add(m)
|
||||
db.session.commit()
|
||||
|
||||
db.session.add(SpecialPrivilege(m, "edit-posts"))
|
||||
db.session.add(SpecialPrivilege(m, "shoutbox-ban"))
|
||||
|
||||
|
@ -55,4 +73,59 @@ def admin():
|
|||
|
||||
users = Member.query.all()
|
||||
groups = Group.query.all()
|
||||
return render('admin.html', users=users, groups=groups, form=form)
|
||||
return render('admin/groups_privileges.html', users=users, groups=groups,
|
||||
form=form)
|
||||
|
||||
@app.route('/admin/edit-account/<user_id>', methods=['GET', 'POST'])
|
||||
@priv_required('edit-account')
|
||||
def adm_edit_account(user_id):
|
||||
user = Member.query.filter_by(id=user_id).first()
|
||||
if not user:
|
||||
abort(404)
|
||||
form = AdminUpdateAccountForm()
|
||||
if request.method == "POST":
|
||||
if form.validate_on_submit():
|
||||
if form.avatar.data:
|
||||
f = form.avatar.data
|
||||
f.save("./app/static/"+user.avatar)
|
||||
user.update(
|
||||
name = form.username.data or None,
|
||||
email = form.email.data or None,
|
||||
password = form.password.data or None,
|
||||
birthday = form.birthday.data,
|
||||
signature = form.signature.data,
|
||||
bio = form.biography.data,
|
||||
newsletter = form.newsletter.data,
|
||||
xp = form.xp.data or None,
|
||||
innovation = form.innovation.data or None
|
||||
)
|
||||
db.session.merge(user)
|
||||
db.session.commit()
|
||||
flash('Modifications effectuées', 'ok')
|
||||
else:
|
||||
flash('Erreur lors de la modification', 'error')
|
||||
|
||||
return render('admin/edit_account.html', user=user, form=form)
|
||||
|
||||
@app.route('/admin/edit-account/<user_id>/delete', methods=['GET', 'POST'])
|
||||
@priv_required('delete-account')
|
||||
def adm_delete_account(user_id):
|
||||
user = Member.query.filter_by(id=user_id).first_or_404()
|
||||
|
||||
# Note: A user deleting their own account will be disconnected.
|
||||
|
||||
# TODO: Add an overview of what will be deleted.
|
||||
# * How many posts will be turned into guest posts
|
||||
# * Option: purely delete the posts in question
|
||||
# * How many PMs will be deleted (can't unassign PMs)
|
||||
# * etc.
|
||||
del_form = AdminDeleteAccountForm()
|
||||
if request.method == "POST":
|
||||
if del_form.validate_on_submit():
|
||||
user.delete()
|
||||
flash('Compte supprimé', 'ok')
|
||||
return redirect(url_for('adm'))
|
||||
else:
|
||||
flash('Erreur lors de la suppression du compte', 'error')
|
||||
del_form.delete.data = False # Force to tick to delete the account
|
||||
return render('admin/delete_account.html', user=user, del_form=del_form)
|
||||
|
|
|
@ -16,5 +16,5 @@ def user(username):
|
|||
def user_by_id(user_id):
|
||||
user = Member.query.filter_by(id=user_id).first()
|
||||
if not user:
|
||||
abort(404)
|
||||
abort(404)
|
||||
return redirect(url_for('user', username=user.name))
|
||||
|
|
|
@ -1,26 +1,25 @@
|
|||
#container {
|
||||
.container {
|
||||
margin-left: 60px;
|
||||
}
|
||||
|
||||
section {
|
||||
min-width: 350px; width: 90%;
|
||||
margin: 20px auto 0; padding: 20px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #dddddd; border-radius: 5px;
|
||||
min-width: 350px; width: 80%;
|
||||
margin: 20px auto 0 auto;
|
||||
}
|
||||
|
||||
section h1 {
|
||||
margin-top: 0;
|
||||
border-bottom: 1px solid #a0a0a0;
|
||||
font-family: Raleway; font-size: 32px;
|
||||
font-weight: 300; color: #242424;
|
||||
border-bottom: 1px solid #d8d8d8;
|
||||
font-family: Cantarell; font-weight: bold;
|
||||
font-size: 26px; color: #101010;
|
||||
}
|
||||
|
||||
section h2 {
|
||||
margin-top: 0;
|
||||
border-bottom: 1px solid #a0a0a0;
|
||||
font-family: Raleway; font-size: 26px;
|
||||
font-weight: 300; color: #242424;
|
||||
margin: 24px 0 16px 0;
|
||||
border-bottom: 1px solid #d8d8d8;
|
||||
font-family: Cantarell; font-weight: bold;
|
||||
font-size: 18px; color: #101010;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
section .avatar {
|
||||
|
@ -28,15 +27,3 @@ section .avatar {
|
|||
border-radius: 100%;
|
||||
width: 150px; height: 150px;
|
||||
}
|
||||
|
||||
/* #container h1 {
|
||||
margin-left: 5%;
|
||||
font-family: Raleway; font-size: 24px;
|
||||
font-weight: 200; color: #242424;
|
||||
}
|
||||
|
||||
#container h2 {
|
||||
margin-left: 5%;
|
||||
font-family: Raleway; font-size: 20px;
|
||||
font-weight: 200; color: #242424;
|
||||
} */
|
||||
|
|
|
@ -1,52 +1,46 @@
|
|||
/*
|
||||
fonts
|
||||
*/
|
||||
/* Fonts */
|
||||
|
||||
@font-face { font-family: NotoSans; src: url(../fonts/noto_sans.ttf); }
|
||||
@font-face { font-family: Raleway; font-weight: 200; src: url(../fonts/raleway_200.ttf); }
|
||||
@font-face { font-family: Raleway; font-weight: 300; src: url(../fonts/raleway_300.ttf); }
|
||||
@font-face { font-family: Cantarell; font-weight: normal; src: url(../fonts/Cantarell-Regular.otf); }
|
||||
@font-face { font-family: Cantarell; font-weight: bold; src: url(../fonts/Cantarell-Bold.otf); }
|
||||
|
||||
|
||||
|
||||
/*
|
||||
ALL
|
||||
*/
|
||||
/* Whole page */
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
transition: .15s ease;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
Body
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: #ffffff;
|
||||
font-family: 'DejaVu Sans', sans-serif;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
Links
|
||||
*/
|
||||
/* General */
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #c61a1a;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
ul {
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
/*
|
||||
Inputs
|
||||
*/
|
||||
/* Forms */
|
||||
|
||||
input,
|
||||
textarea {
|
||||
|
@ -59,30 +53,33 @@ textarea:focus {
|
|||
box-shadow: 0 0 0 0.2rem rgba(0,123,255,0.25);
|
||||
}
|
||||
|
||||
/* Textarea */
|
||||
textarea {
|
||||
width: 100%;
|
||||
border: 1px solid #eeeeee;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.button,
|
||||
input[type="button"],
|
||||
input[type="submit"] {
|
||||
padding: 6px 10px;
|
||||
border: 1px solid transparent; border-radius: 3px;
|
||||
font-family: 'DejaVu Sans', sans-serif; font-size: 14px; font-weight: 400;
|
||||
}
|
||||
|
||||
/* Checkbox */
|
||||
input[type="checkbox"] {
|
||||
display: inline;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
|
||||
.button,
|
||||
input[type="button"],
|
||||
input[type="submit"] {
|
||||
padding: 6px 10px; border-radius: 2px;
|
||||
cursor: pointer;
|
||||
font-family: 'DejaVu Sans', sans-serif; font-weight: 400;
|
||||
cursor: pointer;
|
||||
}
|
||||
input[type="button"]:hover,
|
||||
input[type="submit"]:hover,
|
||||
.button:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
Bootstrap-style rules
|
||||
*/
|
||||
.flex {
|
||||
|
@ -91,22 +88,45 @@ input[type="checkbox"] {
|
|||
|
||||
.bg-green,
|
||||
.bg-green {
|
||||
background-color: #149641;
|
||||
background: #149641;
|
||||
color: #ffffff;
|
||||
}
|
||||
.bg-green:hover,
|
||||
.bg-green:focus,
|
||||
.bg-green:active {
|
||||
background-color: #0f7331;
|
||||
background: #0f7331;
|
||||
}
|
||||
|
||||
.bg-red,
|
||||
.bg-red {
|
||||
background-color: #c0341d;
|
||||
background: #d23a2f;
|
||||
color: #ffffff;
|
||||
}
|
||||
.bg-red:hover,
|
||||
.bg-red:focus,
|
||||
.bg-red:active {
|
||||
background-color: #aa3421;
|
||||
background: #b32a20;
|
||||
}
|
||||
|
||||
.bg-orange {
|
||||
background: #f59f25;
|
||||
color: #ffffff;
|
||||
}
|
||||
.bg-orange:hover,
|
||||
.bg-orange:focus,
|
||||
.bg-orange:active {
|
||||
background: #ea9720;
|
||||
}
|
||||
|
||||
.bg-white,
|
||||
.bg-white {
|
||||
border: 1px solid #e5e5e5;
|
||||
background: #ffffff;
|
||||
color: #000000;
|
||||
}
|
||||
.bg-white:hover,
|
||||
.bg-white:focus,
|
||||
.bg-white:active {
|
||||
background: #f0f0f0;
|
||||
border-color: #e3e3e3;
|
||||
}
|
||||
|
|
|
@ -3,23 +3,56 @@
|
|||
*/
|
||||
|
||||
header {
|
||||
height: 50px; margin: 0; padding: 0 30px;
|
||||
height: 50px; margin: 0; padding: 0 16px;
|
||||
background: #f4f4f6; border-bottom: 1px solid #d0d0d0;
|
||||
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
background: #f8f8fa; border-bottom: 1px solid #d0d0d0;
|
||||
flex-flow: row wrap;
|
||||
}
|
||||
@media screen and (max-width: 1000px) {
|
||||
header {
|
||||
height: 75px;
|
||||
}
|
||||
header .title {
|
||||
page-break-after: always;
|
||||
}
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-family: Raleway; font-weight: 200;
|
||||
header .title a {
|
||||
color: inherit;
|
||||
}
|
||||
header .title h1 {
|
||||
font-family: Cantarell; font-weight: bold; font-size: 18px;
|
||||
color: #181818;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
header .spacer {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
header .links {
|
||||
margin-left: 16px;
|
||||
}
|
||||
header svg {
|
||||
width: 24px; height: 24px; vertical-align: middle;
|
||||
transition: .15s ease;
|
||||
}
|
||||
header a:hover > svg, header a:focus > svg {
|
||||
filter: brightness(.5);
|
||||
fill: black;
|
||||
}
|
||||
header a {
|
||||
fill: #363636;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
header form {
|
||||
/* The search icon is draws inside the input field but its space is allocated
|
||||
on the right. Apply a negative margin to compensate this:
|
||||
-24px for the search icon
|
||||
-2px for the spacing between the search icon and the field */
|
||||
margin-right: -26px;
|
||||
}
|
||||
header input[type="search"] {
|
||||
display: inline-block; width: 250px;
|
||||
padding: 5px 35px 5px 10px;
|
||||
|
@ -40,13 +73,9 @@ header input[type="search"]:focus ~ a > svg > path {
|
|||
fill: #333333;
|
||||
}
|
||||
|
||||
#spotlight {
|
||||
margin-left: 16px;
|
||||
}
|
||||
#spotlight a {
|
||||
padding: 8px 18px 6px 18px;
|
||||
color: #727272; font-size: 15px;
|
||||
border-bottom: 2px solid rgba(93, 123, 141, 0);
|
||||
transition: border .15s ease;
|
||||
display: block;
|
||||
}
|
||||
#spotlight a:hover, header #spotlight a:focus {
|
||||
border-bottom: 2px solid rgba(93, 123, 141, 1);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,98 +1,58 @@
|
|||
/*
|
||||
fonts
|
||||
*/
|
||||
|
||||
@font-face { font-family: NotoSans; src: url(../fonts/noto_sans.ttf); }
|
||||
@font-face { font-family: Raleway; font-weight: 200; src: url(../fonts/raleway_200.ttf); }
|
||||
@font-face { font-family: Raleway; font-weight: 300; src: url(../fonts/raleway_300.ttf); }
|
||||
|
||||
|
||||
/* Global */
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: #ffffff;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
nav a {
|
||||
color: #ffffff; opacity: .7;
|
||||
text-decoration: none;
|
||||
transition: opacity .15s ease;
|
||||
}
|
||||
nav a:hover,
|
||||
nav a:focus {
|
||||
opacity: 1;
|
||||
}
|
||||
/* Whole page */
|
||||
|
||||
.light-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
/* Menu */
|
||||
|
||||
#spacer-menu {
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
#light-menu {
|
||||
list-style: none;
|
||||
display: flex; flex-direction: row; align-items: center;
|
||||
width: 100%; height: 40px;
|
||||
width: 100%; height: 60px;
|
||||
overflow-x: auto; overflow-y: hidden;
|
||||
margin: 0; padding: 0;
|
||||
text-indent: 0;
|
||||
background: #22292c; box-shadow: 0 0 2px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
#logo {
|
||||
position: relative; display: block;
|
||||
height: 100%; opacity: 1;
|
||||
background: -moz-linear-gradient(left, #bf1c11, #ba1203);
|
||||
background: -webkit-linear-gradient(left, #bf1c11, #ba1203);
|
||||
width: auto; height: 100%; margin-bottom: 0;
|
||||
}
|
||||
/*#logo::after {
|
||||
position: absolute; left: 100%; top: 50%;
|
||||
height: 0; width: 0;
|
||||
border: solid transparent; content: " ";
|
||||
border-left-color: #ba1203;
|
||||
border-width: 4px;
|
||||
margin-top: -4px;
|
||||
}*/
|
||||
#logo img {
|
||||
width: 40px;
|
||||
margin: 0; padding: 0;
|
||||
width: 60px;
|
||||
margin-bottom: -4.5px;
|
||||
filter: drop-shadow(0 0 2px rgba(0, 0, 0, .0));
|
||||
transition: filter .15s ease;
|
||||
}
|
||||
#logo:hover img,
|
||||
#logo:focus img {
|
||||
filter: drop-shadow(0 0 2px rgba(0, 0, 0, .7));
|
||||
}
|
||||
|
||||
#light-menu > li {
|
||||
#light-menu li {
|
||||
display: flex; flex-direction: column;
|
||||
align-items: center; flex-grow: 1;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
font-family: Raleway; font-size: 13px;
|
||||
color: #ffffff;
|
||||
}
|
||||
#light-menu li {
|
||||
padding: 0 2px;
|
||||
}
|
||||
#light-menu li > a {
|
||||
display: flex; flex-direction: column;
|
||||
align-items: center; justify-content: center;
|
||||
width: 100%; height: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
#light-menu li > a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
#light-menu li > a > svg {
|
||||
width: 20px;
|
||||
}
|
||||
#light-menu li > a > div {
|
||||
display: none;
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
#light-menu li > a > svg {
|
||||
display: block; width: 20px; flex-shrink: 0;
|
||||
margin: 0 auto 5px auto;
|
||||
#light-menu li:not(.opened) > a:hover::after,
|
||||
#light-menu li:not(.opened) > a:focus::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
#light-menu li span[notifications]:not([notifications="0"])::before {
|
||||
content: attr(notifications);
|
||||
display: inline-block; margin-right: 6px;
|
||||
|
@ -108,18 +68,17 @@ nav a:focus {
|
|||
font-family: NotoSans; font-size: 12px;
|
||||
background: #22292c; box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
|
||||
transition: .1s ease;
|
||||
position: unset;
|
||||
left: unset;
|
||||
}
|
||||
#menu.opened {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
left: unset;
|
||||
}
|
||||
|
||||
#menu > div {
|
||||
width: 100%;
|
||||
display: none;
|
||||
}
|
||||
#menu > div.opened {
|
||||
display: block;
|
||||
}
|
||||
#menu h2 {
|
||||
margin: 10px 0 10px 40px;
|
||||
|
@ -175,15 +134,15 @@ nav a:focus {
|
|||
margin: 5px 0;
|
||||
}
|
||||
|
||||
@media all and (min-width: 550px) {
|
||||
#light-menu {
|
||||
height: 60px;
|
||||
@media all and (max-width: 550px) {
|
||||
#light-menu, #spacer-menu {
|
||||
height: 40px;
|
||||
}
|
||||
#logo img {
|
||||
width: 60px;
|
||||
width: 40px;
|
||||
}
|
||||
#light-menu li > a > div {
|
||||
display: block;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -193,9 +152,6 @@ nav a:focus {
|
|||
font-size: 14px;
|
||||
background: #e8e8e8; transition: background .15s ease;
|
||||
}
|
||||
#menu form input:focus {
|
||||
background: #ffffff;
|
||||
}
|
||||
#menu form input:first-child {
|
||||
margin-bottom: 0; border-bottom: none;
|
||||
border-top-left-radius: 5px;
|
||||
|
@ -222,73 +178,25 @@ nav a:focus {
|
|||
/* Header */
|
||||
|
||||
header {
|
||||
padding: 10px 10px 0px 10px;
|
||||
background: #f8f8fa; border-bottom: 1px solid #d0d0d0;
|
||||
padding: 0 8px;
|
||||
}
|
||||
header svg {
|
||||
width: 24px; height: 24px; vertical-align: middle;
|
||||
transition: .15s ease;
|
||||
}
|
||||
header a:hover > svg, header a:focus > svg {
|
||||
filter: brightness(.5);
|
||||
}
|
||||
|
||||
header input[type="search"] {
|
||||
width: 100%;
|
||||
border: 0; border-radius: 1px;
|
||||
font-family: "Segoe UI", Helvetica, "Droid Sans", Arial,sans-serif;
|
||||
box-shadow: 0 0 1px rgba(0, 0, 0, .4); transition: .15s ease;
|
||||
}
|
||||
|
||||
#spotlight {
|
||||
display: flex;
|
||||
align-items: center; justify-content: space-around;
|
||||
}
|
||||
#spotlight a {
|
||||
padding: 5px 10px; margin: 5px 0;
|
||||
color: #727272; font-size: 14px;
|
||||
/*border-bottom: 2px solid rgba(93, 123, 141, .5);*/
|
||||
transition: border .15s ease;
|
||||
text-decoration: none;
|
||||
}
|
||||
#spotlight a:hover, header #spotlight a:focus {
|
||||
color: #404040;
|
||||
}
|
||||
|
||||
|
||||
/* Homepage */
|
||||
|
||||
#shoutbox {
|
||||
display: none;
|
||||
}
|
||||
|
||||
section {
|
||||
margin: 10px;
|
||||
}
|
||||
section h1 {
|
||||
margin: 10px 0;
|
||||
border-bottom: 1px solid #a0a0a0;
|
||||
font-family: Raleway; font-size: 20px;
|
||||
font-weight: 200; color: #242424;
|
||||
}
|
||||
section * {
|
||||
transition: .15s ease;
|
||||
}
|
||||
.home-title {
|
||||
margin: 20px 0; padding: 10px;
|
||||
background: #bf1c11; box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
|
||||
border-top: 10px solid #ab170c;
|
||||
}
|
||||
.home-title h1 {
|
||||
margin: 0;
|
||||
color: #ffffff; border-color: #ffffff;
|
||||
padding: 10px;
|
||||
}
|
||||
.home-title p {
|
||||
margin-bottom: 0; text-align: justify;
|
||||
color: #ffffff; font-size: 14px;
|
||||
}
|
||||
.home-title a {
|
||||
color: inherit; text-decoration: underline;
|
||||
}
|
||||
#shoutbox {
|
||||
display: none;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.home-pinned-content {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
@ -308,20 +216,18 @@ section * {
|
|||
flex-grow: 1; margin-left: 10px;
|
||||
}
|
||||
.home-pinned-content h2 {
|
||||
margin: 0;
|
||||
font-family: Raleway; font-size: 18px;
|
||||
font-weight: 400; color: #242424;
|
||||
margin: 0; color: #242424;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.home-pinned-content span {
|
||||
color: #000000; font-size: 14px;
|
||||
}
|
||||
|
||||
.home-articles > div {
|
||||
margin-top: 30px;
|
||||
}
|
||||
.home-articles article {
|
||||
margin-bottom: 15px;
|
||||
display: flex; align-items: center;
|
||||
}
|
||||
.home-articles article > img {
|
||||
flex-shrink: 0; width: 128px; height: 64px;
|
||||
|
@ -329,20 +235,11 @@ section * {
|
|||
.home-articles article > div {
|
||||
margin-left: 5px;
|
||||
}
|
||||
.home-articles h1 {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
}
|
||||
.home-articles h1 > a {
|
||||
font-size: 13px; color: #666666;
|
||||
}
|
||||
.home-articles h3 {
|
||||
margin: 0;
|
||||
color: #424242; font-weight: normal;
|
||||
}
|
||||
.home-articles p {
|
||||
margin: 5px 0;
|
||||
text-align: justify;
|
||||
color: #808080; font-size: 14px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
|
||||
|
@ -351,16 +248,3 @@ section * {
|
|||
.alert {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
/* Footer */
|
||||
|
||||
footer {
|
||||
margin: 20px 10% 5px 10%; padding: 10px 0;
|
||||
text-align: center; font-size: 11px; font-style: italic;
|
||||
color: #a0a0a0;
|
||||
border-top: 1px solid rgba(0, 0, 0, .1);
|
||||
}
|
||||
footer p {
|
||||
margin: 3px 0;
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
nav a {
|
||||
color: #ffffff;
|
||||
opacity: .7;
|
||||
opacity: 0.75;
|
||||
cursor: pointer;
|
||||
}
|
||||
nav a:hover,
|
||||
|
@ -11,6 +11,16 @@ nav a:focus {
|
|||
|
||||
/* Menu */
|
||||
|
||||
#light-menu {
|
||||
position: fixed; z-index: 10;
|
||||
list-style: none;
|
||||
width: 60px;
|
||||
height: 100%; overflow-y: auto;
|
||||
margin: 0; padding: 0;
|
||||
text-indent: 0;
|
||||
background: #22292c; box-shadow: 0 0 2px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
#logo {
|
||||
position: relative; display: block;
|
||||
width: 100%;
|
||||
|
@ -21,16 +31,6 @@ nav a:focus {
|
|||
background: #bf1c11;
|
||||
transition: .15s ease;
|
||||
}
|
||||
/* Flèche */
|
||||
/*nav #logo::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 100%; left: 50%;
|
||||
height: 0; width: 0;
|
||||
border: solid transparent;
|
||||
border-top-color: #ba1203;
|
||||
border-width: 12px; margin-left: -12px;
|
||||
}*/
|
||||
#logo img {
|
||||
width: 100%;
|
||||
margin: 0; padding: 0;
|
||||
|
@ -42,20 +42,14 @@ nav a:focus {
|
|||
#logo:focus {
|
||||
background: #d72411;
|
||||
}
|
||||
|
||||
#light-menu {
|
||||
position: fixed; z-index: 10;
|
||||
list-style: none;
|
||||
width: 60px;
|
||||
height: 100%; overflow-y: auto;
|
||||
margin: 0; padding: 0;
|
||||
text-indent: 0;
|
||||
background: #22292c; box-shadow: 0 0 2px rgba(0, 0, 0, 0.3);
|
||||
#logo:hover img,
|
||||
#logo:focus img {
|
||||
filter: drop-shadow(0 0 2px rgba(0, 0, 0, .7));
|
||||
}
|
||||
|
||||
#light-menu li {
|
||||
width: 100%; height: 45px;
|
||||
text-align: center;
|
||||
font-family: Raleway; font-size: 13px;
|
||||
color: #ffffff;
|
||||
}
|
||||
#light-menu li > a {
|
||||
|
@ -75,7 +69,7 @@ nav a:focus {
|
|||
#light-menu li > a::after {
|
||||
content: attr(label);
|
||||
position: fixed; display: none;
|
||||
padding: 4px 8px; /*margin-top: -28px;*/ left: 63px;
|
||||
padding: 4px 8px; left: 63px;
|
||||
font-family: NotoSans; border-radius: 3px;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
|
@ -138,12 +132,23 @@ nav a:focus {
|
|||
font-family: Raleway; font-size: 18px;
|
||||
color: #ffffff;
|
||||
}
|
||||
#menu h2 a {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
font-size: inherit; opacity: inherit;
|
||||
}
|
||||
#menu h2 > svg {
|
||||
width: 42px; vertical-align: middle;
|
||||
}
|
||||
#menu h2 img {
|
||||
width: 64px; border-radius: 50%; vertical-align: middle; margin-right: 10px;
|
||||
}
|
||||
#menu h2 a:hover,
|
||||
#menu h2 a:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#menu h3 {
|
||||
margin: 20px 0 20px 40px;
|
||||
|
|
|
@ -24,8 +24,8 @@
|
|||
|
||||
|
||||
@media all and (min-width: 1400px) {
|
||||
body {
|
||||
font-size: 14px;
|
||||
body, input {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
header input[type="search"] {
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
table {
|
||||
border-collapse: collapse;
|
||||
border-color: #d8d8d8;
|
||||
border-style: solid;
|
||||
border-width: 0 0 1px 0;
|
||||
}
|
||||
table tr:nth-child(even) {
|
||||
background: rgba(0, 0, 0, .05);
|
||||
}
|
||||
table th {
|
||||
background: #e0e0e0;
|
||||
border-color: #d0d0d0;
|
||||
border-style: solid;
|
||||
border-width: 1px 0;
|
||||
padding: 2px;
|
||||
}
|
||||
table td {
|
||||
padding: 4px 6px;
|
||||
}
|
Binary file not shown.
Binary file not shown.
|
@ -17,7 +17,7 @@ var trigger_menu = function(active) {
|
|||
|
||||
var menu = document.getElementById('menu');
|
||||
var buttons = document.getElementById('light-menu').getElementsByTagName('li');
|
||||
var menus = document.getElementById('menu').getElementsByTagName('div');
|
||||
var menus = document.querySelectorAll('#menu > div');
|
||||
|
||||
if(active == -1 || buttons[active].classList.contains('opened')) {
|
||||
hide(menu);
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
{% extends "base/container.html" %}
|
||||
{% extends "base/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="form">
|
||||
<h1>Gestion du compte</h1>
|
||||
|
||||
<form action="{{ url_for('account') }}" method="post" enctype="multipart/form-data">
|
||||
<form action="{{ url_for('edit_account') }}" method="post" enctype="multipart/form-data">
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<h2>Général</h2>
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
{% extends "base/container.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section>
|
||||
<form action='' method='POST'>
|
||||
{{ form.hidden_tag() }}
|
||||
{{ form.submit }}
|
||||
</form>
|
||||
|
||||
<h2>List of members</h2>
|
||||
|
||||
<table style="width:70%; margin: auto;">
|
||||
<tr><th>Name</th><th>Email</th><th>Register</th><th>XP</th><th>Inn.</th>
|
||||
<th>Newsletter</th></tr>
|
||||
|
||||
{% for user in users %}
|
||||
<tr><td><a href="{{ url_for('user_by_id', user_id=user.id) }}" >{{ user.name }}</a></td>
|
||||
<td>{{ user.email }}</td>
|
||||
<td>{{ user.register_date }}</td><td>{{ user.xp }}</td>
|
||||
<td>{{ user.innovation }}</td>
|
||||
<td>{{ "Yes" if user.newsletter else "No" }}</td></tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
<h2>List of groups</h2>
|
||||
|
||||
<table style="width:70%; margin: auto;">
|
||||
<tr><th>Group</th><th>Members</th><th>Privileges</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>
|
||||
{% endfor %}
|
||||
</td></tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -0,0 +1,30 @@
|
|||
{% extends "base/base.html" %}
|
||||
|
||||
{% block title %}
|
||||
<a href="{{ url_for('adm') }}">Panneau d'administration</a> » <h1>Suppression du compte de '{{ user.name }}'</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="form">
|
||||
<h2>Confirmer la suppression du compte</h2>
|
||||
<p>Le compte '{{ user.name }}' que vous allez supprimer est lié à :</p>
|
||||
<ul>
|
||||
<li>{{ user.groups | length }} groupe{{ user.groups|length|pluralize }}</li>
|
||||
<li>{% set sp = user.special_privileges() | length %}
|
||||
{{- sp }} privilège{{sp|pluralize}} spéci{{sp|pluralize("al","aux")}}</li>
|
||||
</ul>
|
||||
|
||||
<form action="{{ url_for('adm_delete_account', user_id=user.id) }}" method=post>
|
||||
{{ del_form.hidden_tag() }}
|
||||
<div>
|
||||
{{ del_form.delete.label }}
|
||||
{{ del_form.delete(checked=False) }}
|
||||
<div style="font-size: 80%; color: gray">{{ del_form.delete.description }}</div>
|
||||
{% for error in del_form.delete.errors %}
|
||||
<span class=msgerror>{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>{{ del_form.submit(class_="bg-red") }}</div>
|
||||
</form>
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -0,0 +1,97 @@
|
|||
{% extends "base/base.html" %}
|
||||
|
||||
{% block title %}
|
||||
<a href="{{ url_for('adm') }}">Panneau d'administration</a> » <h1>Édition du compte de '{{ user.name }}'</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="form">
|
||||
<form action="{{ url_for('adm_edit_account', user_id=user.id) }}" method="post" enctype="multipart/form-data">
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<h2>Général</h2>
|
||||
<div>
|
||||
{{ form.avatar.label }}
|
||||
<div>
|
||||
<img class="avatar" src="{{ url_for('static', filename=user.avatar) }}" meta="{{ user.avatar }}" />
|
||||
{{ form.avatar }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{{ form.username.label }}
|
||||
{{ form.username(placeholder=user.name) }}
|
||||
{% for error in form.username.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>
|
||||
{{ form.email.label }}
|
||||
{{ form.email(placeholder=user.email) }}
|
||||
{% for error in form.email.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>
|
||||
{{ form.password.label }}
|
||||
{{ form.password(placeholder='************') }}
|
||||
{% for error in form.password.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<h2>Participation</h2>
|
||||
<div>
|
||||
{{ form.xp.label }}
|
||||
{{ form.xp(placeholder=user.xp) }}
|
||||
{% for error in form.xp.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>
|
||||
{{ form.innovation.label }}
|
||||
{{ form.innovation(placeholder=user.innovation) }}
|
||||
{% for error in form.innovation.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<h2>À propos</h2>
|
||||
<div>
|
||||
{{ form.birthday.label }}
|
||||
{{ form.birthday(value=user.birthday) }}
|
||||
{% for error in form.birthday.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>
|
||||
{{ form.signature.label }}
|
||||
<textarea id="{{ form.signature.name }}" name="{{ form.signature.name }}">{{ user.signature }}</textarea>
|
||||
{% for error in form.signature.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>
|
||||
{{ form.biography.label }}
|
||||
<textarea id="{{ form.biography.name }}" name="{{ form.biography.name }}">{{ user.bio }}</textarea>
|
||||
{% for error in form.biography.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<h2>Préférences</h2>
|
||||
<div>
|
||||
{{ form.newsletter.label }}
|
||||
{{ form.newsletter(checked=user.newsletter) }}
|
||||
<div style="font-size:80%;color:rgba(0,0,0,.5)">{{ form.newsletter.description }}</div>
|
||||
{% for error in form.newsletter.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>{{ form.submit(class_="bg-green") }}</div>
|
||||
</form>
|
||||
|
||||
<h2 style="margin-top:30px;">Supprimer le compte</h2>
|
||||
<a href="{{ url_for('adm_delete_account', user_id=user.id) }}" class="button bg-red">Supprimer le compte</a>
|
||||
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -0,0 +1,74 @@
|
|||
{% extends "base/base.html" %}
|
||||
|
||||
{% block title %}
|
||||
<a href="{{ url_for('adm') }}">Panneau d'administration</a> » <h1>Groupes et privilèges</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section>
|
||||
<p>Cette page présente une vue d'ensemble des groupes et privilèges
|
||||
associés. Elle supervise également les détenteurs de privilèges.</p>
|
||||
|
||||
<h2>Membres détenteurs de privilèges</h2>
|
||||
|
||||
<table style="width:90%; margin: auto;">
|
||||
<tr><th>Pseudo</th><th>Email</th><th>Groupes</th>
|
||||
<th>Privilèges spéciaux</th><th>Modifier</th></tr>
|
||||
|
||||
{% for user in users %}
|
||||
<tr><td><a href="{{ url_for('user_by_id', user_id=user.id) }}" title="Page de profil publique de {{ user.name }}">{{ user.name }}</a></td>
|
||||
<td>{{ user.email }}</td>
|
||||
<td>{% for g in user.groups %}
|
||||
<span style="{{ g.css }}">{{ g.name }}</span>
|
||||
{{ ', ' if not loop.last }}
|
||||
{% endfor %}</td>
|
||||
<td>{% for priv in user.special_privileges() %}
|
||||
<code>{{ priv }}</code>
|
||||
{{- ', ' if not loop.last }}
|
||||
{% endfor %}</td>
|
||||
<td><a href="{{ url_for('adm_edit_account', user_id=user.id) }}">Modifier</a></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>
|
||||
|
||||
<h2>Restauration des groupes et privilèges</h2>
|
||||
|
||||
<p>Cette fonction régénère un ensemble minimal de groupes et membres
|
||||
permettant de lancer le forum. Elle opère les modifications
|
||||
suivantes :</p>
|
||||
|
||||
<ul>
|
||||
<li>Suppression de tous les groupes.</li>
|
||||
<li>Création des groupes Administrateur, Modérateur, Développeur,
|
||||
Rédacteur, Responsable communauté, Partenaire, Compte communautaire,
|
||||
Robot, Membre de CreativeCalc.</li>
|
||||
<li>Attribution des privilèges associés à ces groupes.</li>
|
||||
<li>Recréation des comptes communs : PlanèteCasio (compte communautaire),
|
||||
GLaDOS (robot).</li>
|
||||
</ul>
|
||||
|
||||
<form action='' method='POST'>
|
||||
{{ form.hidden_tag() }}
|
||||
{{ form.submit(class="bg-orange") }}
|
||||
</form>
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -0,0 +1,14 @@
|
|||
{% extends "base/base.html" %}
|
||||
|
||||
{% block title %}
|
||||
<h1>Panneau d'administration</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<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>
|
||||
</ul>
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -1,13 +1,21 @@
|
|||
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr-FR">
|
||||
{% include "base/head.html" %}
|
||||
<body>
|
||||
{% include "base/navbar.html" %}
|
||||
|
||||
{% block container %}
|
||||
{% endblock container %}
|
||||
<div class=container>
|
||||
<header>
|
||||
<div class=title>{% block title %}<h1>Planète Casio</h1>{% endblock %}</div>
|
||||
{% include "base/header.html" %}
|
||||
</header>
|
||||
|
||||
{% include "base/footer.html" %}
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
|
||||
{% include "base/footer.html" %}
|
||||
</div>
|
||||
|
||||
{% include "base/flash.html" %}
|
||||
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
{% extends "base/base.html" %}
|
||||
|
||||
{% block container %}
|
||||
<div id="container">
|
||||
{% include "base/header.html" %}
|
||||
|
||||
{% block content %}
|
||||
{% endblock content %}
|
||||
</div>
|
||||
{% endblock container %}
|
|
@ -1,5 +1,5 @@
|
|||
<footer>
|
||||
<p>Planète Casio est un site communautaire non affilié à Casio | Toute reproduction de Planète Casio, même partielle, est interdite.</p>
|
||||
<p>Planète Casio est un site communautaire non affilié à Casio. Toute reproduction de Planète Casio, même partielle, est interdite.</p>
|
||||
<p>Les programmes et autres publications présentes sur Planète Casio restent la propriété de leurs auteurs et peuvent être soumis à des licences ou des copyrights.</p>
|
||||
<p>CASIO est une marque déposée par CASIO Computer Co., Ltd.</p>
|
||||
</footer>
|
||||
|
|
|
@ -4,13 +4,14 @@
|
|||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<link rel="stylesheet" media="all and (min-width: 700px)" type="text/css" href={{url_for('static', filename = 'css/global.css')}}>
|
||||
<link rel="stylesheet" media="all and (min-width: 700px)" type="text/css" href={{url_for('static', filename = 'css/navbar.css')}}>
|
||||
<link rel="stylesheet" media="all and (min-width: 700px)" type="text/css" href={{url_for('static', filename = 'css/header.css')}}>
|
||||
<link rel="stylesheet" media="all and (min-width: 700px)" type="text/css" href={{url_for('static', filename = 'css/container.css')}}>
|
||||
<link rel="stylesheet" media="all and (min-width: 700px)" type="text/css" href={{url_for('static', filename = 'css/form.css')}}>
|
||||
<link rel="stylesheet" media="all and (min-width: 700px)" type="text/css" href={{url_for('static', filename = 'css/footer.css')}}>
|
||||
<link rel="stylesheet" media="all and (min-width: 700px)" type="text/css" href={{url_for('static', filename = 'css/flash.css')}}>
|
||||
<link rel="stylesheet" media="all and (min-width: 700px)" type="text/css" href={{url_for('static', filename = 'css/responsive.css')}}>
|
||||
<link rel="stylesheet" type="text/css" href={{url_for('static', filename = 'css/global.css')}}>
|
||||
<link rel="stylesheet" type="text/css" href={{url_for('static', filename = 'css/navbar.css')}}>
|
||||
<link rel="stylesheet" type="text/css" href={{url_for('static', filename = 'css/header.css')}}>
|
||||
<link rel="stylesheet" type="text/css" href={{url_for('static', filename = 'css/container.css')}}>
|
||||
<link rel="stylesheet" type="text/css" href={{url_for('static', filename = 'css/form.css')}}>
|
||||
<link rel="stylesheet" type="text/css" href={{url_for('static', filename = 'css/footer.css')}}>
|
||||
<link rel="stylesheet" type="text/css" href={{url_for('static', filename = 'css/flash.css')}}>
|
||||
<link rel="stylesheet" type="text/css" href={{url_for('static', filename = 'css/responsive.css')}}>
|
||||
<link rel="stylesheet" type="text/css" href={{url_for('static', filename = 'css/table.css')}}>
|
||||
<link rel="stylesheet" media="all and (max-width: 699px)" type="text/css" href={{url_for('static', filename = 'css/light.css')}}>
|
||||
</head>
|
||||
|
|
|
@ -1,15 +1,23 @@
|
|||
<header>
|
||||
<form action={{url_for('search')}} method="get">
|
||||
<input type="search" name="q" id="q" placeholder="{{search_form.label}}" />
|
||||
<a role="button" onclick="this.parentNode.submit();" href="#" class="light-hidden">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path fill="#adb0b4"d="M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z"></path>
|
||||
</svg>
|
||||
</a>
|
||||
</form>
|
||||
<div class=spacer></div>
|
||||
<form action={{url_for('search')}} method="get">
|
||||
<input type="search" name="q" id="q" placeholder="{{search_form.label}}" />
|
||||
<a role=button onclick="this.parentNode.submit();" href=#>
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path fill="#adb0b4"d="M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z"></path>
|
||||
</svg>
|
||||
</a>
|
||||
</form>
|
||||
|
||||
<div id="spotlight">
|
||||
<a href="#">Concours</a>
|
||||
<a href="#">Jeu du mois</a>
|
||||
</div>
|
||||
</header>
|
||||
{% if current_user.is_authenticated %}
|
||||
<div class=links>
|
||||
<a href="{{ url_for('user', username=current_user.name) }}" role=button title='Mon compte'>
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M12,19.2C9.5,19.2 7.29,17.92 6,16C6.03,14 10,12.9 12,12.9C14,12.9 17.97,14 18,16C16.71,17.92 14.5,19.2 12,19.2M12,5A3,3 0 0,1 15,8A3,3 0 0,1 12,11A3,3 0 0,1 9,8A3,3 0 0,1 12,5M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z"></path>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div id="spotlight">
|
||||
<a href="#" class="button bg-red">Jeu du mois : février 2019</a>
|
||||
</div>
|
||||
|
|
|
@ -59,7 +59,7 @@
|
|||
</li>
|
||||
|
||||
<li>
|
||||
<a href="#" label="Outils">
|
||||
<a role="button" label="Outils" tabindex="0">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path fill="#ffffff" d="M22.7,19L13.6,9.9C14.5,7.6 14,4.9 12.1,3C10.1,1 7.1,0.6 4.7,1.7L9,6L6,9L1.6,4.7C0.4,7.1 0.9,10.1 2.9,12.1C4.8,14 7.5,14.5 9.8,13.6L18.9,22.7C19.3,23.1 19.9,23.1 20.3,22.7L22.6,20.4C23.1,20 23.1,19.3 22.7,19Z"></path>
|
||||
</svg>
|
||||
|
@ -68,7 +68,9 @@
|
|||
</li>
|
||||
</ul>
|
||||
|
||||
<div id="menu">
|
||||
<div id=spacer-menu></div>
|
||||
|
||||
<div id=menu>
|
||||
{% include "base/navbar/account.html" %}
|
||||
|
||||
{% include "base/navbar/news.html" %}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
{% if current_user.is_authenticated %}
|
||||
<div>
|
||||
<h2>
|
||||
<img src="{{ url_for('static', filename=current_user.avatar) }}">
|
||||
{{ current_user.name }}
|
||||
<a href="{{ url_for('user', username=current_user.name) }}">
|
||||
<img src="{{ url_for('static', filename=current_user.avatar) }}">
|
||||
<div>{{ current_user.name }}</div>
|
||||
</a>
|
||||
</h2>
|
||||
<a href="#">
|
||||
<svg viewBox="0 0 24 24">
|
||||
|
@ -22,7 +24,7 @@
|
|||
</svg>
|
||||
Topics favoris
|
||||
</a>
|
||||
<a href="{{ url_for('admin') }}">
|
||||
<a href="{{ url_for('adm') }}">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path fill="#ffffff" d="M3,1H19A1,1 0 0,1 20,2V6A1,1 0 0,1 19,7H3A1,1 0 0,1 2,6V2A1,1 0 0,1 3,1M3,9H19A1,1 0 0,1 20,10V10.67L17.5,9.56L11,12.44V15H3A1,1 0 0,1 2,14V10A1,1 0 0,1 3,9M3,17H11C11.06,19.25 12,21.4 13.46,23H3A1,1 0 0,1 2,22V18A1,1 0 0,1 3,17M8,5H9V3H8V5M8,13H9V11H8V13M8,21H9V19H8V21M4,3V5H6V3H4M4,11V13H6V11H4M4,19V21H6V19H4M17.5,12L22,14V17C22,19.78 20.08,22.37 17.5,23C14.92,22.37 13,19.78 13,17V14L17.5,12M17.5,13.94L15,15.06V17.72C15,19.26 16.07,20.7 17.5,21.06V13.94Z"></path>
|
||||
</svg>
|
||||
|
@ -31,7 +33,7 @@
|
|||
|
||||
<hr />
|
||||
|
||||
<a href="{{ url_for('account') }}">
|
||||
<a href="{{ url_for('edit_account') }}">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path fill="#ffffff" d="M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z"></path>
|
||||
</svg>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{% extends "base/container.html" %}
|
||||
{% extends "base/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="form">
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{% extends "base/container.html" %}
|
||||
{% extends "base/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{% extends "base/container.html" %}
|
||||
{% extends "base/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section>
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
{% extends "base/container.html" %}
|
||||
{% extends "base/base.html" %}
|
||||
|
||||
{% block title %}
|
||||
<h1>Planète Casio</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="home-pinned-content">
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{% extends "base/container.html" %}
|
||||
{% extends "base/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="form" style="width:40%;">
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{% extends "base/container.html" %}
|
||||
{% extends "base/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="form" style="width:40%;">
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{% extends "base/container.html" %}
|
||||
{% extends "base/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="form">
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{% extends "base/container.html" %}
|
||||
{% extends "base/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{% extends "base/container.html" %}
|
||||
{% extends "base/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section>
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
from flask import redirect, url_for, flash
|
||||
from flask import current_user
|
||||
import functools
|
||||
|
||||
# Use only with @login_required.
|
||||
def privilege_required(priv):
|
||||
def privilege_decorator(f):
|
||||
@functools.wraps(f)
|
||||
def wrapper():
|
||||
if not current_user.priv(priv):
|
||||
flash(f'Cette page est protégée par le privilège <code>{priv}'+
|
||||
'</code>', 'error')
|
||||
return redirect(url_for('index'))
|
||||
else:
|
||||
f()
|
||||
return wrapper
|
||||
return privilege_decorator
|
|
@ -0,0 +1,11 @@
|
|||
from app import app
|
||||
|
||||
@app.template_filter('pluralize')
|
||||
def pluralize(count, singular="", plural="s"):
|
||||
"""
|
||||
Make a noun plural. Meant for templates; use it like this:
|
||||
Il y a {{ n }} groupe{{ n | pluralize }}.
|
||||
You can specify different suffixes if "s" is not relevant;
|
||||
Il y a {{ n }} hibou{{ n | pluralize("", "x") }}.
|
||||
"""
|
||||
return singular if count <= 1 else plural
|
|
@ -6,19 +6,19 @@ from app import app
|
|||
|
||||
def priv_required(*perms):
|
||||
"""
|
||||
If you decorate a view with this, it will ensure that the current user is
|
||||
authenticated and has required permissions before calling the actual view.
|
||||
(If they are not, it calls the :attr:`LoginManager.unauthorized` callback.)
|
||||
For example::
|
||||
Requires the user to be an authenticated member with privileges [perms].
|
||||
Calls :attr:`LoginManager.unauthorized` if the user is not authenticated,
|
||||
and a 403 if some of the privileges are missing.
|
||||
|
||||
Example:
|
||||
@app.route('/admin')
|
||||
@priv_required('access-admin-board')
|
||||
def admin_board():
|
||||
pass
|
||||
|
||||
It can be convenient to globally turn off authentication when unit testing.
|
||||
To enable this, if the application configuration variable `LOGIN_DISABLED`
|
||||
is set to `True`, this decorator will be ignored.
|
||||
Setting the `LOGIN_DISABLED` configuration variable to `True` will silence
|
||||
this decorator.
|
||||
"""
|
||||
def decorated_view(func):
|
||||
@wraps(func)
|
||||
|
@ -32,6 +32,7 @@ def priv_required(*perms):
|
|||
else:
|
||||
for p in perms:
|
||||
if not current_user.priv(p):
|
||||
# TODO: Add error message and privilege name
|
||||
abort(403)
|
||||
return func(*args, **kwargs)
|
||||
return wrapped
|
||||
|
|
|
@ -3,11 +3,12 @@ from wtforms.validators import ValidationError
|
|||
from app.models.users import User, Member
|
||||
|
||||
def name(form, name):
|
||||
if not User.valid_name(name.data):
|
||||
raise ValidationError("Nom d'utilisateur invalide.")
|
||||
# last check: do not ask db if useless
|
||||
member = Member.query.filter_by(name=name.data).first()
|
||||
if member is not None:
|
||||
raise ValidationError('Pseudo indisponible.')
|
||||
if not User.valid_name(name.data):
|
||||
raise ValidationError("Nom d'utilisateur invalide.")
|
||||
|
||||
def email(form, email):
|
||||
member = Member.query.filter_by(email=email.data).first()
|
||||
|
|
|
@ -20,6 +20,7 @@ Content (topic, progs, tutos, etc) management:
|
|||
move-private-content Change the section of a page in a private section
|
||||
showcase-content Manage stocky content (post-its)
|
||||
edit-static-content Edit static content pages
|
||||
extract-posts Move out-of-topic posts to a new places
|
||||
|
||||
Program evaluation:
|
||||
delete-notes Delete program notes
|
||||
|
@ -35,4 +36,7 @@ Miscellaenous:
|
|||
footer-statistics View performance statistics in the page footer
|
||||
community-login Automatically login as a community account
|
||||
|
||||
Administration panel...
|
||||
Administration panel:
|
||||
access-admin-panel Administration panel of website
|
||||
edit-account Edit details of any account
|
||||
delete-account Remove member accounts
|
||||
|
|
|
@ -6,12 +6,14 @@ class Config(object):
|
|||
'postgresql+psycopg2://' + os.environ.get('USER') + ':@/pcv5'
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
UPLOAD_FOLDER = './app/static/avatars'
|
||||
LOGIN_DISABLED=True
|
||||
|
||||
class V5Config(object):
|
||||
# Length allocated to privilege names (slugs)
|
||||
PRIVS_MAXLEN = 64
|
||||
# Forbidden user names
|
||||
FORBIDDEN_USERNAMES = [ "admin", "root", "webmaster", "contact" ]
|
||||
# Forbidden chars in user names (regex)
|
||||
FORBIDDEN_CHARS_USERNAMES = r"[/]"
|
||||
# Unauthorized message (@priv_required)
|
||||
UNAUTHORIZED_MSG = "Vous n'avez pas l'autorisation d'effectuer cette action !"
|
||||
|
||||
|
|
Loading…
Reference in New Issue