Merge branch 'dev' into preprod
This commit is contained in:
commit
68b3bdd943
|
@ -8,10 +8,12 @@ La liste de paquets fourni est pour Archlinux, les paquets peuvent avoir des nom
|
|||
python3
|
||||
python-flask
|
||||
python-flask-login
|
||||
python-flask-mail
|
||||
python-flask-migrate
|
||||
python-flask-script
|
||||
python-flask-sqlalchemy
|
||||
python-flask-wtf
|
||||
python-itsdangerous
|
||||
python-ldap
|
||||
python-uwsgi
|
||||
python-psycopg2
|
||||
|
|
|
@ -2,6 +2,7 @@ from flask import Flask, g
|
|||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_migrate import Migrate
|
||||
from flask_login import LoginManager
|
||||
from flask_mail import Mail
|
||||
from config import Config
|
||||
import time
|
||||
|
||||
|
@ -14,6 +15,7 @@ if Config.SECRET_KEY == "a-random-secret-key":
|
|||
|
||||
db = SQLAlchemy(app)
|
||||
migrate = Migrate(app, db)
|
||||
mail = Mail(app)
|
||||
|
||||
login = LoginManager(app)
|
||||
login.login_view = 'login'
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
-
|
||||
name: Membre de CreativeCalc
|
||||
is_title: True
|
||||
description: Membre de l'association qui gère Planète Casio !
|
||||
description: Membre de l'association qui gère Planète Casio !
|
||||
hidden: True
|
||||
-
|
||||
name: Membre d'honneur
|
||||
|
@ -49,7 +49,7 @@
|
|||
-
|
||||
name: Romancier émérite
|
||||
is_title: True
|
||||
description: Écrire 5000 messages !
|
||||
description: Écrire 5000 messages !
|
||||
hidden: False
|
||||
|
||||
# Number of posted tutorials
|
||||
|
@ -66,7 +66,7 @@
|
|||
-
|
||||
name: Guerrier du savoir
|
||||
is_title: True
|
||||
description: Élaborer 25 tutoriels !
|
||||
description: Élaborer 25 tutoriels !
|
||||
hidden: False
|
||||
|
||||
# Account age (awarded on login only)
|
||||
|
@ -93,7 +93,7 @@
|
|||
-
|
||||
name: Vétéran mythique
|
||||
is_title: True
|
||||
description: Être membre pendant 10 ans !
|
||||
description: Être membre pendant 10 ans !
|
||||
hidden: False
|
||||
|
||||
# Number of "good" programs
|
||||
|
@ -110,7 +110,7 @@
|
|||
-
|
||||
name: Je code donc je suis
|
||||
is_title: True
|
||||
description: Publier 20 programmes !
|
||||
description: Publier 20 programmes !
|
||||
hidden: False
|
||||
|
||||
# Number of posted tests
|
||||
|
@ -127,7 +127,7 @@
|
|||
-
|
||||
name: Hard tester
|
||||
is_title: True
|
||||
description: Partager 100 tests !
|
||||
description: Partager 100 tests !
|
||||
hidden: False
|
||||
|
||||
# Number of event participations
|
||||
|
@ -144,7 +144,7 @@
|
|||
-
|
||||
name: Concurrent de l'extrême
|
||||
is_title: True
|
||||
description: Participer à 15 événements du site !
|
||||
description: Participer à 15 événements du site !
|
||||
hidden: False
|
||||
|
||||
# Number of posted art
|
||||
|
@ -161,7 +161,7 @@
|
|||
-
|
||||
name: Roi du pixel
|
||||
is_title: True
|
||||
description: Publier 100 assets graphiques !
|
||||
description: Publier 100 assets graphiques !
|
||||
hidden: False
|
||||
|
||||
# Miscellaneous automatically awarded
|
||||
|
@ -178,10 +178,10 @@
|
|||
-
|
||||
name: Maître du code
|
||||
is_title: True
|
||||
description: Être décoré 5 fois du label de qualité !
|
||||
description: Être décoré 5 fois du label de qualité !
|
||||
hidden: False
|
||||
-
|
||||
name: Bourreau des cœurs
|
||||
is_title: True
|
||||
description: Foudroyer les cœurs à 5 reprises !
|
||||
description: Foudroyer les cœurs à 5 reprises !
|
||||
hidden: False
|
||||
|
|
|
@ -137,6 +137,35 @@ class DeleteAccountForm(FlaskForm):
|
|||
)
|
||||
|
||||
|
||||
class AskResetPasswordForm(FlaskForm):
|
||||
email = EmailField(
|
||||
'Adresse email',
|
||||
validators=[
|
||||
Optional(),
|
||||
Email(message="Addresse email invalide."),
|
||||
],
|
||||
)
|
||||
submit = SubmitField('Valider')
|
||||
|
||||
|
||||
class ResetPasswordForm(FlaskForm):
|
||||
password = PasswordField(
|
||||
'Mot de passe',
|
||||
validators=[
|
||||
Optional(),
|
||||
vd.password,
|
||||
],
|
||||
)
|
||||
password2 = PasswordField(
|
||||
'Répéter le mot de passe',
|
||||
validators=[
|
||||
Optional(),
|
||||
EqualTo('password', message="Les mots de passe doivent être identiques."),
|
||||
],
|
||||
)
|
||||
submit = SubmitField('Valider')
|
||||
|
||||
|
||||
class AdminUpdateAccountForm(FlaskForm):
|
||||
username = StringField(
|
||||
'Pseudonyme',
|
||||
|
|
|
@ -76,6 +76,7 @@ class Member(User):
|
|||
norm = db.Column(db.Unicode(V5Config.USER_NAME_MAXLEN), index=True,
|
||||
unique=True)
|
||||
email = db.Column(db.Unicode(120), index=True, unique=True)
|
||||
email_confirmed = db.Column(db.Boolean)
|
||||
password_hash = db.Column(db.String(255))
|
||||
xp = db.Column(db.Integer)
|
||||
register_date = db.Column(db.Date, default=date.today)
|
||||
|
@ -118,6 +119,7 @@ class Member(User):
|
|||
self.name = name
|
||||
self.norm = unicode_names.normalize(name)
|
||||
self.email = email
|
||||
self.email_confirmed = not V5Config.ENABLE_EMAIL_CONFIRMATION
|
||||
if not V5Config.USE_LDAP:
|
||||
self.set_password(password)
|
||||
# Workflow with LDAP enabled is User → Postgresql → LDAP → set password
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
from flask import redirect, url_for, request, flash
|
||||
from flask import redirect, url_for, request, flash, abort
|
||||
from flask_login import login_required, current_user, logout_user
|
||||
from app import app, db
|
||||
from app.forms.account import UpdateAccountForm, RegistrationForm, DeleteAccountForm
|
||||
from app.forms.account import UpdateAccountForm, RegistrationForm, \
|
||||
DeleteAccountForm, AskResetPasswordForm, ResetPasswordForm
|
||||
from app.models.users import Member
|
||||
from app.utils.render import render
|
||||
from app.utils.send_mail import send_validation_mail, send_reset_password_mail
|
||||
from app.utils.priv_required import guest_only
|
||||
import app.utils.ldap as ldap
|
||||
from itsdangerous import URLSafeTimedSerializer
|
||||
from config import V5Config
|
||||
|
||||
|
||||
|
@ -33,6 +37,45 @@ def edit_account():
|
|||
|
||||
return render('account/account.html', form=form)
|
||||
|
||||
@app.route('/compte/reinitialiser', methods=['GET', 'POST'])
|
||||
@guest_only
|
||||
def ask_reset_password():
|
||||
form = AskResetPasswordForm()
|
||||
if form.submit.data:
|
||||
m = Member.query.filter_by(email=form.email.data).first()
|
||||
if m is not None:
|
||||
send_reset_password_mail(m.name, m.email)
|
||||
flash('Un email a été envoyé à l\'adresse renseignée', 'ok')
|
||||
return redirect(url_for('login'))
|
||||
elif request.method == "POST":
|
||||
flash('Une erreur est survenue', 'error')
|
||||
|
||||
return render('account/ask_reset_password.html', form=form)
|
||||
|
||||
@app.route('/compte/reinitialiser/<token>', methods=['GET', 'POST'])
|
||||
@guest_only
|
||||
def reset_password(token):
|
||||
try:
|
||||
ts = URLSafeTimedSerializer(app.config["SECRET_KEY"])
|
||||
email = ts.loads(token, salt="email-confirm-key", max_age=86400)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
abort(404)
|
||||
|
||||
form = ResetPasswordForm()
|
||||
if form.submit.data:
|
||||
if form.validate_on_submit():
|
||||
m = Member.query.filter_by(email=email).first_or_404()
|
||||
m.set_password(form.password.data)
|
||||
db.session.merge(m)
|
||||
db.session.commit()
|
||||
flash('Modifications effectuées', 'ok')
|
||||
return redirect(url_for('login'))
|
||||
else:
|
||||
flash('Erreur lors de la modification', 'error')
|
||||
|
||||
return render('account/reset_password.html', form=form)
|
||||
|
||||
|
||||
@app.route('/compte/supprimer', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
|
@ -52,9 +95,8 @@ def delete_account():
|
|||
|
||||
|
||||
@app.route('/inscription', methods=['GET', 'POST'])
|
||||
@guest_only
|
||||
def register():
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('index'))
|
||||
form = RegistrationForm()
|
||||
if form.validate_on_submit():
|
||||
member = Member(form.username.data, form.email.data, form.password.data)
|
||||
|
@ -66,13 +108,42 @@ def register():
|
|||
ldap.add_member(member)
|
||||
ldap.set_password(member, form.password.data)
|
||||
flash('Inscription réussie', 'ok')
|
||||
|
||||
# Email validation message
|
||||
send_validation_mail(member.name, member.email)
|
||||
|
||||
return redirect(url_for('validation') + "?email=" + form.email.data)
|
||||
return render('account/register.html', title='Register', form=form)
|
||||
|
||||
|
||||
@app.route('/register/validation/', methods=['GET', 'POST'])
|
||||
@app.route('/inscription/validation', methods=['GET'])
|
||||
@guest_only
|
||||
def validation():
|
||||
mail = request.args['email']
|
||||
try:
|
||||
mail = request.args['email']
|
||||
except Exception as e:
|
||||
print("Error: {e}")
|
||||
abort(404)
|
||||
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('index'))
|
||||
return render('account/validation.html', mail=mail)
|
||||
|
||||
@app.route('/inscription/validation/<token>', methods=['GET'])
|
||||
@guest_only
|
||||
def activate_account(token):
|
||||
try:
|
||||
ts = URLSafeTimedSerializer(app.config["SECRET_KEY"])
|
||||
email = ts.loads(token, salt="email-confirm-key", max_age=86400)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
abort(404)
|
||||
|
||||
m = Member.query.filter_by(email=email).first_or_404()
|
||||
m.email_confirmed = True
|
||||
|
||||
db.session.add(m)
|
||||
db.session.commit()
|
||||
|
||||
flash("L'email a bien été confirmé", "ok")
|
||||
return redirect(url_for('login'))
|
||||
|
|
|
@ -6,11 +6,19 @@ from app.forms.login import LoginForm
|
|||
from app.models.users import Member
|
||||
from app.models.privs import Group
|
||||
from app.utils.render import render
|
||||
from app.utils.send_mail import send_validation_mail
|
||||
from config import V5Config
|
||||
|
||||
|
||||
@app.route('/connexion', methods=['GET', 'POST'])
|
||||
def login():
|
||||
# If something failed, return abort("Message")
|
||||
def _abort(msg):
|
||||
flash(msg, 'error')
|
||||
if request.referrer:
|
||||
return redirect(request.referrer)
|
||||
return redirect(url_for('index'))
|
||||
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('index'))
|
||||
|
||||
|
@ -24,17 +32,17 @@ def login():
|
|||
|
||||
# Check if member can login
|
||||
if member is not None and "No login" in [g.name for g in member.groups]:
|
||||
flash('Cet utilisateur ne peut pas se connecter', 'error')
|
||||
if request.referrer:
|
||||
return redirect(request.referrer)
|
||||
return redirect(url_for('index'))
|
||||
return _abort('Cet utilisateur ne peut pas se connecter')
|
||||
|
||||
# Check if password is ok
|
||||
if member is None or not member.check_password(form.password.data):
|
||||
flash('Pseudo ou mot de passe invalide', 'error')
|
||||
if request.referrer:
|
||||
return redirect(request.referrer)
|
||||
return redirect(url_for('index'))
|
||||
return _abort('Pseudo ou mot de passe invalide')
|
||||
|
||||
# Check if user is activated
|
||||
if member.email_confirmed == False:
|
||||
# Send another mail
|
||||
send_validation_mail(member.name, member.email)
|
||||
return _abort(f"Email non confirmé. Un mail de confirmation a de nouveau été envoyé à l'adresse {member.email}")
|
||||
|
||||
# Login & update time-based trophies
|
||||
login_user(member, remember=form.remember_me.data,
|
||||
|
|
|
@ -68,18 +68,9 @@ table.topiclist tr > td:last-child {
|
|||
table.thread {
|
||||
width: 100%;
|
||||
}
|
||||
table.thread td.member {
|
||||
table.thread td.author {
|
||||
width: 20%;
|
||||
}
|
||||
table.thread td.guest {
|
||||
width: 20%; padding-top: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
table.thread td.guest em {
|
||||
display: block;
|
||||
font-weight: bold; font-style: normal;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
table.thread td {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
|
|
@ -32,26 +32,36 @@
|
|||
}
|
||||
.profile-xp div {
|
||||
height: 10px;
|
||||
background: var(--background-xp);
|
||||
border: var(--border-xp);
|
||||
margin: -1px;
|
||||
background: var(--background-xp);
|
||||
border: var(--border-xp);
|
||||
margin: -1px;
|
||||
}
|
||||
|
||||
.profile.guest {
|
||||
flex-direction: column;
|
||||
width: 100%; padding-top: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
.profile.guest em {
|
||||
display: block;
|
||||
font-weight: bold; font-style: normal;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* Trophies */
|
||||
.trophies {
|
||||
display: flex; flex-wrap: wrap; justify-content: space-between;
|
||||
display: flex; flex-wrap: wrap; justify-content: space-between;
|
||||
}
|
||||
.trophy {
|
||||
display: flex; align-items: center;
|
||||
width: 260px;
|
||||
margin: 5px; padding: 5px;
|
||||
display: flex; align-items: center;
|
||||
width: 260px;
|
||||
margin: 5px; padding: 5px;
|
||||
border: 1px solid #c5c5c5;
|
||||
border-left: 5px solid var(--links);
|
||||
border-radius: 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.trophy img {
|
||||
height: 50px; margin-right: 5px;
|
||||
height: 50px; margin-right: 5px;
|
||||
}
|
||||
.trophy div > * {
|
||||
display: block;
|
||||
|
@ -65,7 +75,7 @@
|
|||
}
|
||||
|
||||
.trophy.disabled {
|
||||
filter: grayscale(100%);
|
||||
filter: grayscale(100%);
|
||||
opacity: .5;
|
||||
border-left: 1px solid #c5c5c5;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
{% extends "base/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="form" style="width:40%;">
|
||||
<h1>Réinitialiser le mot de passe</h1>
|
||||
|
||||
<p>Vous recevrez un mail contenant un lien pour réintialiser votre mot de passe.</p>
|
||||
|
||||
<form action="" method="post">
|
||||
{{ form.hidden_tag() }}
|
||||
<div>
|
||||
{{ form.email.label }}
|
||||
<div class=desc>{{ form.email.description }}</div>
|
||||
{{ form.email() }}
|
||||
{% for error in form.email.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>{{ form.submit(class_="bg-ok") }}</div>
|
||||
</form>
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -23,5 +23,6 @@
|
|||
<p>{{ form.submit(class_="bg-ok") }}</p>
|
||||
</form>
|
||||
<p>Pas encore de compte ? <a href="{{ url_for('register') }}">Créé-en un !</a></p>
|
||||
</form>
|
||||
<p><a href="{{ url_for('ask_reset_password') }}">Mot de passe oublié ?</a></p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
{% extends "base/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="form" style="width:40%;">
|
||||
<h1>Réinitialiser le mot de passe</h1>
|
||||
|
||||
<form action="" method="post">
|
||||
{{ form.hidden_tag() }}
|
||||
<div>
|
||||
{{ form.password.label }}
|
||||
<div class=desc>{{ form.password.description }}</div>
|
||||
{{ form.password() }}
|
||||
{% for error in form.password.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>
|
||||
{{ form.password2.label }}
|
||||
<div class=desc>{{ form.password2.description }}</div>
|
||||
{{ form.password2() }}
|
||||
{% for error in form.password2.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>{{ form.submit(class_="bg-ok") }}</div>
|
||||
</form>
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -55,7 +55,7 @@
|
|||
{{ login_form.remember_me.label }} {{ login_form.remember_me() }}
|
||||
</form>
|
||||
<hr />
|
||||
<a href="{{ url_for('register') }}">Mot de passe oublié ?</a>
|
||||
<a href="{{ url_for('register') }}">Inscription</a>
|
||||
<a href="{{ url_for('ask_reset_password') }}">Mot de passe oublié ?</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
<html>
|
||||
<body>
|
||||
<div>Bonjour {{ name }},</div>
|
||||
|
||||
<div>Bienvenue sur Planète Casio !</div>
|
||||
|
||||
<div>Afin de pouvoir vous connecter et utiliser nos services, merci de confirmer cette adresse email en cliquant <a href="{{ confirm_url }}">ici</a>.</div>
|
||||
<div>Si ça ne fonctionne pas, utilisez ce lien : <a href="{{ confirm_url }}">{{ confirm_url }}</a></div>
|
||||
|
||||
<div>À bientôt sur Planète Casio !</div>
|
||||
|
||||
<div>L'équipe du site.</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,9 @@
|
|||
Bonjour {{ name }},
|
||||
|
||||
Bienvenue sur Planète Casio !
|
||||
|
||||
Afin de pouvoir vous connecter et utiliser nos services, merci de confirmer cette adresse email en cliquant sur ce lien : {{ confirm_url }}
|
||||
|
||||
À bientôt sur Planète Casio !
|
||||
|
||||
L'équipe du site.
|
|
@ -0,0 +1,16 @@
|
|||
<html>
|
||||
<body>
|
||||
<div>Bonjour {{ name }},</div>
|
||||
|
||||
<div>Quelqu'un (probablement vous) a demandé a réinitialiser le mot de passe de ce compte.</div>
|
||||
|
||||
<div>Cliquez <a href="{{ confirm_url }}">ici</a> pour changer le mot de passe.</div>
|
||||
<div>Si ça ne fonctionne pas, utilisez ce lien : <a href="{{ confirm_url }}">{{ reset_url }}</a></div>
|
||||
|
||||
<div>Si vous n'êtes pas à l'origine de cette opération, vous pouvez ignorer ce message.</div>
|
||||
|
||||
<div>À bientôt sur Planète Casio !</div>
|
||||
|
||||
<div>L'équipe du site.</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,11 @@
|
|||
Bonjour {{ name }}
|
||||
|
||||
Quelqu'un (probablement vous) a demandé a réinitialiser le mot de passe de ce compte.
|
||||
|
||||
Cliquez sur ce lien pour changer votre mot de passe : {{ reset_url }}
|
||||
|
||||
Si vous n'êtes pas à l'origine de cette opération, vous pouvez ignorer ce message.
|
||||
|
||||
À bientôt sur Planète Casio !
|
||||
|
||||
L'équipe du site.
|
|
@ -1,6 +1,6 @@
|
|||
{% extends "base/base.html" %}
|
||||
{% import "widgets/editor.html" as widget_editor %}
|
||||
{% import "widgets/member.html" as widget_member %}
|
||||
{% import "widgets/user.html" as widget_user %}
|
||||
{% import "widgets/pagination.html" as widget_pagination with context %}
|
||||
|
||||
{% block title %}
|
||||
|
@ -11,11 +11,7 @@
|
|||
<section>
|
||||
<h1>{{ t.title }}</h1>
|
||||
<table class="thread"><tr>
|
||||
{% if t.author.type == "member" %}
|
||||
<td class="member">{{ widget_member.profile(t.author ) }}</td>
|
||||
{% else %}
|
||||
<td class="guest"><em>Invité</em>{{ t.author.name }}</td>
|
||||
{% endif %}
|
||||
<td class="author">{{ widget_user.profile(t.author ) }}</td>
|
||||
<td>{{ t.thread.top_comment.text }}</td>
|
||||
</tr></table>
|
||||
|
||||
|
@ -25,11 +21,7 @@
|
|||
{% for c in comments.items %}
|
||||
<tr id="{{ c.id }}">
|
||||
{% if c != t.thread.top_comment %}
|
||||
{% if c.author.type == "member" %}
|
||||
<td class="member">{{ widget_member.profile(c.author ) }}</td>
|
||||
{% else %}
|
||||
<td class="guest"><em>Invité</em>{{ c.author.name }}</td>
|
||||
{% endif %}
|
||||
<td class="author">{{ widget_user.profile(c.author) }}</td>
|
||||
<td>
|
||||
<div>{% if c.date_created != c.date_modified %}
|
||||
Posté le {{ c.date_created|date }} (Modifié le {{ c.date_modified|date }})
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
{% macro profile(member) %}
|
||||
<div class=profile>
|
||||
<img class=profile-avatar src="{{ url_for('avatar', 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 %}
|
|
@ -1,12 +1,19 @@
|
|||
{% macro profile(member) %}
|
||||
<div class=profile>
|
||||
<img class=profile-avatar src="{{ url_for('avatar', filename=member.avatar) }}" alt="Avatar de {{ member.name }}">
|
||||
{% macro profile(user) %}
|
||||
{% if user.type == "member" %}
|
||||
<div class="profile">
|
||||
<img class="profile-avatar" src="{{ url_for('avatar', filename=user.avatar) }}" alt="Avatar de {{ user.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 class="profile-name">{{ user.name }}</div>
|
||||
<div class="profile-title">Membre</div>
|
||||
<div class="profile-points">Niveau {{ user.level[0] }} <span>({{ user.xp }})</span></div>
|
||||
<div class="profile-points-small">N{{ user.level[0] }} <span>({{ user.xp }})</span></div>
|
||||
<div class="profile-xp"><div style='width: {{ user.level[1] }}%;'></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="profile guest">
|
||||
<em>{{ user.name }}</em>
|
||||
<span>Invité</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
|
|
@ -36,3 +36,15 @@ def priv_required(*perms):
|
|||
return func(*args, **kwargs)
|
||||
return wrapped
|
||||
return decorated_view
|
||||
|
||||
|
||||
def guest_only(func):
|
||||
"""
|
||||
Opposite decorator of @login_required
|
||||
"""
|
||||
@wraps(func)
|
||||
def wrapped(*args, **kwargs):
|
||||
if current_user.is_authenticated:
|
||||
abort(404)
|
||||
return func(*args, **kwargs)
|
||||
return wrapped
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
from flask import url_for, render_template
|
||||
from flask_mail import Message
|
||||
from app import app, mail
|
||||
from itsdangerous import URLSafeTimedSerializer
|
||||
from config import V5Config
|
||||
|
||||
def send_mail(dest, subject, html="", body=""):
|
||||
m = Message(recipients=[dest], subject=subject, html=html, body=body)
|
||||
mail.send(m)
|
||||
|
||||
def send_reset_password_mail(name, email):
|
||||
ts = URLSafeTimedSerializer(app.config["SECRET_KEY"])
|
||||
token = ts.dumps(email, salt='email-confirm-key')
|
||||
reset_url = f"https://{V5Config.DOMAIN}" \
|
||||
+ url_for('reset_password', token=token)
|
||||
subject = "Confirmez votre email pour compléter l'inscription"
|
||||
html = render_template('email/reset_password.html', reset_url=reset_url,
|
||||
name=name)
|
||||
body = render_template('email/reset_password.md', reset_url=reset_url,
|
||||
name=name)
|
||||
if V5Config.SEND_MAILS:
|
||||
send_mail(member.email, subject, html=html, body=body)
|
||||
else:
|
||||
print(f"Reset password url: {reset_url}")
|
||||
|
||||
def send_validation_mail(name, email):
|
||||
ts = URLSafeTimedSerializer(app.config["SECRET_KEY"])
|
||||
token = ts.dumps(email, salt='email-confirm-key')
|
||||
confirm_url = f"https://{V5Config.DOMAIN}" \
|
||||
+ url_for('activate_account', token=token)
|
||||
subject = "Confirmez votre email pour compléter l'inscription"
|
||||
html = render_template('email/activate.html', confirm_url=confirm_url,
|
||||
name=name)
|
||||
body = render_template('email/activate.md', confirm_url=confirm_url,
|
||||
name=name)
|
||||
if V5Config.SEND_MAILS:
|
||||
send_mail(member.email, subject, html=html, body=body)
|
||||
else:
|
||||
print(f"Email confirmation url: {confirm_url}")
|
|
@ -16,9 +16,14 @@ class Config(object):
|
|||
+ LocalConfig.DB_NAME
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
|
||||
MAIL_DEFAULT_SENDER = "noreply@v5.planet-casio.com"
|
||||
MAIL_SUPPRESS_SEND = None
|
||||
|
||||
|
||||
class DefaultConfig(object):
|
||||
"""Every value here can be overrided in the local_config.py class"""
|
||||
# Domain
|
||||
DOMAIN = "v5.planet-casio.com"
|
||||
# Length allocated to privilege names (slugs)
|
||||
PRIVS_MAXLEN = 64
|
||||
# Forbidden user names
|
||||
|
@ -57,6 +62,10 @@ class DefaultConfig(object):
|
|||
AVATARS_FOLDER = '/avatar/folder/'
|
||||
# Enable guest post
|
||||
ENABLE_GUEST_POST = True
|
||||
# Disable email confimation
|
||||
ENABLE_EMAIL_CONFIRMATION = True
|
||||
# Send emails
|
||||
SEND_MAILS = True
|
||||
|
||||
class V5Config(LocalConfig, DefaultConfig):
|
||||
# Values put here cannot be overidden with local_config
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
"""Added email_validated property in model `Member`
|
||||
|
||||
Revision ID: acf72cf31eea
|
||||
Revises: 6fd4c15b8a7b
|
||||
Create Date: 2020-07-21 20:29:44.876704
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'acf72cf31eea'
|
||||
down_revision = '6fd4c15b8a7b'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('member', sa.Column('email_confirmed', sa.Boolean(), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('member', 'email_confirmed')
|
||||
# ### end Alembic commands ###
|
Loading…
Reference in New Issue