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.
Files changed (212) hide show
  1. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/PKG-INFO +10 -8
  2. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/README.md +9 -7
  3. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/docs/architecture.md +2 -2
  4. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/docs/deletes.md +4 -0
  5. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/docs/python/index.md +3 -2
  6. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/examples/fastapi/models.py +8 -9
  7. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/examples/fastapi/session.py +11 -5
  8. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/examples/fastapi/tests/test_app.py +1 -1
  9. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/examples/fastapi/user_admin.py +3 -5
  10. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/examples/fastapi/uv.lock +1 -1
  11. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/__init__.py +2 -0
  12. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/core/admin.py +2 -0
  13. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/core/delete.py +8 -4
  14. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/core/field.py +14 -10
  15. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/core/filter.py +0 -1
  16. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/core/inline.py +2 -7
  17. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/core/model_admin.py +17 -18
  18. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/core/query.py +2 -2
  19. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/core/template_context.py +254 -86
  20. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/fastapi/csrf.py +0 -2
  21. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/fastapi/deletes.py +2 -2
  22. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/fastapi/handlers.py +5 -5
  23. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/fastapi/login.py +1 -1
  24. polyadmin-0.1.0b3/polyadmin/locale/fr/LC_MESSAGES/polyadmin.mo +0 -0
  25. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/locale/fr/LC_MESSAGES/polyadmin.po +18 -9
  26. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/locale/polyadmin.pot +7 -7
  27. polyadmin-0.1.0b3/polyadmin/locale/ru/LC_MESSAGES/polyadmin.mo +0 -0
  28. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/locale/ru/LC_MESSAGES/polyadmin.po +18 -9
  29. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/base.html +1 -0
  30. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/field.html +12 -0
  31. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/icons.html +1 -0
  32. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/inline.html +18 -8
  33. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/ui/field.html +1 -0
  34. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/login.html +8 -4
  35. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/theme.html +5 -0
  36. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templating.py +3 -3
  37. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/ui.py +64 -84
  38. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/pyproject.toml +1 -1
  39. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/test_action_decorator.py +2 -1
  40. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/test_delete.py +12 -11
  41. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/test_delete_selected_i18n.py +3 -1
  42. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/test_i18n.py +2 -3
  43. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/test_parity.py +0 -15
  44. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/test_template_context.py +24 -0
  45. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_actions.py +1 -1
  46. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_async_model_admin.py +13 -0
  47. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_async_relations.py +0 -9
  48. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_audit.py +0 -1
  49. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_code_strings.py +1 -1
  50. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_components.py +10 -14
  51. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_delete_selected.py +0 -1
  52. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_errors.py +0 -2
  53. polyadmin-0.1.0b3/tests/fastapi/test_favicon.py +35 -0
  54. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_filters.py +1 -2
  55. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_login.py +17 -2
  56. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_parity_list.py +0 -11
  57. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_relations.py +4 -3
  58. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_router.py +2 -2
  59. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_scroll_area.py +60 -1
  60. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_switcher.py +2 -2
  61. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_theme.py +2 -1
  62. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/test_format.py +48 -1
  63. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/test_layout.py +2 -2
  64. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/test_templating.py +4 -5
  65. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/test_ui.py +11 -14
  66. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/uv.lock +1 -1
  67. polyadmin-0.1.0b1/polyadmin/locale/fr/LC_MESSAGES/polyadmin.mo +0 -0
  68. polyadmin-0.1.0b1/polyadmin/locale/ru/LC_MESSAGES/polyadmin.mo +0 -0
  69. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/.github/workflows/notify-docs.yml +0 -0
  70. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/.github/workflows/publish.yml +0 -0
  71. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/.gitignore +0 -0
  72. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/LICENSE +0 -0
  73. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/babel.cfg +0 -0
  74. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/browsertests/README.md +0 -0
  75. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/browsertests/conftest.py +0 -0
  76. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/browsertests/test_deletes.py +0 -0
  77. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/browsertests/test_flows.py +0 -0
  78. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/browsertests/test_i18n.py +0 -0
  79. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/browsertests/test_parity.py +0 -0
  80. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/docs/audit.md +0 -0
  81. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/docs/authentication.md +0 -0
  82. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/docs/components.md +0 -0
  83. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/docs/concepts/architecture.md +0 -0
  84. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/docs/concepts/model-admin.md +0 -0
  85. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/docs/dashboard.md +0 -0
  86. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/docs/exports.md +0 -0
  87. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/docs/i18n.md +0 -0
  88. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/docs/inlines.md +0 -0
  89. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/docs/lists.md +0 -0
  90. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/docs/model-admin.md +0 -0
  91. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/docs/permissions.md +0 -0
  92. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/docs/routing.md +0 -0
  93. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/docs/templates.md +0 -0
  94. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/examples/README.md +0 -0
  95. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/examples/fastapi/README.md +0 -0
  96. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/examples/fastapi/main.py +0 -0
  97. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/examples/fastapi/organization_admin.py +0 -0
  98. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/examples/fastapi/pages.py +0 -0
  99. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/examples/fastapi/pyproject.toml +0 -0
  100. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/examples/fastapi/role_admin.py +0 -0
  101. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/examples/fastapi/templates/pages/broadcast.html +0 -0
  102. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/mkdocs.yml +0 -0
  103. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/core/__init__.py +0 -0
  104. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/core/_async.py +0 -0
  105. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/core/action.py +0 -0
  106. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/core/audit.py +0 -0
  107. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/core/auth.py +0 -0
  108. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/core/authorization.py +0 -0
  109. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/core/csrf.py +0 -0
  110. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/core/dashboard.py +0 -0
  111. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/core/exporter.py +0 -0
  112. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/core/login.py +0 -0
  113. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/core/page.py +0 -0
  114. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/core/pagination.py +0 -0
  115. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/core/relation.py +0 -0
  116. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/core/slug.py +0 -0
  117. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/core/widget.py +0 -0
  118. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/fastapi/__init__.py +0 -0
  119. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/fastapi/audit.py +0 -0
  120. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/fastapi/auth.py +0 -0
  121. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/fastapi/errors.py +0 -0
  122. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/fastapi/inlines.py +0 -0
  123. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/fastapi/locale.py +0 -0
  124. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/fastapi/pages.py +0 -0
  125. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/fastapi/relations.py +0 -0
  126. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/fastapi/responses.py +0 -0
  127. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/fastapi/router.py +0 -0
  128. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/fastapi/static.py +0 -0
  129. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/i18n/__init__.py +0 -0
  130. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/i18n/context.py +0 -0
  131. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/i18n/negotiation.py +0 -0
  132. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/i18n/setup.py +0 -0
  133. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/i18n/translator.py +0 -0
  134. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/action_confirm_modal.html +0 -0
  135. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/csrf-field.html +0 -0
  136. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/error_fragment.html +0 -0
  137. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/form_wrapper.html +0 -0
  138. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/inline_fragment.html +0 -0
  139. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/list_content.html +0 -0
  140. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/lookup_results.html +0 -0
  141. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/search.html +0 -0
  142. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/toasts.html +0 -0
  143. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/ui/breadcrumb.html +0 -0
  144. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/ui/bulk-actions.html +0 -0
  145. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/ui/calendar.html +0 -0
  146. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/ui/combobox.html +0 -0
  147. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/ui/delete-preview.html +0 -0
  148. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/ui/dropdown-menu.html +0 -0
  149. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/ui/filter-panel.html +0 -0
  150. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/ui/locale-switcher.html +0 -0
  151. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/ui/multi-select.html +0 -0
  152. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/ui/pagination.html +0 -0
  153. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/ui/radio-group.html +0 -0
  154. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/ui/select.html +0 -0
  155. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/ui/sidebar.html +0 -0
  156. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/ui/slider.html +0 -0
  157. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/ui/switch.html +0 -0
  158. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/ui/table.html +0 -0
  159. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/components/ui/theme-toggle.html +0 -0
  160. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/dashboard.html +0 -0
  161. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/error.html +0 -0
  162. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/resource/delete.html +0 -0
  163. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/resource/delete_selected.html +0 -0
  164. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/resource/detail.html +0 -0
  165. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/resource/form.html +0 -0
  166. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/resource/list.html +0 -0
  167. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/widgets/activity.html +0 -0
  168. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/widgets/chart.html +0 -0
  169. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/widgets/donut.html +0 -0
  170. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/widgets/metric.html +0 -0
  171. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/widgets/progress.html +0 -0
  172. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/widgets/stat.html +0 -0
  173. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/widgets/table.html +0 -0
  174. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/widgets/tabs.html +0 -0
  175. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/polyadmin/templates/admin/widgets/timeline.html +0 -0
  176. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/__init__.py +0 -0
  177. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/conftest.py +0 -0
  178. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/__init__.py +0 -0
  179. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/mo.py +0 -0
  180. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/test_action_placement.py +0 -0
  181. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/test_admin.py +0 -0
  182. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/test_async.py +0 -0
  183. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/test_auth.py +0 -0
  184. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/test_authorization.py +0 -0
  185. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/test_csrf.py +0 -0
  186. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/test_dashboard.py +0 -0
  187. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/test_exporter.py +0 -0
  188. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/test_field.py +0 -0
  189. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/test_filter.py +0 -0
  190. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/test_inline.py +0 -0
  191. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/test_login.py +0 -0
  192. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/test_model_admin.py +0 -0
  193. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/test_pagination.py +0 -0
  194. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/test_query.py +0 -0
  195. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/test_relation.py +0 -0
  196. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/core/test_widget.py +0 -0
  197. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/__init__.py +0 -0
  198. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_action_placement.py +0 -0
  199. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_auth.py +0 -0
  200. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_csrf.py +0 -0
  201. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_dashboard.py +0 -0
  202. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_delete_preview.py +0 -0
  203. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_delete_selected_preview.py +0 -0
  204. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_export.py +0 -0
  205. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_htmx.py +0 -0
  206. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_inlines.py +0 -0
  207. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_list_querier.py +0 -0
  208. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_listopts.py +0 -0
  209. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_locale.py +0 -0
  210. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_pages.py +0 -0
  211. {polyadmin-0.1.0b1 → polyadmin-0.1.0b3}/tests/fastapi/test_pseudo_locale.py +0 -0
  212. {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.0b1
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: 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`
@@ -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, Django-admin style
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 Django-admin-style
86
- right-hand filter panel on wider screens, and breadcrumbs as the page
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.0b1"
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: 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`
@@ -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, Django-admin style
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 Django-admin-style
56
- right-hand filter panel on wider screens, and breadcrumbs as the page
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.0b1"
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 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
@@ -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 / 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)
@@ -163,9 +163,8 @@ def seed(
163
163
  organizations: OrganizationRepository,
164
164
  roles: RoleRepository,
165
165
  ) -> None:
166
- # Founded in the same month for every organization: Task 15's browser
167
- # test checks that this date renders with a French month name under
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
- self._accounts[email] = DemoAccount(
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 (Task 12): a <time datetime="...">
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 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" },
@@ -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. Context-aware rendering (list/detail/form/filter)
5
- is added in Phase 2; this module defines the data-level contract that
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(gettext("%(label)s is required.") % {"label": gettext(self.label)})
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 = self.relation.get_related(obj) if self.relation.get_related is not None else super().get_value(obj)
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, 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] = ()
@@ -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, as Django's is.
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
- 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).
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, 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)