Files
long2ice f574ca313b support datetime picker
readonly support
custom fields_type
2020-04-10 14:03:15 +08:00

184 lines
6.8 KiB
Python

import importlib
from typing import Type
from fastapi import FastAPI
from tortoise import Model, Tortoise
from .site import Site, Resource, Field, Menu
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',
'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
def _get_field_type(self, menu: Menu, name: str, field_type: str) -> str:
"""
get field display type
:param menu:
:param field_type:
:return:
"""
field_type = menu.fields_type.get(name) or self._field_type_mapping.get(field_type) or 'text'
return field_type
async def _build_resource_from_model_describe(self, resource: str, model: Type[Model], model_describe: dict,
exclude_readonly: bool, exclude_m2m_field=True):
"""
build resource
:param resource:
:param model:
:param model_describe:
:param exclude_readonly:
:param exclude_m2m_field:
:return:
"""
data_fields = model_describe.get('data_fields')
pk_field = model_describe.get('pk_field')
fk_fields = model_describe.get('fk_fields')
m2m_fields = model_describe.get('m2m_fields')
menu = self.model_menu_mapping[resource]
search_fields_ret = {}
search_fields = menu.search_fields
sort_fields = menu.sort_fields
fields = {}
name = pk_field.get('name')
if not exclude_readonly and not self._exclude_field(resource, name):
fields = {
name: Field(
label=pk_field.get('name').title(),
required=True,
type=self._get_field_type(menu, name, pk_field.get('field_type')),
sortable=name in sort_fields
)
}
for data_field in data_fields:
readonly = data_field.get('constraints').get('readOnly')
field_type = data_field.get('field_type')
name = data_field.get('name')
if self._exclude_field(resource, name):
continue
type_ = self._get_field_type(menu, name, field_type)
options = []
if type_ == 'select':
for k, v in model._meta.fields_map[name].enum_type.choices().items():
options.append({'text': v, 'value': k})
label = data_field.get('description') or data_field.get('name').title()
field = Field(
label=label,
required=not data_field.get('nullable'),
type=type_,
options=options,
sortable=name in sort_fields,
disabled=readonly
)
fields[name] = field
if name in search_fields:
search_fields_ret[name] = field
for fk_field in fk_fields:
name = fk_field.get('name')
if not self._exclude_field(resource, 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))
field = Field(
label=label,
required=True,
type='select',
options=options,
sortable=name in sort_fields
)
fields[raw_field] = field
if name in search_fields:
search_fields_ret[raw_field] = field
if not exclude_m2m_field:
for m2m_field in m2m_fields:
name = m2m_field.get('name')
if not self._exclude_field(resource, name):
label = m2m_field.get('description') or name.title()
m2m_model_class = model._meta.fields_map[name].model_class
objs = await m2m_model_class.all()
options = list(map(lambda x: {'text': str(x), 'value': x.pk}, objs))
fields[name] = Field(
label=label,
type='tree',
options=options,
multiple=True,
)
return fields, search_fields_ret
async def get_resource(self, resource: str, exclude_readonly=False, exclude_m2m_field=True):
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, exclude_m2m_field)
return Resource(
title=model_describe.get('description') or resource.title(),
fields=fields,
searchFields=search_fields
)
app = AdminApp(
openapi_prefix='/admin',
)