Browse Source

admin: start a panel with a database filler

master
lephe 7 months ago
parent
commit
21e0679557

+ 1
- 1
app/__init__.py View File

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

+ 20
- 7
app/models/privs.py View File

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

+ 41
- 28
app/models/users.py View File

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


+ 9
- 9
app/routes/account.py View File

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

+ 58
- 0
app/routes/admin.py View File

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

+ 42
- 0
app/templates/admin.html View File

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

+ 17
- 0
app/utils/decorators.py View File

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

+ 2
- 2
app/utils/validators.py View File

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

+ 1
- 0
assets/privs.txt View File

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

+ 2
- 0
config.py View File

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

+ 42
- 0
migrations/versions/53f5ab9da859_.py View File

@@ -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…
Cancel
Save