Pārlūkot izejas kodu

trophies: automatically remove undeserved trophies

... and other minor edits from the trophies branch.
notifications
Lephe pirms 11 mēnešiem
vecāks
revīzija
4cefe39c36
11 mainītis faili ar 308 papildinājumiem un 131 dzēšanām
  1. +38
    -11
      app/data/trophies.yaml
  2. +1
    -1
      app/forms/account.py
  3. +1
    -1
      app/forms/trophies.py
  4. +10
    -3
      app/models/trophies.py
  5. +116
    -32
      app/models/users.py
  6. +7
    -9
      app/routes/admin/account.py
  7. +22
    -13
      app/static/css/form.css
  8. +1
    -23
      app/static/css/global.css
  9. +5
    -1
      app/templates/admin/edit_account.html
  10. +5
    -3
      app/templates/admin/trophies.html
  11. +102
    -34
      master.py

+ 38
- 11
app/data/trophies.yaml Parādīt failu

@@ -1,3 +1,8 @@
# This is a list of trophies. For each trophies, the following keys may be set:
# name Trophy name as displayed on the site.
# is_title If True, the trophy can be worn as a title next to the avatar.

# Manually awarded
-
name: Membre de CreativeCalc
is_title: True
@@ -11,28 +16,37 @@
name: Gourou
is_title: True
-
name: Grand Maitre des traits d'esprit
name: Grand Maître des traits d'esprit
is_title: True

# Number of posts of any kind
-
name: Premiers mots
is_title: False
-
name: Beau parleur
is_title: False
-
name: Jeune écrivain
name: Plume infaillible
is_title: False
-
name: Romancier émérite
is_title: True

# Number of posted tutorials
-
name: Apprenti instructeur
name: Pédagogue
is_title: False
-
name: Pédagogue averti
name: Encyclopédie vivante
is_title: False
-
name: Encyclopédie vivante
name: Guerrier du savoir
is_title: True

# Account age (awarded on login only)
-
name: Nouveau
name: Initié
is_title: False
-
name: Aficionado
@@ -43,6 +57,11 @@
-
name: Papy Casio
is_title: True
-
name: Vétéran mythique
is_title: True

# Number of "good" programs
-
name: Programmeur du dimanche
is_title: False
@@ -52,33 +71,41 @@
-
name: Je code donc je suis
is_title: True

# Number of posted tests
-
name: Testeur
is_title: False
-
name: Examinateur
name: Grand joueur
is_title: False
-
name: Hard tester
is_title: True

# Number of event participations
-
name: Participant avéré
name: Participant
is_title: False
-
name: Concourant encore
is_title: False
-
name: Concurrent de lextrême
name: Concurrent de l'extrême
is_title: True

# Number of posted art
-
name: Designer en herbe
name: Dessinateur en herbe
is_title: False
-
name: Graphiste expérimenté
name: Open pixel
is_title: False
-
name: Roi du pixel
is_title: True

# Miscellaneous automatically awarded
-
name: Actif
is_title: False


+ 1
- 1
app/forms/account.py Parādīt failu

@@ -51,7 +51,7 @@ class AdminUpdateAccountForm(FlaskForm):


class AdminAccountEditTrophyForm(FlaskForm):
# Boolean inputs are generated on-the-fly from trophies list
# Boolean inputs are generated on-the-fly from trophy list
submit = SubmitField('Modifier')




+ 1
- 1
app/forms/trophies.py Parādīt failu

@@ -6,7 +6,7 @@ from flask_wtf.file import FileField # Cuz' wtforms' FileField is shitty

class TrophyForm(FlaskForm):
name = StringField('Nom', validators=[DataRequired()])
icon = FileField('Icone')
icon = FileField('Icône')
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')


+ 10
- 3
app/models/trophies.py Parādīt failu

@@ -4,15 +4,16 @@ from app import db
class Trophy(db.Model):
__tablename__ = 'trophy'

# Trophy ID and type (polymorphic discriminator)
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

# Trophy name (in French)
name = db.Column(db.Unicode(64), index=True)

owners = db.relationship('Member', secondary=lambda: TrophyMember,
@@ -21,6 +22,9 @@ class Trophy(db.Model):
def __init__(self, name):
self.name = name

def __repr__(self):
return f'<Trophy: {self.name}>'

# Title: Rare trophies that can be displayed along one's name
class Title(Trophy):
__tablename__ = 'title'
@@ -29,10 +33,13 @@ class Title(Trophy):
id = db.Column(db.Integer, db.ForeignKey('trophy.id'), primary_key=True)
css = db.Column(db.UnicodeText)

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

def __repr__(self):
return f'<Title: {self.name}>'


# Many-to-many relation for users earning trophies
TrophyMember = db.Table('trophy_member', db.Model.metadata,


+ 116
- 32
app/models/users.py Parādīt failu

@@ -33,7 +33,7 @@ class User(UserMixin, db.Model):
}

def __repr__(self):
return f'<User #{self.id}>'
return f'<User: #{self.id}>'

# Guest: Unregistered user with minimal privileges
class Guest(User, db.Model):
@@ -215,8 +215,6 @@ class Member(User, db.Model):
t = Trophy.query.filter_by(name=t).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 '{t.name}'")

@@ -231,8 +229,6 @@ class Member(User, db.Model):
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):
"""
@@ -243,43 +239,131 @@ class Member(User, db.Model):
- new-tutorial
- new-test
- new-event-participation
- new-picture
- on-program-reward
- new-art
- on-program-tested
- on-program-rewarded
- 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:
def progress(trophies, value):
"""Award or delete all trophies from a progressive category."""
for level in trophies:
if value >= level:
self.add_trophy(trophies[level])
else:
self.del_trophy(trophies[level])

if context in ["new-post", "new-program", "new-tutorial", "new-test",
None]:
# TODO: Amount of posts by the user
post_count = 0

levels = {
20: "Premiers mots",
500: "Beau parleur",
1500: "Plume infaillible",
5000: "Romancier émérite",
}
progress(levels, post_count)

if context in ["new-program", None]:
# TODO: Amount of programs by the user
program_count = 0

levels = {
5: "Programmeur du dimanche",
10: "Codeur invétéré",
20: "Je code donc je suis",
}
progress(levels, program_count)

if context in ["new-tutorial", None]:
# TODO: Number of tutorials by user
tutorial_count = 0

levels = {
5: "Pédagogue",
10: "Encyclopédie vivante",
25: "Guerrier du savoir",
}
progress(levels, tutorial_count)

if context in ["new-test", None]:
# TODO: Number of tests by user
test_count = 0

levels = {
5: "Testeur",
25: "Grand joueur",
100: "Hard tester",
}
progress(levels, test_count)

if context in ["new-event-participation", None]:
# TODO: Number of event participations by user
event_participations = 0

levels = {
1: "Participant",
5: "Concourant encore",
15: "Concurrent de l'extrême",
}
progress(levels, event_participations)

if context in ["new-art", None]:
# TODO: Number of art posts by user
art_count = 0

levels = {
5: "Dessinateur en herbe",
30: "Open pixel",
100: "Roi du pixel",
}
progress(levels, art_count)

if context in ["on-program-tested", None]:
# TODO: Number of "coups de coeur" of user
heart_count = 0

levels = {
5: "Bourreau des cœurs",
}
progress(levels, heart_count)

if context in ["on-program-rewarded", None]:
# TODO: Number of programs with labels
label_count = 0

levels = {
5: "Maître du code",
}
progress(levels, label_count)

if context in ["on-login", 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:

levels = {
30: "Initié",
365.25: "Aficionado",
365.25 * 2: "Veni, vidi, casii",
365.25 * 5: "Papy Casio",
365.25 * 10: "Vétéran mythique",
}
progress(levels, age.days)

# TODO: Trophy "actif"

if context in ["on-profile-update", None]:
# TODO: add a better condition (this is for test)
self.add_trophy("Artiste")

db.session.merge(self)
db.session.commit()

def __repr__(self):
return f'<Member: {self.name}>'
return f'<Member: {self.name} ({self.norm})>'


@app.login.user_loader


+ 7
- 9
app/routes/admin/account.py Parādīt failu

@@ -62,20 +62,18 @@ def adm_edit_account(user_id):
else:
print(f"Del trophy {id[1:]}")
user.del_trophy(int(id[1:]))

db.session.merge(user)
db.session.commit()
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')
user_owned = set()
for t in user.trophies:
user_owned.add(f"t{t.id}")

return render('admin/edit_account.html', user=user,
form=form, trophy_form=trophy_form)
form=form, trophy_form=trophy_form, user_owned=user_owned)


@app.route('/admin/account/<user_id>/delete', methods=['GET', 'POST'])


+ 22
- 13
app/static/css/form.css Parādīt failu

@@ -11,7 +11,8 @@
margin-bottom: 16px;
}

.form form label {
.form form label,
.trophies-panel p {
display: inline-block;
margin-bottom: 4px;
}
@@ -19,16 +20,13 @@
margin: 0 0 4px 0;
}

.form input {
cursor: pointer; /* don't know why it is not a cursor by default */
}

.form input[type='text'],
.form input[type='email'],
.form input[type='date'],
.form input[type='password'],
.form input[type='search'],
.form textarea {
.form textarea,
.trophies-panel > div {
display: block;
width: 100%; padding: 6px 8px;
border: 1px solid #c8c8c8;
@@ -51,6 +49,12 @@
resize: vertical;
}

.form input[type="checkbox"],
.form input[type="radio"] {
display: inline;
vertical-align: middle;
}

.form input[type="submit"] {
/*width: 20%;*/
}
@@ -66,14 +70,19 @@
color: gray;
}

.trophies-panel {
display: flex; flex-wrap: wrap;
}
.trophies-panel > div {
margin: 3px 5px; padding: 3px;
border: 1px solid #969696;
border-radius: 3px;
.form hr {
color: white;
height: 3px;
border: 0 solid #b0b0b0;
border-width: 1px 0;
margin: 24px 0;
}
.trophies-panel label {
margin-right: 5px;
}
.trophies-panel p:first-child {
margin-top: 0;
}
.trophies-panel p label {
margin: 0;
}

+ 1
- 23
app/static/css/global.css Parādīt failu

@@ -40,29 +40,6 @@ section ul {
line-height: 24px;
}

/* Forms */

input,
textarea {
display: block;
background: #FFFFFF; color: #000000;
border: none;
}
input:focus:not(type="button"),
textarea:focus {
box-shadow: 0 0 0 0.2rem rgba(0,123,255,0.25);
}

textarea {
width: 100%;
border: 1px solid #eeeeee;
}

input[type="checkbox"] {
display: inline;
vertical-align: middle;
}

/* Buttons */

.button,
@@ -71,6 +48,7 @@ input[type="submit"] {
padding: 6px 10px; border-radius: 2px;
cursor: pointer;
font-family: 'DejaVu Sans', sans-serif; font-weight: 400;
border: 0;
}
input[type="button"]:hover,
input[type="submit"]:hover,


+ 5
- 1
app/templates/admin/edit_account.html Parādīt failu

@@ -91,6 +91,8 @@
<div>{{ form.submit(class_="bg-green") }}</div>
</form>

<hr>

<form action="{{ url_for('adm_edit_account', user_id=user.id) }}" method="post">
{{ trophy_form.hidden_tag() }}
<h2>Trophées</h2>
@@ -99,7 +101,7 @@
{% if id[0] == "t" %}
<div>
{# TODO: add trophies icons #}
{{ input(checked=id in trophy_form.user_trophies) }}
{{ input(checked=id in user_owned) }}
{{ input.label }}
</div>
{% endif %}
@@ -108,6 +110,8 @@
<div>{{ trophy_form.submit(class_="bg-green") }}</div>
</form>

<hr>

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



+ 5
- 3
app/templates/admin/trophies.html Parādīt failu

@@ -6,7 +6,9 @@

{% block content %}
<section>
<p>Cette page présente une vue d'ensemble des titres et trophées.</p>
<p>Cette page présente une vue d'ensemble des titres et trophées. Les
conditions d'obtention exactes des trophées sont définies dans le code et
non dans la base de données.</p>

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

@@ -20,8 +22,8 @@
<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>
<td style="text-align: center"><a href="{{ url_for('adm_edit_trophy', trophy_id=trophy.id) }}">Modifier</a></td>
<td style="text-align: center"><a href="{{ url_for('adm_delete_trophy', trophy_id=trophy.id) }}">Supprimer</a></td>
</tr>
{% endfor %}
</table>


+ 102
- 34
master.py Parādīt failu

@@ -3,17 +3,26 @@
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
from app.models.trophies import Trophy, Title, TrophyMember
from app.utils import unicode_names
import os
import sys
import yaml
import readline

help_msg = """
This is the Planète Casio master shell. Type 'exit' or C-D to leave.

Type 'members' to see a list of members and 'groups' to see a list of groups.
Type a category name to see a list of elements. Available categories are:

Type 'reset-groups-and-privs' to reset all groups and privileges to the
'members' Registered community members
'groups' Privilege groups
'trophies' Trophies
'trophy-members' Trophies owned by members

Type a category name followed by 'clear' to remove all entries in the category.

Type 'create-groups-and-privs' to recreate all groups and privileges to the
default. This function generates a minimal set of groups and members to prepare
the database.
1. Deletes all groups
@@ -26,43 +35,88 @@ the database.

Type 'add-group <member> #<group-id>' to add a new member to a group.

Type 'reset-trophies' to reset trophies and titles.
Type 'create-trophies' to reset trophies and titles.
"""

def members():
#
# Category viewers
#

def members(*args):
if args == ("clear",):
for m in Member.query.all():
m.delete()
db.session.commit()
print("Removed all members.")
return

for m in Member.query.all():
print(m.name)
print(m)

def groups(*args):
if args == ("clear",):
for g in Group.query.all():
g.delete()
db.session.commit()
print("Removed all groups.")
return

def groups():
for g in Group.query.all():
print(f"#{g.id} {g.name}")
print(f"#{g.id} {g.name}")

def trophies(*args):
if args == ("clear",):
for t in Trophy.query.all():
db.session.delete(t)
db.session.commit()
print("Removed all trophies.")
return

for t in Trophy.query.all():
print(t)

def trophy_members(*args):
for t in Trophy.query.all():
if t.owners == []:
continue

print(t)
for m in t.owners:
print(f" {m}")

#
# Creation and edition
#

def reset_groups_and_privs():
def create_groups_and_privs():
# Clean up groups
for g in Group.query.all():
g.delete()
groups("clear")

# Create base groups
groups = []
gr = []
with open(os.path.join(app.root_path, "data", "groups.yaml")) as fp:
groups = yaml.load(fp.read())
gr = yaml.safe_load(fp.read())

for g in groups:
for g in gr:
g["obj"] = Group(g["name"], g["css"], g["descr"])
db.session.add(g["obj"])
db.session.commit()

for g in groups:
for g in gr:
for priv in g.get("privs", "").split():
db.session.add(GroupPrivilege(g["obj"], priv))
db.session.commit()

print(f"Created {len(gr)} groups.")

# Clean up test members
for name in "PlanèteCasio GLaDOS".split():
m = Member.query.filter_by(name=name).first()
if m is not None:
m.delete()

print("Removed test members.")

# Create template members

def addgroup(member, group):
@@ -70,11 +124,11 @@ def reset_groups_and_privs():
if g is not None:
member.groups.append(g)

m = Member('PlanèteCasio', 'contact@planet-casio.com', 'v5-forever')
m = Member("PlanèteCasio", "contact@planet-casio.com", "v5-forever")
addgroup(m, "Compte communautaire")
db.session.add(m)

m = Member('GLaDOS', 'glados@aperture.science', 'v5-forever')
m = Member("GLaDOS", "glados@aperture.science", "v5-forever")
m.xp = 1338
addgroup(m, "Robot")
db.session.add(m)
@@ -85,39 +139,50 @@ def reset_groups_and_privs():

db.session.commit()

print(f"Created 2 test members with some privileges.")

def reset_trophies():

def create_trophies():
# Clean up trophies
for t in Trophy.query.all():
db.session.delete(t)
trophies("clear")

# Create base trophies
trophies = []
tr = []
with open(os.path.join(app.root_path, "data", "trophies.yaml")) as fp:
trophies = yaml.load(fp.read())
tr = yaml.safe_load(fp.read())

for t in trophies:
for t in tr:
if t["is_title"]:
t["obj"] = Title(t["name"], t.get("css", ""))
trophy = Title(t["name"], t.get("css", ""))
else:
t["obj"] = Trophy(t["name"])
db.session.add(t["obj"])
trophy = Trophy(t["name"])
db.session.add(trophy)
db.session.commit()

print(f"Created {len(tr)} trophies.")

def add_group(member, group):
if group[0] != '#':
print("error: group id should start with '#'.")
if group[0] != "#":
print(f"error: group id {group} should start with '#'")
return
gid = int(group[1:])

norm = unicode_names.normalize(member)

g = Group.query.filter_by(id=gid).first()
m = Member.query.filter_by(name=member).first()
m = Member.query.filter_by(norm=norm).first()

if m is None:
print(f"error: no member has a normalized name of '{norm}'")
return

m.groups.append(g)
db.session.add(m)
db.session.commit()

#
# Main program
#

print(help_msg)

@@ -125,20 +190,23 @@ commands = {
"exit": lambda: sys.exit(0),
"members": members,
"groups": groups,
"reset-groups-and-privs": reset_groups_and_privs,
"reset-trophies": reset_trophies,
"trophies": trophies,
"trophy-members": trophy_members,
"create-groups-and-privs": create_groups_and_privs,
"create-trophies": create_trophies,
"add-group": add_group,
}

while True:
try:
print('> ', end='')
print("@> ", end="")
cmd = input().split()
except EOFError:
sys.exit(0)

if not cmd: continue
if not cmd:
continue
if cmd[0] not in commands:
print("error: unknown command.")
print(f"error: unknown command '{cmd[0]}'")
else:
commands[cmd[0]](*cmd[1:])

Notiek ielāde…
Atcelt
Saglabāt