fastadmin 0.6.0__tar.gz → 0.8.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fastadmin-0.6.0 → fastadmin-0.8.0}/PKG-INFO +14 -5
- {fastadmin-0.6.0 → fastadmin-0.8.0}/README.md +10 -3
- {fastadmin-0.6.0 → fastadmin-0.8.0}/fastadmin/__init__.py +6 -0
- fastadmin-0.8.0/fastadmin/api/encoders.py +70 -0
- {fastadmin-0.6.0 → fastadmin-0.8.0}/fastadmin/models/base.py +7 -1
- fastadmin-0.8.0/fastadmin/models/orms/yaraorm.py +348 -0
- {fastadmin-0.6.0 → fastadmin-0.8.0}/fastadmin/static/index.min.js +131 -131
- {fastadmin-0.6.0 → fastadmin-0.8.0}/pyproject.toml +4 -1
- {fastadmin-0.6.0 → fastadmin-0.8.0}/LICENSE.md +0 -0
- {fastadmin-0.6.0 → fastadmin-0.8.0}/fastadmin/api/__init__.py +0 -0
- {fastadmin-0.6.0 → fastadmin-0.8.0}/fastadmin/api/exceptions.py +0 -0
- {fastadmin-0.6.0 → fastadmin-0.8.0}/fastadmin/api/frameworks/__init__.py +0 -0
- {fastadmin-0.6.0 → fastadmin-0.8.0}/fastadmin/api/frameworks/django/__init__.py +0 -0
- {fastadmin-0.6.0 → fastadmin-0.8.0}/fastadmin/api/frameworks/django/app/__init__.py +0 -0
- {fastadmin-0.6.0 → fastadmin-0.8.0}/fastadmin/api/frameworks/django/app/api.py +0 -0
- {fastadmin-0.6.0 → fastadmin-0.8.0}/fastadmin/api/frameworks/django/app/urls.py +0 -0
- {fastadmin-0.6.0 → fastadmin-0.8.0}/fastadmin/api/frameworks/django/app/views.py +0 -0
- {fastadmin-0.6.0 → fastadmin-0.8.0}/fastadmin/api/frameworks/fastapi/__init__.py +0 -0
- {fastadmin-0.6.0 → fastadmin-0.8.0}/fastadmin/api/frameworks/fastapi/api.py +0 -0
- {fastadmin-0.6.0 → fastadmin-0.8.0}/fastadmin/api/frameworks/fastapi/app.py +0 -0
- {fastadmin-0.6.0 → fastadmin-0.8.0}/fastadmin/api/frameworks/fastapi/views.py +0 -0
- {fastadmin-0.6.0 → fastadmin-0.8.0}/fastadmin/api/frameworks/flask/__init__.py +0 -0
- {fastadmin-0.6.0 → fastadmin-0.8.0}/fastadmin/api/frameworks/flask/api.py +0 -0
- {fastadmin-0.6.0 → fastadmin-0.8.0}/fastadmin/api/frameworks/flask/app.py +0 -0
- {fastadmin-0.6.0 → fastadmin-0.8.0}/fastadmin/api/frameworks/flask/views.py +0 -0
- {fastadmin-0.6.0 → fastadmin-0.8.0}/fastadmin/api/helpers.py +0 -0
- {fastadmin-0.6.0 → fastadmin-0.8.0}/fastadmin/api/schemas.py +0 -0
- {fastadmin-0.6.0 → fastadmin-0.8.0}/fastadmin/api/service.py +0 -0
- {fastadmin-0.6.0 → fastadmin-0.8.0}/fastadmin/models/__init__.py +0 -0
- {fastadmin-0.6.0 → fastadmin-0.8.0}/fastadmin/models/decorators.py +0 -0
- {fastadmin-0.6.0 → fastadmin-0.8.0}/fastadmin/models/helpers.py +0 -0
- {fastadmin-0.6.0 → fastadmin-0.8.0}/fastadmin/models/orms/__init__.py +0 -0
- {fastadmin-0.6.0 → fastadmin-0.8.0}/fastadmin/models/orms/django.py +0 -0
- {fastadmin-0.6.0 → fastadmin-0.8.0}/fastadmin/models/orms/ponyorm.py +0 -0
- {fastadmin-0.6.0 → fastadmin-0.8.0}/fastadmin/models/orms/sqlalchemy.py +0 -0
- {fastadmin-0.6.0 → fastadmin-0.8.0}/fastadmin/models/orms/tortoise.py +0 -0
- {fastadmin-0.6.0 → fastadmin-0.8.0}/fastadmin/models/schemas.py +0 -0
- {fastadmin-0.6.0 → fastadmin-0.8.0}/fastadmin/settings.py +0 -0
- {fastadmin-0.6.0 → fastadmin-0.8.0}/fastadmin/static/assets/worker-6Z7niv9l.js +0 -0
- {fastadmin-0.6.0 → fastadmin-0.8.0}/fastadmin/static/assets/worker-C151k0-L.js +0 -0
- {fastadmin-0.6.0 → fastadmin-0.8.0}/fastadmin/static/assets/worker-C9KPOOKD.js +0 -0
- {fastadmin-0.6.0 → fastadmin-0.8.0}/fastadmin/static/assets/worker-D1fCgYB7.js +0 -0
- {fastadmin-0.6.0 → fastadmin-0.8.0}/fastadmin/static/assets/worker-D3TWcJOI.js +0 -0
- {fastadmin-0.6.0 → fastadmin-0.8.0}/fastadmin/static/assets/worker-DRNcaZ-V.js +0 -0
- {fastadmin-0.6.0 → fastadmin-0.8.0}/fastadmin/static/assets/worker-MF2p-l5_.js +0 -0
- {fastadmin-0.6.0 → fastadmin-0.8.0}/fastadmin/static/images/favicon.png +0 -0
- {fastadmin-0.6.0 → fastadmin-0.8.0}/fastadmin/static/images/header-logo.svg +0 -0
- {fastadmin-0.6.0 → fastadmin-0.8.0}/fastadmin/static/images/sign-in-logo.svg +0 -0
- {fastadmin-0.6.0 → fastadmin-0.8.0}/fastadmin/static/index.html +0 -0
- {fastadmin-0.6.0 → fastadmin-0.8.0}/fastadmin/static/index.min.css +0 -0
- {fastadmin-0.6.0 → fastadmin-0.8.0}/fastadmin/templates/index.html +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastadmin
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: FastAdmin is an easy-to-use Admin Dashboard App for FastAPI/Flask/Django inspired by Django Admin.
|
|
5
|
-
Keywords: admin,dashboard,fastapi,flask,django,tortoise-orm,sqlalchemy,pony,admin-dashboard,django-admin
|
|
5
|
+
Keywords: admin,dashboard,fastapi,flask,django,tortoise-orm,sqlalchemy,pony,yara-orm,admin-dashboard,django-admin
|
|
6
6
|
Author: Seva D
|
|
7
7
|
Author-email: Seva D <vsdudakov@gmail.com>
|
|
8
8
|
License-Expression: MIT
|
|
@@ -31,6 +31,7 @@ Requires-Dist: pony>=0.7,<1 ; extra == 'pony'
|
|
|
31
31
|
Requires-Dist: sqlalchemy[asyncio]>=2,<3 ; extra == 'sqlalchemy'
|
|
32
32
|
Requires-Dist: greenlet>=3 ; extra == 'sqlalchemy'
|
|
33
33
|
Requires-Dist: tortoise-orm>=1,<2 ; extra == 'tortoise-orm'
|
|
34
|
+
Requires-Dist: yara-orm>=1,<2 ; extra == 'yara-orm'
|
|
34
35
|
Requires-Python: >=3.12
|
|
35
36
|
Project-URL: Homepage, https://vsdudakov.github.io/fastadmin/
|
|
36
37
|
Project-URL: Repository, https://github.com/vsdudakov/fastadmin
|
|
@@ -42,6 +43,7 @@ Provides-Extra: flask
|
|
|
42
43
|
Provides-Extra: pony
|
|
43
44
|
Provides-Extra: sqlalchemy
|
|
44
45
|
Provides-Extra: tortoise-orm
|
|
46
|
+
Provides-Extra: yara-orm
|
|
45
47
|
Description-Content-Type: text/markdown
|
|
46
48
|
|
|
47
49
|
<h1 align="center">FastAdmin — Admin Dashboard for FastAPI, Flask and Django</h1>
|
|
@@ -57,7 +59,8 @@ Description-Content-Type: text/markdown
|
|
|
57
59
|
**FastAdmin** is an easy-to-use **admin dashboard (admin panel) for FastAPI, Flask and
|
|
58
60
|
Django**, inspired by Django Admin. It gives your Python web application a
|
|
59
61
|
production-ready **CRUD admin interface** in minutes — on top of **Tortoise ORM,
|
|
60
|
-
Django ORM, SQLAlchemy or
|
|
62
|
+
Django ORM, SQLAlchemy, Pony ORM or [Yara ORM](https://github.com/vsdudakov/yara-orm)** —
|
|
63
|
+
with authentication, filters, search,
|
|
61
64
|
inline editing, file uploads, CSV/JSON export and dashboard charts out of the box.
|
|
62
65
|
|
|
63
66
|
FastAdmin is built with relationships in mind and admiration for Django Admin.
|
|
@@ -77,7 +80,8 @@ already know FastAdmin.
|
|
|
77
80
|
- **Any web framework** — mount as a FastAPI sub-app, a Flask blueprint or
|
|
78
81
|
Django urlpatterns.
|
|
79
82
|
- **Any ORM** — first-class admin classes for Tortoise ORM, Django ORM,
|
|
80
|
-
SQLAlchemy (async) and
|
|
83
|
+
SQLAlchemy (async), Pony ORM and [Yara ORM](https://github.com/vsdudakov/yara-orm)
|
|
84
|
+
(a fast, Rust-engine async ORM).
|
|
81
85
|
- **Authentication & permissions** — pluggable sign-in against your own user
|
|
82
86
|
model, per-action permission hooks, request/user context in every admin method.
|
|
83
87
|
- **Rich form widgets** — 20+ antd-based widgets (rich text, JSON, async
|
|
@@ -102,6 +106,7 @@ pip install fastadmin[fastapi,django] # FastAPI with Django ORM
|
|
|
102
106
|
pip install fastadmin[fastapi,tortoise-orm] # FastAPI with Tortoise ORM
|
|
103
107
|
pip install fastadmin[fastapi,pony] # FastAPI with Pony ORM
|
|
104
108
|
pip install fastadmin[fastapi,sqlalchemy] # FastAPI with SQLAlchemy (includes greenlet)
|
|
109
|
+
pip install fastadmin[fastapi,yara-orm] # FastAPI with Yara ORM
|
|
105
110
|
pip install fastadmin[django] # Django with Django ORM
|
|
106
111
|
pip install fastadmin[django,pony] # Django with Pony ORM
|
|
107
112
|
pip install fastadmin[flask,sqlalchemy] # Flask with SQLAlchemy (includes greenlet)
|
|
@@ -193,11 +198,15 @@ Runnable example apps for every framework/ORM combination are in
|
|
|
193
198
|
## Why FastAdmin?
|
|
194
199
|
|
|
195
200
|
If you are looking for a **Django-Admin-like admin panel for FastAPI**, an
|
|
196
|
-
**admin interface for SQLAlchemy
|
|
201
|
+
**admin interface for SQLAlchemy, Tortoise ORM or
|
|
202
|
+
[Yara ORM](https://github.com/vsdudakov/yara-orm)**, or a lightweight
|
|
197
203
|
**alternative to building a custom back office**, FastAdmin gives you a
|
|
198
204
|
batteries-included, themeable admin UI without code generation, without tying
|
|
199
205
|
your app to a specific framework, and without writing a single React component.
|
|
200
206
|
|
|
207
|
+
Pair it with [Yara ORM](https://github.com/vsdudakov/yara-orm) — our fast, async
|
|
208
|
+
Python ORM with a Rust engine — for a high-performance FastAPI + admin stack.
|
|
209
|
+
|
|
201
210
|
## Contributing
|
|
202
211
|
|
|
203
212
|
Contributions are welcome — see the
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
**FastAdmin** is an easy-to-use **admin dashboard (admin panel) for FastAPI, Flask and
|
|
12
12
|
Django**, inspired by Django Admin. It gives your Python web application a
|
|
13
13
|
production-ready **CRUD admin interface** in minutes — on top of **Tortoise ORM,
|
|
14
|
-
Django ORM, SQLAlchemy or
|
|
14
|
+
Django ORM, SQLAlchemy, Pony ORM or [Yara ORM](https://github.com/vsdudakov/yara-orm)** —
|
|
15
|
+
with authentication, filters, search,
|
|
15
16
|
inline editing, file uploads, CSV/JSON export and dashboard charts out of the box.
|
|
16
17
|
|
|
17
18
|
FastAdmin is built with relationships in mind and admiration for Django Admin.
|
|
@@ -31,7 +32,8 @@ already know FastAdmin.
|
|
|
31
32
|
- **Any web framework** — mount as a FastAPI sub-app, a Flask blueprint or
|
|
32
33
|
Django urlpatterns.
|
|
33
34
|
- **Any ORM** — first-class admin classes for Tortoise ORM, Django ORM,
|
|
34
|
-
SQLAlchemy (async) and
|
|
35
|
+
SQLAlchemy (async), Pony ORM and [Yara ORM](https://github.com/vsdudakov/yara-orm)
|
|
36
|
+
(a fast, Rust-engine async ORM).
|
|
35
37
|
- **Authentication & permissions** — pluggable sign-in against your own user
|
|
36
38
|
model, per-action permission hooks, request/user context in every admin method.
|
|
37
39
|
- **Rich form widgets** — 20+ antd-based widgets (rich text, JSON, async
|
|
@@ -56,6 +58,7 @@ pip install fastadmin[fastapi,django] # FastAPI with Django ORM
|
|
|
56
58
|
pip install fastadmin[fastapi,tortoise-orm] # FastAPI with Tortoise ORM
|
|
57
59
|
pip install fastadmin[fastapi,pony] # FastAPI with Pony ORM
|
|
58
60
|
pip install fastadmin[fastapi,sqlalchemy] # FastAPI with SQLAlchemy (includes greenlet)
|
|
61
|
+
pip install fastadmin[fastapi,yara-orm] # FastAPI with Yara ORM
|
|
59
62
|
pip install fastadmin[django] # Django with Django ORM
|
|
60
63
|
pip install fastadmin[django,pony] # Django with Pony ORM
|
|
61
64
|
pip install fastadmin[flask,sqlalchemy] # Flask with SQLAlchemy (includes greenlet)
|
|
@@ -147,11 +150,15 @@ Runnable example apps for every framework/ORM combination are in
|
|
|
147
150
|
## Why FastAdmin?
|
|
148
151
|
|
|
149
152
|
If you are looking for a **Django-Admin-like admin panel for FastAPI**, an
|
|
150
|
-
**admin interface for SQLAlchemy
|
|
153
|
+
**admin interface for SQLAlchemy, Tortoise ORM or
|
|
154
|
+
[Yara ORM](https://github.com/vsdudakov/yara-orm)**, or a lightweight
|
|
151
155
|
**alternative to building a custom back office**, FastAdmin gives you a
|
|
152
156
|
batteries-included, themeable admin UI without code generation, without tying
|
|
153
157
|
your app to a specific framework, and without writing a single React component.
|
|
154
158
|
|
|
159
|
+
Pair it with [Yara ORM](https://github.com/vsdudakov/yara-orm) — our fast, async
|
|
160
|
+
Python ORM with a Rust engine — for a high-performance FastAPI + admin stack.
|
|
161
|
+
|
|
155
162
|
## Contributing
|
|
156
163
|
|
|
157
164
|
Contributions are welcome — see the
|
|
@@ -32,7 +32,13 @@ try:
|
|
|
32
32
|
except ModuleNotFoundError: # pragma: no cover
|
|
33
33
|
logging.info("TortoiseORM is not installed") # pragma: no cover
|
|
34
34
|
|
|
35
|
+
try:
|
|
36
|
+
from fastadmin.models.orms.yaraorm import YaraOrmInlineModelAdmin, YaraOrmModelAdmin # noqa: F401
|
|
37
|
+
except ModuleNotFoundError: # pragma: no cover
|
|
38
|
+
logging.info("Yara ORM is not installed") # pragma: no cover
|
|
39
|
+
|
|
35
40
|
# api
|
|
41
|
+
from fastadmin.api.encoders import register_encoder, unregister_encoder # noqa: F401
|
|
36
42
|
from fastadmin.api.exceptions import AdminApiException # noqa: F401
|
|
37
43
|
|
|
38
44
|
# models
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""User-registerable custom encoders for admin API serialization.
|
|
2
|
+
|
|
3
|
+
Model field values are converted to JSON-serializable representations during
|
|
4
|
+
serialization (see :meth:`fastadmin.models.base.BaseModelAdmin.serialize_obj_attributes`).
|
|
5
|
+
Register a custom encoder to control how instances of a given type are represented
|
|
6
|
+
in every admin API response — e.g. a custom datetime format, an ``Enum``'s label,
|
|
7
|
+
or a domain value object.
|
|
8
|
+
|
|
9
|
+
Example:
|
|
10
|
+
|
|
11
|
+
.. code-block:: python
|
|
12
|
+
|
|
13
|
+
import datetime
|
|
14
|
+
|
|
15
|
+
from fastadmin import register_encoder
|
|
16
|
+
|
|
17
|
+
# Render every datetime as "2024-01-31 14:05" instead of ISO 8601.
|
|
18
|
+
register_encoder(datetime.datetime, lambda dt: dt.strftime("%Y-%m-%d %H:%M"))
|
|
19
|
+
|
|
20
|
+
Encoders are matched via ``isinstance`` in registration order, so register more
|
|
21
|
+
specific types before their base types.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from collections.abc import Callable
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
# Maps a type to a function converting an instance of that type to a JSON-serializable value.
|
|
28
|
+
CUSTOM_ENCODERS: dict[type, Callable[[Any], Any]] = {}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def register_encoder(type_: type, encoder: Callable[[Any], Any]) -> None:
|
|
32
|
+
"""Register a custom encoder for a type used across all admin API responses.
|
|
33
|
+
|
|
34
|
+
:params type_: the type to encode (matched via ``isinstance``).
|
|
35
|
+
:params encoder: a callable converting an instance to a JSON-serializable value.
|
|
36
|
+
:return: None.
|
|
37
|
+
"""
|
|
38
|
+
CUSTOM_ENCODERS[type_] = encoder
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def unregister_encoder(type_: type) -> None:
|
|
42
|
+
"""Remove a previously registered encoder for a type (no-op if absent).
|
|
43
|
+
|
|
44
|
+
:params type_: the type whose encoder should be removed.
|
|
45
|
+
:return: None.
|
|
46
|
+
"""
|
|
47
|
+
CUSTOM_ENCODERS.pop(type_, None)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def clear_encoders() -> None:
|
|
51
|
+
"""Remove all registered custom encoders.
|
|
52
|
+
|
|
53
|
+
:return: None.
|
|
54
|
+
"""
|
|
55
|
+
CUSTOM_ENCODERS.clear()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def apply_custom_encoders(value: Any) -> Any:
|
|
59
|
+
"""Return ``value`` encoded by the first matching custom encoder, else unchanged.
|
|
60
|
+
|
|
61
|
+
Types are matched via ``isinstance`` in registration order, so a more specific
|
|
62
|
+
type registered before its base type wins.
|
|
63
|
+
|
|
64
|
+
:params value: the value to encode.
|
|
65
|
+
:return: the encoded value, or the original value if no encoder matches.
|
|
66
|
+
"""
|
|
67
|
+
for type_, encoder in CUSTOM_ENCODERS.items():
|
|
68
|
+
if isinstance(value, type_):
|
|
69
|
+
return encoder(value)
|
|
70
|
+
return value
|
|
@@ -11,6 +11,7 @@ from uuid import UUID
|
|
|
11
11
|
|
|
12
12
|
from asgiref.sync import sync_to_async
|
|
13
13
|
|
|
14
|
+
from fastadmin.api.encoders import apply_custom_encoders
|
|
14
15
|
from fastadmin.api.schemas import ExportFormat
|
|
15
16
|
from fastadmin.models.schemas import ModelFieldWidgetSchema, WidgetType
|
|
16
17
|
|
|
@@ -419,7 +420,12 @@ class BaseModelAdmin:
|
|
|
419
420
|
serialized_dict: dict[str, Any] = {}
|
|
420
421
|
for field in attributes_to_serizalize:
|
|
421
422
|
value = getattr(obj, field.column_name)
|
|
422
|
-
|
|
423
|
+
# Let user-registered encoders (e.g. a custom datetime format) take
|
|
424
|
+
# precedence over the built-in handling below.
|
|
425
|
+
encoded = apply_custom_encoders(value)
|
|
426
|
+
if encoded is not value:
|
|
427
|
+
value = encoded
|
|
428
|
+
elif isinstance(value, Decimal):
|
|
423
429
|
# Avoid scientific notation for Decimal values in API responses,
|
|
424
430
|
# e.g. 3.75E+3 -> "3750"
|
|
425
431
|
value = format(value, "f")
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import operator
|
|
3
|
+
from typing import Any
|
|
4
|
+
from uuid import UUID
|
|
5
|
+
|
|
6
|
+
from yara_orm import Q
|
|
7
|
+
|
|
8
|
+
from fastadmin.models.base import InlineModelAdmin, ModelAdmin
|
|
9
|
+
from fastadmin.models.schemas import ModelFieldWidgetSchema, WidgetType
|
|
10
|
+
from fastadmin.settings import settings
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class YaraOrmMixin:
|
|
14
|
+
def _get_relation_target(self, field_name: str) -> Any:
|
|
15
|
+
"""Resolve the related model class for a forward relation field."""
|
|
16
|
+
relation = self.model_cls._meta.relations.get(field_name)
|
|
17
|
+
return relation.resolve_target() if relation else None
|
|
18
|
+
|
|
19
|
+
def _resolve_ordering_field(self, ordering_field: str) -> str:
|
|
20
|
+
"""Resolve ordering field for Yara ORM.
|
|
21
|
+
|
|
22
|
+
Relation names (e.g. `tournament`) are not orderable directly; for
|
|
23
|
+
FK/O2O relations order by their backing id column (e.g. `tournament_id`).
|
|
24
|
+
"""
|
|
25
|
+
if not ordering_field:
|
|
26
|
+
return ordering_field
|
|
27
|
+
|
|
28
|
+
prefix = "-" if ordering_field.startswith("-") else ""
|
|
29
|
+
field_name = ordering_field.lstrip("-")
|
|
30
|
+
|
|
31
|
+
# Respect explicit nested ordering (e.g. relation__name)
|
|
32
|
+
if "__" in field_name:
|
|
33
|
+
return ordering_field
|
|
34
|
+
|
|
35
|
+
if field_name in self.model_cls._meta.relations:
|
|
36
|
+
return f"{prefix}{field_name}_id"
|
|
37
|
+
return ordering_field
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def get_model_pk_name(orm_model_cls: Any) -> str:
|
|
41
|
+
"""This method is used to get model pk name.
|
|
42
|
+
|
|
43
|
+
:return: A str.
|
|
44
|
+
"""
|
|
45
|
+
return orm_model_cls._meta.pk_field.model_field_name
|
|
46
|
+
|
|
47
|
+
def _build_relation_widgets(
|
|
48
|
+
self,
|
|
49
|
+
field_type: str,
|
|
50
|
+
field_name: str,
|
|
51
|
+
rel_model_cls: Any,
|
|
52
|
+
form_widget_props: dict,
|
|
53
|
+
filter_widget_props: dict,
|
|
54
|
+
) -> tuple[WidgetType, WidgetType]:
|
|
55
|
+
"""Configure form/filter widgets for a relation field."""
|
|
56
|
+
rel_model = rel_model_cls.__name__
|
|
57
|
+
rel_model_id_field = self.get_model_pk_name(rel_model_cls)
|
|
58
|
+
rel_model_label_fields = ("__str__", rel_model_id_field)
|
|
59
|
+
|
|
60
|
+
if field_name in self.raw_id_fields:
|
|
61
|
+
return WidgetType.Input, WidgetType.Input
|
|
62
|
+
|
|
63
|
+
for props in (form_widget_props, filter_widget_props):
|
|
64
|
+
props["parentModel"] = rel_model
|
|
65
|
+
props["idField"] = rel_model_id_field
|
|
66
|
+
props["labelFields"] = rel_model_label_fields
|
|
67
|
+
|
|
68
|
+
if field_type == "ManyToManyFieldInstance":
|
|
69
|
+
if field_name in self.filter_vertical or field_name in self.filter_horizontal:
|
|
70
|
+
layout = "vertical" if field_name in self.filter_vertical else "horizontal"
|
|
71
|
+
form_widget_props["layout"] = layout
|
|
72
|
+
filter_widget_props["layout"] = layout
|
|
73
|
+
return WidgetType.AsyncTransfer, WidgetType.AsyncTransfer
|
|
74
|
+
form_widget_props["mode"] = "multiple"
|
|
75
|
+
filter_widget_props["mode"] = "multiple"
|
|
76
|
+
return WidgetType.AsyncSelect, WidgetType.AsyncSelect
|
|
77
|
+
|
|
78
|
+
# FK / O2O
|
|
79
|
+
filter_widget_props["mode"] = "multiple"
|
|
80
|
+
return WidgetType.AsyncSelect, WidgetType.AsyncSelect
|
|
81
|
+
|
|
82
|
+
def get_model_fields_with_widget_types(
|
|
83
|
+
self,
|
|
84
|
+
with_m2m: bool | None = None,
|
|
85
|
+
) -> list[ModelFieldWidgetSchema]:
|
|
86
|
+
"""This method is used to get model fields with widget types.
|
|
87
|
+
|
|
88
|
+
:params with_m2m: a flag to include m2m fields.
|
|
89
|
+
:return: A list of ModelFieldWidgetSchema.
|
|
90
|
+
"""
|
|
91
|
+
meta = self.model_cls._meta
|
|
92
|
+
# Yara ORM keeps forward fields in fields_map (with FKs appearing under
|
|
93
|
+
# both the relation name and the `<name>_id` column) and m2m fields in a
|
|
94
|
+
# separate `m2m` mapping.
|
|
95
|
+
entries: list[tuple[str, Any, bool]] = [(name, field, False) for name, field in meta.fields_map.items()]
|
|
96
|
+
entries += [(name, m2m_info.field, True) for name, m2m_info in meta.m2m.items()]
|
|
97
|
+
|
|
98
|
+
fields = []
|
|
99
|
+
for field_name, orm_model_field, is_m2m in entries:
|
|
100
|
+
field_type = orm_model_field.__class__.__name__
|
|
101
|
+
|
|
102
|
+
# Skip the raw `<name>_id` duplicate of a forward relation.
|
|
103
|
+
if not is_m2m and field_name.endswith("_id") and getattr(orm_model_field, "reference", None):
|
|
104
|
+
continue
|
|
105
|
+
|
|
106
|
+
if with_m2m is not None and not with_m2m and is_m2m:
|
|
107
|
+
continue
|
|
108
|
+
if with_m2m is not None and with_m2m and not is_m2m:
|
|
109
|
+
continue
|
|
110
|
+
|
|
111
|
+
column_name = field_name
|
|
112
|
+
if not is_m2m and getattr(orm_model_field, "reference", None):
|
|
113
|
+
column_name = orm_model_field.model_field_name
|
|
114
|
+
|
|
115
|
+
is_pk = getattr(orm_model_field, "pk", False)
|
|
116
|
+
is_immutable = (
|
|
117
|
+
is_pk or getattr(orm_model_field, "auto_now", False) or getattr(orm_model_field, "auto_now_add", False)
|
|
118
|
+
) and field_name not in self.readonly_fields
|
|
119
|
+
required = (
|
|
120
|
+
not getattr(orm_model_field, "null", False)
|
|
121
|
+
and not getattr(orm_model_field, "default", None)
|
|
122
|
+
and not is_pk
|
|
123
|
+
and not is_m2m
|
|
124
|
+
)
|
|
125
|
+
choices = (
|
|
126
|
+
orm_model_field.enum_type._member_map_
|
|
127
|
+
if hasattr(orm_model_field, "enum_type") and hasattr(orm_model_field.enum_type, "_member_map_")
|
|
128
|
+
else {}
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
form_widget_type = WidgetType.Input
|
|
132
|
+
form_widget_props = {
|
|
133
|
+
"required": required,
|
|
134
|
+
"disabled": field_name in self.readonly_fields,
|
|
135
|
+
"readOnly": field_name in self.readonly_fields,
|
|
136
|
+
}
|
|
137
|
+
filter_widget_type = WidgetType.Input
|
|
138
|
+
filter_widget_props: dict = {"required": False}
|
|
139
|
+
|
|
140
|
+
match field_type:
|
|
141
|
+
case "CharField":
|
|
142
|
+
form_widget_type = WidgetType.Input
|
|
143
|
+
filter_widget_type = WidgetType.Input
|
|
144
|
+
case "TextField":
|
|
145
|
+
form_widget_type = WidgetType.TextArea
|
|
146
|
+
filter_widget_type = WidgetType.TextArea
|
|
147
|
+
case "BooleanField":
|
|
148
|
+
form_widget_type = WidgetType.Switch
|
|
149
|
+
form_widget_props["required"] = False
|
|
150
|
+
filter_widget_type = WidgetType.RadioGroup
|
|
151
|
+
filter_widget_props["options"] = [
|
|
152
|
+
{"label": "Yes", "value": True},
|
|
153
|
+
{"label": "No", "value": False},
|
|
154
|
+
]
|
|
155
|
+
case "IntField" | "SmallIntField" | "BigIntField" | "FloatField" | "DecimalField":
|
|
156
|
+
form_widget_type = WidgetType.InputNumber
|
|
157
|
+
filter_widget_type = WidgetType.InputNumber
|
|
158
|
+
case "UUIDField":
|
|
159
|
+
form_widget_type = WidgetType.Input
|
|
160
|
+
filter_widget_type = WidgetType.Input
|
|
161
|
+
case "DateField":
|
|
162
|
+
form_widget_type = WidgetType.DatePicker
|
|
163
|
+
form_widget_props["format"] = settings.ADMIN_DATE_FORMAT
|
|
164
|
+
filter_widget_type = WidgetType.RangePicker
|
|
165
|
+
filter_widget_props["format"] = settings.ADMIN_DATE_FORMAT
|
|
166
|
+
case "DatetimeField":
|
|
167
|
+
form_widget_type = WidgetType.DateTimePicker
|
|
168
|
+
form_widget_props["format"] = settings.ADMIN_DATETIME_FORMAT
|
|
169
|
+
filter_widget_type = WidgetType.RangePicker
|
|
170
|
+
filter_widget_props["format"] = settings.ADMIN_DATETIME_FORMAT
|
|
171
|
+
filter_widget_props["showTime"] = True
|
|
172
|
+
case "TimeField":
|
|
173
|
+
form_widget_type = WidgetType.TimePicker
|
|
174
|
+
form_widget_props["format"] = settings.ADMIN_TIME_FORMAT
|
|
175
|
+
filter_widget_type = WidgetType.RangePicker
|
|
176
|
+
filter_widget_props["format"] = settings.ADMIN_TIME_FORMAT
|
|
177
|
+
filter_widget_props["showTime"] = True
|
|
178
|
+
case "CharEnumField" | "IntEnumField":
|
|
179
|
+
form_widget_props["options"] = [{"label": k, "value": v.value} for k, v in choices.items()]
|
|
180
|
+
filter_widget_props["options"] = [{"label": k, "value": v.value} for k, v in choices.items()]
|
|
181
|
+
if field_name in self.radio_fields:
|
|
182
|
+
form_widget_type = WidgetType.RadioGroup
|
|
183
|
+
filter_widget_type = WidgetType.CheckboxGroup
|
|
184
|
+
else:
|
|
185
|
+
form_widget_type = WidgetType.Select
|
|
186
|
+
filter_widget_type = WidgetType.Select
|
|
187
|
+
filter_widget_props["mode"] = "multiple"
|
|
188
|
+
case "JSONField":
|
|
189
|
+
form_widget_type = WidgetType.JsonTextArea
|
|
190
|
+
|
|
191
|
+
if is_m2m or field_type in ("ForeignKeyFieldInstance", "OneToOneFieldInstance"):
|
|
192
|
+
rel_model_cls = (
|
|
193
|
+
self.model_cls._meta.m2m[field_name].resolve_target()
|
|
194
|
+
if is_m2m
|
|
195
|
+
else (self._get_relation_target(field_name))
|
|
196
|
+
)
|
|
197
|
+
if rel_model_cls is not None:
|
|
198
|
+
form_widget_type, filter_widget_type = self._build_relation_widgets(
|
|
199
|
+
"ManyToManyFieldInstance" if is_m2m else field_type,
|
|
200
|
+
field_name,
|
|
201
|
+
rel_model_cls,
|
|
202
|
+
form_widget_props,
|
|
203
|
+
filter_widget_props,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
form_widget_type, form_widget_props = self.formfield_overrides.get(
|
|
207
|
+
field_name, (form_widget_type, form_widget_props)
|
|
208
|
+
)
|
|
209
|
+
fields.append(
|
|
210
|
+
ModelFieldWidgetSchema(
|
|
211
|
+
name=field_name,
|
|
212
|
+
column_name=column_name,
|
|
213
|
+
is_m2m=is_m2m,
|
|
214
|
+
is_pk=is_pk,
|
|
215
|
+
is_immutable=is_immutable,
|
|
216
|
+
form_widget_type=form_widget_type,
|
|
217
|
+
form_widget_props=form_widget_props,
|
|
218
|
+
filter_widget_type=filter_widget_type,
|
|
219
|
+
filter_widget_props=filter_widget_props,
|
|
220
|
+
)
|
|
221
|
+
)
|
|
222
|
+
return fields
|
|
223
|
+
|
|
224
|
+
async def orm_get_list(
|
|
225
|
+
self,
|
|
226
|
+
offset: int | None = None,
|
|
227
|
+
limit: int | None = None,
|
|
228
|
+
search: str | None = None,
|
|
229
|
+
sort_by: str | None = None,
|
|
230
|
+
filters: dict | None = None,
|
|
231
|
+
) -> tuple[list[Any], int]:
|
|
232
|
+
"""This method is used to get list of orm/db model objects.
|
|
233
|
+
|
|
234
|
+
:params offset: an offset for pagination.
|
|
235
|
+
:params limit: a limit for pagination.
|
|
236
|
+
:params search: a search query.
|
|
237
|
+
:params sort_by: a sort by field name.
|
|
238
|
+
:params filters: a dict of filters.
|
|
239
|
+
:return: A tuple of list of objects and total count.
|
|
240
|
+
"""
|
|
241
|
+
qs = self.model_cls.all()
|
|
242
|
+
|
|
243
|
+
if filters:
|
|
244
|
+
for field_with_condition, value in filters.items():
|
|
245
|
+
field = field_with_condition[0]
|
|
246
|
+
condition = field_with_condition[1]
|
|
247
|
+
qs = qs.filter(**{f"{field}__{condition}" if condition != "exact" else field: value})
|
|
248
|
+
|
|
249
|
+
search_fields = list(self.search_fields)
|
|
250
|
+
if search and search_fields:
|
|
251
|
+
qs = qs.filter(
|
|
252
|
+
functools.reduce(
|
|
253
|
+
operator.or_,
|
|
254
|
+
(Q(**{f + "__icontains": search}) for f in search_fields), # ty: ignore[invalid-argument-type]
|
|
255
|
+
Q(),
|
|
256
|
+
)
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
if sort_by:
|
|
260
|
+
qs = qs.order_by(self._resolve_ordering_field(sort_by))
|
|
261
|
+
elif self.ordering:
|
|
262
|
+
qs = qs.order_by(*(self._resolve_ordering_field(field) for field in self.ordering))
|
|
263
|
+
|
|
264
|
+
total = await qs.count()
|
|
265
|
+
|
|
266
|
+
if self.list_select_related:
|
|
267
|
+
qs = qs.select_related(*self.list_select_related)
|
|
268
|
+
|
|
269
|
+
if offset is not None and limit is not None:
|
|
270
|
+
qs = qs.offset(offset)
|
|
271
|
+
qs = qs.limit(limit)
|
|
272
|
+
|
|
273
|
+
return await qs, total
|
|
274
|
+
|
|
275
|
+
async def orm_get_obj(self, id: UUID | int | str) -> Any | None:
|
|
276
|
+
"""This method is used to get orm/db model object.
|
|
277
|
+
|
|
278
|
+
:params id: an id of object.
|
|
279
|
+
:return: An object.
|
|
280
|
+
"""
|
|
281
|
+
qs = self.model_cls.filter(**{self.get_model_pk_name(self.model_cls): id})
|
|
282
|
+
return await qs.first()
|
|
283
|
+
|
|
284
|
+
async def orm_save_obj(self, id: UUID | Any | None, payload: dict) -> Any:
|
|
285
|
+
"""This method is used to save orm/db model object.
|
|
286
|
+
|
|
287
|
+
:params id: an id of object.
|
|
288
|
+
:params payload: a dict of payload.
|
|
289
|
+
:return: An object.
|
|
290
|
+
"""
|
|
291
|
+
if id:
|
|
292
|
+
obj = await self.model_cls.filter(**{self.get_model_pk_name(self.model_cls): id}).first()
|
|
293
|
+
if not obj:
|
|
294
|
+
return None
|
|
295
|
+
for k, v in payload.items():
|
|
296
|
+
setattr(obj, k, v)
|
|
297
|
+
else:
|
|
298
|
+
obj = self.model_cls(**payload)
|
|
299
|
+
await obj.save(update_fields=list(payload.keys()) if id else None)
|
|
300
|
+
return obj
|
|
301
|
+
|
|
302
|
+
async def orm_delete_obj(self, id: UUID | int | str) -> None:
|
|
303
|
+
"""This method is used to delete orm/db model object.
|
|
304
|
+
|
|
305
|
+
:params id: an id of object.
|
|
306
|
+
:return: None.
|
|
307
|
+
"""
|
|
308
|
+
qs = self.model_cls.filter(**{self.get_model_pk_name(self.model_cls): id})
|
|
309
|
+
await qs.delete()
|
|
310
|
+
|
|
311
|
+
async def orm_get_m2m_ids(self, obj: Any, field: str) -> list[int | UUID]:
|
|
312
|
+
"""This method is used to get m2m ids.
|
|
313
|
+
|
|
314
|
+
:params obj: an object.
|
|
315
|
+
:params field: a m2m field name.
|
|
316
|
+
:return: A list of ids.
|
|
317
|
+
"""
|
|
318
|
+
m2m_rel = getattr(obj, field)
|
|
319
|
+
target_model = m2m_rel.target
|
|
320
|
+
return await m2m_rel.all().values_list(self.get_model_pk_name(target_model), flat=True)
|
|
321
|
+
|
|
322
|
+
async def orm_save_m2m_ids(self, obj: Any, field: str, ids: list[int | str | UUID]) -> None:
|
|
323
|
+
"""This method is used to save m2m ids.
|
|
324
|
+
|
|
325
|
+
:params obj: an object.
|
|
326
|
+
:params field: a m2m field name.
|
|
327
|
+
:params ids: a list of ids.
|
|
328
|
+
:return: None.
|
|
329
|
+
"""
|
|
330
|
+
m2m_rel = getattr(obj, field)
|
|
331
|
+
await m2m_rel.clear()
|
|
332
|
+
target_model = m2m_rel.target
|
|
333
|
+
target_objs = []
|
|
334
|
+
for rel_id in ids:
|
|
335
|
+
target_obj = target_model()
|
|
336
|
+
setattr(target_obj, self.get_model_pk_name(target_model), rel_id)
|
|
337
|
+
target_obj._saved_in_db = True
|
|
338
|
+
target_objs.append(target_obj)
|
|
339
|
+
if target_objs:
|
|
340
|
+
await m2m_rel.add(*target_objs)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
class YaraOrmModelAdmin(YaraOrmMixin, ModelAdmin):
|
|
344
|
+
pass
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
class YaraOrmInlineModelAdmin(YaraOrmMixin, InlineModelAdmin):
|
|
348
|
+
pass
|