Modifications on trophies and titles #10 (and more)

- remove `title` attribute
- do the migration of db
- add initialization routine in `master.py`
- add default trophies and titles in `data/trophies.yaml`
- add `add_trophy` method in `Member` class
- add `update_trophies` method in `Member` class
- add form in admin panel to give a trophy to a member
- same to remove a trophy
- change `if request.method == "POST"` to `if form.submit.data`
This commit is contained in:
Darks 2019-06-11 00:15:23 +02:00
parent a29e0c4411
commit 1d638689c6
Signed by untrusted user: Darks
GPG Key ID: F61F10FA138E797C
17 changed files with 332 additions and 25 deletions

View File

@ -18,3 +18,4 @@ 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, trophies
from app.utils import pluralize # To use pluralize into the templates
from app.utils import is_title

93
app/data/trophies.yaml Normal file
View File

@ -0,0 +1,93 @@
-
name: Membre de CreativeCalc
is_title: True
-
name: Membre d'honneur
is_title: True
-
name: Grand Manitou
is_title: True
-
name: Gourou
is_title: True
-
name: Grand Maitre des traits d'esprit
is_title: True
-
name: Beau parleur
is_title: False
-
name: Jeune écrivain
is_title: False
-
name: Romancier émérite
is_title: True
-
name: Apprenti instructeur
is_title: False
-
name: Pédagogue averti
is_title: False
-
name: Encyclopédie vivante
is_title: True
-
name: Nouveau
is_title: False
-
name: Aficionado
is_title: False
-
name: Veni, vidi, casii
is_title: False
-
name: Papy Casio
is_title: True
-
name: Programmeur du dimanche
is_title: False
-
name: Codeur invétéré
is_title: False
-
name: Je code donc je suis
is_title: True
-
name: Testeur
is_title: False
-
name: Examinateur
is_title: False
-
name: Hard tester
is_title: True
-
name: Participant avéré
is_title: False
-
name: Concourant encore
is_title: False
-
name: Concurrent de lextrême
is_title: True
-
name: Designer en herbe
is_title: False
-
name: Graphiste expérimenté
is_title: False
-
name: Roi du pixel
is_title: True
-
name: Actif
is_title: False
-
name: Artiste
is_title: False
-
name: Maître du code
is_title: True
-
name: Bourreau des cœurs
is_title: True

View File

@ -1,8 +1,9 @@
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, TextAreaField, SubmitField, DecimalField
from wtforms import StringField, PasswordField, BooleanField, TextAreaField, SubmitField, DecimalField, SelectField
from wtforms.fields.html5 import DateField
from wtforms.validators import DataRequired, Optional, Email, EqualTo
from wtforms.validators import DataRequired, InputRequired, Optional, Email, EqualTo
from flask_wtf.file import FileField # Cuz' wtforms' FileField is shitty
from app.models.trophies import Trophy
import app.utils.validators as vd
@ -49,6 +50,16 @@ class AdminUpdateAccountForm(FlaskForm):
submit = SubmitField('Mettre à jour')
class AdminAccountAddTrophyForm(FlaskForm):
trophy = SelectField('Trophée', validators=[InputRequired()], coerce=int)
#trophy = SelectField('Trophée', validators=[DataRequired()])
submit = SubmitField('Ajouter')
class AdminAccountDelTrophyForm(AdminAccountAddTrophyForm):
submit = SubmitField('Supprimer')
class AdminDeleteAccountForm(FlaskForm):
delete = BooleanField('Confirmer la suppression', validators=[DataRequired()], description='Attention, cette opération est irréversible!')
submit = SubmitField('Supprimer le compte')

View File

@ -7,7 +7,7 @@ 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()])
title = BooleanField('Titre', description='Un titre peut être affiché en dessous du pseudo.', validators=[Optional()])
css = StringField('CSS', description='CSS appliqué au titre, le cas échéant.')
submit = SubmitField('Envoyer')

View File

@ -27,12 +27,10 @@ class Title(Trophy):
__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.UnicodeText)
def __init__(self, name, title, css):
def __init__(self, name, css):
self.name = name
self.title = title
self.css = css

View File

@ -1,5 +1,6 @@
from datetime import date
from app import db
from flask import flash
from flask_login import UserMixin
from app.models.contents import Content
from app.models.privs import SpecialPrivilege, Group, GroupMember, \
@ -203,6 +204,75 @@ class Member(User, db.Model):
return werkzeug.security.check_password_hash(self.password_hash,
password)
def add_trophy(self, t):
"""
Add a trophy to the current user. heck whether the request sender has the right
to do this!
"""
if type(t) == str:
t = Trophy.query.filter_by(name=name).first()
if t not in self.trophies:
self.trophies.append(t)
db.session.merge(self)
db.session.commit()
# TODO: implement the notification system
# self.notify(f"Vous venez de débloquer le trophée '{name}'")
def del_trophy(self, t):
"""
Add a trophy to the current user. heck whether the request sender has the right
to do this!
"""
if type(t) == str:
t = Trophy.query.filter_by(name=name).first()
if t in self.trophies:
self.trophies.remove(t)
db.session.merge(self)
db.session.commit()
def update_trophies(self, context=None):
"""
Auto-update trophies for the current user. Please use one of the
following contexts when possible:
- new-post
- new-program
- new-tutorial
- new-test
- new-event-participation
- new-picture
- on-program-reward
- on-login
- on-profile-update
"""
if context == "new-post" or context is None:
pass
if context == "new-program" or context is None:
pass
if context == "new-tutorial" or context is None:
pass
if context == "new-test" or context is None:
pass
if context == "new-event-participation" or context is None:
pass
if context == "new-picture" or context is None:
pass
if context == "on-program-reward" or context is None:
pass
if context == "on-login" or context is None:
# Seniority-based trophies
age = date.today() - self.register_date
if age.days > 30:
self.add_trophy("Nouveau")
if age.days > 365.25:
self.add_trophy("Aficionado")
if age.days > 365.25 * 2:
self.add_trophy("Veni, vidi, casii")
if age.days > 365.25 * 5:
self.add_trophy("Papy Casio")
if context == "on-profile-update" or context is None:
pass
def __repr__(self):
return f'<Member: {self.name}>'

View File

@ -10,7 +10,7 @@ from app.utils.render import render
@login_required
def edit_account():
form = UpdateAccountForm()
if request.method == "POST":
if form.submit.data:
if form.validate_on_submit():
if form.avatar.data:
f = form.avatar.data
@ -36,7 +36,7 @@ def edit_account():
@login_required
def delete_account():
del_form = DeleteAccountForm()
if request.method == "POST":
if del_form.submit.data:
if del_form.validate_on_submit():
db.session.delete(current_user)
logout_user()

View File

@ -18,8 +18,7 @@ def login():
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')
member.update_trophies("on-login")
if request.args.get('next'):
return redirect(request.args.get('next'))
if request.referrer:

View File

@ -1,7 +1,9 @@
from flask import request, flash, redirect, url_for
from flask import flash, redirect, url_for
from app.utils.priv_required import priv_required
from app.models.users import Member
from app.forms.account import AdminUpdateAccountForm, AdminDeleteAccountForm
from app.models.trophies import Trophy
from app.forms.account import AdminUpdateAccountForm, AdminDeleteAccountForm, \
AdminAccountAddTrophyForm, AdminAccountDelTrophyForm
from app.utils.render import render
from app import app, db
@ -11,8 +13,14 @@ from app import app, db
def adm_edit_account(user_id):
user = Member.query.filter_by(id=user_id).first_or_404()
form = AdminUpdateAccountForm()
if request.method == "POST":
form = AdminUpdateAccountForm(prefix="user")
addtrophy_form = AdminAccountAddTrophyForm(prefix="addtrophy")
addtrophy_form.trophy.choices = [(t.id, t.name) for t in Trophy.query.all()]
deltrophy_form = AdminAccountDelTrophyForm(prefix="deltrophy")
deltrophy_form.trophy.choices = [(t.id, t.name) for t in user.trophies]
if form.submit.data:
if form.validate_on_submit():
if form.avatar.data:
f = form.avatar.data
@ -39,7 +47,26 @@ def adm_edit_account(user_id):
else:
flash('Erreur lors de la modification', 'error')
return render('admin/edit_account.html', user=user, form=form)
if addtrophy_form.submit.data:
if addtrophy_form.validate_on_submit():
trophy = Trophy.query.get(addtrophy_form.trophy.data)
if trophy is not None:
user.add_trophy(trophy)
flash('Trophée ajouté', 'ok')
else:
flash("Erreur lors de l'ajout du trophée", 'error')
if deltrophy_form.submit.data:
if deltrophy_form.validate_on_submit():
trophy = Trophy.query.get(deltrophy_form.trophy.data)
if trophy is not None:
user.del_trophy(trophy)
flash('Trophée retiré', 'ok')
else:
flash("Erreur lors du retrait du trophée", 'error')
return render('admin/edit_account.html', user=user, form=form,
addtrophy_form=addtrophy_form, deltrophy_form=deltrophy_form)
@app.route('/admin/account/<user_id>/delete', methods=['GET', 'POST'])
@ -55,7 +82,7 @@ def adm_delete_account(user_id):
# * How many PMs will be deleted (can't unassign PMs)
# * etc.
del_form = AdminDeleteAccountForm()
if request.method == "POST":
if del_form.submit.data:
if del_form.validate_on_submit():
user.delete()
flash('Compte supprimé', 'ok')

View File

@ -12,9 +12,9 @@ def adm_trophies():
form = TrophyForm()
if request.method == "POST":
if form.validate_on_submit():
is_title = form.title.data != ""
is_title = form.title.data
if is_title:
trophy = Title(form.name.data, form.title.data, form.css.data)
trophy = Title(form.name.data, form.css.data)
else:
trophy = Trophy(form.name.data)
db.session.add(trophy)

View File

@ -91,6 +91,32 @@
<div>{{ form.submit(class_="bg-green") }}</div>
</form>
<form action="{{ url_for('adm_edit_account', user_id=user.id) }}" method="post">
{{ addtrophy_form.hidden_tag() }}
<h2>Accorder un trophée</h2>
<div>
{{ addtrophy_form.trophy.label }}
{{ addtrophy_form.trophy }}
{% for error in addtrophy_form.trophy.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
<div>{{ addtrophy_form.submit(class_="bg-green") }}</div>
</form>
<form action="{{ url_for('adm_edit_account', user_id=user.id) }}" method="post">
{{ deltrophy_form.hidden_tag() }}
<h2>Retirer un trophée</h2>
<div>
{{ deltrophy_form.trophy.label }}
{{ deltrophy_form.trophy }}
{% for error in deltrophy_form.trophy.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
<div>{{ deltrophy_form.submit(class_="bg-red") }}</div>
</form>
<h2 style="margin-top:30px;">Supprimer le compte</h2>
<a href="{{ url_for('adm_delete_account', user_id=user.id) }}" class="button bg-red">Supprimer le compte</a>

View File

@ -19,16 +19,16 @@
</div>
<div>
{{ form.title.label }}
{{ form.title(value=trophy.title) }}
<div class=desc>{{ form.title.description }}</div>
{{ 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 }}
<div class=desc>{{ form.css.description }}</div>
{{ form.css(value=trophy.css) }}
<div class=desc>{{ form.css.description }}</div>
{% for error in form.css.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}

View File

@ -17,8 +17,8 @@
{% 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 style="{{ trophy.css }}">{{ trophy.name }}</td>
<td>{{ trophy | is_title }}</td>
<td><code>{{ trophy.css }}</code></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>
@ -40,15 +40,15 @@
</div>
<div>
{{ form.title.label }}
<div class=desc>{{ form.title.description }}</div>
{{ 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 }}
<div class=desc>{{ form.css.description }}</div>
<div class=desc>{{ form.css.description }}</div>
{{ form.css }}
{% for error in form.css.errors %}
<span class="msgerror">{{ error }}</span>

View File

@ -8,5 +8,22 @@
{% block content %}
<section>
{{ widget_member.profile(member) }}
{% if current_user.is_authenticated and current_user.priv('access-admin-panel') %}
<div><a href="{{ url_for('adm_edit_account', user_id=member.id) }}">Modifier</a></div>
{% endif %}
<h2>Trophées</h2>
<div>
{% if member.trophies %}
<ul>
{% for t in member.trophies %}
<li>{{ t.name }}</li>
{% endfor %}
</ul>
{% else %}
Aucun trophée.
{% endif %}
</div>
</section>
{% endblock %}

13
app/utils/is_title.py Normal file
View File

@ -0,0 +1,13 @@
from app import app
from app.models.trophies import Title
@app.template_filter('is_title')
def is_title(object):
"""
Check if an object is a title
"""
if type(object) == Title:
return "Oui"
else:
return "Non"

View File

@ -3,6 +3,7 @@
from app import app, db
from app.models.users import Member, Group, GroupPrivilege
from app.models.privs import SpecialPrivilege
from app.models.trophies import Trophy, Title
import os
import sys
import yaml
@ -24,6 +25,8 @@ the database.
(robot)
Type 'add-group <member> #<group-id>' to add a new member to a group.
Type 'reset-trophies' to reset trophies and titles.
"""
def members():
@ -83,6 +86,25 @@ def reset_groups_and_privs():
db.session.commit()
def reset_trophies():
# Clean up trophies
for t in Trophy.query.all():
db.session.delete(t)
# Create base trophies
trophies = []
with open(os.path.join(app.root_path, "data", "trophies.yaml")) as fp:
trophies = yaml.load(fp.read())
for t in trophies:
if t["is_title"]:
t["obj"] = Title(t["name"], t.get("css", ""))
else:
t["obj"] = Trophy(t["name"])
db.session.add(t["obj"])
db.session.commit()
def add_group(member, group):
if group[0] != '#':
print("error: group id should start with '#'.")
@ -96,6 +118,7 @@ def add_group(member, group):
db.session.add(m)
db.session.commit()
print(help_msg)
commands = {
@ -103,6 +126,7 @@ commands = {
"members": members,
"groups": groups,
"reset-groups-and-privs": reset_groups_and_privs,
"reset-trophies": reset_trophies,
"add-group": add_group,
}

View File

@ -0,0 +1,28 @@
"""Update des titres et trophées 2
Revision ID: 87b039db71a5
Revises: 6ae59d74cf54
Create Date: 2019-06-10 19:27:45.227037
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '87b039db71a5'
down_revision = '6ae59d74cf54'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('title', 'title')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('title', sa.Column('title', sa.TEXT(), autoincrement=False, nullable=True))
# ### end Alembic commands ###