forked from devs/PCv5
Merge branch 'master-fork'
This commit is contained in:
commit
870ec32887
|
@ -1,22 +1,21 @@
|
|||
app.db
|
||||
|
||||
# Python files and caches
|
||||
__pycache__/
|
||||
app/__pycache__/
|
||||
app/static/avatars/
|
||||
|
||||
migrations/
|
||||
## Devlopement files
|
||||
|
||||
# Virtual environment
|
||||
# virtualenv
|
||||
venv/
|
||||
.venv/
|
||||
|
||||
# pipenv
|
||||
Pipfile
|
||||
Pipfile.lock
|
||||
|
||||
# Sublime Text files
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
|
||||
# Deployment files
|
||||
## Deployment files
|
||||
|
||||
# uWSGI configuration file
|
||||
uwsgi.ini
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Fork de Planète Casio v5
|
||||
# Planète Casio v5
|
||||
|
||||
## Style de code
|
||||
|
||||
* Merci d'essayer de respecter la PEP 8
|
||||
* L'indentation se fait avec des tabulations
|
||||
* Merci d'essayer de respecter les 80 colonnes max
|
||||
|
|
5
V5.py
5
V5.py
|
@ -1,7 +1,8 @@
|
|||
from app import app, db
|
||||
from app.models import User, Post
|
||||
from app.models.users import User
|
||||
# from app.models.models import Post
|
||||
|
||||
|
||||
@app.shell_context_processor
|
||||
def make_shell_context():
|
||||
return {'db': db, 'User': User, 'Post': Post}
|
||||
return {'db': db, 'User': User}
|
||||
|
|
|
@ -8,7 +8,10 @@ app = Flask(__name__)
|
|||
app.config.from_object(Config)
|
||||
db = SQLAlchemy(app)
|
||||
migrate = Migrate(app, db)
|
||||
|
||||
login = LoginManager(app)
|
||||
login.login_view = 'login'
|
||||
login.login_message = "Veuillez vous authentifier avant de continuer."
|
||||
|
||||
from app import routes, models
|
||||
from app import models
|
||||
from app.routes import index, login, search, account, admin, users
|
||||
|
|
29
app/forms.py
29
app/forms.py
|
@ -1,29 +0,0 @@
|
|||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, PasswordField, BooleanField, SubmitField
|
||||
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo
|
||||
from app.models import User
|
||||
|
||||
|
||||
class LoginForm(FlaskForm):
|
||||
username = StringField('Pseudonyme :', validators=[DataRequired()])
|
||||
password = PasswordField('Mot de passe :', validators=[DataRequired()])
|
||||
remember_me = BooleanField('Se souvenir de moi :')
|
||||
submit = SubmitField('Connexion')
|
||||
|
||||
|
||||
class RegistrationForm(FlaskForm):
|
||||
username = StringField('Pseudonyme :', validators=[DataRequired()])
|
||||
email = StringField('Adresse Email :', validators=[DataRequired(), Email()])
|
||||
password = PasswordField('Mot de passe :', validators=[DataRequired()])
|
||||
password2 = PasswordField('Répéter le mot de passe', validators=[DataRequired(), EqualTo('password')])
|
||||
submit = SubmitField('S\'enregistrer')
|
||||
|
||||
def validate_username(self, username):
|
||||
user = User.query.filter_by(username=username.data).first()
|
||||
if user is not None:
|
||||
raise ValidationError('Pseudo indisponible.')
|
||||
|
||||
def validate_email(self, email):
|
||||
user = User.query.filter_by(email=email.data).first()
|
||||
if user is not None:
|
||||
raise ValidationError('Adresse email déjà utilisé.')
|
|
@ -0,0 +1,32 @@
|
|||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, PasswordField, BooleanField, TextAreaField, SubmitField
|
||||
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
|
||||
import app.utils.validators as vd
|
||||
|
||||
class RegistrationForm(FlaskForm):
|
||||
username = StringField('Pseudonyme', validators=[DataRequired(), vd.name])
|
||||
email = StringField('Adresse Email', validators=[DataRequired(), Email(), vd.email])
|
||||
password = PasswordField('Mot de passe', validators=[DataRequired(), vd.password])
|
||||
password2 = PasswordField('Répéter le mot de passe', validators=[DataRequired(), EqualTo('password')])
|
||||
guidelines = BooleanField('J’accepte les <a href="#">CGU</a>', validators=[DataRequired()])
|
||||
newsletter = BooleanField('Inscription à la newsletter', description='Un mail par trimestre environ, pour être prévenu des concours, évènements et nouveautés.')
|
||||
submit = SubmitField('S\'enregistrer')
|
||||
|
||||
class UpdateAccountForm(FlaskForm):
|
||||
avatar = FileField('Avatar', validators=[Optional(), vd.avatar])
|
||||
email = StringField('Adresse Email', validators=[Optional(), Email(), vd.email, vd.old_password])
|
||||
password = PasswordField('Mot de passe :', validators=[Optional(), vd.password, vd.old_password])
|
||||
password2 = PasswordField('Répéter le mot de passe', validators=[Optional(), EqualTo('password')])
|
||||
old_password = PasswordField('Mot de passe actuel', 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 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')
|
|
@ -0,0 +1,9 @@
|
|||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, PasswordField, BooleanField, SubmitField
|
||||
from wtforms.validators import DataRequired
|
||||
|
||||
class LoginForm(FlaskForm):
|
||||
username = StringField('Pseudonyme :', validators=[DataRequired()])
|
||||
password = PasswordField('Mot de passe :', validators=[DataRequired()])
|
||||
remember_me = BooleanField('Se souvenir de moi :')
|
||||
submit = SubmitField('Connexion')
|
|
@ -0,0 +1,14 @@
|
|||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, SubmitField
|
||||
from wtforms.fields.html5 import DateField
|
||||
from wtforms.validators import DataRequired, Optional
|
||||
|
||||
# TODO: compléter le formulaire de recherche avancée
|
||||
class AdvancedSearchForm(FlaskForm):
|
||||
q = StringField('Rechercher :', validators=[DataRequired()])
|
||||
date = DateField('Date', validators=[Optional()])
|
||||
submit = SubmitField('Affiner la recherche')
|
||||
|
||||
class SearchForm(FlaskForm):
|
||||
q = StringField('Rechercher', validators=[DataRequired()])
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
from datetime import datetime
|
||||
from app import db, login
|
||||
from flask_login import UserMixin
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
|
||||
|
||||
class User(UserMixin, db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(64), index=True, unique=True)
|
||||
email = db.Column(db.String(120), index=True, unique=True)
|
||||
password_hash = db.Column(db.String(128))
|
||||
posts = db.relationship('Post', backref='author', lazy='dynamic')
|
||||
|
||||
def __repr__(self):
|
||||
return '<User {}>'.format(self.username)
|
||||
|
||||
def set_password(self, password):
|
||||
self.password_hash = generate_password_hash(password)
|
||||
|
||||
def check_password(self, password):
|
||||
return check_password_hash(self.password_hash, password)
|
||||
|
||||
|
||||
@login.user_loader
|
||||
def load_user(id):
|
||||
return User.query.get(int(id))
|
||||
|
||||
|
||||
class Post(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
body = db.Column(db.String(140))
|
||||
timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
||||
|
||||
def __repr__(self):
|
||||
return '<Post {}>'.format(self.body)
|
|
@ -0,0 +1,19 @@
|
|||
from datetime import datetime
|
||||
from app import db
|
||||
from app.models.users import *
|
||||
|
||||
class Content(db.Model):
|
||||
__tablename__ = 'content'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
type = db.Column(db.String(20))
|
||||
__mapper_args__ = {
|
||||
'polymorphic_identity':__tablename__,
|
||||
'polymorphic_on': type
|
||||
}
|
||||
# Standalone properties
|
||||
data = db.Column(db.Text(convert_unicode=True))
|
||||
date_created = db.Column(db.DateTime, default=datetime.now)
|
||||
date_modified = db.Column(db.DateTime, default=datetime.now)
|
||||
# Relationships
|
||||
author_id = db.Column(db.ForeignKey('user.id'))
|
||||
author = db.relationship("User", back_populates="contents")
|
|
@ -0,0 +1,76 @@
|
|||
# Planète Casio v5
|
||||
# models.privs: Database models for groups and privilege management
|
||||
|
||||
from app import db
|
||||
from config import V5Config
|
||||
|
||||
# Privileges are represented by strings (slugs), for instance "post-news" or
|
||||
# "delete-own-posts". Belonging to a group automatically grants a user the
|
||||
# privileges of that group; additionally, administrators (or any people with
|
||||
# the "grant-special-privileges" privilege) can grant privileges on a per-user
|
||||
# basis.
|
||||
|
||||
# SpecialPrivilege: Privilege manually granted to a user
|
||||
class SpecialPrivilege(db.Model):
|
||||
__tablename__ = 'special_privilege'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
# Member that is granted the privilege
|
||||
mid = db.Column(db.Integer, db.ForeignKey('member.id'), index=True)
|
||||
# Privilege name
|
||||
priv = db.Column(db.String(V5Config.PRIVS_MAXLEN))
|
||||
|
||||
def __init__(self, member, priv):
|
||||
self.mid = member.id
|
||||
self.priv = priv
|
||||
|
||||
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'
|
||||
|
||||
# Unique group ID
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
# Full name, such as "Administrateur" or "Membre d'honneur".
|
||||
name = db.Column(db.Unicode(50), unique=True)
|
||||
# The CSS code should not assume any specific layout and typically applies
|
||||
# to a text node. Use attributes like color, font-style, font-weight, etc.
|
||||
css = db.Column(db.UnicodeText)
|
||||
# Textual description
|
||||
description = db.Column(db.UnicodeText)
|
||||
# List of members (lambda delays evaluation)
|
||||
members = db.relationship('Member', secondary=lambda:GroupMember,
|
||||
back_populates='groups')
|
||||
|
||||
def __init__(self, name, css):
|
||||
self.name = name
|
||||
self.css = css
|
||||
self.members = []
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Group "{self.name}">'
|
||||
|
||||
# Many-to-many relation for users belonging to groups
|
||||
GroupMember = db.Table('group_member', db.Model.metadata,
|
||||
db.Column('gid', db.Integer, db.ForeignKey('group.id')),
|
||||
db.Column('uid', db.Integer, db.ForeignKey('member.id')))
|
||||
|
||||
# Meny-to-many relationship for privileges granted to groups
|
||||
class GroupPrivilege(db.Model):
|
||||
__tablename__ = 'group_privilege'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
gid = db.Column(db.Integer, db.ForeignKey('group.id'))
|
||||
priv = db.Column(db.String(V5Config.PRIVS_MAXLEN))
|
||||
|
||||
def __init__(self, group, priv):
|
||||
self.gid = group.id
|
||||
self.priv = priv
|
|
@ -0,0 +1,221 @@
|
|||
from datetime import date, datetime
|
||||
from app import app, db
|
||||
from flask_login import UserMixin
|
||||
from app.models.contents import Content
|
||||
from app.models.privs import SpecialPrivilege, Group, GroupMember, \
|
||||
GroupPrivilege
|
||||
from config import V5Config
|
||||
|
||||
import werkzeug.security
|
||||
import app
|
||||
import re
|
||||
|
||||
# User: Website user that performs actions on the content
|
||||
class User(UserMixin, db.Model):
|
||||
__tablename__ = 'user'
|
||||
|
||||
# User ID, should be used to refer to any user. Thea actual user can either
|
||||
# be a guest (with IP as key) or a member (with this ID as key).
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
# User type (polymorphic discriminator)
|
||||
type = db.Column(db.String(30))
|
||||
|
||||
# TODO: add good relation
|
||||
contents = db.relationship('Content', back_populates="author")
|
||||
|
||||
__mapper_args__ = {
|
||||
'polymorphic_identity': __tablename__,
|
||||
'polymorphic_on': type
|
||||
}
|
||||
|
||||
def __repr__(self):
|
||||
return f'<User #{self.id}>'
|
||||
|
||||
@staticmethod
|
||||
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
|
||||
|
||||
Possibily other intresting criteria:
|
||||
4. Unicode restriction
|
||||
"""
|
||||
|
||||
if type(name) != str or len(name) < 3 or len(name) > 32:
|
||||
return False
|
||||
if name in V5Config.FORBIDDEN_USERNAMES:
|
||||
return False
|
||||
# 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
|
||||
# There must be at least one letter (avoid complete garbage)
|
||||
if re.search(r'\w', name) is None:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
# Guest: Unregistered user with minimal privileges
|
||||
class Guest(User, db.Model):
|
||||
__tablename__ = 'guest'
|
||||
__mapper_args__ = { 'polymorphic_identity': __tablename__ }
|
||||
|
||||
# ID of the [User] entry
|
||||
id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True)
|
||||
# Reusable username, can be the name of a member (will be distinguished at
|
||||
# rendering time)
|
||||
username = db.Column(db.Unicode(64), index=True)
|
||||
# IP address, 47 characters is the max for an IPv6 address
|
||||
ip = db.Column(db.String(47))
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Guest: {self.username} ({self.ip})>'
|
||||
|
||||
# Member: Registered user with full access to the website's services
|
||||
class Member(User, db.Model):
|
||||
__tablename__ = 'member'
|
||||
__mapper_args__ = { 'polymorphic_identity': __tablename__ }
|
||||
|
||||
# Id of the [User] entry
|
||||
id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True)
|
||||
|
||||
# Primary attributes (needed for the system to work)
|
||||
name = db.Column(db.Unicode(32), index=True, unique=True)
|
||||
email = db.Column(db.Unicode(120), index=True, unique=True)
|
||||
password_hash = db.Column(db.String(255))
|
||||
xp = db.Column(db.Integer)
|
||||
innovation = db.Column(db.Integer)
|
||||
register_date = db.Column(db.Date, default=date.today)
|
||||
|
||||
# Avatars # TODO: rendre ça un peu plus propre
|
||||
@property
|
||||
def avatar(self):
|
||||
return 'avatars/' + str(self.id) + '.png'
|
||||
|
||||
# Groups and related privileges
|
||||
groups = db.relationship('Group', secondary=GroupMember,
|
||||
back_populates='members')
|
||||
|
||||
# Personal information, all optional
|
||||
bio = db.Column(db.UnicodeText)
|
||||
signature = db.Column(db.UnicodeText)
|
||||
birthday = db.Column(db.Date)
|
||||
|
||||
# Settings
|
||||
newsletter = db.Column(db.Boolean, default=False)
|
||||
|
||||
# Relations
|
||||
# trophies = db.relationship('Trophy', back_populates='member')
|
||||
# tests = db.relationship('Test', back_populates='author')
|
||||
|
||||
def __init__(self, name, email, password):
|
||||
"""Register a new user."""
|
||||
if not User.valid_name(name):
|
||||
raise Exception(f'{name} is not a valid user name')
|
||||
|
||||
self.name = name
|
||||
self.email = email
|
||||
self.set_password(password)
|
||||
self.xp = 0
|
||||
self.innovation = 0
|
||||
|
||||
self.bio = ""
|
||||
self.signature = ""
|
||||
self.birthday = None
|
||||
|
||||
def priv(self, priv):
|
||||
"""Check whether the member has the specified privilege."""
|
||||
if SpecialPrivilege.filter(uid=self.id, priv=priv):
|
||||
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 update(self, **data):
|
||||
"""
|
||||
Update all or part of the user's metadata. The [data] dictionary
|
||||
accepts the following keys:
|
||||
"name" str User name
|
||||
"email" str User mail ddress
|
||||
"password" str Raw password
|
||||
"bio" str Biograpy
|
||||
"signature" str Post signature
|
||||
"birthday" date Birthday date
|
||||
"newsletter" bool Newsletter setting
|
||||
For future compatibility, other attributes are silently ignored. None
|
||||
values can be specified and are ignored.
|
||||
|
||||
It is the caller's responsibility to check that the request sender has
|
||||
the right to change user names, password... otherwise this method will
|
||||
turn out dangerous!
|
||||
"""
|
||||
|
||||
data = { key: data[key] for key in data if data[key] is not None }
|
||||
|
||||
if "name" in data:
|
||||
if not User.valid_name(data["name"]):
|
||||
raise Exception(f'{data["name"]} is not a valid user name')
|
||||
self.name = data["name"]
|
||||
|
||||
# TODO: verify good type of those args, think about the password mgt
|
||||
if "email" in data:
|
||||
self.email = data["email"]
|
||||
if "password" in data:
|
||||
self.set_password(data["password"])
|
||||
if "bio" in data:
|
||||
self.bio = data["bio"]
|
||||
if "signature" in data:
|
||||
self.signature = data["signature"]
|
||||
if "birthday" in data:
|
||||
self.birthday = data["birthday"]
|
||||
if "newsletter" in data:
|
||||
self.newsletter = data["newsletter"]
|
||||
|
||||
def get_public_data(self):
|
||||
"""Returns the public information of the member."""
|
||||
return {
|
||||
"name": self.name,
|
||||
"xp": self.xp,
|
||||
"innovation": self.innovation,
|
||||
"register_date": self.register_date,
|
||||
"bio": self.bio,
|
||||
"signature": self.signature,
|
||||
"birthday": self.birthday,
|
||||
}
|
||||
|
||||
def add_xp(self, amount):
|
||||
"""
|
||||
Reward xp to a member. If [amount] is negative, the xp total of the
|
||||
member will decrease, down to zero.
|
||||
"""
|
||||
self.xp_points = max(self.xp_points + amount, 0)
|
||||
|
||||
def add_innovation(self, n):
|
||||
"""
|
||||
Reward innovation points to a member. If [amount] is negative, the
|
||||
innovation points total will decrease, down to zero.
|
||||
"""
|
||||
self.innovation = max(self.innovation + amount, 0)
|
||||
|
||||
def set_password(self, password):
|
||||
"""
|
||||
Set the user's password. Check whether the request sender has the right
|
||||
to do this!
|
||||
"""
|
||||
self.password_hash = werkzeug.security.generate_password_hash(password,
|
||||
method='pbkdf2:sha512', salt_length=10)
|
||||
|
||||
def check_password(self, password):
|
||||
"""Compares password against member hash."""
|
||||
return werkzeug.security.check_password_hash(self.password_hash,
|
||||
password)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Member: {self.name}>'
|
||||
|
||||
@app.login.user_loader
|
||||
def load_user(id):
|
||||
return User.query.get(int(id))
|
103
app/routes.py
103
app/routes.py
|
@ -1,103 +0,0 @@
|
|||
from flask import render_template, flash, redirect, url_for, request
|
||||
from flask_login import login_user, logout_user, current_user, login_required
|
||||
from werkzeug.urls import url_parse
|
||||
from app import app, db
|
||||
from app.forms import LoginForm, RegistrationForm
|
||||
from app.models import User
|
||||
|
||||
|
||||
@app.route('/', methods=['GET', 'POST'])
|
||||
def index():
|
||||
|
||||
form = LoginForm()
|
||||
if form.validate_on_submit():
|
||||
user = User.query.filter_by(username=form.username.data).first()
|
||||
if user is None or not user.check_password(form.password.data):
|
||||
flash('pseudo ou mot de passe invalide')
|
||||
return redirect(url_for('index'))
|
||||
login_user(user, remember=form.remember_me.data)
|
||||
|
||||
return render_template('index.html.j2', form=form)
|
||||
|
||||
|
||||
@app.route('/logout/')
|
||||
def logout():
|
||||
logout_user()
|
||||
return redirect(url_for('index'))
|
||||
|
||||
|
||||
@app.route('/register', methods=['GET', 'POST'])
|
||||
def register():
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('index'))
|
||||
form = LoginForm()
|
||||
form2 = RegistrationForm()
|
||||
if form.validate_on_submit():
|
||||
user = User.query.filter_by(username=form.username.data).first()
|
||||
if user is None or not user.check_password(form.password.data):
|
||||
flash('pseudo ou mot de passe invalide')
|
||||
return redirect(url_for('index'))
|
||||
login_user(user, remember=form.remember_me.data)
|
||||
if form2.validate_on_submit():
|
||||
user = User(username=form2.username.data, email=form2.email.data)
|
||||
user.set_password(form2.password.data)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
flash('Congratulations, you are now a registered user!')
|
||||
return redirect(url_for('validation'))
|
||||
return render_template('register.html.j2', title='Register', form=form,
|
||||
form2=form2)
|
||||
|
||||
|
||||
@app.route('/register/validation/')
|
||||
def validation():
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('index'))
|
||||
form = LoginForm()
|
||||
if form.validate_on_submit():
|
||||
user = User.query.filter_by(username=form.username.data).first()
|
||||
if user is None or not user.check_password(form.password.data):
|
||||
flash('pseudo ou mot de passe invalide')
|
||||
return redirect(url_for('index'))
|
||||
login_user(user, remember=form.remember_me.data)
|
||||
return render_template('validation.html.j2', form=form)
|
||||
|
||||
|
||||
@app.errorhandler(400)
|
||||
@app.errorhandler(401)
|
||||
@app.errorhandler(403)
|
||||
@app.errorhandler(404)
|
||||
@app.errorhandler(418)
|
||||
@app.errorhandler(500)
|
||||
@app.errorhandler(501)
|
||||
@app.errorhandler(503)
|
||||
def ma_page_erreur(error):
|
||||
form = LoginForm()
|
||||
if form.validate_on_submit():
|
||||
user = User.query.filter_by(username=form.username.data).first()
|
||||
if user is None or not user.check_password(form.password.data):
|
||||
flash('pseudo ou mot de passe invalide')
|
||||
return redirect(url_for('index'))
|
||||
login_user(user, remember=form.remember_me.data)
|
||||
return render_template("base/errors.html.j2", error_code=error.code,
|
||||
form=form), error.code
|
||||
|
||||
|
||||
@app.route('/error/<int:error>')
|
||||
def errors(error):
|
||||
form = LoginForm()
|
||||
if form.validate_on_submit():
|
||||
user = User.query.filter_by(username=form.username.data).first()
|
||||
if user is None or not user.check_password(form.password.data):
|
||||
flash('pseudo ou mot de passe invalide')
|
||||
return redirect(url_for('index'))
|
||||
login_user(user, remember=form.remember_me.data)
|
||||
if(error == 400 or error == 401 or error == 403 or error == 404 or
|
||||
error == 418 or error == 500 or error == 501 or error == 503):
|
||||
error_code = error
|
||||
else:
|
||||
error_code = 404
|
||||
error = 404
|
||||
|
||||
return render_template("base/errors.html.j2", error_code=error_code,
|
||||
form=form), error
|
|
@ -0,0 +1,67 @@
|
|||
from flask import redirect, url_for, request, flash
|
||||
from flask_login import login_required, current_user, logout_user
|
||||
from app import app, db
|
||||
from app.forms.account import UpdateAccountForm, RegistrationForm, DeleteAccountForm
|
||||
from app.models.users import Member
|
||||
from app.utils.render import render
|
||||
|
||||
@app.route('/account', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def account():
|
||||
form = UpdateAccountForm()
|
||||
if request.method == "POST":
|
||||
if form.validate_on_submit():
|
||||
if form.avatar.data:
|
||||
f = form.avatar.data
|
||||
f.save("./app/static/"+current_user.avatar)
|
||||
current_user.update(
|
||||
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
|
||||
)
|
||||
db.session.merge(current_user)
|
||||
db.session.commit()
|
||||
flash('Modifications effectuées', 'ok')
|
||||
else:
|
||||
flash('Erreur lors de la modification', 'error')
|
||||
|
||||
return render('account.html', form=form)
|
||||
|
||||
@app.route('/account/delete', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def delete_account():
|
||||
del_form = DeleteAccountForm()
|
||||
if request.method == "POST":
|
||||
if del_form.validate_on_submit():
|
||||
db.session.delete(current_user)
|
||||
logout_user()
|
||||
db.session.commit()
|
||||
flash('Compte supprimé', 'ok')
|
||||
return redirect(url_for('index'))
|
||||
else:
|
||||
flash('Erreur lors de la suppression du compte', 'error')
|
||||
del_form.delete.data = False # Force to tick to delete the account
|
||||
return render('delete_account.html', del_form=del_form)
|
||||
|
||||
|
||||
@app.route('/register', methods=['GET', 'POST'])
|
||||
def register():
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('index'))
|
||||
form = RegistrationForm()
|
||||
if form.validate_on_submit():
|
||||
member = Member(form.username.data, form.email.data, form.password.data)
|
||||
db.session.add(member)
|
||||
db.session.commit()
|
||||
flash('Inscription réussie', 'ok')
|
||||
return redirect(url_for('validation'))
|
||||
return render('register.html', title='Register', form=form)
|
||||
|
||||
@app.route('/register/validation/')
|
||||
def validation():
|
||||
if current_user.is_authenticated :
|
||||
return redirect(url_for('index'))
|
||||
return render('validation.html')
|
|
@ -0,0 +1,58 @@
|
|||
from flask_login import login_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.utils.render import render
|
||||
from app.utils.priv_required import priv_required
|
||||
from app import app, db
|
||||
|
||||
@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")')
|
||||
|
||||
form = AdminForm()
|
||||
if form.validate_on_submit():
|
||||
# Clean up groups
|
||||
for g in Group.query.all():
|
||||
db.session.delete(g)
|
||||
db.session.commit( )
|
||||
|
||||
# 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)
|
||||
|
||||
# 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()
|
||||
|
||||
# Create template members
|
||||
m = Member('PlanèteCasio','contact@planet-casio.com','v5-forever')
|
||||
m.groups.append(g_community)
|
||||
db.session.add(m)
|
||||
|
||||
m = Member('GLaDOS', 'glados@aperture.science', 'v5-forever')
|
||||
m.groups.append(g_modos)
|
||||
m.groups.append(g_redacs)
|
||||
db.session.add(m)
|
||||
db.session.add(SpecialPrivilege(m, "edit-posts"))
|
||||
db.session.add(SpecialPrivilege(m, "shoutbox-ban"))
|
||||
|
||||
db.session.commit()
|
||||
|
||||
users = Member.query.all()
|
||||
groups = Group.query.all()
|
||||
return render('admin.html', users=users, groups=groups, form=form)
|
|
@ -0,0 +1,15 @@
|
|||
from app import app
|
||||
|
||||
from app.utils.render import render
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return render('index.html')
|
||||
|
||||
@app.errorhandler(404)
|
||||
def file_not_found(e):
|
||||
return render('errors/404.html'), 404
|
||||
|
||||
@app.errorhandler(403)
|
||||
def unauthorized_access(e):
|
||||
return render('errors/403.html'), 403
|
|
@ -0,0 +1,43 @@
|
|||
from flask import redirect, url_for, request, flash
|
||||
from flask_login import login_user, logout_user, login_required, current_user
|
||||
from app import app
|
||||
from app.forms.login import LoginForm
|
||||
from app.models.users import Member
|
||||
from app.utils.render import render
|
||||
|
||||
# from app.routes.index import index
|
||||
|
||||
@app.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('index'))
|
||||
|
||||
form = LoginForm()
|
||||
if form.validate_on_submit():
|
||||
member = Member.query.filter_by(name=form.username.data).first()
|
||||
if member is None or not member.check_password(form.password.data):
|
||||
flash('Pseudo ou mot de passe invalide', 'error')
|
||||
return redirect(request.referrer)
|
||||
login_user(member, remember=form.remember_me.data)
|
||||
# TODO: est-ce qu'on garde ce foutu message plus chiant qu'autre chose ?
|
||||
flash(f'Bon retour parmi nous, {current_user.name} !', 'info')
|
||||
if request.args.get('next'):
|
||||
return redirect(request.args.get('next'))
|
||||
if request.referrer:
|
||||
return redirect(request.referrer)
|
||||
return redirect(url_for('index'))
|
||||
return render('login.html', form=form)
|
||||
|
||||
@app.route('/logout')
|
||||
@login_required
|
||||
def logout():
|
||||
try:
|
||||
print(request.referrer)
|
||||
except Exception as e:
|
||||
print('No referrer:', e)
|
||||
|
||||
logout_user()
|
||||
flash('Déconnexion réussie', 'info')
|
||||
if request.referrer:
|
||||
return redirect(request.referrer)
|
||||
return redirect(url_for('index'))
|
|
@ -0,0 +1,8 @@
|
|||
from app import app
|
||||
from app.forms.search import AdvancedSearchForm
|
||||
from app.utils.render import render
|
||||
|
||||
@app.route('/search')
|
||||
def search():
|
||||
form = AdvancedSearchForm()
|
||||
return render('search.html', form=form)
|
|
@ -0,0 +1,20 @@
|
|||
from flask import redirect, url_for, abort
|
||||
from flask_login import login_required, current_user, logout_user
|
||||
from app import app, db
|
||||
from app.forms.account import UpdateAccountForm, RegistrationForm, DeleteAccountForm
|
||||
from app.models.users import Member
|
||||
from app.utils.render import render
|
||||
|
||||
@app.route('/user/<username>')
|
||||
def user(username):
|
||||
user = Member.query.filter_by(name=username).first()
|
||||
if not user:
|
||||
abort(404)
|
||||
return render('user.html', user=user)
|
||||
|
||||
@app.route('/user/id/<int:user_id>')
|
||||
def user_by_id(user_id):
|
||||
user = Member.query.filter_by(id=user_id).first()
|
||||
if not user:
|
||||
abort(404)
|
||||
return redirect(url_for('user', username=user.name))
|
|
@ -2,6 +2,33 @@
|
|||
margin-left: 60px;
|
||||
}
|
||||
|
||||
section {
|
||||
min-width: 350px; width: 90%;
|
||||
margin: 20px auto 0; padding: 20px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #dddddd; border-radius: 5px;
|
||||
}
|
||||
|
||||
section h1 {
|
||||
margin-top: 0;
|
||||
border-bottom: 1px solid #a0a0a0;
|
||||
font-family: Raleway; font-size: 32px;
|
||||
font-weight: 300; color: #242424;
|
||||
}
|
||||
|
||||
section h2 {
|
||||
margin-top: 0;
|
||||
border-bottom: 1px solid #a0a0a0;
|
||||
font-family: Raleway; font-size: 26px;
|
||||
font-weight: 300; color: #242424;
|
||||
}
|
||||
|
||||
section .avatar {
|
||||
display: block;
|
||||
border-radius: 100%;
|
||||
width: 150px; height: 150px;
|
||||
}
|
||||
|
||||
/* #container h1 {
|
||||
margin-left: 5%;
|
||||
font-family: Raleway; font-size: 24px;
|
||||
|
@ -13,5 +40,3 @@
|
|||
font-family: Raleway; font-size: 20px;
|
||||
font-weight: 200; color: #242424;
|
||||
} */
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
flash overlay
|
||||
*/
|
||||
|
||||
.flash {
|
||||
position: fixed; left: 15%;
|
||||
display: flex; align-items: center;
|
||||
width: 70%; z-index: 10;
|
||||
font-family: NotoSans; font-size: 14px; color: #212121;
|
||||
background: #ffffff;
|
||||
border-bottom: 5px solid #4caf50;
|
||||
border-radius: 1px; box-shadow: 0 1px 12px rgba(0, 0, 0, 0.3);
|
||||
transition: opacity .15s ease;
|
||||
transition: top .2s ease;
|
||||
}
|
||||
.flash.info {
|
||||
border-color: #2e7aec;
|
||||
}
|
||||
.flash.ok {
|
||||
border-color: #4caf50;
|
||||
}
|
||||
.flash.warning {
|
||||
border-color: #fbbc26;
|
||||
}
|
||||
.flash.error {
|
||||
border-color: #f44336;
|
||||
}
|
||||
.flash span {
|
||||
flex-grow: 1; margin: 15px 10px 10px 0;
|
||||
}
|
||||
.flash input[type="button"] {
|
||||
margin: 3px 30px 0 0; padding: 10px 15px;
|
||||
border: none;
|
||||
background: rgba(0, 0, 0, 0); color: #727272;
|
||||
}
|
||||
.flash input[type="button"]:hover {
|
||||
background: rgba(0, 0, 0, .1);
|
||||
}
|
||||
.flash input[type="button"]:focus {
|
||||
background: rgba(0, 0, 0, .2);
|
||||
}
|
||||
|
||||
.flash svg {
|
||||
margin: 15px 20px 10px 30px;
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
footer
|
||||
Footer
|
||||
*/
|
||||
|
||||
footer {
|
||||
|
@ -10,4 +10,4 @@ footer {
|
|||
}
|
||||
footer p {
|
||||
margin: 3px 0;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
.form .avatar {
|
||||
display: inline-block; vertical-align: middle;
|
||||
border-radius: 100%;
|
||||
width: 150px; height: 150px;
|
||||
}
|
||||
|
||||
.form .avatar + input[type="file"] {
|
||||
display: inline-block; margin-left: 20px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.form form > div:not(:last-child) {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form form label {
|
||||
display: inline-block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.form input {
|
||||
cursor: pointer; /* don't know why it is not a cursor by default */
|
||||
}
|
||||
|
||||
.form input[type='text'],
|
||||
.form input[type='email'],
|
||||
.form input[type='date'],
|
||||
.form input[type='password'],
|
||||
.form textarea {
|
||||
display: block;
|
||||
width: 100%; padding: 6px 2.5%;
|
||||
border: 1px solid #abcdef;
|
||||
}
|
||||
.form input[type='text']:focus,
|
||||
.form input[type='email']:focus,
|
||||
.form input[type='date']:focus,
|
||||
.form input[type='password']:focus,
|
||||
.form textarea:focus {
|
||||
box-shadow: 0 0 4px rgba(0, 102, 255, .9);
|
||||
}
|
||||
|
||||
.form input[type="submit"] {
|
||||
/*width: 20%;*/
|
||||
}
|
||||
|
||||
.form form .msgerror {
|
||||
color: red;
|
||||
font-weight: 400;
|
||||
margin-top: 5px;
|
||||
}
|
|
@ -9,81 +9,30 @@
|
|||
|
||||
|
||||
/*
|
||||
body
|
||||
ALL
|
||||
*/
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
transition: .15s ease;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
Body
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: #fbfbfb;
|
||||
background: #ffffff;
|
||||
font-family: 'DejaVu Sans', sans-serif;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
header
|
||||
*/
|
||||
|
||||
header {
|
||||
height: 50px; margin: 0; padding: 0 30px;
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
background: #f8f8fa; border-bottom: 1px solid #d0d0d0;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-family: Raleway; font-weight: 200;
|
||||
}
|
||||
|
||||
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: 250px;
|
||||
padding: 5px 35px 5px 10px;
|
||||
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;
|
||||
}
|
||||
header input[type="search"] ~ a {
|
||||
position: relative; left: -33px;
|
||||
}
|
||||
header input[type="search"]:focus {
|
||||
box-shadow: 0 0 4px rgba(0, 102, 255, .9);
|
||||
}
|
||||
header input[type="search"] ~ a > svg > path {
|
||||
fill: #cccccc; transition: .15s ease;
|
||||
}
|
||||
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 a:hover, header #spotlight a:focus {
|
||||
border-bottom: 2px solid rgba(93, 123, 141, 1);
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/*
|
||||
links
|
||||
Links
|
||||
*/
|
||||
|
||||
a {
|
||||
|
@ -96,94 +45,68 @@ a:focus {
|
|||
|
||||
|
||||
/*
|
||||
alert overlay
|
||||
Inputs
|
||||
*/
|
||||
|
||||
.alert {
|
||||
position: fixed; left: 15%;
|
||||
display: flex; align-items: center;
|
||||
width: 70%; z-index: 10;
|
||||
font-family: NotoSans; font-size: 14px; color: #212121;
|
||||
background: #ffffff;
|
||||
border-bottom: 5px solid #4caf50;
|
||||
border-radius: 1px; box-shadow: 0 1px 12px rgba(0, 0, 0, 0.3);
|
||||
transition: opacity .15s ease;
|
||||
input,
|
||||
textarea {
|
||||
display: block;
|
||||
background: #FFFFFF; color: #000000;
|
||||
border: none;
|
||||
}
|
||||
.alert.ok {
|
||||
border-color: #4caf50;
|
||||
}
|
||||
.alert.error {
|
||||
border-color: #f44336;
|
||||
}
|
||||
.alert span {
|
||||
flex-grow: 1; margin: 15px 10px 10px 0;
|
||||
}
|
||||
.alert input[type="button"] {
|
||||
margin: 3px 30px 0 0;
|
||||
input:focus:not(type="button"),
|
||||
textarea:focus {
|
||||
box-shadow: 0 0 0 0.2rem rgba(0,123,255,0.25);
|
||||
}
|
||||
|
||||
.alert svg {
|
||||
margin: 15px 20px 10px 30px;
|
||||
/* 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
|
||||
/*
|
||||
Bootstrap-style rules
|
||||
*/
|
||||
|
||||
input[type="button"] {
|
||||
font-family: NotoSans; font-size: 14px; /*font-weight: bold;*/
|
||||
text-align: center;
|
||||
padding: 5px 15px;
|
||||
transition: .1s ease;
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* flat */
|
||||
input[type="button"].flat {
|
||||
border: none;
|
||||
background: rgba(0, 0, 0, 0); color: #727272;
|
||||
.bg-green,
|
||||
.bg-green {
|
||||
background-color: #149641;
|
||||
color: #ffffff;
|
||||
}
|
||||
input[type="button"].flat:hover {
|
||||
background: rgba(0, 0, 0, .1);
|
||||
}
|
||||
input[type="button"].flat:focus {
|
||||
background: rgba(0, 0, 0, .2);
|
||||
.bg-green:hover,
|
||||
.bg-green:focus,
|
||||
.bg-green:active {
|
||||
background-color: #0f7331;
|
||||
}
|
||||
|
||||
/* raised */
|
||||
input[type="button"].raised {
|
||||
border: none;
|
||||
background: #e0e0e0; color: #212121;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, .3);
|
||||
.bg-red,
|
||||
.bg-red {
|
||||
background-color: #c0341d;
|
||||
color: #ffffff;
|
||||
}
|
||||
input[type="button"].raised:hover,
|
||||
input[type="button"].raised:focus {
|
||||
background: #d5d5d5;
|
||||
.bg-red:hover,
|
||||
.bg-red:focus,
|
||||
.bg-red:active {
|
||||
background-color: #aa3421;
|
||||
}
|
||||
input[type="button"].raised:active {
|
||||
background: #d6d6d6;
|
||||
box-shadow: 0 1px 8px rgba(0, 0, 0, .3);
|
||||
}
|
||||
|
||||
/* Input text */
|
||||
input[type="text"]:focus,
|
||||
input[type="search"]:focus,
|
||||
input[type="password"]:focus {
|
||||
|
||||
}
|
||||
|
||||
|
||||
section {
|
||||
margin: 10px 5%;
|
||||
}
|
||||
|
||||
section h1 {
|
||||
border-bottom: 1px solid #a0a0a0;
|
||||
font-family: Raleway; font-size: 24px;
|
||||
font-weight: 200; color: #242424;
|
||||
}
|
||||
|
||||
section * {
|
||||
transition: .15s ease;
|
||||
}
|
|
@ -3,31 +3,29 @@
|
|||
*/
|
||||
|
||||
header {
|
||||
margin: 0; padding: 10px 16px;
|
||||
display: flex; align-items: center; justify-content: flex-end;
|
||||
background: #ffffff; border-bottom: 1px solid #e0e0e0;
|
||||
height: 50px; margin: 0; padding: 0 30px;
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
background: #f8f8fa; border-bottom: 1px solid #d0d0d0;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-family: Raleway; font-weight: 200;
|
||||
}
|
||||
|
||||
header input + span { position: relative; left: -32px; cursor: pointer; }
|
||||
header span > svg {
|
||||
header svg {
|
||||
width: 24px; height: 24px; vertical-align: middle;
|
||||
transition: .15s ease; fill: #969696;
|
||||
transition: .15s ease;
|
||||
}
|
||||
header span:hover > svg, header span:focus > svg {
|
||||
border-color: rgba(32, 128, 255, .6);
|
||||
fill: #484848;
|
||||
header a:hover > svg, header a:focus > svg {
|
||||
filter: brightness(.5);
|
||||
}
|
||||
|
||||
header form {
|
||||
flex-shrink: 0; margin-right: -23px;
|
||||
}
|
||||
header input[type="search"] {
|
||||
width: 220px; height: 30px; padding: 4px 30px 4px 8px;
|
||||
border: 1px solid rgba(0, 0, 0, .2); border-radius: 2px;
|
||||
display: inline-block; width: 250px;
|
||||
padding: 5px 35px 5px 10px;
|
||||
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;
|
||||
}
|
||||
header input[type="search"] ~ a {
|
||||
position: relative; left: -33px;
|
||||
|
@ -42,54 +40,13 @@ header input[type="search"]:focus ~ a > svg > path {
|
|||
fill: #333333;
|
||||
}
|
||||
|
||||
#spotlight {
|
||||
flex-shrink: 0;
|
||||
#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 a {
|
||||
display: inline-block;
|
||||
height: 24px; line-height: 24px;
|
||||
padding: 2px 10px;
|
||||
background: #728bf6; color: white; font-size: 12px;
|
||||
border-radius: 2px; border: 1px solid rgba(0, 0, 255, .05);
|
||||
}
|
||||
#spotlight a:hover { border-bottom: 1px solid rgba(128, 128, 255, .05);
|
||||
background: #7a93ff; border-color: rgba(128, 128, 255, .03);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
subheader
|
||||
*/
|
||||
|
||||
#subheader {
|
||||
margin: 0; padding: 0 32px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
#subheader * {
|
||||
margin: 0; padding: 0;
|
||||
}
|
||||
|
||||
#subheader li {
|
||||
height: 30px; margin: 5px 0;
|
||||
display: inline;
|
||||
/*display: flex; align-items: center;*/
|
||||
}
|
||||
#subheader li:after {
|
||||
content: " »";
|
||||
opacity: 0.3;
|
||||
}
|
||||
#subheader li:last-child:after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
#subheader a {
|
||||
padding: 0 3px;
|
||||
/*border-bottom: 1px solid rgba(93, 123, 141, 0);*/
|
||||
color: #727272;
|
||||
}
|
||||
#subheader a:hover, #subheader a:focus {
|
||||
/*border-bottom: 1px solid rgba(93, 123, 141, 1);*/
|
||||
color: #22292c;
|
||||
text-decoration: none
|
||||
#spotlight a:hover, header #spotlight a:focus {
|
||||
border-bottom: 2px solid rgba(93, 123, 141, 1);
|
||||
}
|
||||
|
||||
|
|
|
@ -180,37 +180,34 @@ nav a:focus {
|
|||
margin-right: 10px;
|
||||
}
|
||||
|
||||
#menu form p {
|
||||
#menu form {
|
||||
padding: 0 5%;
|
||||
}
|
||||
#menu form input[type="text"] {
|
||||
display: block; width: 96%;
|
||||
#menu form input[type="text"],
|
||||
#menu form input[type="password"] {
|
||||
display: block; width: 100%;
|
||||
margin: 0; padding: 5px 2%;
|
||||
font-size: 14px; color: inherit;
|
||||
background: #e8e8e8; transition: background .15s ease;
|
||||
border: none;
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
}
|
||||
#menu form input[type="password"]{
|
||||
display: block; width: 96%;
|
||||
margin: 0; padding: 5px 2%;
|
||||
font-size: 14px; color: inherit;
|
||||
background: #e8e8e8; transition: background .15s ease;
|
||||
border: none; border-top: 1px solid #dddddd;
|
||||
border-bottom-left-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
}
|
||||
#menu form input[type="text"]:focus,
|
||||
#menu form input[type="password"]:focus {
|
||||
background: #ffffff;
|
||||
}
|
||||
#menu form input[type="text"] {
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
}
|
||||
#menu form input[type="password"] {
|
||||
border-bottom-left-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
}
|
||||
#menu form input[type="submit"] {
|
||||
width: 100%;
|
||||
margin-top: 10px; margin-bottom: 5px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
#menu form label {
|
||||
font-size: 13px; color: #FFFFFF; opacity: .7;
|
||||
}
|
||||
#menu form input[type="checkbox"] {
|
||||
vertical-align: middle;
|
||||
}
|
||||
#menu form input[type="submit"] {
|
||||
width: 100%; padding: 5px;
|
||||
}
|
||||
|
|
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 2.4 MiB |
Binary file not shown.
After Width: | Height: | Size: 1.2 MiB |
|
@ -12,15 +12,50 @@ function getCookie(name) {
|
|||
return unescape( document.cookie.substring( debut+name.length+1, end ) );
|
||||
}
|
||||
|
||||
function close_important(element) {
|
||||
/*
|
||||
Flash messages
|
||||
TODO: Find a way to have good flash messages in a KISS & DRY way
|
||||
*/
|
||||
function flash_add(type, message) {
|
||||
template = `<div class="flash {{ category }}" style="top: {{ top }}px;" onclick="flash_close(this)">
|
||||
<svg style='width:24px;height:24px' viewBox='0 0 24 24'>
|
||||
{{ icon }}
|
||||
</svg>
|
||||
<span>
|
||||
{{ message }}
|
||||
</span>
|
||||
<input type="button" value="MASQUER"></input>
|
||||
</div>`;
|
||||
paths = {
|
||||
'error': '<path fill="#727272" d="M11,15H13V17H11V15M11,7H13V13H11V7M12,2C6.47,2 2,6.5 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20Z"></path>',
|
||||
'warning': '<path fill="#727272" d="M12,2L1,21H23M12,6L19.53,19H4.47M11,10V14H13V10M11,16V18H13V16"></path>',
|
||||
'ok': '<path fill="#727272" d="M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4M11,16.5L6.5,12L7.91,10.59L11,13.67L16.59,8.09L18,9.5L11,16.5Z"></path>',
|
||||
'info': '<path fill="#727272" d="M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,17H13V11H11V17Z"></path>'
|
||||
};
|
||||
var top = (document.getElementsByClassName('flash').length + 1) * 70 - 45;
|
||||
template = template.replace("{{ category }}", type);
|
||||
template = template.replace("{{ top }}", top);
|
||||
template = template.replace("{{ icon }}", paths[type]);
|
||||
template = template.replace("{{ message }}", message);
|
||||
document.body.innerHTML += template;
|
||||
}
|
||||
function flash_close(element) {
|
||||
element.style.opacity = 0;
|
||||
setTimeout(function(){ element.parentNode.removeChild(element); }, 200);
|
||||
setTimeout(function(){
|
||||
var parent = element.parentNode;
|
||||
parent.removeChild(element);
|
||||
var childs = parent.getElementsByClassName('flash');
|
||||
for(var i = 0; i < childs.length; i++) {
|
||||
childs[i].style.top = ((i + 1) * 70 - 45) + 'px';
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/*
|
||||
Send post ajax request to url defined in action.
|
||||
Callback the function defined in the callback attribute from the submit type.
|
||||
*/
|
||||
/* We don't need Ajax at that time. Maybe later
|
||||
function ajaxWrapper(evt){
|
||||
evt.preventDefault();
|
||||
var elems = evt.target;
|
||||
|
@ -45,9 +80,7 @@ function ajaxWrapper(evt){
|
|||
req.send(params);
|
||||
}
|
||||
|
||||
/*
|
||||
Add event listener on submit for all form with class with-ajax.
|
||||
*/
|
||||
// Add event listener on submit for all form with class with-ajax.
|
||||
|
||||
window.onload = function(){
|
||||
|
||||
|
@ -71,4 +104,5 @@ window.onload = function(){
|
|||
|
||||
function login(response){
|
||||
alert(response);
|
||||
}
|
||||
}
|
||||
//*/
|
|
@ -0,0 +1,86 @@
|
|||
{% extends "base/container.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="form">
|
||||
<h1>Gestion du compte</h1>
|
||||
|
||||
<form action="{{ url_for('account') }}" 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=current_user.avatar) }}" meta="{{ current_user.avatar }}" />
|
||||
{{ form.avatar }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{{ form.email.label }}
|
||||
{{ form.email(placeholder=current_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>
|
||||
<div>
|
||||
{{ form.password2.label }}
|
||||
{{ form.password2(placeholder='************') }}
|
||||
{% for error in form.password2.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>
|
||||
{{ form.old_password.label }}
|
||||
{{ form.old_password(placeholder='************') }}
|
||||
{% for error in form.old_password.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<h2>À propos</h2>
|
||||
<div>
|
||||
{{ form.birthday.label }}
|
||||
{{ form.birthday(value=current_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 }}">{{ current_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 }}">{{ current_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=current_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('delete_account') }}" class="button bg-red">Supprimer le compte</a>
|
||||
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -0,0 +1,43 @@
|
|||
{% 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 %}
|
|
@ -1,15 +0,0 @@
|
|||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert error" style="top: 95px;" onclick="close_important(this)">
|
||||
<svg style='width:24px;height:24px' viewBox='0 0 24 24'>
|
||||
<path fill="#727272" d="M12,2L1,21H23M12,6L19.53,19H4.47M11,10V14H13V10M11,16V18H13V16"></path>
|
||||
</svg>
|
||||
<span>
|
||||
{{ message }}
|
||||
</span>
|
||||
<input type="button" class="flat" value="MASQUER" onclick="setCookie('pc_notif_2', 'true');"></input>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
|
@ -0,0 +1,16 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="fr-FR">
|
||||
{% include "base/head.html" %}
|
||||
<body>
|
||||
{% include "base/navbar.html" %}
|
||||
|
||||
{% block container %}
|
||||
{% endblock container %}
|
||||
|
||||
{% include "base/footer.html" %}
|
||||
|
||||
{% include "base/flash.html" %}
|
||||
|
||||
{% include "base/scripts.html" %}
|
||||
</body>
|
||||
</html>
|
|
@ -1,16 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="fr-FR">
|
||||
{% include "base/head.html.j2" %}
|
||||
<body>
|
||||
{% include "base/navbar.html.j2" %}
|
||||
|
||||
{% block container %}
|
||||
{% endblock container %}
|
||||
|
||||
{% include "base/footer.html.j2" %}
|
||||
|
||||
{% include "base/alerts.html.j2" %}
|
||||
|
||||
{% include "base/scripts.html.j2" %}
|
||||
</body>
|
||||
</html>
|
|
@ -1,8 +1,8 @@
|
|||
{% extends "base/base.html.j2" %}
|
||||
{% extends "base/base.html" %}
|
||||
|
||||
{% block container %}
|
||||
<div id="container">
|
||||
{% include "base/header.html.j2" %}
|
||||
{% include "base/header.html" %}
|
||||
|
||||
{% block content %}
|
||||
{% endblock content %}
|
|
@ -1,52 +0,0 @@
|
|||
{% extends "base/container.html.j2" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="home-pinned-content">
|
||||
<p>
|
||||
Erreur {{ error_code }}
|
||||
<br>
|
||||
{% if error_code == 400 %}
|
||||
Bad Request : Votre requête semble mal formée.
|
||||
{% elif error_code == 401 %}
|
||||
Unauthorized : Une authentification est nécessaire pour accéder à la ressource.
|
||||
<br>
|
||||
Veuillez vous connecter à l'aide du panneau sur la gauche.
|
||||
{% elif error_code == 403 %}
|
||||
Forbidden : Cette page vous est interdite. Désolé, si vous pensez que c'est une erreur ou que vous voulez avoir accès à cette page malgré tout veuillez contacter un administrateur.
|
||||
{% elif error_code == 404 %}
|
||||
Not Found : La page n'existe pas ou plus, si vous avez rentré l'url à la main vérifiez de ne pas vous être trompé.
|
||||
<pre> /| /‾‾‾‾‾‾‾\ /| |\ | /‾‾‾‾‾\ –––––––––
|
||||
/ | / \ / | | \ | / \ |
|
||||
/ | | | / | | \ | / \ |
|
||||
/ | | | / | | \ | | | |
|
||||
/ | | | / | | \ | | | |
|
||||
/ | | | / | | \ | | | |
|
||||
/______|_ | | /______|_ | \ | | | |
|
||||
| | | | | \ | \ / |
|
||||
| \ / | | \ | \ / |
|
||||
| \_______/ | | \| \_____/ |
|
||||
|
||||
|‾‾‾‾‾‾‾‾‾‾ /‾‾‾‾‾\ | | |\ | |‾‾‾‾‾‾‾\
|
||||
| / \ | | | \ | | \
|
||||
| / \ | | | \ | | \
|
||||
| | | | | | \ | | |
|
||||
|––––––– | | | | | \ | | |
|
||||
| | | | | | \ | | |
|
||||
| | | | | | \ | | |
|
||||
| \ / \ / | \ | | /
|
||||
| \ / \ / | \ | | /
|
||||
| \_____/ \_____/ | \| |_______/</pre>
|
||||
{% elif error_code == 418 %}
|
||||
Oups! Il semblerai que vous m'ayez demandé du café mais je suis une théière.
|
||||
<br>
|
||||
Vous voulez une tasse de thé à la place de votre café?
|
||||
{% elif error_code == 500 %}
|
||||
Internal Server Error : Erreur interne du serveur. Rassurez vous vous n'êtes pas en cause c'est sans doute un développeur qui à fait l'imbécile. Si cette page s'affiche trop souvent essayez de contacter un administrateur par mail à <a href="mailto: devs@planet-casio.com">devs@planet-casio.com</a>.
|
||||
{% elif error_code == 501 %}
|
||||
Not Implemented : La fonctionnalité réclamée n'est pas supportée par le serveur. Désolé si cette fonctionnalité vous tient à cœur vous pouvez la proposer sur le <a href="/#">forum</a>.
|
||||
{% elif error_code == 503 %}
|
||||
Service Unavailable : Service temporairement indisponible ou en maintenance. Patientez!
|
||||
{% endif %}
|
||||
</p>
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -0,0 +1,18 @@
|
|||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="flash {{ category }}" style="top: {{ loop.index * 70 - 45 }}px;" onclick="flash_close(this)">
|
||||
<svg style='width:24px;height:24px' viewBox='0 0 24 24'>
|
||||
{% if category=="error" %}<path fill="#727272" d="M11,15H13V17H11V15M11,7H13V13H11V7M12,2C6.47,2 2,6.5 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20Z"></path>{% endif %}
|
||||
{% if category=="warning" %}<path fill="#727272" d="M12,2L1,21H23M12,6L19.53,19H4.47M11,10V14H13V10M11,16V18H13V16"></path>{% endif %}
|
||||
{% if category=="ok" %}<path fill="#727272" d="M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4M11,16.5L6.5,12L7.91,10.59L11,13.67L16.59,8.09L18,9.5L11,16.5Z"></path>{% endif %}
|
||||
{% if category=="info" %}<path fill="#727272" d="M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,17H13V11H11V17Z"></path>{% endif %}
|
||||
</svg>
|
||||
<span>
|
||||
{{ message }}
|
||||
</span>
|
||||
<input type="button" value="MASQUER"></input>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
|
@ -0,0 +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>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>
|
|
@ -1,8 +0,0 @@
|
|||
<footer>
|
||||
<p>Planète Casio est un site communautaire indépendant, géré bénévolement et n'est donc pas
|
||||
affilié à Casio | Toute reproduction de Planète Casio, même partielle, est interdite.</p>
|
||||
<p>Les fichiers, programmes et autres publications présents sur Planète Casio restent la
|
||||
propriété de leurs auteurs respectifs 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>
|
|
@ -6,7 +6,11 @@
|
|||
|
||||
<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" media="all and (max-width: 699px)" type="text/css" href={{url_for('static', filename = 'css/light.css')}}>
|
||||
</head>
|
|
@ -1,6 +1,6 @@
|
|||
<header>
|
||||
<form>
|
||||
<input type="search" placeholder="Recherche" />
|
||||
<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>
|
|
@ -69,22 +69,18 @@
|
|||
</ul>
|
||||
|
||||
<div id="menu">
|
||||
{% include "base/navbar/account.html" %}
|
||||
|
||||
{% include "base/navbar/account.html.j2" %}
|
||||
{% include "base/navbar/news.html" %}
|
||||
|
||||
{% include "base/navbar/news.html.j2" %}
|
||||
{% include "base/navbar/forum.html" %}
|
||||
|
||||
{% include "base/navbar/forum.html.j2" %}
|
||||
{% include "base/navbar/programs.html" %}
|
||||
|
||||
{% include "base/navbar/programs.html.j2" %}
|
||||
{% include "base/navbar/tutorials.html" %}
|
||||
|
||||
{% include "base/navbar/tutorials.html.j2" %}
|
||||
{% include "base/navbar/sprites.html" %}
|
||||
|
||||
{% include "base/navbar/sprites.html.j2" %}
|
||||
|
||||
{% include "base/navbar/tools.html.j2" %}
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
{% endif %}
|
||||
{% include "base/navbar/tools.html" %}
|
||||
</div>
|
||||
</nav>
|
|
@ -1,8 +1,8 @@
|
|||
{% if current_user.is_authenticated %}
|
||||
<div>
|
||||
<h2>
|
||||
<img src="{{ url_for('static', filename= 'images/3864.png') }}">
|
||||
{{ current_user.username }}
|
||||
<img src="{{ url_for('static', filename=current_user.avatar) }}">
|
||||
{{ current_user.name }}
|
||||
</h2>
|
||||
<a href="#">
|
||||
<svg viewBox="0 0 24 24">
|
||||
|
@ -22,7 +22,7 @@
|
|||
</svg>
|
||||
Topics favoris
|
||||
</a>
|
||||
<a href="#">
|
||||
<a href="{{ url_for('admin') }}">
|
||||
<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 +31,7 @@
|
|||
|
||||
<hr />
|
||||
|
||||
<a href="account.html.j2">
|
||||
<a href="{{ url_for('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>
|
||||
|
@ -49,14 +49,12 @@
|
|||
<h2>
|
||||
Invité
|
||||
</h2>
|
||||
<form method="post" action="" class="login with-ajax">
|
||||
{{ form.hidden_tag() }}
|
||||
<p>
|
||||
{{ form.username(size=32, placeholder="Identifiant") }}
|
||||
{{ form.password(size=32, placeholder="Mot de passe") }}
|
||||
</p>
|
||||
<p>{{ form.submit() }}</p>
|
||||
<p>{{ form.remember_me.label }} {{ form.remember_me() }}</p>
|
||||
<form method="post" action="{{url_for('login')}}" class="login">
|
||||
{{ login_form.hidden_tag() }}
|
||||
{{ login_form.username(size=32, placeholder="Identifiant") }}
|
||||
{{ login_form.password(size=32, placeholder="Mot de passe") }}
|
||||
{{ login_form.submit(class_="bg-green") }}
|
||||
{{ login_form.remember_me.label }} {{ login_form.remember_me() }}
|
||||
</form>
|
||||
<hr />
|
||||
<a href="{{ url_for('register') }}">Mot de passe oublié ?</a>
|
|
@ -0,0 +1,25 @@
|
|||
<div>
|
||||
<h2>
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path fill="#ffffff" d="M17,12V3A1,1 0 0,0 16,2H3A1,1 0 0,0 2,3V17L6,13H16A1,1 0 0,0 17,12M21,6H19V15H6V17A1,1 0 0,0 7,18H18L22,22V7A1,1 0 0,0 21,6Z"></path>
|
||||
</svg>
|
||||
Forum
|
||||
</h2>
|
||||
<a href="#">Vie communautaire</a>
|
||||
<a href="#">Projets de programmation</a>
|
||||
<a href="#">Questions et problèmes</a>
|
||||
<a href="#">Discussions</a>
|
||||
<a href="#">Administration</a>
|
||||
<a href="#">CreativeCalc</a>
|
||||
|
||||
<hr />
|
||||
|
||||
<h3>Derniers commentaires</h3>
|
||||
<ul>
|
||||
<li><a href="#">Legolas</a> sur <a href="#">Bugs de la v5</a></li>
|
||||
<li><a href="#">Dark Storm</a> sur <a href="#">fxSDK support</a></li>
|
||||
<li><a href="#">Gollum</a> sur <a href="#">Le nom de topic qui fout le bordel car il est trop long…</a></li>
|
||||
<li><a href="#">Lephenixnoir</a> sur <a href="#">fxSDK support</a></li>
|
||||
<li><a href="#">Kristaba</a> sur <a href="#">FiXos, le retour</a></li>
|
||||
</ul>
|
||||
</div>
|
|
@ -1,25 +0,0 @@
|
|||
<div>
|
||||
<h2>
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path fill="#ffffff" d="M17,12V3A1,1 0 0,0 16,2H3A1,1 0 0,0 2,3V17L6,13H16A1,1 0 0,0 17,12M21,6H19V15H6V17A1,1 0 0,0 7,18H18L22,22V7A1,1 0 0,0 21,6Z"></path>
|
||||
</svg>
|
||||
Forum
|
||||
</h2>
|
||||
<a href="/forum">Vie communautaire</a>
|
||||
<a href="/forum">Projets de programmation</a>
|
||||
<a href="/forum">Questions et problèmes</a>
|
||||
<a href="/forum">Discussions</a>
|
||||
<a href="/forum">Administration</a>
|
||||
<a href="/forum">CreativeCalc</a>
|
||||
|
||||
<hr />
|
||||
|
||||
<h3>Derniers commentaires</h3>
|
||||
<ul>
|
||||
<li><a href="/forum">Legolas</a> sur <a href="#">Bugs de la v5</a></li>
|
||||
<li><a href="/forum">Dark Storm</a> sur <a href="#">fxSDK support</a></li>
|
||||
<li><a href="/forum">Gollum</a> sur <a href="#">Le nom de topic qui fout le bordel car il est trop long…</a></li>
|
||||
<li><a href="/forum">Lephenixnoir</a> sur <a href="#">fxSDK support</a></li>
|
||||
<li><a href="/forum">Kristaba</a> sur <a href="#">FiXos, le retour</a></li>
|
||||
</ul>
|
||||
</div>
|
|
@ -6,7 +6,7 @@
|
|||
Outils
|
||||
</h2>
|
||||
|
||||
<a href="https://git.planet-casio.com/">
|
||||
<a href="#">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path fill="#ffffff" d="M2.6,10.59L8.38,4.8L10.07,6.5C9.83,7.35 10.22,8.28 11,8.73V14.27C10.4,14.61 10,15.26 10,16A2,2 0 0,0 12,18A2,2 0 0,0 14,16C14,15.26 13.6,14.61 13,14.27V9.41L15.07,11.5C15,11.65 15,11.82 15,12A2,2 0 0,0 17,14A2,2 0 0,0 19,12A2,2 0 0,0 17,10C16.82,10 16.65,10 16.5,10.07L13.93,7.5C14.19,6.57 13.71,5.55 12.78,5.16C12.35,5 11.9,4.96 11.5,5.07L9.8,3.38L10.59,2.6C11.37,1.81 12.63,1.81 13.41,2.6L21.4,10.59C22.19,11.37 22.19,12.63 21.4,13.41L13.41,21.4C12.63,22.19 11.37,22.19 10.59,21.4L2.6,13.41C1.81,12.63 1.81,11.37 2.6,10.59Z"></path>
|
||||
</svg>
|
|
@ -5,9 +5,9 @@
|
|||
</svg>
|
||||
Tutoriels
|
||||
</h2>
|
||||
<a href="/tutoriels/basiccasio">Basic Casio</a>
|
||||
<a href="/tutoriels/ccplusplus">C/C++ Casio</a>
|
||||
<a href="/tutoriels/arduino">Arduino</a>
|
||||
<a href="#">Basic Casio</a>
|
||||
<a href="#">C/C++ Casio</a>
|
||||
<a href="#">Arduino</a>
|
||||
<hr>
|
||||
<a href="/tutoriels/faq">Foire aux questions (FAQ)</a>
|
||||
<a href="#">Foire aux questions (FAQ)</a>
|
||||
</div>
|
|
@ -0,0 +1,26 @@
|
|||
{% extends "base/container.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="form">
|
||||
<h1>Suppression du compte</h2>
|
||||
<form action="{{ url_for('delete_account') }}" method="post">
|
||||
{{ del_form.hidden_tag() }}
|
||||
<div>
|
||||
{{ del_form.delete.label }}
|
||||
{{ del_form.delete(checked=False) }}
|
||||
<div style="font-size:80%;color:rgba(0,0,0,.5)">{{ del_form.delete.description }}</div>
|
||||
{% for error in del_form.delete.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>
|
||||
{{ del_form.old_password.label }}
|
||||
{{ del_form.old_password(placeholder='************') }}
|
||||
{% for error in del_form.old_password.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>{{ del_form.submit(class_="bg-red") }}</div>
|
||||
</form>
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -0,0 +1,9 @@
|
|||
{% extends "base/container.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section>
|
||||
<h1>403 - Accès non autorisé</h1>
|
||||
|
||||
<img src="{{url_for('static', filename = 'images/403.webp')}}" style="display:block;margin:auto"; />
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -0,0 +1,9 @@
|
|||
{% extends "base/container.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section>
|
||||
<h1>404 - Page non trouvée</h1>
|
||||
|
||||
<img src="{{url_for('static', filename = 'images/404.webp')}}" style="display:block;margin:auto"; />
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -1,9 +1,9 @@
|
|||
{% extends "base/container.html.j2" %}
|
||||
{% extends "base/container.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="home-pinned-content">
|
||||
<div>
|
||||
<p>du contenu....</p>
|
||||
<p>du contenu....</p>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -0,0 +1,27 @@
|
|||
{% extends "base/container.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="form" style="width:40%;">
|
||||
<h1>Connexion</h1>
|
||||
<form action="" method="post">
|
||||
{{ form.hidden_tag() }}
|
||||
<p>
|
||||
{{ form.username.label }}<br>
|
||||
{{ form.username(size=32) }}<br>
|
||||
{% for error in form.username.errors %}
|
||||
<span style="color: red;">[{{ error }}]</span>
|
||||
{% endfor %}
|
||||
</p>
|
||||
<p>
|
||||
{{ form.password.label }}<br>
|
||||
{{ form.password(size=32) }}<br>
|
||||
{% for error in form.password.errors %}
|
||||
<span style="color: red;">[{{ error }}]</span>
|
||||
{% endfor %}
|
||||
</p>
|
||||
<p>{{ form.remember_me() }} {{ form.remember_me.label }}</p>
|
||||
<p>{{ form.submit() }}</p>
|
||||
</form>
|
||||
<p>Pas encore de compte ? <a href="{{ url_for('register') }}">Créé-en un !</a></p>
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -1,25 +0,0 @@
|
|||
{% extends "container.html.j2" %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Sign In</h1>
|
||||
<form action="" method="post">
|
||||
{{ form.hidden_tag() }}
|
||||
<p>
|
||||
{{ form.username.label }}<br>
|
||||
{{ form.username(size=32) }}<br>
|
||||
{% for error in form.username.errors %}
|
||||
<span style="color: red;">[{{ error }}]</span>
|
||||
{% endfor %}
|
||||
</p>
|
||||
<p>
|
||||
{{ form.password.label }}<br>
|
||||
{{ form.password(size=32) }}<br>
|
||||
{% for error in form.password.errors %}
|
||||
<span style="color: red;">[{{ error }}]</span>
|
||||
{% endfor %}
|
||||
</p>
|
||||
<p>{{ form.remember_me() }} {{ form.remember_me.label }}</p>
|
||||
<p>{{ form.submit() }}</p>
|
||||
</form>
|
||||
<p>New User? <a href="{{ url_for('register') }}">Click to Register!</a></p>
|
||||
{% endblock %}
|
|
@ -0,0 +1,56 @@
|
|||
{% extends "base/container.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="form" style="width:40%;">
|
||||
<h1>Inscription</h1>
|
||||
|
||||
<form action="" method="post">
|
||||
{{ form.hidden_tag() }}
|
||||
<div>
|
||||
{{ form.username.label }}
|
||||
{{ form.username() }}
|
||||
{% for error in form.username.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>
|
||||
{{ form.email.label }}
|
||||
{{ form.email() }}
|
||||
{% for error in form.email.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>
|
||||
{{ form.password.label }}
|
||||
{{ form.password() }}
|
||||
{% for error in form.password.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>
|
||||
{{ form.password2.label }}
|
||||
{{ form.password2() }}
|
||||
{% for error in form.password2.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>
|
||||
{{ form.guidelines.label }}
|
||||
{{ form.guidelines() }}
|
||||
{% for error in form.guidelines.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>
|
||||
{{ form.newsletter.label }}
|
||||
{{ form.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>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -1,41 +0,0 @@
|
|||
{% extends "base/container.html.j2" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="home-pinned-content">
|
||||
|
||||
<h1>Inscription :</h1>
|
||||
|
||||
<form action="" method="post">
|
||||
{{ form2.hidden_tag() }}
|
||||
<p>
|
||||
{{ form2.username.label }}<br>
|
||||
{{ form2.username(size=32) }}<br>
|
||||
{% for error in form2.username.errors %}
|
||||
<span style="color: red;">[{{ error }}]</span>
|
||||
{% endfor %}
|
||||
</p>
|
||||
<p>
|
||||
{{ form2.email.label }}<br>
|
||||
{{ form2.email(size=64) }}<br>
|
||||
{% for error in form2.email.errors %}
|
||||
<span style="color: red;">[{{ error }}]</span>
|
||||
{% endfor %}
|
||||
</p>
|
||||
<p>
|
||||
{{ form2.password.label }}<br>
|
||||
{{ form2.password(size=32) }}<br>
|
||||
{% for error in form2.password.errors %}
|
||||
<span style="color: red;">[{{ error }}]</span>
|
||||
{% endfor %}
|
||||
</p>
|
||||
<p>
|
||||
{{ form2.password2.label }}<br>
|
||||
{{ form2.password2(size=32) }}<br>
|
||||
{% for error in form2.password2.errors %}
|
||||
<span style="color: red;">[{{ error }}]</span>
|
||||
{% endfor %}
|
||||
</p>
|
||||
<p>{{ form2.submit() }}</p>
|
||||
</form>
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -0,0 +1,19 @@
|
|||
{% extends "base/container.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="form">
|
||||
<h1>Recherche avancée</h1>
|
||||
|
||||
<form action="" method="get">
|
||||
<div>
|
||||
{{ form.q.label }}
|
||||
{{ form.q(value=request.args.get('q')) }}
|
||||
</div>
|
||||
<div>
|
||||
{{ form.date.label }}
|
||||
{{ form.date }}
|
||||
</div>
|
||||
<div>{{ form.submit(class_="bg-green") }}</div>
|
||||
</form>
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -0,0 +1,9 @@
|
|||
{% extends "base/container.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section>
|
||||
<h1>
|
||||
<img class="avatar" src="{{ url_for('static', filename=user.avatar) }}" alt="{{ user.name }}" style="display:inline; vertical-align:middle;"/>
|
||||
Profil de {{ user.name }}</h1>
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -0,0 +1,10 @@
|
|||
{% extends "base/container.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section>
|
||||
<div>
|
||||
<p>ici il y aura la page qui demande de checker les mails pour valider</p><br>
|
||||
<a href="{{url_for('index')}}">Retour à la page d'accueil</a>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock content %}
|
|
@ -1,11 +0,0 @@
|
|||
{% extends "base/container.html.j2" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="home-pinned-content">
|
||||
|
||||
<div>
|
||||
<p>ici il y aura la page qui demande de checker les mails pour valider</p><br>
|
||||
<a href="{{url_for('index')}}">Retour à la page d'acceuil</a>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock content %}
|
|
@ -0,0 +1,17 @@
|
|||
from flask import redirect, url_for, flash
|
||||
from flask import current_user
|
||||
import functools
|
||||
|
||||
# Use only with @login_required.
|
||||
def privilege_required(priv):
|
||||
def privilege_decorator(f):
|
||||
@functools.wraps(f)
|
||||
def wrapper():
|
||||
if not current_user.priv(priv):
|
||||
flash(f'Cette page est protégée par le privilège <code>{priv}'+
|
||||
'</code>', 'error')
|
||||
return redirect(url_for('index'))
|
||||
else:
|
||||
f()
|
||||
return wrapper
|
||||
return privilege_decorator
|
|
@ -0,0 +1,38 @@
|
|||
from functools import wraps
|
||||
from flask import redirect, url_for, request, flash, abort
|
||||
from flask_login import current_user
|
||||
from flask_login.config import EXEMPT_METHODS
|
||||
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::
|
||||
|
||||
@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.
|
||||
"""
|
||||
def decorated_view(func):
|
||||
@wraps(func)
|
||||
def wrapped(*args, **kwargs):
|
||||
if request.method in EXEMPT_METHODS:
|
||||
return func(*args, **kwargs)
|
||||
elif app.config.get('LOGIN_DISABLED'):
|
||||
return func(*args, **kwargs)
|
||||
elif not current_user.is_authenticated:
|
||||
return app.login_manager.unauthorized()
|
||||
else:
|
||||
for p in perms:
|
||||
if not current_user.priv(p):
|
||||
abort(403)
|
||||
return func(*args, **kwargs)
|
||||
return wrapped
|
||||
return decorated_view
|
|
@ -0,0 +1,14 @@
|
|||
from flask import render_template
|
||||
from app.forms.login import LoginForm
|
||||
from app.forms.search import SearchForm
|
||||
|
||||
def render(*args, **kwargs):
|
||||
# TODO: debugguer cette merde : au logout, ça foire
|
||||
# if current_user.is_authenticated:
|
||||
# login_form = LoginForm()
|
||||
# return render_template(*args, **kwargs, login_form=login_form)
|
||||
# return render_template(*args, **kwargs)
|
||||
login_form = LoginForm()
|
||||
search_form = SearchForm()
|
||||
return render_template(*args, **kwargs,
|
||||
login_form=login_form, search_form=search_form)
|
|
@ -0,0 +1,30 @@
|
|||
from flask_login import current_user
|
||||
from wtforms.validators import ValidationError
|
||||
from app.models.users import User, Member
|
||||
|
||||
def name(form, name):
|
||||
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()
|
||||
if member is not None:
|
||||
raise ValidationError('Adresse email déjà utilisée.')
|
||||
|
||||
def password(form, password):
|
||||
if len(password.data) != 0 and len(password.data) < 10:
|
||||
raise ValidationError('Mot de passe est trop court (10 caractères minimum).')
|
||||
# TODO: add more rules >:]
|
||||
|
||||
def avatar(form, avatar):
|
||||
pass
|
||||
|
||||
def old_password(form, field):
|
||||
if field.data:
|
||||
if not form.old_password.data:
|
||||
raise ValidationError('Votre ancien mot de passe est requis pour cette modification.')
|
||||
if not current_user.check_password(form.old_password.data):
|
||||
raise ValidationError('Mot de passe actuel erroné.')
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 98 KiB |
|
@ -0,0 +1,38 @@
|
|||
# Privileges
|
||||
|
||||
Read/write access to forum boards:
|
||||
access-admin-board Administration board of the forum
|
||||
access-assoc-board CreativeCalc discussion board
|
||||
write-news Post articles on the news board
|
||||
|
||||
Shared file upload (like /Fr/adpc/img.php for any file):
|
||||
upload-shared-files Upload files and images on the website server
|
||||
delete-shared-files Delete files uploaded with upload-shared-files
|
||||
|
||||
Post management:
|
||||
edit-posts Edit any post on the website
|
||||
delete-posts Remove any post from the website
|
||||
scheduled-posting Schedule a post or content creation in the future
|
||||
|
||||
Content (topic, progs, tutos, etc) management:
|
||||
delete-content Delete whole topics, program pages, or tutorials
|
||||
move-public-content Change the section of a page in a public section
|
||||
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
|
||||
|
||||
Program evaluation:
|
||||
delete-notes Delete program notes
|
||||
delete-tests Delete program tests
|
||||
|
||||
Shoutbox:
|
||||
shoutbox-post Write messages in the shoutbox
|
||||
shoutbox-kick Kick people using the shoutbox
|
||||
shoutbox-ban Ban people using the shoutbox
|
||||
|
||||
Miscellaenous:
|
||||
unlimited-pms Removes the limit on the number of private messages
|
||||
footer-statistics View performance statistics in the page footer
|
||||
community-login Automatically login as a community account
|
||||
|
||||
Administration panel...
|
|
@ -0,0 +1,73 @@
|
|||
# User management
|
||||
|
||||
User information:
|
||||
|
||||
Name Unique, no space, at least one letter
|
||||
Avatar Stored in a server folder. Size limit?
|
||||
Password Hashed, of course
|
||||
Email Mail address, used to send newsletters
|
||||
|
||||
Points Participation measure (mainly number of posts)
|
||||
Innovation points A different kind of participation measure
|
||||
|
||||
Biography Description of the user
|
||||
Signature Short signature added at the end of every post
|
||||
Birthday Birthday date
|
||||
|
||||
Newsletter Subscription to newsletter
|
||||
Settings...
|
||||
|
||||
Relations:
|
||||
|
||||
Notifications 1 to many
|
||||
Groups 1 to many
|
||||
Sent PMs 1 to many
|
||||
Received PMs 1 to many
|
||||
Trophies/Titles 1 to many
|
||||
|
||||
All posts many to many (tutorials can have several authors)
|
||||
Privileges many to many
|
||||
|
||||
Rest API for users:
|
||||
Requests where "Search" is set to "Yes" accept search patterns. The syntax
|
||||
needs to be chosen, it could be something like "/users[name~=/*storm*/i]".
|
||||
|
||||
Method URL Search Description
|
||||
-----------------------------------------------------------------------------
|
||||
GET /users Yes Query users
|
||||
POST /users - Create new user
|
||||
-----------------------------------------------------------------------------
|
||||
GET /users/<id> - Get user information/settings
|
||||
PATCH /users/<id> - Update user information/settings
|
||||
DELETE /users/<id> - Delete user account
|
||||
-----------------------------------------------------------------------------
|
||||
GET /users/<id>/trophies - Get unlocked trophies
|
||||
-----------------------------------------------------------------------------
|
||||
GET /users/<id>/messages Yes Query private messages
|
||||
POST /users/<id>/messages - Send PM (<id> is sender)
|
||||
DELETE /users/<id>/messages Required Delete PMs matching pattern
|
||||
-----------------------------------------------------------------------------
|
||||
GET /users/<id>/groups - Get user groups
|
||||
PATCH /users/<id>/groups - Add or remove group memberships (*)
|
||||
-----------------------------------------------------------------------------
|
||||
GET /users/<id>/privs - Get user privileges
|
||||
PATCH /users/<id>/privs - Grant/revoke special privileges (*)
|
||||
-----------------------------------------------------------------------------
|
||||
|
||||
Updating the participation scores is not a request, it's tied to posting
|
||||
contents, so it has nothing to do in the API.
|
||||
|
||||
(*) Not sure if this is relevant, since these are administrator duties.
|
||||
|
||||
Rest API for groups:
|
||||
|
||||
Method URL Search Description
|
||||
-----------------------------------------------------------------------------
|
||||
GET /groups - Get list of groups
|
||||
POST /groups - Create group
|
||||
-----------------------------------------------------------------------------
|
||||
GET /groups/<id> - Get list of users in groups
|
||||
-----------------------------------------------------------------------------
|
||||
|
||||
There are no methods to change the privileges associated to each groups
|
||||
because this task is clearly for administrators, not API users.
|
20
config.py
20
config.py
|
@ -1,9 +1,17 @@
|
|||
import os
|
||||
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
|
||||
class Config(object):
|
||||
SECRET_KEY = os.environ.get('SECRET_KEY') or 'a-random-secret-key'
|
||||
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
|
||||
'sqlite:///' + os.path.join(basedir, 'app.db')
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
SECRET_KEY = os.environ.get('SECRET_KEY') or 'a-random-secret-key'
|
||||
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
|
||||
'postgresql+psycopg2://' + os.environ.get('USER') + ':@/pcv5'
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
UPLOAD_FOLDER = './app/static/avatars'
|
||||
|
||||
class V5Config(object):
|
||||
# Length allocated to privilege names (slugs)
|
||||
PRIVS_MAXLEN = 64
|
||||
# Forbidden user names
|
||||
FORBIDDEN_USERNAMES = [ "admin", "root", "webmaster", "contact" ]
|
||||
# Unauthorized message (@priv_required)
|
||||
UNAUTHORIZED_MSG = "Vous n'avez pas l'autorisation d'effectuer cette action !"
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Generic single-database configuration.
|
|
@ -0,0 +1,45 @@
|
|||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# template used to generate migration files
|
||||
# file_template = %%(rev)s_%%(slug)s
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
|
@ -0,0 +1,90 @@
|
|||
from __future__ import with_statement
|
||||
from alembic import context
|
||||
from sqlalchemy import engine_from_config, pool
|
||||
from logging.config import fileConfig
|
||||
import logging
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
fileConfig(config.config_file_name)
|
||||
logger = logging.getLogger('alembic.env')
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
from flask import current_app
|
||||
config.set_main_option('sqlalchemy.url',
|
||||
current_app.config.get('SQLALCHEMY_DATABASE_URI'))
|
||||
target_metadata = current_app.extensions['migrate'].db.metadata
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
|
||||
def run_migrations_offline():
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(url=url)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online():
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
|
||||
# this callback is used to prevent an auto-migration from being generated
|
||||
# when there are no changes to the schema
|
||||
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
|
||||
def process_revision_directives(context, revision, directives):
|
||||
if getattr(config.cmd_opts, 'autogenerate', False):
|
||||
script = directives[0]
|
||||
if script.upgrade_ops.is_empty():
|
||||
directives[:] = []
|
||||
logger.info('No changes in schema detected.')
|
||||
|
||||
engine = engine_from_config(config.get_section(config.config_ini_section),
|
||||
prefix='sqlalchemy.',
|
||||
poolclass=pool.NullPool)
|
||||
|
||||
connection = engine.connect()
|
||||
context.configure(connection=connection,
|
||||
target_metadata=target_metadata,
|
||||
process_revision_directives=process_revision_directives,
|
||||
**current_app.extensions['migrate'].configure_args)
|
||||
|
||||
try:
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
except Exception as exception:
|
||||
logger.error(exception)
|
||||
raise exception
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
|
@ -0,0 +1,24 @@
|
|||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = ${repr(up_revision)}
|
||||
down_revision = ${repr(down_revision)}
|
||||
branch_labels = ${repr(branch_labels)}
|
||||
depends_on = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade():
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade():
|
||||
${downgrades if downgrades else "pass"}
|
|
@ -0,0 +1,73 @@
|
|||
"""empty message
|
||||
|
||||
Revision ID: 27c0fff58193
|
||||
Revises:
|
||||
Create Date: 2019-02-02 16:06:49.372395
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '27c0fff58193'
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('user',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('type', sa.String(length=20), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('content',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('type', sa.String(length=20), nullable=True),
|
||||
sa.Column('data', sa.Text(convert_unicode=True), nullable=True),
|
||||
sa.Column('date_created', sa.DateTime(), nullable=True),
|
||||
sa.Column('date_modified', sa.DateTime(), nullable=True),
|
||||
sa.Column('author_id', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['author_id'], ['user.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('guest',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('username', sa.Unicode(length=64), nullable=True),
|
||||
sa.Column('last_post', sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['id'], ['user.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_guest_username'), 'guest', ['username'], unique=False)
|
||||
op.create_table('member',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('username', sa.Unicode(length=64), nullable=True),
|
||||
sa.Column('email', sa.String(length=120), nullable=True),
|
||||
sa.Column('password_hash', sa.String(length=255), nullable=True),
|
||||
sa.Column('xp_points', sa.Integer(), nullable=True),
|
||||
sa.Column('innovation_points', sa.Integer(), nullable=True),
|
||||
sa.Column('biography', sa.Text(convert_unicode=True), nullable=True),
|
||||
sa.Column('signature', sa.Text(convert_unicode=True), nullable=True),
|
||||
sa.Column('birthday', sa.Date(), nullable=True),
|
||||
sa.Column('register_date', sa.Date(), nullable=True),
|
||||
sa.Column('receive_newsletter', sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['id'], ['user.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_member_email'), 'member', ['email'], unique=True)
|
||||
op.create_index(op.f('ix_member_username'), 'member', ['username'], unique=True)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f('ix_member_username'), table_name='member')
|
||||
op.drop_index(op.f('ix_member_email'), table_name='member')
|
||||
op.drop_table('member')
|
||||
op.drop_index(op.f('ix_guest_username'), table_name='guest')
|
||||
op.drop_table('guest')
|
||||
op.drop_table('content')
|
||||
op.drop_table('user')
|
||||
# ### end Alembic commands ###
|
|
@ -0,0 +1,53 @@
|
|||
"""empty message
|
||||
|
||||
Revision ID: 29ca8250bd4a
|
||||
Revises: d2c96bebc596
|
||||
Create Date: 2019-02-03 14:45:18.339043
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '29ca8250bd4a'
|
||||
down_revision = 'd2c96bebc596'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('group_privilege',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('gid', sa.Integer(), nullable=True),
|
||||
sa.Column('priv', sa.String(length=64), nullable=True),
|
||||
sa.ForeignKeyConstraint(['gid'], ['group.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_group_privilege_gid'), 'group_privilege', ['gid'], unique=False)
|
||||
op.create_table('group_user',
|
||||
sa.Column('gid', sa.Integer(), nullable=True),
|
||||
sa.Column('uid', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['gid'], ['group.id'], ),
|
||||
sa.ForeignKeyConstraint(['uid'], ['user.id'], )
|
||||
)
|
||||
op.create_table('special_privilege',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('uid', sa.Integer(), nullable=True),
|
||||
sa.Column('priv', sa.String(length=64), nullable=True),
|
||||
sa.ForeignKeyConstraint(['uid'], ['user.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_special_privilege_uid'), 'special_privilege', ['uid'], unique=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f('ix_special_privilege_uid'), table_name='special_privilege')
|
||||
op.drop_table('special_privilege')
|
||||
op.drop_table('group_user')
|
||||
op.drop_index(op.f('ix_group_privilege_gid'), table_name='group_privilege')
|
||||
op.drop_table('group_privilege')
|
||||
# ### end Alembic commands ###
|
|
@ -0,0 +1,42 @@
|
|||
"""empty message
|
||||
|
||||
Revision ID: 53f5ab9da859
|
||||
Revises: 8b2cd63804b3
|
||||
Create Date: 2019-02-04 15:09:58.315410
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '53f5ab9da859'
|
||||
down_revision = '8b2cd63804b3'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('group', sa.Column('description', sa.UnicodeText(), nullable=True))
|
||||
op.drop_index('ix_group_privilege_gid', table_name='group_privilege')
|
||||
op.add_column('special_privilege', sa.Column('mid', sa.Integer(), nullable=True))
|
||||
op.create_index(op.f('ix_special_privilege_mid'), 'special_privilege', ['mid'], unique=False)
|
||||
op.drop_index('ix_special_privilege_uid', table_name='special_privilege')
|
||||
op.drop_constraint('special_privilege_uid_fkey', 'special_privilege', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'special_privilege', 'member', ['mid'], ['id'])
|
||||
op.drop_column('special_privilege', 'uid')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('special_privilege', sa.Column('uid', sa.INTEGER(), autoincrement=False, nullable=True))
|
||||
op.drop_constraint(None, 'special_privilege', type_='foreignkey')
|
||||
op.create_foreign_key('special_privilege_uid_fkey', 'special_privilege', 'user', ['uid'], ['id'])
|
||||
op.create_index('ix_special_privilege_uid', 'special_privilege', ['uid'], unique=False)
|
||||
op.drop_index(op.f('ix_special_privilege_mid'), table_name='special_privilege')
|
||||
op.drop_column('special_privilege', 'mid')
|
||||
op.create_index('ix_group_privilege_gid', 'group_privilege', ['gid'], unique=False)
|
||||
op.drop_column('group', 'description')
|
||||
# ### end Alembic commands ###
|
|
@ -0,0 +1,36 @@
|
|||
"""empty message
|
||||
|
||||
Revision ID: 7697154d0a4b
|
||||
Revises: 7dfd5e3aa1fb
|
||||
Create Date: 2019-02-02 20:54:59.683353
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '7697154d0a4b'
|
||||
down_revision = '7dfd5e3aa1fb'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('group',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.Unicode(length=50), nullable=True),
|
||||
sa.Column('css', sa.UnicodeText(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('name')
|
||||
)
|
||||
op.drop_column('guest', 'last_post')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('guest', sa.Column('last_post', postgresql.TIMESTAMP(), autoincrement=False, nullable=True))
|
||||
op.drop_table('group')
|
||||
# ### end Alembic commands ###
|
|
@ -0,0 +1,28 @@
|
|||
"""Fix a typo in a field name
|
||||
|
||||
Revision ID: 7dfd5e3aa1fb
|
||||
Revises: 27c0fff58193
|
||||
Create Date: 2019-02-02 16:13:13.229250
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '7dfd5e3aa1fb'
|
||||
down_revision = '27c0fff58193'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('guest', sa.Column('ip', sa.String(length=47), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('guest', 'ip')
|
||||
# ### end Alembic commands ###
|
|
@ -0,0 +1,40 @@
|
|||
"""empty message
|
||||
|
||||
Revision ID: 8b2cd63804b3
|
||||
Revises: 29ca8250bd4a
|
||||
Create Date: 2019-02-03 14:54:10.804975
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '8b2cd63804b3'
|
||||
down_revision = '29ca8250bd4a'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('group_member',
|
||||
sa.Column('gid', sa.Integer(), nullable=True),
|
||||
sa.Column('uid', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['gid'], ['group.id'], ),
|
||||
sa.ForeignKeyConstraint(['uid'], ['member.id'], )
|
||||
)
|
||||
op.drop_table('group_user')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('group_user',
|
||||
sa.Column('gid', sa.INTEGER(), autoincrement=False, nullable=True),
|
||||
sa.Column('uid', sa.INTEGER(), autoincrement=False, nullable=True),
|
||||
sa.ForeignKeyConstraint(['gid'], ['group.id'], name='group_user_gid_fkey'),
|
||||
sa.ForeignKeyConstraint(['uid'], ['user.id'], name='group_user_uid_fkey')
|
||||
)
|
||||
op.drop_table('group_member')
|
||||
# ### end Alembic commands ###
|
|
@ -0,0 +1,50 @@
|
|||
"""empty message
|
||||
|
||||
Revision ID: d2c96bebc596
|
||||
Revises: 7697154d0a4b
|
||||
Create Date: 2019-02-02 21:37:53.802259
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'd2c96bebc596'
|
||||
down_revision = '7697154d0a4b'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('member', sa.Column('bio', sa.UnicodeText(), nullable=True))
|
||||
op.add_column('member', sa.Column('innovation', sa.Integer(), nullable=True))
|
||||
op.add_column('member', sa.Column('name', sa.Unicode(length=32), nullable=True))
|
||||
op.add_column('member', sa.Column('newsletter', sa.Boolean(), nullable=True))
|
||||
op.add_column('member', sa.Column('xp', sa.Integer(), nullable=True))
|
||||
op.create_index(op.f('ix_member_name'), 'member', ['name'], unique=True)
|
||||
op.drop_index('ix_member_username', table_name='member')
|
||||
op.drop_column('member', 'xp_points')
|
||||
op.drop_column('member', 'receive_newsletter')
|
||||
op.drop_column('member', 'biography')
|
||||
op.drop_column('member', 'username')
|
||||
op.drop_column('member', 'innovation_points')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('member', sa.Column('innovation_points', sa.INTEGER(), autoincrement=False, nullable=True))
|
||||
op.add_column('member', sa.Column('username', sa.VARCHAR(length=64), autoincrement=False, nullable=True))
|
||||
op.add_column('member', sa.Column('biography', sa.TEXT(), autoincrement=False, nullable=True))
|
||||
op.add_column('member', sa.Column('receive_newsletter', sa.BOOLEAN(), autoincrement=False, nullable=True))
|
||||
op.add_column('member', sa.Column('xp_points', sa.INTEGER(), autoincrement=False, nullable=True))
|
||||
op.create_index('ix_member_username', 'member', ['username'], unique=True)
|
||||
op.drop_index(op.f('ix_member_name'), table_name='member')
|
||||
op.drop_column('member', 'xp')
|
||||
op.drop_column('member', 'newsletter')
|
||||
op.drop_column('member', 'name')
|
||||
op.drop_column('member', 'innovation')
|
||||
op.drop_column('member', 'bio')
|
||||
# ### end Alembic commands ###
|
|
@ -1,3 +1,4 @@
|
|||
#!/usr/bin/env sh
|
||||
flask db init
|
||||
flask db migrate -m "initialisation"
|
||||
flask db upgrade
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
#!/usr/bin/env sh
|
||||
flask db migrate -m $1
|
||||
flask db upgrade
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
#!/usr/bin/env sh
|
||||
FLASK_DEBUG=1
|
||||
flask run
|
||||
|
|
Loading…
Reference in New Issue