django-unfold 0.48.0__tar.gz → 0.49.0__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (249) hide show
  1. {django_unfold-0.48.0 → django_unfold-0.49.0}/PKG-INFO +1 -1
  2. {django_unfold-0.48.0 → django_unfold-0.49.0}/pyproject.toml +1 -1
  3. django_unfold-0.49.0/src/unfold/admin.py +183 -0
  4. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/dataclasses.py +1 -0
  5. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/decorators.py +14 -1
  6. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/fields.py +6 -5
  7. django_unfold-0.49.0/src/unfold/mixins/__init__.py +4 -0
  8. django_unfold-0.49.0/src/unfold/mixins/action_model_admin.py +329 -0
  9. django_unfold-0.49.0/src/unfold/mixins/base_model_admin.py +110 -0
  10. django_unfold-0.49.0/src/unfold/overrides.py +73 -0
  11. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/sites.py +3 -3
  12. django_unfold-0.49.0/src/unfold/static/unfold/css/styles.css +1 -0
  13. django_unfold-0.49.0/src/unfold/static/unfold/fonts/inter/Inter-Bold.woff2 +0 -0
  14. django_unfold-0.49.0/src/unfold/static/unfold/fonts/inter/Inter-Medium.woff2 +0 -0
  15. django_unfold-0.49.0/src/unfold/static/unfold/fonts/inter/Inter-Regular.woff2 +0 -0
  16. django_unfold-0.49.0/src/unfold/static/unfold/fonts/inter/Inter-SemiBold.woff2 +0 -0
  17. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/admin/submit_line.html +7 -1
  18. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/actions_row.html +14 -4
  19. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/navigation_header.html +6 -3
  20. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/site_dropdown.html +1 -1
  21. django_unfold-0.49.0/src/unfold/templates/unfold/helpers/site_icon.html +31 -0
  22. django_unfold-0.49.0/src/unfold/templates/unfold/helpers/tab_action.html +38 -0
  23. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/typing.py +2 -1
  24. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/widgets.py +2 -0
  25. django_unfold-0.48.0/src/unfold/admin.py +0 -578
  26. django_unfold-0.48.0/src/unfold/static/unfold/css/styles.css +0 -1
  27. django_unfold-0.48.0/src/unfold/static/unfold/fonts/inter/Inter-Bold.woff2 +0 -0
  28. django_unfold-0.48.0/src/unfold/static/unfold/fonts/inter/Inter-Medium.woff2 +0 -0
  29. django_unfold-0.48.0/src/unfold/static/unfold/fonts/inter/Inter-Regular.woff2 +0 -0
  30. django_unfold-0.48.0/src/unfold/static/unfold/fonts/inter/Inter-SemiBold.woff2 +0 -0
  31. django_unfold-0.48.0/src/unfold/templates/unfold/helpers/site_icon.html +0 -29
  32. django_unfold-0.48.0/src/unfold/templates/unfold/helpers/tab_action.html +0 -5
  33. {django_unfold-0.48.0 → django_unfold-0.49.0}/LICENSE.md +0 -0
  34. {django_unfold-0.48.0 → django_unfold-0.49.0}/README.md +0 -0
  35. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/__init__.py +0 -0
  36. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/apps.py +0 -0
  37. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/checks.py +0 -0
  38. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/components.py +0 -0
  39. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/__init__.py +0 -0
  40. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/filters/__init__.py +0 -0
  41. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/filters/admin/__init__.py +0 -0
  42. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/filters/admin/autocomplete_filters.py +0 -0
  43. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/filters/admin/datetime_filters.py +0 -0
  44. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/filters/admin/dropdown_filters.py +0 -0
  45. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/filters/admin/mixins.py +0 -0
  46. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/filters/admin/numeric_filters.py +0 -0
  47. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/filters/admin/text_filters.py +0 -0
  48. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/filters/admin.py +0 -0
  49. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/filters/apps.py +0 -0
  50. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/filters/forms.py +0 -0
  51. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/filters/static/unfold/filters/css/nouislider.min.css +0 -0
  52. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/filters/static/unfold/filters/js/DateTimeShortcuts.js +0 -0
  53. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/filters/static/unfold/filters/js/admin-numeric-filter.js +0 -0
  54. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/filters/static/unfold/filters/js/nouislider.min.js +0 -0
  55. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/filters/static/unfold/filters/js/wNumb.min.js +0 -0
  56. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/filters/templates/unfold/filters/filters_date_range.html +0 -0
  57. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/filters/templates/unfold/filters/filters_datetime_range.html +0 -0
  58. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/filters/templates/unfold/filters/filters_field.html +0 -0
  59. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/filters/templates/unfold/filters/filters_numeric_range.html +0 -0
  60. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/filters/templates/unfold/filters/filters_numeric_single.html +0 -0
  61. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/filters/templates/unfold/filters/filters_numeric_slider.html +0 -0
  62. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/forms/__init__.py +0 -0
  63. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/forms/apps.py +0 -0
  64. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/forms/static/unfold/forms/css/trix.css +0 -0
  65. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/forms/static/unfold/forms/js/trix.config.js +0 -0
  66. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/forms/static/unfold/forms/js/trix.js +0 -0
  67. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/forms/templates/unfold/forms/array.html +0 -0
  68. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/forms/templates/unfold/forms/helpers/toolbar.html +0 -0
  69. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/forms/templates/unfold/forms/wysiwyg.html +0 -0
  70. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/forms/widgets.py +0 -0
  71. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/guardian/__init__.py +0 -0
  72. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/guardian/apps.py +0 -0
  73. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/guardian/templates/admin/guardian/model/change_form.html +0 -0
  74. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/guardian/templates/admin/guardian/model/field.html +0 -0
  75. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/guardian/templates/admin/guardian/model/obj_perms_manage.html +0 -0
  76. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/guardian/templates/admin/guardian/model/obj_perms_manage_group.html +0 -0
  77. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/guardian/templates/admin/guardian/model/obj_perms_manage_user.html +0 -0
  78. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/guardian/templates/unfold/guardian/group_form.html +0 -0
  79. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/guardian/templates/unfold/guardian/user_form.html +0 -0
  80. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/import_export/__init__.py +0 -0
  81. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/import_export/apps.py +0 -0
  82. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/import_export/forms.py +0 -0
  83. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/import_export/templates/admin/import_export/base.html +0 -0
  84. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/import_export/templates/admin/import_export/change_form.html +0 -0
  85. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/import_export/templates/admin/import_export/change_list_export.html +0 -0
  86. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/import_export/templates/admin/import_export/change_list_export_item.html +0 -0
  87. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/import_export/templates/admin/import_export/change_list_import_export.html +0 -0
  88. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/import_export/templates/admin/import_export/change_list_import_item.html +0 -0
  89. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/import_export/templates/admin/import_export/export.html +0 -0
  90. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/import_export/templates/admin/import_export/import.html +0 -0
  91. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/import_export/templates/admin/import_export/import_confirm.html +0 -0
  92. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/import_export/templates/admin/import_export/import_errors.html +0 -0
  93. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/import_export/templates/admin/import_export/import_form.html +0 -0
  94. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/import_export/templates/admin/import_export/import_preview.html +0 -0
  95. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/import_export/templates/admin/import_export/import_validation.html +0 -0
  96. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/import_export/templates/admin/import_export/resource_fields_list.html +0 -0
  97. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/inlines/__init__.py +0 -0
  98. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/inlines/admin.py +0 -0
  99. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/inlines/apps.py +0 -0
  100. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/inlines/checks.py +0 -0
  101. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/inlines/forms.py +0 -0
  102. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/simple_history/__init__.py +0 -0
  103. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/simple_history/apps.py +0 -0
  104. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/simple_history/templates/simple_history/object_history.html +0 -0
  105. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/simple_history/templates/simple_history/object_history_form.html +0 -0
  106. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/simple_history/templates/simple_history/object_history_list.html +0 -0
  107. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/contrib/simple_history/templates/simple_history/submit_line.html +0 -0
  108. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/exceptions.py +0 -0
  109. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/forms.py +0 -0
  110. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/settings.py +0 -0
  111. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/static/admin/js/admin/RelatedObjectLookups.js +0 -0
  112. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/static/admin/js/inlines.js +0 -0
  113. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/static/unfold/css/simplebar.css +0 -0
  114. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/static/unfold/fonts/inter/styles.css +0 -0
  115. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/static/unfold/fonts/material-symbols/Material-Symbols-Outlined.woff2 +0 -0
  116. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/static/unfold/fonts/material-symbols/styles.css +0 -0
  117. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/static/unfold/js/alpine.anchor.js +0 -0
  118. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/static/unfold/js/alpine.js +0 -0
  119. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/static/unfold/js/alpine.persist.js +0 -0
  120. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/static/unfold/js/alpine.resize.js +0 -0
  121. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/static/unfold/js/alpine.sort.js +0 -0
  122. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/static/unfold/js/app.js +0 -0
  123. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/static/unfold/js/chart.js +0 -0
  124. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/static/unfold/js/htmx.js +0 -0
  125. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/static/unfold/js/select2.init.js +0 -0
  126. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/static/unfold/js/simplebar.js +0 -0
  127. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/styles.css +0 -0
  128. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/admin/actions.html +0 -0
  129. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/admin/app_index.html +0 -0
  130. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/admin/app_list.html +0 -0
  131. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/admin/auth/user/add_form.html +0 -0
  132. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/admin/auth/user/change_password.html +0 -0
  133. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/admin/base.html +0 -0
  134. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/admin/base_site.html +0 -0
  135. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/admin/change_form.html +0 -0
  136. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/admin/change_form_object_tools.html +0 -0
  137. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/admin/change_list.html +0 -0
  138. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/admin/change_list_object_tools.html +0 -0
  139. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/admin/change_list_results.html +0 -0
  140. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/admin/date_hierarchy.html +0 -0
  141. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/admin/delete_confirmation.html +0 -0
  142. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/admin/delete_selected_confirmation.html +0 -0
  143. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/admin/edit_inline/stacked.html +0 -0
  144. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/admin/edit_inline/tabular.html +0 -0
  145. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/admin/filter.html +0 -0
  146. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/admin/includes/fieldset.html +0 -0
  147. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/admin/includes/object_delete_summary.html +0 -0
  148. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/admin/index.html +0 -0
  149. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/admin/login.html +0 -0
  150. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/admin/nav_sidebar.html +0 -0
  151. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/admin/object_history.html +0 -0
  152. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/admin/pagination.html +0 -0
  153. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/admin/search_form.html +0 -0
  154. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/auth/widgets/read_only_password_hash.html +0 -0
  155. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/registration/logged_out.html +0 -0
  156. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/registration/password_change_done.html +0 -0
  157. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/registration/password_change_form.html +0 -0
  158. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/components/button.html +0 -0
  159. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/components/card.html +0 -0
  160. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/components/chart/bar.html +0 -0
  161. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/components/chart/cohort.html +0 -0
  162. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/components/chart/line.html +0 -0
  163. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/components/container.html +0 -0
  164. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/components/flex.html +0 -0
  165. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/components/icon.html +0 -0
  166. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/components/navigation.html +0 -0
  167. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/components/progress.html +0 -0
  168. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/components/separator.html +0 -0
  169. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/components/table.html +0 -0
  170. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/components/text.html +0 -0
  171. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/components/title.html +0 -0
  172. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/components/tracker.html +0 -0
  173. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/account_links.html +0 -0
  174. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/add_link.html +0 -0
  175. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/app_list.html +0 -0
  176. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/app_list_default.html +0 -0
  177. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/attrs.html +0 -0
  178. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/boolean.html +0 -0
  179. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/breadcrumb_item.html +0 -0
  180. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/change_list_actions.html +0 -0
  181. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/change_list_filter.html +0 -0
  182. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/change_list_filter_actions.html +0 -0
  183. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/delete_submit_line.html +0 -0
  184. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/display_header.html +0 -0
  185. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/display_label.html +0 -0
  186. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/edit_inline/tabular_delete.html +0 -0
  187. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/edit_inline/tabular_error.html +0 -0
  188. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/edit_inline/tabular_field.html +0 -0
  189. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/edit_inline/tabular_heading.html +0 -0
  190. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/edit_inline/tabular_row.html +0 -0
  191. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/edit_inline/tabular_title.html +0 -0
  192. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/field.html +0 -0
  193. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/field_readonly.html +0 -0
  194. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/field_readonly_value.html +0 -0
  195. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/fieldset_row.html +0 -0
  196. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/fieldset_row_checkbox.html +0 -0
  197. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/fieldset_row_field.html +0 -0
  198. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/fieldsets_tabs.html +0 -0
  199. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/form_errors.html +0 -0
  200. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/form_label.html +0 -0
  201. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/header.html +0 -0
  202. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/header_back_button.html +0 -0
  203. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/help_text.html +0 -0
  204. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/history.html +0 -0
  205. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/label.html +0 -0
  206. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/language_switch.html +0 -0
  207. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/messages/debug.html +0 -0
  208. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/messages/error.html +0 -0
  209. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/messages/errornote.html +0 -0
  210. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/messages/info.html +0 -0
  211. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/messages/success.html +0 -0
  212. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/messages/warning.html +0 -0
  213. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/messages.html +0 -0
  214. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/navigation.html +0 -0
  215. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/pagination_current_item.html +0 -0
  216. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/pagination_ellipsis.html +0 -0
  217. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/search.html +0 -0
  218. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/search_results.html +0 -0
  219. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/site_branding.html +0 -0
  220. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/site_logo.html +0 -0
  221. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/submit.html +0 -0
  222. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/tab_list.html +0 -0
  223. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/theme_switch.html +0 -0
  224. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/userlinks.html +0 -0
  225. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/helpers/welcomemsg.html +0 -0
  226. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/layouts/base.html +0 -0
  227. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/layouts/base_simple.html +0 -0
  228. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/layouts/skeleton.html +0 -0
  229. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/templatetags/preserve_changelist_filters.html +0 -0
  230. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/widgets/clearable_file_input.html +0 -0
  231. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/widgets/clearable_file_input_small.html +0 -0
  232. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/widgets/date.html +0 -0
  233. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/widgets/foreign_key_raw_id.html +0 -0
  234. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/widgets/radio.html +0 -0
  235. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/widgets/radio_option.html +0 -0
  236. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/widgets/range.html +0 -0
  237. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/widgets/related_widget_wrapper.html +0 -0
  238. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/widgets/split_datetime.html +0 -0
  239. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/widgets/split_datetime_vertical.html +0 -0
  240. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/widgets/split_money.html +0 -0
  241. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/widgets/textarea.html +0 -0
  242. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/widgets/textarea_expandable.html +0 -0
  243. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/widgets/time.html +0 -0
  244. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templates/unfold/widgets/url.html +0 -0
  245. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templatetags/__init__.py +0 -0
  246. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templatetags/unfold.py +0 -0
  247. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/templatetags/unfold_list.py +0 -0
  248. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/utils.py +0 -0
  249. {django_unfold-0.48.0 → django_unfold-0.49.0}/src/unfold/views.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: django-unfold
3
- Version: 0.48.0
3
+ Version: 0.49.0
4
4
  Summary: Modern Django admin theme for seamless interface development
5
5
  License: MIT
6
6
  Keywords: django,admin,tailwind,theme
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "django-unfold"
3
- version = "0.48.0"
3
+ version = "0.49.0"
4
4
  description = "Modern Django admin theme for seamless interface development"
5
5
  license = "MIT"
6
6
  readme = "README.md"
@@ -0,0 +1,183 @@
1
+ from functools import update_wrapper
2
+ from typing import Optional
3
+
4
+ from django import forms
5
+ from django.contrib.admin import ModelAdmin as BaseModelAdmin
6
+ from django.contrib.admin import StackedInline as BaseStackedInline
7
+ from django.contrib.admin import TabularInline as BaseTabularInline
8
+ from django.contrib.admin import display, helpers
9
+ from django.db.models import BLANK_CHOICE_DASH, Model
10
+ from django.http import HttpRequest, HttpResponse
11
+ from django.shortcuts import redirect
12
+ from django.urls import URLPattern, path
13
+ from django.utils.safestring import mark_safe
14
+ from django.utils.translation import gettext_lazy as _
15
+ from django.views import View
16
+
17
+ from unfold.checks import UnfoldModelAdminChecks
18
+ from unfold.fields import UnfoldAdminField, UnfoldAdminReadonlyField
19
+ from unfold.forms import ActionForm
20
+ from unfold.mixins import ActionModelAdminMixin, BaseModelAdminMixin
21
+ from unfold.overrides import FORMFIELD_OVERRIDES_INLINE
22
+ from unfold.typing import FieldsetsType
23
+ from unfold.widgets import UnfoldBooleanWidget
24
+
25
+ checkbox = UnfoldBooleanWidget(
26
+ {
27
+ "class": "action-select",
28
+ "aria-label": _("Select record"),
29
+ },
30
+ lambda value: False,
31
+ )
32
+
33
+ helpers.AdminField = UnfoldAdminField
34
+
35
+ helpers.AdminReadonlyField = UnfoldAdminReadonlyField
36
+
37
+
38
+ class ModelAdmin(BaseModelAdminMixin, ActionModelAdminMixin, BaseModelAdmin):
39
+ action_form = ActionForm
40
+ custom_urls = ()
41
+ add_fieldsets = ()
42
+ list_horizontal_scrollbar_top = False
43
+ list_filter_submit = False
44
+ list_filter_sheet = True
45
+ list_fullwidth = False
46
+ list_disable_select_all = False
47
+ list_before_template = None
48
+ list_after_template = None
49
+ change_form_before_template = None
50
+ change_form_after_template = None
51
+ change_form_outer_before_template = None
52
+ change_form_outer_after_template = None
53
+ compressed_fields = False
54
+ readonly_preprocess_fields = {}
55
+ warn_unsaved_form = False
56
+ checks_class = UnfoldModelAdminChecks
57
+
58
+ @property
59
+ def media(self):
60
+ media = super().media
61
+ additional_media = forms.Media()
62
+
63
+ for filter in self.list_filter:
64
+ if (
65
+ isinstance(filter, (tuple, list))
66
+ and hasattr(filter[1], "form_class")
67
+ and hasattr(filter[1].form_class, "Media")
68
+ ):
69
+ additional_media += forms.Media(filter[1].form_class.Media)
70
+ elif hasattr(filter, "form_class") and hasattr(filter.form_class, "Media"):
71
+ additional_media += forms.Media(filter.form_class.Media)
72
+
73
+ return media + additional_media
74
+
75
+ def get_fieldsets(self, request: HttpRequest, obj=None) -> FieldsetsType:
76
+ if not obj and self.add_fieldsets:
77
+ return self.add_fieldsets
78
+ return super().get_fieldsets(request, obj)
79
+
80
+ def get_custom_urls(self) -> tuple[tuple[str, str, View], ...]:
81
+ """
82
+ Method to get custom views for ModelAdmin with their urls
83
+
84
+ Format of custom_urls item:
85
+ ("path_to_view", "name_of_view", view_itself)
86
+ """
87
+ return () if self.custom_urls is None else self.custom_urls
88
+
89
+ def get_urls(self) -> list[URLPattern]:
90
+ urls = super().get_urls()
91
+
92
+ def wrap(view):
93
+ def wrapper(*args, **kwargs):
94
+ return self.admin_site.admin_view(view)(*args, **kwargs)
95
+
96
+ wrapper.model_admin = self
97
+ return update_wrapper(wrapper, view)
98
+
99
+ custom_urls = [
100
+ self._path_from_custom_url(custom_url)
101
+ for custom_url in self.get_custom_urls()
102
+ ]
103
+
104
+ actions_list_urls = [
105
+ path(
106
+ action.path,
107
+ wrap(action.method),
108
+ name=action.action_name,
109
+ )
110
+ for action in self._get_base_actions_list()
111
+ ]
112
+
113
+ action_detail_urls = [
114
+ path(
115
+ f"<path:object_id>/{action.path}/",
116
+ wrap(action.method),
117
+ name=action.action_name,
118
+ )
119
+ for action in self._get_base_actions_detail()
120
+ ]
121
+
122
+ action_row_urls = [
123
+ path(
124
+ f"<path:object_id>/{action.path}",
125
+ wrap(action.method),
126
+ name=action.action_name,
127
+ )
128
+ for action in self._get_base_actions_row()
129
+ ]
130
+
131
+ return (
132
+ custom_urls
133
+ + action_row_urls
134
+ + actions_list_urls
135
+ + action_detail_urls
136
+ + urls
137
+ )
138
+
139
+ def _path_from_custom_url(self, custom_url) -> URLPattern:
140
+ return path(
141
+ custom_url[0],
142
+ self.admin_site.admin_view(custom_url[2]),
143
+ {"model_admin": self},
144
+ name=custom_url[1],
145
+ )
146
+
147
+ def get_action_choices(
148
+ self, request: HttpRequest, default_choices=BLANK_CHOICE_DASH
149
+ ):
150
+ default_choices = [("", _("Select action"))]
151
+ return super().get_action_choices(request, default_choices)
152
+
153
+ @display(description=mark_safe(checkbox.render("action_toggle_all", 1)))
154
+ def action_checkbox(self, obj: Model):
155
+ return checkbox.render(helpers.ACTION_CHECKBOX_NAME, str(obj.pk))
156
+
157
+ def response_change(self, request: HttpRequest, obj: Model) -> HttpResponse:
158
+ res = super().response_change(request, obj)
159
+ if "next" in request.GET:
160
+ return redirect(request.GET["next"])
161
+ return res
162
+
163
+ def response_add(
164
+ self, request: HttpRequest, obj: Model, post_url_continue: Optional[str] = None
165
+ ) -> HttpResponse:
166
+ res = super().response_add(request, obj, post_url_continue)
167
+ if "next" in request.GET:
168
+ return redirect(request.GET["next"])
169
+ return res
170
+
171
+
172
+ class TabularInline(BaseModelAdminMixin, BaseTabularInline):
173
+ formfield_overrides = FORMFIELD_OVERRIDES_INLINE
174
+ readonly_preprocess_fields = {}
175
+ ordering_field = None
176
+ hide_ordering_field = False
177
+
178
+
179
+ class StackedInline(BaseModelAdminMixin, BaseStackedInline):
180
+ formfield_overrides = FORMFIELD_OVERRIDES_INLINE
181
+ readonly_preprocess_fields = {}
182
+ ordering_field = None
183
+ hide_ordering_field = False
@@ -12,6 +12,7 @@ class UnfoldAction:
12
12
  path: str
13
13
  attrs: Optional[dict] = None
14
14
  object_id: Optional[Union[int, str]] = None
15
+ icon: Optional[str] = None
15
16
 
16
17
 
17
18
  @dataclass
@@ -17,6 +17,7 @@ def action(
17
17
  description: Optional[str] = None,
18
18
  url_path: Optional[str] = None,
19
19
  attrs: Optional[dict[str, Any]] = None,
20
+ icon: Optional[str] = None,
20
21
  ) -> ActionFunction:
21
22
  def decorator(func: Callable) -> ActionFunction:
22
23
  def inner(
@@ -33,8 +34,14 @@ def action(
33
34
  # Permissions methods have following syntax: has_<some>_permission(self, request, obj=None):
34
35
  # But obj is not examined by default in django admin and it would also require additional
35
36
  # fetch from database, therefore it is not supported yet
36
- if not any(
37
+ has_object_argument = (
38
+ func.__name__ in model_admin.actions_detail
39
+ or func.__name__ in model_admin.actions_submit_line
40
+ )
41
+ if not all(
37
42
  has_permission(request, kwargs.get("object_id"))
43
+ if has_object_argument
44
+ else has_permission(request)
38
45
  for has_permission in permission_checks
39
46
  ):
40
47
  raise PermissionDenied
@@ -42,10 +49,16 @@ def action(
42
49
 
43
50
  if permissions is not None:
44
51
  inner.allowed_permissions = permissions
52
+
45
53
  if description is not None:
46
54
  inner.short_description = description
55
+
47
56
  if url_path is not None:
48
57
  inner.url_path = url_path
58
+
59
+ if icon is not None:
60
+ inner.icon = icon
61
+
49
62
  inner.attrs = attrs or {}
50
63
  return inner
51
64
 
@@ -17,17 +17,18 @@ from django.utils.module_loading import import_string
17
17
  from django.utils.safestring import SafeText, mark_safe
18
18
  from django.utils.text import capfirst
19
19
 
20
- from .settings import get_config
21
- from .utils import display_for_field, prettify_json
22
- from .widgets import CHECKBOX_LABEL_CLASSES, LABEL_CLASSES
20
+ from unfold.mixins import BaseModelAdminMixin
21
+ from unfold.settings import get_config
22
+ from unfold.utils import display_for_field, prettify_json
23
+ from unfold.widgets import CHECKBOX_LABEL_CLASSES, LABEL_CLASSES
23
24
 
24
25
 
25
26
  class UnfoldAdminReadonlyField(helpers.AdminReadonlyField):
26
27
  def label_tag(self) -> SafeText:
27
- from .admin import ModelAdmin, ModelAdminMixin
28
+ from .admin import ModelAdmin
28
29
 
29
30
  if not isinstance(self.model_admin, ModelAdmin) and not isinstance(
30
- self.model_admin, ModelAdminMixin
31
+ self.model_admin, BaseModelAdminMixin
31
32
  ):
32
33
  return super().label_tag()
33
34
 
@@ -0,0 +1,4 @@
1
+ from unfold.mixins.action_model_admin import ActionModelAdminMixin
2
+ from unfold.mixins.base_model_admin import BaseModelAdminMixin
3
+
4
+ __all__ = ["BaseModelAdminMixin", "ActionModelAdminMixin"]
@@ -0,0 +1,329 @@
1
+ from typing import Any, Callable, Optional, Union
2
+
3
+ from django.db.models import Model
4
+ from django.forms import Form
5
+ from django.http import HttpRequest
6
+ from django.template.response import TemplateResponse
7
+ from django.urls import reverse
8
+
9
+ from unfold.dataclasses import UnfoldAction
10
+ from unfold.exceptions import UnfoldException
11
+
12
+
13
+ class ActionModelAdminMixin:
14
+ """
15
+ Adds support for various ModelAdmin actions (list, detail, row, submit line)
16
+ """
17
+
18
+ actions_list = () # Displayed in changelist at the top
19
+ actions_row = () # Displayed in changelist for each row in the table
20
+ actions_detail = () # Displayed in changeform at the top
21
+ actions_submit_line = () # Displayed in changeform in the submit line (form buttons)
22
+
23
+ def changelist_view(
24
+ self, request: HttpRequest, extra_context: Optional[dict[str, str]] = None
25
+ ) -> TemplateResponse:
26
+ """
27
+ Changelist contains `actions_list` and `actions_row` custom actions. In case of `actions_row` they
28
+ are displayed in the each row of the table.
29
+ """
30
+ extra_context = extra_context or {}
31
+
32
+ actions_row = [
33
+ {
34
+ "title": action.description,
35
+ "icon": action.icon,
36
+ "attrs": action.method.attrs,
37
+ # This is just a path name as string and in template is used in {% url %} tag
38
+ # with custom instance pk value
39
+ "raw_path": f"{self.admin_site.name}:{action.action_name}",
40
+ }
41
+ for action in self.get_actions_row(request)
42
+ ]
43
+
44
+ # `actions_list` may contain custom structure with dropdowns so it is needed
45
+ # to use `_get_actions_navigation` to build the final structure for the template
46
+ actions_list = self._get_actions_navigation(
47
+ self.actions_list, self.get_actions_list(request)
48
+ )
49
+
50
+ extra_context.update(
51
+ {
52
+ "actions_list": actions_list,
53
+ "actions_row": actions_row,
54
+ }
55
+ )
56
+
57
+ return super().changelist_view(request, extra_context)
58
+
59
+ def changeform_view(
60
+ self,
61
+ request: HttpRequest,
62
+ object_id: Optional[str] = None,
63
+ form_url: str = "",
64
+ extra_context: Optional[dict[str, bool]] = None,
65
+ ) -> Any:
66
+ """
67
+ Changeform contains `actions_submit_line` and `actions_detail` custom actions.
68
+ """
69
+ extra_context = extra_context or {}
70
+
71
+ # `actions_submit_line` is a list of actions that are displayed in the submit line they
72
+ # are displayed as form buttons
73
+ actions_submit_line = self.get_actions_submit_line(request, object_id)
74
+
75
+ # `actions_detail` may contain custom structure with dropdowns so it is needed
76
+ # to use `_get_actions_navigation` to build the final structure for the template
77
+ actions_detail = self._get_actions_navigation(
78
+ self.actions_detail,
79
+ self.get_actions_detail(request, object_id),
80
+ object_id,
81
+ )
82
+
83
+ extra_context.update(
84
+ {
85
+ "actions_submit_line": actions_submit_line,
86
+ "actions_detail": actions_detail,
87
+ }
88
+ )
89
+
90
+ return super().changeform_view(request, object_id, form_url, extra_context)
91
+
92
+ def save_model(
93
+ self, request: HttpRequest, obj: Model, form: Form, change: Any
94
+ ) -> None:
95
+ """
96
+ When saving object, run all appropriate actions from `actions_submit_line`
97
+ """
98
+ super().save_model(request, obj, form, change)
99
+
100
+ # After saving object, check if any button from `actions_submit_line` was pressed
101
+ # and call the corresponding method
102
+ for action in self.get_actions_submit_line(request, obj.pk):
103
+ if action.action_name not in request.POST:
104
+ continue
105
+
106
+ action.method(request, obj)
107
+
108
+ def get_unfold_action(self, action: str) -> UnfoldAction:
109
+ """
110
+ Converts action name into UnfoldAction object.
111
+ """
112
+ method = self._get_instance_method(action)
113
+
114
+ return UnfoldAction(
115
+ action_name=f"{self.model._meta.app_label}_{self.model._meta.model_name}_{action}",
116
+ method=method,
117
+ description=self._get_action_description(method, action),
118
+ path=getattr(method, "url_path", action),
119
+ attrs=method.attrs if hasattr(method, "attrs") else None,
120
+ icon=method.icon if hasattr(method, "icon") else None,
121
+ )
122
+
123
+ def get_actions_list(self, request: HttpRequest) -> list[UnfoldAction]:
124
+ """
125
+ Filters `actions_list` by permissions and returns list of UnfoldAction objects.
126
+ """
127
+ return self._filter_unfold_actions_by_permissions(
128
+ request, self._get_base_actions_list()
129
+ )
130
+
131
+ def get_actions_detail(
132
+ self, request: HttpRequest, object_id: int
133
+ ) -> list[UnfoldAction]:
134
+ """
135
+ Filters `actions_detail` by permissions and returns list of UnfoldAction objects.
136
+ """
137
+ return self._filter_unfold_actions_by_permissions(
138
+ request, self._get_base_actions_detail(), object_id
139
+ )
140
+
141
+ def get_actions_row(self, request: HttpRequest) -> list[UnfoldAction]:
142
+ """
143
+ Filters `actions_row` by permissions and returns list of UnfoldAction objects.
144
+ """
145
+ return self._filter_unfold_actions_by_permissions(
146
+ request, self._get_base_actions_row()
147
+ )
148
+
149
+ def get_actions_submit_line(
150
+ self, request: HttpRequest, object_id: int
151
+ ) -> list[UnfoldAction]:
152
+ """
153
+ Filters `actions_submit_line` by permissions and returns list of UnfoldAction objects.
154
+ """
155
+ return self._filter_unfold_actions_by_permissions(
156
+ request, self._get_base_actions_submit_line(), object_id
157
+ )
158
+
159
+ def _extract_action_names(self, actions: list[Union[str, dict]]) -> list[str]:
160
+ """
161
+ Gets the list of only actions names from the actions structure provided in ModelAdmin
162
+ """
163
+ results = []
164
+
165
+ for action in actions or []:
166
+ if isinstance(action, dict) and "items" in action:
167
+ results.extend(action["items"])
168
+ else:
169
+ results.append(action)
170
+
171
+ return results
172
+
173
+ def _get_base_actions_list(self) -> list[UnfoldAction]:
174
+ """
175
+ Returns list of UnfoldAction objects for `actions_list`.
176
+ """
177
+ return [
178
+ self.get_unfold_action(action)
179
+ for action in self._extract_action_names(self.actions_list)
180
+ ]
181
+
182
+ def _get_base_actions_detail(self) -> list[UnfoldAction]:
183
+ """
184
+ Returns list of UnfoldAction objects for `actions_detail`.
185
+ """
186
+ return [
187
+ self.get_unfold_action(action)
188
+ for action in self._extract_action_names(self.actions_detail) or []
189
+ ]
190
+
191
+ def _get_base_actions_row(self) -> list[UnfoldAction]:
192
+ """
193
+ Returns list of UnfoldAction objects for `actions_row`.
194
+ """
195
+ return [
196
+ self.get_unfold_action(action)
197
+ for action in self._extract_action_names(self.actions_row) or []
198
+ ]
199
+
200
+ def _get_base_actions_submit_line(self) -> list[UnfoldAction]:
201
+ """
202
+ Returns list of UnfoldAction objects for `actions_submit_line`.
203
+ """
204
+ return [
205
+ self.get_unfold_action(action)
206
+ for action in self._extract_action_names(self.actions_submit_line) or []
207
+ ]
208
+
209
+ def _get_instance_method(self, method_name: str) -> Callable:
210
+ """
211
+ Searches for method on self instance based on method_name and returns it if it exists.
212
+ If it does not exist or is not callable, it raises UnfoldException
213
+ """
214
+ try:
215
+ method = getattr(self, method_name)
216
+ except AttributeError as e:
217
+ raise UnfoldException(
218
+ f"Method {method_name} specified does not exist on current object"
219
+ ) from e
220
+
221
+ if not callable(method):
222
+ raise UnfoldException(f"{method_name} is not callable")
223
+
224
+ return method
225
+
226
+ def _get_actions_navigation(
227
+ self,
228
+ provided_actions: list[Union[str, dict]],
229
+ allowed_actions: list[UnfoldAction],
230
+ object_id: Optional[str] = None,
231
+ ) -> list[Union[str, dict]]:
232
+ """
233
+ Builds navigation structure for the actions which is going to be provided to the template.
234
+ """
235
+ navigation = []
236
+
237
+ def get_action_by_name(name: str) -> UnfoldAction:
238
+ """
239
+ Searches for an action in allowed_actions by its name.
240
+ """
241
+ for action in allowed_actions:
242
+ full_action_name = (
243
+ f"{self.model._meta.app_label}_{self.model._meta.model_name}_{name}"
244
+ )
245
+
246
+ if action.action_name == full_action_name:
247
+ return action
248
+
249
+ def get_action_path(action: UnfoldAction) -> str:
250
+ """
251
+ Returns the URL path for an action.
252
+ """
253
+ path_name = f"{self.admin_site.name}:{action.action_name}"
254
+
255
+ if object_id:
256
+ return reverse(path_name, args=(object_id,))
257
+
258
+ return reverse(path_name)
259
+
260
+ def get_action_attrs(action: UnfoldAction) -> dict:
261
+ """
262
+ Returns the attributes for an action which will be used in the template.
263
+ """
264
+ return {
265
+ "title": action.description,
266
+ "icon": action.icon,
267
+ "attrs": action.method.attrs,
268
+ "path": get_action_path(action),
269
+ }
270
+
271
+ def build_dropdown(nav_item: dict) -> Optional[dict]:
272
+ """
273
+ Builds a dropdown structure for the action.
274
+ """
275
+ dropdown = {
276
+ "title": nav_item["title"],
277
+ "icon": nav_item.get("icon"),
278
+ "items": [],
279
+ }
280
+
281
+ for child in nav_item["items"]:
282
+ if action := get_action_by_name(child):
283
+ dropdown["items"].append(get_action_attrs(action))
284
+
285
+ if len(dropdown["items"]) > 0:
286
+ return dropdown
287
+
288
+ for nav_item in provided_actions:
289
+ if isinstance(nav_item, str):
290
+ if action := get_action_by_name(nav_item):
291
+ navigation.append(get_action_attrs(action))
292
+ elif isinstance(nav_item, dict):
293
+ if dropdown := build_dropdown(nav_item):
294
+ navigation.append(dropdown)
295
+
296
+ return navigation
297
+
298
+ def _filter_unfold_actions_by_permissions(
299
+ self,
300
+ request: HttpRequest,
301
+ actions: list[UnfoldAction],
302
+ object_id: Optional[Union[int, str]] = None,
303
+ ) -> list[UnfoldAction]:
304
+ """
305
+ Filters out actions that the user doesn't have access to.
306
+ """
307
+ filtered_actions = []
308
+
309
+ for action in actions:
310
+ if not hasattr(action.method, "allowed_permissions"):
311
+ filtered_actions.append(action)
312
+ continue
313
+
314
+ permission_checks = (
315
+ getattr(self, f"has_{permission}_permission")
316
+ for permission in action.method.allowed_permissions
317
+ )
318
+
319
+ if object_id:
320
+ if all(
321
+ has_permission(request, object_id)
322
+ for has_permission in permission_checks
323
+ ):
324
+ filtered_actions.append(action)
325
+ else:
326
+ if all(has_permission(request) for has_permission in permission_checks):
327
+ filtered_actions.append(action)
328
+
329
+ return filtered_actions