class HideableMixin(object): query_class = HideableQuery hidden = db.Column(db.Boolean, default=False, nullable=True) hidden_at = db.Column(UTCDateTime(timezone=True), nullable=True) @declared_attr def hidden_by_id(cls): # noqa: B902 return db.Column( db.Integer, db.ForeignKey("users.id", name="fk_{}_hidden_by".format(cls.__name__)), nullable=True, ) @declared_attr def hidden_by(cls): # noqa: B902 return db.relationship("User", uselist=False, foreign_keys=[cls.hidden_by_id]) def hide(self, user, *args, **kwargs): from flaskbb.utils.helpers import time_utcnow self.hidden_by = user self.hidden = True self.hidden_at = time_utcnow() return self def unhide(self, *args, **kwargs): self.hidden_by = None self.hidden = False self.hidden_at = None return self
class TopicsRead(db.Model): __tablename__ = "topicsread" user_id = db.Column(db.Integer, db.ForeignKey("users.id"), primary_key=True) topic_id = db.Column(db.Integer, db.ForeignKey("topics.id", use_alter=True, name="fk_tr_topic_id"), primary_key=True) forum_id = db.Column(db.Integer, db.ForeignKey("forums.id", use_alter=True, name="fk_tr_forum_id"), primary_key=True) last_read = db.Column(db.DateTime, default=datetime.utcnow()) def save(self): db.session.add(self) db.session.commit() return self def delete(self): db.session.delete(self) db.session.commit() return self
class ForumsRead(db.Model): __tablename__ = "forumsread" user_id = db.Column(db.Integer, db.ForeignKey("users.id"), primary_key=True) forum_id = db.Column(db.Integer, db.ForeignKey("forums.id", use_alter=True, name="fk_fr_forum_id"), primary_key=True) last_read = db.Column(db.DateTime, default=datetime.utcnow()) cleared = db.Column(db.DateTime) def __repr__(self): return "<{}>".format(self.__class__.__name__) def save(self): """Saves a ForumsRead entry.""" db.session.add(self) db.session.commit() return self def delete(self): """Deletes a ForumsRead entry.""" db.session.delete(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 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 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 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 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 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 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 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 ForumsRead(db.Model, CRUDMixin): __tablename__ = "forumsread" user_id = db.Column(db.Integer, db.ForeignKey("users.id"), primary_key=True) forum_id = db.Column(db.Integer, db.ForeignKey("forums.id", use_alter=True, name="fk_fr_forum_id"), primary_key=True) last_read = db.Column(UTCDateTime(timezone=True), default=time_utcnow) cleared = db.Column(UTCDateTime(timezone=True))
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 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 PluginStore(CRUDMixin, db.Model): id = db.Column(db.Integer, primary_key=True) key = db.Column(db.Unicode(255), nullable=False) value = db.Column(db.PickleType, nullable=False) # Available types: string, integer, float, boolean, select, selectmultiple value_type = db.Column(db.Enum(SettingValueType), nullable=False) # Extra attributes like, validation things (min, max length...) # For Select*Fields required: choices extra = db.Column(db.PickleType, nullable=True) plugin_id = db.Column( db.Integer, db.ForeignKey("plugin_registry.id", ondelete="CASCADE")) # Display stuff name = db.Column(db.Unicode(255), nullable=False) description = db.Column(db.Text, nullable=True) __table_args__ = (UniqueConstraint('key', 'plugin_id', name='plugin_kv_uniq'), ) def __repr__(self): return '<PluginSetting plugin={} key={} value={}>'.format( self.plugin.name, self.key, self.value) @classmethod def get_or_create(cls, plugin_id, key): """Returns the PluginStore object or an empty one. The created object still needs to be added to the database session """ obj = cls.query.filter_by(plugin_id=plugin_id, key=key).first() if obj is not None: return obj return PluginStore()
class RawData(db.Model, CRUDMixin): __tablename__ = "rawdata" id = db.Column(db.Integer, primary_key=True) thread_id = db.Column(db.Integer, nullable=False) forum_id = db.Column(db.Integer, nullable=False) thread_name = db.Column(db.String, nullable=False) post_num = db.Column(db.Integer, nullable=False) username = db.Column(db.String, nullable=False) message = db.Column(db.String, nullable=False) date_created = db.Column(UTCDateTime(timezone=True), default=time_utcnow, nullable=False) def save(self): """Saves a raw_data and returns the saved raw_data object. :param raw_data: If given, it will also save the raw_data for the raw_data. It expects a Message object. """ db.session.add(self) db.session.commit() return self
def hidden_by_id(cls): # noqa: B902 return db.Column( db.Integer, db.ForeignKey("users.id", name="fk_{}_hidden_by".format(cls.__name__)), nullable=True, )
class TopicsRead(db.Model, CRUDMixin): __tablename__ = "topicsread" user_id = db.Column(db.Integer, db.ForeignKey("users.id"), primary_key=True) topic_id = db.Column(db.Integer, db.ForeignKey("topics.id", use_alter=True, name="fk_tr_topic_id"), primary_key=True) forum_id = db.Column(db.Integer, db.ForeignKey("forums.id", use_alter=True, name="fk_tr_forum_id"), primary_key=True) last_read = db.Column(db.DateTime, default=datetime.utcnow())
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): __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 TopicsRead(db.Model, CRUDMixin): __tablename__ = "topicsread" user_id = db.Column(db.Integer, db.ForeignKey("users.id"), primary_key=True) topic_id = db.Column(db.Integer, db.ForeignKey("topics.id", use_alter=True, name="fk_tr_topic_id"), primary_key=True) forum_id = db.Column(db.Integer, db.ForeignKey("forums.id", use_alter=True, name="fk_tr_forum_id"), primary_key=True) last_read = db.Column(UTCDateTime(timezone=True), default=time_utcnow, nullable=False)
class Rank(db.Model): __tablename__ = "ranks" id = db.Column(db.Integer(), primary_key=True) rank_code = db.Column(db.String(255), nullable=False) rank_name = db.Column(db.String(255), default="") requirement = db.Column(db.Integer(), nullable=True, unique=True) users = association_proxy("user_ranks", "user", creator=lambda user: UserRank(user=user)) def is_custom(self): return self.requirement is None @staticmethod def has_rank(user): if user.is_anonymous: return False return user.rank is not None @classmethod def has_custom_rank(cls, user): return cls.has_rank(user) and user.rank.is_custom() def __repr__(self): return "<Rank name={} requirement={}>".format(self.rank_name, self.requirement) @classmethod def partition_ranks(cls, ranks): r = {"custom": [], "requirement": []} for rank in ranks: if cls.is_custom(rank): r["custom"].append(rank) else: r["requirement"].append(rank) return r
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 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 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 Setting(db.Model): __tablename__ = "settings" key = db.Column(db.String, primary_key=True) _value = db.Column("value", db.String, nullable=False) settingsgroup = db.Column(db.String, db.ForeignKey('settingsgroup.key', use_alter=True, name="fk_settingsgroup"), nullable=False) # The name (displayed in the form) name = db.Column(db.String, nullable=False) # The description (displayed in the form) description = db.Column(db.String, nullable=False) # Available types: string, integer, boolean, array, float value_type = db.Column(db.String, nullable=False) # Available types: text, number, choice, yesno # They are used in the form creation process input_type = db.Column(db.String, nullable=False) # Extra attributes like, validation things (min, max length...) _extra = db.Column("extra", db.String) # Properties @property def value(self): return normalize_to(self._value, self.value_type) @value.setter def value(self, value): self._value = normalize_to(value, self.value_type, reverse=True) @property def extra(self): return pickle.loads(base64.decodestring(self._extra)) @extra.setter def extra(self, extra): self._extra = base64.encodestring( pickle.dumps((extra), pickle.HIGHEST_PROTOCOL) ) @classmethod def get_form(cls, group): """Returns a Form for all settings found in :class:`SettingsGroup`. :param group: The settingsgroup name. It is used to get the settings which are in the specified group. """ class SettingsForm(Form): pass # now parse that shit for setting in group.settings: field_validators = [] # generate the validators # TODO: Do this in another function if "min" in setting.extra: # Min number validator if setting.value_type in ("integer", "float"): field_validators.append( validators.NumberRange(min=setting.extra["min"]) ) # Min text length validator elif setting.value_type in ("string", "array"): field_validators.append( validators.Length(min=setting.extra["min"]) ) if "max" in setting.extra: # Max number validator if setting.value_type in ("integer", "float"): field_validators.append( validators.NumberRange(max=setting.extra["max"]) ) # Max text length validator elif setting.value_type in ("string", "array"): field_validators.append( validators.Length(max=setting.extra["max"]) ) # Generate the fields based on input_type and value_type if setting.input_type == "number": # IntegerField if setting.value_type == "integer": setattr( SettingsForm, setting.key, IntegerField(setting.name, validators=field_validators, description=setting.description) ) # FloatField elif setting.value_type == "float": setattr( SettingsForm, setting.key, FloatField(setting.name, validators=field_validators, description=setting.description) ) # TextField if setting.input_type == "text": setattr( SettingsForm, setting.key, TextField(setting.name, validators=field_validators, description=setting.description) ) if setting.input_type == "array": setattr( SettingsForm, setting.key, TextField(setting.name, validators=field_validators, description=setting.description) ) # SelectField if setting.input_type == "choice" and "choices" in setting.extra: setattr( SettingsForm, setting.key, SelectField(setting.name, choices=setting.extra['choices'], description=setting.description) ) # BooleanField if setting.input_type == "yesno": setattr( SettingsForm, setting.key, BooleanField(setting.name, description=setting.description) ) return SettingsForm @classmethod def get_all(cls): return cls.query.all() @classmethod def update(cls, settings, app=None): """Updates the current_app's config and stores the changes in the database. :param config: A dictionary with configuration items. """ updated_settings = {} for key, value in settings.iteritems(): setting = cls.query.filter(Setting.key == key.lower()).first() setting.value = value updated_settings[setting.key.upper()] = setting.value db.session.add(setting) db.session.commit() if app is not None: app.config.update(updated_settings) @classmethod def as_dict(cls, from_group=None, upper=False): """Returns the settings key and value as a dict. :param from_group: Returns only the settings from the group as a dict. :param upper: If upper is ``True``, the key will use upper-case letters. Defaults to ``False``. """ settings = {} result = None if from_group is not None: result = SettingsGroup.query.filter_by(key=from_group).\ first_or_404() result = result.settings else: result = cls.query.all() for setting in result: if upper: settings[setting.key.upper()] = setting.value else: settings[setting.key] = setting.value return settings def save(self): """Saves a setting""" db.session.add(self) db.session.commit() def delete(self): """Deletes a setting""" db.session.delete(self) db.session.commit()
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