models.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. # -*- coding: utf-8 -*-
  2. """
  3. flaskbb.user.models
  4. ~~~~~~~~~~~~~~~~~~~~
  5. This module provides the models for the user.
  6. :copyright: (c) 2014 by the FlaskBB Team.
  7. :license: BSD, see LICENSE for more details.
  8. """
  9. import sys
  10. from datetime import datetime
  11. from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
  12. from itsdangerous import SignatureExpired
  13. from werkzeug import generate_password_hash, check_password_hash
  14. from flask import current_app, url_for
  15. from flask.ext.login import UserMixin, AnonymousUserMixin
  16. from flaskbb.extensions import db, cache
  17. from flaskbb.utils.settings import flaskbb_config
  18. from flaskbb.forum.models import (Post, Topic, topictracker, TopicsRead,
  19. ForumsRead)
  20. groups_users = db.Table(
  21. 'groups_users',
  22. db.Column('user_id', db.Integer(), db.ForeignKey('users.id')),
  23. db.Column('group_id', db.Integer(), db.ForeignKey('groups.id')))
  24. class Group(db.Model):
  25. __tablename__ = "groups"
  26. id = db.Column(db.Integer, primary_key=True)
  27. name = db.Column(db.String(255), unique=True, nullable=False)
  28. description = db.Column(db.Text)
  29. # Group types
  30. admin = db.Column(db.Boolean, default=False, nullable=False)
  31. super_mod = db.Column(db.Boolean, default=False, nullable=False)
  32. mod = db.Column(db.Boolean, default=False, nullable=False)
  33. guest = db.Column(db.Boolean, default=False, nullable=False)
  34. banned = db.Column(db.Boolean, default=False, nullable=False)
  35. # Moderator permissions (only available when the user a moderator)
  36. mod_edituser = db.Column(db.Boolean, default=False, nullable=False)
  37. mod_banuser = db.Column(db.Boolean, default=False, nullable=False)
  38. # User permissions
  39. editpost = db.Column(db.Boolean, default=True, nullable=False)
  40. deletepost = db.Column(db.Boolean, default=False, nullable=False)
  41. deletetopic = db.Column(db.Boolean, default=False, nullable=False)
  42. posttopic = db.Column(db.Boolean, default=True, nullable=False)
  43. postreply = db.Column(db.Boolean, default=True, nullable=False)
  44. # Methods
  45. def __repr__(self):
  46. """Set to a unique key specific to the object in the database.
  47. Required for cache.memoize() to work across requests.
  48. """
  49. return "<{} {}>".format(self.__class__.__name__, self.id)
  50. def save(self):
  51. """Saves a group"""
  52. db.session.add(self)
  53. db.session.commit()
  54. return self
  55. def delete(self):
  56. """Deletes a group"""
  57. db.session.delete(self)
  58. db.session.commit()
  59. return self
  60. class User(db.Model, UserMixin):
  61. __tablename__ = "users"
  62. __searchable__ = ['username', 'email']
  63. id = db.Column(db.Integer, primary_key=True)
  64. username = db.Column(db.String(200), unique=True, nullable=False)
  65. email = db.Column(db.String(200), unique=True, nullable=False)
  66. _password = db.Column('password', db.String(88), nullable=False)
  67. date_joined = db.Column(db.DateTime, default=datetime.utcnow())
  68. lastseen = db.Column(db.DateTime, default=datetime.utcnow())
  69. birthday = db.Column(db.DateTime)
  70. gender = db.Column(db.String(10))
  71. website = db.Column(db.String(200))
  72. location = db.Column(db.String(100))
  73. signature = db.Column(db.Text)
  74. avatar = db.Column(db.String(200))
  75. notes = db.Column(db.Text)
  76. theme = db.Column(db.String(15))
  77. posts = db.relationship("Post", backref="user", lazy="dynamic")
  78. topics = db.relationship("Topic", backref="user", lazy="dynamic")
  79. post_count = db.Column(db.Integer, default=0)
  80. primary_group_id = db.Column(db.Integer, db.ForeignKey('groups.id'),
  81. nullable=False)
  82. primary_group = db.relationship('Group', lazy="joined",
  83. backref="user_group", uselist=False,
  84. foreign_keys=[primary_group_id])
  85. secondary_groups = \
  86. db.relationship('Group',
  87. secondary=groups_users,
  88. primaryjoin=(groups_users.c.user_id == id),
  89. backref=db.backref('users', lazy='dynamic'),
  90. lazy='dynamic')
  91. tracked_topics = \
  92. db.relationship("Topic", secondary=topictracker,
  93. primaryjoin=(topictracker.c.user_id == id),
  94. backref=db.backref("topicstracked", lazy="dynamic"),
  95. lazy="dynamic")
  96. # Properties
  97. @property
  98. def last_post(self):
  99. """Returns the latest post from the user"""
  100. return Post.query.filter(Post.user_id == self.id).\
  101. order_by(Post.date_created.desc()).first()
  102. @property
  103. def url(self):
  104. """Returns the url for the user"""
  105. return url_for("user.profile", username=self.username)
  106. @property
  107. def permissions(self):
  108. """Returns the permissions for the user"""
  109. return self.get_permissions()
  110. @property
  111. def days_registered(self):
  112. """Returns the amount of days the user is registered."""
  113. days_registered = (datetime.utcnow() - self.date_joined).days
  114. if not days_registered:
  115. return 1
  116. return days_registered
  117. @property
  118. def topic_count(self):
  119. """Returns the thread count"""
  120. return Topic.query.filter(Topic.user_id == self.id).count()
  121. @property
  122. def posts_per_day(self):
  123. """Returns the posts per day count"""
  124. return round((float(self.post_count) / float(self.days_registered)), 1)
  125. @property
  126. def topics_per_day(self):
  127. """Returns the topics per day count"""
  128. return round((float(self.topic_count) / float(self.days_registered)), 1)
  129. # Methods
  130. def __repr__(self):
  131. """Set to a unique key specific to the object in the database.
  132. Required for cache.memoize() to work across requests.
  133. """
  134. return "<{} {}>".format(self.__class__.__name__, self.username)
  135. def _get_password(self):
  136. """Returns the hashed password"""
  137. return self._password
  138. def _set_password(self, password):
  139. """Generates a password hash for the provided password"""
  140. self._password = generate_password_hash(password)
  141. password = db.synonym('_password',
  142. descriptor=property(_get_password,
  143. _set_password))
  144. def check_password(self, password):
  145. """Check passwords. If passwords match it returns true, else false"""
  146. if self.password is None:
  147. return False
  148. return check_password_hash(self.password, password)
  149. @classmethod
  150. def authenticate(cls, login, password):
  151. """A classmethod for authenticating users
  152. It returns true if the user exists and has entered a correct password
  153. :param login: This can be either a username or a email address.
  154. :param password: The password that is connected to username and email.
  155. """
  156. user = cls.query.filter(db.or_(User.username == login,
  157. User.email == login)).first()
  158. if user:
  159. authenticated = user.check_password(password)
  160. else:
  161. authenticated = False
  162. return user, authenticated
  163. def _make_token(self, data, timeout):
  164. s = Serializer(current_app.config['SECRET_KEY'], timeout)
  165. return s.dumps(data)
  166. def _verify_token(self, token):
  167. s = Serializer(current_app.config['SECRET_KEY'])
  168. data = None
  169. expired, invalid = False, False
  170. try:
  171. data = s.loads(token)
  172. except SignatureExpired:
  173. expired = True
  174. except Exception:
  175. invalid = True
  176. return expired, invalid, data
  177. def make_reset_token(self, expiration=3600):
  178. """Creates a reset token. The duration can be configured through the
  179. expiration parameter.
  180. :param expiration: The time in seconds how long the token is valid.
  181. """
  182. return self._make_token({'id': self.id, 'op': 'reset'}, expiration)
  183. def verify_reset_token(self, token):
  184. """Verifies a reset token. It returns three boolean values based on
  185. the state of the token (expired, invalid, data)
  186. :param token: The reset token that should be checked.
  187. """
  188. expired, invalid, data = self._verify_token(token)
  189. if data and data.get('id') == self.id and data.get('op') == 'reset':
  190. data = True
  191. else:
  192. data = False
  193. return expired, invalid, data
  194. def all_topics(self, page):
  195. """Returns a paginated result with all topics the user has created."""
  196. return Topic.query.filter(Topic.user_id == self.id).\
  197. filter(Post.topic_id == Topic.id).\
  198. order_by(Post.id.desc()).\
  199. paginate(page, flaskbb_config['TOPICS_PER_PAGE'], False)
  200. def all_posts(self, page):
  201. """Returns a paginated result with all posts the user has created."""
  202. return Post.query.filter(Post.user_id == self.id).\
  203. paginate(page, flaskbb_config['TOPICS_PER_PAGE'], False)
  204. def track_topic(self, topic):
  205. """Tracks the specified topic
  206. :param topic: The topic which should be added to the topic tracker.
  207. """
  208. if not self.is_tracking_topic(topic):
  209. self.tracked_topics.append(topic)
  210. return self
  211. def untrack_topic(self, topic):
  212. """Untracks the specified topic
  213. :param topic: The topic which should be removed from the
  214. topic tracker.
  215. """
  216. if self.is_tracking_topic(topic):
  217. self.tracked_topics.remove(topic)
  218. return self
  219. def is_tracking_topic(self, topic):
  220. """Checks if the user is already tracking this topic
  221. :param topic: The topic which should be checked.
  222. """
  223. return self.tracked_topics.filter(
  224. topictracker.c.topic_id == topic.id).count() > 0
  225. def add_to_group(self, group):
  226. """Adds the user to the `group` if he isn't in it.
  227. :param group: The group which should be added to the user.
  228. """
  229. if not self.in_group(group):
  230. self.secondary_groups.append(group)
  231. return self
  232. def remove_from_group(self, group):
  233. """Removes the user from the `group` if he is in it.
  234. :param group: The group which should be removed from the user.
  235. """
  236. if self.in_group(group):
  237. self.secondary_groups.remove(group)
  238. return self
  239. def in_group(self, group):
  240. """Returns True if the user is in the specified group
  241. :param group: The group which should be checked.
  242. """
  243. return self.secondary_groups.filter(
  244. groups_users.c.group_id == group.id).count() > 0
  245. @cache.memoize(timeout=sys.maxint)
  246. def get_permissions(self, exclude=None):
  247. """Returns a dictionary with all the permissions the user has.
  248. :param exclude: a list with excluded permissions. default is None.
  249. """
  250. exclude = exclude or []
  251. exclude.extend(['id', 'name', 'description'])
  252. perms = {}
  253. groups = self.secondary_groups.all()
  254. groups.append(self.primary_group)
  255. for group in groups:
  256. for c in group.__table__.columns:
  257. # try if the permission already exists in the dictionary
  258. # and if the permission is true, set it to True
  259. try:
  260. if not perms[c.name] and getattr(group, c.name):
  261. perms[c.name] = True
  262. # if the permission doesn't exist in the dictionary
  263. # add it to the dictionary
  264. except KeyError:
  265. # if the permission is in the exclude list,
  266. # skip to the next permission
  267. if c.name in exclude:
  268. continue
  269. perms[c.name] = getattr(group, c.name)
  270. return perms
  271. def invalidate_cache(self):
  272. """Invalidates this objects cached metadata."""
  273. cache.delete_memoized(self.get_permissions, self)
  274. def ban(self):
  275. """Bans the user. Returns True upon success."""
  276. if not self.get_permissions()['banned']:
  277. banned_group = Group.query.filter(
  278. Group.banned == True
  279. ).first()
  280. self.primary_group_id = banned_group.id
  281. self.save()
  282. self.invalidate_cache()
  283. return True
  284. return False
  285. def unban(self):
  286. """Unbans the user. Returns True upon success."""
  287. if self.get_permissions()['banned']:
  288. member_group = Group.query.filter(
  289. Group.admin == False,
  290. Group.super_mod == False,
  291. Group.mod == False,
  292. Group.guest == False,
  293. Group.banned == False
  294. ).first()
  295. self.primary_group_id = member_group.id
  296. self.save()
  297. self.invalidate_cache()
  298. return True
  299. return False
  300. def save(self, groups=None):
  301. """Saves a user. If a list with groups is provided, it will add those
  302. to the secondary groups from the user.
  303. :param groups: A list with groups that should be added to the
  304. secondary groups from user.
  305. """
  306. if groups:
  307. # TODO: Only remove/add groups that are selected
  308. secondary_groups = self.secondary_groups.all()
  309. for group in secondary_groups:
  310. self.remove_from_group(group)
  311. db.session.commit()
  312. for group in groups:
  313. # Do not add the primary group to the secondary groups
  314. if group.id == self.primary_group_id:
  315. continue
  316. self.add_to_group(group)
  317. self.invalidate_cache()
  318. db.session.add(self)
  319. db.session.commit()
  320. return self
  321. def delete(self):
  322. """Deletes the User."""
  323. # This isn't done automatically...
  324. PrivateMessage.query.filter_by(user_id=self.id).delete()
  325. ForumsRead.query.filter_by(user_id=self.id).delete()
  326. TopicsRead.query.filter_by(user_id=self.id).delete()
  327. db.session.delete(self)
  328. db.session.commit()
  329. return self
  330. class Guest(AnonymousUserMixin):
  331. @property
  332. def permissions(self):
  333. return self.get_permissions()
  334. def get_permissions(self, exclude=None):
  335. """Returns a dictionary with all permissions the user has"""
  336. exclude = exclude or []
  337. exclude.extend(['id', 'name', 'description'])
  338. perms = {}
  339. # Get the Guest group
  340. group = Group.query.filter_by(guest=True).first()
  341. for c in group.__table__.columns:
  342. if c.name in exclude:
  343. continue
  344. perms[c.name] = getattr(group, c.name)
  345. return perms
  346. class PrivateMessage(db.Model):
  347. __tablename__ = "privatemessages"
  348. id = db.Column(db.Integer, primary_key=True)
  349. user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
  350. from_user_id = db.Column(db.Integer, db.ForeignKey("users.id"))
  351. to_user_id = db.Column(db.Integer, db.ForeignKey("users.id"))
  352. subject = db.Column(db.String(255))
  353. message = db.Column(db.Text)
  354. date_created = db.Column(db.DateTime, default=datetime.utcnow())
  355. trash = db.Column(db.Boolean, nullable=False, default=False)
  356. draft = db.Column(db.Boolean, nullable=False, default=False)
  357. unread = db.Column(db.Boolean, nullable=False, default=True)
  358. user = db.relationship("User", backref="pms", lazy="joined",
  359. foreign_keys=[user_id])
  360. from_user = db.relationship("User", lazy="joined",
  361. foreign_keys=[from_user_id])
  362. to_user = db.relationship("User", lazy="joined", foreign_keys=[to_user_id])
  363. def save(self, from_user=None, to_user=None, user_id=None, draft=False):
  364. """Saves a private message.
  365. :param from_user: The user who has sent the message
  366. :param to_user: The user who should recieve the message
  367. :param user_id: The senders user id - This is the id to which user the
  368. Inbox belongs.
  369. :param draft: If the message is a draft. Defaults to ``False``.
  370. """
  371. if self.id:
  372. db.session.add(self)
  373. db.session.commit()
  374. return self
  375. # Defaults to ``False``.
  376. self.draft = draft
  377. # Add the message to the user's pm box
  378. self.user_id = user_id
  379. self.from_user_id = from_user
  380. self.to_user_id = to_user
  381. self.date_created = datetime.utcnow()
  382. db.session.add(self)
  383. db.session.commit()
  384. return self
  385. def delete(self):
  386. """Deletes a private message"""
  387. db.session.delete(self)
  388. db.session.commit()
  389. return self