Browse Source

Merge branch 'trophies' of devs/PCv5 into master

master
Darks 5 months ago
parent
commit
1bdad7c9e4

+ 1
- 1
app/__init__.py View File

@@ -16,5 +16,5 @@ login.login_message = "Veuillez vous authentifier avant de continuer."
from app import models # IDK why this is here, but it works
from app.routes import index, search, users # To load routes at initialization
from app.routes.account import login, account
from app.routes.admin import index, groups, account
from app.routes.admin import index, groups, account, trophies
from app.utils import pluralize # To use pluralize into the templates

+ 5
- 14
app/forms/account.py View File

@@ -36,20 +36,11 @@ class DeleteAccountForm(FlaskForm):


class AdminUpdateAccountForm(FlaskForm):
username = StringField('Pseudonyme',
validators=[Optional(), vd.name_valid])
avatar = FileField('Avatar',
validators=[Optional(), vd.avatar])
email = StringField('Adresse email',
validators=[Optional(), Email(), vd.email])
email_validate = BooleanField("""Envoyer un email de validation à la
nouvelle adresse""",
description="""Si décoché, l'utilisateur devra demander explicitement
un email de validation, ou faire valider son adresse email par un
administrateur.""")
password = PasswordField('Mot de passe',
description="L'ancien mot de passe ne pourra pas être récupéré !",
validators=[Optional(), vd.password])
username = StringField('Pseudonyme', validators=[Optional(), vd.name_valid])
avatar = FileField('Avatar', validators=[Optional(), vd.avatar])
email = StringField('Adresse email', validators=[Optional(), Email(), vd.email])
email_validate = BooleanField("Envoyer un email de validation à la nouvelle adresse", description="Si décoché, l'utilisateur devra demander explicitement un email de validation, ou faire valider son adresse email par un administrateur.")
password = PasswordField('Mot de passe', description="L'ancien mot de passe ne pourra pas être récupéré !", validators=[Optional(), vd.password])
xp = DecimalField('XP', validators=[Optional()])
birthday = DateField('Anniversaire', validators=[Optional()])
signature = TextAreaField('Signature', validators=[Optional()])

+ 16
- 0
app/forms/trophies.py View File

@@ -0,0 +1,16 @@
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, BooleanField
from wtforms.validators import DataRequired, Optional
from flask_wtf.file import FileField # Cuz' wtforms' FileField is shitty


class TrophyForm(FlaskForm):
name = StringField('Nom', validators=[DataRequired()])
icon = FileField('Icone')
title = StringField('Titre', description='Titre affiché dans le cas échéant. Laisser vide pour un simple trophée.', validators=[Optional()])
css = StringField('CSS', description='CSS appliqué au titre, le cas échéant.')
submit = SubmitField('Envoyer')

class DeleteTrophyForm(FlaskForm):
delete = BooleanField('Confirmer la suppression', validators=[DataRequired()], description='Attention, cette opération est irréversible !')
submit = SubmitField('Supprimer le trophée')

+ 39
- 0
app/models/trophies.py View File

@@ -0,0 +1,39 @@
from app import db


class Trophy(db.Model):
__tablename__ = 'trophy'
id = db.Column(db.Integer, primary_key=True)
# Trophy type (polymorphic discriminator)
type = db.Column(db.String(20))
__mapper_args__ = {
'polymorphic_identity': __tablename__,
'polymorphic_on': type
}
# Standalone properties
name = db.Column(db.Unicode(64), index=True)

owners = db.relationship('Member', secondary=lambda: TrophyMember,
back_populates='trophies')

def __init__(self, name):
self.name = name

class Title(Trophy):
__tablename__ = 'title'
__mapper_args__ = {'polymorphic_identity': __tablename__}

id = db.Column(db.Integer, db.ForeignKey('trophy.id'), primary_key=True)
title = db.Column(db.Unicode(64))
css = db.Column(db.Text(convert_unicode=True))

def __init__(self, name, title, css):
self.name = name
self.title = title
self.css = css


# Many-to-many relation for users earning trophies
TrophyMember = db.Table('trophy_member', db.Model.metadata,
db.Column('tid', db.Integer, db.ForeignKey('trophy.id')),
db.Column('uid', db.Integer, db.ForeignKey('member.id')))

+ 3
- 1
app/models/users.py View File

@@ -4,6 +4,7 @@ from flask_login import UserMixin
from app.models.contents import Content
from app.models.privs import SpecialPrivilege, Group, GroupMember, \
GroupPrivilege
from app.models.trophies import Trophy, TrophyMember
import app.utils.unicode_names as unicode_names
from config import V5Config

@@ -90,7 +91,8 @@ class Member(User, db.Model):
newsletter = db.Column(db.Boolean, default=False)

# Relations
# trophies = db.relationship('Trophy', back_populates='member')
trophies = db.relationship('Trophy', secondary=TrophyMember,
back_populates='owners')
# tests = db.relationship('Test', back_populates='author')

def __init__(self, name, email, password):

+ 4
- 4
app/routes/admin/account.py View File

@@ -6,8 +6,8 @@ from app.utils.render import render
from app import app, db


@app.route('/admin/edit-account/<user_id>', methods=['GET', 'POST'])
@priv_required('edit-account')
@app.route('/admin/account/<user_id>/edit', methods=['GET', 'POST'])
@priv_required('access-admin-panel', 'edit-account')
def adm_edit_account(user_id):
user = Member.query.filter_by(id=user_id).first_or_404()

@@ -42,8 +42,8 @@ def adm_edit_account(user_id):
return render('admin/edit_account.html', user=user, form=form)


@app.route('/admin/edit-account/<user_id>/delete', methods=['GET', 'POST'])
@priv_required('delete-account')
@app.route('/admin/account/<user_id>/delete', methods=['GET', 'POST'])
@priv_required('access-admin-panel', 'delete-account')
def adm_delete_account(user_id):
user = Member.query.filter_by(id=user_id).first_or_404()


+ 1
- 1
app/routes/admin/index.py View File

@@ -3,7 +3,7 @@ from app.utils.render import render
from app import app


@app.route('/admin', methods=['GET', 'POST'])
@app.route('/admin', methods=['GET'])
@priv_required('access-admin-panel')
def adm():
return render('admin/index.html')

+ 71
- 0
app/routes/admin/trophies.py View File

@@ -0,0 +1,71 @@
from flask import request, flash, redirect, url_for
from app.utils.priv_required import priv_required
from app.models.trophies import Trophy, Title
from app.forms.trophies import TrophyForm, DeleteTrophyForm
from app.utils.render import render
from app import app, db


@app.route('/admin/trophies', methods=['GET', 'POST'])
@priv_required('access-admin-panel', 'edit-trophies')
def adm_trophies():
form = TrophyForm()
if request.method == "POST":
if form.validate_on_submit():
is_title = form.title.data != ""
if is_title:
trophy = Title(form.name.data, form.title.data, form.css.data)
else:
trophy = Trophy(form.name.data)
db.session.add(trophy)
db.session.commit()
flash(f'Nouveau {["trophée", "titre"][is_title]} ajouté', 'ok')
else:
flash('Erreur lors de la création du trophée', 'error')

trophies = Trophy.query.all()
return render('admin/trophies.html', trophies=trophies,
form=form)


@app.route('/admin/trophies/<trophy_id>/edit', methods=['GET', 'POST'])
@priv_required('access-admin-panel', 'edit-trophies')
def adm_edit_trophy(trophy_id):
trophy = Trophy.query.filter_by(id=trophy_id).first_or_404()

form = TrophyForm()
if request.method == "POST":
if form.validate_on_submit():
is_title = form.title.data != ""
if is_title:
trophy.name = form.name.data
trophy.title = form.title.data
trophy.css = form.css.data
else:
trophy.name = form.name.data
db.session.merge(trophy)
db.session.commit()
flash(f'{["Trophée", "Titre"][is_title]} modifié', 'ok')
return redirect(url_for('adm_trophies'))
else:
flash('Erreur lors de la création du trophée', 'error')
return render('admin/edit_trophy.html', trophy=trophy, form=form)


@app.route('/admin/trophies/<trophy_id>/delete', methods=['GET', 'POST'])
@priv_required('access-admin-panel', 'edit-trophies')
def adm_delete_trophy(trophy_id):
trophy = Trophy.query.filter_by(id=trophy_id).first_or_404()

# TODO: Add an overview of what will be deleted.
del_form = DeleteTrophyForm()
if request.method == "POST":
if del_form.validate_on_submit():
db.session.delete(trophy)
db.session.commit()
flash('Trophée supprimé', 'ok')
return redirect(url_for('adm_trophies'))
else:
flash('Erreur lors de la suppression du trophée', 'error')
del_form.delete.data = False # Force to tick to delete the account
return render('admin/delete_trophy.html', trophy=trophy, del_form=del_form)

+ 28
- 0
app/templates/admin/delete_trophy.html View File

@@ -0,0 +1,28 @@
{% extends "base/base.html" %}

{% block title %}
<a href="{{ url_for('adm') }}">Panneau d'administration</a> » <a href={{ url_for('adm_trophies') }}>Titres et trophées</a> » <h1>Suppression du trophée '{{ trophy.name }}'</h1>
{% endblock %}

{% block content %}
<section class="form">
<h2>Confirmer la suppression du trophée</h2>
<p>Le trophée '{{ trophy.name }}' que vous allez supprimer est lié à :</p>
<ul>
<li>{{ trophy.owners | length }} membre{{ trophy.owners|length|pluralize }}</li>
</ul>

<form action="{{ url_for('adm_delete_trophy', trophy_id=trophy.id) }}" method=post>
{{ del_form.hidden_tag() }}
<div>
{{ del_form.delete.label }}
{{ del_form.delete(checked=False) }}
<div style="font-size: 80%; color: gray">{{ del_form.delete.description }}</div>
{% for error in del_form.delete.errors %}
<span class=msgerror>{{ error }}</span>
{% endfor %}
</div>
<div>{{ del_form.submit(class_="bg-red") }}</div>
</form>
</section>
{% endblock %}

+ 38
- 0
app/templates/admin/edit_trophy.html View File

@@ -0,0 +1,38 @@
{% extends "base/base.html" %}

{% block title %}
<a href="{{ url_for('adm') }}">Panneau d'administration</a> » <a href={{ url_for('adm_trophies') }}>Titres et trophées</a> » <h1>Édition du trophée '{{ trophy.name }}'</h1>
{% endblock %}

{% block content %}
<section class="form">
<form action="{{ url_for('adm_edit_trophy', trophy_id=trophy.id) }}" method="post" enctype="multipart/form-data">
{{ form.hidden_tag() }}
<h2>Éditer le trophée</h2>

<div>
{{ form.name.label }}
{{ form.name(value=trophy.name) }}
{% for error in form.name.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
<div>
{{ form.title.label }}
{{ form.title(value=trophy.title) }}
<div class=desc>{{ form.title.description }}</div>
{% for error in form.title.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
<div>
{{ form.css.label }}
{{ form.css(value=trophy.css) }}
<div class=desc>{{ form.css.description }}</div>
{% for error in form.css.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
<div>{{ form.submit(class_="bg-green") }}</div>
</section>
{% endblock %}

+ 1
- 0
app/templates/admin/index.html View File

@@ -9,6 +9,7 @@
<p>Pages générales du panneau d'administration :</p>
<ul>
<li><a href="{{ url_for('adm_groups') }}">Groupes et privilèges</a></li>
<li><a href="{{ url_for('adm_trophies') }}">Titres et trophées</a></li>
</ul>
</section>
{% endblock %}

+ 59
- 0
app/templates/admin/trophies.html View File

@@ -0,0 +1,59 @@
{% extends "base/base.html" %}

{% block title %}
<a href="{{ url_for('adm') }}">Panneau d'administration</a> » <h1>Titres et trophées</h1>
{% endblock %}

{% block content %}
<section>
<p>Cette page présente une vue d'ensemble des titres et trophées.</p>

<h2>Titres et trophées</h2>

<table style="width:90%; margin: auto;">
<tr><th>id</th><th>Icône</th><th>Nom</th><th>Titre</th>
<th>Style</th><th>Modifier</th><th>Supprimer</th></tr>

{% for trophy in trophies %}
<tr><td>{{ trophy.id }}</td>
<td><img src="{{ url_for('static', filename='images/account-circle.svg') }}" alt="{{ trophy.name }}"></td>
<td>{{ trophy.name }}</td>
<td style="{{ trophy.css }}">{{ trophy.title }}</td>
<td>{{ trophy.css }}</td>
<td><a href="{{ url_for('adm_edit_trophy', trophy_id=trophy.id) }}">Modifier</a></td>
<td><a href="{{ url_for('adm_delete_trophy', trophy_id=trophy.id) }}">Supprimer</a></td>
</tr>
{% endfor %}
</table>
</section>

<section class="form">
<form action="{{ url_for('adm_trophies') }}" method="post" enctype="multipart/form-data">
{{ form.hidden_tag() }}
<h2>Nouveau trophée</h2>
<div>
{{ form.name.label }}
{{ form.name }}
{% for error in form.name.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
<div>
{{ form.title.label }}
{{ form.title }}
<div class=desc>{{ form.title.description }}</div>
{% for error in form.title.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
<div>
{{ form.css.label }}
{{ form.css }}
<div class=desc>{{ form.css.description }}</div>
{% for error in form.css.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
<div>{{ form.submit(class_="bg-green") }}</div>
</section>
{% endblock %}

+ 19
- 1
app/utils/validators.py View File

@@ -1,6 +1,6 @@
from flask_login import current_user
from wtforms.validators import ValidationError
from app.models.users import User, Member
from app.models.users import Member
from app.utils.valid_name import valid_name
from app.utils.unicode_names import normalize
from config import V5Config
@@ -65,3 +65,21 @@ def old_password(form, field):
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é.')


def id_exists(object):
"""Check if an id exists in a table"""
def _id_exists(form, id):
try:
id = int(id.data)
except ValueError:
raise ValidationError('L\'id n\'est pas un entier valide')
r = object.query.filter_by(id=id)
if not r:
raise ValidationError('L\'id n\'existe pas dans la BDD')
return _id_exists


def css(form, css):
"""Check if input is valid and sane CSS"""
pass

+ 1
- 0
config.py View File

@@ -7,6 +7,7 @@ class Config(object):
'postgresql+psycopg2://' + os.environ.get('USER') + ':@/pcv5'
SQLALCHEMY_TRACK_MODIFICATIONS = False
UPLOAD_FOLDER = './app/static/avatars'
LOGIN_DISABLED = True


class V5Config(object):

+ 30
- 0
migrations/versions/6ae59d74cf54_update_des_titres_et_trophées.py View File

@@ -0,0 +1,30 @@
"""Update des titres et trophées

Revision ID: 6ae59d74cf54
Revises: c961d7b7a7ea
Create Date: 2019-06-06 23:34:53.521239

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '6ae59d74cf54'
down_revision = 'c961d7b7a7ea'
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('title', sa.Column('css', sa.Text(_expect_unicode=True), nullable=True))
op.create_index(op.f('ix_trophy_name'), 'trophy', ['name'], unique=False)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_trophy_name'), table_name='trophy')
op.drop_column('title', 'css')
# ### end Alembic commands ###

+ 47
- 0
migrations/versions/c961d7b7a7ea_ajout_des_titres_et_trophées.py View File

@@ -0,0 +1,47 @@
"""Ajout des titres et trophées

Revision ID: c961d7b7a7ea
Revises: a6e89f3510d9
Create Date: 2019-06-06 15:18:09.893001

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = 'c961d7b7a7ea'
down_revision = 'a6e89f3510d9'
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('trophy',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('type', sa.String(length=20), nullable=True),
sa.Column('name', sa.Text(_expect_unicode=True), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('title',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('title', sa.Text(_expect_unicode=True), nullable=True),
sa.ForeignKeyConstraint(['id'], ['trophy.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('trophy_member',
sa.Column('tid', sa.Integer(), nullable=True),
sa.Column('uid', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['tid'], ['trophy.id'], ),
sa.ForeignKeyConstraint(['uid'], ['member.id'], )
)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('trophy_member')
op.drop_table('title')
op.drop_table('trophy')
# ### end Alembic commands ###

Loading…
Cancel
Save