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 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. from datetime import date
  2. from app import db
  3. from flask import flash
  4. from flask_login import UserMixin
  5. from app.models.contents import Content
  6. from app.models.privs import SpecialPrivilege, Group, GroupMember, \
  7. GroupPrivilege
  8. from app.models.trophies import Trophy, TrophyMember
  9. import app.utils.unicode_names as unicode_names
  10. from config import V5Config
  11. import werkzeug.security
  12. import re
  13. import math
  14. import app
  15. # User: Website user that performs actions on the content
  16. class User(UserMixin, db.Model):
  17. __tablename__ = 'user'
  18. # User ID, should be used to refer to any user. Thea actual user can either
  19. # be a guest (with IP as key) or a member (with this ID as key).
  20. id = db.Column(db.Integer, primary_key=True)
  21. # User type (polymorphic discriminator)
  22. type = db.Column(db.String(30))
  23. # TODO: add good relation
  24. contents = db.relationship('Content', back_populates="author")
  25. __mapper_args__ = {
  26. 'polymorphic_identity': __tablename__,
  27. 'polymorphic_on': type
  28. }
  29. def __repr__(self):
  30. return f'<User: #{self.id}>'
  31. # Guest: Unregistered user with minimal privileges
  32. class Guest(User, db.Model):
  33. __tablename__ = 'guest'
  34. __mapper_args__ = {'polymorphic_identity': __tablename__}
  35. # ID of the [User] entry
  36. id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True)
  37. # Reusable username, can be the name of a member (will be distinguished at
  38. # rendering time)
  39. username = db.Column(db.Unicode(64), index=True)
  40. # IP address, 47 characters is the max for an IPv6 address
  41. ip = db.Column(db.String(47))
  42. def __repr__(self):
  43. return f'<Guest: {self.username} ({self.ip})>'
  44. # Member: Registered user with full access to the website's services
  45. class Member(User, db.Model):
  46. __tablename__ = 'member'
  47. __mapper_args__ = {'polymorphic_identity': __tablename__}
  48. # Id of the [User] entry
  49. id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True)
  50. # Primary attributes (needed for the system to work)
  51. name = db.Column(db.Unicode(V5Config.USER_NAME_MAXLEN), index=True)
  52. norm = db.Column(db.Unicode(V5Config.USER_NAME_MAXLEN), index=True,
  53. unique=True)
  54. email = db.Column(db.Unicode(120), index=True, unique=True)
  55. password_hash = db.Column(db.String(255))
  56. xp = db.Column(db.Integer)
  57. register_date = db.Column(db.Date, default=date.today)
  58. # Avatars # TODO: rendre ça un peu plus propre
  59. @property
  60. def avatar(self):
  61. return 'avatars/' + str(self.id) + '.png'
  62. @property
  63. def level(self):
  64. level = math.asinh(self.xp / 1000) * (100 / math.asinh(10))
  65. return int(level), int(level * 100) % 100
  66. # Groups and related privileges
  67. groups = db.relationship('Group', secondary=GroupMember,
  68. back_populates='members')
  69. # Personal information, all optional
  70. bio = db.Column(db.UnicodeText)
  71. signature = db.Column(db.UnicodeText)
  72. birthday = db.Column(db.Date)
  73. # Settings
  74. newsletter = db.Column(db.Boolean, default=False)
  75. # Relations
  76. trophies = db.relationship('Trophy', secondary=TrophyMember,
  77. back_populates='owners')
  78. # tests = db.relationship('Test', back_populates='author')
  79. def __init__(self, name, email, password):
  80. """Register a new user."""
  81. self.name = name
  82. self.norm = unicode_names.normalize(name)
  83. self.email = email
  84. self.set_password(password)
  85. self.xp = 0
  86. self.bio = ""
  87. self.signature = ""
  88. self.birthday = None
  89. def delete(self):
  90. """
  91. Deletes the user and the associated information:
  92. * Special privileges
  93. """
  94. for sp in SpecialPrivilege.query.filter_by(mid=self.id).all():
  95. db.session.delete(sp)
  96. db.session.commit()
  97. db.session.delete(self)
  98. db.session.commit()
  99. def priv(self, priv):
  100. """Check whether the member has the specified privilege."""
  101. if SpecialPrivilege.query.filter_by(mid=self.id, priv=priv).first():
  102. return True
  103. return db.session.query(Group, GroupPrivilege).filter(
  104. Group.id.in_([ g.id for g in self.groups ]),
  105. GroupPrivilege.gid==Group.id,
  106. GroupPrivilege.priv==priv).first() is not None
  107. def special_privileges(self):
  108. """List member's special privileges."""
  109. sp = SpecialPrivilege.query.filter_by(mid=self.id).all()
  110. return sorted(row.priv for row in sp)
  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. "email" str User mail ddress
  116. "password" str Raw password
  117. "bio" str Biograpy
  118. "signature" str Post signature
  119. "birthday" date Birthday date
  120. "newsletter" bool Newsletter setting
  121. For future compatibility, other attributes are silently ignored. None
  122. values can be specified and are ignored.
  123. It is the caller's responsibility to check that the request sender has
  124. the right to change user names, password... otherwise this method will
  125. turn out dangerous!
  126. """
  127. data = {key: data[key] for key in data if data[key] is not None}
  128. # TODO: verify good type of those args, think about the password mgt
  129. if "email" in data:
  130. self.email = data["email"]
  131. if "password" in data:
  132. self.set_password(data["password"])
  133. if "bio" in data:
  134. self.bio = data["bio"]
  135. if "signature" in data:
  136. self.signature = data["signature"]
  137. if "birthday" in data:
  138. self.birthday = data["birthday"]
  139. if "newsletter" in data:
  140. self.newsletter = data["newsletter"]
  141. # For admins only
  142. if "xp" in data:
  143. self.xp = data["xp"]
  144. def get_public_data(self):
  145. """Returns the public information of the member."""
  146. return {
  147. "name": self.name,
  148. "xp": self.xp,
  149. "register_date": self.register_date,
  150. "bio": self.bio,
  151. "signature": self.signature,
  152. "birthday": self.birthday,
  153. }
  154. def add_xp(self, amount):
  155. """
  156. Reward xp to a member. If [amount] is negative, the xp total of the
  157. member will decrease, down to zero.
  158. """
  159. self.xp_points = min(max(self.xp_points + amount, 0), 1000000000)
  160. def set_password(self, password):
  161. """
  162. Set the user's password. Check whether the request sender has the right
  163. to do this!
  164. """
  165. self.password_hash = werkzeug.security.generate_password_hash(password,
  166. method='pbkdf2:sha512', salt_length=10)
  167. def check_password(self, password):
  168. """Compares password against member hash."""
  169. return werkzeug.security.check_password_hash(self.password_hash,
  170. password)
  171. def add_trophy(self, t):
  172. """
  173. Add a trophy to the current user. Check whether the request sender has
  174. the right to do this!
  175. """
  176. if type(t) == int:
  177. t = Trophy.query.get(t)
  178. if type(t) == str:
  179. t = Trophy.query.filter_by(name=t).first()
  180. if t not in self.trophies:
  181. self.trophies.append(t)
  182. # TODO: implement the notification system
  183. # self.notify(f"Vous venez de débloquer le trophée '{t.name}'")
  184. def del_trophy(self, t):
  185. """
  186. Add a trophy to the current user. Check whether the request sender has
  187. the right to do this!
  188. """
  189. if type(t) == int:
  190. t = Trophy.query.get(t)
  191. if type(t) == str:
  192. t = Trophy.query.filter_by(name=name).first()
  193. if t in self.trophies:
  194. self.trophies.remove(t)
  195. def update_trophies(self, context=None):
  196. """
  197. Auto-update trophies for the current user. Please use one of the
  198. following contexts when possible:
  199. - new-post
  200. - new-program
  201. - new-tutorial
  202. - new-test
  203. - new-event-participation
  204. - new-art
  205. - on-program-tested
  206. - on-program-rewarded
  207. - on-login
  208. - on-profile-update
  209. """
  210. def progress(trophies, value):
  211. """Award or delete all trophies from a progressive category."""
  212. for level in trophies:
  213. if value >= level:
  214. self.add_trophy(trophies[level])
  215. else:
  216. self.del_trophy(trophies[level])
  217. if context in ["new-post", "new-program", "new-tutorial", "new-test",
  218. None]:
  219. # TODO: Amount of posts by the user
  220. post_count = 0
  221. levels = {
  222. 20: "Premiers mots",
  223. 500: "Beau parleur",
  224. 1500: "Plume infaillible",
  225. 5000: "Romancier émérite",
  226. }
  227. progress(levels, post_count)
  228. if context in ["new-program", None]:
  229. # TODO: Amount of programs by the user
  230. program_count = 0
  231. levels = {
  232. 5: "Programmeur du dimanche",
  233. 10: "Codeur invétéré",
  234. 20: "Je code donc je suis",
  235. }
  236. progress(levels, program_count)
  237. if context in ["new-tutorial", None]:
  238. # TODO: Number of tutorials by user
  239. tutorial_count = 0
  240. levels = {
  241. 5: "Pédagogue",
  242. 10: "Encyclopédie vivante",
  243. 25: "Guerrier du savoir",
  244. }
  245. progress(levels, tutorial_count)
  246. if context in ["new-test", None]:
  247. # TODO: Number of tests by user
  248. test_count = 0
  249. levels = {
  250. 5: "Testeur",
  251. 25: "Grand joueur",
  252. 100: "Hard tester",
  253. }
  254. progress(levels, test_count)
  255. if context in ["new-event-participation", None]:
  256. # TODO: Number of event participations by user
  257. event_participations = 0
  258. levels = {
  259. 1: "Participant",
  260. 5: "Concourant encore",
  261. 15: "Concurrent de l'extrême",
  262. }
  263. progress(levels, event_participations)
  264. if context in ["new-art", None]:
  265. # TODO: Number of art posts by user
  266. art_count = 0
  267. levels = {
  268. 5: "Dessinateur en herbe",
  269. 30: "Open pixel",
  270. 100: "Roi du pixel",
  271. }
  272. progress(levels, art_count)
  273. if context in ["on-program-tested", None]:
  274. # TODO: Number of "coups de coeur" of user
  275. heart_count = 0
  276. levels = {
  277. 5: "Bourreau des cœurs",
  278. }
  279. progress(levels, heart_count)
  280. if context in ["on-program-rewarded", None]:
  281. # TODO: Number of programs with labels
  282. label_count = 0
  283. levels = {
  284. 5: "Maître du code",
  285. }
  286. progress(levels, label_count)
  287. if context in ["on-login", None]:
  288. # Seniority-based trophies
  289. age = date.today() - self.register_date
  290. levels = {
  291. 30: "Initié",
  292. 365.25: "Aficionado",
  293. 365.25 * 2: "Veni, vidi, casii",
  294. 365.25 * 5: "Papy Casio",
  295. 365.25 * 10: "Vétéran mythique",
  296. }
  297. progress(levels, age.days)
  298. # TODO: Trophy "actif"
  299. if context in ["on-profile-update", None]:
  300. # TODO: add a better condition (this is for test)
  301. self.add_trophy("Artiste")
  302. db.session.merge(self)
  303. db.session.commit()
  304. def __repr__(self):
  305. return f'<Member: {self.name} ({self.norm})>'
  306. @app.login.user_loader
  307. def load_user(id):
  308. return User.query.get(int(id))