class Article(UUIDMixin, TimestampMixin, ModelBase): """Article Model""" __tablename__ = "articles" excludes = ["content", "user_id", "category_id", "source_id"] title = db.Column(db.String(255), nullable=False) content = deferred(db.Column(db.Text, nullable=False)) summary = db.Column(db.String(255), nullable=True) stars = db.Column(db.Integer, default=0) views = db.Column(db.Integer, default=0) published = db.Column(db.Boolean, default=False) published_at = db.Column(db.TIMESTAMP, nullable=True) protected = db.Column(db.Boolean, default=False) user_id = db.Column(db.String(64), db.ForeignKey("users.id", ondelete="SET NULL")) category_id = db.Column( db.String(64), db.ForeignKey("categories.id", ondelete="SET NULL")) source_id = db.Column(db.String(64), db.ForeignKey("sources.id", ondelete="SET NULL")) # tags = db.relationship("Tag", secondary=article_tag_mapping, # backref="articles", lazy="dynamic") comments = db.relationship("Comment", backref=db.backref("article", lazy="subquery"), lazy="dynamic") def get_tags(self): """Get article tags order by associate time asc""" return self.tags.order_by(article_tag_mapping.c.created_at.asc()).all()
class Comment(UUIDMixin, TimestampMixin, ModelBase): """Article Comment Model""" __tablename__ = "comments" article_id = db.Column(db.String(64), db.ForeignKey("articles.id", ondelete="CASCADE")) content = db.Column(db.Text, nullable=False) user_id = db.Column(db.String(64), db.ForeignKey("users.id", ondelete="SET NULL"))
class LocalUser(IdMixin, TimestampMixin, ModelBase): """Local User Model""" __tablename__ = "local_users" excludes = ["id", "user_id", "password"] user_id = db.Column(db.String(64), db.ForeignKey("users.id", ondelete="CASCADE"), unique=True) name = db.Column(db.String(255), unique=True, nullable=False) display_name = db.Column(db.String(255), nullable=True) email = db.Column(db.String(255), unique=True, nullable=True) password = db.Column(db.String(128), nullable=False)
class FederatedUser(IdMixin, TimestampMixin, ModelBase): """Federated User Model Use for third party auth! """ __tablename__ = "federated_users" excludes = ["id", "user_id"] user_id = db.Column(db.String(64), db.ForeignKey("users.id", ondelete="CASCADE")) idp_name = db.Column(db.String(64), nullable=False) protocol = db.Column(db.String(64), nullable=False) unique_id = db.Column(db.String(255), nullable=False) display_name = db.Column(db.String(255), nullable=True)
class Role(UUIDMixin, ModelBase): """User Role model Use for RBAC(Role-Based Access Control)! """ __tablename__ = "roles" name = db.Column(db.String(64), unique=True, nullable=False) description = db.Column(db.Text, nullable=True) users = db.relationship("User", backref="role", lazy="dynamic") # role constants ADMIN = "admin" USER = "******" @classmethod def insert_default_values(cls): for attr in dir(cls): if attr.startswith('_') or not attr.isupper(): continue name = getattr(cls, attr, None) if not isinstance(name, str): continue role = Role(name=name, description=attr) db.session.add(role) db.session.commit()
class Tag(UUIDMixin, TimestampMixin, ModelBase): """Article Tag Model""" __tablename__ = "tags" name = db.Column(db.String(64), unique=True, nullable=False) articles = db.relationship("Article", secondary=article_tag_mapping, backref=db.backref("tags", lazy="dynamic"), lazy="dynamic")
class Source(UUIDMixin, ModelBase): """Article Source Model""" __tablename__ = "sources" name = db.Column(db.String(32), nullable=False) articles = db.relationship("Article", backref=db.backref("source", uselist=False), lazy="dynamic") @staticmethod def insert_default_values(): names = ("原创", "转载", "翻译") for name in names: source = Source(name=name) db.session.add(source) db.session.commit()
class Category(UUIDMixin, TimestampMixin, ModelBase): """Article Category Model""" __tablename__ = "categories" name = db.Column(db.String(255), unique=True, nullable=False) description = db.Column(db.Text, nullable=True) display_order = db.Column(db.Integer, default=lambda: Category.get_default_order()) protected = db.Column(db.Boolean, default=False) parent_id = db.Column(db.String(64), db.ForeignKey("categories.id", ondelete="CASCADE")) children = db.relationship("Category", backref=db.backref("parent", remote_side="Category.id"), cascade="all,delete", lazy="dynamic") articles = db.relationship("Article", backref="category", lazy="dynamic") @classmethod def get_max_order(cls, parent_id=None): """Returns max display order""" return db.session.query(func.max(cls.display_order)).\ filter_by(parent_id=parent_id).scalar() @classmethod def get_min_order(cls, parent_id=None): """Returns min display order""" return db.session.query(func.min(cls.display_order)).\ filter_by(parent_id=parent_id).scalar() @classmethod def get_default_order(cls, parent_id=None, method="max"): """Returns default display order""" def _max(): max_order = cls.get_max_order(parent_id) return 1 if max_order is None else max_order + 1 def _min(): min_order = cls.get_min_order(parent_id) return 1 if min_order is None else min_order - 1 def _random(): min_order, max_order = db.session.query( func.min(cls.display_order), func.max(cls.display_order)).\ filter_by(parent_id=parent_id).one() return 1 if min_order is None else \ random.randint(min_order - 1, max_order + 1) mapper = { "max": _max, "min": _min, "random": _random, } generator = mapper.get(method) return generator() if generator else None @staticmethod def insert_default_values(): category = Category(name="database", description="database related", display_order=0) db.session.add(category) db.session.commit()
class UUIDMixin(object): """UUID Mixin""" id = db.Column(db.String(64), primary_key=True, default=_get_uuid)
return generator() if generator else None @staticmethod def insert_default_values(): category = Category(name="database", description="database related", display_order=0) db.session.add(category) db.session.commit() # Multiple-Multiple-Mapping Table: associate articles with tags article_tag_mapping = db.Table( "article_tag_mapping", ModelBase.metadata, db.Column("id", db.String(64), primary_key=True, default=_get_uuid), db.Column("article_id", db.String(64), db.ForeignKey("articles.id", ondelete="CASCADE")), db.Column("tag_id", db.String(64), db.ForeignKey("tags.id", ondelete="CASCADE")), db.Column("created_at", db.TIMESTAMP, default=datetime.datetime.utcnow), db.UniqueConstraint("article_id", "tag_id", name="article_tag_unique"), ) class Tag(UUIDMixin, TimestampMixin, ModelBase): """Article Tag Model""" __tablename__ = "tags" name = db.Column(db.String(64), unique=True, nullable=False)
class User(UUIDMixin, ModelBase): """User Model""" __tablename__ = "users" excludes = ["role_id"] role_id = db.Column(db.String(64), db.ForeignKey("roles.id", ondelete="CASCADE")) enabled = db.Column(db.Boolean, default=True) local_user = db.relationship( "LocalUser", uselist=False, lazy="subquery", cascade="all,delete-orphan", backref="user", ) federated_users = db.relationship( "FederatedUser", single_parent=True, lazy="subquery", cascade="all,delete-orphan", backref="user", ) articles = db.relationship("Article", lazy="dynamic", backref="user") def __init__(self, **kwargs): for arg in ["name", "display_name", "email", "password"]: val = kwargs.get(arg) if val is not None: setattr(self, arg, val) role_id = kwargs.get("role_id") role = Role.query.get(role_id) if role_id else None if not role: role = Role.query.filter_by(name=Role.USER).first() self.role = role self.enabled = kwargs.get("enabled", True) @hybrid_property def name(self): if self.local_user: return self.local_user.name elif self.federated_users: return self.federated_users[0].unique_id else: return None @name.setter def name(self, value): self.local_user = self.local_user or LocalUser() # TODO: local user name can only set once self.local_user.name = value @hybrid_property def display_name(self): if self.local_user: return self.local_user.display_name or self.local_user.name elif self.federated_users: return self.federated_users[0].display_name else: return None @display_name.setter def display_name(self, value): self.local_user = self.local_user or LocalUser() self.local_user.display_name = value @hybrid_property def email(self): return self.local_user.email if self.local_user else None @email.setter def email(self, value): # TODO: email validate self.local_user = self.local_user or LocalUser() self.local_user.email = value @hybrid_property def password(self): return self.local_user.password if self.local_user else None @password.setter def password(self, value): self.local_user = self.local_user or LocalUser() self.local_user.password = generate_password_hash(value) def check_password(self, password): """Check that a plaintext password matches hashed.""" if self.password is None or password is None: return False return check_password_hash(self.password, password) @staticmethod def get(user_id=None, name_email=None): """Get user by id or name/email""" assert any((user_id, name_email)) if user_id: return User.query.get(user_id) elif name_email: local_user = LocalUser.query.join(LocalUser.user).filter( db.or_(LocalUser.name == name_email, LocalUser.email == name_email)).first() return local_user.user if local_user else None else: return None def generate_auth_token(self): """Generate a `JWS` token for user""" data = dict(id=self.id) return jws.encode(data) @staticmethod def verify_auth_token(token): """Verify auth token and returns a user object.""" try: data = jws.decode(token) except Exception: LOG.exception("Verify auth token {!r} failed".format(token)) return None else: if data is None or data.get("id") is None: return None else: return User.query.get(data["id"]) @staticmethod def insert_default_values(): users = [ { "name": "admin", "password": "******", "email": "*****@*****.**", "role": Role.ADMIN }, { "name": "common", "password": "******", "email": "*****@*****.**", "role": Role.USER }, ] for info in users: local_user = LocalUser.query.filter_by(name=info["name"]).first() if local_user: continue user = User(enabled=True) user.role = Role.query.filter_by(name=info.pop("role")).first() user.name = info["name"] user.password = info["password"] user.email = info["email"] db.session.add(user) db.session.commit()