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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (541) hide show
  1. django_spire/ai/admin.py +11 -11
  2. django_spire/ai/chat/apps.py +1 -0
  3. django_spire/ai/chat/templates/django_spire/ai/chat/widget/dialog_widget.html +1 -1
  4. django_spire/ai/chat/tests/factories.py +15 -0
  5. django_spire/ai/chat/tests/test_controller.py +45 -0
  6. django_spire/ai/chat/tests/test_models.py +301 -0
  7. django_spire/ai/chat/tests/test_prompts.py +48 -0
  8. django_spire/ai/chat/tests/test_responses.py +208 -0
  9. django_spire/ai/chat/tests/test_router/test_base_chat_router.py +66 -6
  10. django_spire/ai/chat/tests/test_router/test_chat_workflow.py +73 -3
  11. django_spire/ai/chat/tests/test_router/test_integration.py +86 -6
  12. django_spire/ai/chat/tests/test_router/test_intent_decoder.py +93 -1
  13. django_spire/ai/chat/tests/test_router/test_message_intel.py +60 -1
  14. django_spire/ai/chat/tests/test_router/test_spire_chat_router.py +110 -0
  15. django_spire/ai/chat/tests/test_urls/test_json_urls.py +202 -1
  16. django_spire/ai/context/tests/__init__.py +0 -0
  17. django_spire/ai/context/tests/test_context.py +188 -0
  18. django_spire/ai/decorators.py +7 -6
  19. django_spire/ai/prompt/tests/test_bots.py +100 -10
  20. django_spire/ai/prompt/tests/test_prompt_intel.py +83 -0
  21. django_spire/ai/prompt/tests/test_prompt_tuning.py +126 -0
  22. django_spire/ai/sms/decorators.py +8 -2
  23. django_spire/ai/sms/tests/test_sms.py +240 -16
  24. django_spire/ai/sms/tests/test_sms_intel.py +42 -0
  25. django_spire/ai/sms/tests/test_webhook.py +155 -7
  26. django_spire/ai/sms/views.py +23 -24
  27. django_spire/ai/tests/test_ai.py +131 -7
  28. django_spire/auth/apps.py +4 -2
  29. django_spire/auth/controller/controller.py +36 -23
  30. django_spire/auth/controller/exceptions.py +9 -0
  31. django_spire/auth/group/admin.py +1 -0
  32. django_spire/auth/group/apps.py +2 -0
  33. django_spire/auth/group/factories.py +17 -8
  34. django_spire/auth/group/forms.py +7 -0
  35. django_spire/auth/group/tests/test_factories.py +146 -0
  36. django_spire/auth/group/tests/test_forms.py +282 -0
  37. django_spire/auth/group/tests/test_models.py +192 -0
  38. django_spire/auth/group/tests/test_querysets.py +98 -0
  39. django_spire/auth/group/tests/test_utils.py +341 -0
  40. django_spire/auth/group/tests/test_views.py +377 -0
  41. django_spire/auth/group/urls/__init__.py +3 -1
  42. django_spire/auth/group/urls/form_urls.py +2 -0
  43. django_spire/auth/group/urls/json_urls.py +3 -0
  44. django_spire/auth/group/urls/page_urls.py +2 -0
  45. django_spire/auth/group/utils.py +6 -2
  46. django_spire/auth/group/views/form_views.py +6 -3
  47. django_spire/auth/group/views/json_views.py +6 -2
  48. django_spire/auth/mfa/admin.py +2 -0
  49. django_spire/auth/mfa/apps.py +2 -0
  50. django_spire/auth/mfa/forms.py +1 -0
  51. django_spire/auth/mfa/querysets.py +9 -2
  52. django_spire/auth/mfa/tests/test_models.py +233 -0
  53. django_spire/auth/mfa/tests/test_utils.py +106 -0
  54. django_spire/auth/mfa/urls/__init__.py +2 -0
  55. django_spire/auth/mfa/urls/page_urls.py +2 -0
  56. django_spire/auth/mfa/urls/redirect_urls.py +2 -0
  57. django_spire/auth/mfa/views/page_views.py +2 -1
  58. django_spire/auth/permissions/consts.py +2 -2
  59. django_spire/auth/permissions/decorators.py +8 -8
  60. django_spire/auth/permissions/permissions.py +28 -35
  61. django_spire/auth/permissions/tests/test_decorators.py +333 -0
  62. django_spire/auth/permissions/tests/test_permissions.py +337 -0
  63. django_spire/auth/permissions/tests/test_tools.py +305 -0
  64. django_spire/auth/permissions/tools.py +21 -15
  65. django_spire/auth/seeding/seed.py +3 -0
  66. django_spire/auth/seeding/seeder.py +2 -0
  67. django_spire/auth/tests/test_controller.py +323 -0
  68. django_spire/auth/tests/test_url_endpoints.py +9 -9
  69. django_spire/auth/tests/test_views.py +406 -0
  70. django_spire/auth/urls/admin_urls.py +2 -0
  71. django_spire/auth/urls/redirect_urls.py +2 -0
  72. django_spire/auth/user/apps.py +2 -0
  73. django_spire/auth/user/forms.py +9 -0
  74. django_spire/auth/user/models.py +1 -1
  75. django_spire/auth/user/services/services.py +1 -0
  76. django_spire/auth/user/tests/factories.py +14 -13
  77. django_spire/auth/user/tests/test_factories.py +166 -2
  78. django_spire/auth/user/tests/test_forms.py +573 -0
  79. django_spire/auth/user/tests/test_models.py +257 -0
  80. django_spire/auth/user/tests/test_services.py +200 -0
  81. django_spire/auth/user/tests/test_tools.py +153 -0
  82. django_spire/auth/user/tests/test_user_factories.py +139 -0
  83. django_spire/auth/user/tests/test_views.py +363 -0
  84. django_spire/auth/user/tools.py +7 -1
  85. django_spire/auth/user/urls/form_urls.py +3 -0
  86. django_spire/auth/user/urls/page_urls.py +3 -0
  87. django_spire/auth/user/views/form_views.py +19 -10
  88. django_spire/auth/user/views/page_views.py +8 -2
  89. django_spire/auth/views/redirect_views.py +14 -9
  90. django_spire/comment/admin.py +2 -0
  91. django_spire/comment/apps.py +2 -0
  92. django_spire/comment/templatetags/comment_tags.py +1 -0
  93. django_spire/comment/tests/test_forms.py +27 -0
  94. django_spire/comment/tests/test_models.py +215 -0
  95. django_spire/comment/tests/test_querysets.py +101 -0
  96. django_spire/comment/tests/test_utils.py +90 -0
  97. django_spire/comment/urls.py +2 -0
  98. django_spire/comment/utils.py +22 -13
  99. django_spire/comment/views.py +1 -1
  100. django_spire/conf.py +8 -6
  101. django_spire/consts.py +1 -1
  102. django_spire/contrib/breadcrumb/apps.py +2 -0
  103. django_spire/contrib/breadcrumb/breadcrumbs.py +18 -18
  104. django_spire/contrib/breadcrumb/tests/test_breadcrumbs.py +198 -0
  105. django_spire/contrib/constructor/__init__.py +3 -3
  106. django_spire/contrib/constructor/constructor.py +15 -15
  107. django_spire/contrib/constructor/django_model_constructor.py +5 -4
  108. django_spire/contrib/constructor/exceptions.py +5 -3
  109. django_spire/contrib/constructor/tests/__init__.py +0 -0
  110. django_spire/contrib/constructor/tests/test_constructor.py +193 -0
  111. django_spire/contrib/form/tests/__init__.py +0 -0
  112. django_spire/contrib/form/tests/test_forms.py +203 -0
  113. django_spire/contrib/generic_views/modal_views.py +2 -1
  114. django_spire/contrib/generic_views/portal_views.py +20 -19
  115. django_spire/contrib/generic_views/tests/__init__.py +0 -0
  116. django_spire/contrib/generic_views/tests/test_views.py +459 -0
  117. django_spire/contrib/help/apps.py +2 -0
  118. django_spire/contrib/help/templatetags/help.py +1 -0
  119. django_spire/contrib/help/tests/__init__.py +0 -0
  120. django_spire/contrib/help/tests/test_templatetags.py +100 -0
  121. django_spire/contrib/options/mixins.py +6 -5
  122. django_spire/contrib/options/tests/factories.py +5 -1
  123. django_spire/contrib/options/tests/test_options.py +234 -0
  124. django_spire/contrib/ordering/exceptions.py +7 -3
  125. django_spire/contrib/ordering/mixins.py +2 -0
  126. django_spire/contrib/ordering/querysets.py +3 -1
  127. django_spire/contrib/ordering/services/processor_service.py +8 -4
  128. django_spire/contrib/ordering/services/service.py +1 -2
  129. django_spire/contrib/ordering/tests/__init__.py +0 -0
  130. django_spire/contrib/ordering/tests/test_ordering.py +165 -0
  131. django_spire/contrib/ordering/validators.py +6 -6
  132. django_spire/contrib/pagination/templatetags/pagination_tags.py +12 -5
  133. django_spire/contrib/pagination/tests/__init__.py +0 -0
  134. django_spire/contrib/pagination/tests/test_pagination.py +179 -0
  135. django_spire/contrib/performance/decorators.py +16 -6
  136. django_spire/contrib/performance/tests/__init__.py +0 -0
  137. django_spire/contrib/performance/tests/test_performance.py +107 -0
  138. django_spire/contrib/progress/__init__.py +1 -3
  139. django_spire/contrib/progress/static/django_spire/js/contrib/progress/progress.js +38 -82
  140. django_spire/contrib/queryset/enums.py +3 -1
  141. django_spire/contrib/queryset/filter_tools.py +10 -5
  142. django_spire/contrib/queryset/mixins.py +16 -16
  143. django_spire/contrib/queryset/tests/__init__.py +0 -0
  144. django_spire/contrib/queryset/tests/test_queryset.py +137 -0
  145. django_spire/contrib/seeding/field/base.py +13 -7
  146. django_spire/contrib/seeding/field/callable.py +8 -1
  147. django_spire/contrib/seeding/field/cleaners.py +5 -5
  148. django_spire/contrib/seeding/field/custom.py +20 -10
  149. django_spire/contrib/seeding/field/django/seeder.py +8 -6
  150. django_spire/contrib/seeding/field/enums.py +7 -5
  151. django_spire/contrib/seeding/field/override.py +16 -6
  152. django_spire/contrib/seeding/field/static.py +9 -2
  153. django_spire/contrib/seeding/field/tests/test_base.py +18 -14
  154. django_spire/contrib/seeding/field/tests/test_callable.py +13 -9
  155. django_spire/contrib/seeding/field/tests/test_cleaners.py +51 -38
  156. django_spire/contrib/seeding/field/tests/test_static.py +13 -9
  157. django_spire/contrib/seeding/intelligence/bots/seeder_generator_bot.py +2 -0
  158. django_spire/contrib/seeding/intelligence/intel.py +5 -1
  159. django_spire/contrib/seeding/intelligence/prompts/factory.py +6 -1
  160. django_spire/contrib/seeding/intelligence/prompts/foreign_key_selection_prompt.py +6 -1
  161. django_spire/contrib/seeding/intelligence/prompts/generate_django_model_seeder_prompts.py +2 -0
  162. django_spire/contrib/seeding/intelligence/prompts/generic_relationship_selection_prompt.py +7 -1
  163. django_spire/contrib/seeding/intelligence/prompts/hierarchical_selection_prompt.py +6 -2
  164. django_spire/contrib/seeding/intelligence/prompts/model_field_choices_prompt.py +8 -2
  165. django_spire/contrib/seeding/intelligence/prompts/negation_prompt.py +2 -0
  166. django_spire/contrib/seeding/intelligence/prompts/objective_prompt.py +6 -1
  167. django_spire/contrib/seeding/management/commands/seeding.py +9 -3
  168. django_spire/contrib/seeding/management/example.py +2 -0
  169. django_spire/contrib/seeding/model/base.py +16 -7
  170. django_spire/contrib/seeding/model/config.py +31 -15
  171. django_spire/contrib/seeding/model/django/config.py +13 -13
  172. django_spire/contrib/seeding/model/django/seeder.py +4 -4
  173. django_spire/contrib/seeding/model/django/tests/test_seeder.py +34 -23
  174. django_spire/contrib/seeding/model/enums.py +2 -0
  175. django_spire/contrib/seeding/tests/test_config.py +71 -0
  176. django_spire/contrib/seeding/tests/test_custom.py +35 -0
  177. django_spire/contrib/seeding/tests/test_enums.py +40 -0
  178. django_spire/contrib/seeding/tests/test_intel.py +32 -0
  179. django_spire/contrib/seeding/tests/test_override.py +63 -0
  180. django_spire/contrib/service/__init__.py +2 -2
  181. django_spire/contrib/service/django_model_service.py +16 -15
  182. django_spire/contrib/service/exceptions.py +5 -3
  183. django_spire/contrib/service/tests/__init__.py +0 -0
  184. django_spire/contrib/service/tests/test_service.py +153 -0
  185. django_spire/contrib/session/apps.py +2 -0
  186. django_spire/contrib/session/controller.py +48 -42
  187. django_spire/contrib/session/templatetags/session_tags.py +11 -2
  188. django_spire/contrib/session/tests/test_session_controller.py +117 -53
  189. django_spire/contrib/tests/__init__.py +0 -0
  190. django_spire/contrib/tests/test_utils.py +37 -0
  191. django_spire/contrib/utils.py +4 -1
  192. django_spire/core/apps.py +2 -0
  193. django_spire/core/converters/tests/test_to_data.py +353 -0
  194. django_spire/core/converters/tests/test_to_enums.py +61 -41
  195. django_spire/core/converters/tests/test_to_pydantic.py +138 -109
  196. django_spire/core/converters/to_data.py +29 -10
  197. django_spire/core/converters/to_enums.py +4 -2
  198. django_spire/core/converters/to_pydantic.py +22 -22
  199. django_spire/core/decorators.py +19 -6
  200. django_spire/core/forms/widgets.py +4 -0
  201. django_spire/core/maps.py +3 -1
  202. django_spire/core/middleware/maintenance.py +3 -3
  203. django_spire/core/middleware.py +8 -6
  204. django_spire/core/redirect/__init__.py +5 -0
  205. django_spire/core/redirect/generic_redirect.py +1 -2
  206. django_spire/core/redirect/tests/__init__.py +0 -0
  207. django_spire/core/redirect/tests/test_generic_redirect.py +34 -0
  208. django_spire/core/{tests/tests_redirect.py → redirect/tests/test_safe_redirect.py} +55 -81
  209. django_spire/core/shortcuts.py +3 -3
  210. django_spire/core/static/django_spire/css/app-layout.css +1 -1
  211. django_spire/core/static/django_spire/css/app-navigation.css +3 -3
  212. django_spire/core/static/django_spire/css/bootstrap-override.css +4 -0
  213. django_spire/core/tag/admin.py +12 -0
  214. django_spire/core/tag/intelligence/tag_set_bot.py +2 -0
  215. django_spire/core/tag/mixins.py +2 -0
  216. django_spire/core/tag/models.py +2 -0
  217. django_spire/core/tag/querysets.py +2 -0
  218. django_spire/core/tag/service/tag_service.py +6 -3
  219. django_spire/core/tag/tests/test_intelligence.py +9 -9
  220. django_spire/core/tag/tests/test_tags.py +44 -54
  221. django_spire/core/tag/tests/test_tools.py +191 -0
  222. django_spire/core/tag/tools.py +3 -0
  223. django_spire/core/templates/django_spire/card/card.html +5 -2
  224. django_spire/core/templates/django_spire/card/title_card.html +9 -4
  225. django_spire/core/templates/django_spire/infinite_scroll/base.html +1 -0
  226. django_spire/core/templates/django_spire/navigation/side_navigation.html +19 -24
  227. django_spire/core/templates/django_spire/page/full_page.html +46 -16
  228. django_spire/core/templates/django_spire/table/base.html +4 -2
  229. django_spire/core/templatetags/json.py +6 -2
  230. django_spire/core/templatetags/message.py +13 -8
  231. django_spire/core/templatetags/string_formating.py +8 -5
  232. django_spire/core/templatetags/tests/__init__.py +0 -0
  233. django_spire/core/templatetags/tests/test_templatetags.py +427 -0
  234. django_spire/core/templatetags/variable_types.py +17 -9
  235. django_spire/core/tests/test_cases.py +1 -1
  236. django_spire/core/tests/test_conf.py +43 -0
  237. django_spire/core/tests/test_consts.py +28 -0
  238. django_spire/core/tests/test_context_processors.py +93 -0
  239. django_spire/core/tests/test_decorators.py +95 -0
  240. django_spire/core/tests/test_django_spire_utils.py +56 -0
  241. django_spire/core/tests/test_exceptions.py +37 -0
  242. django_spire/core/tests/test_models.py +54 -0
  243. django_spire/core/tests/test_settings.py +45 -0
  244. django_spire/core/tests/test_shortcuts.py +74 -0
  245. django_spire/core/tests/test_urls.py +16 -0
  246. django_spire/core/tests/test_utils.py +58 -0
  247. django_spire/core/urls.py +4 -1
  248. django_spire/core/utils.py +12 -8
  249. django_spire/exceptions.py +16 -1
  250. django_spire/file/admin.py +4 -2
  251. django_spire/file/apps.py +8 -10
  252. django_spire/file/fields.py +7 -7
  253. django_spire/file/forms.py +1 -1
  254. django_spire/file/interfaces.py +15 -15
  255. django_spire/file/mixins.py +1 -4
  256. django_spire/file/models.py +3 -5
  257. django_spire/file/tests/factories.py +59 -0
  258. django_spire/file/tests/test_admin.py +69 -0
  259. django_spire/file/tests/test_apps.py +24 -0
  260. django_spire/file/tests/test_fields.py +114 -0
  261. django_spire/file/tests/test_forms.py +20 -0
  262. django_spire/file/tests/test_interfaces.py +183 -0
  263. django_spire/file/tests/test_models.py +82 -0
  264. django_spire/file/tests/test_querysets.py +102 -0
  265. django_spire/file/tests/test_utils.py +32 -0
  266. django_spire/file/tests/test_views.py +145 -0
  267. django_spire/file/tests/test_widgets.py +82 -0
  268. django_spire/file/tools.py +8 -2
  269. django_spire/file/views.py +7 -3
  270. django_spire/file/widgets.py +12 -12
  271. django_spire/help_desk/admin.py +15 -0
  272. django_spire/help_desk/apps.py +2 -0
  273. django_spire/help_desk/auth/controller.py +2 -0
  274. django_spire/help_desk/choices.py +2 -0
  275. django_spire/help_desk/enums.py +2 -0
  276. django_spire/help_desk/exceptions.py +31 -3
  277. django_spire/help_desk/forms.py +2 -0
  278. django_spire/help_desk/models.py +2 -0
  279. django_spire/help_desk/querysets.py +4 -1
  280. django_spire/help_desk/services/notification_service.py +26 -27
  281. django_spire/help_desk/services/service.py +2 -3
  282. django_spire/help_desk/tests/factories.py +8 -3
  283. django_spire/help_desk/tests/test_admin.py +41 -0
  284. django_spire/help_desk/tests/test_apps.py +41 -0
  285. django_spire/help_desk/tests/test_choices.py +50 -0
  286. django_spire/help_desk/tests/test_controller.py +87 -0
  287. django_spire/help_desk/tests/test_enums.py +18 -0
  288. django_spire/help_desk/tests/test_exceptions.py +37 -0
  289. django_spire/help_desk/tests/test_forms.py +89 -0
  290. django_spire/help_desk/tests/test_models.py +59 -0
  291. django_spire/help_desk/tests/test_querysets.py +38 -0
  292. django_spire/help_desk/tests/test_services/test_notification_service.py +15 -8
  293. django_spire/help_desk/tests/test_services/test_service.py +92 -0
  294. django_spire/help_desk/tests/test_urls/test_form_urls.py +6 -6
  295. django_spire/help_desk/tests/test_urls/test_page_urls.py +8 -9
  296. django_spire/help_desk/tests/test_views/test_form_views.py +46 -19
  297. django_spire/help_desk/tests/test_views/test_page_views.py +32 -9
  298. django_spire/help_desk/urls/__init__.py +4 -1
  299. django_spire/help_desk/urls/form_urls.py +3 -0
  300. django_spire/help_desk/urls/page_urls.py +3 -0
  301. django_spire/help_desk/views/form_views.py +13 -5
  302. django_spire/help_desk/views/page_views.py +11 -3
  303. django_spire/history/activity/admin.py +2 -0
  304. django_spire/history/activity/apps.py +3 -1
  305. django_spire/history/activity/mixins.py +13 -7
  306. django_spire/history/activity/models.py +6 -5
  307. django_spire/history/activity/querysets.py +2 -0
  308. django_spire/history/activity/tests/__init__.py +0 -0
  309. django_spire/history/activity/tests/test_activity.py +176 -0
  310. django_spire/history/admin.py +9 -2
  311. django_spire/history/choices.py +3 -0
  312. django_spire/history/models.py +5 -5
  313. django_spire/history/tests/test_admin.py +93 -0
  314. django_spire/history/tests/test_history.py +101 -0
  315. django_spire/history/tests/test_mixins.py +84 -0
  316. django_spire/history/viewed/admin.py +3 -1
  317. django_spire/history/viewed/apps.py +3 -1
  318. django_spire/history/viewed/models.py +2 -0
  319. django_spire/history/viewed/tests/__init__.py +0 -0
  320. django_spire/history/viewed/tests/test_viewed.py +46 -0
  321. django_spire/knowledge/auth/tests/__init__.py +0 -0
  322. django_spire/knowledge/auth/tests/test_controller.py +116 -0
  323. django_spire/knowledge/collection/admin.py +5 -1
  324. django_spire/knowledge/collection/models.py +3 -1
  325. django_spire/knowledge/collection/seeding/seed.py +1 -0
  326. django_spire/knowledge/collection/services/factory_service.py +10 -11
  327. django_spire/knowledge/collection/services/ordering_service.py +1 -2
  328. django_spire/knowledge/collection/services/service.py +5 -10
  329. django_spire/knowledge/collection/services/tag_service.py +5 -2
  330. django_spire/knowledge/collection/tests/factories.py +28 -1
  331. django_spire/knowledge/collection/tests/test_models.py +48 -0
  332. django_spire/knowledge/collection/tests/test_querysets.py +93 -0
  333. django_spire/knowledge/collection/tests/test_services/test_factory_service.py +100 -0
  334. django_spire/knowledge/collection/tests/test_services/test_services.py +160 -0
  335. django_spire/knowledge/collection/tests/test_urls/test_form_urls.py +21 -3
  336. django_spire/knowledge/collection/tests/test_urls/test_json_urls.py +39 -1
  337. django_spire/knowledge/collection/tests/test_urls/test_page_urls.py +12 -4
  338. django_spire/knowledge/collection/urls/__init__.py +3 -0
  339. django_spire/knowledge/collection/urls/form_urls.py +2 -0
  340. django_spire/knowledge/collection/urls/json_urls.py +2 -0
  341. django_spire/knowledge/collection/urls/page_urls.py +2 -0
  342. django_spire/knowledge/collection/views/form_views.py +4 -4
  343. django_spire/knowledge/collection/views/json_views.py +5 -1
  344. django_spire/knowledge/collection/views/page_views.py +5 -2
  345. django_spire/knowledge/entry/admin.py +7 -1
  346. django_spire/knowledge/entry/forms.py +2 -0
  347. django_spire/knowledge/entry/models.py +2 -0
  348. django_spire/knowledge/entry/seeding/seed.py +3 -0
  349. django_spire/knowledge/entry/services/automation_service.py +5 -4
  350. django_spire/knowledge/entry/services/factory_service.py +7 -5
  351. django_spire/knowledge/entry/services/service.py +4 -7
  352. django_spire/knowledge/entry/services/tag_service.py +0 -1
  353. django_spire/knowledge/entry/services/tool_service.py +1 -0
  354. django_spire/knowledge/entry/services/transformation_services.py +1 -5
  355. django_spire/knowledge/entry/tests/factories.py +1 -2
  356. django_spire/knowledge/entry/tests/test_factory_service.py +20 -0
  357. django_spire/knowledge/entry/tests/test_models.py +41 -0
  358. django_spire/knowledge/entry/tests/test_querysets.py +71 -0
  359. django_spire/knowledge/entry/tests/test_services.py +94 -0
  360. django_spire/knowledge/entry/tests/test_urls/test_form_urls.py +9 -14
  361. django_spire/knowledge/entry/tests/test_urls/test_json_urls.py +48 -5
  362. django_spire/knowledge/entry/tests/test_urls/test_page_urls.py +6 -8
  363. django_spire/knowledge/entry/tests/test_urls/test_template_urls.py +40 -0
  364. django_spire/knowledge/entry/urls/form_urls.py +2 -0
  365. django_spire/knowledge/entry/urls/json_urls.py +2 -0
  366. django_spire/knowledge/entry/urls/page_urls.py +2 -0
  367. django_spire/knowledge/entry/urls/template_urls.py +2 -0
  368. django_spire/knowledge/entry/version/block/choices.py +2 -0
  369. django_spire/knowledge/entry/version/block/data/data.py +1 -0
  370. django_spire/knowledge/entry/version/block/data/list/data.py +8 -13
  371. django_spire/knowledge/entry/version/block/data/list/maps.py +3 -0
  372. django_spire/knowledge/entry/version/block/data/list/meta.py +1 -2
  373. django_spire/knowledge/entry/version/block/data/list/tests/__init__.py +0 -0
  374. django_spire/knowledge/entry/version/block/data/list/tests/test_maps.py +32 -0
  375. django_spire/knowledge/entry/version/block/data/list/tests/test_meta.py +58 -0
  376. django_spire/knowledge/entry/version/block/data/maps.py +3 -6
  377. django_spire/knowledge/entry/version/block/models.py +7 -5
  378. django_spire/knowledge/entry/version/block/seeding/constants.py +5 -4
  379. django_spire/knowledge/entry/version/block/services/service.py +2 -3
  380. django_spire/knowledge/entry/version/block/tests/factories.py +4 -10
  381. django_spire/knowledge/entry/version/block/tests/test_choices.py +56 -0
  382. django_spire/knowledge/entry/version/block/tests/test_data.py +90 -0
  383. django_spire/knowledge/entry/version/block/tests/test_maps.py +37 -0
  384. django_spire/knowledge/entry/version/block/tests/test_models.py +55 -0
  385. django_spire/knowledge/entry/version/block/tests/test_querysets.py +35 -0
  386. django_spire/knowledge/entry/version/block/tests/test_services.py +65 -0
  387. django_spire/knowledge/entry/version/choices.py +2 -0
  388. django_spire/knowledge/entry/version/converters/converter.py +1 -1
  389. django_spire/knowledge/entry/version/converters/docx_converter.py +4 -7
  390. django_spire/knowledge/entry/version/converters/markdown_converter.py +20 -20
  391. django_spire/knowledge/entry/version/maps.py +4 -5
  392. django_spire/knowledge/entry/version/querysets.py +1 -1
  393. django_spire/knowledge/entry/version/seeding/seeder.py +1 -2
  394. django_spire/knowledge/entry/version/services/processor_service.py +5 -4
  395. django_spire/knowledge/entry/version/services/service.py +1 -2
  396. django_spire/knowledge/entry/version/tests/factories.py +2 -2
  397. django_spire/knowledge/entry/version/tests/test_choices.py +18 -0
  398. django_spire/knowledge/entry/version/tests/test_converters/test_docx_converter.py +56 -8
  399. django_spire/knowledge/entry/version/tests/test_converters/test_markdown_converter.py +78 -0
  400. django_spire/knowledge/entry/version/tests/test_maps.py +58 -0
  401. django_spire/knowledge/entry/version/tests/test_models.py +23 -0
  402. django_spire/knowledge/entry/version/tests/test_querysets.py +26 -0
  403. django_spire/knowledge/entry/version/tests/test_services.py +62 -0
  404. django_spire/knowledge/entry/version/tests/test_urls/test_json_urls.py +27 -8
  405. django_spire/knowledge/entry/version/tests/test_urls/test_page_urls.py +15 -8
  406. django_spire/knowledge/entry/version/tests/test_urls/test_redirect_urls.py +38 -0
  407. django_spire/knowledge/entry/version/urls/__init__.py +3 -0
  408. django_spire/knowledge/entry/version/urls/json_urls.py +2 -1
  409. django_spire/knowledge/entry/version/urls/page_urls.py +2 -0
  410. django_spire/knowledge/entry/version/urls/redirect_urls.py +2 -0
  411. django_spire/knowledge/entry/version/views/json_views.py +5 -1
  412. django_spire/knowledge/entry/version/views/page_views.py +10 -3
  413. django_spire/knowledge/entry/version/views/redirect_views.py +5 -1
  414. django_spire/knowledge/entry/views/form_views.py +16 -8
  415. django_spire/knowledge/entry/views/json_views.py +3 -1
  416. django_spire/knowledge/entry/views/page_views.py +8 -2
  417. django_spire/knowledge/entry/views/template_views.py +7 -1
  418. django_spire/knowledge/exceptions.py +2 -1
  419. django_spire/knowledge/intelligence/intel/answer_intel.py +2 -1
  420. django_spire/knowledge/intelligence/intel/entry_intel.py +0 -1
  421. django_spire/knowledge/intelligence/workflows/knowledge_workflow.py +4 -5
  422. django_spire/knowledge/models.py +1 -2
  423. django_spire/knowledge/tests/__init__.py +0 -0
  424. django_spire/knowledge/tests/test_templatetags.py +40 -0
  425. django_spire/knowledge/tests/test_urls/__init__.py +0 -0
  426. django_spire/knowledge/tests/test_urls/test_page_urls.py +24 -0
  427. django_spire/knowledge/urls/__init__.py +2 -0
  428. django_spire/knowledge/urls/page_urls.py +2 -0
  429. django_spire/knowledge/views/page_views.py +8 -3
  430. django_spire/notification/admin.py +3 -1
  431. django_spire/notification/app/admin.py +2 -0
  432. django_spire/notification/app/apps.py +3 -1
  433. django_spire/notification/app/exceptions.py +9 -2
  434. django_spire/notification/app/models.py +8 -4
  435. django_spire/notification/app/processor.py +22 -26
  436. django_spire/notification/app/querysets.py +2 -0
  437. django_spire/notification/app/tests/__init__.py +0 -0
  438. django_spire/notification/app/tests/factories.py +34 -0
  439. django_spire/notification/app/tests/test_apps.py +24 -0
  440. django_spire/notification/app/tests/test_models.py +72 -0
  441. django_spire/notification/app/tests/test_processor.py +111 -0
  442. django_spire/notification/app/tests/test_querysets.py +90 -0
  443. django_spire/notification/app/tests/test_views/__init__.py +0 -0
  444. django_spire/notification/app/tests/test_views/test_json_views.py +48 -0
  445. django_spire/notification/app/tests/test_views/test_page_views.py +19 -0
  446. django_spire/notification/app/urls/__init__.py +3 -1
  447. django_spire/notification/app/urls/json_urls.py +6 -4
  448. django_spire/notification/app/urls/page_urls.py +4 -3
  449. django_spire/notification/app/urls/template_urls.py +4 -2
  450. django_spire/notification/apps.py +4 -1
  451. django_spire/notification/email/admin.py +5 -1
  452. django_spire/notification/email/apps.py +3 -1
  453. django_spire/notification/email/exceptions.py +4 -2
  454. django_spire/notification/email/helper.py +5 -3
  455. django_spire/notification/email/models.py +4 -0
  456. django_spire/notification/email/processor.py +19 -15
  457. django_spire/notification/email/querysets.py +3 -0
  458. django_spire/notification/email/tests/__init__.py +0 -0
  459. django_spire/notification/email/tests/factories.py +35 -0
  460. django_spire/notification/email/tests/test_apps.py +24 -0
  461. django_spire/notification/email/tests/test_models.py +52 -0
  462. django_spire/notification/email/tests/test_processor.py +92 -0
  463. django_spire/notification/email/tests/test_querysets.py +43 -0
  464. django_spire/notification/exceptions.py +17 -2
  465. django_spire/notification/managers.py +7 -1
  466. django_spire/notification/maps.py +4 -1
  467. django_spire/notification/mixins.py +2 -0
  468. django_spire/notification/models.py +3 -1
  469. django_spire/notification/processors/notification.py +12 -5
  470. django_spire/notification/processors/processor.py +2 -0
  471. django_spire/notification/processors/tests/__init__.py +0 -0
  472. django_spire/notification/processors/tests/test_notification.py +106 -0
  473. django_spire/notification/push/admin.py +10 -1
  474. django_spire/notification/push/apps.py +3 -1
  475. django_spire/notification/push/models.py +2 -3
  476. django_spire/notification/push/tests/__init__.py +0 -0
  477. django_spire/notification/push/tests/test_apps.py +24 -0
  478. django_spire/notification/push/tests/test_models.py +28 -0
  479. django_spire/notification/querysets.py +7 -1
  480. django_spire/notification/sms/admin.py +2 -0
  481. django_spire/notification/sms/apps.py +4 -1
  482. django_spire/notification/sms/automations.py +2 -0
  483. django_spire/notification/sms/choices.py +2 -0
  484. django_spire/notification/sms/exceptions.py +19 -5
  485. django_spire/notification/sms/helper.py +33 -23
  486. django_spire/notification/sms/models.py +5 -1
  487. django_spire/notification/sms/processor.py +20 -20
  488. django_spire/notification/sms/querysets.py +2 -0
  489. django_spire/notification/sms/tests/factories.py +33 -0
  490. django_spire/notification/sms/tests/test_apps.py +24 -0
  491. django_spire/notification/sms/tests/test_automation.py +38 -0
  492. django_spire/notification/sms/tests/test_choices.py +15 -0
  493. django_spire/notification/sms/tests/test_consts.py +17 -0
  494. django_spire/notification/sms/tests/test_exceptions.py +27 -0
  495. django_spire/notification/sms/tests/test_helper.py +50 -0
  496. django_spire/notification/sms/tests/test_models.py +81 -0
  497. django_spire/notification/sms/tests/test_processor.py +107 -0
  498. django_spire/notification/sms/tests/test_tools.py +25 -11
  499. django_spire/notification/sms/tools.py +16 -5
  500. django_spire/notification/sms/urls/__init__.py +3 -1
  501. django_spire/notification/sms/urls/media_urls.py +2 -0
  502. django_spire/notification/sms/views/media_views.py +14 -4
  503. django_spire/notification/tests/__init__.py +0 -0
  504. django_spire/notification/tests/factories.py +26 -0
  505. django_spire/notification/tests/test_admin.py +55 -0
  506. django_spire/notification/tests/test_apps.py +30 -0
  507. django_spire/notification/tests/test_automation.py +18 -0
  508. django_spire/notification/tests/test_choices.py +59 -0
  509. django_spire/notification/tests/test_exceptions.py +58 -0
  510. django_spire/notification/tests/test_managers.py +100 -0
  511. django_spire/notification/tests/test_maps.py +31 -0
  512. django_spire/notification/tests/test_models.py +76 -0
  513. django_spire/notification/tests/test_querysets.py +184 -0
  514. django_spire/notification/tests/test_utils.py +23 -0
  515. django_spire/notification/urls.py +3 -1
  516. django_spire/notification/utils.py +3 -1
  517. django_spire/settings.py +3 -0
  518. django_spire/theme/tests/test_context_processor.py +15 -13
  519. django_spire/theme/tests/test_enums.py +2 -2
  520. django_spire/theme/tests/test_filesystem.py +2 -5
  521. django_spire/theme/tests/test_integration.py +12 -12
  522. django_spire/theme/tests/test_model.py +40 -38
  523. django_spire/theme/tests/test_views/test_json_views.py +33 -33
  524. django_spire/theme/urls/json_urls.py +3 -0
  525. django_spire/theme/urls/page_urls.py +3 -0
  526. django_spire/urls.py +19 -15
  527. django_spire/utils.py +13 -4
  528. {django_spire-0.23.6.dist-info → django_spire-0.23.8.dist-info}/METADATA +1 -1
  529. {django_spire-0.23.6.dist-info → django_spire-0.23.8.dist-info}/RECORD +532 -361
  530. django_spire/contrib/options/tests/test_unit.py +0 -148
  531. django_spire/contrib/progress/views.py +0 -64
  532. django_spire/contrib/seeding/tests/test_seeding.py +0 -25
  533. django_spire/core/tests/test_templatetags.py +0 -117
  534. django_spire/core/tests/tests_shortcuts.py +0 -73
  535. django_spire/history/activity/tests.py +0 -3
  536. django_spire/history/activity/views.py +0 -3
  537. django_spire/knowledge/collection/tests/test_services/test_transformation_service.py +0 -71
  538. django_spire/notification/app/tests.py +0 -3
  539. {django_spire-0.23.6.dist-info → django_spire-0.23.8.dist-info}/WHEEL +0 -0
  540. {django_spire-0.23.6.dist-info → django_spire-0.23.8.dist-info}/licenses/LICENSE.md +0 -0
  541. {django_spire-0.23.6.dist-info → django_spire-0.23.8.dist-info}/top_level.txt +0 -0
@@ -1,12 +1,18 @@
1
- from django.core.handlers.wsgi import WSGIRequest
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
2
5
  from django.shortcuts import get_object_or_404
3
- from django.template.response import TemplateResponse
4
6
  from django.urls import reverse
5
7
 
6
8
  from django_spire.auth.controller.controller import AppAuthController
7
9
  from django_spire.contrib.generic_views import portal_views
8
10
  from django_spire.knowledge.entry.models import Entry
9
11
 
12
+ if TYPE_CHECKING:
13
+ from django.core.handlers.wsgi import WSGIRequest
14
+ from django.template.response import TemplateResponse
15
+
10
16
 
11
17
  @AppAuthController('knowledge').permission_required('can_delete')
12
18
  def delete_view(request: WSGIRequest, pk: int) -> TemplateResponse:
@@ -1,4 +1,7 @@
1
- from django.core.handlers.wsgi import WSGIRequest
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
2
5
  from django.template.response import TemplateResponse
3
6
  from django.urls import reverse
4
7
 
@@ -7,6 +10,9 @@ from django_spire.contrib import Breadcrumbs
7
10
  from django_spire.knowledge.collection.models import Collection
8
11
  from django_spire.knowledge.entry.models import Entry
9
12
 
13
+ if TYPE_CHECKING:
14
+ from django.core.handlers.wsgi import WSGIRequest
15
+
10
16
 
11
17
  @AppAuthController('knowledge').permission_required('can_view')
12
18
  def file_list_view(request: WSGIRequest, collection_pk: int = 0) -> TemplateResponse:
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- class KnowledgeBaseConversionException(Exception):
3
+
4
+ class KnowledgeBaseConversionError(Exception):
4
5
  pass
@@ -1,5 +1,6 @@
1
- from dandy import BaseIntel
1
+ from __future__ import annotations
2
2
 
3
+ from dandy import BaseIntel
3
4
 
4
5
 
5
6
  class AnswerIntel(BaseIntel):
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
-
4
3
  from dandy import BaseIntel, BaseListIntel
5
4
 
6
5
  from django_spire.knowledge.entry.models import Entry
@@ -15,15 +15,16 @@ if TYPE_CHECKING:
15
15
  from django.core.handlers.wsgi import WSGIRequest
16
16
  from dandy.llm.request.message import MessageHistory
17
17
 
18
+
18
19
  NO_KNOWLEDGE_MESSAGE_INTEL = DefaultMessageIntel(
19
20
  text='Sorry, I could not find any information on that.'
20
21
  )
21
22
 
22
23
 
23
24
  def knowledge_search_workflow(
24
- request: WSGIRequest,
25
- user_input: str,
26
- message_history: MessageHistory | None = None,
25
+ request: WSGIRequest,
26
+ user_input: str,
27
+ message_history: MessageHistory | None = None
27
28
  ) -> BaseMessageIntel | None:
28
29
  user_tag_set = TagSetBot().process(user_input)
29
30
 
@@ -76,8 +77,6 @@ def knowledge_search_workflow(
76
77
  entries=entries
77
78
  )
78
79
 
79
- print(entries_intel_future.result)
80
-
81
80
  return KnowledgeMessageIntel(
82
81
  answer_intel=answer_intel_future.result,
83
82
  entries_intel=entries_intel_future.result,
@@ -4,8 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  from django_spire.knowledge.entry import models as entry_models
6
6
  from django_spire.knowledge.entry.version import models as entry_version_models
7
- from django_spire.knowledge.entry.version.block import models as \
8
- entry_version_block_models
7
+ from django_spire.knowledge.entry.version.block import models as entry_version_block_models
9
8
  from django_spire.knowledge.collection import models as collection_models
10
9
 
11
10
  # Must assert to stop unused import removals.
File without changes
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ from django_spire.core.tests.test_cases import BaseTestCase
4
+ from django_spire.knowledge.templatetags.spire_knowledge_tags import format_to_html
5
+
6
+
7
+ class SpireKnowledgeTagsTests(BaseTestCase):
8
+ def test_format_to_html_empty_string(self):
9
+ result = format_to_html('')
10
+ assert result == ' '
11
+
12
+ def test_format_to_html_none(self):
13
+ result = format_to_html(None)
14
+ assert result == ' '
15
+
16
+ def test_format_to_html_line_breaks(self):
17
+ result = format_to_html('Line1\nLine2')
18
+ assert '<br>' in result
19
+
20
+ def test_format_to_html_bold(self):
21
+ result = format_to_html('**bold text**')
22
+ assert '<span class="fw-bold">bold text</span>' in result
23
+
24
+ def test_format_to_html_italic(self):
25
+ result = format_to_html('*italic text*')
26
+ assert '<span class="fst-italic">italic text</span>' in result
27
+
28
+ def test_format_to_html_strikethrough(self):
29
+ result = format_to_html('~~strikethrough~~')
30
+ assert '<span class="text-decoration-line-through">strikethrough</span>' in result
31
+
32
+ def test_format_to_html_mixed(self):
33
+ result = format_to_html('**bold** and *italic* and ~~strike~~')
34
+ assert '<span class="fw-bold">bold</span>' in result
35
+ assert '<span class="fst-italic">italic</span>' in result
36
+ assert '<span class="text-decoration-line-through">strike</span>' in result
37
+
38
+ def test_format_to_html_plain_text(self):
39
+ result = format_to_html('plain text')
40
+ assert result == 'plain text'
File without changes
@@ -0,0 +1,24 @@
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.knowledge.collection.tests.factories import create_test_collection
7
+
8
+
9
+ class KnowledgePageUrlsTests(BaseTestCase):
10
+ def setUp(self):
11
+ super().setUp()
12
+ self.collection = create_test_collection()
13
+
14
+ def test_home_view_url_path(self):
15
+ response = self.client.get(
16
+ reverse('django_spire:knowledge:page:home')
17
+ )
18
+ assert response.status_code == 200
19
+
20
+ def test_home_view_contains_collections(self):
21
+ response = self.client.get(
22
+ reverse('django_spire:knowledge:page:home')
23
+ )
24
+ assert 'collections' in response.context
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from django.urls import include, path
2
4
 
3
5
 
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from django.urls import path
2
4
 
3
5
  from django_spire.knowledge.views import page_views
@@ -1,16 +1,21 @@
1
1
  from __future__ import annotations
2
2
 
3
- from django.core.handlers.wsgi import WSGIRequest
4
- from django.template.response import TemplateResponse
3
+ from typing import TYPE_CHECKING
5
4
 
6
5
  from django_spire.auth.controller.controller import AppAuthController
7
6
  from django_spire.contrib.generic_views import portal_views
8
7
  from django_spire.knowledge.collection.models import Collection
9
8
 
9
+ if TYPE_CHECKING:
10
+ from django.template.response import TemplateResponse
11
+ from django.core.handlers.wsgi import WSGIRequest
12
+
13
+ from django_spire.contrib.breadcrumb import Breadcrumbs
14
+
10
15
 
11
16
  @AppAuthController('knowledge').permission_required('can_view')
12
17
  def home_view(request: WSGIRequest) -> TemplateResponse:
13
- def breadcrumbs_func(breadcrumbs):
18
+ def breadcrumbs_func(breadcrumbs: Breadcrumbs):
14
19
  breadcrumbs.add_breadcrumb(name='Knowledge')
15
20
 
16
21
  return portal_views.list_view(
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from django.contrib import admin
2
4
  from django.utils.html import format_html
3
5
 
@@ -25,7 +27,7 @@ class NotificationAdmin(admin.ModelAdmin):
25
27
 
26
28
  def url_link(self, notification: models.Notification) -> str:
27
29
  if notification.url:
28
- return format_html(f'<a href="{notification.url}" target="_blank">Link</a>')
30
+ return format_html('<a href="{}" target="_blank">Link</a>', notification.url)
29
31
 
30
32
  return 'No URL'
31
33
 
@@ -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.app.models import AppNotification
@@ -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 NotificationAppConfig(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,12 @@
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 AppNotificationException(DjangoSpireException):
5
+
6
+ class AppNotificationError(DjangoSpireError):
5
7
  pass
8
+
9
+
10
+ class MissingUserError(AppNotificationError):
11
+ def __init__(self) -> None:
12
+ super().__init__('AppNotifications must have a user associated with them')
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import json
2
4
 
3
5
  from django.db import models
@@ -36,12 +38,14 @@ class AppNotification(ViewedModelMixin, HistoryModelMixin):
36
38
 
37
39
  if days > 0:
38
40
  return f"{int(days)} day{'s' if days != 1 else ''} ago"
39
- elif hours > 0:
41
+
42
+ if hours > 0:
40
43
  return f"{int(hours)} hour{'s' if hours != 1 else ''} ago"
41
- elif minutes > 0:
44
+
45
+ if minutes > 0:
42
46
  return f"{int(minutes)} minute{'s' if minutes != 1 else ''} ago"
43
- else:
44
- return "just now"
47
+
48
+ return 'just now'
45
49
 
46
50
  def as_dict(self) -> dict:
47
51
  return {
@@ -1,32 +1,36 @@
1
+ from __future__ import annotations
2
+
1
3
  from django.utils.timezone import now
2
4
 
3
- from django_spire.notification.app.exceptions import AppNotificationException
4
- from django_spire.notification.choices import NotificationTypeChoices, \
5
+ from django_spire.notification.app.exceptions import MissingUserError
6
+ from django_spire.notification.choices import (
7
+ NotificationTypeChoices,
5
8
  NotificationStatusChoices
6
- from django_spire.notification.exceptions import NotificationException
9
+ )
10
+ from django_spire.notification.exceptions import InvalidNotificationTypeError
7
11
  from django_spire.notification.models import Notification
8
12
  from django_spire.notification.processors.processor import BaseNotificationProcessor
9
13
 
10
14
 
11
15
  class AppNotificationProcessor(BaseNotificationProcessor):
12
- def process(self, notification: Notification):
13
- try:
14
- if notification.type != NotificationTypeChoices.APP:
15
- raise NotificationException(
16
- f'AppNotificationProcessor only processes '
17
- f'App notifications. Was provided {notification.type}'
18
- )
16
+ def _validate_notification_type(self, notification: Notification):
17
+ if notification.type != NotificationTypeChoices.APP:
18
+ raise InvalidNotificationTypeError(NotificationTypeChoices.APP, notification.type)
19
19
 
20
- if notification.user_id is None:
21
- raise AppNotificationException(
22
- 'AppNotifications must have a user associated with them'
23
- )
20
+ def _validate_user(self, notification: Notification):
21
+ if notification.user_id is None:
22
+ raise MissingUserError
24
23
 
24
+ def process(self, notification: Notification):
25
+ try:
26
+ self._validate_notification_type(notification)
27
+ self._validate_user(notification)
25
28
  except Exception as e:
26
29
  notification.status = NotificationStatusChoices.ERRORED
27
30
  notification.status_message = str(e)
28
31
  notification.save()
29
- raise e
32
+
33
+ raise
30
34
 
31
35
  notification.status = NotificationStatusChoices.SENT
32
36
  notification.sent_datetime = now()
@@ -38,16 +42,8 @@ class AppNotificationProcessor(BaseNotificationProcessor):
38
42
 
39
43
  for notification in notifications:
40
44
  try:
41
- if notification.type != NotificationTypeChoices.APP:
42
- raise NotificationException(
43
- f'AppNotificationProcessor only processes '
44
- f'App notifications. Was provided {notification.type}'
45
- )
46
-
47
- if notification.user_id is None:
48
- raise AppNotificationException(
49
- 'AppNotifications must have a user associated with them'
50
- )
45
+ self._validate_notification_type(notification)
46
+ self._validate_user(notification)
51
47
 
52
48
  notification.status = NotificationStatusChoices.SENT
53
49
  notification.sent_datetime = now()
@@ -60,7 +56,7 @@ class AppNotificationProcessor(BaseNotificationProcessor):
60
56
  notifications,
61
57
  ['status', 'sent_datetime', 'status_message']
62
58
  )
63
- raise e
59
+ raise
64
60
 
65
61
  Notification.objects.bulk_update(
66
62
  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, IntegerField
4
6
 
5
7
  from django_spire.history.querysets import HistoryQuerySet
File without changes
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ from django.contrib.auth.models import User
4
+ from django.utils.timezone import now
5
+
6
+ from django_spire.notification.app.models import AppNotification
7
+ from django_spire.notification.choices import (
8
+ NotificationStatusChoices,
9
+ NotificationTypeChoices,
10
+ )
11
+ from django_spire.notification.models import Notification
12
+
13
+
14
+ def create_test_app_notification(**kwargs) -> AppNotification:
15
+ if 'notification' not in kwargs:
16
+ user = kwargs.pop('user', None) or User.objects.first()
17
+ notification = Notification.objects.create(
18
+ user=user,
19
+ type=NotificationTypeChoices.APP,
20
+ title=kwargs.pop('title', 'Test App Notification'),
21
+ body=kwargs.pop('body', 'This is a test app notification.'),
22
+ url=kwargs.pop('url', 'https://example.com'),
23
+ status=kwargs.pop('status', NotificationStatusChoices.SENT),
24
+ priority=kwargs.pop('priority', 'low'),
25
+ sent_datetime=kwargs.pop('sent_datetime', now()),
26
+ )
27
+ kwargs['notification'] = notification
28
+
29
+ data = {
30
+ 'template': 'django_spire/notification/app/item/notification_item.html',
31
+ 'context_data': {},
32
+ }
33
+ data.update(kwargs)
34
+ return AppNotification.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.app.apps import NotificationAppConfig
7
+
8
+
9
+ class NotificationAppConfigTests(BaseTestCase):
10
+ def test_app_name(self):
11
+ assert NotificationAppConfig.name == 'django_spire.notification.app'
12
+
13
+ def test_app_label(self):
14
+ assert NotificationAppConfig.label == 'django_spire_notification_app'
15
+
16
+ def test_default_auto_field(self):
17
+ assert NotificationAppConfig.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 == NotificationAppConfig.REQUIRED_APPS
22
+
23
+ def test_app_is_installed(self):
24
+ assert apps.is_installed('django_spire.notification.app')
@@ -0,0 +1,72 @@
1
+ from __future__ import annotations
2
+
3
+ from django.utils.timezone import now, timedelta
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
+ from django_spire.notification.models import Notification
8
+
9
+
10
+ class AppNotificationModelTests(BaseTestCase):
11
+ def setUp(self):
12
+ super().setUp()
13
+ self.app_notification = create_test_app_notification()
14
+
15
+ def test_str(self):
16
+ assert str(self.app_notification) == self.app_notification.notification.title
17
+
18
+ def test_verbose_time_since_delivered_just_now(self):
19
+ self.app_notification.notification.sent_datetime = now()
20
+ self.app_notification.notification.save()
21
+
22
+ assert self.app_notification.verbose_time_since_delivered == 'just now'
23
+
24
+ def test_verbose_time_since_delivered_minutes(self):
25
+ self.app_notification.notification.sent_datetime = now() - timedelta(minutes=5)
26
+ self.app_notification.notification.save()
27
+
28
+ assert '5 minutes ago' in self.app_notification.verbose_time_since_delivered
29
+
30
+ def test_verbose_time_since_delivered_hours(self):
31
+ self.app_notification.notification.sent_datetime = now() - timedelta(hours=3)
32
+ self.app_notification.notification.save()
33
+
34
+ assert '3 hours ago' in self.app_notification.verbose_time_since_delivered
35
+
36
+ def test_verbose_time_since_delivered_days(self):
37
+ self.app_notification.notification.sent_datetime = now() - timedelta(days=2)
38
+ self.app_notification.notification.save()
39
+
40
+ assert '2 days ago' in self.app_notification.verbose_time_since_delivered
41
+
42
+ def test_verbose_time_since_delivered_singular_day(self):
43
+ self.app_notification.notification.sent_datetime = now() - timedelta(days=1)
44
+ self.app_notification.notification.save()
45
+
46
+ assert '1 day ago' in self.app_notification.verbose_time_since_delivered
47
+
48
+ def test_as_dict(self):
49
+ result = self.app_notification.as_dict()
50
+
51
+ assert result['id'] == self.app_notification.id
52
+ assert result['title'] == self.app_notification.notification.title
53
+ assert result['body'] == self.app_notification.notification.body
54
+ assert result['url'] == self.app_notification.notification.url
55
+ assert result['priority'] == self.app_notification.notification.priority
56
+ assert 'time_since_delivered' in result
57
+ assert 'context_data' in result
58
+
59
+ def test_as_json(self):
60
+ result = self.app_notification.as_json()
61
+ assert isinstance(result, str)
62
+ assert self.app_notification.notification.title in result
63
+
64
+ def test_notification_relationship(self):
65
+ assert self.app_notification.notification is not None
66
+ assert isinstance(self.app_notification.notification, Notification)
67
+
68
+ def test_default_template(self):
69
+ assert self.app_notification.template == 'django_spire/notification/app/item/notification_item.html'
70
+
71
+ def test_default_context_data(self):
72
+ assert self.app_notification.context_data == {}
@@ -0,0 +1,111 @@
1
+ from __future__ import annotations
2
+
3
+ import pytest
4
+
5
+ from django_spire.auth.user.tests.factories import create_user
6
+ from django_spire.core.tests.test_cases import BaseTestCase
7
+ from django_spire.notification.app.exceptions import AppNotificationError
8
+ from django_spire.notification.app.models import AppNotification
9
+ from django_spire.notification.app.processor import AppNotificationProcessor
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
+
17
+
18
+ class AppNotificationProcessorTests(BaseTestCase):
19
+ def setUp(self):
20
+ super().setUp()
21
+ self.user = create_user(username='test_processor_user')
22
+ self.processor = AppNotificationProcessor()
23
+
24
+ def _create_notification(self, **kwargs) -> Notification:
25
+ data = {
26
+ 'user': self.user,
27
+ 'type': NotificationTypeChoices.APP,
28
+ 'title': 'Test Notification',
29
+ 'body': 'Test body',
30
+ 'status': NotificationStatusChoices.PENDING,
31
+ }
32
+ data.update(kwargs)
33
+ notification = Notification.objects.create(**data)
34
+ AppNotification.objects.create(notification=notification)
35
+ return notification
36
+
37
+ def test_process_sets_status_to_sent(self):
38
+ notification = self._create_notification()
39
+
40
+ self.processor.process(notification)
41
+
42
+ notification.refresh_from_db()
43
+ assert notification.status == NotificationStatusChoices.SENT
44
+
45
+ def test_process_sets_sent_datetime(self):
46
+ notification = self._create_notification()
47
+
48
+ self.processor.process(notification)
49
+
50
+ notification.refresh_from_db()
51
+ assert notification.sent_datetime is not None
52
+
53
+ def test_process_raises_error_for_wrong_type(self):
54
+ notification = self._create_notification(type=NotificationTypeChoices.EMAIL)
55
+
56
+ with pytest.raises(NotificationError):
57
+ self.processor.process(notification)
58
+
59
+ def test_process_raises_error_for_missing_user(self):
60
+ notification = Notification.objects.create(
61
+ user=None,
62
+ type=NotificationTypeChoices.APP,
63
+ title='Test',
64
+ body='Test',
65
+ status=NotificationStatusChoices.PENDING,
66
+ )
67
+ AppNotification.objects.create(notification=notification)
68
+
69
+ with pytest.raises(AppNotificationError):
70
+ self.processor.process(notification)
71
+
72
+ def test_process_list(self):
73
+ notifications = [self._create_notification() for _ in range(3)]
74
+
75
+ self.processor.process_list(notifications)
76
+
77
+ for notification in notifications:
78
+ notification.refresh_from_db()
79
+ assert notification.status == NotificationStatusChoices.SENT
80
+ assert notification.sent_datetime is not None
81
+
82
+ def test_process_list_raises_error_for_wrong_type(self):
83
+ notifications = [
84
+ self._create_notification(),
85
+ self._create_notification(type=NotificationTypeChoices.EMAIL),
86
+ ]
87
+
88
+ with pytest.raises(NotificationError):
89
+ self.processor.process_list(notifications)
90
+
91
+ def test_process_ready(self):
92
+ pending_notification = self._create_notification()
93
+
94
+ sent_notification = self._create_notification(
95
+ status=NotificationStatusChoices.SENT
96
+ )
97
+
98
+ self.processor.process_ready()
99
+
100
+ pending_notification.refresh_from_db()
101
+ assert pending_notification.status == NotificationStatusChoices.SENT
102
+
103
+ def test_process_errored(self):
104
+ errored_notification = self._create_notification(
105
+ status=NotificationStatusChoices.ERRORED
106
+ )
107
+
108
+ self.processor.process_errored()
109
+
110
+ errored_notification.refresh_from_db()
111
+ assert errored_notification.status == NotificationStatusChoices.SENT