mirror of
https://github.com/fastapi-admin/fastapi-admin.git
synced 2026-03-13 10:32:25 +08:00
add builtin permissions
remove orjson fix tortoise error
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
from enum import IntEnum
|
||||
|
||||
from fastapi_admin.enum import EnumMixin
|
||||
from fastapi_admin.enums import EnumMixin
|
||||
|
||||
|
||||
class ProductType(EnumMixin, IntEnum):
|
||||
|
||||
@@ -38,12 +38,14 @@ def create_app():
|
||||
fast_app = FastAPI(debug=True)
|
||||
|
||||
register_tortoise(fast_app, config=TORTOISE_ORM, generate_schemas=True)
|
||||
fast_app.mount('/admin', admin_app)
|
||||
|
||||
fast_app.mount('/admin', admin_app)
|
||||
admin_app.debug = True
|
||||
admin_app.init(
|
||||
user_model='User',
|
||||
admin_secret='test',
|
||||
models='examples.models',
|
||||
permission=True,
|
||||
site=Site(
|
||||
name='FastAPI-admin Demo',
|
||||
logo='https://github.com/long2ice/fastapi-admin/raw/master/front/static/img/logo.png',
|
||||
@@ -101,12 +103,19 @@ def create_app():
|
||||
name='User',
|
||||
url='/rest/User',
|
||||
icon='fa fa-user',
|
||||
fields_type={
|
||||
'avatar': 'image'
|
||||
},
|
||||
exclude=('password',),
|
||||
search_fields=('username',)
|
||||
),
|
||||
Menu(
|
||||
name='Role',
|
||||
url='/rest/Role',
|
||||
icon='fa fa-group'
|
||||
),
|
||||
Menu(
|
||||
name='Permission',
|
||||
url='/rest/Permission',
|
||||
icon='fa fa-copyright'
|
||||
),
|
||||
Menu(
|
||||
name='Logout',
|
||||
url='/logout',
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import datetime
|
||||
|
||||
from tortoise import fields, Model
|
||||
|
||||
from fastapi_admin.models import User as AdminUser
|
||||
from fastapi_admin.models import User as AdminUser, Permission, Role
|
||||
from .enums import ProductType
|
||||
|
||||
|
||||
class User(AdminUser, Model):
|
||||
last_login = fields.DatetimeField(description='Last Login')
|
||||
last_login = fields.DatetimeField(description='Last Login', default=datetime.datetime.now)
|
||||
is_active = fields.BooleanField(default=True, description='Is Active')
|
||||
avatar = fields.CharField(max_length=200)
|
||||
intro = fields.TextField()
|
||||
is_superuser = fields.BooleanField(default=False, description='Is SuperUser')
|
||||
avatar = fields.CharField(max_length=200, default='')
|
||||
intro = fields.TextField(default='')
|
||||
created_at = fields.DatetimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
|
||||
105
fastapi_admin/cli.py
Normal file
105
fastapi_admin/cli.py
Normal file
@@ -0,0 +1,105 @@
|
||||
import argparse
|
||||
import importlib
|
||||
|
||||
from colorama import Fore, init
|
||||
from prompt_toolkit import PromptSession
|
||||
from tortoise import Tortoise, run_async
|
||||
|
||||
from fastapi_admin import enums
|
||||
from fastapi_admin.common import pwd_context
|
||||
from fastapi_admin.models import Permission
|
||||
|
||||
init(autoreset=True)
|
||||
|
||||
|
||||
class Logger:
|
||||
@classmethod
|
||||
def success(cls, text):
|
||||
print(Fore.GREEN + text)
|
||||
|
||||
@classmethod
|
||||
def waring(cls, text):
|
||||
print(Fore.YELLOW + text)
|
||||
|
||||
@classmethod
|
||||
def error(cls, text):
|
||||
print(Fore.RED + text)
|
||||
|
||||
|
||||
def import_obj(path):
|
||||
module_name, class_name = path.rsplit('.', 1)
|
||||
return getattr(importlib.import_module(module_name), class_name)
|
||||
|
||||
|
||||
async def init_tortoise(args):
|
||||
await Tortoise.init(config=import_obj(args.config))
|
||||
|
||||
|
||||
async def register_permissions(args):
|
||||
await init_tortoise(args)
|
||||
if args.clean:
|
||||
await Permission.all().delete()
|
||||
Logger.waring('Cleaned all permissions success.')
|
||||
models = Tortoise.apps.get('models').keys()
|
||||
models = list(models)
|
||||
for model in models:
|
||||
for action in enums.PermissionAction:
|
||||
label = f'{enums.PermissionAction.choices().get(action)} {model}'
|
||||
defaults = dict(
|
||||
label=label,
|
||||
model=model,
|
||||
action=action,
|
||||
)
|
||||
_, created = await Permission.get_or_create(
|
||||
**defaults,
|
||||
)
|
||||
if created:
|
||||
Logger.success(f'Create permission {label} success.')
|
||||
|
||||
|
||||
async def createsuperuser(args):
|
||||
await init_tortoise(args)
|
||||
|
||||
user_model = import_obj(args.user_model)
|
||||
prompt = PromptSession()
|
||||
while True:
|
||||
try:
|
||||
username = await prompt.prompt_async('Username: ')
|
||||
password = await prompt.prompt_async('Password: ', is_password=True)
|
||||
try:
|
||||
await user_model.create(
|
||||
username=username,
|
||||
password=pwd_context.hash(password),
|
||||
is_superuser=True
|
||||
)
|
||||
Logger.success(f'Create superuser {username} success.')
|
||||
return
|
||||
except Exception as e:
|
||||
Logger.error(f'Create superuser {username} error,{e}')
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
Logger.success(f'Exit success!')
|
||||
return
|
||||
|
||||
|
||||
def cli():
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers(title='subcommands')
|
||||
parser.add_argument('-c', '--config', required=True,
|
||||
help='Tortoise-orm config dict import path,like settings.TORTOISE_ORM.')
|
||||
|
||||
parser_register_permissions = subparsers.add_parser('register_permissions')
|
||||
parser_register_permissions.add_argument('--clean', required=False, action='store_true',
|
||||
help='Clean up old permissions then renew.')
|
||||
parser_register_permissions.set_defaults(func=register_permissions)
|
||||
|
||||
parser_createsuperuser = subparsers.add_parser('createsuperuser')
|
||||
parser_createsuperuser.add_argument('--user-model', required=True,
|
||||
help='User model import path,like examples.models.User.')
|
||||
parser_createsuperuser.set_defaults(func=createsuperuser)
|
||||
|
||||
parse_args = parser.parse_args()
|
||||
run_async(parse_args.func(parse_args))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
cli()
|
||||
@@ -1,5 +1,9 @@
|
||||
from copy import deepcopy
|
||||
|
||||
from passlib.context import CryptContext
|
||||
|
||||
pwd_context = CryptContext(schemes=['bcrypt'], deprecated='auto')
|
||||
|
||||
|
||||
async def handle_m2m_fields_create_or_update(body, m2m_fields, model, create=True, pk=None):
|
||||
"""
|
||||
@@ -25,7 +29,7 @@ async def handle_m2m_fields_create_or_update(body, m2m_fields, model, create=Tru
|
||||
m2m_related = getattr(obj, k)
|
||||
if not create:
|
||||
await m2m_related.clear()
|
||||
m2m_model = m2m_related.model
|
||||
m2m_model = m2m_related.remote_model
|
||||
m2m_objs = await m2m_model.filter(pk__in=v)
|
||||
await m2m_related.add(*m2m_objs)
|
||||
return obj
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import json
|
||||
|
||||
import jwt
|
||||
import orjson
|
||||
from fastapi import Query, Path, Depends, HTTPException
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
from fastapi.security.utils import get_authorization_scheme_param
|
||||
from pydantic import BaseModel
|
||||
from starlette.requests import Request
|
||||
from starlette.status import HTTP_401_UNAUTHORIZED
|
||||
from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN, HTTP_404_NOT_FOUND
|
||||
|
||||
from . import enums
|
||||
from .factory import app
|
||||
|
||||
auth_schema = HTTPBearer()
|
||||
@@ -38,7 +42,7 @@ class QueryItem(BaseModel):
|
||||
|
||||
|
||||
def get_query(query=Query(...)):
|
||||
query = orjson.loads(query)
|
||||
query = json.loads(query)
|
||||
return QueryItem.parse_obj(query)
|
||||
|
||||
|
||||
@@ -57,3 +61,35 @@ async def parse_body(request: Request, resource: str = Path(...)):
|
||||
if v is not None:
|
||||
ret[key] = v
|
||||
return ret, resource_fields
|
||||
|
||||
|
||||
async def get_current_user(user_id=Depends(jwt_required)):
|
||||
user = await app.user_model.get_or_none(pk=user_id)
|
||||
if not user:
|
||||
raise HTTPException(HTTP_404_NOT_FOUND)
|
||||
return user
|
||||
|
||||
|
||||
class PermissionsChecker:
|
||||
def __init__(self, action: enums.PermissionAction):
|
||||
self.action = action
|
||||
|
||||
async def __call__(self, resource: str = Path(...), user=Depends(get_current_user)):
|
||||
if not app.permission or user.is_superuser:
|
||||
return
|
||||
if not user.is_active:
|
||||
raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail='User is not Active!')
|
||||
has_permission = False
|
||||
await user.fetch_related('roles')
|
||||
for role in user.roles:
|
||||
if await role.permissions.filter(model=resource, action=self.action):
|
||||
has_permission = True
|
||||
break
|
||||
if not has_permission:
|
||||
raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail='No Permission!')
|
||||
|
||||
|
||||
read_checker = PermissionsChecker(action=enums.PermissionAction.read)
|
||||
create_checker = PermissionsChecker(action=enums.PermissionAction.create)
|
||||
update_checker = PermissionsChecker(action=enums.PermissionAction.update)
|
||||
delete_checker = PermissionsChecker(action=enums.PermissionAction.delete)
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import abc
|
||||
|
||||
|
||||
class EnumMixin:
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def choices(cls):
|
||||
pass
|
||||
25
fastapi_admin/enums.py
Normal file
25
fastapi_admin/enums.py
Normal file
@@ -0,0 +1,25 @@
|
||||
import abc
|
||||
from enum import IntEnum
|
||||
|
||||
|
||||
class EnumMixin:
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def choices(cls):
|
||||
pass
|
||||
|
||||
|
||||
class PermissionAction(EnumMixin, IntEnum):
|
||||
create = 1
|
||||
delete = 2
|
||||
update = 3
|
||||
read = 4
|
||||
|
||||
@classmethod
|
||||
def choices(cls):
|
||||
return {
|
||||
cls.create: 'Create',
|
||||
cls.delete: 'Delete',
|
||||
cls.update: 'Update',
|
||||
cls.read: 'Read',
|
||||
}
|
||||
10
fastapi_admin/exceptions.py
Normal file
10
fastapi_admin/exceptions.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from starlette.requests import Request
|
||||
from fastapi.exceptions import HTTPException
|
||||
from starlette.responses import UJSONResponse
|
||||
|
||||
|
||||
async def exception_handler(request: Request, exc: HTTPException):
|
||||
return UJSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content={'message': exc.detail},
|
||||
)
|
||||
@@ -1,30 +1,35 @@
|
||||
import importlib
|
||||
from typing import Type, List, Dict
|
||||
|
||||
from fastapi import FastAPI
|
||||
from tortoise import Model, Tortoise
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from tortoise import Model
|
||||
from tortoise.fields import IntField, BooleanField, DatetimeField, DateField
|
||||
from tortoise.fields.data import IntEnumFieldInstance, CharEnumFieldInstance, DecimalField, FloatField, TextField, \
|
||||
SmallIntField, JSONField
|
||||
|
||||
from .exceptions import exception_handler
|
||||
from .site import Site, Resource, Field, Menu
|
||||
|
||||
|
||||
class AdminApp(FastAPI):
|
||||
models: str
|
||||
admin_secret: str
|
||||
user_model: str
|
||||
user_model: Model
|
||||
site: Site
|
||||
permission: bool
|
||||
_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',
|
||||
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: Dict[str, Menu] = {}
|
||||
|
||||
@@ -35,16 +40,18 @@ class AdminApp(FastAPI):
|
||||
else:
|
||||
self.model_menu_mapping[menu.url.split('?')[0].split('/')[-1]] = menu
|
||||
|
||||
def init(self, site: Site, user_model: str, admin_secret: str, models: str, ):
|
||||
def init(self, site: Site, user_model: str, admin_secret: str, models: str, permission: bool = False):
|
||||
"""
|
||||
init admin site
|
||||
:param permission: active builtin permission
|
||||
:param models:
|
||||
: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.permission = permission
|
||||
self.admin_secret = admin_secret
|
||||
self.models = importlib.import_module(models)
|
||||
self.user_model = getattr(self.models, user_model)
|
||||
@@ -160,7 +167,7 @@ class AdminApp(FastAPI):
|
||||
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
|
||||
m2m_model_class = m2m_field.get('python_type')
|
||||
objs = await m2m_model_class.all()
|
||||
options = list(map(lambda x: {'text': str(x), 'value': x.pk}, objs))
|
||||
fields[name] = Field(
|
||||
@@ -190,3 +197,4 @@ class AdminApp(FastAPI):
|
||||
app = AdminApp(
|
||||
openapi_prefix='/admin',
|
||||
)
|
||||
app.add_exception_handler(HTTPException, exception_handler)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from tortoise import Model, fields
|
||||
|
||||
from fastapi_admin import enums
|
||||
|
||||
|
||||
class User(Model):
|
||||
username = fields.CharField(max_length=20, unique=True)
|
||||
@@ -7,3 +9,22 @@ class User(Model):
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class Permission(Model):
|
||||
label = fields.CharField(max_length=50)
|
||||
model = fields.CharField(max_length=50)
|
||||
action: enums.PermissionAction = fields.IntEnumField(enums.PermissionAction, default=enums.PermissionAction.read)
|
||||
|
||||
def __str__(self):
|
||||
return self.label
|
||||
|
||||
|
||||
class Role(Model):
|
||||
label = fields.CharField(max_length=50)
|
||||
users = fields.ManyToManyField('models.User')
|
||||
|
||||
permissions: fields.ManyToManyRelation[Permission] = fields.ManyToManyField('models.Permission')
|
||||
|
||||
def __str__(self):
|
||||
return self.label
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from fastapi import Depends, APIRouter
|
||||
from fastapi.responses import ORJSONResponse
|
||||
from fastapi.responses import UJSONResponse
|
||||
from starlette.status import HTTP_409_CONFLICT
|
||||
from tortoise import Model
|
||||
from tortoise.contrib.pydantic import pydantic_model_creator
|
||||
@@ -7,7 +7,8 @@ from tortoise.exceptions import IntegrityError
|
||||
from tortoise.fields import ManyToManyRelation
|
||||
|
||||
from ..common import handle_m2m_fields_create_or_update
|
||||
from ..depends import QueryItem, get_query, parse_body, get_model
|
||||
from ..depends import QueryItem, get_query, parse_body, get_model, read_checker, delete_checker, update_checker, \
|
||||
create_checker
|
||||
from ..factory import app
|
||||
from ..responses import GetManyOut
|
||||
from ..schemas import BulkIn
|
||||
@@ -17,7 +18,8 @@ router = APIRouter()
|
||||
|
||||
|
||||
@router.get(
|
||||
'/{resource}'
|
||||
'/{resource}',
|
||||
dependencies=[Depends(read_checker)]
|
||||
)
|
||||
async def get_resource(
|
||||
resource: str,
|
||||
@@ -45,7 +47,8 @@ async def get_resource(
|
||||
|
||||
|
||||
@router.get(
|
||||
'/{resource}/form'
|
||||
'/{resource}/form',
|
||||
dependencies=[Depends(read_checker)]
|
||||
)
|
||||
async def form(
|
||||
resource: str,
|
||||
@@ -55,7 +58,8 @@ async def form(
|
||||
|
||||
|
||||
@router.get(
|
||||
'/{resource}/grid'
|
||||
'/{resource}/grid',
|
||||
dependencies=[Depends(read_checker)]
|
||||
)
|
||||
async def grid(
|
||||
resource: str,
|
||||
@@ -65,7 +69,8 @@ async def grid(
|
||||
|
||||
|
||||
@router.get(
|
||||
'/{resource}/view'
|
||||
'/{resource}/view',
|
||||
dependencies=[Depends(read_checker)]
|
||||
)
|
||||
async def view(
|
||||
resource: str,
|
||||
@@ -75,7 +80,8 @@ async def view(
|
||||
|
||||
|
||||
@router.post(
|
||||
'/{resource}/bulk/delete'
|
||||
'/{resource}/bulk/delete',
|
||||
dependencies=[Depends(delete_checker)]
|
||||
)
|
||||
async def bulk_delete(
|
||||
bulk_in: BulkIn,
|
||||
@@ -86,7 +92,8 @@ async def bulk_delete(
|
||||
|
||||
|
||||
@router.delete(
|
||||
'/{resource}/{id}'
|
||||
'/{resource}/{id}',
|
||||
dependencies=[Depends(delete_checker)]
|
||||
)
|
||||
async def delete_one(
|
||||
id: int,
|
||||
@@ -97,7 +104,8 @@ async def delete_one(
|
||||
|
||||
|
||||
@router.put(
|
||||
'/{resource}/{id}'
|
||||
'/{resource}/{id}',
|
||||
dependencies=[Depends(update_checker)]
|
||||
)
|
||||
async def update_one(
|
||||
id: int,
|
||||
@@ -109,7 +117,7 @@ async def update_one(
|
||||
try:
|
||||
obj = await handle_m2m_fields_create_or_update(body, m2m_fields, model, False, id)
|
||||
except IntegrityError as e:
|
||||
return ORJSONResponse(status_code=HTTP_409_CONFLICT, content=dict(
|
||||
return UJSONResponse(status_code=HTTP_409_CONFLICT, content=dict(
|
||||
message=f'Update Error,{e}'
|
||||
))
|
||||
creator = pydantic_model_creator(model, include=resource_fields, exclude=m2m_fields)
|
||||
@@ -117,7 +125,8 @@ async def update_one(
|
||||
|
||||
|
||||
@router.post(
|
||||
'/{resource}'
|
||||
'/{resource}',
|
||||
dependencies=[Depends(create_checker)]
|
||||
)
|
||||
async def create_one(
|
||||
parsed=Depends(parse_body),
|
||||
@@ -129,14 +138,15 @@ async def create_one(
|
||||
try:
|
||||
obj = await handle_m2m_fields_create_or_update(body, m2m_fields, model)
|
||||
except IntegrityError as e:
|
||||
return ORJSONResponse(status_code=HTTP_409_CONFLICT, content=dict(
|
||||
return UJSONResponse(status_code=HTTP_409_CONFLICT, content=dict(
|
||||
message=f'Create Error,{e}'
|
||||
))
|
||||
return creator.from_orm(obj).dict()
|
||||
|
||||
|
||||
@router.get(
|
||||
'/{resource}/{id}'
|
||||
'/{resource}/{id}',
|
||||
dependencies=[Depends(read_checker)]
|
||||
)
|
||||
async def get_one(
|
||||
id: int,
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import jwt
|
||||
from fastapi import APIRouter
|
||||
from fastapi.responses import ORJSONResponse
|
||||
from passlib.context import CryptContext
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from starlette.status import HTTP_403_FORBIDDEN
|
||||
|
||||
from ..common import pwd_context
|
||||
from ..factory import app
|
||||
from ..schemas import LoginIn
|
||||
from ..shortcuts import get_object_or_404
|
||||
|
||||
pwd_context = CryptContext(schemes=['bcrypt'], deprecated='auto')
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@@ -21,13 +18,16 @@ async def login(
|
||||
):
|
||||
user_model = app.user_model
|
||||
user = await get_object_or_404(user_model, username=login_in.username)
|
||||
if not user.is_active:
|
||||
raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail='User is not Active!')
|
||||
if not pwd_context.verify(login_in.password, user.password):
|
||||
return ORJSONResponse(status_code=HTTP_403_FORBIDDEN, content={
|
||||
'message': 'Incorrect Password'
|
||||
})
|
||||
return {
|
||||
raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail='Incorrect Password!')
|
||||
ret = {
|
||||
'user': {
|
||||
'username': user.username
|
||||
'username': user.username,
|
||||
'is_superuser': user.is_superuser,
|
||||
|
||||
},
|
||||
'token': jwt.encode({'user_id': user.pk}, app.admin_secret, algorithm='HS256')
|
||||
}
|
||||
return ret
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
from fastapi import APIRouter
|
||||
from copy import deepcopy
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from ..depends import get_current_user
|
||||
from ..factory import app
|
||||
|
||||
router = APIRouter()
|
||||
@@ -17,5 +20,19 @@ async def home():
|
||||
@router.get(
|
||||
'/site',
|
||||
)
|
||||
async def site():
|
||||
return app.site.dict(by_alias=True, exclude_unset=True)
|
||||
async def site(
|
||||
user=Depends(get_current_user)
|
||||
):
|
||||
site_ = app.site
|
||||
if app.permission and not user.is_superuser:
|
||||
site_ = deepcopy(site_)
|
||||
await user.fetch_related('roles')
|
||||
for menu in site_.menus:
|
||||
has_permission = False
|
||||
if not has_permission:
|
||||
for role in user.roles:
|
||||
if await role.permissions.filter(model=menu.name):
|
||||
has_permission = True
|
||||
if not has_permission:
|
||||
site_.menus.remove(menu)
|
||||
return site_.dict(by_alias=True, exclude_unset=True)
|
||||
|
||||
@@ -328,9 +328,7 @@ export default {
|
||||
return this.onSubmit(this.model);
|
||||
}
|
||||
const methodName = String(this.method).toLowerCase();
|
||||
// console.log(this.$refs.form);
|
||||
let formData = this.model;
|
||||
console.log(formData,)
|
||||
if (this.useFormData) {
|
||||
formData = new FormData();
|
||||
_.mapValues(this.model, (v, k) => formData.append(k, v));
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
<b-img class="site-logo" :src="site.logo" fluid/>
|
||||
</a>
|
||||
|
||||
<!-- <b-img class="site-logo" :src="require('../assets/img/gengyi-logo.svg')" fluid style="border-radius: 5px;" /> -->
|
||||
<!-- <b-img rounded="circle" :src="auth.user.avatar" height="70" blank-color="#777" alt="avatar" class="m-2" /> -->
|
||||
<b-img rounded="circle" :src="auth.user.avatar" height="70" blank-color="#777" alt="avatar" class="m-2"/>
|
||||
<div class="my-3" v-if="site.sidebar_userinfo !== false" v-show="!collapsed">
|
||||
<h5 style="letter-spacing:2px">{{site.name}}</h5>
|
||||
<template v-if="auth.user">
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
export default {
|
||||
get(key, defaultValue) {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(`rest_admin_${key}`))
|
||||
return JSON.parse(localStorage.getItem(`fastapi_admin_${key}`))
|
||||
} catch (e) {
|
||||
return defaultValue
|
||||
}
|
||||
},
|
||||
set(key, value) {
|
||||
localStorage.setItem(`rest_admin_${key}`, JSON.stringify(value))
|
||||
localStorage.setItem(`fastapi_admin_${key}`, JSON.stringify(value))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -348,6 +348,8 @@
|
||||
if (this.$refs.table) {
|
||||
this.$refs.table.refresh();
|
||||
}
|
||||
}).catch(error => {
|
||||
this.table = {}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
90
poetry.lock
generated
90
poetry.lock
generated
@@ -119,6 +119,14 @@ optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
version = "7.1.1"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Cross-platform colored terminal text."
|
||||
name = "colorama"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
version = "0.4.3"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
|
||||
@@ -162,18 +170,6 @@ version = "1.0.5"
|
||||
dnspython = ">=1.15.0"
|
||||
idna = ">=2.0.0"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "Faker is a Python package that generates fake data for you."
|
||||
name = "faker"
|
||||
optional = false
|
||||
python-versions = ">=3.4"
|
||||
version = "4.0.2"
|
||||
|
||||
[package.dependencies]
|
||||
python-dateutil = ">=2.4"
|
||||
text-unidecode = "1.3"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
|
||||
@@ -344,7 +340,7 @@ description = "A very fast and expressive template engine."
|
||||
name = "jinja2"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
version = "2.11.1"
|
||||
version = "2.11.2"
|
||||
|
||||
[package.dependencies]
|
||||
MarkupSafe = ">=0.23"
|
||||
@@ -396,6 +392,17 @@ six = "*"
|
||||
[package.extras]
|
||||
test = ["pytest (>=2.7.3)", "pytest-cov", "coveralls", "futures", "pytest-benchmark", "mock"]
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Library for building powerful interactive command lines in Python"
|
||||
name = "prompt-toolkit"
|
||||
optional = false
|
||||
python-versions = ">=3.6.1"
|
||||
version = "3.0.5"
|
||||
|
||||
[package.dependencies]
|
||||
wcwidth = "*"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "C parser in Python"
|
||||
@@ -449,17 +456,6 @@ optional = false
|
||||
python-versions = "*"
|
||||
version = "0.36.3"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "Extensions to the standard Python datetime module"
|
||||
name = "python-dateutil"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
|
||||
version = "2.8.1"
|
||||
|
||||
[package.dependencies]
|
||||
six = ">=1.5"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Add .env support to your django/flask apps in development and deployments"
|
||||
@@ -554,14 +550,6 @@ version = "1.2.1"
|
||||
[package.dependencies]
|
||||
toml = ">=0.10.0,<0.11.0"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "The most basic Text::Unidecode port"
|
||||
name = "text-unidecode"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "1.3"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "Python Library for Tom's Obvious, Minimal Language"
|
||||
@@ -641,6 +629,14 @@ optional = false
|
||||
python-versions = "*"
|
||||
version = "0.14.0"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Measures number of Terminal column cells of wide-character codes"
|
||||
name = "wcwidth"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "0.1.9"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)"
|
||||
@@ -658,7 +654,7 @@ python-versions = "*"
|
||||
version = "1.2.8"
|
||||
|
||||
[metadata]
|
||||
content-hash = "826fcab0d9d69efbbecc67bb195b8dcf7da1b2b2ddd2bae7a4d4468d95270fde"
|
||||
content-hash = "9c9adb8acd429a938c06fd7ad2da8b3416af5edfd391c38f3e7a8ffb3bd0db74"
|
||||
python-versions = "^3.8"
|
||||
|
||||
[metadata.files]
|
||||
@@ -754,6 +750,10 @@ click = [
|
||||
{file = "click-7.1.1-py2.py3-none-any.whl", hash = "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a"},
|
||||
{file = "click-7.1.1.tar.gz", hash = "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc"},
|
||||
]
|
||||
colorama = [
|
||||
{file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"},
|
||||
{file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"},
|
||||
]
|
||||
cryptography = [
|
||||
{file = "cryptography-2.9-cp27-cp27m-macosx_10_9_intel.whl", hash = "sha256:ef9a55013676907df6c9d7dd943eb1770d014f68beaa7e73250fb43c759f4585"},
|
||||
{file = "cryptography-2.9-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:2a2ad24d43398d89f92209289f15265107928f22a8d10385f70def7a698d6a02"},
|
||||
@@ -782,10 +782,6 @@ dnspython = [
|
||||
email-validator = [
|
||||
{file = "email_validator-1.0.5-py2.py3-none-any.whl", hash = "sha256:e3e6ede1765d7c1e580d2050d834b2689361f7da2d50ce74df6a5968fca7cb13"},
|
||||
]
|
||||
faker = [
|
||||
{file = "Faker-4.0.2-py3-none-any.whl", hash = "sha256:b89aa33837498498e15c709eb40c31386408a901a53c7a5e12a425737a767976"},
|
||||
{file = "Faker-4.0.2.tar.gz", hash = "sha256:2d3f866ef25e1a5af80e7b0ceeacc3c92dec5d0fdbad3e2cb6adf6e60b22188f"},
|
||||
]
|
||||
fastapi = [
|
||||
{file = "fastapi-0.54.1-py3-none-any.whl", hash = "sha256:1ee9a49f28d510b62b3b51a9452b274853bfc9c5d4b947ed054366e2d49f9efa"},
|
||||
{file = "fastapi-0.54.1.tar.gz", hash = "sha256:72f40f47e5235cb5cbbad1d4f97932ede6059290c07e12e9784028dcd1063d28"},
|
||||
@@ -834,8 +830,8 @@ itsdangerous = [
|
||||
{file = "itsdangerous-1.1.0.tar.gz", hash = "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19"},
|
||||
]
|
||||
jinja2 = [
|
||||
{file = "Jinja2-2.11.1-py2.py3-none-any.whl", hash = "sha256:b0eaf100007721b5c16c1fc1eecb87409464edc10469ddc9a22a27a99123be49"},
|
||||
{file = "Jinja2-2.11.1.tar.gz", hash = "sha256:93187ffbc7808079673ef52771baa950426fd664d3aad1d0fa3e95644360e250"},
|
||||
{file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"},
|
||||
{file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"},
|
||||
]
|
||||
markupsafe = [
|
||||
{file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"},
|
||||
@@ -896,6 +892,10 @@ passlib = [
|
||||
promise = [
|
||||
{file = "promise-2.3.tar.gz", hash = "sha256:dfd18337c523ba4b6a58801c164c1904a9d4d1b1747c7d5dbf45b693a49d93d0"},
|
||||
]
|
||||
prompt-toolkit = [
|
||||
{file = "prompt_toolkit-3.0.5-py3-none-any.whl", hash = "sha256:df7e9e63aea609b1da3a65641ceaf5bc7d05e0a04de5bd45d05dbeffbabf9e04"},
|
||||
{file = "prompt_toolkit-3.0.5.tar.gz", hash = "sha256:563d1a4140b63ff9dd587bda9557cffb2fe73650205ab6f4383092fb882e7dc8"},
|
||||
]
|
||||
pycparser = [
|
||||
{file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"},
|
||||
{file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"},
|
||||
@@ -927,10 +927,6 @@ pymysql = [
|
||||
pypika = [
|
||||
{file = "PyPika-0.36.3.tar.gz", hash = "sha256:4fd6455955faa9d9016c2e39281a4808f82bbcf6065726f2c73e923a9e32b1bc"},
|
||||
]
|
||||
python-dateutil = [
|
||||
{file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"},
|
||||
{file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"},
|
||||
]
|
||||
python-dotenv = [
|
||||
{file = "python-dotenv-0.12.0.tar.gz", hash = "sha256:92b3123fb2d58a284f76cc92bfe4ee6c502c32ded73e8b051c4f6afc8b6751ed"},
|
||||
{file = "python_dotenv-0.12.0-py2.py3-none-any.whl", hash = "sha256:81822227f771e0cab235a2939f0f265954ac4763cafd806d845801c863bf372f"},
|
||||
@@ -1002,10 +998,6 @@ taskipy = [
|
||||
{file = "taskipy-1.2.1-py3-none-any.whl", hash = "sha256:99bdaf5b19791c2345806847147e0fc2d28e1ac9446058def5a8b6b3fc9f23e2"},
|
||||
{file = "taskipy-1.2.1.tar.gz", hash = "sha256:5eb2c3b1606c896c7fa799848e71e8883b880759224958d07ba760e5db263175"},
|
||||
]
|
||||
text-unidecode = [
|
||||
{file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"},
|
||||
{file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"},
|
||||
]
|
||||
toml = [
|
||||
{file = "toml-0.10.0-py2.7.egg", hash = "sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"},
|
||||
{file = "toml-0.10.0-py2.py3-none-any.whl", hash = "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"},
|
||||
@@ -1045,6 +1037,10 @@ uvloop = [
|
||||
{file = "uvloop-0.14.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:4315d2ec3ca393dd5bc0b0089d23101276778c304d42faff5dc4579cb6caef09"},
|
||||
{file = "uvloop-0.14.0.tar.gz", hash = "sha256:123ac9c0c7dd71464f58f1b4ee0bbd81285d96cdda8bc3519281b8973e3a461e"},
|
||||
]
|
||||
wcwidth = [
|
||||
{file = "wcwidth-0.1.9-py2.py3-none-any.whl", hash = "sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1"},
|
||||
{file = "wcwidth-0.1.9.tar.gz", hash = "sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1"},
|
||||
]
|
||||
websockets = [
|
||||
{file = "websockets-8.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c"},
|
||||
{file = "websockets-8.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170"},
|
||||
|
||||
@@ -17,11 +17,12 @@ passlib = "*"
|
||||
bcrypt = "*"
|
||||
pyjwt = "*"
|
||||
xlsxwriter = "*"
|
||||
colorama = "*"
|
||||
prompt_toolkit = "*"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
taskipy = "*"
|
||||
asynctest = "*"
|
||||
faker = "*"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry>=0.12"]
|
||||
|
||||
@@ -10,6 +10,7 @@ cffi==1.14.0
|
||||
chardet==3.0.4
|
||||
ciso8601==2.1.3; sys_platform != "win32" and implementation_name == "cpython"
|
||||
click==7.1.1
|
||||
colorama==0.4.3
|
||||
cryptography==2.9
|
||||
dnspython==1.16.0
|
||||
email-validator==1.0.5
|
||||
@@ -22,11 +23,11 @@ httptools==0.1.1; sys_platform != "win32" and sys_platform != "cygwin" and platf
|
||||
idna==2.9
|
||||
iso8601==0.1.12; sys_platform == "win32" or implementation_name != "cpython"
|
||||
itsdangerous==1.1.0
|
||||
jinja2==2.11.1
|
||||
jinja2==2.11.2
|
||||
markupsafe==1.1.1
|
||||
orjson==2.6.5
|
||||
passlib==1.7.2
|
||||
promise==2.3
|
||||
prompt-toolkit==3.0.5
|
||||
pycparser==2.20
|
||||
pydantic==1.4
|
||||
pyjwt==1.7.1
|
||||
@@ -45,5 +46,6 @@ ujson==2.0.3
|
||||
urllib3==1.25.8
|
||||
uvicorn==0.11.3
|
||||
uvloop==0.14.0
|
||||
wcwidth==0.1.9
|
||||
websockets==8.1
|
||||
xlsxwriter==1.2.8
|
||||
|
||||
Reference in New Issue
Block a user