diff --git a/app/__init__.py b/app/__init__.py index 44d965e..1e1fef4 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -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 diff --git a/app/data/groups.yaml b/app/data/groups.yaml new file mode 100644 index 0000000..caf888f --- /dev/null +++ b/app/data/groups.yaml @@ -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 diff --git a/app/forms/account.py b/app/forms/account.py index 9e6f7ca..f965237 100644 --- a/app/forms/account.py +++ b/app/forms/account.py @@ -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') \ No newline at end of file diff --git a/app/models/privs.py b/app/models/privs.py index 6e0ed2f..63a489d 100644 --- a/app/models/privs.py +++ b/app/models/privs.py @@ -27,12 +27,6 @@ class SpecialPrivilege(db.Model): def __repr__(self): return f'' - # 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'' diff --git a/app/models/users.py b/app/models/users.py index ab4ff29..91d90b9 100644 --- a/app/models/users.py +++ b/app/models/users.py @@ -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.""" diff --git a/app/routes/account.py b/app/routes/account.py index 4e5b0e7..2b5a108 100644 --- a/app/routes/account.py +++ b/app/routes/account.py @@ -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(): diff --git a/app/routes/admin.py b/app/routes/admin.py index a64b586..24a8b1f 100644 --- a/app/routes/admin.py +++ b/app/routes/admin.py @@ -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/', 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//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) diff --git a/app/routes/users.py b/app/routes/users.py index 111389b..b9165b5 100644 --- a/app/routes/users.py +++ b/app/routes/users.py @@ -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)) diff --git a/app/static/css/container.css b/app/static/css/container.css index 4ba38df..ac88958 100644 --- a/app/static/css/container.css +++ b/app/static/css/container.css @@ -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; -} */ diff --git a/app/static/css/global.css b/app/static/css/global.css index 7eac437..7912e14 100644 --- a/app/static/css/global.css +++ b/app/static/css/global.css @@ -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; } diff --git a/app/static/css/header.css b/app/static/css/header.css index 22e3d92..e694515 100644 --- a/app/static/css/header.css +++ b/app/static/css/header.css @@ -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); -} - diff --git a/app/static/css/light.css b/app/static/css/light.css index ba80eb1..3ff5693 100644 --- a/app/static/css/light.css +++ b/app/static/css/light.css @@ -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; -} \ No newline at end of file diff --git a/app/static/css/navbar.css b/app/static/css/navbar.css index c3545b5..e039c6a 100644 --- a/app/static/css/navbar.css +++ b/app/static/css/navbar.css @@ -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; diff --git a/app/static/css/responsive.css b/app/static/css/responsive.css index c96f7a5..016960e 100644 --- a/app/static/css/responsive.css +++ b/app/static/css/responsive.css @@ -24,8 +24,8 @@ @media all and (min-width: 1400px) { - body { - font-size: 14px; + body, input { + font-size: 13px; } header input[type="search"] { diff --git a/app/static/css/table.css b/app/static/css/table.css new file mode 100644 index 0000000..5deaa26 --- /dev/null +++ b/app/static/css/table.css @@ -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; +} diff --git a/app/static/fonts/Cantarell-Bold.otf b/app/static/fonts/Cantarell-Bold.otf new file mode 100644 index 0000000..260419e Binary files /dev/null and b/app/static/fonts/Cantarell-Bold.otf differ diff --git a/app/static/fonts/Cantarell-Regular.otf b/app/static/fonts/Cantarell-Regular.otf new file mode 100644 index 0000000..e668e6e Binary files /dev/null and b/app/static/fonts/Cantarell-Regular.otf differ diff --git a/app/static/scripts/trigger_menu.js b/app/static/scripts/trigger_menu.js index 3311cf0..f6de2c5 100644 --- a/app/static/scripts/trigger_menu.js +++ b/app/static/scripts/trigger_menu.js @@ -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); diff --git a/app/templates/account.html b/app/templates/account.html index 7c4aa88..4e00ae3 100644 --- a/app/templates/account.html +++ b/app/templates/account.html @@ -1,10 +1,10 @@ -{% extends "base/container.html" %} +{% extends "base/base.html" %} {% block content %}

Gestion du compte

-
+ {{ form.hidden_tag() }}

Général

diff --git a/app/templates/admin.html b/app/templates/admin.html deleted file mode 100644 index d0498e6..0000000 --- a/app/templates/admin.html +++ /dev/null @@ -1,43 +0,0 @@ -{% extends "base/container.html" %} - -{% block content %} -
- - {{ form.hidden_tag() }} - {{ form.submit }} - - -

List of members

- - - - - - {% for user in users %} - - - - - - {% endfor %} -
NameEmailRegisterXPInn.Newsletter
{{ user.name }}{{ user.email }}{{ user.register_date }}{{ user.xp }}{{ user.innovation }}{{ "Yes" if user.newsletter else "No" }}
- -

List of groups

- - - - - {% for group in groups %} - - {% endfor %} -
GroupMembersPrivileges
{{ group.name }} - {% for user in group.members %} - {{ user.name }} - {% endfor %} - - {% for priv in group.privs %} - {{ priv }} - {% endfor %} -
-
-{% endblock %} diff --git a/app/templates/admin/delete_account.html b/app/templates/admin/delete_account.html new file mode 100644 index 0000000..1f3ee83 --- /dev/null +++ b/app/templates/admin/delete_account.html @@ -0,0 +1,30 @@ +{% extends "base/base.html" %} + +{% block title %} +Panneau d'administration »

Suppression du compte de '{{ user.name }}'

+{% endblock %} + +{% block content %} +
+

Confirmer la suppression du compte

+

Le compte '{{ user.name }}' que vous allez supprimer est lié à :

+
    +
  • {{ user.groups | length }} groupe{{ user.groups|length|pluralize }}
  • +
  • {% set sp = user.special_privileges() | length %} + {{- sp }} privilège{{sp|pluralize}} spéci{{sp|pluralize("al","aux")}}
  • +
+ +
+ {{ del_form.hidden_tag() }} +
+ {{ del_form.delete.label }} + {{ del_form.delete(checked=False) }} +
{{ del_form.delete.description }}
+ {% for error in del_form.delete.errors %} + {{ error }} + {% endfor %} +
+
{{ del_form.submit(class_="bg-red") }}
+
+
+{% endblock %} diff --git a/app/templates/admin/edit_account.html b/app/templates/admin/edit_account.html new file mode 100644 index 0000000..f743d81 --- /dev/null +++ b/app/templates/admin/edit_account.html @@ -0,0 +1,97 @@ +{% extends "base/base.html" %} + +{% block title %} +Panneau d'administration »

Édition du compte de '{{ user.name }}'

+{% endblock %} + +{% block content %} +
+
+ {{ form.hidden_tag() }} + +

Général

+
+ {{ form.avatar.label }} +
+ + {{ form.avatar }} +
+
+
+ {{ form.username.label }} + {{ form.username(placeholder=user.name) }} + {% for error in form.username.errors %} + {{ error }} + {% endfor %} +
+
+ {{ form.email.label }} + {{ form.email(placeholder=user.email) }} + {% for error in form.email.errors %} + {{ error }} + {% endfor %} +
+
+ {{ form.password.label }} + {{ form.password(placeholder='************') }} + {% for error in form.password.errors %} + {{ error }} + {% endfor %} +
+ +

Participation

+
+ {{ form.xp.label }} + {{ form.xp(placeholder=user.xp) }} + {% for error in form.xp.errors %} + {{ error }} + {% endfor %} +
+
+ {{ form.innovation.label }} + {{ form.innovation(placeholder=user.innovation) }} + {% for error in form.innovation.errors %} + {{ error }} + {% endfor %} +
+ +

À propos

+
+ {{ form.birthday.label }} + {{ form.birthday(value=user.birthday) }} + {% for error in form.birthday.errors %} + {{ error }} + {% endfor %} +
+
+ {{ form.signature.label }} + + {% for error in form.signature.errors %} + {{ error }} + {% endfor %} +
+
+ {{ form.biography.label }} + + {% for error in form.biography.errors %} + {{ error }} + {% endfor %} +
+ +

Préférences

+
+ {{ form.newsletter.label }} + {{ form.newsletter(checked=user.newsletter) }} +
{{ form.newsletter.description }}
+ {% for error in form.newsletter.errors %} + {{ error }} + {% endfor %} +
+
{{ form.submit(class_="bg-green") }}
+
+ +

Supprimer le compte

+ Supprimer le compte + +
+{% endblock %} diff --git a/app/templates/admin/groups_privileges.html b/app/templates/admin/groups_privileges.html new file mode 100644 index 0000000..b529a6c --- /dev/null +++ b/app/templates/admin/groups_privileges.html @@ -0,0 +1,74 @@ +{% extends "base/base.html" %} + +{% block title %} +Panneau d'administration »

Groupes et privilèges

+{% endblock %} + +{% block content %} +
+

Cette page présente une vue d'ensemble des groupes et privilèges + associés. Elle supervise également les détenteurs de privilèges.

+ +

Membres détenteurs de privilèges

+ + + + + + {% for user in users %} + + + + + + + {% endfor %} +
PseudoEmailGroupesPrivilèges spéciauxModifier
{{ user.name }}{{ user.email }}{% for g in user.groups %} + {{ g.name }} + {{ ', ' if not loop.last }} + {% endfor %}{% for priv in user.special_privileges() %} + {{ priv }} + {{- ', ' if not loop.last }} + {% endfor %}Modifier
+ +

Liste des groupes

+ + + + + {% for group in groups %} + + {% endfor %} +
GroupeMembresPrivilèges
{{ group.name }} + {% for user in group.members %} + {{ user.name }} + {% endfor %} + + {% for priv in group.privs() %} + {{ priv }} + {{- ', ' if not loop.last }} + {% endfor %} +
+ +

Restauration des groupes et privilèges

+ +

Cette fonction régénère un ensemble minimal de groupes et membres + permettant de lancer le forum. Elle opère les modifications + suivantes :

+ +
    +
  • Suppression de tous les groupes.
  • +
  • Création des groupes Administrateur, Modérateur, Développeur, + Rédacteur, Responsable communauté, Partenaire, Compte communautaire, + Robot, Membre de CreativeCalc.
  • +
  • Attribution des privilèges associés à ces groupes.
  • +
  • Recréation des comptes communs : PlanèteCasio (compte communautaire), + GLaDOS (robot).
  • +
+ +
+ {{ form.hidden_tag() }} + {{ form.submit(class="bg-orange") }} +
+
+{% endblock %} diff --git a/app/templates/admin/index.html b/app/templates/admin/index.html new file mode 100644 index 0000000..7ea09d0 --- /dev/null +++ b/app/templates/admin/index.html @@ -0,0 +1,14 @@ +{% extends "base/base.html" %} + +{% block title %} +

Panneau d'administration

+{% endblock %} + +{% block content %} +
+

Pages générales du panneau d'administration :

+ +
+{% endblock %} diff --git a/app/templates/base/base.html b/app/templates/base/base.html index aedf74f..1ff4744 100644 --- a/app/templates/base/base.html +++ b/app/templates/base/base.html @@ -1,13 +1,21 @@ + {% include "base/head.html" %} {% include "base/navbar.html" %} - {% block container %} - {% endblock container %} +
+
+
{% block title %}

Planète Casio

{% endblock %}
+ {% include "base/header.html" %} +
- {% include "base/footer.html" %} + {% block content %} + {% endblock %} + + {% include "base/footer.html" %} +
{% include "base/flash.html" %} diff --git a/app/templates/base/container.html b/app/templates/base/container.html deleted file mode 100644 index cf4302e..0000000 --- a/app/templates/base/container.html +++ /dev/null @@ -1,10 +0,0 @@ -{% extends "base/base.html" %} - -{% block container %} -
- {% include "base/header.html" %} - - {% block content %} - {% endblock content %} -
-{% endblock container %} diff --git a/app/templates/base/footer.html b/app/templates/base/footer.html index 4880fb9..c7cb38d 100644 --- a/app/templates/base/footer.html +++ b/app/templates/base/footer.html @@ -1,5 +1,5 @@
-

Planète Casio est un site communautaire non affilié à Casio | Toute reproduction de Planète Casio, même partielle, est interdite.

+

Planète Casio est un site communautaire non affilié à Casio. Toute reproduction de Planète Casio, même partielle, est interdite.

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.

CASIO est une marque déposée par CASIO Computer Co., Ltd.

diff --git a/app/templates/base/head.html b/app/templates/base/head.html index a3fe2eb..055e0c0 100644 --- a/app/templates/base/head.html +++ b/app/templates/base/head.html @@ -4,13 +4,14 @@ - - - - - - - - + + + + + + + + + diff --git a/app/templates/base/header.html b/app/templates/base/header.html index b0e9837..5c59cfc 100644 --- a/app/templates/base/header.html +++ b/app/templates/base/header.html @@ -1,15 +1,23 @@ -
-
- - - - - - -
+
+
+ + + + + + +
- -
+{% if current_user.is_authenticated %} + +{% endif %} + + diff --git a/app/templates/base/navbar.html b/app/templates/base/navbar.html index c526f14..278b5d4 100644 --- a/app/templates/base/navbar.html +++ b/app/templates/base/navbar.html @@ -59,7 +59,7 @@
  • - + @@ -68,7 +68,9 @@
  • -