models.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480
  1. # -*- coding: utf-8 -*-
  2. """
  3. flaskbb.forum.models
  4. ~~~~~~~~~~~~~~~~~~~~
  5. It provides the models for the forum
  6. :copyright: (c) 2014 by the FlaskBB Team.
  7. :license: BSD, see LICENSE for more details.
  8. """
  9. from datetime import datetime, timedelta
  10. from flask import current_app
  11. from flaskbb.extensions import db
  12. from flaskbb.utils.types import SetType, MutableSet
  13. from flaskbb.utils.query import TopicQuery
  14. class Post(db.Model):
  15. __tablename__ = "posts"
  16. id = db.Column(db.Integer, primary_key=True)
  17. topic_id = db.Column(db.Integer, db.ForeignKey("topics.id", use_alter=True,
  18. name="fk_topic_id",
  19. ondelete="CASCADE"))
  20. user_id = db.Column(db.Integer, db.ForeignKey("users.id"))
  21. content = db.Column(db.Text)
  22. date_created = db.Column(db.DateTime, default=datetime.utcnow())
  23. date_modified = db.Column(db.DateTime)
  24. # Methods
  25. def __repr__(self):
  26. """
  27. Set to a unique key specific to the object in the database.
  28. Required for cache.memoize() to work across requests.
  29. """
  30. return "<{} {}>".format(self.__class__.__name__, self.id)
  31. def save(self, user=None, topic=None):
  32. """Saves a new post. If no parameters are passed we assume that
  33. you will just update an existing post. It returns the object after the
  34. operation was successful.
  35. :param user: The user who has created the post
  36. :param topic: The topic in which the post was created
  37. """
  38. # update/edit the post
  39. if self.id:
  40. db.session.add(self)
  41. db.session.commit()
  42. return self
  43. # Adding a new post
  44. if user and topic:
  45. self.user_id = user.id
  46. self.topic_id = topic.id
  47. self.date_created = datetime.utcnow()
  48. # This needs to be done before I update the last_post_id.
  49. db.session.add(self)
  50. db.session.commit()
  51. # Now lets update the last post id
  52. topic.last_post_id = self.id
  53. topic.forum.last_post_id = self.id
  54. # Update the post counts
  55. user.post_count += 1
  56. topic.post_count += 1
  57. topic.forum.post_count += 1
  58. # And commit it!
  59. db.session.add(topic)
  60. db.session.commit()
  61. return self
  62. def delete(self):
  63. """Deletes a post and returns self"""
  64. # This will delete the whole topic
  65. if self.topic.first_post_id == self.id:
  66. self.topic.delete()
  67. return self
  68. # Delete the last post
  69. if self.topic.last_post_id == self.id:
  70. # Now the second last post will be the last post
  71. self.topic.last_post_id = self.topic.second_last_post
  72. # check if the last_post is also the last post in the forum
  73. if self.topic.last_post_id == self.id:
  74. self.topic.last_post_id = self.topic.second_last_post
  75. self.topic.forum.last_post_id = self.topic.second_last_post
  76. db.session.commit()
  77. # Update the post counts
  78. self.user.post_count -= 1
  79. self.topic.post_count -= 1
  80. self.topic.forum.post_count -= 1
  81. db.session.delete(self)
  82. db.session.commit()
  83. return self
  84. class Topic(db.Model):
  85. __tablename__ = "topics"
  86. query_class = TopicQuery
  87. id = db.Column(db.Integer, primary_key=True)
  88. forum_id = db.Column(db.Integer, db.ForeignKey("forums.id", use_alter=True,
  89. name="fk_forum_id"))
  90. title = db.Column(db.String)
  91. user_id = db.Column(db.Integer, db.ForeignKey("users.id"))
  92. date_created = db.Column(db.DateTime, default=datetime.utcnow())
  93. last_updated = db.Column(db.DateTime, default=datetime.utcnow())
  94. locked = db.Column(db.Boolean, default=False)
  95. important = db.Column(db.Boolean, default=False)
  96. views = db.Column(db.Integer, default=0)
  97. post_count = db.Column(db.Integer, default=0)
  98. # One-to-one (uselist=False) relationship between first_post and topic
  99. first_post_id = db.Column(db.Integer, db.ForeignKey("posts.id",
  100. ondelete="CASCADE"))
  101. first_post = db.relationship("Post", backref="first_post", uselist=False,
  102. foreign_keys=[first_post_id])
  103. # One-to-one
  104. last_post_id = db.Column(db.Integer, db.ForeignKey("posts.id"))
  105. last_post = db.relationship("Post", backref="last_post", uselist=False,
  106. foreign_keys=[last_post_id])
  107. # One-to-many
  108. posts = db.relationship("Post", backref="topic", lazy="joined",
  109. primaryjoin="Post.topic_id == Topic.id",
  110. cascade="all, delete-orphan", post_update=True)
  111. # Properties
  112. @property
  113. def second_last_post(self):
  114. """Returns the second last post."""
  115. return self.posts[-2].id
  116. # Methods
  117. def __init__(self, title=None):
  118. if title:
  119. self.title = title
  120. def __repr__(self):
  121. """
  122. Set to a unique key specific to the object in the database.
  123. Required for cache.memoize() to work across requests.
  124. """
  125. return "<{} {}>".format(self.__class__.__name__, self.id)
  126. def save(self, user=None, forum=None, post=None):
  127. """Saves a topic and returns the topic object. If no parameters are
  128. given, it will only update the topic.
  129. :param user: The user who has created the topic
  130. :param forum: The forum where the topic is stored
  131. :param post: The post object which is connected to the topic
  132. """
  133. # Updates the topic
  134. if self.id:
  135. db.session.add(self)
  136. db.session.commit()
  137. return self
  138. # Set the forum and user id
  139. self.forum_id = forum.id
  140. self.user_id = user.id
  141. # Insert and commit the topic
  142. db.session.add(self)
  143. db.session.commit()
  144. # Create the topic post
  145. post.save(user, self)
  146. # Update the first post id
  147. self.first_post_id = post.id
  148. # Update the topic count
  149. forum.topic_count += 1
  150. db.session.commit()
  151. return self
  152. def delete(self, users=None):
  153. """Deletes a topic with the corresponding posts. If a list with
  154. user objects is passed it will also update their post counts
  155. :param users: A list with user objects
  156. """
  157. # Grab the second last topic in the forum + parents/childs
  158. topic = Topic.query.\
  159. filter_by(forum_id=self.forum_id).\
  160. order_by(Topic.last_post_id.desc()).limit(2).offset(0).all()
  161. # do want to delete the topic with the last post?
  162. if topic and topic[0].id == self.id:
  163. try:
  164. # Now the second last post will be the last post
  165. self.forum.last_post_id = topic[1].last_post_id
  166. # Catch an IndexError when you delete the last topic in the forum
  167. except IndexError:
  168. self.forum.last_post_id = None
  169. # These things needs to be stored in a variable before they are deleted
  170. forum = self.forum
  171. # Delete the topic
  172. db.session.delete(self)
  173. db.session.commit()
  174. # Update the post counts
  175. if users:
  176. for user in users:
  177. user.post_count = Post.query.filter_by(user_id=user.id).count()
  178. db.session.commit()
  179. forum.topic_count = Topic.query.\
  180. filter_by(forum_id=self.forum_id).\
  181. count()
  182. forum.post_count = Post.query.\
  183. filter(Post.topic_id == Topic.id,
  184. Topic.forum_id == self.forum_id).\
  185. count()
  186. db.session.commit()
  187. return self
  188. def update_read(self, user, forum, forumsread=None):
  189. """Update the topics read status if the user hasn't read the latest
  190. post.
  191. :param user: The user for whom the readstracker should be updated
  192. :param forum: The forum in which the topic is
  193. :param forumsread: The forumsread object. It is used to check if there
  194. is a new post since the forum has been marked as
  195. read
  196. """
  197. read_cutoff = datetime.utcnow() - timedelta(
  198. days=current_app.config['TRACKER_LENGTH'])
  199. # Anonymous User or the post is too old for inserting it in the
  200. # TopicsRead model
  201. if not user.is_authenticated() or \
  202. read_cutoff > self.last_post.date_created:
  203. return
  204. topicread = TopicsRead.query.\
  205. filter(TopicsRead.user_id == user.id,
  206. TopicsRead.topic_id == self.id).first()
  207. # Can be None if the user has never marked the forum as read. If this
  208. # condition is false - we need to update the tracker
  209. if forumsread and forumsread.cleared is not None and \
  210. forumsread.cleared >= self.last_post.date_created:
  211. return
  212. # A new post has been submitted that the user hasn't read.
  213. # Updating...
  214. if topicread and (topicread.last_read < self.last_post.date_created):
  215. topicread.last_read = datetime.utcnow()
  216. topicread.save()
  217. # The user has not visited the topic before. Inserting him in
  218. # the TopicsRead model.
  219. elif not topicread:
  220. topicread = TopicsRead()
  221. topicread.user_id = user.id
  222. topicread.topic_id = self.id
  223. topicread.forum_id = self.forum_id
  224. topicread.last_read = datetime.utcnow()
  225. topicread.save()
  226. # else: no unread posts
  227. if forum:
  228. # fetch the unread posts in the forum
  229. unread_count = Topic.query.\
  230. outerjoin(TopicsRead,
  231. db.and_(TopicsRead.topic_id == Topic.id,
  232. TopicsRead.user_id == user.id)).\
  233. outerjoin(ForumsRead,
  234. db.and_(ForumsRead.forum_id == Topic.forum_id,
  235. ForumsRead.user_id == user.id)).\
  236. filter(Topic.forum_id == forum.id,
  237. db.or_(TopicsRead.last_read == None,
  238. TopicsRead.last_read < Topic.last_updated)).\
  239. count()
  240. #No unread topics available - trying to mark the forum as read
  241. if unread_count == 0:
  242. forumread = ForumsRead.query.\
  243. filter(ForumsRead.user_id == user.id,
  244. ForumsRead.forum_id == forum.id).first()
  245. # ForumsRead is already up-to-date.
  246. if forumread and forumread.last_read > topicread.last_read:
  247. return
  248. # ForumRead Entry exists - Updating it because a new post
  249. # has been submitted that the user hasn't read.
  250. elif forumread:
  251. forumread.last_read = datetime.utcnow()
  252. forumread.save()
  253. # No ForumRead Entry existing - creating one.
  254. else:
  255. forumread = ForumsRead()
  256. forumread.user_id = user.id
  257. forumread.forum_id = forum.id
  258. forumread.last_read = datetime.utcnow()
  259. forumread.save()
  260. class Forum(db.Model):
  261. __tablename__ = "forums"
  262. id = db.Column(db.Integer, primary_key=True)
  263. category_id = db.Column(db.Integer, db.ForeignKey("categories.id"))
  264. title = db.Column(db.String)
  265. description = db.Column(db.String)
  266. position = db.Column(db.Integer, default=0)
  267. locked = db.Column(db.Boolean, default=False)
  268. post_count = db.Column(db.Integer, default=0)
  269. topic_count = db.Column(db.Integer, default=0)
  270. # TODO: Make a own relation for this
  271. moderators = db.Column(MutableSet.as_mutable(SetType))
  272. # One-to-one
  273. last_post_id = db.Column(db.Integer, db.ForeignKey("posts.id"))
  274. last_post = db.relationship("Post", backref="last_post_forum",
  275. uselist=False, foreign_keys=[last_post_id])
  276. # One-to-many
  277. topics = db.relationship("Topic", backref="forum", lazy="joined",
  278. cascade="all, delete-orphan")
  279. # Methods
  280. def __repr__(self):
  281. """Set to a unique key specific to the object in the database.
  282. Required for cache.memoize() to work across requests.
  283. """
  284. return "<{} {}>".format(self.__class__.__name__, self.id)
  285. def add_moderator(self, user_id):
  286. self.moderators.add(user_id)
  287. def remove_moderator(self, user_id):
  288. self.moderators.remove(user_id)
  289. def save(self):
  290. """Saves a forum"""
  291. db.session.add(self)
  292. db.session.commit()
  293. return self
  294. def delete(self, users=None):
  295. """Deletes forum. If a list with involved user objects is passed,
  296. it will also update their post counts
  297. :param users: A list with user objects
  298. """
  299. # Delete the forum
  300. db.session.delete(self)
  301. db.session.commit()
  302. # Update the users post count
  303. # Need to import it from here, because otherwise it would be
  304. # a circular import
  305. from flaskbb.user.models import User
  306. users = User.query.\
  307. filter(Topic.forum_id == self.id,
  308. Post.topic_id == Topic.id).\
  309. all()
  310. for user in users:
  311. user.post_count = Post.query.filter_by(user_id=user.id).count()
  312. db.session.commit()
  313. return self
  314. class Category(db.Model):
  315. __tablename__ = "categories"
  316. id = db.Column(db.Integer, primary_key=True)
  317. title = db.Column(db.String)
  318. description = db.Column(db.String)
  319. position = db.Column(db.Integer, default=0)
  320. # One-to-many
  321. forums = db.relationship("Forum", backref="category", lazy="dynamic",
  322. primaryjoin='Forum.category_id == Category.id',
  323. order_by='asc(Forum.position)')
  324. def save(self):
  325. """Saves a category"""
  326. db.session.add(self)
  327. db.session.commit()
  328. return self
  329. def delete(self):
  330. """Deletes a category"""
  331. # Delete all the forums in the category
  332. for forum in self.forums:
  333. forum.delete()
  334. # and finally delete the category itself
  335. db.session.delete(self)
  336. db.session.commit()
  337. return self
  338. topictracker = db.Table(
  339. 'topictracker',
  340. db.Column('user_id', db.Integer(), db.ForeignKey('users.id')),
  341. db.Column('topic_id', db.Integer(), db.ForeignKey('topics.id')))
  342. class TopicsRead(db.Model):
  343. __tablename__ = "topicsread"
  344. user_id = db.Column(db.Integer, db.ForeignKey("users.id"),
  345. primary_key=True)
  346. topic_id = db.Column(db.Integer, db.ForeignKey("topics.id"),
  347. primary_key=True)
  348. forum_id = db.Column(db.Integer, db.ForeignKey("forums.id"),
  349. primary_key=True)
  350. last_read = db.Column(db.DateTime, default=datetime.utcnow())
  351. def save(self):
  352. db.session.add(self)
  353. db.session.commit()
  354. return self
  355. def delete(self):
  356. db.session.delete(self)
  357. db.session.commit()
  358. return self
  359. class ForumsRead(db.Model):
  360. __tablename__ = "forumsread"
  361. user_id = db.Column(db.Integer, db.ForeignKey("users.id"),
  362. primary_key=True)
  363. forum_id = db.Column(db.Integer, db.ForeignKey("topics.id"),
  364. primary_key=True)
  365. last_read = db.Column(db.DateTime, default=datetime.utcnow())
  366. cleared = db.Column(db.DateTime)
  367. def save(self):
  368. db.session.add(self)
  369. db.session.commit()
  370. return self
  371. def delete(self):
  372. db.session.delete(self)
  373. db.session.commit()
  374. return self