Beispiel #1
0
class Forum(db.Model, CRUDMixin):
    __tablename__ = "forums"

    id = db.Column(db.Integer, primary_key=True)
    category_id = db.Column(db.Integer,
                            db.ForeignKey("categories.id", ondelete="CASCADE"),
                            nullable=False)
    title = db.Column(db.String(255), nullable=False)
    description = db.Column(db.Text, nullable=True)
    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), nullable=True)

    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"),
                             nullable=True)  # we handle this case ourselfs
    last_post = db.relationship("Post",
                                backref="last_post_forum",
                                uselist=False,
                                foreign_keys=[last_post_id])

    # set to null if the user got deleted
    last_post_user_id = db.Column(db.Integer,
                                  db.ForeignKey("users.id",
                                                ondelete="SET NULL"),
                                  nullable=True)

    last_post_user = db.relationship("User",
                                     uselist=False,
                                     foreign_keys=[last_post_user_id])

    # Not nice, but needed to improve the performance; can be set to NULL
    # if the forum has no posts
    last_post_title = db.Column(db.String(255), nullable=True)
    last_post_username = db.Column(db.String(255), nullable=True)
    last_post_created = db.Column(UTCDateTime(timezone=True),
                                  default=time_utcnow,
                                  nullable=True)

    # 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, commit=True):
        """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()).\
            limit(1)\
            .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 last_post != self.last_post:
                self.last_post = last_post
                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 = None
            self.last_post_title = None
            self.last_post_user = None
            self.last_post_username = None
            self.last_post_created = None

        if commit:
            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,  # noqa: E711
                          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 = user
            forumsread.forum = self
            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(Topic.forum_id == self.id,
                                         Topic.hidden != True).count()
        post_count = Post.query.filter(Post.topic_id == Topic.id,
                                       Topic.forum_id == self.id,
                                       Post.hidden != True,
                                       Topic.hidden != True).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()

        # 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
Beispiel #2
0
class Post(HideableCRUDMixin, db.Model):
    __tablename__ = "posts"

    id = db.Column(db.Integer, primary_key=True)
    topic_id = db.Column(db.Integer,
                         db.ForeignKey("topics.id",
                                       ondelete="CASCADE",
                                       use_alter=True),
                         nullable=True)
    user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True)
    username = db.Column(db.String(200), nullable=False)
    content = db.Column(db.Text, nullable=False)
    date_created = db.Column(UTCDateTime(timezone=True),
                             default=time_utcnow,
                             nullable=False)
    date_modified = db.Column(UTCDateTime(timezone=True), nullable=True)
    modified_by = db.Column(db.String(200), nullable=True)

    # Properties
    @property
    def url(self):
        """Returns the url for the post."""
        return url_for("forum.view_post", post_id=self.id)

    # Methods
    def __init__(self, content=None, user=None, topic=None):
        """Creates a post object with some initial values.

        :param content: The content of the post.
        :param user: The user of the post.
        :param topic: Can either be the topic_id or the topic object.
        """
        if content:
            self.content = content

        if user:
            # setting user here -- even with setting the user id explicitly
            # breaks the bulk insert for some reason
            self.user_id = user.id
            self.username = user.username

        if topic:
            self.topic_id = topic if isinstance(topic, int) else topic.id

        self.date_created = 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)

    def save(self, user=None, topic=None):
        """Saves a new post. If no parameters are passed we assume that
        you will just update an existing post. It returns the object after the
        operation was successful.

        :param user: The user who has created the post
        :param topic: The topic in which the post was created
        """
        current_app.pluggy.hook.flaskbb_event_post_save_before(post=self)

        # update/edit the post
        if self.id:
            db.session.add(self)
            db.session.commit()
            current_app.pluggy.hook.flaskbb_event_post_save_after(post=self,
                                                                  is_new=False)
            return self

        # Adding a new post
        if user and topic:
            created = time_utcnow()
            self.user = user
            self.username = user.username
            self.topic = topic
            self.date_created = created

            if not topic.hidden:
                topic.last_updated = created
                topic.last_post = self

                # Update the last post info for the forum
                topic.forum.last_post = self
                topic.forum.last_post_user = self.user
                topic.forum.last_post_title = topic.title
                topic.forum.last_post_username = user.username
                topic.forum.last_post_created = created

                # Update the post counts
                user.post_count += 1
                topic.post_count += 1
                topic.forum.post_count += 1

            # And commit it!
            db.session.add(topic)
            db.session.commit()
            current_app.pluggy.hook.flaskbb_event_post_save_after(post=self,
                                                                  is_new=True)
            return self

    def delete(self):
        """Deletes a post and returns self."""
        # This will delete the whole topic
        if self.topic.first_post == self:
            self.topic.delete()
            return self

        db.session.delete(self)

        self._deal_with_last_post()
        self._update_counts()

        db.session.commit()
        return self

    def hide(self, user):
        if self.hidden:
            return

        if self.topic.first_post == self:
            self.topic.hide(user)
            return self

        super(Post, self).hide(user)
        self._deal_with_last_post()
        self._update_counts()
        db.session.commit()
        return self

    def unhide(self):
        if not self.hidden:
            return

        if self.topic.first_post == self:
            self.topic.unhide()
            return self

        self._restore_post_to_topic()
        super(Post, self).unhide()
        self._update_counts()
        db.session.commit()
        return self

    def _deal_with_last_post(self):
        if self.topic.last_post == self:

            # update the last post in the forum
            if self.topic.last_post == self.topic.forum.last_post:
                # We need the second last post in the forum here,
                # because the last post will be deleted
                second_last_post = Post.query.filter(
                    Post.topic_id == Topic.id,
                    Topic.forum_id == self.topic.forum.id, Post.hidden != True,
                    Post.id != self.id).order_by(
                        Post.id.desc()).limit(1).first()

                if second_last_post:
                    # now lets update the second last post to the last post
                    self.topic.forum.last_post = second_last_post
                    self.topic.forum.last_post_title = second_last_post.topic.title
                    self.topic.forum.last_post_user = second_last_post.user
                    self.topic.forum.last_post_username = second_last_post.username
                    self.topic.forum.last_post_created = second_last_post.date_created
                else:
                    self.topic.forum.last_post = None
                    self.topic.forum.last_post_title = None
                    self.topic.forum.last_post_user = None
                    self.topic.forum.last_post_username = None
                    self.topic.forum.last_post_created = None

            # check if there is a second last post in this topic
            if self.topic.second_last_post is not None:
                # Now the second last post will be the last post
                self.topic.last_post_id = self.topic.second_last_post

            # there is no second last post, now the last post is also the
            # first post
            else:
                self.topic.last_post = self.topic.first_post

            self.topic.last_updated = self.topic.last_post.date_created

    def _update_counts(self):
        if self.hidden:
            clauses = [Post.hidden != True, Post.id != self.id]
        else:
            clauses = [db.or_(Post.hidden != True, Post.id == self.id)]

        user_post_clauses = clauses + [
            Post.user_id == self.user.id,
            Topic.id == Post.topic_id,
            Topic.hidden != True,
        ]

        # Update the post counts
        self.user.post_count = Post.query.filter(*user_post_clauses).count()

        if self.topic.hidden:
            self.topic.post_count = 0
        else:
            topic_post_clauses = clauses + [
                Post.topic_id == self.topic.id,
            ]
            self.topic.post_count = Post.query.filter(
                *topic_post_clauses).count()

        forum_post_clauses = clauses + [
            Post.topic_id == Topic.id,
            Topic.forum_id == self.topic.forum.id,
            Topic.hidden != True,
        ]

        self.topic.forum.post_count = Post.query.filter(
            *forum_post_clauses).count()

    def _restore_post_to_topic(self):
        last_unhidden_post = Post.query.filter(
            Post.topic_id == self.topic_id,
            Post.id != self.id,
            Post.hidden != True,
        ).limit(1).first()

        # should never be None, but deal with it anyways to be safe
        if last_unhidden_post and self.date_created > last_unhidden_post.date_created:
            self.topic.last_post = self
            self.second_last_post = last_unhidden_post

            # if we're the newest in the topic again, we might be the newest
            # in the forum again only set if our parent topic isn't hidden
            if (not self.topic.hidden and
                (not self.topic.forum.last_post or
                 self.date_created > self.topic.forum.last_post.date_created)):
                self.topic.forum.last_post = self
                self.topic.forum.last_post_title = self.topic.title
                self.topic.forum.last_post_user = self.user
                self.topic.forum.last_post_username = self.username
                self.topic.forum.last_post_created = self.date_created
Beispiel #3
0
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)
Beispiel #4
0
class Topic(db.Model, CRUDMixin):
    __tablename__ = "topics"

    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 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 any else to the topic
        itself.

        :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:
            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_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
        created = time_utcnow()
        self.date_created = self.last_updated = created

        # 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
Beispiel #5
0
class Category(db.Model, CRUDMixin):
    __tablename__ = "categories"

    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(255), nullable=False)
    description = db.Column(db.Text, nullable=True)
    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)
Beispiel #6
0
class Pet(db.Model):
    __tablename__ = "pets"

    id = db.Column(db.Integer, primary_key=True)

    user_id = db.Column(
        db.Integer,
        db.ForeignKey("users.id",
                      use_alter=True,
                      name="fk_pets_owners_id",
                      ondelete="CASCADE"))

    petname = db.Column(db.String(200), nullable=False)
    birthday = db.Column(db.DateTime)
    gender = db.Column(db.String(10), default=False, nullable=False)
    breed = db.Column(db.String(50), default=False, nullable=False)
    avatar = db.Column(db.String(200))
    info = db.Column(db.Text)

    # Properties
    @property
    def url(self):
        """Returns the url for the pet."""
        return url_for("pet.profile",
                       username=self.user.username,
                       pet_id=self.id)

    # Methods
    def __init__(self,
                 petname,
                 birthday,
                 gender,
                 breed=None,
                 avatar=None,
                 info=None):
        """Creates a pet object with some initial values."""

        self.petname = petname
        self.birthday = birthday
        self.gender = gender
        self.breed = breed
        self.avatar = avatar
        self.info = info

    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 save(self, user=None):
        """Saves a new pet. If no parameters are passed we assume that
        you will just update an existing pet. It returns the object after the
        operation was successful.

        :param user: The user who has added the pet
        """
        # update/edit the pet
        if self.id:
            db.session.add(self)
            db.session.commit()
            return self

        # Adding a new pet
        if user:

            self.user_id = user.id

            db.session.add(self)
            db.session.commit()

            return self

    def delete(self):
        """Deletes a pet and returns self."""

        db.session.delete(self)
        db.session.commit()
        return self
Beispiel #7
0
class Post(db.Model, CRUDMixin):
    __tablename__ = "posts"

    id = db.Column(db.Integer, primary_key=True)
    topic_id = db.Column(db.Integer,
                         db.ForeignKey("topics.id",
                                       use_alter=True,
                                       name="fk_post_topic_id",
                                       ondelete="CASCADE"))
    user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True)
    username = db.Column(db.String(200), nullable=False)
    content = db.Column(db.Text, nullable=False)
    date_created = db.Column(UTCDateTime(timezone=True), default=time_utcnow)
    date_modified = db.Column(UTCDateTime(timezone=True))
    modified_by = db.Column(db.String(200))

    # Properties
    @property
    def url(self):
        """Returns the url for the post."""
        return url_for("forum.view_post", post_id=self.id)

    # Methods
    def __init__(self, content=None, user=None, topic=None):
        """Creates a post object with some initial values.

        :param content: The content of the post.
        :param user: The user of the post.
        :param topic: Can either be the topic_id or the topic object.
        """
        if content:
            self.content = content

        if user:
            self.user_id = user.id
            self.username = user.username

        if topic:
            self.topic_id = topic if isinstance(topic, int) else topic.id

        self.date_created = 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)

    def save(self, user=None, topic=None):
        """Saves a new post. If no parameters are passed we assume that
        you will just update an existing post. It returns the object after the
        operation was successful.

        :param user: The user who has created the post
        :param topic: The topic in which the post was created
        """
        # update/edit the post
        if self.id:
            db.session.add(self)
            db.session.commit()
            return self

        # Adding a new post
        if user and topic:
            created = time_utcnow()
            self.user_id = user.id
            self.username = user.username
            self.topic_id = topic.id
            self.date_created = created

            topic.last_updated = created

            # This needs to be done before the last_post_id gets updated.
            db.session.add(self)
            db.session.commit()

            # Now lets update the last post id
            topic.last_post_id = self.id

            # Update the last post info for the forum
            topic.forum.last_post_id = self.id
            topic.forum.last_post_title = topic.title
            topic.forum.last_post_user_id = user.id
            topic.forum.last_post_username = user.username
            topic.forum.last_post_created = created

            # Update the post counts
            user.post_count += 1
            topic.post_count += 1
            topic.forum.post_count += 1

            # And commit it!
            db.session.add(topic)
            db.session.commit()
            return self

    def delete(self):
        """Deletes a post and returns self."""
        # This will delete the whole topic
        if self.topic.first_post_id == self.id:
            self.topic.delete()
            return self

        # Delete the last post
        if self.topic.last_post_id == self.id:

            # update the last post in the forum
            if self.topic.last_post_id == self.topic.forum.last_post_id:
                # We need the second last post in the forum here,
                # because the last post will be deleted
                second_last_post = Post.query.\
                    filter(Post.topic_id == Topic.id,
                           Topic.forum_id == self.topic.forum.id).\
                    order_by(Post.id.desc()).limit(2).offset(0).\
                    all()

                second_last_post = second_last_post[1]

                self.topic.forum.last_post_id = second_last_post.id

            # check if there is a second last post, else it is the first post
            if self.topic.second_last_post:
                # Now the second last post will be the last post
                self.topic.last_post_id = self.topic.second_last_post

            # there is no second last post, now the last post is also the
            # first post
            else:
                self.topic.last_post_id = self.topic.first_post_id

        # Update the post counts
        self.user.post_count -= 1
        self.topic.post_count -= 1
        self.topic.forum.post_count -= 1
        db.session.commit()

        db.session.delete(self)
        db.session.commit()
        return self
Beispiel #8
0
class Setting(db.Model):
    __tablename__ = "settings"

    key = db.Column(db.String(255), primary_key=True)
    value = db.Column(db.PickleType, 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(200), nullable=False)

    # The description (displayed in the form)
    description = db.Column(db.Text, nullable=False)

    # Available types: string, integer, float, boolean, select, selectmultiple
    value_type = db.Column(db.String(20), nullable=False)

    # Extra attributes like, validation things (min, max length...)
    # For Select*Fields required: choices
    extra = db.Column(db.PickleType)

    @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 the settings in this group
        for setting in group.settings:
            field_validators = []

            # generate the validators
            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"):
                    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"):
                    field_validators.append(
                        validators.Length(max=setting.extra["max"])
                    )

            # Generate the fields based on value_type
            # 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.value_type == "string":
                setattr(
                    SettingsForm, setting.key,
                    TextField(setting.name, validators=field_validators,
                              description=setting.description)
                )

            # SelectMultipleField
            if setting.value_type == "selectmultiple":
                # if no coerce is found, it will fallback to unicode
                if "coerce" in setting.extra:
                    coerce_to = setting.extra['coerce']
                else:
                    coerce_to = unicode

                setattr(
                    SettingsForm, setting.key,
                    SelectMultipleField(
                        setting.name,
                        choices=setting.extra['choices'](),
                        coerce=coerce_to,
                        description=setting.description
                    )
                )

            # SelectField
            if setting.value_type == "select":
                # if no coerce is found, it will fallback to unicode
                if "coerce" in setting.extra:
                    coerce_to = setting.extra['coerce']
                else:
                    coerce_to = unicode

                setattr(
                    SettingsForm, setting.key,
                    SelectField(
                        setting.name,
                        coerce=coerce_to,
                        choices=setting.extra['choices'](),
                        description=setting.description)
                )

            # BooleanField
            if setting.value_type == "boolean":
                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 cache and stores the changes in the
        database.

        :param settings: A dictionary with setting items.
        """
        # update the database
        for key, value in settings.iteritems():
            setting = cls.query.filter(Setting.key == key.lower()).first()

            setting.value = value

            db.session.add(setting)
            db.session.commit()

        cls.invalidate_cache()

    @classmethod
    def get_settings(cls, from_group=None):
        """This will return all settings with the key as the key for the dict
        and the values are packed again in a dict which contains
        the remaining attributes.

        :param from_group: Optionally - Returns only the settings from a group.
        """
        result = None
        if from_group is not None:
            result = from_group.settings
        else:
            result = cls.query.all()

        settings = {}
        for setting in result:
            settings[setting.key] = {
                'name': setting.name,
                'description': setting.description,
                'value': setting.value,
                'value_type': setting.value_type,
                'extra': setting.extra
            }

        return settings

    @classmethod
    @cache.memoize(timeout=sys.maxint)
    def as_dict(cls, from_group=None, upper=True):
        """Returns all settings as a dict. This method is cached. If you want
        to invalidate the cache, simply execute ``self.invalidate_cache()``.

        :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:
                setting_key = setting.key.upper()
            else:
                setting_key = setting.key

            settings[setting_key] = setting.value

        return settings

    @classmethod
    def invalidate_cache(cls):
        """Invalidates this objects cached metadata."""
        cache.delete_memoized(cls.as_dict, cls)

    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()
Beispiel #9
0
class Setting(db.Model, CRUDMixin):
    __tablename__ = "settings"

    key = db.Column(db.String(255), primary_key=True)
    value = db.Column(db.PickleType, nullable=False)
    settingsgroup = db.Column(db.String(255),
                              db.ForeignKey('settingsgroup.key',
                                            use_alter=True,
                                            name="fk_settingsgroup"),
                              nullable=False)

    # The name (displayed in the form)
    name = db.Column(db.String(200), nullable=False)

    # The description (displayed in the form)
    description = db.Column(db.Text, 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)

    @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.
        """
        return generate_settings_form(settings=group.settings)

    @classmethod
    def get_all(cls):
        return cls.query.all()

    @classmethod
    def update(cls, settings, app=None):
        """Updates the cache and stores the changes in the
        database.

        :param settings: A dictionary with setting items.
        """
        # update the database
        for key, value in iteritems(settings):
            setting = cls.query.filter(Setting.key == key.lower()).first()

            setting.value = value

            db.session.add(setting)
            db.session.commit()

        cls.invalidate_cache()

    @classmethod
    def get_settings(cls, from_group=None):
        """This will return all settings with the key as the key for the dict
        and the values are packed again in a dict which contains
        the remaining attributes.

        :param from_group: Optionally - Returns only the settings from a group.
        """
        result = None
        if from_group is not None:
            result = from_group.settings
        else:
            result = cls.query.all()

        settings = {}
        for setting in result:
            settings[setting.key] = setting.value

        return settings

    @classmethod
    @cache.cached(key_prefix="settings")
    def as_dict(cls, from_group=None, upper=True):
        """Returns all settings as a dict. This method is cached. If you want
        to invalidate the cache, simply execute ``self.invalidate_cache()``.

        :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:
                setting_key = setting.key.upper()
            else:
                setting_key = setting.key

            settings[setting_key] = setting.value

        return settings

    @classmethod
    def invalidate_cache(cls):
        """Invalidates this objects cached metadata."""
        cache.delete_memoized(cls.as_dict, cls)
Beispiel #10
0
class Group(db.Model, CRUDMixin):
    __tablename__ = "groups"

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(255), unique=True, nullable=False)
    description = db.Column(db.Text, nullable=True)

    # Group types
    admin = db.Column(db.Boolean, default=False, nullable=False)
    super_mod = db.Column(db.Boolean, default=False, nullable=False)
    mod = db.Column(db.Boolean, default=False, nullable=False)
    guest = db.Column(db.Boolean, default=False, nullable=False)
    banned = db.Column(db.Boolean, default=False, nullable=False)

    # Hub permissions
    onyx_base = db.Column(db.Boolean, default=False, nullable=False)
    onyx_additional = db.Column(db.Boolean, default=False, nullable=False)
    onyx_management = db.Column(db.Boolean, default=False, nullable=False)
    dragon_base = db.Column(db.Boolean, default=False, nullable=False)
    dragon_additional = db.Column(db.Boolean, default=False, nullable=False)
    dragon_management = db.Column(db.Boolean, default=False, nullable=False)
    eos_base = db.Column(db.Boolean, default=False, nullable=False)
    eos_additional = db.Column(db.Boolean, default=False, nullable=False)
    eos_management = db.Column(db.Boolean, default=False, nullable=False)

    # Moderator permissions (only available when the user a moderator)
    mod_edituser = db.Column(db.Boolean, default=False, nullable=False)
    mod_banuser = db.Column(db.Boolean, default=False, nullable=False)

    # User permissions
    editpost = db.Column(db.Boolean, default=True, nullable=False)
    deletepost = db.Column(db.Boolean, default=False, nullable=False)
    deletetopic = db.Column(db.Boolean, default=False, nullable=False)
    posttopic = db.Column(db.Boolean, default=True, nullable=False)
    postreply = db.Column(db.Boolean, default=True, nullable=False)
    viewhidden = db.Column(db.Boolean, default=False, nullable=False)
    makehidden = db.Column(db.Boolean, default=False, nullable=False)

    # 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, self.name)

    @classmethod
    def selectable_groups_choices(cls):
        return Group.query.order_by(Group.name.asc()).with_entities(
            Group.id, Group.name).all()

    @classmethod
    def get_guest_group(cls):
        return cls.query.filter(cls.guest == True).first()

    @classmethod
    def get_member_group(cls):
        """Returns the first member group."""
        # This feels ugly..
        return cls.query.filter(cls.id == 4).first()

    def save(self):
        User.invalidate_cache_for_all()  # for a case if we changed permissions
        super().save()
Beispiel #11
0
class Forum(db.Model):
    __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(db.DateTime, default=datetime.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")

    # 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

        # No post found..
        else:
            self.last_post_id = 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 = datetime.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 = datetime.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 = datetime.utcnow()
            forumsread.save()
            return True

        # Nothing updated, because there are still more than 0 unread topicsread
        return False

    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 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

    # 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
Beispiel #12
0
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
Beispiel #13
0
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()
Beispiel #14
0
class Post(db.Model):
    __tablename__ = "posts"

    id = db.Column(db.Integer, primary_key=True)
    topic_id = db.Column(db.Integer,
                         db.ForeignKey("topics.id",
                                       use_alter=True,
                                       name="fk_post_topic_id",
                                       ondelete="CASCADE"),
                         nullable=False)
    user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
    username = db.Column(db.String(15), nullable=False)
    content = db.Column(db.Text, nullable=False)
    date_created = db.Column(db.DateTime, default=datetime.utcnow())
    date_modified = db.Column(db.DateTime)
    modified_by = db.Column(db.String(15))

    # Properties
    @property
    def url(self):
        """Returns the url for the post"""
        return url_for("forum.view_post", post_id=self.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 save(self, user=None, topic=None):
        """Saves a new post. If no parameters are passed we assume that
        you will just update an existing post. It returns the object after the
        operation was successful.

        :param user: The user who has created the post

        :param topic: The topic in which the post was created
        """
        # update/edit the post
        if self.id:
            db.session.add(self)
            db.session.commit()
            return self

        # Adding a new post
        if user and topic:
            self.user_id = user.id
            self.username = user.username
            self.topic_id = topic.id
            self.date_created = datetime.utcnow()

            # This needs to be done before I update the last_post_id.
            db.session.add(self)
            db.session.commit()

            # Now lets update the last post id
            topic.last_post_id = self.id
            topic.forum.last_post_id = self.id

            # Update the post counts
            user.post_count += 1
            topic.post_count += 1
            topic.forum.post_count += 1

            # And commit it!
            db.session.add(topic)
            db.session.commit()
            return self

    def delete(self):
        """Deletes a post and returns self"""
        # This will delete the whole topic
        if self.topic.first_post_id == self.id:
            self.topic.delete()
            return self

        # Delete the last post
        if self.topic.last_post_id == self.id:

            # Now the second last post will be the last post
            self.topic.last_post_id = self.topic.second_last_post

            # check if the last_post is also the last post in the forum
            if self.topic.last_post_id == self.id:
                self.topic.last_post_id = self.topic.second_last_post
                self.topic.forum.last_post_id = self.topic.second_last_post
                db.session.commit()

        # Update the post counts
        self.user.post_count -= 1
        self.topic.post_count -= 1
        self.topic.forum.post_count -= 1

        db.session.delete(self)
        db.session.commit()
        return self
Beispiel #15
0
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"),
                             nullable=True)
    to_user_id = db.Column(db.Integer,
                           db.ForeignKey("users.id"),
                           nullable=True)
    shared_id = db.Column(UUIDType, nullable=False)
    subject = db.Column(db.String(255), nullable=True)
    date_created = db.Column(UTCDateTime(timezone=True),
                             default=time_utcnow,
                             nullable=False)
    date_modified = db.Column(UTCDateTime(timezone=True),
                              default=time_utcnow,
                              nullable=False)
    trash = db.Column(db.Boolean, default=False, nullable=False)
    draft = db.Column(db.Boolean, default=False, nullable=False)
    unread = db.Column(db.Boolean, default=False, nullable=False)

    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

        self.date_modified = time_utcnow()
        db.session.add(self)
        db.session.commit()
        return self
Beispiel #16
0
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)
    lastseen = db.Column(UTCDateTime(timezone=True), default=time_utcnow)
    birthday = db.Column(UTCDateTime(timezone=True))
    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(UTCDateTime(timezone=True))
    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 = (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
    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."""
        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,
                   Topic.id == Post.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).\
            order_by(Post.id.desc()).\
            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
Beispiel #17
0
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
Beispiel #18
0
class Reminder(db.Model):
    __tablename__ = 'reminders'

    id = db.Column(db.Integer, primary_key=True)

    user_id = db.Column(
        db.Integer,
        db.ForeignKey("users.id",
                      use_alter=True,
                      name="fk_reminders_users_id",
                      ondelete="CASCADE"))
    subject = db.Column(db.String(100))

    content = db.Column(db.Text, nullable=False)

    delta = db.Column(db.Integer, nullable=False)
    time = db.Column(db.DateTime, nullable=False)
    timezone = db.Column(db.String(50), nullable=False)

    def __init__(self, subject, content, delta, time, timezone):
        self.subject = subject
        self.content = content
        self.delta = delta
        self.time = time
        self.timezone = timezone

    # # Properties
    # @property
    # def url(self):
    #     """Returns the url for the reminder."""
    #     return url_for("pet.reminder", username=self.user.username, petname=self.pet.petname)

    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 save(self, user_id=None):

        # update/edit the reminder
        if self.id:
            db.session.add(self)
            db.session.commit()
            return self

        # Adding a new pet
        if user_id:
            self.user_id = user_id

            db.session.add(self)
            db.session.commit()

            return self

    def delete(self):
        """Deletes a reminder and returns self."""

        db.session.delete(self)
        db.session.commit()
        return self

    def get_notification_time(self):
        remi_time = arrow.get(self.time)
        reminder_time = remi_time.replace(minutes=-self.delta)
        return reminder_time