update README.rst

add support to fk
This commit is contained in:
long2ice
2020-04-06 21:41:06 +08:00
parent 07f32af541
commit e8069484f5
9 changed files with 275 additions and 56 deletions

View File

@@ -93,7 +93,7 @@ Only you should do is runing a fastapi app and mount admin app from fastapi-admi
),
Menu(
name='User',
url='/rest/TestUser', #important,TestUser is same of the Model class TestUser.
url='/rest/TestUser', #important,TestUser is same of the Model class TestUser and must be /rest/<Model>.
icon='fa fa-user',
),
Menu(
@@ -156,6 +156,10 @@ Verbose Name
~~~~~~~~~~~~
FastAPI-admin will auto read ``description`` defined in tortoise-orm model ``Field`` and display in front.
ForeignKeyField Support
~~~~~~~~~~~~~~~~~~~~~~~
If ``ForeignKeyField`` not passed in ``menu.raw_id_fields``,FastAPI-admin will get all related objects and display ``select`` in front with ``Model.__str__``.
Front
-----

View File

@@ -0,0 +1,9 @@
from tortoise import fields, Model
class BaseModel(Model):
create_at = fields.DatetimeField(auto_now_add=True, description='创建时间')
update_at = fields.DatetimeField(auto_now=True, description='更新时间')
class Meta:
abstract = True

60
examples/enums.py Normal file
View File

@@ -0,0 +1,60 @@
from enum import IntEnum
class EnumMixin:
@classmethod
def choices(cls):
raise NotImplementedError
class Status(EnumMixin, IntEnum):
"""
状态
"""
on = 1
off = 0
@classmethod
def choices(cls):
return {
cls.on: '开启',
cls.off: '关闭'
}
class BaiduAiCategory(EnumMixin, IntEnum):
id_card = 1
censor = 2
@classmethod
def choices(cls):
return {
cls.id_card: '身份证',
cls.censor: '审核'
}
class AliYunSecretCategory(EnumMixin, IntEnum):
sms = 1
oss = 2
alert = 3
@classmethod
def choices(cls):
return {
cls.sms: '短信',
cls.oss: 'oss',
cls.alert: '报警'
}
class DeviceSys(EnumMixin, IntEnum):
android = 1
ios = 2
@classmethod
def choices(cls):
return {
cls.android: '安卓',
cls.ios: 'iOS',
}

View File

@@ -8,7 +8,7 @@ from fastapi_admin.site import Site, Menu
TORTOISE_ORM = {
'connections': {
'default': 'mysql://root:123456@127.0.0.1:3306/test'
'default': 'mysql://root:123456@127.0.0.1:3306/micro'
},
'apps': {
'models': {
@@ -27,34 +27,86 @@ def create_app():
fast_app.mount('/admin', admin_app)
admin_app.init(
user_model='TestUser',
user_model='User',
admin_secret='test',
models='examples.models',
site=Site(
name='FastAPI-Admin',
name='微服务管理后台',
logo='https://github.com/long2ice/fastapi-admin/raw/master/front/static/img/logo.png',
locale='en-US',
locale_switcher=True,
locale='zh-CN',
locale_switcher=False,
menu=[
Menu(
name='Home',
url='/home',
icon='fa fa-home',
name='首页',
url='/',
icon='fa fa-home'
),
Menu(
name='Information',
title=True,
icon='fa fa-user',
name='配置',
title=True
),
Menu(
name='User',
url='/rest/TestUser',
icon='fa fa-user',
name='应用',
url='/rest/App',
icon='fa fa-pencil'
),
Menu(
name='Logout',
url='/logout',
icon='icon-lock',
name='阿里云秘钥',
url='/rest/AliYunSecret',
icon='fa fa-user-secret'
),
Menu(
name='阿里云OSS',
url='/rest/AliYunOss',
icon='fa fa-database'
),
Menu(
name='App短信',
url='/rest/AppSms',
icon='fa fa-envelope-o'
),
Menu(
name='百度AI',
url='/rest/BaiduAi',
icon='fa fa-desktop'
),
Menu(
name='App百度AI',
url='/rest/AppBaiduAi',
icon='fa fa-laptop',
),
Menu(
name='在线参数',
url='/rest/Config',
icon='fa fa-cog'
),
Menu(
name='基本信息',
title=True
),
Menu(
name='请求日志',
url='/rest/ApiLog',
icon='fa fa-sticky-note'
),
Menu(
name='App版本',
url='/rest/AppVersion',
icon='fa fa-mobile'
),
Menu(
name='授权',
title=True
),
Menu(
name='用户',
url='/rest/User',
icon='fa fa-user'
),
Menu(
name='注销',
url='/login',
icon='fa fa-lock'
)
]
)

View File

@@ -1,24 +1,89 @@
from enum import IntEnum
from fastapi_admin.enum import EnumMixin
from tortoise import fields
from fastapi_admin.models import User
from . import BaseModel
from .enums import AliYunSecretCategory, BaiduAiCategory, Status, DeviceSys
from fastapi_admin.models import User as AdminUser
class Status(EnumMixin, IntEnum):
on = 1
off = 2
class User(AdminUser, BaseModel):
last_login = fields.DatetimeField(description='上次登录')
@classmethod
def choices(cls):
return {
cls.on: 'ON',
cls.off: 'OFF'
}
def __str__(self):
return f'{self.pk}#{self.username}'
class TestUser(User):
is_active = fields.BooleanField(default=False, description='Is Active')
status = fields.IntEnumField(Status, description='User Status')
created_at = fields.DatetimeField(auto_now_add=True)
updated_at = fields.DatetimeField(auto_now=True)
class AliYunSecret(BaseModel):
app_id = fields.CharField(max_length=50)
app_secret = fields.CharField(max_length=50)
category = fields.IntEnumField(AliYunSecretCategory, description='类别', default=AliYunSecretCategory.sms)
class AliYunOss(BaseModel):
bucket = fields.CharField(max_length=50, unique=True, description='仓库')
domain = fields.CharField(max_length=200, description='域名')
endpoint = fields.CharField(max_length=200, description='端点')
aliyun_secret = fields.ForeignKeyField('models.AliYunSecret', related_name='aliyunoss')
class App(BaseModel):
uaid = fields.IntField(unique=True)
label = fields.CharField(max_length=20)
secret = fields.CharField(max_length=16, description='通信密钥')
work_secret = fields.CharField(max_length=100, description='企业微信密钥')
def __str__(self):
return f'{self.uaid}#{self.label}'
class AppSms(BaseModel):
app = fields.ForeignKeyField('models.App', related_name='app_sms')
scene = fields.CharField(max_length=50, description='使用场景')
aliyun_secret = fields.ForeignKeyField('models.AliYunSecret', related_name='app_sms')
sign_name = fields.CharField(max_length=20, description='签名')
template_code = fields.CharField(max_length=20, description='模板编号')
class Meta:
unique_together = (('app', 'scene'),)
class BaiduAi(BaseModel):
app_id = fields.CharField(max_length=50)
api_key = fields.CharField(max_length=50)
app_secret = fields.CharField(max_length=50)
category = fields.IntEnumField(BaiduAiCategory, description='应用类别', default=BaiduAiCategory.id_card)
def __str__(self):
return f'{self.pk}#{self.app_id}'
class AppBaiduAi(BaseModel):
app = fields.ForeignKeyField('models.App', related_name='app_baidu_ais')
baidu_ai = fields.ForeignKeyField('models.BaiduAi', related_name='app_baidu_ais')
class Config(BaseModel):
label = fields.CharField(max_length=200)
key = fields.CharField(max_length=20)
value = fields.JSONField()
status: Status = fields.IntEnumField(Status, default=Status.on, description='状态')
class ApiLog(BaseModel):
app = fields.ForeignKeyField('models.App', related_name='api_logs')
endpoint = fields.CharField(max_length=50)
success = fields.BooleanField(default=True, description='是否成功')
status_code = fields.SmallIntField(description='状态码')
class AppVersion(BaseModel):
app = fields.ForeignKeyField('models.App', related_name='app_versions')
channel = fields.CharField(max_length=20, description='渠道')
device_sys = fields.IntEnumField(DeviceSys, default=DeviceSys.android, description='操作系统')
version_code = fields.CharField(max_length=20, description='版本号')
describe = fields.CharField(max_length=200, description='描述')
download_url = fields.CharField(max_length=200, description='下载链接')
is_update = fields.BooleanField(default=False, description='是否更新')
class Meta:
ordering = ['-id']
indexes = (('app', 'device_sys', 'channel'),)

View File

@@ -1,3 +1,3 @@
from . import routes
__version__ = '0.1.1'
__version__ = '0.1.2'

View File

@@ -3,6 +3,7 @@ from typing import Type
from fastapi import FastAPI
from tortoise import Model, Tortoise
from .site import Site, Resource, Field
@@ -22,7 +23,13 @@ class AdminApp(FastAPI):
'FloatField': 'number',
'TextField': 'textarea',
'SmallIntField': 'number',
'ForeignKeyFieldInstance': 'select',
}
_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, ):
"""
@@ -38,6 +45,7 @@ class AdminApp(FastAPI):
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):
"""
@@ -46,20 +54,20 @@ class AdminApp(FastAPI):
:param field:
:return:
"""
for menu in filter(lambda x: x.url, self.site.menus):
if resource == menu.url.split('?')[0].split('/')[-1]:
if menu.include:
if field not in menu.include:
return True
if menu.exclude:
if field in menu.exclude:
return True
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 _build_resource_from_model_describe(self, resource: str, model: Type[Model], model_describe: dict,
exclude_readonly: bool):
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')
if exclude_readonly:
ret = {}
else:
@@ -90,15 +98,29 @@ class AdminApp(FastAPI):
type=type_,
options=options
)
menu = self._model_menu_mapping[resource]
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()
ret[fk_field.get('raw_field')] = Field(
label=fk_field.get('description') or name.title(),
required=True,
type='select',
options=list(map(lambda x: {'text': str(x), 'value': x.pk}, objs))
)
return ret
def get_resource(self, resource: str, exclude_readonly=False):
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)
return Resource(
title=model_describe.get('description') or resource.title(),
fields=self._build_resource_from_model_describe(resource, model, model_describe, exclude_readonly)
fields=await self._build_resource_from_model_describe(resource, model, model_describe, exclude_readonly)
)

View File

@@ -24,7 +24,8 @@ async def get_resource(
if query.where:
qs = qs.filter(**query.where)
result = await qs.limit(query.size).offset((query.page - 1) * query.size)
creator = pydantic_model_creator(model, include=app.get_resource(resource).resource_fields.keys())
resource = await app.get_resource(resource)
creator = pydantic_model_creator(model, include=resource.resource_fields.keys())
return GetManyOut(
total=await qs.count(),
data=list(map(lambda x: creator.from_orm(x).dict(), result))
@@ -37,7 +38,8 @@ async def get_resource(
async def form(
resource: str,
):
return app.get_resource(resource, exclude_readonly=True).dict(by_alias=True, exclude_unset=True)
resource = await app.get_resource(resource, exclude_readonly=True)
return resource.dict(by_alias=True, exclude_unset=True)
@router.get(
@@ -46,7 +48,8 @@ async def form(
async def grid(
resource: str,
):
return app.get_resource(resource).dict(by_alias=True, exclude_unset=True)
resource = await app.get_resource(resource)
return resource.dict(by_alias=True, exclude_unset=True)
@router.get(
@@ -55,7 +58,8 @@ async def grid(
async def view(
resource: str,
):
return app.get_resource(resource)
resource = await app.get_resource(resource)
return resource.dict(by_alias=True, exclude_unset=True)
@router.delete(
@@ -85,8 +89,8 @@ async def update_one(
message=f'Update Error,{e}'
))
user_ = await get_object_or_404(model, pk=id)
creator = pydantic_model_creator(model,
include=app.get_resource(resource, exclude_readonly=True).resource_fields.keys())
resource = await app.get_resource(resource, exclude_readonly=True)
creator = pydantic_model_creator(model, include=resource.resource_fields.keys())
return creator.from_orm(user_).dict()
@@ -98,7 +102,8 @@ async def create_one(
body=Depends(get_body),
model=Depends(get_model)
):
creator = pydantic_model_creator(model, include=app.get_resource(resource).resource_fields.keys())
resource = await app.get_resource(resource)
creator = pydantic_model_creator(model, include=resource.resource_fields.keys())
try:
obj = await model.create(**body)
except IntegrityError as e:
@@ -117,6 +122,7 @@ async def get_one(
model=Depends(get_model)
):
obj = await get_object_or_404(model, pk=id)
include = app.get_resource(resource, exclude_readonly=True).resource_fields.keys()
resource = await app.get_resource(resource)
include = resource.resource_fields.keys()
creator = pydantic_model_creator(model, include=include)
return creator.from_orm(obj).dict()

View File

@@ -11,6 +11,7 @@ class Menu(BaseModel):
include: Optional[Tuple[str]]
exclude: Optional[Tuple[str]]
external: Optional[bool] = False
raw_id_fields: Optional[Tuple[str]] = ()
class Site(BaseModel):