django-spire 0.23.7__py3-none-any.whl → 0.23.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (542) hide show
  1. django_spire/ai/admin.py +11 -11
  2. django_spire/ai/chat/apps.py +1 -0
  3. django_spire/ai/chat/templates/django_spire/ai/chat/widget/dialog_widget.html +1 -1
  4. django_spire/ai/chat/tests/factories.py +15 -0
  5. django_spire/ai/chat/tests/test_controller.py +45 -0
  6. django_spire/ai/chat/tests/test_models.py +301 -0
  7. django_spire/ai/chat/tests/test_prompts.py +48 -0
  8. django_spire/ai/chat/tests/test_responses.py +208 -0
  9. django_spire/ai/chat/tests/test_router/test_base_chat_router.py +66 -6
  10. django_spire/ai/chat/tests/test_router/test_chat_workflow.py +73 -3
  11. django_spire/ai/chat/tests/test_router/test_integration.py +86 -6
  12. django_spire/ai/chat/tests/test_router/test_intent_decoder.py +93 -1
  13. django_spire/ai/chat/tests/test_router/test_message_intel.py +60 -1
  14. django_spire/ai/chat/tests/test_router/test_spire_chat_router.py +110 -0
  15. django_spire/ai/chat/tests/test_urls/test_json_urls.py +202 -1
  16. django_spire/ai/context/tests/__init__.py +0 -0
  17. django_spire/ai/context/tests/test_context.py +188 -0
  18. django_spire/ai/decorators.py +7 -6
  19. django_spire/ai/prompt/tests/test_bots.py +100 -10
  20. django_spire/ai/prompt/tests/test_prompt_intel.py +83 -0
  21. django_spire/ai/prompt/tests/test_prompt_tuning.py +126 -0
  22. django_spire/ai/sms/decorators.py +8 -2
  23. django_spire/ai/sms/tests/test_sms.py +240 -16
  24. django_spire/ai/sms/tests/test_sms_intel.py +42 -0
  25. django_spire/ai/sms/tests/test_webhook.py +155 -7
  26. django_spire/ai/sms/views.py +23 -24
  27. django_spire/ai/tests/test_ai.py +131 -7
  28. django_spire/auth/apps.py +4 -2
  29. django_spire/auth/controller/controller.py +36 -23
  30. django_spire/auth/controller/exceptions.py +9 -0
  31. django_spire/auth/group/admin.py +1 -0
  32. django_spire/auth/group/apps.py +2 -0
  33. django_spire/auth/group/factories.py +17 -8
  34. django_spire/auth/group/forms.py +7 -0
  35. django_spire/auth/group/tests/test_factories.py +146 -0
  36. django_spire/auth/group/tests/test_forms.py +282 -0
  37. django_spire/auth/group/tests/test_models.py +192 -0
  38. django_spire/auth/group/tests/test_querysets.py +98 -0
  39. django_spire/auth/group/tests/test_utils.py +341 -0
  40. django_spire/auth/group/tests/test_views.py +377 -0
  41. django_spire/auth/group/urls/__init__.py +3 -1
  42. django_spire/auth/group/urls/form_urls.py +2 -0
  43. django_spire/auth/group/urls/json_urls.py +3 -0
  44. django_spire/auth/group/urls/page_urls.py +2 -0
  45. django_spire/auth/group/utils.py +6 -2
  46. django_spire/auth/group/views/form_views.py +6 -3
  47. django_spire/auth/group/views/json_views.py +6 -2
  48. django_spire/auth/mfa/admin.py +2 -0
  49. django_spire/auth/mfa/apps.py +2 -0
  50. django_spire/auth/mfa/forms.py +1 -0
  51. django_spire/auth/mfa/querysets.py +9 -2
  52. django_spire/auth/mfa/tests/test_models.py +233 -0
  53. django_spire/auth/mfa/tests/test_utils.py +106 -0
  54. django_spire/auth/mfa/urls/__init__.py +2 -0
  55. django_spire/auth/mfa/urls/page_urls.py +2 -0
  56. django_spire/auth/mfa/urls/redirect_urls.py +2 -0
  57. django_spire/auth/mfa/views/page_views.py +2 -1
  58. django_spire/auth/permissions/consts.py +2 -2
  59. django_spire/auth/permissions/decorators.py +8 -8
  60. django_spire/auth/permissions/permissions.py +28 -35
  61. django_spire/auth/permissions/tests/test_decorators.py +333 -0
  62. django_spire/auth/permissions/tests/test_permissions.py +337 -0
  63. django_spire/auth/permissions/tests/test_tools.py +305 -0
  64. django_spire/auth/permissions/tools.py +21 -15
  65. django_spire/auth/seeding/seed.py +3 -0
  66. django_spire/auth/seeding/seeder.py +2 -0
  67. django_spire/auth/tests/test_controller.py +323 -0
  68. django_spire/auth/tests/test_url_endpoints.py +9 -9
  69. django_spire/auth/tests/test_views.py +406 -0
  70. django_spire/auth/urls/admin_urls.py +2 -0
  71. django_spire/auth/urls/redirect_urls.py +2 -0
  72. django_spire/auth/user/apps.py +2 -0
  73. django_spire/auth/user/forms.py +9 -0
  74. django_spire/auth/user/models.py +1 -1
  75. django_spire/auth/user/services/services.py +1 -0
  76. django_spire/auth/user/tests/factories.py +14 -13
  77. django_spire/auth/user/tests/test_factories.py +166 -2
  78. django_spire/auth/user/tests/test_forms.py +573 -0
  79. django_spire/auth/user/tests/test_models.py +257 -0
  80. django_spire/auth/user/tests/test_services.py +200 -0
  81. django_spire/auth/user/tests/test_tools.py +153 -0
  82. django_spire/auth/user/tests/test_user_factories.py +139 -0
  83. django_spire/auth/user/tests/test_views.py +363 -0
  84. django_spire/auth/user/tools.py +7 -1
  85. django_spire/auth/user/urls/form_urls.py +3 -0
  86. django_spire/auth/user/urls/page_urls.py +3 -0
  87. django_spire/auth/user/views/form_views.py +19 -10
  88. django_spire/auth/user/views/page_views.py +8 -2
  89. django_spire/auth/views/redirect_views.py +14 -9
  90. django_spire/comment/admin.py +2 -0
  91. django_spire/comment/apps.py +2 -0
  92. django_spire/comment/templatetags/comment_tags.py +1 -0
  93. django_spire/comment/tests/test_forms.py +27 -0
  94. django_spire/comment/tests/test_models.py +215 -0
  95. django_spire/comment/tests/test_querysets.py +101 -0
  96. django_spire/comment/tests/test_utils.py +90 -0
  97. django_spire/comment/urls.py +2 -0
  98. django_spire/comment/utils.py +22 -13
  99. django_spire/comment/views.py +1 -1
  100. django_spire/conf.py +8 -6
  101. django_spire/consts.py +1 -1
  102. django_spire/contrib/breadcrumb/apps.py +2 -0
  103. django_spire/contrib/breadcrumb/breadcrumbs.py +18 -18
  104. django_spire/contrib/breadcrumb/tests/test_breadcrumbs.py +198 -0
  105. django_spire/contrib/constructor/__init__.py +3 -3
  106. django_spire/contrib/constructor/constructor.py +15 -15
  107. django_spire/contrib/constructor/django_model_constructor.py +5 -4
  108. django_spire/contrib/constructor/exceptions.py +5 -3
  109. django_spire/contrib/constructor/tests/__init__.py +0 -0
  110. django_spire/contrib/constructor/tests/test_constructor.py +193 -0
  111. django_spire/contrib/form/tests/__init__.py +0 -0
  112. django_spire/contrib/form/tests/test_forms.py +203 -0
  113. django_spire/contrib/generic_views/modal_views.py +2 -1
  114. django_spire/contrib/generic_views/portal_views.py +20 -19
  115. django_spire/contrib/generic_views/tests/__init__.py +0 -0
  116. django_spire/contrib/generic_views/tests/test_views.py +459 -0
  117. django_spire/contrib/help/apps.py +2 -0
  118. django_spire/contrib/help/templatetags/help.py +1 -0
  119. django_spire/contrib/help/tests/__init__.py +0 -0
  120. django_spire/contrib/help/tests/test_templatetags.py +100 -0
  121. django_spire/contrib/options/mixins.py +6 -5
  122. django_spire/contrib/options/tests/factories.py +5 -1
  123. django_spire/contrib/options/tests/test_options.py +234 -0
  124. django_spire/contrib/ordering/exceptions.py +7 -3
  125. django_spire/contrib/ordering/mixins.py +2 -0
  126. django_spire/contrib/ordering/querysets.py +3 -1
  127. django_spire/contrib/ordering/services/processor_service.py +8 -4
  128. django_spire/contrib/ordering/services/service.py +1 -2
  129. django_spire/contrib/ordering/tests/__init__.py +0 -0
  130. django_spire/contrib/ordering/tests/test_ordering.py +165 -0
  131. django_spire/contrib/ordering/validators.py +6 -6
  132. django_spire/contrib/pagination/templatetags/pagination_tags.py +12 -5
  133. django_spire/contrib/pagination/tests/__init__.py +0 -0
  134. django_spire/contrib/pagination/tests/test_pagination.py +179 -0
  135. django_spire/contrib/performance/decorators.py +16 -6
  136. django_spire/contrib/performance/tests/__init__.py +0 -0
  137. django_spire/contrib/performance/tests/test_performance.py +107 -0
  138. django_spire/contrib/queryset/enums.py +3 -1
  139. django_spire/contrib/queryset/filter_tools.py +10 -5
  140. django_spire/contrib/queryset/mixins.py +16 -16
  141. django_spire/contrib/queryset/tests/__init__.py +0 -0
  142. django_spire/contrib/queryset/tests/test_queryset.py +137 -0
  143. django_spire/contrib/seeding/field/base.py +13 -7
  144. django_spire/contrib/seeding/field/callable.py +8 -1
  145. django_spire/contrib/seeding/field/cleaners.py +5 -5
  146. django_spire/contrib/seeding/field/custom.py +20 -10
  147. django_spire/contrib/seeding/field/django/seeder.py +8 -6
  148. django_spire/contrib/seeding/field/enums.py +7 -5
  149. django_spire/contrib/seeding/field/override.py +16 -6
  150. django_spire/contrib/seeding/field/static.py +9 -2
  151. django_spire/contrib/seeding/field/tests/test_base.py +18 -14
  152. django_spire/contrib/seeding/field/tests/test_callable.py +13 -9
  153. django_spire/contrib/seeding/field/tests/test_cleaners.py +51 -38
  154. django_spire/contrib/seeding/field/tests/test_static.py +13 -9
  155. django_spire/contrib/seeding/intelligence/bots/seeder_generator_bot.py +2 -0
  156. django_spire/contrib/seeding/intelligence/intel.py +5 -1
  157. django_spire/contrib/seeding/intelligence/prompts/factory.py +6 -1
  158. django_spire/contrib/seeding/intelligence/prompts/foreign_key_selection_prompt.py +6 -1
  159. django_spire/contrib/seeding/intelligence/prompts/generate_django_model_seeder_prompts.py +2 -0
  160. django_spire/contrib/seeding/intelligence/prompts/generic_relationship_selection_prompt.py +7 -1
  161. django_spire/contrib/seeding/intelligence/prompts/hierarchical_selection_prompt.py +6 -2
  162. django_spire/contrib/seeding/intelligence/prompts/model_field_choices_prompt.py +8 -2
  163. django_spire/contrib/seeding/intelligence/prompts/negation_prompt.py +2 -0
  164. django_spire/contrib/seeding/intelligence/prompts/objective_prompt.py +6 -1
  165. django_spire/contrib/seeding/management/commands/seeding.py +9 -3
  166. django_spire/contrib/seeding/management/example.py +2 -0
  167. django_spire/contrib/seeding/model/base.py +16 -7
  168. django_spire/contrib/seeding/model/config.py +31 -15
  169. django_spire/contrib/seeding/model/django/config.py +13 -13
  170. django_spire/contrib/seeding/model/django/seeder.py +4 -4
  171. django_spire/contrib/seeding/model/django/tests/test_seeder.py +34 -23
  172. django_spire/contrib/seeding/model/enums.py +2 -0
  173. django_spire/contrib/seeding/tests/test_config.py +71 -0
  174. django_spire/contrib/seeding/tests/test_custom.py +35 -0
  175. django_spire/contrib/seeding/tests/test_enums.py +40 -0
  176. django_spire/contrib/seeding/tests/test_intel.py +32 -0
  177. django_spire/contrib/seeding/tests/test_override.py +63 -0
  178. django_spire/contrib/service/__init__.py +2 -2
  179. django_spire/contrib/service/django_model_service.py +16 -15
  180. django_spire/contrib/service/exceptions.py +5 -3
  181. django_spire/contrib/service/tests/__init__.py +0 -0
  182. django_spire/contrib/service/tests/test_service.py +153 -0
  183. django_spire/contrib/session/apps.py +2 -0
  184. django_spire/contrib/session/controller.py +48 -42
  185. django_spire/contrib/session/templatetags/session_tags.py +11 -2
  186. django_spire/contrib/session/tests/test_session_controller.py +117 -53
  187. django_spire/contrib/tests/__init__.py +0 -0
  188. django_spire/contrib/tests/test_utils.py +37 -0
  189. django_spire/contrib/utils.py +4 -1
  190. django_spire/core/apps.py +2 -0
  191. django_spire/core/converters/tests/test_to_data.py +353 -0
  192. django_spire/core/converters/tests/test_to_enums.py +61 -41
  193. django_spire/core/converters/tests/test_to_pydantic.py +138 -109
  194. django_spire/core/converters/to_data.py +29 -10
  195. django_spire/core/converters/to_enums.py +4 -2
  196. django_spire/core/converters/to_pydantic.py +22 -22
  197. django_spire/core/decorators.py +19 -6
  198. django_spire/core/forms/widgets.py +4 -0
  199. django_spire/core/management/commands/spire_startapp_pkg/template/app/apps.py.template +2 -0
  200. django_spire/core/management/commands/spire_startapp_pkg/template/app/urls/__init__.py.template +2 -0
  201. django_spire/core/management/commands/spire_startapp_pkg/template/app/urls/form_urls.py.template +2 -0
  202. django_spire/core/management/commands/spire_startapp_pkg/template/app/urls/page_urls.py.template +2 -0
  203. django_spire/core/maps.py +3 -1
  204. django_spire/core/middleware/maintenance.py +3 -3
  205. django_spire/core/middleware.py +8 -6
  206. django_spire/core/redirect/__init__.py +5 -0
  207. django_spire/core/redirect/generic_redirect.py +1 -2
  208. django_spire/core/redirect/tests/__init__.py +0 -0
  209. django_spire/core/redirect/tests/test_generic_redirect.py +34 -0
  210. django_spire/core/{tests/tests_redirect.py → redirect/tests/test_safe_redirect.py} +55 -81
  211. django_spire/core/shortcuts.py +3 -3
  212. django_spire/core/static/django_spire/css/app-layout.css +1 -1
  213. django_spire/core/static/django_spire/css/app-navigation.css +3 -3
  214. django_spire/core/static/django_spire/css/bootstrap-override.css +4 -0
  215. django_spire/core/tag/admin.py +12 -0
  216. django_spire/core/tag/intelligence/tag_set_bot.py +2 -0
  217. django_spire/core/tag/mixins.py +2 -0
  218. django_spire/core/tag/models.py +2 -0
  219. django_spire/core/tag/querysets.py +2 -0
  220. django_spire/core/tag/service/tag_service.py +6 -3
  221. django_spire/core/tag/tests/test_intelligence.py +9 -9
  222. django_spire/core/tag/tests/test_tags.py +44 -54
  223. django_spire/core/tag/tests/test_tools.py +191 -0
  224. django_spire/core/tag/tools.py +3 -0
  225. django_spire/core/templates/django_spire/card/card.html +5 -2
  226. django_spire/core/templates/django_spire/card/title_card.html +12 -4
  227. django_spire/core/templates/django_spire/infinite_scroll/base.html +1 -0
  228. django_spire/core/templates/django_spire/navigation/side_navigation.html +19 -24
  229. django_spire/core/templates/django_spire/page/full_page.html +46 -16
  230. django_spire/core/templates/django_spire/table/base.html +5 -3
  231. django_spire/core/templatetags/json.py +6 -2
  232. django_spire/core/templatetags/message.py +13 -8
  233. django_spire/core/templatetags/string_formating.py +8 -5
  234. django_spire/core/templatetags/tests/__init__.py +0 -0
  235. django_spire/core/templatetags/tests/test_templatetags.py +427 -0
  236. django_spire/core/templatetags/variable_types.py +17 -9
  237. django_spire/core/tests/test_cases.py +1 -1
  238. django_spire/core/tests/test_conf.py +43 -0
  239. django_spire/core/tests/test_consts.py +28 -0
  240. django_spire/core/tests/test_context_processors.py +93 -0
  241. django_spire/core/tests/test_decorators.py +95 -0
  242. django_spire/core/tests/test_django_spire_utils.py +56 -0
  243. django_spire/core/tests/test_exceptions.py +37 -0
  244. django_spire/core/tests/test_models.py +54 -0
  245. django_spire/core/tests/test_settings.py +45 -0
  246. django_spire/core/tests/test_shortcuts.py +74 -0
  247. django_spire/core/tests/test_urls.py +16 -0
  248. django_spire/core/tests/test_utils.py +58 -0
  249. django_spire/core/urls.py +4 -1
  250. django_spire/core/utils.py +12 -8
  251. django_spire/exceptions.py +16 -1
  252. django_spire/file/admin.py +4 -2
  253. django_spire/file/apps.py +8 -10
  254. django_spire/file/fields.py +7 -7
  255. django_spire/file/forms.py +1 -1
  256. django_spire/file/interfaces.py +15 -15
  257. django_spire/file/mixins.py +1 -4
  258. django_spire/file/models.py +3 -5
  259. django_spire/file/tests/factories.py +59 -0
  260. django_spire/file/tests/test_admin.py +69 -0
  261. django_spire/file/tests/test_apps.py +24 -0
  262. django_spire/file/tests/test_fields.py +114 -0
  263. django_spire/file/tests/test_forms.py +20 -0
  264. django_spire/file/tests/test_interfaces.py +183 -0
  265. django_spire/file/tests/test_models.py +82 -0
  266. django_spire/file/tests/test_querysets.py +102 -0
  267. django_spire/file/tests/test_utils.py +32 -0
  268. django_spire/file/tests/test_views.py +145 -0
  269. django_spire/file/tests/test_widgets.py +82 -0
  270. django_spire/file/tools.py +8 -2
  271. django_spire/file/views.py +7 -3
  272. django_spire/file/widgets.py +12 -12
  273. django_spire/help_desk/admin.py +15 -0
  274. django_spire/help_desk/apps.py +2 -0
  275. django_spire/help_desk/auth/controller.py +2 -0
  276. django_spire/help_desk/choices.py +2 -0
  277. django_spire/help_desk/enums.py +2 -0
  278. django_spire/help_desk/exceptions.py +31 -3
  279. django_spire/help_desk/forms.py +2 -0
  280. django_spire/help_desk/models.py +2 -0
  281. django_spire/help_desk/querysets.py +4 -1
  282. django_spire/help_desk/services/notification_service.py +26 -27
  283. django_spire/help_desk/services/service.py +2 -3
  284. django_spire/help_desk/tests/factories.py +8 -3
  285. django_spire/help_desk/tests/test_admin.py +41 -0
  286. django_spire/help_desk/tests/test_apps.py +41 -0
  287. django_spire/help_desk/tests/test_choices.py +50 -0
  288. django_spire/help_desk/tests/test_controller.py +87 -0
  289. django_spire/help_desk/tests/test_enums.py +18 -0
  290. django_spire/help_desk/tests/test_exceptions.py +37 -0
  291. django_spire/help_desk/tests/test_forms.py +89 -0
  292. django_spire/help_desk/tests/test_models.py +59 -0
  293. django_spire/help_desk/tests/test_querysets.py +38 -0
  294. django_spire/help_desk/tests/test_services/test_notification_service.py +15 -8
  295. django_spire/help_desk/tests/test_services/test_service.py +92 -0
  296. django_spire/help_desk/tests/test_urls/test_form_urls.py +6 -6
  297. django_spire/help_desk/tests/test_urls/test_page_urls.py +8 -9
  298. django_spire/help_desk/tests/test_views/test_form_views.py +46 -19
  299. django_spire/help_desk/tests/test_views/test_page_views.py +32 -9
  300. django_spire/help_desk/urls/__init__.py +4 -1
  301. django_spire/help_desk/urls/form_urls.py +3 -0
  302. django_spire/help_desk/urls/page_urls.py +3 -0
  303. django_spire/help_desk/views/form_views.py +13 -5
  304. django_spire/help_desk/views/page_views.py +11 -3
  305. django_spire/history/activity/admin.py +2 -0
  306. django_spire/history/activity/apps.py +3 -1
  307. django_spire/history/activity/mixins.py +13 -7
  308. django_spire/history/activity/models.py +6 -5
  309. django_spire/history/activity/querysets.py +2 -0
  310. django_spire/history/activity/tests/__init__.py +0 -0
  311. django_spire/history/activity/tests/test_activity.py +176 -0
  312. django_spire/history/admin.py +9 -2
  313. django_spire/history/choices.py +3 -0
  314. django_spire/history/models.py +5 -5
  315. django_spire/history/tests/test_admin.py +93 -0
  316. django_spire/history/tests/test_history.py +101 -0
  317. django_spire/history/tests/test_mixins.py +84 -0
  318. django_spire/history/viewed/admin.py +3 -1
  319. django_spire/history/viewed/apps.py +3 -1
  320. django_spire/history/viewed/models.py +2 -0
  321. django_spire/history/viewed/tests/__init__.py +0 -0
  322. django_spire/history/viewed/tests/test_viewed.py +46 -0
  323. django_spire/knowledge/auth/tests/__init__.py +0 -0
  324. django_spire/knowledge/auth/tests/test_controller.py +116 -0
  325. django_spire/knowledge/collection/admin.py +5 -1
  326. django_spire/knowledge/collection/models.py +3 -1
  327. django_spire/knowledge/collection/seeding/seed.py +1 -0
  328. django_spire/knowledge/collection/services/factory_service.py +10 -11
  329. django_spire/knowledge/collection/services/ordering_service.py +1 -2
  330. django_spire/knowledge/collection/services/service.py +5 -10
  331. django_spire/knowledge/collection/services/tag_service.py +5 -2
  332. django_spire/knowledge/collection/tests/factories.py +28 -1
  333. django_spire/knowledge/collection/tests/test_models.py +48 -0
  334. django_spire/knowledge/collection/tests/test_querysets.py +93 -0
  335. django_spire/knowledge/collection/tests/test_services/test_factory_service.py +100 -0
  336. django_spire/knowledge/collection/tests/test_services/test_services.py +160 -0
  337. django_spire/knowledge/collection/tests/test_urls/test_form_urls.py +21 -3
  338. django_spire/knowledge/collection/tests/test_urls/test_json_urls.py +39 -1
  339. django_spire/knowledge/collection/tests/test_urls/test_page_urls.py +12 -4
  340. django_spire/knowledge/collection/urls/__init__.py +3 -0
  341. django_spire/knowledge/collection/urls/form_urls.py +2 -0
  342. django_spire/knowledge/collection/urls/json_urls.py +2 -0
  343. django_spire/knowledge/collection/urls/page_urls.py +2 -0
  344. django_spire/knowledge/collection/views/form_views.py +4 -4
  345. django_spire/knowledge/collection/views/json_views.py +5 -1
  346. django_spire/knowledge/collection/views/page_views.py +5 -2
  347. django_spire/knowledge/entry/admin.py +7 -1
  348. django_spire/knowledge/entry/forms.py +2 -0
  349. django_spire/knowledge/entry/models.py +2 -0
  350. django_spire/knowledge/entry/seeding/seed.py +3 -0
  351. django_spire/knowledge/entry/services/automation_service.py +5 -4
  352. django_spire/knowledge/entry/services/factory_service.py +7 -5
  353. django_spire/knowledge/entry/services/service.py +4 -7
  354. django_spire/knowledge/entry/services/tag_service.py +0 -1
  355. django_spire/knowledge/entry/services/tool_service.py +1 -0
  356. django_spire/knowledge/entry/services/transformation_services.py +1 -5
  357. django_spire/knowledge/entry/tests/factories.py +1 -2
  358. django_spire/knowledge/entry/tests/test_factory_service.py +20 -0
  359. django_spire/knowledge/entry/tests/test_models.py +41 -0
  360. django_spire/knowledge/entry/tests/test_querysets.py +71 -0
  361. django_spire/knowledge/entry/tests/test_services.py +94 -0
  362. django_spire/knowledge/entry/tests/test_urls/test_form_urls.py +9 -14
  363. django_spire/knowledge/entry/tests/test_urls/test_json_urls.py +48 -5
  364. django_spire/knowledge/entry/tests/test_urls/test_page_urls.py +6 -8
  365. django_spire/knowledge/entry/tests/test_urls/test_template_urls.py +40 -0
  366. django_spire/knowledge/entry/urls/form_urls.py +2 -0
  367. django_spire/knowledge/entry/urls/json_urls.py +2 -0
  368. django_spire/knowledge/entry/urls/page_urls.py +2 -0
  369. django_spire/knowledge/entry/urls/template_urls.py +2 -0
  370. django_spire/knowledge/entry/version/block/choices.py +2 -0
  371. django_spire/knowledge/entry/version/block/data/data.py +1 -0
  372. django_spire/knowledge/entry/version/block/data/list/data.py +8 -13
  373. django_spire/knowledge/entry/version/block/data/list/maps.py +3 -0
  374. django_spire/knowledge/entry/version/block/data/list/meta.py +1 -2
  375. django_spire/knowledge/entry/version/block/data/list/tests/__init__.py +0 -0
  376. django_spire/knowledge/entry/version/block/data/list/tests/test_maps.py +32 -0
  377. django_spire/knowledge/entry/version/block/data/list/tests/test_meta.py +58 -0
  378. django_spire/knowledge/entry/version/block/data/maps.py +3 -6
  379. django_spire/knowledge/entry/version/block/models.py +7 -5
  380. django_spire/knowledge/entry/version/block/seeding/constants.py +5 -4
  381. django_spire/knowledge/entry/version/block/services/service.py +2 -3
  382. django_spire/knowledge/entry/version/block/tests/factories.py +4 -10
  383. django_spire/knowledge/entry/version/block/tests/test_choices.py +56 -0
  384. django_spire/knowledge/entry/version/block/tests/test_data.py +90 -0
  385. django_spire/knowledge/entry/version/block/tests/test_maps.py +37 -0
  386. django_spire/knowledge/entry/version/block/tests/test_models.py +55 -0
  387. django_spire/knowledge/entry/version/block/tests/test_querysets.py +35 -0
  388. django_spire/knowledge/entry/version/block/tests/test_services.py +65 -0
  389. django_spire/knowledge/entry/version/choices.py +2 -0
  390. django_spire/knowledge/entry/version/converters/converter.py +1 -1
  391. django_spire/knowledge/entry/version/converters/docx_converter.py +4 -7
  392. django_spire/knowledge/entry/version/converters/markdown_converter.py +20 -20
  393. django_spire/knowledge/entry/version/maps.py +4 -5
  394. django_spire/knowledge/entry/version/querysets.py +1 -1
  395. django_spire/knowledge/entry/version/seeding/seeder.py +1 -2
  396. django_spire/knowledge/entry/version/services/processor_service.py +5 -4
  397. django_spire/knowledge/entry/version/services/service.py +1 -2
  398. django_spire/knowledge/entry/version/tests/factories.py +2 -2
  399. django_spire/knowledge/entry/version/tests/test_choices.py +18 -0
  400. django_spire/knowledge/entry/version/tests/test_converters/test_docx_converter.py +56 -8
  401. django_spire/knowledge/entry/version/tests/test_converters/test_markdown_converter.py +78 -0
  402. django_spire/knowledge/entry/version/tests/test_maps.py +58 -0
  403. django_spire/knowledge/entry/version/tests/test_models.py +23 -0
  404. django_spire/knowledge/entry/version/tests/test_querysets.py +26 -0
  405. django_spire/knowledge/entry/version/tests/test_services.py +62 -0
  406. django_spire/knowledge/entry/version/tests/test_urls/test_json_urls.py +27 -8
  407. django_spire/knowledge/entry/version/tests/test_urls/test_page_urls.py +15 -8
  408. django_spire/knowledge/entry/version/tests/test_urls/test_redirect_urls.py +38 -0
  409. django_spire/knowledge/entry/version/urls/__init__.py +3 -0
  410. django_spire/knowledge/entry/version/urls/json_urls.py +2 -1
  411. django_spire/knowledge/entry/version/urls/page_urls.py +2 -0
  412. django_spire/knowledge/entry/version/urls/redirect_urls.py +2 -0
  413. django_spire/knowledge/entry/version/views/json_views.py +5 -1
  414. django_spire/knowledge/entry/version/views/page_views.py +10 -3
  415. django_spire/knowledge/entry/version/views/redirect_views.py +5 -1
  416. django_spire/knowledge/entry/views/form_views.py +16 -8
  417. django_spire/knowledge/entry/views/json_views.py +3 -1
  418. django_spire/knowledge/entry/views/page_views.py +8 -2
  419. django_spire/knowledge/entry/views/template_views.py +7 -1
  420. django_spire/knowledge/exceptions.py +2 -1
  421. django_spire/knowledge/intelligence/intel/answer_intel.py +2 -1
  422. django_spire/knowledge/intelligence/intel/entry_intel.py +0 -1
  423. django_spire/knowledge/intelligence/workflows/knowledge_workflow.py +4 -5
  424. django_spire/knowledge/models.py +1 -2
  425. django_spire/knowledge/tests/__init__.py +0 -0
  426. django_spire/knowledge/tests/test_templatetags.py +40 -0
  427. django_spire/knowledge/tests/test_urls/__init__.py +0 -0
  428. django_spire/knowledge/tests/test_urls/test_page_urls.py +24 -0
  429. django_spire/knowledge/urls/__init__.py +2 -0
  430. django_spire/knowledge/urls/page_urls.py +2 -0
  431. django_spire/knowledge/views/page_views.py +8 -3
  432. django_spire/notification/admin.py +3 -1
  433. django_spire/notification/app/admin.py +2 -0
  434. django_spire/notification/app/apps.py +3 -1
  435. django_spire/notification/app/exceptions.py +9 -2
  436. django_spire/notification/app/models.py +8 -4
  437. django_spire/notification/app/processor.py +22 -26
  438. django_spire/notification/app/querysets.py +2 -0
  439. django_spire/notification/app/tests/__init__.py +0 -0
  440. django_spire/notification/app/tests/factories.py +34 -0
  441. django_spire/notification/app/tests/test_apps.py +24 -0
  442. django_spire/notification/app/tests/test_models.py +72 -0
  443. django_spire/notification/app/tests/test_processor.py +111 -0
  444. django_spire/notification/app/tests/test_querysets.py +90 -0
  445. django_spire/notification/app/tests/test_views/__init__.py +0 -0
  446. django_spire/notification/app/tests/test_views/test_json_views.py +48 -0
  447. django_spire/notification/app/tests/test_views/test_page_views.py +19 -0
  448. django_spire/notification/app/urls/__init__.py +3 -1
  449. django_spire/notification/app/urls/json_urls.py +6 -4
  450. django_spire/notification/app/urls/page_urls.py +4 -3
  451. django_spire/notification/app/urls/template_urls.py +4 -2
  452. django_spire/notification/apps.py +4 -1
  453. django_spire/notification/email/admin.py +5 -1
  454. django_spire/notification/email/apps.py +3 -1
  455. django_spire/notification/email/exceptions.py +4 -2
  456. django_spire/notification/email/helper.py +5 -3
  457. django_spire/notification/email/models.py +4 -0
  458. django_spire/notification/email/processor.py +19 -15
  459. django_spire/notification/email/querysets.py +3 -0
  460. django_spire/notification/email/tests/__init__.py +0 -0
  461. django_spire/notification/email/tests/factories.py +35 -0
  462. django_spire/notification/email/tests/test_apps.py +24 -0
  463. django_spire/notification/email/tests/test_models.py +52 -0
  464. django_spire/notification/email/tests/test_processor.py +92 -0
  465. django_spire/notification/email/tests/test_querysets.py +43 -0
  466. django_spire/notification/exceptions.py +17 -2
  467. django_spire/notification/managers.py +7 -1
  468. django_spire/notification/maps.py +4 -1
  469. django_spire/notification/mixins.py +2 -0
  470. django_spire/notification/models.py +3 -1
  471. django_spire/notification/processors/notification.py +12 -5
  472. django_spire/notification/processors/processor.py +2 -0
  473. django_spire/notification/processors/tests/__init__.py +0 -0
  474. django_spire/notification/processors/tests/test_notification.py +106 -0
  475. django_spire/notification/push/admin.py +10 -1
  476. django_spire/notification/push/apps.py +3 -1
  477. django_spire/notification/push/models.py +2 -3
  478. django_spire/notification/push/tests/__init__.py +0 -0
  479. django_spire/notification/push/tests/test_apps.py +24 -0
  480. django_spire/notification/push/tests/test_models.py +28 -0
  481. django_spire/notification/querysets.py +7 -1
  482. django_spire/notification/sms/admin.py +2 -0
  483. django_spire/notification/sms/apps.py +4 -1
  484. django_spire/notification/sms/automations.py +2 -0
  485. django_spire/notification/sms/choices.py +2 -0
  486. django_spire/notification/sms/exceptions.py +19 -5
  487. django_spire/notification/sms/helper.py +33 -23
  488. django_spire/notification/sms/models.py +5 -1
  489. django_spire/notification/sms/processor.py +20 -20
  490. django_spire/notification/sms/querysets.py +2 -0
  491. django_spire/notification/sms/tests/factories.py +33 -0
  492. django_spire/notification/sms/tests/test_apps.py +24 -0
  493. django_spire/notification/sms/tests/test_automation.py +38 -0
  494. django_spire/notification/sms/tests/test_choices.py +15 -0
  495. django_spire/notification/sms/tests/test_consts.py +17 -0
  496. django_spire/notification/sms/tests/test_exceptions.py +27 -0
  497. django_spire/notification/sms/tests/test_helper.py +50 -0
  498. django_spire/notification/sms/tests/test_models.py +81 -0
  499. django_spire/notification/sms/tests/test_processor.py +107 -0
  500. django_spire/notification/sms/tests/test_tools.py +25 -11
  501. django_spire/notification/sms/tools.py +16 -5
  502. django_spire/notification/sms/urls/__init__.py +3 -1
  503. django_spire/notification/sms/urls/media_urls.py +2 -0
  504. django_spire/notification/sms/views/media_views.py +14 -4
  505. django_spire/notification/tests/__init__.py +0 -0
  506. django_spire/notification/tests/factories.py +26 -0
  507. django_spire/notification/tests/test_admin.py +55 -0
  508. django_spire/notification/tests/test_apps.py +30 -0
  509. django_spire/notification/tests/test_automation.py +18 -0
  510. django_spire/notification/tests/test_choices.py +59 -0
  511. django_spire/notification/tests/test_exceptions.py +58 -0
  512. django_spire/notification/tests/test_managers.py +100 -0
  513. django_spire/notification/tests/test_maps.py +31 -0
  514. django_spire/notification/tests/test_models.py +76 -0
  515. django_spire/notification/tests/test_querysets.py +184 -0
  516. django_spire/notification/tests/test_utils.py +23 -0
  517. django_spire/notification/urls.py +3 -1
  518. django_spire/notification/utils.py +3 -1
  519. django_spire/settings.py +3 -0
  520. django_spire/theme/tests/test_context_processor.py +15 -13
  521. django_spire/theme/tests/test_enums.py +2 -2
  522. django_spire/theme/tests/test_filesystem.py +2 -5
  523. django_spire/theme/tests/test_integration.py +12 -12
  524. django_spire/theme/tests/test_model.py +40 -38
  525. django_spire/theme/tests/test_views/test_json_views.py +33 -33
  526. django_spire/theme/urls/json_urls.py +3 -0
  527. django_spire/theme/urls/page_urls.py +3 -0
  528. django_spire/urls.py +19 -15
  529. django_spire/utils.py +13 -4
  530. {django_spire-0.23.7.dist-info → django_spire-0.23.9.dist-info}/METADATA +2 -2
  531. {django_spire-0.23.7.dist-info → django_spire-0.23.9.dist-info}/RECORD +534 -362
  532. {django_spire-0.23.7.dist-info → django_spire-0.23.9.dist-info}/licenses/LICENSE.md +1 -1
  533. django_spire/contrib/options/tests/test_unit.py +0 -148
  534. django_spire/contrib/seeding/tests/test_seeding.py +0 -25
  535. django_spire/core/tests/test_templatetags.py +0 -117
  536. django_spire/core/tests/tests_shortcuts.py +0 -73
  537. django_spire/history/activity/tests.py +0 -3
  538. django_spire/history/activity/views.py +0 -3
  539. django_spire/knowledge/collection/tests/test_services/test_transformation_service.py +0 -71
  540. django_spire/notification/app/tests.py +0 -3
  541. {django_spire-0.23.7.dist-info → django_spire-0.23.9.dist-info}/WHEEL +0 -0
  542. {django_spire-0.23.7.dist-info → django_spire-0.23.9.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ from django_spire.auth.user.tests.factories import create_user
4
+ from django_spire.core.tests.test_cases import BaseTestCase
5
+ from django_spire.notification.choices import NotificationStatusChoices
6
+ from django_spire.notification.email.models import EmailNotification
7
+ from django_spire.notification.email.tests.factories import create_test_email_notification
8
+
9
+
10
+ class EmailNotificationQuerySetTests(BaseTestCase):
11
+ def setUp(self):
12
+ super().setUp()
13
+ self.user = create_user(username='test_email_queryset_user')
14
+ self.email_notification = create_test_email_notification(
15
+ user=self.user,
16
+ status=NotificationStatusChoices.SENT
17
+ )
18
+
19
+ def test_by_user(self):
20
+ other_user = create_user(username='other_user')
21
+ other_notification = create_test_email_notification(user=other_user)
22
+
23
+ result = EmailNotification.objects.by_user(self.user)
24
+ assert self.email_notification in result
25
+ assert other_notification not in result
26
+
27
+ def test_by_users(self):
28
+ other_user = create_user(username='other_user')
29
+ other_notification = create_test_email_notification(user=other_user)
30
+
31
+ result = EmailNotification.objects.by_users([self.user, other_user])
32
+ assert self.email_notification in result
33
+ assert other_notification in result
34
+
35
+ def test_is_sent(self):
36
+ pending_notification = create_test_email_notification(
37
+ user=self.user,
38
+ status=NotificationStatusChoices.PENDING
39
+ )
40
+
41
+ result = EmailNotification.objects.is_sent()
42
+ assert self.email_notification in result
43
+ assert pending_notification not in result
@@ -1,5 +1,20 @@
1
- from django_spire.exceptions import DjangoSpireException
1
+ from __future__ import annotations
2
2
 
3
+ from typing import TYPE_CHECKING
3
4
 
4
- class NotificationException(DjangoSpireException):
5
+ from django_spire.exceptions import DjangoSpireError
6
+
7
+ if TYPE_CHECKING:
8
+ from django_spire.notification.choices import NotificationTypeChoices
9
+
10
+
11
+ class NotificationError(DjangoSpireError):
5
12
  pass
13
+
14
+
15
+ class InvalidNotificationTypeError(NotificationError):
16
+ def __init__(self, expected_type: NotificationTypeChoices, actual_type: NotificationTypeChoices) -> None:
17
+ super().__init__(
18
+ f'Expected notification type {expected_type}, '
19
+ f'but received {actual_type}'
20
+ )
@@ -1,9 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
1
5
  from django_spire.notification.app.processor import AppNotificationProcessor
2
6
  from django_spire.notification.email.processor import EmailNotificationProcessor
3
- from django_spire.notification.models import Notification
4
7
  from django_spire.notification.processors.notification import NotificationProcessor
5
8
  from django_spire.notification.sms.processor import SMSNotificationProcessor
6
9
 
10
+ if TYPE_CHECKING:
11
+ from django_spire.notification.models import Notification
12
+
7
13
 
8
14
  class NotificationManager:
9
15
  @staticmethod
@@ -1,12 +1,15 @@
1
+ from __future__ import annotations
2
+
1
3
  from django_spire.notification.app.models import AppNotification
2
4
  from django_spire.notification.choices import NotificationTypeChoices
3
5
  from django_spire.notification.email.models import EmailNotification
4
6
  from django_spire.notification.push.models import PushNotification
5
7
  from django_spire.notification.sms.models import SmsNotification
6
8
 
9
+
7
10
  NOTIFICATION_TYPE_CHOICE_TO_MODEL_MAP: dict[NotificationTypeChoices, type] = {
8
11
  NotificationTypeChoices.APP: AppNotification,
9
12
  NotificationTypeChoices.EMAIL: EmailNotification,
10
13
  NotificationTypeChoices.PUSH: PushNotification,
11
14
  NotificationTypeChoices.SMS: SmsNotification
12
- }
15
+ }
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from django.contrib.contenttypes.fields import GenericRelation
2
4
  from django.db import models
3
5
 
@@ -8,7 +8,9 @@ from django.contrib.contenttypes.fields import GenericForeignKey
8
8
 
9
9
  from django_spire.history.mixins import HistoryModelMixin
10
10
  from django_spire.notification.choices import (
11
- NotificationTypeChoices, NotificationPriorityChoices, NotificationStatusChoices,
11
+ NotificationPriorityChoices,
12
+ NotificationStatusChoices,
13
+ NotificationTypeChoices
12
14
  )
13
15
  from django_spire.notification.querysets import NotificationQuerySet
14
16
 
@@ -1,10 +1,14 @@
1
+ from __future__ import annotations
2
+
1
3
  from collections import defaultdict
2
4
 
3
5
  from django_spire.notification.app.processor import AppNotificationProcessor
4
- from django_spire.notification.choices import NotificationStatusChoices, \
6
+ from django_spire.notification.choices import (
7
+ NotificationStatusChoices,
5
8
  NotificationTypeChoices
9
+ )
6
10
  from django_spire.notification.email.processor import EmailNotificationProcessor
7
- from django_spire.notification.exceptions import NotificationException
11
+ from django_spire.notification.exceptions import NotificationError
8
12
  from django_spire.notification.models import Notification
9
13
  from django_spire.notification.processors.processor import BaseNotificationProcessor
10
14
  from django_spire.notification.sms.processor import SMSNotificationProcessor
@@ -13,10 +17,13 @@ from django_spire.notification.sms.processor import SMSNotificationProcessor
13
17
  class NotificationProcessor(BaseNotificationProcessor):
14
18
  def process(self, notification: Notification):
15
19
  processor = self._get_processor(notification.type)
20
+
16
21
  if processor is None:
17
22
  notification.status = NotificationStatusChoices.FAILED
18
23
  notification.save()
19
- raise NotificationException(f'Unknown notification type: {notification.type}')
24
+
25
+ message = f'Unknown notification type: {notification.type}'
26
+ raise NotificationError(message)
20
27
 
21
28
  processor().process(notification)
22
29
 
@@ -45,10 +52,10 @@ class NotificationProcessor(BaseNotificationProcessor):
45
52
  if notification_type == NotificationTypeChoices.APP:
46
53
  return AppNotificationProcessor
47
54
 
48
- elif notification_type == NotificationTypeChoices.EMAIL:
55
+ if notification_type == NotificationTypeChoices.EMAIL:
49
56
  return EmailNotificationProcessor
50
57
 
51
- elif notification_type == NotificationTypeChoices.SMS:
58
+ if notification_type == NotificationTypeChoices.SMS:
52
59
  return SMSNotificationProcessor
53
60
 
54
61
  return None
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from abc import ABC, abstractmethod
2
4
 
3
5
  from django_spire.notification.choices import NotificationStatusChoices
File without changes
@@ -0,0 +1,106 @@
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.app.models import AppNotification
10
+ from django_spire.notification.choices import (
11
+ NotificationStatusChoices,
12
+ NotificationTypeChoices,
13
+ )
14
+ from django_spire.notification.exceptions import NotificationError
15
+ from django_spire.notification.models import Notification
16
+ from django_spire.notification.processors.notification import NotificationProcessor
17
+
18
+
19
+ class NotificationProcessorTests(BaseTestCase):
20
+ def setUp(self):
21
+ super().setUp()
22
+
23
+ self.user = create_user(username='test_processor_user')
24
+ self.processor = NotificationProcessor()
25
+
26
+ def _create_notification(self, **kwargs) -> Notification:
27
+ data = {
28
+ 'user': self.user,
29
+ 'type': NotificationTypeChoices.APP,
30
+ 'title': 'Test Notification',
31
+ 'body': 'Test body',
32
+ 'status': NotificationStatusChoices.PENDING,
33
+ }
34
+ data.update(kwargs)
35
+ notification = Notification.objects.create(**data)
36
+
37
+ if data['type'] == NotificationTypeChoices.APP:
38
+ AppNotification.objects.create(notification=notification)
39
+
40
+ return notification
41
+
42
+ @patch('django_spire.notification.processors.notification.AppNotificationProcessor')
43
+ def test_process_app_notification(self, mock_processor_class: MagicMock):
44
+ mock_processor = MagicMock()
45
+ mock_processor_class.return_value = mock_processor
46
+
47
+ notification = self._create_notification(type=NotificationTypeChoices.APP)
48
+
49
+ self.processor.process(notification)
50
+
51
+ mock_processor.process.assert_called_once_with(notification)
52
+
53
+ @patch('django_spire.notification.processors.notification.EmailNotificationProcessor')
54
+ def test_process_email_notification(self, mock_processor_class: MagicMock):
55
+ mock_processor = MagicMock()
56
+ mock_processor_class.return_value = mock_processor
57
+
58
+ notification = self._create_notification(type=NotificationTypeChoices.EMAIL)
59
+
60
+ self.processor.process(notification)
61
+
62
+ mock_processor.process.assert_called_once_with(notification)
63
+
64
+ @patch('django_spire.notification.processors.notification.SMSNotificationProcessor')
65
+ def test_process_sms_notification(self, mock_processor_class: MagicMock):
66
+ mock_processor = MagicMock()
67
+ mock_processor_class.return_value = mock_processor
68
+
69
+ notification = self._create_notification(type=NotificationTypeChoices.SMS)
70
+
71
+ self.processor.process(notification)
72
+
73
+ mock_processor.process.assert_called_once_with(notification)
74
+
75
+ def test_process_unknown_type_raises_error(self):
76
+ notification = self._create_notification()
77
+ notification.type = 'unknown'
78
+
79
+ with pytest.raises(NotificationError):
80
+ self.processor.process(notification)
81
+
82
+ def test_process_unknown_type_sets_status_failed(self):
83
+ notification = self._create_notification()
84
+ notification.type = 'unknown'
85
+
86
+ with pytest.raises(NotificationError):
87
+ self.processor.process(notification)
88
+
89
+ notification.refresh_from_db()
90
+ assert notification.status == NotificationStatusChoices.FAILED
91
+
92
+ @patch('django_spire.notification.processors.notification.AppNotificationProcessor')
93
+ def test_process_list_groups_by_type(self, mock_processor_class: MagicMock):
94
+ mock_processor = MagicMock()
95
+ mock_processor_class.return_value = mock_processor
96
+
97
+ notifications = [
98
+ self._create_notification(type=NotificationTypeChoices.APP),
99
+ self._create_notification(type=NotificationTypeChoices.APP),
100
+ ]
101
+
102
+ self.processor.process_list(notifications)
103
+
104
+ mock_processor.process_list.assert_called_once()
105
+ call_args = mock_processor.process_list.call_args[0][0]
106
+ assert len(call_args) == 2
@@ -1,3 +1,12 @@
1
+ from __future__ import annotations
2
+
1
3
  from django.contrib import admin
2
4
 
3
- # Register your models here.
5
+ from django_spire.notification.push.models import PushNotification
6
+
7
+
8
+ @admin.register(PushNotification)
9
+ class PushNotificationAdmin(admin.ModelAdmin):
10
+ list_display = ('id', 'created_datetime', 'is_active', 'is_deleted')
11
+ list_filter = ('is_active', 'is_deleted')
12
+ ordering = ('-created_datetime',)
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from django.apps import AppConfig
2
4
 
3
5
  from django_spire.utils import check_required_apps
@@ -11,4 +13,4 @@ class NotificationPushConfig(AppConfig):
11
13
  REQUIRED_APPS = ('django_spire_core', 'django_spire_notification')
12
14
 
13
15
  def ready(self) -> None:
14
- check_required_apps(self.label)
16
+ check_required_apps(self.label)
@@ -1,11 +1,10 @@
1
- from django.db import models
1
+ from __future__ import annotations
2
2
 
3
3
  from django_spire.history.mixins import HistoryModelMixin
4
4
 
5
5
 
6
6
  class PushNotification(HistoryModelMixin):
7
-
8
7
  class Meta:
9
8
  db_table = 'django_spire_notification_push'
10
9
  verbose_name = 'Push Notification'
11
- verbose_name_plural = 'Push Notifications'
10
+ verbose_name_plural = 'Push Notifications'
File without changes
@@ -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.push.apps import NotificationPushConfig
7
+
8
+
9
+ class NotificationPushConfigTests(BaseTestCase):
10
+ def test_app_name(self):
11
+ assert NotificationPushConfig.name == 'django_spire.notification.push'
12
+
13
+ def test_app_label(self):
14
+ assert NotificationPushConfig.label == 'django_spire_notification_push'
15
+
16
+ def test_default_auto_field(self):
17
+ assert NotificationPushConfig.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 == NotificationPushConfig.REQUIRED_APPS
22
+
23
+ def test_app_is_installed(self):
24
+ assert apps.is_installed('django_spire.notification.push')
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ from django_spire.core.tests.test_cases import BaseTestCase
4
+ from django_spire.notification.push.models import PushNotification
5
+
6
+
7
+ class PushNotificationModelTests(BaseTestCase):
8
+ def setUp(self):
9
+ super().setUp()
10
+ self.push_notification = PushNotification.objects.create()
11
+
12
+ def test_created_datetime_auto_set(self):
13
+ assert self.push_notification.created_datetime is not None
14
+
15
+ def test_is_active_default_true(self):
16
+ assert self.push_notification.is_active is True
17
+
18
+ def test_is_deleted_default_false(self):
19
+ assert self.push_notification.is_deleted is False
20
+
21
+ def test_meta_verbose_name(self):
22
+ assert PushNotification._meta.verbose_name == 'Push Notification'
23
+
24
+ def test_meta_verbose_name_plural(self):
25
+ assert PushNotification._meta.verbose_name_plural == 'Push Notifications'
26
+
27
+ def test_meta_db_table(self):
28
+ assert PushNotification._meta.db_table == 'django_spire_notification_push'
@@ -1,7 +1,9 @@
1
+ from __future__ import annotations
2
+
1
3
  from collections import defaultdict
4
+ from typing import TYPE_CHECKING
2
5
 
3
6
  from django.contrib.admin.options import get_content_type_for_model
4
- from django.contrib.auth.models import User
5
7
  from django.db.models import QuerySet, Q, Model
6
8
  from django.utils.timezone import now
7
9
 
@@ -12,6 +14,9 @@ from django_spire.notification.choices import (
12
14
  NotificationPriorityChoices,
13
15
  )
14
16
 
17
+ if TYPE_CHECKING:
18
+ from django.contrib.auth.models import User
19
+
15
20
 
16
21
  class NotificationQuerySet(HistoryQuerySet):
17
22
  def app_notifications(self):
@@ -108,6 +113,7 @@ class NotificationContentObjectQuerySet(QuerySet):
108
113
  ].append(content_object.pk)
109
114
 
110
115
  queries = Q()
116
+
111
117
  for content_type, object_ids in content_type_to_object_ids.items():
112
118
  queries |= Q(
113
119
  notification__content_type=content_type,
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from django.contrib import admin
2
4
  from django.urls import reverse
3
5
  from django.utils.html import format_html
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from django.apps import AppConfig
2
4
  from django.conf import settings
3
5
 
@@ -14,6 +16,7 @@ class NotificationSmsConfig(AppConfig):
14
16
 
15
17
  def ready(self) -> None:
16
18
  if not isinstance(getattr(settings, TWILIO_SMS_BATCH_SIZE_NAME), int):
17
- raise ValueError(f'"{TWILIO_SMS_BATCH_SIZE_NAME}" must be set in the django settings when using "{self.label}".')
19
+ message = f'"{TWILIO_SMS_BATCH_SIZE_NAME}" must be set in the django settings when using "{self.label}".'
20
+ raise TypeError(message)
18
21
 
19
22
  check_required_apps(self.label)
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from django_spire.notification.sms.models import SmsTemporaryMedia
2
4
  from django_spire.notification.sms.tools import update_unsent_notification_status_for_deleted_temporary_media
3
5
 
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from django.db import models
2
4
 
3
5
 
@@ -1,17 +1,31 @@
1
- from django_spire.exceptions import DjangoSpireException
1
+ from __future__ import annotations
2
2
 
3
+ from django_spire.exceptions import DjangoSpireError
3
4
 
4
- class SmsNotificationException(DjangoSpireException):
5
+
6
+ class SmsNotificationError(DjangoSpireError):
5
7
  pass
6
8
 
7
9
 
8
- class SmsTemporaryMediaException(SmsNotificationException):
10
+ class SmsTemporaryMediaError(SmsNotificationError):
9
11
  pass
10
12
 
11
13
 
12
- class TwilioException(Exception):
14
+ class TwilioError(Exception):
13
15
  pass
14
16
 
15
17
 
16
- class TwilioAPIConcurrentException(TwilioException):
18
+ class InvalidPhoneNumberError(TwilioError):
19
+ def __init__(self, phone_number: str) -> None:
20
+ super().__init__(f'Invalid phone number format: {phone_number}')
21
+
22
+
23
+ class TwilioResponseError(TwilioError):
24
+ def __init__(self, error_code: int | None, error_message: str | None) -> None:
25
+ super().__init__(
26
+ f'Twilio Error: code={error_code}, message={error_message}'
27
+ )
28
+
29
+
30
+ class TwilioAPIConcurrentError(TwilioError):
17
31
  pass
@@ -1,17 +1,27 @@
1
+ from __future__ import annotations
2
+
1
3
  import time
4
+
2
5
  from collections import defaultdict
3
6
 
4
7
  from django.utils.timezone import now
5
- from twilio.rest.api.v2010.account.message import MessageInstance
6
- from twilio.rest import Client
7
8
 
8
9
  from django.conf import settings
9
10
 
10
11
  from django_spire.notification.choices import NotificationStatusChoices
11
- from django_spire.notification.models import Notification
12
12
  from django_spire.notification.sms.consts import TWILIO_UNSUCCESSFUL_STATUSES
13
- from django_spire.notification.sms.exceptions import TwilioException, \
14
- TwilioAPIConcurrentException
13
+ from django_spire.notification.sms.exceptions import (
14
+ InvalidPhoneNumberError,
15
+ TwilioAPIConcurrentError,
16
+ TwilioError,
17
+ TwilioResponseError
18
+ )
19
+ from typing import TYPE_CHECKING
20
+
21
+ if TYPE_CHECKING:
22
+ from django_spire.notification.models import Notification
23
+ from twilio.rest.api.v2010.account.message import MessageInstance
24
+ from twilio.rest import Client
15
25
 
16
26
 
17
27
  class BulkTwilioSMSHelper:
@@ -31,6 +41,7 @@ class BulkTwilioSMSHelper:
31
41
  self._find_notification_segments()
32
42
 
33
43
  sent_segments = 0
44
+
34
45
  for notification, segments in self.notification_segments.items():
35
46
  try:
36
47
  TwilioSMSHelper(notification, self.client).send()
@@ -44,18 +55,17 @@ class BulkTwilioSMSHelper:
44
55
  sent_segments = 0
45
56
 
46
57
  except Exception as e:
47
- if isinstance(e, TwilioAPIConcurrentException):
58
+ if isinstance(e, TwilioAPIConcurrentError):
48
59
  notification.status = NotificationStatusChoices.PENDING
49
60
 
50
- if isinstance(e, TwilioException):
61
+ if isinstance(e, TwilioError):
51
62
  notification.status_message = str(e)
52
63
  notification.status = NotificationStatusChoices.ERRORED
53
- raise e
64
+ raise
54
65
 
55
- else:
56
- notification.status_message = str(e)
57
- notification.status = NotificationStatusChoices.FAILED
58
- raise e
66
+ notification.status_message = str(e)
67
+ notification.status = NotificationStatusChoices.FAILED
68
+ raise
59
69
 
60
70
 
61
71
  class TwilioSMSHelper:
@@ -69,14 +79,17 @@ class TwilioSMSHelper:
69
79
 
70
80
  def _attempt_send(self) -> MessageInstance:
71
81
  try:
82
+ media_url = self.notification.sms.media_url
83
+
72
84
  return self.client.messages.create(
73
85
  to=self.to_phone_number,
74
86
  from_=settings.TWILIO_PHONE_NUMBER,
75
87
  body=self.message,
76
- media_url=[self.notification.sms.media_url] if self.notification.sms.media_url else [],
88
+ media_url=[media_url] if media_url else [],
77
89
  )
78
90
  except Exception as e:
79
- raise TwilioException(f'Twilio Exception: {str(e)}')
91
+ message = f'Twilio Error: {e!s}'
92
+ raise TwilioError(message) from e
80
93
 
81
94
  def send(self):
82
95
  response = self._attempt_send()
@@ -84,16 +97,15 @@ class TwilioSMSHelper:
84
97
 
85
98
  def _handle_response(self, response: MessageInstance):
86
99
  if response.error_code == 429:
87
- raise TwilioAPIConcurrentException(
88
- 'Twilio API concurrent request limit exceeded.'
89
- )
100
+ raise TwilioAPIConcurrentError
101
+
90
102
  if response.status in TWILIO_UNSUCCESSFUL_STATUSES:
91
103
  retry_response = self._attempt_send()
92
104
 
93
105
  if retry_response.status in TWILIO_UNSUCCESSFUL_STATUSES:
94
- raise TwilioException(
95
- f'Twilio Exception: code={retry_response.error_code}, '
96
- f'message={retry_response.error_message}'
106
+ raise TwilioResponseError(
107
+ retry_response.error_code,
108
+ retry_response.error_message
97
109
  )
98
110
 
99
111
  @staticmethod
@@ -105,8 +117,6 @@ class TwilioSMSHelper:
105
117
  elif len(cleaned_number) == 11 and cleaned_number.startswith('1'):
106
118
  formatted_number = cleaned_number
107
119
  else:
108
- raise TwilioException(
109
- f'Invalid phone number format: {phone_number}.'
110
- )
120
+ raise InvalidPhoneNumberError(phone_number)
111
121
 
112
122
  return '+' + formatted_number
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from django.contrib.sites.models import Site
2
4
  from django.db import models
3
5
  from django.urls import reverse
@@ -5,8 +7,10 @@ from django.utils.timezone import now
5
7
 
6
8
  from django_spire.notification.models import Notification
7
9
  from django_spire.notification.sms.choices import SmsMediaContentTypeChoices
8
- from django_spire.notification.sms.querysets import SmsNotificationQuerySet, \
10
+ from django_spire.notification.sms.querysets import (
11
+ SmsNotificationQuerySet,
9
12
  SmsTemporaryMediaQuerySet
13
+ )
10
14
 
11
15
 
12
16
  class SmsTemporaryMedia(models.Model):