|
|
@@ -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) |
|
|
|
# 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) |
|
|
|
|
|
|
|
# Personal information, all optional |
|
|
|
bio = db.Column(db.UnicodeText) |
|
|
|
signature = db.Column(db.UnicodeText) |
|
|
|
birthday = db.Column(db.Date) |
|
|
|
|
|
|
|
def __init__(self, username, email, password): |
|
|
|
self.username = username |
|
|
|
# Settings |
|
|
|
newsletter = db.Column(db.Boolean, default=False) |
|
|
|
|
|
|
|
# Relations |
|
|
|
# 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 |
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
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 add_xp(self, n): |
|
|
|
self.xp_points += n |
|
|
|
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): |
|
|
|
"""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, 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) |
|
|
|
|
|
|
|
|
|
|
|
def __repr__(self): |
|
|
|
return f'<Guest {self.username} ({self.ip})>' |
|
|
|
class Group(db.Model): |
|
|
|
__tablename__ = 'group' |
|
|
|
|
|
|
|
# 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) |