fastadmin 0.6.0__tar.gz → 0.7.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.
Files changed (50) hide show
  1. {fastadmin-0.6.0 → fastadmin-0.7.0}/PKG-INFO +14 -5
  2. {fastadmin-0.6.0 → fastadmin-0.7.0}/README.md +10 -3
  3. {fastadmin-0.6.0 → fastadmin-0.7.0}/fastadmin/__init__.py +5 -0
  4. fastadmin-0.7.0/fastadmin/models/orms/yaraorm.py +348 -0
  5. {fastadmin-0.6.0 → fastadmin-0.7.0}/fastadmin/static/index.min.js +131 -131
  6. {fastadmin-0.6.0 → fastadmin-0.7.0}/pyproject.toml +4 -1
  7. {fastadmin-0.6.0 → fastadmin-0.7.0}/LICENSE.md +0 -0
  8. {fastadmin-0.6.0 → fastadmin-0.7.0}/fastadmin/api/__init__.py +0 -0
  9. {fastadmin-0.6.0 → fastadmin-0.7.0}/fastadmin/api/exceptions.py +0 -0
  10. {fastadmin-0.6.0 → fastadmin-0.7.0}/fastadmin/api/frameworks/__init__.py +0 -0
  11. {fastadmin-0.6.0 → fastadmin-0.7.0}/fastadmin/api/frameworks/django/__init__.py +0 -0
  12. {fastadmin-0.6.0 → fastadmin-0.7.0}/fastadmin/api/frameworks/django/app/__init__.py +0 -0
  13. {fastadmin-0.6.0 → fastadmin-0.7.0}/fastadmin/api/frameworks/django/app/api.py +0 -0
  14. {fastadmin-0.6.0 → fastadmin-0.7.0}/fastadmin/api/frameworks/django/app/urls.py +0 -0
  15. {fastadmin-0.6.0 → fastadmin-0.7.0}/fastadmin/api/frameworks/django/app/views.py +0 -0
  16. {fastadmin-0.6.0 → fastadmin-0.7.0}/fastadmin/api/frameworks/fastapi/__init__.py +0 -0
  17. {fastadmin-0.6.0 → fastadmin-0.7.0}/fastadmin/api/frameworks/fastapi/api.py +0 -0
  18. {fastadmin-0.6.0 → fastadmin-0.7.0}/fastadmin/api/frameworks/fastapi/app.py +0 -0
  19. {fastadmin-0.6.0 → fastadmin-0.7.0}/fastadmin/api/frameworks/fastapi/views.py +0 -0
  20. {fastadmin-0.6.0 → fastadmin-0.7.0}/fastadmin/api/frameworks/flask/__init__.py +0 -0
  21. {fastadmin-0.6.0 → fastadmin-0.7.0}/fastadmin/api/frameworks/flask/api.py +0 -0
  22. {fastadmin-0.6.0 → fastadmin-0.7.0}/fastadmin/api/frameworks/flask/app.py +0 -0
  23. {fastadmin-0.6.0 → fastadmin-0.7.0}/fastadmin/api/frameworks/flask/views.py +0 -0
  24. {fastadmin-0.6.0 → fastadmin-0.7.0}/fastadmin/api/helpers.py +0 -0
  25. {fastadmin-0.6.0 → fastadmin-0.7.0}/fastadmin/api/schemas.py +0 -0
  26. {fastadmin-0.6.0 → fastadmin-0.7.0}/fastadmin/api/service.py +0 -0
  27. {fastadmin-0.6.0 → fastadmin-0.7.0}/fastadmin/models/__init__.py +0 -0
  28. {fastadmin-0.6.0 → fastadmin-0.7.0}/fastadmin/models/base.py +0 -0
  29. {fastadmin-0.6.0 → fastadmin-0.7.0}/fastadmin/models/decorators.py +0 -0
  30. {fastadmin-0.6.0 → fastadmin-0.7.0}/fastadmin/models/helpers.py +0 -0
  31. {fastadmin-0.6.0 → fastadmin-0.7.0}/fastadmin/models/orms/__init__.py +0 -0
  32. {fastadmin-0.6.0 → fastadmin-0.7.0}/fastadmin/models/orms/django.py +0 -0
  33. {fastadmin-0.6.0 → fastadmin-0.7.0}/fastadmin/models/orms/ponyorm.py +0 -0
  34. {fastadmin-0.6.0 → fastadmin-0.7.0}/fastadmin/models/orms/sqlalchemy.py +0 -0
  35. {fastadmin-0.6.0 → fastadmin-0.7.0}/fastadmin/models/orms/tortoise.py +0 -0
  36. {fastadmin-0.6.0 → fastadmin-0.7.0}/fastadmin/models/schemas.py +0 -0
  37. {fastadmin-0.6.0 → fastadmin-0.7.0}/fastadmin/settings.py +0 -0
  38. {fastadmin-0.6.0 → fastadmin-0.7.0}/fastadmin/static/assets/worker-6Z7niv9l.js +0 -0
  39. {fastadmin-0.6.0 → fastadmin-0.7.0}/fastadmin/static/assets/worker-C151k0-L.js +0 -0
  40. {fastadmin-0.6.0 → fastadmin-0.7.0}/fastadmin/static/assets/worker-C9KPOOKD.js +0 -0
  41. {fastadmin-0.6.0 → fastadmin-0.7.0}/fastadmin/static/assets/worker-D1fCgYB7.js +0 -0
  42. {fastadmin-0.6.0 → fastadmin-0.7.0}/fastadmin/static/assets/worker-D3TWcJOI.js +0 -0
  43. {fastadmin-0.6.0 → fastadmin-0.7.0}/fastadmin/static/assets/worker-DRNcaZ-V.js +0 -0
  44. {fastadmin-0.6.0 → fastadmin-0.7.0}/fastadmin/static/assets/worker-MF2p-l5_.js +0 -0
  45. {fastadmin-0.6.0 → fastadmin-0.7.0}/fastadmin/static/images/favicon.png +0 -0
  46. {fastadmin-0.6.0 → fastadmin-0.7.0}/fastadmin/static/images/header-logo.svg +0 -0
  47. {fastadmin-0.6.0 → fastadmin-0.7.0}/fastadmin/static/images/sign-in-logo.svg +0 -0
  48. {fastadmin-0.6.0 → fastadmin-0.7.0}/fastadmin/static/index.html +0 -0
  49. {fastadmin-0.6.0 → fastadmin-0.7.0}/fastadmin/static/index.min.css +0 -0
  50. {fastadmin-0.6.0 → fastadmin-0.7.0}/fastadmin/templates/index.html +0 -0
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastadmin
3
- Version: 0.6.0
3
+ Version: 0.7.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 Pony ORM** — with authentication, filters, search,
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 Pony ORM.
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 or Tortoise ORM**, or a lightweight
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 Pony ORM** — with authentication, filters, search,
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 Pony ORM.
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 or Tortoise ORM**, or a lightweight
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,6 +32,11 @@ 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
36
41
  from fastadmin.api.exceptions import AdminApiException # noqa: F401
37
42
 
@@ -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