django-spire 0.23.6__py3-none-any.whl → 0.23.8__py3-none-any.whl

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 (541) hide show
  1. django_spire/ai/admin.py +11 -11
  2. django_spire/ai/chat/apps.py +1 -0
  3. django_spire/ai/chat/templates/django_spire/ai/chat/widget/dialog_widget.html +1 -1
  4. django_spire/ai/chat/tests/factories.py +15 -0
  5. django_spire/ai/chat/tests/test_controller.py +45 -0
  6. django_spire/ai/chat/tests/test_models.py +301 -0
  7. django_spire/ai/chat/tests/test_prompts.py +48 -0
  8. django_spire/ai/chat/tests/test_responses.py +208 -0
  9. django_spire/ai/chat/tests/test_router/test_base_chat_router.py +66 -6
  10. django_spire/ai/chat/tests/test_router/test_chat_workflow.py +73 -3
  11. django_spire/ai/chat/tests/test_router/test_integration.py +86 -6
  12. django_spire/ai/chat/tests/test_router/test_intent_decoder.py +93 -1
  13. django_spire/ai/chat/tests/test_router/test_message_intel.py +60 -1
  14. django_spire/ai/chat/tests/test_router/test_spire_chat_router.py +110 -0
  15. django_spire/ai/chat/tests/test_urls/test_json_urls.py +202 -1
  16. django_spire/ai/context/tests/__init__.py +0 -0
  17. django_spire/ai/context/tests/test_context.py +188 -0
  18. django_spire/ai/decorators.py +7 -6
  19. django_spire/ai/prompt/tests/test_bots.py +100 -10
  20. django_spire/ai/prompt/tests/test_prompt_intel.py +83 -0
  21. django_spire/ai/prompt/tests/test_prompt_tuning.py +126 -0
  22. django_spire/ai/sms/decorators.py +8 -2
  23. django_spire/ai/sms/tests/test_sms.py +240 -16
  24. django_spire/ai/sms/tests/test_sms_intel.py +42 -0
  25. django_spire/ai/sms/tests/test_webhook.py +155 -7
  26. django_spire/ai/sms/views.py +23 -24
  27. django_spire/ai/tests/test_ai.py +131 -7
  28. django_spire/auth/apps.py +4 -2
  29. django_spire/auth/controller/controller.py +36 -23
  30. django_spire/auth/controller/exceptions.py +9 -0
  31. django_spire/auth/group/admin.py +1 -0
  32. django_spire/auth/group/apps.py +2 -0
  33. django_spire/auth/group/factories.py +17 -8
  34. django_spire/auth/group/forms.py +7 -0
  35. django_spire/auth/group/tests/test_factories.py +146 -0
  36. django_spire/auth/group/tests/test_forms.py +282 -0
  37. django_spire/auth/group/tests/test_models.py +192 -0
  38. django_spire/auth/group/tests/test_querysets.py +98 -0
  39. django_spire/auth/group/tests/test_utils.py +341 -0
  40. django_spire/auth/group/tests/test_views.py +377 -0
  41. django_spire/auth/group/urls/__init__.py +3 -1
  42. django_spire/auth/group/urls/form_urls.py +2 -0
  43. django_spire/auth/group/urls/json_urls.py +3 -0
  44. django_spire/auth/group/urls/page_urls.py +2 -0
  45. django_spire/auth/group/utils.py +6 -2
  46. django_spire/auth/group/views/form_views.py +6 -3
  47. django_spire/auth/group/views/json_views.py +6 -2
  48. django_spire/auth/mfa/admin.py +2 -0
  49. django_spire/auth/mfa/apps.py +2 -0
  50. django_spire/auth/mfa/forms.py +1 -0
  51. django_spire/auth/mfa/querysets.py +9 -2
  52. django_spire/auth/mfa/tests/test_models.py +233 -0
  53. django_spire/auth/mfa/tests/test_utils.py +106 -0
  54. django_spire/auth/mfa/urls/__init__.py +2 -0
  55. django_spire/auth/mfa/urls/page_urls.py +2 -0
  56. django_spire/auth/mfa/urls/redirect_urls.py +2 -0
  57. django_spire/auth/mfa/views/page_views.py +2 -1
  58. django_spire/auth/permissions/consts.py +2 -2
  59. django_spire/auth/permissions/decorators.py +8 -8
  60. django_spire/auth/permissions/permissions.py +28 -35
  61. django_spire/auth/permissions/tests/test_decorators.py +333 -0
  62. django_spire/auth/permissions/tests/test_permissions.py +337 -0
  63. django_spire/auth/permissions/tests/test_tools.py +305 -0
  64. django_spire/auth/permissions/tools.py +21 -15
  65. django_spire/auth/seeding/seed.py +3 -0
  66. django_spire/auth/seeding/seeder.py +2 -0
  67. django_spire/auth/tests/test_controller.py +323 -0
  68. django_spire/auth/tests/test_url_endpoints.py +9 -9
  69. django_spire/auth/tests/test_views.py +406 -0
  70. django_spire/auth/urls/admin_urls.py +2 -0
  71. django_spire/auth/urls/redirect_urls.py +2 -0
  72. django_spire/auth/user/apps.py +2 -0
  73. django_spire/auth/user/forms.py +9 -0
  74. django_spire/auth/user/models.py +1 -1
  75. django_spire/auth/user/services/services.py +1 -0
  76. django_spire/auth/user/tests/factories.py +14 -13
  77. django_spire/auth/user/tests/test_factories.py +166 -2
  78. django_spire/auth/user/tests/test_forms.py +573 -0
  79. django_spire/auth/user/tests/test_models.py +257 -0
  80. django_spire/auth/user/tests/test_services.py +200 -0
  81. django_spire/auth/user/tests/test_tools.py +153 -0
  82. django_spire/auth/user/tests/test_user_factories.py +139 -0
  83. django_spire/auth/user/tests/test_views.py +363 -0
  84. django_spire/auth/user/tools.py +7 -1
  85. django_spire/auth/user/urls/form_urls.py +3 -0
  86. django_spire/auth/user/urls/page_urls.py +3 -0
  87. django_spire/auth/user/views/form_views.py +19 -10
  88. django_spire/auth/user/views/page_views.py +8 -2
  89. django_spire/auth/views/redirect_views.py +14 -9
  90. django_spire/comment/admin.py +2 -0
  91. django_spire/comment/apps.py +2 -0
  92. django_spire/comment/templatetags/comment_tags.py +1 -0
  93. django_spire/comment/tests/test_forms.py +27 -0
  94. django_spire/comment/tests/test_models.py +215 -0
  95. django_spire/comment/tests/test_querysets.py +101 -0
  96. django_spire/comment/tests/test_utils.py +90 -0
  97. django_spire/comment/urls.py +2 -0
  98. django_spire/comment/utils.py +22 -13
  99. django_spire/comment/views.py +1 -1
  100. django_spire/conf.py +8 -6
  101. django_spire/consts.py +1 -1
  102. django_spire/contrib/breadcrumb/apps.py +2 -0
  103. django_spire/contrib/breadcrumb/breadcrumbs.py +18 -18
  104. django_spire/contrib/breadcrumb/tests/test_breadcrumbs.py +198 -0
  105. django_spire/contrib/constructor/__init__.py +3 -3
  106. django_spire/contrib/constructor/constructor.py +15 -15
  107. django_spire/contrib/constructor/django_model_constructor.py +5 -4
  108. django_spire/contrib/constructor/exceptions.py +5 -3
  109. django_spire/contrib/constructor/tests/__init__.py +0 -0
  110. django_spire/contrib/constructor/tests/test_constructor.py +193 -0
  111. django_spire/contrib/form/tests/__init__.py +0 -0
  112. django_spire/contrib/form/tests/test_forms.py +203 -0
  113. django_spire/contrib/generic_views/modal_views.py +2 -1
  114. django_spire/contrib/generic_views/portal_views.py +20 -19
  115. django_spire/contrib/generic_views/tests/__init__.py +0 -0
  116. django_spire/contrib/generic_views/tests/test_views.py +459 -0
  117. django_spire/contrib/help/apps.py +2 -0
  118. django_spire/contrib/help/templatetags/help.py +1 -0
  119. django_spire/contrib/help/tests/__init__.py +0 -0
  120. django_spire/contrib/help/tests/test_templatetags.py +100 -0
  121. django_spire/contrib/options/mixins.py +6 -5
  122. django_spire/contrib/options/tests/factories.py +5 -1
  123. django_spire/contrib/options/tests/test_options.py +234 -0
  124. django_spire/contrib/ordering/exceptions.py +7 -3
  125. django_spire/contrib/ordering/mixins.py +2 -0
  126. django_spire/contrib/ordering/querysets.py +3 -1
  127. django_spire/contrib/ordering/services/processor_service.py +8 -4
  128. django_spire/contrib/ordering/services/service.py +1 -2
  129. django_spire/contrib/ordering/tests/__init__.py +0 -0
  130. django_spire/contrib/ordering/tests/test_ordering.py +165 -0
  131. django_spire/contrib/ordering/validators.py +6 -6
  132. django_spire/contrib/pagination/templatetags/pagination_tags.py +12 -5
  133. django_spire/contrib/pagination/tests/__init__.py +0 -0
  134. django_spire/contrib/pagination/tests/test_pagination.py +179 -0
  135. django_spire/contrib/performance/decorators.py +16 -6
  136. django_spire/contrib/performance/tests/__init__.py +0 -0
  137. django_spire/contrib/performance/tests/test_performance.py +107 -0
  138. django_spire/contrib/progress/__init__.py +1 -3
  139. django_spire/contrib/progress/static/django_spire/js/contrib/progress/progress.js +38 -82
  140. django_spire/contrib/queryset/enums.py +3 -1
  141. django_spire/contrib/queryset/filter_tools.py +10 -5
  142. django_spire/contrib/queryset/mixins.py +16 -16
  143. django_spire/contrib/queryset/tests/__init__.py +0 -0
  144. django_spire/contrib/queryset/tests/test_queryset.py +137 -0
  145. django_spire/contrib/seeding/field/base.py +13 -7
  146. django_spire/contrib/seeding/field/callable.py +8 -1
  147. django_spire/contrib/seeding/field/cleaners.py +5 -5
  148. django_spire/contrib/seeding/field/custom.py +20 -10
  149. django_spire/contrib/seeding/field/django/seeder.py +8 -6
  150. django_spire/contrib/seeding/field/enums.py +7 -5
  151. django_spire/contrib/seeding/field/override.py +16 -6
  152. django_spire/contrib/seeding/field/static.py +9 -2
  153. django_spire/contrib/seeding/field/tests/test_base.py +18 -14
  154. django_spire/contrib/seeding/field/tests/test_callable.py +13 -9
  155. django_spire/contrib/seeding/field/tests/test_cleaners.py +51 -38
  156. django_spire/contrib/seeding/field/tests/test_static.py +13 -9
  157. django_spire/contrib/seeding/intelligence/bots/seeder_generator_bot.py +2 -0
  158. django_spire/contrib/seeding/intelligence/intel.py +5 -1
  159. django_spire/contrib/seeding/intelligence/prompts/factory.py +6 -1
  160. django_spire/contrib/seeding/intelligence/prompts/foreign_key_selection_prompt.py +6 -1
  161. django_spire/contrib/seeding/intelligence/prompts/generate_django_model_seeder_prompts.py +2 -0
  162. django_spire/contrib/seeding/intelligence/prompts/generic_relationship_selection_prompt.py +7 -1
  163. django_spire/contrib/seeding/intelligence/prompts/hierarchical_selection_prompt.py +6 -2
  164. django_spire/contrib/seeding/intelligence/prompts/model_field_choices_prompt.py +8 -2
  165. django_spire/contrib/seeding/intelligence/prompts/negation_prompt.py +2 -0
  166. django_spire/contrib/seeding/intelligence/prompts/objective_prompt.py +6 -1
  167. django_spire/contrib/seeding/management/commands/seeding.py +9 -3
  168. django_spire/contrib/seeding/management/example.py +2 -0
  169. django_spire/contrib/seeding/model/base.py +16 -7
  170. django_spire/contrib/seeding/model/config.py +31 -15
  171. django_spire/contrib/seeding/model/django/config.py +13 -13
  172. django_spire/contrib/seeding/model/django/seeder.py +4 -4
  173. django_spire/contrib/seeding/model/django/tests/test_seeder.py +34 -23
  174. django_spire/contrib/seeding/model/enums.py +2 -0
  175. django_spire/contrib/seeding/tests/test_config.py +71 -0
  176. django_spire/contrib/seeding/tests/test_custom.py +35 -0
  177. django_spire/contrib/seeding/tests/test_enums.py +40 -0
  178. django_spire/contrib/seeding/tests/test_intel.py +32 -0
  179. django_spire/contrib/seeding/tests/test_override.py +63 -0
  180. django_spire/contrib/service/__init__.py +2 -2
  181. django_spire/contrib/service/django_model_service.py +16 -15
  182. django_spire/contrib/service/exceptions.py +5 -3
  183. django_spire/contrib/service/tests/__init__.py +0 -0
  184. django_spire/contrib/service/tests/test_service.py +153 -0
  185. django_spire/contrib/session/apps.py +2 -0
  186. django_spire/contrib/session/controller.py +48 -42
  187. django_spire/contrib/session/templatetags/session_tags.py +11 -2
  188. django_spire/contrib/session/tests/test_session_controller.py +117 -53
  189. django_spire/contrib/tests/__init__.py +0 -0
  190. django_spire/contrib/tests/test_utils.py +37 -0
  191. django_spire/contrib/utils.py +4 -1
  192. django_spire/core/apps.py +2 -0
  193. django_spire/core/converters/tests/test_to_data.py +353 -0
  194. django_spire/core/converters/tests/test_to_enums.py +61 -41
  195. django_spire/core/converters/tests/test_to_pydantic.py +138 -109
  196. django_spire/core/converters/to_data.py +29 -10
  197. django_spire/core/converters/to_enums.py +4 -2
  198. django_spire/core/converters/to_pydantic.py +22 -22
  199. django_spire/core/decorators.py +19 -6
  200. django_spire/core/forms/widgets.py +4 -0
  201. django_spire/core/maps.py +3 -1
  202. django_spire/core/middleware/maintenance.py +3 -3
  203. django_spire/core/middleware.py +8 -6
  204. django_spire/core/redirect/__init__.py +5 -0
  205. django_spire/core/redirect/generic_redirect.py +1 -2
  206. django_spire/core/redirect/tests/__init__.py +0 -0
  207. django_spire/core/redirect/tests/test_generic_redirect.py +34 -0
  208. django_spire/core/{tests/tests_redirect.py → redirect/tests/test_safe_redirect.py} +55 -81
  209. django_spire/core/shortcuts.py +3 -3
  210. django_spire/core/static/django_spire/css/app-layout.css +1 -1
  211. django_spire/core/static/django_spire/css/app-navigation.css +3 -3
  212. django_spire/core/static/django_spire/css/bootstrap-override.css +4 -0
  213. django_spire/core/tag/admin.py +12 -0
  214. django_spire/core/tag/intelligence/tag_set_bot.py +2 -0
  215. django_spire/core/tag/mixins.py +2 -0
  216. django_spire/core/tag/models.py +2 -0
  217. django_spire/core/tag/querysets.py +2 -0
  218. django_spire/core/tag/service/tag_service.py +6 -3
  219. django_spire/core/tag/tests/test_intelligence.py +9 -9
  220. django_spire/core/tag/tests/test_tags.py +44 -54
  221. django_spire/core/tag/tests/test_tools.py +191 -0
  222. django_spire/core/tag/tools.py +3 -0
  223. django_spire/core/templates/django_spire/card/card.html +5 -2
  224. django_spire/core/templates/django_spire/card/title_card.html +9 -4
  225. django_spire/core/templates/django_spire/infinite_scroll/base.html +1 -0
  226. django_spire/core/templates/django_spire/navigation/side_navigation.html +19 -24
  227. django_spire/core/templates/django_spire/page/full_page.html +46 -16
  228. django_spire/core/templates/django_spire/table/base.html +4 -2
  229. django_spire/core/templatetags/json.py +6 -2
  230. django_spire/core/templatetags/message.py +13 -8
  231. django_spire/core/templatetags/string_formating.py +8 -5
  232. django_spire/core/templatetags/tests/__init__.py +0 -0
  233. django_spire/core/templatetags/tests/test_templatetags.py +427 -0
  234. django_spire/core/templatetags/variable_types.py +17 -9
  235. django_spire/core/tests/test_cases.py +1 -1
  236. django_spire/core/tests/test_conf.py +43 -0
  237. django_spire/core/tests/test_consts.py +28 -0
  238. django_spire/core/tests/test_context_processors.py +93 -0
  239. django_spire/core/tests/test_decorators.py +95 -0
  240. django_spire/core/tests/test_django_spire_utils.py +56 -0
  241. django_spire/core/tests/test_exceptions.py +37 -0
  242. django_spire/core/tests/test_models.py +54 -0
  243. django_spire/core/tests/test_settings.py +45 -0
  244. django_spire/core/tests/test_shortcuts.py +74 -0
  245. django_spire/core/tests/test_urls.py +16 -0
  246. django_spire/core/tests/test_utils.py +58 -0
  247. django_spire/core/urls.py +4 -1
  248. django_spire/core/utils.py +12 -8
  249. django_spire/exceptions.py +16 -1
  250. django_spire/file/admin.py +4 -2
  251. django_spire/file/apps.py +8 -10
  252. django_spire/file/fields.py +7 -7
  253. django_spire/file/forms.py +1 -1
  254. django_spire/file/interfaces.py +15 -15
  255. django_spire/file/mixins.py +1 -4
  256. django_spire/file/models.py +3 -5
  257. django_spire/file/tests/factories.py +59 -0
  258. django_spire/file/tests/test_admin.py +69 -0
  259. django_spire/file/tests/test_apps.py +24 -0
  260. django_spire/file/tests/test_fields.py +114 -0
  261. django_spire/file/tests/test_forms.py +20 -0
  262. django_spire/file/tests/test_interfaces.py +183 -0
  263. django_spire/file/tests/test_models.py +82 -0
  264. django_spire/file/tests/test_querysets.py +102 -0
  265. django_spire/file/tests/test_utils.py +32 -0
  266. django_spire/file/tests/test_views.py +145 -0
  267. django_spire/file/tests/test_widgets.py +82 -0
  268. django_spire/file/tools.py +8 -2
  269. django_spire/file/views.py +7 -3
  270. django_spire/file/widgets.py +12 -12
  271. django_spire/help_desk/admin.py +15 -0
  272. django_spire/help_desk/apps.py +2 -0
  273. django_spire/help_desk/auth/controller.py +2 -0
  274. django_spire/help_desk/choices.py +2 -0
  275. django_spire/help_desk/enums.py +2 -0
  276. django_spire/help_desk/exceptions.py +31 -3
  277. django_spire/help_desk/forms.py +2 -0
  278. django_spire/help_desk/models.py +2 -0
  279. django_spire/help_desk/querysets.py +4 -1
  280. django_spire/help_desk/services/notification_service.py +26 -27
  281. django_spire/help_desk/services/service.py +2 -3
  282. django_spire/help_desk/tests/factories.py +8 -3
  283. django_spire/help_desk/tests/test_admin.py +41 -0
  284. django_spire/help_desk/tests/test_apps.py +41 -0
  285. django_spire/help_desk/tests/test_choices.py +50 -0
  286. django_spire/help_desk/tests/test_controller.py +87 -0
  287. django_spire/help_desk/tests/test_enums.py +18 -0
  288. django_spire/help_desk/tests/test_exceptions.py +37 -0
  289. django_spire/help_desk/tests/test_forms.py +89 -0
  290. django_spire/help_desk/tests/test_models.py +59 -0
  291. django_spire/help_desk/tests/test_querysets.py +38 -0
  292. django_spire/help_desk/tests/test_services/test_notification_service.py +15 -8
  293. django_spire/help_desk/tests/test_services/test_service.py +92 -0
  294. django_spire/help_desk/tests/test_urls/test_form_urls.py +6 -6
  295. django_spire/help_desk/tests/test_urls/test_page_urls.py +8 -9
  296. django_spire/help_desk/tests/test_views/test_form_views.py +46 -19
  297. django_spire/help_desk/tests/test_views/test_page_views.py +32 -9
  298. django_spire/help_desk/urls/__init__.py +4 -1
  299. django_spire/help_desk/urls/form_urls.py +3 -0
  300. django_spire/help_desk/urls/page_urls.py +3 -0
  301. django_spire/help_desk/views/form_views.py +13 -5
  302. django_spire/help_desk/views/page_views.py +11 -3
  303. django_spire/history/activity/admin.py +2 -0
  304. django_spire/history/activity/apps.py +3 -1
  305. django_spire/history/activity/mixins.py +13 -7
  306. django_spire/history/activity/models.py +6 -5
  307. django_spire/history/activity/querysets.py +2 -0
  308. django_spire/history/activity/tests/__init__.py +0 -0
  309. django_spire/history/activity/tests/test_activity.py +176 -0
  310. django_spire/history/admin.py +9 -2
  311. django_spire/history/choices.py +3 -0
  312. django_spire/history/models.py +5 -5
  313. django_spire/history/tests/test_admin.py +93 -0
  314. django_spire/history/tests/test_history.py +101 -0
  315. django_spire/history/tests/test_mixins.py +84 -0
  316. django_spire/history/viewed/admin.py +3 -1
  317. django_spire/history/viewed/apps.py +3 -1
  318. django_spire/history/viewed/models.py +2 -0
  319. django_spire/history/viewed/tests/__init__.py +0 -0
  320. django_spire/history/viewed/tests/test_viewed.py +46 -0
  321. django_spire/knowledge/auth/tests/__init__.py +0 -0
  322. django_spire/knowledge/auth/tests/test_controller.py +116 -0
  323. django_spire/knowledge/collection/admin.py +5 -1
  324. django_spire/knowledge/collection/models.py +3 -1
  325. django_spire/knowledge/collection/seeding/seed.py +1 -0
  326. django_spire/knowledge/collection/services/factory_service.py +10 -11
  327. django_spire/knowledge/collection/services/ordering_service.py +1 -2
  328. django_spire/knowledge/collection/services/service.py +5 -10
  329. django_spire/knowledge/collection/services/tag_service.py +5 -2
  330. django_spire/knowledge/collection/tests/factories.py +28 -1
  331. django_spire/knowledge/collection/tests/test_models.py +48 -0
  332. django_spire/knowledge/collection/tests/test_querysets.py +93 -0
  333. django_spire/knowledge/collection/tests/test_services/test_factory_service.py +100 -0
  334. django_spire/knowledge/collection/tests/test_services/test_services.py +160 -0
  335. django_spire/knowledge/collection/tests/test_urls/test_form_urls.py +21 -3
  336. django_spire/knowledge/collection/tests/test_urls/test_json_urls.py +39 -1
  337. django_spire/knowledge/collection/tests/test_urls/test_page_urls.py +12 -4
  338. django_spire/knowledge/collection/urls/__init__.py +3 -0
  339. django_spire/knowledge/collection/urls/form_urls.py +2 -0
  340. django_spire/knowledge/collection/urls/json_urls.py +2 -0
  341. django_spire/knowledge/collection/urls/page_urls.py +2 -0
  342. django_spire/knowledge/collection/views/form_views.py +4 -4
  343. django_spire/knowledge/collection/views/json_views.py +5 -1
  344. django_spire/knowledge/collection/views/page_views.py +5 -2
  345. django_spire/knowledge/entry/admin.py +7 -1
  346. django_spire/knowledge/entry/forms.py +2 -0
  347. django_spire/knowledge/entry/models.py +2 -0
  348. django_spire/knowledge/entry/seeding/seed.py +3 -0
  349. django_spire/knowledge/entry/services/automation_service.py +5 -4
  350. django_spire/knowledge/entry/services/factory_service.py +7 -5
  351. django_spire/knowledge/entry/services/service.py +4 -7
  352. django_spire/knowledge/entry/services/tag_service.py +0 -1
  353. django_spire/knowledge/entry/services/tool_service.py +1 -0
  354. django_spire/knowledge/entry/services/transformation_services.py +1 -5
  355. django_spire/knowledge/entry/tests/factories.py +1 -2
  356. django_spire/knowledge/entry/tests/test_factory_service.py +20 -0
  357. django_spire/knowledge/entry/tests/test_models.py +41 -0
  358. django_spire/knowledge/entry/tests/test_querysets.py +71 -0
  359. django_spire/knowledge/entry/tests/test_services.py +94 -0
  360. django_spire/knowledge/entry/tests/test_urls/test_form_urls.py +9 -14
  361. django_spire/knowledge/entry/tests/test_urls/test_json_urls.py +48 -5
  362. django_spire/knowledge/entry/tests/test_urls/test_page_urls.py +6 -8
  363. django_spire/knowledge/entry/tests/test_urls/test_template_urls.py +40 -0
  364. django_spire/knowledge/entry/urls/form_urls.py +2 -0
  365. django_spire/knowledge/entry/urls/json_urls.py +2 -0
  366. django_spire/knowledge/entry/urls/page_urls.py +2 -0
  367. django_spire/knowledge/entry/urls/template_urls.py +2 -0
  368. django_spire/knowledge/entry/version/block/choices.py +2 -0
  369. django_spire/knowledge/entry/version/block/data/data.py +1 -0
  370. django_spire/knowledge/entry/version/block/data/list/data.py +8 -13
  371. django_spire/knowledge/entry/version/block/data/list/maps.py +3 -0
  372. django_spire/knowledge/entry/version/block/data/list/meta.py +1 -2
  373. django_spire/knowledge/entry/version/block/data/list/tests/__init__.py +0 -0
  374. django_spire/knowledge/entry/version/block/data/list/tests/test_maps.py +32 -0
  375. django_spire/knowledge/entry/version/block/data/list/tests/test_meta.py +58 -0
  376. django_spire/knowledge/entry/version/block/data/maps.py +3 -6
  377. django_spire/knowledge/entry/version/block/models.py +7 -5
  378. django_spire/knowledge/entry/version/block/seeding/constants.py +5 -4
  379. django_spire/knowledge/entry/version/block/services/service.py +2 -3
  380. django_spire/knowledge/entry/version/block/tests/factories.py +4 -10
  381. django_spire/knowledge/entry/version/block/tests/test_choices.py +56 -0
  382. django_spire/knowledge/entry/version/block/tests/test_data.py +90 -0
  383. django_spire/knowledge/entry/version/block/tests/test_maps.py +37 -0
  384. django_spire/knowledge/entry/version/block/tests/test_models.py +55 -0
  385. django_spire/knowledge/entry/version/block/tests/test_querysets.py +35 -0
  386. django_spire/knowledge/entry/version/block/tests/test_services.py +65 -0
  387. django_spire/knowledge/entry/version/choices.py +2 -0
  388. django_spire/knowledge/entry/version/converters/converter.py +1 -1
  389. django_spire/knowledge/entry/version/converters/docx_converter.py +4 -7
  390. django_spire/knowledge/entry/version/converters/markdown_converter.py +20 -20
  391. django_spire/knowledge/entry/version/maps.py +4 -5
  392. django_spire/knowledge/entry/version/querysets.py +1 -1
  393. django_spire/knowledge/entry/version/seeding/seeder.py +1 -2
  394. django_spire/knowledge/entry/version/services/processor_service.py +5 -4
  395. django_spire/knowledge/entry/version/services/service.py +1 -2
  396. django_spire/knowledge/entry/version/tests/factories.py +2 -2
  397. django_spire/knowledge/entry/version/tests/test_choices.py +18 -0
  398. django_spire/knowledge/entry/version/tests/test_converters/test_docx_converter.py +56 -8
  399. django_spire/knowledge/entry/version/tests/test_converters/test_markdown_converter.py +78 -0
  400. django_spire/knowledge/entry/version/tests/test_maps.py +58 -0
  401. django_spire/knowledge/entry/version/tests/test_models.py +23 -0
  402. django_spire/knowledge/entry/version/tests/test_querysets.py +26 -0
  403. django_spire/knowledge/entry/version/tests/test_services.py +62 -0
  404. django_spire/knowledge/entry/version/tests/test_urls/test_json_urls.py +27 -8
  405. django_spire/knowledge/entry/version/tests/test_urls/test_page_urls.py +15 -8
  406. django_spire/knowledge/entry/version/tests/test_urls/test_redirect_urls.py +38 -0
  407. django_spire/knowledge/entry/version/urls/__init__.py +3 -0
  408. django_spire/knowledge/entry/version/urls/json_urls.py +2 -1
  409. django_spire/knowledge/entry/version/urls/page_urls.py +2 -0
  410. django_spire/knowledge/entry/version/urls/redirect_urls.py +2 -0
  411. django_spire/knowledge/entry/version/views/json_views.py +5 -1
  412. django_spire/knowledge/entry/version/views/page_views.py +10 -3
  413. django_spire/knowledge/entry/version/views/redirect_views.py +5 -1
  414. django_spire/knowledge/entry/views/form_views.py +16 -8
  415. django_spire/knowledge/entry/views/json_views.py +3 -1
  416. django_spire/knowledge/entry/views/page_views.py +8 -2
  417. django_spire/knowledge/entry/views/template_views.py +7 -1
  418. django_spire/knowledge/exceptions.py +2 -1
  419. django_spire/knowledge/intelligence/intel/answer_intel.py +2 -1
  420. django_spire/knowledge/intelligence/intel/entry_intel.py +0 -1
  421. django_spire/knowledge/intelligence/workflows/knowledge_workflow.py +4 -5
  422. django_spire/knowledge/models.py +1 -2
  423. django_spire/knowledge/tests/__init__.py +0 -0
  424. django_spire/knowledge/tests/test_templatetags.py +40 -0
  425. django_spire/knowledge/tests/test_urls/__init__.py +0 -0
  426. django_spire/knowledge/tests/test_urls/test_page_urls.py +24 -0
  427. django_spire/knowledge/urls/__init__.py +2 -0
  428. django_spire/knowledge/urls/page_urls.py +2 -0
  429. django_spire/knowledge/views/page_views.py +8 -3
  430. django_spire/notification/admin.py +3 -1
  431. django_spire/notification/app/admin.py +2 -0
  432. django_spire/notification/app/apps.py +3 -1
  433. django_spire/notification/app/exceptions.py +9 -2
  434. django_spire/notification/app/models.py +8 -4
  435. django_spire/notification/app/processor.py +22 -26
  436. django_spire/notification/app/querysets.py +2 -0
  437. django_spire/notification/app/tests/__init__.py +0 -0
  438. django_spire/notification/app/tests/factories.py +34 -0
  439. django_spire/notification/app/tests/test_apps.py +24 -0
  440. django_spire/notification/app/tests/test_models.py +72 -0
  441. django_spire/notification/app/tests/test_processor.py +111 -0
  442. django_spire/notification/app/tests/test_querysets.py +90 -0
  443. django_spire/notification/app/tests/test_views/__init__.py +0 -0
  444. django_spire/notification/app/tests/test_views/test_json_views.py +48 -0
  445. django_spire/notification/app/tests/test_views/test_page_views.py +19 -0
  446. django_spire/notification/app/urls/__init__.py +3 -1
  447. django_spire/notification/app/urls/json_urls.py +6 -4
  448. django_spire/notification/app/urls/page_urls.py +4 -3
  449. django_spire/notification/app/urls/template_urls.py +4 -2
  450. django_spire/notification/apps.py +4 -1
  451. django_spire/notification/email/admin.py +5 -1
  452. django_spire/notification/email/apps.py +3 -1
  453. django_spire/notification/email/exceptions.py +4 -2
  454. django_spire/notification/email/helper.py +5 -3
  455. django_spire/notification/email/models.py +4 -0
  456. django_spire/notification/email/processor.py +19 -15
  457. django_spire/notification/email/querysets.py +3 -0
  458. django_spire/notification/email/tests/__init__.py +0 -0
  459. django_spire/notification/email/tests/factories.py +35 -0
  460. django_spire/notification/email/tests/test_apps.py +24 -0
  461. django_spire/notification/email/tests/test_models.py +52 -0
  462. django_spire/notification/email/tests/test_processor.py +92 -0
  463. django_spire/notification/email/tests/test_querysets.py +43 -0
  464. django_spire/notification/exceptions.py +17 -2
  465. django_spire/notification/managers.py +7 -1
  466. django_spire/notification/maps.py +4 -1
  467. django_spire/notification/mixins.py +2 -0
  468. django_spire/notification/models.py +3 -1
  469. django_spire/notification/processors/notification.py +12 -5
  470. django_spire/notification/processors/processor.py +2 -0
  471. django_spire/notification/processors/tests/__init__.py +0 -0
  472. django_spire/notification/processors/tests/test_notification.py +106 -0
  473. django_spire/notification/push/admin.py +10 -1
  474. django_spire/notification/push/apps.py +3 -1
  475. django_spire/notification/push/models.py +2 -3
  476. django_spire/notification/push/tests/__init__.py +0 -0
  477. django_spire/notification/push/tests/test_apps.py +24 -0
  478. django_spire/notification/push/tests/test_models.py +28 -0
  479. django_spire/notification/querysets.py +7 -1
  480. django_spire/notification/sms/admin.py +2 -0
  481. django_spire/notification/sms/apps.py +4 -1
  482. django_spire/notification/sms/automations.py +2 -0
  483. django_spire/notification/sms/choices.py +2 -0
  484. django_spire/notification/sms/exceptions.py +19 -5
  485. django_spire/notification/sms/helper.py +33 -23
  486. django_spire/notification/sms/models.py +5 -1
  487. django_spire/notification/sms/processor.py +20 -20
  488. django_spire/notification/sms/querysets.py +2 -0
  489. django_spire/notification/sms/tests/factories.py +33 -0
  490. django_spire/notification/sms/tests/test_apps.py +24 -0
  491. django_spire/notification/sms/tests/test_automation.py +38 -0
  492. django_spire/notification/sms/tests/test_choices.py +15 -0
  493. django_spire/notification/sms/tests/test_consts.py +17 -0
  494. django_spire/notification/sms/tests/test_exceptions.py +27 -0
  495. django_spire/notification/sms/tests/test_helper.py +50 -0
  496. django_spire/notification/sms/tests/test_models.py +81 -0
  497. django_spire/notification/sms/tests/test_processor.py +107 -0
  498. django_spire/notification/sms/tests/test_tools.py +25 -11
  499. django_spire/notification/sms/tools.py +16 -5
  500. django_spire/notification/sms/urls/__init__.py +3 -1
  501. django_spire/notification/sms/urls/media_urls.py +2 -0
  502. django_spire/notification/sms/views/media_views.py +14 -4
  503. django_spire/notification/tests/__init__.py +0 -0
  504. django_spire/notification/tests/factories.py +26 -0
  505. django_spire/notification/tests/test_admin.py +55 -0
  506. django_spire/notification/tests/test_apps.py +30 -0
  507. django_spire/notification/tests/test_automation.py +18 -0
  508. django_spire/notification/tests/test_choices.py +59 -0
  509. django_spire/notification/tests/test_exceptions.py +58 -0
  510. django_spire/notification/tests/test_managers.py +100 -0
  511. django_spire/notification/tests/test_maps.py +31 -0
  512. django_spire/notification/tests/test_models.py +76 -0
  513. django_spire/notification/tests/test_querysets.py +184 -0
  514. django_spire/notification/tests/test_utils.py +23 -0
  515. django_spire/notification/urls.py +3 -1
  516. django_spire/notification/utils.py +3 -1
  517. django_spire/settings.py +3 -0
  518. django_spire/theme/tests/test_context_processor.py +15 -13
  519. django_spire/theme/tests/test_enums.py +2 -2
  520. django_spire/theme/tests/test_filesystem.py +2 -5
  521. django_spire/theme/tests/test_integration.py +12 -12
  522. django_spire/theme/tests/test_model.py +40 -38
  523. django_spire/theme/tests/test_views/test_json_views.py +33 -33
  524. django_spire/theme/urls/json_urls.py +3 -0
  525. django_spire/theme/urls/page_urls.py +3 -0
  526. django_spire/urls.py +19 -15
  527. django_spire/utils.py +13 -4
  528. {django_spire-0.23.6.dist-info → django_spire-0.23.8.dist-info}/METADATA +1 -1
  529. {django_spire-0.23.6.dist-info → django_spire-0.23.8.dist-info}/RECORD +532 -361
  530. django_spire/contrib/options/tests/test_unit.py +0 -148
  531. django_spire/contrib/progress/views.py +0 -64
  532. django_spire/contrib/seeding/tests/test_seeding.py +0 -25
  533. django_spire/core/tests/test_templatetags.py +0 -117
  534. django_spire/core/tests/tests_shortcuts.py +0 -73
  535. django_spire/history/activity/tests.py +0 -3
  536. django_spire/history/activity/views.py +0 -3
  537. django_spire/knowledge/collection/tests/test_services/test_transformation_service.py +0 -71
  538. django_spire/notification/app/tests.py +0 -3
  539. {django_spire-0.23.6.dist-info → django_spire-0.23.8.dist-info}/WHEEL +0 -0
  540. {django_spire-0.23.6.dist-info → django_spire-0.23.8.dist-info}/licenses/LICENSE.md +0 -0
  541. {django_spire-0.23.6.dist-info → django_spire-0.23.8.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,333 @@
1
+ from __future__ import annotations
2
+
3
+ import pytest
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from django.core.exceptions import PermissionDenied
8
+ from django.test import RequestFactory
9
+
10
+ from django_spire.auth.permissions.decorators import (
11
+ permission_required,
12
+ permission_required_decorator_function,
13
+ )
14
+ from django_spire.auth.user.tests.factories import create_super_user, create_user
15
+ from django_spire.core.tests.test_cases import BaseTestCase
16
+
17
+ if TYPE_CHECKING:
18
+ from django.core.handlers.wsgi import WSGIRequest
19
+
20
+
21
+ class PermissionRequiredDecoratorFunctionTestCase(BaseTestCase):
22
+ def setUp(self) -> None:
23
+ super().setUp()
24
+
25
+ self.factory = RequestFactory()
26
+ self.user = create_user(username='testuser')
27
+ self.user.set_password('password')
28
+ self.user.save()
29
+
30
+ def test_unauthenticated_user_redirects(self) -> None:
31
+ request = self.factory.get('/test/')
32
+ request.user = type('AnonymousUser', (), {'is_authenticated': False})()
33
+
34
+ def dummy_view(_request: WSGIRequest) -> str:
35
+ return 'success'
36
+
37
+ response = permission_required_decorator_function(
38
+ 'some.permission',
39
+ dummy_view,
40
+ request
41
+ )
42
+ assert response.status_code == 302
43
+
44
+ def test_user_without_permission_raises_denied(self) -> None:
45
+ request = self.factory.get('/test/')
46
+ request.user = self.user
47
+
48
+ def dummy_view(_request: WSGIRequest) -> str:
49
+ return 'success'
50
+
51
+ with pytest.raises(PermissionDenied):
52
+ permission_required_decorator_function(
53
+ 'some.nonexistent_permission',
54
+ dummy_view,
55
+ request
56
+ )
57
+
58
+ def test_superuser_has_all_permissions(self) -> None:
59
+ superuser = create_super_user()
60
+ request = self.factory.get('/test/')
61
+ request.user = superuser
62
+
63
+ def dummy_view(_request: WSGIRequest) -> str:
64
+ return 'success'
65
+
66
+ result = permission_required_decorator_function(
67
+ 'any.permission',
68
+ dummy_view,
69
+ request
70
+ )
71
+ assert result == 'success'
72
+
73
+ def test_redirects_to_login(self) -> None:
74
+ request = self.factory.get('/test/')
75
+ request.user = type('AnonymousUser', (), {'is_authenticated': False})()
76
+
77
+ def dummy_view(_request: WSGIRequest) -> str:
78
+ return 'success'
79
+
80
+ response = permission_required_decorator_function(
81
+ 'some.permission',
82
+ dummy_view,
83
+ request
84
+ )
85
+ assert 'login' in response.url.lower()
86
+
87
+ def test_multiple_permissions_all_required(self) -> None:
88
+ request = self.factory.get('/test/')
89
+ request.user = self.user
90
+
91
+ def dummy_view(_request: WSGIRequest) -> str:
92
+ return 'success'
93
+
94
+ with pytest.raises(PermissionDenied):
95
+ permission_required_decorator_function(
96
+ ['perm1', 'perm2'],
97
+ dummy_view,
98
+ request,
99
+ all_required=True
100
+ )
101
+
102
+ def test_multiple_permissions_any_required(self) -> None:
103
+ superuser = create_super_user()
104
+ request = self.factory.get('/test/')
105
+ request.user = superuser
106
+
107
+ def dummy_view(_request: WSGIRequest) -> str:
108
+ return 'success'
109
+
110
+ result = permission_required_decorator_function(
111
+ ['perm1', 'perm2'],
112
+ dummy_view,
113
+ request,
114
+ all_required=False
115
+ )
116
+ assert result == 'success'
117
+
118
+ def test_passes_args_to_view(self) -> None:
119
+ superuser = create_super_user()
120
+ request = self.factory.get('/test/')
121
+ request.user = superuser
122
+
123
+ def dummy_view(_request: WSGIRequest, arg1: str, arg2: str) -> str:
124
+ return f'{arg1}-{arg2}'
125
+
126
+ result = permission_required_decorator_function(
127
+ 'any.permission',
128
+ dummy_view,
129
+ request,
130
+ 'hello',
131
+ 'world'
132
+ )
133
+ assert result == 'hello-world'
134
+
135
+ def test_passes_kwargs_to_view(self) -> None:
136
+ superuser = create_super_user()
137
+ request = self.factory.get('/test/')
138
+ request.user = superuser
139
+
140
+ def dummy_view(_request: WSGIRequest, **kwargs) -> str:
141
+ return kwargs.get('key', 'default')
142
+
143
+ result = permission_required_decorator_function(
144
+ 'any.permission',
145
+ dummy_view,
146
+ request,
147
+ key='value'
148
+ )
149
+ assert result == 'value'
150
+
151
+ def test_single_permission_as_string(self) -> None:
152
+ superuser = create_super_user()
153
+ request = self.factory.get('/test/')
154
+ request.user = superuser
155
+
156
+ def dummy_view(_request: WSGIRequest) -> str:
157
+ return 'success'
158
+
159
+ result = permission_required_decorator_function(
160
+ 'single.permission',
161
+ dummy_view,
162
+ request
163
+ )
164
+ assert result == 'success'
165
+
166
+ def test_empty_permissions_list(self) -> None:
167
+ request = self.factory.get('/test/')
168
+ request.user = self.user
169
+
170
+ def dummy_view(_request: WSGIRequest) -> str:
171
+ return 'success'
172
+
173
+ result = permission_required_decorator_function(
174
+ [],
175
+ dummy_view,
176
+ request
177
+ )
178
+ assert result == 'success'
179
+
180
+
181
+ class PermissionRequiredDecoratorTestCase(BaseTestCase):
182
+ def setUp(self) -> None:
183
+ super().setUp()
184
+
185
+ self.factory = RequestFactory()
186
+ self.user = create_user(username='testuser')
187
+ self.user.set_password('password')
188
+ self.user.save()
189
+
190
+ def test_decorator_without_permission(self) -> None:
191
+ @permission_required('django_spire_auth_group.delete_authgroup')
192
+ def dummy_view(_request: WSGIRequest) -> str:
193
+ return 'success'
194
+
195
+ request = self.factory.get('/test/')
196
+ request.user = self.user
197
+
198
+ with pytest.raises(PermissionDenied):
199
+ dummy_view(request)
200
+
201
+ def test_decorator_superuser_succeeds(self) -> None:
202
+ superuser = create_super_user()
203
+
204
+ @permission_required('any.permission')
205
+ def dummy_view(_request: WSGIRequest) -> str:
206
+ return 'success'
207
+
208
+ request = self.factory.get('/test/')
209
+ request.user = superuser
210
+ result = dummy_view(request)
211
+ assert result == 'success'
212
+
213
+ def test_decorator_multiple_permissions_all_required_fails(self) -> None:
214
+ @permission_required(
215
+ 'django_spire_auth_group.view_authgroup',
216
+ 'django_spire_auth_group.change_authgroup',
217
+ all_required=True
218
+ )
219
+ def dummy_view(_request: WSGIRequest) -> str:
220
+ return 'success'
221
+
222
+ request = self.factory.get('/test/')
223
+ request.user = self.user
224
+
225
+ with pytest.raises(PermissionDenied):
226
+ dummy_view(request)
227
+
228
+ def test_decorator_preserves_function_name(self) -> None:
229
+ @permission_required('any.permission')
230
+ def my_special_view(_request: WSGIRequest) -> str:
231
+ return 'success'
232
+
233
+ assert my_special_view.__name__ == 'my_special_view'
234
+
235
+ def test_decorator_with_args(self) -> None:
236
+ superuser = create_super_user()
237
+
238
+ @permission_required('any.permission')
239
+ def dummy_view(_request: WSGIRequest, arg1: str, arg2: str) -> str:
240
+ return f'{arg1}-{arg2}'
241
+
242
+ request = self.factory.get('/test/')
243
+ request.user = superuser
244
+ result = dummy_view(request, 'hello', 'world')
245
+ assert result == 'hello-world'
246
+
247
+ def test_decorator_with_kwargs(self) -> None:
248
+ superuser = create_super_user()
249
+
250
+ @permission_required('any.permission')
251
+ def dummy_view(_request: WSGIRequest, **kwargs) -> str:
252
+ return kwargs.get('key', 'default')
253
+
254
+ request = self.factory.get('/test/')
255
+ request.user = superuser
256
+ result = dummy_view(request, key='value')
257
+ assert result == 'value'
258
+
259
+ def test_decorator_multiple_permissions_any_required_superuser(self) -> None:
260
+ superuser = create_super_user()
261
+
262
+ @permission_required('perm1', 'perm2', all_required=False)
263
+ def dummy_view(_request: WSGIRequest) -> str:
264
+ return 'success'
265
+
266
+ request = self.factory.get('/test/')
267
+ request.user = superuser
268
+ result = dummy_view(request)
269
+ assert result == 'success'
270
+
271
+ def test_decorator_empty_permissions(self) -> None:
272
+ @permission_required()
273
+ def dummy_view(_request: WSGIRequest) -> str:
274
+ return 'success'
275
+
276
+ request = self.factory.get('/test/')
277
+ request.user = self.user
278
+ result = dummy_view(request)
279
+ assert result == 'success'
280
+
281
+ def test_decorator_unauthenticated_redirects(self) -> None:
282
+ @permission_required('any.permission')
283
+ def dummy_view(_request: WSGIRequest) -> str:
284
+ return 'success'
285
+
286
+ request = self.factory.get('/test/')
287
+ request.user = type('AnonymousUser', (), {'is_authenticated': False})()
288
+
289
+ response = dummy_view(request)
290
+ assert response.status_code == 302
291
+
292
+ def test_decorator_view_returns_none(self) -> None:
293
+ superuser = create_super_user()
294
+
295
+ @permission_required('any.permission')
296
+ def dummy_view(_request: WSGIRequest) -> None:
297
+ return None
298
+
299
+ request = self.factory.get('/test/')
300
+ request.user = superuser
301
+ result = dummy_view(request)
302
+ assert result is None
303
+
304
+ def test_decorator_view_returns_dict(self) -> None:
305
+ superuser = create_super_user()
306
+
307
+ @permission_required('any.permission')
308
+ def dummy_view(_request: WSGIRequest) -> dict:
309
+ return {'key': 'value'}
310
+
311
+ request = self.factory.get('/test/')
312
+ request.user = superuser
313
+ result = dummy_view(request)
314
+ assert result == {'key': 'value'}
315
+
316
+ def test_decorator_single_permission_denied(self) -> None:
317
+ @permission_required('nonexistent.permission')
318
+ def dummy_view(_request: WSGIRequest) -> str:
319
+ return 'success'
320
+
321
+ request = self.factory.get('/test/')
322
+ request.user = self.user
323
+
324
+ with pytest.raises(PermissionDenied):
325
+ dummy_view(request)
326
+
327
+ def test_decorator_callable_preserves_docstring(self) -> None:
328
+ @permission_required('any.permission')
329
+ def dummy_view(_request: WSGIRequest) -> str:
330
+ """A docstring."""
331
+ return 'success'
332
+
333
+ assert dummy_view.__doc__ == 'A docstring.'
@@ -0,0 +1,337 @@
1
+ from __future__ import annotations
2
+
3
+ from django.contrib.auth.models import Permission
4
+ from django.contrib.contenttypes.models import ContentType
5
+
6
+ from django_spire.auth.group.models import AuthGroup
7
+ from django_spire.auth.permissions.permissions import (
8
+ GroupPermissions,
9
+ ModelPermission,
10
+ ModelPermissions,
11
+ UserPermissionHelper,
12
+ )
13
+ from django_spire.auth.user.tests.factories import create_user
14
+ from django_spire.core.tests.test_cases import BaseTestCase
15
+
16
+
17
+ class ModelPermissionTestCase(BaseTestCase):
18
+ def test_model_permission_attributes(self) -> None:
19
+ mp = ModelPermission(
20
+ name='test',
21
+ model_class_path='django_spire.auth.group.models.AuthGroup',
22
+ is_proxy_model=True,
23
+ verbose_name='Test Permission'
24
+ )
25
+ assert mp.name == 'test'
26
+ assert mp.model_class_path == 'django_spire.auth.group.models.AuthGroup'
27
+ assert mp.is_proxy_model
28
+ assert mp.verbose_name == 'Test Permission'
29
+
30
+ def test_model_class_property(self) -> None:
31
+ mp = ModelPermission(
32
+ name='group',
33
+ model_class_path='django_spire.auth.group.models.AuthGroup',
34
+ is_proxy_model=True
35
+ )
36
+ assert mp.model_class == AuthGroup
37
+
38
+ def test_model_permission_without_verbose_name(self) -> None:
39
+ mp = ModelPermission(
40
+ name='test',
41
+ model_class_path='django_spire.auth.group.models.AuthGroup',
42
+ is_proxy_model=True
43
+ )
44
+ assert mp.verbose_name is None
45
+
46
+ def test_model_permission_non_proxy(self) -> None:
47
+ mp = ModelPermission(
48
+ name='test',
49
+ model_class_path='django_spire.auth.group.models.AuthGroup',
50
+ is_proxy_model=False
51
+ )
52
+ assert not mp.is_proxy_model
53
+
54
+ def test_model_permission_name_case_preserved(self) -> None:
55
+ mp = ModelPermission(
56
+ name='TestName',
57
+ model_class_path='django_spire.auth.group.models.AuthGroup',
58
+ is_proxy_model=True
59
+ )
60
+ assert mp.name == 'TestName'
61
+
62
+ def test_model_permission_verbose_name_unicode(self) -> None:
63
+ mp = ModelPermission(
64
+ name='test',
65
+ model_class_path='django_spire.auth.group.models.AuthGroup',
66
+ is_proxy_model=True,
67
+ verbose_name='Tëst Përmission'
68
+ )
69
+ assert mp.verbose_name == 'Tëst Përmission'
70
+
71
+
72
+ class ModelPermissionsTestCase(BaseTestCase):
73
+ def setUp(self) -> None:
74
+ super().setUp()
75
+
76
+ self.model_permission = ModelPermission(
77
+ name='group',
78
+ model_class_path='django_spire.auth.group.models.AuthGroup',
79
+ is_proxy_model=True
80
+ )
81
+ self.model_permissions = ModelPermissions(self.model_permission)
82
+
83
+ def test_app_label(self) -> None:
84
+ assert self.model_permissions.app_label == 'django_spire_auth_group'
85
+
86
+ def test_model_name(self) -> None:
87
+ assert self.model_permissions.model_name == 'authgroup'
88
+
89
+ def test_permissions_queryset(self) -> None:
90
+ assert self.model_permissions.permissions.count() > 0
91
+
92
+ def test_find_permissions_by_level_none(self) -> None:
93
+ perms = self.model_permissions.find_permissions_by_level(0)
94
+ assert len(perms) == 0
95
+
96
+ def test_find_permissions_by_level_view(self) -> None:
97
+ perms = self.model_permissions.find_permissions_by_level(1)
98
+ codenames = [p.codename for p in perms]
99
+ assert 'view_authgroup' in codenames
100
+
101
+ def test_find_permissions_by_level_add(self) -> None:
102
+ perms = self.model_permissions.find_permissions_by_level(2)
103
+ codenames = [p.codename for p in perms]
104
+ assert 'view_authgroup' in codenames
105
+ assert 'add_authgroup' in codenames
106
+
107
+ def test_find_permissions_by_level_change(self) -> None:
108
+ perms = self.model_permissions.find_permissions_by_level(3)
109
+ codenames = [p.codename for p in perms]
110
+ assert 'view_authgroup' in codenames
111
+ assert 'add_authgroup' in codenames
112
+ assert 'change_authgroup' in codenames
113
+
114
+ def test_find_permissions_by_level_delete(self) -> None:
115
+ perms = self.model_permissions.find_permissions_by_level(4)
116
+ codenames = [p.codename for p in perms]
117
+ assert 'view_authgroup' in codenames
118
+ assert 'add_authgroup' in codenames
119
+ assert 'change_authgroup' in codenames
120
+ assert 'delete_authgroup' in codenames
121
+
122
+ def test_special_role_list(self) -> None:
123
+ roles = self.model_permissions.special_role_list()
124
+ for role in roles:
125
+ assert role.codename.startswith('can')
126
+
127
+ def test_get_special_role_not_found(self) -> None:
128
+ result = self.model_permissions.get_special_role('nonexistent_role')
129
+ assert result is None
130
+
131
+ def test_permissions_returns_queryset(self) -> None:
132
+ assert hasattr(self.model_permissions.permissions, 'filter')
133
+
134
+ def test_model_attribute(self) -> None:
135
+ assert self.model_permissions.model == AuthGroup
136
+
137
+ def test_is_proxy_model_attribute(self) -> None:
138
+ assert self.model_permissions.is_proxy_model
139
+
140
+ def test_find_permissions_by_level_returns_list(self) -> None:
141
+ perms = self.model_permissions.find_permissions_by_level(2)
142
+ assert isinstance(perms, list)
143
+
144
+ def test_special_role_list_returns_list(self) -> None:
145
+ roles = self.model_permissions.special_role_list()
146
+ assert isinstance(roles, list)
147
+
148
+
149
+ class GroupPermissionsTestCase(BaseTestCase):
150
+ def setUp(self) -> None:
151
+ super().setUp()
152
+
153
+ self.group = AuthGroup.objects.create(name='Test Group')
154
+ self.model_permission = ModelPermission(
155
+ name='group',
156
+ model_class_path='django_spire.auth.group.models.AuthGroup',
157
+ is_proxy_model=True
158
+ )
159
+ self.group_permissions = GroupPermissions(self.group, self.model_permission)
160
+
161
+ def test_initial_perm_level_is_zero(self) -> None:
162
+ assert self.group_permissions.perm_level() == 0
163
+
164
+ def test_perm_level_verbose_none(self) -> None:
165
+ assert self.group_permissions.perm_level_verbose() == 'None'
166
+
167
+ def test_update_perms_to_view(self) -> None:
168
+ self.group_permissions.update_perms(1)
169
+ assert self.group_permissions.perm_level() == 1
170
+ assert self.group_permissions.perm_level_verbose() == 'View'
171
+
172
+ def test_update_perms_to_add(self) -> None:
173
+ self.group_permissions.update_perms(2)
174
+ assert self.group_permissions.perm_level() == 2
175
+ assert self.group_permissions.perm_level_verbose() == 'Add'
176
+
177
+ def test_update_perms_to_change(self) -> None:
178
+ self.group_permissions.update_perms(3)
179
+ assert self.group_permissions.perm_level() == 3
180
+ assert self.group_permissions.perm_level_verbose() == 'Change'
181
+
182
+ def test_update_perms_to_delete(self) -> None:
183
+ self.group_permissions.update_perms(4)
184
+ assert self.group_permissions.perm_level() == 4
185
+ assert self.group_permissions.perm_level_verbose() == 'Delete'
186
+
187
+ def test_update_perms_downgrades(self) -> None:
188
+ self.group_permissions.update_perms(4)
189
+ self.group_permissions.update_perms(1)
190
+ assert self.group_permissions.perm_level() == 1
191
+
192
+ def test_update_perms_to_none(self) -> None:
193
+ self.group_permissions.update_perms(4)
194
+ self.group_permissions.update_perms(0)
195
+ assert self.group_permissions.perm_level() == 0
196
+
197
+ def test_remove_special_permissions(self) -> None:
198
+ content_type = ContentType.objects.get_for_model(AuthGroup)
199
+ perms = Permission.objects.filter(content_type=content_type)
200
+ filtered = GroupPermissions.remove_special_permissions(perms)
201
+ for perm in filtered:
202
+ assert not perm.codename.startswith('can')
203
+
204
+ def test_set_group_perms_updates(self) -> None:
205
+ self.group_permissions.update_perms(2)
206
+ self.group_permissions.set_group_perms()
207
+ assert self.group_permissions.perm_level() == 2
208
+
209
+ def test_has_special_role_false(self) -> None:
210
+ assert not self.group_permissions.has_special_role('nonexistent')
211
+
212
+ def test_group_attribute(self) -> None:
213
+ assert self.group_permissions.group == self.group
214
+
215
+ def test_model_permissions_attribute(self) -> None:
216
+ assert self.group_permissions.model_permissions is not None
217
+
218
+ def test_group_perms_attribute(self) -> None:
219
+ assert self.group_permissions.group_perms is not None
220
+
221
+ def test_update_perms_multiple_times(self) -> None:
222
+ for level in [1, 2, 3, 4, 3, 2, 1, 0]:
223
+ self.group_permissions.update_perms(level)
224
+ assert self.group_permissions.perm_level() == level
225
+
226
+ def test_perm_level_returns_int(self) -> None:
227
+ level = self.group_permissions.perm_level()
228
+ assert isinstance(level, int)
229
+
230
+ def test_perm_level_verbose_returns_str(self) -> None:
231
+ verbose = self.group_permissions.perm_level_verbose()
232
+ assert isinstance(verbose, str)
233
+
234
+ def test_remove_special_permissions_with_list(self) -> None:
235
+ content_type = ContentType.objects.get_for_model(AuthGroup)
236
+ perms = list(Permission.objects.filter(content_type=content_type))
237
+ filtered = GroupPermissions.remove_special_permissions(perms)
238
+ assert isinstance(filtered, list)
239
+
240
+
241
+ class UserPermissionHelperTestCase(BaseTestCase):
242
+ def setUp(self) -> None:
243
+ super().setUp()
244
+
245
+ self.user = create_user(username='testuser')
246
+ self.group = AuthGroup.objects.create(name='Test Group')
247
+ self.model_permission = ModelPermission(
248
+ name='group',
249
+ model_class_path='django_spire.auth.group.models.AuthGroup',
250
+ is_proxy_model=True
251
+ )
252
+
253
+ def test_initial_perm_level_is_zero(self) -> None:
254
+ helper = UserPermissionHelper(self.user, self.model_permission)
255
+ assert helper.perm_level() == 0
256
+
257
+ def test_perm_level_verbose_none(self) -> None:
258
+ helper = UserPermissionHelper(self.user, self.model_permission)
259
+ assert helper.perm_level_verbose() == 'None'
260
+
261
+ def test_user_inherits_group_permissions(self) -> None:
262
+ self.user.groups.add(self.group)
263
+ group_perms = GroupPermissions(self.group, self.model_permission)
264
+ group_perms.update_perms(3)
265
+
266
+ helper = UserPermissionHelper(self.user, self.model_permission)
267
+ assert helper.perm_level() == 3
268
+ assert helper.perm_level_verbose() == 'Change'
269
+
270
+ def test_user_inherits_highest_permission(self) -> None:
271
+ group2 = AuthGroup.objects.create(name='Group 2')
272
+ self.user.groups.add(self.group, group2)
273
+
274
+ group_perms1 = GroupPermissions(self.group, self.model_permission)
275
+ group_perms1.update_perms(1)
276
+
277
+ group_perms2 = GroupPermissions(group2, self.model_permission)
278
+ group_perms2.update_perms(4)
279
+
280
+ helper = UserPermissionHelper(self.user, self.model_permission)
281
+ assert helper.perm_level() == 4
282
+
283
+ def test_user_no_groups_no_permissions(self) -> None:
284
+ helper = UserPermissionHelper(self.user, self.model_permission)
285
+ assert helper.perm_level() == 0
286
+
287
+ def test_get_user_perms_returns_queryset(self) -> None:
288
+ helper = UserPermissionHelper(self.user, self.model_permission)
289
+ perms = helper.get_user_perms()
290
+ assert perms is not None
291
+
292
+ def test_user_attribute(self) -> None:
293
+ helper = UserPermissionHelper(self.user, self.model_permission)
294
+ assert helper.user == self.user
295
+
296
+ def test_model_permissions_attribute(self) -> None:
297
+ helper = UserPermissionHelper(self.user, self.model_permission)
298
+ assert helper.model_permissions is not None
299
+
300
+ def test_user_perms_attribute(self) -> None:
301
+ helper = UserPermissionHelper(self.user, self.model_permission)
302
+ assert helper.user_perms is not None
303
+
304
+ def test_perm_level_returns_int(self) -> None:
305
+ helper = UserPermissionHelper(self.user, self.model_permission)
306
+ level = helper.perm_level()
307
+ assert isinstance(level, int)
308
+
309
+ def test_perm_level_verbose_returns_str(self) -> None:
310
+ helper = UserPermissionHelper(self.user, self.model_permission)
311
+ verbose = helper.perm_level_verbose()
312
+ assert isinstance(verbose, str)
313
+
314
+ def test_user_removed_from_group_loses_permissions(self) -> None:
315
+ self.user.groups.add(self.group)
316
+ group_perms = GroupPermissions(self.group, self.model_permission)
317
+ group_perms.update_perms(3)
318
+
319
+ helper = UserPermissionHelper(self.user, self.model_permission)
320
+ assert helper.perm_level() == 3
321
+
322
+ self.user.groups.remove(self.group)
323
+ helper2 = UserPermissionHelper(self.user, self.model_permission)
324
+ assert helper2.perm_level() == 0
325
+
326
+ def test_multiple_groups_same_permission_level(self) -> None:
327
+ group2 = AuthGroup.objects.create(name='Group 2')
328
+ self.user.groups.add(self.group, group2)
329
+
330
+ group_perms1 = GroupPermissions(self.group, self.model_permission)
331
+ group_perms1.update_perms(2)
332
+
333
+ group_perms2 = GroupPermissions(group2, self.model_permission)
334
+ group_perms2.update_perms(2)
335
+
336
+ helper = UserPermissionHelper(self.user, self.model_permission)
337
+ assert helper.perm_level() == 2