|
40 | 40 | from divbase_api.models.announcements import AnnouncementDB, AnnouncementLevel, AnnouncementTarget |
41 | 41 | from divbase_api.models.project_versions import ProjectVersionDB |
42 | 42 | from divbase_api.models.projects import ProjectDB, ProjectMembershipDB, ProjectRoles |
| 43 | +from divbase_api.models.queue_status import QueueStatus |
43 | 44 | from divbase_api.models.revoked_tokens import RevokedTokenDB, TokenRevokeReason |
44 | 45 | from divbase_api.models.task_history import CeleryTaskMeta, TaskHistoryDB, TaskStartedAtDB |
45 | 46 | from divbase_api.models.users import UserDB |
@@ -628,6 +629,101 @@ class AnnouncementView(ModelView): |
628 | 629 | exclude_fields_from_detail = [] |
629 | 630 |
|
630 | 631 |
|
| 632 | +class QueueStatusView(ModelView): |
| 633 | + """ |
| 634 | + Custom admin panel View for the QueueStatus model. |
| 635 | +
|
| 636 | + This is a singleton table (only 1 row allowed). |
| 637 | + Deletion and creation are disabled - only editing the existing row is allowed. |
| 638 | + """ |
| 639 | + |
| 640 | + page_size_options = PAGINATION_DEFAULTS |
| 641 | + fields = [ |
| 642 | + IntegerField("id", disabled=True), |
| 643 | + BooleanField( |
| 644 | + "is_closed", |
| 645 | + help_text=( |
| 646 | + "Is the queue closed for new tasks? " |
| 647 | + "If the scheduled start time is in the past or not set, the queue is closed. " |
| 648 | + "If scheduled start time is in the future, queue will be closed at that time." |
| 649 | + ), |
| 650 | + ), |
| 651 | + DateTimeField( |
| 652 | + "scheduled_start", |
| 653 | + label="Scheduled closure start time (UTC time)", |
| 654 | + help_text=( |
| 655 | + "(THIS IS UTC TIME) Optional: When should the queue closure set above take effect? " |
| 656 | + "Leave empty for queue to be closed immediatialy " |
| 657 | + "If the queue is open and you set this field, it will do nothing." |
| 658 | + ), |
| 659 | + ), |
| 660 | + TextAreaField( |
| 661 | + "reason_for_users", |
| 662 | + help_text=( |
| 663 | + "Message shown to users when the queue is closed (max 500 characters). " |
| 664 | + "You could write: " |
| 665 | + "The queuing system is currently closed for new tasks due to planned upcoming maintenance. Please try again later." |
| 666 | + ), |
| 667 | + ), |
| 668 | + DateTimeField( |
| 669 | + "created_at", |
| 670 | + label="Created At (UTC time)", |
| 671 | + help_text="Timestamp when the entry was created. Value determined by system.", |
| 672 | + disabled=True, |
| 673 | + ), |
| 674 | + DateTimeField( |
| 675 | + "updated_at", |
| 676 | + label="Updated At (UTC time)", |
| 677 | + help_text="Timestamp when the entry was last updated. Value determined by system.", |
| 678 | + disabled=True, |
| 679 | + ), |
| 680 | + ] |
| 681 | + |
| 682 | + exclude_fields_from_list = [] |
| 683 | + exclude_fields_from_edit = ["id", "created_at", "updated_at"] |
| 684 | + exclude_fields_from_detail = [] |
| 685 | + |
| 686 | + def can_delete(self, request: Request) -> bool: |
| 687 | + """Disable deletion - this is a singleton table.""" |
| 688 | + return False |
| 689 | + |
| 690 | + def can_create(self, request: Request) -> bool: |
| 691 | + """Disable creation - this is a singleton table with only 1 row.""" |
| 692 | + # 1st row created by the alembic migration |
| 693 | + return False |
| 694 | + |
| 695 | + async def edit(self, request: Request, pk: Any, data: dict) -> Any: |
| 696 | + """ |
| 697 | + Override the default edit method to ensure that the `scheduled_start` field is set to None |
| 698 | + if `is_closed` is set to False. |
| 699 | + """ |
| 700 | + if not data.get("is_closed"): |
| 701 | + data["scheduled_start"] = None |
| 702 | + |
| 703 | + return await super().edit(request=request, pk=pk, data=data) |
| 704 | + |
| 705 | + async def validate(self, request: Request, data: dict[str, Any]) -> None: |
| 706 | + errors: dict[str, str] = {} |
| 707 | + |
| 708 | + if data.get("scheduled_start") and not data.get("is_closed"): |
| 709 | + errors["scheduled_start"] = "Cannot have a scheduled start time without the queue being closed." |
| 710 | + |
| 711 | + reason = data.get("reason_for_users") or "" |
| 712 | + if data.get("is_closed") and not reason.strip(): |
| 713 | + errors["reason_for_users"] = "Reason for users is required when the queue is closed." |
| 714 | + |
| 715 | + if len(reason) > 500: |
| 716 | + errors["reason_for_users"] = "Message must be 500 characters or less." |
| 717 | + |
| 718 | + if errors: |
| 719 | + raise FormValidationError(errors=errors) |
| 720 | + |
| 721 | + return await super().validate(request=request, data=data) |
| 722 | + |
| 723 | + # NOTE, no serialize_field_value/_format_cet_datetime override on purpose here |
| 724 | + # as does not work well when modifying the timestamp in edit view, easier to just display as UTC |
| 725 | + |
| 726 | + |
631 | 727 | class DivBaseAuthProvider(AuthProvider): |
632 | 728 | """ |
633 | 729 | This class enables starlette-admin to make use of DivBase's pre-existing auth system. |
@@ -711,4 +807,5 @@ def register_admin_panel(app: FastAPI, engine: AsyncEngine) -> None: |
711 | 807 | admin.add_view( |
712 | 808 | AnnouncementView(AnnouncementDB, icon="fas fa-bullhorn", label="Announcements", identity="announcement") |
713 | 809 | ) |
| 810 | + admin.add_view(QueueStatusView(QueueStatus, icon="fas fa-power-off", label="Queue Status", identity="queue-status")) |
714 | 811 | admin.mount_to(app) |
0 commit comments