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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (541) hide show
  1. django_spire/ai/admin.py +11 -11
  2. django_spire/ai/chat/apps.py +1 -0
  3. django_spire/ai/chat/templates/django_spire/ai/chat/widget/dialog_widget.html +1 -1
  4. django_spire/ai/chat/tests/factories.py +15 -0
  5. django_spire/ai/chat/tests/test_controller.py +45 -0
  6. django_spire/ai/chat/tests/test_models.py +301 -0
  7. django_spire/ai/chat/tests/test_prompts.py +48 -0
  8. django_spire/ai/chat/tests/test_responses.py +208 -0
  9. django_spire/ai/chat/tests/test_router/test_base_chat_router.py +66 -6
  10. django_spire/ai/chat/tests/test_router/test_chat_workflow.py +73 -3
  11. django_spire/ai/chat/tests/test_router/test_integration.py +86 -6
  12. django_spire/ai/chat/tests/test_router/test_intent_decoder.py +93 -1
  13. django_spire/ai/chat/tests/test_router/test_message_intel.py +60 -1
  14. django_spire/ai/chat/tests/test_router/test_spire_chat_router.py +110 -0
  15. django_spire/ai/chat/tests/test_urls/test_json_urls.py +202 -1
  16. django_spire/ai/context/tests/__init__.py +0 -0
  17. django_spire/ai/context/tests/test_context.py +188 -0
  18. django_spire/ai/decorators.py +7 -6
  19. django_spire/ai/prompt/tests/test_bots.py +100 -10
  20. django_spire/ai/prompt/tests/test_prompt_intel.py +83 -0
  21. django_spire/ai/prompt/tests/test_prompt_tuning.py +126 -0
  22. django_spire/ai/sms/decorators.py +8 -2
  23. django_spire/ai/sms/tests/test_sms.py +240 -16
  24. django_spire/ai/sms/tests/test_sms_intel.py +42 -0
  25. django_spire/ai/sms/tests/test_webhook.py +155 -7
  26. django_spire/ai/sms/views.py +23 -24
  27. django_spire/ai/tests/test_ai.py +131 -7
  28. django_spire/auth/apps.py +4 -2
  29. django_spire/auth/controller/controller.py +36 -23
  30. django_spire/auth/controller/exceptions.py +9 -0
  31. django_spire/auth/group/admin.py +1 -0
  32. django_spire/auth/group/apps.py +2 -0
  33. django_spire/auth/group/factories.py +17 -8
  34. django_spire/auth/group/forms.py +7 -0
  35. django_spire/auth/group/tests/test_factories.py +146 -0
  36. django_spire/auth/group/tests/test_forms.py +282 -0
  37. django_spire/auth/group/tests/test_models.py +192 -0
  38. django_spire/auth/group/tests/test_querysets.py +98 -0
  39. django_spire/auth/group/tests/test_utils.py +341 -0
  40. django_spire/auth/group/tests/test_views.py +377 -0
  41. django_spire/auth/group/urls/__init__.py +3 -1
  42. django_spire/auth/group/urls/form_urls.py +2 -0
  43. django_spire/auth/group/urls/json_urls.py +3 -0
  44. django_spire/auth/group/urls/page_urls.py +2 -0
  45. django_spire/auth/group/utils.py +6 -2
  46. django_spire/auth/group/views/form_views.py +6 -3
  47. django_spire/auth/group/views/json_views.py +6 -2
  48. django_spire/auth/mfa/admin.py +2 -0
  49. django_spire/auth/mfa/apps.py +2 -0
  50. django_spire/auth/mfa/forms.py +1 -0
  51. django_spire/auth/mfa/querysets.py +9 -2
  52. django_spire/auth/mfa/tests/test_models.py +233 -0
  53. django_spire/auth/mfa/tests/test_utils.py +106 -0
  54. django_spire/auth/mfa/urls/__init__.py +2 -0
  55. django_spire/auth/mfa/urls/page_urls.py +2 -0
  56. django_spire/auth/mfa/urls/redirect_urls.py +2 -0
  57. django_spire/auth/mfa/views/page_views.py +2 -1
  58. django_spire/auth/permissions/consts.py +2 -2
  59. django_spire/auth/permissions/decorators.py +8 -8
  60. django_spire/auth/permissions/permissions.py +28 -35
  61. django_spire/auth/permissions/tests/test_decorators.py +333 -0
  62. django_spire/auth/permissions/tests/test_permissions.py +337 -0
  63. django_spire/auth/permissions/tests/test_tools.py +305 -0
  64. django_spire/auth/permissions/tools.py +21 -15
  65. django_spire/auth/seeding/seed.py +3 -0
  66. django_spire/auth/seeding/seeder.py +2 -0
  67. django_spire/auth/tests/test_controller.py +323 -0
  68. django_spire/auth/tests/test_url_endpoints.py +9 -9
  69. django_spire/auth/tests/test_views.py +406 -0
  70. django_spire/auth/urls/admin_urls.py +2 -0
  71. django_spire/auth/urls/redirect_urls.py +2 -0
  72. django_spire/auth/user/apps.py +2 -0
  73. django_spire/auth/user/forms.py +9 -0
  74. django_spire/auth/user/models.py +1 -1
  75. django_spire/auth/user/services/services.py +1 -0
  76. django_spire/auth/user/tests/factories.py +14 -13
  77. django_spire/auth/user/tests/test_factories.py +166 -2
  78. django_spire/auth/user/tests/test_forms.py +573 -0
  79. django_spire/auth/user/tests/test_models.py +257 -0
  80. django_spire/auth/user/tests/test_services.py +200 -0
  81. django_spire/auth/user/tests/test_tools.py +153 -0
  82. django_spire/auth/user/tests/test_user_factories.py +139 -0
  83. django_spire/auth/user/tests/test_views.py +363 -0
  84. django_spire/auth/user/tools.py +7 -1
  85. django_spire/auth/user/urls/form_urls.py +3 -0
  86. django_spire/auth/user/urls/page_urls.py +3 -0
  87. django_spire/auth/user/views/form_views.py +19 -10
  88. django_spire/auth/user/views/page_views.py +8 -2
  89. django_spire/auth/views/redirect_views.py +14 -9
  90. django_spire/comment/admin.py +2 -0
  91. django_spire/comment/apps.py +2 -0
  92. django_spire/comment/templatetags/comment_tags.py +1 -0
  93. django_spire/comment/tests/test_forms.py +27 -0
  94. django_spire/comment/tests/test_models.py +215 -0
  95. django_spire/comment/tests/test_querysets.py +101 -0
  96. django_spire/comment/tests/test_utils.py +90 -0
  97. django_spire/comment/urls.py +2 -0
  98. django_spire/comment/utils.py +22 -13
  99. django_spire/comment/views.py +1 -1
  100. django_spire/conf.py +8 -6
  101. django_spire/consts.py +1 -1
  102. django_spire/contrib/breadcrumb/apps.py +2 -0
  103. django_spire/contrib/breadcrumb/breadcrumbs.py +18 -18
  104. django_spire/contrib/breadcrumb/tests/test_breadcrumbs.py +198 -0
  105. django_spire/contrib/constructor/__init__.py +3 -3
  106. django_spire/contrib/constructor/constructor.py +15 -15
  107. django_spire/contrib/constructor/django_model_constructor.py +5 -4
  108. django_spire/contrib/constructor/exceptions.py +5 -3
  109. django_spire/contrib/constructor/tests/__init__.py +0 -0
  110. django_spire/contrib/constructor/tests/test_constructor.py +193 -0
  111. django_spire/contrib/form/tests/__init__.py +0 -0
  112. django_spire/contrib/form/tests/test_forms.py +203 -0
  113. django_spire/contrib/generic_views/modal_views.py +2 -1
  114. django_spire/contrib/generic_views/portal_views.py +20 -19
  115. django_spire/contrib/generic_views/tests/__init__.py +0 -0
  116. django_spire/contrib/generic_views/tests/test_views.py +459 -0
  117. django_spire/contrib/help/apps.py +2 -0
  118. django_spire/contrib/help/templatetags/help.py +1 -0
  119. django_spire/contrib/help/tests/__init__.py +0 -0
  120. django_spire/contrib/help/tests/test_templatetags.py +100 -0
  121. django_spire/contrib/options/mixins.py +6 -5
  122. django_spire/contrib/options/tests/factories.py +5 -1
  123. django_spire/contrib/options/tests/test_options.py +234 -0
  124. django_spire/contrib/ordering/exceptions.py +7 -3
  125. django_spire/contrib/ordering/mixins.py +2 -0
  126. django_spire/contrib/ordering/querysets.py +3 -1
  127. django_spire/contrib/ordering/services/processor_service.py +8 -4
  128. django_spire/contrib/ordering/services/service.py +1 -2
  129. django_spire/contrib/ordering/tests/__init__.py +0 -0
  130. django_spire/contrib/ordering/tests/test_ordering.py +165 -0
  131. django_spire/contrib/ordering/validators.py +6 -6
  132. django_spire/contrib/pagination/templatetags/pagination_tags.py +12 -5
  133. django_spire/contrib/pagination/tests/__init__.py +0 -0
  134. django_spire/contrib/pagination/tests/test_pagination.py +179 -0
  135. django_spire/contrib/performance/decorators.py +16 -6
  136. django_spire/contrib/performance/tests/__init__.py +0 -0
  137. django_spire/contrib/performance/tests/test_performance.py +107 -0
  138. django_spire/contrib/progress/__init__.py +1 -3
  139. django_spire/contrib/progress/static/django_spire/js/contrib/progress/progress.js +38 -82
  140. django_spire/contrib/queryset/enums.py +3 -1
  141. django_spire/contrib/queryset/filter_tools.py +10 -5
  142. django_spire/contrib/queryset/mixins.py +16 -16
  143. django_spire/contrib/queryset/tests/__init__.py +0 -0
  144. django_spire/contrib/queryset/tests/test_queryset.py +137 -0
  145. django_spire/contrib/seeding/field/base.py +13 -7
  146. django_spire/contrib/seeding/field/callable.py +8 -1
  147. django_spire/contrib/seeding/field/cleaners.py +5 -5
  148. django_spire/contrib/seeding/field/custom.py +20 -10
  149. django_spire/contrib/seeding/field/django/seeder.py +8 -6
  150. django_spire/contrib/seeding/field/enums.py +7 -5
  151. django_spire/contrib/seeding/field/override.py +16 -6
  152. django_spire/contrib/seeding/field/static.py +9 -2
  153. django_spire/contrib/seeding/field/tests/test_base.py +18 -14
  154. django_spire/contrib/seeding/field/tests/test_callable.py +13 -9
  155. django_spire/contrib/seeding/field/tests/test_cleaners.py +51 -38
  156. django_spire/contrib/seeding/field/tests/test_static.py +13 -9
  157. django_spire/contrib/seeding/intelligence/bots/seeder_generator_bot.py +2 -0
  158. django_spire/contrib/seeding/intelligence/intel.py +5 -1
  159. django_spire/contrib/seeding/intelligence/prompts/factory.py +6 -1
  160. django_spire/contrib/seeding/intelligence/prompts/foreign_key_selection_prompt.py +6 -1
  161. django_spire/contrib/seeding/intelligence/prompts/generate_django_model_seeder_prompts.py +2 -0
  162. django_spire/contrib/seeding/intelligence/prompts/generic_relationship_selection_prompt.py +7 -1
  163. django_spire/contrib/seeding/intelligence/prompts/hierarchical_selection_prompt.py +6 -2
  164. django_spire/contrib/seeding/intelligence/prompts/model_field_choices_prompt.py +8 -2
  165. django_spire/contrib/seeding/intelligence/prompts/negation_prompt.py +2 -0
  166. django_spire/contrib/seeding/intelligence/prompts/objective_prompt.py +6 -1
  167. django_spire/contrib/seeding/management/commands/seeding.py +9 -3
  168. django_spire/contrib/seeding/management/example.py +2 -0
  169. django_spire/contrib/seeding/model/base.py +16 -7
  170. django_spire/contrib/seeding/model/config.py +31 -15
  171. django_spire/contrib/seeding/model/django/config.py +13 -13
  172. django_spire/contrib/seeding/model/django/seeder.py +4 -4
  173. django_spire/contrib/seeding/model/django/tests/test_seeder.py +34 -23
  174. django_spire/contrib/seeding/model/enums.py +2 -0
  175. django_spire/contrib/seeding/tests/test_config.py +71 -0
  176. django_spire/contrib/seeding/tests/test_custom.py +35 -0
  177. django_spire/contrib/seeding/tests/test_enums.py +40 -0
  178. django_spire/contrib/seeding/tests/test_intel.py +32 -0
  179. django_spire/contrib/seeding/tests/test_override.py +63 -0
  180. django_spire/contrib/service/__init__.py +2 -2
  181. django_spire/contrib/service/django_model_service.py +16 -15
  182. django_spire/contrib/service/exceptions.py +5 -3
  183. django_spire/contrib/service/tests/__init__.py +0 -0
  184. django_spire/contrib/service/tests/test_service.py +153 -0
  185. django_spire/contrib/session/apps.py +2 -0
  186. django_spire/contrib/session/controller.py +48 -42
  187. django_spire/contrib/session/templatetags/session_tags.py +11 -2
  188. django_spire/contrib/session/tests/test_session_controller.py +117 -53
  189. django_spire/contrib/tests/__init__.py +0 -0
  190. django_spire/contrib/tests/test_utils.py +37 -0
  191. django_spire/contrib/utils.py +4 -1
  192. django_spire/core/apps.py +2 -0
  193. django_spire/core/converters/tests/test_to_data.py +353 -0
  194. django_spire/core/converters/tests/test_to_enums.py +61 -41
  195. django_spire/core/converters/tests/test_to_pydantic.py +138 -109
  196. django_spire/core/converters/to_data.py +29 -10
  197. django_spire/core/converters/to_enums.py +4 -2
  198. django_spire/core/converters/to_pydantic.py +22 -22
  199. django_spire/core/decorators.py +19 -6
  200. django_spire/core/forms/widgets.py +4 -0
  201. django_spire/core/maps.py +3 -1
  202. django_spire/core/middleware/maintenance.py +3 -3
  203. django_spire/core/middleware.py +8 -6
  204. django_spire/core/redirect/__init__.py +5 -0
  205. django_spire/core/redirect/generic_redirect.py +1 -2
  206. django_spire/core/redirect/tests/__init__.py +0 -0
  207. django_spire/core/redirect/tests/test_generic_redirect.py +34 -0
  208. django_spire/core/{tests/tests_redirect.py → redirect/tests/test_safe_redirect.py} +55 -81
  209. django_spire/core/shortcuts.py +3 -3
  210. django_spire/core/static/django_spire/css/app-layout.css +1 -1
  211. django_spire/core/static/django_spire/css/app-navigation.css +3 -3
  212. django_spire/core/static/django_spire/css/bootstrap-override.css +4 -0
  213. django_spire/core/tag/admin.py +12 -0
  214. django_spire/core/tag/intelligence/tag_set_bot.py +2 -0
  215. django_spire/core/tag/mixins.py +2 -0
  216. django_spire/core/tag/models.py +2 -0
  217. django_spire/core/tag/querysets.py +2 -0
  218. django_spire/core/tag/service/tag_service.py +6 -3
  219. django_spire/core/tag/tests/test_intelligence.py +9 -9
  220. django_spire/core/tag/tests/test_tags.py +44 -54
  221. django_spire/core/tag/tests/test_tools.py +191 -0
  222. django_spire/core/tag/tools.py +3 -0
  223. django_spire/core/templates/django_spire/card/card.html +5 -2
  224. django_spire/core/templates/django_spire/card/title_card.html +9 -4
  225. django_spire/core/templates/django_spire/infinite_scroll/base.html +1 -0
  226. django_spire/core/templates/django_spire/navigation/side_navigation.html +19 -24
  227. django_spire/core/templates/django_spire/page/full_page.html +46 -16
  228. django_spire/core/templates/django_spire/table/base.html +4 -2
  229. django_spire/core/templatetags/json.py +6 -2
  230. django_spire/core/templatetags/message.py +13 -8
  231. django_spire/core/templatetags/string_formating.py +8 -5
  232. django_spire/core/templatetags/tests/__init__.py +0 -0
  233. django_spire/core/templatetags/tests/test_templatetags.py +427 -0
  234. django_spire/core/templatetags/variable_types.py +17 -9
  235. django_spire/core/tests/test_cases.py +1 -1
  236. django_spire/core/tests/test_conf.py +43 -0
  237. django_spire/core/tests/test_consts.py +28 -0
  238. django_spire/core/tests/test_context_processors.py +93 -0
  239. django_spire/core/tests/test_decorators.py +95 -0
  240. django_spire/core/tests/test_django_spire_utils.py +56 -0
  241. django_spire/core/tests/test_exceptions.py +37 -0
  242. django_spire/core/tests/test_models.py +54 -0
  243. django_spire/core/tests/test_settings.py +45 -0
  244. django_spire/core/tests/test_shortcuts.py +74 -0
  245. django_spire/core/tests/test_urls.py +16 -0
  246. django_spire/core/tests/test_utils.py +58 -0
  247. django_spire/core/urls.py +4 -1
  248. django_spire/core/utils.py +12 -8
  249. django_spire/exceptions.py +16 -1
  250. django_spire/file/admin.py +4 -2
  251. django_spire/file/apps.py +8 -10
  252. django_spire/file/fields.py +7 -7
  253. django_spire/file/forms.py +1 -1
  254. django_spire/file/interfaces.py +15 -15
  255. django_spire/file/mixins.py +1 -4
  256. django_spire/file/models.py +3 -5
  257. django_spire/file/tests/factories.py +59 -0
  258. django_spire/file/tests/test_admin.py +69 -0
  259. django_spire/file/tests/test_apps.py +24 -0
  260. django_spire/file/tests/test_fields.py +114 -0
  261. django_spire/file/tests/test_forms.py +20 -0
  262. django_spire/file/tests/test_interfaces.py +183 -0
  263. django_spire/file/tests/test_models.py +82 -0
  264. django_spire/file/tests/test_querysets.py +102 -0
  265. django_spire/file/tests/test_utils.py +32 -0
  266. django_spire/file/tests/test_views.py +145 -0
  267. django_spire/file/tests/test_widgets.py +82 -0
  268. django_spire/file/tools.py +8 -2
  269. django_spire/file/views.py +7 -3
  270. django_spire/file/widgets.py +12 -12
  271. django_spire/help_desk/admin.py +15 -0
  272. django_spire/help_desk/apps.py +2 -0
  273. django_spire/help_desk/auth/controller.py +2 -0
  274. django_spire/help_desk/choices.py +2 -0
  275. django_spire/help_desk/enums.py +2 -0
  276. django_spire/help_desk/exceptions.py +31 -3
  277. django_spire/help_desk/forms.py +2 -0
  278. django_spire/help_desk/models.py +2 -0
  279. django_spire/help_desk/querysets.py +4 -1
  280. django_spire/help_desk/services/notification_service.py +26 -27
  281. django_spire/help_desk/services/service.py +2 -3
  282. django_spire/help_desk/tests/factories.py +8 -3
  283. django_spire/help_desk/tests/test_admin.py +41 -0
  284. django_spire/help_desk/tests/test_apps.py +41 -0
  285. django_spire/help_desk/tests/test_choices.py +50 -0
  286. django_spire/help_desk/tests/test_controller.py +87 -0
  287. django_spire/help_desk/tests/test_enums.py +18 -0
  288. django_spire/help_desk/tests/test_exceptions.py +37 -0
  289. django_spire/help_desk/tests/test_forms.py +89 -0
  290. django_spire/help_desk/tests/test_models.py +59 -0
  291. django_spire/help_desk/tests/test_querysets.py +38 -0
  292. django_spire/help_desk/tests/test_services/test_notification_service.py +15 -8
  293. django_spire/help_desk/tests/test_services/test_service.py +92 -0
  294. django_spire/help_desk/tests/test_urls/test_form_urls.py +6 -6
  295. django_spire/help_desk/tests/test_urls/test_page_urls.py +8 -9
  296. django_spire/help_desk/tests/test_views/test_form_views.py +46 -19
  297. django_spire/help_desk/tests/test_views/test_page_views.py +32 -9
  298. django_spire/help_desk/urls/__init__.py +4 -1
  299. django_spire/help_desk/urls/form_urls.py +3 -0
  300. django_spire/help_desk/urls/page_urls.py +3 -0
  301. django_spire/help_desk/views/form_views.py +13 -5
  302. django_spire/help_desk/views/page_views.py +11 -3
  303. django_spire/history/activity/admin.py +2 -0
  304. django_spire/history/activity/apps.py +3 -1
  305. django_spire/history/activity/mixins.py +13 -7
  306. django_spire/history/activity/models.py +6 -5
  307. django_spire/history/activity/querysets.py +2 -0
  308. django_spire/history/activity/tests/__init__.py +0 -0
  309. django_spire/history/activity/tests/test_activity.py +176 -0
  310. django_spire/history/admin.py +9 -2
  311. django_spire/history/choices.py +3 -0
  312. django_spire/history/models.py +5 -5
  313. django_spire/history/tests/test_admin.py +93 -0
  314. django_spire/history/tests/test_history.py +101 -0
  315. django_spire/history/tests/test_mixins.py +84 -0
  316. django_spire/history/viewed/admin.py +3 -1
  317. django_spire/history/viewed/apps.py +3 -1
  318. django_spire/history/viewed/models.py +2 -0
  319. django_spire/history/viewed/tests/__init__.py +0 -0
  320. django_spire/history/viewed/tests/test_viewed.py +46 -0
  321. django_spire/knowledge/auth/tests/__init__.py +0 -0
  322. django_spire/knowledge/auth/tests/test_controller.py +116 -0
  323. django_spire/knowledge/collection/admin.py +5 -1
  324. django_spire/knowledge/collection/models.py +3 -1
  325. django_spire/knowledge/collection/seeding/seed.py +1 -0
  326. django_spire/knowledge/collection/services/factory_service.py +10 -11
  327. django_spire/knowledge/collection/services/ordering_service.py +1 -2
  328. django_spire/knowledge/collection/services/service.py +5 -10
  329. django_spire/knowledge/collection/services/tag_service.py +5 -2
  330. django_spire/knowledge/collection/tests/factories.py +28 -1
  331. django_spire/knowledge/collection/tests/test_models.py +48 -0
  332. django_spire/knowledge/collection/tests/test_querysets.py +93 -0
  333. django_spire/knowledge/collection/tests/test_services/test_factory_service.py +100 -0
  334. django_spire/knowledge/collection/tests/test_services/test_services.py +160 -0
  335. django_spire/knowledge/collection/tests/test_urls/test_form_urls.py +21 -3
  336. django_spire/knowledge/collection/tests/test_urls/test_json_urls.py +39 -1
  337. django_spire/knowledge/collection/tests/test_urls/test_page_urls.py +12 -4
  338. django_spire/knowledge/collection/urls/__init__.py +3 -0
  339. django_spire/knowledge/collection/urls/form_urls.py +2 -0
  340. django_spire/knowledge/collection/urls/json_urls.py +2 -0
  341. django_spire/knowledge/collection/urls/page_urls.py +2 -0
  342. django_spire/knowledge/collection/views/form_views.py +4 -4
  343. django_spire/knowledge/collection/views/json_views.py +5 -1
  344. django_spire/knowledge/collection/views/page_views.py +5 -2
  345. django_spire/knowledge/entry/admin.py +7 -1
  346. django_spire/knowledge/entry/forms.py +2 -0
  347. django_spire/knowledge/entry/models.py +2 -0
  348. django_spire/knowledge/entry/seeding/seed.py +3 -0
  349. django_spire/knowledge/entry/services/automation_service.py +5 -4
  350. django_spire/knowledge/entry/services/factory_service.py +7 -5
  351. django_spire/knowledge/entry/services/service.py +4 -7
  352. django_spire/knowledge/entry/services/tag_service.py +0 -1
  353. django_spire/knowledge/entry/services/tool_service.py +1 -0
  354. django_spire/knowledge/entry/services/transformation_services.py +1 -5
  355. django_spire/knowledge/entry/tests/factories.py +1 -2
  356. django_spire/knowledge/entry/tests/test_factory_service.py +20 -0
  357. django_spire/knowledge/entry/tests/test_models.py +41 -0
  358. django_spire/knowledge/entry/tests/test_querysets.py +71 -0
  359. django_spire/knowledge/entry/tests/test_services.py +94 -0
  360. django_spire/knowledge/entry/tests/test_urls/test_form_urls.py +9 -14
  361. django_spire/knowledge/entry/tests/test_urls/test_json_urls.py +48 -5
  362. django_spire/knowledge/entry/tests/test_urls/test_page_urls.py +6 -8
  363. django_spire/knowledge/entry/tests/test_urls/test_template_urls.py +40 -0
  364. django_spire/knowledge/entry/urls/form_urls.py +2 -0
  365. django_spire/knowledge/entry/urls/json_urls.py +2 -0
  366. django_spire/knowledge/entry/urls/page_urls.py +2 -0
  367. django_spire/knowledge/entry/urls/template_urls.py +2 -0
  368. django_spire/knowledge/entry/version/block/choices.py +2 -0
  369. django_spire/knowledge/entry/version/block/data/data.py +1 -0
  370. django_spire/knowledge/entry/version/block/data/list/data.py +8 -13
  371. django_spire/knowledge/entry/version/block/data/list/maps.py +3 -0
  372. django_spire/knowledge/entry/version/block/data/list/meta.py +1 -2
  373. django_spire/knowledge/entry/version/block/data/list/tests/__init__.py +0 -0
  374. django_spire/knowledge/entry/version/block/data/list/tests/test_maps.py +32 -0
  375. django_spire/knowledge/entry/version/block/data/list/tests/test_meta.py +58 -0
  376. django_spire/knowledge/entry/version/block/data/maps.py +3 -6
  377. django_spire/knowledge/entry/version/block/models.py +7 -5
  378. django_spire/knowledge/entry/version/block/seeding/constants.py +5 -4
  379. django_spire/knowledge/entry/version/block/services/service.py +2 -3
  380. django_spire/knowledge/entry/version/block/tests/factories.py +4 -10
  381. django_spire/knowledge/entry/version/block/tests/test_choices.py +56 -0
  382. django_spire/knowledge/entry/version/block/tests/test_data.py +90 -0
  383. django_spire/knowledge/entry/version/block/tests/test_maps.py +37 -0
  384. django_spire/knowledge/entry/version/block/tests/test_models.py +55 -0
  385. django_spire/knowledge/entry/version/block/tests/test_querysets.py +35 -0
  386. django_spire/knowledge/entry/version/block/tests/test_services.py +65 -0
  387. django_spire/knowledge/entry/version/choices.py +2 -0
  388. django_spire/knowledge/entry/version/converters/converter.py +1 -1
  389. django_spire/knowledge/entry/version/converters/docx_converter.py +4 -7
  390. django_spire/knowledge/entry/version/converters/markdown_converter.py +20 -20
  391. django_spire/knowledge/entry/version/maps.py +4 -5
  392. django_spire/knowledge/entry/version/querysets.py +1 -1
  393. django_spire/knowledge/entry/version/seeding/seeder.py +1 -2
  394. django_spire/knowledge/entry/version/services/processor_service.py +5 -4
  395. django_spire/knowledge/entry/version/services/service.py +1 -2
  396. django_spire/knowledge/entry/version/tests/factories.py +2 -2
  397. django_spire/knowledge/entry/version/tests/test_choices.py +18 -0
  398. django_spire/knowledge/entry/version/tests/test_converters/test_docx_converter.py +56 -8
  399. django_spire/knowledge/entry/version/tests/test_converters/test_markdown_converter.py +78 -0
  400. django_spire/knowledge/entry/version/tests/test_maps.py +58 -0
  401. django_spire/knowledge/entry/version/tests/test_models.py +23 -0
  402. django_spire/knowledge/entry/version/tests/test_querysets.py +26 -0
  403. django_spire/knowledge/entry/version/tests/test_services.py +62 -0
  404. django_spire/knowledge/entry/version/tests/test_urls/test_json_urls.py +27 -8
  405. django_spire/knowledge/entry/version/tests/test_urls/test_page_urls.py +15 -8
  406. django_spire/knowledge/entry/version/tests/test_urls/test_redirect_urls.py +38 -0
  407. django_spire/knowledge/entry/version/urls/__init__.py +3 -0
  408. django_spire/knowledge/entry/version/urls/json_urls.py +2 -1
  409. django_spire/knowledge/entry/version/urls/page_urls.py +2 -0
  410. django_spire/knowledge/entry/version/urls/redirect_urls.py +2 -0
  411. django_spire/knowledge/entry/version/views/json_views.py +5 -1
  412. django_spire/knowledge/entry/version/views/page_views.py +10 -3
  413. django_spire/knowledge/entry/version/views/redirect_views.py +5 -1
  414. django_spire/knowledge/entry/views/form_views.py +16 -8
  415. django_spire/knowledge/entry/views/json_views.py +3 -1
  416. django_spire/knowledge/entry/views/page_views.py +8 -2
  417. django_spire/knowledge/entry/views/template_views.py +7 -1
  418. django_spire/knowledge/exceptions.py +2 -1
  419. django_spire/knowledge/intelligence/intel/answer_intel.py +2 -1
  420. django_spire/knowledge/intelligence/intel/entry_intel.py +0 -1
  421. django_spire/knowledge/intelligence/workflows/knowledge_workflow.py +4 -5
  422. django_spire/knowledge/models.py +1 -2
  423. django_spire/knowledge/tests/__init__.py +0 -0
  424. django_spire/knowledge/tests/test_templatetags.py +40 -0
  425. django_spire/knowledge/tests/test_urls/__init__.py +0 -0
  426. django_spire/knowledge/tests/test_urls/test_page_urls.py +24 -0
  427. django_spire/knowledge/urls/__init__.py +2 -0
  428. django_spire/knowledge/urls/page_urls.py +2 -0
  429. django_spire/knowledge/views/page_views.py +8 -3
  430. django_spire/notification/admin.py +3 -1
  431. django_spire/notification/app/admin.py +2 -0
  432. django_spire/notification/app/apps.py +3 -1
  433. django_spire/notification/app/exceptions.py +9 -2
  434. django_spire/notification/app/models.py +8 -4
  435. django_spire/notification/app/processor.py +22 -26
  436. django_spire/notification/app/querysets.py +2 -0
  437. django_spire/notification/app/tests/__init__.py +0 -0
  438. django_spire/notification/app/tests/factories.py +34 -0
  439. django_spire/notification/app/tests/test_apps.py +24 -0
  440. django_spire/notification/app/tests/test_models.py +72 -0
  441. django_spire/notification/app/tests/test_processor.py +111 -0
  442. django_spire/notification/app/tests/test_querysets.py +90 -0
  443. django_spire/notification/app/tests/test_views/__init__.py +0 -0
  444. django_spire/notification/app/tests/test_views/test_json_views.py +48 -0
  445. django_spire/notification/app/tests/test_views/test_page_views.py +19 -0
  446. django_spire/notification/app/urls/__init__.py +3 -1
  447. django_spire/notification/app/urls/json_urls.py +6 -4
  448. django_spire/notification/app/urls/page_urls.py +4 -3
  449. django_spire/notification/app/urls/template_urls.py +4 -2
  450. django_spire/notification/apps.py +4 -1
  451. django_spire/notification/email/admin.py +5 -1
  452. django_spire/notification/email/apps.py +3 -1
  453. django_spire/notification/email/exceptions.py +4 -2
  454. django_spire/notification/email/helper.py +5 -3
  455. django_spire/notification/email/models.py +4 -0
  456. django_spire/notification/email/processor.py +19 -15
  457. django_spire/notification/email/querysets.py +3 -0
  458. django_spire/notification/email/tests/__init__.py +0 -0
  459. django_spire/notification/email/tests/factories.py +35 -0
  460. django_spire/notification/email/tests/test_apps.py +24 -0
  461. django_spire/notification/email/tests/test_models.py +52 -0
  462. django_spire/notification/email/tests/test_processor.py +92 -0
  463. django_spire/notification/email/tests/test_querysets.py +43 -0
  464. django_spire/notification/exceptions.py +17 -2
  465. django_spire/notification/managers.py +7 -1
  466. django_spire/notification/maps.py +4 -1
  467. django_spire/notification/mixins.py +2 -0
  468. django_spire/notification/models.py +3 -1
  469. django_spire/notification/processors/notification.py +12 -5
  470. django_spire/notification/processors/processor.py +2 -0
  471. django_spire/notification/processors/tests/__init__.py +0 -0
  472. django_spire/notification/processors/tests/test_notification.py +106 -0
  473. django_spire/notification/push/admin.py +10 -1
  474. django_spire/notification/push/apps.py +3 -1
  475. django_spire/notification/push/models.py +2 -3
  476. django_spire/notification/push/tests/__init__.py +0 -0
  477. django_spire/notification/push/tests/test_apps.py +24 -0
  478. django_spire/notification/push/tests/test_models.py +28 -0
  479. django_spire/notification/querysets.py +7 -1
  480. django_spire/notification/sms/admin.py +2 -0
  481. django_spire/notification/sms/apps.py +4 -1
  482. django_spire/notification/sms/automations.py +2 -0
  483. django_spire/notification/sms/choices.py +2 -0
  484. django_spire/notification/sms/exceptions.py +19 -5
  485. django_spire/notification/sms/helper.py +33 -23
  486. django_spire/notification/sms/models.py +5 -1
  487. django_spire/notification/sms/processor.py +20 -20
  488. django_spire/notification/sms/querysets.py +2 -0
  489. django_spire/notification/sms/tests/factories.py +33 -0
  490. django_spire/notification/sms/tests/test_apps.py +24 -0
  491. django_spire/notification/sms/tests/test_automation.py +38 -0
  492. django_spire/notification/sms/tests/test_choices.py +15 -0
  493. django_spire/notification/sms/tests/test_consts.py +17 -0
  494. django_spire/notification/sms/tests/test_exceptions.py +27 -0
  495. django_spire/notification/sms/tests/test_helper.py +50 -0
  496. django_spire/notification/sms/tests/test_models.py +81 -0
  497. django_spire/notification/sms/tests/test_processor.py +107 -0
  498. django_spire/notification/sms/tests/test_tools.py +25 -11
  499. django_spire/notification/sms/tools.py +16 -5
  500. django_spire/notification/sms/urls/__init__.py +3 -1
  501. django_spire/notification/sms/urls/media_urls.py +2 -0
  502. django_spire/notification/sms/views/media_views.py +14 -4
  503. django_spire/notification/tests/__init__.py +0 -0
  504. django_spire/notification/tests/factories.py +26 -0
  505. django_spire/notification/tests/test_admin.py +55 -0
  506. django_spire/notification/tests/test_apps.py +30 -0
  507. django_spire/notification/tests/test_automation.py +18 -0
  508. django_spire/notification/tests/test_choices.py +59 -0
  509. django_spire/notification/tests/test_exceptions.py +58 -0
  510. django_spire/notification/tests/test_managers.py +100 -0
  511. django_spire/notification/tests/test_maps.py +31 -0
  512. django_spire/notification/tests/test_models.py +76 -0
  513. django_spire/notification/tests/test_querysets.py +184 -0
  514. django_spire/notification/tests/test_utils.py +23 -0
  515. django_spire/notification/urls.py +3 -1
  516. django_spire/notification/utils.py +3 -1
  517. django_spire/settings.py +3 -0
  518. django_spire/theme/tests/test_context_processor.py +15 -13
  519. django_spire/theme/tests/test_enums.py +2 -2
  520. django_spire/theme/tests/test_filesystem.py +2 -5
  521. django_spire/theme/tests/test_integration.py +12 -12
  522. django_spire/theme/tests/test_model.py +40 -38
  523. django_spire/theme/tests/test_views/test_json_views.py +33 -33
  524. django_spire/theme/urls/json_urls.py +3 -0
  525. django_spire/theme/urls/page_urls.py +3 -0
  526. django_spire/urls.py +19 -15
  527. django_spire/utils.py +13 -4
  528. {django_spire-0.23.6.dist-info → django_spire-0.23.8.dist-info}/METADATA +1 -1
  529. {django_spire-0.23.6.dist-info → django_spire-0.23.8.dist-info}/RECORD +532 -361
  530. django_spire/contrib/options/tests/test_unit.py +0 -148
  531. django_spire/contrib/progress/views.py +0 -64
  532. django_spire/contrib/seeding/tests/test_seeding.py +0 -25
  533. django_spire/core/tests/test_templatetags.py +0 -117
  534. django_spire/core/tests/tests_shortcuts.py +0 -73
  535. django_spire/history/activity/tests.py +0 -3
  536. django_spire/history/activity/views.py +0 -3
  537. django_spire/knowledge/collection/tests/test_services/test_transformation_service.py +0 -71
  538. django_spire/notification/app/tests.py +0 -3
  539. {django_spire-0.23.6.dist-info → django_spire-0.23.8.dist-info}/WHEEL +0 -0
  540. {django_spire-0.23.6.dist-info → django_spire-0.23.8.dist-info}/licenses/LICENSE.md +0 -0
  541. {django_spire-0.23.6.dist-info → django_spire-0.23.8.dist-info}/top_level.txt +0 -0
@@ -1,29 +1,35 @@
1
+ from __future__ import annotations
2
+
1
3
  from django.utils.timezone import now
2
4
  from django.conf import settings
3
5
 
4
6
  from twilio.rest import Client
5
7
 
6
- from django_spire.notification.choices import NotificationTypeChoices, \
8
+ from django_spire.notification.choices import (
9
+ NotificationTypeChoices,
7
10
  NotificationStatusChoices
8
- from django_spire.notification.exceptions import NotificationException
11
+ )
12
+ from django_spire.notification.exceptions import InvalidNotificationTypeError
9
13
  from django_spire.notification.models import Notification
10
14
  from django_spire.notification.processors.processor import BaseNotificationProcessor
11
- from django_spire.notification.sms.exceptions import TwilioException, \
12
- TwilioAPIConcurrentException
15
+ from django_spire.notification.sms.exceptions import (
16
+ TwilioAPIConcurrentError,
17
+ TwilioError
18
+ )
13
19
  from django_spire.notification.sms.helper import TwilioSMSHelper, BulkTwilioSMSHelper
14
20
 
15
21
 
16
22
  class SMSNotificationProcessor(BaseNotificationProcessor):
23
+ def _validate_notification_type(self, notification: Notification):
24
+ if notification.type != NotificationTypeChoices.SMS:
25
+ raise InvalidNotificationTypeError(NotificationTypeChoices.SMS, notification.type)
26
+
17
27
  def process(self, notification: Notification):
18
28
  notification.status = NotificationStatusChoices.PROCESSING
19
29
  notification.save()
20
30
 
21
31
  try:
22
- if notification.type != NotificationTypeChoices.SMS:
23
- raise NotificationException(
24
- f'SMSNotificationProcessor only processes '
25
- f'SMS notifications. Was provided {notification.type}'
26
- )
32
+ self._validate_notification_type(notification)
27
33
 
28
34
  twilio_sms_client = Client(settings.TWILIO_ACCOUNT_SID, settings.TWILIO_AUTH_TOKEN)
29
35
  TwilioSMSHelper(notification, twilio_sms_client).send()
@@ -31,35 +37,29 @@ class SMSNotificationProcessor(BaseNotificationProcessor):
31
37
  notification.status = NotificationStatusChoices.SENT
32
38
  notification.sent_datetime = now()
33
39
  except Exception as e:
34
- # Requeue notification as api concurrency is full
35
- if isinstance(e, TwilioAPIConcurrentException):
40
+ if isinstance(e, TwilioAPIConcurrentError):
36
41
  notification.status = NotificationStatusChoices.PENDING
37
42
  notification.save()
38
43
  return
39
44
 
40
45
  notification.status_message = str(e)
41
46
 
42
- if isinstance(e, TwilioException):
47
+ if isinstance(e, TwilioError):
43
48
  notification.status = NotificationStatusChoices.ERRORED
44
49
  else:
45
50
  notification.status = NotificationStatusChoices.FAILED
46
- raise e
51
+ raise
47
52
  finally:
48
53
  notification.save()
49
54
 
50
55
  def process_list(self, notifications: list):
51
56
  twilio_sms_client = Client(settings.TWILIO_ACCOUNT_SID, settings.TWILIO_AUTH_TOKEN)
57
+
52
58
  for notification in notifications:
53
- if notification.type != NotificationTypeChoices.SMS:
54
- raise NotificationException(
55
- f'SMSNotificationProcessor only processes '
56
- f'SMS notifications. Was provided {notification.type}'
57
- )
59
+ self._validate_notification_type(notification)
58
60
 
59
61
  try:
60
62
  BulkTwilioSMSHelper(notifications, twilio_sms_client).send_notifications()
61
- except Exception as e:
62
- raise e
63
63
  finally:
64
64
  Notification.objects.bulk_update(
65
65
  notifications,
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from django_spire.history.querysets import HistoryQuerySet
2
4
  from django_spire.notification.querysets import NotificationContentObjectQuerySet
3
5
 
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ from django.contrib.auth.models import User
4
+
5
+ from django_spire.notification.choices import (
6
+ NotificationStatusChoices,
7
+ NotificationTypeChoices,
8
+ )
9
+ from django_spire.notification.models import Notification
10
+ from django_spire.notification.sms.models import SmsNotification
11
+
12
+
13
+ def create_test_sms_notification(**kwargs) -> SmsNotification:
14
+ if 'notification' not in kwargs:
15
+ user = kwargs.pop('user', None) or User.objects.first()
16
+ notification = Notification.objects.create(
17
+ user=user,
18
+ type=NotificationTypeChoices.SMS,
19
+ title=kwargs.pop('title', 'Test SMS Notification'),
20
+ body=kwargs.pop('body', 'This is a test SMS notification.'),
21
+ url=kwargs.pop('url', ''),
22
+ status=kwargs.pop('status', NotificationStatusChoices.PENDING),
23
+ priority=kwargs.pop('priority', 'low'),
24
+ )
25
+ kwargs['notification'] = notification
26
+
27
+ data = {
28
+ 'to_phone_number': '5551234567',
29
+ 'media_url': None,
30
+ 'temporary_media': None,
31
+ }
32
+ data.update(kwargs)
33
+ return SmsNotification.objects.create(**data)
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from django.apps import apps
4
+
5
+ from django_spire.core.tests.test_cases import BaseTestCase
6
+ from django_spire.notification.sms.apps import NotificationSmsConfig
7
+
8
+
9
+ class NotificationSmsConfigTests(BaseTestCase):
10
+ def test_app_name(self):
11
+ assert NotificationSmsConfig.name == 'django_spire.notification.sms'
12
+
13
+ def test_app_label(self):
14
+ assert NotificationSmsConfig.label == 'django_spire_notification_sms'
15
+
16
+ def test_default_auto_field(self):
17
+ assert NotificationSmsConfig.default_auto_field == 'django.db.models.BigAutoField'
18
+
19
+ def test_required_apps(self):
20
+ expected = ('django_spire_core', 'django_spire_notification')
21
+ assert expected == NotificationSmsConfig.REQUIRED_APPS
22
+
23
+ def test_app_is_installed(self):
24
+ assert apps.is_installed('django_spire.notification.sms')
@@ -0,0 +1,38 @@
1
+ from __future__ import annotations
2
+
3
+ import uuid
4
+
5
+ from django.utils.timezone import now, timedelta
6
+
7
+ from django_spire.core.tests.test_cases import BaseTestCase
8
+ from django_spire.notification.sms.choices import SmsMediaContentTypeChoices
9
+ from django_spire.notification.sms.models import SmsTemporaryMedia
10
+
11
+
12
+ class SmsTemporaryMediaTests(BaseTestCase):
13
+ def setUp(self):
14
+ super().setUp()
15
+ self.expired_media = SmsTemporaryMedia.objects.create(
16
+ content='base64content',
17
+ content_type=SmsMediaContentTypeChoices.PNG,
18
+ name='expired.png',
19
+ expire_datetime=now() - timedelta(hours=1),
20
+ external_access_key=uuid.uuid4(),
21
+ )
22
+ self.valid_media = SmsTemporaryMedia.objects.create(
23
+ content='base64content',
24
+ content_type=SmsMediaContentTypeChoices.PNG,
25
+ name='valid.png',
26
+ expire_datetime=now() + timedelta(hours=1),
27
+ external_access_key=uuid.uuid4(),
28
+ )
29
+
30
+ def test_is_expired_true(self):
31
+ assert self.expired_media.is_expired() is True
32
+
33
+ def test_is_expired_false(self):
34
+ assert self.valid_media.is_expired() is False
35
+
36
+ def test_external_url_contains_access_key(self):
37
+ url = self.valid_media.external_url
38
+ assert str(self.valid_media.external_access_key) in url
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from django_spire.core.tests.test_cases import BaseTestCase
4
+ from django_spire.notification.sms.choices import SmsMediaContentTypeChoices
5
+
6
+
7
+ class SmsMediaContentTypeChoicesTests(BaseTestCase):
8
+ def test_png_value(self):
9
+ assert SmsMediaContentTypeChoices.PNG == 'image/png'
10
+
11
+ def test_jpeg_value(self):
12
+ assert SmsMediaContentTypeChoices.JPEG == 'image/jpeg'
13
+
14
+ def test_choices_count(self):
15
+ assert len(SmsMediaContentTypeChoices.choices) == 2
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ from django_spire.core.tests.test_cases import BaseTestCase
4
+ from django_spire.notification.sms.consts import (
5
+ TWILIO_SMS_BATCH_SIZE_NAME,
6
+ TWILIO_UNSUCCESSFUL_STATUSES,
7
+ )
8
+
9
+
10
+ class SmsConstsTests(BaseTestCase):
11
+ def test_twilio_unsuccessful_statuses(self):
12
+ assert 'failed' in TWILIO_UNSUCCESSFUL_STATUSES
13
+ assert 'undelivered' in TWILIO_UNSUCCESSFUL_STATUSES
14
+ assert len(TWILIO_UNSUCCESSFUL_STATUSES) == 2
15
+
16
+ def test_twilio_sms_batch_size_name(self):
17
+ assert TWILIO_SMS_BATCH_SIZE_NAME == 'TWILIO_SMS_BATCH_SIZE'
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ from django_spire.core.tests.test_cases import BaseTestCase
4
+ from django_spire.notification.sms.exceptions import (
5
+ SmsNotificationError,
6
+ SmsTemporaryMediaError,
7
+ TwilioAPIConcurrentError,
8
+ TwilioError,
9
+ )
10
+
11
+
12
+ class SmsExceptionsTests(BaseTestCase):
13
+ def test_sms_notification_error_message(self):
14
+ error = SmsNotificationError('SMS error')
15
+ assert str(error) == 'SMS error'
16
+
17
+ def test_sms_temporary_media_error_message(self):
18
+ error = SmsTemporaryMediaError('Media error')
19
+ assert str(error) == 'Media error'
20
+
21
+ def test_twilio_error_message(self):
22
+ error = TwilioError('Twilio error')
23
+ assert str(error) == 'Twilio error'
24
+
25
+ def test_twilio_api_concurrent_error_message(self):
26
+ error = TwilioAPIConcurrentError('Concurrent error')
27
+ assert str(error) == 'Concurrent error'
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ from unittest.mock import MagicMock
4
+
5
+ import pytest
6
+
7
+ from django_spire.auth.user.tests.factories import create_user
8
+ from django_spire.core.tests.test_cases import BaseTestCase
9
+ from django_spire.notification.sms.exceptions import TwilioError
10
+ from django_spire.notification.sms.helper import TwilioSMSHelper
11
+ from django_spire.notification.sms.tests.factories import create_test_sms_notification
12
+
13
+
14
+ class TwilioSMSHelperTests(BaseTestCase):
15
+ def setUp(self):
16
+ super().setUp()
17
+ self.user = create_user(username='test_helper_user')
18
+ self.sms_notification = create_test_sms_notification(user=self.user)
19
+ self.mock_client = MagicMock()
20
+
21
+ def test_format_phone_number_10_digits(self):
22
+ result = TwilioSMSHelper._format_phone_number('5551234567')
23
+ assert result == '+15551234567'
24
+
25
+ def test_format_phone_number_11_digits_with_country_code(self):
26
+ result = TwilioSMSHelper._format_phone_number('15551234567')
27
+ assert result == '+15551234567'
28
+
29
+ def test_format_phone_number_invalid_raises_error(self):
30
+ with pytest.raises(TwilioError):
31
+ TwilioSMSHelper._format_phone_number('123')
32
+
33
+ def test_format_phone_number_too_long_raises_error(self):
34
+ with pytest.raises(TwilioError):
35
+ TwilioSMSHelper._format_phone_number('123456789012345')
36
+
37
+ def test_message_format(self):
38
+ helper = TwilioSMSHelper(
39
+ self.sms_notification.notification,
40
+ self.mock_client
41
+ )
42
+ expected = f'{self.sms_notification.notification.title}: {self.sms_notification.notification.body}'
43
+ assert helper.message == expected
44
+
45
+ def test_to_phone_number_formatted(self):
46
+ helper = TwilioSMSHelper(
47
+ self.sms_notification.notification,
48
+ self.mock_client
49
+ )
50
+ assert helper.to_phone_number.startswith('+1')
@@ -0,0 +1,81 @@
1
+ from __future__ import annotations
2
+
3
+ import uuid
4
+
5
+ from django.utils.timezone import now, timedelta
6
+
7
+ from django_spire.core.tests.test_cases import BaseTestCase
8
+ from django_spire.notification.models import Notification
9
+ from django_spire.notification.sms.choices import SmsMediaContentTypeChoices
10
+ from django_spire.notification.sms.models import SmsTemporaryMedia
11
+ from django_spire.notification.sms.tests.factories import create_test_sms_notification
12
+
13
+
14
+ class SmsNotificationModelTests(BaseTestCase):
15
+ def setUp(self):
16
+ super().setUp()
17
+ self.sms_notification = create_test_sms_notification()
18
+
19
+ def test_str(self):
20
+ expected = f'{self.sms_notification.to_phone_number} - {self.sms_notification.notification.title}'
21
+ assert str(self.sms_notification) == expected
22
+
23
+ def test_notification_relationship(self):
24
+ assert self.sms_notification.notification is not None
25
+ assert isinstance(self.sms_notification.notification, Notification)
26
+
27
+ def test_to_phone_number(self):
28
+ assert self.sms_notification.to_phone_number == '5551234567'
29
+
30
+ def test_media_url_default_none(self):
31
+ assert self.sms_notification.media_url is None
32
+
33
+ def test_temporary_media_default_none(self):
34
+ assert self.sms_notification.temporary_media is None
35
+
36
+ def test_with_media_url(self):
37
+ sms_notification = create_test_sms_notification(
38
+ media_url='https://example.com/image.png'
39
+ )
40
+ assert sms_notification.media_url == 'https://example.com/image.png'
41
+
42
+
43
+ class SmsTemporaryMediaModelTests(BaseTestCase):
44
+ def setUp(self):
45
+ super().setUp()
46
+ self.temporary_media = SmsTemporaryMedia.objects.create(
47
+ content='base64encodedcontent',
48
+ content_type=SmsMediaContentTypeChoices.PNG,
49
+ name='test_image.png',
50
+ expire_datetime=now() + timedelta(hours=1),
51
+ external_access_key=uuid.uuid4(),
52
+ )
53
+
54
+ def test_str(self):
55
+ expected = f'{self.temporary_media.name} - {self.temporary_media.content_type}'
56
+ assert str(self.temporary_media) == expected
57
+
58
+ def test_is_expired_false(self):
59
+ assert self.temporary_media.is_expired() is False
60
+
61
+ def test_is_expired_true(self):
62
+ self.temporary_media.expire_datetime = now() - timedelta(hours=1)
63
+ self.temporary_media.save()
64
+ assert self.temporary_media.is_expired() is True
65
+
66
+ def test_external_url(self):
67
+ url = self.temporary_media.external_url
68
+ assert str(self.temporary_media.external_access_key) in url
69
+
70
+ def test_content_type_png(self):
71
+ assert self.temporary_media.content_type == SmsMediaContentTypeChoices.PNG
72
+
73
+ def test_content_type_jpeg(self):
74
+ media = SmsTemporaryMedia.objects.create(
75
+ content='base64encodedcontent',
76
+ content_type=SmsMediaContentTypeChoices.JPEG,
77
+ name='test_image.jpg',
78
+ expire_datetime=now() + timedelta(hours=1),
79
+ external_access_key=uuid.uuid4(),
80
+ )
81
+ assert media.content_type == SmsMediaContentTypeChoices.JPEG
@@ -0,0 +1,107 @@
1
+ from __future__ import annotations
2
+
3
+ from unittest.mock import MagicMock, patch
4
+
5
+ import pytest
6
+
7
+ from django_spire.auth.user.tests.factories import create_user
8
+ from django_spire.core.tests.test_cases import BaseTestCase
9
+ from django_spire.notification.choices import (
10
+ NotificationStatusChoices,
11
+ NotificationTypeChoices,
12
+ )
13
+ from django_spire.notification.exceptions import NotificationError
14
+ from django_spire.notification.models import Notification
15
+ from django_spire.notification.sms.processor import SMSNotificationProcessor
16
+ from django_spire.notification.sms.tests.factories import create_test_sms_notification
17
+
18
+
19
+ class SMSNotificationProcessorTests(BaseTestCase):
20
+ def setUp(self):
21
+ super().setUp()
22
+ self.user = create_user(username='test_sms_processor_user')
23
+ self.processor = SMSNotificationProcessor()
24
+
25
+ @patch('django_spire.notification.sms.processor.Client')
26
+ @patch('django_spire.notification.sms.processor.TwilioSMSHelper')
27
+ def test_process_sets_status_to_sent(
28
+ self,
29
+ mock_helper_class: MagicMock,
30
+ mock_client_class: MagicMock
31
+ ):
32
+ mock_helper = MagicMock()
33
+ mock_helper_class.return_value = mock_helper
34
+
35
+ sms_notification = create_test_sms_notification(user=self.user)
36
+ notification = sms_notification.notification
37
+
38
+ self.processor.process(notification)
39
+
40
+ notification.refresh_from_db()
41
+ assert notification.status == NotificationStatusChoices.SENT
42
+
43
+ @patch('django_spire.notification.sms.processor.Client')
44
+ @patch('django_spire.notification.sms.processor.TwilioSMSHelper')
45
+ def test_process_sets_sent_datetime(
46
+ self,
47
+ mock_helper_class: MagicMock,
48
+ mock_client_class: MagicMock
49
+ ):
50
+ mock_helper = MagicMock()
51
+ mock_helper_class.return_value = mock_helper
52
+
53
+ sms_notification = create_test_sms_notification(user=self.user)
54
+ notification = sms_notification.notification
55
+
56
+ self.processor.process(notification)
57
+
58
+ notification.refresh_from_db()
59
+ assert notification.sent_datetime is not None
60
+
61
+ @patch('django_spire.notification.sms.processor.Client')
62
+ @patch('django_spire.notification.sms.processor.TwilioSMSHelper')
63
+ def test_process_calls_send(
64
+ self,
65
+ mock_helper_class: MagicMock,
66
+ mock_client_class: MagicMock
67
+ ):
68
+ mock_helper = MagicMock()
69
+ mock_helper_class.return_value = mock_helper
70
+
71
+ sms_notification = create_test_sms_notification(user=self.user)
72
+ notification = sms_notification.notification
73
+
74
+ self.processor.process(notification)
75
+
76
+ mock_helper.send.assert_called_once()
77
+
78
+ def test_process_raises_error_for_wrong_type(self):
79
+ notification = Notification.objects.create(
80
+ user=self.user,
81
+ type=NotificationTypeChoices.APP,
82
+ title='Test',
83
+ body='Test',
84
+ status=NotificationStatusChoices.PENDING,
85
+ )
86
+
87
+ with pytest.raises(NotificationError):
88
+ self.processor.process(notification)
89
+
90
+ @patch('django_spire.notification.sms.processor.Client')
91
+ @patch('django_spire.notification.sms.processor.BulkTwilioSMSHelper')
92
+ def test_process_list(
93
+ self,
94
+ mock_helper_class: MagicMock,
95
+ mock_client_class: MagicMock
96
+ ):
97
+ mock_helper = MagicMock()
98
+ mock_helper_class.return_value = mock_helper
99
+
100
+ notifications = [
101
+ create_test_sms_notification(user=self.user).notification
102
+ for _ in range(3)
103
+ ]
104
+
105
+ self.processor.process_list(notifications)
106
+
107
+ mock_helper.send_notifications.assert_called_once()
@@ -1,9 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ import pytest
4
+
1
5
  from django_spire.core.tests.test_cases import BaseTestCase
2
6
  from django_spire.notification.sms.tools import format_to_international_phone_number
3
7
 
4
8
 
5
9
  class TestSMSTools(BaseTestCase):
6
-
7
10
  def test_local_to_international_phone_number(self):
8
11
  phone_numbers = (
9
12
  '622 415 2736',
@@ -19,6 +22,7 @@ class TestSMSTools(BaseTestCase):
19
22
  '368.447-9514',
20
23
  '4563219876'
21
24
  )
25
+
22
26
  expected_phone_numbers = (
23
27
  '+16224152736',
24
28
  '+18815534599',
@@ -33,8 +37,12 @@ class TestSMSTools(BaseTestCase):
33
37
  '+13684479514',
34
38
  '+14563219876'
35
39
  )
36
- formatted_phone_numbers = [format_to_international_phone_number(phone_number) for phone_number in phone_numbers]
37
- self.assertSequenceEqual(formatted_phone_numbers, expected_phone_numbers)
40
+
41
+ formatted_phone_numbers = tuple(
42
+ format_to_international_phone_number(phone_number)
43
+ for phone_number in phone_numbers
44
+ )
45
+ assert formatted_phone_numbers == expected_phone_numbers
38
46
 
39
47
  def test_invalid_phone_number(self):
40
48
  phone_numbers = (
@@ -43,13 +51,19 @@ class TestSMSTools(BaseTestCase):
43
51
  '1',
44
52
  '12345678901234567890',
45
53
  '',
46
- '32145698765', # excess 1 digit
47
- '403000123', # missing 1 digit
48
- '36800012', # missing 2 digits
54
+ '32145698765',
55
+ '403000123',
56
+ '36800012',
49
57
  )
58
+
50
59
  for phone_number in phone_numbers:
51
- self.assertRaises(
52
- ValueError,
53
- format_to_international_phone_number,
54
- phone_number
55
- )
60
+ with pytest.raises(ValueError):
61
+ format_to_international_phone_number(phone_number)
62
+
63
+ def test_format_with_country_code(self):
64
+ result = format_to_international_phone_number('5551234567', country_code='1')
65
+ assert result == '+15551234567'
66
+
67
+ def test_format_already_has_country_code(self):
68
+ result = format_to_international_phone_number('15551234567', country_code='1')
69
+ assert result == '+15551234567'
@@ -1,7 +1,13 @@
1
+ from __future__ import annotations
2
+
1
3
  import re
2
4
 
5
+ from typing import TYPE_CHECKING
6
+
3
7
  from django_spire.notification.choices import NotificationStatusChoices
4
- from django_spire.notification.sms.models import SmsTemporaryMedia
8
+
9
+ if TYPE_CHECKING:
10
+ from django_spire.notification.sms.models import SmsTemporaryMedia
5
11
 
6
12
 
7
13
  def update_unsent_notification_status_for_deleted_temporary_media(
@@ -19,21 +25,26 @@ def format_to_international_phone_number(phone_number: str, country_code: str='1
19
25
  Args: phone_number:
20
26
  Returns: international phone number format
21
27
  """
28
+
22
29
  if not phone_number:
23
- raise ValueError(f'No phone number provided: {phone_number}')
30
+ message = f'No phone number provided: {phone_number}'
31
+ raise ValueError(message)
24
32
 
25
33
  # Remove extension numbers
26
34
  main_number = re.split(r'(?:ext\.?|x)\s*\d+', phone_number, flags=re.IGNORECASE)[0]
27
35
 
28
36
  # Get all digit characters
29
37
  digit_number = re.sub(r'\D', '', main_number)
38
+
30
39
  if digit_number.startswith(country_code) and len(digit_number) == 10 + len(country_code):
31
40
  digit_number = digit_number[len(country_code):]
32
41
 
33
42
  # Check if the number is in local format or already in international format
34
43
  if len(digit_number) == 10:
35
44
  return f'+{country_code}{digit_number}'
36
- elif len(digit_number) == 10 + len(country_code) and digit_number.startswith(country_code):
45
+
46
+ if len(digit_number) == 10 + len(country_code) and digit_number.startswith(country_code):
37
47
  return f'+{digit_number}'
38
- else:
39
- raise ValueError(f'Invalid phone number: {phone_number}')
48
+
49
+ message = f'Invalid phone number: {phone_number}'
50
+ raise ValueError(message)
@@ -1,7 +1,9 @@
1
+ from __future__ import annotations
2
+
1
3
  from django.urls import include, path
2
4
 
3
- app_name = 'sms'
4
5
 
6
+ app_name = 'sms'
5
7
 
6
8
  urlpatterns = [
7
9
  path('media/', include('django_spire.notification.sms.urls.media_urls', namespace='media')),
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from django.urls import path
2
4
 
3
5
  from django_spire.notification.sms.views import media_views
@@ -1,29 +1,39 @@
1
+ from __future__ import annotations
2
+
1
3
  import base64
2
- import uuid
4
+
3
5
  from io import BytesIO
6
+ from typing import TYPE_CHECKING
4
7
 
5
8
  from PIL import Image
6
9
 
7
10
  from django.http import HttpResponse
8
11
  from django.views.decorators.csrf import csrf_exempt
9
12
 
10
- from django_spire.notification.sms.exceptions import SmsTemporaryMediaException
13
+ from django_spire.notification.sms.exceptions import SmsTemporaryMediaError
11
14
  from django_spire.notification.sms.models import SmsTemporaryMedia
12
15
 
16
+ if TYPE_CHECKING:
17
+ import uuid
18
+
19
+ from django.core.handlers.wsgi import WSGIRequest
20
+
13
21
 
14
22
  @csrf_exempt
15
- def external_temporary_media_view(request, external_access_key: uuid.UUID) -> HttpResponse:
23
+ def external_temporary_media_view(request: WSGIRequest, external_access_key: uuid.UUID) -> HttpResponse:
16
24
  try:
17
25
  temporary_media = SmsTemporaryMedia.objects.get(external_access_key=external_access_key)
18
26
  except SmsTemporaryMedia.DoesNotExist:
19
27
  temporary_media = None
20
28
 
21
29
  if temporary_media is None or temporary_media.content == '':
22
- raise SmsTemporaryMediaException("Content for Temporary Media cannot be empty")
30
+ message = 'Content for Temporary Media cannot be empty'
31
+ raise SmsTemporaryMediaError(message)
23
32
 
24
33
  image = Image.open(
25
34
  BytesIO(base64.b64decode(temporary_media.content))
26
35
  )
36
+
27
37
  image = image.convert('P', palette=Image.ADAPTIVE, colors=32)
28
38
 
29
39
  buffer = BytesIO()
File without changes