models.py 15 KB

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