polyadmin 0.1.0b1__tar.gz → 0.1.0b3__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.0b3}/PKG-INFO +10 -8
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/README.md +9 -7
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/docs/architecture.md +2 -2
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/docs/deletes.md +4 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/docs/python/index.md +3 -2
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/examples/fastapi/models.py +8 -9
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/examples/fastapi/session.py +11 -5
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/examples/fastapi/tests/test_app.py +1 -1
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/examples/fastapi/user_admin.py +3 -5
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/examples/fastapi/uv.lock +1 -1
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/__init__.py +2 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/core/admin.py +2 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/core/delete.py +8 -4
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/core/field.py +14 -10
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/core/filter.py +0 -1
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/core/inline.py +2 -7
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/core/model_admin.py +17 -18
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/core/query.py +2 -2
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/core/template_context.py +254 -86
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/fastapi/csrf.py +0 -2
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/fastapi/deletes.py +2 -2
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/fastapi/handlers.py +5 -5
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/fastapi/login.py +1 -1
- polyadmin-0.1.0b3/polyadmin/locale/fr/LC_MESSAGES/polyadmin.mo +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/locale/fr/LC_MESSAGES/polyadmin.po +18 -9
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/locale/polyadmin.pot +7 -7
- polyadmin-0.1.0b3/polyadmin/locale/ru/LC_MESSAGES/polyadmin.mo +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/locale/ru/LC_MESSAGES/polyadmin.po +18 -9
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/base.html +1 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/field.html +12 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/icons.html +1 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/inline.html +18 -8
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/ui/field.html +1 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/login.html +8 -4
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/theme.html +5 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templating.py +3 -3
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/ui.py +64 -84
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/pyproject.toml +1 -1
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/test_action_decorator.py +2 -1
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/test_delete.py +12 -11
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/test_delete_selected_i18n.py +3 -1
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/test_i18n.py +2 -3
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/test_parity.py +0 -15
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/test_template_context.py +24 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_actions.py +1 -1
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_async_model_admin.py +13 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_async_relations.py +0 -9
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_audit.py +0 -1
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_code_strings.py +1 -1
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_components.py +10 -14
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_delete_selected.py +0 -1
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_errors.py +0 -2
- polyadmin-0.1.0b3/tests/fastapi/test_favicon.py +35 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_filters.py +1 -2
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_login.py +17 -2
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_parity_list.py +0 -11
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_relations.py +4 -3
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_router.py +2 -2
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_scroll_area.py +60 -1
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_switcher.py +2 -2
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_theme.py +2 -1
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/test_format.py +48 -1
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/test_layout.py +2 -2
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/test_templating.py +4 -5
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/test_ui.py +11 -14
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/uv.lock +1 -1
- polyadmin-0.1.0b1/polyadmin/locale/fr/LC_MESSAGES/polyadmin.mo +0 -0
- polyadmin-0.1.0b1/polyadmin/locale/ru/LC_MESSAGES/polyadmin.mo +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/.github/workflows/notify-docs.yml +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/.github/workflows/publish.yml +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/.gitignore +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/LICENSE +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/babel.cfg +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/browsertests/README.md +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/browsertests/conftest.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/browsertests/test_deletes.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/browsertests/test_flows.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/browsertests/test_i18n.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/browsertests/test_parity.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/docs/audit.md +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/docs/authentication.md +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/docs/components.md +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/docs/concepts/architecture.md +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/docs/concepts/model-admin.md +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/docs/dashboard.md +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/docs/exports.md +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/docs/i18n.md +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/docs/inlines.md +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/docs/lists.md +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/docs/model-admin.md +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/docs/permissions.md +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/docs/routing.md +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/docs/templates.md +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/examples/README.md +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/examples/fastapi/README.md +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/examples/fastapi/main.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/examples/fastapi/organization_admin.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/examples/fastapi/pages.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/examples/fastapi/pyproject.toml +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/examples/fastapi/role_admin.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/examples/fastapi/templates/pages/broadcast.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/mkdocs.yml +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/core/__init__.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/core/_async.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/core/action.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/core/audit.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/core/auth.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/core/authorization.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/core/csrf.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/core/dashboard.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/core/exporter.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/core/login.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/core/page.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/core/pagination.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/core/relation.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/core/slug.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/core/widget.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/fastapi/__init__.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/fastapi/audit.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/fastapi/auth.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/fastapi/errors.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/fastapi/inlines.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/fastapi/locale.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/fastapi/pages.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/fastapi/relations.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/fastapi/responses.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/fastapi/router.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/fastapi/static.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/i18n/__init__.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/i18n/context.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/i18n/negotiation.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/i18n/setup.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/i18n/translator.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/action_confirm_modal.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/csrf-field.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/error_fragment.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/form_wrapper.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/inline_fragment.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/list_content.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/lookup_results.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/search.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/toasts.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/ui/breadcrumb.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/ui/bulk-actions.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/ui/calendar.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/ui/combobox.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/ui/delete-preview.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/ui/dropdown-menu.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/ui/filter-panel.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/ui/locale-switcher.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/ui/multi-select.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/ui/pagination.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/ui/radio-group.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/ui/select.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/ui/sidebar.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/ui/slider.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/ui/switch.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/ui/table.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/ui/theme-toggle.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/dashboard.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/error.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/resource/delete.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/resource/delete_selected.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/resource/detail.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/resource/form.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/resource/list.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/widgets/activity.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/widgets/chart.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/widgets/donut.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/widgets/metric.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/widgets/progress.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/widgets/stat.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/widgets/table.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/widgets/tabs.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/widgets/timeline.html +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/__init__.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/conftest.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/__init__.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/mo.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/test_action_placement.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/test_admin.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/test_async.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/test_auth.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/test_authorization.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/test_csrf.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/test_dashboard.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/test_exporter.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/test_field.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/test_filter.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/test_inline.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/test_login.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/test_model_admin.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/test_pagination.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/test_query.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/test_relation.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/test_widget.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/__init__.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_action_placement.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_auth.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_csrf.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_dashboard.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_delete_preview.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_delete_selected_preview.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_export.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_htmx.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_inlines.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_list_querier.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_listopts.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_locale.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_pages.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_pseudo_locale.py +0 -0
- {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/test_catalogs.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.0b3
|
|
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`
|
|
@@ -59,9 +59,10 @@ permissions) and you get:
|
|
|
59
59
|
- Relation fields rendered as links, or as a searchable shadcn/ui
|
|
60
60
|
Command-style autocomplete backed by a server-side `/lookup` route
|
|
61
61
|
(never dumps the target model's full queryset into the page)
|
|
62
|
+
- Image fields with upload handling and preview rendering
|
|
62
63
|
- Inline related records (`StackedInline`/`TabularInline`) — a
|
|
63
64
|
parent's create/detail/edit pages can show and manage a child's
|
|
64
|
-
records that point back at it,
|
|
65
|
+
records that point back at it, directly within the parent workflow
|
|
65
66
|
- Record and bulk Actions, with a shadcn/ui Dialog confirmation step for
|
|
66
67
|
destructive ones
|
|
67
68
|
- Delete previews: the confirmation page says what else a delete takes
|
|
@@ -73,6 +74,7 @@ permissions) and you get:
|
|
|
73
74
|
- CSV and XLSX export
|
|
74
75
|
- Internationalisation: per-request locale, language switcher, French
|
|
75
76
|
and Russian
|
|
77
|
+
- Built-in favicon support for the admin shell
|
|
76
78
|
- Toast notifications for every create/update/delete/action
|
|
77
79
|
- Per-resource (and per-widget) template overrides, so an application
|
|
78
80
|
can replace one view's markup without forking the framework
|
|
@@ -82,8 +84,8 @@ Alpine.js + Tailwind — its CSS-variable token system and component
|
|
|
82
84
|
markup, without React or Radix. That gives the admin **dark mode and
|
|
83
85
|
themability**: every color resolves through a CSS variable, so restyling
|
|
84
86
|
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
|
-
|
|
87
|
+
with a collapsible sidebar (a Sheet below `md`), a right-hand filter panel
|
|
88
|
+
for focused workflows on wider screens, and breadcrumbs as the page
|
|
87
89
|
title. Tailwind/Alpine/HTMX are all CDN-loaded — no frontend build step.
|
|
88
90
|
See [`docs/components.md`](docs/components.md).
|
|
89
91
|
|
|
@@ -92,7 +94,7 @@ See [`docs/components.md`](docs/components.md).
|
|
|
92
94
|
The first beta release is available on PyPI:
|
|
93
95
|
|
|
94
96
|
```bash
|
|
95
|
-
pip install "polyadmin[fastapi]==0.1.
|
|
97
|
+
pip install "polyadmin[fastapi]==0.1.0b3"
|
|
96
98
|
```
|
|
97
99
|
|
|
98
100
|
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`
|
|
@@ -29,9 +29,10 @@ permissions) and you get:
|
|
|
29
29
|
- Relation fields rendered as links, or as a searchable shadcn/ui
|
|
30
30
|
Command-style autocomplete backed by a server-side `/lookup` route
|
|
31
31
|
(never dumps the target model's full queryset into the page)
|
|
32
|
+
- Image fields with upload handling and preview rendering
|
|
32
33
|
- Inline related records (`StackedInline`/`TabularInline`) — a
|
|
33
34
|
parent's create/detail/edit pages can show and manage a child's
|
|
34
|
-
records that point back at it,
|
|
35
|
+
records that point back at it, directly within the parent workflow
|
|
35
36
|
- Record and bulk Actions, with a shadcn/ui Dialog confirmation step for
|
|
36
37
|
destructive ones
|
|
37
38
|
- Delete previews: the confirmation page says what else a delete takes
|
|
@@ -43,6 +44,7 @@ permissions) and you get:
|
|
|
43
44
|
- CSV and XLSX export
|
|
44
45
|
- Internationalisation: per-request locale, language switcher, French
|
|
45
46
|
and Russian
|
|
47
|
+
- Built-in favicon support for the admin shell
|
|
46
48
|
- Toast notifications for every create/update/delete/action
|
|
47
49
|
- Per-resource (and per-widget) template overrides, so an application
|
|
48
50
|
can replace one view's markup without forking the framework
|
|
@@ -52,8 +54,8 @@ Alpine.js + Tailwind — its CSS-variable token system and component
|
|
|
52
54
|
markup, without React or Radix. That gives the admin **dark mode and
|
|
53
55
|
themability**: every color resolves through a CSS variable, so restyling
|
|
54
56
|
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
|
-
|
|
57
|
+
with a collapsible sidebar (a Sheet below `md`), a right-hand filter panel
|
|
58
|
+
for focused workflows on wider screens, and breadcrumbs as the page
|
|
57
59
|
title. Tailwind/Alpine/HTMX are all CDN-loaded — no frontend build step.
|
|
58
60
|
See [`docs/components.md`](docs/components.md).
|
|
59
61
|
|
|
@@ -62,7 +64,7 @@ See [`docs/components.md`](docs/components.md).
|
|
|
62
64
|
The first beta release is available on PyPI:
|
|
63
65
|
|
|
64
66
|
```bash
|
|
65
|
-
pip install "polyadmin[fastapi]==0.1.
|
|
67
|
+
pip install "polyadmin[fastapi]==0.1.0b3"
|
|
66
68
|
```
|
|
67
69
|
|
|
68
70
|
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
|
|
@@ -39,6 +39,10 @@ class DeletePreviewer(Protocol):
|
|
|
39
39
|
def delete_preview(self, objects: list[Any]) -> DeletePreview: ...
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
+
It may be a coroutine function — the same optional-async shape as
|
|
43
|
+
`get_object`/`create`/`update`/`delete` — for a ModelAdmin backed by an
|
|
44
|
+
async client that needs to look up the cascade over the network.
|
|
45
|
+
|
|
42
46
|
`objects` is always a list — one record from the delete page, the whole
|
|
43
47
|
selection from `delete_selected` — so one implementation serves both,
|
|
44
48
|
and a bulk delete costs one query per relation rather than one per row:
|
|
@@ -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)
|
|
@@ -163,9 +163,8 @@ def seed(
|
|
|
163
163
|
organizations: OrganizationRepository,
|
|
164
164
|
roles: RoleRepository,
|
|
165
165
|
) -> None:
|
|
166
|
-
#
|
|
167
|
-
#
|
|
168
|
-
# the fr locale, and it doesn't matter which organization it looks at.
|
|
166
|
+
# A browser test checks that this date renders with a French month name
|
|
167
|
+
# under the fr locale, regardless of which organization it looks at.
|
|
169
168
|
# Acme keeps March 2019: a browser test pins "Mar 1, 2019" as a data
|
|
170
169
|
# value and checks the same date renders "mars" under fr. The others
|
|
171
170
|
# spread out, so the date drill-down has years and months to walk.
|
|
@@ -40,6 +40,7 @@ class DemoAccount:
|
|
|
40
40
|
"""One row of what would be a users table."""
|
|
41
41
|
|
|
42
42
|
email: str
|
|
43
|
+
username: str
|
|
43
44
|
# Salt and hash, never the password. Derived at import time only
|
|
44
45
|
# because a runnable demo has to document its own credentials; a real
|
|
45
46
|
# table stores these and has never seen the plaintext.
|
|
@@ -55,9 +56,9 @@ class DemoAccount:
|
|
|
55
56
|
# whose preferred locale the host resolver -- not the switcher cookie or
|
|
56
57
|
# Accept-Language -- decides.
|
|
57
58
|
DEMO_CREDENTIALS = [
|
|
58
|
-
("admin@example.com", "polyadmin", "Demo Admin", True, ""),
|
|
59
|
-
("viewer@example.com", "polyadmin", "Demo Viewer", False, ""),
|
|
60
|
-
("amelie@example.com", "polyadmin", "Amélie", True, "fr"),
|
|
59
|
+
("admin@example.com", "admin", "polyadmin", "Demo Admin", True, ""),
|
|
60
|
+
("viewer@example.com", "viewer", "polyadmin", "Demo Viewer", False, ""),
|
|
61
|
+
("amelie@example.com", "amelie", "polyadmin", "Amélie", True, "fr"),
|
|
61
62
|
]
|
|
62
63
|
|
|
63
64
|
|
|
@@ -114,16 +115,21 @@ class CookieSessionBackend:
|
|
|
114
115
|
def __init__(self) -> None:
|
|
115
116
|
self._secret = _session_secret()
|
|
116
117
|
self._accounts: dict[str, DemoAccount] = {}
|
|
117
|
-
for email, password, display_name, is_superuser, locale in DEMO_CREDENTIALS:
|
|
118
|
+
for email, username, password, display_name, is_superuser, locale in DEMO_CREDENTIALS:
|
|
118
119
|
salt = secrets.token_bytes(16)
|
|
119
|
-
|
|
120
|
+
account = DemoAccount(
|
|
120
121
|
email=email,
|
|
122
|
+
username=username,
|
|
121
123
|
salt=salt,
|
|
122
124
|
hash=_derive(password, salt),
|
|
123
125
|
display_name=display_name,
|
|
124
126
|
is_superuser=is_superuser,
|
|
125
127
|
locale=locale,
|
|
126
128
|
)
|
|
129
|
+
# Indexed under both so the login page's "username or email"
|
|
130
|
+
# field resolves either one to the same account.
|
|
131
|
+
self._accounts[email] = account
|
|
132
|
+
self._accounts[username] = account
|
|
127
133
|
|
|
128
134
|
def verify_credentials(self, request: Any, identifier: str, password: str) -> Principal | None:
|
|
129
135
|
"""Answers "are these good?" and does not touch the response. Establishing
|
|
@@ -130,7 +130,7 @@ def test_create_organization_persists_founded_and_balance():
|
|
|
130
130
|
location = response.headers["location"]
|
|
131
131
|
detail = client.get(location)
|
|
132
132
|
assert detail.status_code == 200
|
|
133
|
-
# Rendered as the markup contract
|
|
133
|
+
# Rendered as the markup contract: a <time datetime="...">
|
|
134
134
|
# for the date and a data-value carrying the exact posted precision.
|
|
135
135
|
# This exercises the real create() pipeline end to end -- Field.parse_
|
|
136
136
|
# form_value (float for "decimal", raw string for "date"), the example
|
|
@@ -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
|
|
@@ -13,6 +13,7 @@ from polyadmin.core.field import (
|
|
|
13
13
|
EmailField,
|
|
14
14
|
EnumField,
|
|
15
15
|
Field,
|
|
16
|
+
ImageField,
|
|
16
17
|
IntegerField,
|
|
17
18
|
JSONField,
|
|
18
19
|
PasswordField,
|
|
@@ -36,6 +37,7 @@ __all__ = [
|
|
|
36
37
|
"EmailField",
|
|
37
38
|
"EnumField",
|
|
38
39
|
"Field",
|
|
40
|
+
"ImageField",
|
|
39
41
|
"IntegerField",
|
|
40
42
|
"JSONField",
|
|
41
43
|
"ModelAdmin",
|
|
@@ -28,6 +28,7 @@ class Admin:
|
|
|
28
28
|
authorizer: Any | None = None,
|
|
29
29
|
site_title: str = "PolyAdmin",
|
|
30
30
|
site_logo_url: str | None = None,
|
|
31
|
+
site_favicon_url: str | None = None,
|
|
31
32
|
disable_csrf: bool = False,
|
|
32
33
|
audit_logger: Any | None = None,
|
|
33
34
|
login_backend: Any | None = None,
|
|
@@ -45,6 +46,7 @@ class Admin:
|
|
|
45
46
|
self.authorizer = authorizer
|
|
46
47
|
self.site_title = site_title
|
|
47
48
|
self.site_logo_url = site_logo_url
|
|
49
|
+
self.site_favicon_url = site_favicon_url
|
|
48
50
|
# Opt-out, never opt-in: a security control that defaults to off
|
|
49
51
|
# is one nobody turns on. The token cookie is still minted when
|
|
50
52
|
# this is set, so templates and custom pages behave identically.
|
|
@@ -6,10 +6,11 @@ that makes a host's answer safe to show one principal.
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
8
|
import hashlib
|
|
9
|
-
from collections.abc import Sequence
|
|
9
|
+
from collections.abc import Awaitable, Sequence
|
|
10
10
|
from dataclasses import dataclass, field
|
|
11
11
|
from typing import Any, Protocol
|
|
12
12
|
|
|
13
|
+
from polyadmin.core._async import maybe_await
|
|
13
14
|
from polyadmin.core.authorization import resource_permission
|
|
14
15
|
|
|
15
16
|
# How many records of a group are listed; the rest are counted.
|
|
@@ -46,9 +47,12 @@ class DeletePreviewer(Protocol):
|
|
|
46
47
|
`objects` is always a list -- one record from the delete page, the whole
|
|
47
48
|
selection from delete_selected -- so an implementation over SQL can
|
|
48
49
|
answer a bulk delete with one count and one sample query per relation.
|
|
50
|
+
|
|
51
|
+
May be a coroutine function; resolve_delete_preview awaits it when it is
|
|
52
|
+
one, the same optional-async shape as get_object/create/update/delete.
|
|
49
53
|
"""
|
|
50
54
|
|
|
51
|
-
def delete_preview(self, objects: list[Any]) -> DeletePreview: ...
|
|
55
|
+
def delete_preview(self, objects: list[Any]) -> DeletePreview | Awaitable[DeletePreview]: ...
|
|
52
56
|
|
|
53
57
|
|
|
54
58
|
@dataclass
|
|
@@ -74,14 +78,14 @@ def previews_deletes(model_admin: Any) -> bool:
|
|
|
74
78
|
return callable(getattr(model_admin, "delete_preview", None))
|
|
75
79
|
|
|
76
80
|
|
|
77
|
-
def resolve_delete_preview(admin: Any, model_admin: Any, principal: Any, objects: Sequence[Any]) -> ResolvedDeletePreview:
|
|
81
|
+
async def resolve_delete_preview(admin: Any, model_admin: Any, principal: Any, objects: Sequence[Any]) -> ResolvedDeletePreview:
|
|
78
82
|
"""Ask model_admin what deleting objects takes with it and filter the answer
|
|
79
83
|
for principal. Without the capability: an empty, non-blocking result, and
|
|
80
84
|
nothing is called.
|
|
81
85
|
"""
|
|
82
86
|
if not previews_deletes(model_admin):
|
|
83
87
|
return ResolvedDeletePreview()
|
|
84
|
-
preview = model_admin.delete_preview(list(objects))
|
|
88
|
+
preview = await maybe_await(model_admin.delete_preview(list(objects)))
|
|
85
89
|
out = ResolvedDeletePreview()
|
|
86
90
|
for group in preview.cascades:
|
|
87
91
|
resolved = _resolve_group(admin, principal, group)
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
"""Field abstraction.
|
|
2
2
|
|
|
3
3
|
A Field represents a model property plus its admin presentation, not
|
|
4
|
-
merely an HTML input.
|
|
5
|
-
|
|
6
|
-
rendering will build on.
|
|
4
|
+
merely an HTML input. This module defines the data-level contract that
|
|
5
|
+
context-aware rendering (list/detail/form/filter) builds on.
|
|
7
6
|
"""
|
|
8
7
|
|
|
9
8
|
from __future__ import annotations
|
|
@@ -20,8 +19,6 @@ _UNSET = object()
|
|
|
20
19
|
|
|
21
20
|
|
|
22
21
|
class Field:
|
|
23
|
-
"""Base class for all admin field types."""
|
|
24
|
-
|
|
25
22
|
field_type: str = "field"
|
|
26
23
|
|
|
27
24
|
def __init__(
|
|
@@ -52,7 +49,6 @@ class Field:
|
|
|
52
49
|
return self.default is not _UNSET
|
|
53
50
|
|
|
54
51
|
def get_value(self, obj: Any) -> Any:
|
|
55
|
-
"""Read this field's value off a model instance."""
|
|
56
52
|
value = getattr(obj, self.name, _UNSET)
|
|
57
53
|
if value is _UNSET:
|
|
58
54
|
return self.default if self.has_default else None
|
|
@@ -90,7 +86,9 @@ class Field:
|
|
|
90
86
|
"""Run configured validators, returning a list of error messages."""
|
|
91
87
|
errors: list[str] = []
|
|
92
88
|
if self.required and (value is None or value == ""):
|
|
93
|
-
errors.append(
|
|
89
|
+
errors.append(
|
|
90
|
+
gettext("%(label)s is required.") % {"label": gettext(self.label)}
|
|
91
|
+
)
|
|
94
92
|
return errors
|
|
95
93
|
for validator in self.validators:
|
|
96
94
|
try:
|
|
@@ -141,6 +139,10 @@ class URLField(Field):
|
|
|
141
139
|
field_type = "url"
|
|
142
140
|
|
|
143
141
|
|
|
142
|
+
class ImageField(Field):
|
|
143
|
+
field_type = "image"
|
|
144
|
+
|
|
145
|
+
|
|
144
146
|
class UUIDField(Field):
|
|
145
147
|
field_type = "uuid"
|
|
146
148
|
|
|
@@ -188,8 +190,6 @@ class OneToOneField(ForeignKeyField):
|
|
|
188
190
|
|
|
189
191
|
|
|
190
192
|
class ManyToManyField(Field):
|
|
191
|
-
"""A collection of related objects."""
|
|
192
|
-
|
|
193
193
|
field_type = "manytomany"
|
|
194
194
|
|
|
195
195
|
def __init__(self, name: str, *, relation: Relation, **kwargs: Any) -> None:
|
|
@@ -197,5 +197,9 @@ class ManyToManyField(Field):
|
|
|
197
197
|
self.relation = relation
|
|
198
198
|
|
|
199
199
|
def get_value(self, obj: Any) -> Any:
|
|
200
|
-
value =
|
|
200
|
+
value = (
|
|
201
|
+
self.relation.get_related(obj)
|
|
202
|
+
if self.relation.get_related is not None
|
|
203
|
+
else super().get_value(obj)
|
|
204
|
+
)
|
|
201
205
|
return value if value is not None else []
|
|
@@ -299,7 +299,6 @@ class RelationFilter(Filter):
|
|
|
299
299
|
if not many:
|
|
300
300
|
if self._pk(related) == raw_value:
|
|
301
301
|
kept.append(obj)
|
|
302
|
-
# A `many` relation matches when any member does.
|
|
303
302
|
elif any(self._pk(member) == raw_value for member in related):
|
|
304
303
|
kept.append(obj)
|
|
305
304
|
return kept
|
|
@@ -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] = ()
|
|
@@ -48,6 +49,9 @@ class ModelAdmin:
|
|
|
48
49
|
# Sidebar icon name (see components/icons.html), shown flat or nested
|
|
49
50
|
# inside a category's accordion alike.
|
|
50
51
|
icon: ClassVar[str] = "collection"
|
|
52
|
+
# Browser-tab icon shown while viewing this ModelAdmin's pages. ""
|
|
53
|
+
# falls back to Admin.site_favicon_url.
|
|
54
|
+
favicon_url: ClassVar[str] = ""
|
|
51
55
|
|
|
52
56
|
list_display: ClassVar[Sequence[str]] = ()
|
|
53
57
|
form_fields: ClassVar[Sequence[str]] = ()
|
|
@@ -65,7 +69,6 @@ class ModelAdmin:
|
|
|
65
69
|
# stable between requests.
|
|
66
70
|
ordering: ClassVar[str | None] = None
|
|
67
71
|
# How many rows a list page holds; None means DEFAULT_PAGE_SIZE.
|
|
68
|
-
# Django's list_per_page.
|
|
69
72
|
list_per_page: ClassVar[int | None] = None
|
|
70
73
|
# What a read-only view shows for a None or blank value; None means
|
|
71
74
|
# DEFAULT_EMPTY_VALUE.
|
|
@@ -78,8 +81,7 @@ class ModelAdmin:
|
|
|
78
81
|
# action whose `where` includes the detail page; a list overrides `where`
|
|
79
82
|
# (so a "list" action can be named here), and [] offers none.
|
|
80
83
|
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.
|
|
84
|
+
# Removes the built-in bulk delete. Opt-out, so the default keeps it.
|
|
83
85
|
disable_delete_selected: ClassVar[bool] = False
|
|
84
86
|
# Child ModelAdmins whose records point back at this one, managed
|
|
85
87
|
# inline on its create/detail/edit pages. See docs/inlines.md.
|
|
@@ -108,7 +110,7 @@ class ModelAdmin:
|
|
|
108
110
|
# transliterated to ASCII -- see polyadmin.core.slug.
|
|
109
111
|
prepopulated_unicode: ClassVar[Sequence[str]] = ()
|
|
110
112
|
# 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
|
|
113
|
+
# as a new record and leaves the original alone. Opt-in.
|
|
112
114
|
save_as: ClassVar[bool] = False
|
|
113
115
|
# Whether the list hands its search, filters, sort and page to the pages
|
|
114
116
|
# reached from it, so they lead back into the list as it was left.
|
|
@@ -209,11 +211,9 @@ class ModelAdmin:
|
|
|
209
211
|
return self.empty_value_display or DEFAULT_EMPTY_VALUE
|
|
210
212
|
|
|
211
213
|
def get_default_ordering(self) -> str | None:
|
|
212
|
-
"""The sort to use when the request names none."""
|
|
213
214
|
return self.ordering
|
|
214
215
|
|
|
215
216
|
def is_readonly(self, name: str, obj: Any = None) -> bool:
|
|
216
|
-
"""The question every call site actually asks."""
|
|
217
217
|
return name in self.get_readonly_fields(obj)
|
|
218
218
|
|
|
219
219
|
def get_fieldsets(self) -> list[Fieldset]:
|
|
@@ -275,14 +275,14 @@ class ModelAdmin:
|
|
|
275
275
|
permission="delete",
|
|
276
276
|
where="list",
|
|
277
277
|
)
|
|
278
|
-
def delete_selected(self, objects: Sequence[Any], principal: Principal | None) -> str | None:
|
|
278
|
+
async def delete_selected(self, objects: Sequence[Any], principal: Principal | None) -> str | None:
|
|
279
279
|
"""The bulk delete every admin gets for free.
|
|
280
280
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
281
|
+
This is the single most common action anyone would otherwise write
|
|
282
|
+
by hand. It is expressed entirely in terms of this ModelAdmin's own
|
|
283
|
+
`delete` hook, so it works against whatever storage the application
|
|
284
|
+
has and honours whatever that hook already does (cascades, soft
|
|
285
|
+
deletes, hooks of its own).
|
|
286
286
|
|
|
287
287
|
permission="delete", not the resource's bare "view": the action route
|
|
288
288
|
checks it on top, so a principal who may look at a list but not destroy
|
|
@@ -292,7 +292,7 @@ class ModelAdmin:
|
|
|
292
292
|
deleted = 0
|
|
293
293
|
for obj in objects:
|
|
294
294
|
try:
|
|
295
|
-
self.delete(obj)
|
|
295
|
+
await maybe_await(self.delete(obj))
|
|
296
296
|
except Exception as exc:
|
|
297
297
|
# Stop at the first failure and report how far it got:
|
|
298
298
|
# silently continuing would leave the user unable to
|
|
@@ -336,7 +336,6 @@ class ModelAdmin:
|
|
|
336
336
|
raise NotImplementedError(f"{type(self).__name__} must implement update().")
|
|
337
337
|
|
|
338
338
|
def delete(self, obj: Any) -> None:
|
|
339
|
-
"""Delete an existing record."""
|
|
340
339
|
raise NotImplementedError(f"{type(self).__name__} must implement delete().")
|
|
341
340
|
|
|
342
341
|
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)
|