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
@@ -0,0 +1,126 @@
1
+ from __future__ import annotations
2
+
3
+ from django_spire.ai.prompt.tuning.prompts import (
4
+ duplication_removal_bot_instruction_prompt,
5
+ example_optimization_bot_instruction_prompt,
6
+ formatting_bot_instruction_prompt,
7
+ instruction_clarity_bot_instruction_prompt,
8
+ persona_bot_instruction_prompt,
9
+ prompt_tuning_input_prompt,
10
+ prompt_tuning_instruction_bot_prompt,
11
+ specialized_bot_input_prompt,
12
+ )
13
+ from django_spire.ai.prompt.tuning.choices import OutcomeRatingChoices
14
+ from django_spire.ai.prompt.tuning.intel import PromptTestingIntel, PromptTuningIntel
15
+ from django_spire.core.tests.test_cases import BaseTestCase
16
+
17
+
18
+ class OutcomeRatingChoicesTests(BaseTestCase):
19
+ def test_terrible_rating(self) -> None:
20
+ assert OutcomeRatingChoices.TERRIBLE.value == 1
21
+ assert OutcomeRatingChoices.TERRIBLE.label == 'terrible'
22
+
23
+ def test_bad_rating(self) -> None:
24
+ assert OutcomeRatingChoices.BAD.value == 2
25
+ assert OutcomeRatingChoices.BAD.label == 'bad'
26
+
27
+ def test_ok_rating(self) -> None:
28
+ assert OutcomeRatingChoices.OK.value == 3
29
+ assert OutcomeRatingChoices.OK.label == 'ok'
30
+
31
+ def test_good_rating(self) -> None:
32
+ assert OutcomeRatingChoices.GOOD.value == 4
33
+ assert OutcomeRatingChoices.GOOD.label == 'good'
34
+
35
+ def test_great_rating(self) -> None:
36
+ assert OutcomeRatingChoices.GREAT.value == 5
37
+ assert OutcomeRatingChoices.GREAT.label == 'great'
38
+
39
+ def test_amazing_rating(self) -> None:
40
+ assert OutcomeRatingChoices.AMAZING.value == 6
41
+ assert OutcomeRatingChoices.AMAZING.label == 'amazing'
42
+
43
+ def test_all_ratings_count(self) -> None:
44
+ assert len(OutcomeRatingChoices) == 6
45
+
46
+
47
+ class PromptTuningIntelTests(BaseTestCase):
48
+ def test_prompt_tuning_intel_creation(self) -> None:
49
+ intel = PromptTuningIntel(prompt='Test prompt')
50
+
51
+ assert intel.prompt == 'Test prompt'
52
+
53
+ def test_prompt_tuning_intel_empty_prompt(self) -> None:
54
+ intel = PromptTuningIntel(prompt='')
55
+
56
+ assert intel.prompt == ''
57
+
58
+ def test_prompt_tuning_intel_long_prompt(self) -> None:
59
+ long_prompt = 'A' * 10000
60
+ intel = PromptTuningIntel(prompt=long_prompt)
61
+
62
+ assert intel.prompt == long_prompt
63
+ assert len(intel.prompt) == 10000
64
+
65
+
66
+ class PromptTestingIntelTests(BaseTestCase):
67
+ def test_prompt_testing_intel_creation(self) -> None:
68
+ intel = PromptTestingIntel(result='Test result')
69
+
70
+ assert intel.result == 'Test result'
71
+
72
+ def test_prompt_testing_intel_empty_result(self) -> None:
73
+ intel = PromptTestingIntel(result='')
74
+
75
+ assert intel.result == ''
76
+
77
+
78
+ class PromptTuningPromptsTests(BaseTestCase):
79
+ def test_prompt_tuning_instruction_bot_prompt(self) -> None:
80
+ prompt = prompt_tuning_instruction_bot_prompt()
81
+
82
+ assert prompt is not None
83
+ assert 'System Prompt Tuning Expert' in prompt.to_str()
84
+
85
+ def test_prompt_tuning_input_prompt(self) -> None:
86
+ prompt = prompt_tuning_input_prompt('Test system prompt', 'Test feedback')
87
+
88
+ assert prompt is not None
89
+ assert 'Test system prompt' in prompt.to_str()
90
+ assert 'Test feedback' in prompt.to_str()
91
+
92
+ def test_formatting_bot_instruction_prompt(self) -> None:
93
+ prompt = formatting_bot_instruction_prompt()
94
+
95
+ assert prompt is not None
96
+ assert 'Prompt Formatting Expert' in prompt.to_str()
97
+
98
+ def test_instruction_clarity_bot_instruction_prompt(self) -> None:
99
+ prompt = instruction_clarity_bot_instruction_prompt()
100
+
101
+ assert prompt is not None
102
+ assert 'Instruction Clarity Expert' in prompt.to_str()
103
+
104
+ def test_persona_bot_instruction_prompt(self) -> None:
105
+ prompt = persona_bot_instruction_prompt()
106
+
107
+ assert prompt is not None
108
+ assert 'Persona Consistency Expert' in prompt.to_str()
109
+
110
+ def test_duplication_removal_bot_instruction_prompt(self) -> None:
111
+ prompt = duplication_removal_bot_instruction_prompt()
112
+
113
+ assert prompt is not None
114
+ assert 'Duplication Removal Expert' in prompt.to_str()
115
+
116
+ def test_example_optimization_bot_instruction_prompt(self) -> None:
117
+ prompt = example_optimization_bot_instruction_prompt()
118
+
119
+ assert prompt is not None
120
+ assert 'Example Optimization Expert' in prompt.to_str()
121
+
122
+ def test_specialized_bot_input_prompt(self) -> None:
123
+ prompt = specialized_bot_input_prompt('Test system prompt')
124
+
125
+ assert prompt is not None
126
+ assert 'Test system prompt' in prompt.to_str()
@@ -3,13 +3,19 @@ from __future__ import annotations
3
3
  import functools
4
4
  import os
5
5
 
6
+ from typing import TYPE_CHECKING, Callable
7
+
6
8
  from django.http import HttpResponseForbidden
7
9
  from twilio.request_validator import RequestValidator
8
10
 
11
+ if TYPE_CHECKING:
12
+ from django.core.handlers.wsgi import WSGIRequest
13
+ from django.http import HttpResponse
14
+
9
15
 
10
- def twilio_auth_required(func):
16
+ def twilio_auth_required(func: Callable[..., HttpResponse]) -> Callable[..., HttpResponse]:
11
17
  @functools.wraps(func)
12
- def decorated_function(request, *args, **kwargs):
18
+ def decorated_function(request: WSGIRequest, *args, **kwargs) -> HttpResponse:
13
19
  request_validator = RequestValidator(os.environ.get('TWILIO_AUTH_TOKEN', ''))
14
20
 
15
21
  absolute_uri = request.build_absolute_uri()
@@ -1,30 +1,254 @@
1
+ from __future__ import annotations
2
+
1
3
  from django.test import TestCase
2
4
 
3
- from django_spire.ai.sms.models import SmsConversation
5
+ from django_spire.ai.sms.models import SmsConversation, SmsMessage
6
+
7
+
8
+ class SmsConversationModelTests(TestCase):
9
+ def test_conversation_creation(self) -> None:
10
+ conversation = SmsConversation.objects.create(
11
+ phone_number='+15551234567'
12
+ )
13
+
14
+ assert conversation.phone_number == '+15551234567'
15
+ assert conversation.messages.count() == 0
16
+ assert conversation.is_empty
17
+
18
+ def test_conversation_str(self) -> None:
19
+ conversation = SmsConversation.objects.create(
20
+ phone_number='+15551234567'
21
+ )
22
+
23
+ assert '+15551234567' in str(conversation)
24
+ assert 'SMS Conversation' in str(conversation)
25
+
26
+ def test_add_message(self) -> None:
27
+ conversation = SmsConversation.objects.create(
28
+ phone_number='+15551234567'
29
+ )
30
+
31
+ message = conversation.add_message(body='Hello', is_inbound=True, twilio_sid='')
32
+
33
+ assert message.body == 'Hello'
34
+ assert message.is_inbound
35
+
36
+ message = conversation.add_message(body='Hi there', is_inbound=False, twilio_sid='')
37
+
38
+ assert message.body == 'Hi there'
39
+ assert not message.is_inbound
40
+
41
+ assert conversation.messages.count() == 2
42
+ assert not conversation.is_empty
43
+
44
+ def test_add_message_updates_last_message_datetime(self) -> None:
45
+ conversation = SmsConversation.objects.create(
46
+ phone_number='+15551234567'
47
+ )
48
+
49
+ initial_datetime = conversation.last_message_datetime
50
+
51
+ conversation.add_message(body='Test', is_inbound=True, twilio_sid='')
52
+ conversation.refresh_from_db()
53
+
54
+ assert conversation.last_message_datetime >= initial_datetime
55
+
56
+ def test_generate_message_history(self) -> None:
57
+ conversation = SmsConversation.objects.create(
58
+ phone_number='+15551234567'
59
+ )
60
+
61
+ conversation.add_message(body='User message 1', is_inbound=True, twilio_sid='')
62
+ conversation.add_message(body='Assistant response 1', is_inbound=False, twilio_sid='')
63
+ conversation.add_message(body='User message 2', is_inbound=True, twilio_sid='')
4
64
 
65
+ message_history = conversation.generate_message_history(
66
+ message_count=20,
67
+ exclude_last_message=True
68
+ )
69
+
70
+ assert message_history is not None
5
71
 
6
- class SmsModelTests(TestCase):
7
- def test_conversation_creation(self):
72
+ def test_generate_message_history_includes_last_message(self) -> None:
8
73
  conversation = SmsConversation.objects.create(
9
74
  phone_number='+15551234567'
10
75
  )
11
- self.assertEqual(conversation.phone_number, '+15551234567')
12
- self.assertEqual(conversation.messages.count(), 0)
13
- self.assertTrue(conversation.is_empty)
14
76
 
15
- def test_add_message(self):
77
+ conversation.add_message(body='User message 1', is_inbound=True, twilio_sid='')
78
+ conversation.add_message(body='User message 2', is_inbound=True, twilio_sid='')
79
+
80
+ message_history = conversation.generate_message_history(
81
+ message_count=20,
82
+ exclude_last_message=False
83
+ )
84
+
85
+ assert message_history is not None
86
+
87
+ def test_is_empty_property(self) -> None:
88
+ conversation = SmsConversation.objects.create(
89
+ phone_number='+15551234567'
90
+ )
91
+
92
+ assert conversation.is_empty
93
+
94
+ conversation.add_message(body='Test', is_inbound=True, twilio_sid='')
95
+
96
+ assert not conversation.is_empty
97
+
98
+
99
+ class SmsMessageModelTests(TestCase):
100
+ def test_message_creation(self) -> None:
101
+ conversation = SmsConversation.objects.create(
102
+ phone_number='+15551234567'
103
+ )
104
+ message = SmsMessage.objects.create(
105
+ conversation=conversation,
106
+ body='Test message',
107
+ is_inbound=True,
108
+ twilio_sid='SM123'
109
+ )
110
+
111
+ assert message.body == 'Test message'
112
+ assert message.is_inbound
113
+ assert message.twilio_sid == 'SM123'
114
+
115
+ def test_message_str_short(self) -> None:
116
+ conversation = SmsConversation.objects.create(
117
+ phone_number='+15551234567'
118
+ )
119
+ message = SmsMessage.objects.create(
120
+ conversation=conversation,
121
+ body='Short message',
122
+ is_inbound=True
123
+ )
124
+
125
+ assert 'Inbound' in str(message)
126
+ assert 'Short message' in str(message)
127
+
128
+ def test_message_str_long(self) -> None:
129
+ conversation = SmsConversation.objects.create(
130
+ phone_number='+15551234567'
131
+ )
132
+ long_body = 'A' * 100
133
+ message = SmsMessage.objects.create(
134
+ conversation=conversation,
135
+ body=long_body,
136
+ is_inbound=True
137
+ )
138
+
139
+ assert '...' in str(message)
140
+
141
+ def test_direction_property_inbound(self) -> None:
142
+ conversation = SmsConversation.objects.create(
143
+ phone_number='+15551234567'
144
+ )
145
+ message = SmsMessage.objects.create(
146
+ conversation=conversation,
147
+ body='Test',
148
+ is_inbound=True
149
+ )
150
+
151
+ assert message.direction == 'Inbound'
152
+
153
+ def test_direction_property_outbound(self) -> None:
154
+ conversation = SmsConversation.objects.create(
155
+ phone_number='+15551234567'
156
+ )
157
+ message = SmsMessage.objects.create(
158
+ conversation=conversation,
159
+ body='Test',
160
+ is_inbound=False
161
+ )
162
+
163
+ assert message.direction == 'Outbound'
164
+
165
+ def test_is_outbound_property(self) -> None:
166
+ conversation = SmsConversation.objects.create(
167
+ phone_number='+15551234567'
168
+ )
169
+
170
+ inbound_message = SmsMessage.objects.create(
171
+ conversation=conversation,
172
+ body='Test',
173
+ is_inbound=True
174
+ )
175
+
176
+ outbound_message = SmsMessage.objects.create(
177
+ conversation=conversation,
178
+ body='Test',
179
+ is_inbound=False
180
+ )
181
+
182
+ assert not inbound_message.is_outbound
183
+ assert outbound_message.is_outbound
184
+
185
+ def test_role_property_inbound(self) -> None:
186
+ conversation = SmsConversation.objects.create(
187
+ phone_number='+15551234567'
188
+ )
189
+ message = SmsMessage.objects.create(
190
+ conversation=conversation,
191
+ body='Test',
192
+ is_inbound=True
193
+ )
194
+
195
+ assert message.role == 'user'
196
+
197
+ def test_role_property_outbound(self) -> None:
198
+ conversation = SmsConversation.objects.create(
199
+ phone_number='+15551234567'
200
+ )
201
+ message = SmsMessage.objects.create(
202
+ conversation=conversation,
203
+ body='Test',
204
+ is_inbound=False
205
+ )
206
+
207
+ assert message.role == 'assistant'
208
+
209
+ def test_message_is_processed_default(self) -> None:
210
+ conversation = SmsConversation.objects.create(
211
+ phone_number='+15551234567'
212
+ )
213
+ message = SmsMessage.objects.create(
214
+ conversation=conversation,
215
+ body='Test',
216
+ is_inbound=True
217
+ )
218
+
219
+ assert message.is_processed is False
220
+
221
+
222
+ class SmsQuerySetTests(TestCase):
223
+ def test_by_phone_number(self) -> None:
224
+ SmsConversation.objects.create(phone_number='+15551234567')
225
+ SmsConversation.objects.create(phone_number='+15559876543')
226
+
227
+ result = SmsConversation.objects.by_phone_number('+15551234567')
228
+
229
+ assert result.count() == 1
230
+ assert result.first().phone_number == '+15551234567'
231
+
232
+ def test_newest_by_count(self) -> None:
233
+ conversation = SmsConversation.objects.create(
234
+ phone_number='+15551234567'
235
+ )
236
+
237
+ for i in range(25):
238
+ conversation.add_message(body=f'Message {i}', is_inbound=True, twilio_sid='')
239
+
240
+ messages = conversation.messages.newest_by_count(20)
241
+
242
+ assert len(messages) == 20
243
+
244
+ def test_newest_by_count_reversed(self) -> None:
16
245
  conversation = SmsConversation.objects.create(
17
246
  phone_number='+15551234567'
18
247
  )
19
-
20
- message = conversation.add_message(body="Hello", is_inbound=True, twilio_sid='')
21
- self.assertEqual(message.body, "Hello")
22
- self.assertTrue(message.is_inbound)
23
248
 
24
- message = conversation.add_message(body="Hi there", is_inbound=False, twilio_sid='')
25
- self.assertEqual(message.body, "Hi there")
26
- self.assertFalse(message.is_inbound)
249
+ for i in range(5):
250
+ conversation.add_message(body=f'Message {i}', is_inbound=True, twilio_sid='')
27
251
 
28
- self.assertEqual(conversation.messages.count(), 2)
29
- self.assertFalse(conversation.is_empty)
252
+ messages = conversation.messages.newest_by_count_reversed(5)
30
253
 
254
+ assert len(messages) == 5
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+
3
+ from django_spire.ai.sms.intel import SmsIntel
4
+ from django_spire.core.tests.test_cases import BaseTestCase
5
+
6
+
7
+ class SmsIntelTests(BaseTestCase):
8
+ def test_sms_intel_creation(self) -> None:
9
+ intel = SmsIntel(body='Test message')
10
+
11
+ assert intel.body == 'Test message'
12
+
13
+ def test_sms_intel_empty_body(self) -> None:
14
+ intel = SmsIntel(body='')
15
+
16
+ assert intel.body == ''
17
+
18
+ def test_sms_intel_long_body(self) -> None:
19
+ long_body = 'A' * 1000
20
+ intel = SmsIntel(body=long_body)
21
+
22
+ assert intel.body == long_body
23
+ assert len(intel.body) == 1000
24
+
25
+ def test_sms_intel_unicode_body(self) -> None:
26
+ unicode_body = 'Hello 世界 🌍'
27
+ intel = SmsIntel(body=unicode_body)
28
+
29
+ assert intel.body == unicode_body
30
+
31
+ def test_sms_intel_model_dump(self) -> None:
32
+ intel = SmsIntel(body='Test')
33
+ dump = intel.model_dump()
34
+
35
+ assert 'body' in dump
36
+ assert dump['body'] == 'Test'
37
+
38
+ def test_sms_intel_special_characters(self) -> None:
39
+ special_body = 'Line 1\nLine 2\tTabbed'
40
+ intel = SmsIntel(body=special_body)
41
+
42
+ assert intel.body == special_body
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from unittest.mock import patch
2
4
 
3
5
  from django.urls import reverse
@@ -7,13 +9,13 @@ from django_spire.core.tests.test_cases import BaseTestCase
7
9
 
8
10
 
9
11
  class SmsWebhookTests(BaseTestCase):
10
- def setUp(self):
12
+ def setUp(self) -> None:
11
13
  super().setUp()
12
14
 
13
15
  self.webhook_url = reverse('django_spire:ai:sms:webhook')
14
16
 
15
17
  @patch('twilio.request_validator.RequestValidator.validate')
16
- def test_webhook_receives_message(self, mock_validate):
18
+ def test_webhook_receives_message(self, mock_validate) -> None:
17
19
  mock_validate.return_value = True
18
20
 
19
21
  response = self.client.post(
@@ -25,14 +27,160 @@ class SmsWebhookTests(BaseTestCase):
25
27
  }
26
28
  )
27
29
 
28
- self.assertEqual(response.status_code, 200)
30
+ assert response.status_code == 200
29
31
 
30
32
  conversation = SmsConversation.objects.get(phone_number='+15551234567')
31
- self.assertEqual(conversation.messages.count(), 2) # Inbound and outbound
33
+ assert conversation.messages.count() == 2
32
34
 
33
35
  inbound_message = conversation.messages.filter(is_inbound=True).first()
34
- self.assertEqual(inbound_message.body, 'Hello')
35
- self.assertEqual(inbound_message.twilio_sid, 'SM123456789')
36
+ assert inbound_message.body == 'Hello'
37
+ assert inbound_message.twilio_sid == 'SM123456789'
36
38
 
37
39
  outbound_message = conversation.messages.filter(is_inbound=False).first()
38
- self.assertTrue(len(outbound_message.body) > 0)
40
+ assert len(outbound_message.body) > 0
41
+
42
+ @patch('twilio.request_validator.RequestValidator.validate')
43
+ def test_webhook_creates_conversation(self, mock_validate) -> None:
44
+ mock_validate.return_value = True
45
+
46
+ initial_count = SmsConversation.objects.count()
47
+
48
+ self.client.post(
49
+ self.webhook_url,
50
+ {
51
+ 'From': '+15559999999',
52
+ 'Body': 'New conversation',
53
+ 'MessageSid': 'SM999999999'
54
+ }
55
+ )
56
+
57
+ assert SmsConversation.objects.count() == initial_count + 1
58
+
59
+ @patch('twilio.request_validator.RequestValidator.validate')
60
+ def test_webhook_reuses_existing_conversation(self, mock_validate) -> None:
61
+ mock_validate.return_value = True
62
+
63
+ SmsConversation.objects.create(phone_number='+15551234567')
64
+
65
+ initial_count = SmsConversation.objects.count()
66
+
67
+ self.client.post(
68
+ self.webhook_url,
69
+ {
70
+ 'From': '+15551234567',
71
+ 'Body': 'Another message',
72
+ 'MessageSid': 'SM123456789'
73
+ }
74
+ )
75
+
76
+ assert SmsConversation.objects.count() == initial_count
77
+
78
+ @patch('twilio.request_validator.RequestValidator.validate')
79
+ def test_webhook_invalid_signature(self, mock_validate) -> None:
80
+ mock_validate.return_value = False
81
+
82
+ response = self.client.post(
83
+ self.webhook_url,
84
+ {
85
+ 'From': '+15551234567',
86
+ 'Body': 'Hello',
87
+ 'MessageSid': 'SM123456789'
88
+ }
89
+ )
90
+
91
+ assert response.status_code == 403
92
+
93
+ @patch('twilio.request_validator.RequestValidator.validate')
94
+ def test_webhook_short_phone_number(self, mock_validate) -> None:
95
+ mock_validate.return_value = True
96
+
97
+ response = self.client.post(
98
+ self.webhook_url,
99
+ {
100
+ 'From': '1234',
101
+ 'Body': 'Hello',
102
+ 'MessageSid': 'SM123456789'
103
+ }
104
+ )
105
+
106
+ assert response.status_code == 403
107
+
108
+ @patch('twilio.request_validator.RequestValidator.validate')
109
+ def test_webhook_empty_body(self, mock_validate) -> None:
110
+ mock_validate.return_value = True
111
+
112
+ response = self.client.post(
113
+ self.webhook_url,
114
+ {
115
+ 'From': '+15551234567',
116
+ 'Body': '',
117
+ 'MessageSid': 'SM123456789'
118
+ }
119
+ )
120
+
121
+ assert response.status_code == 200
122
+
123
+ @patch('twilio.request_validator.RequestValidator.validate')
124
+ def test_webhook_marks_inbound_as_processed(self, mock_validate) -> None:
125
+ mock_validate.return_value = True
126
+
127
+ self.client.post(
128
+ self.webhook_url,
129
+ {
130
+ 'From': '+15551234567',
131
+ 'Body': 'Test message',
132
+ 'MessageSid': 'SM123456789'
133
+ }
134
+ )
135
+
136
+ conversation = SmsConversation.objects.get(phone_number='+15551234567')
137
+ inbound_message = conversation.messages.filter(is_inbound=True).first()
138
+
139
+ assert inbound_message.is_processed
140
+
141
+ @patch('twilio.request_validator.RequestValidator.validate')
142
+ def test_webhook_response_is_twiml(self, mock_validate) -> None:
143
+ mock_validate.return_value = True
144
+
145
+ response = self.client.post(
146
+ self.webhook_url,
147
+ {
148
+ 'From': '+15551234567',
149
+ 'Body': 'Hello',
150
+ 'MessageSid': 'SM123456789'
151
+ }
152
+ )
153
+
154
+ assert b'<Response>' in response.content or b'Response' in response.content
155
+
156
+ @patch('twilio.request_validator.RequestValidator.validate')
157
+ def test_webhook_missing_from(self, mock_validate) -> None:
158
+ mock_validate.return_value = True
159
+
160
+ response = self.client.post(
161
+ self.webhook_url,
162
+ {
163
+ 'Body': 'Hello',
164
+ 'MessageSid': 'SM123456789'
165
+ }
166
+ )
167
+
168
+ assert response.status_code == 403
169
+
170
+ @patch('twilio.request_validator.RequestValidator.validate')
171
+ def test_webhook_stores_twilio_sid(self, mock_validate) -> None:
172
+ mock_validate.return_value = True
173
+
174
+ self.client.post(
175
+ self.webhook_url,
176
+ {
177
+ 'From': '+15551234567',
178
+ 'Body': 'Hello',
179
+ 'MessageSid': 'SM_TEST_SID_123'
180
+ }
181
+ )
182
+
183
+ conversation = SmsConversation.objects.get(phone_number='+15551234567')
184
+ inbound_message = conversation.messages.filter(is_inbound=True).first()
185
+
186
+ assert inbound_message.twilio_sid == 'SM_TEST_SID_123'