add auto pk field mapping

This commit is contained in:
long2ice
2020-04-11 15:03:21 +08:00
parent 3cdfdd2b36
commit eb7b0742d6
10 changed files with 255 additions and 248 deletions

View File

@ -85,7 +85,10 @@ def create_app():
Menu(
name='在线参数',
url='/rest/Config',
icon='fa fa-cog'
icon='fa fa-cog',
actions={
'delete': False
}
),
Menu(
name='基本信息',

View File

@ -75,7 +75,7 @@ class AdminApp(FastAPI):
return field_type
async def _build_resource_from_model_describe(self, resource: str, model: Type[Model], model_describe: dict,
exclude_pk: bool, exclude_m2m_field=True):
exclude_pk: bool, exclude_m2m_field=True, exclude_actions=False):
"""
build resource
:param resource:
@ -94,7 +94,7 @@ class AdminApp(FastAPI):
search_fields = menu.search_fields
sort_fields = menu.sort_fields
fields = {}
name = pk_field.get('name')
pk = name = pk_field.get('name')
if not exclude_pk and not self._exclude_field(resource, name):
fields = {
name: Field(
@ -104,8 +104,8 @@ class AdminApp(FastAPI):
sortable=name in sort_fields
)
}
fields['_actions'] = menu.actions
if not exclude_actions:
fields['_actions'] = menu.actions
for data_field in data_fields:
readonly = data_field.get('constraints').get('readOnly')
@ -166,18 +166,20 @@ class AdminApp(FastAPI):
options=options,
multiple=True,
)
return fields, search_fields_ret
return pk, fields, search_fields_ret
async def get_resource(self, resource: str, exclude_pk=False, exclude_m2m_field=True):
async def get_resource(self, resource: str, exclude_pk=False, exclude_m2m_field=True, exclude_actions=False):
assert self._inited, 'must call init() first!'
model = getattr(self.models, resource) # type:Type[Model]
model_describe = Tortoise.describe_model(model)
fields, search_fields = await self._build_resource_from_model_describe(resource, model, model_describe,
exclude_pk, exclude_m2m_field)
pk, fields, search_fields = await self._build_resource_from_model_describe(resource, model, model_describe,
exclude_pk, exclude_m2m_field,
exclude_actions)
return Resource(
title=model_describe.get('description') or resource.title(),
fields=fields,
searchFields=search_fields
searchFields=search_fields,
pk=pk
)

View File

@ -49,7 +49,7 @@ async def get_resource(
async def form(
resource: str,
):
resource = await app.get_resource(resource, exclude_pk=True, exclude_m2m_field=False)
resource = await app.get_resource(resource, exclude_pk=True, exclude_m2m_field=False, exclude_actions=True)
return resource.dict(by_alias=True, exclude_unset=True)
@ -142,4 +142,5 @@ async def get_one(
relate_model = getattr(obj, m2m_field) # type:ManyToManyRelation
ids = await relate_model.all().values_list(relate_model.model._meta.pk_attr)
ret[m2m_field] = list(map(lambda x: x[0], ids))
ret['__str__'] = str(obj)
return ret

View File

@ -69,6 +69,7 @@ class Field(BaseModel):
class Resource(BaseModel):
title: str
pk: str
resource_fields: Dict[str, Union[Field, Dict]]
searchFields: Optional[Dict[str, Field]]

View File

@ -3,13 +3,11 @@
# Change if you put the built files to a sub directory. e.g. /admin/
PRODUCTION_BASE_URL=/
# Your backend restful api base url, end with slash `/`
# Your backend restful api base url, end with slash `/`
# for example
# VUE_APP_API_URL=http://localhost:3333/admin/api/
# this is the built-in test api server, use `npm run test-api` to run it.
VUE_APP_API_URL=http://localhost:8088/admin/api/
# The primary key field name in you database, e.g: mongodb use `_id`
VUE_APP_PRIMARY_KEY=_id

View File

@ -1,9 +1,7 @@
# Change if you put the built files to a sub directory. e.g. /admin/
BASE_URL=/admin/
# Your backend restful api base url, end with slash `/`
# Your backend restful api base url, end with slash `/`
VUE_APP_API_URL=/admin/api/
# The primary key field name in you database, e.g: mongodb use `_id`
VUE_APP_PRIMARY_KEY=_id

View File

@ -4,8 +4,9 @@
<div class="row d-none">
<div class="col col-md-8">
<legend
v-if="model[$config.primaryKey] && false"
>{{$t('actions.edit')}}: {{model[$config.primaryKey]}}</legend>
v-if="model[pk] && false"
>{{$t('actions.edit')}}: {{model[pk]}}
</legend>
</div>
<div class="col col-md-4 text-right hidden-sm-down">
<b-btn @click="$router.go(-1)">{{$t('actions.back')}}</b-btn>
@ -30,136 +31,138 @@
</template>
<script>
import { mapState, mapGetters } from "vuex";
import _ from "lodash";
import {mapState, mapGetters} from "vuex";
import _ from "lodash";
export default {
components: {},
props: {
resource: {
type: String,
required: true
export default {
components: {},
props: {
resource: {
type: String,
required: true
},
id: {
default: "",
required: true
},
formPath: {
type: String,
default: "form",
required: false
}
},
id: {
default: "",
required: true
},
formPath: {
type: String,
default: "form",
required: false
}
},
data() {
return {
redirect: -1,
loaded: false,
choices: {},
fields: {},
model: {},
errors: [],
tag: "b-card",
header: `
data() {
return {
redirect: -1,
loaded: false,
choices: {},
fields: {},
model: {},
errors: [],
pk: null,
tag: "b-card",
header: `
${_.get(this.currentMenu, "name", "") || ""}
<small> ${this.resource.toUpperCase()} </small>
`
};
},
watch: {
id: "fetchForm",
"site.fetched"(val){
if (val) {
this.fetchForm(true)
}
};
},
},
computed: {
resourceUri() {
let url = [this.site.resource_prefix, this.resource, this.id]
.filter(v => v)
.join("/");
let group = this.$route.params.group;
if (group) {
url += "?group=" + group;
}
return url;
watch: {
id: "fetchForm",
"site.fetched"(val) {
if (val) {
this.fetchForm(true)
}
},
},
formUri() {
let url = [this.site.resource_prefix, this.resource, this.formPath]
.filter(v => v)
.join("/");
url += "?id=" + (this.id || "");
return url;
computed: {
resourceUri() {
let url = [this.site.resource_prefix, this.resource, this.id]
.filter(v => v)
.join("/");
let group = this.$route.params.group;
if (group) {
url += "?group=" + group;
}
return url;
},
formUri() {
let url = [this.site.resource_prefix, this.resource, this.formPath]
.filter(v => v)
.join("/");
url += "?id=" + (this.id || "");
return url;
},
isNew() {
return !this.id;
},
method() {
return this.isNew ? "post" : "put";
},
with() {
return _.filter(
_.map(this.fields, v => v.ref && v.ref.split(".").shift())
);
},
...mapState(["nav", "auth", "site"]),
...mapGetters(["currentMenu"])
},
isNew() {
return !this.id;
},
method() {
return this.isNew ? "post" : "put";
},
with() {
return _.filter(
_.map(this.fields, v => v.ref && v.ref.split(".").shift())
);
},
...mapState(["nav", "auth", "site"]),
...mapGetters(["currentMenu"])
},
methods: {
fetch() {
if (this.isNew) {
this.model = {};
this.loaded = true;
return;
}
this.$http
.get(this.resourceUri, {
params: {
query: {
with: this.with
}
}
})
.then(({ data }) => {
methods: {
fetch() {
if (this.isNew) {
this.model = {};
this.loaded = true;
this.model = data;
});
},
fetchForm() {
this.$http
.get(this.formUri, {
params: this.$route.params
})
.then(({ data }) => {
this.fields = data.fields;
this.layout = data.layout;
this.redirect = data.redirect;
if (data.header) {
this.header = data.header;
}
if (data.tag) {
this.tag = data.tag;
}
this.fetch();
});
},
return;
}
this.$http
.get(this.resourceUri, {
params: {
query: {
with: this.with
}
}
})
.then(({data}) => {
this.loaded = true;
this.model = data;
});
},
fetchForm() {
this.$http
.get(this.formUri, {
params: this.$route.params
})
.then(({data}) => {
this.fields = data.fields;
this.pk = data.pk;
this.layout = data.layout;
this.redirect = data.redirect;
if (data.header) {
this.header = data.header;
}
if (data.tag) {
this.tag = data.tag;
}
this.fetch();
});
},
onSuccess() {
if (this.redirect === false) {
this.fetchForm();
} else if (this.redirect === -1 || !this.redirect) {
this.$router.go(-1);
} else {
this.$router.go(this.redirect);
onSuccess() {
if (this.redirect === false) {
this.fetchForm();
} else if (this.redirect === -1 || !this.redirect) {
this.$router.go(-1);
} else {
this.$router.go(this.redirect);
}
}
},
mounted() {
this.site.fetched && this.fetchForm()
},
created() {
// this.fetchForm();
}
},
mounted() {
this.site.fetched && this.fetchForm()
},
created() {
// this.fetchForm();
}
};
};
</script>

View File

@ -1,24 +1,24 @@
<template>
<div class="page-view">
<div class="data-view">
<legend v-if="model[$config.primaryKey]">{{$t('actions.view')}}: {{model[$config.primaryKey]}}</legend>
<legend v-if="model['__str__']">{{$t('actions.view')}}: {{model['__str__']}}</legend>
<table class="table ">
<tbody>
<tr v-for="(field, key) in fields" :key="key">
<th style="min-width:120px">{{field.label || key}}</th>
<td>
<div v-if="['array'].includes(field.type)">
<b-table :items="model[key]" :fields="field.fields">
<template v-for="(child, k) in field.fields" :slot="k" slot-scope="row">
<b-data-value :lang="currentLanguage" :field="child" :name="k" :key="k" :model="row.item" />
</template>
</b-table>
</div>
<div v-else>
<b-data-value :lang="currentLanguage" :field="field" :name="key" :model="model" />
</div>
</td>
</tr>
<tr v-for="(field, key) in fields" :key="key">
<th style="min-width:120px">{{field.label || key}}</th>
<td>
<div v-if="['array'].includes(field.type)">
<b-table :items="model[key]" :fields="field.fields">
<template v-for="(child, k) in field.fields" :slot="k" slot-scope="row">
<b-data-value :lang="currentLanguage" :field="child" :name="k" :key="k" :model="row.item"/>
</template>
</b-table>
</div>
<div v-else>
<b-data-value :lang="currentLanguage" :field="field" :name="key" :model="model"/>
</div>
</td>
</tr>
</tbody>
</table>
</div>
@ -29,103 +29,103 @@
</template>
<script>
import BDataValue from "./DataValue";
import _ from 'lodash'
import BDataValue from "./DataValue";
import _ from 'lodash'
import { mapState, mapGetters } from 'vuex'
export default {
components: {
BDataValue
},
props: {
resource: {
type: String,
required: true
import {mapGetters, mapState} from 'vuex'
export default {
components: {
BDataValue
},
id: {
default: '',
required: true
props: {
resource: {
type: String,
required: true
},
id: {
default: '',
required: true
},
viewPath: {
type: String,
default: "view",
required: false
},
},
viewPath: {
type: String,
default: "view",
required: false
data() {
return {
choices: {},
fields: {},
model: {},
errors: [],
items: [],
pk: null
};
},
},
data() {
return {
choices: {},
fields: {},
model: {},
errors: [],
items: []
};
},
computed: {
resourceUri() {
return [this.site.resource_prefix, this.resource, this.id]
.filter(v => v)
.join("/");
},
viewUri() {
let url = [this.site.resource_prefix, this.resource, this.viewPath]
.filter(v => v)
.join("/");
url += "?id=" + (this.id || "");
return url;
},
with() {
return _.filter(
_.map(this.fields, (v) => v.ref && v.ref.replace(/\.\w+$/, ''))
);
},
computed: {
resourceUri() {
let url = [this.site.resource_prefix, this.resource, this.id]
.filter(v => v)
.join("/");
return url;
},
viewUri() {
let url = [this.site.resource_prefix, this.resource, this.viewPath]
.filter(v => v)
.join("/");
url += "?id=" + (this.id || "");
return url;
},
with() {
return _.filter(
_.map(this.fields, (v) => v.ref && v.ref.replace(/\.\w+$/, ''))
);
},
...mapState(['site']),
...mapGetters(["currentMenu", "currentLanguage"]),
header() {
return `
...mapState(['site']),
...mapGetters(["currentMenu", "currentLanguage"]),
header() {
return `
''
<small> ${this.resource.toUpperCase()} </small>
`
},
},
},
methods: {
fetch() {
this.$http.get(this.resourceUri, {
params: {
query: { with: this.with }
methods: {
fetch() {
this.$http.get(this.resourceUri, {
params: {
query: {with: this.with}
}
}).then(({data}) => {
this.model = data;
});
},
fetchView() {
this.$http.get(this.viewUri).then(({data}) => {
this.fields = data.fields;
this.pk = data.pk;
delete this.fields._actions
this.fetch();
});
},
},
watch: {
id: "fetchForm",
"site.fetched"(val) {
if (val) {
this.fetchView(true)
}
}).then(({ data }) => {
this.model = data;
});
},
},
fetchView() {
this.$http.get(this.viewUri).then(({ data }) => {
this.fields = data.fields;
delete this.fields._actions
this.fetch();
});
mounted() {
},
},
watch: {
id: "fetchForm",
"site.fetched"(val){
if (val) {
this.fetchView(true)
}
},
},
mounted() {
},
created() {
this.site.fetched && this.fetchView();
}
};
created() {
this.site.fetched && this.fetchView();
}
};
</script>

View File

@ -21,8 +21,8 @@ import { mapState } from "vuex";
export default {
components: {},
props: {
},
data() {
return {
@ -43,7 +43,7 @@ export default {
},
methods: {
fetch() {
},
fetchForm() {
this.$http.get(this.uri).then(({ data }) => {

View File

@ -120,7 +120,7 @@
v-if="actions.edit !== false"
variant="success"
size="sm"
:to="`/rest/${uri}/${row.item[$config.primaryKey]}`"
:to="`/rest/${uri}/${row.item[pk]}`"
class="mr-1"
>{{$t('actions.view')}}
</b-btn>
@ -128,14 +128,14 @@
v-if="actions.edit !== false"
variant="primary"
size="sm"
:to="`/rest/${uri}/${row.item[$config.primaryKey]}/edit`"
:to="`/rest/${uri}/${row.item[pk]}/edit`"
class="mr-1"
>{{$t('actions.edit')}}
</b-btn>
<b-btn
v-if="actions.delete !== false"
size="sm"
@click.stop="remove(row.item[$config.primaryKey])"
@click.stop="remove(row.item[pk])"
>{{$t('actions.delete')}}
</b-btn>
</template>
@ -173,12 +173,12 @@
total: 0, //total rows
pageLimit: 10, //display how many page buttons
currentPage: 1,
sortBy: this.$config.primaryKey,
sortBy: this.pk,
sortDesc: true,
sortDirection: null,
perPage: 10,
where: {},
iframeSrc: ""
pk: null,
};
},
watch: {
@ -249,7 +249,7 @@
sortBy && (this.sortBy = sortBy);
if (sortDesc) {
this.sortDesc = sortDesc === -1 ? true : false;
this.sortDesc = sortDesc === -1;
}
this.total = page * this.perPage;
this.currentPage = page;
@ -305,6 +305,7 @@
});
this.table = res.data;
this.pk = res.data.pk;
if (_.get(this.table, "fields._actions") !== false) {
_.set(