import importlib from typing import Type from fastapi import FastAPI from tortoise import Model, Tortoise from .site import Site, Resource, Field class AdminApp(FastAPI): models: str admin_secret: str user_model: str site: Site _inited: bool = False _field_type_mapping = { 'IntField': 'number', 'BooleanField': 'checkbox', 'DatetimeField': 'datetime', 'DateField': 'date', 'IntEnumFieldInstance': 'select', 'CharEnumFieldInstance': 'select', 'DecimalField': 'number', 'FloatField': 'number', 'TextField': 'textarea', 'SmallIntField': 'number', 'ForeignKeyFieldInstance': 'select', 'JSONField': 'json', } _model_menu_mapping = {} def _get_model_menu_mapping(self): for menu in filter(lambda x: x.url, self.site.menus): self._model_menu_mapping[menu.url.split('?')[0].split('/')[-1]] = menu def init(self, site: Site, user_model: str, admin_secret: str, models: str, ): """ init admin site :param site: :param user_model: admin user model path,like admin.models.user :param admin_secret: admin jwt secret. :param models: tortoise models :return: """ self.site = site self.admin_secret = admin_secret self.models = importlib.import_module(models) self.user_model = getattr(self.models, user_model) self._inited = True self._get_model_menu_mapping() def _exclude_field(self, resource: str, field: str): """ exclude field by menu include and exclude :param resource: :param field: :return: """ menu = self._model_menu_mapping[resource] if menu.include: if field not in menu.include: return True if menu.exclude: if field in menu.exclude: return True return False async def _build_resource_from_model_describe(self, resource: str, model: Type[Model], model_describe: dict, exclude_readonly: bool): data_fields = model_describe.get('data_fields') pk_field = model_describe.get('pk_field') fk_fields = model_describe.get('fk_fields') menu = self._model_menu_mapping[resource] search_fields_ret = {} search_fields = menu.search_fields if exclude_readonly: fields = {} else: fields = { pk_field.get('name'): Field( label=pk_field.get('name').title(), required=True, type=self._field_type_mapping.get(pk_field.get('field_type')) or 'text', ) } for field in data_fields: read_only = field.get('constraints').get('readOnly') field_type = field.get('field_type') name = field.get('name') if (read_only and exclude_readonly) or self._exclude_field(resource, name): continue type_ = self._field_type_mapping.get(field_type) or 'text' options = [] if type_ == 'select': for k, v in model._meta.fields_map[name].enum_type.choices().items(): options.append({'text': v, 'value': k}) label = field.get('description') or field.get('name').title() fields[name] = Field( label=label, required=not field.get('nullable'), type=type_, options=options ) if name in search_fields: search_fields_ret[name] = Field( label=label, ) for fk_field in fk_fields: name = fk_field.get('name') if name not in menu.raw_id_fields: fk_model_class = model._meta.fields_map[name].model_class objs = await fk_model_class.all() raw_field = fk_field.get('raw_field') label = fk_field.get('description') or name.title() options = list(map(lambda x: {'text': str(x), 'value': x.pk}, objs)) fields[raw_field] = Field( label=label, required=True, type='select', options=options ) if name in search_fields: search_fields_ret[raw_field] = Field( label=label, type='select', options=options, ) return fields, search_fields_ret async def get_resource(self, resource: str, exclude_readonly=False, ): assert self._inited, 'must call init() first!' model = getattr(self.models, resource) # type:Type[Model] model_describe = Tortoise.describe_model(model) fields, search_fields = await self._build_resource_from_model_describe(resource, model, model_describe, exclude_readonly) return Resource( title=model_describe.get('description') or resource.title(), fields=fields, searchFields=search_fields ) app = AdminApp( openapi_prefix='/admin', )