add bulk actions support

This commit is contained in:
long2ice
2020-04-11 19:53:41 +08:00
parent eb7b0742d6
commit 51e5f7c043
16 changed files with 158 additions and 60 deletions

View File

@@ -173,10 +173,43 @@ Search Fields
~~~~~~~~~~~~~
Defined ``menu.search_fields`` in ``menu`` will render a search form by fields.
Excel Export
~~~~~~~~~~~~
Xlsx Export
~~~~~~~~~~~
FastAPI-admin can export searched data to excel file when define ``{export : True}`` in ``menu.actions``.
Bulk Actions
~~~~~~~~~~~~
Current FastAPI-admin support builtin bulk action ``delete_all``,if you want write your own bulk actions:
1. pass ``bulk_actions`` in ``Menu``,example:
.. code-block:: python
Menu(
...
bulk_actions=[{
'value': 'delete', # this is fastapi router path param.
'text': 'delete_all', # this will show in front.
}]
)
2. write fastapi route,example:
.. code-block:: python
from fastapi_admin.schemas import BulkIn
from fastapi_admin.factory import app as admin_app
@admin_app.post(
'/{resource}/bulk/delete' # delete is defined before.
)
async def bulk_delete(
bulk_in: BulkIn,
model=Depends(get_model)
):
await model.filter(pk__in=bulk_in.pk_list).delete()
return {'success': True}
Deployment
==========
1. Deploy fastapi app by gunicorn+uvicorn or reference https://fastapi.tiangolo.com/deployment/.

View File

@@ -50,7 +50,7 @@ def create_app():
url='/rest/App',
icon='fa fa-pencil',
sort_fields=('uaid',),
search_fields=('uaid',)
search_fields=('uaid',),
),
Menu(
name='多对多测试',
@@ -97,7 +97,8 @@ def create_app():
Menu(
name='请求日志',
url='/rest/ApiLog',
icon='fa fa-sticky-note'
icon='fa fa-sticky-note',
search_fields=('app',),
),
Menu(
name='App版本',

View File

@@ -1,3 +1,3 @@
from . import routes
__version__ = '0.1.7'
__version__ = '0.1.8'

View File

@@ -27,7 +27,7 @@ class QueryItem(BaseModel):
page: int = 1
sort: dict
where: dict
with_: dict
with_: dict = {}
size: int = 10
sort: dict = {}

View File

@@ -1,5 +1,5 @@
import importlib
from typing import Type
from typing import Type, List, Dict
from fastapi import FastAPI
from tortoise import Model, Tortoise
@@ -26,7 +26,7 @@ class AdminApp(FastAPI):
'SmallIntField': 'number',
'JSONField': 'json',
}
model_menu_mapping = {}
model_menu_mapping: Dict[str, Menu] = {}
def _get_model_menu_mapping(self):
for menu in filter(lambda x: x.url, self.site.menus):
@@ -179,7 +179,8 @@ class AdminApp(FastAPI):
title=model_describe.get('description') or resource.title(),
fields=fields,
searchFields=search_fields,
pk=pk
pk=pk,
bulk_actions=self.model_menu_mapping[resource].bulk_actions,
)

View File

@@ -10,6 +10,7 @@ from ..common import handle_m2m_fields_create_or_update
from ..depends import QueryItem, get_query, parse_body, get_model
from ..factory import app
from ..responses import GetManyOut
from ..schemas import BulkIn
from ..shortcuts import get_object_or_404
router = APIRouter()
@@ -73,6 +74,17 @@ async def view(
return resource.dict(by_alias=True, exclude_unset=True)
@router.post(
'/{resource}/bulk/delete'
)
async def bulk_delete(
bulk_in: BulkIn,
model=Depends(get_model)
):
await model.filter(pk__in=bulk_in.pk_list).delete()
return {'success': True}
@router.delete(
'/{resource}/{id}'
)

View File

@@ -1,3 +1,5 @@
from typing import List
from fastapi import Body
from pydantic import BaseModel
@@ -5,3 +7,7 @@ from pydantic import BaseModel
class LoginIn(BaseModel):
username: str = Body(..., example='long2ice')
password: str = Body(..., example='123456')
class BulkIn(BaseModel):
pk_list: List = Body(..., example=[1, 2, 3])

View File

@@ -23,11 +23,12 @@ class Menu(BaseModel):
# define field type,like select,radiolist,text,date
fields_type: Dict = {}
actions: Dict = {
'toolbar': {
'delete_all': True
},
'export': True
}
bulk_actions: List[Dict] = [{
'value': 'delete',
'text': 'delete_all',
}]
class Site(BaseModel):
@@ -72,6 +73,7 @@ class Resource(BaseModel):
pk: str
resource_fields: Dict[str, Union[Field, Dict]]
searchFields: Optional[Dict[str, Field]]
bulk_actions: Optional[List[Dict]]
class Config:
fields = {

View File

@@ -1,6 +1,8 @@
{
"actions": {
"select_all": "Select all",
"clear_selected": "Clear selected",
"delete_all": "Delete all"
},
"messages": {
"paginate": "Total: {total}",
@@ -9,10 +11,10 @@
"deleted_all": "All deleted",
"confirm_delete": "Are you sure you want to delete?",
"image_size": "Size limit: {width}x{height}",
"confirm_delete_all": "Are you sure you want to delete all data?",
"no_options_text": "No Options Here!"
"confirm_bulk_action": "Are you sure you want to do bulk action?",
"no_options_text": "No Options Here!",
"bulk_success": "Bulk action success!"
},
"errors": {
}
}
}

View File

@@ -16,10 +16,11 @@
"change": "更换",
"choose": "选择",
"login": "登录",
"edit": "编辑"
"edit": "编辑",
"select_all": "选择全部",
"clear_selected": "清除选择"
},
"texts": {
},
"fields": {
"username": "用户名",
@@ -38,8 +39,9 @@
"deleted_all": "全部删除成功",
"confirm_delete": "确定要删除吗?",
"image_size": "尺寸要求:{width}x{height}",
"confirm_delete_all": "此次操作不可恢复,确定要全部删除吗",
"no_options_text": "暂无可选项"
"confirm_bulk_action": "确定执行批量操作",
"no_options_text": "暂无可选项",
"bulk_success": "批量操作成功!"
},
"errors": {
"too_large": "请上传小于{limit}KB的文件",

View File

@@ -1,62 +1,71 @@
/**
* See https://bootswatch.com/
* Change `lumen` to any other words blow:
*
*
* cerulean darkly litera materia sandstone slate superhero
* cosmo flatly lumen minty simplex solar united
* cyborg journal lux pulse sketchy spacelab yeti
*/
// @import "node_modules/bootswatch/dist/lumen/bootstrap";
.app-footer{
border-top: none;
color: #666;
}
.card {
.app-footer {
border-top: none;
color: #666;
}
.card {
border: none;
box-shadow: 0 0 5px #ccc;
}
.card-header{
border:none;
.card-header {
border: none;
font-weight: bold;
}
.card-footer{
.card-footer {
border: none;
}
.nav-tabs .nav-link {
border-color:transparent !important;
border-color: transparent !important;
border-radius: 4px;
margin: 0 0.2em;
padding: 0.3em 1em !important;
font-weight: bold;
}
.data-form .tabs .nav-tabs {
margin: 0 1em 1em 1em;
border:none;
}
.nav-tabs .nav-link.active{
border-color:#ccc !important;
border-radius: 2em;
}
.tab-content{
border-color:transparent;
}
.btn{
.data-form .tabs .nav-tabs {
margin: 0 1em 1em 1em;
border: none;
}
.nav-tabs .nav-link.active {
border-color: #ccc !important;
border-radius: 2em;
}
.tab-content {
border-color: transparent;
}
.btn {
padding: 0.5em 1.2em;
// border-radius: 2em;
//border-radius: 2em;
}
.table th, .table td {
border-top: 1px solid #dee2e65c;
}
.table thead th {
vertical-align: bottom;
border-bottom: 2px solid #dee2e6;
vertical-align: bottom;
border-bottom: 2px solid #dee2e6;
}
.table thead th {
border-bottom: 1px solid #dee2e6;
}

View File

@@ -30,15 +30,6 @@
v-bind="button"
>{{button.label}}
</b-btn>
<b-btn
@click="removeAll"
class="pull-right"
variant="second"
v-if="_.get(actions, 'toolbar.delete_all') === true"
>
<i class="icon-trash"></i>
{{$t('actions.delete_all')}}
</b-btn>
</div>
</div>
<div class>
@@ -88,6 +79,9 @@
ref="table"
:items="fetchItems"
:fields="columns"
selectable
@row-selected="onRowSelected"
:select-mode="selectModel"
:current-page="currentPage"
:sort-by.sync="sortBy"
:sort-desc.sync="sortDesc"
@@ -140,7 +134,12 @@
</b-btn>
</template>
</b-table>
<div class="form-inline my-2">
<b-button class="mr-1" @click="selectAllRows">{{ $t("actions.select_all") }}</b-button>
<b-button class="mr-1" @click="clearSelected">{{ $t("actions.clear_selected")}}</b-button>
<b-form-select class="mr-1" v-model="selectBulkAction" :options="bulkActions"></b-form-select>
<b-button @click="submitBulk" variant="primary">{{ $t("actions.submit")}}</b-button>
</div>
<div class="row align-items-center">
<div class="col-md-10">
<b-pagination
@@ -170,6 +169,8 @@
init: false,
loaded: false,
table: {},
modes: ['multi', 'single', 'range'],
selectModel: 'range',
total: 0, //total rows
pageLimit: 10, //display how many page buttons
currentPage: 1,
@@ -179,6 +180,9 @@
perPage: 10,
where: {},
pk: null,
selected_pk_list: [],
bulkActions: {},
selectBulkAction: null
};
},
watch: {
@@ -221,10 +225,30 @@
}
},
methods: {
submitBulk() {
if (window.confirm(this.$t("messages.confirm_bulk_action"))) {
this.$http.post(this.uri + '/bulk/' + this.selectBulkAction, {
pk_list: this.selected_pk_list
}).then(() => {
this.$snotify.success(this.$t("messages.bulk_success"));
this.fetch();
});
}
},
selectAllRows() {
this.$refs.table.selectAllRows()
},
onRowSelected(items) {
this.selected_pk_list = _.map(items, item => {
return item[this.pk];
})
},
clearSelected() {
this.$refs.table.clearSelected()
},
doSearch(params) {
this.where = _.omitBy(params, v => v === null);
this.$refs.table.refresh();
// console.log(params);
},
searchAndExport() {
const query = JSON.stringify({
@@ -300,6 +324,12 @@
fetch() {
this.init = false;
this.$http.get(this.uri + "/grid").then(res => {
_.mapValues(res.data.bulk_actions, action => {
action.text = this.$t(`actions.${action.text}`)
});
this.bulkActions = res.data.bulk_actions;
_.mapValues(res.data.fields, field => {
field.thClass = "bg-light";
});

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 KiB

After

Width:  |  Height:  |  Size: 271 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 256 KiB

After

Width:  |  Height:  |  Size: 389 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 168 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 212 KiB

After

Width:  |  Height:  |  Size: 280 KiB