forked from devs/PCv5
account: implement leveling and a simple profile widget
This commit is contained in:
parent
8ce3785134
commit
bc7580de25
|
@ -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()])
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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%;
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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_)
|
||||
|
|
Loading…
Reference in New Issue