"""SQLAlchemy models for OPS Resource Portal.""" from datetime import datetime, timezone from extensions import db def utcnow(): return datetime.now(timezone.utc) # --------------------------------------------------------------------------- # Association table: resource <-> option (faceted tagging) # --------------------------------------------------------------------------- resource_options = db.Table( "resource_options", db.Column("resource_id", db.Integer, db.ForeignKey("resources.id", ondelete="CASCADE"), primary_key=True), db.Column("option_id", db.Integer, db.ForeignKey("options.id", ondelete="CASCADE"), primary_key=True), ) class User(db.Model): __tablename__ = "users" id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(80), unique=True, nullable=False) password = db.Column(db.String(128), nullable=False) # bcrypt hash created_at = db.Column(db.DateTime, default=utcnow) def set_password(self, raw): import bcrypt self.password = bcrypt.hashpw(raw.encode("utf-8"), bcrypt.gensalt()).decode("utf-8") def check_password(self, raw): import bcrypt try: return bcrypt.checkpw(raw.encode("utf-8"), self.password.encode("utf-8")) except Exception: return False class SiteSetting(db.Model): """Key-value site configuration editable from the admin panel.""" __tablename__ = "site_settings" key = db.Column(db.String(64), primary_key=True) value = db.Column(db.Text, default="") updated_at = db.Column(db.DateTime, default=utcnow, onupdate=utcnow) class Category(db.Model): """Left-nav menu entry, e.g. 基础设备 / pypi源.""" __tablename__ = "categories" id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(80), nullable=False) icon = db.Column(db.String(16), default="📁") # emoji icon description = db.Column(db.String(255), default="") sort_order = db.Column(db.Integer, default=0) is_active = db.Column(db.Boolean, default=True) created_at = db.Column(db.DateTime, default=utcnow) rows = db.relationship("OptionRow", backref="category", lazy=True, cascade="all, delete-orphan", order_by="OptionRow.sort_order") resources = db.relationship("Resource", backref="category", lazy=True, cascade="all, delete-orphan") def to_dict(self, with_rows=True): d = { "id": self.id, "name": self.name, "icon": self.icon, "description": self.description, "sort_order": self.sort_order, "is_active": self.is_active, "resource_count": len(self.resources), } if with_rows: d["rows"] = [r.to_dict() for r in self.rows] return d class OptionRow(db.Model): """A selection row inside a category, e.g. 运行环境 / 操作系统.""" __tablename__ = "option_rows" 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(80), nullable=False) # e.g. 运行环境 sort_order = db.Column(db.Integer, default=0) options = db.relationship("Option", backref="row", lazy=True, cascade="all, delete-orphan", order_by="Option.sort_order") def to_dict(self): return { "id": self.id, "title": self.title, "sort_order": self.sort_order, "options": [o.to_dict() for o in self.options], } class Option(db.Model): """A selectable tile inside a row, e.g. 物理机 / ubuntu.""" __tablename__ = "options" id = db.Column(db.Integer, primary_key=True) row_id = db.Column(db.Integer, db.ForeignKey("option_rows.id", ondelete="CASCADE"), nullable=False) label = db.Column(db.String(80), nullable=False) icon = db.Column(db.String(16), default="") sort_order = db.Column(db.Integer, default=0) def to_dict(self): return {"id": self.id, "label": self.label, "icon": self.icon, "sort_order": self.sort_order} class Resource(db.Model): """A published internal resource: command / link / text / file.""" __tablename__ = "resources" 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(160), nullable=False) type = db.Column(db.String(16), nullable=False, default="command") # command|link|text|file content = db.Column(db.Text, default="") # command text / rich text / note url = db.Column(db.String(500), default="") # link target / file URL description = db.Column(db.String(255), default="") sort_order = db.Column(db.Integer, default=0) is_active = db.Column(db.Boolean, default=True) created_at = db.Column(db.DateTime, default=utcnow) updated_at = db.Column(db.DateTime, default=utcnow, onupdate=utcnow) options = db.relationship("Option", secondary=resource_options, lazy="subquery", backref=db.backref("resources", lazy=True)) def to_dict(self): return { "id": self.id, "category_id": self.category_id, "title": self.title, "type": self.type, "content": self.content, "url": self.url, "description": self.description, "sort_order": self.sort_order, "is_active": self.is_active, "option_ids": sorted(o.id for o in self.options), "created_at": self.created_at.strftime("%Y-%m-%d %H:%M") if self.created_at else "", "updated_at": self.updated_at.strftime("%Y-%m-%d %H:%M") if self.updated_at else "", } class SideLink(db.Model): """Right-panel 其他链接 entry.""" __tablename__ = "side_links" id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(160), nullable=False) url = db.Column(db.String(500), nullable=False) group_name = db.Column(db.String(80), default="其他链接") sort_order = db.Column(db.Integer, default=0) is_active = db.Column(db.Boolean, default=True) created_at = db.Column(db.DateTime, default=utcnow) def to_dict(self): return { "id": self.id, "title": self.title, "url": self.url, "group_name": self.group_name, "sort_order": self.sort_order, "is_active": self.is_active, }