models.py 14 KB

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