mirror of
https://github.com/fastapi-admin/fastapi-admin.git
synced 2026-03-13 10:32:25 +08:00
Merge remote-tracking branch 'origin/dev' into dev
# Conflicts: # examples/main.py
This commit is contained in:
@@ -2,6 +2,11 @@
|
||||
|
||||
## 0.3
|
||||
|
||||
### 0.3.2
|
||||
|
||||
- Hide menu when no children menu.
|
||||
- Add custom filters.
|
||||
|
||||
### 0.3.1
|
||||
|
||||
- Auto register permission.
|
||||
|
||||
28
examples/filters.py
Normal file
28
examples/filters.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from typing import Any
|
||||
|
||||
from tortoise import QuerySet
|
||||
from tortoise.query_utils import Q
|
||||
|
||||
from fastapi_admin.filters import Filter, SearchFilter, register_filter
|
||||
from fastapi_admin.site import Field
|
||||
|
||||
|
||||
class CustomFilter(Filter):
|
||||
@classmethod
|
||||
def get_queryset(cls, queryset: QuerySet) -> QuerySet:
|
||||
return queryset.filter(~Q(key="test"))
|
||||
|
||||
|
||||
@register_filter
|
||||
class LikeFilter(SearchFilter):
|
||||
@classmethod
|
||||
def get_queryset(cls, queryset: QuerySet, value: Any) -> QuerySet:
|
||||
return queryset.filter(name__icontains=value)
|
||||
|
||||
@classmethod
|
||||
async def get_field(cls) -> Field:
|
||||
return Field(label="NameLike", type="text")
|
||||
|
||||
@classmethod
|
||||
def get_name(cls) -> str:
|
||||
return "filter"
|
||||
@@ -3,14 +3,11 @@ import os
|
||||
import uvicorn
|
||||
from fastapi import Depends, FastAPI
|
||||
from starlette.middleware.cors import CORSMiddleware
|
||||
from starlette.requests import Request
|
||||
from starlette.templating import Jinja2Templates
|
||||
from tortoise.contrib.fastapi import register_tortoise
|
||||
from tortoise.contrib.pydantic import pydantic_queryset_creator
|
||||
from tortoise.exceptions import IntegrityError
|
||||
|
||||
from examples.common import get_client_ip
|
||||
from examples.models import Visitor
|
||||
from examples.filters import CustomFilter, LikeFilter
|
||||
from fastapi_admin.depends import get_model
|
||||
from fastapi_admin.factory import app as admin_app
|
||||
from fastapi_admin.schemas import BulkIn
|
||||
@@ -56,18 +53,6 @@ def create_app():
|
||||
app = create_app()
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
async def add_process_time_header(request: Request, call_next):
|
||||
ip = get_client_ip(request)
|
||||
v = await Visitor.get_or_none(ip=ip)
|
||||
try:
|
||||
await v.save() if v else await Visitor.create(ip=ip)
|
||||
except IntegrityError:
|
||||
pass
|
||||
response = await call_next(request)
|
||||
return response
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
async def start_up():
|
||||
await admin_app.init( # nosec
|
||||
@@ -90,7 +75,7 @@ async def start_up():
|
||||
name="Category",
|
||||
url="/rest/Category",
|
||||
icon="fa fa-list",
|
||||
search_fields=("slug",),
|
||||
search_fields=("slug", LikeFilter),
|
||||
),
|
||||
Menu(
|
||||
name="Config",
|
||||
@@ -98,6 +83,7 @@ async def start_up():
|
||||
icon="fa fa-gear",
|
||||
import_=True,
|
||||
search_fields=("key",),
|
||||
custom_filters=[CustomFilter],
|
||||
),
|
||||
Menu(
|
||||
name="Product",
|
||||
@@ -105,9 +91,6 @@ async def start_up():
|
||||
icon="fa fa-table",
|
||||
search_fields=("name",),
|
||||
),
|
||||
Menu(
|
||||
name="Visitor", url="/rest/Visitor", icon="fa fa-child", exclude=("ip",)
|
||||
),
|
||||
],
|
||||
),
|
||||
Menu(
|
||||
|
||||
@@ -5,4 +5,4 @@ def version():
|
||||
# with open("pyproject.toml") as f:
|
||||
# ret = re.findall(r'version = "(\d+\.\d+\.\d+)"', f.read())
|
||||
# return ret[0]
|
||||
return "0.3.1"
|
||||
return "0.3.2"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import json
|
||||
from typing import Optional
|
||||
|
||||
import jwt
|
||||
from fastapi import Depends, HTTPException, Path, Query
|
||||
|
||||
@@ -8,6 +8,7 @@ from tortoise import Model
|
||||
from . import enums
|
||||
from .common import get_all_models, import_obj, pwd_context
|
||||
from .exceptions import exception_handler
|
||||
from .filters import SearchFilter
|
||||
from .models import AbstractAdminLog, AbstractPermission, AbstractRole, AbstractUser
|
||||
from .schemas import LoginIn
|
||||
from .shortcuts import get_object_or_404
|
||||
@@ -244,6 +245,11 @@ class AdminApp(FastAPI):
|
||||
sort_fields = menu.sort_fields
|
||||
fields = {}
|
||||
pk = name = pk_field.get("name")
|
||||
# CustomSearchFilters
|
||||
for search_filter in filter(
|
||||
lambda x: type(x).__name__ == "type" and issubclass(x, SearchFilter), search_fields
|
||||
):
|
||||
search_fields_ret[search_filter.get_name()] = await search_filter.get_field()
|
||||
if not exclude_pk and not self._exclude_field(resource, name):
|
||||
field = Field(
|
||||
label=pk_field.get("name").title(),
|
||||
|
||||
36
fastapi_admin/filters.py
Normal file
36
fastapi_admin/filters.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from typing import Any
|
||||
|
||||
from tortoise import QuerySet
|
||||
|
||||
from fastapi_admin.site import Field
|
||||
|
||||
_search_filters = {}
|
||||
|
||||
|
||||
class Filter:
|
||||
@classmethod
|
||||
def get_queryset(cls, queryset: QuerySet) -> QuerySet:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class SearchFilter:
|
||||
@classmethod
|
||||
def get_queryset(cls, queryset: QuerySet, option: Any) -> QuerySet:
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
async def get_field(cls) -> Field:
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def get_name(cls) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def register_filter(cls: SearchFilter):
|
||||
_search_filters[cls.get_name()] = cls
|
||||
return cls
|
||||
|
||||
|
||||
def get_filter_by_name(name: str):
|
||||
return _search_filters.get(name)
|
||||
@@ -27,6 +27,7 @@ from ..depends import (
|
||||
update_checker,
|
||||
)
|
||||
from ..factory import app
|
||||
from ..filters import get_filter_by_name
|
||||
from ..responses import GetManyOut
|
||||
from ..schemas import BulkIn
|
||||
from ..shortcuts import get_object_or_404
|
||||
@@ -83,8 +84,15 @@ async def get_resource(
|
||||
):
|
||||
menu = app.model_menu_mapping[resource]
|
||||
qs = model.all()
|
||||
for filter_ in menu.custom_filters:
|
||||
qs = filter_.get_queryset(qs)
|
||||
if query.where:
|
||||
qs = qs.filter(**query.where)
|
||||
for name, value in query.where.items():
|
||||
filter_cls = get_filter_by_name(name)
|
||||
if filter_cls:
|
||||
qs = filter_cls.get_queryset(qs, value)
|
||||
else:
|
||||
qs = qs.filter(**{name: value})
|
||||
sort = query.sort
|
||||
for k, v in sort.items():
|
||||
if k in menu.sort_fields:
|
||||
|
||||
@@ -30,4 +30,5 @@ async def site(user_id=Depends(jwt_optional)):
|
||||
model = child.url.split("/")[-1]
|
||||
if not await check_has_permission(user, model):
|
||||
site_copy.menus[index].children.remove(child)
|
||||
site_copy.menus = list(filter(lambda x: x.children != [], site_copy.menus))
|
||||
return site_copy.dict(by_alias=True, exclude_unset=True)
|
||||
|
||||
@@ -19,7 +19,7 @@ class Menu(BaseModel):
|
||||
# raw id fields
|
||||
raw_id_fields: Optional[Tuple[str, ...]] = tuple()
|
||||
# searchable fields
|
||||
search_fields: Optional[Tuple[str, ...]] = tuple()
|
||||
search_fields = tuple()
|
||||
# sortable fields
|
||||
sort_fields: Optional[Tuple[str, ...]] = tuple()
|
||||
# define field type,like select,radiolist,text,date
|
||||
@@ -31,6 +31,7 @@ class Menu(BaseModel):
|
||||
import_: bool = False
|
||||
actions: Optional[Dict]
|
||||
bulk_actions: List[Dict] = [{"value": "delete", "text": "delete_all"}]
|
||||
custom_filters: List = []
|
||||
|
||||
|
||||
Menu.update_forward_refs()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "fastapi-admin"
|
||||
version = "0.3.1"
|
||||
version = "0.3.2"
|
||||
description = "Fast Admin Dashboard based on fastapi and tortoise-orm."
|
||||
authors = ["long2ice <long2ice@gmail.com>"]
|
||||
license = "Apache-2.0"
|
||||
|
||||
Reference in New Issue
Block a user