Merge branch 'master-fork'

This commit is contained in:
Eragon 2019-02-17 21:51:00 +01:00
commit b6d758c197
46 changed files with 765 additions and 424 deletions

View File

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

72
app/data/groups.yaml Normal file
View File

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

View File

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

View File

@ -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}">'

View File

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

View File

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

View File

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

View File

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

View File

@ -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;
} */

View File

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

View File

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

View File

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

View File

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

View File

@ -24,8 +24,8 @@
@media all and (min-width: 1400px) {
body {
font-size: 14px;
body, input {
font-size: 13px;
}
header input[type="search"] {

19
app/static/css/table.css Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +0,0 @@
{% extends "base/base.html" %}
{% block container %}
<div id="container">
{% include "base/header.html" %}
{% block content %}
{% endblock content %}
</div>
{% endblock container %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
{% extends "base/container.html" %}
{% extends "base/base.html" %}
{% block content %}
<section class="form">

View File

@ -1,4 +1,4 @@
{% extends "base/container.html" %}
{% extends "base/base.html" %}
{% block content %}
<section>

View File

@ -1,4 +1,4 @@
{% extends "base/container.html" %}
{% extends "base/base.html" %}
{% block content %}
<section>

View File

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

View File

@ -1,4 +1,4 @@
{% extends "base/container.html" %}
{% extends "base/base.html" %}
{% block content %}
<section class="form" style="width:40%;">

View File

@ -1,4 +1,4 @@
{% extends "base/container.html" %}
{% extends "base/base.html" %}
{% block content %}
<section class="form" style="width:40%;">

View File

@ -1,4 +1,4 @@
{% extends "base/container.html" %}
{% extends "base/base.html" %}
{% block content %}
<section class="form">

View File

@ -1,4 +1,4 @@
{% extends "base/container.html" %}
{% extends "base/base.html" %}
{% block content %}
<section>

View File

@ -1,4 +1,4 @@
{% extends "base/container.html" %}
{% extends "base/base.html" %}
{% block content %}
<section>

View File

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

11
app/utils/pluralize.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

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