You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
iTi-System/iti_system/models/__init__.py

343 lines
13 KiB
Python

from __future__ import annotations
from datetime import datetime
from typing import Any
from passlib.context import CryptContext
from sqlalchemy import (
JSON,
BigInteger,
Boolean,
Column,
DateTime,
Float,
ForeignKey,
Integer,
String,
Table,
Text,
UniqueConstraint,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from iti.db import AuditMixin, Base, IdMixin, TimestampMixin
from iti_system.enums import GenderEnum, LogType, MenuTypeEnum, StatusEnum
pwd_context = CryptContext(schemes=["argon2"], deprecated="auto")
sys_user_role = Table(
"sys_user_role",
Base.metadata,
Column("user_id", String(36), ForeignKey("sys_user.id", ondelete="CASCADE"), primary_key=True),
Column("role_id", String(36), ForeignKey("sys_role.id", ondelete="CASCADE"), primary_key=True),
)
sys_role_menu = Table(
"sys_role_menu",
Base.metadata,
Column("role_id", String(36), ForeignKey("sys_role.id", ondelete="CASCADE"), primary_key=True),
Column("menu_id", String(36), ForeignKey("sys_menu.id", ondelete="CASCADE"), primary_key=True),
)
sys_user_dept = Table(
"sys_user_dept",
Base.metadata,
Column("user_id", String(36), ForeignKey("sys_user.id", ondelete="CASCADE"), primary_key=True),
Column("dept_id", String(36), ForeignKey("sys_dept.id", ondelete="CASCADE"), primary_key=True),
)
class User(IdMixin, TimestampMixin, AuditMixin, Base):
__tablename__ = "sys_user"
username: Mapped[str] = mapped_column(String(64), unique=True, index=True)
phone: Mapped[str | None] = mapped_column(String(32), unique=True, nullable=True)
email: Mapped[str | None] = mapped_column(String(255), unique=True, nullable=True)
password_hash: Mapped[str] = mapped_column(String(255))
realname: Mapped[str | None] = mapped_column(String(64), nullable=True)
desc: Mapped[str | None] = mapped_column(Text, nullable=True)
avatar: Mapped[str | None] = mapped_column(String(512), nullable=True)
gender: Mapped[str] = mapped_column(String(16), default=GenderEnum.SECURE.value)
status: Mapped[str] = mapped_column(String(16), default=StatusEnum.ENABLED.value)
roles: Mapped[list["Role"]] = relationship(
secondary=sys_user_role,
back_populates="users",
lazy="selectin",
)
depts: Mapped[list["SysDept"]] = relationship(
secondary=sys_user_dept,
back_populates="users",
lazy="selectin",
)
attributes: Mapped[list["SysUserAttribute"]] = relationship(
back_populates="user",
lazy="selectin",
cascade="all, delete-orphan",
)
def set_password(self, value: str) -> None:
self.password_hash = pwd_context.hash(value)
def check_password(self, value: str) -> bool:
return pwd_context.verify(value, self.password_hash)
@property
def permissions(self) -> list[str]:
codes: set[str] = set()
for role in self.roles:
for menu in role.menus:
if menu.status == StatusEnum.ENABLED.value and menu.auth_code:
codes.add(menu.auth_code)
return sorted(codes)
@property
def role_codes(self) -> list[str]:
return [role.code for role in self.roles]
@property
def dept_ids(self) -> list[str]:
return [dept.id for dept in self.depts]
@property
def attribute_map(self) -> dict[str, dict[str, Any]]:
result: dict[str, dict[str, Any]] = {}
for item in self.attributes:
result.setdefault(item.attr_group, {})[item.attr_key] = item.typed_value
return result
class Role(IdMixin, TimestampMixin, AuditMixin, Base):
__tablename__ = "sys_role"
name: Mapped[str] = mapped_column(String(64))
code: Mapped[str] = mapped_column(String(64), unique=True, index=True)
desc: Mapped[str | None] = mapped_column(Text, nullable=True)
sort: Mapped[int] = mapped_column(Integer, default=0)
status: Mapped[str] = mapped_column(String(16), default=StatusEnum.ENABLED.value)
users: Mapped[list[User]] = relationship(
secondary=sys_user_role,
back_populates="roles",
lazy="selectin",
)
menus: Mapped[list["SysMenu"]] = relationship(
secondary=sys_role_menu,
back_populates="roles",
lazy="selectin",
)
@property
def permissions(self) -> list[str]:
return [menu.id for menu in self.menus]
class SysMenu(IdMixin, TimestampMixin, AuditMixin, Base):
__tablename__ = "sys_menu"
name: Mapped[str] = mapped_column(String(255), unique=True)
type: Mapped[str] = mapped_column(String(32), default=MenuTypeEnum.MENU.value)
path: Mapped[str | None] = mapped_column(String(255), nullable=True)
component: Mapped[str | None] = mapped_column(String(255), nullable=True)
redirect: Mapped[str | None] = mapped_column(String(255), nullable=True)
sort: Mapped[int] = mapped_column(Integer, default=0)
auth_code: Mapped[str | None] = mapped_column(String(128), index=True, nullable=True)
meta: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True)
status: Mapped[str] = mapped_column(String(16), default=StatusEnum.ENABLED.value)
parent_id: Mapped[str | None] = mapped_column(
String(36),
ForeignKey("sys_menu.id", ondelete="SET NULL"),
nullable=True,
)
parent: Mapped["SysMenu | None"] = relationship(remote_side="SysMenu.id", back_populates="children")
children: Mapped[list["SysMenu"]] = relationship(back_populates="parent", lazy="selectin")
roles: Mapped[list[Role]] = relationship(
secondary=sys_role_menu,
back_populates="menus",
lazy="selectin",
)
class SysDept(IdMixin, TimestampMixin, AuditMixin, Base):
__tablename__ = "sys_dept"
name: Mapped[str] = mapped_column(String(255))
parent_id: Mapped[str | None] = mapped_column(
String(36),
ForeignKey("sys_dept.id", ondelete="SET NULL"),
nullable=True,
)
desc: Mapped[str | None] = mapped_column(Text, nullable=True)
sort: Mapped[int] = mapped_column(Integer, default=0)
leader_id: Mapped[str | None] = mapped_column(String(36), nullable=True)
status: Mapped[str] = mapped_column(String(16), default=StatusEnum.ENABLED.value)
parent: Mapped["SysDept | None"] = relationship(remote_side="SysDept.id", back_populates="children")
children: Mapped[list["SysDept"]] = relationship(back_populates="parent", lazy="selectin")
users: Mapped[list[User]] = relationship(
secondary=sys_user_dept,
back_populates="depts",
lazy="selectin",
)
class SysConfig(IdMixin, TimestampMixin, AuditMixin, Base):
__tablename__ = "sys_config"
__table_args__ = (UniqueConstraint("type", "code", name="uk_sys_config_type_code"),)
type: Mapped[str] = mapped_column(String(64), default="SYSTEM")
name: Mapped[str] = mapped_column(String(255))
code: Mapped[str] = mapped_column(String(128), index=True)
value: Mapped[str | None] = mapped_column(Text, nullable=True)
desc: Mapped[str | None] = mapped_column(Text, nullable=True)
sort: Mapped[int] = mapped_column(Integer, default=0)
status: Mapped[str] = mapped_column(String(16), default=StatusEnum.ENABLED.value)
class SysDictType(IdMixin, TimestampMixin, AuditMixin, Base):
__tablename__ = "sys_dict_type"
type_name: Mapped[str] = mapped_column(String(255))
type_code: Mapped[str] = mapped_column(String(128), unique=True, index=True)
desc: Mapped[str | None] = mapped_column(Text, nullable=True)
sort: Mapped[int] = mapped_column(Integer, default=0)
status: Mapped[str] = mapped_column(String(16), default=StatusEnum.ENABLED.value)
data_list: Mapped[list["SysDictData"]] = relationship(
back_populates="type",
lazy="selectin",
cascade="all, delete-orphan",
primaryjoin="SysDictType.type_code == foreign(SysDictData.type_code)",
)
class SysDictData(IdMixin, TimestampMixin, AuditMixin, Base):
__tablename__ = "sys_dict_data"
__table_args__ = (UniqueConstraint("type_code", "code", name="uk_sys_dict_data_type_code_code"),)
type_code: Mapped[str] = mapped_column(String(128), index=True)
label: Mapped[str] = mapped_column(String(255))
code: Mapped[str] = mapped_column(String(128))
value: Mapped[str | None] = mapped_column(Text, nullable=True)
desc: Mapped[str | None] = mapped_column(Text, nullable=True)
sort: Mapped[int] = mapped_column(Integer, default=0)
status: Mapped[str] = mapped_column(String(16), default=StatusEnum.ENABLED.value)
type: Mapped[SysDictType | None] = relationship(
back_populates="data_list",
primaryjoin="foreign(SysDictData.type_code) == SysDictType.type_code",
)
class SysFile(IdMixin, TimestampMixin, AuditMixin, Base):
__tablename__ = "sys_file"
filename: Mapped[str] = mapped_column(String(255))
file_key: Mapped[str] = mapped_column(String(512), unique=True, index=True)
file_hash: Mapped[str | None] = mapped_column(String(128), index=True, nullable=True)
mime_type: Mapped[str | None] = mapped_column(String(128), nullable=True)
file_size: Mapped[int] = mapped_column(BigInteger, default=0)
extension: Mapped[str | None] = mapped_column(String(32), nullable=True)
storage_type: Mapped[str] = mapped_column(String(32), default="local")
storage_info: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True)
directory_id: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True)
metadata_: Mapped[dict[str, Any] | None] = mapped_column("metadata", JSON, nullable=True)
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
deleted_by: Mapped[str | None] = mapped_column(String(36), nullable=True)
share_code: Mapped[str | None] = mapped_column(String(64), unique=True, nullable=True)
share_password: Mapped[str | None] = mapped_column(String(64), nullable=True)
share_expire_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
share_count: Mapped[int] = mapped_column(Integer, default=0)
status: Mapped[str] = mapped_column(String(16), default=StatusEnum.ENABLED.value)
class SysLog(IdMixin, TimestampMixin, Base):
__tablename__ = "sys_log"
name: Mapped[str | None] = mapped_column(String(100), nullable=True)
method: Mapped[str | None] = mapped_column(String(10), nullable=True)
user_id: Mapped[str | None] = mapped_column(String(36), index=True, nullable=True)
path: Mapped[str | None] = mapped_column(String(255), nullable=True)
ip: Mapped[str | None] = mapped_column(String(255), nullable=True)
user_agent: Mapped[str | None] = mapped_column(Text, nullable=True)
headers: Mapped[str | None] = mapped_column(Text, nullable=True)
query_params: Mapped[str | None] = mapped_column(Text, nullable=True)
body_params: Mapped[str | None] = mapped_column(Text, nullable=True)
execution_time: Mapped[float | None] = mapped_column(Float, nullable=True)
response: Mapped[str | None] = mapped_column(Text, nullable=True)
exception: Mapped[str | None] = mapped_column(Text, nullable=True)
success: Mapped[bool | None] = mapped_column(Boolean, nullable=True)
desc: Mapped[str | None] = mapped_column(Text, nullable=True)
type: Mapped[str] = mapped_column(String(32), default=LogType.OPERATION.value)
class SysUserAttribute(IdMixin, TimestampMixin, AuditMixin, Base):
__tablename__ = "sys_user_attribute"
__table_args__ = (
UniqueConstraint("user_id", "attr_group", "attr_key", name="uk_user_group_key"),
)
user_id: Mapped[str] = mapped_column(String(36), ForeignKey("sys_user.id", ondelete="CASCADE"), index=True)
attr_group: Mapped[str] = mapped_column(String(64), index=True)
attr_key: Mapped[str] = mapped_column(String(128))
attr_value: Mapped[str | None] = mapped_column(Text, nullable=True)
attr_type: Mapped[str] = mapped_column(String(32), default="string")
description: Mapped[str | None] = mapped_column(String(255), nullable=True)
sort: Mapped[int] = mapped_column(Integer, default=0)
user: Mapped[User] = relationship(back_populates="attributes")
@property
def typed_value(self) -> Any:
if self.attr_value is None:
return None
if self.attr_type == "int":
return int(self.attr_value)
if self.attr_type == "float":
return float(self.attr_value)
if self.attr_type == "bool":
return self.attr_value.lower() in {"true", "1", "yes", "on"}
if self.attr_type == "json":
import json
return json.loads(self.attr_value)
if self.attr_type == "encrypted":
return "******"
return self.attr_value
def set_typed_value(self, value: Any) -> None:
if value is None:
self.attr_value = None
elif self.attr_type == "json":
import json
self.attr_value = json.dumps(value, ensure_ascii=False)
elif self.attr_type == "bool":
self.attr_value = "true" if value else "false"
else:
self.attr_value = str(value)
__all__ = [
"Role",
"SysConfig",
"SysDept",
"SysDictData",
"SysDictType",
"SysFile",
"SysLog",
"SysMenu",
"SysUserAttribute",
"User",
"sys_role_menu",
"sys_user_dept",
"sys_user_role",
]