models.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. # -*- coding: utf-8 -*-
  2. """
  3. flaskbb.user.models
  4. ~~~~~~~~~~~~~~~~~~~~
  5. This module provides the models for the user.
  6. :copyright: (c) 2013 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
  18. groups_users = db.Table('groups_users',
  19. db.Column('user_id', db.Integer(), db.ForeignKey('users.id')),
  20. db.Column('group_id', db.Integer(), db.ForeignKey('groups.id')))
  21. class Group(db.Model):
  22. __tablename__ = "groups"
  23. id = db.Column(db.Integer, primary_key=True)
  24. name = db.Column(db.String, unique=True)
  25. description = db.Column(db.String(80))
  26. # I bet there is a nicer way for this :P
  27. admin = db.Column(db.Boolean, default=False)
  28. super_mod = db.Column(db.Boolean, default=False)
  29. mod = db.Column(db.Boolean, default=False)
  30. guest = db.Column(db.Boolean, default=False)
  31. banned = db.Column(db.Boolean, default=False)
  32. editpost = db.Column(db.Boolean, default=True)
  33. deletepost = db.Column(db.Boolean, default=False)
  34. deletetopic = db.Column(db.Boolean, default=False)
  35. posttopic = db.Column(db.Boolean, default=True)
  36. postreply = db.Column(db.Boolean, default=True)
  37. # Methods
  38. def __repr__(self):
  39. """
  40. Set to a unique key specific to the object in the database.
  41. Required for cache.memoize() to work across requests.
  42. """
  43. return "<{} {})>".format(self.__class__.__name__, self.id)
  44. def save(self):
  45. db.session.add(self)
  46. db.session.commit()
  47. return self
  48. def delete(self):
  49. db.session.delete(self)
  50. db.session.commit()
  51. return self
  52. class User(db.Model, UserMixin):
  53. __tablename__ = "users"
  54. id = db.Column(db.Integer, primary_key=True)
  55. username = db.Column(db.String, unique=True)
  56. email = db.Column(db.String, unique=True)
  57. _password = db.Column('password', db.String(80), nullable=False)
  58. date_joined = db.Column(db.DateTime, default=datetime.utcnow())
  59. lastseen = db.Column(db.DateTime, default=datetime.utcnow())
  60. birthday = db.Column(db.DateTime)
  61. gender = db.Column(db.String)
  62. website = db.Column(db.String)
  63. location = db.Column(db.String)
  64. signature = db.Column(db.String)
  65. avatar = db.Column(db.String)
  66. notes = db.Column(db.Text(5000))
  67. posts = db.relationship("Post", backref="user", lazy="dynamic")
  68. topics = db.relationship("Topic", backref="user", lazy="dynamic")
  69. primary_group_id = db.Column(db.Integer, db.ForeignKey('groups.id'))
  70. primary_group = db.relationship('Group', lazy="joined",
  71. backref="user_group", uselist=False,
  72. foreign_keys=[primary_group_id])
  73. secondary_groups = \
  74. db.relationship('Group',
  75. secondary=groups_users,
  76. primaryjoin=(groups_users.c.user_id == id),
  77. backref=db.backref('users', lazy='dynamic'),
  78. lazy='dynamic')
  79. tracked_topics = \
  80. db.relationship("Topic", secondary=topictracker,
  81. primaryjoin=(topictracker.c.user_id == id),
  82. backref=db.backref("topicstracked", lazy="dynamic"),
  83. lazy="dynamic")
  84. # Properties
  85. @property
  86. def post_count(self):
  87. """
  88. Property interface for get_post_count method.
  89. Method seperate for easy invalidation of cache.
  90. """
  91. return self.get_post_count()
  92. @property
  93. def last_post(self):
  94. """
  95. Property interface for get_last_post method.
  96. Method seperate for easy invalidation of cache.
  97. """
  98. return self.get_last_post()
  99. # Methods
  100. def __repr__(self):
  101. """
  102. Set to a unique key specific to the object in the database.
  103. Required for cache.memoize() to work across requests.
  104. """
  105. return "Username: %s" % self.username
  106. def _get_password(self):
  107. return self._password
  108. def _set_password(self, 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. """
  116. Check passwords. If passwords match it returns true, else false
  117. """
  118. if self.password is None:
  119. return False
  120. return check_password_hash(self.password, password)
  121. @classmethod
  122. def authenticate(cls, login, password):
  123. """
  124. A classmethod for authenticating users
  125. It returns true if the user exists and has entered a correct password
  126. """
  127. user = cls.query.filter(db.or_(User.username == login,
  128. User.email == login)).first()
  129. if user:
  130. authenticated = user.check_password(password)
  131. else:
  132. authenticated = False
  133. return user, authenticated
  134. def _make_token(self, data, timeout):
  135. s = Serializer(current_app.config['SECRET_KEY'], timeout)
  136. return s.dumps(data)
  137. def _verify_token(self, token):
  138. s = Serializer(current_app.config['SECRET_KEY'])
  139. data = None
  140. expired, invalid = False, False
  141. try:
  142. data = s.loads(token)
  143. except SignatureExpired:
  144. expired = True
  145. except Exception:
  146. invalid = True
  147. return expired, invalid, data
  148. def make_reset_token(self, expiration=3600):
  149. return self._make_token({'id': self.id, 'op': 'reset'}, expiration)
  150. def verify_reset_token(self, token):
  151. expired, invalid, data = self._verify_token(token)
  152. if data and data.get('id') == self.id and data.get('op') == 'reset':
  153. data = True
  154. else:
  155. data = False
  156. return expired, invalid, data
  157. def all_topics(self, page):
  158. """
  159. Returns a paginated query result with all topics the user has created.
  160. """
  161. return Topic.query.filter(Topic.user_id == self.id).\
  162. filter(Post.topic_id == Topic.id).\
  163. order_by(Post.id.desc()).\
  164. paginate(page, current_app.config['TOPICS_PER_PAGE'], False)
  165. def all_posts(self, page):
  166. """
  167. Returns a paginated query result with all posts the user has created.
  168. """
  169. return Post.query.filter(Post.user_id == self.id).\
  170. paginate(page, current_app.config['TOPICS_PER_PAGE'], False)
  171. def track_topic(self, topic):
  172. """
  173. Tracks the specified topic
  174. """
  175. if not self.is_tracking_topic(topic):
  176. self.tracked_topics.append(topic)
  177. return self
  178. def untrack_topic(self, topic):
  179. """
  180. Untracks the specified topic
  181. """
  182. if self.is_tracking_topic(topic):
  183. self.tracked_topics.remove(topic)
  184. return self
  185. def is_tracking_topic(self, topic):
  186. """
  187. Checks if the user is already tracking this topic
  188. """
  189. return self.tracked_topics.filter(
  190. topictracker.c.topic_id == topic.id).count() > 0
  191. def add_to_group(self, group):
  192. """
  193. Adds the user to the `group` if he isn't in it.
  194. """
  195. if not self.in_group(group):
  196. self.secondary_groups.append(group)
  197. return self
  198. def remove_from_group(self, group):
  199. """
  200. Removes the user from the `group` if he is in it.
  201. """
  202. if self.in_group(group):
  203. self.secondary_groups.remove(group)
  204. return self
  205. def in_group(self, group):
  206. """
  207. Returns True if the user is in the specified group
  208. """
  209. return self.secondary_groups.filter(
  210. groups_users.c.group_id == group.id).count() > 0
  211. @cache.memoize(60*5)
  212. def get_permissions(self, exclude=None):
  213. """
  214. Returns a dictionary with all the permissions the user has.
  215. """
  216. exclude = exclude or []
  217. exclude.extend(['id', 'name', 'description'])
  218. perms = {}
  219. groups = self.secondary_groups.all()
  220. groups.append(self.primary_group)
  221. for group in groups:
  222. for c in group.__table__.columns:
  223. # try if the permission already exists in the dictionary
  224. # and if the permission is true, set it to True
  225. try:
  226. if not perms[c.name] and getattr(group, c.name):
  227. perms[c.name] = True
  228. # if the permission doesn't exist in the dictionary
  229. # add it to the dictionary
  230. except KeyError:
  231. # if the permission is in the exclude list,
  232. # skip to the next permission
  233. if c.name in exclude:
  234. continue
  235. perms[c.name] = getattr(group, c.name)
  236. return perms
  237. def save(self, groups=None):
  238. if groups:
  239. # TODO: Only remove/add groups that are selected
  240. secondary_groups = self.secondary_groups.all()
  241. for group in secondary_groups:
  242. self.remove_from_group(group)
  243. db.session.commit()
  244. for group in groups:
  245. # Do not add the primary group to the secondary groups
  246. if group.id == self.primary_group_id:
  247. continue
  248. self.add_to_group(group)
  249. db.session.add(self)
  250. db.session.commit()
  251. return self
  252. @cache.memoize(timeout=sys.maxint)
  253. def get_post_count(self):
  254. """
  255. Returns the amount of posts within the current topic.
  256. """
  257. return Post.query.filter(Post.user_id == self.id).\
  258. count()
  259. # @cache.memoize(timeout=sys.maxint) # TODO: DetachedInstanceError if we return a Flask-SQLAlchemy model.
  260. def get_last_post(self):
  261. """
  262. Returns the latest post from the user
  263. """
  264. return Post.query.filter(Post.user_id == self.id).\
  265. order_by(Post.date_created.desc()).first()
  266. def invalidate_cache(self):
  267. """
  268. Invalidates this objects cached metadata.
  269. """
  270. cache.delete_memoized(self.get_post_count, self)
  271. #cache.delete_memoized(self.get_last_post, self) # TODO: Cannot use til we can cache this object.
  272. class Guest(AnonymousUserMixin):
  273. @cache.memoize(60*5)
  274. def get_permissions(self, exclude=None):
  275. """
  276. Returns a dictionary with all permissions the user has
  277. """
  278. exclude = exclude or []
  279. exclude.extend(['id', 'name', 'description'])
  280. perms = {}
  281. # Get the Guest group
  282. group = Group.query.filter_by(guest=True).first()
  283. for c in group.__table__.columns:
  284. if c.name in exclude:
  285. continue
  286. perms[c.name] = getattr(group, c.name)
  287. return perms