Skip to content

Commit aabcea7

Browse files
Use evaluate() function with Component/Function types (#757)
1 parent 1835531 commit aabcea7

18 files changed

+296
-223
lines changed

.pre-commit-config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ repos:
44
hooks:
55
- id: check-merge-conflict
66
- repo: https://github.com/asottile/yesqa
7-
rev: v1.4.0
7+
rev: v1.5.0
88
hooks:
99
- id: yesqa
1010
additional_dependencies: ["flake8-bandit", "flake8-bugbear"]
@@ -38,7 +38,7 @@ repos:
3838
- id: detect-private-key
3939
exclude: ^examples/
4040
- repo: https://github.com/PyCQA/flake8
41-
rev: '6.0.0'
41+
rev: '6.1.0'
4242
hooks:
4343
- id: flake8
4444
exclude: "^docs/"

admin-js/src/App.js

Lines changed: 48 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,7 @@ const COMPONENTS = {
6565
BooleanInput, DateInput, DateTimeInput, NumberInput, ReferenceInput, SelectInput,
6666
TextInput, TimeInput
6767
};
68-
const USER_FUNCS = {};
69-
const VALIDATORS = {email, maxLength, maxValue, minLength, minValue, regex, required};
68+
const FUNCTIONS = {email, maxLength, maxValue, minLength, minValue, regex, required};
7069
const _body = document.querySelector("body");
7170
const STATE = Object.freeze(JSON.parse(_body.dataset.state));
7271

@@ -76,8 +75,7 @@ if (STATE["js_module"]) {
7675
// browser's import() function. Needed to dynamically import a module.
7776
MODULE_LOADER = import(/* webpackIgnore: true */ STATE["js_module"]).then((mod) => {
7877
Object.assign(COMPONENTS, mod.components);
79-
Object.assign(VALIDATORS, mod.validators);
80-
Object.assign(USER_FUNCS, mod.funcs);
78+
Object.assign(FUNCTIONS, mod.functions);
8179
});
8280
} else {
8381
MODULE_LOADER = Promise.resolve();
@@ -157,46 +155,58 @@ const authProvider = {
157155
},
158156
};
159157

158+
function evaluate(obj) {
159+
if (obj === null || obj === undefined)
160+
return obj;
161+
if (Array.isArray(obj))
162+
return obj.map(evaluate);
163+
if (obj["__type__"] === "component") {
164+
const C = COMPONENTS[obj["type"]];
165+
if (C === undefined)
166+
throw Error(`Unknown component '${obj["type"]}'`);
167+
168+
let {children, ...props} = obj["props"];
169+
props = Object.fromEntries(Object.entries(props).map(([k, v]) => [k, evaluate(v)]));
170+
if (children)
171+
return <C {...props}>{evaluate(children)}</C>;
172+
return <C {...props} />;
173+
}
174+
if (obj["__type__"] === "function") {
175+
const f = FUNCTIONS[obj["name"]];
176+
if (f === undefined)
177+
throw Error(`Unknown function '${obj["name"]}'`);
178+
if (obj["args"] === null)
179+
return f;
180+
return f(...evaluate(obj["args"]));
181+
}
182+
if (obj["__type__"] === "regexp")
183+
return new RegExp(obj["value"]);
184+
return obj;
185+
}
186+
160187

161-
function createFields(resource, name, permissions) {
188+
function createFields(fields, name, permissions) {
162189
let components = [];
163-
for (const [field, state] of Object.entries(resource["fields"])) {
190+
for (const [field, state] of Object.entries(fields)) {
164191
if (!hasPermission(`${name}.${field}.view`, permissions))
165192
continue;
166193

167-
const C = COMPONENTS[state["type"]];
168-
if (C === undefined)
169-
throw Error(`Unknown component '${state["type"]}'`);
170-
171-
const {children, ...props} = state["props"];
172-
let c;
173-
if (children) {
174-
let child_fields = createFields(
175-
{"fields": children, "display": Object.keys(children)}, name, permissions);
176-
c = <C source={field} {...props}>{child_fields}</C>;
177-
} else {
178-
c = <C source={field} {...props} />;
179-
}
180-
if (field === "_") {
181-
// Layout component, not related to a specific field.
182-
components.push(c);
183-
} else {
184-
const withRecordProps = {
185-
"source": field, "label": props["label"], "sortable": props["sortable"],
186-
"sortBy": props["sortBy"], "sortByOrder": props["sortByOrder"]}
187-
// Show icon if user doesn't have permission to view this field (based on filters).
188-
components.push(<WithRecord {...withRecordProps} render={
189-
(record) => hasPermission(`${name}.${field}.view`, permissions, record) ? c : <VisibilityOffIcon />
190-
} />);
191-
}
194+
const c = evaluate(state);
195+
const withRecordPropNames = ["label", "sortable", "sortBy", "sortByOrder"];
196+
const withRecordProps = Object.fromEntries(withRecordPropNames.map(
197+
(k) => [k, evaluate(state["props"][k])]));
198+
// Show icon if user doesn't have permission to view this field (based on filters).
199+
components.push(<WithRecord source={field} {...withRecordProps} render={
200+
(record) => hasPermission(`${name}.${field}.view`, permissions, record) ? c : <VisibilityOffIcon />
201+
} />);
192202
}
193203
return components;
194204
}
195205

196206
function createInputs(resource, name, perm_type, permissions) {
197207
let components = [];
198208
const resource_filters = getFilters(name, perm_type, permissions);
199-
for (const [field, state] of Object.entries(resource["inputs"])) {
209+
for (let [field, state] of Object.entries(resource["inputs"])) {
200210
if ((perm_type === "add" && !state["show_create"])
201211
|| !hasPermission(`${name}.${field}.${perm_type}`, permissions))
202212
continue;
@@ -216,19 +226,11 @@ function createInputs(resource, name, perm_type, permissions) {
216226
<SelectInput source={field} choices={choices} defaultValue={nullable < 0 && fvalues[0]}
217227
validate={nullable < 0 && required()} disabled={disabled} />);
218228
} else {
219-
const C = COMPONENTS[state["type"]];
220-
if (C === undefined)
221-
throw Error(`Unknown component '${state["type"]}'`);
222-
223-
let validators = [];
224-
if (perm_type !== "view") {
225-
for (let validator of state["validators"]) {
226-
if (validator[0] === "regex")
227-
validator[1] = new RegExp(validator[1]);
228-
validators.push(VALIDATORS[validator[0]](...validator.slice(1)))
229-
}
229+
if (perm_type === "view") {
230+
state = structuredClone(state);
231+
delete state["props"]["validate"];
230232
}
231-
const c = <C source={field} validate={validators} {...state["props"]} />;
233+
const c = evaluate(state);
232234
if (perm_type === "edit")
233235
// Don't render if filters disallow editing this field.
234236
components.push(<WithRecord source={field} render={
@@ -277,7 +279,7 @@ const AiohttpList = (resource, name, permissions) => {
277279
return (
278280
<List actions={<ListActions />} filters={createInputs(resource, name, "view", permissions)}>
279281
<DatagridConfigurable omit={resource["list_omit"]} rowClick="show" bulkActionButtons={<BulkActionButtons />}>
280-
{createFields(resource, name, permissions)}
282+
{createFields(resource["fields"], name, permissions)}
281283
<WithRecord label="[Edit]" render={(record) => hasPermission(`${name}.edit`, permissions, record) && <EditButton />} />
282284
</DatagridConfigurable>
283285
</List>
@@ -294,7 +296,7 @@ const AiohttpShow = (resource, name, permissions) => {
294296
return (
295297
<Show actions={<ShowActions />}>
296298
<SimpleShowLayout>
297-
{createFields(resource, name, permissions)}
299+
{createFields(resource["fields"], name, permissions)}
298300
</SimpleShowLayout>
299301
</Show>
300302
);

aiohttp_admin/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
from aiohttp import web
88
from aiohttp.typedefs import Handler
99
from aiohttp_session.cookie_storage import EncryptedCookieStorage
10-
from pydantic import ValidationError, parse_obj_as
10+
from pydantic import ValidationError
1111

1212
from .routes import setup_resources, setup_routes
13-
from .security import AdminAuthorizationPolicy, Permissions, TokenIdentityPolicy
13+
from .security import AdminAuthorizationPolicy, Permissions, TokenIdentityPolicy, check
1414
from .types import Schema, UserDetails
1515

1616
__all__ = ("Permissions", "Schema", "UserDetails", "setup")
@@ -67,7 +67,7 @@ def value(r: web.RouteDef) -> tuple[str, str]:
6767
m = res["model"]
6868
admin["state"]["resources"][m.name]["urls"] = {key(r): value(r) for r in m.routes}
6969

70-
schema = parse_obj_as(Schema, schema)
70+
schema = check(Schema, schema)
7171
if secret is None:
7272
secret = secrets.token_bytes()
7373

aiohttp_admin/backends/abc.py

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,25 @@
11
import asyncio
22
import json
3+
import sys
34
import warnings
45
from abc import ABC, abstractmethod
56
from datetime import date, datetime, time
67
from enum import Enum
78
from functools import cached_property, partial
89
from types import MappingProxyType
9-
from typing import Any, Literal, Optional, TypedDict, Union
10+
from typing import Any, Literal, Optional, Union
1011

1112
from aiohttp import web
1213
from aiohttp_security import check_permission, permits
13-
from pydantic import Json, parse_obj_as
14+
from pydantic import Json
1415

15-
from ..security import permissions_as_dict
16-
from ..types import FieldState, InputState
16+
from ..security import check, permissions_as_dict
17+
from ..types import ComponentState, InputState
18+
19+
if sys.version_info >= (3, 12):
20+
from typing import TypedDict
21+
else:
22+
from typing_extensions import TypedDict
1723

1824
Record = dict[str, object]
1925

@@ -93,7 +99,7 @@ class DeleteManyParams(_Params):
9399

94100
class AbstractAdminResource(ABC):
95101
name: str
96-
fields: dict[str, FieldState]
102+
fields: dict[str, ComponentState]
97103
inputs: dict[str, InputState]
98104
primary_key: str
99105
omit_fields: set[str]
@@ -149,7 +155,7 @@ async def delete_many(self, params: DeleteManyParams) -> list[Union[int, str]]:
149155

150156
async def _get_list(self, request: web.Request) -> web.Response:
151157
await check_permission(request, f"admin.{self.name}.view", context=(request, None))
152-
query = parse_obj_as(GetListParams, request.query)
158+
query = check(GetListParams, request.query)
153159

154160
# When sort order refers to "id", this should be translated to primary key.
155161
if query["sort"]["field"] == "id":
@@ -174,7 +180,7 @@ async def _get_list(self, request: web.Request) -> web.Response:
174180

175181
async def _get_one(self, request: web.Request) -> web.Response:
176182
await check_permission(request, f"admin.{self.name}.view", context=(request, None))
177-
query = parse_obj_as(GetOneParams, request.query)
183+
query = check(GetOneParams, request.query)
178184

179185
result = await self.get_one(query)
180186
if not await permits(request, f"admin.{self.name}.view", context=(request, result)):
@@ -185,7 +191,7 @@ async def _get_one(self, request: web.Request) -> web.Response:
185191

186192
async def _get_many(self, request: web.Request) -> web.Response:
187193
await check_permission(request, f"admin.{self.name}.view", context=(request, None))
188-
query = parse_obj_as(GetManyParams, request.query)
194+
query = check(GetManyParams, request.query)
189195

190196
results = await self.get_many(query)
191197
if not results:
@@ -198,12 +204,12 @@ async def _get_many(self, request: web.Request) -> web.Response:
198204
return json_response({"data": results})
199205

200206
async def _create(self, request: web.Request) -> web.Response:
201-
query = parse_obj_as(CreateParams, request.query)
207+
query = check(CreateParams, request.query)
202208
# TODO(Pydantic): Dissallow extra arguments
203209
for k in query["data"]:
204210
if k not in self.inputs and k != "id":
205211
raise web.HTTPBadRequest(reason=f"Invalid field '{k}'")
206-
query["data"] = parse_obj_as(self._record_type, query["data"])
212+
query["data"] = check(self._record_type, query["data"])
207213
await check_permission(request, f"admin.{self.name}.add", context=(request, query["data"]))
208214
for k, v in query["data"].items():
209215
if v is not None:
@@ -217,13 +223,13 @@ async def _create(self, request: web.Request) -> web.Response:
217223

218224
async def _update(self, request: web.Request) -> web.Response:
219225
await check_permission(request, f"admin.{self.name}.edit", context=(request, None))
220-
query = parse_obj_as(UpdateParams, request.query)
226+
query = check(UpdateParams, request.query)
221227
# TODO(Pydantic): Dissallow extra arguments
222228
for k in query["data"]:
223229
if k not in self.inputs and k != "id":
224230
raise web.HTTPBadRequest(reason=f"Invalid field '{k}'")
225-
query["data"] = parse_obj_as(self._record_type, query["data"])
226-
query["previousData"] = parse_obj_as(self._record_type, query["previousData"])
231+
query["data"] = check(self._record_type, query["data"])
232+
query["previousData"] = check(self._record_type, query["previousData"])
227233

228234
if self.primary_key != "id":
229235
query["data"].pop("id", None)
@@ -251,12 +257,12 @@ async def _update(self, request: web.Request) -> web.Response:
251257

252258
async def _update_many(self, request: web.Request) -> web.Response:
253259
await check_permission(request, f"admin.{self.name}.edit", context=(request, None))
254-
query = parse_obj_as(UpdateManyParams, request.query)
260+
query = check(UpdateManyParams, request.query)
255261
# TODO(Pydantic): Dissallow extra arguments
256262
for k in query["data"]:
257263
if k not in self.inputs and k != "id":
258264
raise web.HTTPBadRequest(reason=f"Invalid field '{k}'")
259-
query["data"] = parse_obj_as(self._record_type, query["data"])
265+
query["data"] = check(self._record_type, query["data"])
260266

261267
# Check original records are allowed by permission filters.
262268
originals = await self.get_many({"ids": query["ids"]})
@@ -278,8 +284,8 @@ async def _update_many(self, request: web.Request) -> web.Response:
278284

279285
async def _delete(self, request: web.Request) -> web.Response:
280286
await check_permission(request, f"admin.{self.name}.delete", context=(request, None))
281-
query = parse_obj_as(DeleteParams, request.query)
282-
query["previousData"] = parse_obj_as(self._record_type, query["previousData"])
287+
query = check(DeleteParams, request.query)
288+
query["previousData"] = check(self._record_type, query["previousData"])
283289

284290
original = await self.get_one({"id": query["id"]})
285291
if not await permits(request, f"admin.{self.name}.delete", context=(request, original)):
@@ -292,7 +298,7 @@ async def _delete(self, request: web.Request) -> web.Response:
292298

293299
async def _delete_many(self, request: web.Request) -> web.Response:
294300
await check_permission(request, f"admin.{self.name}.delete", context=(request, None))
295-
query = parse_obj_as(DeleteManyParams, request.query)
301+
query = check(DeleteManyParams, request.query)
296302

297303
originals = await self.get_many(query)
298304
allowed = await asyncio.gather(*(permits(request, f"admin.{self.name}.delete",

0 commit comments

Comments
 (0)