utils.models.descriptor 源代码

'''接口描述符

本模块实现模型接口描述符,用于标记模型属性、方法的性质,并提供处理方法。

Note:
- 私密性顺序:公开(前端可用) > 前端不可用 > 仅管理员可用(后端相同) > 仅用于调试(命令行)
- 接口除字符串方法外均默认公开,即前端可用,后端可用,但可以通过标记接口为私有。
- 字符串方法应为管理员可用
'''
from typing import Callable, TypeVar, overload, Any

from django.db.models import Field, Model


__all__ = [
    # 描述符
    'necessary_for_frontend',
    'invalid_for_frontend',
    'admin_only',
    'debug_only',
    # 处理方法
    'export_to_frontend',
]

T = TypeVar('T')

@overload
def necessary_for_frontend(*fields: str | Field) -> Callable[[T], T]: ...
@overload
def necessary_for_frontend(method: T) -> T: ...

[文档] def necessary_for_frontend(method: T | str | Field, *fields: str | Field): '''前端必须使用此方法代替直接访问相关属性,如限制choice的属性,可以在参数中标记相关字段''' if isinstance(method, (str, Field)): def _wrapper(*args): return necessary_for_frontend(*args, method, *fields) return _wrapper method.frontend_available = True # type: ignore method.frontend_cover_fields = fields # type: ignore return method
[文档] def invalid_for_frontend(method: T) -> T: '''前端不能使用这个方法''' method.frontend_available = False # type: ignore return method
[文档] def admin_only(method: T) -> T: '''仅管理员用户可用,如后台、管理员操作的其他页面等,必须对用户不可见''' method.frontend_available = 'admin' # type: ignore return method
[文档] def debug_only(method: T) -> T: '''仅用于提供调试信息,如报错、后台、日志记录等,必须对用户不可见''' method.frontend_available = 'debug' # type: ignore return method
def _data_object(datas: dict) -> object: '''将字典转换为对象,用于导出数据,支持__str__方法''' class Data: # str调用type(self).__str__,而不是getattr(self, '__str__'),需要重写 __str__ = lambda self: datas['__str__']() if '__str__' in datas else '***' __repr__ = lambda self: self.__str__() data = Data() for k, v in datas.items(): setattr(data, k, v) return data
[文档] def export_to_frontend( instance: Model | Any, *, keep_fields: bool = False, recursive: bool = False, ) -> object: '''将模型实例导出为前端可用的数据结构,仅保留前端允许使用的接口 Args: - instance: 模型实例 Keyword Args: - keep_fields: 是否默认保留字段数据,若为True,则默认保留,否则仅保留方法和属性 - recursive: 是否递归导出,若为True,则关联字段也会被导出,否则仅保留原始模型实例 Returns: - frontend_data: 前端可用的数据结构,移除被标记应替代或删除的字段,后端请勿使用 Warning: - 会访问模型实例的所有属性,关联字段因此会被查询,可能导致性能问题 - 仅支持导出模型实例,不支持导出QuerySet ''' if not isinstance(instance, Model): return instance datas = {} cover_fields = set() for name in dir(instance): if name.startswith('_') and name != '__str__': continue value = getattr(instance, name) if getattr(value, 'frontend_cover_fields', None) is not None: fields: tuple[str | Field, ...] = value.frontend_cover_fields fields = tuple(f if isinstance(f, str) else f.attname for f in fields) cover_fields.update(fields) available = getattr(value, 'frontend_available', None) match available: case None: # 非私有方法、属性、字段(参数允许时),默认均可用 available = not name.startswith('_') and ( callable(value) or isinstance(value, property) or keep_fields ) case 'admin': # 仅管理员可用的方法,在前端暂无需求,不导出 continue case 'debug': continue if available is True: datas[name] = value datas = {k: export_to_frontend(v, keep_fields=keep_fields) if recursive else v for k, v in datas.items() if k not in cover_fields} return _data_object(datas)