class Conversation(db.Model, CRUDMixin): __tablename__ = "conversations" id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False) from_user_id = db.Column(db.Integer, db.ForeignKey("users.id")) to_user_id = db.Column(db.Integer, db.ForeignKey("users.id")) shared_id = db.Column(UUIDType, nullable=False) subject = db.Column(db.String(255)) date_created = db.Column(UTCDateTime(timezone=True), default=time_utcnow) trash = db.Column(db.Boolean, nullable=False, default=False) draft = db.Column(db.Boolean, nullable=False, default=False) unread = db.Column(db.Boolean, nullable=False, default=True) messages = db.relationship( "Message", lazy="joined", backref="conversation", primaryjoin="Message.conversation_id == Conversation.id", order_by="asc(Message.id)", cascade="all, delete-orphan") # this is actually the users message box user = db.relationship("User", lazy="joined", foreign_keys=[user_id]) # the user to whom the conversation is addressed to_user = db.relationship("User", lazy="joined", foreign_keys=[to_user_id]) # the user who sent the message from_user = db.relationship("User", lazy="joined", foreign_keys=[from_user_id]) @property def first_message(self): """Returns the first message object.""" return self.messages[0] @property def last_message(self): """Returns the last message object.""" return self.messages[-1] def save(self, message=None): """Saves a conversation and returns the saved conversation object. :param message: If given, it will also save the message for the conversation. It expects a Message object. """ if message is not None: # create the conversation self.date_created = time_utcnow() db.session.add(self) db.session.commit() # create the actual message for the conversation message.save(self) return self db.session.add(self) db.session.commit() return self
class UserRank(db.Model): id = db.Column(db.Integer(), primary_key=True) user_id = db.Column(db.ForeignKey("users.id"), nullable=False) rank_id = db.Column(db.ForeignKey("ranks.id")) user = db.relationship( User, backref=db.backref("user_rank", uselist=False, cascade="all, delete-orphan", lazy="joined"), uselist=False, lazy="joined", foreign_keys=[user_id], ) rank = db.relationship( Rank, backref=db.backref("user_ranks", lazy="joined", cascade="all, delete-orphan"), uselist=False, lazy="joined", foreign_keys=[rank_id], ) name = association_proxy("rank", "rank_name") code = association_proxy("rank", "rank_code") def is_custom(self): return self.rank.is_custom() def __repr__(self): return "<UserRank user={} name={}>".format(self.user.username, self.name)
class Subscription(db.Model, CRUDMixin): 'Forum subscriptions for user' __tablename__ = 'subby_subscriptions' #: ID of the user user_id = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), primary_key=True, nullable=False) #: ID of a forum they subscribe to forum_id = db.Column(db.Integer, db.ForeignKey('forums.id', ondelete='CASCADE'), primary_key=True, nullable=False) #: Subscription owner user = db.relationship('User', lazy='joined', foreign_keys=(user_id, )) #: Forum subscribed to forum = db.relationship('Forum', lazy='joined', foreign_keys=(forum_id, )) def save(self): 'Saves forum subscription' db.session.add(self) db.session.commit() return self
class PrivateMessage(db.Model): __tablename__ = "privatemessages" id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False) from_user_id = db.Column(db.Integer, db.ForeignKey("users.id")) to_user_id = db.Column(db.Integer, db.ForeignKey("users.id")) subject = db.Column(db.String(255)) message = db.Column(db.Text) date_created = db.Column(db.DateTime, default=datetime.utcnow()) trash = db.Column(db.Boolean, nullable=False, default=False) draft = db.Column(db.Boolean, nullable=False, default=False) unread = db.Column(db.Boolean, nullable=False, default=True) user = db.relationship("User", backref="pms", lazy="joined", foreign_keys=[user_id]) from_user = db.relationship("User", lazy="joined", foreign_keys=[from_user_id]) to_user = db.relationship("User", lazy="joined", foreign_keys=[to_user_id]) def save(self, from_user=None, to_user=None, user_id=None, draft=False): """Saves a private message. :param from_user: The user who has sent the message :param to_user: The user who should recieve the message :param user_id: The senders user id - This is the id to which user the Inbox belongs. :param draft: If the message is a draft. Defaults to ``False``. """ if self.id: db.session.add(self) db.session.commit() return self # Defaults to ``False``. self.draft = draft # Add the message to the user's pm box self.user_id = user_id self.from_user_id = from_user self.to_user_id = to_user self.date_created = datetime.utcnow() db.session.add(self) db.session.commit() return self def delete(self): """Deletes a private message""" db.session.delete(self) db.session.commit() return self
class Report(db.Model, CRUDMixin): __tablename__ = "reports" # TODO: Store in addition to the info below topic title and username # as well. So that in case a user or post gets deleted, we can # still view the report id = db.Column(db.Integer, primary_key=True) reporter_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True) reported = db.Column(UTCDateTime(timezone=True), default=time_utcnow, nullable=False) post_id = db.Column(db.Integer, db.ForeignKey("posts.id"), nullable=True) zapped = db.Column(UTCDateTime(timezone=True), nullable=True) zapped_by = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True) reason = db.Column(db.Text, nullable=True) post = db.relationship("Post", lazy="joined", backref=db.backref('report', cascade='all, delete-orphan')) reporter = db.relationship("User", lazy="joined", foreign_keys=[reporter_id]) zapper = db.relationship("User", lazy="joined", foreign_keys=[zapped_by]) def __repr__(self): return "<{} {}>".format(self.__class__.__name__, self.id) def save(self, post=None, user=None): """Saves a report. :param post: The post that should be reported :param user: The user who has reported the post :param reason: The reason why the user has reported the post """ if self.id: db.session.add(self) db.session.commit() return self if post and user: self.reporter = user self.reported = time_utcnow() self.post = post db.session.add(self) db.session.commit() return self
class Report(db.Model): __tablename__ = "reports" id = db.Column(db.Integer, primary_key=True) reporter_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False) reported = db.Column(db.DateTime, default=datetime.utcnow()) post_id = db.Column(db.Integer, db.ForeignKey("posts.id"), nullable=False) zapped = db.Column(db.DateTime) zapped_by = db.Column(db.Integer, db.ForeignKey("users.id")) reason = db.Column(db.Text) post = db.relationship("Post", backref="report", lazy="joined") reporter = db.relationship("User", lazy="joined", foreign_keys=[reporter_id]) zapper = db.relationship("User", lazy="joined", foreign_keys=[zapped_by]) def __repr__(self): return "<{} {}>".format(self.__class__.__name__, self.id) def save(self, post=None, user=None): """Saves a report. :param post: The post that should be reported :param user: The user who has reported the post :param reason: The reason why the user has reported the post """ if self.id: db.session.add(self) db.session.commit() return self if post and user: self.reporter_id = user.id self.reported = datetime.utcnow() self.post_id = post.id db.session.add(self) db.session.commit() return self def delete(self): """Deletes a report.""" db.session.delete(self) db.session.commit() return self
class ForumsRead(db.Model, CRUDMixin): __tablename__ = "forumsread" user_id = db.Column(db.Integer, db.ForeignKey("users.id", ondelete="CASCADE"), primary_key=True) user = db.relationship('User', uselist=False, foreign_keys=[user_id]) forum_id = db.Column(db.Integer, db.ForeignKey("forums.id", ondelete="CASCADE"), primary_key=True) forum = db.relationship('Forum', uselist=False, foreign_keys=[forum_id]) last_read = db.Column(UTCDateTime(timezone=True), default=time_utcnow, nullable=False) cleared = db.Column(UTCDateTime(timezone=True), nullable=True)
class SubscriptionSettings(db.Model, CRUDMixin): 'Subscription settings for user' __tablename__ = 'subby_settings' #: ID of the user user_id = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), primary_key=True, nullable=False) #: Include tracked topics tracked_topics = db.Column(db.Boolean, nullable=False, default=False) #: Enable email notifications email = db.Column(db.Boolean, nullable=False, default=True) #: RSS key rss_key = db.Column(db.String) #: Settings owner user = db.relationship('User', lazy='joined', foreign_keys=(user_id, )) def save(self): 'Saves subscription settings' if not self.rss_key: self.rss_key = SubscriptionSettings._regenerate_rss_key() db.session.add(self) db.session.commit() @staticmethod def _regenerate_rss_key(): 'Regenerates unique RSS key' return str(uuid4())
class Message(db.Model, CRUDMixin): __tablename__ = "messages" id = db.Column(db.Integer, primary_key=True) conversation_id = db.Column(db.Integer, db.ForeignKey("conversations.id"), nullable=False) # the user who wrote the message user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False) message = db.Column(db.Text, nullable=False) date_created = db.Column(UTCDateTime(timezone=True), default=time_utcnow, nullable=False) user = db.relationship("User", lazy="joined") def save(self, conversation=None): """Saves a private message. :param conversation: The conversation to which the message belongs to. """ if conversation is not None: self.conversation = conversation conversation.date_modified = time_utcnow() self.date_created = time_utcnow() db.session.add(self) db.session.commit() return self
class TopicsRead(db.Model, CRUDMixin): __tablename__ = "topicsread" user_id = db.Column(db.Integer, db.ForeignKey("users.id"), primary_key=True) user = db.relationship('User', uselist=False, foreign_keys=[user_id]) topic_id = db.Column(db.Integer, db.ForeignKey("topics.id", use_alter=True, name="fk_tr_topic_id"), primary_key=True) topic = db.relationship('Topic', uselist=False, foreign_keys=[topic_id]) forum_id = db.Column(db.Integer, db.ForeignKey("forums.id", use_alter=True, name="fk_tr_forum_id"), primary_key=True) forum = db.relationship('Forum', uselist=False, foreign_keys=[forum_id]) last_read = db.Column(UTCDateTime(timezone=True), default=time_utcnow, nullable=False)
class SettingsGroup(db.Model, CRUDMixin): __tablename__ = "settingsgroup" key = db.Column(db.String(255), primary_key=True) name = db.Column(db.String(255), nullable=False) description = db.Column(db.Text, nullable=False) settings = db.relationship("Setting", lazy="dynamic", backref="group", cascade="all, delete-orphan")
class SettingsGroup(db.Model, CRUDMixin): __tablename__ = "settingsgroup" key = db.Column(db.String(255), primary_key=True) name = db.Column(db.String(255), nullable=False) description = db.Column(db.Text, nullable=False) settings = db.relationship("Setting", lazy="dynamic", backref="group", cascade="all, delete-orphan") def __repr__(self): return "<{} {}>".format(self.__class__.__name__, self.key)
class Category(db.Model): __tablename__ = "categories" id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(63), nullable=False) description = db.Column(db.String(255)) position = db.Column(db.Integer, default=1, nullable=False) # One-to-many forums = db.relationship("Forum", backref="category", lazy="dynamic", primaryjoin='Forum.category_id == Category.id', order_by='asc(Forum.position)', cascade="all, delete-orphan") # Properties @property def slug(self): """Returns a slugified version from the category title""" return slugify(self.title) @property def url(self): """Returns the url for the category""" return url_for("forum.view_category", category_id=self.id, slug=self.slug) # Methods def save(self): """Saves a category""" db.session.add(self) db.session.commit() return self def delete(self, users=None): """Deletes a category. If a list with involved user objects is passed, it will also update their post counts :param users: A list with user objects """ # Update the users post count if users: for user in users: user.post_count = Post.query.filter_by(user_id=user.id).count() db.session.commit() # and finally delete the category itself db.session.delete(self) db.session.commit() return self
class PostLike(db.Model): __tablename__ = 'vanity_association' post_id = db.Column(db.ForeignKey('posts.id'), primary_key=True) post = db.relationship( Post, backref=db.backref( "liked_by_users", lazy='joined', cascade='all, delete-orphan' ) ) user_id = db.Column(db.ForeignKey('users.id'), primary_key=True) user = db.relationship( User, uselist=False, #lazy='joined', # see: https://github.com/flaskbb/flaskbb/issues/503#issuecomment-415713742 backref=db.backref( "user_liked_posts", cascade="all, delete-orphan", ), )
class SettingsGroup(db.Model): __tablename__ = "settingsgroup" key = db.Column(db.String, primary_key=True) name = db.Column(db.String, nullable=False) description = db.Column(db.String, nullable=False) settings = db.relationship("Setting", lazy="dynamic", backref="group", cascade="all, delete-orphan") def save(self): """Saves a settingsgroup.""" db.session.add(self) db.session.commit() def delete(self): """Deletes a settingsgroup.""" db.session.delete(self) db.session.commit()
class HubLog(db.Model): id = db.Column(db.Integer, primary_key=True) server_id = db.Column(String(50), nullable=False) datetime = db.Column(UTCDateTime(timezone=True), default=time_utcnow, nullable=False) user_id = db.Column( db.Integer, db.ForeignKey("users.id", ondelete="SET NULL"), nullable=True, ) message = db.Column(db.Text, nullable=False) user = db.relationship("User", lazy="joined", foreign_keys=[user_id]) def save(self): db.session.add(self) db.session.commit() return self
class Message(db.Model): __tablename__ = "messages" id = db.Column(db.Integer, primary_key=True) conversation_id = db.Column(db.Integer, db.ForeignKey("conversations.id"), nullable=False) # the user who wrote the message user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False) message = db.Column(db.Text, nullable=False) date_created = db.Column(db.DateTime, default=datetime.utcnow()) user = db.relationship("User", lazy="joined") def save(self, conversation=None): """Saves a private message. :param conversation_id: The id of the conversation to which the message belongs to. """ if conversation is not None: self.conversation_id = conversation.id self.user_id = conversation.from_user_id self.date_created = datetime.utcnow() db.session.add(self) db.session.commit() return self def delete(self): """Deletes a private message""" db.session.delete(self) db.session.commit() return self
class Topic(db.Model): __tablename__ = "topics" query_class = TopicQuery id = db.Column(db.Integer, primary_key=True) forum_id = db.Column(db.Integer, db.ForeignKey("forums.id", use_alter=True, name="fk_topic_forum_id"), nullable=False) title = db.Column(db.String(63), nullable=False) user_id = db.Column(db.Integer, db.ForeignKey("users.id")) username = db.Column(db.String(15), nullable=False) date_created = db.Column(db.DateTime, default=datetime.utcnow()) last_updated = db.Column(db.DateTime, default=datetime.utcnow()) locked = db.Column(db.Boolean, default=False) important = db.Column(db.Boolean, default=False) views = db.Column(db.Integer, default=0) post_count = db.Column(db.Integer, default=0) # One-to-one (uselist=False) relationship between first_post and topic first_post_id = db.Column(db.Integer, db.ForeignKey("posts.id", ondelete="CASCADE")) first_post = db.relationship("Post", backref="first_post", uselist=False, foreign_keys=[first_post_id]) # One-to-one last_post_id = db.Column(db.Integer, db.ForeignKey("posts.id")) last_post = db.relationship("Post", backref="last_post", uselist=False, foreign_keys=[last_post_id]) # One-to-many posts = db.relationship("Post", backref="topic", lazy="joined", primaryjoin="Post.topic_id == Topic.id", cascade="all, delete-orphan", post_update=True) # Properties @property def second_last_post(self): """Returns the second last post.""" return self.posts[-2].id @property def slug(self): """Returns a slugified version from the topic title""" return slugify(self.title) @property def url(self): """Returns the url for the topic""" return url_for("forum.view_topic", topic_id=self.id, slug=self.slug) # Methods def __init__(self, title=None): if title: self.title = title def __repr__(self): """ Set to a unique key specific to the object in the database. Required for cache.memoize() to work across requests. """ return "<{} {}>".format(self.__class__.__name__, self.id) def move(self, forum): """Moves a topic to the given forum. Returns True if it could successfully move the topic to forum. :param forum: The new forum for the topic """ # if the target forum is the current forum, abort if self.forum_id == forum.id: return False old_forum = self.forum self.forum.post_count -= self.post_count self.forum.topic_count -= 1 self.forum_id = forum.id forum.post_count += self.post_count forum.topic_count += 1 db.session.commit() forum.update_last_post() old_forum.update_last_post() TopicsRead.query.filter_by(topic_id=self.id).delete() return True def save(self, user=None, forum=None, post=None): """Saves a topic and returns the topic object. If no parameters are given, it will only update the topic. :param user: The user who has created the topic :param forum: The forum where the topic is stored :param post: The post object which is connected to the topic """ # Updates the topic if self.id: db.session.add(self) db.session.commit() return self # Set the forum and user id self.forum_id = forum.id self.user_id = user.id self.username = user.username # Insert and commit the topic db.session.add(self) db.session.commit() # Create the topic post post.save(user, self) # Update the first post id self.first_post_id = post.id # Update the topic count forum.topic_count += 1 db.session.commit() return self def delete(self, users=None): """Deletes a topic with the corresponding posts. If a list with user objects is passed it will also update their post counts :param users: A list with user objects """ # Grab the second last topic in the forum + parents/childs topic = Topic.query.\ filter_by(forum_id=self.forum_id).\ order_by(Topic.last_post_id.desc()).limit(2).offset(0).all() # do want to delete the topic with the last post? if topic and topic[0].id == self.id: try: # Now the second last post will be the last post self.forum.last_post_id = topic[1].last_post_id # Catch an IndexError when you delete the last topic in the forum except IndexError: self.forum.last_post_id = None # These things needs to be stored in a variable before they are deleted forum = self.forum # Delete the topic db.session.delete(self) db.session.commit() # Update the post counts if users: for user in users: user.post_count = Post.query.filter_by(user_id=user.id).count() db.session.commit() forum.topic_count = Topic.query.\ filter_by(forum_id=self.forum_id).\ count() forum.post_count = Post.query.\ filter(Post.topic_id == Topic.id, Topic.forum_id == self.forum_id).\ count() TopicsRead.query.filter_by(topic_id=self.id).delete() db.session.commit() return self def update_read(self, user, forum, forumsread=None): """Update the topics read status if the user hasn't read the latest post. :param user: The user for whom the readstracker should be updated :param forum: The forum in which the topic is :param forumsread: The forumsread object. It is used to check if there is a new post since the forum has been marked as read """ read_cutoff = datetime.utcnow() - timedelta( days=current_app.config['TRACKER_LENGTH']) # Anonymous User or the post is too old for inserting it in the # TopicsRead model if not user.is_authenticated() or \ read_cutoff > self.last_post.date_created: return topicread = TopicsRead.query.\ filter(TopicsRead.user_id == user.id, TopicsRead.topic_id == self.id).first() # Can be None if the user has never marked the forum as read. If this # condition is false - we need to update the tracker if forumsread and forumsread.cleared is not None and \ forumsread.cleared >= self.last_post.date_created: return # A new post has been submitted that the user hasn't read. # Updating... if topicread and (topicread.last_read < self.last_post.date_created): topicread.last_read = datetime.utcnow() topicread.save() # The user has not visited the topic before. Inserting him in # the TopicsRead model. elif not topicread: topicread = TopicsRead() topicread.user_id = user.id topicread.topic_id = self.id topicread.forum_id = self.forum_id topicread.last_read = datetime.utcnow() topicread.save() # else: no unread posts if forum: # fetch the unread posts in the forum unread_count = Topic.query.\ outerjoin(TopicsRead, db.and_(TopicsRead.topic_id == Topic.id, TopicsRead.user_id == user.id)).\ outerjoin(ForumsRead, db.and_(ForumsRead.forum_id == Topic.forum_id, ForumsRead.user_id == user.id)).\ filter(Topic.forum_id == forum.id, db.or_(TopicsRead.last_read == None, TopicsRead.last_read < Topic.last_updated)).\ count() # No unread topics available - trying to mark the forum as read if unread_count == 0: forumread = ForumsRead.query.\ filter(ForumsRead.user_id == user.id, ForumsRead.forum_id == forum.id).first() # ForumsRead is already up-to-date. if forumread and forumread.last_read > topicread.last_read: return # ForumRead Entry exists - Updating it because a new post # has been submitted that the user hasn't read. elif forumread: forumread.last_read = datetime.utcnow() forumread.save() # No ForumRead Entry existing - creating one. else: forumread = ForumsRead() forumread.user_id = user.id forumread.forum_id = forum.id forumread.last_read = datetime.utcnow() forumread.save()
class Category(db.Model, CRUDMixin): __tablename__ = "categories" __searchable__ = ['title', 'description'] id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(255), nullable=False) description = db.Column(db.Text) position = db.Column(db.Integer, default=1, nullable=False) # One-to-many forums = db.relationship("Forum", backref="category", lazy="dynamic", primaryjoin='Forum.category_id == Category.id', order_by='asc(Forum.position)', cascade="all, delete-orphan") # Properties @property def slug(self): """Returns a slugified version from the category title""" return slugify(self.title) @property def url(self): """Returns the slugified url for the category""" return url_for("forum.view_category", category_id=self.id, slug=self.slug) # Methods def __repr__(self): """Set to a unique key specific to the object in the database. Required for cache.memoize() to work across requests. """ return "<{} {}>".format(self.__class__.__name__, self.id) def delete(self, users=None): """Deletes a category. If a list with involved user objects is passed, it will also update their post counts :param users: A list with user objects """ # and finally delete the category itself db.session.delete(self) db.session.commit() # Update the users post count if users: for user in users: user.post_count = Post.query.filter_by(user_id=user.id).count() db.session.commit() return self # Classmethods @classmethod def get_all(cls, user): """Get all categories with all associated forums. It returns a list with tuples. Those tuples are containing the category and their associated forums (whose are stored in a list). For example:: [(<Category 1>, [(<Forum 2>, <ForumsRead>), (<Forum 1>, None)]), (<Category 2>, [(<Forum 3>, None), (<Forum 4>, None)])] :param user: The user object is needed to check if we also need their forumsread object. """ # import Group model locally to avoid cicular imports from flaskbb.user.models import Group if user.is_authenticated: # get list of user group ids user_groups = [gr.id for gr in user.groups] # filter forums by user groups user_forums = Forum.query.\ filter(Forum.groups.any(Group.id.in_(user_groups))).\ subquery() forum_alias = aliased(Forum, user_forums) # get all forums = cls.query.\ join(forum_alias, cls.id == forum_alias.category_id).\ outerjoin(ForumsRead, db.and_(ForumsRead.forum_id == forum_alias.id, ForumsRead.user_id == user.id)).\ add_entity(forum_alias).\ add_entity(ForumsRead).\ order_by(Category.position, Category.id, forum_alias.position).\ all() else: guest_group = Group.get_guest_group() # filter forums by guest groups guest_forums = Forum.query.\ filter(Forum.groups.any(Group.id == guest_group.id)).\ subquery() forum_alias = aliased(Forum, guest_forums) forums = cls.query.\ join(forum_alias, cls.id == forum_alias.category_id).\ add_entity(forum_alias).\ order_by(Category.position, Category.id, forum_alias.position).\ all() return get_categories_and_forums(forums, user) @classmethod def get_forums(cls, category_id, user): """Get the forums for the category. It returns a tuple with the category and the forums with their forumsread object are stored in a list. A return value can look like this for a category with two forums:: (<Category 1>, [(<Forum 1>, None), (<Forum 2>, None)]) :param category_id: The category id :param user: The user object is needed to check if we also need their forumsread object. """ from flaskbb.user.models import Group if user.is_authenticated: # get list of user group ids user_groups = [gr.id for gr in user.groups] # filter forums by user groups user_forums = Forum.query.\ filter(Forum.groups.any(Group.id.in_(user_groups))).\ subquery() forum_alias = aliased(Forum, user_forums) forums = cls.query.\ filter(cls.id == category_id).\ join(forum_alias, cls.id == forum_alias.category_id).\ outerjoin(ForumsRead, db.and_(ForumsRead.forum_id == forum_alias.id, ForumsRead.user_id == user.id)).\ add_entity(forum_alias).\ add_entity(ForumsRead).\ order_by(forum_alias.position).\ all() else: guest_group = Group.get_guest_group() # filter forums by guest groups guest_forums = Forum.query.\ filter(Forum.groups.any(Group.id == guest_group.id)).\ subquery() forum_alias = aliased(Forum, guest_forums) forums = cls.query.\ filter(cls.id == category_id).\ join(forum_alias, cls.id == forum_alias.category_id).\ add_entity(forum_alias).\ order_by(forum_alias.position).\ all() if not forums: abort(404) return get_forums(forums, user)
class Forum(db.Model, CRUDMixin): __tablename__ = "forums" __searchable__ = ['title', 'description'] id = db.Column(db.Integer, primary_key=True) category_id = db.Column(db.Integer, db.ForeignKey("categories.id"), nullable=False) title = db.Column(db.String(255), nullable=False) description = db.Column(db.Text) position = db.Column(db.Integer, default=1, nullable=False) locked = db.Column(db.Boolean, default=False, nullable=False) show_moderators = db.Column(db.Boolean, default=False, nullable=False) external = db.Column(db.String(200)) post_count = db.Column(db.Integer, default=0, nullable=False) topic_count = db.Column(db.Integer, default=0, nullable=False) # One-to-one last_post_id = db.Column(db.Integer, db.ForeignKey("posts.id")) last_post = db.relationship("Post", backref="last_post_forum", uselist=False, foreign_keys=[last_post_id]) # Not nice, but needed to improve the performance last_post_title = db.Column(db.String(255)) last_post_user_id = db.Column(db.Integer, db.ForeignKey("users.id")) last_post_username = db.Column(db.String(255)) last_post_created = db.Column(UTCDateTime(timezone=True), default=time_utcnow) # One-to-many topics = db.relationship("Topic", backref="forum", lazy="dynamic", cascade="all, delete-orphan") # Many-to-many moderators = db.relationship("User", secondary=moderators, primaryjoin=(moderators.c.forum_id == id), backref=db.backref("forummoderator", lazy="dynamic"), lazy="joined") groups = db.relationship( "Group", secondary=forumgroups, primaryjoin=(forumgroups.c.forum_id == id), backref="forumgroups", lazy="joined", ) # Properties @property def slug(self): """Returns a slugified version from the forum title""" return slugify(self.title) @property def url(self): """Returns the slugified url for the forum""" if self.external: return self.external return url_for("forum.view_forum", forum_id=self.id, slug=self.slug) @property def last_post_url(self): """Returns the url for the last post in the forum""" return url_for("forum.view_post", post_id=self.last_post_id) # Methods def __repr__(self): """Set to a unique key specific to the object in the database. Required for cache.memoize() to work across requests. """ return "<{} {}>".format(self.__class__.__name__, self.id) def update_last_post(self): """Updates the last post in the forum.""" last_post = Post.query.\ filter(Post.topic_id == Topic.id, Topic.forum_id == self.id).\ order_by(Post.date_created.desc()).\ first() # Last post is none when there are no topics in the forum if last_post is not None: # a new last post was found in the forum if not last_post.id == self.last_post_id: self.last_post_id = last_post.id self.last_post_title = last_post.topic.title self.last_post_user_id = last_post.user_id self.last_post_username = last_post.username self.last_post_created = last_post.date_created # No post found.. else: self.last_post_id = None self.last_post_title = None self.last_post_user_id = None self.last_post_username = None self.last_post_created = None db.session.commit() def update_read(self, user, forumsread, topicsread): """Updates the ForumsRead status for the user. In order to work correctly, be sure that `topicsread is **not** `None`. :param user: The user for whom we should check if he has read the forum. :param forumsread: The forumsread object. It is needed to check if if the forum is unread. If `forumsread` is `None` and the forum is unread, it will create a new entry in the `ForumsRead` relation, else (and the forum is still unread) we are just going to update the entry in the `ForumsRead` relation. :param topicsread: The topicsread object is used in combination with the forumsread object to check if the forumsread relation should be updated and therefore is unread. """ if not user.is_authenticated or topicsread is None: return False read_cutoff = None if flaskbb_config['TRACKER_LENGTH'] > 0: read_cutoff = time_utcnow() - timedelta( days=flaskbb_config['TRACKER_LENGTH']) # fetch the unread posts in the forum unread_count = Topic.query.\ outerjoin(TopicsRead, db.and_(TopicsRead.topic_id == Topic.id, TopicsRead.user_id == user.id)).\ outerjoin(ForumsRead, db.and_(ForumsRead.forum_id == Topic.forum_id, ForumsRead.user_id == user.id)).\ filter(Topic.forum_id == self.id, Topic.last_updated > read_cutoff, db.or_(TopicsRead.last_read == None, TopicsRead.last_read < Topic.last_updated)).\ count() # No unread topics available - trying to mark the forum as read if unread_count == 0: if forumsread and forumsread.last_read > topicsread.last_read: return False # ForumRead Entry exists - Updating it because a new topic/post # has been submitted and has read everything (obviously, else the # unread_count would be useless). elif forumsread: forumsread.last_read = time_utcnow() forumsread.save() return True # No ForumRead Entry existing - creating one. forumsread = ForumsRead() forumsread.user_id = user.id forumsread.forum_id = self.id forumsread.last_read = time_utcnow() forumsread.save() return True # Nothing updated, because there are still more than 0 unread # topicsread return False def recalculate(self, last_post=False): """Recalculates the post_count and topic_count in the forum. Returns the forum with the recounted stats. :param last_post: If set to ``True`` it will also try to update the last post columns in the forum. """ topic_count = Topic.query.filter_by(forum_id=self.id).count() post_count = Post.query.\ filter(Post.topic_id == Topic.id, Topic.forum_id == self.id).\ count() self.topic_count = topic_count self.post_count = post_count if last_post: self.update_last_post() self.save() return self def save(self, groups=None): """Saves a forum :param moderators: If given, it will update the moderators in this forum with the given iterable of user objects. :param groups: A list with group objects. """ if self.id: db.session.merge(self) else: if groups is None: # importing here because of circular dependencies from flaskbb.user.models import Group self.groups = Group.query.order_by(Group.name.asc()).all() db.session.add(self) db.session.commit() return self def delete(self, users=None): """Deletes forum. If a list with involved user objects is passed, it will also update their post counts :param users: A list with user objects """ # Delete the forum db.session.delete(self) db.session.commit() # Delete the entries for the forum in the ForumsRead and TopicsRead # relation ForumsRead.query.filter_by(forum_id=self.id).delete() TopicsRead.query.filter_by(forum_id=self.id).delete() # Update the users post count if users: users_list = [] for user in users: user.post_count = Post.query.filter_by(user_id=user.id).count() users_list.append(user) db.session.add_all(users_list) db.session.commit() return self def move_topics_to(self, topics): """Moves a bunch a topics to the forum. Returns ``True`` if all topics were moved successfully to the forum. :param topics: A iterable with topic objects. """ status = False for topic in topics: status = topic.move(self) return status # Classmethods @classmethod def get_forum(cls, forum_id, user): """Returns the forum and forumsread object as a tuple for the user. :param forum_id: The forum id :param user: The user object is needed to check if we also need their forumsread object. """ if user.is_authenticated: forum, forumsread = Forum.query.\ filter(Forum.id == forum_id).\ options(db.joinedload("category")).\ outerjoin(ForumsRead, db.and_(ForumsRead.forum_id == Forum.id, ForumsRead.user_id == user.id)).\ add_entity(ForumsRead).\ first_or_404() else: forum = Forum.query.filter(Forum.id == forum_id).first_or_404() forumsread = None return forum, forumsread @classmethod def get_topics(cls, forum_id, user, page=1, per_page=20): """Get the topics for the forum. If the user is logged in, it will perform an outerjoin for the topics with the topicsread and forumsread relation to check if it is read or unread. :param forum_id: The forum id :param user: The user object :param page: The page whom should be loaded :param per_page: How many topics per page should be shown """ if user.is_authenticated: topics = Topic.query.filter_by(forum_id=forum_id).\ outerjoin(TopicsRead, db.and_(TopicsRead.topic_id == Topic.id, TopicsRead.user_id == user.id)).\ add_entity(TopicsRead).\ order_by(Topic.important.desc(), Topic.last_updated.desc()).\ paginate(page, per_page, True) else: topics = Topic.query.filter_by(forum_id=forum_id).\ order_by(Topic.important.desc(), Topic.last_updated.desc()).\ paginate(page, per_page, True) topics.items = [(topic, None) for topic in topics.items] return topics
class Topic(db.Model, CRUDMixin): __tablename__ = "topics" __searchable__ = ['title', 'username'] id = db.Column(db.Integer, primary_key=True) forum_id = db.Column(db.Integer, db.ForeignKey("forums.id", use_alter=True, name="fk_topic_forum_id"), nullable=False) title = db.Column(db.String(255), nullable=False) user_id = db.Column(db.Integer, db.ForeignKey("users.id")) username = db.Column(db.String(200), nullable=False) date_created = db.Column(UTCDateTime(timezone=True), default=time_utcnow) last_updated = db.Column(UTCDateTime(timezone=True), default=time_utcnow) locked = db.Column(db.Boolean, default=False) important = db.Column(db.Boolean, default=False) views = db.Column(db.Integer, default=0) post_count = db.Column(db.Integer, default=0) # One-to-one (uselist=False) relationship between first_post and topic first_post_id = db.Column(db.Integer, db.ForeignKey("posts.id", ondelete="CASCADE")) first_post = db.relationship("Post", backref="first_post", uselist=False, foreign_keys=[first_post_id]) # One-to-one last_post_id = db.Column(db.Integer, db.ForeignKey("posts.id")) last_post = db.relationship("Post", backref="last_post", uselist=False, foreign_keys=[last_post_id]) # One-to-many posts = db.relationship("Post", backref="topic", lazy="dynamic", primaryjoin="Post.topic_id == Topic.id", cascade="all, delete-orphan", post_update=True) # Properties @property def second_last_post(self): """Returns the second last post.""" return self.posts[-2].id @property def slug(self): """Returns a slugified version from the topic title""" return slugify(self.title) @property def url(self): """Returns the slugified url for the topic""" return url_for("forum.view_topic", topic_id=self.id, slug=self.slug) # Methods def __init__(self, title=None): if title: self.title = title def __repr__(self): """ Set to a unique key specific to the object in the database. Required for cache.memoize() to work across requests. """ return "<{} {}>".format(self.__class__.__name__, self.id) @classmethod def get_topic(cls, topic_id, user): topic = Topic.query.filter_by(id=topic_id).first_or_404() return topic def tracker_needs_update(self, forumsread, topicsread): """Returns True if the topicsread tracker needs an update. Also, if the ``TRACKER_LENGTH`` is configured, it will just recognize topics that are newer than the ``TRACKER_LENGTH`` (in days) as unread. :param forumsread: The ForumsRead object is needed because we also need to check if the forum has been cleared sometime ago. :param topicsread: The topicsread object is used to check if there is a new post in the topic. """ read_cutoff = None if flaskbb_config['TRACKER_LENGTH'] > 0: read_cutoff = time_utcnow() - timedelta( days=flaskbb_config['TRACKER_LENGTH']) # The tracker is disabled - abort if read_cutoff is None: return False # Else the topic is still below the read_cutoff elif read_cutoff > self.last_post.date_created: return False # Can be None (cleared) if the user has never marked the forum as read. # If this condition is false - we need to update the tracker if forumsread and forumsread.cleared is not None and \ forumsread.cleared >= self.last_post.date_created: return False if topicsread and topicsread.last_read >= self.last_post.date_created: return False return True def update_read(self, user, forum, forumsread): """Updates the topicsread and forumsread tracker for a specified user, if the topic contains new posts or the user hasn't read the topic. Returns True if the tracker has been updated. :param user: The user for whom the readstracker should be updated. :param forum: The forum in which the topic is. :param forumsread: The forumsread object. It is used to check if there is a new post since the forum has been marked as read. """ # User is not logged in - abort if not user.is_authenticated: return False topicsread = TopicsRead.query.\ filter(TopicsRead.user_id == user.id, TopicsRead.topic_id == self.id).first() if not self.tracker_needs_update(forumsread, topicsread): return False # Because we return True/False if the trackers have been # updated, we need to store the status in a temporary variable updated = False # A new post has been submitted that the user hasn't read. # Updating... if topicsread: topicsread.last_read = time_utcnow() topicsread.save() updated = True # The user has not visited the topic before. Inserting him in # the TopicsRead model. elif not topicsread: topicsread = TopicsRead() topicsread.user_id = user.id topicsread.topic_id = self.id topicsread.forum_id = self.forum_id topicsread.last_read = time_utcnow() topicsread.save() updated = True # No unread posts else: updated = False # Save True/False if the forums tracker has been updated. updated = forum.update_read(user, forumsread, topicsread) return updated def recalculate(self): """Recalculates the post count in the topic.""" post_count = Post.query.filter_by(topic_id=self.id).count() self.post_count = post_count self.save() return self def move(self, new_forum): """Moves a topic to the given forum. Returns True if it could successfully move the topic to forum. :param new_forum: The new forum for the topic """ # if the target forum is the current forum, abort if self.forum_id == new_forum.id: return False old_forum = self.forum self.forum.post_count -= self.post_count self.forum.topic_count -= 1 self.forum_id = new_forum.id new_forum.post_count += self.post_count new_forum.topic_count += 1 db.session.commit() new_forum.update_last_post() old_forum.update_last_post() TopicsRead.query.filter_by(topic_id=self.id).delete() return True def save(self, user=None, forum=None, post=None): """Saves a topic and returns the topic object. If no parameters are given, it will only update the topic. :param user: The user who has created the topic :param forum: The forum where the topic is stored :param post: The post object which is connected to the topic """ # Updates the topic if self.id: db.session.add(self) db.session.commit() return self # Set the forum and user id self.forum_id = forum.id self.user_id = user.id self.username = user.username # Set the last_updated time. Needed for the readstracker self.last_updated = time_utcnow() self.date_created = time_utcnow() # Insert and commit the topic db.session.add(self) db.session.commit() # Create the topic post post.save(user, self) # Update the first post id self.first_post_id = post.id # Update the topic count forum.topic_count += 1 db.session.commit() return self def delete(self, users=None): """Deletes a topic with the corresponding posts. If a list with user objects is passed it will also update their post counts :param users: A list with user objects """ # Grab the second last topic in the forum + parents/childs topic = Topic.query.\ filter_by(forum_id=self.forum_id).\ order_by(Topic.last_post_id.desc()).limit(2).offset(0).all() # do we want to delete the topic with the last post in the forum? if topic and topic[0].id == self.id: try: # Now the second last post will be the last post self.forum.last_post_id = topic[1].last_post_id self.forum.last_post_title = topic[1].title self.forum.last_post_user_id = topic[1].user_id self.forum.last_post_username = topic[1].username self.forum.last_post_created = topic[1].last_updated # Catch an IndexError when you delete the last topic in the forum # There is no second last post except IndexError: self.forum.last_post_id = None self.forum.last_post_title = None self.forum.last_post_user_id = None self.forum.last_post_username = None self.forum.last_post_created = None # Commit the changes db.session.commit() # These things needs to be stored in a variable before they are deleted forum = self.forum TopicsRead.query.filter_by(topic_id=self.id).delete() # Delete the topic db.session.delete(self) db.session.commit() # Update the post counts if users: for user in users: user.post_count = Post.query.filter_by(user_id=user.id).count() db.session.commit() forum.topic_count = Topic.query.\ filter_by(forum_id=self.forum_id).\ count() forum.post_count = Post.query.\ filter(Post.topic_id == Topic.id, Topic.forum_id == self.forum_id).\ count() db.session.commit() return self
def hidden_by(cls): # noqa: B902 return db.relationship("User", uselist=False, foreign_keys=[cls.hidden_by_id])
class User(db.Model, UserMixin, CRUDMixin): __tablename__ = "users" __searchable__ = ['username', 'email'] id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(200), unique=True, nullable=False) email = db.Column(db.String(200), unique=True, nullable=False) _password = db.Column('password', db.String(120), nullable=False) date_joined = db.Column(db.DateTime, default=datetime.utcnow()) lastseen = db.Column(db.DateTime, default=datetime.utcnow()) birthday = db.Column(db.DateTime) gender = db.Column(db.String(10)) website = db.Column(db.String(200)) location = db.Column(db.String(100)) signature = db.Column(db.Text) avatar = db.Column(db.String(200)) notes = db.Column(db.Text) last_failed_login = db.Column(db.DateTime) login_attempts = db.Column(db.Integer, default=0) activated = db.Column(db.Boolean, default=False) theme = db.Column(db.String(15)) language = db.Column(db.String(15), default="en") posts = db.relationship("Post", backref="user", lazy="dynamic") topics = db.relationship("Topic", backref="user", lazy="dynamic") post_count = db.Column(db.Integer, default=0) primary_group_id = db.Column(db.Integer, db.ForeignKey('groups.id'), nullable=False) primary_group = db.relationship('Group', lazy="joined", backref="user_group", uselist=False, foreign_keys=[primary_group_id]) secondary_groups = \ db.relationship('Group', secondary=groups_users, primaryjoin=(groups_users.c.user_id == id), backref=db.backref('users', lazy='dynamic'), lazy='dynamic') tracked_topics = \ db.relationship("Topic", secondary=topictracker, primaryjoin=(topictracker.c.user_id == id), backref=db.backref("topicstracked", lazy="dynamic"), lazy="dynamic") # Properties @property def is_active(self): """Returns the state of the account. If the ``ACTIVATE_ACCOUNT`` option has been disabled, it will always return ``True``. Is the option activated, it will, depending on the state of the account, either return ``True`` or ``False``. """ if flaskbb_config["ACTIVATE_ACCOUNT"]: if self.activated: return True return False return True @property def last_post(self): """Returns the latest post from the user.""" return Post.query.filter(Post.user_id == self.id).\ order_by(Post.date_created.desc()).first() @property def url(self): """Returns the url for the user.""" return url_for("user.profile", username=self.username) @property def permissions(self): """Returns the permissions for the user.""" return self.get_permissions() @property def groups(self): """Returns the user groups.""" return self.get_groups() @property def unread_messages(self): """Returns the unread messages for the user.""" return self.get_unread_messages() @property def unread_count(self): """Returns the unread message count for the user.""" return len(self.unread_messages) @property def days_registered(self): """Returns the amount of days the user is registered.""" days_registered = (datetime.utcnow() - self.date_joined).days if not days_registered: return 1 return days_registered @property def topic_count(self): """Returns the thread count.""" return Topic.query.filter(Topic.user_id == self.id).count() @property def posts_per_day(self): """Returns the posts per day count.""" return round((float(self.post_count) / float(self.days_registered)), 1) @property def topics_per_day(self): """Returns the topics per day count.""" return round((float(self.topic_count) / float(self.days_registered)), 1) # Methods def __repr__(self): """Set to a unique key specific to the object in the database. Required for cache.memoize() to work across requests. """ return "<{} {}>".format(self.__class__.__name__, self.username) def _get_password(self): """Returns the hashed password.""" return self._password def _set_password(self, password): """Generates a password hash for the provided password.""" if not password: return self._password = generate_password_hash(password) # Hide password encryption by exposing password field only. password = db.synonym('_password', descriptor=property(_get_password, _set_password)) def check_password(self, password): """Check passwords. If passwords match it returns true, else false.""" if self.password is None: return False return check_password_hash(self.password, password) @classmethod def authenticate(cls, login, password): """A classmethod for authenticating users. It returns the user object if the user/password combination is ok. If the user has entered too often a wrong password, he will be locked out of his account for a specified time. :param login: This can be either a username or a email address. :param password: The password that is connected to username and email. """ user = cls.query.filter( db.or_(User.username == login, User.email == login)).first() if user: if user.check_password(password): # reset them after a successful login attempt user.login_attempts = 0 user.save() return user # user exists, wrong password user.login_attempts += 1 user.last_failed_login = datetime.utcnow() user.save() # protection against account enumeration timing attacks dummy_password = os.urandom(15).encode("base-64") check_password_hash(dummy_password, password) raise AuthenticationError def recalculate(self): """Recalculates the post count from the user.""" post_count = Post.query.filter_by(user_id=self.id).count() self.post_count = post_count self.save() return self def all_topics(self, page): """Returns a paginated result with all topics the user has created.""" return Topic.query.filter(Topic.user_id == self.id).\ filter(Post.topic_id == Topic.id).\ order_by(Post.id.desc()).\ paginate(page, flaskbb_config['TOPICS_PER_PAGE'], False) def all_posts(self, page): """Returns a paginated result with all posts the user has created.""" return Post.query.filter(Post.user_id == self.id).\ paginate(page, flaskbb_config['TOPICS_PER_PAGE'], False) def track_topic(self, topic): """Tracks the specified topic. :param topic: The topic which should be added to the topic tracker. """ if not self.is_tracking_topic(topic): self.tracked_topics.append(topic) return self def untrack_topic(self, topic): """Untracks the specified topic. :param topic: The topic which should be removed from the topic tracker. """ if self.is_tracking_topic(topic): self.tracked_topics.remove(topic) return self def is_tracking_topic(self, topic): """Checks if the user is already tracking this topic. :param topic: The topic which should be checked. """ return self.tracked_topics.filter( topictracker.c.topic_id == topic.id).count() > 0 def add_to_group(self, group): """Adds the user to the `group` if he isn't in it. :param group: The group which should be added to the user. """ if not self.in_group(group): self.secondary_groups.append(group) return self def remove_from_group(self, group): """Removes the user from the `group` if he is in it. :param group: The group which should be removed from the user. """ if self.in_group(group): self.secondary_groups.remove(group) return self def in_group(self, group): """Returns True if the user is in the specified group. :param group: The group which should be checked. """ return self.secondary_groups.filter( groups_users.c.group_id == group.id).count() > 0 @cache.memoize(timeout=max_integer) def get_groups(self): """Returns all the groups the user is in.""" return [self.primary_group] + list(self.secondary_groups) @cache.memoize(timeout=max_integer) def get_permissions(self, exclude=None): """Returns a dictionary with all permissions the user has""" if exclude: exclude = set(exclude) else: exclude = set() exclude.update(['id', 'name', 'description']) perms = {} # Get the Guest group for group in self.groups: columns = set(group.__table__.columns.keys()) - set(exclude) for c in columns: perms[c] = getattr(group, c) or perms.get(c, False) return perms @cache.memoize(timeout=max_integer) def get_unread_messages(self): """Returns all unread messages for the user.""" unread_messages = Conversation.query.\ filter(Conversation.unread, Conversation.user_id == self.id).all() return unread_messages def invalidate_cache(self, permissions=True, messages=True): """Invalidates this objects cached metadata. :param permissions_only: If set to ``True`` it will only invalidate the permissions cache. Otherwise it will also invalidate the user's unread message cache. """ if messages: cache.delete_memoized(self.get_unread_messages, self) if permissions: cache.delete_memoized(self.get_permissions, self) cache.delete_memoized(self.get_groups, self) def ban(self): """Bans the user. Returns True upon success.""" if not self.get_permissions()['banned']: banned_group = Group.query.filter(Group.banned == True).first() self.primary_group_id = banned_group.id self.save() self.invalidate_cache() return True return False def unban(self): """Unbans the user. Returns True upon success.""" if self.get_permissions()['banned']: member_group = Group.query.filter(Group.admin == False, Group.super_mod == False, Group.mod == False, Group.guest == False, Group.banned == False).first() self.primary_group_id = member_group.id self.save() self.invalidate_cache() return True return False def save(self, groups=None): """Saves a user. If a list with groups is provided, it will add those to the secondary groups from the user. :param groups: A list with groups that should be added to the secondary groups from user. """ if groups is not None: # TODO: Only remove/add groups that are selected secondary_groups = self.secondary_groups.all() for group in secondary_groups: self.remove_from_group(group) db.session.commit() for group in groups: # Do not add the primary group to the secondary groups if group.id == self.primary_group_id: continue self.add_to_group(group) self.invalidate_cache() db.session.add(self) db.session.commit() return self def delete(self): """Deletes the User.""" # This isn't done automatically... Conversation.query.filter_by(user_id=self.id).delete() ForumsRead.query.filter_by(user_id=self.id).delete() TopicsRead.query.filter_by(user_id=self.id).delete() # This should actually be handeld by the dbms.. but dunno why it doesnt # work here from flaskbb.forum.models import Forum last_post_forums = Forum.query.\ filter_by(last_post_user_id=self.id).all() for forum in last_post_forums: forum.last_post_user_id = None forum.save() db.session.delete(self) db.session.commit() return self
class User(db.Model, UserMixin, CRUDMixin): __tablename__ = "users" id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(200), unique=True, nullable=False) email = db.Column(db.String(200), unique=True, nullable=False) _password = db.Column('password', db.String(120), nullable=False) date_joined = db.Column(UTCDateTime(timezone=True), default=time_utcnow, nullable=False) lastseen = db.Column(UTCDateTime(timezone=True), default=time_utcnow, nullable=True) birthday = db.Column(db.DateTime, nullable=True) gender = db.Column(db.String(10), nullable=True) website = db.Column(db.String(200), nullable=True) location = db.Column(db.String(100), nullable=True) signature = db.Column(db.Text, nullable=True) avatar = db.Column(db.String(200), nullable=True) notes = db.Column(db.Text, nullable=True) last_failed_login = db.Column(UTCDateTime(timezone=True), nullable=True) login_attempts = db.Column(db.Integer, default=0, nullable=False) activated = db.Column(db.Boolean, default=False, nullable=False) theme = db.Column(db.String(15), nullable=True) language = db.Column(db.String(15), default="en", nullable=True) post_count = db.Column(db.Integer, default=0) primary_group_id = db.Column(db.Integer, db.ForeignKey('groups.id'), nullable=False) posts = db.relationship("Post", backref="user", primaryjoin="User.id == Post.user_id", lazy="dynamic") topics = db.relationship("Topic", backref="user", primaryjoin="User.id == Topic.user_id", lazy="dynamic") primary_group = db.relationship("Group", backref="user_group", uselist=False, lazy="joined", foreign_keys=[primary_group_id]) secondary_groups = db.relationship( "Group", secondary=groups_users, primaryjoin=(groups_users.c.user_id == id), backref=db.backref("users", lazy="dynamic"), lazy="dynamic") tracked_topics = db.relationship( "Topic", secondary=topictracker, primaryjoin=(topictracker.c.user_id == id), backref=db.backref("topicstracked", lazy="dynamic"), lazy="dynamic", single_parent=True) # Properties @property def is_active(self): """Returns the state of the account. If the ``ACTIVATE_ACCOUNT`` option has been disabled, it will always return ``True``. Is the option activated, it will, depending on the state of the account, either return ``True`` or ``False``. """ if flaskbb_config["ACTIVATE_ACCOUNT"]: if self.activated: return True return False return True @property def last_post(self): """Returns the latest post from the user.""" return Post.query.filter(Post.user_id == self.id).\ order_by(Post.date_created.desc()).first() @property def url(self): """Returns the url for the user.""" return url_for("user.profile", username=self.username) @property def permissions(self): """Returns the permissions for the user.""" return self.get_permissions() @property def groups(self): """Returns the user groups.""" return self.get_groups() @property def days_registered(self): """Returns the amount of days the user is registered.""" days_registered = (time_utcnow() - self.date_joined).days if not days_registered: return 1 return days_registered @property def topic_count(self): """Returns the thread count.""" return Topic.query.filter(Topic.user_id == self.id).count() @property def posts_per_day(self): """Returns the posts per day count.""" return round((float(self.post_count) / float(self.days_registered)), 1) @property def topics_per_day(self): """Returns the topics per day count.""" return round((float(self.topic_count) / float(self.days_registered)), 1) # Methods def __repr__(self): """Set to a unique key specific to the object in the database. Required for cache.memoize() to work across requests. """ return "<{} {}>".format(self.__class__.__name__, self.username) def _get_password(self): """Returns the hashed password.""" return self._password def _set_password(self, password): """Generates a password hash for the provided password.""" if not password: return self._password = generate_password_hash(password) # Hide password encryption by exposing password field only. password = db.synonym('_password', descriptor=property(_get_password, _set_password)) def check_password(self, password): """Check passwords. If passwords match it returns true, else false.""" if self.password is None: return False return check_password_hash(self.password, password) @classmethod @deprecated("Use authentication services instead.") def authenticate(cls, login, password): """A classmethod for authenticating users. It returns the user object if the user/password combination is ok. If the user has entered too often a wrong password, he will be locked out of his account for a specified time. :param login: This can be either a username or a email address. :param password: The password that is connected to username and email. """ user = cls.query.filter( db.or_(User.username == login, User.email == login)).first() if user is not None: if user.check_password(password): # reset them after a successful login attempt user.login_attempts = 0 user.save() return user # user exists, wrong password # never had a bad login before if user.login_attempts is None: user.login_attempts = 1 else: user.login_attempts += 1 user.last_failed_login = time_utcnow() user.save() # protection against account enumeration timing attacks check_password_hash("dummy password", password) raise AuthenticationError def recalculate(self): """Recalculates the post count from the user.""" self.post_count = Post.query.filter_by(user_id=self.id).count() self.save() return self def all_topics(self, page, viewer): """Topics made by a given user, most recent first. :param page: The page which should be displayed. :param viewer: The user who is viewing the page. Only posts accessible to the viewer will be returned. :rtype: flask_sqlalchemy.Pagination """ group_ids = [g.id for g in viewer.groups] topics = Topic.query.\ filter(Topic.user_id == self.id, Forum.id == Topic.forum_id, Forum.groups.any(Group.id.in_(group_ids))).\ order_by(Topic.id.desc()).\ paginate(page, flaskbb_config['TOPICS_PER_PAGE'], False) return topics def all_posts(self, page, viewer): """Posts made by a given user, most recent first. :param page: The page which should be displayed. :param viewer: The user who is viewing the page. Only posts accessible to the viewer will be returned. :rtype: flask_sqlalchemy.Pagination """ group_ids = [g.id for g in viewer.groups] posts = Post.query.\ filter(Post.user_id == self.id, Post.topic_id == Topic.id, Topic.forum_id == Forum.id, Forum.groups.any(Group.id.in_(group_ids))).\ order_by(Post.id.desc()).\ paginate(page, flaskbb_config['TOPICS_PER_PAGE'], False) return posts def track_topic(self, topic): """Tracks the specified topic. :param topic: The topic which should be added to the topic tracker. """ if not self.is_tracking_topic(topic): self.tracked_topics.append(topic) return self def untrack_topic(self, topic): """Untracks the specified topic. :param topic: The topic which should be removed from the topic tracker. """ if self.is_tracking_topic(topic): self.tracked_topics.remove(topic) return self def is_tracking_topic(self, topic): """Checks if the user is already tracking this topic. :param topic: The topic which should be checked. """ return self.tracked_topics.filter( topictracker.c.topic_id == topic.id).count() > 0 def add_to_group(self, group): """Adds the user to the `group` if he isn't in it. :param group: The group which should be added to the user. """ if not self.in_group(group): self.secondary_groups.append(group) return self def remove_from_group(self, group): """Removes the user from the `group` if he is in it. :param group: The group which should be removed from the user. """ if self.in_group(group): self.secondary_groups.remove(group) return self def in_group(self, group): """Returns True if the user is in the specified group. :param group: The group which should be checked. """ return self.secondary_groups.filter( groups_users.c.group_id == group.id).count() > 0 @cache.memoize() def get_groups(self): """Returns all the groups the user is in.""" return [self.primary_group] + list(self.secondary_groups) @cache.memoize() def get_permissions(self, exclude=None): """Returns a dictionary with all permissions the user has""" if exclude: exclude = set(exclude) else: exclude = set() exclude.update(['id', 'name', 'description']) perms = {} # Get the Guest group for group in self.groups: columns = set(group.__table__.columns.keys()) - set(exclude) for c in columns: perms[c] = getattr(group, c) or perms.get(c, False) return perms def invalidate_cache(self): """Invalidates this objects cached metadata.""" cache.delete_memoized(self.get_permissions, self) cache.delete_memoized(self.get_groups, self) def ban(self): """Bans the user. Returns True upon success.""" if not self.get_permissions()['banned']: banned_group = Group.query.filter(Group.banned == True).first() self.primary_group = banned_group self.save() self.invalidate_cache() return True return False def unban(self): """Unbans the user. Returns True upon success.""" if self.get_permissions()['banned']: member_group = Group.query.filter(Group.admin == False, Group.super_mod == False, Group.mod == False, Group.guest == False, Group.banned == False).first() self.primary_group = member_group self.save() self.invalidate_cache() return True return False def save(self, groups=None): """Saves a user. If a list with groups is provided, it will add those to the secondary groups from the user. :param groups: A list with groups that should be added to the secondary groups from user. """ if groups is not None: # TODO: Only remove/add groups that are selected with db.session.no_autoflush: secondary_groups = self.secondary_groups.all() for group in secondary_groups: self.remove_from_group(group) for group in groups: # Do not add the primary group to the secondary groups if group == self.primary_group: continue self.add_to_group(group) self.invalidate_cache() db.session.add(self) db.session.commit() return self def delete(self): """Deletes the User.""" db.session.delete(self) db.session.commit() return self
class User(db.Model, UserMixin, CRUDMixin): __tablename__ = "users" __searchable__ = ['username', 'email'] id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(200), unique=True, nullable=False) email = db.Column(db.String(200), unique=True, nullable=False) _password = db.Column('password', db.String(120), nullable=False) date_joined = db.Column(db.DateTime, default=datetime.utcnow()) lastseen = db.Column(db.DateTime, default=datetime.utcnow()) birthday = db.Column(db.DateTime) gender = db.Column(db.String(10)) website = db.Column(db.String(200)) location = db.Column(db.String(100)) signature = db.Column(db.Text) avatar = db.Column(db.String(200)) notes = db.Column(db.Text) theme = db.Column(db.String(15)) language = db.Column(db.String(15), default="en") posts = db.relationship("Post", backref="user", lazy="dynamic") topics = db.relationship("Topic", backref="user", lazy="dynamic") post_count = db.Column(db.Integer, default=0) primary_group_id = db.Column(db.Integer, db.ForeignKey('groups.id'), nullable=False) primary_group = db.relationship('Group', lazy="joined", backref="user_group", uselist=False, foreign_keys=[primary_group_id]) secondary_groups = \ db.relationship('Group', secondary=groups_users, primaryjoin=(groups_users.c.user_id == id), backref=db.backref('users', lazy='dynamic'), lazy='dynamic') tracked_topics = \ db.relationship("Topic", secondary=topictracker, primaryjoin=(topictracker.c.user_id == id), backref=db.backref("topicstracked", lazy="dynamic"), lazy="dynamic") # Properties @property def last_post(self): """Returns the latest post from the user""" return Post.query.filter(Post.user_id == self.id).\ order_by(Post.date_created.desc()).first() @property def url(self): """Returns the url for the user""" return url_for("user.profile", username=self.username) @property def permissions(self): """Returns the permissions for the user""" return self.get_permissions() @property def groups(self): """Returns user groups""" return self.get_groups() @property def days_registered(self): """Returns the amount of days the user is registered.""" days_registered = (datetime.utcnow() - self.date_joined).days if not days_registered: return 1 return days_registered @property def topic_count(self): """Returns the thread count""" return Topic.query.filter(Topic.user_id == self.id).count() @property def posts_per_day(self): """Returns the posts per day count""" return round((float(self.post_count) / float(self.days_registered)), 1) @property def topics_per_day(self): """Returns the topics per day count""" return round((float(self.topic_count) / float(self.days_registered)), 1) # Methods def __repr__(self): """Set to a unique key specific to the object in the database. Required for cache.memoize() to work across requests. """ return "<{} {}>".format(self.__class__.__name__, self.username) def _get_password(self): """Returns the hashed password""" return self._password def _set_password(self, password): """Generates a password hash for the provided password""" self._password = generate_password_hash(password) # Hide password encryption by exposing password field only. password = db.synonym('_password', descriptor=property(_get_password, _set_password)) def check_password(self, password): """Check passwords. If passwords match it returns true, else false""" if self.password is None: return False return check_password_hash(self.password, password) @classmethod def authenticate(cls, login, password): """A classmethod for authenticating users It returns true if the user exists and has entered a correct password :param login: This can be either a username or a email address. :param password: The password that is connected to username and email. """ user = cls.query.filter( db.or_(User.username == login, User.email == login)).first() if user: authenticated = user.check_password(password) else: authenticated = False return user, authenticated def _make_token(self, data, timeout): s = Serializer(current_app.config['SECRET_KEY'], timeout) return s.dumps(data) def _verify_token(self, token): s = Serializer(current_app.config['SECRET_KEY']) data = None expired, invalid = False, False try: data = s.loads(token) except SignatureExpired: expired = True except Exception: invalid = True return expired, invalid, data def make_reset_token(self, expiration=3600): """Creates a reset token. The duration can be configured through the expiration parameter. :param expiration: The time in seconds how long the token is valid. """ return self._make_token({'id': self.id, 'op': 'reset'}, expiration) def verify_reset_token(self, token): """Verifies a reset token. It returns three boolean values based on the state of the token (expired, invalid, data) :param token: The reset token that should be checked. """ expired, invalid, data = self._verify_token(token) if data and data.get('id') == self.id and data.get('op') == 'reset': data = True else: data = False return expired, invalid, data def recalculate(self): """Recalculates the post count from the user.""" post_count = Post.query.filter_by(user_id=self.id).count() self.post_count = post_count self.save() return self def all_topics(self, page): """Returns a paginated result with all topics the user has created.""" return Topic.query.filter(Topic.user_id == self.id).\ filter(Post.topic_id == Topic.id).\ order_by(Post.id.desc()).\ paginate(page, flaskbb_config['TOPICS_PER_PAGE'], False) def all_posts(self, page): """Returns a paginated result with all posts the user has created.""" return Post.query.filter(Post.user_id == self.id).\ paginate(page, flaskbb_config['TOPICS_PER_PAGE'], False) def track_topic(self, topic): """Tracks the specified topic :param topic: The topic which should be added to the topic tracker. """ if not self.is_tracking_topic(topic): self.tracked_topics.append(topic) return self def untrack_topic(self, topic): """Untracks the specified topic :param topic: The topic which should be removed from the topic tracker. """ if self.is_tracking_topic(topic): self.tracked_topics.remove(topic) return self def is_tracking_topic(self, topic): """Checks if the user is already tracking this topic :param topic: The topic which should be checked. """ return self.tracked_topics.filter( topictracker.c.topic_id == topic.id).count() > 0 def add_to_group(self, group): """Adds the user to the `group` if he isn't in it. :param group: The group which should be added to the user. """ if not self.in_group(group): self.secondary_groups.append(group) return self def remove_from_group(self, group): """Removes the user from the `group` if he is in it. :param group: The group which should be removed from the user. """ if self.in_group(group): self.secondary_groups.remove(group) return self def in_group(self, group): """Returns True if the user is in the specified group :param group: The group which should be checked. """ return self.secondary_groups.filter( groups_users.c.group_id == group.id).count() > 0 @cache.memoize(timeout=max_integer) def get_groups(self): """Returns all the groups the user is in.""" return [self.primary_group] + list(self.secondary_groups) @cache.memoize(timeout=max_integer) def get_permissions(self, exclude=None): """Returns a dictionary with all the permissions the user has. :param exclude: a list with excluded permissions. default is None. """ exclude = exclude or [] exclude.extend(['id', 'name', 'description']) perms = {} groups = self.secondary_groups.all() groups.append(self.primary_group) for group in groups: for c in group.__table__.columns: # try if the permission already exists in the dictionary # and if the permission is true, set it to True try: if not perms[c.name] and getattr(group, c.name): perms[c.name] = True # if the permission doesn't exist in the dictionary # add it to the dictionary except KeyError: # if the permission is in the exclude list, # skip to the next permission if c.name in exclude: continue perms[c.name] = getattr(group, c.name) return perms def invalidate_cache(self): """Invalidates this objects cached metadata.""" cache.delete_memoized(self.get_permissions, self) cache.delete_memoized(self.get_groups, self) def ban(self): """Bans the user. Returns True upon success.""" if not self.get_permissions()['banned']: banned_group = Group.query.filter(Group.banned == True).first() self.primary_group_id = banned_group.id self.save() self.invalidate_cache() return True return False def unban(self): """Unbans the user. Returns True upon success.""" if self.get_permissions()['banned']: member_group = Group.query.filter(Group.admin == False, Group.super_mod == False, Group.mod == False, Group.guest == False, Group.banned == False).first() self.primary_group_id = member_group.id self.save() self.invalidate_cache() return True return False def save(self, groups=None): """Saves a user. If a list with groups is provided, it will add those to the secondary groups from the user. :param groups: A list with groups that should be added to the secondary groups from user. """ if groups is not None: # TODO: Only remove/add groups that are selected secondary_groups = self.secondary_groups.all() for group in secondary_groups: self.remove_from_group(group) db.session.commit() for group in groups: # Do not add the primary group to the secondary groups if group.id == self.primary_group_id: continue self.add_to_group(group) self.invalidate_cache() db.session.add(self) db.session.commit() return self def delete(self): """Deletes the User.""" # This isn't done automatically... Conversation.query.filter_by(user_id=self.id).delete() ForumsRead.query.filter_by(user_id=self.id).delete() TopicsRead.query.filter_by(user_id=self.id).delete() # This should actually be handeld by the dbms.. but dunno why it doesnt # work here from flaskbb.forum.models import Forum last_post_forums = Forum.query.\ filter_by(last_post_user_id=self.id).all() for forum in last_post_forums: forum.last_post_user_id = None forum.save() db.session.delete(self) db.session.commit() return self
class Topic(HideableCRUDMixin, db.Model): __tablename__ = "topics" id = db.Column(db.Integer, primary_key=True) forum_id = db.Column(db.Integer, db.ForeignKey("forums.id", ondelete="CASCADE"), nullable=False) title = db.Column(db.String(255), nullable=False) user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True) username = db.Column(db.String(200), nullable=False) date_created = db.Column(UTCDateTime(timezone=True), default=time_utcnow, nullable=False) last_updated = db.Column(UTCDateTime(timezone=True), default=time_utcnow, nullable=False) locked = db.Column(db.Boolean, default=False, nullable=False) important = db.Column(db.Boolean, default=False, nullable=False) views = db.Column(db.Integer, default=0, nullable=False) post_count = db.Column(db.Integer, default=0, nullable=False) # One-to-one (uselist=False) relationship between first_post and topic first_post_id = db.Column(db.Integer, db.ForeignKey("posts.id", ondelete="CASCADE"), nullable=True) first_post = db.relationship("Post", backref="first_post", uselist=False, foreign_keys=[first_post_id]) # One-to-one last_post_id = db.Column(db.Integer, db.ForeignKey("posts.id"), nullable=True) last_post = db.relationship("Post", backref="last_post", uselist=False, foreign_keys=[last_post_id]) # One-to-many posts = db.relationship("Post", backref="topic", lazy="dynamic", primaryjoin="Post.topic_id == Topic.id", cascade="all, delete-orphan", post_update=True) # Properties @property def second_last_post(self): """Returns the second last post or None.""" try: return self.posts[-2].id except IndexError: return None @property def slug(self): """Returns a slugified version of the topic title.""" return slugify(self.title) @property def url(self): """Returns the slugified url for the topic.""" return url_for("forum.view_topic", topic_id=self.id, slug=self.slug) def first_unread(self, topicsread, user, forumsread=None): """Returns the url to the first unread post. If no unread posts exist it will return the url to the topic. :param topicsread: The topicsread object for the topic :param user: The user who should be checked if he has read the last post in the topic :param forumsread: The forumsread object in which the topic is. If you also want to check if the user has marked the forum as read, than you will also need to pass an forumsread object. """ # If the topic is unread try to get the first unread post if topic_is_unread(self, topicsread, user, forumsread): query = Post.query.filter(Post.topic_id == self.id) if topicsread is not None: query = query.filter(Post.date_created > topicsread.last_read) post = query.order_by(Post.id.asc()).first() if post is not None: return post.url return self.url # Methods def __init__(self, title=None, user=None): """Creates a topic object with some initial values. :param title: The title of the topic. :param user: The user of the post. """ if title: self.title = title if user: # setting the user here, even with setting the id, breaks the bulk insert # stuff as they use the session.bulk_save_objects which does not trigger # relationships self.user_id = user.id self.username = user.username self.date_created = self.last_updated = time_utcnow() def __repr__(self): """Set to a unique key specific to the object in the database. Required for cache.memoize() to work across requests. """ return "<{} {}>".format(self.__class__.__name__, self.id) @classmethod def get_topic(cls, topic_id, user): topic = Topic.query.filter_by(id=topic_id).first_or_404() return topic def tracker_needs_update(self, forumsread, topicsread): """Returns True if the topicsread tracker needs an update. Also, if the ``TRACKER_LENGTH`` is configured, it will just recognize topics that are newer than the ``TRACKER_LENGTH`` (in days) as unread. :param forumsread: The ForumsRead object is needed because we also need to check if the forum has been cleared sometime ago. :param topicsread: The topicsread object is used to check if there is a new post in the topic. """ read_cutoff = None if flaskbb_config['TRACKER_LENGTH'] > 0: read_cutoff = time_utcnow() - timedelta( days=flaskbb_config['TRACKER_LENGTH']) # The tracker is disabled - abort if read_cutoff is None: return False # Else the topic is still below the read_cutoff elif read_cutoff > self.last_post.date_created: return False # Can be None (cleared) if the user has never marked the forum as read. # If this condition is false - we need to update the tracker if forumsread and forumsread.cleared is not None and \ forumsread.cleared >= self.last_post.date_created: return False if topicsread and topicsread.last_read >= self.last_post.date_created: return False return True def update_read(self, user, forum, forumsread): """Updates the topicsread and forumsread tracker for a specified user, if the topic contains new posts or the user hasn't read the topic. Returns True if the tracker has been updated. :param user: The user for whom the readstracker should be updated. :param forum: The forum in which the topic is. :param forumsread: The forumsread object. It is used to check if there is a new post since the forum has been marked as read. """ # User is not logged in - abort if not user.is_authenticated: return False topicsread = TopicsRead.query.\ filter(TopicsRead.user_id == user.id, TopicsRead.topic_id == self.id).first() if not self.tracker_needs_update(forumsread, topicsread): return False # Because we return True/False if the trackers have been # updated, we need to store the status in a temporary variable updated = False # A new post has been submitted that the user hasn't read. # Updating... if topicsread: topicsread.last_read = time_utcnow() topicsread.save() updated = True # The user has not visited the topic before. Inserting him in # the TopicsRead model. elif not topicsread: topicsread = TopicsRead() topicsread.user = user topicsread.topic = self topicsread.forum = self.forum topicsread.last_read = time_utcnow() topicsread.save() updated = True # No unread posts else: updated = False # Save True/False if the forums tracker has been updated. updated = forum.update_read(user, forumsread, topicsread) return updated def recalculate(self): """Recalculates the post count in the topic.""" post_count = Post.query.filter_by(topic_id=self.id).count() self.post_count = post_count self.save() return self def move(self, new_forum): """Moves a topic to the given forum. Returns True if it could successfully move the topic to forum. :param new_forum: The new forum for the topic """ # if the target forum is the current forum, abort if self.forum == new_forum: return False old_forum = self.forum self.forum.post_count -= self.post_count self.forum.topic_count -= 1 self.forum = new_forum new_forum.post_count += self.post_count new_forum.topic_count += 1 db.session.commit() new_forum.update_last_post() old_forum.update_last_post() TopicsRead.query.filter_by(topic_id=self.id).delete() return True def save(self, user=None, forum=None, post=None): """Saves a topic and returns the topic object. If no parameters are given, it will only update the topic. :param user: The user who has created the topic :param forum: The forum where the topic is stored :param post: The post object which is connected to the topic """ current_app.pluggy.hook.flaskbb_event_topic_save_before(topic=self) # Updates the topic if self.id: db.session.add(self) db.session.commit() current_app.pluggy.hook.flaskbb_event_topic_save_after( topic=self, is_new=False) return self # Set the forum and user id self.forum = forum self.user = user self.username = user.username # Set the last_updated time. Needed for the readstracker self.date_created = self.last_updated = time_utcnow() # Insert and commit the topic db.session.add(self) db.session.commit() # Create the topic post post.save(user, self) # Update the first and last post id self.last_post = self.first_post = post # Update the topic count forum.topic_count += 1 db.session.commit() current_app.pluggy.hook.flaskbb_event_topic_save_after(topic=self, is_new=True) return self def delete(self, users=None): """Deletes a topic with the corresponding posts. If a list with user objects is passed it will also update their post counts :param users: A list with user objects """ forum = self.forum db.session.delete(self) self._fix_user_post_counts(users or self.involved_users().all()) self._fix_post_counts(forum) # forum.last_post_id shouldn't usually be none if forum.last_post_id is None or \ self.last_post_id == forum.last_post_id: forum.update_last_post(commit=False) db.session.commit() return self def hide(self, user, users=None): """Soft deletes a topic from a forum """ if self.hidden: return self._remove_topic_from_forum() super(Topic, self).hide(user) self._handle_first_post() self._fix_user_post_counts(users or self.involved_users().all()) self._fix_post_counts(self.forum) db.session.commit() return self def unhide(self, users=None): """Restores a hidden topic to a forum """ if not self.hidden: return super(Topic, self).unhide() self._handle_first_post() self._restore_topic_to_forum() self._fix_user_post_counts(users or self.involved_users().all()) self.forum.recalculate() db.session.commit() return self def _remove_topic_from_forum(self): # Grab the second last topic in the forum + parents/childs topics = Topic.query.filter( Topic.forum_id == self.forum_id, Topic.hidden != True).order_by( Topic.last_post_id.desc()).limit(2).offset(0).all() # do we want to replace the topic with the last post in the forum? if len(topics) > 1: if topics[0] == self: # Now the second last post will be the last post self.forum.last_post = topics[1].last_post self.forum.last_post_title = topics[1].title self.forum.last_post_user = topics[1].user self.forum.last_post_username = topics[1].username self.forum.last_post_created = topics[1].last_updated else: self.forum.last_post = None self.forum.last_post_title = None self.forum.last_post_user = None self.forum.last_post_username = None self.forum.last_post_created = None def _fix_user_post_counts(self, users=None): # Update the post counts if users: for user in users: user.post_count = Post.query.filter( Post.user_id == user.id, Topic.id == Post.topic_id, Topic.hidden != True, Post.hidden != True).count() def _fix_post_counts(self, forum): clauses = [Topic.forum_id == forum.id] if self.hidden: clauses.extend([ Topic.id != self.id, Topic.hidden != True, ]) else: clauses.append(db.or_(Topic.id == self.id, Topic.hidden != True)) forum.topic_count = Topic.query.filter(*clauses).count() post_count = clauses + [ Post.topic_id == Topic.id, ] if self.hidden: post_count.append(Post.hidden != True) else: post_count.append( db.or_(Post.hidden != True, Post.id == self.first_post.id)) forum.post_count = Post.query.distinct().filter(*post_count).count() def _restore_topic_to_forum(self): if self.forum.last_post is None or self.forum.last_post_created < self.last_updated: self.forum.last_post = self.last_post self.forum.last_post_title = self.title self.forum.last_post_user = self.user self.forum.last_post_username = self.username self.forum.last_post_created = self.last_updated def _handle_first_post(self): # have to do this specially because otherwise we start recurisve calls self.first_post.hidden = self.hidden self.first_post.hidden_by = self.hidden_by self.first_post.hidden_at = self.hidden_at def involved_users(self): """ Returns a query of all users involved in the topic """ # todo: Find circular import and break it from flaskbb.user.models import User return User.query.distinct().filter(Post.topic_id == self.id, User.id == Post.user_id)
def hidden_by(cls): return db.relationship( 'User', uselist=False, foreign_keys=[cls.hidden_by_id], )
class PluginRegistry(CRUDMixin, db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.Unicode(255), unique=True, nullable=False) enabled = db.Column(db.Boolean, default=True) values = db.relationship( 'PluginStore', collection_class=attribute_mapped_collection('key'), backref='plugin', cascade="all, delete-orphan", ) @property def settings(self): """Returns a dict with contains all the settings in a plugin.""" return {kv.key: kv.value for kv in itervalues(self.values)} @property def info(self): """Returns some information about the plugin.""" return current_app.pluggy.list_plugin_metadata().get(self.name, {}) @property def is_installable(self): """Returns True if the plugin has settings that can be installed.""" plugin_module = current_app.pluggy.get_plugin(self.name) return True if plugin_module.SETTINGS else False @property def is_installed(self): """Returns True if the plugin is installed.""" if self.settings: return True return False def get_settings_form(self): """Generates a settings form based on the settings.""" return generate_settings_form(self.values.values())() def update_settings(self, settings): """Updates the given settings of the plugin. :param settings: A dictionary containing setting items. """ pluginstore = PluginStore.query.filter( PluginStore.plugin_id == self.id, PluginStore.key.in_(settings.keys())).all() setting_list = [] for pluginsetting in pluginstore: pluginsetting.value = settings[pluginsetting.key] setting_list.append(pluginsetting) db.session.add_all(setting_list) db.session.commit() def add_settings(self, settings, force=False): """Adds the given settings to the plugin. :param settings: A dictionary containing setting items. :param force: Forcefully overwrite existing settings. """ plugin_settings = [] for key in settings: if force: with db.session.no_autoflush: pluginstore = PluginStore.get_or_create(self.id, key) else: # otherwise we assume that no such setting exist pluginstore = PluginStore() pluginstore.key = key pluginstore.plugin = self pluginstore.value = settings[key]['value'] pluginstore.value_type = settings[key]['value_type'] pluginstore.extra = settings[key]['extra'] pluginstore.name = settings[key]['name'] pluginstore.description = settings[key]['description'] plugin_settings.append(pluginstore) db.session.add_all(plugin_settings) db.session.commit() def __repr__(self): return '<Plugin name={} enabled={}>'.format(self.name, self.enabled)
class Forum(db.Model): __tablename__ = "forums" id = db.Column(db.Integer, primary_key=True) category_id = db.Column(db.Integer, db.ForeignKey("categories.id"), nullable=False) title = db.Column(db.String(15), nullable=False) description = db.Column(db.String(255)) position = db.Column(db.Integer, default=1, nullable=False) locked = db.Column(db.Boolean, default=False, nullable=False) show_moderators = db.Column(db.Boolean, default=False, nullable=False) external = db.Column(db.String(63)) post_count = db.Column(db.Integer, default=0, nullable=False) topic_count = db.Column(db.Integer, default=0, nullable=False) # One-to-one last_post_id = db.Column(db.Integer, db.ForeignKey("posts.id")) last_post = db.relationship("Post", backref="last_post_forum", uselist=False, foreign_keys=[last_post_id]) # One-to-many topics = db.relationship("Topic", backref="forum", lazy="joined", cascade="all, delete-orphan") # Many-to-many moderators = \ db.relationship("User", secondary=moderators, primaryjoin=(moderators.c.forum_id == id), backref=db.backref("forummoderator", lazy="dynamic"), lazy="joined") # Properties @property def slug(self): """Returns a slugified version from the forum title""" return slugify(self.title) @property def url(self): """Returns the url for the forum""" return url_for("forum.view_forum", forum_id=self.id, slug=self.slug) # Methods def __repr__(self): """Set to a unique key specific to the object in the database. Required for cache.memoize() to work across requests. """ return "<{} {}>".format(self.__class__.__name__, self.id) def update_last_post(self): """Updates the last post. This is useful if you move a topic in another forum """ last_post = Post.query.\ filter(Post.topic_id == Topic.id, Topic.forum_id == self.id).\ order_by(Post.date_created.desc()).\ first() # Last post is none when there are no topics in the forum if last_post is not None: # a new last post was found in the forum if not last_post.id == self.last_post_id: self.last_post_id = last_post.id # No post found.. else: self.last_post_id = 0 db.session.commit() def save(self, moderators=None): """Saves a forum""" if moderators is not None: for moderator in self.moderators: self.moderators.remove(moderator) db.session.commit() for moderator in moderators: if moderator: self.moderators.append(moderator) db.session.add(self) db.session.commit() return self def delete(self, users=None): """Deletes forum. If a list with involved user objects is passed, it will also update their post counts :param users: A list with user objects """ # Delete the forum db.session.delete(self) db.session.commit() # Delete all entries from the ForumsRead and TopicsRead relation ForumsRead.query.filter_by(forum_id=self.id).delete() TopicsRead.query.filter_by(forum_id=self.id).delete() # Update the users post count if users: users_list = [] for user in users: user.post_count = Post.query.filter_by(user_id=user.id).count() users_list.append(user) db.session.add_all(users_list) db.session.commit() return self