Quellcode durchsuchen

Merge branch 'master-fork'

pull/5/head
Samuel vor 8 Monaten
Ursprung
Commit
b6d758c197
46 geänderte Dateien mit 765 neuen und 424 gelöschten Zeilen
  1. 1
    0
      app/__init__.py
  2. 72
    0
      app/data/groups.yaml
  3. 19
    1
      app/forms/account.py
  4. 19
    7
      app/models/privs.py
  5. 39
    7
      app/models/users.py
  6. 1
    1
      app/routes/account.py
  7. 97
    24
      app/routes/admin.py
  8. 1
    1
      app/routes/users.py
  9. 11
    24
      app/static/css/container.css
  10. 58
    38
      app/static/css/global.css
  11. 42
    13
      app/static/css/header.css
  12. 44
    160
      app/static/css/light.css
  13. 27
    22
      app/static/css/navbar.css
  14. 2
    2
      app/static/css/responsive.css
  15. 19
    0
      app/static/css/table.css
  16. BIN
      app/static/fonts/Cantarell-Bold.otf
  17. BIN
      app/static/fonts/Cantarell-Regular.otf
  18. 1
    1
      app/static/scripts/trigger_menu.js
  19. 2
    2
      app/templates/account.html
  20. 0
    43
      app/templates/admin.html
  21. 30
    0
      app/templates/admin/delete_account.html
  22. 97
    0
      app/templates/admin/edit_account.html
  23. 74
    0
      app/templates/admin/groups_privileges.html
  24. 14
    0
      app/templates/admin/index.html
  25. 11
    3
      app/templates/base/base.html
  26. 0
    10
      app/templates/base/container.html
  27. 1
    1
      app/templates/base/footer.html
  28. 9
    8
      app/templates/base/head.html
  29. 22
    14
      app/templates/base/header.html
  30. 4
    2
      app/templates/base/navbar.html
  31. 6
    4
      app/templates/base/navbar/account.html
  32. 1
    1
      app/templates/delete_account.html
  33. 1
    1
      app/templates/errors/403.html
  34. 1
    1
      app/templates/errors/404.html
  35. 5
    1
      app/templates/index.html
  36. 1
    1
      app/templates/login.html
  37. 1
    1
      app/templates/register.html
  38. 1
    1
      app/templates/search.html
  39. 1
    1
      app/templates/user.html
  40. 1
    1
      app/templates/validation.html
  41. 0
    17
      app/utils/decorators.py
  42. 11
    0
      app/utils/pluralize.py
  43. 7
    6
      app/utils/priv_required.py
  44. 3
    2
      app/utils/validators.py
  45. 5
    1
      assets/privs.txt
  46. 3
    1
      config.py

+ 1
- 0
app/__init__.py Datei anzeigen

@@ -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
- 0
app/data/groups.yaml Datei anzeigen

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

+ 19
- 1
app/forms/account.py Datei anzeigen

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

+ 19
- 7
app/models/privs.py Datei anzeigen

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


+ 39
- 7
app/models/users.py Datei anzeigen

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

+ 1
- 1
app/routes/account.py Datei anzeigen

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

+ 97
- 24
app/routes/admin.py Datei anzeigen

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

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

+ 1
- 1
app/routes/users.py Datei anzeigen

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

+ 11
- 24
app/static/css/container.css Datei anzeigen

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

+ 58
- 38
app/static/css/global.css Datei anzeigen

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

+ 42
- 13
app/static/css/header.css Datei anzeigen

@@ -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 .title a {
color: inherit;
}
header .title h1 {
font-family: Cantarell; font-weight: bold; font-size: 18px;
color: #181818;
display: inline;
}

header h1 {
font-family: Raleway; font-weight: 200;
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 a {
padding: 8px 18px 6px 18px;
color: #727272; font-size: 15px;
border-bottom: 2px solid rgba(93, 123, 141, 0);
transition: border .15s ease;
#spotlight {
margin-left: 16px;
}
#spotlight a:hover, header #spotlight a:focus {
border-bottom: 2px solid rgba(93, 123, 141, 1);
#spotlight a {
display: block;
}


+ 44
- 160
app/static/css/light.css Datei anzeigen

@@ -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);
}
/*#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;
}*/
width: auto; height: 100%; margin-bottom: 0;
}
#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;
}
header svg {
width: 24px; height: 24px; vertical-align: middle;
transition: .15s ease;
}
header a:hover > svg, header a:focus > svg {
filter: brightness(.5);
padding: 0 8px;
}

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

#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;
#shoutbox {
display: none;
}


/* Homepage */

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

+ 27
- 22
app/static/css/navbar.css Datei anzeigen

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

+ 2
- 2
app/static/css/responsive.css Datei anzeigen

@@ -24,8 +24,8 @@


@media all and (min-width: 1400px) {
body {
font-size: 14px;
body, input {
font-size: 13px;
}

header input[type="search"] {

+ 19
- 0
app/static/css/table.css Datei anzeigen

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

BIN
app/static/fonts/Cantarell-Bold.otf Datei anzeigen


BIN
app/static/fonts/Cantarell-Regular.otf Datei anzeigen


+ 1
- 1
app/static/scripts/trigger_menu.js Datei anzeigen

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

+ 2
- 2
app/templates/account.html Datei anzeigen

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

+ 0
- 43
app/templates/admin.html Datei anzeigen

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

+ 30
- 0
app/templates/admin/delete_account.html Datei anzeigen

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

+ 97
- 0
app/templates/admin/edit_account.html Datei anzeigen

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

+ 74
- 0
app/templates/admin/groups_privileges.html Datei anzeigen

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

+ 14
- 0
app/templates/admin/index.html Datei anzeigen

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

+ 11
- 3
app/templates/base/base.html Datei anzeigen

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


+ 0
- 10
app/templates/base/container.html Datei anzeigen

@@ -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
- 1
app/templates/base/footer.html Datei anzeigen

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

+ 9
- 8
app/templates/base/head.html Datei anzeigen

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

+ 22
- 14
app/templates/base/header.html Datei anzeigen

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

+ 4
- 2
app/templates/base/navbar.html Datei anzeigen

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

+ 6
- 4
app/templates/base/navbar/account.html Datei anzeigen

@@ -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
- 1
app/templates/delete_account.html Datei anzeigen

@@ -1,4 +1,4 @@
{% extends "base/container.html" %}
{% extends "base/base.html" %}

{% block content %}
<section class="form">

+ 1
- 1
app/templates/errors/403.html Datei anzeigen

@@ -1,4 +1,4 @@
{% extends "base/container.html" %}
{% extends "base/base.html" %}

{% block content %}
<section>

+ 1
- 1
app/templates/errors/404.html Datei anzeigen

@@ -1,4 +1,4 @@
{% extends "base/container.html" %}
{% extends "base/base.html" %}

{% block content %}
<section>

+ 5
- 1
app/templates/index.html Datei anzeigen

@@ -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
- 1
app/templates/login.html Datei anzeigen

@@ -1,4 +1,4 @@
{% extends "base/container.html" %}
{% extends "base/base.html" %}

{% block content %}
<section class="form" style="width:40%;">

+ 1
- 1
app/templates/register.html Datei anzeigen

@@ -1,4 +1,4 @@
{% extends "base/container.html" %}
{% extends "base/base.html" %}

{% block content %}
<section class="form" style="width:40%;">

+ 1
- 1
app/templates/search.html Datei anzeigen

@@ -1,4 +1,4 @@
{% extends "base/container.html" %}
{% extends "base/base.html" %}

{% block content %}
<section class="form">

+ 1
- 1
app/templates/user.html Datei anzeigen

@@ -1,4 +1,4 @@
{% extends "base/container.html" %}
{% extends "base/base.html" %}

{% block content %}
<section>

+ 1
- 1
app/templates/validation.html Datei anzeigen

@@ -1,4 +1,4 @@
{% extends "base/container.html" %}
{% extends "base/base.html" %}

{% block content %}
<section>

+ 0
- 17
app/utils/decorators.py Datei anzeigen

@@ -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
- 0
app/utils/pluralize.py Datei anzeigen

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

+ 7
- 6
app/utils/priv_required.py Datei anzeigen

@@ -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
- 2
app/utils/validators.py Datei anzeigen

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

+ 5
- 1
assets/privs.txt Datei anzeigen

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

+ 3
- 1
config.py Datei anzeigen

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


Laden…
Abbrechen
Speichern