models.py 32 KB

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