polyadmin 0.1.0b1__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/.github/workflows/notify-docs.yml +21 -0
- polyadmin-0.1.0b1/.github/workflows/publish.yml +32 -0
- polyadmin-0.1.0b1/.gitignore +17 -0
- polyadmin-0.1.0b1/LICENSE +21 -0
- polyadmin-0.1.0b1/PKG-INFO +239 -0
- polyadmin-0.1.0b1/README.md +209 -0
- polyadmin-0.1.0b1/babel.cfg +4 -0
- polyadmin-0.1.0b1/browsertests/README.md +31 -0
- polyadmin-0.1.0b1/browsertests/conftest.py +126 -0
- polyadmin-0.1.0b1/browsertests/test_deletes.py +61 -0
- polyadmin-0.1.0b1/browsertests/test_flows.py +419 -0
- polyadmin-0.1.0b1/browsertests/test_i18n.py +199 -0
- polyadmin-0.1.0b1/browsertests/test_parity.py +90 -0
- polyadmin-0.1.0b1/docs/architecture.md +104 -0
- polyadmin-0.1.0b1/docs/audit.md +60 -0
- polyadmin-0.1.0b1/docs/authentication.md +224 -0
- polyadmin-0.1.0b1/docs/components.md +219 -0
- polyadmin-0.1.0b1/docs/concepts/architecture.md +28 -0
- polyadmin-0.1.0b1/docs/concepts/model-admin.md +21 -0
- polyadmin-0.1.0b1/docs/dashboard.md +148 -0
- polyadmin-0.1.0b1/docs/deletes.md +188 -0
- polyadmin-0.1.0b1/docs/exports.md +74 -0
- polyadmin-0.1.0b1/docs/i18n.md +327 -0
- polyadmin-0.1.0b1/docs/inlines.md +124 -0
- polyadmin-0.1.0b1/docs/lists.md +238 -0
- polyadmin-0.1.0b1/docs/model-admin.md +491 -0
- polyadmin-0.1.0b1/docs/permissions.md +124 -0
- polyadmin-0.1.0b1/docs/python/index.md +18 -0
- polyadmin-0.1.0b1/docs/routing.md +181 -0
- polyadmin-0.1.0b1/docs/templates.md +174 -0
- polyadmin-0.1.0b1/examples/README.md +8 -0
- polyadmin-0.1.0b1/examples/fastapi/README.md +55 -0
- polyadmin-0.1.0b1/examples/fastapi/main.py +140 -0
- polyadmin-0.1.0b1/examples/fastapi/models.py +219 -0
- polyadmin-0.1.0b1/examples/fastapi/organization_admin.py +131 -0
- polyadmin-0.1.0b1/examples/fastapi/pages.py +42 -0
- polyadmin-0.1.0b1/examples/fastapi/pyproject.toml +27 -0
- polyadmin-0.1.0b1/examples/fastapi/role_admin.py +58 -0
- polyadmin-0.1.0b1/examples/fastapi/session.py +214 -0
- polyadmin-0.1.0b1/examples/fastapi/templates/pages/broadcast.html +52 -0
- polyadmin-0.1.0b1/examples/fastapi/tests/test_app.py +314 -0
- polyadmin-0.1.0b1/examples/fastapi/user_admin.py +200 -0
- polyadmin-0.1.0b1/examples/fastapi/uv.lock +1154 -0
- polyadmin-0.1.0b1/mkdocs.yml +72 -0
- polyadmin-0.1.0b1/polyadmin/__init__.py +47 -0
- polyadmin-0.1.0b1/polyadmin/core/__init__.py +0 -0
- polyadmin-0.1.0b1/polyadmin/core/_async.py +21 -0
- polyadmin-0.1.0b1/polyadmin/core/action.py +135 -0
- polyadmin-0.1.0b1/polyadmin/core/admin.py +129 -0
- polyadmin-0.1.0b1/polyadmin/core/audit.py +65 -0
- polyadmin-0.1.0b1/polyadmin/core/auth.py +52 -0
- polyadmin-0.1.0b1/polyadmin/core/authorization.py +48 -0
- polyadmin-0.1.0b1/polyadmin/core/csrf.py +70 -0
- polyadmin-0.1.0b1/polyadmin/core/dashboard.py +41 -0
- polyadmin-0.1.0b1/polyadmin/core/delete.py +146 -0
- polyadmin-0.1.0b1/polyadmin/core/exporter.py +136 -0
- polyadmin-0.1.0b1/polyadmin/core/field.py +201 -0
- polyadmin-0.1.0b1/polyadmin/core/filter.py +305 -0
- polyadmin-0.1.0b1/polyadmin/core/inline.py +97 -0
- polyadmin-0.1.0b1/polyadmin/core/login.py +110 -0
- polyadmin-0.1.0b1/polyadmin/core/model_admin.py +349 -0
- polyadmin-0.1.0b1/polyadmin/core/page.py +62 -0
- polyadmin-0.1.0b1/polyadmin/core/pagination.py +70 -0
- polyadmin-0.1.0b1/polyadmin/core/query.py +243 -0
- polyadmin-0.1.0b1/polyadmin/core/relation.py +40 -0
- polyadmin-0.1.0b1/polyadmin/core/slug.py +57 -0
- polyadmin-0.1.0b1/polyadmin/core/template_context.py +679 -0
- polyadmin-0.1.0b1/polyadmin/core/widget.py +280 -0
- polyadmin-0.1.0b1/polyadmin/fastapi/__init__.py +3 -0
- polyadmin-0.1.0b1/polyadmin/fastapi/audit.py +52 -0
- polyadmin-0.1.0b1/polyadmin/fastapi/auth.py +112 -0
- polyadmin-0.1.0b1/polyadmin/fastapi/csrf.py +93 -0
- polyadmin-0.1.0b1/polyadmin/fastapi/deletes.py +76 -0
- polyadmin-0.1.0b1/polyadmin/fastapi/errors.py +93 -0
- polyadmin-0.1.0b1/polyadmin/fastapi/handlers.py +798 -0
- polyadmin-0.1.0b1/polyadmin/fastapi/inlines.py +177 -0
- polyadmin-0.1.0b1/polyadmin/fastapi/locale.py +128 -0
- polyadmin-0.1.0b1/polyadmin/fastapi/login.py +110 -0
- polyadmin-0.1.0b1/polyadmin/fastapi/pages.py +97 -0
- polyadmin-0.1.0b1/polyadmin/fastapi/relations.py +264 -0
- polyadmin-0.1.0b1/polyadmin/fastapi/responses.py +55 -0
- polyadmin-0.1.0b1/polyadmin/fastapi/router.py +174 -0
- polyadmin-0.1.0b1/polyadmin/fastapi/static.py +21 -0
- polyadmin-0.1.0b1/polyadmin/i18n/__init__.py +50 -0
- polyadmin-0.1.0b1/polyadmin/i18n/context.py +54 -0
- polyadmin-0.1.0b1/polyadmin/i18n/negotiation.py +56 -0
- polyadmin-0.1.0b1/polyadmin/i18n/setup.py +81 -0
- polyadmin-0.1.0b1/polyadmin/i18n/translator.py +124 -0
- polyadmin-0.1.0b1/polyadmin/locale/fr/LC_MESSAGES/polyadmin.mo +0 -0
- polyadmin-0.1.0b1/polyadmin/locale/fr/LC_MESSAGES/polyadmin.po +486 -0
- polyadmin-0.1.0b1/polyadmin/locale/polyadmin.pot +485 -0
- polyadmin-0.1.0b1/polyadmin/locale/ru/LC_MESSAGES/polyadmin.mo +0 -0
- polyadmin-0.1.0b1/polyadmin/locale/ru/LC_MESSAGES/polyadmin.po +496 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/base.html +91 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/components/action_confirm_modal.html +86 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/components/csrf-field.html +5 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/components/error_fragment.html +6 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/components/field.html +60 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/components/form_wrapper.html +131 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/components/icons.html +67 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/components/inline.html +251 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/components/inline_fragment.html +2 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/components/list_content.html +54 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/components/lookup_results.html +19 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/components/search.html +19 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/components/toasts.html +151 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/components/ui/breadcrumb.html +30 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/components/ui/bulk-actions.html +69 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/components/ui/calendar.html +175 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/components/ui/combobox.html +82 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/components/ui/delete-preview.html +37 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/components/ui/dropdown-menu.html +71 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/components/ui/field.html +110 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/components/ui/filter-panel.html +155 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/components/ui/locale-switcher.html +30 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/components/ui/multi-select.html +253 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/components/ui/pagination.html +81 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/components/ui/radio-group.html +28 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/components/ui/select.html +165 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/components/ui/sidebar.html +175 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/components/ui/slider.html +22 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/components/ui/switch.html +36 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/components/ui/table.html +221 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/components/ui/theme-toggle.html +33 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/dashboard.html +35 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/error.html +33 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/login.html +94 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/resource/delete.html +29 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/resource/delete_selected.html +49 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/resource/detail.html +78 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/resource/form.html +5 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/resource/list.html +5 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/theme.html +372 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/widgets/activity.html +8 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/widgets/chart.html +15 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/widgets/donut.html +59 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/widgets/metric.html +1 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/widgets/progress.html +7 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/widgets/stat.html +22 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/widgets/table.html +29 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/widgets/tabs.html +34 -0
- polyadmin-0.1.0b1/polyadmin/templates/admin/widgets/timeline.html +21 -0
- polyadmin-0.1.0b1/polyadmin/templating.py +528 -0
- polyadmin-0.1.0b1/polyadmin/ui.py +817 -0
- polyadmin-0.1.0b1/pyproject.toml +64 -0
- polyadmin-0.1.0b1/tests/__init__.py +0 -0
- polyadmin-0.1.0b1/tests/conftest.py +49 -0
- polyadmin-0.1.0b1/tests/core/__init__.py +0 -0
- polyadmin-0.1.0b1/tests/core/mo.py +34 -0
- polyadmin-0.1.0b1/tests/core/test_action_decorator.py +173 -0
- polyadmin-0.1.0b1/tests/core/test_action_placement.py +66 -0
- polyadmin-0.1.0b1/tests/core/test_admin.py +88 -0
- polyadmin-0.1.0b1/tests/core/test_async.py +18 -0
- polyadmin-0.1.0b1/tests/core/test_auth.py +17 -0
- polyadmin-0.1.0b1/tests/core/test_authorization.py +26 -0
- polyadmin-0.1.0b1/tests/core/test_csrf.py +65 -0
- polyadmin-0.1.0b1/tests/core/test_dashboard.py +42 -0
- polyadmin-0.1.0b1/tests/core/test_delete.py +201 -0
- polyadmin-0.1.0b1/tests/core/test_delete_selected_i18n.py +24 -0
- polyadmin-0.1.0b1/tests/core/test_exporter.py +131 -0
- polyadmin-0.1.0b1/tests/core/test_field.py +82 -0
- polyadmin-0.1.0b1/tests/core/test_filter.py +269 -0
- polyadmin-0.1.0b1/tests/core/test_i18n.py +194 -0
- polyadmin-0.1.0b1/tests/core/test_inline.py +185 -0
- polyadmin-0.1.0b1/tests/core/test_login.py +55 -0
- polyadmin-0.1.0b1/tests/core/test_model_admin.py +217 -0
- polyadmin-0.1.0b1/tests/core/test_pagination.py +34 -0
- polyadmin-0.1.0b1/tests/core/test_parity.py +199 -0
- polyadmin-0.1.0b1/tests/core/test_query.py +193 -0
- polyadmin-0.1.0b1/tests/core/test_relation.py +59 -0
- polyadmin-0.1.0b1/tests/core/test_template_context.py +173 -0
- polyadmin-0.1.0b1/tests/core/test_widget.py +108 -0
- polyadmin-0.1.0b1/tests/fastapi/__init__.py +0 -0
- polyadmin-0.1.0b1/tests/fastapi/test_action_placement.py +61 -0
- polyadmin-0.1.0b1/tests/fastapi/test_actions.py +414 -0
- polyadmin-0.1.0b1/tests/fastapi/test_async_model_admin.py +114 -0
- polyadmin-0.1.0b1/tests/fastapi/test_async_relations.py +402 -0
- polyadmin-0.1.0b1/tests/fastapi/test_audit.py +145 -0
- polyadmin-0.1.0b1/tests/fastapi/test_auth.py +234 -0
- polyadmin-0.1.0b1/tests/fastapi/test_code_strings.py +135 -0
- polyadmin-0.1.0b1/tests/fastapi/test_components.py +535 -0
- polyadmin-0.1.0b1/tests/fastapi/test_csrf.py +128 -0
- polyadmin-0.1.0b1/tests/fastapi/test_dashboard.py +212 -0
- polyadmin-0.1.0b1/tests/fastapi/test_delete_preview.py +164 -0
- polyadmin-0.1.0b1/tests/fastapi/test_delete_selected.py +88 -0
- polyadmin-0.1.0b1/tests/fastapi/test_delete_selected_preview.py +100 -0
- polyadmin-0.1.0b1/tests/fastapi/test_errors.py +74 -0
- polyadmin-0.1.0b1/tests/fastapi/test_export.py +94 -0
- polyadmin-0.1.0b1/tests/fastapi/test_filters.py +160 -0
- polyadmin-0.1.0b1/tests/fastapi/test_htmx.py +126 -0
- polyadmin-0.1.0b1/tests/fastapi/test_inlines.py +407 -0
- polyadmin-0.1.0b1/tests/fastapi/test_list_querier.py +124 -0
- polyadmin-0.1.0b1/tests/fastapi/test_listopts.py +143 -0
- polyadmin-0.1.0b1/tests/fastapi/test_locale.py +204 -0
- polyadmin-0.1.0b1/tests/fastapi/test_login.py +353 -0
- polyadmin-0.1.0b1/tests/fastapi/test_pages.py +94 -0
- polyadmin-0.1.0b1/tests/fastapi/test_parity_list.py +300 -0
- polyadmin-0.1.0b1/tests/fastapi/test_pseudo_locale.py +227 -0
- polyadmin-0.1.0b1/tests/fastapi/test_relations.py +459 -0
- polyadmin-0.1.0b1/tests/fastapi/test_router.py +192 -0
- polyadmin-0.1.0b1/tests/fastapi/test_scroll_area.py +128 -0
- polyadmin-0.1.0b1/tests/fastapi/test_switcher.py +105 -0
- polyadmin-0.1.0b1/tests/fastapi/test_theme.py +122 -0
- polyadmin-0.1.0b1/tests/test_catalogs.py +146 -0
- polyadmin-0.1.0b1/tests/test_format.py +109 -0
- polyadmin-0.1.0b1/tests/test_layout.py +90 -0
- polyadmin-0.1.0b1/tests/test_templating.py +180 -0
- polyadmin-0.1.0b1/tests/test_ui.py +213 -0
- polyadmin-0.1.0b1/uv.lock +939 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: Notify documentation site
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
paths:
|
|
7
|
+
- docs/**
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
notify:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- name: Trigger documentation rebuild
|
|
17
|
+
uses: peter-evans/repository-dispatch@v3
|
|
18
|
+
with:
|
|
19
|
+
token: ${{ secrets.DOCS_REPO_TOKEN }}
|
|
20
|
+
repository: MagicRodri/polyadmin-docs
|
|
21
|
+
event-type: source-docs-updated
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: Publish Python package
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
publish:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- name: Check out repository
|
|
17
|
+
uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Set up Python
|
|
20
|
+
uses: actions/setup-python@v5
|
|
21
|
+
with:
|
|
22
|
+
python-version: "3.x"
|
|
23
|
+
|
|
24
|
+
- name: Build package
|
|
25
|
+
run: |
|
|
26
|
+
python -m pip install --upgrade build
|
|
27
|
+
python -m build
|
|
28
|
+
|
|
29
|
+
- name: Publish to PyPI
|
|
30
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
31
|
+
with:
|
|
32
|
+
password: ${{ secrets.PYPI_API_TOKEN }}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MagicRodri
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: polyadmin
|
|
3
|
+
Version: 0.1.0b1
|
|
4
|
+
Summary: PolyAdmin: cross-language server-rendered admin framework (Python core)
|
|
5
|
+
Project-URL: Documentation, https://magicrodri.github.io/polyadmin-docs/
|
|
6
|
+
Project-URL: Source, https://github.com/MagicRodri/polyadmin
|
|
7
|
+
Project-URL: Issues, https://github.com/MagicRodri/polyadmin/issues
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: admin,fastapi,htmx,server-rendered
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Framework :: FastAPI
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: jinja2>=3.1
|
|
24
|
+
Provides-Extra: export-xlsx
|
|
25
|
+
Requires-Dist: openpyxl>=3.1; extra == 'export-xlsx'
|
|
26
|
+
Provides-Extra: fastapi
|
|
27
|
+
Requires-Dist: fastapi>=0.110; extra == 'fastapi'
|
|
28
|
+
Requires-Dist: python-multipart>=0.0.9; extra == 'fastapi'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# PolyAdmin
|
|
32
|
+
|
|
33
|
+
[](https://www.python.org/)
|
|
34
|
+
[](https://github.com/MagicRodri/polyadmin/actions/workflows/publish.yml)
|
|
35
|
+
[](LICENSE)
|
|
36
|
+
[](https://github.com/MagicRodri/polyadmin)
|
|
37
|
+
|
|
38
|
+
A Django-admin-style, server-rendered admin framework for
|
|
39
|
+
[FastAPI](https://fastapi.tiangolo.com). See [`docs/`](docs/) for
|
|
40
|
+
reference documentation, or visit the [PolyAdmin documentation site](https://magicrodri.github.io/polyadmin-docs/)
|
|
41
|
+
for the shared Python and Go guides.
|
|
42
|
+
|
|
43
|
+
## What you get from declaring a `ModelAdmin`
|
|
44
|
+
|
|
45
|
+
Point one at your model (fields, `list_display`, search/filter config,
|
|
46
|
+
permissions) and you get:
|
|
47
|
+
|
|
48
|
+
- Full CRUD with search, filter, sort, and pagination, all swapped in
|
|
49
|
+
via HTMX partials — no full-page reloads
|
|
50
|
+
- A dashboard of pluggable widgets: metric, stat (value + trend),
|
|
51
|
+
progress bar, bar chart, donut/pie breakdown, table, activity feed,
|
|
52
|
+
timeline, and tabs (several widgets in one card)
|
|
53
|
+
- Sidebar grouping — resources sharing a `category` collapse into one
|
|
54
|
+
collapsible accordion section, e.g. everything CRUD-shaped under
|
|
55
|
+
one "Directory" group
|
|
56
|
+
- Custom admin pages (`admin.route`) for functionality that isn't
|
|
57
|
+
resource CRUD — reports, wizards, internal tools — rendered inside
|
|
58
|
+
the same layout, auth, and sidebar grouping as everything else
|
|
59
|
+
- Relation fields rendered as links, or as a searchable shadcn/ui
|
|
60
|
+
Command-style autocomplete backed by a server-side `/lookup` route
|
|
61
|
+
(never dumps the target model's full queryset into the page)
|
|
62
|
+
- Inline related records (`StackedInline`/`TabularInline`) — a
|
|
63
|
+
parent's create/detail/edit pages can show and manage a child's
|
|
64
|
+
records that point back at it, Django-admin style
|
|
65
|
+
- Record and bulk Actions, with a shadcn/ui Dialog confirmation step for
|
|
66
|
+
destructive ones
|
|
67
|
+
- Delete previews: the confirmation page says what else a delete takes
|
|
68
|
+
with it, and protected records block it
|
|
69
|
+
- List niceties: a date drill-down, restricted sorting, chosen link
|
|
70
|
+
columns, and filters that survive the trip to a record and back
|
|
71
|
+
- Authentication/authorization hooks gating every route *and* every
|
|
72
|
+
control the templates render
|
|
73
|
+
- CSV and XLSX export
|
|
74
|
+
- Internationalisation: per-request locale, language switcher, French
|
|
75
|
+
and Russian
|
|
76
|
+
- Toast notifications for every create/update/delete/action
|
|
77
|
+
- Per-resource (and per-widget) template overrides, so an application
|
|
78
|
+
can replace one view's markup without forking the framework
|
|
79
|
+
|
|
80
|
+
Styling is [shadcn/ui](https://ui.shadcn.com), hand-ported to
|
|
81
|
+
Alpine.js + Tailwind — its CSS-variable token system and component
|
|
82
|
+
markup, without React or Radix. That gives the admin **dark mode and
|
|
83
|
+
themability**: every color resolves through a CSS variable, so restyling
|
|
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 Django-admin-style
|
|
86
|
+
right-hand filter panel on wider screens, and breadcrumbs as the page
|
|
87
|
+
title. Tailwind/Alpine/HTMX are all CDN-loaded — no frontend build step.
|
|
88
|
+
See [`docs/components.md`](docs/components.md).
|
|
89
|
+
|
|
90
|
+
## Quickstart
|
|
91
|
+
|
|
92
|
+
The first beta release is available on PyPI:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
pip install "polyadmin[fastapi]==0.1.0b1"
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
For the latest unreleased code, install from Git:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
pip install "polyadmin[fastapi] @ git+https://github.com/MagicRodri/polyadmin.git"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Declare a `ModelAdmin` against your own storage and mount it on a
|
|
105
|
+
FastAPI app:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from dataclasses import dataclass
|
|
109
|
+
|
|
110
|
+
from fastapi import FastAPI
|
|
111
|
+
from polyadmin import BooleanField, EmailField, ModelAdmin
|
|
112
|
+
from polyadmin.core.admin import Admin
|
|
113
|
+
from polyadmin.fastapi.router import create_router
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass
|
|
117
|
+
class User:
|
|
118
|
+
id: int
|
|
119
|
+
email: str
|
|
120
|
+
is_active: bool = True
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
_users: list[User] = []
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class UserAdmin(ModelAdmin):
|
|
127
|
+
model = User
|
|
128
|
+
list_display = ("id", "email", "is_active")
|
|
129
|
+
form_fields = ("email", "is_active")
|
|
130
|
+
search_fields = ("email",)
|
|
131
|
+
fields = (
|
|
132
|
+
EmailField("email", required=True),
|
|
133
|
+
BooleanField("is_active", default=True),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
def get_queryset(self):
|
|
137
|
+
return _users
|
|
138
|
+
|
|
139
|
+
def get_object(self, pk):
|
|
140
|
+
return next((u for u in _users if u.id == int(pk)), None)
|
|
141
|
+
|
|
142
|
+
def create(self, data):
|
|
143
|
+
user = User(id=len(_users) + 1, **data)
|
|
144
|
+
_users.append(user)
|
|
145
|
+
return user
|
|
146
|
+
|
|
147
|
+
def update(self, obj, data):
|
|
148
|
+
obj.email = data["email"]
|
|
149
|
+
obj.is_active = data["is_active"]
|
|
150
|
+
return obj
|
|
151
|
+
|
|
152
|
+
def delete(self, obj):
|
|
153
|
+
_users.remove(obj)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
admin = Admin(model_admins=[UserAdmin()])
|
|
157
|
+
|
|
158
|
+
app = FastAPI()
|
|
159
|
+
app.include_router(create_router(admin, base_path="/admin"), prefix="/admin")
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
uv run uvicorn main:app --reload
|
|
164
|
+
# open http://127.0.0.1:8000/admin
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
That's a full CRUD admin for `User` — search, sort, create, edit,
|
|
168
|
+
delete, CSV/XLSX export, all with zero routes or templates of your
|
|
169
|
+
own. With no `authenticator`/`authorizer` set, every request is
|
|
170
|
+
allowed by default (fine for exploring locally, not for anything
|
|
171
|
+
real) — see [`docs/authentication.md`](docs/authentication.md)
|
|
172
|
+
and [`docs/permissions.md`](docs/permissions.md) before
|
|
173
|
+
deploying. For everything else a `ModelAdmin` supports (relations,
|
|
174
|
+
filters, actions, a dashboard, exports, delete previews), see
|
|
175
|
+
[`docs/model-admin.md`](docs/model-admin.md); for the UI components and
|
|
176
|
+
theming, [`docs/components.md`](docs/components.md); and for the rest,
|
|
177
|
+
[`docs/`](docs/).
|
|
178
|
+
|
|
179
|
+
## Languages
|
|
180
|
+
|
|
181
|
+
French and Russian are **on by default** next to English: a visitor
|
|
182
|
+
whose browser asks for either gets the admin's own text in it, and the
|
|
183
|
+
language switcher appears in the header. Their catalogs are **drafts,
|
|
184
|
+
awaiting review by native speakers** — corrections are welcome. To keep
|
|
185
|
+
an admin English-only, restrict the supported set:
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
admin = Admin(model_admins=[...], locales=["en"])
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
See [`docs/i18n.md`](docs/i18n.md) for the rest: translating your own
|
|
192
|
+
strings, the switcher, and how a request's language is chosen.
|
|
193
|
+
|
|
194
|
+
## Upgrading: CSRF protection
|
|
195
|
+
|
|
196
|
+
Every mutating route now requires a CSRF token, on by default. The
|
|
197
|
+
framework's own pages carry it automatically; a **custom `AdminPage` that
|
|
198
|
+
renders its own `<form>`** must add the hidden field, or its posts will be
|
|
199
|
+
rejected with `403`:
|
|
200
|
+
|
|
201
|
+
```html
|
|
202
|
+
{% from "admin/components/csrf-field.html" import csrf_field %}
|
|
203
|
+
{{ csrf_field(csrf_token) }}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Forms that only submit via `hx-post` need no change. Every admin response
|
|
207
|
+
also sends `X-Frame-Options: DENY`. See
|
|
208
|
+
[`docs/authentication.md`](docs/authentication.md#csrf-protection) for the
|
|
209
|
+
cookie/header/field names, the proxy caveat (`X-Forwarded-Proto`), and how
|
|
210
|
+
to opt out.
|
|
211
|
+
|
|
212
|
+
## Status
|
|
213
|
+
|
|
214
|
+
All 9 implementation phases are done:
|
|
215
|
+
|
|
216
|
+
- **Core** (`polyadmin/core/`): `Admin`, `ModelAdmin`, `Field` (incl. relation
|
|
217
|
+
field types), `Relation`, `Filter`, the list query pipeline (`query.py`),
|
|
218
|
+
`Paginator`, `Authenticator`/`Principal`, `Authorizer`, `Dashboard`/`Widget`,
|
|
219
|
+
`Exporter` (CSV + XLSX).
|
|
220
|
+
- **Rendering** (`polyadmin/templating.py`, `polyadmin/templates/`): Jinja2
|
|
221
|
+
templates with framework/app/resource override resolution and
|
|
222
|
+
`TemplateContext` builders. Tailwind/Alpine/HTMX are CDN-loaded
|
|
223
|
+
— no frontend build step required.
|
|
224
|
+
- **FastAPI adapter** (`polyadmin/fastapi/`): `create_router` mounts full CRUD +
|
|
225
|
+
list/detail/create/edit/delete + relation lookup + CSV/XLSX export routes,
|
|
226
|
+
with HTMX partial-swap for list search/filter/sort/pagination and for
|
|
227
|
+
forms, flash messages surviving a redirect, delete previews (see what a
|
|
228
|
+
delete takes with it; protected records block it), and auth/permission
|
|
229
|
+
enforcement on every route (also gates which controls the templates show).
|
|
230
|
+
|
|
231
|
+
See [`examples/fastapi`](examples/fastapi) for a full runnable
|
|
232
|
+
reference app exercising all of this.
|
|
233
|
+
|
|
234
|
+
## Development
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
uv sync
|
|
238
|
+
uv run pytest # 205 tests
|
|
239
|
+
```
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# PolyAdmin
|
|
2
|
+
|
|
3
|
+
[](https://www.python.org/)
|
|
4
|
+
[](https://github.com/MagicRodri/polyadmin/actions/workflows/publish.yml)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://github.com/MagicRodri/polyadmin)
|
|
7
|
+
|
|
8
|
+
A Django-admin-style, server-rendered admin framework for
|
|
9
|
+
[FastAPI](https://fastapi.tiangolo.com). See [`docs/`](docs/) for
|
|
10
|
+
reference documentation, or visit the [PolyAdmin documentation site](https://magicrodri.github.io/polyadmin-docs/)
|
|
11
|
+
for the shared Python and Go guides.
|
|
12
|
+
|
|
13
|
+
## What you get from declaring a `ModelAdmin`
|
|
14
|
+
|
|
15
|
+
Point one at your model (fields, `list_display`, search/filter config,
|
|
16
|
+
permissions) and you get:
|
|
17
|
+
|
|
18
|
+
- Full CRUD with search, filter, sort, and pagination, all swapped in
|
|
19
|
+
via HTMX partials — no full-page reloads
|
|
20
|
+
- A dashboard of pluggable widgets: metric, stat (value + trend),
|
|
21
|
+
progress bar, bar chart, donut/pie breakdown, table, activity feed,
|
|
22
|
+
timeline, and tabs (several widgets in one card)
|
|
23
|
+
- Sidebar grouping — resources sharing a `category` collapse into one
|
|
24
|
+
collapsible accordion section, e.g. everything CRUD-shaped under
|
|
25
|
+
one "Directory" group
|
|
26
|
+
- Custom admin pages (`admin.route`) for functionality that isn't
|
|
27
|
+
resource CRUD — reports, wizards, internal tools — rendered inside
|
|
28
|
+
the same layout, auth, and sidebar grouping as everything else
|
|
29
|
+
- Relation fields rendered as links, or as a searchable shadcn/ui
|
|
30
|
+
Command-style autocomplete backed by a server-side `/lookup` route
|
|
31
|
+
(never dumps the target model's full queryset into the page)
|
|
32
|
+
- Inline related records (`StackedInline`/`TabularInline`) — a
|
|
33
|
+
parent's create/detail/edit pages can show and manage a child's
|
|
34
|
+
records that point back at it, Django-admin style
|
|
35
|
+
- Record and bulk Actions, with a shadcn/ui Dialog confirmation step for
|
|
36
|
+
destructive ones
|
|
37
|
+
- Delete previews: the confirmation page says what else a delete takes
|
|
38
|
+
with it, and protected records block it
|
|
39
|
+
- List niceties: a date drill-down, restricted sorting, chosen link
|
|
40
|
+
columns, and filters that survive the trip to a record and back
|
|
41
|
+
- Authentication/authorization hooks gating every route *and* every
|
|
42
|
+
control the templates render
|
|
43
|
+
- CSV and XLSX export
|
|
44
|
+
- Internationalisation: per-request locale, language switcher, French
|
|
45
|
+
and Russian
|
|
46
|
+
- Toast notifications for every create/update/delete/action
|
|
47
|
+
- Per-resource (and per-widget) template overrides, so an application
|
|
48
|
+
can replace one view's markup without forking the framework
|
|
49
|
+
|
|
50
|
+
Styling is [shadcn/ui](https://ui.shadcn.com), hand-ported to
|
|
51
|
+
Alpine.js + Tailwind — its CSS-variable token system and component
|
|
52
|
+
markup, without React or Radix. That gives the admin **dark mode and
|
|
53
|
+
themability**: every color resolves through a CSS variable, so restyling
|
|
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 Django-admin-style
|
|
56
|
+
right-hand filter panel on wider screens, and breadcrumbs as the page
|
|
57
|
+
title. Tailwind/Alpine/HTMX are all CDN-loaded — no frontend build step.
|
|
58
|
+
See [`docs/components.md`](docs/components.md).
|
|
59
|
+
|
|
60
|
+
## Quickstart
|
|
61
|
+
|
|
62
|
+
The first beta release is available on PyPI:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pip install "polyadmin[fastapi]==0.1.0b1"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
For the latest unreleased code, install from Git:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pip install "polyadmin[fastapi] @ git+https://github.com/MagicRodri/polyadmin.git"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Declare a `ModelAdmin` against your own storage and mount it on a
|
|
75
|
+
FastAPI app:
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from dataclasses import dataclass
|
|
79
|
+
|
|
80
|
+
from fastapi import FastAPI
|
|
81
|
+
from polyadmin import BooleanField, EmailField, ModelAdmin
|
|
82
|
+
from polyadmin.core.admin import Admin
|
|
83
|
+
from polyadmin.fastapi.router import create_router
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class User:
|
|
88
|
+
id: int
|
|
89
|
+
email: str
|
|
90
|
+
is_active: bool = True
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
_users: list[User] = []
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class UserAdmin(ModelAdmin):
|
|
97
|
+
model = User
|
|
98
|
+
list_display = ("id", "email", "is_active")
|
|
99
|
+
form_fields = ("email", "is_active")
|
|
100
|
+
search_fields = ("email",)
|
|
101
|
+
fields = (
|
|
102
|
+
EmailField("email", required=True),
|
|
103
|
+
BooleanField("is_active", default=True),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def get_queryset(self):
|
|
107
|
+
return _users
|
|
108
|
+
|
|
109
|
+
def get_object(self, pk):
|
|
110
|
+
return next((u for u in _users if u.id == int(pk)), None)
|
|
111
|
+
|
|
112
|
+
def create(self, data):
|
|
113
|
+
user = User(id=len(_users) + 1, **data)
|
|
114
|
+
_users.append(user)
|
|
115
|
+
return user
|
|
116
|
+
|
|
117
|
+
def update(self, obj, data):
|
|
118
|
+
obj.email = data["email"]
|
|
119
|
+
obj.is_active = data["is_active"]
|
|
120
|
+
return obj
|
|
121
|
+
|
|
122
|
+
def delete(self, obj):
|
|
123
|
+
_users.remove(obj)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
admin = Admin(model_admins=[UserAdmin()])
|
|
127
|
+
|
|
128
|
+
app = FastAPI()
|
|
129
|
+
app.include_router(create_router(admin, base_path="/admin"), prefix="/admin")
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
uv run uvicorn main:app --reload
|
|
134
|
+
# open http://127.0.0.1:8000/admin
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
That's a full CRUD admin for `User` — search, sort, create, edit,
|
|
138
|
+
delete, CSV/XLSX export, all with zero routes or templates of your
|
|
139
|
+
own. With no `authenticator`/`authorizer` set, every request is
|
|
140
|
+
allowed by default (fine for exploring locally, not for anything
|
|
141
|
+
real) — see [`docs/authentication.md`](docs/authentication.md)
|
|
142
|
+
and [`docs/permissions.md`](docs/permissions.md) before
|
|
143
|
+
deploying. For everything else a `ModelAdmin` supports (relations,
|
|
144
|
+
filters, actions, a dashboard, exports, delete previews), see
|
|
145
|
+
[`docs/model-admin.md`](docs/model-admin.md); for the UI components and
|
|
146
|
+
theming, [`docs/components.md`](docs/components.md); and for the rest,
|
|
147
|
+
[`docs/`](docs/).
|
|
148
|
+
|
|
149
|
+
## Languages
|
|
150
|
+
|
|
151
|
+
French and Russian are **on by default** next to English: a visitor
|
|
152
|
+
whose browser asks for either gets the admin's own text in it, and the
|
|
153
|
+
language switcher appears in the header. Their catalogs are **drafts,
|
|
154
|
+
awaiting review by native speakers** — corrections are welcome. To keep
|
|
155
|
+
an admin English-only, restrict the supported set:
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
admin = Admin(model_admins=[...], locales=["en"])
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
See [`docs/i18n.md`](docs/i18n.md) for the rest: translating your own
|
|
162
|
+
strings, the switcher, and how a request's language is chosen.
|
|
163
|
+
|
|
164
|
+
## Upgrading: CSRF protection
|
|
165
|
+
|
|
166
|
+
Every mutating route now requires a CSRF token, on by default. The
|
|
167
|
+
framework's own pages carry it automatically; a **custom `AdminPage` that
|
|
168
|
+
renders its own `<form>`** must add the hidden field, or its posts will be
|
|
169
|
+
rejected with `403`:
|
|
170
|
+
|
|
171
|
+
```html
|
|
172
|
+
{% from "admin/components/csrf-field.html" import csrf_field %}
|
|
173
|
+
{{ csrf_field(csrf_token) }}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Forms that only submit via `hx-post` need no change. Every admin response
|
|
177
|
+
also sends `X-Frame-Options: DENY`. See
|
|
178
|
+
[`docs/authentication.md`](docs/authentication.md#csrf-protection) for the
|
|
179
|
+
cookie/header/field names, the proxy caveat (`X-Forwarded-Proto`), and how
|
|
180
|
+
to opt out.
|
|
181
|
+
|
|
182
|
+
## Status
|
|
183
|
+
|
|
184
|
+
All 9 implementation phases are done:
|
|
185
|
+
|
|
186
|
+
- **Core** (`polyadmin/core/`): `Admin`, `ModelAdmin`, `Field` (incl. relation
|
|
187
|
+
field types), `Relation`, `Filter`, the list query pipeline (`query.py`),
|
|
188
|
+
`Paginator`, `Authenticator`/`Principal`, `Authorizer`, `Dashboard`/`Widget`,
|
|
189
|
+
`Exporter` (CSV + XLSX).
|
|
190
|
+
- **Rendering** (`polyadmin/templating.py`, `polyadmin/templates/`): Jinja2
|
|
191
|
+
templates with framework/app/resource override resolution and
|
|
192
|
+
`TemplateContext` builders. Tailwind/Alpine/HTMX are CDN-loaded
|
|
193
|
+
— no frontend build step required.
|
|
194
|
+
- **FastAPI adapter** (`polyadmin/fastapi/`): `create_router` mounts full CRUD +
|
|
195
|
+
list/detail/create/edit/delete + relation lookup + CSV/XLSX export routes,
|
|
196
|
+
with HTMX partial-swap for list search/filter/sort/pagination and for
|
|
197
|
+
forms, flash messages surviving a redirect, delete previews (see what a
|
|
198
|
+
delete takes with it; protected records block it), and auth/permission
|
|
199
|
+
enforcement on every route (also gates which controls the templates show).
|
|
200
|
+
|
|
201
|
+
See [`examples/fastapi`](examples/fastapi) for a full runnable
|
|
202
|
+
reference app exercising all of this.
|
|
203
|
+
|
|
204
|
+
## Development
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
uv sync
|
|
208
|
+
uv run pytest # 205 tests
|
|
209
|
+
```
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# browsertests
|
|
2
|
+
|
|
3
|
+
Playwright coverage for the flows a unit test cannot see: the login
|
|
4
|
+
gate, the five mutating flows, the htmx-driven list, the error pages,
|
|
5
|
+
and the layout facts only a browser can measure.
|
|
6
|
+
|
|
7
|
+
## Running
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install playwright pytest-playwright
|
|
11
|
+
playwright install chromium
|
|
12
|
+
pytest browsertests
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The suite starts the reference app itself on port 3100 (override with
|
|
16
|
+
`POLYADMIN_TEST_PORT`) and stops it afterwards, so nothing needs to be
|
|
17
|
+
running first.
|
|
18
|
+
|
|
19
|
+
## The rule these tests follow
|
|
20
|
+
|
|
21
|
+
Every assertion checks state the server actually holds, or a style the
|
|
22
|
+
browser actually resolved. Two failures are the reason:
|
|
23
|
+
|
|
24
|
+
- throwaway scripts that asserted the absence of an error string, and
|
|
25
|
+
passed against a form that had never been submitted;
|
|
26
|
+
- a login page that shipped with a panel the same colour as the page
|
|
27
|
+
behind it, past twenty passing assertions and two unit suites.
|
|
28
|
+
|
|
29
|
+
So after a mutation, reload and count the rows. For layout, read
|
|
30
|
+
`getComputedStyle` or a bounding box and compare — never assert on a
|
|
31
|
+
class name.
|