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,90 @@
1
+ from __future__ import annotations
2
+
3
+ from django.contrib.contenttypes.models import ContentType
4
+ from django.utils.timezone import now
5
+
6
+ from django_spire.auth.user.tests.factories import create_user
7
+ from django_spire.core.tests.test_cases import BaseTestCase
8
+ from django_spire.history.viewed.models import Viewed
9
+ from django_spire.notification.app.models import AppNotification
10
+ from django_spire.notification.app.tests.factories import create_test_app_notification
11
+ from django_spire.notification.choices import NotificationStatusChoices
12
+
13
+
14
+ class AppNotificationQuerySetTests(BaseTestCase):
15
+ def setUp(self):
16
+ super().setUp()
17
+ self.user = create_user(username='test_app_notification_user')
18
+ self.app_notification = create_test_app_notification(user=self.user)
19
+
20
+ def _create_viewed_for_notification(self, app_notification: AppNotification, user):
21
+ content_type = ContentType.objects.get_for_model(AppNotification)
22
+ Viewed.objects.create(
23
+ user=user,
24
+ object_id=app_notification.id,
25
+ content_type=content_type
26
+ )
27
+
28
+ def test_by_user(self):
29
+ other_user = create_user(username='other_user')
30
+ other_notification = create_test_app_notification(user=other_user)
31
+
32
+ result = AppNotification.objects.by_user(self.user)
33
+ assert self.app_notification in result
34
+ assert other_notification not in result
35
+
36
+ def test_by_users(self):
37
+ other_user = create_user(username='other_user')
38
+ other_notification = create_test_app_notification(user=other_user)
39
+
40
+ result = AppNotification.objects.by_users([self.user, other_user])
41
+ assert self.app_notification in result
42
+ assert other_notification in result
43
+
44
+ def test_is_sent(self):
45
+ pending_notification = create_test_app_notification(
46
+ user=self.user,
47
+ status=NotificationStatusChoices.PENDING
48
+ )
49
+
50
+ result = AppNotification.objects.is_sent()
51
+ assert self.app_notification in result
52
+ assert pending_notification not in result
53
+
54
+ def test_exclude_viewed_by_user(self):
55
+ self._create_viewed_for_notification(self.app_notification, self.user)
56
+
57
+ result = AppNotification.objects.exclude_viewed_by_user(self.user)
58
+ assert self.app_notification not in result
59
+
60
+ def test_annotate_is_viewed_by_user_false(self):
61
+ result = AppNotification.objects.annotate_is_viewed_by_user(self.user).first()
62
+ assert result.viewed is False
63
+
64
+ def test_annotate_is_viewed_by_user_true(self):
65
+ self._create_viewed_for_notification(self.app_notification, self.user)
66
+
67
+ result = AppNotification.objects.annotate_is_viewed_by_user(self.user).first()
68
+ assert result.viewed is True
69
+
70
+ def test_ordered_by_priority_and_sent_datetime(self):
71
+ same_time = now()
72
+ high_priority = create_test_app_notification(
73
+ user=self.user,
74
+ priority='high',
75
+ sent_datetime=same_time
76
+ )
77
+ low_priority = create_test_app_notification(
78
+ user=self.user,
79
+ priority='low',
80
+ sent_datetime=same_time
81
+ )
82
+
83
+ result = list(
84
+ AppNotification.objects
85
+ .filter(pk__in=[high_priority.pk, low_priority.pk])
86
+ .ordered_by_priority_and_sent_datetime()
87
+ )
88
+
89
+ assert result[0].pk == high_priority.pk
90
+ assert result[1].pk == low_priority.pk
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ from django.urls import reverse
4
+
5
+ from django_spire.core.tests.test_cases import BaseTestCase
6
+ from django_spire.notification.app.tests.factories import create_test_app_notification
7
+
8
+
9
+ class AppNotificationJsonViewsTests(BaseTestCase):
10
+ def setUp(self):
11
+ super().setUp()
12
+ self.app_notification = create_test_app_notification(user=self.super_user)
13
+
14
+ def test_check_new_notifications_ajax_view(self):
15
+ response = self.client.get(
16
+ reverse('django_spire:notification:app:json:check_new')
17
+ )
18
+
19
+ assert response.status_code == 200
20
+ assert 'has_new_notifications' in response.json()
21
+
22
+ def test_check_new_notifications_returns_true_when_unviewed(self):
23
+ response = self.client.get(
24
+ reverse('django_spire:notification:app:json:check_new')
25
+ )
26
+
27
+ data = response.json()
28
+ assert data['has_new_notifications'] is True
29
+
30
+ def test_set_notifications_as_viewed_ajax_view(self):
31
+ response = self.client.get(
32
+ reverse('django_spire:notification:app:json:set_viewed')
33
+ )
34
+
35
+ assert response.status_code == 200
36
+ assert response.json()['status'] == 200
37
+
38
+ def test_set_notifications_as_viewed_marks_as_viewed(self):
39
+ self.client.get(
40
+ reverse('django_spire:notification:app:json:set_viewed')
41
+ )
42
+
43
+ check_response = self.client.get(
44
+ reverse('django_spire:notification:app:json:check_new')
45
+ )
46
+
47
+ data = check_response.json()
48
+ assert data['has_new_notifications'] is False
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ from django.urls import reverse
4
+
5
+ from django_spire.core.tests.test_cases import BaseTestCase
6
+ from django_spire.notification.app.tests.factories import create_test_app_notification
7
+
8
+
9
+ class AppNotificationPageViewsTests(BaseTestCase):
10
+ def setUp(self):
11
+ super().setUp()
12
+ self.app_notification = create_test_app_notification(user=self.super_user)
13
+
14
+ def test_app_notification_list_view_status_code(self):
15
+ response = self.client.get(
16
+ reverse('django_spire:notification:app:page:list')
17
+ )
18
+
19
+ assert response.status_code == 200
@@ -1,7 +1,9 @@
1
+ from __future__ import annotations
2
+
1
3
  from django.urls import include, path
2
4
 
3
- app_name = 'app'
4
5
 
6
+ app_name = 'app'
5
7
 
6
8
  urlpatterns = [
7
9
  path('json/', include('django_spire.notification.app.urls.json_urls', namespace='json')),
@@ -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.app.views import json_views
@@ -7,10 +9,10 @@ app_name = 'django_spire_notification'
7
9
 
8
10
  urlpatterns = [
9
11
  path('check/notification/ajax/',
10
- json_views.check_new_notifications_ajax_view,
11
- name='check_new'),
12
+ json_views.check_new_notifications_ajax_view,
13
+ name='check_new'),
12
14
 
13
15
  path('set_viewed/notification/ajax/',
14
- json_views.set_notifications_as_viewed_ajax,
15
- name='set_viewed'),
16
+ json_views.set_notifications_as_viewed_ajax,
17
+ name='set_viewed'),
16
18
  ]
@@ -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.app.views import page_views
@@ -7,7 +9,6 @@ app_name = 'django_spire_notification'
7
9
 
8
10
  urlpatterns = [
9
11
  path('django_spire/notification/list/',
10
- view=page_views.app_notification_list_view,
11
- name='list'
12
- )
12
+ view=page_views.app_notification_list_view,
13
+ name='list')
13
14
  ]
@@ -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.app.views import template_views
@@ -7,6 +9,6 @@ app_name = 'django_spire_notification'
7
9
 
8
10
  urlpatterns = [
9
11
  path('notficiation/dropdown/template/',
10
- template_views.notification_dropdown_template_view,
11
- name='notification_dropdown')
12
+ template_views.notification_dropdown_template_view,
13
+ name='notification_dropdown')
12
14
  ]
@@ -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
 
@@ -17,6 +19,7 @@ class NotificationConfig(AppConfig):
17
19
 
18
20
  def ready(self) -> None:
19
21
  if not isinstance(getattr(settings, NOTIFICATION_THROTTLE_RATE_PER_MINUTE_SETTINGS_NAME), int):
20
- raise ValueError(f'"{NOTIFICATION_THROTTLE_RATE_PER_MINUTE_SETTINGS_NAME}" must be set in the django settings when using "{self.label}".')
22
+ message = f'"{NOTIFICATION_THROTTLE_RATE_PER_MINUTE_SETTINGS_NAME}" must be set in the django settings when using "{self.label}".'
23
+ raise TypeError(message)
21
24
 
22
25
  check_required_apps(self.label)
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from django.contrib import admin
2
4
 
3
5
  from django_spire.notification.email.models import EmailNotification
@@ -6,5 +8,7 @@ from django_spire.notification.email.models import EmailNotification
6
8
  @admin.register(EmailNotification)
7
9
  class EmailNotificationAdmin(admin.ModelAdmin):
8
10
  list_display = (
9
- 'id', 'notification', 'to_email_address'
11
+ 'id',
12
+ 'notification',
13
+ 'to_email_address'
10
14
  )
@@ -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 NotificationEmailConfig(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,5 +1,7 @@
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 EmailNotificationException(DjangoSpireException):
5
+
6
+ class EmailNotificationError(DjangoSpireError):
5
7
  pass
@@ -1,10 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from typing import TYPE_CHECKING
4
+
3
5
  from django.conf import settings
4
6
  from django.core.mail import EmailMessage
5
7
 
6
- from django_spire.file.models import File
7
- from django_spire.notification.models import Notification
8
+ if TYPE_CHECKING:
9
+ from django_spire.notification.models import Notification
8
10
 
9
11
 
10
12
  class EmailHelper:
@@ -29,6 +31,7 @@ class EmailHelper:
29
31
  self.fail_silently = fail_silently
30
32
 
31
33
  self.attachments = []
34
+
32
35
  for attachment in list(email.attachments.all()):
33
36
  with attachment.file.open('rb') as f:
34
37
  self.attachments.append((attachment.name, f.read(), attachment.type))
@@ -44,7 +47,6 @@ class SendGridEmailHelper(EmailHelper):
44
47
 
45
48
  if notification.email.template_id == '':
46
49
  self.template_id = settings.SENDGRID_TEMPLATE_ID
47
-
48
50
  else:
49
51
  self.template_id = notification.email.template_id
50
52
 
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from django.db import models
2
4
 
3
5
  from django_spire.file.models import File
@@ -9,7 +11,9 @@ class EmailNotification(models.Model):
9
11
  """
10
12
  It is important to note size limits for email content contained in this model. E.g., Sendgrid has a hard total email
11
13
  limit of 30mb (and a recommended limit of 10mb for attachments): https://www.twilio.com/docs/sendgrid/ui/sending-email/attachments-with-digioh#-Limitations
14
+
12
15
  """
16
+
13
17
  notification = models.OneToOneField(
14
18
  Notification,
15
19
  editable=False,
@@ -1,25 +1,30 @@
1
+ from __future__ import annotations
2
+
1
3
  from django.utils.timezone import now
4
+
2
5
  from sendgrid import SendGridException
3
6
 
4
- from django_spire.notification.choices import NotificationTypeChoices, \
5
- NotificationStatusChoices
7
+ from django_spire.notification.choices import (
8
+ NotificationStatusChoices,
9
+ NotificationTypeChoices
10
+ )
6
11
  from django_spire.notification.email.helper import SendGridEmailHelper
7
- from django_spire.notification.exceptions import NotificationException
12
+ from django_spire.notification.exceptions import InvalidNotificationTypeError
8
13
  from django_spire.notification.models import Notification
9
14
  from django_spire.notification.processors.processor import BaseNotificationProcessor
10
15
 
11
16
 
12
17
  class EmailNotificationProcessor(BaseNotificationProcessor):
18
+ def _validate_notification_type(self, notification: Notification):
19
+ if notification.type != NotificationTypeChoices.EMAIL:
20
+ raise InvalidNotificationTypeError(NotificationTypeChoices.EMAIL, notification.type)
21
+
13
22
  def process(self, notification: Notification):
14
23
  notification.status = NotificationStatusChoices.PROCESSING
15
24
  notification.save()
16
25
 
17
26
  try:
18
- if notification.type != NotificationTypeChoices.EMAIL:
19
- raise NotificationException(
20
- f"EmailNotificationProcessor only processes "
21
- f"Email notifications. Was provided {notification.type}"
22
- )
27
+ self._validate_notification_type(notification)
23
28
 
24
29
  SendGridEmailHelper(notification).send()
25
30
 
@@ -27,11 +32,12 @@ class EmailNotificationProcessor(BaseNotificationProcessor):
27
32
  notification.sent_datetime = now()
28
33
  except Exception as e:
29
34
  notification.status_message = str(e)
35
+
30
36
  if isinstance(e, SendGridException):
31
37
  notification.status = NotificationStatusChoices.ERRORED
32
38
  else:
33
39
  notification.status = NotificationStatusChoices.FAILED
34
- raise e
40
+ raise
35
41
  finally:
36
42
  notification.save()
37
43
 
@@ -40,11 +46,7 @@ class EmailNotificationProcessor(BaseNotificationProcessor):
40
46
 
41
47
  for notification in notifications:
42
48
  try:
43
- if notification.type != NotificationTypeChoices.EMAIL:
44
- raise NotificationException(
45
- f"EmailNotificationProcessor only processes "
46
- f"Email notifications. Was provided {notification.type}"
47
- )
49
+ self._validate_notification_type(notification)
48
50
 
49
51
  SendGridEmailHelper(notification).send()
50
52
 
@@ -56,11 +58,13 @@ class EmailNotificationProcessor(BaseNotificationProcessor):
56
58
  notification.status = NotificationStatusChoices.ERRORED
57
59
  else:
58
60
  notification.status = NotificationStatusChoices.FAILED
61
+
59
62
  Notification.objects.bulk_update(
60
63
  notifications,
61
64
  ['status', 'sent_datetime', 'status_message']
62
65
  )
63
- raise e
66
+
67
+ raise
64
68
 
65
69
  Notification.objects.bulk_update(
66
70
  notifications,
@@ -1,5 +1,7 @@
1
1
  from __future__ import annotations
2
+
2
3
  from typing import TYPE_CHECKING
4
+
3
5
  from django.db.models import QuerySet, Value, When, Case, BooleanField
4
6
 
5
7
  from django_spire.history.querysets import HistoryQuerySet
@@ -9,6 +11,7 @@ from django_spire.notification.querysets import NotificationContentObjectQuerySe
9
11
  if TYPE_CHECKING:
10
12
  from django.contrib.auth.models import User
11
13
 
14
+
12
15
  class EmailNotificationQuerySet(HistoryQuerySet, NotificationContentObjectQuerySet):
13
16
  def annotate_is_viewed_by_user(self, user: User) -> QuerySet:
14
17
  return self.by_user(user=user).annotate(viewed=Case(
File without changes
@@ -0,0 +1,35 @@
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.email.models import EmailNotification
10
+ from django_spire.notification.models import Notification
11
+
12
+
13
+ def create_test_email_notification(**kwargs) -> EmailNotification:
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.EMAIL,
19
+ title=kwargs.pop('title', 'Test Email Notification'),
20
+ body=kwargs.pop('body', 'This is a test email notification.'),
21
+ url=kwargs.pop('url', 'https://example.com'),
22
+ status=kwargs.pop('status', NotificationStatusChoices.PENDING),
23
+ priority=kwargs.pop('priority', 'low'),
24
+ )
25
+ kwargs['notification'] = notification
26
+
27
+ data = {
28
+ 'to_email_address': 'test@example.com',
29
+ 'template_id': '',
30
+ 'context_data': {},
31
+ 'cc': [],
32
+ 'bcc': [],
33
+ }
34
+ data.update(kwargs)
35
+ return EmailNotification.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.email.apps import NotificationEmailConfig
7
+
8
+
9
+ class NotificationEmailConfigTests(BaseTestCase):
10
+ def test_app_name(self):
11
+ assert NotificationEmailConfig.name == 'django_spire.notification.email'
12
+
13
+ def test_app_label(self):
14
+ assert NotificationEmailConfig.label == 'django_spire_notification_email'
15
+
16
+ def test_default_auto_field(self):
17
+ assert NotificationEmailConfig.default_auto_field == 'django.db.models.BigAutoField'
18
+
19
+ def test_required_apps(self):
20
+ expected = ('django_spire_core', 'django_spire_notification')
21
+ assert NotificationEmailConfig.REQUIRED_APPS == expected
22
+
23
+ def test_app_is_installed(self):
24
+ assert apps.is_installed('django_spire.notification.email')
@@ -0,0 +1,52 @@
1
+ from __future__ import annotations
2
+
3
+ from django_spire.core.tests.test_cases import BaseTestCase
4
+ from django_spire.notification.email.tests.factories import create_test_email_notification
5
+ from django_spire.notification.models import Notification
6
+
7
+
8
+ class EmailNotificationModelTests(BaseTestCase):
9
+ def setUp(self):
10
+ super().setUp()
11
+ self.email_notification = create_test_email_notification()
12
+
13
+ def test_str(self):
14
+ expected = f'{self.email_notification.notification.title} - {self.email_notification.to_email_address}'
15
+ assert str(self.email_notification) == expected
16
+
17
+ def test_notification_relationship(self):
18
+ assert self.email_notification.notification is not None
19
+ assert isinstance(self.email_notification.notification, Notification)
20
+
21
+ def test_default_template_id(self):
22
+ assert self.email_notification.template_id == ''
23
+
24
+ def test_default_context_data(self):
25
+ assert self.email_notification.context_data == {}
26
+
27
+ def test_default_cc(self):
28
+ assert self.email_notification.cc == []
29
+
30
+ def test_default_bcc(self):
31
+ assert self.email_notification.bcc == []
32
+
33
+ def test_to_email_address(self):
34
+ assert self.email_notification.to_email_address == 'test@example.com'
35
+
36
+ def test_with_cc(self):
37
+ email_notification = create_test_email_notification(
38
+ cc=['cc1@example.com', 'cc2@example.com']
39
+ )
40
+ assert email_notification.cc == ['cc1@example.com', 'cc2@example.com']
41
+
42
+ def test_with_bcc(self):
43
+ email_notification = create_test_email_notification(
44
+ bcc=['bcc1@example.com', 'bcc2@example.com']
45
+ )
46
+ assert email_notification.bcc == ['bcc1@example.com', 'bcc2@example.com']
47
+
48
+ def test_with_context_data(self):
49
+ email_notification = create_test_email_notification(
50
+ context_data={'key': 'value'}
51
+ )
52
+ assert email_notification.context_data == {'key': 'value'}
@@ -0,0 +1,92 @@
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.email.models import EmailNotification
14
+ from django_spire.notification.email.processor import EmailNotificationProcessor
15
+ from django_spire.notification.email.tests.factories import create_test_email_notification
16
+ from django_spire.notification.exceptions import NotificationError
17
+ from django_spire.notification.models import Notification
18
+
19
+
20
+ class EmailNotificationProcessorTests(BaseTestCase):
21
+ def setUp(self):
22
+ super().setUp()
23
+ self.user = create_user(username='test_email_processor_user')
24
+ self.processor = EmailNotificationProcessor()
25
+
26
+ @patch('django_spire.notification.email.processor.SendGridEmailHelper')
27
+ def test_process_sets_status_to_sent(self, mock_helper_class: MagicMock):
28
+ mock_helper = MagicMock()
29
+ mock_helper_class.return_value = mock_helper
30
+
31
+ email_notification = create_test_email_notification(user=self.user)
32
+ notification = email_notification.notification
33
+
34
+ self.processor.process(notification)
35
+
36
+ notification.refresh_from_db()
37
+ assert notification.status == NotificationStatusChoices.SENT
38
+
39
+ @patch('django_spire.notification.email.processor.SendGridEmailHelper')
40
+ def test_process_sets_sent_datetime(self, mock_helper_class: MagicMock):
41
+ mock_helper = MagicMock()
42
+ mock_helper_class.return_value = mock_helper
43
+
44
+ email_notification = create_test_email_notification(user=self.user)
45
+ notification = email_notification.notification
46
+
47
+ self.processor.process(notification)
48
+
49
+ notification.refresh_from_db()
50
+ assert notification.sent_datetime is not None
51
+
52
+ @patch('django_spire.notification.email.processor.SendGridEmailHelper')
53
+ def test_process_calls_send(self, mock_helper_class: MagicMock):
54
+ mock_helper = MagicMock()
55
+ mock_helper_class.return_value = mock_helper
56
+
57
+ email_notification = create_test_email_notification(user=self.user)
58
+ notification = email_notification.notification
59
+
60
+ self.processor.process(notification)
61
+
62
+ mock_helper.send.assert_called_once()
63
+
64
+ def test_process_raises_error_for_wrong_type(self):
65
+ notification = Notification.objects.create(
66
+ user=self.user,
67
+ type=NotificationTypeChoices.APP,
68
+ title='Test',
69
+ body='Test',
70
+ status=NotificationStatusChoices.PENDING,
71
+ )
72
+
73
+ with pytest.raises(NotificationError):
74
+ self.processor.process(notification)
75
+
76
+ @patch('django_spire.notification.email.processor.SendGridEmailHelper')
77
+ def test_process_list(self, mock_helper_class: MagicMock):
78
+ mock_helper = MagicMock()
79
+ mock_helper_class.return_value = mock_helper
80
+
81
+ notifications = [
82
+ create_test_email_notification(user=self.user).notification
83
+ for _ in range(3)
84
+ ]
85
+
86
+ self.processor.process_list(notifications)
87
+
88
+ for notification in notifications:
89
+ notification.refresh_from_db()
90
+ assert notification.status == NotificationStatusChoices.SENT
91
+
92
+ assert mock_helper.send.call_count == 3