forked from devs/PCv5
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:
parent
a29e0c4411
commit
1d638689c6
|
@ -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
|
||||
|
|
|
@ -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 l’extrê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
|
|
@ -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')
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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}>'
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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"
|
24
master.py
24
master.py
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
@ -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 ###
|
Loading…
Reference in New Issue