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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (538) hide show
  1. django_spire/ai/admin.py +11 -11
  2. django_spire/ai/chat/apps.py +1 -0
  3. django_spire/ai/chat/templates/django_spire/ai/chat/widget/dialog_widget.html +1 -1
  4. django_spire/ai/chat/tests/factories.py +15 -0
  5. django_spire/ai/chat/tests/test_controller.py +45 -0
  6. django_spire/ai/chat/tests/test_models.py +301 -0
  7. django_spire/ai/chat/tests/test_prompts.py +48 -0
  8. django_spire/ai/chat/tests/test_responses.py +208 -0
  9. django_spire/ai/chat/tests/test_router/test_base_chat_router.py +66 -6
  10. django_spire/ai/chat/tests/test_router/test_chat_workflow.py +73 -3
  11. django_spire/ai/chat/tests/test_router/test_integration.py +86 -6
  12. django_spire/ai/chat/tests/test_router/test_intent_decoder.py +93 -1
  13. django_spire/ai/chat/tests/test_router/test_message_intel.py +60 -1
  14. django_spire/ai/chat/tests/test_router/test_spire_chat_router.py +110 -0
  15. django_spire/ai/chat/tests/test_urls/test_json_urls.py +202 -1
  16. django_spire/ai/context/tests/__init__.py +0 -0
  17. django_spire/ai/context/tests/test_context.py +188 -0
  18. django_spire/ai/decorators.py +7 -6
  19. django_spire/ai/prompt/tests/test_bots.py +100 -10
  20. django_spire/ai/prompt/tests/test_prompt_intel.py +83 -0
  21. django_spire/ai/prompt/tests/test_prompt_tuning.py +126 -0
  22. django_spire/ai/sms/decorators.py +8 -2
  23. django_spire/ai/sms/tests/test_sms.py +240 -16
  24. django_spire/ai/sms/tests/test_sms_intel.py +42 -0
  25. django_spire/ai/sms/tests/test_webhook.py +155 -7
  26. django_spire/ai/sms/views.py +23 -24
  27. django_spire/ai/tests/test_ai.py +131 -7
  28. django_spire/auth/apps.py +4 -2
  29. django_spire/auth/controller/controller.py +36 -23
  30. django_spire/auth/controller/exceptions.py +9 -0
  31. django_spire/auth/group/admin.py +1 -0
  32. django_spire/auth/group/apps.py +2 -0
  33. django_spire/auth/group/factories.py +17 -8
  34. django_spire/auth/group/forms.py +7 -0
  35. django_spire/auth/group/tests/test_factories.py +146 -0
  36. django_spire/auth/group/tests/test_forms.py +282 -0
  37. django_spire/auth/group/tests/test_models.py +192 -0
  38. django_spire/auth/group/tests/test_querysets.py +98 -0
  39. django_spire/auth/group/tests/test_utils.py +341 -0
  40. django_spire/auth/group/tests/test_views.py +377 -0
  41. django_spire/auth/group/urls/__init__.py +3 -1
  42. django_spire/auth/group/urls/form_urls.py +2 -0
  43. django_spire/auth/group/urls/json_urls.py +3 -0
  44. django_spire/auth/group/urls/page_urls.py +2 -0
  45. django_spire/auth/group/utils.py +6 -2
  46. django_spire/auth/group/views/form_views.py +6 -3
  47. django_spire/auth/group/views/json_views.py +6 -2
  48. django_spire/auth/mfa/admin.py +2 -0
  49. django_spire/auth/mfa/apps.py +2 -0
  50. django_spire/auth/mfa/forms.py +1 -0
  51. django_spire/auth/mfa/querysets.py +9 -2
  52. django_spire/auth/mfa/tests/test_models.py +233 -0
  53. django_spire/auth/mfa/tests/test_utils.py +106 -0
  54. django_spire/auth/mfa/urls/__init__.py +2 -0
  55. django_spire/auth/mfa/urls/page_urls.py +2 -0
  56. django_spire/auth/mfa/urls/redirect_urls.py +2 -0
  57. django_spire/auth/mfa/views/page_views.py +2 -1
  58. django_spire/auth/permissions/consts.py +2 -2
  59. django_spire/auth/permissions/decorators.py +8 -8
  60. django_spire/auth/permissions/permissions.py +28 -35
  61. django_spire/auth/permissions/tests/test_decorators.py +333 -0
  62. django_spire/auth/permissions/tests/test_permissions.py +337 -0
  63. django_spire/auth/permissions/tests/test_tools.py +305 -0
  64. django_spire/auth/permissions/tools.py +21 -15
  65. django_spire/auth/seeding/seed.py +3 -0
  66. django_spire/auth/seeding/seeder.py +2 -0
  67. django_spire/auth/tests/test_controller.py +323 -0
  68. django_spire/auth/tests/test_url_endpoints.py +9 -9
  69. django_spire/auth/tests/test_views.py +406 -0
  70. django_spire/auth/urls/admin_urls.py +2 -0
  71. django_spire/auth/urls/redirect_urls.py +2 -0
  72. django_spire/auth/user/apps.py +2 -0
  73. django_spire/auth/user/forms.py +9 -0
  74. django_spire/auth/user/models.py +1 -1
  75. django_spire/auth/user/services/services.py +1 -0
  76. django_spire/auth/user/tests/factories.py +14 -13
  77. django_spire/auth/user/tests/test_factories.py +166 -2
  78. django_spire/auth/user/tests/test_forms.py +573 -0
  79. django_spire/auth/user/tests/test_models.py +257 -0
  80. django_spire/auth/user/tests/test_services.py +200 -0
  81. django_spire/auth/user/tests/test_tools.py +153 -0
  82. django_spire/auth/user/tests/test_user_factories.py +139 -0
  83. django_spire/auth/user/tests/test_views.py +363 -0
  84. django_spire/auth/user/tools.py +7 -1
  85. django_spire/auth/user/urls/form_urls.py +3 -0
  86. django_spire/auth/user/urls/page_urls.py +3 -0
  87. django_spire/auth/user/views/form_views.py +19 -10
  88. django_spire/auth/user/views/page_views.py +8 -2
  89. django_spire/auth/views/redirect_views.py +14 -9
  90. django_spire/comment/admin.py +2 -0
  91. django_spire/comment/apps.py +2 -0
  92. django_spire/comment/templatetags/comment_tags.py +1 -0
  93. django_spire/comment/tests/test_forms.py +27 -0
  94. django_spire/comment/tests/test_models.py +215 -0
  95. django_spire/comment/tests/test_querysets.py +101 -0
  96. django_spire/comment/tests/test_utils.py +90 -0
  97. django_spire/comment/urls.py +2 -0
  98. django_spire/comment/utils.py +22 -13
  99. django_spire/comment/views.py +1 -1
  100. django_spire/conf.py +8 -6
  101. django_spire/consts.py +1 -1
  102. django_spire/contrib/breadcrumb/apps.py +2 -0
  103. django_spire/contrib/breadcrumb/breadcrumbs.py +18 -18
  104. django_spire/contrib/breadcrumb/tests/test_breadcrumbs.py +198 -0
  105. django_spire/contrib/constructor/__init__.py +3 -3
  106. django_spire/contrib/constructor/constructor.py +15 -15
  107. django_spire/contrib/constructor/django_model_constructor.py +5 -4
  108. django_spire/contrib/constructor/exceptions.py +5 -3
  109. django_spire/contrib/constructor/tests/__init__.py +0 -0
  110. django_spire/contrib/constructor/tests/test_constructor.py +193 -0
  111. django_spire/contrib/form/tests/__init__.py +0 -0
  112. django_spire/contrib/form/tests/test_forms.py +203 -0
  113. django_spire/contrib/generic_views/modal_views.py +2 -1
  114. django_spire/contrib/generic_views/portal_views.py +20 -19
  115. django_spire/contrib/generic_views/tests/__init__.py +0 -0
  116. django_spire/contrib/generic_views/tests/test_views.py +459 -0
  117. django_spire/contrib/help/apps.py +2 -0
  118. django_spire/contrib/help/templatetags/help.py +1 -0
  119. django_spire/contrib/help/tests/__init__.py +0 -0
  120. django_spire/contrib/help/tests/test_templatetags.py +100 -0
  121. django_spire/contrib/options/mixins.py +6 -5
  122. django_spire/contrib/options/tests/factories.py +5 -1
  123. django_spire/contrib/options/tests/test_options.py +234 -0
  124. django_spire/contrib/ordering/exceptions.py +7 -3
  125. django_spire/contrib/ordering/mixins.py +2 -0
  126. django_spire/contrib/ordering/querysets.py +3 -1
  127. django_spire/contrib/ordering/services/processor_service.py +8 -4
  128. django_spire/contrib/ordering/services/service.py +1 -2
  129. django_spire/contrib/ordering/tests/__init__.py +0 -0
  130. django_spire/contrib/ordering/tests/test_ordering.py +165 -0
  131. django_spire/contrib/ordering/validators.py +6 -6
  132. django_spire/contrib/pagination/templatetags/pagination_tags.py +12 -5
  133. django_spire/contrib/pagination/tests/__init__.py +0 -0
  134. django_spire/contrib/pagination/tests/test_pagination.py +179 -0
  135. django_spire/contrib/performance/decorators.py +16 -6
  136. django_spire/contrib/performance/tests/__init__.py +0 -0
  137. django_spire/contrib/performance/tests/test_performance.py +107 -0
  138. django_spire/contrib/queryset/enums.py +3 -1
  139. django_spire/contrib/queryset/filter_tools.py +10 -5
  140. django_spire/contrib/queryset/mixins.py +16 -16
  141. django_spire/contrib/queryset/tests/__init__.py +0 -0
  142. django_spire/contrib/queryset/tests/test_queryset.py +137 -0
  143. django_spire/contrib/seeding/field/base.py +13 -7
  144. django_spire/contrib/seeding/field/callable.py +8 -1
  145. django_spire/contrib/seeding/field/cleaners.py +5 -5
  146. django_spire/contrib/seeding/field/custom.py +20 -10
  147. django_spire/contrib/seeding/field/django/seeder.py +8 -6
  148. django_spire/contrib/seeding/field/enums.py +7 -5
  149. django_spire/contrib/seeding/field/override.py +16 -6
  150. django_spire/contrib/seeding/field/static.py +9 -2
  151. django_spire/contrib/seeding/field/tests/test_base.py +18 -14
  152. django_spire/contrib/seeding/field/tests/test_callable.py +13 -9
  153. django_spire/contrib/seeding/field/tests/test_cleaners.py +51 -38
  154. django_spire/contrib/seeding/field/tests/test_static.py +13 -9
  155. django_spire/contrib/seeding/intelligence/bots/seeder_generator_bot.py +2 -0
  156. django_spire/contrib/seeding/intelligence/intel.py +5 -1
  157. django_spire/contrib/seeding/intelligence/prompts/factory.py +6 -1
  158. django_spire/contrib/seeding/intelligence/prompts/foreign_key_selection_prompt.py +6 -1
  159. django_spire/contrib/seeding/intelligence/prompts/generate_django_model_seeder_prompts.py +2 -0
  160. django_spire/contrib/seeding/intelligence/prompts/generic_relationship_selection_prompt.py +7 -1
  161. django_spire/contrib/seeding/intelligence/prompts/hierarchical_selection_prompt.py +6 -2
  162. django_spire/contrib/seeding/intelligence/prompts/model_field_choices_prompt.py +8 -2
  163. django_spire/contrib/seeding/intelligence/prompts/negation_prompt.py +2 -0
  164. django_spire/contrib/seeding/intelligence/prompts/objective_prompt.py +6 -1
  165. django_spire/contrib/seeding/management/commands/seeding.py +9 -3
  166. django_spire/contrib/seeding/management/example.py +2 -0
  167. django_spire/contrib/seeding/model/base.py +16 -7
  168. django_spire/contrib/seeding/model/config.py +31 -15
  169. django_spire/contrib/seeding/model/django/config.py +13 -13
  170. django_spire/contrib/seeding/model/django/seeder.py +4 -4
  171. django_spire/contrib/seeding/model/django/tests/test_seeder.py +34 -23
  172. django_spire/contrib/seeding/model/enums.py +2 -0
  173. django_spire/contrib/seeding/tests/test_config.py +71 -0
  174. django_spire/contrib/seeding/tests/test_custom.py +35 -0
  175. django_spire/contrib/seeding/tests/test_enums.py +40 -0
  176. django_spire/contrib/seeding/tests/test_intel.py +32 -0
  177. django_spire/contrib/seeding/tests/test_override.py +63 -0
  178. django_spire/contrib/service/__init__.py +2 -2
  179. django_spire/contrib/service/django_model_service.py +16 -15
  180. django_spire/contrib/service/exceptions.py +5 -3
  181. django_spire/contrib/service/tests/__init__.py +0 -0
  182. django_spire/contrib/service/tests/test_service.py +153 -0
  183. django_spire/contrib/session/apps.py +2 -0
  184. django_spire/contrib/session/controller.py +48 -42
  185. django_spire/contrib/session/templatetags/session_tags.py +11 -2
  186. django_spire/contrib/session/tests/test_session_controller.py +117 -53
  187. django_spire/contrib/tests/__init__.py +0 -0
  188. django_spire/contrib/tests/test_utils.py +37 -0
  189. django_spire/contrib/utils.py +4 -1
  190. django_spire/core/apps.py +2 -0
  191. django_spire/core/converters/tests/test_to_data.py +353 -0
  192. django_spire/core/converters/tests/test_to_enums.py +61 -41
  193. django_spire/core/converters/tests/test_to_pydantic.py +138 -109
  194. django_spire/core/converters/to_data.py +29 -10
  195. django_spire/core/converters/to_enums.py +4 -2
  196. django_spire/core/converters/to_pydantic.py +22 -22
  197. django_spire/core/decorators.py +19 -6
  198. django_spire/core/forms/widgets.py +4 -0
  199. django_spire/core/maps.py +3 -1
  200. django_spire/core/middleware/maintenance.py +3 -3
  201. django_spire/core/middleware.py +8 -6
  202. django_spire/core/redirect/__init__.py +5 -0
  203. django_spire/core/redirect/generic_redirect.py +1 -2
  204. django_spire/core/redirect/tests/__init__.py +0 -0
  205. django_spire/core/redirect/tests/test_generic_redirect.py +34 -0
  206. django_spire/core/{tests/tests_redirect.py → redirect/tests/test_safe_redirect.py} +55 -81
  207. django_spire/core/shortcuts.py +3 -3
  208. django_spire/core/static/django_spire/css/app-layout.css +1 -1
  209. django_spire/core/static/django_spire/css/app-navigation.css +3 -3
  210. django_spire/core/static/django_spire/css/bootstrap-override.css +4 -0
  211. django_spire/core/tag/admin.py +12 -0
  212. django_spire/core/tag/intelligence/tag_set_bot.py +2 -0
  213. django_spire/core/tag/mixins.py +2 -0
  214. django_spire/core/tag/models.py +2 -0
  215. django_spire/core/tag/querysets.py +2 -0
  216. django_spire/core/tag/service/tag_service.py +6 -3
  217. django_spire/core/tag/tests/test_intelligence.py +9 -9
  218. django_spire/core/tag/tests/test_tags.py +44 -54
  219. django_spire/core/tag/tests/test_tools.py +191 -0
  220. django_spire/core/tag/tools.py +3 -0
  221. django_spire/core/templates/django_spire/card/card.html +5 -2
  222. django_spire/core/templates/django_spire/card/title_card.html +9 -4
  223. django_spire/core/templates/django_spire/infinite_scroll/base.html +1 -0
  224. django_spire/core/templates/django_spire/navigation/side_navigation.html +19 -24
  225. django_spire/core/templates/django_spire/page/full_page.html +46 -16
  226. django_spire/core/templates/django_spire/table/base.html +4 -2
  227. django_spire/core/templatetags/json.py +6 -2
  228. django_spire/core/templatetags/message.py +13 -8
  229. django_spire/core/templatetags/string_formating.py +8 -5
  230. django_spire/core/templatetags/tests/__init__.py +0 -0
  231. django_spire/core/templatetags/tests/test_templatetags.py +427 -0
  232. django_spire/core/templatetags/variable_types.py +17 -9
  233. django_spire/core/tests/test_cases.py +1 -1
  234. django_spire/core/tests/test_conf.py +43 -0
  235. django_spire/core/tests/test_consts.py +28 -0
  236. django_spire/core/tests/test_context_processors.py +93 -0
  237. django_spire/core/tests/test_decorators.py +95 -0
  238. django_spire/core/tests/test_django_spire_utils.py +56 -0
  239. django_spire/core/tests/test_exceptions.py +37 -0
  240. django_spire/core/tests/test_models.py +54 -0
  241. django_spire/core/tests/test_settings.py +45 -0
  242. django_spire/core/tests/test_shortcuts.py +74 -0
  243. django_spire/core/tests/test_urls.py +16 -0
  244. django_spire/core/tests/test_utils.py +58 -0
  245. django_spire/core/urls.py +4 -1
  246. django_spire/core/utils.py +12 -8
  247. django_spire/exceptions.py +16 -1
  248. django_spire/file/admin.py +4 -2
  249. django_spire/file/apps.py +8 -10
  250. django_spire/file/fields.py +7 -7
  251. django_spire/file/forms.py +1 -1
  252. django_spire/file/interfaces.py +15 -15
  253. django_spire/file/mixins.py +1 -4
  254. django_spire/file/models.py +3 -5
  255. django_spire/file/tests/factories.py +59 -0
  256. django_spire/file/tests/test_admin.py +69 -0
  257. django_spire/file/tests/test_apps.py +24 -0
  258. django_spire/file/tests/test_fields.py +114 -0
  259. django_spire/file/tests/test_forms.py +20 -0
  260. django_spire/file/tests/test_interfaces.py +183 -0
  261. django_spire/file/tests/test_models.py +82 -0
  262. django_spire/file/tests/test_querysets.py +102 -0
  263. django_spire/file/tests/test_utils.py +32 -0
  264. django_spire/file/tests/test_views.py +145 -0
  265. django_spire/file/tests/test_widgets.py +82 -0
  266. django_spire/file/tools.py +8 -2
  267. django_spire/file/views.py +7 -3
  268. django_spire/file/widgets.py +12 -12
  269. django_spire/help_desk/admin.py +15 -0
  270. django_spire/help_desk/apps.py +2 -0
  271. django_spire/help_desk/auth/controller.py +2 -0
  272. django_spire/help_desk/choices.py +2 -0
  273. django_spire/help_desk/enums.py +2 -0
  274. django_spire/help_desk/exceptions.py +31 -3
  275. django_spire/help_desk/forms.py +2 -0
  276. django_spire/help_desk/models.py +2 -0
  277. django_spire/help_desk/querysets.py +4 -1
  278. django_spire/help_desk/services/notification_service.py +26 -27
  279. django_spire/help_desk/services/service.py +2 -3
  280. django_spire/help_desk/tests/factories.py +8 -3
  281. django_spire/help_desk/tests/test_admin.py +41 -0
  282. django_spire/help_desk/tests/test_apps.py +41 -0
  283. django_spire/help_desk/tests/test_choices.py +50 -0
  284. django_spire/help_desk/tests/test_controller.py +87 -0
  285. django_spire/help_desk/tests/test_enums.py +18 -0
  286. django_spire/help_desk/tests/test_exceptions.py +37 -0
  287. django_spire/help_desk/tests/test_forms.py +89 -0
  288. django_spire/help_desk/tests/test_models.py +59 -0
  289. django_spire/help_desk/tests/test_querysets.py +38 -0
  290. django_spire/help_desk/tests/test_services/test_notification_service.py +15 -8
  291. django_spire/help_desk/tests/test_services/test_service.py +92 -0
  292. django_spire/help_desk/tests/test_urls/test_form_urls.py +6 -6
  293. django_spire/help_desk/tests/test_urls/test_page_urls.py +8 -9
  294. django_spire/help_desk/tests/test_views/test_form_views.py +46 -19
  295. django_spire/help_desk/tests/test_views/test_page_views.py +32 -9
  296. django_spire/help_desk/urls/__init__.py +4 -1
  297. django_spire/help_desk/urls/form_urls.py +3 -0
  298. django_spire/help_desk/urls/page_urls.py +3 -0
  299. django_spire/help_desk/views/form_views.py +13 -5
  300. django_spire/help_desk/views/page_views.py +11 -3
  301. django_spire/history/activity/admin.py +2 -0
  302. django_spire/history/activity/apps.py +3 -1
  303. django_spire/history/activity/mixins.py +13 -7
  304. django_spire/history/activity/models.py +6 -5
  305. django_spire/history/activity/querysets.py +2 -0
  306. django_spire/history/activity/tests/__init__.py +0 -0
  307. django_spire/history/activity/tests/test_activity.py +176 -0
  308. django_spire/history/admin.py +9 -2
  309. django_spire/history/choices.py +3 -0
  310. django_spire/history/models.py +5 -5
  311. django_spire/history/tests/test_admin.py +93 -0
  312. django_spire/history/tests/test_history.py +101 -0
  313. django_spire/history/tests/test_mixins.py +84 -0
  314. django_spire/history/viewed/admin.py +3 -1
  315. django_spire/history/viewed/apps.py +3 -1
  316. django_spire/history/viewed/models.py +2 -0
  317. django_spire/history/viewed/tests/__init__.py +0 -0
  318. django_spire/history/viewed/tests/test_viewed.py +46 -0
  319. django_spire/knowledge/auth/tests/__init__.py +0 -0
  320. django_spire/knowledge/auth/tests/test_controller.py +116 -0
  321. django_spire/knowledge/collection/admin.py +5 -1
  322. django_spire/knowledge/collection/models.py +3 -1
  323. django_spire/knowledge/collection/seeding/seed.py +1 -0
  324. django_spire/knowledge/collection/services/factory_service.py +10 -11
  325. django_spire/knowledge/collection/services/ordering_service.py +1 -2
  326. django_spire/knowledge/collection/services/service.py +5 -10
  327. django_spire/knowledge/collection/services/tag_service.py +5 -2
  328. django_spire/knowledge/collection/tests/factories.py +28 -1
  329. django_spire/knowledge/collection/tests/test_models.py +48 -0
  330. django_spire/knowledge/collection/tests/test_querysets.py +93 -0
  331. django_spire/knowledge/collection/tests/test_services/test_factory_service.py +100 -0
  332. django_spire/knowledge/collection/tests/test_services/test_services.py +160 -0
  333. django_spire/knowledge/collection/tests/test_urls/test_form_urls.py +21 -3
  334. django_spire/knowledge/collection/tests/test_urls/test_json_urls.py +39 -1
  335. django_spire/knowledge/collection/tests/test_urls/test_page_urls.py +12 -4
  336. django_spire/knowledge/collection/urls/__init__.py +3 -0
  337. django_spire/knowledge/collection/urls/form_urls.py +2 -0
  338. django_spire/knowledge/collection/urls/json_urls.py +2 -0
  339. django_spire/knowledge/collection/urls/page_urls.py +2 -0
  340. django_spire/knowledge/collection/views/form_views.py +4 -4
  341. django_spire/knowledge/collection/views/json_views.py +5 -1
  342. django_spire/knowledge/collection/views/page_views.py +5 -2
  343. django_spire/knowledge/entry/admin.py +7 -1
  344. django_spire/knowledge/entry/forms.py +2 -0
  345. django_spire/knowledge/entry/models.py +2 -0
  346. django_spire/knowledge/entry/seeding/seed.py +3 -0
  347. django_spire/knowledge/entry/services/automation_service.py +5 -4
  348. django_spire/knowledge/entry/services/factory_service.py +7 -5
  349. django_spire/knowledge/entry/services/service.py +4 -7
  350. django_spire/knowledge/entry/services/tag_service.py +0 -1
  351. django_spire/knowledge/entry/services/tool_service.py +1 -0
  352. django_spire/knowledge/entry/services/transformation_services.py +1 -5
  353. django_spire/knowledge/entry/tests/factories.py +1 -2
  354. django_spire/knowledge/entry/tests/test_factory_service.py +20 -0
  355. django_spire/knowledge/entry/tests/test_models.py +41 -0
  356. django_spire/knowledge/entry/tests/test_querysets.py +71 -0
  357. django_spire/knowledge/entry/tests/test_services.py +94 -0
  358. django_spire/knowledge/entry/tests/test_urls/test_form_urls.py +9 -14
  359. django_spire/knowledge/entry/tests/test_urls/test_json_urls.py +48 -5
  360. django_spire/knowledge/entry/tests/test_urls/test_page_urls.py +6 -8
  361. django_spire/knowledge/entry/tests/test_urls/test_template_urls.py +40 -0
  362. django_spire/knowledge/entry/urls/form_urls.py +2 -0
  363. django_spire/knowledge/entry/urls/json_urls.py +2 -0
  364. django_spire/knowledge/entry/urls/page_urls.py +2 -0
  365. django_spire/knowledge/entry/urls/template_urls.py +2 -0
  366. django_spire/knowledge/entry/version/block/choices.py +2 -0
  367. django_spire/knowledge/entry/version/block/data/data.py +1 -0
  368. django_spire/knowledge/entry/version/block/data/list/data.py +8 -13
  369. django_spire/knowledge/entry/version/block/data/list/maps.py +3 -0
  370. django_spire/knowledge/entry/version/block/data/list/meta.py +1 -2
  371. django_spire/knowledge/entry/version/block/data/list/tests/__init__.py +0 -0
  372. django_spire/knowledge/entry/version/block/data/list/tests/test_maps.py +32 -0
  373. django_spire/knowledge/entry/version/block/data/list/tests/test_meta.py +58 -0
  374. django_spire/knowledge/entry/version/block/data/maps.py +3 -6
  375. django_spire/knowledge/entry/version/block/models.py +7 -5
  376. django_spire/knowledge/entry/version/block/seeding/constants.py +5 -4
  377. django_spire/knowledge/entry/version/block/services/service.py +2 -3
  378. django_spire/knowledge/entry/version/block/tests/factories.py +4 -10
  379. django_spire/knowledge/entry/version/block/tests/test_choices.py +56 -0
  380. django_spire/knowledge/entry/version/block/tests/test_data.py +90 -0
  381. django_spire/knowledge/entry/version/block/tests/test_maps.py +37 -0
  382. django_spire/knowledge/entry/version/block/tests/test_models.py +55 -0
  383. django_spire/knowledge/entry/version/block/tests/test_querysets.py +35 -0
  384. django_spire/knowledge/entry/version/block/tests/test_services.py +65 -0
  385. django_spire/knowledge/entry/version/choices.py +2 -0
  386. django_spire/knowledge/entry/version/converters/converter.py +1 -1
  387. django_spire/knowledge/entry/version/converters/docx_converter.py +4 -7
  388. django_spire/knowledge/entry/version/converters/markdown_converter.py +20 -20
  389. django_spire/knowledge/entry/version/maps.py +4 -5
  390. django_spire/knowledge/entry/version/querysets.py +1 -1
  391. django_spire/knowledge/entry/version/seeding/seeder.py +1 -2
  392. django_spire/knowledge/entry/version/services/processor_service.py +5 -4
  393. django_spire/knowledge/entry/version/services/service.py +1 -2
  394. django_spire/knowledge/entry/version/tests/factories.py +2 -2
  395. django_spire/knowledge/entry/version/tests/test_choices.py +18 -0
  396. django_spire/knowledge/entry/version/tests/test_converters/test_docx_converter.py +56 -8
  397. django_spire/knowledge/entry/version/tests/test_converters/test_markdown_converter.py +78 -0
  398. django_spire/knowledge/entry/version/tests/test_maps.py +58 -0
  399. django_spire/knowledge/entry/version/tests/test_models.py +23 -0
  400. django_spire/knowledge/entry/version/tests/test_querysets.py +26 -0
  401. django_spire/knowledge/entry/version/tests/test_services.py +62 -0
  402. django_spire/knowledge/entry/version/tests/test_urls/test_json_urls.py +27 -8
  403. django_spire/knowledge/entry/version/tests/test_urls/test_page_urls.py +15 -8
  404. django_spire/knowledge/entry/version/tests/test_urls/test_redirect_urls.py +38 -0
  405. django_spire/knowledge/entry/version/urls/__init__.py +3 -0
  406. django_spire/knowledge/entry/version/urls/json_urls.py +2 -1
  407. django_spire/knowledge/entry/version/urls/page_urls.py +2 -0
  408. django_spire/knowledge/entry/version/urls/redirect_urls.py +2 -0
  409. django_spire/knowledge/entry/version/views/json_views.py +5 -1
  410. django_spire/knowledge/entry/version/views/page_views.py +10 -3
  411. django_spire/knowledge/entry/version/views/redirect_views.py +5 -1
  412. django_spire/knowledge/entry/views/form_views.py +16 -8
  413. django_spire/knowledge/entry/views/json_views.py +3 -1
  414. django_spire/knowledge/entry/views/page_views.py +8 -2
  415. django_spire/knowledge/entry/views/template_views.py +7 -1
  416. django_spire/knowledge/exceptions.py +2 -1
  417. django_spire/knowledge/intelligence/intel/answer_intel.py +2 -1
  418. django_spire/knowledge/intelligence/intel/entry_intel.py +0 -1
  419. django_spire/knowledge/intelligence/workflows/knowledge_workflow.py +4 -5
  420. django_spire/knowledge/models.py +1 -2
  421. django_spire/knowledge/tests/__init__.py +0 -0
  422. django_spire/knowledge/tests/test_templatetags.py +40 -0
  423. django_spire/knowledge/tests/test_urls/__init__.py +0 -0
  424. django_spire/knowledge/tests/test_urls/test_page_urls.py +24 -0
  425. django_spire/knowledge/urls/__init__.py +2 -0
  426. django_spire/knowledge/urls/page_urls.py +2 -0
  427. django_spire/knowledge/views/page_views.py +8 -3
  428. django_spire/notification/admin.py +3 -1
  429. django_spire/notification/app/admin.py +2 -0
  430. django_spire/notification/app/apps.py +3 -1
  431. django_spire/notification/app/exceptions.py +9 -2
  432. django_spire/notification/app/models.py +8 -4
  433. django_spire/notification/app/processor.py +22 -26
  434. django_spire/notification/app/querysets.py +2 -0
  435. django_spire/notification/app/tests/__init__.py +0 -0
  436. django_spire/notification/app/tests/factories.py +34 -0
  437. django_spire/notification/app/tests/test_apps.py +24 -0
  438. django_spire/notification/app/tests/test_models.py +72 -0
  439. django_spire/notification/app/tests/test_processor.py +111 -0
  440. django_spire/notification/app/tests/test_querysets.py +90 -0
  441. django_spire/notification/app/tests/test_views/__init__.py +0 -0
  442. django_spire/notification/app/tests/test_views/test_json_views.py +48 -0
  443. django_spire/notification/app/tests/test_views/test_page_views.py +19 -0
  444. django_spire/notification/app/urls/__init__.py +3 -1
  445. django_spire/notification/app/urls/json_urls.py +6 -4
  446. django_spire/notification/app/urls/page_urls.py +4 -3
  447. django_spire/notification/app/urls/template_urls.py +4 -2
  448. django_spire/notification/apps.py +4 -1
  449. django_spire/notification/email/admin.py +5 -1
  450. django_spire/notification/email/apps.py +3 -1
  451. django_spire/notification/email/exceptions.py +4 -2
  452. django_spire/notification/email/helper.py +5 -3
  453. django_spire/notification/email/models.py +4 -0
  454. django_spire/notification/email/processor.py +19 -15
  455. django_spire/notification/email/querysets.py +3 -0
  456. django_spire/notification/email/tests/__init__.py +0 -0
  457. django_spire/notification/email/tests/factories.py +35 -0
  458. django_spire/notification/email/tests/test_apps.py +24 -0
  459. django_spire/notification/email/tests/test_models.py +52 -0
  460. django_spire/notification/email/tests/test_processor.py +92 -0
  461. django_spire/notification/email/tests/test_querysets.py +43 -0
  462. django_spire/notification/exceptions.py +17 -2
  463. django_spire/notification/managers.py +7 -1
  464. django_spire/notification/maps.py +4 -1
  465. django_spire/notification/mixins.py +2 -0
  466. django_spire/notification/models.py +3 -1
  467. django_spire/notification/processors/notification.py +12 -5
  468. django_spire/notification/processors/processor.py +2 -0
  469. django_spire/notification/processors/tests/__init__.py +0 -0
  470. django_spire/notification/processors/tests/test_notification.py +106 -0
  471. django_spire/notification/push/admin.py +10 -1
  472. django_spire/notification/push/apps.py +3 -1
  473. django_spire/notification/push/models.py +2 -3
  474. django_spire/notification/push/tests/__init__.py +0 -0
  475. django_spire/notification/push/tests/test_apps.py +24 -0
  476. django_spire/notification/push/tests/test_models.py +28 -0
  477. django_spire/notification/querysets.py +7 -1
  478. django_spire/notification/sms/admin.py +2 -0
  479. django_spire/notification/sms/apps.py +4 -1
  480. django_spire/notification/sms/automations.py +2 -0
  481. django_spire/notification/sms/choices.py +2 -0
  482. django_spire/notification/sms/exceptions.py +19 -5
  483. django_spire/notification/sms/helper.py +33 -23
  484. django_spire/notification/sms/models.py +5 -1
  485. django_spire/notification/sms/processor.py +20 -20
  486. django_spire/notification/sms/querysets.py +2 -0
  487. django_spire/notification/sms/tests/factories.py +33 -0
  488. django_spire/notification/sms/tests/test_apps.py +24 -0
  489. django_spire/notification/sms/tests/test_automation.py +38 -0
  490. django_spire/notification/sms/tests/test_choices.py +15 -0
  491. django_spire/notification/sms/tests/test_consts.py +17 -0
  492. django_spire/notification/sms/tests/test_exceptions.py +27 -0
  493. django_spire/notification/sms/tests/test_helper.py +50 -0
  494. django_spire/notification/sms/tests/test_models.py +81 -0
  495. django_spire/notification/sms/tests/test_processor.py +107 -0
  496. django_spire/notification/sms/tests/test_tools.py +25 -11
  497. django_spire/notification/sms/tools.py +16 -5
  498. django_spire/notification/sms/urls/__init__.py +3 -1
  499. django_spire/notification/sms/urls/media_urls.py +2 -0
  500. django_spire/notification/sms/views/media_views.py +14 -4
  501. django_spire/notification/tests/__init__.py +0 -0
  502. django_spire/notification/tests/factories.py +26 -0
  503. django_spire/notification/tests/test_admin.py +55 -0
  504. django_spire/notification/tests/test_apps.py +30 -0
  505. django_spire/notification/tests/test_automation.py +18 -0
  506. django_spire/notification/tests/test_choices.py +59 -0
  507. django_spire/notification/tests/test_exceptions.py +58 -0
  508. django_spire/notification/tests/test_managers.py +100 -0
  509. django_spire/notification/tests/test_maps.py +31 -0
  510. django_spire/notification/tests/test_models.py +76 -0
  511. django_spire/notification/tests/test_querysets.py +184 -0
  512. django_spire/notification/tests/test_utils.py +23 -0
  513. django_spire/notification/urls.py +3 -1
  514. django_spire/notification/utils.py +3 -1
  515. django_spire/settings.py +3 -0
  516. django_spire/theme/tests/test_context_processor.py +15 -13
  517. django_spire/theme/tests/test_enums.py +2 -2
  518. django_spire/theme/tests/test_filesystem.py +2 -5
  519. django_spire/theme/tests/test_integration.py +12 -12
  520. django_spire/theme/tests/test_model.py +40 -38
  521. django_spire/theme/tests/test_views/test_json_views.py +33 -33
  522. django_spire/theme/urls/json_urls.py +3 -0
  523. django_spire/theme/urls/page_urls.py +3 -0
  524. django_spire/urls.py +19 -15
  525. django_spire/utils.py +13 -4
  526. {django_spire-0.23.7.dist-info → django_spire-0.23.8.dist-info}/METADATA +1 -1
  527. {django_spire-0.23.7.dist-info → django_spire-0.23.8.dist-info}/RECORD +530 -358
  528. django_spire/contrib/options/tests/test_unit.py +0 -148
  529. django_spire/contrib/seeding/tests/test_seeding.py +0 -25
  530. django_spire/core/tests/test_templatetags.py +0 -117
  531. django_spire/core/tests/tests_shortcuts.py +0 -73
  532. django_spire/history/activity/tests.py +0 -3
  533. django_spire/history/activity/views.py +0 -3
  534. django_spire/knowledge/collection/tests/test_services/test_transformation_service.py +0 -71
  535. django_spire/notification/app/tests.py +0 -3
  536. {django_spire-0.23.7.dist-info → django_spire-0.23.8.dist-info}/WHEEL +0 -0
  537. {django_spire-0.23.7.dist-info → django_spire-0.23.8.dist-info}/licenses/LICENSE.md +0 -0
  538. {django_spire-0.23.7.dist-info → django_spire-0.23.8.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,153 @@
1
+ from __future__ import annotations
2
+
3
+ import pytest
4
+
5
+ from unittest.mock import patch
6
+
7
+ from django.contrib.auth.models import User
8
+ from django.core.exceptions import ValidationError
9
+ from django.test import TestCase
10
+
11
+ from django_spire.contrib.service import BaseDjangoModelService, ServiceError
12
+
13
+
14
+ class UserService(BaseDjangoModelService[User]):
15
+ obj: User
16
+
17
+
18
+ class TestServiceError(TestCase):
19
+ def test_can_be_raised(self) -> None:
20
+ with pytest.raises(ServiceError):
21
+ message = 'Test'
22
+ raise ServiceError(message)
23
+
24
+ def test_is_exception(self) -> None:
25
+ assert issubclass(ServiceError, Exception)
26
+
27
+ def test_message(self) -> None:
28
+ error = ServiceError('Test error message')
29
+
30
+ assert str(error) == 'Test error message'
31
+
32
+
33
+ class TestBaseDjangoModelService(TestCase):
34
+ def setUp(self) -> None:
35
+ self.user = User.objects.create_user(
36
+ username='testuser',
37
+ password='testpass' # noqa: S106
38
+ )
39
+
40
+ def test_get_concrete_fields_returns_dict(self) -> None:
41
+ service = UserService(self.user)
42
+
43
+ result = service._get_concrete_fields()
44
+
45
+ assert isinstance(result, dict)
46
+ assert 'username' in result
47
+ assert 'email' in result
48
+
49
+ def test_get_touched_fields_returns_list(self) -> None:
50
+ service = UserService(self.user)
51
+ concrete_fields = service._get_concrete_fields()
52
+
53
+ result = service._get_touched_fields(concrete_fields, username='newuser')
54
+
55
+ assert isinstance(result, list)
56
+
57
+ def test_get_touched_fields_sets_attribute(self) -> None:
58
+ service = UserService(self.user)
59
+ concrete_fields = service._get_concrete_fields()
60
+
61
+ service._get_touched_fields(concrete_fields, first_name='NewName')
62
+
63
+ assert service.obj.first_name == 'NewName'
64
+
65
+ def test_get_touched_fields_logs_warning_for_invalid_field(self) -> None:
66
+ service = UserService(self.user)
67
+ concrete_fields = service._get_concrete_fields()
68
+
69
+ with patch('django_spire.contrib.service.django_model_service.log') as mock_log:
70
+ service._get_touched_fields(concrete_fields, invalid_field='value')
71
+
72
+ mock_log.warning.assert_called_once()
73
+
74
+ def test_get_touched_fields_skips_auto_created_fields(self) -> None:
75
+ service = UserService(self.user)
76
+ concrete_fields = service._get_concrete_fields()
77
+
78
+ result = service._get_touched_fields(concrete_fields, id=999)
79
+
80
+ assert 'id' not in result
81
+
82
+ def test_has_get_concrete_fields_method(self) -> None:
83
+ assert hasattr(BaseDjangoModelService, '_get_concrete_fields')
84
+
85
+ def test_has_get_touched_fields_method(self) -> None:
86
+ assert hasattr(BaseDjangoModelService, '_get_touched_fields')
87
+
88
+ def test_has_save_model_obj_method(self) -> None:
89
+ assert hasattr(BaseDjangoModelService, 'save_model_obj')
90
+
91
+ def test_has_validate_model_obj_method(self) -> None:
92
+ assert hasattr(BaseDjangoModelService, 'validate_model_obj')
93
+
94
+ def test_save_model_obj_raises_error_without_field_data(self) -> None:
95
+ service = UserService(self.user)
96
+
97
+ with pytest.raises(ServiceError, match='Field data is required'):
98
+ service.save_model_obj()
99
+
100
+ def test_save_model_obj_returns_tuple(self) -> None:
101
+ service = UserService(self.user)
102
+
103
+ result = service.save_model_obj(first_name='Updated')
104
+
105
+ assert isinstance(result, tuple)
106
+ assert len(result) == 2
107
+
108
+ def test_save_model_obj_returns_model_and_bool(self) -> None:
109
+ service = UserService(self.user)
110
+
111
+ obj, created = service.save_model_obj(first_name='Updated')
112
+
113
+ assert isinstance(obj, User)
114
+ assert isinstance(created, bool)
115
+
116
+ def test_save_model_obj_updates_existing_model(self) -> None:
117
+ service = UserService(self.user)
118
+
119
+ obj, created = service.save_model_obj(first_name='UpdatedName')
120
+
121
+ assert created is False
122
+ assert obj.first_name == 'UpdatedName'
123
+
124
+ def test_save_model_obj_creates_new_model(self) -> None:
125
+ new_user = User(username='newuser', email='new@test.com')
126
+ service = UserService(new_user)
127
+
128
+ obj, created = service.save_model_obj(username='newuser', email='new@test.com')
129
+
130
+ assert created is True
131
+ assert obj.pk is not None
132
+
133
+ def test_save_model_obj_logs_warning_when_no_changes(self) -> None:
134
+ service = UserService(self.user)
135
+
136
+ with patch('django_spire.contrib.service.django_model_service.log') as mock_log:
137
+ service.save_model_obj(id=self.user.id)
138
+
139
+ mock_log.warning.assert_called_once()
140
+
141
+ def test_validate_model_obj_returns_touched_fields(self) -> None:
142
+ service = UserService(self.user)
143
+
144
+ result = service.validate_model_obj(first_name='Valid')
145
+
146
+ assert isinstance(result, list)
147
+ assert 'first_name' in result
148
+
149
+ def test_validate_model_obj_raises_validation_error(self) -> None:
150
+ service = UserService(self.user)
151
+
152
+ with pytest.raises(ValidationError):
153
+ service.validate_model_obj(email='invalid-email')
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from django.apps import AppConfig
2
4
 
3
5
 
@@ -1,34 +1,40 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import json
4
+
4
5
  from datetime import datetime, timedelta
5
- from typing import Any
6
+ from typing import TYPE_CHECKING
6
7
 
7
8
  from django.core.serializers.json import DjangoJSONEncoder
8
- from django.http import HttpRequest
9
+
10
+ if TYPE_CHECKING:
11
+ from typing import Any
12
+
13
+ from django.http import HttpRequest
9
14
 
10
15
 
11
16
  class SessionController:
12
17
  """
13
- This class provides an interface for storing, retrieving, and managing session data
14
- with automatic expiration. It handles session data under a specific key and supports
15
- timeout-based cleanup of expired sessions.
16
-
17
- Args:
18
- request (HttpRequest): The Django request object containing the session.
19
- session_key (str): Unique key under which the session data will be stored.
20
- seconds_till_expiry (int, optional): Number of seconds until session expiry.
21
- Defaults to 300 seconds (5 minutes).
18
+ This class provides an interface for storing, retrieving, and managing session data
19
+ with automatic expiration. It handles session data under a specific key and supports
20
+ timeout-based cleanup of expired sessions.
21
+
22
+ Args:
23
+ request (HttpRequest): The Django request object containing the session.
24
+ session_key (str): Unique key under which the session data will be stored.
25
+ seconds_till_expiry (int, optional): Number of seconds until session expiry.
26
+ Defaults to 300 seconds (5 minutes).
27
+
22
28
  """
23
29
 
24
30
  _TIMEOUT_KEY = '_timeout_datestamp'
25
31
 
26
32
  def __init__(
27
- self,
28
- request: HttpRequest,
29
- session_key: str,
30
- seconds_till_expiry: int = 60 * 5
31
- ):
33
+ self,
34
+ request: HttpRequest,
35
+ session_key: str,
36
+ seconds_till_expiry: int = 60 * 5
37
+ ) -> None:
32
38
  self.request = request
33
39
  self.session_key = session_key
34
40
  self.seconds_till_expiry = seconds_till_expiry
@@ -41,28 +47,44 @@ class SessionController:
41
47
  def __getitem__(self, key: str) -> Any:
42
48
  return self.data[key]
43
49
 
44
- def __setitem__(self, key: str, value: Any):
50
+ def __setitem__(self, key: str, value: Any) -> None:
45
51
  self.add_data(key, value)
46
52
 
47
- def add_data(self, key: str, data: Any):
53
+ def _clean(self) -> None:
54
+ if self._TIMEOUT_KEY in self.data and self.is_expired:
55
+ self.request.session.pop(self.session_key)
56
+ self._set_modified()
57
+
58
+ def _set_modified(self) -> None:
59
+ self.request.session.modified = True
60
+
61
+ def _set_timeout_datestamp(self) -> None:
62
+ timeout_datetime = datetime.now() + timedelta(seconds=self.seconds_till_expiry)
63
+ self.data[self._TIMEOUT_KEY] = timeout_datetime.timestamp()
64
+
65
+ def add_data(self, key: str, data: Any) -> None:
48
66
  self._session[key] = data
49
67
  self._set_timeout_datestamp()
50
68
  self._set_modified()
51
69
 
52
70
  @property
53
- def data(self):
71
+ def data(self) -> dict[str, Any]:
54
72
  return self._session
55
73
 
74
+ @property
75
+ def has_data(self) -> bool:
76
+ return bool(self.data)
77
+
56
78
  @property
57
79
  def is_expired(self) -> bool:
58
80
  current_timestamp = datetime.now().timestamp()
59
81
  return self.timeout_datestamp < current_timestamp
60
82
 
61
- @property
62
- def timeout_datestamp(self) -> float:
63
- return self.data.get(self._TIMEOUT_KEY, 0)
83
+ def purge(self) -> None:
84
+ self.request.session.pop(self.session_key)
85
+ self._set_modified()
64
86
 
65
- def remove_data(self, key: str):
87
+ def remove_data(self, key: str) -> None:
66
88
  self.data.pop(key)
67
89
  self._set_modified()
68
90
 
@@ -70,25 +92,9 @@ class SessionController:
70
92
  if self._TIMEOUT_KEY in self.data and len(self.data.keys()) == 1:
71
93
  self.data.pop(self._TIMEOUT_KEY)
72
94
 
73
- def purge(self):
74
- self.request.session.pop(self.session_key)
75
- self._set_modified()
76
-
77
- def _clean(self) -> None:
78
- if self._TIMEOUT_KEY in self.data and self.is_expired:
79
- self.request.session.pop(self.session_key)
80
- self._set_modified()
81
-
82
- def _set_modified(self):
83
- self.request.session.modified = True
84
-
85
95
  @property
86
- def has_data(self) -> bool:
87
- return bool(self.data)
88
-
89
- def _set_timeout_datestamp(self):
90
- timeout_datetime = datetime.now() + timedelta(seconds=self.seconds_till_expiry)
91
- self.data[self._TIMEOUT_KEY] = timeout_datetime.timestamp()
96
+ def timeout_datestamp(self) -> float:
97
+ return self.data.get(self._TIMEOUT_KEY, 0)
92
98
 
93
- def to_json(self):
99
+ def to_json(self) -> str:
94
100
  return json.dumps(self.data, cls=DjangoJSONEncoder)
@@ -1,15 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
1
5
  from django import template
2
6
  from django.utils.html import escapejs
3
7
  from django.utils.safestring import mark_safe
4
8
 
5
9
  from django_spire.contrib.session.controller import SessionController
6
10
 
11
+ if TYPE_CHECKING:
12
+ from django.template import Context
13
+ from django.utils.safestring import SafeString
14
+
15
+
7
16
  register = template.Library()
8
17
 
9
18
 
10
19
  @register.simple_tag(takes_context=True)
11
- def session_controller_to_json(context, key):
12
- request = context.get("request")
20
+ def session_controller_to_json(context: Context, key: str) -> SafeString:
21
+ request = context.get('request')
13
22
  controller = SessionController(request, key)
14
23
  data = controller.to_json()
15
24
  return mark_safe(escapejs(data))
@@ -1,18 +1,21 @@
1
+ from __future__ import annotations
2
+
1
3
  from datetime import datetime
4
+ from unittest.mock import MagicMock
2
5
 
3
- from django.test import TestCase, RequestFactory
4
6
  from django.contrib.sessions.middleware import SessionMiddleware
5
7
  from django.contrib.sessions.models import Session
8
+ from django.test import RequestFactory, TestCase
6
9
 
7
10
  from django_spire.contrib.session.controller import SessionController
11
+ from django_spire.contrib.session.templatetags.session_tags import session_controller_to_json
8
12
 
9
13
 
10
- class BaseSessionTestCase(TestCase):
11
- def setUp(self):
14
+ class TestSessionController(TestCase):
15
+ def setUp(self) -> None:
12
16
  self.factory = RequestFactory()
13
17
  self.request = self.factory.get('/')
14
18
 
15
- # Attach real session
16
19
  middleware = SessionMiddleware(lambda req: None)
17
20
  middleware.process_request(self.request)
18
21
 
@@ -21,85 +24,146 @@ class BaseSessionTestCase(TestCase):
21
24
  session_key='shopping_cart'
22
25
  )
23
26
 
24
- def test_base_session_add_data(self):
27
+ def test_add_data(self) -> None:
25
28
  key = 'currency'
26
29
  value = 'CAD'
27
30
 
28
31
  self.shopping_cart_session.add_data(key, value)
29
- self.assertIn(key, self.shopping_cart_session.data)
30
- self.assertEqual(self.shopping_cart_session.data[key], value)
31
32
 
32
- def test_base_session_set_item(self):
33
- key = 'currency'
34
- value = 'CAD'
33
+ assert key in self.shopping_cart_session.data
34
+ assert self.shopping_cart_session.data[key] == value
35
35
 
36
- self.shopping_cart_session[key] = value
37
- self.assertIn(key, self.shopping_cart_session.data)
38
- self.assertEqual(self.shopping_cart_session.data[key], value)
36
+ def test_data_property_returns_dict(self) -> None:
37
+ assert isinstance(self.shopping_cart_session.data, dict)
39
38
 
40
- def test_base_session_remove_data(self):
39
+ def test_getitem(self) -> None:
41
40
  self.shopping_cart_session.add_data('currency', 'CAD')
42
- self.shopping_cart_session.add_data('discount', None)
43
- self.shopping_cart_session.remove_data('currency')
44
41
 
45
- self.assertNotIn('currency', self.shopping_cart_session.data)
46
- self.assertIn('discount', self.shopping_cart_session.data)
42
+ assert self.shopping_cart_session['currency'] == 'CAD'
47
43
 
48
- def test_set_session_modified(self):
44
+ def test_has_data_false_when_empty(self) -> None:
45
+ assert self.shopping_cart_session.has_data is False
46
+
47
+ def test_has_data_true_when_data_exists(self) -> None:
49
48
  self.shopping_cart_session.add_data('currency', 'CAD')
50
- self.assertTrue(self.request.session.modified)
51
49
 
52
- def test_session_has_data(self):
53
- self.assertFalse(self.shopping_cart_session.has_data)
50
+ assert self.shopping_cart_session.has_data is True
54
51
 
52
+ def test_has_data_false_after_remove(self) -> None:
55
53
  self.shopping_cart_session.add_data('currency', 'CAD')
56
- self.assertTrue(self.shopping_cart_session.has_data)
54
+ self.shopping_cart_session.remove_data('currency')
57
55
 
56
+ assert self.shopping_cart_session.has_data is False
58
57
 
59
- self.shopping_cart_session.remove_data('currency')
60
- self.assertFalse(self.shopping_cart_session.has_data)
58
+ def test_is_expired_false_when_not_expired(self) -> None:
59
+ self.shopping_cart_session.add_data('currency', 'CAD')
61
60
 
62
- def test_session_data_to_json(self):
61
+ assert self.shopping_cart_session.is_expired is False
62
+
63
+ def test_is_expired_true_when_expired(self) -> None:
63
64
  self.shopping_cart_session.add_data('currency', 'CAD')
64
- self.shopping_cart_session.add_data('discount', None)
65
+ self.shopping_cart_session.seconds_till_expiry = -5
66
+ self.shopping_cart_session._set_timeout_datestamp()
65
67
 
66
- self.assertTrue(isinstance(self.shopping_cart_session.to_json(), str))
68
+ assert self.shopping_cart_session.is_expired is True
67
69
 
68
- # def test_to_json_errors_when_not_json_serializable(self):
69
- # self.shopping_cart_session.json_serializable = False
70
- #
71
- # with self.assertRaises(ValueError):
72
- # self.shopping_cart_session.to_json()
70
+ def test_purge_removes_session_data(self) -> None:
71
+ self.shopping_cart_session.add_data('currency', 'CAD')
72
+ self.shopping_cart_session.purge()
73
73
 
74
- def test_set_timeout_datestamp(self):
75
- self.shopping_cart_session._set_timeout_datestamp()
76
- self.assertIn('_timeout_datestamp', self.shopping_cart_session.data)
77
- self.assertTrue(datetime.now().timestamp() < self.shopping_cart_session['_timeout_datestamp'])
74
+ assert 'shopping_cart' not in self.request.session
78
75
 
79
- def test_session_data_expiry(self):
76
+ def test_remove_data(self) -> None:
80
77
  self.shopping_cart_session.add_data('currency', 'CAD')
81
- self.assertFalse(self.shopping_cart_session.is_expired)
78
+ self.shopping_cart_session.add_data('discount', None)
79
+ self.shopping_cart_session.remove_data('currency')
82
80
 
83
- self.shopping_cart_session.seconds_till_expiry = -5
84
- self.shopping_cart_session._set_timeout_datestamp()
85
- self.assertTrue(self.shopping_cart_session.is_expired)
81
+ assert 'currency' not in self.shopping_cart_session.data
82
+ assert 'discount' in self.shopping_cart_session.data
86
83
 
87
- def test_session_data_added_to_database(self):
88
- # Add something to the session
89
- self.shopping_cart_session.add_data('favorites', ['cool_shirt', 'fun_hat'] )
84
+ def test_session_data_added_to_database(self) -> None:
85
+ self.shopping_cart_session.add_data('favorites', ['cool_shirt', 'fun_hat'])
90
86
  self.request.session.save()
91
87
 
92
- # Get session from DB using session_key
93
88
  session_key = self.request.session.session_key
94
89
  session_in_db = Session.objects.get(session_key=session_key)
95
-
96
- # Decode the session data
97
90
  session_data = session_in_db.get_decoded()
98
91
 
99
- self.assertIn(self.shopping_cart_session.session_key, session_data)
100
- self.assertIn('favorites', session_data[self.shopping_cart_session.session_key])
101
- self.assertEqual(
102
- session_data[self.shopping_cart_session.session_key]['favorites'],
103
- ['cool_shirt', 'fun_hat']
92
+ assert self.shopping_cart_session.session_key in session_data
93
+ assert 'favorites' in session_data[self.shopping_cart_session.session_key]
94
+ assert session_data[self.shopping_cart_session.session_key]['favorites'] == ['cool_shirt', 'fun_hat']
95
+
96
+ def test_set_modified(self) -> None:
97
+ self.shopping_cart_session.add_data('currency', 'CAD')
98
+
99
+ assert self.request.session.modified is True
100
+
101
+ def test_set_timeout_datestamp(self) -> None:
102
+ self.shopping_cart_session._set_timeout_datestamp()
103
+
104
+ assert '_timeout_datestamp' in self.shopping_cart_session.data
105
+ assert datetime.now().timestamp() < self.shopping_cart_session['_timeout_datestamp']
106
+
107
+ def test_setitem(self) -> None:
108
+ key = 'currency'
109
+ value = 'CAD'
110
+
111
+ self.shopping_cart_session[key] = value
112
+
113
+ assert key in self.shopping_cart_session.data
114
+ assert self.shopping_cart_session.data[key] == value
115
+
116
+ def test_timeout_datestamp_returns_float(self) -> None:
117
+ self.shopping_cart_session._set_timeout_datestamp()
118
+
119
+ assert isinstance(self.shopping_cart_session.timeout_datestamp, float)
120
+
121
+ def test_timeout_datestamp_returns_zero_when_not_set(self) -> None:
122
+ controller = SessionController(
123
+ request=self.request,
124
+ session_key='new_session'
104
125
  )
105
126
 
127
+ assert controller.timeout_datestamp == 0
128
+
129
+ def test_to_json_returns_string(self) -> None:
130
+ self.shopping_cart_session.add_data('currency', 'CAD')
131
+ self.shopping_cart_session.add_data('discount', None)
132
+
133
+ assert isinstance(self.shopping_cart_session.to_json(), str)
134
+
135
+
136
+ class TestSessionControllerToJson(TestCase):
137
+ def setUp(self) -> None:
138
+ self.factory = RequestFactory()
139
+
140
+ def _create_context_with_session(self) -> MagicMock:
141
+ request = self.factory.get('/')
142
+ middleware = SessionMiddleware(lambda req: None)
143
+ middleware.process_request(request)
144
+
145
+ context = MagicMock()
146
+ context.get.return_value = request
147
+
148
+ return context
149
+
150
+ def test_returns_escaped_json(self) -> None:
151
+ context = self._create_context_with_session()
152
+
153
+ result = session_controller_to_json(context, 'test_key')
154
+
155
+ assert isinstance(result, str)
156
+
157
+ def test_calls_context_get_with_request(self) -> None:
158
+ context = self._create_context_with_session()
159
+
160
+ session_controller_to_json(context, 'test_key')
161
+
162
+ context.get.assert_called_once_with('request')
163
+
164
+ def test_returns_safe_string(self) -> None:
165
+ context = self._create_context_with_session()
166
+
167
+ result = session_controller_to_json(context, 'test_key')
168
+
169
+ assert hasattr(result, '__html__')
File without changes
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ from django.test import TestCase
4
+
5
+ from django_spire.contrib.utils import truncate_string
6
+
7
+
8
+ class TestTruncateString(TestCase):
9
+ def test_returns_original_when_equal_to_length(self) -> None:
10
+ result = truncate_string('hello', 5)
11
+
12
+ assert result == 'hello'
13
+
14
+ def test_returns_original_when_shorter_than_length(self) -> None:
15
+ result = truncate_string('hi', 10)
16
+
17
+ assert result == 'hi'
18
+
19
+ def test_truncates_when_longer_than_length(self) -> None:
20
+ result = truncate_string('hello world', 8)
21
+
22
+ assert result == 'hello...'
23
+
24
+ def test_truncates_with_ellipsis(self) -> None:
25
+ result = truncate_string('this is a long string', 10)
26
+
27
+ assert result.endswith('...')
28
+
29
+ def test_truncated_length_equals_specified_length(self) -> None:
30
+ result = truncate_string('this is a long string', 10)
31
+
32
+ assert len(result) == 10
33
+
34
+ def test_empty_string(self) -> None:
35
+ result = truncate_string('', 5)
36
+
37
+ assert result == ''
@@ -1,2 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+
1
4
  def truncate_string(string: str, length: int) -> str:
2
- return string[:(length - 3)] + '...' if len(string) > length else string
5
+ return string[:(length - 3)] + '...' if len(string) > length else string
django_spire/core/apps.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from django.apps import AppConfig
2
4
 
3
5