diff --git a/CHANGELOG.md b/CHANGELOG.md index 3512fd0..59e26be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## 0.3 +### 0.3.2 + +- Hide menu when no children menu. +- Add custom filters. + ### 0.3.1 - Auto register permission. diff --git a/examples/filters.py b/examples/filters.py new file mode 100644 index 0000000..51e7256 --- /dev/null +++ b/examples/filters.py @@ -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" diff --git a/examples/main.py b/examples/main.py index 58c6751..e068ae9 100644 --- a/examples/main.py +++ b/examples/main.py @@ -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( diff --git a/fastapi_admin/__init__.py b/fastapi_admin/__init__.py index dcf4f71..0202887 100644 --- a/fastapi_admin/__init__.py +++ b/fastapi_admin/__init__.py @@ -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" diff --git a/fastapi_admin/depends.py b/fastapi_admin/depends.py index a093d2d..1fcdf24 100644 --- a/fastapi_admin/depends.py +++ b/fastapi_admin/depends.py @@ -1,5 +1,4 @@ import json -from typing import Optional import jwt from fastapi import Depends, HTTPException, Path, Query diff --git a/fastapi_admin/factory.py b/fastapi_admin/factory.py index 0448847..9a145c8 100644 --- a/fastapi_admin/factory.py +++ b/fastapi_admin/factory.py @@ -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(), diff --git a/fastapi_admin/filters.py b/fastapi_admin/filters.py new file mode 100644 index 0000000..96083f2 --- /dev/null +++ b/fastapi_admin/filters.py @@ -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) diff --git a/fastapi_admin/routes/rest.py b/fastapi_admin/routes/rest.py index e624719..babd24d 100644 --- a/fastapi_admin/routes/rest.py +++ b/fastapi_admin/routes/rest.py @@ -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: diff --git a/fastapi_admin/routes/site.py b/fastapi_admin/routes/site.py index 976b444..a443f08 100644 --- a/fastapi_admin/routes/site.py +++ b/fastapi_admin/routes/site.py @@ -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) diff --git a/fastapi_admin/site.py b/fastapi_admin/site.py index c871bcd..83f657c 100644 --- a/fastapi_admin/site.py +++ b/fastapi_admin/site.py @@ -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() diff --git a/pyproject.toml b/pyproject.toml index 5a01c98..e3bf45d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 "] license = "Apache-2.0"