PCv5/app/models/users.py

371 lines
12 KiB
Python

from datetime import date
from app import db
from flask import flash
from flask_login import UserMixin
from app.models.post import Post
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 post
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
posts = db.relationship('Post', backref="author", lazy=False)
__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', secondary=TrophyMember,
back_populates='owners')
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. Check whether the request sender has
the right to do this!
"""
if type(t) == int:
t = Trophy.query.get(t)
if type(t) == str:
t = Trophy.query.filter_by(name=t).first()
if t not in self.trophies:
self.trophies.append(t)
# TODO: implement the notification system
# self.notify(f"Vous venez de débloquer le trophée '{t.name}'")
def del_trophy(self, t):
"""
Add a trophy to the current user. Check whether the request sender has
the right to do this!
"""
if type(t) == int:
t = Trophy.query.get(t)
if type(t) == str:
t = Trophy.query.filter_by(name=name).first()
if t in self.trophies:
self.trophies.remove(t)
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-art
- on-program-tested
- on-program-rewarded
- on-login
- on-profile-update
"""
def progress(trophies, value):
"""Award or delete all trophies from a progressive category."""
for level in trophies:
if value >= level:
self.add_trophy(trophies[level])
else:
self.del_trophy(trophies[level])
if context in ["new-post", "new-program", "new-tutorial", "new-test",
None]:
# TODO: Amount of posts by the user
post_count = 0
levels = {
20: "Premiers mots",
500: "Beau parleur",
1500: "Plume infaillible",
5000: "Romancier émérite",
}
progress(levels, post_count)
if context in ["new-program", None]:
# TODO: Amount of programs by the user
program_count = 0
levels = {
5: "Programmeur du dimanche",
10: "Codeur invétéré",
20: "Je code donc je suis",
}
progress(levels, program_count)
if context in ["new-tutorial", None]:
# TODO: Number of tutorials by user
tutorial_count = 0
levels = {
5: "Pédagogue",
10: "Encyclopédie vivante",
25: "Guerrier du savoir",
}
progress(levels, tutorial_count)
if context in ["new-test", None]:
# TODO: Number of tests by user
test_count = 0
levels = {
5: "Testeur",
25: "Grand joueur",
100: "Hard tester",
}
progress(levels, test_count)
if context in ["new-event-participation", None]:
# TODO: Number of event participations by user
event_participations = 0
levels = {
1: "Participant",
5: "Concourant encore",
15: "Concurrent de l'extrême",
}
progress(levels, event_participations)
if context in ["new-art", None]:
# TODO: Number of art posts by user
art_count = 0
levels = {
5: "Dessinateur en herbe",
30: "Open pixel",
100: "Roi du pixel",
}
progress(levels, art_count)
if context in ["on-program-tested", None]:
# TODO: Number of "coups de coeur" of user
heart_count = 0
levels = {
5: "Bourreau des cœurs",
}
progress(levels, heart_count)
if context in ["on-program-rewarded", None]:
# TODO: Number of programs with labels
label_count = 0
levels = {
5: "Maître du code",
}
progress(levels, label_count)
if context in ["on-login", None]:
# Seniority-based trophies
age = date.today() - self.register_date
levels = {
30: "Initié",
365.25: "Aficionado",
365.25 * 2: "Veni, vidi, casii",
365.25 * 5: "Papy Casio",
365.25 * 10: "Vétéran mythique",
}
progress(levels, age.days)
# TODO: Trophy "actif"
if context in ["on-profile-update", None]:
# TODO: add a better condition (this is for test)
self.add_trophy("Artiste")
db.session.merge(self)
db.session.commit()
def __repr__(self):
return f'<Member: {self.name} ({self.norm})>'
@app.login.user_loader
def load_user(id):
return User.query.get(int(id))