La version 5 de Planète Casio. Regroupe le forum, les programmes, les tutoriel, les sprites et tous les autres outils développés par nos soins.
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 8.4KB

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