account: implement leveling and a simple profile widget

This commit is contained in:
Lephe 2019-04-04 21:31:14 +02:00
parent 8ce3785134
commit bc7580de25
12 changed files with 219 additions and 111 deletions

View File

@ -33,10 +33,20 @@ 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])
password = PasswordField('Mot de passe', 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()])
innovation = DecimalField('Innovation', validators=[Optional()])
birthday = DateField('Anniversaire', validators=[Optional()])

View File

@ -9,6 +9,7 @@ from config import V5Config
import werkzeug.security
import app
import re
import math
# User: Website user that performs actions on the content
class User(UserMixin, db.Model):
@ -103,6 +104,12 @@ class Member(User, db.Model):
def avatar(self):
return 'avatars/' + str(self.id) + '.png'
@property
def level(self):
xp = self.xp + 2 * self.innovation
level = math.asinh(xp / 1000) * (100 / math.asinh(10))
return int(level), int(level * 100) % 100
# Groups and related privileges
groups = db.relationship('Group', secondary=GroupMember,
back_populates='members')

View File

@ -62,6 +62,7 @@ def adm_groups():
db.session.add(m)
m = Member('GLaDOS', 'glados@aperture.science', 'v5-forever')
m.xp = 1337
addgroup(m, "Robot")
db.session.add(m)
db.session.commit()
@ -111,7 +112,7 @@ def adm_edit_account(user_id):
else:
flash('Erreur lors de la modification', 'error')
return render('admin/edit_account.html', user=user, form=form, styles=['-css/form.css'])
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')

View File

@ -7,14 +7,10 @@ from app.utils.render import render
@app.route('/user/<username>')
def user(username):
user = Member.query.filter_by(name=username).first()
if not user:
abort(404)
return render('user.html', user=user)
member = Member.query.filter_by(name=username).first_or_404()
return render('user.html', member=member)
@app.route('/user/id/<int:user_id>')
def user_by_id(user_id):
user = Member.query.filter_by(id=user_id).first()
if not user:
abort(404)
return redirect(url_for('user', username=user.name))
member = Member.query.filter_by(id=user_id).first_or_404()
return redirect(url_for('user', username=member.name))

View File

@ -8,12 +8,15 @@
}
.form form > div:not(:last-child) {
margin-bottom: 15px;
margin-bottom: 16px;
}
.form form label {
display: inline-block;
margin-bottom: 5px;
margin-bottom: 4px;
}
.form label + .desc {
margin: 0 0 4px 0;
}
.form input {
@ -39,8 +42,8 @@
.form input[type='password']:focus,
.form input[type='search']:focus,
.form textarea:focus {
border-color: #91bfef;
box-shadow: 0 0 0 3px rgba(87, 143, 228, 0.4);
border-color: #7cade0;
box-shadow: 0 0 0 3px rgba(87, 143, 228, 0.5);
}
.form textarea {
@ -57,3 +60,8 @@
font-weight: 400;
margin-top: 5px;
}
.form .desc {
font-size: 80%;
color: gray;
}

View File

@ -78,6 +78,67 @@ input[type="submit"]:hover,
text-decoration: none;
}
/* Profile summaries */
.profile {
display: flex;
align-items: center;
}
.profile-avatar {
width: 128px;
height: 128px;
margin-right: 16px;
}
.profile-name {
font-weight: bold;
}
.profile-title {
margin-bottom: 8px;
}
.profile-points {
font-size: 11px;
}
.profile-points span {
color: gray;
}
.profile-points-small {
display: none;
}
.profile-xp {
height: 10px;
min-width: 96px;
background: #e0e0e0;
border: 1px solid #c0c0c0;
}
.profile-xp div {
height: 10px;
background: #f85555;
border: 1px solid #d03333;
margin: -1px;
}
@media screen and (max-width: 1499px) {
.profile-avatar {
width: 96px;
height: 96px;
}
.profile-xp {
height: 8px;
min-width: 64px;
}
.profile-xp div {
height: 8px;
}
}
@media screen and (max-width: 1199px) {
.profile-points {
display: none;
}
.profile-points-small {
display: unset;
}
}
/*
Bootstrap-style rules
*/

View File

@ -189,15 +189,15 @@ nav a:focus {
}
#menu form input[type="text"],
#menu form input[type="password"] {
margin: 3px 0 8px 0; padding: 5px 2%;
margin: 8px 0; padding: 5px 2%;
font-size: 14px; color: inherit;
border-color: #141719;
border: none; border-color: #141719;
}
#menu form input[type="text"]:focus,
#menu form input[type="password"]:focus {
background: #ffffff;
box-shadow: 0 0 0 3px rgba(87, 143, 228, 0.6);
border-color: #2d4b5f;
box-shadow: 0 0 0 4px rgba(87, 143, 228, 0.65);
border-color: #325871;
}
#menu form input[type="submit"] {
width: 100%;

View File

@ -1,97 +1,100 @@
{% extends "base/base.html" %}
{% block title %}
<a href="{{ url_for('adm') }}">Panneau d'administration</a> » <h1>Édition du compte de '{{ user.name }}'</h1>
<a href="{{ url_for('adm') }}">Panneau d'administration</a> » <h1>Édition du compte de {{ user.name }}</h1>
{% endblock %}
{% block content %}
<section class="form">
<a href="/user/{{ user.name }}">Visiter la page de profil de {{ user.name }}</a>
<form action="{{ url_for('adm_edit_account', user_id=user.id) }}" method="post" enctype="multipart/form-data">
{{ form.hidden_tag() }}
<fieldset>
<legend>Général</legend>
<div style="float:left;">
{{ form.avatar.label }}
<div>
<img class="avatar" src="{{ url_for('static', filename=user.avatar) }}" meta="{{ user.avatar }}" />
{{ form.avatar }}
</div>
</div>
<h2>Participation</h2>
<div>
{{ form.avatar.label }}
<div>
{{ form.username.label }}
{{ form.username(placeholder=user.name) }}
{% for error in form.username.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
<img class="avatar" src="{{ url_for('static', filename=user.avatar) }}" meta="{{ user.avatar }}" />
{{ form.avatar }}
</div>
<div>
{{ form.email.label }}
{{ form.email(placeholder=user.email) }}
{% for error in form.email.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
<div>
{{ form.password.label }}
{{ form.password(placeholder='************') }}
{% for error in form.password.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
<div>{{ form.submit(class_="bg-green") }}</div>
</fieldset>
<fieldset>
<legend>Participation</legend>
<div>
{{ form.xp.label }}
{{ form.xp(placeholder=user.xp) }}
{% for error in form.xp.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
<div>
{{ form.innovation.label }}
{{ form.innovation(placeholder=user.innovation) }}
{% for error in form.innovation.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
</fieldset>
<fieldset>
<legend>À propos</legend>
<div>
{{ form.birthday.label }}
{{ form.birthday(value=user.birthday) }}
{% for error in form.birthday.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
<div>
{{ form.signature.label }}
<textarea id="{{ form.signature.name }}" name="{{ form.signature.name }}">{{ user.signature }}</textarea>
{% for error in form.signature.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
<div>
{{ form.biography.label }}
<textarea id="{{ form.biography.name }}" name="{{ form.biography.name }}">{{ user.bio }}</textarea>
{% for error in form.biography.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
</fieldset>
<fieldset>
<legend>Préférences</legend>
<div>
{{ form.newsletter.label }}
{{ form.newsletter(checked=user.newsletter) }}
<div style="font-size:80%;color:rgba(0,0,0,.5)">{{ form.newsletter.description }}</div>
{% for error in form.newsletter.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
</fieldset>
</div>
<div>
{{ form.username.label }}
{{ form.username(placeholder=user.name) }}
{% for error in form.username.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
<div>
{{ form.email.label }}
{{ form.email(placeholder=user.email) }}
{% for error in form.email.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
<div>
{{ form.email_validate.label }}
{{ form.email_validate(checked=True) }}
<div class=desc>{{ form.email_validate.description }}</div>
</div>
<div>
{{ form.password.label }}
<div class=desc>{{ form.password.description }}</div>
{{ form.password(placeholder='************') }}
{% for error in form.password.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
<h2>Participation</h2>
<div>
{{ form.xp.label }}
{{ form.xp(placeholder=user.xp) }}
{% for error in form.xp.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
<div>
{{ form.innovation.label }}
{{ form.innovation(placeholder=user.innovation) }}
{% for error in form.innovation.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
<h2>À propos</h2>
<div>
{{ form.birthday.label }}
{{ form.birthday(value=user.birthday) }}
{% for error in form.birthday.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
<div>
{{ form.signature.label }}
<textarea id="{{ form.signature.name }}" name="{{ form.signature.name }}">{{ user.signature }}</textarea>
{% for error in form.signature.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
<div>
{{ form.biography.label }}
<textarea id="{{ form.biography.name }}" name="{{ form.biography.name }}">{{ user.bio }}</textarea>
{% for error in form.biography.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
<h2>Préférences</h2>
<div>
{{ form.newsletter.label }}
{{ form.newsletter(checked=user.newsletter) }}
<div class=desc>{{ form.newsletter.description }}</div>
{% for error in form.newsletter.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
<div>{{ form.submit(class_="bg-green") }}</div>
</form>

View File

@ -1,14 +1,12 @@
{% extends "base/base.html" %}
{% import "widgets/user.html" as widget_member %}
{% block title %}
<h1>Profil de '{{ user.name }}'</h1>
<h1>Profil de {{ member.name }}</h1>
{% endblock %}
{% block content %}
<section>
<div>
<img class="avatar" src="{{ url_for('static', filename=user.avatar) }}" alt="{{ user.name }}" style="display:inline; vertical-align:middle;"/>
{{ user.name }}
</div>
{{ widget_member.profile(member) }}
</section>
{% endblock %}

View File

@ -0,0 +1,12 @@
{% macro profile(member) %}
<div class=profile>
<img class=profile-avatar src="{{ url_for('static', filename=member.avatar) }}" alt="Avatar de {{ member.name }}">
<div>
<div class=profile-name><a href="{{ url_for('user', username=member.name) }}">{{ member.name }}</a></div>
<div class=profile-title>Membre</div>
<div class=profile-points>Niveau {{ member.level[0] }} <span>({{ member.xp }})</span></div>
<div class=profile-points-small>N{{ member.level[0] }} <span>({{ member.xp }})</span></div>
<div class=profile-xp><div style='width: {{ member.level[1] }}%;'></div></div>
</div>
</div>
{% endmacro %}

View File

@ -0,0 +1,12 @@
{% macro profile(member) %}
<div class=profile>
<img class=profile-avatar src="{{ url_for('static', filename=member.avatar) }}" alt="Avatar de {{ member.name }}">
<div>
<div class=profile-name>{{ member.name }}</div>
<div class=profile-title>Membre</div>
<div class=profile-points>Niveau {{ member.level[0] }} <span>({{ member.xp }})</span></div>
<div class=profile-points-small>N{{ member.level[0] }} <span>({{ member.xp }})</span></div>
<div class=profile-xp><div style='width: {{ member.level[1] }}%;'></div></div>
</div>
</div>
{% endmacro %}

View File

@ -2,7 +2,7 @@ from flask import render_template
from app.forms.login import LoginForm
from app.forms.search import SearchForm
def render(*args, styles=None, **kwargs):
def render(*args, styles=[], **kwargs):
# TODO: debugguer cette merde : au logout, ça foire
# if current_user.is_authenticated:
# login_form = LoginForm()
@ -33,4 +33,4 @@ def render(*args, styles=None, **kwargs):
login_form = LoginForm()
search_form = SearchForm()
return render_template(*args, **kwargs,
login_form=login_form, search_form=search_form, styles=styles_)
login_form=login_form, search_form=search_form, styles=styles_)