polyadmin 0.1.0b1__tar.gz → 0.1.0b2__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.
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/PKG-INFO +8 -8
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/README.md +7 -7
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/docs/architecture.md +2 -2
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/docs/python/index.md +3 -2
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/examples/fastapi/models.py +6 -6
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/examples/fastapi/user_admin.py +3 -5
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/examples/fastapi/uv.lock +1 -1
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/core/field.py +0 -5
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/core/inline.py +2 -7
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/core/model_admin.py +14 -18
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/core/query.py +2 -2
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/fastapi/csrf.py +0 -2
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templating.py +3 -3
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/ui.py +43 -37
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/pyproject.toml +1 -1
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/test_action_decorator.py +2 -1
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/test_delete_selected_i18n.py +3 -1
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/test_parity.py +0 -15
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_async_model_admin.py +13 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_async_relations.py +0 -9
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_audit.py +0 -1
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_code_strings.py +1 -1
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_components.py +10 -10
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_errors.py +0 -2
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_filters.py +1 -2
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_parity_list.py +0 -11
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_relations.py +4 -3
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_router.py +1 -1
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_theme.py +2 -1
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/test_layout.py +2 -2
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/test_templating.py +0 -1
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/test_ui.py +11 -14
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/uv.lock +1 -1
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/.github/workflows/notify-docs.yml +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/.github/workflows/publish.yml +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/.gitignore +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/LICENSE +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/babel.cfg +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/browsertests/README.md +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/browsertests/conftest.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/browsertests/test_deletes.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/browsertests/test_flows.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/browsertests/test_i18n.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/browsertests/test_parity.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/docs/audit.md +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/docs/authentication.md +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/docs/components.md +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/docs/concepts/architecture.md +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/docs/concepts/model-admin.md +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/docs/dashboard.md +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/docs/deletes.md +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/docs/exports.md +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/docs/i18n.md +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/docs/inlines.md +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/docs/lists.md +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/docs/model-admin.md +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/docs/permissions.md +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/docs/routing.md +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/docs/templates.md +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/examples/README.md +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/examples/fastapi/README.md +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/examples/fastapi/main.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/examples/fastapi/organization_admin.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/examples/fastapi/pages.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/examples/fastapi/pyproject.toml +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/examples/fastapi/role_admin.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/examples/fastapi/session.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/examples/fastapi/templates/pages/broadcast.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/examples/fastapi/tests/test_app.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/mkdocs.yml +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/__init__.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/core/__init__.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/core/_async.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/core/action.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/core/admin.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/core/audit.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/core/auth.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/core/authorization.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/core/csrf.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/core/dashboard.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/core/delete.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/core/exporter.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/core/filter.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/core/login.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/core/page.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/core/pagination.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/core/relation.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/core/slug.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/core/template_context.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/core/widget.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/fastapi/__init__.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/fastapi/audit.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/fastapi/auth.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/fastapi/deletes.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/fastapi/errors.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/fastapi/handlers.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/fastapi/inlines.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/fastapi/locale.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/fastapi/login.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/fastapi/pages.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/fastapi/relations.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/fastapi/responses.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/fastapi/router.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/fastapi/static.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/i18n/__init__.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/i18n/context.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/i18n/negotiation.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/i18n/setup.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/i18n/translator.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/locale/fr/LC_MESSAGES/polyadmin.mo +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/locale/fr/LC_MESSAGES/polyadmin.po +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/locale/polyadmin.pot +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/locale/ru/LC_MESSAGES/polyadmin.mo +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/locale/ru/LC_MESSAGES/polyadmin.po +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/base.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/action_confirm_modal.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/csrf-field.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/error_fragment.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/field.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/form_wrapper.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/icons.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/inline.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/inline_fragment.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/list_content.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/lookup_results.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/search.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/toasts.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/ui/breadcrumb.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/ui/bulk-actions.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/ui/calendar.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/ui/combobox.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/ui/delete-preview.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/ui/dropdown-menu.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/ui/field.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/ui/filter-panel.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/ui/locale-switcher.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/ui/multi-select.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/ui/pagination.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/ui/radio-group.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/ui/select.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/ui/sidebar.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/ui/slider.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/ui/switch.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/ui/table.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/ui/theme-toggle.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/dashboard.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/error.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/login.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/resource/delete.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/resource/delete_selected.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/resource/detail.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/resource/form.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/resource/list.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/theme.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/widgets/activity.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/widgets/chart.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/widgets/donut.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/widgets/metric.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/widgets/progress.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/widgets/stat.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/widgets/table.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/widgets/tabs.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/widgets/timeline.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/__init__.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/conftest.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/__init__.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/mo.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/test_action_placement.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/test_admin.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/test_async.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/test_auth.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/test_authorization.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/test_csrf.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/test_dashboard.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/test_delete.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/test_exporter.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/test_field.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/test_filter.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/test_i18n.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/test_inline.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/test_login.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/test_model_admin.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/test_pagination.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/test_query.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/test_relation.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/test_template_context.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/test_widget.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/__init__.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_action_placement.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_actions.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_auth.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_csrf.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_dashboard.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_delete_preview.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_delete_selected.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_delete_selected_preview.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_export.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_htmx.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_inlines.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_list_querier.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_listopts.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_locale.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_login.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_pages.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_pseudo_locale.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_scroll_area.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_switcher.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/test_catalogs.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/test_format.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: polyadmin
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.0b2
|
|
4
4
|
Summary: PolyAdmin: cross-language server-rendered admin framework (Python core)
|
|
5
5
|
Project-URL: Documentation, https://magicrodri.github.io/polyadmin-docs/
|
|
6
6
|
Project-URL: Source, https://github.com/MagicRodri/polyadmin
|
|
@@ -35,9 +35,9 @@ Description-Content-Type: text/markdown
|
|
|
35
35
|
[](LICENSE)
|
|
36
36
|
[](https://github.com/MagicRodri/polyadmin)
|
|
37
37
|
|
|
38
|
-
A
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
A server-rendered operations workspace for Python applications, with a
|
|
39
|
+
FastAPI adapter. See [`docs/`](docs/) for reference documentation, or visit
|
|
40
|
+
the [PolyAdmin documentation site](https://magicrodri.github.io/polyadmin-docs/)
|
|
41
41
|
for the shared Python and Go guides.
|
|
42
42
|
|
|
43
43
|
## What you get from declaring a `ModelAdmin`
|
|
@@ -61,7 +61,7 @@ permissions) and you get:
|
|
|
61
61
|
(never dumps the target model's full queryset into the page)
|
|
62
62
|
- Inline related records (`StackedInline`/`TabularInline`) — a
|
|
63
63
|
parent's create/detail/edit pages can show and manage a child's
|
|
64
|
-
records that point back at it,
|
|
64
|
+
records that point back at it, directly within the parent workflow
|
|
65
65
|
- Record and bulk Actions, with a shadcn/ui Dialog confirmation step for
|
|
66
66
|
destructive ones
|
|
67
67
|
- Delete previews: the confirmation page says what else a delete takes
|
|
@@ -82,8 +82,8 @@ Alpine.js + Tailwind — its CSS-variable token system and component
|
|
|
82
82
|
markup, without React or Radix. That gives the admin **dark mode and
|
|
83
83
|
themability**: every color resolves through a CSS variable, so restyling
|
|
84
84
|
the whole thing is a change to one template. The layout is mobile-first
|
|
85
|
-
with a collapsible sidebar (a Sheet below `md`), a
|
|
86
|
-
|
|
85
|
+
with a collapsible sidebar (a Sheet below `md`), a right-hand filter panel
|
|
86
|
+
for focused workflows on wider screens, and breadcrumbs as the page
|
|
87
87
|
title. Tailwind/Alpine/HTMX are all CDN-loaded — no frontend build step.
|
|
88
88
|
See [`docs/components.md`](docs/components.md).
|
|
89
89
|
|
|
@@ -92,7 +92,7 @@ See [`docs/components.md`](docs/components.md).
|
|
|
92
92
|
The first beta release is available on PyPI:
|
|
93
93
|
|
|
94
94
|
```bash
|
|
95
|
-
pip install "polyadmin[fastapi]==0.1.
|
|
95
|
+
pip install "polyadmin[fastapi]==0.1.0b2"
|
|
96
96
|
```
|
|
97
97
|
|
|
98
98
|
For the latest unreleased code, install from Git:
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
[](https://github.com/MagicRodri/polyadmin)
|
|
7
7
|
|
|
8
|
-
A
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
A server-rendered operations workspace for Python applications, with a
|
|
9
|
+
FastAPI adapter. See [`docs/`](docs/) for reference documentation, or visit
|
|
10
|
+
the [PolyAdmin documentation site](https://magicrodri.github.io/polyadmin-docs/)
|
|
11
11
|
for the shared Python and Go guides.
|
|
12
12
|
|
|
13
13
|
## What you get from declaring a `ModelAdmin`
|
|
@@ -31,7 +31,7 @@ permissions) and you get:
|
|
|
31
31
|
(never dumps the target model's full queryset into the page)
|
|
32
32
|
- Inline related records (`StackedInline`/`TabularInline`) — a
|
|
33
33
|
parent's create/detail/edit pages can show and manage a child's
|
|
34
|
-
records that point back at it,
|
|
34
|
+
records that point back at it, directly within the parent workflow
|
|
35
35
|
- Record and bulk Actions, with a shadcn/ui Dialog confirmation step for
|
|
36
36
|
destructive ones
|
|
37
37
|
- Delete previews: the confirmation page says what else a delete takes
|
|
@@ -52,8 +52,8 @@ Alpine.js + Tailwind — its CSS-variable token system and component
|
|
|
52
52
|
markup, without React or Radix. That gives the admin **dark mode and
|
|
53
53
|
themability**: every color resolves through a CSS variable, so restyling
|
|
54
54
|
the whole thing is a change to one template. The layout is mobile-first
|
|
55
|
-
with a collapsible sidebar (a Sheet below `md`), a
|
|
56
|
-
|
|
55
|
+
with a collapsible sidebar (a Sheet below `md`), a right-hand filter panel
|
|
56
|
+
for focused workflows on wider screens, and breadcrumbs as the page
|
|
57
57
|
title. Tailwind/Alpine/HTMX are all CDN-loaded — no frontend build step.
|
|
58
58
|
See [`docs/components.md`](docs/components.md).
|
|
59
59
|
|
|
@@ -62,7 +62,7 @@ See [`docs/components.md`](docs/components.md).
|
|
|
62
62
|
The first beta release is available on PyPI:
|
|
63
63
|
|
|
64
64
|
```bash
|
|
65
|
-
pip install "polyadmin[fastapi]==0.1.
|
|
65
|
+
pip install "polyadmin[fastapi]==0.1.0b2"
|
|
66
66
|
```
|
|
67
67
|
|
|
68
68
|
For the latest unreleased code, install from Git:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Architecture
|
|
2
2
|
|
|
3
|
-
PolyAdmin is a
|
|
4
|
-
[FastAPI](https://fastapi.tiangolo.com)
|
|
3
|
+
PolyAdmin is a server-rendered operations framework for Python applications.
|
|
4
|
+
The current [FastAPI](https://fastapi.tiangolo.com) adapter owns presentation —
|
|
5
5
|
routes, forms, tables, permissions checks, HTML — and owns no storage:
|
|
6
6
|
your `ModelAdmin` implements the lifecycle hooks against whatever
|
|
7
7
|
database or service you already have. This document explains how the
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
# Python
|
|
1
|
+
# Python
|
|
2
2
|
|
|
3
|
-
The Python implementation
|
|
3
|
+
The Python implementation provides the core admin experience for Python
|
|
4
|
+
applications. The current web adapter mounts PolyAdmin into FastAPI.
|
|
4
5
|
|
|
5
6
|
## Install from Git
|
|
6
7
|
|
|
@@ -55,9 +55,8 @@ class OrganizationRepository:
|
|
|
55
55
|
class Role:
|
|
56
56
|
"""The many-to-many target: a user holds any number of these.
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
bearable once the list is long.
|
|
58
|
+
A permissions-style list, long enough that the searchable multi-select
|
|
59
|
+
on the user form is there to make it bearable.
|
|
61
60
|
"""
|
|
62
61
|
|
|
63
62
|
id: int
|
|
@@ -90,9 +89,10 @@ class User:
|
|
|
90
89
|
email: str
|
|
91
90
|
is_active: bool = True
|
|
92
91
|
# A plain choice field, so the reference app exercises ui/select
|
|
93
|
-
# (the
|
|
94
|
-
# a relation, which renders one of the
|
|
95
|
-
# instead -- without this, ui/select appeared
|
|
92
|
+
# (the reference design system's Select port). Every other
|
|
93
|
+
# choice-shaped field here is a relation, which renders one of the
|
|
94
|
+
# two combobox widgets instead -- without this, ui/select appeared
|
|
95
|
+
# nowhere in the app.
|
|
96
96
|
plan: str = "Free"
|
|
97
97
|
organization: Organization | None = None
|
|
98
98
|
roles: list[Role] = field(default_factory=list)
|
|
@@ -32,9 +32,8 @@ ROLES_RELATION = Relation(
|
|
|
32
32
|
class PlanFilter(Filter):
|
|
33
33
|
"""A filter this application wrote itself: it subclasses the published
|
|
34
34
|
base and implements the two methods, borrowing nothing from the
|
|
35
|
-
framework's own filter types. This
|
|
36
|
-
|
|
37
|
-
constraint.
|
|
35
|
+
framework's own filter types. This hook lets the admin supply both
|
|
36
|
+
the options and the constraint itself.
|
|
38
37
|
|
|
39
38
|
It exists in the example so the extension point is exercised by the
|
|
40
39
|
browser suite rather than only described in docs/lists.md.
|
|
@@ -62,8 +61,7 @@ class UserAdmin(ModelAdmin):
|
|
|
62
61
|
list_display = ["id", "email", "is_active", "plan", "organization"]
|
|
63
62
|
detail_fields = ["id", "email", "is_active", "plan", "organization", "roles"]
|
|
64
63
|
# roles is on the form but not in list_display: a many-to-many
|
|
65
|
-
# column costs a lookup per row and reads as noise in a table
|
|
66
|
-
# is why Django keeps it off list_display too.
|
|
64
|
+
# column costs a lookup per row and reads as noise in a table.
|
|
67
65
|
# Grouped rather than flat, to exercise fieldsets -- the other
|
|
68
66
|
# admins in this app stay flat, so both paths have example coverage.
|
|
69
67
|
# Declaring these replaces form_fields: the groups are the form's
|
|
@@ -20,8 +20,6 @@ _UNSET = object()
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
class Field:
|
|
23
|
-
"""Base class for all admin field types."""
|
|
24
|
-
|
|
25
23
|
field_type: str = "field"
|
|
26
24
|
|
|
27
25
|
def __init__(
|
|
@@ -52,7 +50,6 @@ class Field:
|
|
|
52
50
|
return self.default is not _UNSET
|
|
53
51
|
|
|
54
52
|
def get_value(self, obj: Any) -> Any:
|
|
55
|
-
"""Read this field's value off a model instance."""
|
|
56
53
|
value = getattr(obj, self.name, _UNSET)
|
|
57
54
|
if value is _UNSET:
|
|
58
55
|
return self.default if self.has_default else None
|
|
@@ -188,8 +185,6 @@ class OneToOneField(ForeignKeyField):
|
|
|
188
185
|
|
|
189
186
|
|
|
190
187
|
class ManyToManyField(Field):
|
|
191
|
-
"""A collection of related objects."""
|
|
192
|
-
|
|
193
188
|
field_type = "manytomany"
|
|
194
189
|
|
|
195
190
|
def __init__(self, name: str, *, relation: Relation, **kwargs: Any) -> None:
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
"""Inline: a reverse-relation admin declaration -- lets a parent
|
|
2
2
|
ModelAdmin manage/display a child ModelAdmin's records that point back
|
|
3
|
-
at it via one FK/OneToOne field
|
|
4
|
-
style. See docs/inlines.md.
|
|
3
|
+
at it via one FK/OneToOne field. See docs/inlines.md.
|
|
5
4
|
|
|
6
5
|
`layout` is presentation-only, not behavioral, so there is one Inline
|
|
7
6
|
type with a layout discriminator, not two structurally different
|
|
8
7
|
classes -- StackedInline/TabularInline are just layout-preset
|
|
9
|
-
subclasses
|
|
8
|
+
subclasses.
|
|
10
9
|
"""
|
|
11
10
|
|
|
12
11
|
from __future__ import annotations
|
|
@@ -17,10 +16,6 @@ from polyadmin.core._async import maybe_await
|
|
|
17
16
|
from polyadmin.core.query import ListRequest, alist_objects
|
|
18
17
|
|
|
19
18
|
if TYPE_CHECKING:
|
|
20
|
-
# Only needed for type hints -- inline.py must not import
|
|
21
|
-
# model_admin.py at runtime, since model_admin.py imports Inline
|
|
22
|
-
# (a real ClassVar default, not just a hint) from here. Guarding
|
|
23
|
-
# only this side is enough to avoid a circular import.
|
|
24
19
|
from polyadmin.core.model_admin import ModelAdmin
|
|
25
20
|
|
|
26
21
|
STACKED = "stacked"
|
|
@@ -11,6 +11,7 @@ from collections.abc import Sequence
|
|
|
11
11
|
from dataclasses import dataclass
|
|
12
12
|
from typing import Any, ClassVar
|
|
13
13
|
|
|
14
|
+
from polyadmin.core._async import maybe_await
|
|
14
15
|
from polyadmin.core.action import DELETE_SELECTED_NAME, Action, action, collect_actions
|
|
15
16
|
from polyadmin.core.auth import Principal
|
|
16
17
|
from polyadmin.core.field import Field
|
|
@@ -21,10 +22,10 @@ from polyadmin.i18n import N_, gettext, ngettext
|
|
|
21
22
|
|
|
22
23
|
@dataclass
|
|
23
24
|
class Fieldset:
|
|
24
|
-
"""One titled group of form fields
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
"""One titled group of form fields. A title of None renders the group
|
|
26
|
+
with no header, which is how the undeclared default renders as a plain
|
|
27
|
+
flat form. `collapsed` only seeds the initial state; the group can
|
|
28
|
+
always be opened.
|
|
28
29
|
"""
|
|
29
30
|
|
|
30
31
|
fields: Sequence[str] = ()
|
|
@@ -65,7 +66,6 @@ class ModelAdmin:
|
|
|
65
66
|
# stable between requests.
|
|
66
67
|
ordering: ClassVar[str | None] = None
|
|
67
68
|
# How many rows a list page holds; None means DEFAULT_PAGE_SIZE.
|
|
68
|
-
# Django's list_per_page.
|
|
69
69
|
list_per_page: ClassVar[int | None] = None
|
|
70
70
|
# What a read-only view shows for a None or blank value; None means
|
|
71
71
|
# DEFAULT_EMPTY_VALUE.
|
|
@@ -78,8 +78,7 @@ class ModelAdmin:
|
|
|
78
78
|
# action whose `where` includes the detail page; a list overrides `where`
|
|
79
79
|
# (so a "list" action can be named here), and [] offers none.
|
|
80
80
|
detail_actions: ClassVar[Sequence[str] | None] = None
|
|
81
|
-
# Removes the built-in bulk delete. Opt-out, so the default keeps it
|
|
82
|
-
# as Django does.
|
|
81
|
+
# Removes the built-in bulk delete. Opt-out, so the default keeps it.
|
|
83
82
|
disable_delete_selected: ClassVar[bool] = False
|
|
84
83
|
# Child ModelAdmins whose records point back at this one, managed
|
|
85
84
|
# inline on its create/detail/edit pages. See docs/inlines.md.
|
|
@@ -108,7 +107,7 @@ class ModelAdmin:
|
|
|
108
107
|
# transliterated to ASCII -- see polyadmin.core.slug.
|
|
109
108
|
prepopulated_unicode: ClassVar[Sequence[str]] = ()
|
|
110
109
|
# Adds "Save as new" to the edit form, which saves the submitted values
|
|
111
|
-
# as a new record and leaves the original alone. Opt-in
|
|
110
|
+
# as a new record and leaves the original alone. Opt-in.
|
|
112
111
|
save_as: ClassVar[bool] = False
|
|
113
112
|
# Whether the list hands its search, filters, sort and page to the pages
|
|
114
113
|
# reached from it, so they lead back into the list as it was left.
|
|
@@ -209,11 +208,9 @@ class ModelAdmin:
|
|
|
209
208
|
return self.empty_value_display or DEFAULT_EMPTY_VALUE
|
|
210
209
|
|
|
211
210
|
def get_default_ordering(self) -> str | None:
|
|
212
|
-
"""The sort to use when the request names none."""
|
|
213
211
|
return self.ordering
|
|
214
212
|
|
|
215
213
|
def is_readonly(self, name: str, obj: Any = None) -> bool:
|
|
216
|
-
"""The question every call site actually asks."""
|
|
217
214
|
return name in self.get_readonly_fields(obj)
|
|
218
215
|
|
|
219
216
|
def get_fieldsets(self) -> list[Fieldset]:
|
|
@@ -275,14 +272,14 @@ class ModelAdmin:
|
|
|
275
272
|
permission="delete",
|
|
276
273
|
where="list",
|
|
277
274
|
)
|
|
278
|
-
def delete_selected(self, objects: Sequence[Any], principal: Principal | None) -> str | None:
|
|
275
|
+
async def delete_selected(self, objects: Sequence[Any], principal: Principal | None) -> str | None:
|
|
279
276
|
"""The bulk delete every admin gets for free.
|
|
280
277
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
278
|
+
This is the single most common action anyone would otherwise write
|
|
279
|
+
by hand. It is expressed entirely in terms of this ModelAdmin's own
|
|
280
|
+
`delete` hook, so it works against whatever storage the application
|
|
281
|
+
has and honours whatever that hook already does (cascades, soft
|
|
282
|
+
deletes, hooks of its own).
|
|
286
283
|
|
|
287
284
|
permission="delete", not the resource's bare "view": the action route
|
|
288
285
|
checks it on top, so a principal who may look at a list but not destroy
|
|
@@ -292,7 +289,7 @@ class ModelAdmin:
|
|
|
292
289
|
deleted = 0
|
|
293
290
|
for obj in objects:
|
|
294
291
|
try:
|
|
295
|
-
self.delete(obj)
|
|
292
|
+
await maybe_await(self.delete(obj))
|
|
296
293
|
except Exception as exc:
|
|
297
294
|
# Stop at the first failure and report how far it got:
|
|
298
295
|
# silently continuing would leave the user unable to
|
|
@@ -336,7 +333,6 @@ class ModelAdmin:
|
|
|
336
333
|
raise NotImplementedError(f"{type(self).__name__} must implement update().")
|
|
337
334
|
|
|
338
335
|
def delete(self, obj: Any) -> None:
|
|
339
|
-
"""Delete an existing record."""
|
|
340
336
|
raise NotImplementedError(f"{type(self).__name__} must implement delete().")
|
|
341
337
|
|
|
342
338
|
def validate(self, data: dict[str, Any]) -> dict[str, list[str]]:
|
|
@@ -90,8 +90,8 @@ def is_sortable(model_admin: Any, name: str) -> bool:
|
|
|
90
90
|
|
|
91
91
|
def links_to_record(model_admin: Any, name: str) -> bool:
|
|
92
92
|
"""Whether a list cell links to the record. An unset list_display_links
|
|
93
|
-
links the first column
|
|
94
|
-
|
|
93
|
+
links the first column; an empty one links nothing and leaves the
|
|
94
|
+
row menu as the way in."""
|
|
95
95
|
linked = model_admin.list_display_links
|
|
96
96
|
if linked is None:
|
|
97
97
|
display = list(model_admin.list_display)
|
|
@@ -28,8 +28,6 @@ from polyadmin.fastapi.errors import csrf_failure
|
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
def make_csrf_route(admin, base_path: str) -> type[APIRoute]:
|
|
31
|
-
"""Build the APIRoute subclass for one mounted admin."""
|
|
32
|
-
|
|
33
31
|
class CSRFRoute(APIRoute):
|
|
34
32
|
def get_route_handler(self) -> Callable:
|
|
35
33
|
original = super().get_route_handler()
|
|
@@ -97,9 +97,9 @@ def decimal_display(value: Any) -> str:
|
|
|
97
97
|
is what Field.parse_form_value hands back for the "decimal" field
|
|
98
98
|
type -- see core/field.py), so it is shown with the shortest digit
|
|
99
99
|
string that round-trips back to the same float, in fixed-point, with
|
|
100
|
-
no trailing ".0" -- the same contract as Go's decimalText
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
no trailing ".0" -- the same contract as Go's decimalText, which uses
|
|
101
|
+
strconv.FormatFloat(f, 'f', -1, 64). Anything else (an int) falls back
|
|
102
|
+
to plain str().
|
|
103
103
|
"""
|
|
104
104
|
if isinstance(value, Decimal):
|
|
105
105
|
return format(value, "f")
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""The reference design system's variant lookups, resolved server-side.
|
|
2
2
|
|
|
3
|
-
`UI_REGISTRY` resolves
|
|
4
|
-
while the Jinja template renders, via the `ui` global::
|
|
3
|
+
`UI_REGISTRY` resolves the reference design system's class-variance-authority
|
|
4
|
+
(cva) lookups while the Jinja template renders, via the `ui` global::
|
|
5
5
|
|
|
6
6
|
{{ ui("button", "outline", "size-sm") }}
|
|
7
7
|
|
|
8
|
-
Each entry splits its class lists the way
|
|
8
|
+
Each entry splits its class lists the way the reference design system does:
|
|
9
9
|
``base``/``variants``/``sizes`` are cva axes and compose; ``parts`` are
|
|
10
10
|
sub-components (CardTitle, TableHead) and *replace* the base.
|
|
11
11
|
|
|
@@ -17,8 +17,9 @@ which throws on a value containing a space.
|
|
|
17
17
|
|
|
18
18
|
Colors are the CSS variables from ``admin/theme.html`` rather than a
|
|
19
19
|
literal palette, which is what makes the admin themeable. Sizes are
|
|
20
|
-
prefixed ``size-`` because
|
|
21
|
-
"default"; the prefix also tells the resolver which axis
|
|
20
|
+
prefixed ``size-`` because the reference design system has both a variant
|
|
21
|
+
and a size named "default"; the prefix also tells the resolver which axis
|
|
22
|
+
was given.
|
|
22
23
|
"""
|
|
23
24
|
from __future__ import annotations
|
|
24
25
|
|
|
@@ -32,14 +33,16 @@ UI_REGISTRY: dict[str, dict[str, object]] = {
|
|
|
32
33
|
# note on why a base must not fight its own axes.
|
|
33
34
|
"base": "inline-flex items-center justify-center gap-1.5 whitespace-nowrap rounded-md text-sm font-medium tracking-wide transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50",
|
|
34
35
|
"variants": {
|
|
35
|
-
#
|
|
36
|
+
# The reference design system's six stock button variants,
|
|
37
|
+
# verbatim in intent.
|
|
36
38
|
"default": "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
37
39
|
"destructive": "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
|
38
40
|
"outline": "border border-input bg-background text-foreground hover:bg-accent hover:text-accent-foreground",
|
|
39
41
|
"secondary": "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
40
42
|
"ghost": "text-foreground hover:bg-accent hover:text-accent-foreground",
|
|
41
43
|
"link": "text-primary underline-offset-4 hover:underline",
|
|
42
|
-
#
|
|
44
|
+
# The reference design system ships no destructive
|
|
45
|
+
# counterpart to `outline`. A
|
|
43
46
|
# detail page's Delete must match the weight of the outline
|
|
44
47
|
# Edit beside it (solid `destructive` is reserved for the
|
|
45
48
|
# confirmation page's submit), and a row's icon-only Delete
|
|
@@ -81,10 +84,11 @@ UI_REGISTRY: dict[str, dict[str, object]] = {
|
|
|
81
84
|
"sizes": {"size-default": "min-h-[80px] px-3 py-2"},
|
|
82
85
|
},
|
|
83
86
|
|
|
84
|
-
# A native <select>, not
|
|
85
|
-
# custom listbox does not post its value with a
|
|
86
|
-
# and the admin's forms have to keep working
|
|
87
|
-
# match the
|
|
87
|
+
# A native <select>, not the reference design system's Radix-backed
|
|
88
|
+
# SelectTrigger: a custom listbox does not post its value with a
|
|
89
|
+
# plain form submit, and the admin's forms have to keep working
|
|
90
|
+
# without JS. Styled to match the reference trigger (same height,
|
|
91
|
+
# border, ring).
|
|
88
92
|
"select": {
|
|
89
93
|
"base": "flex w-full items-center rounded-md border border-input bg-background text-sm text-foreground ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
|
90
94
|
"sizes": {
|
|
@@ -154,11 +158,11 @@ UI_REGISTRY: dict[str, dict[str, object]] = {
|
|
|
154
158
|
},
|
|
155
159
|
},
|
|
156
160
|
|
|
157
|
-
#
|
|
158
|
-
# your password?" are deliberately absent: the framework has
|
|
159
|
-
# behind any of them.
|
|
160
|
-
# photograph, which a framework cannot ship, so "aside"
|
|
161
|
-
# typographic panel instead.
|
|
161
|
+
# The reference login block. Its social buttons, "Sign up" and
|
|
162
|
+
# "Forgot your password?" are deliberately absent: the framework has
|
|
163
|
+
# no route behind any of them. That block fills the right column
|
|
164
|
+
# with a photograph, which a framework cannot ship, so "aside"
|
|
165
|
+
# styles a typographic panel instead.
|
|
162
166
|
"login": {
|
|
163
167
|
"parts": {
|
|
164
168
|
"page": "flex min-h-svh flex-col items-center justify-center bg-muted p-6 md:p-10",
|
|
@@ -289,8 +293,9 @@ UI_REGISTRY: dict[str, dict[str, object]] = {
|
|
|
289
293
|
},
|
|
290
294
|
},
|
|
291
295
|
|
|
292
|
-
# Sonner, the toaster
|
|
293
|
-
# ships: a 356px column in the viewport's
|
|
296
|
+
# Sonner, the toaster the reference design system ships. Its
|
|
297
|
+
# geometry is kept as it ships: a 356px column in the viewport's
|
|
298
|
+
# corner, 14px between toasts,
|
|
294
299
|
# 16px of padding, 13px text, and a close button parked half outside
|
|
295
300
|
# the top-left corner. What is dropped is Sonner's collapsed
|
|
296
301
|
# peek-stack, which is choreography for an app raising toasts
|
|
@@ -344,7 +349,7 @@ UI_REGISTRY: dict[str, dict[str, object]] = {
|
|
|
344
349
|
},
|
|
345
350
|
|
|
346
351
|
|
|
347
|
-
#
|
|
352
|
+
# The reference sidebar block, at its exact widths (16rem open, 3rem
|
|
348
353
|
# collapsed, 18rem mobile sheet). Its --sidebar-* colour scale is
|
|
349
354
|
# deliberately not reproduced: the Zinc values are within a hair of
|
|
350
355
|
# card/accent/border, and restyling the admin must mean editing
|
|
@@ -438,8 +443,8 @@ UI_REGISTRY: dict[str, dict[str, object]] = {
|
|
|
438
443
|
},
|
|
439
444
|
},
|
|
440
445
|
|
|
441
|
-
# Toolbar + footer of the list table, from
|
|
442
|
-
# search and filters left, page actions right; below, the
|
|
446
|
+
# Toolbar + footer of the list table, from the reference data-table
|
|
447
|
+
# example: search and filters left, page actions right; below, the
|
|
443
448
|
# DataTablePagination row.
|
|
444
449
|
#
|
|
445
450
|
# Stacked until lg, a row from lg up. lg, not sm, because the sidebar
|
|
@@ -507,9 +512,10 @@ UI_REGISTRY: dict[str, dict[str, object]] = {
|
|
|
507
512
|
"selection": "flex-1 text-sm text-muted-foreground",
|
|
508
513
|
"controls": "flex items-center gap-4 lg:gap-8",
|
|
509
514
|
"rows-per-page": "flex items-center gap-2 text-sm font-medium",
|
|
510
|
-
#
|
|
511
|
-
# is wider than that in most
|
|
512
|
-
#
|
|
515
|
+
# The reference implementation fixes this at w-[100px]; a
|
|
516
|
+
# translated "Page 1 of 8" is wider than that in most
|
|
517
|
+
# languages, so the width is a floor and the label never
|
|
518
|
+
# wraps to a second line.
|
|
513
519
|
"page-indicator": (
|
|
514
520
|
"flex min-w-[100px] items-center justify-center whitespace-nowrap "
|
|
515
521
|
"text-sm font-medium"
|
|
@@ -535,10 +541,10 @@ UI_REGISTRY: dict[str, dict[str, object]] = {
|
|
|
535
541
|
"head": "[&_tr]:border-b [&_tr]:border-border",
|
|
536
542
|
"th": "px-4 py-2.5 text-left align-middle font-medium whitespace-nowrap text-muted-foreground",
|
|
537
543
|
"body": "divide-y divide-border",
|
|
538
|
-
# data-[state=selected], from
|
|
539
|
-
# checked row-checkbox (list_content.html)
|
|
540
|
-
# row, the same as a hover, so a selection
|
|
541
|
-
# instead of only through the toolbar's "N of M selected"
|
|
544
|
+
# data-[state=selected], from the reference design system's
|
|
545
|
+
# own TableRow: a checked row-checkbox (list_content.html)
|
|
546
|
+
# tints the whole row, the same as a hover, so a selection
|
|
547
|
+
# reads at a glance instead of only through the toolbar's "N of M selected"
|
|
542
548
|
# count.
|
|
543
549
|
"row": "text-foreground transition-colors hover:bg-muted/50 has-[.row-checkbox:checked]:bg-muted",
|
|
544
550
|
"cell": "px-4 py-2.5 align-middle text-sm whitespace-nowrap",
|
|
@@ -551,8 +557,8 @@ UI_REGISTRY: dict[str, dict[str, object]] = {
|
|
|
551
557
|
},
|
|
552
558
|
|
|
553
559
|
|
|
554
|
-
# The label + control + description + error unit.
|
|
555
|
-
# FormItem/FormLabel/FormDescription/FormMessage; here it is what
|
|
560
|
+
# The label + control + description + error unit. The reference
|
|
561
|
+
# design system calls this FormItem/FormLabel/FormDescription/FormMessage; here it is what
|
|
556
562
|
# wraps every generated form input.
|
|
557
563
|
"field": {
|
|
558
564
|
"base": "mb-4",
|
|
@@ -595,10 +601,10 @@ UI_REGISTRY: dict[str, dict[str, object]] = {
|
|
|
595
601
|
},
|
|
596
602
|
},
|
|
597
603
|
|
|
598
|
-
# The many-to-many control:
|
|
599
|
-
# relation's options, selection
|
|
600
|
-
#
|
|
601
|
-
# this form column doesn't have.
|
|
604
|
+
# The many-to-many control: the reference design system's
|
|
605
|
+
# Combobox/Command idiom over the relation's options, selection
|
|
606
|
+
# shown as removable chips. Not a two-pane layout, which would need
|
|
607
|
+
# width this form column doesn't have.
|
|
602
608
|
"multi-select": {
|
|
603
609
|
"parts": {
|
|
604
610
|
# min-h matches the single Select's h-10 so a field with
|
|
@@ -667,8 +673,8 @@ UI_REGISTRY: dict[str, dict[str, object]] = {
|
|
|
667
673
|
},
|
|
668
674
|
},
|
|
669
675
|
|
|
670
|
-
# Form fieldset
|
|
671
|
-
#
|
|
676
|
+
# Form fieldset: a titled, collapsible group of fields inside the
|
|
677
|
+
# record form. A group with no title renders
|
|
672
678
|
# bare -- no header, no border -- so the default single-group case
|
|
673
679
|
# looks exactly like the flat form it replaces.
|
|
674
680
|
"fieldset": {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""@action: declaring actions as ModelAdmin methods."""
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
3
4
|
from collections.abc import Sequence
|
|
4
5
|
|
|
5
6
|
import pytest
|
|
@@ -168,6 +169,6 @@ def test_overriding_delete_selected_replaces_the_deletion_and_keeps_the_rest():
|
|
|
168
169
|
def test_delete_selected_deletes_through_the_delete_hook():
|
|
169
170
|
model_admin = InMemoryUserAdmin()
|
|
170
171
|
user = model_admin.create({"email": "a@example.com"})
|
|
171
|
-
message = model_admin.delete_selected([user], None)
|
|
172
|
+
message = asyncio.run(model_admin.delete_selected([user], None))
|
|
172
173
|
assert model_admin.get_object(user.id) is None
|
|
173
174
|
assert "1" in message
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""delete_selected's failure message reads in the request's language."""
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
3
5
|
import pytest
|
|
4
6
|
|
|
5
7
|
from polyadmin.i18n import GettextTranslator, use_locale
|
|
@@ -19,6 +21,6 @@ def test_failure_message_is_translated(locale, expected):
|
|
|
19
21
|
model_admin = FailingUserAdmin()
|
|
20
22
|
user = model_admin.create({"email": "a@example.com"})
|
|
21
23
|
with use_locale(locale, GettextTranslator()), pytest.raises(RuntimeError) as caught:
|
|
22
|
-
model_admin.delete_selected([user], None)
|
|
24
|
+
asyncio.run(model_admin.delete_selected([user], None))
|
|
23
25
|
assert expected in str(caught.value)
|
|
24
26
|
assert "disk full" in str(caught.value)
|
|
@@ -45,9 +45,6 @@ def titles(objects):
|
|
|
45
45
|
return [o.title for o in objects]
|
|
46
46
|
|
|
47
47
|
|
|
48
|
-
# --- slugify ---------------------------------------------------------
|
|
49
|
-
|
|
50
|
-
|
|
51
48
|
@pytest.mark.parametrize(
|
|
52
49
|
("value", "want"),
|
|
53
50
|
[
|
|
@@ -77,9 +74,6 @@ def test_slugify_unicode_keeps_its_letters(value, want):
|
|
|
77
74
|
assert slugify_unicode(value) == want
|
|
78
75
|
|
|
79
76
|
|
|
80
|
-
# --- the date filter -------------------------------------------------
|
|
81
|
-
|
|
82
|
-
|
|
83
77
|
class DatedPostAdmin(PostAdmin):
|
|
84
78
|
filters = [DateFilter("created")]
|
|
85
79
|
|
|
@@ -129,9 +123,6 @@ def test_date_filter_is_declared_like_any_other():
|
|
|
129
123
|
assert [f.name for f in admin.filters] == ["created"]
|
|
130
124
|
|
|
131
125
|
|
|
132
|
-
# --- sortable_by -----------------------------------------------------
|
|
133
|
-
|
|
134
|
-
|
|
135
126
|
def test_unset_sortable_by_leaves_every_column_sortable():
|
|
136
127
|
admin = PostAdmin()
|
|
137
128
|
assert all(is_sortable(admin, name) for name in ("id", "title", "created"))
|
|
@@ -169,9 +160,6 @@ def test_the_model_admins_own_default_ordering_is_exempt_from_the_restriction():
|
|
|
169
160
|
assert apply_defaults(Restricted(), ListRequest()).ordering == "-created"
|
|
170
161
|
|
|
171
162
|
|
|
172
|
-
# --- list_display_links ----------------------------------------------
|
|
173
|
-
|
|
174
|
-
|
|
175
163
|
def test_unset_list_display_links_links_the_first_column_only():
|
|
176
164
|
admin = PostAdmin()
|
|
177
165
|
assert links_to_record(admin, "id") and not links_to_record(admin, "title")
|
|
@@ -189,9 +177,6 @@ def test_list_display_links_chooses_and_empty_means_no_links():
|
|
|
189
177
|
assert not any(links_to_record(Unlinked(), name) for name in ("id", "title", "created"))
|
|
190
178
|
|
|
191
179
|
|
|
192
|
-
# --- the other options' defaults -------------------------------------
|
|
193
|
-
|
|
194
|
-
|
|
195
180
|
def test_preserve_filters_is_on_and_save_as_is_off_by_default():
|
|
196
181
|
admin = PostAdmin()
|
|
197
182
|
assert admin.preserve_filters is True
|
|
@@ -75,6 +75,19 @@ def test_delete_post_awaits_async_get_object_and_async_delete():
|
|
|
75
75
|
assert asyncio.run(user_admin.get_object(user.id)) is None
|
|
76
76
|
|
|
77
77
|
|
|
78
|
+
def test_delete_selected_awaits_async_delete():
|
|
79
|
+
client, user_admin = async_client()
|
|
80
|
+
user = asyncio.run(user_admin.create({"email": "gone@example.com"}))
|
|
81
|
+
response = client.post(
|
|
82
|
+
"/admin/users/actions/delete_selected",
|
|
83
|
+
data={"pks": [str(user.id)]},
|
|
84
|
+
headers=csrf(client),
|
|
85
|
+
follow_redirects=False,
|
|
86
|
+
)
|
|
87
|
+
assert response.status_code in (302, 303)
|
|
88
|
+
assert asyncio.run(user_admin.get_object(user.id)) is None
|
|
89
|
+
|
|
90
|
+
|
|
78
91
|
class AsyncListPageUserAdmin(InMemoryUserAdmin):
|
|
79
92
|
async def list_page(self, list_request):
|
|
80
93
|
await asyncio.sleep(0)
|