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()
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))
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()
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)
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()
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)
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), )
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), )
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), )
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)
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)
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)
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)
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))
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))
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)
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
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))
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)
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)