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
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ from django.test import TestCase
4
+
5
+ from django_spire.contrib.seeding.intelligence.intel import SeedingIntel, SourceIntel
6
+
7
+
8
+ class TestSeedingIntel(TestCase):
9
+ def test_is_iterable(self) -> None:
10
+ intel = SeedingIntel(items=[{'name': 'test1'}, {'name': 'test2'}])
11
+
12
+ result = list(intel)
13
+
14
+ assert result == [{'name': 'test1'}, {'name': 'test2'}]
15
+
16
+ def test_items_attribute(self) -> None:
17
+ items = [{'name': 'test'}]
18
+ intel = SeedingIntel(items=items)
19
+
20
+ assert intel.items == items
21
+
22
+
23
+ class TestSourceIntel(TestCase):
24
+ def test_file_name_attribute(self) -> None:
25
+ intel = SourceIntel(file_name='test.py', python_source_code='print("hello")')
26
+
27
+ assert intel.file_name == 'test.py'
28
+
29
+ def test_python_source_code_attribute(self) -> None:
30
+ intel = SourceIntel(file_name='test.py', python_source_code='print("hello")')
31
+
32
+ assert intel.python_source_code == 'print("hello")'
@@ -0,0 +1,63 @@
1
+ from __future__ import annotations
2
+
3
+ from unittest.mock import MagicMock
4
+
5
+ from django.test import TestCase
6
+
7
+ from django_spire.contrib.seeding.field.override import FieldOverride
8
+
9
+
10
+ class TestFieldOverride(TestCase):
11
+ def setUp(self) -> None:
12
+ self.seeder_class = MagicMock()
13
+ self.seeder_class.seed.return_value = [{'name': 'test'}]
14
+
15
+ def test_filter_returns_self(self) -> None:
16
+ override = FieldOverride(self.seeder_class)
17
+
18
+ result = override.filter(name='test')
19
+
20
+ assert result is override
21
+
22
+ def test_filter_updates_overrides(self) -> None:
23
+ override = FieldOverride(self.seeder_class)
24
+
25
+ override.filter(name='test', status='active')
26
+
27
+ assert override.overrides == {'name': 'test', 'status': 'active'}
28
+
29
+ def test_getattr_delegates_to_seeder_class(self) -> None:
30
+ self.seeder_class.some_attribute = 'value'
31
+ override = FieldOverride(self.seeder_class)
32
+
33
+ result = override.some_attribute
34
+
35
+ assert result == 'value'
36
+
37
+ def test_init_sets_seeder_class(self) -> None:
38
+ override = FieldOverride(self.seeder_class)
39
+
40
+ assert override.seeder_class is self.seeder_class
41
+
42
+ def test_init_sets_empty_overrides(self) -> None:
43
+ override = FieldOverride(self.seeder_class)
44
+
45
+ assert override.overrides == {}
46
+
47
+ def test_seed_calls_seeder_class_seed(self) -> None:
48
+ override = FieldOverride(self.seeder_class)
49
+ override.filter(name='override_value')
50
+
51
+ override.seed(count=2)
52
+
53
+ self.seeder_class.seed.assert_called_once_with(
54
+ count=2,
55
+ fields={'name': 'override_value'}
56
+ )
57
+
58
+ def test_seed_returns_seeder_class_result(self) -> None:
59
+ override = FieldOverride(self.seeder_class)
60
+
61
+ result = override.seed(count=1)
62
+
63
+ assert result == [{'name': 'test'}]
@@ -1,7 +1,7 @@
1
- from django_spire.contrib.service.exceptions import ServiceException
1
+ from django_spire.contrib.service.exceptions import ServiceError
2
2
  from django_spire.contrib.service.django_model_service import BaseDjangoModelService
3
3
 
4
4
  __all__ = [
5
5
  'BaseDjangoModelService',
6
- 'ServiceException',
6
+ 'ServiceError',
7
7
  ]
@@ -1,22 +1,26 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
+
4
5
  from abc import ABC
5
- from typing import Type, Generic, TypeVar
6
+ from typing import Generic, TypeVar
6
7
 
7
8
  from django.db import transaction
8
9
  from django.db.models import Model
9
10
 
10
11
  from django_spire.contrib.constructor.django_model_constructor import BaseDjangoModelConstructor
11
- from django_spire.contrib.service.exceptions import ServiceException
12
+ from django_spire.contrib.service.exceptions import ServiceError
13
+
14
+
15
+ log = logging.getLogger(__name__)
12
16
 
13
- TypeDjangoModel = TypeVar('TypeDjangoModel', bound=Model, covariant=True)
17
+ TypeDjangoModel_co = TypeVar('TypeDjangoModel_co', bound=Model, covariant=True)
14
18
 
15
19
 
16
20
  class BaseDjangoModelService(
17
- BaseDjangoModelConstructor[TypeDjangoModel],
21
+ BaseDjangoModelConstructor[TypeDjangoModel_co],
18
22
  ABC,
19
- Generic[TypeDjangoModel]
23
+ Generic[TypeDjangoModel_co]
20
24
  ):
21
25
  def _get_concrete_fields(self) -> dict:
22
26
  return {
@@ -33,7 +37,7 @@ class BaseDjangoModelService(
33
37
 
34
38
  for field, value in field_data.items():
35
39
  if field not in allowed:
36
- logging.warning(f'Field {field!r} is not valid for {self.obj.__class__.__name__}')
40
+ log.warning(f'Field {field!r} is not valid for {self.obj.__class__.__name__}')
37
41
  continue
38
42
 
39
43
  model_field = concrete_fields.get(field.removesuffix("_id"), None)
@@ -51,12 +55,8 @@ class BaseDjangoModelService(
51
55
  concrete_fields = self._get_concrete_fields()
52
56
  touched_fields = self._get_touched_fields(concrete_fields, **field_data)
53
57
 
54
- try:
55
- self.obj.full_clean(
56
- exclude=[field for field in concrete_fields if field not in touched_fields]
57
- )
58
- except:
59
- raise
58
+ exclude = [field for field in concrete_fields if field not in touched_fields]
59
+ self.obj.full_clean(exclude=exclude)
60
60
 
61
61
  return touched_fields
62
62
 
@@ -65,7 +65,8 @@ class BaseDjangoModelService(
65
65
  new_model_obj_was_created = False
66
66
 
67
67
  if not field_data:
68
- raise ServiceException(f'Field data is required to save on {self.obj.__class__.__name__}')
68
+ message = f'Field data is required to save on {self.obj.__class__.__name__}'
69
+ raise ServiceError(message)
69
70
 
70
71
  touched_fields = self.validate_model_obj(**field_data)
71
72
 
@@ -77,7 +78,7 @@ class BaseDjangoModelService(
77
78
  self.obj.save(update_fields=touched_fields)
78
79
 
79
80
  else:
80
- logging.warning(
81
- f'{self.obj.__class__.__name__} is not a new object or there was no touched fields to update.')
81
+ message = f'{self.obj.__class__.__name__} is not a new object or there was no touched fields to update.'
82
+ log.warning(message)
82
83
 
83
84
  return self.obj, new_model_obj_was_created
@@ -1,5 +1,7 @@
1
- from django_spire.exceptions import DjangoSpireException
1
+ from __future__ import annotations
2
2
 
3
+ from django_spire.exceptions import DjangoSpireError
3
4
 
4
- class ServiceException(DjangoSpireException):
5
- pass
5
+
6
+ class ServiceError(DjangoSpireError):
7
+ pass
File without changes
@@ -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))