admin: start a panel with a database filler
This commit is contained in:
parent
5777ab1e78
commit
21e0679557
|
@ -14,4 +14,4 @@ login.login_view = 'login'
|
|||
login.login_message = "Veuillez vous authentifier avant de continuer."
|
||||
|
||||
from app import models
|
||||
from app.routes import index, login, search, account
|
||||
from app.routes import index, login, search, account, admin
|
||||
|
|
|
@ -15,13 +15,17 @@ class SpecialPrivilege(db.Model):
|
|||
__tablename__ = 'special_privilege'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
# User that is granted the privilege
|
||||
uid = db.Column(db.Integer, db.ForeignKey('user.id'), index=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 user #{uid}>'
|
||||
return f'<Privilege "{self.priv}" of member #{mid}>'
|
||||
|
||||
# Group: User group, corresponds to a community role and a set of privileges
|
||||
class Group(db.Model):
|
||||
|
@ -34,10 +38,17 @@ class Group(db.Model):
|
|||
# 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}">'
|
||||
|
||||
|
@ -46,12 +57,14 @@ 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')))
|
||||
|
||||
# GroupPrivilege: Privilege granted to all users in a group
|
||||
# Meny-to-many relationship for privileges granted to groups
|
||||
class GroupPrivilege(db.Model):
|
||||
__tablename__ = 'group_privilege'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
# Group that is granted the privilege
|
||||
gid = db.Column(db.Integer, db.ForeignKey('group.id'), index=True)
|
||||
# Privilege name
|
||||
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
|
||||
|
|
|
@ -2,7 +2,9 @@ from datetime import date, datetime
|
|||
from app import db
|
||||
from flask_login import UserMixin
|
||||
from app.models.contents import Content
|
||||
from app.models.privs import Group, GroupMember
|
||||
from app.models.privs import SpecialPrivilege, Group, GroupMember, \
|
||||
GroupPrivilege
|
||||
from config import V5Config
|
||||
|
||||
import werkzeug.security
|
||||
import app
|
||||
|
@ -29,6 +31,32 @@ class User(UserMixin, db.Model):
|
|||
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'
|
||||
|
@ -77,33 +105,9 @@ class Member(User, db.Model):
|
|||
# trophies = db.relationship('Trophy', back_populates='member')
|
||||
# tests = db.relationship('Test', back_populates='author')
|
||||
|
||||
@staticmethod
|
||||
def valid_name(name):
|
||||
"""
|
||||
Checks whether a string is a valid member name. The criteria are:
|
||||
1. No whitespace-class character
|
||||
2. At least one letter
|
||||
3. No longer than 32 characters
|
||||
|
||||
Possibily other intresting criteria:
|
||||
4. Unicode restriction
|
||||
"""
|
||||
|
||||
if type(name) != str or len(name) > 32:
|
||||
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
|
||||
|
||||
def __init__(self, name, email, password):
|
||||
"""Register a new user."""
|
||||
if not Member.valid_name(name):
|
||||
if not User.valid_name(name):
|
||||
raise Exception(f'{name} is not a valid user name')
|
||||
|
||||
self.name = name
|
||||
|
@ -116,7 +120,16 @@ class Member(User, db.Model):
|
|||
self.signature = ""
|
||||
self.birthday = None
|
||||
|
||||
def update(self, data):
|
||||
def priv(self, priv):
|
||||
"""Check whether the member has the specified privilege."""
|
||||
if SpecialPrivilege.filter(uif=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:
|
||||
|
@ -138,7 +151,7 @@ class Member(User, db.Model):
|
|||
data = { key: data[key] for key in data if data[key] is not None }
|
||||
|
||||
if "name" in data:
|
||||
if not Member.valid_name(data["name"]):
|
||||
if not User.valid_name(data["name"]):
|
||||
raise Exception(f'{data["name"]} is not a valid user name')
|
||||
self.name = data["name"]
|
||||
|
||||
|
|
|
@ -11,14 +11,14 @@ def account():
|
|||
form = UpdateAccountForm()
|
||||
if request.method == "POST":
|
||||
if form.validate_on_submit():
|
||||
current_user.update({
|
||||
"email": form.email.data,
|
||||
"password": form.password.data,
|
||||
"signature": form.signature.data,
|
||||
"bio": form.biography.data,
|
||||
"birthday": form.birthday.data,
|
||||
"newsletter": form.newsletter.data
|
||||
})
|
||||
current_user.update(
|
||||
email= form.email.data,
|
||||
password= form.password.data,
|
||||
signature= form.signature.data,
|
||||
bio= form.biography.data,
|
||||
birthday= form.birthday.data,
|
||||
newsletter= form.newsletter.data
|
||||
)
|
||||
db.session.add(current_user)
|
||||
db.session.commit()
|
||||
flash('Modifications effectuées', 'ok')
|
||||
|
@ -43,4 +43,4 @@ def register():
|
|||
def validation():
|
||||
if current_user.is_authenticated :
|
||||
return redirect(url_for('index'))
|
||||
return render('validation.html')
|
||||
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 import app, db
|
||||
|
||||
@app.route('/admin', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
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,42 @@
|
|||
{% extends "base/container.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<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>{{ user.name }}</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>
|
||||
|
||||
{% endblock %}
|
|
@ -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
|
|
@ -4,7 +4,7 @@ def name(form, name):
|
|||
member = Member.query.filter_by(name=name.data).first()
|
||||
if member is not None:
|
||||
raise ValidationError('Pseudo indisponible.')
|
||||
if not Member.valid_name(name.data):
|
||||
if not User.valid_name(name.data):
|
||||
raise ValidationError("Nom d'utilisateur invalide.")
|
||||
|
||||
def email(form, email):
|
||||
|
@ -19,4 +19,4 @@ def password(form, password):
|
|||
|
||||
def authentication(form, old_password):
|
||||
if not current_user.check_password(old_password.data):
|
||||
raise ValidationError('Mot de passe erroné')
|
||||
raise ValidationError('Mot de passe erroné')
|
||||
|
|
|
@ -33,5 +33,6 @@ 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...
|
||||
|
|
|
@ -9,3 +9,5 @@ class Config(object):
|
|||
class V5Config(object):
|
||||
# Length allocated to privilege names (slugs)
|
||||
PRIVS_MAXLEN = 64
|
||||
# Forbidden user names
|
||||
FORBIDDEN_USERNAMES = [ "admin", "root", "webmaster", "contact" ]
|
||||
|
|
|
@ -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 ###
|
Loading…
Reference in New Issue