Code source de Planète Casio https://planet-casio.com
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

users.py 7.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. from datetime import date, datetime
  2. from app import app, db
  3. from flask_login import UserMixin
  4. from app.models.contents import Content
  5. from app.models.privs import SpecialPrivilege, Group, GroupMember, \
  6. GroupPrivilege
  7. from config import V5Config
  8. import werkzeug.security
  9. import app
  10. import re
  11. # User: Website user that performs actions on the content
  12. class User(UserMixin, db.Model):
  13. __tablename__ = 'user'
  14. # User ID, should be used to refer to any user. Thea actual user can either
  15. # be a guest (with IP as key) or a member (with this ID as key).
  16. id = db.Column(db.Integer, primary_key=True)
  17. # User type (polymorphic discriminator)
  18. type = db.Column(db.String(30))
  19. # TODO: add good relation
  20. contents = db.relationship('Content', back_populates="author")
  21. __mapper_args__ = {
  22. 'polymorphic_identity': __tablename__,
  23. 'polymorphic_on': type
  24. }
  25. def __repr__(self):
  26. return f'<User #{self.id}>'
  27. @staticmethod
  28. def valid_name(name):
  29. """
  30. Checks whether a string is a valid user name. The criteria are:
  31. 1. No whitespace-class character
  32. 2. At least one letter
  33. 3. At least 3 characters and no longer than 32 characters
  34. Possibily other intresting criteria:
  35. 4. Unicode restriction
  36. """
  37. if type(name) != str or len(name) < 3 or len(name) > 32:
  38. return False
  39. if name in V5Config.FORBIDDEN_USERNAMES:
  40. return False
  41. # Reject all Unicode whitespaces. This is important to avoid the most
  42. # common Unicode tricks!
  43. if re.search(r'\s', name) is not None:
  44. return False
  45. # There must be at least one letter (avoid complete garbage)
  46. if re.search(r'\w', name) is None:
  47. return False
  48. return True
  49. # Guest: Unregistered user with minimal privileges
  50. class Guest(User, db.Model):
  51. __tablename__ = 'guest'
  52. __mapper_args__ = { 'polymorphic_identity': __tablename__ }
  53. # ID of the [User] entry
  54. id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True)
  55. # Reusable username, can be the name of a member (will be distinguished at
  56. # rendering time)
  57. username = db.Column(db.Unicode(64), index=True)
  58. # IP address, 47 characters is the max for an IPv6 address
  59. ip = db.Column(db.String(47))
  60. def __repr__(self):
  61. return f'<Guest: {self.username} ({self.ip})>'
  62. # Member: Registered user with full access to the website's services
  63. class Member(User, db.Model):
  64. __tablename__ = 'member'
  65. __mapper_args__ = { 'polymorphic_identity': __tablename__ }
  66. # Id of the [User] entry
  67. id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True)
  68. # Primary attributes (needed for the system to work)
  69. name = db.Column(db.Unicode(32), index=True, unique=True)
  70. email = db.Column(db.Unicode(120), index=True, unique=True)
  71. password_hash = db.Column(db.String(255))
  72. xp = db.Column(db.Integer)
  73. innovation = db.Column(db.Integer)
  74. register_date = db.Column(db.Date, default=date.today)
  75. # Avatars # TODO: rendre ça un peu plus propre
  76. @property
  77. def avatar(self):
  78. return 'avatars/' + str(self.id) + '.png'
  79. # Groups and related privileges
  80. groups = db.relationship('Group', secondary=GroupMember,
  81. back_populates='members')
  82. # Personal information, all optional
  83. bio = db.Column(db.UnicodeText)
  84. signature = db.Column(db.UnicodeText)
  85. birthday = db.Column(db.Date)
  86. # Settings
  87. newsletter = db.Column(db.Boolean, default=False)
  88. # Relations
  89. # trophies = db.relationship('Trophy', back_populates='member')
  90. # tests = db.relationship('Test', back_populates='author')
  91. def __init__(self, name, email, password):
  92. """Register a new user."""
  93. if not User.valid_name(name):
  94. raise Exception(f'{name} is not a valid user name')
  95. self.name = name
  96. self.email = email
  97. self.set_password(password)
  98. self.xp = 0
  99. self.innovation = 0
  100. self.bio = ""
  101. self.signature = ""
  102. self.birthday = None
  103. def priv(self, priv):
  104. """Check whether the member has the specified privilege."""
  105. if SpecialPrivilege.filter(uif=self.id, priv=priv):
  106. return True
  107. return False
  108. # return db.session.query(User, Group, GroupPrivilege).filter(
  109. # Group.id.in_(User.groups), GroupPrivilege.gid==Group.id,
  110. # GroupPrivilege.priv==priv).first() is not None
  111. def update(self, **data):
  112. """
  113. Update all or part of the user's metadata. The [data] dictionary
  114. accepts the following keys:
  115. "name" str User name
  116. "email" str User mail ddress
  117. "password" str Raw password
  118. "bio" str Biograpy
  119. "signature" str Post signature
  120. "birthday" date Birthday date
  121. "newsletter" bool Newsletter setting
  122. For future compatibility, other attributes are silently ignored. None
  123. values can be specified and are ignored.
  124. It is the caller's responsibility to check that the request sender has
  125. the right to change user names, password... otherwise this method will
  126. turn out dangerous!
  127. """
  128. data = { key: data[key] for key in data if data[key] is not None }
  129. if "name" in data:
  130. if not User.valid_name(data["name"]):
  131. raise Exception(f'{data["name"]} is not a valid user name')
  132. self.name = data["name"]
  133. # TODO: verify good type of those args, think about the password mgt
  134. if "email" in data:
  135. self.email = data["email"]
  136. if "password" in data:
  137. self.set_password(data["password"])
  138. if "bio" in data:
  139. self.biography = data["bio"]
  140. if "signature" in data:
  141. self.signature = data["signature"]
  142. if "birthday" in data:
  143. self.birthday = data["birthday"]
  144. if "newsletter" in data:
  145. self.newsletter = data["newsletter"]
  146. def get_public_data(self):
  147. """Returns the public information of the member."""
  148. return {
  149. "name": self.name,
  150. "xp": self.xp,
  151. "innovation": self.innovation,
  152. "register_date": self.register_date,
  153. "bio": self.bio,
  154. "signature": self.signature,
  155. "birthday": self.birthday,
  156. }
  157. def add_xp(self, amount):
  158. """
  159. Reward xp to a member. If [amount] is negative, the xp total of the
  160. member will decrease, down to zero.
  161. """
  162. self.xp_points = max(self.xp_points + amount, 0)
  163. def add_innovation(self, n):
  164. """
  165. Reward innovation points to a member. If [amount] is negative, the
  166. innovation points total will decrease, down to zero.
  167. """
  168. self.innovation = max(self.innovation + amount, 0)
  169. def set_password(self, password):
  170. """
  171. Set the user's password. Check whether the request sender has the right
  172. to do this!
  173. """
  174. self.password_hash = werkzeug.security.generate_password_hash(password,
  175. method='pbkdf2:sha512', salt_length=10)
  176. def check_password(self, password):
  177. """Compares password against member hash."""
  178. return werkzeug.security.check_password_hash(self.password_hash,
  179. password)
  180. def __repr__(self):
  181. return f'<Member: {self.name}>'
  182. @app.login.user_loader
  183. def load_user(id):
  184. return User.query.get(int(id))