users: review code and expand a little

Nothing ground-breaking here, but lays the ground for
later expansions.
This commit is contained in:
lephe 2019-02-03 00:45:39 +01:00
parent 2f848501fe
commit 2311c7f3d8
10 changed files with 272 additions and 101 deletions

View File

@ -9,10 +9,12 @@ from wtforms.meta import DefaultMeta
# TODO: Put those validators into a specific file
def validate_username(self, username):
member = Member.query.filter_by(username=username.data).first()
def validate_name(form, name):
member = Member.query.filter_by(name=name.data).first()
if member is not None:
raise ValidationError('Pseudo indisponible.')
if not Member.valid_name(name.data):
raise ValidationError("Nom d'utilisateur invalide.")
def validate_email(form, email):
member = Member.query.filter_by(email=email.data).first()
@ -39,7 +41,7 @@ class LoginForm(FlaskForm):
class RegistrationForm(FlaskForm):
username = StringField('Pseudonyme :', validators=[DataRequired(), validate_username])
username = StringField('Pseudonyme :', validators=[DataRequired(), validate_name])
email = StringField('Adresse Email :', validators=[DataRequired(), Email(), validate_email])
password = PasswordField('Mot de passe', validators=[DataRequired(), validate_password])
password2 = PasswordField('Répéter le mot de passe', validators=[DataRequired(), EqualTo('password')])

View File

@ -1,122 +1,208 @@
from datetime import date, datetime
from app import db, login
from app import db
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
from app.models.contents import Content
import werkzeug.security
import app
import re
# User: Website user that performs actions on the content
class User(UserMixin, db.Model):
__tablename__ = 'user'
# User ID, should be used to refer to any user. Thea actual user can either
# be a guest (with IP as key) or a member (with this ID as key).
id = db.Column(db.Integer, primary_key=True)
type = db.Column(db.String(20))
# User type (polymorphic discriminator)
type = db.Column(db.String(30))
# TODO: add good relation
contents = db.relationship('Content', back_populates="author")
__mapper_args__ = {
'polymorphic_identity': __tablename__,
'polymorphic_on': type
}
# TODO: add good relation
contents = db.relationship('Content', back_populates="author")
def __repr__(self):
return f'<User #{self.id}>'
# Guest: Unregistered user with minimal privileges
class Guest(User, db.Model):
__tablename__ = 'guest'
__mapper_args__ = { 'polymorphic_identity': __tablename__ }
# ID of the [User] entry
id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True)
# Reusable username, can be the name of a member (will be distinguished at
# rendering time)
username = db.Column(db.Unicode(64), index=True)
# IP address, 47 characters is the max for an IPv6 address
ip = db.Column(db.String(47))
def __repr__(self):
return f'<Guest: {self.username} ({self.ip})>'
# Member: Registered user with full access to the website's services
class Member(User, db.Model):
__tablename__ = 'member'
__mapper_args__ = { 'polymorphic_identity': __tablename__ }
# Id of the [User] entry
id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True)
# Standalone properties
username = db.Column(db.Unicode(64), index=True, unique=True)
email = db.Column(db.String(120), index=True, unique=True)
# Primary attributes (needed for the system to work)
name = db.Column(db.Unicode(32), index=True, unique=True)
email = db.Column(db.Unicode(120), index=True, unique=True)
password_hash = db.Column(db.String(255))
xp_points = db.Column(db.Integer)
innovation_points = db.Column(db.Integer)
biography = db.Column(db.Text(convert_unicode=True))
signature = db.Column(db.Text(convert_unicode=True))
birthday = db.Column(db.Date)
xp = db.Column(db.Integer)
innovation = db.Column(db.Integer)
register_date = db.Column(db.Date, default=date.today)
# Personal information, all optional
bio = db.Column(db.UnicodeText)
signature = db.Column(db.UnicodeText)
birthday = db.Column(db.Date)
# Settings
newsletter = db.Column(db.Boolean, default=False)
# Relations
#trophies = db.relationship('Trophy') # TODO: add good relation
#tests = db.relationship('Test') # TODO: add good relation
# Privacy assets
receive_newsletter = db.Column(db.Boolean, default=False)
# trophies = db.relationship('Trophy', back_populates='member')
# tests = db.relationship('Test', back_populates='author')
@staticmethod
def valid_name(name):
"""
Checks whether a string is a valid member name. The criteria are:
1. No whitespace-class character
2. At least one letter
3. No longer than 32 characters
def __init__(self, username, email, password):
self.username = username
Possibily other intresting criteria:
4. Unicode restriction
"""
if type(name) != str or len(name) > 32:
return False
# Reject all Unicode whitespaces. This is important to avoid the most
# common Unicode tricks!
if re.search(r'\s', name) is not None:
return False
# There must be at least one letter (avoid complete garbage)
if re.search(r'\w', name) is None:
return False
return True
def __init__(self, name, email, password):
"""Register a new user."""
if not Member.valid_name(name):
raise Exception(f'{name} is not a valid user name')
self.name = name
self.email = email
self.set_password(password)
self.xp = 0
self.innovation = 0
self.bio = ""
self.signature = ""
self.birthday = None
def update(self, data):
"""
Update all or part of the user's metadata. The [data] dictionary
accepts the following keys:
"name" str User name
"email" str User mail ddress
"password" str Raw password
"bio" str Biograpy
"signature" str Post signature
"birthday" date Birthday date
"newsletter" bool Newsletter setting
For future compatibility, other attributes are silently ignored. None
values can be specified and are ignored.
It is the caller's responsibility to check that the request sender has
the right to change user names, password... otherwise this method will
turn out dangerous!
"""
data = { key: data[key] for key in data if data[key] is not None }
if "name" in data:
if not Member.valid_name(data["name"]):
raise Exception(f'{data["name"]} is not a valid user name')
self.name = data["name"]
def update(self, username=None, email=None, password=None,
biography=None, signature=None, birthday=None,
receive_newsletter=None):
# TODO: verify good type of those args, think about the password mgt
if username:
self.username = username
if email:
self.email = email
if password:
self.set_password(password)
if biography:
self.biography = biography
if signature:
self.signature = signature
if birthday:
self.birthday = birthday
if receive_newsletter:
self.receive_newsletter = receive_newsletter
if "email" in data:
self.email = data["email"]
if "password" in data:
self.set_password(data["password"])
if "bio" in data:
self.biography = data["bio"]
if "signature" in data:
self.signature = data["signature"]
if "birthday" in data:
self.birthday = data["birthday"]
if "newsletter" in data:
self.newsletter = data["newsletter"]
def get_public_data(self, username=False, xp_points=False,
innovation_points=False, biography=None, signature=None,
birthday=None, register_date=False):
out = {}
if username:
out['username'] = username
if xp_points:
out['xp_points'] = xp_points
if innovation_points:
out['innovation_points'] = innovation_points
if biography:
out['biography'] = biography
if signature:
out['signature'] = signature
if birthday:
out['birthday'] = birthday
if register_date:
out['register_date'] = register_date
def get_public_data(self):
"""Returns the public information of the member."""
return {
"name": self.name,
"xp": self.xp,
"innovation": self.innovation,
"register_date": self.register_date,
"bio": self.bio,
"signature": self.signature,
"birthday": self.birthday,
}
def add_xp(self, n):
self.xp_points += n
def add_xp(self, amount):
"""
Reward xp to a member. If [amount] is negative, the xp total of the
member will decrease, down to zero.
"""
self.xp_points = max(self.xp_points + amount, 0)
def add_innovation(self, n):
self.innovation_points += n
"""
Reward innovation points to a member. If [amount] is negative, the
innovation points total will decrease, down to zero.
"""
self.innovation = max(self.innovation + amount, 0)
def set_password(self, password):
self.password_hash = generate_password_hash(password,
"""
Set the user's password. Check whether the request sender has the right
to do this!
"""
self.password_hash = werkzeug.security.generate_password_hash(password,
method='pbkdf2:sha512', salt_length=10)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
"""Compares password against member hash."""
return werkzeug.security.check_password_hash(self.password_hash,
password)
def __repr__(self):
return f'<Member {self.username}>'
return f'<Member: {self.name}>'
@login.user_loader
@app.login.user_loader
def load_user(id):
return User.query.get(int(id))
class Guest(User, db.Model):
__tablename__ = 'guest'
id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True)
__mapper_args__ = { 'polymorphic_identity': __tablename__ }
# Standalone propeties
username = db.Column(db.Unicode(64), index=True)
ip = db.Column(db.String(47)) # Max IPv6 adress length
last_post = db.Column(db.DateTime, default=datetime.now)
class Group(db.Model):
__tablename__ = 'group'
def __repr__(self):
return f'<Guest {self.username} ({self.ip})>'
# Can be implemented if needed
# class Organization(User, db.Model):
# pass
id = db.Column(db.Integer, primary_key=True)
# Full name, such as "Administrateur" or "Membre d'honneur".
name = db.Column(db.Unicode(50), unique=True)
# The CSS code should not assume any specific layout and typically applies
# to a text node. Use attributes like color, font-style, font-weight, etc.
css = db.Column(db.UnicodeText)

View File

@ -27,7 +27,7 @@ def login():
return redirect(url_for('index'))
login_form = LoginForm()
if login_form.validate_on_submit():
member = Member.query.filter_by(username=login_form.username.data).first()
member = Member.query.filter_by(name=login_form.username.data).first()
if member is None or not member.check_password(login_form.password.data):
flash('Pseudo ou mot de passe invalide', 'error')
return redirect(request.referrer)
@ -50,13 +50,14 @@ def account():
form = UpdateAccountForm()
if request.method == "POST":
if form.validate_on_submit():
current_user.update(
email = form.email.data,
password = form.password.data,
signature = form.signature.data,
biography = form.biography.data,
birthday = form.birthday.data,
receive_newsletter = form.newsletter.data)
current_user.update({
"email": form.email.data,
"password": form.password.data,
"signature": form.signature.data,
"bio": form.biography.data,
"birthday": form.birthday.data,
"newsletter": form.newsletter.data
})
db.session.add(current_user)
db.session.commit()
flash('Modifications effectuées', 'ok')
@ -71,8 +72,6 @@ def register():
form = RegistrationForm()
if form.validate_on_submit():
member = Member(form.username.data, form.email.data, form.password.data)
member.biography = ""
member.signature = ""
db.session.add(member)
db.session.commit()
flash('Inscription réussie', 'ok')

View File

@ -6,7 +6,7 @@
<form action="" method="post">
{{ form.hidden_tag() }}
<div>
{{ form.avatar.label }}
<div>
@ -51,7 +51,7 @@
</div>
<div>
{{ form.biography.label }}
<textarea id="{{ form.biography.name }}" name="{{ form.biography.name }}">{{ current_user.biography }}</textarea>
<textarea id="{{ form.biography.name }}" name="{{ form.biography.name }}">{{ current_user.bio }}</textarea>
{% for error in form.biography.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}

View File

@ -1,8 +1,5 @@
<footer>
<p>Planète Casio est un site communautaire indépendant, géré bénévolement et n'est donc pas
affilié à Casio | Toute reproduction de Planète Casio, même partielle, est interdite.</p>
<p>Les fichiers, programmes et autres publications présents sur Planète Casio restent la
propriété de leurs auteurs respectifs et peuvent être soumis à des licences ou des
copyrights.</p>
<p>Planète Casio est un site communautaire non affilié à Casio | Toute reproduction de Planète Casio, même partielle, est interdite.</p>
<p>Les programmes et autres publications présentes sur Planète Casio restent la propriété de leurs auteurs et peuvent être soumis à des licences ou des copyrights.</p>
<p>CASIO est une marque déposée par CASIO Computer Co., Ltd.</p>
</footer>

View File

@ -2,7 +2,7 @@
<div>
<h2>
<img src="{{ url_for('static', filename= 'images/3864.png') }}">
{{ current_user.username }}
{{ current_user.name }}
</h2>
<a href="#">
<svg viewBox="0 0 24 24">

View File

@ -10,6 +10,11 @@ User information:
Points Participation measure (mainly number of posts)
Innovation points A different kind of participation measure
Biography Description of the user
Signature Short signature added at the end of every post
Birthday Birthday date
Newsletter Subscription to newsletter
Settings...
Relations:

View File

@ -1,11 +1,7 @@
import os
basedir = os.path.abspath(os.path.dirname(__file__))
class Config(object):
SECRET_KEY = os.environ.get('SECRET_KEY') or 'a-random-secret-key'
# SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
# 'sqlite:///' + os.path.join(basediru, 'app.db')
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
'postgresql+psycopg2://' + os.environ.get('USER') + ':@/pcv5'
SQLALCHEMY_TRACK_MODIFICATIONS = False

View File

@ -0,0 +1,36 @@
"""empty message
Revision ID: 7697154d0a4b
Revises: 7dfd5e3aa1fb
Create Date: 2019-02-02 20:54:59.683353
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '7697154d0a4b'
down_revision = '7dfd5e3aa1fb'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('group',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.Unicode(length=50), nullable=True),
sa.Column('css', sa.UnicodeText(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
op.drop_column('guest', 'last_post')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('guest', sa.Column('last_post', postgresql.TIMESTAMP(), autoincrement=False, nullable=True))
op.drop_table('group')
# ### end Alembic commands ###

View File

@ -0,0 +1,50 @@
"""empty message
Revision ID: d2c96bebc596
Revises: 7697154d0a4b
Create Date: 2019-02-02 21:37:53.802259
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'd2c96bebc596'
down_revision = '7697154d0a4b'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('member', sa.Column('bio', sa.UnicodeText(), nullable=True))
op.add_column('member', sa.Column('innovation', sa.Integer(), nullable=True))
op.add_column('member', sa.Column('name', sa.Unicode(length=32), nullable=True))
op.add_column('member', sa.Column('newsletter', sa.Boolean(), nullable=True))
op.add_column('member', sa.Column('xp', sa.Integer(), nullable=True))
op.create_index(op.f('ix_member_name'), 'member', ['name'], unique=True)
op.drop_index('ix_member_username', table_name='member')
op.drop_column('member', 'xp_points')
op.drop_column('member', 'receive_newsletter')
op.drop_column('member', 'biography')
op.drop_column('member', 'username')
op.drop_column('member', 'innovation_points')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('member', sa.Column('innovation_points', sa.INTEGER(), autoincrement=False, nullable=True))
op.add_column('member', sa.Column('username', sa.VARCHAR(length=64), autoincrement=False, nullable=True))
op.add_column('member', sa.Column('biography', sa.TEXT(), autoincrement=False, nullable=True))
op.add_column('member', sa.Column('receive_newsletter', sa.BOOLEAN(), autoincrement=False, nullable=True))
op.add_column('member', sa.Column('xp_points', sa.INTEGER(), autoincrement=False, nullable=True))
op.create_index('ix_member_username', 'member', ['username'], unique=True)
op.drop_index(op.f('ix_member_name'), table_name='member')
op.drop_column('member', 'xp')
op.drop_column('member', 'newsletter')
op.drop_column('member', 'name')
op.drop_column('member', 'innovation')
op.drop_column('member', 'bio')
# ### end Alembic commands ###