django-spire 0.23.7__py3-none-any.whl → 0.23.9__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 (542) 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/queryset/enums.py +3 -1
  139. django_spire/contrib/queryset/filter_tools.py +10 -5
  140. django_spire/contrib/queryset/mixins.py +16 -16
  141. django_spire/contrib/queryset/tests/__init__.py +0 -0
  142. django_spire/contrib/queryset/tests/test_queryset.py +137 -0
  143. django_spire/contrib/seeding/field/base.py +13 -7
  144. django_spire/contrib/seeding/field/callable.py +8 -1
  145. django_spire/contrib/seeding/field/cleaners.py +5 -5
  146. django_spire/contrib/seeding/field/custom.py +20 -10
  147. django_spire/contrib/seeding/field/django/seeder.py +8 -6
  148. django_spire/contrib/seeding/field/enums.py +7 -5
  149. django_spire/contrib/seeding/field/override.py +16 -6
  150. django_spire/contrib/seeding/field/static.py +9 -2
  151. django_spire/contrib/seeding/field/tests/test_base.py +18 -14
  152. django_spire/contrib/seeding/field/tests/test_callable.py +13 -9
  153. django_spire/contrib/seeding/field/tests/test_cleaners.py +51 -38
  154. django_spire/contrib/seeding/field/tests/test_static.py +13 -9
  155. django_spire/contrib/seeding/intelligence/bots/seeder_generator_bot.py +2 -0
  156. django_spire/contrib/seeding/intelligence/intel.py +5 -1
  157. django_spire/contrib/seeding/intelligence/prompts/factory.py +6 -1
  158. django_spire/contrib/seeding/intelligence/prompts/foreign_key_selection_prompt.py +6 -1
  159. django_spire/contrib/seeding/intelligence/prompts/generate_django_model_seeder_prompts.py +2 -0
  160. django_spire/contrib/seeding/intelligence/prompts/generic_relationship_selection_prompt.py +7 -1
  161. django_spire/contrib/seeding/intelligence/prompts/hierarchical_selection_prompt.py +6 -2
  162. django_spire/contrib/seeding/intelligence/prompts/model_field_choices_prompt.py +8 -2
  163. django_spire/contrib/seeding/intelligence/prompts/negation_prompt.py +2 -0
  164. django_spire/contrib/seeding/intelligence/prompts/objective_prompt.py +6 -1
  165. django_spire/contrib/seeding/management/commands/seeding.py +9 -3
  166. django_spire/contrib/seeding/management/example.py +2 -0
  167. django_spire/contrib/seeding/model/base.py +16 -7
  168. django_spire/contrib/seeding/model/config.py +31 -15
  169. django_spire/contrib/seeding/model/django/config.py +13 -13
  170. django_spire/contrib/seeding/model/django/seeder.py +4 -4
  171. django_spire/contrib/seeding/model/django/tests/test_seeder.py +34 -23
  172. django_spire/contrib/seeding/model/enums.py +2 -0
  173. django_spire/contrib/seeding/tests/test_config.py +71 -0
  174. django_spire/contrib/seeding/tests/test_custom.py +35 -0
  175. django_spire/contrib/seeding/tests/test_enums.py +40 -0
  176. django_spire/contrib/seeding/tests/test_intel.py +32 -0
  177. django_spire/contrib/seeding/tests/test_override.py +63 -0
  178. django_spire/contrib/service/__init__.py +2 -2
  179. django_spire/contrib/service/django_model_service.py +16 -15
  180. django_spire/contrib/service/exceptions.py +5 -3
  181. django_spire/contrib/service/tests/__init__.py +0 -0
  182. django_spire/contrib/service/tests/test_service.py +153 -0
  183. django_spire/contrib/session/apps.py +2 -0
  184. django_spire/contrib/session/controller.py +48 -42
  185. django_spire/contrib/session/templatetags/session_tags.py +11 -2
  186. django_spire/contrib/session/tests/test_session_controller.py +117 -53
  187. django_spire/contrib/tests/__init__.py +0 -0
  188. django_spire/contrib/tests/test_utils.py +37 -0
  189. django_spire/contrib/utils.py +4 -1
  190. django_spire/core/apps.py +2 -0
  191. django_spire/core/converters/tests/test_to_data.py +353 -0
  192. django_spire/core/converters/tests/test_to_enums.py +61 -41
  193. django_spire/core/converters/tests/test_to_pydantic.py +138 -109
  194. django_spire/core/converters/to_data.py +29 -10
  195. django_spire/core/converters/to_enums.py +4 -2
  196. django_spire/core/converters/to_pydantic.py +22 -22
  197. django_spire/core/decorators.py +19 -6
  198. django_spire/core/forms/widgets.py +4 -0
  199. django_spire/core/management/commands/spire_startapp_pkg/template/app/apps.py.template +2 -0
  200. django_spire/core/management/commands/spire_startapp_pkg/template/app/urls/__init__.py.template +2 -0
  201. django_spire/core/management/commands/spire_startapp_pkg/template/app/urls/form_urls.py.template +2 -0
  202. django_spire/core/management/commands/spire_startapp_pkg/template/app/urls/page_urls.py.template +2 -0
  203. django_spire/core/maps.py +3 -1
  204. django_spire/core/middleware/maintenance.py +3 -3
  205. django_spire/core/middleware.py +8 -6
  206. django_spire/core/redirect/__init__.py +5 -0
  207. django_spire/core/redirect/generic_redirect.py +1 -2
  208. django_spire/core/redirect/tests/__init__.py +0 -0
  209. django_spire/core/redirect/tests/test_generic_redirect.py +34 -0
  210. django_spire/core/{tests/tests_redirect.py → redirect/tests/test_safe_redirect.py} +55 -81
  211. django_spire/core/shortcuts.py +3 -3
  212. django_spire/core/static/django_spire/css/app-layout.css +1 -1
  213. django_spire/core/static/django_spire/css/app-navigation.css +3 -3
  214. django_spire/core/static/django_spire/css/bootstrap-override.css +4 -0
  215. django_spire/core/tag/admin.py +12 -0
  216. django_spire/core/tag/intelligence/tag_set_bot.py +2 -0
  217. django_spire/core/tag/mixins.py +2 -0
  218. django_spire/core/tag/models.py +2 -0
  219. django_spire/core/tag/querysets.py +2 -0
  220. django_spire/core/tag/service/tag_service.py +6 -3
  221. django_spire/core/tag/tests/test_intelligence.py +9 -9
  222. django_spire/core/tag/tests/test_tags.py +44 -54
  223. django_spire/core/tag/tests/test_tools.py +191 -0
  224. django_spire/core/tag/tools.py +3 -0
  225. django_spire/core/templates/django_spire/card/card.html +5 -2
  226. django_spire/core/templates/django_spire/card/title_card.html +12 -4
  227. django_spire/core/templates/django_spire/infinite_scroll/base.html +1 -0
  228. django_spire/core/templates/django_spire/navigation/side_navigation.html +19 -24
  229. django_spire/core/templates/django_spire/page/full_page.html +46 -16
  230. django_spire/core/templates/django_spire/table/base.html +5 -3
  231. django_spire/core/templatetags/json.py +6 -2
  232. django_spire/core/templatetags/message.py +13 -8
  233. django_spire/core/templatetags/string_formating.py +8 -5
  234. django_spire/core/templatetags/tests/__init__.py +0 -0
  235. django_spire/core/templatetags/tests/test_templatetags.py +427 -0
  236. django_spire/core/templatetags/variable_types.py +17 -9
  237. django_spire/core/tests/test_cases.py +1 -1
  238. django_spire/core/tests/test_conf.py +43 -0
  239. django_spire/core/tests/test_consts.py +28 -0
  240. django_spire/core/tests/test_context_processors.py +93 -0
  241. django_spire/core/tests/test_decorators.py +95 -0
  242. django_spire/core/tests/test_django_spire_utils.py +56 -0
  243. django_spire/core/tests/test_exceptions.py +37 -0
  244. django_spire/core/tests/test_models.py +54 -0
  245. django_spire/core/tests/test_settings.py +45 -0
  246. django_spire/core/tests/test_shortcuts.py +74 -0
  247. django_spire/core/tests/test_urls.py +16 -0
  248. django_spire/core/tests/test_utils.py +58 -0
  249. django_spire/core/urls.py +4 -1
  250. django_spire/core/utils.py +12 -8
  251. django_spire/exceptions.py +16 -1
  252. django_spire/file/admin.py +4 -2
  253. django_spire/file/apps.py +8 -10
  254. django_spire/file/fields.py +7 -7
  255. django_spire/file/forms.py +1 -1
  256. django_spire/file/interfaces.py +15 -15
  257. django_spire/file/mixins.py +1 -4
  258. django_spire/file/models.py +3 -5
  259. django_spire/file/tests/factories.py +59 -0
  260. django_spire/file/tests/test_admin.py +69 -0
  261. django_spire/file/tests/test_apps.py +24 -0
  262. django_spire/file/tests/test_fields.py +114 -0
  263. django_spire/file/tests/test_forms.py +20 -0
  264. django_spire/file/tests/test_interfaces.py +183 -0
  265. django_spire/file/tests/test_models.py +82 -0
  266. django_spire/file/tests/test_querysets.py +102 -0
  267. django_spire/file/tests/test_utils.py +32 -0
  268. django_spire/file/tests/test_views.py +145 -0
  269. django_spire/file/tests/test_widgets.py +82 -0
  270. django_spire/file/tools.py +8 -2
  271. django_spire/file/views.py +7 -3
  272. django_spire/file/widgets.py +12 -12
  273. django_spire/help_desk/admin.py +15 -0
  274. django_spire/help_desk/apps.py +2 -0
  275. django_spire/help_desk/auth/controller.py +2 -0
  276. django_spire/help_desk/choices.py +2 -0
  277. django_spire/help_desk/enums.py +2 -0
  278. django_spire/help_desk/exceptions.py +31 -3
  279. django_spire/help_desk/forms.py +2 -0
  280. django_spire/help_desk/models.py +2 -0
  281. django_spire/help_desk/querysets.py +4 -1
  282. django_spire/help_desk/services/notification_service.py +26 -27
  283. django_spire/help_desk/services/service.py +2 -3
  284. django_spire/help_desk/tests/factories.py +8 -3
  285. django_spire/help_desk/tests/test_admin.py +41 -0
  286. django_spire/help_desk/tests/test_apps.py +41 -0
  287. django_spire/help_desk/tests/test_choices.py +50 -0
  288. django_spire/help_desk/tests/test_controller.py +87 -0
  289. django_spire/help_desk/tests/test_enums.py +18 -0
  290. django_spire/help_desk/tests/test_exceptions.py +37 -0
  291. django_spire/help_desk/tests/test_forms.py +89 -0
  292. django_spire/help_desk/tests/test_models.py +59 -0
  293. django_spire/help_desk/tests/test_querysets.py +38 -0
  294. django_spire/help_desk/tests/test_services/test_notification_service.py +15 -8
  295. django_spire/help_desk/tests/test_services/test_service.py +92 -0
  296. django_spire/help_desk/tests/test_urls/test_form_urls.py +6 -6
  297. django_spire/help_desk/tests/test_urls/test_page_urls.py +8 -9
  298. django_spire/help_desk/tests/test_views/test_form_views.py +46 -19
  299. django_spire/help_desk/tests/test_views/test_page_views.py +32 -9
  300. django_spire/help_desk/urls/__init__.py +4 -1
  301. django_spire/help_desk/urls/form_urls.py +3 -0
  302. django_spire/help_desk/urls/page_urls.py +3 -0
  303. django_spire/help_desk/views/form_views.py +13 -5
  304. django_spire/help_desk/views/page_views.py +11 -3
  305. django_spire/history/activity/admin.py +2 -0
  306. django_spire/history/activity/apps.py +3 -1
  307. django_spire/history/activity/mixins.py +13 -7
  308. django_spire/history/activity/models.py +6 -5
  309. django_spire/history/activity/querysets.py +2 -0
  310. django_spire/history/activity/tests/__init__.py +0 -0
  311. django_spire/history/activity/tests/test_activity.py +176 -0
  312. django_spire/history/admin.py +9 -2
  313. django_spire/history/choices.py +3 -0
  314. django_spire/history/models.py +5 -5
  315. django_spire/history/tests/test_admin.py +93 -0
  316. django_spire/history/tests/test_history.py +101 -0
  317. django_spire/history/tests/test_mixins.py +84 -0
  318. django_spire/history/viewed/admin.py +3 -1
  319. django_spire/history/viewed/apps.py +3 -1
  320. django_spire/history/viewed/models.py +2 -0
  321. django_spire/history/viewed/tests/__init__.py +0 -0
  322. django_spire/history/viewed/tests/test_viewed.py +46 -0
  323. django_spire/knowledge/auth/tests/__init__.py +0 -0
  324. django_spire/knowledge/auth/tests/test_controller.py +116 -0
  325. django_spire/knowledge/collection/admin.py +5 -1
  326. django_spire/knowledge/collection/models.py +3 -1
  327. django_spire/knowledge/collection/seeding/seed.py +1 -0
  328. django_spire/knowledge/collection/services/factory_service.py +10 -11
  329. django_spire/knowledge/collection/services/ordering_service.py +1 -2
  330. django_spire/knowledge/collection/services/service.py +5 -10
  331. django_spire/knowledge/collection/services/tag_service.py +5 -2
  332. django_spire/knowledge/collection/tests/factories.py +28 -1
  333. django_spire/knowledge/collection/tests/test_models.py +48 -0
  334. django_spire/knowledge/collection/tests/test_querysets.py +93 -0
  335. django_spire/knowledge/collection/tests/test_services/test_factory_service.py +100 -0
  336. django_spire/knowledge/collection/tests/test_services/test_services.py +160 -0
  337. django_spire/knowledge/collection/tests/test_urls/test_form_urls.py +21 -3
  338. django_spire/knowledge/collection/tests/test_urls/test_json_urls.py +39 -1
  339. django_spire/knowledge/collection/tests/test_urls/test_page_urls.py +12 -4
  340. django_spire/knowledge/collection/urls/__init__.py +3 -0
  341. django_spire/knowledge/collection/urls/form_urls.py +2 -0
  342. django_spire/knowledge/collection/urls/json_urls.py +2 -0
  343. django_spire/knowledge/collection/urls/page_urls.py +2 -0
  344. django_spire/knowledge/collection/views/form_views.py +4 -4
  345. django_spire/knowledge/collection/views/json_views.py +5 -1
  346. django_spire/knowledge/collection/views/page_views.py +5 -2
  347. django_spire/knowledge/entry/admin.py +7 -1
  348. django_spire/knowledge/entry/forms.py +2 -0
  349. django_spire/knowledge/entry/models.py +2 -0
  350. django_spire/knowledge/entry/seeding/seed.py +3 -0
  351. django_spire/knowledge/entry/services/automation_service.py +5 -4
  352. django_spire/knowledge/entry/services/factory_service.py +7 -5
  353. django_spire/knowledge/entry/services/service.py +4 -7
  354. django_spire/knowledge/entry/services/tag_service.py +0 -1
  355. django_spire/knowledge/entry/services/tool_service.py +1 -0
  356. django_spire/knowledge/entry/services/transformation_services.py +1 -5
  357. django_spire/knowledge/entry/tests/factories.py +1 -2
  358. django_spire/knowledge/entry/tests/test_factory_service.py +20 -0
  359. django_spire/knowledge/entry/tests/test_models.py +41 -0
  360. django_spire/knowledge/entry/tests/test_querysets.py +71 -0
  361. django_spire/knowledge/entry/tests/test_services.py +94 -0
  362. django_spire/knowledge/entry/tests/test_urls/test_form_urls.py +9 -14
  363. django_spire/knowledge/entry/tests/test_urls/test_json_urls.py +48 -5
  364. django_spire/knowledge/entry/tests/test_urls/test_page_urls.py +6 -8
  365. django_spire/knowledge/entry/tests/test_urls/test_template_urls.py +40 -0
  366. django_spire/knowledge/entry/urls/form_urls.py +2 -0
  367. django_spire/knowledge/entry/urls/json_urls.py +2 -0
  368. django_spire/knowledge/entry/urls/page_urls.py +2 -0
  369. django_spire/knowledge/entry/urls/template_urls.py +2 -0
  370. django_spire/knowledge/entry/version/block/choices.py +2 -0
  371. django_spire/knowledge/entry/version/block/data/data.py +1 -0
  372. django_spire/knowledge/entry/version/block/data/list/data.py +8 -13
  373. django_spire/knowledge/entry/version/block/data/list/maps.py +3 -0
  374. django_spire/knowledge/entry/version/block/data/list/meta.py +1 -2
  375. django_spire/knowledge/entry/version/block/data/list/tests/__init__.py +0 -0
  376. django_spire/knowledge/entry/version/block/data/list/tests/test_maps.py +32 -0
  377. django_spire/knowledge/entry/version/block/data/list/tests/test_meta.py +58 -0
  378. django_spire/knowledge/entry/version/block/data/maps.py +3 -6
  379. django_spire/knowledge/entry/version/block/models.py +7 -5
  380. django_spire/knowledge/entry/version/block/seeding/constants.py +5 -4
  381. django_spire/knowledge/entry/version/block/services/service.py +2 -3
  382. django_spire/knowledge/entry/version/block/tests/factories.py +4 -10
  383. django_spire/knowledge/entry/version/block/tests/test_choices.py +56 -0
  384. django_spire/knowledge/entry/version/block/tests/test_data.py +90 -0
  385. django_spire/knowledge/entry/version/block/tests/test_maps.py +37 -0
  386. django_spire/knowledge/entry/version/block/tests/test_models.py +55 -0
  387. django_spire/knowledge/entry/version/block/tests/test_querysets.py +35 -0
  388. django_spire/knowledge/entry/version/block/tests/test_services.py +65 -0
  389. django_spire/knowledge/entry/version/choices.py +2 -0
  390. django_spire/knowledge/entry/version/converters/converter.py +1 -1
  391. django_spire/knowledge/entry/version/converters/docx_converter.py +4 -7
  392. django_spire/knowledge/entry/version/converters/markdown_converter.py +20 -20
  393. django_spire/knowledge/entry/version/maps.py +4 -5
  394. django_spire/knowledge/entry/version/querysets.py +1 -1
  395. django_spire/knowledge/entry/version/seeding/seeder.py +1 -2
  396. django_spire/knowledge/entry/version/services/processor_service.py +5 -4
  397. django_spire/knowledge/entry/version/services/service.py +1 -2
  398. django_spire/knowledge/entry/version/tests/factories.py +2 -2
  399. django_spire/knowledge/entry/version/tests/test_choices.py +18 -0
  400. django_spire/knowledge/entry/version/tests/test_converters/test_docx_converter.py +56 -8
  401. django_spire/knowledge/entry/version/tests/test_converters/test_markdown_converter.py +78 -0
  402. django_spire/knowledge/entry/version/tests/test_maps.py +58 -0
  403. django_spire/knowledge/entry/version/tests/test_models.py +23 -0
  404. django_spire/knowledge/entry/version/tests/test_querysets.py +26 -0
  405. django_spire/knowledge/entry/version/tests/test_services.py +62 -0
  406. django_spire/knowledge/entry/version/tests/test_urls/test_json_urls.py +27 -8
  407. django_spire/knowledge/entry/version/tests/test_urls/test_page_urls.py +15 -8
  408. django_spire/knowledge/entry/version/tests/test_urls/test_redirect_urls.py +38 -0
  409. django_spire/knowledge/entry/version/urls/__init__.py +3 -0
  410. django_spire/knowledge/entry/version/urls/json_urls.py +2 -1
  411. django_spire/knowledge/entry/version/urls/page_urls.py +2 -0
  412. django_spire/knowledge/entry/version/urls/redirect_urls.py +2 -0
  413. django_spire/knowledge/entry/version/views/json_views.py +5 -1
  414. django_spire/knowledge/entry/version/views/page_views.py +10 -3
  415. django_spire/knowledge/entry/version/views/redirect_views.py +5 -1
  416. django_spire/knowledge/entry/views/form_views.py +16 -8
  417. django_spire/knowledge/entry/views/json_views.py +3 -1
  418. django_spire/knowledge/entry/views/page_views.py +8 -2
  419. django_spire/knowledge/entry/views/template_views.py +7 -1
  420. django_spire/knowledge/exceptions.py +2 -1
  421. django_spire/knowledge/intelligence/intel/answer_intel.py +2 -1
  422. django_spire/knowledge/intelligence/intel/entry_intel.py +0 -1
  423. django_spire/knowledge/intelligence/workflows/knowledge_workflow.py +4 -5
  424. django_spire/knowledge/models.py +1 -2
  425. django_spire/knowledge/tests/__init__.py +0 -0
  426. django_spire/knowledge/tests/test_templatetags.py +40 -0
  427. django_spire/knowledge/tests/test_urls/__init__.py +0 -0
  428. django_spire/knowledge/tests/test_urls/test_page_urls.py +24 -0
  429. django_spire/knowledge/urls/__init__.py +2 -0
  430. django_spire/knowledge/urls/page_urls.py +2 -0
  431. django_spire/knowledge/views/page_views.py +8 -3
  432. django_spire/notification/admin.py +3 -1
  433. django_spire/notification/app/admin.py +2 -0
  434. django_spire/notification/app/apps.py +3 -1
  435. django_spire/notification/app/exceptions.py +9 -2
  436. django_spire/notification/app/models.py +8 -4
  437. django_spire/notification/app/processor.py +22 -26
  438. django_spire/notification/app/querysets.py +2 -0
  439. django_spire/notification/app/tests/__init__.py +0 -0
  440. django_spire/notification/app/tests/factories.py +34 -0
  441. django_spire/notification/app/tests/test_apps.py +24 -0
  442. django_spire/notification/app/tests/test_models.py +72 -0
  443. django_spire/notification/app/tests/test_processor.py +111 -0
  444. django_spire/notification/app/tests/test_querysets.py +90 -0
  445. django_spire/notification/app/tests/test_views/__init__.py +0 -0
  446. django_spire/notification/app/tests/test_views/test_json_views.py +48 -0
  447. django_spire/notification/app/tests/test_views/test_page_views.py +19 -0
  448. django_spire/notification/app/urls/__init__.py +3 -1
  449. django_spire/notification/app/urls/json_urls.py +6 -4
  450. django_spire/notification/app/urls/page_urls.py +4 -3
  451. django_spire/notification/app/urls/template_urls.py +4 -2
  452. django_spire/notification/apps.py +4 -1
  453. django_spire/notification/email/admin.py +5 -1
  454. django_spire/notification/email/apps.py +3 -1
  455. django_spire/notification/email/exceptions.py +4 -2
  456. django_spire/notification/email/helper.py +5 -3
  457. django_spire/notification/email/models.py +4 -0
  458. django_spire/notification/email/processor.py +19 -15
  459. django_spire/notification/email/querysets.py +3 -0
  460. django_spire/notification/email/tests/__init__.py +0 -0
  461. django_spire/notification/email/tests/factories.py +35 -0
  462. django_spire/notification/email/tests/test_apps.py +24 -0
  463. django_spire/notification/email/tests/test_models.py +52 -0
  464. django_spire/notification/email/tests/test_processor.py +92 -0
  465. django_spire/notification/email/tests/test_querysets.py +43 -0
  466. django_spire/notification/exceptions.py +17 -2
  467. django_spire/notification/managers.py +7 -1
  468. django_spire/notification/maps.py +4 -1
  469. django_spire/notification/mixins.py +2 -0
  470. django_spire/notification/models.py +3 -1
  471. django_spire/notification/processors/notification.py +12 -5
  472. django_spire/notification/processors/processor.py +2 -0
  473. django_spire/notification/processors/tests/__init__.py +0 -0
  474. django_spire/notification/processors/tests/test_notification.py +106 -0
  475. django_spire/notification/push/admin.py +10 -1
  476. django_spire/notification/push/apps.py +3 -1
  477. django_spire/notification/push/models.py +2 -3
  478. django_spire/notification/push/tests/__init__.py +0 -0
  479. django_spire/notification/push/tests/test_apps.py +24 -0
  480. django_spire/notification/push/tests/test_models.py +28 -0
  481. django_spire/notification/querysets.py +7 -1
  482. django_spire/notification/sms/admin.py +2 -0
  483. django_spire/notification/sms/apps.py +4 -1
  484. django_spire/notification/sms/automations.py +2 -0
  485. django_spire/notification/sms/choices.py +2 -0
  486. django_spire/notification/sms/exceptions.py +19 -5
  487. django_spire/notification/sms/helper.py +33 -23
  488. django_spire/notification/sms/models.py +5 -1
  489. django_spire/notification/sms/processor.py +20 -20
  490. django_spire/notification/sms/querysets.py +2 -0
  491. django_spire/notification/sms/tests/factories.py +33 -0
  492. django_spire/notification/sms/tests/test_apps.py +24 -0
  493. django_spire/notification/sms/tests/test_automation.py +38 -0
  494. django_spire/notification/sms/tests/test_choices.py +15 -0
  495. django_spire/notification/sms/tests/test_consts.py +17 -0
  496. django_spire/notification/sms/tests/test_exceptions.py +27 -0
  497. django_spire/notification/sms/tests/test_helper.py +50 -0
  498. django_spire/notification/sms/tests/test_models.py +81 -0
  499. django_spire/notification/sms/tests/test_processor.py +107 -0
  500. django_spire/notification/sms/tests/test_tools.py +25 -11
  501. django_spire/notification/sms/tools.py +16 -5
  502. django_spire/notification/sms/urls/__init__.py +3 -1
  503. django_spire/notification/sms/urls/media_urls.py +2 -0
  504. django_spire/notification/sms/views/media_views.py +14 -4
  505. django_spire/notification/tests/__init__.py +0 -0
  506. django_spire/notification/tests/factories.py +26 -0
  507. django_spire/notification/tests/test_admin.py +55 -0
  508. django_spire/notification/tests/test_apps.py +30 -0
  509. django_spire/notification/tests/test_automation.py +18 -0
  510. django_spire/notification/tests/test_choices.py +59 -0
  511. django_spire/notification/tests/test_exceptions.py +58 -0
  512. django_spire/notification/tests/test_managers.py +100 -0
  513. django_spire/notification/tests/test_maps.py +31 -0
  514. django_spire/notification/tests/test_models.py +76 -0
  515. django_spire/notification/tests/test_querysets.py +184 -0
  516. django_spire/notification/tests/test_utils.py +23 -0
  517. django_spire/notification/urls.py +3 -1
  518. django_spire/notification/utils.py +3 -1
  519. django_spire/settings.py +3 -0
  520. django_spire/theme/tests/test_context_processor.py +15 -13
  521. django_spire/theme/tests/test_enums.py +2 -2
  522. django_spire/theme/tests/test_filesystem.py +2 -5
  523. django_spire/theme/tests/test_integration.py +12 -12
  524. django_spire/theme/tests/test_model.py +40 -38
  525. django_spire/theme/tests/test_views/test_json_views.py +33 -33
  526. django_spire/theme/urls/json_urls.py +3 -0
  527. django_spire/theme/urls/page_urls.py +3 -0
  528. django_spire/urls.py +19 -15
  529. django_spire/utils.py +13 -4
  530. {django_spire-0.23.7.dist-info → django_spire-0.23.9.dist-info}/METADATA +2 -2
  531. {django_spire-0.23.7.dist-info → django_spire-0.23.9.dist-info}/RECORD +534 -362
  532. {django_spire-0.23.7.dist-info → django_spire-0.23.9.dist-info}/licenses/LICENSE.md +1 -1
  533. django_spire/contrib/options/tests/test_unit.py +0 -148
  534. django_spire/contrib/seeding/tests/test_seeding.py +0 -25
  535. django_spire/core/tests/test_templatetags.py +0 -117
  536. django_spire/core/tests/tests_shortcuts.py +0 -73
  537. django_spire/history/activity/tests.py +0 -3
  538. django_spire/history/activity/views.py +0 -3
  539. django_spire/knowledge/collection/tests/test_services/test_transformation_service.py +0 -71
  540. django_spire/notification/app/tests.py +0 -3
  541. {django_spire-0.23.7.dist-info → django_spire-0.23.9.dist-info}/WHEEL +0 -0
  542. {django_spire-0.23.7.dist-info → django_spire-0.23.9.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from typing import TYPE_CHECKING
4
+
3
5
  from django.http import HttpResponse, HttpResponseForbidden
4
6
  from django.views.decorators.csrf import csrf_exempt
5
7
  from django.views.decorators.http import require_POST
@@ -9,11 +11,14 @@ from django_spire.ai.sms.decorators import twilio_auth_required
9
11
  from django_spire.ai.sms.intelligence.workflows.sms_conversation_workflow import sms_conversation_workflow
10
12
  from django_spire.ai.sms.models import SmsConversation
11
13
 
14
+ if TYPE_CHECKING:
15
+ from django.core.handlers.wsgi import WSGIRequest
16
+
12
17
 
13
18
  @csrf_exempt
14
19
  @require_POST
15
20
  @twilio_auth_required
16
- def webhook_view(request):
21
+ def webhook_view(request: WSGIRequest):
17
22
  from_number = request.POST.get('From', '')
18
23
 
19
24
  if len(from_number) < 11:
@@ -32,30 +37,24 @@ def webhook_view(request):
32
37
  twilio_sid=message_sid,
33
38
  )
34
39
 
35
- try:
36
- sms_intel = sms_conversation_workflow(
37
- request=request,
38
- user_input=body,
39
- message_history=conversation.generate_message_history(),
40
- actor=from_number,
41
- )
42
-
43
- twiml_response = MessagingResponse()
44
- twiml_response.message(sms_intel.body)
45
-
46
- conversation.add_message(
47
- body=sms_intel.body,
48
- is_inbound=False,
49
- twilio_sid=message_sid,
50
- is_processed=True
51
- )
52
-
53
- message.is_processed = True
54
- message.save()
40
+ sms_intel = sms_conversation_workflow(
41
+ request=request,
42
+ user_input=body,
43
+ message_history=conversation.generate_message_history(),
44
+ actor=from_number,
45
+ )
55
46
 
56
- return HttpResponse(twiml_response)
57
- except:
58
- raise
47
+ twiml_response = MessagingResponse()
48
+ twiml_response.message(sms_intel.body)
59
49
 
50
+ conversation.add_message(
51
+ body=sms_intel.body,
52
+ is_inbound=False,
53
+ twilio_sid=message_sid,
54
+ is_processed=True
55
+ )
60
56
 
57
+ message.is_processed = True
58
+ message.save()
61
59
 
60
+ return HttpResponse(twiml_response)
@@ -3,16 +3,20 @@ from __future__ import annotations
3
3
  from dandy import BaseIntel, Bot, recorder_to_html_file
4
4
 
5
5
  from django_spire.ai.decorators import log_ai_interaction_from_recorder
6
+ from django_spire.ai.models import AiInteraction, AiUsage
6
7
  from django_spire.core.tests.test_cases import BaseTestCase
7
8
 
8
9
 
9
- class AiTestCase(BaseTestCase):
10
- def test_ai_interaction_decorator(self):
11
- class HorseIntel(BaseIntel):
12
- first_name: str
13
- breed: str
14
- color: str
15
- has_cone_taped_to_head: bool
10
+ class HorseIntel(BaseIntel):
11
+ breed: str
12
+ color: str
13
+ first_name: str
14
+ has_cone_taped_to_head: bool
15
+
16
+
17
+ class AiDecoratorTestCase(BaseTestCase):
18
+ def test_ai_interaction_decorator_creates_usage_record(self) -> None:
19
+ initial_count = AiUsage.objects.count()
16
20
 
17
21
  @log_ai_interaction_from_recorder(self.super_user, 'horse')
18
22
  @recorder_to_html_file('horse')
@@ -26,3 +30,123 @@ class AiTestCase(BaseTestCase):
26
30
  horse_intel = generate_horse_intel('Make me a magical horse that grants wishes!')
27
31
 
28
32
  assert horse_intel.first_name != ''
33
+ assert AiUsage.objects.count() >= initial_count
34
+
35
+ def test_ai_interaction_decorator_creates_interaction_record(self) -> None:
36
+ initial_count = AiInteraction.objects.count()
37
+
38
+ @log_ai_interaction_from_recorder(self.super_user, 'test_actor')
39
+ @recorder_to_html_file('test_interaction')
40
+ def generate_horse_intel(user_input: str) -> HorseIntel:
41
+ bot = Bot()
42
+ return bot.llm.prompt_to_intel(
43
+ prompt=user_input,
44
+ intel_class=HorseIntel,
45
+ )
46
+
47
+ generate_horse_intel('Create a horse')
48
+
49
+ assert AiInteraction.objects.count() > initial_count
50
+
51
+ def test_ai_interaction_decorator_requires_user_or_actor(self) -> None:
52
+ try:
53
+ @log_ai_interaction_from_recorder()
54
+ def dummy_func() -> None:
55
+ pass
56
+ except ValueError as e:
57
+ assert 'user or actor must be provided' in str(e)
58
+ else:
59
+ assert False, 'Expected ValueError'
60
+
61
+ def test_ai_interaction_decorator_with_actor_only(self) -> None:
62
+ @log_ai_interaction_from_recorder(actor='test_actor_only')
63
+ @recorder_to_html_file('test_actor_only')
64
+ def generate_horse_intel(user_input: str) -> HorseIntel:
65
+ bot = Bot()
66
+ return bot.llm.prompt_to_intel(
67
+ prompt=user_input,
68
+ intel_class=HorseIntel,
69
+ )
70
+
71
+ horse_intel = generate_horse_intel('Create a horse')
72
+
73
+ assert horse_intel is not None
74
+
75
+ interaction = AiInteraction.objects.filter(actor='test_actor_only').first()
76
+ assert interaction is not None
77
+ assert interaction.actor == 'test_actor_only'
78
+
79
+ def test_ai_interaction_decorator_records_module_and_callable(self) -> None:
80
+ @log_ai_interaction_from_recorder(self.super_user, 'module_test')
81
+ @recorder_to_html_file('module_test')
82
+ def test_callable(user_input: str) -> HorseIntel:
83
+ bot = Bot()
84
+ return bot.llm.prompt_to_intel(
85
+ prompt=user_input,
86
+ intel_class=HorseIntel,
87
+ )
88
+
89
+ test_callable('Test input')
90
+
91
+ interaction = AiInteraction.objects.filter(actor='module_test').first()
92
+ assert interaction is not None
93
+ assert 'test_callable' in interaction.callable_name
94
+
95
+
96
+ class AiUsageModelTestCase(BaseTestCase):
97
+ def test_ai_usage_str(self) -> None:
98
+ ai_usage = AiUsage.objects.create()
99
+
100
+ assert 'ai usage' in str(ai_usage)
101
+
102
+ def test_ai_usage_default_values(self) -> None:
103
+ ai_usage = AiUsage.objects.create()
104
+
105
+ assert ai_usage.event_count == 0
106
+ assert ai_usage.token_usage == 0
107
+ assert ai_usage.run_time_seconds == 0.0
108
+ assert ai_usage.was_successful is True
109
+
110
+
111
+ class AiInteractionModelTestCase(BaseTestCase):
112
+ def test_ai_interaction_str(self) -> None:
113
+ ai_usage = AiUsage.objects.create()
114
+ ai_interaction = AiInteraction.objects.create(
115
+ ai_usage=ai_usage,
116
+ actor='test_actor',
117
+ module_name='test_module',
118
+ callable_name='test_callable',
119
+ )
120
+
121
+ assert 'test_actor' in str(ai_interaction)
122
+ assert 'interaction' in str(ai_interaction)
123
+
124
+ def test_ai_interaction_saves_user_info(self) -> None:
125
+ ai_usage = AiUsage.objects.create()
126
+ ai_interaction = AiInteraction.objects.create(
127
+ ai_usage=ai_usage,
128
+ user=self.super_user,
129
+ actor=None,
130
+ module_name='test_module',
131
+ callable_name='test_callable',
132
+ )
133
+
134
+ assert ai_interaction.user_email == self.super_user.email
135
+ assert ai_interaction.user_first_name == self.super_user.first_name
136
+ assert ai_interaction.user_last_name == self.super_user.last_name
137
+
138
+ def test_ai_interaction_default_values(self) -> None:
139
+ ai_usage = AiUsage.objects.create()
140
+ ai_interaction = AiInteraction.objects.create(
141
+ ai_usage=ai_usage,
142
+ actor='test',
143
+ module_name='test',
144
+ callable_name='test',
145
+ )
146
+
147
+ assert ai_interaction.event_count == 0
148
+ assert ai_interaction.token_usage == 0
149
+ assert ai_interaction.run_time_seconds == 0.0
150
+ assert ai_interaction.was_successful is True
151
+ assert ai_interaction.exception is None
152
+ assert ai_interaction.stack_trace is None
django_spire/auth/apps.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from django.apps import AppConfig
2
4
 
3
5
  from django_spire.utils import check_required_apps
@@ -9,9 +11,9 @@ class AuthConfig(AppConfig):
9
11
  name = 'django_spire.auth'
10
12
 
11
13
  REQUIRED_APPS = ('django_spire_core',)
12
-
14
+
13
15
  URLPATTERNS_INCLUDE = 'django_spire.auth.urls'
14
16
  URLPATTERNS_NAMESPACE = 'auth'
15
17
 
16
18
  def ready(self) -> None:
17
- check_required_apps(self.label)
19
+ check_required_apps(self.label)
@@ -1,23 +1,34 @@
1
+ from __future__ import annotations
2
+
1
3
  import functools
2
4
 
5
+ from typing import TYPE_CHECKING
6
+
3
7
  from django.core.exceptions import PermissionDenied
4
- from django.core.handlers.wsgi import WSGIRequest
5
8
 
9
+ from django_spire.auth.controller.exceptions import (
10
+ AuthControllerNotFoundError,
11
+ AuthControllerRequestError
12
+ )
6
13
  from django_spire.auth.permissions.decorators import permission_required_decorator_function
14
+ from django_spire.conf import settings
7
15
  from django_spire.core.utils import get_object_from_module_string
8
16
 
17
+ if TYPE_CHECKING:
18
+ from typing import Any, Callable
19
+
20
+ from django.core.handlers.wsgi import WSGIRequest
21
+
9
22
 
10
23
  class BaseAuthController:
11
- def __init__(
12
- self,
13
- request: WSGIRequest | None = None
14
- ):
24
+ def __init__(self, request: WSGIRequest | None = None):
15
25
  self._request = request
16
26
 
17
27
  @property
18
28
  def request(self):
19
29
  if self._request is None:
20
- raise Exception('AuthController.request is None')
30
+ message = 'AuthController.request is None'
31
+ raise AuthControllerRequestError(message)
21
32
 
22
33
  return self._request
23
34
 
@@ -26,12 +37,11 @@ class BaseAuthController:
26
37
  self._request = value
27
38
 
28
39
  def permission_required(
29
- self,
30
- *permissions: str,
31
- all_required: bool = True
40
+ self,
41
+ *permissions: str,
42
+ all_required: bool = True
32
43
  ):
33
-
34
- def decorator(method):
44
+ def decorator(method: Callable[..., Any]):
35
45
  @functools.wraps(method)
36
46
  def wrapper(request: WSGIRequest, *args, **kwargs):
37
47
  self.request = request
@@ -40,12 +50,16 @@ class BaseAuthController:
40
50
 
41
51
  for perm in permissions:
42
52
  callable_permission = (
43
- getattr(self, perm) if hasattr(self, perm) else perm
53
+ getattr(self, perm)
54
+ if hasattr(self, perm)
55
+ else perm
44
56
  )
57
+
45
58
  if callable(callable_permission):
46
59
  if not all_required and callable_permission():
47
60
  return method(request, *args, **kwargs)
48
- elif not callable_permission():
61
+
62
+ if not callable_permission():
49
63
  raise PermissionDenied
50
64
 
51
65
  else:
@@ -67,18 +81,17 @@ class BaseAuthController:
67
81
 
68
82
  class AppAuthController:
69
83
  def __new__(
70
- cls,
71
- app_name: str,
72
- request: WSGIRequest | None = None,
73
- **kwargs
84
+ cls,
85
+ app_name: str,
86
+ request: WSGIRequest | None = None,
87
+ **kwargs: dict[str, Any]
74
88
  ):
75
- from django_spire.conf import settings
76
-
77
89
  if app_name not in settings.DJANGO_SPIRE_AUTH_CONTROLLERS:
78
- raise Exception(f'Controller {app_name} not found in settings.AUTH_CONTROLLERS')
90
+ message = f'Controller {app_name} not found in settings.AUTH_CONTROLLERS'
91
+ raise AuthControllerNotFoundError(message)
79
92
 
80
93
  try:
81
94
  return get_object_from_module_string(settings.DJANGO_SPIRE_AUTH_CONTROLLERS[app_name])(request)
82
-
83
- except ModuleNotFoundError:
84
- raise Exception(f'Auth Controller for {app_name} not found')
95
+ except ModuleNotFoundError as err:
96
+ message = f'Auth Controller for {app_name} not found'
97
+ raise AuthControllerNotFoundError(message) from err
@@ -0,0 +1,9 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class AuthControllerRequestError(Exception):
5
+ pass
6
+
7
+
8
+ class AuthControllerNotFoundError(Exception):
9
+ pass
@@ -5,6 +5,7 @@ from django.urls import reverse
5
5
  from django.utils.html import format_html
6
6
 
7
7
  import django_spire.auth.user.models
8
+
8
9
  from django_spire.auth.group import models
9
10
  from django_spire.auth.user.tools import add_user_to_all_user_group
10
11
 
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from django.apps import AppConfig
2
4
 
3
5
  from django_spire.utils import check_required_apps
@@ -3,13 +3,22 @@ from __future__ import annotations
3
3
  from django_spire.auth.group.models import AuthGroup
4
4
 
5
5
 
6
- def bulk_create_groups_from_names(names: list[str]):
7
- existing_groups = AuthGroup.objects.all().values_list('name', flat=True)
8
-
9
- new_groups = [
10
- AuthGroup(name=name)
11
- for name in names
12
- if name not in existing_groups
13
- ]
6
+ def bulk_create_groups_from_names(names: list[str]) -> list[AuthGroup]:
7
+ existing_groups = set(AuthGroup.objects.all().values_list('name', flat=True))
8
+ seen_names = set()
9
+
10
+ new_groups = []
11
+
12
+ for name in names:
13
+ if name in existing_groups or name in seen_names:
14
+ continue
15
+
16
+ sanitized_name = name.replace('\x00', '')
17
+
18
+ if sanitized_name in existing_groups or sanitized_name in seen_names:
19
+ continue
20
+
21
+ seen_names.add(sanitized_name)
22
+ new_groups.append(AuthGroup(name=sanitized_name))
14
23
 
15
24
  return AuthGroup.objects.bulk_create(new_groups)
@@ -12,6 +12,12 @@ class GroupNamesField(forms.CharField):
12
12
  """Receives a list of group names as a json string"""
13
13
 
14
14
  def clean(self, value) -> list[str]:
15
+ value = super().clean(value)
16
+
17
+ if value in (None, ''):
18
+ message = 'This field is required.'
19
+ raise forms.ValidationError(message)
20
+
15
21
  return json.loads(value)
16
22
 
17
23
 
@@ -31,6 +37,7 @@ class GroupForm(forms.ModelForm):
31
37
  raise forms.ValidationError(message)
32
38
 
33
39
  return name
40
+
34
41
  class Meta:
35
42
  model = Group
36
43
  exclude = ['permissions']
@@ -0,0 +1,146 @@
1
+ from __future__ import annotations
2
+
3
+ from django_spire.auth.group.factories import bulk_create_groups_from_names
4
+ from django_spire.auth.group.models import AuthGroup
5
+ from django_spire.core.tests.test_cases import BaseTestCase
6
+
7
+
8
+ class BulkCreateGroupsFromNamesTestCase(BaseTestCase):
9
+ def test_creates_new_groups(self) -> None:
10
+ names = ['Group A', 'Group B', 'Group C']
11
+ groups = bulk_create_groups_from_names(names)
12
+ assert len(groups) == 3
13
+ assert AuthGroup.objects.filter(name__in=names).count() == 3
14
+
15
+ def test_skips_existing_groups(self) -> None:
16
+ AuthGroup.objects.create(name='Existing Group')
17
+ names = ['Existing Group', 'New Group']
18
+ groups = bulk_create_groups_from_names(names)
19
+ assert len(groups) == 1
20
+ assert groups[0].name == 'New Group'
21
+
22
+ def test_empty_list(self) -> None:
23
+ groups = bulk_create_groups_from_names([])
24
+ assert len(groups) == 0
25
+
26
+ def test_all_existing_groups(self) -> None:
27
+ AuthGroup.objects.create(name='Group A')
28
+ AuthGroup.objects.create(name='Group B')
29
+ groups = bulk_create_groups_from_names(['Group A', 'Group B'])
30
+ assert len(groups) == 0
31
+
32
+ def test_single_group(self) -> None:
33
+ groups = bulk_create_groups_from_names(['Single Group'])
34
+ assert len(groups) == 1
35
+ assert groups[0].name == 'Single Group'
36
+
37
+ def test_unique_names_only(self) -> None:
38
+ groups = bulk_create_groups_from_names(['Group A', 'Group B'])
39
+ assert len(groups) == 2
40
+
41
+ def test_groups_are_persisted(self) -> None:
42
+ bulk_create_groups_from_names(['Persisted Group'])
43
+ assert AuthGroup.objects.filter(name='Persisted Group').exists()
44
+
45
+ def test_mixed_existing_and_new(self) -> None:
46
+ AuthGroup.objects.create(name='Existing 1')
47
+ AuthGroup.objects.create(name='Existing 2')
48
+ names = ['Existing 1', 'New 1', 'Existing 2', 'New 2']
49
+ groups = bulk_create_groups_from_names(names)
50
+ assert len(groups) == 2
51
+ group_names = [g.name for g in groups]
52
+ assert 'New 1' in group_names
53
+ assert 'New 2' in group_names
54
+
55
+ def test_special_characters_in_names(self) -> None:
56
+ names = ['Group & Co', 'Test <Group>', 'Group "Name"']
57
+ groups = bulk_create_groups_from_names(names)
58
+ assert len(groups) == 3
59
+
60
+ def test_unicode_names(self) -> None:
61
+ names = ['Tëst Grøup', '日本語グループ', 'Группа']
62
+ groups = bulk_create_groups_from_names(names)
63
+ assert len(groups) == 3
64
+
65
+ def test_whitespace_names(self) -> None:
66
+ names = [' Spaces ', 'Tab\tGroup']
67
+ groups = bulk_create_groups_from_names(names)
68
+ assert len(groups) == 2
69
+
70
+ def test_many_groups(self) -> None:
71
+ names = [f'Group {i}' for i in range(50)]
72
+ groups = bulk_create_groups_from_names(names)
73
+ assert len(groups) == 50
74
+
75
+ def test_returns_group_instances(self) -> None:
76
+ groups = bulk_create_groups_from_names(['Test Group'])
77
+ assert isinstance(groups[0], AuthGroup)
78
+
79
+ def test_groups_have_primary_keys(self) -> None:
80
+ groups = bulk_create_groups_from_names(['PK Test Group'])
81
+ for group in groups:
82
+ assert group.pk is not None
83
+
84
+ def test_duplicate_names_in_input_list(self) -> None:
85
+ names = ['Duplicate', 'Duplicate', 'Duplicate']
86
+ groups = bulk_create_groups_from_names(names)
87
+ assert AuthGroup.objects.filter(name='Duplicate').count() >= 1
88
+
89
+ def test_empty_string_name(self) -> None:
90
+ names = ['', 'Valid Group']
91
+ groups = bulk_create_groups_from_names(names)
92
+ assert len(groups) >= 1
93
+
94
+ def test_very_long_name(self) -> None:
95
+ long_name = 'A' * 150
96
+ groups = bulk_create_groups_from_names([long_name])
97
+ assert len(groups) == 1
98
+ assert groups[0].name == long_name
99
+
100
+ def test_name_with_only_whitespace(self) -> None:
101
+ names = [' ', '\t\t', '\n\n']
102
+ groups = bulk_create_groups_from_names(names)
103
+ assert len(groups) == 3
104
+
105
+ def test_name_with_leading_trailing_whitespace(self) -> None:
106
+ names = [' Leading', 'Trailing ', ' Both ']
107
+ groups = bulk_create_groups_from_names(names)
108
+ assert len(groups) == 3
109
+
110
+ def test_case_sensitive_names(self) -> None:
111
+ names = ['group', 'Group', 'GROUP']
112
+ groups = bulk_create_groups_from_names(names)
113
+ assert len(groups) == 3
114
+
115
+ def test_numeric_names(self) -> None:
116
+ names = ['123', '456', '789']
117
+ groups = bulk_create_groups_from_names(names)
118
+ assert len(groups) == 3
119
+
120
+ def test_mixed_alphanumeric_names(self) -> None:
121
+ names = ['Group1', '2Group', 'Gr0up3']
122
+ groups = bulk_create_groups_from_names(names)
123
+ assert len(groups) == 3
124
+
125
+ def test_names_with_newlines(self) -> None:
126
+ names = ['Group\nWith\nNewlines']
127
+ groups = bulk_create_groups_from_names(names)
128
+ assert len(groups) == 1
129
+
130
+ def test_names_with_null_characters(self) -> None:
131
+ names = ['Group\x00Name']
132
+ groups = bulk_create_groups_from_names(names)
133
+ assert len(groups) == 1
134
+
135
+ def test_preserves_order_of_creation(self) -> None:
136
+ names = ['First', 'Second', 'Third']
137
+ groups = bulk_create_groups_from_names(names)
138
+ group_names = [g.name for g in groups]
139
+ assert group_names == names
140
+
141
+ def test_does_not_modify_existing_groups(self) -> None:
142
+ existing = AuthGroup.objects.create(name='Existing')
143
+ original_pk = existing.pk
144
+ bulk_create_groups_from_names(['Existing', 'New'])
145
+ existing.refresh_from_db()
146
+ assert existing.pk == original_pk