django-spire 0.23.7__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 (538) 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/maps.py +3 -1
  200. django_spire/core/middleware/maintenance.py +3 -3
  201. django_spire/core/middleware.py +8 -6
  202. django_spire/core/redirect/__init__.py +5 -0
  203. django_spire/core/redirect/generic_redirect.py +1 -2
  204. django_spire/core/redirect/tests/__init__.py +0 -0
  205. django_spire/core/redirect/tests/test_generic_redirect.py +34 -0
  206. django_spire/core/{tests/tests_redirect.py → redirect/tests/test_safe_redirect.py} +55 -81
  207. django_spire/core/shortcuts.py +3 -3
  208. django_spire/core/static/django_spire/css/app-layout.css +1 -1
  209. django_spire/core/static/django_spire/css/app-navigation.css +3 -3
  210. django_spire/core/static/django_spire/css/bootstrap-override.css +4 -0
  211. django_spire/core/tag/admin.py +12 -0
  212. django_spire/core/tag/intelligence/tag_set_bot.py +2 -0
  213. django_spire/core/tag/mixins.py +2 -0
  214. django_spire/core/tag/models.py +2 -0
  215. django_spire/core/tag/querysets.py +2 -0
  216. django_spire/core/tag/service/tag_service.py +6 -3
  217. django_spire/core/tag/tests/test_intelligence.py +9 -9
  218. django_spire/core/tag/tests/test_tags.py +44 -54
  219. django_spire/core/tag/tests/test_tools.py +191 -0
  220. django_spire/core/tag/tools.py +3 -0
  221. django_spire/core/templates/django_spire/card/card.html +5 -2
  222. django_spire/core/templates/django_spire/card/title_card.html +9 -4
  223. django_spire/core/templates/django_spire/infinite_scroll/base.html +1 -0
  224. django_spire/core/templates/django_spire/navigation/side_navigation.html +19 -24
  225. django_spire/core/templates/django_spire/page/full_page.html +46 -16
  226. django_spire/core/templates/django_spire/table/base.html +4 -2
  227. django_spire/core/templatetags/json.py +6 -2
  228. django_spire/core/templatetags/message.py +13 -8
  229. django_spire/core/templatetags/string_formating.py +8 -5
  230. django_spire/core/templatetags/tests/__init__.py +0 -0
  231. django_spire/core/templatetags/tests/test_templatetags.py +427 -0
  232. django_spire/core/templatetags/variable_types.py +17 -9
  233. django_spire/core/tests/test_cases.py +1 -1
  234. django_spire/core/tests/test_conf.py +43 -0
  235. django_spire/core/tests/test_consts.py +28 -0
  236. django_spire/core/tests/test_context_processors.py +93 -0
  237. django_spire/core/tests/test_decorators.py +95 -0
  238. django_spire/core/tests/test_django_spire_utils.py +56 -0
  239. django_spire/core/tests/test_exceptions.py +37 -0
  240. django_spire/core/tests/test_models.py +54 -0
  241. django_spire/core/tests/test_settings.py +45 -0
  242. django_spire/core/tests/test_shortcuts.py +74 -0
  243. django_spire/core/tests/test_urls.py +16 -0
  244. django_spire/core/tests/test_utils.py +58 -0
  245. django_spire/core/urls.py +4 -1
  246. django_spire/core/utils.py +12 -8
  247. django_spire/exceptions.py +16 -1
  248. django_spire/file/admin.py +4 -2
  249. django_spire/file/apps.py +8 -10
  250. django_spire/file/fields.py +7 -7
  251. django_spire/file/forms.py +1 -1
  252. django_spire/file/interfaces.py +15 -15
  253. django_spire/file/mixins.py +1 -4
  254. django_spire/file/models.py +3 -5
  255. django_spire/file/tests/factories.py +59 -0
  256. django_spire/file/tests/test_admin.py +69 -0
  257. django_spire/file/tests/test_apps.py +24 -0
  258. django_spire/file/tests/test_fields.py +114 -0
  259. django_spire/file/tests/test_forms.py +20 -0
  260. django_spire/file/tests/test_interfaces.py +183 -0
  261. django_spire/file/tests/test_models.py +82 -0
  262. django_spire/file/tests/test_querysets.py +102 -0
  263. django_spire/file/tests/test_utils.py +32 -0
  264. django_spire/file/tests/test_views.py +145 -0
  265. django_spire/file/tests/test_widgets.py +82 -0
  266. django_spire/file/tools.py +8 -2
  267. django_spire/file/views.py +7 -3
  268. django_spire/file/widgets.py +12 -12
  269. django_spire/help_desk/admin.py +15 -0
  270. django_spire/help_desk/apps.py +2 -0
  271. django_spire/help_desk/auth/controller.py +2 -0
  272. django_spire/help_desk/choices.py +2 -0
  273. django_spire/help_desk/enums.py +2 -0
  274. django_spire/help_desk/exceptions.py +31 -3
  275. django_spire/help_desk/forms.py +2 -0
  276. django_spire/help_desk/models.py +2 -0
  277. django_spire/help_desk/querysets.py +4 -1
  278. django_spire/help_desk/services/notification_service.py +26 -27
  279. django_spire/help_desk/services/service.py +2 -3
  280. django_spire/help_desk/tests/factories.py +8 -3
  281. django_spire/help_desk/tests/test_admin.py +41 -0
  282. django_spire/help_desk/tests/test_apps.py +41 -0
  283. django_spire/help_desk/tests/test_choices.py +50 -0
  284. django_spire/help_desk/tests/test_controller.py +87 -0
  285. django_spire/help_desk/tests/test_enums.py +18 -0
  286. django_spire/help_desk/tests/test_exceptions.py +37 -0
  287. django_spire/help_desk/tests/test_forms.py +89 -0
  288. django_spire/help_desk/tests/test_models.py +59 -0
  289. django_spire/help_desk/tests/test_querysets.py +38 -0
  290. django_spire/help_desk/tests/test_services/test_notification_service.py +15 -8
  291. django_spire/help_desk/tests/test_services/test_service.py +92 -0
  292. django_spire/help_desk/tests/test_urls/test_form_urls.py +6 -6
  293. django_spire/help_desk/tests/test_urls/test_page_urls.py +8 -9
  294. django_spire/help_desk/tests/test_views/test_form_views.py +46 -19
  295. django_spire/help_desk/tests/test_views/test_page_views.py +32 -9
  296. django_spire/help_desk/urls/__init__.py +4 -1
  297. django_spire/help_desk/urls/form_urls.py +3 -0
  298. django_spire/help_desk/urls/page_urls.py +3 -0
  299. django_spire/help_desk/views/form_views.py +13 -5
  300. django_spire/help_desk/views/page_views.py +11 -3
  301. django_spire/history/activity/admin.py +2 -0
  302. django_spire/history/activity/apps.py +3 -1
  303. django_spire/history/activity/mixins.py +13 -7
  304. django_spire/history/activity/models.py +6 -5
  305. django_spire/history/activity/querysets.py +2 -0
  306. django_spire/history/activity/tests/__init__.py +0 -0
  307. django_spire/history/activity/tests/test_activity.py +176 -0
  308. django_spire/history/admin.py +9 -2
  309. django_spire/history/choices.py +3 -0
  310. django_spire/history/models.py +5 -5
  311. django_spire/history/tests/test_admin.py +93 -0
  312. django_spire/history/tests/test_history.py +101 -0
  313. django_spire/history/tests/test_mixins.py +84 -0
  314. django_spire/history/viewed/admin.py +3 -1
  315. django_spire/history/viewed/apps.py +3 -1
  316. django_spire/history/viewed/models.py +2 -0
  317. django_spire/history/viewed/tests/__init__.py +0 -0
  318. django_spire/history/viewed/tests/test_viewed.py +46 -0
  319. django_spire/knowledge/auth/tests/__init__.py +0 -0
  320. django_spire/knowledge/auth/tests/test_controller.py +116 -0
  321. django_spire/knowledge/collection/admin.py +5 -1
  322. django_spire/knowledge/collection/models.py +3 -1
  323. django_spire/knowledge/collection/seeding/seed.py +1 -0
  324. django_spire/knowledge/collection/services/factory_service.py +10 -11
  325. django_spire/knowledge/collection/services/ordering_service.py +1 -2
  326. django_spire/knowledge/collection/services/service.py +5 -10
  327. django_spire/knowledge/collection/services/tag_service.py +5 -2
  328. django_spire/knowledge/collection/tests/factories.py +28 -1
  329. django_spire/knowledge/collection/tests/test_models.py +48 -0
  330. django_spire/knowledge/collection/tests/test_querysets.py +93 -0
  331. django_spire/knowledge/collection/tests/test_services/test_factory_service.py +100 -0
  332. django_spire/knowledge/collection/tests/test_services/test_services.py +160 -0
  333. django_spire/knowledge/collection/tests/test_urls/test_form_urls.py +21 -3
  334. django_spire/knowledge/collection/tests/test_urls/test_json_urls.py +39 -1
  335. django_spire/knowledge/collection/tests/test_urls/test_page_urls.py +12 -4
  336. django_spire/knowledge/collection/urls/__init__.py +3 -0
  337. django_spire/knowledge/collection/urls/form_urls.py +2 -0
  338. django_spire/knowledge/collection/urls/json_urls.py +2 -0
  339. django_spire/knowledge/collection/urls/page_urls.py +2 -0
  340. django_spire/knowledge/collection/views/form_views.py +4 -4
  341. django_spire/knowledge/collection/views/json_views.py +5 -1
  342. django_spire/knowledge/collection/views/page_views.py +5 -2
  343. django_spire/knowledge/entry/admin.py +7 -1
  344. django_spire/knowledge/entry/forms.py +2 -0
  345. django_spire/knowledge/entry/models.py +2 -0
  346. django_spire/knowledge/entry/seeding/seed.py +3 -0
  347. django_spire/knowledge/entry/services/automation_service.py +5 -4
  348. django_spire/knowledge/entry/services/factory_service.py +7 -5
  349. django_spire/knowledge/entry/services/service.py +4 -7
  350. django_spire/knowledge/entry/services/tag_service.py +0 -1
  351. django_spire/knowledge/entry/services/tool_service.py +1 -0
  352. django_spire/knowledge/entry/services/transformation_services.py +1 -5
  353. django_spire/knowledge/entry/tests/factories.py +1 -2
  354. django_spire/knowledge/entry/tests/test_factory_service.py +20 -0
  355. django_spire/knowledge/entry/tests/test_models.py +41 -0
  356. django_spire/knowledge/entry/tests/test_querysets.py +71 -0
  357. django_spire/knowledge/entry/tests/test_services.py +94 -0
  358. django_spire/knowledge/entry/tests/test_urls/test_form_urls.py +9 -14
  359. django_spire/knowledge/entry/tests/test_urls/test_json_urls.py +48 -5
  360. django_spire/knowledge/entry/tests/test_urls/test_page_urls.py +6 -8
  361. django_spire/knowledge/entry/tests/test_urls/test_template_urls.py +40 -0
  362. django_spire/knowledge/entry/urls/form_urls.py +2 -0
  363. django_spire/knowledge/entry/urls/json_urls.py +2 -0
  364. django_spire/knowledge/entry/urls/page_urls.py +2 -0
  365. django_spire/knowledge/entry/urls/template_urls.py +2 -0
  366. django_spire/knowledge/entry/version/block/choices.py +2 -0
  367. django_spire/knowledge/entry/version/block/data/data.py +1 -0
  368. django_spire/knowledge/entry/version/block/data/list/data.py +8 -13
  369. django_spire/knowledge/entry/version/block/data/list/maps.py +3 -0
  370. django_spire/knowledge/entry/version/block/data/list/meta.py +1 -2
  371. django_spire/knowledge/entry/version/block/data/list/tests/__init__.py +0 -0
  372. django_spire/knowledge/entry/version/block/data/list/tests/test_maps.py +32 -0
  373. django_spire/knowledge/entry/version/block/data/list/tests/test_meta.py +58 -0
  374. django_spire/knowledge/entry/version/block/data/maps.py +3 -6
  375. django_spire/knowledge/entry/version/block/models.py +7 -5
  376. django_spire/knowledge/entry/version/block/seeding/constants.py +5 -4
  377. django_spire/knowledge/entry/version/block/services/service.py +2 -3
  378. django_spire/knowledge/entry/version/block/tests/factories.py +4 -10
  379. django_spire/knowledge/entry/version/block/tests/test_choices.py +56 -0
  380. django_spire/knowledge/entry/version/block/tests/test_data.py +90 -0
  381. django_spire/knowledge/entry/version/block/tests/test_maps.py +37 -0
  382. django_spire/knowledge/entry/version/block/tests/test_models.py +55 -0
  383. django_spire/knowledge/entry/version/block/tests/test_querysets.py +35 -0
  384. django_spire/knowledge/entry/version/block/tests/test_services.py +65 -0
  385. django_spire/knowledge/entry/version/choices.py +2 -0
  386. django_spire/knowledge/entry/version/converters/converter.py +1 -1
  387. django_spire/knowledge/entry/version/converters/docx_converter.py +4 -7
  388. django_spire/knowledge/entry/version/converters/markdown_converter.py +20 -20
  389. django_spire/knowledge/entry/version/maps.py +4 -5
  390. django_spire/knowledge/entry/version/querysets.py +1 -1
  391. django_spire/knowledge/entry/version/seeding/seeder.py +1 -2
  392. django_spire/knowledge/entry/version/services/processor_service.py +5 -4
  393. django_spire/knowledge/entry/version/services/service.py +1 -2
  394. django_spire/knowledge/entry/version/tests/factories.py +2 -2
  395. django_spire/knowledge/entry/version/tests/test_choices.py +18 -0
  396. django_spire/knowledge/entry/version/tests/test_converters/test_docx_converter.py +56 -8
  397. django_spire/knowledge/entry/version/tests/test_converters/test_markdown_converter.py +78 -0
  398. django_spire/knowledge/entry/version/tests/test_maps.py +58 -0
  399. django_spire/knowledge/entry/version/tests/test_models.py +23 -0
  400. django_spire/knowledge/entry/version/tests/test_querysets.py +26 -0
  401. django_spire/knowledge/entry/version/tests/test_services.py +62 -0
  402. django_spire/knowledge/entry/version/tests/test_urls/test_json_urls.py +27 -8
  403. django_spire/knowledge/entry/version/tests/test_urls/test_page_urls.py +15 -8
  404. django_spire/knowledge/entry/version/tests/test_urls/test_redirect_urls.py +38 -0
  405. django_spire/knowledge/entry/version/urls/__init__.py +3 -0
  406. django_spire/knowledge/entry/version/urls/json_urls.py +2 -1
  407. django_spire/knowledge/entry/version/urls/page_urls.py +2 -0
  408. django_spire/knowledge/entry/version/urls/redirect_urls.py +2 -0
  409. django_spire/knowledge/entry/version/views/json_views.py +5 -1
  410. django_spire/knowledge/entry/version/views/page_views.py +10 -3
  411. django_spire/knowledge/entry/version/views/redirect_views.py +5 -1
  412. django_spire/knowledge/entry/views/form_views.py +16 -8
  413. django_spire/knowledge/entry/views/json_views.py +3 -1
  414. django_spire/knowledge/entry/views/page_views.py +8 -2
  415. django_spire/knowledge/entry/views/template_views.py +7 -1
  416. django_spire/knowledge/exceptions.py +2 -1
  417. django_spire/knowledge/intelligence/intel/answer_intel.py +2 -1
  418. django_spire/knowledge/intelligence/intel/entry_intel.py +0 -1
  419. django_spire/knowledge/intelligence/workflows/knowledge_workflow.py +4 -5
  420. django_spire/knowledge/models.py +1 -2
  421. django_spire/knowledge/tests/__init__.py +0 -0
  422. django_spire/knowledge/tests/test_templatetags.py +40 -0
  423. django_spire/knowledge/tests/test_urls/__init__.py +0 -0
  424. django_spire/knowledge/tests/test_urls/test_page_urls.py +24 -0
  425. django_spire/knowledge/urls/__init__.py +2 -0
  426. django_spire/knowledge/urls/page_urls.py +2 -0
  427. django_spire/knowledge/views/page_views.py +8 -3
  428. django_spire/notification/admin.py +3 -1
  429. django_spire/notification/app/admin.py +2 -0
  430. django_spire/notification/app/apps.py +3 -1
  431. django_spire/notification/app/exceptions.py +9 -2
  432. django_spire/notification/app/models.py +8 -4
  433. django_spire/notification/app/processor.py +22 -26
  434. django_spire/notification/app/querysets.py +2 -0
  435. django_spire/notification/app/tests/__init__.py +0 -0
  436. django_spire/notification/app/tests/factories.py +34 -0
  437. django_spire/notification/app/tests/test_apps.py +24 -0
  438. django_spire/notification/app/tests/test_models.py +72 -0
  439. django_spire/notification/app/tests/test_processor.py +111 -0
  440. django_spire/notification/app/tests/test_querysets.py +90 -0
  441. django_spire/notification/app/tests/test_views/__init__.py +0 -0
  442. django_spire/notification/app/tests/test_views/test_json_views.py +48 -0
  443. django_spire/notification/app/tests/test_views/test_page_views.py +19 -0
  444. django_spire/notification/app/urls/__init__.py +3 -1
  445. django_spire/notification/app/urls/json_urls.py +6 -4
  446. django_spire/notification/app/urls/page_urls.py +4 -3
  447. django_spire/notification/app/urls/template_urls.py +4 -2
  448. django_spire/notification/apps.py +4 -1
  449. django_spire/notification/email/admin.py +5 -1
  450. django_spire/notification/email/apps.py +3 -1
  451. django_spire/notification/email/exceptions.py +4 -2
  452. django_spire/notification/email/helper.py +5 -3
  453. django_spire/notification/email/models.py +4 -0
  454. django_spire/notification/email/processor.py +19 -15
  455. django_spire/notification/email/querysets.py +3 -0
  456. django_spire/notification/email/tests/__init__.py +0 -0
  457. django_spire/notification/email/tests/factories.py +35 -0
  458. django_spire/notification/email/tests/test_apps.py +24 -0
  459. django_spire/notification/email/tests/test_models.py +52 -0
  460. django_spire/notification/email/tests/test_processor.py +92 -0
  461. django_spire/notification/email/tests/test_querysets.py +43 -0
  462. django_spire/notification/exceptions.py +17 -2
  463. django_spire/notification/managers.py +7 -1
  464. django_spire/notification/maps.py +4 -1
  465. django_spire/notification/mixins.py +2 -0
  466. django_spire/notification/models.py +3 -1
  467. django_spire/notification/processors/notification.py +12 -5
  468. django_spire/notification/processors/processor.py +2 -0
  469. django_spire/notification/processors/tests/__init__.py +0 -0
  470. django_spire/notification/processors/tests/test_notification.py +106 -0
  471. django_spire/notification/push/admin.py +10 -1
  472. django_spire/notification/push/apps.py +3 -1
  473. django_spire/notification/push/models.py +2 -3
  474. django_spire/notification/push/tests/__init__.py +0 -0
  475. django_spire/notification/push/tests/test_apps.py +24 -0
  476. django_spire/notification/push/tests/test_models.py +28 -0
  477. django_spire/notification/querysets.py +7 -1
  478. django_spire/notification/sms/admin.py +2 -0
  479. django_spire/notification/sms/apps.py +4 -1
  480. django_spire/notification/sms/automations.py +2 -0
  481. django_spire/notification/sms/choices.py +2 -0
  482. django_spire/notification/sms/exceptions.py +19 -5
  483. django_spire/notification/sms/helper.py +33 -23
  484. django_spire/notification/sms/models.py +5 -1
  485. django_spire/notification/sms/processor.py +20 -20
  486. django_spire/notification/sms/querysets.py +2 -0
  487. django_spire/notification/sms/tests/factories.py +33 -0
  488. django_spire/notification/sms/tests/test_apps.py +24 -0
  489. django_spire/notification/sms/tests/test_automation.py +38 -0
  490. django_spire/notification/sms/tests/test_choices.py +15 -0
  491. django_spire/notification/sms/tests/test_consts.py +17 -0
  492. django_spire/notification/sms/tests/test_exceptions.py +27 -0
  493. django_spire/notification/sms/tests/test_helper.py +50 -0
  494. django_spire/notification/sms/tests/test_models.py +81 -0
  495. django_spire/notification/sms/tests/test_processor.py +107 -0
  496. django_spire/notification/sms/tests/test_tools.py +25 -11
  497. django_spire/notification/sms/tools.py +16 -5
  498. django_spire/notification/sms/urls/__init__.py +3 -1
  499. django_spire/notification/sms/urls/media_urls.py +2 -0
  500. django_spire/notification/sms/views/media_views.py +14 -4
  501. django_spire/notification/tests/__init__.py +0 -0
  502. django_spire/notification/tests/factories.py +26 -0
  503. django_spire/notification/tests/test_admin.py +55 -0
  504. django_spire/notification/tests/test_apps.py +30 -0
  505. django_spire/notification/tests/test_automation.py +18 -0
  506. django_spire/notification/tests/test_choices.py +59 -0
  507. django_spire/notification/tests/test_exceptions.py +58 -0
  508. django_spire/notification/tests/test_managers.py +100 -0
  509. django_spire/notification/tests/test_maps.py +31 -0
  510. django_spire/notification/tests/test_models.py +76 -0
  511. django_spire/notification/tests/test_querysets.py +184 -0
  512. django_spire/notification/tests/test_utils.py +23 -0
  513. django_spire/notification/urls.py +3 -1
  514. django_spire/notification/utils.py +3 -1
  515. django_spire/settings.py +3 -0
  516. django_spire/theme/tests/test_context_processor.py +15 -13
  517. django_spire/theme/tests/test_enums.py +2 -2
  518. django_spire/theme/tests/test_filesystem.py +2 -5
  519. django_spire/theme/tests/test_integration.py +12 -12
  520. django_spire/theme/tests/test_model.py +40 -38
  521. django_spire/theme/tests/test_views/test_json_views.py +33 -33
  522. django_spire/theme/urls/json_urls.py +3 -0
  523. django_spire/theme/urls/page_urls.py +3 -0
  524. django_spire/urls.py +19 -15
  525. django_spire/utils.py +13 -4
  526. {django_spire-0.23.7.dist-info → django_spire-0.23.8.dist-info}/METADATA +1 -1
  527. {django_spire-0.23.7.dist-info → django_spire-0.23.8.dist-info}/RECORD +530 -358
  528. django_spire/contrib/options/tests/test_unit.py +0 -148
  529. django_spire/contrib/seeding/tests/test_seeding.py +0 -25
  530. django_spire/core/tests/test_templatetags.py +0 -117
  531. django_spire/core/tests/tests_shortcuts.py +0 -73
  532. django_spire/history/activity/tests.py +0 -3
  533. django_spire/history/activity/views.py +0 -3
  534. django_spire/knowledge/collection/tests/test_services/test_transformation_service.py +0 -71
  535. django_spire/notification/app/tests.py +0 -3
  536. {django_spire-0.23.7.dist-info → django_spire-0.23.8.dist-info}/WHEEL +0 -0
  537. {django_spire-0.23.7.dist-info → django_spire-0.23.8.dist-info}/licenses/LICENSE.md +0 -0
  538. {django_spire-0.23.7.dist-info → django_spire-0.23.8.dist-info}/top_level.txt +0 -0
@@ -4,6 +4,7 @@ import pytest
4
4
 
5
5
  from typing import TYPE_CHECKING
6
6
 
7
+ from dandy.llm.request.message import MessageHistory
7
8
  from django.test import RequestFactory
8
9
 
9
10
  from django_spire.ai.chat.message_intel import BaseMessageIntel, DefaultMessageIntel
@@ -11,7 +12,6 @@ from django_spire.ai.chat.router import BaseChatRouter
11
12
  from django_spire.core.tests.test_cases import BaseTestCase
12
13
 
13
14
  if TYPE_CHECKING:
14
- from dandy.llm.request.message import MessageHistory
15
15
  from django.core.handlers.wsgi import WSGIRequest
16
16
 
17
17
 
@@ -53,7 +53,12 @@ class TestBaseChatRouter(BaseTestCase):
53
53
 
54
54
  def test_process_validates_workflow_return_type(self) -> None:
55
55
  class InvalidRouter(BaseChatRouter):
56
- def workflow(self, request, user_input, message_history=None) -> str:
56
+ def workflow(
57
+ self,
58
+ request: WSGIRequest,
59
+ user_input: str,
60
+ message_history: MessageHistory | None = None
61
+ ) -> str:
57
62
  return 'Invalid return type'
58
63
 
59
64
  router = InvalidRouter()
@@ -69,7 +74,12 @@ class TestBaseChatRouter(BaseTestCase):
69
74
 
70
75
  def test_process_handles_none_return(self) -> None:
71
76
  class NoneRouter(BaseChatRouter):
72
- def workflow(self, request, user_input, message_history=None) -> None:
77
+ def workflow(
78
+ self,
79
+ request: WSGIRequest,
80
+ user_input: str,
81
+ message_history: MessageHistory | None = None
82
+ ) -> None:
73
83
  return None
74
84
 
75
85
  router = NoneRouter()
@@ -83,12 +93,15 @@ class TestBaseChatRouter(BaseTestCase):
83
93
  assert result.text == 'I apologize, but I was unable to process your request.'
84
94
 
85
95
  def test_process_accepts_all_parameters(self) -> None:
86
- from dandy.llm.request.message import MessageHistory
87
-
88
96
  class ParamTestRouter(BaseChatRouter):
89
97
  received_params = {}
90
98
 
91
- def workflow(self, request, user_input, message_history=None) -> DefaultMessageIntel:
99
+ def workflow(
100
+ self,
101
+ request: WSGIRequest,
102
+ user_input: str,
103
+ message_history: MessageHistory | None = None
104
+ ) -> DefaultMessageIntel:
92
105
  self.received_params = {
93
106
  'request': request,
94
107
  'user_input': user_input,
@@ -108,3 +121,50 @@ class TestBaseChatRouter(BaseTestCase):
108
121
  assert router.received_params['request'] == self.request
109
122
  assert router.received_params['user_input'] == 'Test input'
110
123
  assert router.received_params['message_history'] == message_history
124
+
125
+ def test_process_returns_base_message_intel_subclass(self) -> None:
126
+ class CustomMessageIntel(BaseMessageIntel):
127
+ _template: str = 'django_spire/ai/chat/message/default_message.html'
128
+ custom_field: str
129
+
130
+ def render_to_str(self) -> str:
131
+ return self.custom_field
132
+
133
+ class CustomRouter(BaseChatRouter):
134
+ def workflow(
135
+ self,
136
+ request: WSGIRequest,
137
+ user_input: str,
138
+ message_history: MessageHistory | None = None
139
+ ) -> CustomMessageIntel:
140
+ return CustomMessageIntel(custom_field='Custom value')
141
+
142
+ router = CustomRouter()
143
+ result = router.process(
144
+ request=self.request,
145
+ user_input='Hello',
146
+ message_history=None
147
+ )
148
+
149
+ assert isinstance(result, CustomMessageIntel)
150
+ assert result.custom_field == 'Custom value'
151
+
152
+ def test_process_with_empty_user_input(self) -> None:
153
+ class EmptyInputRouter(BaseChatRouter):
154
+ def workflow(
155
+ self,
156
+ request: WSGIRequest,
157
+ user_input: str,
158
+ message_history: MessageHistory | None = None
159
+ ) -> DefaultMessageIntel:
160
+ return DefaultMessageIntel(text=f'Received: {user_input}')
161
+
162
+ router = EmptyInputRouter()
163
+ result = router.process(
164
+ request=self.request,
165
+ user_input='',
166
+ message_history=None
167
+ )
168
+
169
+ assert isinstance(result, DefaultMessageIntel)
170
+ assert result.text == 'Received: '
@@ -1,7 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from typing import TYPE_CHECKING
3
4
  from unittest.mock import Mock, patch
4
5
 
6
+ from dandy.llm.request.message import MessageHistory
5
7
  from django.test import RequestFactory, override_settings
6
8
 
7
9
  from django_spire.ai.chat.intelligence.workflows.chat_workflow import chat_workflow
@@ -9,9 +11,17 @@ from django_spire.ai.chat.message_intel import DefaultMessageIntel
9
11
  from django_spire.ai.chat.router import BaseChatRouter
10
12
  from django_spire.core.tests.test_cases import BaseTestCase
11
13
 
14
+ if TYPE_CHECKING:
15
+ from django.core.handlers.wsgi import WSGIRequest
16
+
12
17
 
13
18
  class MockRouter(BaseChatRouter):
14
- def workflow(self, request, user_input, message_history=None) -> DefaultMessageIntel:
19
+ def workflow(
20
+ self,
21
+ request: WSGIRequest,
22
+ user_input: str,
23
+ message_history: MessageHistory | None = None
24
+ ) -> DefaultMessageIntel:
15
25
  return DefaultMessageIntel(text='Mock response')
16
26
 
17
27
 
@@ -95,8 +105,6 @@ class TestChatWorkflow(BaseTestCase):
95
105
  }
96
106
  )
97
107
  def test_workflow_passes_message_history(self) -> None:
98
- from dandy.llm.request.message import MessageHistory
99
-
100
108
  message_history = MessageHistory()
101
109
 
102
110
  with patch('django_spire.ai.chat.tests.test_router.test_chat_workflow.MockRouter.process') as mock_process:
@@ -111,3 +119,65 @@ class TestChatWorkflow(BaseTestCase):
111
119
  mock_process.assert_called_once()
112
120
  call_kwargs = mock_process.call_args[1]
113
121
  assert call_kwargs['message_history'] == message_history
122
+
123
+ @override_settings(
124
+ DJANGO_SPIRE_AI_DEFAULT_CHAT_ROUTER='TEST',
125
+ DJANGO_SPIRE_AI_CHAT_ROUTERS={
126
+ 'TEST': 'django_spire.ai.chat.tests.test_router.test_chat_workflow.MockRouter'
127
+ }
128
+ )
129
+ def test_workflow_passes_request(self) -> None:
130
+ with patch('django_spire.ai.chat.tests.test_router.test_chat_workflow.MockRouter.process') as mock_process:
131
+ mock_process.return_value = DefaultMessageIntel(text='Response')
132
+
133
+ chat_workflow(
134
+ request=self.request,
135
+ user_input='Hello',
136
+ message_history=None
137
+ )
138
+
139
+ mock_process.assert_called_once()
140
+ call_kwargs = mock_process.call_args[1]
141
+ assert call_kwargs['request'] == self.request
142
+
143
+ @override_settings(
144
+ DJANGO_SPIRE_AI_DEFAULT_CHAT_ROUTER='TEST',
145
+ DJANGO_SPIRE_AI_CHAT_ROUTERS={
146
+ 'TEST': 'django_spire.ai.chat.tests.test_router.test_chat_workflow.MockRouter'
147
+ }
148
+ )
149
+ def test_workflow_passes_user_input(self) -> None:
150
+ with patch('django_spire.ai.chat.tests.test_router.test_chat_workflow.MockRouter.process') as mock_process:
151
+ mock_process.return_value = DefaultMessageIntel(text='Response')
152
+
153
+ chat_workflow(
154
+ request=self.request,
155
+ user_input='Test user input',
156
+ message_history=None
157
+ )
158
+
159
+ mock_process.assert_called_once()
160
+ call_kwargs = mock_process.call_args[1]
161
+ assert call_kwargs['user_input'] == 'Test user input'
162
+
163
+ @override_settings(
164
+ DJANGO_SPIRE_AI_CHAT_ROUTERS={}
165
+ )
166
+ def test_workflow_uses_fallback_router_path(self) -> None:
167
+ with patch('django_spire.ai.chat.intelligence.workflows.chat_workflow.get_callable_from_module_string_and_validate_arguments') as mock_get_callable:
168
+ mock_router_class = Mock()
169
+ mock_router_instance = Mock()
170
+ mock_router_instance.process.return_value = DefaultMessageIntel(text='Fallback')
171
+ mock_router_class.return_value = mock_router_instance
172
+ mock_get_callable.return_value = mock_router_class
173
+
174
+ chat_workflow(
175
+ request=self.request,
176
+ user_input='Hello',
177
+ message_history=None
178
+ )
179
+
180
+ mock_get_callable.assert_called_once_with(
181
+ 'django_spire.ai.chat.router.SpireChatRouter',
182
+ []
183
+ )
@@ -1,23 +1,39 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from typing import TYPE_CHECKING
3
4
  from unittest.mock import Mock, patch
4
5
 
6
+ from dandy.llm.request.message import MessageHistory
5
7
  from django.contrib.auth.models import Permission, User
6
8
  from django.test import RequestFactory, override_settings
7
9
 
10
+ from django_spire.ai.chat.intelligence.decoders.intent_decoder import generate_intent_decoder
8
11
  from django_spire.ai.chat.intelligence.workflows.chat_workflow import chat_workflow
9
12
  from django_spire.ai.chat.message_intel import BaseMessageIntel, DefaultMessageIntel
10
13
  from django_spire.ai.chat.router import BaseChatRouter
11
14
  from django_spire.core.tests.test_cases import BaseTestCase
12
15
 
16
+ if TYPE_CHECKING:
17
+ from django.core.handlers.wsgi import WSGIRequest
18
+
13
19
 
14
20
  class KnowledgeRouter(BaseChatRouter):
15
- def workflow(self, request, user_input, message_history=None) -> DefaultMessageIntel:
21
+ def workflow(
22
+ self,
23
+ request: WSGIRequest,
24
+ user_input: str,
25
+ message_history: MessageHistory | None = None
26
+ ) -> DefaultMessageIntel:
16
27
  return DefaultMessageIntel(text='Knowledge search result')
17
28
 
18
29
 
19
30
  class SupportRouter(BaseChatRouter):
20
- def workflow(self, request, user_input, message_history=None) -> DefaultMessageIntel:
31
+ def workflow(
32
+ self,
33
+ request: WSGIRequest,
34
+ user_input: str,
35
+ message_history: MessageHistory | None = None
36
+ ) -> DefaultMessageIntel:
21
37
  return DefaultMessageIntel(text='Support response')
22
38
 
23
39
 
@@ -95,8 +111,6 @@ class TestRouterIntegration(BaseTestCase):
95
111
  }
96
112
  )
97
113
  def test_intent_excluded_without_permission(self) -> None:
98
- from django_spire.ai.chat.intelligence.decoders.intent_decoder import generate_intent_decoder
99
-
100
114
  regular_user = User.objects.create_user(username='regular', password='test')
101
115
 
102
116
  request = self.factory.get('/')
@@ -113,8 +127,6 @@ class TestRouterIntegration(BaseTestCase):
113
127
  }
114
128
  )
115
129
  def test_end_to_end_workflow(self) -> None:
116
- from dandy.llm.request.message import MessageHistory
117
-
118
130
  message_history = MessageHistory()
119
131
  message_history.add_message(role='user', content='Previous message')
120
132
 
@@ -145,3 +157,71 @@ class TestRouterIntegration(BaseTestCase):
145
157
  )
146
158
 
147
159
  assert isinstance(result, DefaultMessageIntel)
160
+
161
+ @override_settings(
162
+ DJANGO_SPIRE_AI_DEFAULT_CHAT_ROUTER='KNOWLEDGE',
163
+ DJANGO_SPIRE_AI_CHAT_ROUTERS={
164
+ 'KNOWLEDGE': 'django_spire.ai.chat.tests.test_router.test_integration.KnowledgeRouter',
165
+ 'SUPPORT': 'django_spire.ai.chat.tests.test_router.test_integration.SupportRouter',
166
+ }
167
+ )
168
+ def test_router_selection_by_key(self) -> None:
169
+ result = chat_workflow(
170
+ request=self.request,
171
+ user_input='Test',
172
+ message_history=None
173
+ )
174
+
175
+ assert isinstance(result, DefaultMessageIntel)
176
+ assert result.text == 'Knowledge search result'
177
+
178
+ @override_settings(
179
+ DJANGO_SPIRE_AI_DEFAULT_CHAT_ROUTER='SUPPORT',
180
+ DJANGO_SPIRE_AI_CHAT_ROUTERS={
181
+ 'SUPPORT': 'django_spire.ai.chat.tests.test_router.test_integration.SupportRouter',
182
+ }
183
+ )
184
+ def test_workflow_with_none_message_history(self) -> None:
185
+ result = chat_workflow(
186
+ request=self.request,
187
+ user_input='Test input',
188
+ message_history=None
189
+ )
190
+
191
+ assert isinstance(result, DefaultMessageIntel)
192
+ assert result.text == 'Support response'
193
+
194
+ @override_settings(
195
+ DJANGO_SPIRE_AI_DEFAULT_CHAT_ROUTER='SUPPORT',
196
+ DJANGO_SPIRE_AI_CHAT_ROUTERS={
197
+ 'SUPPORT': 'django_spire.ai.chat.tests.test_router.test_integration.SupportRouter',
198
+ }
199
+ )
200
+ def test_workflow_preserves_user_input(self) -> None:
201
+ test_input = 'This is a specific test input'
202
+
203
+ with patch.object(SupportRouter, 'workflow', wraps=SupportRouter().workflow) as mock_workflow:
204
+ mock_workflow.return_value = DefaultMessageIntel(text='Response')
205
+
206
+ chat_workflow(
207
+ request=self.request,
208
+ user_input=test_input,
209
+ message_history=None
210
+ )
211
+
212
+ @override_settings(
213
+ DJANGO_SPIRE_AI_DEFAULT_CHAT_ROUTER='KNOWLEDGE',
214
+ DJANGO_SPIRE_AI_CHAT_ROUTERS={
215
+ 'KNOWLEDGE': 'django_spire.ai.chat.tests.test_router.test_integration.KnowledgeRouter',
216
+ },
217
+ DJANGO_SPIRE_AI_INTENT_CHAT_ROUTERS={}
218
+ )
219
+ def test_workflow_without_intent_routers(self) -> None:
220
+ result = chat_workflow(
221
+ request=self.request,
222
+ user_input='Test',
223
+ message_history=None
224
+ )
225
+
226
+ assert isinstance(result, DefaultMessageIntel)
227
+ assert result.text == 'Knowledge search result'
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from typing import TYPE_CHECKING
3
4
  from unittest.mock import Mock
4
5
 
5
6
  from django.contrib.auth.models import Permission, User
@@ -10,15 +11,25 @@ from django_spire.ai.chat.message_intel import DefaultMessageIntel
10
11
  from django_spire.ai.chat.router import BaseChatRouter
11
12
  from django_spire.core.tests.test_cases import BaseTestCase
12
13
 
14
+ if TYPE_CHECKING:
15
+ from dandy.llm.request.message import MessageHistory
16
+ from django.core.handlers.wsgi import WSGIRequest
17
+
13
18
 
14
19
  class TestRouter(BaseChatRouter):
15
- def workflow(self, request, user_input, message_history=None) -> DefaultMessageIntel:
20
+ def workflow(
21
+ self,
22
+ request: WSGIRequest,
23
+ user_input: str,
24
+ message_history: MessageHistory | None = None
25
+ ) -> DefaultMessageIntel:
16
26
  return DefaultMessageIntel(text='Test response')
17
27
 
18
28
 
19
29
  class TestIntentDecoder(BaseTestCase):
20
30
  def setUp(self) -> None:
21
31
  super().setUp()
32
+
22
33
  self.factory = RequestFactory()
23
34
  self.request = self.factory.get('/')
24
35
  self.request.user = self.super_user
@@ -139,3 +150,84 @@ class TestIntentDecoder(BaseTestCase):
139
150
  assert 'First intent' in decoder.mapping
140
151
  assert 'Second intent' in decoder.mapping
141
152
  assert len(decoder.mapping) == 2
153
+
154
+ @override_settings(
155
+ DJANGO_SPIRE_AI_INTENT_CHAT_ROUTERS={
156
+ 'TEST_INTENT': {
157
+ 'INTENT_DESCRIPTION': 'Test intent',
158
+ 'CHAT_ROUTER': 'django_spire.ai.chat.tests.test_router.test_intent_decoder.TestRouter',
159
+ }
160
+ }
161
+ )
162
+ def test_decoder_mapping_keys_description(self) -> None:
163
+ decoder = generate_intent_decoder(
164
+ request=self.request,
165
+ default_callable=None
166
+ )
167
+
168
+ assert decoder.mapping_keys_description == "Intent of the User's Request"
169
+
170
+ @override_settings(
171
+ DJANGO_SPIRE_AI_INTENT_CHAT_ROUTERS={
172
+ 'TEST_INTENT': {
173
+ 'CHAT_ROUTER': 'django_spire.ai.chat.tests.test_router.test_intent_decoder.TestRouter',
174
+ }
175
+ }
176
+ )
177
+ def test_decoder_with_missing_intent_description(self) -> None:
178
+ decoder = generate_intent_decoder(
179
+ request=self.request,
180
+ default_callable=None
181
+ )
182
+
183
+ assert '' in decoder.mapping
184
+
185
+ @override_settings(
186
+ DJANGO_SPIRE_AI_INTENT_CHAT_ROUTERS={
187
+ 'NO_ROUTER': {
188
+ 'INTENT_DESCRIPTION': 'No router intent',
189
+ }
190
+ }
191
+ )
192
+ def test_decoder_with_missing_chat_router(self) -> None:
193
+ decoder = generate_intent_decoder(
194
+ request=self.request,
195
+ default_callable=None
196
+ )
197
+
198
+ assert 'No router intent' not in decoder.mapping
199
+
200
+ def test_decoder_with_none_default_callable(self) -> None:
201
+ decoder = generate_intent_decoder(
202
+ request=self.request,
203
+ default_callable=None
204
+ )
205
+
206
+ assert "None of the above choices match the user's intent" not in decoder.mapping
207
+
208
+ @override_settings(
209
+ DJANGO_SPIRE_AI_INTENT_CHAT_ROUTERS={
210
+ 'INTENT_A': {
211
+ 'INTENT_DESCRIPTION': 'Intent A',
212
+ 'REQUIRED_PERMISSION': 'auth.add_user',
213
+ 'CHAT_ROUTER': 'django_spire.ai.chat.tests.test_router.test_intent_decoder.TestRouter',
214
+ },
215
+ 'INTENT_B': {
216
+ 'INTENT_DESCRIPTION': 'Intent B',
217
+ 'CHAT_ROUTER': 'django_spire.ai.chat.tests.test_router.test_intent_decoder.TestRouter',
218
+ }
219
+ }
220
+ )
221
+ def test_decoder_mixed_permission_intents(self) -> None:
222
+ regular_user = User.objects.create_user(username='mixed_user', password='test')
223
+
224
+ request = self.factory.get('/')
225
+ request.user = regular_user
226
+
227
+ decoder = generate_intent_decoder(
228
+ request=request,
229
+ default_callable=None
230
+ )
231
+
232
+ assert 'Intent A' not in decoder.mapping
233
+ assert 'Intent B' in decoder.mapping
@@ -8,8 +8,8 @@ from django_spire.core.tests.test_cases import BaseTestCase
8
8
 
9
9
  class CustomMessageIntel(BaseMessageIntel):
10
10
  _template: str = 'django_spire/ai/chat/message/default_message.html'
11
- text: str
12
11
  extra_data: str = 'Extra'
12
+ text: str
13
13
 
14
14
  def render_to_str(self) -> str:
15
15
  return f'{self.text} - {self.extra_data}'
@@ -18,6 +18,7 @@ class CustomMessageIntel(BaseMessageIntel):
18
18
  class TestMessageIntel(BaseTestCase):
19
19
  def test_default_message_intel_has_template(self) -> None:
20
20
  intel = DefaultMessageIntel(text='Test')
21
+
21
22
  assert intel.template == 'django_spire/ai/chat/message/default_message.html'
22
23
 
23
24
  def test_default_message_intel_render_to_str(self) -> None:
@@ -65,3 +66,61 @@ class TestMessageIntel(BaseTestCase):
65
66
 
66
67
  def render_to_str(self) -> str:
67
68
  return 'test'
69
+
70
+ def test_default_message_intel_model_dump(self) -> None:
71
+ intel = DefaultMessageIntel(text='Test text')
72
+ dump = intel.model_dump()
73
+
74
+ assert 'text' in dump
75
+ assert dump['text'] == 'Test text'
76
+
77
+ def test_custom_message_intel_default_extra_data(self) -> None:
78
+ intel = CustomMessageIntel(text='Test')
79
+
80
+ assert intel.extra_data == 'Extra'
81
+
82
+ def test_default_message_intel_empty_text(self) -> None:
83
+ intel = DefaultMessageIntel(text='')
84
+ result = intel.render_to_str()
85
+
86
+ assert result == ''
87
+
88
+ def test_default_message_intel_long_text(self) -> None:
89
+ long_text = 'A' * 10000
90
+ intel = DefaultMessageIntel(text=long_text)
91
+ result = intel.render_to_str()
92
+
93
+ assert result == long_text
94
+ assert len(result) == 10000
95
+
96
+ def test_default_message_intel_special_characters(self) -> None:
97
+ special_text = '<script>alert("xss")</script>'
98
+ intel = DefaultMessageIntel(text=special_text)
99
+ result = intel.render_to_str()
100
+
101
+ assert result == special_text
102
+
103
+ def test_default_message_intel_unicode(self) -> None:
104
+ unicode_text = 'Hello 世界 🌍'
105
+ intel = DefaultMessageIntel(text=unicode_text)
106
+ result = intel.render_to_str()
107
+
108
+ assert result == unicode_text
109
+
110
+ def test_render_template_to_str_with_none_context(self) -> None:
111
+ intel = DefaultMessageIntel(text='Test')
112
+ result = intel.render_template_to_str(context_data=None)
113
+
114
+ assert isinstance(result, str)
115
+
116
+ def test_render_template_to_str_with_empty_context(self) -> None:
117
+ intel = DefaultMessageIntel(text='Test')
118
+ result = intel.render_template_to_str(context_data={})
119
+
120
+ assert isinstance(result, str)
121
+
122
+ def test_custom_message_intel_override_extra_data(self) -> None:
123
+ intel = CustomMessageIntel(text='Test', extra_data='Override')
124
+
125
+ assert intel.extra_data == 'Override'
126
+ assert intel.render_to_str() == 'Test - Override'
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from unittest.mock import Mock, patch
4
4
 
5
+ from dandy.llm.request.message import MessageHistory
5
6
  from django.test import RequestFactory, override_settings
6
7
 
7
8
  from django_spire.ai.chat.message_intel import DefaultMessageIntel
@@ -12,12 +13,14 @@ from django_spire.core.tests.test_cases import BaseTestCase
12
13
  class TestSpireChatRouter(BaseTestCase):
13
14
  def setUp(self) -> None:
14
15
  super().setUp()
16
+
15
17
  self.factory = RequestFactory()
16
18
  self.request = self.factory.get('/')
17
19
  self.request.user = self.super_user
18
20
 
19
21
  def test_router_can_be_instantiated(self) -> None:
20
22
  router = SpireChatRouter()
23
+
21
24
  assert isinstance(router, SpireChatRouter)
22
25
 
23
26
  def test_default_chat_callable_returns_message_intel(self) -> None:
@@ -90,3 +93,110 @@ class TestSpireChatRouter(BaseTestCase):
90
93
  )
91
94
 
92
95
  assert isinstance(result, DefaultMessageIntel)
96
+
97
+ def test_workflow_passes_message_history_to_decoder(self) -> None:
98
+ router = SpireChatRouter()
99
+ message_history = MessageHistory()
100
+
101
+ with patch('django_spire.ai.chat.router.generate_intent_decoder') as mock_decoder:
102
+ mock_decoder_instance = Mock()
103
+ mock_decoder.return_value = mock_decoder_instance
104
+ mock_decoder_instance.process.return_value = [
105
+ lambda **kwargs: DefaultMessageIntel(text='Response')
106
+ ]
107
+
108
+ router.workflow(
109
+ request=self.request,
110
+ user_input='Hello',
111
+ message_history=message_history
112
+ )
113
+
114
+ mock_decoder.assert_called_once()
115
+
116
+ def test_workflow_returns_message_intel(self) -> None:
117
+ router = SpireChatRouter()
118
+
119
+ with patch('django_spire.ai.chat.router.generate_intent_decoder') as mock_decoder:
120
+ mock_decoder_instance = Mock()
121
+ mock_decoder.return_value = mock_decoder_instance
122
+ mock_decoder_instance.process.return_value = [
123
+ lambda **kwargs: DefaultMessageIntel(text='Test Response')
124
+ ]
125
+
126
+ result = router.workflow(
127
+ request=self.request,
128
+ user_input='Hello',
129
+ message_history=None
130
+ )
131
+
132
+ assert isinstance(result, DefaultMessageIntel)
133
+ assert result.text == 'Test Response'
134
+
135
+ @override_settings(DJANGO_SPIRE_AI_PERSONA_NAME='Custom Persona')
136
+ def test_default_callable_with_custom_persona(self) -> None:
137
+ router = SpireChatRouter()
138
+
139
+ with patch('django_spire.ai.chat.router.Bot') as MockBot:
140
+ mock_bot_instance = Mock()
141
+ MockBot.return_value = mock_bot_instance
142
+ mock_bot_instance.llm.prompt_to_intel.return_value = DefaultMessageIntel(text='Response')
143
+
144
+ router._default_chat_callable(
145
+ request=self.request,
146
+ user_input='Hello',
147
+ message_history=None
148
+ )
149
+
150
+ assert mock_bot_instance.llm_role is not None
151
+
152
+ def test_default_callable_passes_message_history(self) -> None:
153
+ router = SpireChatRouter()
154
+ message_history = MessageHistory()
155
+
156
+ with patch('django_spire.ai.chat.router.Bot') as MockBot:
157
+ mock_bot_instance = Mock()
158
+ MockBot.return_value = mock_bot_instance
159
+ mock_bot_instance.llm.prompt_to_intel.return_value = DefaultMessageIntel(text='Response')
160
+
161
+ router._default_chat_callable(
162
+ request=self.request,
163
+ user_input='Hello',
164
+ message_history=message_history
165
+ )
166
+
167
+ call_kwargs = mock_bot_instance.llm.prompt_to_intel.call_args[1]
168
+ assert call_kwargs['message_history'] == message_history
169
+
170
+ def test_default_callable_passes_user_input(self) -> None:
171
+ router = SpireChatRouter()
172
+
173
+ with patch('django_spire.ai.chat.router.Bot') as MockBot:
174
+ mock_bot_instance = Mock()
175
+ MockBot.return_value = mock_bot_instance
176
+ mock_bot_instance.llm.prompt_to_intel.return_value = DefaultMessageIntel(text='Response')
177
+
178
+ router._default_chat_callable(
179
+ request=self.request,
180
+ user_input='Test input',
181
+ message_history=None
182
+ )
183
+
184
+ call_kwargs = mock_bot_instance.llm.prompt_to_intel.call_args[1]
185
+ assert call_kwargs['prompt'] == 'Test input'
186
+
187
+ def test_default_callable_uses_default_message_intel(self) -> None:
188
+ router = SpireChatRouter()
189
+
190
+ with patch('django_spire.ai.chat.router.Bot') as MockBot:
191
+ mock_bot_instance = Mock()
192
+ MockBot.return_value = mock_bot_instance
193
+ mock_bot_instance.llm.prompt_to_intel.return_value = DefaultMessageIntel(text='Response')
194
+
195
+ router._default_chat_callable(
196
+ request=self.request,
197
+ user_input='Hello',
198
+ message_history=None
199
+ )
200
+
201
+ call_kwargs = mock_bot_instance.llm.prompt_to_intel.call_args[1]
202
+ assert call_kwargs['intel_class'] == DefaultMessageIntel