fastadmin 0.4.7__tar.gz → 0.4.9__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 (49) hide show
  1. {fastadmin-0.4.7 → fastadmin-0.4.9}/PKG-INFO +1 -1
  2. {fastadmin-0.4.7 → fastadmin-0.4.9}/fastadmin/api/frameworks/django/app/api.py +2 -0
  3. {fastadmin-0.4.7 → fastadmin-0.4.9}/fastadmin/api/frameworks/fastapi/api.py +3 -0
  4. {fastadmin-0.4.7 → fastadmin-0.4.9}/fastadmin/api/frameworks/flask/api.py +2 -0
  5. {fastadmin-0.4.7 → fastadmin-0.4.9}/fastadmin/api/service.py +8 -0
  6. {fastadmin-0.4.7 → fastadmin-0.4.9}/fastadmin/models/base.py +43 -3
  7. {fastadmin-0.4.7 → fastadmin-0.4.9}/fastadmin/models/orms/django.py +1 -1
  8. {fastadmin-0.4.7 → fastadmin-0.4.9}/fastadmin/models/orms/ponyorm.py +13 -3
  9. {fastadmin-0.4.7 → fastadmin-0.4.9}/fastadmin/models/orms/sqlalchemy.py +1 -1
  10. {fastadmin-0.4.7 → fastadmin-0.4.9}/fastadmin/models/orms/tortoise.py +1 -1
  11. {fastadmin-0.4.7 → fastadmin-0.4.9}/fastadmin/models/schemas.py +1 -1
  12. {fastadmin-0.4.7 → fastadmin-0.4.9}/fastadmin/static/index.min.js +180 -180
  13. {fastadmin-0.4.7 → fastadmin-0.4.9}/pyproject.toml +4 -1
  14. {fastadmin-0.4.7 → fastadmin-0.4.9}/LICENSE +0 -0
  15. {fastadmin-0.4.7 → fastadmin-0.4.9}/README.md +0 -0
  16. {fastadmin-0.4.7 → fastadmin-0.4.9}/fastadmin/__init__.py +0 -0
  17. {fastadmin-0.4.7 → fastadmin-0.4.9}/fastadmin/api/__init__.py +0 -0
  18. {fastadmin-0.4.7 → fastadmin-0.4.9}/fastadmin/api/exceptions.py +0 -0
  19. {fastadmin-0.4.7 → fastadmin-0.4.9}/fastadmin/api/frameworks/__init__.py +0 -0
  20. {fastadmin-0.4.7 → fastadmin-0.4.9}/fastadmin/api/frameworks/django/__init__.py +0 -0
  21. {fastadmin-0.4.7 → fastadmin-0.4.9}/fastadmin/api/frameworks/django/app/__init__.py +0 -0
  22. {fastadmin-0.4.7 → fastadmin-0.4.9}/fastadmin/api/frameworks/django/app/urls.py +0 -0
  23. {fastadmin-0.4.7 → fastadmin-0.4.9}/fastadmin/api/frameworks/django/app/views.py +0 -0
  24. {fastadmin-0.4.7 → fastadmin-0.4.9}/fastadmin/api/frameworks/fastapi/__init__.py +0 -0
  25. {fastadmin-0.4.7 → fastadmin-0.4.9}/fastadmin/api/frameworks/fastapi/app.py +0 -0
  26. {fastadmin-0.4.7 → fastadmin-0.4.9}/fastadmin/api/frameworks/fastapi/views.py +0 -0
  27. {fastadmin-0.4.7 → fastadmin-0.4.9}/fastadmin/api/frameworks/flask/__init__.py +0 -0
  28. {fastadmin-0.4.7 → fastadmin-0.4.9}/fastadmin/api/frameworks/flask/app.py +0 -0
  29. {fastadmin-0.4.7 → fastadmin-0.4.9}/fastadmin/api/frameworks/flask/views.py +0 -0
  30. {fastadmin-0.4.7 → fastadmin-0.4.9}/fastadmin/api/helpers.py +0 -0
  31. {fastadmin-0.4.7 → fastadmin-0.4.9}/fastadmin/api/schemas.py +0 -0
  32. {fastadmin-0.4.7 → fastadmin-0.4.9}/fastadmin/models/__init__.py +0 -0
  33. {fastadmin-0.4.7 → fastadmin-0.4.9}/fastadmin/models/decorators.py +0 -0
  34. {fastadmin-0.4.7 → fastadmin-0.4.9}/fastadmin/models/helpers.py +0 -0
  35. {fastadmin-0.4.7 → fastadmin-0.4.9}/fastadmin/models/orms/__init__.py +0 -0
  36. {fastadmin-0.4.7 → fastadmin-0.4.9}/fastadmin/settings.py +0 -0
  37. {fastadmin-0.4.7 → fastadmin-0.4.9}/fastadmin/static/assets/worker-6Z7niv9l.js +0 -0
  38. {fastadmin-0.4.7 → fastadmin-0.4.9}/fastadmin/static/assets/worker-C151k0-L.js +0 -0
  39. {fastadmin-0.4.7 → fastadmin-0.4.9}/fastadmin/static/assets/worker-C9KPOOKD.js +0 -0
  40. {fastadmin-0.4.7 → fastadmin-0.4.9}/fastadmin/static/assets/worker-D1fCgYB7.js +0 -0
  41. {fastadmin-0.4.7 → fastadmin-0.4.9}/fastadmin/static/assets/worker-D3TWcJOI.js +0 -0
  42. {fastadmin-0.4.7 → fastadmin-0.4.9}/fastadmin/static/assets/worker-DRNcaZ-V.js +0 -0
  43. {fastadmin-0.4.7 → fastadmin-0.4.9}/fastadmin/static/assets/worker-MF2p-l5_.js +0 -0
  44. {fastadmin-0.4.7 → fastadmin-0.4.9}/fastadmin/static/images/favicon.png +0 -0
  45. {fastadmin-0.4.7 → fastadmin-0.4.9}/fastadmin/static/images/header-logo.svg +0 -0
  46. {fastadmin-0.4.7 → fastadmin-0.4.9}/fastadmin/static/images/sign-in-logo.svg +0 -0
  47. {fastadmin-0.4.7 → fastadmin-0.4.9}/fastadmin/static/index.html +0 -0
  48. {fastadmin-0.4.7 → fastadmin-0.4.9}/fastadmin/static/index.min.css +0 -0
  49. {fastadmin-0.4.7 → fastadmin-0.4.9}/fastadmin/templates/index.html +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastadmin
3
- Version: 0.4.7
3
+ Version: 0.4.9
4
4
  Summary: FastAdmin is an easy-to-use Admin Dashboard App for FastAPI/Flask/Django inspired by Django Admin.
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -301,12 +301,14 @@ async def upload_file(
301
301
  return JsonResponse({"error": "File not found"}, status=400)
302
302
  file_name = file.name
303
303
  file_content = file.read()
304
+ obj_id = request.GET.get("id") or None
304
305
  file_url = await api_service.upload_file(
305
306
  request.COOKIES.get(settings.ADMIN_SESSION_ID_KEY, None),
306
307
  model,
307
308
  field_name,
308
309
  file_name,
309
310
  file_content,
311
+ id=obj_id,
310
312
  request=request,
311
313
  )
312
314
  return JsonResponse(file_url, safe=False)
@@ -244,6 +244,7 @@ async def upload_file(
244
244
  model: str,
245
245
  field_name: str,
246
246
  file: UploadFile,
247
+ id: str | None = None,
247
248
  ) -> str:
248
249
  """This method is used to upload files.
249
250
 
@@ -251,6 +252,7 @@ async def upload_file(
251
252
  :params model: a name of model.
252
253
  :params field_name: a name of field.
253
254
  :params file: a file object.
255
+ :params id: an optional id of an existing object (change page). None on the add page.
254
256
  :return: A file url.
255
257
  """
256
258
  try:
@@ -264,6 +266,7 @@ async def upload_file(
264
266
  field_name,
265
267
  file_name,
266
268
  file_content,
269
+ id=id,
267
270
  request=request,
268
271
  )
269
272
  except AdminApiException as e:
@@ -267,12 +267,14 @@ async def upload_file(
267
267
  http_exception.code = 422
268
268
  raise http_exception from None
269
269
  file_content = file.read()
270
+ obj_id = request.args.get("id") or None
270
271
  return await api_service.upload_file(
271
272
  request.cookies.get(settings.ADMIN_SESSION_ID_KEY, None),
272
273
  model,
273
274
  field_name,
274
275
  file_name,
275
276
  file_content,
277
+ id=obj_id,
276
278
  request=request,
277
279
  )
278
280
  except AdminApiException as e:
@@ -329,6 +329,7 @@ class ApiService:
329
329
  field_name: str,
330
330
  file_name: str,
331
331
  file_content: bytes,
332
+ id: UUID | int | str | None = None,
332
333
  request: Any | None = None,
333
334
  ) -> str:
334
335
  _current_user_id, current_user = await self._get_authenticated_user(session_id)
@@ -339,6 +340,10 @@ class ApiService:
339
340
  self._bind_admin_context(admin_model, request=request, user=current_user)
340
341
 
341
342
  try:
343
+ obj = await admin_model.orm_get_obj(id) if id is not None else None
344
+ if id is not None and obj is None:
345
+ raise AdminApiException(404, detail=f"{model} with id {id} not found.")
346
+
342
347
  if inspect.iscoroutinefunction(admin_model.upload_file):
343
348
  upload_file_fn = admin_model.upload_file
344
349
  else:
@@ -348,7 +353,10 @@ class ApiService:
348
353
  field_name,
349
354
  file_name,
350
355
  file_content,
356
+ obj=obj,
351
357
  )
358
+ except AdminApiException:
359
+ raise
352
360
  except Exception as e:
353
361
  raise AdminApiException(500, detail=f"Error uploading file for {model}: {e}") from e
354
362
 
@@ -14,6 +14,8 @@ from asgiref.sync import sync_to_async
14
14
  from fastadmin.api.schemas import ExportFormat
15
15
  from fastadmin.models.schemas import ModelFieldWidgetSchema, WidgetType
16
16
 
17
+ Model = Any
18
+
17
19
 
18
20
  class BaseModelAdmin:
19
21
  """Base class for model admin"""
@@ -313,7 +315,7 @@ class BaseModelAdmin:
313
315
  """
314
316
  raise NotImplementedError
315
317
 
316
- async def orm_save_m2m_ids(self, obj: Any, field: str, ids: list[int | UUID]) -> None:
318
+ async def orm_save_m2m_ids(self, obj: Any, field: str, ids: list[int | str | UUID]) -> None:
317
319
  """This method is used to get m2m ids.
318
320
 
319
321
  :params obj: an object.
@@ -373,7 +375,7 @@ class BaseModelAdmin:
373
375
  return sort_by
374
376
 
375
377
  async def serialize_obj_attributes(
376
- self, obj: Any, attributes_to_serizalize: list[ModelFieldWidgetSchema]
378
+ self, obj: Any, attributes_to_serizalize: list[ModelFieldWidgetSchema], list_view: bool = False
377
379
  ) -> dict[str, Any]:
378
380
  """Serialize orm model obj attribute to dict.
379
381
 
@@ -388,7 +390,10 @@ class BaseModelAdmin:
388
390
  # Avoid scientific notation for Decimal values in API responses,
389
391
  # e.g. 3.75E+3 -> "3750"
390
392
  value = format(value, "f")
393
+
391
394
  serialized_dict[field.name] = value
395
+ if not list_view and field.form_widget_type in (WidgetType.UploadFile, WidgetType.UploadImage) and value:
396
+ serialized_dict[f"{field.name}__url"] = await self.get_file_url(field.name, value, obj)
392
397
  if inspect.iscoroutinefunction(obj.__str__):
393
398
  str_fn = obj.__str__
394
399
  else:
@@ -435,7 +440,7 @@ class BaseModelAdmin:
435
440
  else:
436
441
  attributes_to_serizalize.append(field)
437
442
 
438
- obj_dict.update(await self.serialize_obj_attributes(obj, attributes_to_serizalize))
443
+ obj_dict.update(await self.serialize_obj_attributes(obj, attributes_to_serizalize, list_view=list_view))
439
444
 
440
445
  for field_name in fields_for_serialize:
441
446
  display_field_function = getattr(self, field_name, None)
@@ -464,6 +469,7 @@ class BaseModelAdmin:
464
469
  return datetime.datetime.fromisoformat(value).date()
465
470
  case WidgetType.DateTimePicker:
466
471
  return datetime.datetime.fromisoformat(value)
472
+
467
473
  case _:
468
474
  return value
469
475
 
@@ -541,17 +547,51 @@ class BaseModelAdmin:
541
547
  """
542
548
  await self.orm_delete_obj(id)
543
549
 
550
+ async def get_file_url(self, field_name: str, value: str, obj: Any | None = None) -> str:
551
+ """Return the display URL for an uploaded file field.
552
+
553
+ Called during serialization for every ``UploadFile`` and ``UploadImage``
554
+ field that has a non-empty stored value. The result is emitted alongside
555
+ the stored value as ``{field_name}__url`` and forwarded to the upload
556
+ widget as ``valueRepr`` so the clickable link can differ from the raw
557
+ stored key (e.g. an S3 presigned URL instead of an ``s3://`` URI).
558
+
559
+ The stored ``value`` is never modified — it is still what the form
560
+ submits on save — so there is no risk of data corruption.
561
+
562
+ Override to return a dynamic URL. Example using aiobotocore:
563
+
564
+ .. code-block:: python
565
+
566
+ async def get_file_url(self, field_name: str, value: str, obj=None) -> str:
567
+ bucket, key = value.replace("s3://", "").split("/", 1)
568
+ async with aiobotocore.session.get_session().create_client("s3") as s3:
569
+ return await s3.generate_presigned_url(
570
+ "get_object",
571
+ Params={"Bucket": bucket, "Key": key},
572
+ ExpiresIn=3600,
573
+ )
574
+
575
+ :params field_name: name of the upload field being serialized.
576
+ :params value: raw stored value (e.g. ``"s3://bucket/key"`` or ``"/media/file.pdf"``).
577
+ :params obj: the orm/db model instance being serialized, or None.
578
+ :return: URL string to expose to the frontend for display/download.
579
+ """
580
+ return value
581
+
544
582
  async def upload_file(
545
583
  self,
546
584
  field_name: str,
547
585
  file_name: str,
548
586
  file_content: bytes,
587
+ obj: Any | None = None,
549
588
  ) -> str:
550
589
  """This method is used to upload files.
551
590
 
552
591
  :params field_name: a name of field.
553
592
  :params file_name: a name of file.
554
593
  :params file_content: a content of file.
594
+ :params obj: an orm/db model object. None on the add (create) page; the existing model instance on the change (edit) page.
555
595
  :return: A file url.
556
596
  """
557
597
  raise NotImplementedError
@@ -331,7 +331,7 @@ class DjangoORMMixin:
331
331
  return list(m2m_rel.all().values_list(self.get_model_pk_name(remote_model), flat=True))
332
332
 
333
333
  @sync_to_async
334
- def orm_save_m2m_ids(self, obj: Any, field: str, ids: list[int | UUID]) -> None:
334
+ def orm_save_m2m_ids(self, obj: Any, field: str, ids: list[int | str | UUID]) -> None:
335
335
  """This method is used to get m2m ids.
336
336
 
337
337
  :params obj: an object.
@@ -408,7 +408,7 @@ class PonyORMMixin:
408
408
 
409
409
  @sync_to_async
410
410
  @db_session
411
- def orm_save_m2m_ids(self, obj: Any, field: str, ids: list[int | UUID]) -> None:
411
+ def orm_save_m2m_ids(self, obj: Any, field: str, ids: list[int | str | UUID]) -> None:
412
412
  """This method is used to get m2m ids.
413
413
 
414
414
  :params obj: an object.
@@ -425,7 +425,17 @@ class PonyORMMixin:
425
425
  if ids:
426
426
  rel_model_cls = getattr(self.model_cls, field).py_type
427
427
  rel_key_id = self.get_model_pk_name(rel_model_cls)
428
- rel_objs = list(rel_model_cls.select(lambda o: getattr(o, rel_key_id) in ids))
428
+ rel_id_type = getattr(getattr(rel_model_cls, "_pk_", None), "py_type", None)
429
+ normalized_ids = []
430
+ for rel_id in ids:
431
+ normalized_id = rel_id
432
+ if rel_id_type:
433
+ try:
434
+ normalized_id = rel_id_type(rel_id)
435
+ except (ValueError, TypeError) as e:
436
+ raise ValueError(f"Invalid id {rel_id!r} for relation {field}.") from e
437
+ normalized_ids.append(normalized_id)
438
+ rel_objs = list(rel_model_cls.select(lambda o: getattr(o, rel_key_id) in normalized_ids))
429
439
  getattr(obj, field).add(rel_objs)
430
440
  flush()
431
441
  commit()
@@ -433,7 +443,7 @@ class PonyORMMixin:
433
443
  @sync_to_async
434
444
  @db_session
435
445
  def serialize_obj_attributes(
436
- self, obj: Any, attributes_to_serizalize: list[ModelFieldWidgetSchema]
446
+ self, obj: Any, attributes_to_serizalize: list[ModelFieldWidgetSchema], list_view: bool = False
437
447
  ) -> dict[str, Any]:
438
448
  """Serialize orm model obj attribute to dict.
439
449
 
@@ -467,7 +467,7 @@ class SqlAlchemyMixin:
467
467
  rel_pk_name = self.get_model_pk_name(related_class)
468
468
  return [getattr(related_obj, rel_pk_name) for related_obj in getattr(loaded, field, [])]
469
469
 
470
- async def orm_save_m2m_ids(self, obj: Any, field: str, ids: list[int | UUID]) -> None:
470
+ async def orm_save_m2m_ids(self, obj: Any, field: str, ids: list[int | str | UUID]) -> None:
471
471
  """This method is used to get m2m ids.
472
472
 
473
473
  :params obj: an object.
@@ -338,7 +338,7 @@ class TortoiseMixin:
338
338
  remote_model = m2m_rel.remote_model
339
339
  return await m2m_rel.all().values_list(self.get_model_pk_name(remote_model), flat=True)
340
340
 
341
- async def orm_save_m2m_ids(self, obj: Any, field: str, ids: list[int | UUID]) -> None:
341
+ async def orm_save_m2m_ids(self, obj: Any, field: str, ids: list[int | str | UUID]) -> None:
342
342
  """This method is used to get m2m ids.
343
343
 
344
344
  :params obj: an object.
@@ -256,7 +256,7 @@ class ModelFieldWidgetSchema:
256
256
  class ActionInputSchema:
257
257
  """Action input schema"""
258
258
 
259
- ids: list[int | UUID]
259
+ ids: list[int | str | UUID]
260
260
 
261
261
 
262
262
  @dataclass