Пример #1
0
class Contact(BaseMixin, db.Model):
    """关注和被关注"""
    __tablename__ = 'contacts'
    to_id = db.Column(db.Integer)
    from_id = db.Column(db.Integer)

    __table_args__ = (
        db.UniqueConstraint('from_id', 'to_id', name='uk_from_to'),
        db.Index('idx_to_time_from', to_id, 'created_at', from_id),
        db.Index('idx_time_to_from', 'created_at', to_id, from_id),
    )

    @classmethod
    def __flush_event__(cls, target):
        rdb.delete(MC_KEY_CONTACT_N % (target.target_id, target.target_kind))

    @classmethod
    @cache(MC_KEY_CONTACT_N % ('{target_id}', '{target_kind}'))
    def get_count_by_target(cls, target_id, target_kind):
        return cls.query.filter_by(target_id=target_id,
                                   target_kind=target_kind).count()

    @classmethod
    def get(cls, user_id, target_id, target_kind):
        return cls.query.filter_by(user_id=user_id,
                                   target_id=target_id,
                                   target_kind=target_kind).first()
Пример #2
0
class Comment(BaseMixin, LikeMixin, db.Model):
    __tablename__ = 'comments'
    user_id = db.Column(db.Integer)
    target_id = db.Column(db.Integer)  # 评论的目标
    target_kind = db.Column(db.Integer)  # 评论目标的类型
    ref_id = db.Column(db.Integer, default=0)  # 引用其他人的评论, 可以不引用
    content = PropsItem('content', '')
    kind = K_COMMENT  # 自己是评论类型

    __table_args__ = (db.Index('idx_ti_tk_ui', target_id, target_kind,
                               user_id), )

    @cached_hybrid_property
    def html_content(self):
        return self.content

    @cached_hybrid_property
    def user(self):
        return User.get(self.user_id)

    @classmethod
    def __flush_event__(cls, target):
        """BaseModel里面配置了event, 会在增删改的时候把缓存清空"""
        for key in (MC_KEY_COMMENT_LIST, MC_KEY_COMMENT_N):
            rdb.delete(key % (target.id, target.kind))
Пример #3
0
class CollectItem(BaseMixin, db.Model):
    """收藏"""
    __tablename__ = 'collect_items'
    user_id = db.Column(db.Integer)  # 谁收藏的
    target_id = db.Column(db.Integer)  # 收藏的目标id
    target_kind = db.Column(db.Integer)  # 收藏的种类, 比如post

    __table_args__ = (db.Index('idx_ti_tk_ui', target_id, target_kind,
                               user_id), )

    @classmethod
    def __flush_event__(cls, target):
        """BaseModel里面配置了event, 会在增删改的时候把缓存清空"""
        rdb.delete(MC_KEY_COLLECT_N % (target.target_id, target.target_kind))

    # 读数据是用下缓存
    @classmethod
    @cache(MC_KEY_COLLECT_N % ('{target_id}', '{target_kind}'))
    def get_count_by_target(cls, target_id, target_kind):
        return cls.query.filter_by(target_id=target_id,
                                   target_kind=target_kind).count()

    @classmethod
    def get(cls, user_id, target_id, target_kind):
        return cls.query.filter_by(user_id=user_id,
                                   target_id=target_id,
                                   target_kind=target_kind).first()
Пример #4
0
class Tag(BaseMixin, db.Model):
    __tablename__ = "tags"
    name = db.Column(db.String(128), default="", unique=True)

    __table_args__ = (db.Index("idx_name", name),)

    @classmethod
    def get_by_name(cls, name):
        return cls.query.filter_by(name=name).first()

    def update(self, **kwargs):
        raise NotImplementedError("tag table can`t update ")

    def delete(self):
        raise NotImplementedError("tag table can`t delete ")

    @classmethod
    def create(cls, **kwargs):
        name = kwargs.pop("name")
        kwargs["name"] = name.lower()
        return super().create(**kwargs)

    @classmethod
    @cache(MC_KEY_ALL_TAGS)
    def all_tags(cls):
        return cls.query.all()

    @classmethod
    def __flush_event__(cls, target):
        rdb.delete(MC_KEY_ALL_TAGS)
Пример #5
0
class LikeItem(BaseMixin, db.Model):
    __tablename__ = 'like_items'
    user_id = db.Column(db.Integer)
    target_id = db.Column(db.Integer)  # 喜欢的目标
    target_kind = db.Column(db.Integer)

    __table_args__ = (
        db.Index('idx_ti_tk_ui', target_id, target_kind, user_id),
    )

    @classmethod
    def __flush_event__(cls, target):
        """BaseModel里面配置了event, 会在增删改的时候把缓存清空"""
        rdb.delete(MC_KEY_LIKE_N % (target.target_id, target.target_kind))

    @classmethod
    @cache(MC_KEY_LIKE_N % ('{target_id}', '{target_kind}'))
    def get_count_by_target(cls, target_id, target_kind):
        return cls.query.filter_by(target_id=target_id,
                                   target_kind=target_kind).count()

    @classmethod
    def get(cls, user_id, target_id, target_kind):
        return cls.query.filter_by(user_id=user_id, target_id=target_id,
                                   target_kind=target_kind).first()
Пример #6
0
class Tag(BaseMixin, db.Model):
    __tablename__ = 'tags'
    name = db.Column(db.String(128), default='', unique=True)

    __table_args__ = (db.Index('idx_name', name), )

    @classmethod
    def get_by_name(cls, name):
        return cls.query.filter_by(name=name).first()

    def delete(self):
        raise NotAllowedException

    def update(self, **kwargs):
        raise NotAllowedException

    @classmethod
    def create(cls, **kwargs):
        name = kwargs.pop('name')
        kwargs['name'] = name.lower()
        return super().create(**kwargs)

    @classmethod
    def __flush_event__(cls, target):
        rdb.delete(MC_KEY_ALL_TAGS)
Пример #7
0
class OAuth(db.Model, BaseMixin):
    __tablename__ = "oauth"
    provider = db.Column(db.String(50))
    token = db.Column(MutableDict.as_mutable(JSONType))
    provider_user_id = db.Column(db.String(256))
    user_id = db.Column(db.Integer)

    __table_args__ = (db.Index("idx_pd_pu", provider, provider_user_id), )
Пример #8
0
class CollectItem(ActionMixin, db.Model):
    __tablename__ = "collect_items"
    user_id = db.Column(db.Integer)
    target_id = db.Column(db.Integer)
    target_kind = db.Column(db.Integer)

    action_type = "collect"

    __table_args__ = (db.Index("idx_ti_tk_ui", target_id, target_kind,
                               user_id), )
Пример #9
0
class LikeItem(ActionMixin, db.Model):
    __tablename__ = 'like_items'
    user_id = db.Column(db.Integer)
    target_id = db.Column(db.Integer)
    target_kind = db.Column(db.Integer)

    action_type = 'like'

    __table_args_ = (db.Index('idx_ti_tk_u_id', target_id, target_kind,
                              user_id), )
Пример #10
0
class CommentItem(ActionMixin, LikeMixin, db.Model):
    __tablename__ = "comment_items"
    user_id = db.Column(db.Integer)
    target_id = db.Column(db.Integer)
    target_kind = db.Column(db.Integer)
    ref_id = db.Column(db.Integer, default=0)
    content = PropsItem("content", "")
    kind = K_COMMENT

    action_type = "comment"

    __table_args__ = (db.Index("idx_ti_tk_ui", target_id, target_kind,
                               user_id), )

    @cached_hybrid_property
    def user(self):
        return User.get(self.user_id)
Пример #11
0
class CommentItem(ActionMixin, db.Model):
    __tablename__ = 'comment_items'
    user_id = db.Column(db.Integer)
    target_id = db.Column(db.Integer)
    target_kind = db.Column(db.Integer)
    ref_id = db.Column(
        db.Integer,
        default=0)  # ref_id = 0 if comment on Post, else comment on comment.
    content = PropsItem('content', '')  # this field is stored in key-value db
    kind = K_COMMENT

    action_type = 'comment'

    __table_args__ = (db.Index('idx_ti_tk_ui', target_id, target_kind,
                               user_id), )

    @cached_hybrid_property
    def html_content(self):
        return self.content

    @cached_hybrid_property
    def user(self):
        return User.get(self.user_id)
Пример #12
0
class Post(BaseMixin, CommentMixin, LikeMixin, CollectMixin, db.Model):
    __tablename__ = 'posts'
    author_id = db.Column(db.Integer)
    title = db.Column(db.String(128), default='')
    orig_url = db.Column(db.String(255), default='')
    can_comment = db.Column(db.Boolean, default=True)
    content = PropsItem('content', '')
    kind = K_POST

    __table_args__ = (
        db.Index('idx_title', title),
    )

    def url(self):
        return '/{}/{}/'.format(self.__class__.__name__.lower(), self.id)

    @classmethod
    def __flush_event__(cls, target):
        rdb.delete(MC_KEY_ALL_TAGS)

    @classmethod
    def get(cls, identifier):
        if is_numeric(identifier):
            return cls.cache.get(identifier)
        return cls.cache.filter(title=identifier).first()

    @property
    def tags(self):
        at_ids = PostTag.query.with_entities(
            PostTag.tag_id).filter(
            PostTag.post_id == self.id
        ).all()
        tags = Tag.query.filter(Tag.id.in_(id_ for id_, in at_ids)).all()
        return tags

    @cached_hybrid_property
    def abstract_content(self):
        return trunc_utf8(self.content, 100)

    @cached_hybrid_property
    def author(self):
        return User.get(self.author_id)

    @classmethod
    def create_or_update(cls, **kwargs):
        tags = kwargs.pop('tags', [])
        created, obj = super(Post, cls).create_or_update(**kwargs)
        if tags:
            PostTag.update_multi(obj.id, tags)
        return created, obj

    def delete(self):
        id = self.id
        super().delete()
        for pt in PostTag.query.filter_by(post_id=id):
            pt.delete()

    @cached_hybrid_property
    def netloc(self):
        return '{0.scheme}://{0.netloc}'.format(urlparse(self.orig_url))

    @staticmethod
    def _flush_insert_event(mapper, connection, target):
        target._flush_event(mapper, connection, target)
        target.__flush_insert_event__(target)
Пример #13
0
class User(db.Model, UserMixin, BaseMixin):
    __tablename__ = 'users'
    bio = db.Column(db.String(128), default='')
    name = db.Column(db.String(128), default='')
    nickname = db.Column(db.String(128), default='')
    email = db.Column(db.String(191), default='')
    password = db.Column(db.String(191))
    website = db.Column(db.String(191), default='')
    github_id = db.Column(db.String(191), default='')
    last_login_at = db.Column(db.DateTime())
    current_login_at = db.Column(db.DateTime())
    last_login_ip = db.Column(db.String(100))
    current_login_ip = db.Column(db.String(100))
    login_count = db.Column(db.Integer)
    active = db.Column(db.Boolean())
    icon_color = db.Column(db.String(7))
    confirmed_at = db.Column(db.DateTime())
    company = db.Column(db.String(191), default='')
    avatar_id = db.Column(db.String(20), default='')
    roles = db.relationship('Role',
                            secondary=roles_users,
                            backref=db.backref('users', lazy='dynamic'))

    __table_args__ = (
        db.Index('idx_name', name),
        db.Index('idx_email', email),
    )

    def url(self):
        return '/user/{}'.format(self.id)

    @property
    def avatar_path(self):
        avatar_id = self.avatar_id
        return '' if not avatar_id else '/static/avatars/{}.png'.format(
            avatar_id)

    def update_avatar(self, avatar_id):
        self.avatar_id = avatar_id
        self.save()

    def upload_avatar(self, img):
        avatar_id = generate_id()
        filename = os.path.join(UPLOAD_FOLDER, 'avatars',
                                '{}.png'.format(avatar_id))

        if isinstance(img, str) and img.startswith('http'):
            r = requests.get(img, stream=True)
            if r.status_code == 200:
                with open(filename, 'wb') as f:
                    for chunk in r.iter_content(1024):
                        f.write(chunk)
        else:
            img.save(filename)
        self.update_avatar(avatar_id)

    def follow(self, from_id):
        ok, _ = Contact.create(to_id=self.id, from_id=from_id)
        if ok:
            self._stats = None
        return ok

    def unfollow(self, from_id):
        contact = Contact.get_follow_item(from_id, self.id)
        if contact:
            contact.delete()
            self._stats = None
            return True
        return False

    def is_followed_by(self, user_id):
        contact = Contact.get_follow_item(user_id, self.id)
        return bool(contact)
Пример #14
0
class Contact(BaseMixin, db.Model):
    __tablename__ = "contacts"
    to_id = db.Column(db.Integer)
    from_id = db.Column(db.Integer)

    __table_args__ = (
        db.UniqueConstraint("from_id", "to_id", name="uk_from_to"),
        db.Index("idx_to_time_from", to_id, "created_at", from_id),
        db.Index("idx_time_to_from", "created_at", to_id, from_id),
    )

    def update(self, **kwargs):
        # Contact表不应该被更新
        raise NotImplementedError("contact table can`t update ")

    @classmethod
    def create(cls, **kwargs):
        ok, obj = super().create(**kwargs)
        cls.clear_mc(obj, 1)
        if ok:
            from .feed import feed_to_followers

            feed_to_followers(obj.from_id, obj.to_id)
            # from handler.tasks import feed_to_followers

            # feed_to_followers.delay(obj.from_id, obj.to_id)
        return ok, obj

    def delete(self):
        super().delete()
        self.clear_mc(self, -1)
        from handler.tasks import remove_user_posts_from_feed

        remove_user_posts_from_feed.delay(self.from_id, self.to_id)

    @classmethod
    @cache(MC_KEY_FOLLOW_ITEM.format("{from_id}", "{to_id}"))
    def get_follow_item(cls, from_id, to_id):
        return cls.query.filter_by(from_id=from_id, to_id=to_id).first()

    @classmethod
    @cache(MC_KEY_FOLLOWING.format("{user_id}", "{page}"))
    def get_following_ids(cls, user_id, page=1):
        query = cls.query.with_entities(cls.to_id).filter_by(from_id=user_id)
        following = query.paginate(page, PER_PAGE)
        following.items = [id for id, in following.items]
        del following.query
        return following

    @classmethod
    @cache(MC_KEY_FOLLOWERS.format("{user_id}", "{page}"))
    def get_follower_ids(cls, user_id, page=1):
        query = cls.query.with_entities(cls.from_id).filter_by(to_id=user_id)
        follower = query.paginate(page, PER_PAGE)
        follower.items = [id for id, in follower.items]
        del follower.query
        return follower

    @classmethod
    def clear_mc(cls, target, amount):
        to_id = target.to_id
        from_id = target.from_id

        st = userFollowStats.get_or_create(to_id)
        follower_count = st.follower_count or 0
        st.follower_count = follower_count + amount
        st.save()
        st = userFollowStats.get_or_create(from_id)
        following_count = st.following_count or 0
        st.following_count = following_count + amount
        st.save()

        rdb.delete(MC_KEY_FOLLOW_ITEM.format(from_id, to_id))

        for user_id, total, mc_key in (
            (to_id, follower_count, MC_KEY_FOLLOWERS),
            (from_id, following_count, MC_KEY_FOLLOWING),
        ):
            pages = math.ceil((max(total, 0) or 1) / PER_PAGE)
            for p in range(1, pages + 1):
                rdb.delete(mc_key.format(user_id, p))
Пример #15
0
class Contact(BaseMixin, db.Model):
    __tablename__ = 'contacts'
    to_id = db.Column(db.Integer)
    from_id = db.Column(db.Integer)

    __table_args__ = (
        db.UniqueConstraint('from_id', 'to_id', name='uk_from_to'),
        db.Index('idx_to_time_from', to_id, 'created_at', from_id),
        db.Index('idx_time_to_from', 'created_at', to_id, from_id),
    )

    def update(self, **kwargs):
        raise NotAllowedException

    @classmethod
    def create(cls, **kwargs):
        ok, obj = super().create(**kwargs)
        cls.clear_mc(obj, 1)  # amount = 1
        if ok:
            from handler.tasks import feed_to_followers
            feed_to_followers.delay(obj.from_id, obj.to_id)
        return ok, obj

    def delete(self):
        super().delete()
        self.clear_mc(self, -1)
        from handler.tasks import remove_user_posts_from_feed
        remove_user_posts_from_feed.delay(self.from_id, self.to_id)

    @classmethod
    @cache(MC_KEY_FOLLOWERS % ('{user_id}', '{page}'))
    def get_follower_ids(cls, user_id, page=1):
        query = cls.query.with_entities(cls.from_id).filter_by(to_id=user_id)
        followers = query.paginate(page, PER_PAGE)
        followers.items = [id for id, in followers.items]
        del followers.query  # fix 'TypeError: can't pickle _thread.lock objects'
        return followers

    @classmethod
    @cache(MC_KEY_FOLLOWING % ('{user_id}', '{page}'))
    def get_following_ids(cls, user_id, page=1):
        query = cls.query.with_entities(cls.to_id).filter_by(from_id=user_id)
        following = query.paginate(page, PER_PAGE)
        following.items = [id for id, in following.items]
        del following.query
        return following

    @classmethod
    @cache(MC_KEY_FOLLOW_ITEM % ('{from_id}', '{to_id}'))
    def get_follow_item(cls, from_id, to_id):
        return cls.query.filter_by(from_id=from_id, to_id=to_id).first()

    @classmethod
    def clear_mc(cls, target, amount):
        to_id = target.to_id
        from_id = target.from_id

        st = userFollowStats.get_or_create(to_id)
        follower_count = st.follower_count or 0
        st.follower_count = follower_count + amount
        st.save()

        st = userFollowStats.get_or_create(from_id)
        following_count = st.following_count or 0
        st.following_count = following_count + amount
        st.save()  # `class:BaseMixin:save`

        rdb.delete(MC_KEY_FOLLOW_ITEM % (from_id, to_id))

        # should be `follower_count + amount`, `following_count + amount`?
        for user_id, total, mc_key in ((to_id, follower_count,
                                        MC_KEY_FOLLOWERS),
                                       (from_id, following_count,
                                        MC_KEY_FOLLOWING)):
            pages = math.ceil((max(total, 0) or 1) / PER_PAGE)
            for p in range(1, pages + 1):
                rdb.delete(mc_key % (user_id, p))
Пример #16
0
class Post(BaseMixin, CommentMixin, LikeMixin, CollectMixin, db.Model):
    __tablename__ = "posts"
    author_id = db.Column(db.Integer)
    title = db.Column(db.String(128), default="")
    orig_url = db.Column(db.String(255), default="")
    can_comment = db.Column(db.Boolean, default=True)
    content = PropsItem("content", "")
    kind = K_POST

    __table_args__ = (db.Index("idx_title", title), db.Index("idx_authorId", author_id))

    @cached_hybrid_property
    def abstract_content(self):
        return trunc_utf8(self.content, 100)

    @cached_hybrid_property
    def author(self):
        return User.get(self.author_id)

    @cached_hybrid_property
    def orig_netloc(self):
        return urlparse(self.orig_url).netloc

    @classmethod
    def get(cls, identifier):
        if is_numeric(identifier):
            return cls.cache.get(identifier)
        return cls.cache.filter(title=identifier).first()

    @property
    @cache(MC_KEY_POST_TAGS.format("{self.id}"))
    def tags(self):
        at_ids = (
            PostTag.query.with_entities(PostTag.tag_id)
            .filter(PostTag.post_id == self.id)
            .all()
        )
        tags = Tag.query.filter(Tag.id.in_((id for id, in at_ids))).all()
        return tags

    @classmethod
    def create_or_update(cls, **kwargs):
        tags = kwargs.pop("tags", [])
        created, obj = super(Post, cls).create_or_update(**kwargs)
        if tags:
            PostTag.update_multi(obj.id, tags)
        if created:
            from handler.tasks import feed_post, reindex

            reindex.delay(obj.id, obj.kind, op_type="create")
            feed_post.delay(obj.id)
        return created, obj

    def delete(self):
        post_id = self.id
        author_id = self.author_id
        super().delete()
        for pt in PostTag.query.filter_by(post_id=post_id):
            pt.delete()
        from handler.tasks import remove_post_from_feed

        remove_post_from_feed.delay(post_id, author_id)

    @classmethod
    def __flush_event__(cls, target):
        rdb.delete(MC_KEY_ALL_TAGS)

    @staticmethod
    def _flush_insert_event(mapper, connection, target):
        target._flush_event(mapper, connection, target)
        target.__flush_insert_event__(target)
Пример #17
0
class User(db.Model, UserMixin, BaseMixin):
    __tablename__ = "users"
    bio = db.Column(db.String(128), default="")
    name = db.Column(db.String(128), default="")
    nickname = db.Column(db.String(128), default="")
    email = db.Column(db.String(191), default="")
    password = db.Column(db.String(191))
    website = db.Column(db.String(191), default="")
    github_id = db.Column(db.String(191), default="")
    last_login_at = db.Column(db.DateTime())
    current_login_at = db.Column(db.DateTime())
    last_login_ip = db.Column(db.String(100))
    current_login_ip = db.Column(db.String(100))
    login_count = db.Column(db.Integer)
    active = db.Column(db.Boolean())
    icon_color = db.Column(db.String(7))
    confirmed_at = db.Column(db.DateTime())
    company = db.Column(db.String(191), default="")
    avatar_id = db.Column(db.String(20), default="")
    roles = db.relationship("Role",
                            secondary=roles_users,
                            backref=db.backref("users", lazy="dynamic"))

    _stats = None

    __table_args__ = (db.Index("idx_name", name), db.Index("idx_email", email))

    def url(self):
        return f"/user/{self.id}"

    @property
    def avatar_path(self):
        avatar_id = self.avatar_id
        return "" if not avatar_id else f"/static/avatars/{avatar_id}.png"

    def update_avatar(self, avatar_id):
        self.avatar_id = avatar_id
        self.save()

    def upload_avatar(self, img):
        avatar_id = generate_id()
        filename = UPLOAD_FOLDER / "avatars" / f"{avatar_id}.png"
        if isinstance(img, str) and img.startswith("http"):
            r = requests.get(img, stream=True)
            if r.status_code == 200:
                with open(filename, "wb") as f:
                    for chunk in r.iter_content(1024):
                        f.write(chunk)
        else:
            with open(filename, "wb") as f:
                img.save(f)
        self.update_avatar(avatar_id)

    def follow(self, from_id):
        ok, _ = Contact.create(to_id=self.id, from_id=from_id)
        if ok:
            self._stats = None
        return ok

    def unfollow(self, from_id):
        contact = Contact.get_follow_item(from_id, self.id)
        if contact:
            contact.delete()
            self._stats = None
            return True
        return False

    def is_followed_by(self, user_id):
        contact = Contact.get_follow_item(user_id, self.id)
        return bool(contact)

    @property
    def n_followers(self):
        return self._follow_stats[0]

    @property
    def n_following(self):
        return self._follow_stats[1]

    @property
    def _follow_stats(self):
        if self._stats is None:
            stats = userFollowStats.get(self.id)
            if not stats:
                self._stats = 0, 0
            else:
                self._stats = stats.follower_count, stats.following_count
        return self._stats
Пример #18
0
class PostTag(BaseMixin, db.Model):
    __tablename__ = 'post_tags'
    post_id = db.Column(db.Integer)
    tag_id = db.Column(db.Integer)

    __table_args__ = (
        db.Index('idx_post_id', post_id, 'updated_at'),
        db.Index('idx_tag_id', tag_id, 'updated_at'),
    )

    @classmethod
    def _get_posts_by_tag(cls, identifier):
        if not identifier:
            return []
        if not is_numeric(identifier):
            tag = Tag.get_by_name(identifier)
            if not tag:
                return
            identifier = tag.id
        at_ids = cls.query.with_entities(
            cls.post_id).filter(cls.tag_id == identifier).all()

        query = Post.query.filter(Post.id.in_(id for id, in at_ids)).order_by(
            Post.id.desc())
        return query

    @classmethod
    @cache(MC_KEY_POSTS_BY_TAG % ('{identifier}', '{page}'))
    def get_posts_by_tag(cls, identifier, page=1):
        query = cls._get_posts_by_tag(identifier)
        posts = query.paginate(page, PER_PAGE)
        del posts.query  # Fix 'TypeError: can't pickle _thread.lock objects'
        return posts

    @classmethod
    @cache(MC_KEY_POST_STATS_BY_TAG % ('{identifier}'))
    def get_count_by_tag(cls, identifier):
        query = cls._get_posts_by_tag(identifier)
        return query.count()

    @classmethod
    def update_multi(cls, post_id, tags, origin_tags=None):
        if origin_tags is None:
            origin_tags = Post.get(post_id).tags
        need_add = set()
        need_del = set()
        for tag in tags:
            if tag not in origin_tags:
                need_add.add(tag)
        for tag in origin_tags:
            if tag not in tags:
                need_del.add(tag)
        need_add_tag_ids = set()
        need_del_tag_ids = set()
        for tag_name in need_add:
            _, tag = Tag.create(name=tag_name)
            need_add_tag_ids.add(tag.id)
        for tag_name in need_del:
            _, tag = Tag.create(name=tag_name)
            need_del_tag_ids.add(tag.id)

        if need_del_tag_ids:
            obj = cls.query.filter(cls.post_id == post_id,
                                   cls.tag_id.in_(need_del_tag_ids))
            obj.delete(
                synchronize_session='fetch'
            )  #  `fetch`: perform a select query, and matched objects are removed from session.
        for tag_id in need_add_tag_ids:
            cls.create(post_id=post_id, tag_id=tag_id)
        db.session.commit()

    # Since `@staticmethod` is ignorant of the base class it is `attached` to, so below methods all need clear arguments.
    # super(PostTag, target)
    # Note: `@classmethod` can directly use `super()`
    @staticmethod
    def _flush_insert_event(mapper, connection, target):
        super(PostTag, target)._flush_insert_event(mapper, connection, target)
        target.clear_mc(target, 1)

    @staticmethod
    def _flush_delete_event(mapper, connection, target):
        super(PostTag, target)._flush_delete_event(mapper, connection, target)
        target.clear_mc(target, -1)

    @staticmethod
    def _flush_after_update_event(mapper, connection, target):
        super(PostTag,
              target)._flush_after_update_event(mapper, connection, target)
        target.clear_mc(target, 1)

    @staticmethod
    def _flush_before_update_event(mapper, connection, target):
        super(PostTag,
              target)._flush_before_update_event(mapper, connection, target)
        target.clear_mc(target, -1)

    @staticmethod
    def clear_mc(target, amount):
        tag_name = Tag.get(target.tag_id).name
        stat_key = MC_KEY_POST_STATS_BY_TAG % tag_name
        total = int(PostTag.get_count_by_tag(tag_name))
        try:
            rdb.incr(stat_key, amount)
        except redis.exceptions.ResponseError:
            rdb.delete(stat_key)
            rdb.incr(stat_key, amount)
        pages = math.ceil((max(total, 0) or 1) / PER_PAGE)
        for p in range(1, pages + 1):
            rdb.delete(MC_KEY_POSTS_BY_TAG % (tag_name, p))
Пример #19
0
class Post(BaseMixin, CommentMixin, LikeMixin, CollectMixin, db.Model):
    __tablename__ = 'posts'
    author_id = db.Column(db.Integer)
    title = db.Column(db.String(128), default='')
    orig_url = db.Column(db.String(255), default='')
    can_comment = db.Column(db.Boolean, default=True)
    content = PropsItem(
        'content', ''
    )  # key-value db field, since `content` is only stored, normally not need search/filter/query...
    kind = K_POST

    __table_args__ = (db.Index('idx_title',
                               title), db.Index('idx_authorId', author_id))

    def url(self):
        return '/{}/{}/'.format(self.__class__.__name__.lower(), self.id)

    @classmethod
    def __flush_event__(cls, target):
        rdb.delete(MC_KEY_ALL_TAGS)

    @classmethod
    def get(cls, identifier):
        if is_numeric(identifier):
            return cls.cache.get(identifier)  # get post via title
        return cls.cache.filter(title=identifier).first()

    @property
    @cache(MC_KEY_POST_TAGS % ('{self.id}'))
    def tags(self):
        at_ids = PostTag.query.with_entities(
            PostTag.tag_id).filter(PostTag.post_id == self.id).all()

        tags = Tag.query.filter(Tag.id.in_((id for id, in at_ids))).all()
        return tags

    @cached_hybrid_property
    def abstract_content(self):
        return trunc_utf8(self.content, 100)

    @cached_hybrid_property
    def author(self):
        return User.get(self.author_id)

    @classmethod
    def create_or_update(cls, **kwargs):
        # not default `create_or_update' in BaseMixin
        tags = kwargs.pop('tags', [])
        created, obj = super(Post, cls).create_or_update(**kwargs)
        if tags:
            PostTag.update_multi(obj.id, tags, [])

        if created:
            from handler.tasks import feed_post, reindex
            reindex.delay(obj.id, obj.kind, op_type='create')
            feed_post.delay(obj.id)
        return created, obj

    def delete(self):
        id = self.id
        super().delete()  # defined in BaseMixin
        for pt in PostTag.query.filter_by(post_id=id):
            pt.delete()

        from handler.tasks import remove_post_from_feed
        remove_post_from_feed.delay(self.id, self.author_id)

    @cached_hybrid_property
    def netloc(self):
        return '{0.scheme}://{0.netloc}'.format(urlparse(self.orig_url))

    @staticmethod
    def _flush_insert_event(mapper, connection, target):
        target._flush_event(mapper, connection, target)
        target.__flush_insert_event__(target)
Пример #20
0
class PostTag(BaseMixin, db.Model):
    __tablename__ = "post_tags"
    post_id = db.Column(db.Integer)
    tag_id = db.Column(db.Integer)

    __table_args__ = (
        db.Index("idx_post_id", post_id, "updated_at"),
        db.Index("idx_tag_id", tag_id, "updated_at"),
    )

    @classmethod
    def _get_post_by_tag(cls, identifier):
        if not identifier:
            return []
        if not is_numeric(identifier):
            tag = Tag.get_by_name(identifier)
            if not tag:
                return []
            identifier = tag.id
        at_ids = (
            cls.query.with_entities(cls.post_id).filter(cls.tag_id == identifier).all()
        )
        query = Post.query.filter(Post.id.in_(id for id, in at_ids)).order_by(
            Post.id.desc()
        )
        return query

    @classmethod
    @cache(MC_KEY_POSTS_BY_TAG.format("{identifier}", "{page}"))
    def get_post_by_tag(cls, identifier, page=1):
        query = cls._get_post_by_tag(identifier)
        posts = query.paginate(page, PER_PAGE)
        del posts.query  # Fix `TypeError: can't pickle _thread.lock objects`
        return posts

    @classmethod
    @cache(MC_KEY_POST_COUNT_BY_TAG.format("{identifier}"))
    def get_count_by_tag(cls, identifier):
        query = cls._get_post_by_tag(identifier)
        return query.count()

    @classmethod
    def update_multi(cls, post_id, tags):
        origin_tags = Post.get(post_id).tags
        need_add = set()
        need_del = set()
        for tag in tags:
            if tag not in origin_tags:
                need_add.add(tag)
        for tag in origin_tags:
            if tag not in tags:
                need_del.add(tag)
        need_add_tag_ids = set()
        need_del_tag_ids = set()
        for tag_name in need_add:
            _, tag = Tag.create(name=tag_name)
            need_add_tag_ids.add(tag.id)
        for tag_name in need_del:
            _, tag = Tag.create(name=tag_name)
            need_del_tag_ids.add(tag.id)

        if need_del_tag_ids:
            obj = cls.query.filter(
                cls.post_id == post_id, cls.tag_id.in_(need_del_tag_ids)
            )
            # 更新之前进行查询,获取最新的更新对象
            obj.delete(synchronize_session="fetch")
        for tag_id in need_add_tag_ids:
            cls.create(post_id=post_id, tag_id=tag_id)
        db.session.commit()

    @staticmethod
    def clear_mc(target, amount):
        post_id = target.post_id
        tag_name = Tag.get(target.tag_id).name
        for ident in (post_id, tag_name):
            total = int(PostTag.get_count_by_tag(ident))
            try:
                rdb.incr(MC_KEY_POST_COUNT_BY_TAG.format(ident), amount)
            except redis.exceptions.ResponseError:
                rdb.delete(MC_KEY_POST_COUNT_BY_TAG.format(ident))
                rdb.incr(MC_KEY_POST_COUNT_BY_TAG.format(ident), amount)
            pages = math.ceil((max(total, 0) or 1) / PER_PAGE)
            for p in range(1, pages + 1):
                rdb.delete(MC_KEY_POSTS_BY_TAG.format(ident, p))

    @staticmethod
    def _flush_insert_event(mapper, connection, target):
        super(PostTag, target)._flush_insert_event(mapper, connection, target)
        target.clear_mc(target, 1)

    @staticmethod
    def _flush_before_update_event(mapper, connection, target):
        super(PostTag, target)._flush_before_update_event(mapper, connection, target)
        target.clear_mc(target, -1)

    @staticmethod
    def _flush_after_update_event(mapper, connection, target):
        super(PostTag, target)._flush_after_update_event(mapper, connection, target)
        target.clear_mc(target, 1)

    @staticmethod
    def _flush_delete_event(mapper, connection, target):
        super(PostTag, target)._flush_delete_event(mapper, connection, target)
        target.clear_mc(target, -1)