8570b8660f
Adds a normalized name field to the user record. Also uses normalized names conflicts to deny new user names.
211 lines
6.9 KiB
Python
211 lines
6.9 KiB
Python
from datetime import date
|
|
from app import db
|
|
from flask_login import UserMixin
|
|
from app.models.contents import Content
|
|
from app.models.privs import SpecialPrivilege, Group, GroupMember, \
|
|
GroupPrivilege
|
|
import app.utils.unicode_names as unicode_names
|
|
from config import V5Config
|
|
|
|
import werkzeug.security
|
|
import re
|
|
import math
|
|
import app
|
|
|
|
# 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)
|
|
# 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
|
|
}
|
|
|
|
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)
|
|
|
|
# Primary attributes (needed for the system to work)
|
|
name = db.Column(db.Unicode(V5Config.USER_NAME_MAXLEN), index=True)
|
|
norm = db.Column(db.Unicode(V5Config.USER_NAME_MAXLEN), index=True,
|
|
unique=True)
|
|
email = db.Column(db.Unicode(120), index=True, unique=True)
|
|
password_hash = db.Column(db.String(255))
|
|
xp = db.Column(db.Integer)
|
|
register_date = db.Column(db.Date, default=date.today)
|
|
|
|
# Avatars # TODO: rendre ça un peu plus propre
|
|
@property
|
|
def avatar(self):
|
|
return 'avatars/' + str(self.id) + '.png'
|
|
|
|
@property
|
|
def level(self):
|
|
level = math.asinh(self.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')
|
|
|
|
# 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', back_populates='member')
|
|
# tests = db.relationship('Test', back_populates='author')
|
|
|
|
def __init__(self, name, email, password):
|
|
"""Register a new user."""
|
|
self.name = name
|
|
self.norm = unicode_names.normalize(name)
|
|
self.email = email
|
|
self.set_password(password)
|
|
self.xp = 0
|
|
|
|
self.bio = ""
|
|
self.signature = ""
|
|
self.birthday = None
|
|
|
|
def delete(self):
|
|
"""
|
|
Deletes the user and the associated information:
|
|
* Special privileges
|
|
"""
|
|
|
|
for sp in SpecialPrivilege.query.filter_by(mid=self.id).all():
|
|
db.session.delete(sp)
|
|
db.session.commit()
|
|
|
|
db.session.delete(self)
|
|
db.session.commit()
|
|
|
|
def priv(self, priv):
|
|
"""Check whether the member has the specified privilege."""
|
|
if SpecialPrivilege.query.filter_by(mid=self.id, priv=priv).first():
|
|
return True
|
|
return False
|
|
# return db.session.query(User, Group, GroupPrivilege).filter(
|
|
# Group.id.in_(User.groups), GroupPrivilege.gid==Group.id,
|
|
# GroupPrivilege.priv==priv).first() is not None
|
|
|
|
def special_privileges(self):
|
|
"""List member's special privileges."""
|
|
sp = SpecialPrivilege.query.filter_by(mid=self.id).all()
|
|
return sorted(row.priv for row in sp)
|
|
|
|
def update(self, **data):
|
|
"""
|
|
Update all or part of the user's metadata. The [data] dictionary
|
|
accepts the following keys:
|
|
"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}
|
|
|
|
# TODO: verify good type of those args, think about the password mgt
|
|
if "email" in data:
|
|
self.email = data["email"]
|
|
if "password" in data:
|
|
self.set_password(data["password"])
|
|
if "bio" in data:
|
|
self.bio = 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"]
|
|
|
|
# For admins only
|
|
if "xp" in data:
|
|
self.xp = data["xp"]
|
|
|
|
def get_public_data(self):
|
|
"""Returns the public information of the member."""
|
|
return {
|
|
"name": self.name,
|
|
"xp": self.xp,
|
|
"register_date": self.register_date,
|
|
"bio": self.bio,
|
|
"signature": self.signature,
|
|
"birthday": self.birthday,
|
|
}
|
|
|
|
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 = min(max(self.xp_points + amount, 0), 1000000000)
|
|
|
|
def set_password(self, 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):
|
|
"""Compares password against member hash."""
|
|
return werkzeug.security.check_password_hash(self.password_hash,
|
|
password)
|
|
|
|
def __repr__(self):
|
|
return f'<Member: {self.name}>'
|
|
|
|
|
|
@app.login.user_loader
|
|
def load_user(id):
|
|
return User.query.get(int(id))
|