models.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950
  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 url_for, abort
  11. from flaskbb.extensions import db
  12. from flaskbb.utils.helpers import slugify, get_categories_and_forums, get_forums
  13. from flaskbb.utils.settings import flaskbb_config
  14. moderators = db.Table(
  15. 'moderators',
  16. db.Column('user_id', db.Integer(), db.ForeignKey('users.id'),
  17. nullable=False),
  18. db.Column('forum_id', db.Integer(),
  19. db.ForeignKey('forums.id', use_alter=True, name="fk_forum_id"),
  20. nullable=False))
  21. topictracker = db.Table(
  22. 'topictracker',
  23. db.Column('user_id', db.Integer(), db.ForeignKey('users.id'),
  24. nullable=False),
  25. db.Column('topic_id', db.Integer(),
  26. db.ForeignKey('topics.id',
  27. use_alter=True, name="fk_tracker_topic_id"),
  28. nullable=False))
  29. class TopicsRead(db.Model):
  30. __tablename__ = "topicsread"
  31. user_id = db.Column(db.Integer, db.ForeignKey("users.id"),
  32. primary_key=True)
  33. topic_id = db.Column(db.Integer,
  34. db.ForeignKey("topics.id", use_alter=True,
  35. name="fk_tr_topic_id"),
  36. primary_key=True)
  37. forum_id = db.Column(db.Integer,
  38. db.ForeignKey("forums.id", use_alter=True,
  39. name="fk_tr_forum_id"),
  40. primary_key=True)
  41. last_read = db.Column(db.DateTime, default=datetime.utcnow())
  42. def __repr__(self):
  43. return "<{}>".format(self.__class__.__name__)
  44. def save(self):
  45. """Saves a TopicsRead entry."""
  46. db.session.add(self)
  47. db.session.commit()
  48. return self
  49. def delete(self):
  50. """Deletes a TopicsRead entry."""
  51. db.session.delete(self)
  52. db.session.commit()
  53. return self
  54. class ForumsRead(db.Model):
  55. __tablename__ = "forumsread"
  56. user_id = db.Column(db.Integer, db.ForeignKey("users.id"),
  57. primary_key=True)
  58. forum_id = db.Column(db.Integer,
  59. db.ForeignKey("forums.id", use_alter=True,
  60. name="fk_fr_forum_id"),
  61. primary_key=True)
  62. last_read = db.Column(db.DateTime, default=datetime.utcnow())
  63. cleared = db.Column(db.DateTime)
  64. def __repr__(self):
  65. return "<{}>".format(self.__class__.__name__)
  66. def save(self):
  67. """Saves a ForumsRead entry."""
  68. db.session.add(self)
  69. db.session.commit()
  70. return self
  71. def delete(self):
  72. """Deletes a ForumsRead entry."""
  73. db.session.delete(self)
  74. db.session.commit()
  75. return self
  76. class Report(db.Model):
  77. __tablename__ = "reports"
  78. id = db.Column(db.Integer, primary_key=True)
  79. reporter_id = db.Column(db.Integer, db.ForeignKey("users.id"),
  80. nullable=False)
  81. reported = db.Column(db.DateTime, default=datetime.utcnow())
  82. post_id = db.Column(db.Integer, db.ForeignKey("posts.id"), nullable=False)
  83. zapped = db.Column(db.DateTime)
  84. zapped_by = db.Column(db.Integer, db.ForeignKey("users.id"))
  85. reason = db.Column(db.Text)
  86. post = db.relationship("Post", backref="report", lazy="joined")
  87. reporter = db.relationship("User", lazy="joined",
  88. foreign_keys=[reporter_id])
  89. zapper = db.relationship("User", lazy="joined", foreign_keys=[zapped_by])
  90. def __repr__(self):
  91. return "<{} {}>".format(self.__class__.__name__, self.id)
  92. def save(self, post=None, user=None):
  93. """Saves a report.
  94. :param post: The post that should be reported
  95. :param user: The user who has reported the post
  96. :param reason: The reason why the user has reported the post
  97. """
  98. if self.id:
  99. db.session.add(self)
  100. db.session.commit()
  101. return self
  102. if post and user:
  103. self.reporter_id = user.id
  104. self.reported = datetime.utcnow()
  105. self.post_id = post.id
  106. db.session.add(self)
  107. db.session.commit()
  108. return self
  109. def delete(self):
  110. """Deletes a report."""
  111. db.session.delete(self)
  112. db.session.commit()
  113. return self
  114. class Post(db.Model):
  115. __tablename__ = "posts"
  116. __searchable__ = ['content', 'username']
  117. id = db.Column(db.Integer, primary_key=True)
  118. topic_id = db.Column(db.Integer,
  119. db.ForeignKey("topics.id",
  120. use_alter=True,
  121. name="fk_post_topic_id",
  122. ondelete="CASCADE"))
  123. user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
  124. username = db.Column(db.String(200), nullable=False)
  125. content = db.Column(db.Text, nullable=False)
  126. date_created = db.Column(db.DateTime, default=datetime.utcnow())
  127. date_modified = db.Column(db.DateTime)
  128. modified_by = db.Column(db.String(200))
  129. # Properties
  130. @property
  131. def url(self):
  132. """Returns the url for the post"""
  133. return url_for("forum.view_post", post_id=self.id)
  134. # Methods
  135. def __init__(self, content=None):
  136. if content:
  137. self.content = content
  138. def __repr__(self):
  139. """
  140. Set to a unique key specific to the object in the database.
  141. Required for cache.memoize() to work across requests.
  142. """
  143. return "<{} {}>".format(self.__class__.__name__, self.id)
  144. def save(self, user=None, topic=None):
  145. """Saves a new post. If no parameters are passed we assume that
  146. you will just update an existing post. It returns the object after the
  147. operation was successful.
  148. :param user: The user who has created the post
  149. :param topic: The topic in which the post was created
  150. """
  151. # update/edit the post
  152. if self.id:
  153. db.session.add(self)
  154. db.session.commit()
  155. return self
  156. # Adding a new post
  157. if user and topic:
  158. self.user_id = user.id
  159. self.username = user.username
  160. self.topic_id = topic.id
  161. self.date_created = datetime.utcnow()
  162. topic.last_updated = datetime.utcnow()
  163. # This needs to be done before I update the last_post_id.
  164. db.session.add(self)
  165. db.session.commit()
  166. # Now lets update the last post id
  167. topic.last_post_id = self.id
  168. topic.forum.last_post_id = self.id
  169. # Update the post counts
  170. user.post_count += 1
  171. topic.post_count += 1
  172. topic.forum.post_count += 1
  173. # And commit it!
  174. db.session.add(topic)
  175. db.session.commit()
  176. return self
  177. def delete(self):
  178. """Deletes a post and returns self"""
  179. # This will delete the whole topic
  180. if self.topic.first_post_id == self.id:
  181. self.topic.delete()
  182. return self
  183. # Delete the last post
  184. if self.topic.last_post_id == self.id:
  185. # update the last post in the forum
  186. if self.topic.last_post_id == self.topic.forum.last_post_id:
  187. # We need the second last post in the forum here,
  188. # because the last post will be deleted
  189. second_last_post = Post.query.\
  190. filter(Post.topic_id == Topic.id,
  191. Topic.forum_id == self.topic.forum.id).\
  192. order_by(Post.id.desc()).limit(2).offset(0).\
  193. all()
  194. second_last_post = second_last_post[1]
  195. self.topic.forum.last_post_id = second_last_post.id
  196. # check if there is a second last post, else it is the first post
  197. if self.topic.second_last_post:
  198. # Now the second last post will be the last post
  199. self.topic.last_post_id = self.topic.second_last_post
  200. # there is no second last post, now the last post is also the
  201. # first post
  202. else:
  203. self.topic.last_post_id = self.topic.first_post_id
  204. # Update the post counts
  205. self.user.post_count -= 1
  206. self.topic.post_count -= 1
  207. self.topic.forum.post_count -= 1
  208. db.session.commit()
  209. db.session.delete(self)
  210. db.session.commit()
  211. return self
  212. class Topic(db.Model):
  213. __tablename__ = "topics"
  214. __searchable__ = ['title', 'username']
  215. id = db.Column(db.Integer, primary_key=True)
  216. forum_id = db.Column(db.Integer,
  217. db.ForeignKey("forums.id",
  218. use_alter=True,
  219. name="fk_topic_forum_id"),
  220. nullable=False)
  221. title = db.Column(db.String(255), nullable=False)
  222. user_id = db.Column(db.Integer, db.ForeignKey("users.id"))
  223. username = db.Column(db.String(200), nullable=False)
  224. date_created = db.Column(db.DateTime, default=datetime.utcnow())
  225. last_updated = db.Column(db.DateTime, default=datetime.utcnow())
  226. locked = db.Column(db.Boolean, default=False)
  227. important = db.Column(db.Boolean, default=False)
  228. views = db.Column(db.Integer, default=0)
  229. post_count = db.Column(db.Integer, default=0)
  230. # One-to-one (uselist=False) relationship between first_post and topic
  231. first_post_id = db.Column(db.Integer, db.ForeignKey("posts.id",
  232. ondelete="CASCADE"))
  233. first_post = db.relationship("Post", backref="first_post", uselist=False,
  234. foreign_keys=[first_post_id])
  235. # One-to-one
  236. last_post_id = db.Column(db.Integer, db.ForeignKey("posts.id"))
  237. last_post = db.relationship("Post", backref="last_post", uselist=False,
  238. foreign_keys=[last_post_id])
  239. # One-to-many
  240. posts = db.relationship("Post", backref="topic", lazy="joined",
  241. primaryjoin="Post.topic_id == Topic.id",
  242. cascade="all, delete-orphan", post_update=True)
  243. # Properties
  244. @property
  245. def second_last_post(self):
  246. """Returns the second last post."""
  247. return self.posts[-2].id
  248. @property
  249. def slug(self):
  250. """Returns a slugified version from the topic title"""
  251. return slugify(self.title)
  252. @property
  253. def url(self):
  254. """Returns the slugified url for the topic"""
  255. return url_for("forum.view_topic", topic_id=self.id, slug=self.slug)
  256. # Methods
  257. def __init__(self, title=None):
  258. if title:
  259. self.title = title
  260. def __repr__(self):
  261. """
  262. Set to a unique key specific to the object in the database.
  263. Required for cache.memoize() to work across requests.
  264. """
  265. return "<{} {}>".format(self.__class__.__name__, self.id)
  266. def tracker_needs_update(self, forumsread, topicsread):
  267. """Returns True if the topicsread tracker needs an update.
  268. Also, if the ``TRACKER_LENGTH`` is configured, it will just recognize
  269. topics that are newer than the ``TRACKER_LENGTH`` (in days) as unread.
  270. TODO: Couldn't think of a better name for this method - ideas?
  271. :param forumsread: The ForumsRead object is needed because we also
  272. need to check if the forum has been cleared
  273. sometime ago.
  274. :param topicsread: The topicsread object is used to check if there is
  275. a new post in the topic.
  276. """
  277. read_cutoff = None
  278. if flaskbb_config['TRACKER_LENGTH'] > 0:
  279. read_cutoff = datetime.utcnow() - timedelta(
  280. days=flaskbb_config['TRACKER_LENGTH'])
  281. # The tracker is disabled - abort
  282. if read_cutoff is None:
  283. return False
  284. # Else the topic is still below the read_cutoff
  285. elif read_cutoff > self.last_post.date_created:
  286. return False
  287. # Can be None (cleared) if the user has never marked the forum as read.
  288. # If this condition is false - we need to update the tracker
  289. if forumsread and forumsread.cleared is not None and \
  290. forumsread.cleared >= self.last_post.date_created:
  291. return False
  292. if topicsread and topicsread.last_read >= self.last_post.date_created:
  293. return False
  294. return True
  295. def update_read(self, user, forum, forumsread):
  296. """Updates the topicsread and forumsread tracker for a specified user,
  297. if the topic contains new posts or the user hasn't read the topic.
  298. Returns True if the tracker has been updated.
  299. :param user: The user for whom the readstracker should be updated.
  300. :param forum: The forum in which the topic is.
  301. :param forumsread: The forumsread object. It is used to check if there
  302. is a new post since the forum has been marked as
  303. read.
  304. """
  305. # User is not logged in - abort
  306. if not user.is_authenticated():
  307. return False
  308. topicsread = TopicsRead.query.\
  309. filter(TopicsRead.user_id == user.id,
  310. TopicsRead.topic_id == self.id).first()
  311. if not self.tracker_needs_update(forumsread, topicsread):
  312. return False
  313. # Because we return True/False if the trackers have been
  314. # updated, we need to store the status in a temporary variable
  315. updated = False
  316. # A new post has been submitted that the user hasn't read.
  317. # Updating...
  318. if topicsread:
  319. topicsread.last_read = datetime.utcnow()
  320. topicsread.save()
  321. updated = True
  322. # The user has not visited the topic before. Inserting him in
  323. # the TopicsRead model.
  324. elif not topicsread:
  325. topicsread = TopicsRead()
  326. topicsread.user_id = user.id
  327. topicsread.topic_id = self.id
  328. topicsread.forum_id = self.forum_id
  329. topicsread.last_read = datetime.utcnow()
  330. topicsread.save()
  331. updated = True
  332. # No unread posts
  333. else:
  334. updated = False
  335. # Save True/False if the forums tracker has been updated.
  336. updated = forum.update_read(user, forumsread, topicsread)
  337. return updated
  338. def move(self, forum):
  339. """Moves a topic to the given forum.
  340. Returns True if it could successfully move the topic to forum.
  341. :param forum: The new forum for the topic
  342. """
  343. # if the target forum is the current forum, abort
  344. if self.forum_id == forum.id:
  345. return False
  346. old_forum = self.forum
  347. self.forum.post_count -= self.post_count
  348. self.forum.topic_count -= 1
  349. self.forum_id = forum.id
  350. forum.post_count += self.post_count
  351. forum.topic_count += 1
  352. db.session.commit()
  353. forum.update_last_post()
  354. old_forum.update_last_post()
  355. TopicsRead.query.filter_by(topic_id=self.id).delete()
  356. return True
  357. def merge(self, topic):
  358. """Merges a topic with another topic
  359. :param topic: The new topic for the posts in this topic
  360. """
  361. # You can only merge a topic with a differrent topic in the same forum
  362. if self.id == topic.id or not self.forum_id == topic.forum_id:
  363. return False
  364. # Update the topic id
  365. Post.query.filter_by(topic_id=self.id).\
  366. update({Post.topic_id: topic.id})
  367. # Update the last post
  368. if topic.last_post.date_created < self.last_post.date_created:
  369. topic.last_post_id = self.last_post_id
  370. # Increase the post and views count
  371. topic.post_count += self.post_count
  372. topic.views += self.views
  373. topic.save()
  374. # Finally delete the old topic
  375. Topic.query.filter_by(id=self.id).delete()
  376. return True
  377. def save(self, user=None, forum=None, post=None):
  378. """Saves a topic and returns the topic object. If no parameters are
  379. given, it will only update the topic.
  380. :param user: The user who has created the topic
  381. :param forum: The forum where the topic is stored
  382. :param post: The post object which is connected to the topic
  383. """
  384. # Updates the topic
  385. if self.id:
  386. db.session.add(self)
  387. db.session.commit()
  388. return self
  389. # Set the forum and user id
  390. self.forum_id = forum.id
  391. self.user_id = user.id
  392. self.username = user.username
  393. # Set the last_updated time. Needed for the readstracker
  394. self.last_updated = datetime.utcnow()
  395. self.date_created = datetime.utcnow()
  396. # Insert and commit the topic
  397. db.session.add(self)
  398. db.session.commit()
  399. # Create the topic post
  400. post.save(user, self)
  401. # Update the first post id
  402. self.first_post_id = post.id
  403. # Update the topic count
  404. forum.topic_count += 1
  405. db.session.commit()
  406. return self
  407. def delete(self, users=None):
  408. """Deletes a topic with the corresponding posts. If a list with
  409. user objects is passed it will also update their post counts
  410. :param users: A list with user objects
  411. """
  412. # Grab the second last topic in the forum + parents/childs
  413. topic = Topic.query.\
  414. filter_by(forum_id=self.forum_id).\
  415. order_by(Topic.last_post_id.desc()).limit(2).offset(0).all()
  416. # do want to delete the topic with the last post?
  417. if topic and topic[0].id == self.id:
  418. try:
  419. # Now the second last post will be the last post
  420. self.forum.last_post_id = topic[1].last_post_id
  421. # Catch an IndexError when you delete the last topic in the forum
  422. # There is no second last post
  423. except IndexError:
  424. self.forum.last_post_id = None
  425. # Commit the changes
  426. db.session.commit()
  427. # These things needs to be stored in a variable before they are deleted
  428. forum = self.forum
  429. TopicsRead.query.filter_by(topic_id=self.id).delete()
  430. # Delete the topic
  431. db.session.delete(self)
  432. db.session.commit()
  433. # Update the post counts
  434. if users:
  435. for user in users:
  436. user.post_count = Post.query.filter_by(user_id=user.id).count()
  437. db.session.commit()
  438. forum.topic_count = Topic.query.\
  439. filter_by(forum_id=self.forum_id).\
  440. count()
  441. forum.post_count = Post.query.\
  442. filter(Post.topic_id == Topic.id,
  443. Topic.forum_id == self.forum_id).\
  444. count()
  445. db.session.commit()
  446. return self
  447. class Forum(db.Model):
  448. __tablename__ = "forums"
  449. __searchable__ = ['title', 'description']
  450. id = db.Column(db.Integer, primary_key=True)
  451. category_id = db.Column(db.Integer, db.ForeignKey("categories.id"),
  452. nullable=False)
  453. title = db.Column(db.String(255), nullable=False)
  454. description = db.Column(db.Text)
  455. position = db.Column(db.Integer, default=1, nullable=False)
  456. locked = db.Column(db.Boolean, default=False, nullable=False)
  457. show_moderators = db.Column(db.Boolean, default=False, nullable=False)
  458. external = db.Column(db.String(200))
  459. post_count = db.Column(db.Integer, default=0, nullable=False)
  460. topic_count = db.Column(db.Integer, default=0, nullable=False)
  461. # One-to-one
  462. last_post_id = db.Column(db.Integer, db.ForeignKey("posts.id"))
  463. last_post = db.relationship("Post", backref="last_post_forum",
  464. uselist=False, foreign_keys=[last_post_id])
  465. # One-to-many
  466. topics = db.relationship("Topic", backref="forum", lazy="joined",
  467. cascade="all, delete-orphan")
  468. # Many-to-many
  469. moderators = \
  470. db.relationship("User", secondary=moderators,
  471. primaryjoin=(moderators.c.forum_id == id),
  472. backref=db.backref("forummoderator", lazy="dynamic"),
  473. lazy="joined")
  474. # Properties
  475. @property
  476. def slug(self):
  477. """Returns a slugified version from the forum title"""
  478. return slugify(self.title)
  479. @property
  480. def url(self):
  481. """Returns the slugified url for the forum"""
  482. if self.external:
  483. return self.external
  484. return url_for("forum.view_forum", forum_id=self.id, slug=self.slug)
  485. # Methods
  486. def __repr__(self):
  487. """Set to a unique key specific to the object in the database.
  488. Required for cache.memoize() to work across requests.
  489. """
  490. return "<{} {}>".format(self.__class__.__name__, self.id)
  491. def update_last_post(self):
  492. """Updates the last post in the forum."""
  493. last_post = Post.query.\
  494. filter(Post.topic_id == Topic.id,
  495. Topic.forum_id == self.id).\
  496. order_by(Post.date_created.desc()).\
  497. first()
  498. # Last post is none when there are no topics in the forum
  499. if last_post is not None:
  500. # a new last post was found in the forum
  501. if not last_post.id == self.last_post_id:
  502. self.last_post_id = last_post.id
  503. # No post found..
  504. else:
  505. self.last_post_id = None
  506. db.session.commit()
  507. def update_read(self, user, forumsread, topicsread):
  508. """Updates the ForumsRead status for the user. In order to work
  509. correctly, be sure that `topicsread is **not** `None`.
  510. :param user: The user for whom we should check if he has read the
  511. forum.
  512. :param forumsread: The forumsread object. It is needed to check if
  513. if the forum is unread. If `forumsread` is `None`
  514. and the forum is unread, it will create a new entry
  515. in the `ForumsRead` relation, else (and the forum
  516. is still unread) we are just going to update the
  517. entry in the `ForumsRead` relation.
  518. :param topicsread: The topicsread object is used in combination
  519. with the forumsread object to check if the
  520. forumsread relation should be updated and
  521. therefore is unread.
  522. """
  523. if not user.is_authenticated() or topicsread is None:
  524. return False
  525. read_cutoff = None
  526. if flaskbb_config['TRACKER_LENGTH'] > 0:
  527. read_cutoff = datetime.utcnow() - timedelta(
  528. days=flaskbb_config['TRACKER_LENGTH'])
  529. # fetch the unread posts in the forum
  530. unread_count = Topic.query.\
  531. outerjoin(TopicsRead,
  532. db.and_(TopicsRead.topic_id == Topic.id,
  533. TopicsRead.user_id == user.id)).\
  534. outerjoin(ForumsRead,
  535. db.and_(ForumsRead.forum_id == Topic.forum_id,
  536. ForumsRead.user_id == user.id)).\
  537. filter(Topic.forum_id == self.id,
  538. Topic.last_updated > read_cutoff,
  539. db.or_(TopicsRead.last_read == None,
  540. TopicsRead.last_read < Topic.last_updated)).\
  541. count()
  542. # No unread topics available - trying to mark the forum as read
  543. if unread_count == 0:
  544. if forumsread and forumsread.last_read > topicsread.last_read:
  545. return False
  546. # ForumRead Entry exists - Updating it because a new topic/post
  547. # has been submitted and has read everything (obviously, else the
  548. # unread_count would be useless).
  549. elif forumsread:
  550. forumsread.last_read = datetime.utcnow()
  551. forumsread.save()
  552. return True
  553. # No ForumRead Entry existing - creating one.
  554. forumsread = ForumsRead()
  555. forumsread.user_id = user.id
  556. forumsread.forum_id = self.id
  557. forumsread.last_read = datetime.utcnow()
  558. forumsread.save()
  559. return True
  560. # Nothing updated, because there are still more than 0 unread topicsread
  561. return False
  562. def save(self, moderators=None):
  563. """Saves a forum"""
  564. if moderators is not None:
  565. for moderator in self.moderators:
  566. self.moderators.remove(moderator)
  567. db.session.commit()
  568. for moderator in moderators:
  569. if moderator:
  570. self.moderators.append(moderator)
  571. db.session.add(self)
  572. db.session.commit()
  573. return self
  574. def delete(self, users=None):
  575. """Deletes forum. If a list with involved user objects is passed,
  576. it will also update their post counts
  577. :param users: A list with user objects
  578. """
  579. # Delete the forum
  580. db.session.delete(self)
  581. db.session.commit()
  582. # Delete the entries for the forum in the ForumsRead and TopicsRead
  583. # relation
  584. ForumsRead.query.filter_by(forum_id=self.id).delete()
  585. TopicsRead.query.filter_by(forum_id=self.id).delete()
  586. # Update the users post count
  587. if users:
  588. users_list = []
  589. for user in users:
  590. user.post_count = Post.query.filter_by(user_id=user.id).count()
  591. users_list.append(user)
  592. db.session.add_all(users_list)
  593. db.session.commit()
  594. return self
  595. # Classmethods
  596. @classmethod
  597. def get_forum(cls, forum_id, user):
  598. """Returns the forum and forumsread object as a tuple for the user.
  599. :param forum_id: The forum id
  600. :param user: The user object is needed to check if we also need their
  601. forumsread object.
  602. """
  603. if user.is_authenticated():
  604. forum, forumsread = Forum.query.\
  605. filter(Forum.id == forum_id).\
  606. options(db.joinedload("category")).\
  607. outerjoin(ForumsRead,
  608. db.and_(ForumsRead.forum_id == Forum.id,
  609. ForumsRead.user_id == user.id)).\
  610. add_entity(ForumsRead).\
  611. first_or_404()
  612. else:
  613. forum = Forum.query.filter(Forum.id == forum_id).first_or_404()
  614. forumsread = None
  615. return forum, forumsread
  616. @classmethod
  617. def get_topics(cls, forum_id, user, page=1, per_page=20):
  618. """Get the topics for the forum. If the user is logged in,
  619. it will perform an outerjoin for the topics with the topicsread and
  620. forumsread relation to check if it is read or unread.
  621. :param forum_id: The forum id
  622. :param user: The user object
  623. :param page: The page whom should be loaded
  624. :param per_page: How many topics per page should be shown
  625. """
  626. if user.is_authenticated():
  627. topics = Topic.query.filter_by(forum_id=forum_id).\
  628. outerjoin(TopicsRead,
  629. db.and_(TopicsRead.topic_id == Topic.id,
  630. TopicsRead.user_id == user.id)).\
  631. add_entity(TopicsRead).\
  632. order_by(Topic.last_updated.desc()).\
  633. paginate(page, per_page, True)
  634. else:
  635. topics = Topic.query.filter_by(forum_id=forum_id).\
  636. order_by(Topic.last_updated.desc()).\
  637. paginate(page, per_page, True)
  638. topics.items = [(topic, None) for topic in topics.items]
  639. return topics
  640. class Category(db.Model):
  641. __tablename__ = "categories"
  642. __searchable__ = ['title', 'description']
  643. id = db.Column(db.Integer, primary_key=True)
  644. title = db.Column(db.String(255), nullable=False)
  645. description = db.Column(db.Text)
  646. position = db.Column(db.Integer, default=1, nullable=False)
  647. # One-to-many
  648. forums = db.relationship("Forum", backref="category", lazy="dynamic",
  649. primaryjoin='Forum.category_id == Category.id',
  650. order_by='asc(Forum.position)',
  651. cascade="all, delete-orphan")
  652. # Properties
  653. @property
  654. def slug(self):
  655. """Returns a slugified version from the category title"""
  656. return slugify(self.title)
  657. @property
  658. def url(self):
  659. """Returns the slugified url for the category"""
  660. return url_for("forum.view_category", category_id=self.id,
  661. slug=self.slug)
  662. # Methods
  663. def __repr__(self):
  664. """Set to a unique key specific to the object in the database.
  665. Required for cache.memoize() to work across requests.
  666. """
  667. return "<{} {}>".format(self.__class__.__name__, self.id)
  668. def save(self):
  669. """Saves a category"""
  670. db.session.add(self)
  671. db.session.commit()
  672. return self
  673. def delete(self, users=None):
  674. """Deletes a category. If a list with involved user objects is passed,
  675. it will also update their post counts
  676. :param users: A list with user objects
  677. """
  678. # and finally delete the category itself
  679. db.session.delete(self)
  680. db.session.commit()
  681. # Update the users post count
  682. if users:
  683. for user in users:
  684. user.post_count = Post.query.filter_by(user_id=user.id).count()
  685. db.session.commit()
  686. return self
  687. # Classmethods
  688. @classmethod
  689. def get_all(cls, user):
  690. """Get all categories with all associated forums.
  691. It returns a list with tuples. Those tuples are containing the category
  692. and their associated forums (whose are stored in a list).
  693. For example::
  694. [(<Category 1>, [(<Forum 2>, <ForumsRead>), (<Forum 1>, None)]),
  695. (<Category 2>, [(<Forum 3>, None), (<Forum 4>, None)])]
  696. :param user: The user object is needed to check if we also need their
  697. forumsread object.
  698. """
  699. if user.is_authenticated():
  700. forums = cls.query.\
  701. join(Forum, cls.id == Forum.category_id).\
  702. outerjoin(ForumsRead,
  703. db.and_(ForumsRead.forum_id == Forum.id,
  704. ForumsRead.user_id == user.id)).\
  705. add_entity(Forum).\
  706. add_entity(ForumsRead).\
  707. order_by(Category.id, Category.position, Forum.position).\
  708. all()
  709. else:
  710. # Get all the forums
  711. forums = cls.query.\
  712. join(Forum, cls.id == Forum.category_id).\
  713. add_entity(Forum).\
  714. order_by(Category.id, Category.position, Forum.position).\
  715. all()
  716. return get_categories_and_forums(forums, user)
  717. @classmethod
  718. def get_forums(cls, category_id, user):
  719. """Get the forums for the category.
  720. It returns a tuple with the category and the forums with their
  721. forumsread object are stored in a list.
  722. A return value can look like this for a category with two forums::
  723. (<Category 1>, [(<Forum 1>, None), (<Forum 2>, None)])
  724. :param category_id: The category id
  725. :param user: The user object is needed to check if we also need their
  726. forumsread object.
  727. """
  728. if user.is_authenticated():
  729. forums = cls.query.\
  730. filter(cls.id == category_id).\
  731. join(Forum, cls.id == Forum.category_id).\
  732. outerjoin(ForumsRead,
  733. db.and_(ForumsRead.forum_id == Forum.id,
  734. ForumsRead.user_id == user.id)).\
  735. add_entity(Forum).\
  736. add_entity(ForumsRead).\
  737. order_by(Forum.position).\
  738. all()
  739. else:
  740. forums = cls.query.\
  741. filter(cls.id == category_id).\
  742. join(Forum, cls.id == Forum.category_id).\
  743. add_entity(Forum).\
  744. order_by(Forum.position).\
  745. all()
  746. if not forums:
  747. abort(404)
  748. return get_forums(forums, user)