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.
Files changed (209) hide show
  1. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/PKG-INFO +8 -8
  2. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/README.md +7 -7
  3. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/docs/architecture.md +2 -2
  4. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/docs/python/index.md +3 -2
  5. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/examples/fastapi/models.py +6 -6
  6. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/examples/fastapi/user_admin.py +3 -5
  7. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/examples/fastapi/uv.lock +1 -1
  8. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/core/field.py +0 -5
  9. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/core/inline.py +2 -7
  10. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/core/model_admin.py +14 -18
  11. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/core/query.py +2 -2
  12. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/fastapi/csrf.py +0 -2
  13. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templating.py +3 -3
  14. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/ui.py +43 -37
  15. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/pyproject.toml +1 -1
  16. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/test_action_decorator.py +2 -1
  17. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/test_delete_selected_i18n.py +3 -1
  18. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/test_parity.py +0 -15
  19. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_async_model_admin.py +13 -0
  20. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_async_relations.py +0 -9
  21. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_audit.py +0 -1
  22. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_code_strings.py +1 -1
  23. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_components.py +10 -10
  24. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_errors.py +0 -2
  25. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_filters.py +1 -2
  26. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_parity_list.py +0 -11
  27. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_relations.py +4 -3
  28. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_router.py +1 -1
  29. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_theme.py +2 -1
  30. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/test_layout.py +2 -2
  31. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/test_templating.py +0 -1
  32. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/test_ui.py +11 -14
  33. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/uv.lock +1 -1
  34. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/.github/workflows/notify-docs.yml +0 -0
  35. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/.github/workflows/publish.yml +0 -0
  36. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/.gitignore +0 -0
  37. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/LICENSE +0 -0
  38. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/babel.cfg +0 -0
  39. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/browsertests/README.md +0 -0
  40. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/browsertests/conftest.py +0 -0
  41. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/browsertests/test_deletes.py +0 -0
  42. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/browsertests/test_flows.py +0 -0
  43. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/browsertests/test_i18n.py +0 -0
  44. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/browsertests/test_parity.py +0 -0
  45. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/docs/audit.md +0 -0
  46. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/docs/authentication.md +0 -0
  47. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/docs/components.md +0 -0
  48. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/docs/concepts/architecture.md +0 -0
  49. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/docs/concepts/model-admin.md +0 -0
  50. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/docs/dashboard.md +0 -0
  51. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/docs/deletes.md +0 -0
  52. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/docs/exports.md +0 -0
  53. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/docs/i18n.md +0 -0
  54. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/docs/inlines.md +0 -0
  55. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/docs/lists.md +0 -0
  56. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/docs/model-admin.md +0 -0
  57. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/docs/permissions.md +0 -0
  58. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/docs/routing.md +0 -0
  59. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/docs/templates.md +0 -0
  60. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/examples/README.md +0 -0
  61. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/examples/fastapi/README.md +0 -0
  62. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/examples/fastapi/main.py +0 -0
  63. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/examples/fastapi/organization_admin.py +0 -0
  64. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/examples/fastapi/pages.py +0 -0
  65. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/examples/fastapi/pyproject.toml +0 -0
  66. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/examples/fastapi/role_admin.py +0 -0
  67. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/examples/fastapi/session.py +0 -0
  68. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/examples/fastapi/templates/pages/broadcast.html +0 -0
  69. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/examples/fastapi/tests/test_app.py +0 -0
  70. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/mkdocs.yml +0 -0
  71. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/__init__.py +0 -0
  72. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/core/__init__.py +0 -0
  73. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/core/_async.py +0 -0
  74. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/core/action.py +0 -0
  75. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/core/admin.py +0 -0
  76. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/core/audit.py +0 -0
  77. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/core/auth.py +0 -0
  78. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/core/authorization.py +0 -0
  79. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/core/csrf.py +0 -0
  80. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/core/dashboard.py +0 -0
  81. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/core/delete.py +0 -0
  82. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/core/exporter.py +0 -0
  83. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/core/filter.py +0 -0
  84. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/core/login.py +0 -0
  85. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/core/page.py +0 -0
  86. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/core/pagination.py +0 -0
  87. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/core/relation.py +0 -0
  88. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/core/slug.py +0 -0
  89. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/core/template_context.py +0 -0
  90. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/core/widget.py +0 -0
  91. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/fastapi/__init__.py +0 -0
  92. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/fastapi/audit.py +0 -0
  93. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/fastapi/auth.py +0 -0
  94. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/fastapi/deletes.py +0 -0
  95. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/fastapi/errors.py +0 -0
  96. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/fastapi/handlers.py +0 -0
  97. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/fastapi/inlines.py +0 -0
  98. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/fastapi/locale.py +0 -0
  99. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/fastapi/login.py +0 -0
  100. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/fastapi/pages.py +0 -0
  101. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/fastapi/relations.py +0 -0
  102. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/fastapi/responses.py +0 -0
  103. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/fastapi/router.py +0 -0
  104. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/fastapi/static.py +0 -0
  105. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/i18n/__init__.py +0 -0
  106. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/i18n/context.py +0 -0
  107. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/i18n/negotiation.py +0 -0
  108. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/i18n/setup.py +0 -0
  109. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/i18n/translator.py +0 -0
  110. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/locale/fr/LC_MESSAGES/polyadmin.mo +0 -0
  111. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/locale/fr/LC_MESSAGES/polyadmin.po +0 -0
  112. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/locale/polyadmin.pot +0 -0
  113. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/locale/ru/LC_MESSAGES/polyadmin.mo +0 -0
  114. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/locale/ru/LC_MESSAGES/polyadmin.po +0 -0
  115. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/base.html +0 -0
  116. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/action_confirm_modal.html +0 -0
  117. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/csrf-field.html +0 -0
  118. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/error_fragment.html +0 -0
  119. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/field.html +0 -0
  120. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/form_wrapper.html +0 -0
  121. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/icons.html +0 -0
  122. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/inline.html +0 -0
  123. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/inline_fragment.html +0 -0
  124. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/list_content.html +0 -0
  125. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/lookup_results.html +0 -0
  126. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/search.html +0 -0
  127. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/toasts.html +0 -0
  128. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/ui/breadcrumb.html +0 -0
  129. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/ui/bulk-actions.html +0 -0
  130. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/ui/calendar.html +0 -0
  131. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/ui/combobox.html +0 -0
  132. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/ui/delete-preview.html +0 -0
  133. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/ui/dropdown-menu.html +0 -0
  134. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/ui/field.html +0 -0
  135. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/ui/filter-panel.html +0 -0
  136. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/ui/locale-switcher.html +0 -0
  137. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/ui/multi-select.html +0 -0
  138. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/ui/pagination.html +0 -0
  139. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/ui/radio-group.html +0 -0
  140. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/ui/select.html +0 -0
  141. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/ui/sidebar.html +0 -0
  142. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/ui/slider.html +0 -0
  143. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/ui/switch.html +0 -0
  144. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/ui/table.html +0 -0
  145. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/components/ui/theme-toggle.html +0 -0
  146. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/dashboard.html +0 -0
  147. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/error.html +0 -0
  148. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/login.html +0 -0
  149. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/resource/delete.html +0 -0
  150. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/resource/delete_selected.html +0 -0
  151. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/resource/detail.html +0 -0
  152. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/resource/form.html +0 -0
  153. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/resource/list.html +0 -0
  154. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/theme.html +0 -0
  155. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/widgets/activity.html +0 -0
  156. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/widgets/chart.html +0 -0
  157. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/widgets/donut.html +0 -0
  158. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/widgets/metric.html +0 -0
  159. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/widgets/progress.html +0 -0
  160. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/widgets/stat.html +0 -0
  161. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/widgets/table.html +0 -0
  162. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/widgets/tabs.html +0 -0
  163. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/polyadmin/templates/admin/widgets/timeline.html +0 -0
  164. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/__init__.py +0 -0
  165. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/conftest.py +0 -0
  166. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/__init__.py +0 -0
  167. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/mo.py +0 -0
  168. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/test_action_placement.py +0 -0
  169. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/test_admin.py +0 -0
  170. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/test_async.py +0 -0
  171. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/test_auth.py +0 -0
  172. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/test_authorization.py +0 -0
  173. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/test_csrf.py +0 -0
  174. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/test_dashboard.py +0 -0
  175. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/test_delete.py +0 -0
  176. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/test_exporter.py +0 -0
  177. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/test_field.py +0 -0
  178. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/test_filter.py +0 -0
  179. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/test_i18n.py +0 -0
  180. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/test_inline.py +0 -0
  181. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/test_login.py +0 -0
  182. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/test_model_admin.py +0 -0
  183. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/test_pagination.py +0 -0
  184. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/test_query.py +0 -0
  185. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/test_relation.py +0 -0
  186. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/test_template_context.py +0 -0
  187. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/core/test_widget.py +0 -0
  188. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/__init__.py +0 -0
  189. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_action_placement.py +0 -0
  190. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_actions.py +0 -0
  191. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_auth.py +0 -0
  192. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_csrf.py +0 -0
  193. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_dashboard.py +0 -0
  194. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_delete_preview.py +0 -0
  195. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_delete_selected.py +0 -0
  196. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_delete_selected_preview.py +0 -0
  197. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_export.py +0 -0
  198. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_htmx.py +0 -0
  199. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_inlines.py +0 -0
  200. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_list_querier.py +0 -0
  201. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_listopts.py +0 -0
  202. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_locale.py +0 -0
  203. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_login.py +0 -0
  204. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_pages.py +0 -0
  205. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_pseudo_locale.py +0 -0
  206. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_scroll_area.py +0 -0
  207. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/fastapi/test_switcher.py +0 -0
  208. {polyadmin-0.1.0b1 → polyadmin-0.1.0b2}/tests/test_catalogs.py +0 -0
  209. {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.0b1
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: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
36
36
  [![GitHub Repo stars](https://img.shields.io/github/stars/MagicRodri/polyadmin?style=flat-square)](https://github.com/MagicRodri/polyadmin)
37
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/)
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, Django-admin style
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 Django-admin-style
86
- right-hand filter panel on wider screens, and breadcrumbs as the page
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.0b1"
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: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
6
6
  [![GitHub Repo stars](https://img.shields.io/github/stars/MagicRodri/polyadmin?style=flat-square)](https://github.com/MagicRodri/polyadmin)
7
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/)
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, Django-admin style
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 Django-admin-style
56
- right-hand filter panel on wider screens, and breadcrumbs as the page
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.0b1"
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 Django-admin-style admin framework for
4
- [FastAPI](https://fastapi.tiangolo.com). It owns presentation —
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 / FastAPI
1
+ # Python
2
2
 
3
- The Python implementation mounts PolyAdmin into a FastAPI application.
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
- Modelled after the permissions list Django's admin is known for, and
59
- what the searchable multi-select on the user form is there to make
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 shadcn Select port). Every other choice-shaped field here is
94
- # a relation, which renders one of the two combobox widgets
95
- # instead -- without this, ui/select appeared nowhere in the app.
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 is the hook Django calls a
36
- SimpleListFilter -- the admin supplies both the options and the
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, which
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
@@ -339,7 +339,7 @@ wheels = [
339
339
 
340
340
  [[package]]
341
341
  name = "polyadmin"
342
- version = "0.1.0"
342
+ version = "0.1.0b2"
343
343
  source = { editable = "../../" }
344
344
  dependencies = [
345
345
  { name = "jinja2" },
@@ -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, Django-admin TabularInline/StackedInline
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, for a Django-familiar spelling.
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 -- Django's `fieldsets`. A title of None
25
- renders the group with no header, which is how the undeclared default
26
- renders as a plain flat form. `collapsed` only seeds the initial state; the
27
- group can always be opened.
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, as Django's is.
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
- Django ships the same one, and it is the single most common action
282
- anyone would otherwise write by hand. It is expressed entirely in terms
283
- of this ModelAdmin's own `delete` hook, so it works against whatever
284
- storage the application has and honours whatever that hook already
285
- does (cascades, soft deletes, hooks of its own).
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, as Django does; an empty one links nothing and
94
- leaves the row menu as the way in."""
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
- (fiber/render_helpers.go), which uses strconv.FormatFloat(f, 'f', -1,
102
- 64). Anything else (an int) falls back to plain str().
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
- """shadcn/ui's variant lookups, resolved server-side.
1
+ """The reference design system's variant lookups, resolved server-side.
2
2
 
3
- `UI_REGISTRY` resolves shadcn's class-variance-authority (cva) lookups
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 shadcn does:
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 shadcn has both a variant and a size named
21
- "default"; the prefix also tells the resolver which axis was given.
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
- # shadcn's six stock button variants, verbatim in intent.
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
- # shadcn ships no destructive counterpart to `outline`. A
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 shadcn's Radix-backed SelectTrigger: a
85
- # custom listbox does not post its value with a plain form submit,
86
- # and the admin's forms have to keep working without JS. Styled to
87
- # match the shadcn trigger (same height, border, ring).
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
- # shadcn's login-04 block. Its social buttons, "Sign up" and "Forgot
158
- # your password?" are deliberately absent: the framework has no route
159
- # behind any of them. login-04 fills the right column with a
160
- # photograph, which a framework cannot ship, so "aside" styles a
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 shadcn/ui ships. Its geometry is kept as it
293
- # ships: a 356px column in the viewport's corner, 14px between toasts,
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
- # shadcn's sidebar-07 block, at its exact widths (16rem open, 3rem
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 shadcn's Tasks example:
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
- # shadcn fixes this at w-[100px]; a translated "Page 1 of 8"
511
- # is wider than that in most languages, so the width is a
512
- # floor and the label never wraps to a second line.
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 shadcn/ui's own TableRow: a
539
- # checked row-checkbox (list_content.html) tints the whole
540
- # row, the same as a hover, so a selection reads at a glance
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. shadcn calls this
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: shadcn's Combobox/Command idiom over the
599
- # relation's options, selection shown as removable chips. Django's
600
- # filter_horizontal without the two-pane layout, which needs width
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 (Django's `fieldsets`): a titled, collapsible group
671
- # of fields inside the record form. A group with no title renders
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,6 +1,6 @@
1
1
  [project]
2
2
  name = "polyadmin"
3
- version = "0.1.0b1"
3
+ version = "0.1.0b2"
4
4
  description = "PolyAdmin: cross-language server-rendered admin framework (Python core)"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -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)