models.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624
  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.query import TopicQuery
  13. moderators = db.Table(
  14. 'moderators',
  15. db.Column('user_id', db.Integer(), db.ForeignKey('users.id'),
  16. nullable=False),
  17. db.Column('forum_id', db.Integer(),
  18. db.ForeignKey('forums.id', use_alter=True, name="fk_forum_id"),
  19. nullable=False))
  20. topictracker = db.Table(
  21. 'topictracker',
  22. db.Column('user_id', db.Integer(), db.ForeignKey('users.id'),
  23. nullable=False),
  24. db.Column('topic_id', db.Integer(),
  25. db.ForeignKey('topics.id',
  26. use_alter=True, name="fk_tracker_topic_id"),
  27. nullable=False))
  28. class TopicsRead(db.Model):
  29. __tablename__ = "topicsread"
  30. user_id = db.Column(db.Integer, db.ForeignKey("users.id"),
  31. primary_key=True)
  32. topic_id = db.Column(db.Integer,
  33. db.ForeignKey("topics.id", use_alter=True,
  34. name="fk_tr_topic_id"),
  35. primary_key=True)
  36. forum_id = db.Column(db.Integer,
  37. db.ForeignKey("forums.id", use_alter=True,
  38. name="fk_tr_forum_id"),
  39. primary_key=True)
  40. last_read = db.Column(db.DateTime, default=datetime.utcnow())
  41. def save(self):
  42. db.session.add(self)
  43. db.session.commit()
  44. return self
  45. def delete(self):
  46. db.session.delete(self)
  47. db.session.commit()
  48. return self
  49. class ForumsRead(db.Model):
  50. __tablename__ = "forumsread"
  51. user_id = db.Column(db.Integer, db.ForeignKey("users.id"),
  52. primary_key=True)
  53. forum_id = db.Column(db.Integer,
  54. db.ForeignKey("topics.id", use_alter=True,
  55. name="fk_fr_forum_id"),
  56. primary_key=True)
  57. last_read = db.Column(db.DateTime, default=datetime.utcnow())
  58. cleared = db.Column(db.DateTime)
  59. def save(self):
  60. db.session.add(self)
  61. db.session.commit()
  62. return self
  63. def delete(self):
  64. db.session.delete(self)
  65. db.session.commit()
  66. return self
  67. class Report(db.Model):
  68. __tablename__ = "reports"
  69. id = db.Column(db.Integer, primary_key=True)
  70. reporter_id = db.Column(db.Integer, db.ForeignKey("users.id"),
  71. nullable=False)
  72. reported = db.Column(db.DateTime, default=datetime.utcnow())
  73. post_id = db.Column(db.Integer, db.ForeignKey("posts.id"), nullable=False)
  74. zapped = db.Column(db.DateTime)
  75. zapped_by = db.Column(db.Integer, db.ForeignKey("users.id"))
  76. reason = db.Column(db.String(63))
  77. post = db.relationship("Post", backref="report", lazy="joined")
  78. reporter = db.relationship("User", lazy="joined",
  79. foreign_keys=[reporter_id])
  80. zapper = db.relationship("User", lazy="joined", foreign_keys=[zapped_by])
  81. def save(self, post=None, user=None):
  82. """Saves a report.
  83. :param post: The post that should be reported
  84. :param user: The user who has reported the post
  85. :param reason: The reason why the user has reported the post
  86. """
  87. if self.id:
  88. db.session.add(self)
  89. db.session.commit()
  90. return self
  91. if post and user:
  92. self.reporter_id = user.id
  93. self.reported = datetime.utcnow()
  94. self.post_id = post.id
  95. db.session.add(self)
  96. db.session.commit()
  97. return self
  98. def delete(self):
  99. db.session.delete(self)
  100. db.session.commit()
  101. return self
  102. class Post(db.Model):
  103. __tablename__ = "posts"
  104. id = db.Column(db.Integer, primary_key=True)
  105. topic_id = db.Column(db.Integer,
  106. db.ForeignKey("topics.id",
  107. use_alter=True,
  108. name="fk_post_topic_id",
  109. ondelete="CASCADE"),
  110. nullable=False)
  111. user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
  112. username = db.Column(db.String(15), nullable=False)
  113. content = db.Column(db.Text, nullable=False)
  114. date_created = db.Column(db.DateTime, default=datetime.utcnow())
  115. date_modified = db.Column(db.DateTime)
  116. modified_by = db.Column(db.String(15))
  117. # Methods
  118. def __repr__(self):
  119. """
  120. Set to a unique key specific to the object in the database.
  121. Required for cache.memoize() to work across requests.
  122. """
  123. return "<{} {}>".format(self.__class__.__name__, self.id)
  124. def save(self, user=None, topic=None):
  125. """Saves a new post. If no parameters are passed we assume that
  126. you will just update an existing post. It returns the object after the
  127. operation was successful.
  128. :param user: The user who has created the post
  129. :param topic: The topic in which the post was created
  130. """
  131. # update/edit the post
  132. if self.id:
  133. db.session.add(self)
  134. db.session.commit()
  135. return self
  136. # Adding a new post
  137. if user and topic:
  138. self.user_id = user.id
  139. self.username = user.username
  140. self.topic_id = topic.id
  141. self.date_created = datetime.utcnow()
  142. # This needs to be done before I update the last_post_id.
  143. db.session.add(self)
  144. db.session.commit()
  145. # Now lets update the last post id
  146. topic.last_post_id = self.id
  147. topic.forum.last_post_id = self.id
  148. # Update the post counts
  149. user.post_count += 1
  150. topic.post_count += 1
  151. topic.forum.post_count += 1
  152. # And commit it!
  153. db.session.add(topic)
  154. db.session.commit()
  155. return self
  156. def delete(self):
  157. """Deletes a post and returns self"""
  158. # This will delete the whole topic
  159. if self.topic.first_post_id == self.id:
  160. self.topic.delete()
  161. return self
  162. # Delete the last post
  163. if self.topic.last_post_id == self.id:
  164. # Now the second last post will be the last post
  165. self.topic.last_post_id = self.topic.second_last_post
  166. # check if the last_post is also the last post in the forum
  167. if self.topic.last_post_id == self.id:
  168. self.topic.last_post_id = self.topic.second_last_post
  169. self.topic.forum.last_post_id = self.topic.second_last_post
  170. db.session.commit()
  171. # Update the post counts
  172. self.user.post_count -= 1
  173. self.topic.post_count -= 1
  174. self.topic.forum.post_count -= 1
  175. db.session.delete(self)
  176. db.session.commit()
  177. return self
  178. class Topic(db.Model):
  179. __tablename__ = "topics"
  180. query_class = TopicQuery
  181. id = db.Column(db.Integer, primary_key=True)
  182. forum_id = db.Column(db.Integer,
  183. db.ForeignKey("forums.id",
  184. use_alter=True,
  185. name="fk_topic_forum_id"),
  186. nullable=False)
  187. title = db.Column(db.String(63), nullable=False)
  188. user_id = db.Column(db.Integer, db.ForeignKey("users.id"))
  189. username = db.Column(db.String(15), nullable=False)
  190. date_created = db.Column(db.DateTime, default=datetime.utcnow())
  191. last_updated = db.Column(db.DateTime, default=datetime.utcnow())
  192. locked = db.Column(db.Boolean, default=False)
  193. important = db.Column(db.Boolean, default=False)
  194. views = db.Column(db.Integer, default=0)
  195. post_count = db.Column(db.Integer, default=0)
  196. # One-to-one (uselist=False) relationship between first_post and topic
  197. first_post_id = db.Column(db.Integer, db.ForeignKey("posts.id",
  198. ondelete="CASCADE"))
  199. first_post = db.relationship("Post", backref="first_post", uselist=False,
  200. foreign_keys=[first_post_id])
  201. # One-to-one
  202. last_post_id = db.Column(db.Integer, db.ForeignKey("posts.id"))
  203. last_post = db.relationship("Post", backref="last_post", uselist=False,
  204. foreign_keys=[last_post_id])
  205. # One-to-many
  206. posts = db.relationship("Post", backref="topic", lazy="joined",
  207. primaryjoin="Post.topic_id == Topic.id",
  208. cascade="all, delete-orphan", post_update=True)
  209. # Properties
  210. @property
  211. def second_last_post(self):
  212. """Returns the second last post."""
  213. return self.posts[-2].id
  214. # Methods
  215. def __init__(self, title=None):
  216. if title:
  217. self.title = title
  218. def __repr__(self):
  219. """
  220. Set to a unique key specific to the object in the database.
  221. Required for cache.memoize() to work across requests.
  222. """
  223. return "<{} {}>".format(self.__class__.__name__, self.id)
  224. def move(self, forum):
  225. """Moves a topic to the given forum.
  226. Returns True if it could successfully move the topic to forum.
  227. :param forum: The new forum for the topic
  228. """
  229. # if the target forum is the current forum, abort
  230. if self.forum_id == forum.id:
  231. return False
  232. old_forum = self.forum
  233. self.forum.post_count -= self.post_count
  234. self.forum.topic_count -= 1
  235. self.forum_id = forum.id
  236. forum.post_count += self.post_count
  237. forum.topic_count += 1
  238. db.session.commit()
  239. forum.update_last_post()
  240. old_forum.update_last_post()
  241. TopicsRead.query.filter_by(topic_id=self.id).delete()
  242. return True
  243. def save(self, user=None, forum=None, post=None):
  244. """Saves a topic and returns the topic object. If no parameters are
  245. given, it will only update the topic.
  246. :param user: The user who has created the topic
  247. :param forum: The forum where the topic is stored
  248. :param post: The post object which is connected to the topic
  249. """
  250. # Updates the topic
  251. if self.id:
  252. db.session.add(self)
  253. db.session.commit()
  254. return self
  255. # Set the forum and user id
  256. self.forum_id = forum.id
  257. self.user_id = user.id
  258. self.username = user.username
  259. # Insert and commit the topic
  260. db.session.add(self)
  261. db.session.commit()
  262. # Create the topic post
  263. post.save(user, self)
  264. # Update the first post id
  265. self.first_post_id = post.id
  266. # Update the topic count
  267. forum.topic_count += 1
  268. db.session.commit()
  269. return self
  270. def delete(self, users=None):
  271. """Deletes a topic with the corresponding posts. If a list with
  272. user objects is passed it will also update their post counts
  273. :param users: A list with user objects
  274. """
  275. # Grab the second last topic in the forum + parents/childs
  276. topic = Topic.query.\
  277. filter_by(forum_id=self.forum_id).\
  278. order_by(Topic.last_post_id.desc()).limit(2).offset(0).all()
  279. # do want to delete the topic with the last post?
  280. if topic and topic[0].id == self.id:
  281. try:
  282. # Now the second last post will be the last post
  283. self.forum.last_post_id = topic[1].last_post_id
  284. # Catch an IndexError when you delete the last topic in the forum
  285. except IndexError:
  286. self.forum.last_post_id = None
  287. # These things needs to be stored in a variable before they are deleted
  288. forum = self.forum
  289. # Delete the topic
  290. db.session.delete(self)
  291. db.session.commit()
  292. # Update the post counts
  293. if users:
  294. for user in users:
  295. user.post_count = Post.query.filter_by(user_id=user.id).count()
  296. db.session.commit()
  297. forum.topic_count = Topic.query.\
  298. filter_by(forum_id=self.forum_id).\
  299. count()
  300. forum.post_count = Post.query.\
  301. filter(Post.topic_id == Topic.id,
  302. Topic.forum_id == self.forum_id).\
  303. count()
  304. TopicsRead.query.filter_by(topic_id=self.id).delete()
  305. db.session.commit()
  306. return self
  307. def update_read(self, user, forum, forumsread=None):
  308. """Update the topics read status if the user hasn't read the latest
  309. post.
  310. :param user: The user for whom the readstracker should be updated
  311. :param forum: The forum in which the topic is
  312. :param forumsread: The forumsread object. It is used to check if there
  313. is a new post since the forum has been marked as
  314. read
  315. """
  316. read_cutoff = datetime.utcnow() - timedelta(
  317. days=current_app.config['TRACKER_LENGTH'])
  318. # Anonymous User or the post is too old for inserting it in the
  319. # TopicsRead model
  320. if not user.is_authenticated() or \
  321. read_cutoff > self.last_post.date_created:
  322. return
  323. topicread = TopicsRead.query.\
  324. filter(TopicsRead.user_id == user.id,
  325. TopicsRead.topic_id == self.id).first()
  326. # Can be None if the user has never marked the forum as read. If this
  327. # condition is false - we need to update the tracker
  328. if forumsread and forumsread.cleared is not None and \
  329. forumsread.cleared >= self.last_post.date_created:
  330. return
  331. # A new post has been submitted that the user hasn't read.
  332. # Updating...
  333. if topicread and (topicread.last_read < self.last_post.date_created):
  334. topicread.last_read = datetime.utcnow()
  335. topicread.save()
  336. # The user has not visited the topic before. Inserting him in
  337. # the TopicsRead model.
  338. elif not topicread:
  339. topicread = TopicsRead()
  340. topicread.user_id = user.id
  341. topicread.topic_id = self.id
  342. topicread.forum_id = self.forum_id
  343. topicread.last_read = datetime.utcnow()
  344. topicread.save()
  345. # else: no unread posts
  346. if forum:
  347. # fetch the unread posts in the forum
  348. unread_count = Topic.query.\
  349. outerjoin(TopicsRead,
  350. db.and_(TopicsRead.topic_id == Topic.id,
  351. TopicsRead.user_id == user.id)).\
  352. outerjoin(ForumsRead,
  353. db.and_(ForumsRead.forum_id == Topic.forum_id,
  354. ForumsRead.user_id == user.id)).\
  355. filter(Topic.forum_id == forum.id,
  356. db.or_(TopicsRead.last_read == None,
  357. TopicsRead.last_read < Topic.last_updated)).\
  358. count()
  359. # No unread topics available - trying to mark the forum as read
  360. if unread_count == 0:
  361. forumread = ForumsRead.query.\
  362. filter(ForumsRead.user_id == user.id,
  363. ForumsRead.forum_id == forum.id).first()
  364. # ForumsRead is already up-to-date.
  365. if forumread and forumread.last_read > topicread.last_read:
  366. return
  367. # ForumRead Entry exists - Updating it because a new post
  368. # has been submitted that the user hasn't read.
  369. elif forumread:
  370. forumread.last_read = datetime.utcnow()
  371. forumread.save()
  372. # No ForumRead Entry existing - creating one.
  373. else:
  374. forumread = ForumsRead()
  375. forumread.user_id = user.id
  376. forumread.forum_id = forum.id
  377. forumread.last_read = datetime.utcnow()
  378. forumread.save()
  379. class Forum(db.Model):
  380. __tablename__ = "forums"
  381. id = db.Column(db.Integer, primary_key=True)
  382. category_id = db.Column(db.Integer, db.ForeignKey("categories.id"),
  383. nullable=False)
  384. title = db.Column(db.String(15), nullable=False)
  385. description = db.Column(db.String(255))
  386. position = db.Column(db.Integer, default=1, nullable=False)
  387. locked = db.Column(db.Boolean, default=False, nullable=False)
  388. show_moderators = db.Column(db.Boolean, default=False, nullable=False)
  389. external = db.Column(db.String(63))
  390. post_count = db.Column(db.Integer, default=0, nullable=False)
  391. topic_count = db.Column(db.Integer, default=0, nullable=False)
  392. # One-to-one
  393. last_post_id = db.Column(db.Integer, db.ForeignKey("posts.id"))
  394. last_post = db.relationship("Post", backref="last_post_forum",
  395. uselist=False, foreign_keys=[last_post_id])
  396. # One-to-many
  397. topics = db.relationship("Topic", backref="forum", lazy="joined",
  398. cascade="all, delete-orphan")
  399. moderators = \
  400. db.relationship("User", secondary=moderators,
  401. primaryjoin=(moderators.c.forum_id == id),
  402. backref=db.backref("forummoderator", lazy="dynamic"),
  403. lazy="joined")
  404. # Methods
  405. def __repr__(self):
  406. """Set to a unique key specific to the object in the database.
  407. Required for cache.memoize() to work across requests.
  408. """
  409. return "<{} {}>".format(self.__class__.__name__, self.id)
  410. def update_last_post(self):
  411. """Updates the last post. This is useful if you move a topic
  412. in another forum
  413. """
  414. last_post = Post.query.\
  415. filter(Post.topic_id == Topic.id,
  416. Topic.forum_id == self.id).\
  417. order_by(Post.date_created.desc()).\
  418. first()
  419. # Last post is none when there are no topics in the forum
  420. if last_post is not None:
  421. # a new last post was found in the forum
  422. if not last_post.id == self.last_post_id:
  423. self.last_post_id = last_post.id
  424. # No post found..
  425. else:
  426. self.last_post_id = 0
  427. db.session.commit()
  428. def save(self, moderators=None):
  429. """Saves a forum"""
  430. if moderators is not None:
  431. for moderator in self.moderators:
  432. self.moderators.remove(moderator)
  433. db.session.commit()
  434. for moderator in moderators:
  435. if moderator:
  436. self.moderators.append(moderator)
  437. db.session.add(self)
  438. db.session.commit()
  439. return self
  440. def delete(self, users=None):
  441. """Deletes forum. If a list with involved user objects is passed,
  442. it will also update their post counts
  443. :param users: A list with user objects
  444. """
  445. # Delete the forum
  446. db.session.delete(self)
  447. db.session.commit()
  448. # Delete all entries from the ForumsRead and TopicsRead relation
  449. ForumsRead.query.filter_by(forum_id=self.id).delete()
  450. TopicsRead.query.filter_by(forum_id=self.id).delete()
  451. # Update the users post count
  452. if users:
  453. users_list = []
  454. for user in users:
  455. user.post_count = Post.query.filter_by(user_id=user.id).count()
  456. users_list.append(user)
  457. db.session.add_all(users_list)
  458. db.session.commit()
  459. return self
  460. class Category(db.Model):
  461. __tablename__ = "categories"
  462. id = db.Column(db.Integer, primary_key=True)
  463. title = db.Column(db.String(63), nullable=False)
  464. description = db.Column(db.String(255))
  465. position = db.Column(db.Integer, default=1, nullable=False)
  466. # One-to-many
  467. forums = db.relationship("Forum", backref="category", lazy="dynamic",
  468. primaryjoin='Forum.category_id == Category.id',
  469. order_by='asc(Forum.position)',
  470. cascade="all, delete-orphan")
  471. def save(self):
  472. """Saves a category"""
  473. db.session.add(self)
  474. db.session.commit()
  475. return self
  476. def delete(self, users=None):
  477. """Deletes a category. If a list with involved user objects is passed,
  478. it will also update their post counts
  479. :param users: A list with user objects
  480. """
  481. # Update the users post count
  482. if users:
  483. for user in users:
  484. user.post_count = Post.query.filter_by(user_id=user.id).count()
  485. db.session.commit()
  486. # and finally delete the category itself
  487. db.session.delete(self)
  488. db.session.commit()
  489. return self