fastadmin 0.2.20__tar.gz → 0.2.22__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 (48) hide show
  1. {fastadmin-0.2.20 → fastadmin-0.2.22}/PKG-INFO +86 -18
  2. {fastadmin-0.2.20 → fastadmin-0.2.22}/README.md +85 -17
  3. {fastadmin-0.2.20 → fastadmin-0.2.22}/fastadmin/models/base.py +6 -3
  4. {fastadmin-0.2.20 → fastadmin-0.2.22}/fastadmin/static/index.min.js +52 -52
  5. {fastadmin-0.2.20 → fastadmin-0.2.22}/pyproject.toml +1 -1
  6. {fastadmin-0.2.20 → fastadmin-0.2.22}/LICENSE +0 -0
  7. {fastadmin-0.2.20 → fastadmin-0.2.22}/fastadmin/__init__.py +0 -0
  8. {fastadmin-0.2.20 → fastadmin-0.2.22}/fastadmin/api/__init__.py +0 -0
  9. {fastadmin-0.2.20 → fastadmin-0.2.22}/fastadmin/api/exceptions.py +0 -0
  10. {fastadmin-0.2.20 → fastadmin-0.2.22}/fastadmin/api/frameworks/__init__.py +0 -0
  11. {fastadmin-0.2.20 → fastadmin-0.2.22}/fastadmin/api/frameworks/django/__init__.py +0 -0
  12. {fastadmin-0.2.20 → fastadmin-0.2.22}/fastadmin/api/frameworks/django/app/__init__.py +0 -0
  13. {fastadmin-0.2.20 → fastadmin-0.2.22}/fastadmin/api/frameworks/django/app/api.py +0 -0
  14. {fastadmin-0.2.20 → fastadmin-0.2.22}/fastadmin/api/frameworks/django/app/urls.py +0 -0
  15. {fastadmin-0.2.20 → fastadmin-0.2.22}/fastadmin/api/frameworks/django/app/views.py +0 -0
  16. {fastadmin-0.2.20 → fastadmin-0.2.22}/fastadmin/api/frameworks/fastapi/__init__.py +0 -0
  17. {fastadmin-0.2.20 → fastadmin-0.2.22}/fastadmin/api/frameworks/fastapi/api.py +0 -0
  18. {fastadmin-0.2.20 → fastadmin-0.2.22}/fastadmin/api/frameworks/fastapi/app.py +0 -0
  19. {fastadmin-0.2.20 → fastadmin-0.2.22}/fastadmin/api/frameworks/fastapi/views.py +0 -0
  20. {fastadmin-0.2.20 → fastadmin-0.2.22}/fastadmin/api/frameworks/flask/__init__.py +0 -0
  21. {fastadmin-0.2.20 → fastadmin-0.2.22}/fastadmin/api/frameworks/flask/api.py +0 -0
  22. {fastadmin-0.2.20 → fastadmin-0.2.22}/fastadmin/api/frameworks/flask/app.py +0 -0
  23. {fastadmin-0.2.20 → fastadmin-0.2.22}/fastadmin/api/frameworks/flask/views.py +0 -0
  24. {fastadmin-0.2.20 → fastadmin-0.2.22}/fastadmin/api/helpers.py +0 -0
  25. {fastadmin-0.2.20 → fastadmin-0.2.22}/fastadmin/api/schemas.py +0 -0
  26. {fastadmin-0.2.20 → fastadmin-0.2.22}/fastadmin/api/service.py +0 -0
  27. {fastadmin-0.2.20 → fastadmin-0.2.22}/fastadmin/models/__init__.py +0 -0
  28. {fastadmin-0.2.20 → fastadmin-0.2.22}/fastadmin/models/decorators.py +0 -0
  29. {fastadmin-0.2.20 → fastadmin-0.2.22}/fastadmin/models/helpers.py +0 -0
  30. {fastadmin-0.2.20 → fastadmin-0.2.22}/fastadmin/models/orms/__init__.py +0 -0
  31. {fastadmin-0.2.20 → fastadmin-0.2.22}/fastadmin/models/orms/django.py +0 -0
  32. {fastadmin-0.2.20 → fastadmin-0.2.22}/fastadmin/models/orms/ponyorm.py +0 -0
  33. {fastadmin-0.2.20 → fastadmin-0.2.22}/fastadmin/models/orms/sqlalchemy.py +0 -0
  34. {fastadmin-0.2.20 → fastadmin-0.2.22}/fastadmin/models/orms/tortoise.py +0 -0
  35. {fastadmin-0.2.20 → fastadmin-0.2.22}/fastadmin/models/schemas.py +0 -0
  36. {fastadmin-0.2.20 → fastadmin-0.2.22}/fastadmin/settings.py +0 -0
  37. {fastadmin-0.2.20 → fastadmin-0.2.22}/fastadmin/static/assets/worker-6Z7niv9l.js +0 -0
  38. {fastadmin-0.2.20 → fastadmin-0.2.22}/fastadmin/static/assets/worker-C151k0-L.js +0 -0
  39. {fastadmin-0.2.20 → fastadmin-0.2.22}/fastadmin/static/assets/worker-D1fCgYB7.js +0 -0
  40. {fastadmin-0.2.20 → fastadmin-0.2.22}/fastadmin/static/assets/worker-D3TWcJOI.js +0 -0
  41. {fastadmin-0.2.20 → fastadmin-0.2.22}/fastadmin/static/assets/worker-DRNcaZ-V.js +0 -0
  42. {fastadmin-0.2.20 → fastadmin-0.2.22}/fastadmin/static/assets/worker-MF2p-l5_.js +0 -0
  43. {fastadmin-0.2.20 → fastadmin-0.2.22}/fastadmin/static/images/favicon.png +0 -0
  44. {fastadmin-0.2.20 → fastadmin-0.2.22}/fastadmin/static/images/header-logo.svg +0 -0
  45. {fastadmin-0.2.20 → fastadmin-0.2.22}/fastadmin/static/images/sign-in-logo.svg +0 -0
  46. {fastadmin-0.2.20 → fastadmin-0.2.22}/fastadmin/static/index.html +0 -0
  47. {fastadmin-0.2.20 → fastadmin-0.2.22}/fastadmin/static/index.min.css +0 -0
  48. {fastadmin-0.2.20 → fastadmin-0.2.22}/fastadmin/templates/index.html +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: fastadmin
3
- Version: 0.2.20
3
+ Version: 0.2.22
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
  Keywords: fastapi,admin
@@ -475,13 +475,14 @@ Register ORM models
475
475
 
476
476
 
477
477
  ```python
478
+ import typing as tp
478
479
  from uuid import UUID
479
480
 
480
481
  import bcrypt
481
482
  from tortoise import fields
482
483
  from tortoise.models import Model
483
484
 
484
- from fastadmin import TortoiseModelAdmin, register
485
+ from fastadmin import TortoiseModelAdmin, WidgetType, register
485
486
 
486
487
 
487
488
  class User(Model):
@@ -489,6 +490,7 @@ class User(Model):
489
490
  hash_password = fields.CharField(max_length=255)
490
491
  is_superuser = fields.BooleanField(default=False)
491
492
  is_active = fields.BooleanField(default=False)
493
+ avatar_url = fields.TextField(null=True)
492
494
 
493
495
  def __str__(self):
494
496
  return self.username
@@ -501,15 +503,39 @@ class UserAdmin(TortoiseModelAdmin):
501
503
  list_display_links = ("id", "username")
502
504
  list_filter = ("id", "username", "is_superuser", "is_active")
503
505
  search_fields = ("username",)
504
-
505
- async def authenticate(self, username: str, password: str) -> UUID | int | None:
506
- user = await User.filter(username=username, is_superuser=True).first()
506
+ formfield_overrides = { # noqa: RUF012
507
+ "username": (WidgetType.SlugInput, {"required": True}),
508
+ "password": (WidgetType.PasswordInput, {"passwordModalForm": True}),
509
+ "avatar_url": (
510
+ WidgetType.Upload,
511
+ {
512
+ "required": False,
513
+ # Disable crop image for upload field
514
+ # "disableCropImage": True,
515
+ },
516
+ ),
517
+ }
518
+
519
+ async def authenticate(self, username: str, password: str) -> int | None:
520
+ user = await self.model_cls.filter(phone=username, is_superuser=True).first()
507
521
  if not user:
508
522
  return None
509
523
  if not bcrypt.checkpw(password.encode(), user.hash_password.encode()):
510
524
  return None
511
525
  return user.id
512
526
 
527
+ async def change_password(self, id: UUID | int, password: str) -> None:
528
+ user = await self.model_cls.filter(id=id).first()
529
+ if not user:
530
+ return
531
+ user.hash_password = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
532
+ await user.save(update_fields=("hash_password",))
533
+
534
+ async def orm_save_upload_field(self, obj: tp.Any, field: str, base64: str) -> None:
535
+ # convert base64 to bytes, upload to s3/filestorage, get url and save or save base64 as is to db (don't recomment it)
536
+ setattr(obj, field, base64)
537
+ await obj.save(update_fields=(field,))
538
+
513
539
  ```
514
540
 
515
541
 
@@ -583,8 +609,11 @@ class UserAdmin(DjangoModelAdmin):
583
609
 
584
610
 
585
611
  ```python
612
+ import typing as tp
613
+ import uuid
614
+
586
615
  import bcrypt
587
- from sqlalchemy import Boolean, Integer, String, select
616
+ from sqlalchemy import Boolean, Integer, String, Text, select, update
588
617
  from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
589
618
  from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
590
619
 
@@ -609,6 +638,7 @@ class User(Base):
609
638
  hash_password: Mapped[str] = mapped_column(String(length=255), nullable=False)
610
639
  is_superuser: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
611
640
  is_active: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
641
+ avatar_url: Mapped[str | None] = mapped_column(Text, nullable=True)
612
642
 
613
643
  def __str__(self):
614
644
  return self.username
@@ -622,17 +652,33 @@ class UserAdmin(SqlAlchemyModelAdmin):
622
652
  list_filter = ("id", "username", "is_superuser", "is_active")
623
653
  search_fields = ("username",)
624
654
 
625
- async def authenticate(self, username, password):
655
+ async def authenticate(self, username: str, password: str) -> uuid.UUID | int | None:
626
656
  sessionmaker = self.get_sessionmaker()
627
657
  async with sessionmaker() as session:
628
- query = select(User).filter_by(username=username, password=password, is_superuser=True)
658
+ query = select(self.model_cls).filter_by(username=username, password=password, is_superuser=True)
629
659
  result = await session.scalars(query)
630
- user = result.first()
631
- if not user:
660
+ obj = result.first()
661
+ if not obj:
632
662
  return None
633
- if not bcrypt.checkpw(password.encode(), user.hash_password.encode()):
663
+ if not bcrypt.checkpw(password.encode(), obj.hash_password.encode()):
634
664
  return None
635
- return user.id
665
+ return obj.id
666
+
667
+ async def change_password(self, id: uuid.UUID | int, password: str) -> None:
668
+ sessionmaker = self.get_sessionmaker()
669
+ async with sessionmaker() as session:
670
+ hash_password = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
671
+ query = update(self.model_cls).where(User.id.in_([id])).values(hash_password=hash_password)
672
+ await session.execute(query)
673
+ await session.commit()
674
+
675
+ async def orm_save_upload_field(self, obj: tp.Any, field: str, base64: str) -> None:
676
+ sessionmaker = self.get_sessionmaker()
677
+ async with sessionmaker() as session:
678
+ # convert base64 to bytes, upload to s3/filestorage, get url and save or save base64 as is to db (don't recomment it)
679
+ query = update(self.model_cls).where(User.id.in_([obj.id])).values(**{field: base64})
680
+ await session.execute(query)
681
+ await session.commit()
636
682
 
637
683
  ```
638
684
 
@@ -655,8 +701,11 @@ class UserAdmin(SqlAlchemyModelAdmin):
655
701
 
656
702
 
657
703
  ```python
704
+ import typing as tp
705
+ import uuid
706
+
658
707
  import bcrypt
659
- from pony.orm import Database, PrimaryKey, Required, db_session
708
+ from pony.orm import Database, LongStr, Optional, PrimaryKey, Required, commit, db_session
660
709
 
661
710
  from fastadmin import PonyORMModelAdmin, register
662
711
 
@@ -671,6 +720,7 @@ class User(db.Entity): # type: ignore [name-defined]
671
720
  hash_password = Required(str)
672
721
  is_superuser = Required(bool, default=False)
673
722
  is_active = Required(bool, default=False)
723
+ avatar_url = Optional(LongStr, nullable=True)
674
724
 
675
725
  def __str__(self):
676
726
  return self.username
@@ -685,13 +735,31 @@ class UserAdmin(PonyORMModelAdmin):
685
735
  search_fields = ("username",)
686
736
 
687
737
  @db_session
688
- def authenticate(self, username, password):
689
- user = next((f for f in self.model_cls.select(username=username, password=password, is_superuser=True)), None)
690
- if not user:
738
+ def authenticate(self, username: str, password: str) -> uuid.UUID | int | None:
739
+ obj = next((f for f in User.select(username=username, password=password, is_superuser=True)), None) # fmt: skip
740
+ if not obj:
691
741
  return None
692
- if not bcrypt.checkpw(password.encode(), user.hash_password.encode()):
742
+ if not bcrypt.checkpw(password.encode(), obj.hash_password.encode()):
693
743
  return None
694
- return user.id
744
+ return obj.id
745
+
746
+ @db_session
747
+ def change_password(self, id: uuid.UUID | int, password: str) -> None:
748
+ obj = next((f for f in self.model_cls.select(id=id)), None)
749
+ if not obj:
750
+ return
751
+ hash_password = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
752
+ obj.hash_password = hash_password
753
+ commit()
754
+
755
+ @db_session
756
+ def orm_save_upload_field(self, obj: tp.Any, field: str, base64: str) -> None:
757
+ obj = next((f for f in self.model_cls.select(id=obj.id)), None)
758
+ if not obj:
759
+ return
760
+ # convert base64 to bytes, upload to s3/filestorage, get url and save or save base64 as is to db (don't recomment it)
761
+ setattr(obj, field, base64)
762
+ commit()
695
763
 
696
764
  ```
697
765
 
@@ -434,13 +434,14 @@ Register ORM models
434
434
 
435
435
 
436
436
  ```python
437
+ import typing as tp
437
438
  from uuid import UUID
438
439
 
439
440
  import bcrypt
440
441
  from tortoise import fields
441
442
  from tortoise.models import Model
442
443
 
443
- from fastadmin import TortoiseModelAdmin, register
444
+ from fastadmin import TortoiseModelAdmin, WidgetType, register
444
445
 
445
446
 
446
447
  class User(Model):
@@ -448,6 +449,7 @@ class User(Model):
448
449
  hash_password = fields.CharField(max_length=255)
449
450
  is_superuser = fields.BooleanField(default=False)
450
451
  is_active = fields.BooleanField(default=False)
452
+ avatar_url = fields.TextField(null=True)
451
453
 
452
454
  def __str__(self):
453
455
  return self.username
@@ -460,15 +462,39 @@ class UserAdmin(TortoiseModelAdmin):
460
462
  list_display_links = ("id", "username")
461
463
  list_filter = ("id", "username", "is_superuser", "is_active")
462
464
  search_fields = ("username",)
463
-
464
- async def authenticate(self, username: str, password: str) -> UUID | int | None:
465
- user = await User.filter(username=username, is_superuser=True).first()
465
+ formfield_overrides = { # noqa: RUF012
466
+ "username": (WidgetType.SlugInput, {"required": True}),
467
+ "password": (WidgetType.PasswordInput, {"passwordModalForm": True}),
468
+ "avatar_url": (
469
+ WidgetType.Upload,
470
+ {
471
+ "required": False,
472
+ # Disable crop image for upload field
473
+ # "disableCropImage": True,
474
+ },
475
+ ),
476
+ }
477
+
478
+ async def authenticate(self, username: str, password: str) -> int | None:
479
+ user = await self.model_cls.filter(phone=username, is_superuser=True).first()
466
480
  if not user:
467
481
  return None
468
482
  if not bcrypt.checkpw(password.encode(), user.hash_password.encode()):
469
483
  return None
470
484
  return user.id
471
485
 
486
+ async def change_password(self, id: UUID | int, password: str) -> None:
487
+ user = await self.model_cls.filter(id=id).first()
488
+ if not user:
489
+ return
490
+ user.hash_password = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
491
+ await user.save(update_fields=("hash_password",))
492
+
493
+ async def orm_save_upload_field(self, obj: tp.Any, field: str, base64: str) -> None:
494
+ # convert base64 to bytes, upload to s3/filestorage, get url and save or save base64 as is to db (don't recomment it)
495
+ setattr(obj, field, base64)
496
+ await obj.save(update_fields=(field,))
497
+
472
498
  ```
473
499
 
474
500
 
@@ -542,8 +568,11 @@ class UserAdmin(DjangoModelAdmin):
542
568
 
543
569
 
544
570
  ```python
571
+ import typing as tp
572
+ import uuid
573
+
545
574
  import bcrypt
546
- from sqlalchemy import Boolean, Integer, String, select
575
+ from sqlalchemy import Boolean, Integer, String, Text, select, update
547
576
  from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
548
577
  from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
549
578
 
@@ -568,6 +597,7 @@ class User(Base):
568
597
  hash_password: Mapped[str] = mapped_column(String(length=255), nullable=False)
569
598
  is_superuser: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
570
599
  is_active: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
600
+ avatar_url: Mapped[str | None] = mapped_column(Text, nullable=True)
571
601
 
572
602
  def __str__(self):
573
603
  return self.username
@@ -581,17 +611,33 @@ class UserAdmin(SqlAlchemyModelAdmin):
581
611
  list_filter = ("id", "username", "is_superuser", "is_active")
582
612
  search_fields = ("username",)
583
613
 
584
- async def authenticate(self, username, password):
614
+ async def authenticate(self, username: str, password: str) -> uuid.UUID | int | None:
585
615
  sessionmaker = self.get_sessionmaker()
586
616
  async with sessionmaker() as session:
587
- query = select(User).filter_by(username=username, password=password, is_superuser=True)
617
+ query = select(self.model_cls).filter_by(username=username, password=password, is_superuser=True)
588
618
  result = await session.scalars(query)
589
- user = result.first()
590
- if not user:
619
+ obj = result.first()
620
+ if not obj:
591
621
  return None
592
- if not bcrypt.checkpw(password.encode(), user.hash_password.encode()):
622
+ if not bcrypt.checkpw(password.encode(), obj.hash_password.encode()):
593
623
  return None
594
- return user.id
624
+ return obj.id
625
+
626
+ async def change_password(self, id: uuid.UUID | int, password: str) -> None:
627
+ sessionmaker = self.get_sessionmaker()
628
+ async with sessionmaker() as session:
629
+ hash_password = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
630
+ query = update(self.model_cls).where(User.id.in_([id])).values(hash_password=hash_password)
631
+ await session.execute(query)
632
+ await session.commit()
633
+
634
+ async def orm_save_upload_field(self, obj: tp.Any, field: str, base64: str) -> None:
635
+ sessionmaker = self.get_sessionmaker()
636
+ async with sessionmaker() as session:
637
+ # convert base64 to bytes, upload to s3/filestorage, get url and save or save base64 as is to db (don't recomment it)
638
+ query = update(self.model_cls).where(User.id.in_([obj.id])).values(**{field: base64})
639
+ await session.execute(query)
640
+ await session.commit()
595
641
 
596
642
  ```
597
643
 
@@ -614,8 +660,11 @@ class UserAdmin(SqlAlchemyModelAdmin):
614
660
 
615
661
 
616
662
  ```python
663
+ import typing as tp
664
+ import uuid
665
+
617
666
  import bcrypt
618
- from pony.orm import Database, PrimaryKey, Required, db_session
667
+ from pony.orm import Database, LongStr, Optional, PrimaryKey, Required, commit, db_session
619
668
 
620
669
  from fastadmin import PonyORMModelAdmin, register
621
670
 
@@ -630,6 +679,7 @@ class User(db.Entity): # type: ignore [name-defined]
630
679
  hash_password = Required(str)
631
680
  is_superuser = Required(bool, default=False)
632
681
  is_active = Required(bool, default=False)
682
+ avatar_url = Optional(LongStr, nullable=True)
633
683
 
634
684
  def __str__(self):
635
685
  return self.username
@@ -644,13 +694,31 @@ class UserAdmin(PonyORMModelAdmin):
644
694
  search_fields = ("username",)
645
695
 
646
696
  @db_session
647
- def authenticate(self, username, password):
648
- user = next((f for f in self.model_cls.select(username=username, password=password, is_superuser=True)), None)
649
- if not user:
697
+ def authenticate(self, username: str, password: str) -> uuid.UUID | int | None:
698
+ obj = next((f for f in User.select(username=username, password=password, is_superuser=True)), None) # fmt: skip
699
+ if not obj:
650
700
  return None
651
- if not bcrypt.checkpw(password.encode(), user.hash_password.encode()):
701
+ if not bcrypt.checkpw(password.encode(), obj.hash_password.encode()):
652
702
  return None
653
- return user.id
703
+ return obj.id
704
+
705
+ @db_session
706
+ def change_password(self, id: uuid.UUID | int, password: str) -> None:
707
+ obj = next((f for f in self.model_cls.select(id=id)), None)
708
+ if not obj:
709
+ return
710
+ hash_password = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
711
+ obj.hash_password = hash_password
712
+ commit()
713
+
714
+ @db_session
715
+ def orm_save_upload_field(self, obj: tp.Any, field: str, base64: str) -> None:
716
+ obj = next((f for f in self.model_cls.select(id=obj.id)), None)
717
+ if not obj:
718
+ return
719
+ # convert base64 to bytes, upload to s3/filestorage, get url and save or save base64 as is to db (don't recomment it)
720
+ setattr(obj, field, base64)
721
+ commit()
654
722
 
655
723
  ```
656
724
 
@@ -9,7 +9,6 @@ from uuid import UUID
9
9
 
10
10
  from asgiref.sync import sync_to_async
11
11
 
12
- from fastadmin.api.helpers import is_valid_base64
13
12
  from fastadmin.api.schemas import ExportFormat
14
13
  from fastadmin.models.schemas import DashboardWidgetType, ModelFieldWidgetSchema, WidgetType
15
14
 
@@ -456,8 +455,12 @@ class BaseModelAdmin:
456
455
  return None
457
456
 
458
457
  for upload_field in upload_fields:
459
- if upload_field.name in payload and is_valid_base64(payload[upload_field.name]):
460
- await self.orm_save_upload_field(obj, upload_field.column_name, payload[upload_field.name])
458
+ if upload_field.name in payload:
459
+ if inspect.iscoroutinefunction(self.orm_save_upload_field):
460
+ orm_save_upload_field_fn = self.orm_save_upload_field
461
+ else:
462
+ orm_save_upload_field_fn = sync_to_async(self.orm_save_upload_field) # type: ignore [arg-type]
463
+ await orm_save_upload_field_fn(obj, upload_field.column_name, payload[upload_field.name])
461
464
 
462
465
  for m2m_field in m2m_fields:
463
466
  if m2m_field.name in payload: