diff --git a/app/__init__.py b/app/__init__.py index b101104..ddbb914 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -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 diff --git a/app/data/trophies.yaml b/app/data/trophies.yaml new file mode 100644 index 0000000..b6a11fc --- /dev/null +++ b/app/data/trophies.yaml @@ -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 diff --git a/app/forms/account.py b/app/forms/account.py index 999c940..33c35c7 100644 --- a/app/forms/account.py +++ b/app/forms/account.py @@ -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') diff --git a/app/forms/trophies.py b/app/forms/trophies.py index 49535ba..1634ea3 100644 --- a/app/forms/trophies.py +++ b/app/forms/trophies.py @@ -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') diff --git a/app/models/trophies.py b/app/models/trophies.py index 4ff388b..ae81ffa 100644 --- a/app/models/trophies.py +++ b/app/models/trophies.py @@ -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 diff --git a/app/models/users.py b/app/models/users.py index fd62e6f..aa2964c 100644 --- a/app/models/users.py +++ b/app/models/users.py @@ -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'' diff --git a/app/routes/account/account.py b/app/routes/account/account.py index 6a58ac2..601f5af 100644 --- a/app/routes/account/account.py +++ b/app/routes/account/account.py @@ -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() diff --git a/app/routes/account/login.py b/app/routes/account/login.py index b594f9a..a12bda2 100644 --- a/app/routes/account/login.py +++ b/app/routes/account/login.py @@ -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: diff --git a/app/routes/admin/account.py b/app/routes/admin/account.py index 55a400b..c1c71fb 100644 --- a/app/routes/admin/account.py +++ b/app/routes/admin/account.py @@ -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//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') diff --git a/app/routes/admin/trophies.py b/app/routes/admin/trophies.py index b722bbd..94ded1c 100644 --- a/app/routes/admin/trophies.py +++ b/app/routes/admin/trophies.py @@ -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) diff --git a/app/templates/admin/edit_account.html b/app/templates/admin/edit_account.html index 0d92d10..13e8616 100644 --- a/app/templates/admin/edit_account.html +++ b/app/templates/admin/edit_account.html @@ -91,6 +91,32 @@
{{ form.submit(class_="bg-green") }}
+
+ {{ addtrophy_form.hidden_tag() }} +

Accorder un trophée

+
+ {{ addtrophy_form.trophy.label }} + {{ addtrophy_form.trophy }} + {% for error in addtrophy_form.trophy.errors %} + {{ error }} + {% endfor %} +
+
{{ addtrophy_form.submit(class_="bg-green") }}
+
+ +
+ {{ deltrophy_form.hidden_tag() }} +

Retirer un trophée

+
+ {{ deltrophy_form.trophy.label }} + {{ deltrophy_form.trophy }} + {% for error in deltrophy_form.trophy.errors %} + {{ error }} + {% endfor %} +
+
{{ deltrophy_form.submit(class_="bg-red") }}
+
+

Supprimer le compte

Supprimer le compte diff --git a/app/templates/admin/edit_trophy.html b/app/templates/admin/edit_trophy.html index 83aacd9..c6b197d 100644 --- a/app/templates/admin/edit_trophy.html +++ b/app/templates/admin/edit_trophy.html @@ -19,16 +19,16 @@
{{ form.title.label }} - {{ form.title(value=trophy.title) }} -
{{ form.title.description }}
+ {{ form.title() }} +
{{ form.title.description }}
{% for error in form.title.errors %} {{ error }} {% endfor %}
{{ form.css.label }} +
{{ form.css.description }}
{{ form.css(value=trophy.css) }} -
{{ form.css.description }}
{% for error in form.css.errors %} {{ error }} {% endfor %} diff --git a/app/templates/admin/trophies.html b/app/templates/admin/trophies.html index 4b27f88..d434e95 100644 --- a/app/templates/admin/trophies.html +++ b/app/templates/admin/trophies.html @@ -17,8 +17,8 @@ {% for trophy in trophies %} {{ trophy.id }} {{ trophy.name }} - {{ trophy.name }} - {{ trophy.title }} + {{ trophy.name }} + {{ trophy | is_title }} {{ trophy.css }} Modifier Supprimer @@ -40,15 +40,15 @@
{{ form.title.label }} -
{{ form.title.description }}
{{ form.title }} +
{{ form.title.description }}
{% for error in form.title.errors %} {{ error }} {% endfor %}
{{ form.css.label }} -
{{ form.css.description }}
+
{{ form.css.description }}
{{ form.css }} {% for error in form.css.errors %} {{ error }} diff --git a/app/templates/user.html b/app/templates/user.html index 264def3..d276164 100644 --- a/app/templates/user.html +++ b/app/templates/user.html @@ -8,5 +8,22 @@ {% block content %}
{{ widget_member.profile(member) }} + + {% if current_user.is_authenticated and current_user.priv('access-admin-panel') %} +
Modifier
+ {% endif %} + +

Trophées

+
+ {% if member.trophies %} +
    + {% for t in member.trophies %} +
  • {{ t.name }}
  • + {% endfor %} +
+ {% else %} + Aucun trophée. + {% endif %} +
{% endblock %} diff --git a/app/utils/is_title.py b/app/utils/is_title.py new file mode 100644 index 0000000..67530de --- /dev/null +++ b/app/utils/is_title.py @@ -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" diff --git a/master.py b/master.py index 4d8887d..dc40c40 100755 --- a/master.py +++ b/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 #' 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, } diff --git a/migrations/versions/87b039db71a5_update_des_titres_et_trophées_2.py b/migrations/versions/87b039db71a5_update_des_titres_et_trophées_2.py new file mode 100644 index 0000000..2b31a51 --- /dev/null +++ b/migrations/versions/87b039db71a5_update_des_titres_et_trophées_2.py @@ -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 ###