from datetime import date from app import db from flask import flash from flask_login import UserMixin from app.models.contents import Content from app.models.privs import SpecialPrivilege, Group, GroupMember, \ GroupPrivilege from app.models.trophies import Trophy, TrophyMember 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'' # 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'' # 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', secondary=TrophyMember, back_populates='owners') # 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 db.session.query(Group, GroupPrivilege).filter( Group.id.in_([ g.id for g in self.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 add_trophy(self, t): """ Add a trophy to the current user. heck whether the request sender has the right to do this! """ if type(t) == str: t = Trophy.query.filter_by(name=name).first() if t not in self.trophies: self.trophies.append(t) db.session.merge(self) db.session.commit() # TODO: implement the notification system # self.notify(f"Vous venez de débloquer le trophée '{name}'") def del_trophy(self, t): """ Add a trophy to the current user. heck whether the request sender has the right to do this! """ if type(t) == str: t = Trophy.query.filter_by(name=name).first() if t in self.trophies: self.trophies.remove(t) db.session.merge(self) db.session.commit() def update_trophies(self, context=None): """ Auto-update trophies for the current user. Please use one of the following contexts when possible: - new-post - new-program - new-tutorial - new-test - new-event-participation - new-picture - on-program-reward - on-login - on-profile-update """ if context == "new-post" or context is None: pass if context == "new-program" or context is None: pass if context == "new-tutorial" or context is None: pass if context == "new-test" or context is None: pass if context == "new-event-participation" or context is None: pass if context == "new-picture" or context is None: pass if context == "on-program-reward" or context is None: pass if context == "on-login" or context is None: # Seniority-based trophies age = date.today() - self.register_date if age.days > 30: self.add_trophy("Nouveau") if age.days > 365.25: self.add_trophy("Aficionado") if age.days > 365.25 * 2: self.add_trophy("Veni, vidi, casii") if age.days > 365.25 * 5: self.add_trophy("Papy Casio") if context == "on-profile-update" or context is None: pass def __repr__(self): return f'' @app.login.user_loader def load_user(id): return User.query.get(int(id))