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,406 @@
1
+ from __future__ import annotations
2
+
3
+ from django.contrib.auth import get_user
4
+ from django.urls import reverse
5
+
6
+ from django_spire.auth.user.tests.factories import create_user
7
+ from django_spire.core.tests.test_cases import BaseTestCase
8
+
9
+
10
+ class LoginViewTestCase(BaseTestCase):
11
+ def setUp(self) -> None:
12
+ super().setUp()
13
+
14
+ self.client.logout()
15
+ self.user = create_user(username='testuser', email='test@example.com')
16
+ self.user.set_password('testpassword123')
17
+ self.user.save()
18
+
19
+ def test_login_page_loads(self) -> None:
20
+ response = self.client.get(reverse('django_spire:auth:admin:login'))
21
+ assert response.status_code == 200
22
+
23
+ def test_login_with_valid_credentials(self) -> None:
24
+ response = self.client.post(
25
+ reverse('django_spire:auth:admin:login'),
26
+ data={'username': 'testuser', 'password': 'testpassword123'}
27
+ )
28
+ assert response.status_code == 302
29
+
30
+ def test_login_with_invalid_credentials(self) -> None:
31
+ response = self.client.post(
32
+ reverse('django_spire:auth:admin:login'),
33
+ data={'username': 'testuser', 'password': 'wrongpassword'}
34
+ )
35
+ assert response.status_code == 200
36
+
37
+ def test_login_with_nonexistent_user(self) -> None:
38
+ response = self.client.post(
39
+ reverse('django_spire:auth:admin:login'),
40
+ data={'username': 'nonexistent', 'password': 'password'}
41
+ )
42
+ assert response.status_code == 200
43
+
44
+ def test_login_with_empty_username(self) -> None:
45
+ response = self.client.post(
46
+ reverse('django_spire:auth:admin:login'),
47
+ data={'username': '', 'password': 'testpassword123'}
48
+ )
49
+ assert response.status_code == 200
50
+
51
+ def test_login_with_empty_password(self) -> None:
52
+ response = self.client.post(
53
+ reverse('django_spire:auth:admin:login'),
54
+ data={'username': 'testuser', 'password': ''}
55
+ )
56
+ assert response.status_code == 200
57
+
58
+ def test_login_inactive_user(self) -> None:
59
+ self.user.is_active = False
60
+ self.user.save()
61
+ response = self.client.post(
62
+ reverse('django_spire:auth:admin:login'),
63
+ data={'username': 'testuser', 'password': 'testpassword123'}
64
+ )
65
+ assert response.status_code == 200
66
+
67
+ def test_login_with_whitespace_username(self) -> None:
68
+ response = self.client.post(
69
+ reverse('django_spire:auth:admin:login'),
70
+ data={'username': ' ', 'password': 'testpassword123'}
71
+ )
72
+ assert response.status_code == 200
73
+
74
+ def test_login_case_sensitive_username(self) -> None:
75
+ response = self.client.post(
76
+ reverse('django_spire:auth:admin:login'),
77
+ data={'username': 'TESTUSER', 'password': 'testpassword123'}
78
+ )
79
+ assert response.status_code == 200
80
+
81
+ def test_login_case_sensitive_password(self) -> None:
82
+ response = self.client.post(
83
+ reverse('django_spire:auth:admin:login'),
84
+ data={'username': 'testuser', 'password': 'TESTPASSWORD123'}
85
+ )
86
+ assert response.status_code == 200
87
+
88
+ def test_login_sets_session(self) -> None:
89
+ self.client.post(
90
+ reverse('django_spire:auth:admin:login'),
91
+ data={'username': 'testuser', 'password': 'testpassword123'}
92
+ )
93
+ user = get_user(self.client)
94
+ assert user.is_authenticated
95
+
96
+ def test_login_with_sql_injection_attempt(self) -> None:
97
+ response = self.client.post(
98
+ reverse('django_spire:auth:admin:login'),
99
+ data={'username': "'; DROP TABLE users; --", 'password': 'password'}
100
+ )
101
+ assert response.status_code == 200
102
+
103
+ def test_login_with_xss_attempt(self) -> None:
104
+ response = self.client.post(
105
+ reverse('django_spire:auth:admin:login'),
106
+ data={'username': '<script>alert("xss")</script>', 'password': 'password'}
107
+ )
108
+ assert response.status_code == 200
109
+
110
+ def test_login_with_very_long_username(self) -> None:
111
+ response = self.client.post(
112
+ reverse('django_spire:auth:admin:login'),
113
+ data={'username': 'a' * 1000, 'password': 'password'}
114
+ )
115
+ assert response.status_code == 200
116
+
117
+ def test_login_with_very_long_password(self) -> None:
118
+ response = self.client.post(
119
+ reverse('django_spire:auth:admin:login'),
120
+ data={'username': 'testuser', 'password': 'a' * 1000}
121
+ )
122
+ assert response.status_code == 200
123
+
124
+ def test_login_with_unicode_username(self) -> None:
125
+ response = self.client.post(
126
+ reverse('django_spire:auth:admin:login'),
127
+ data={'username': 'tëstüsér', 'password': 'password'}
128
+ )
129
+ assert response.status_code == 200
130
+
131
+ def test_login_with_unicode_password(self) -> None:
132
+ response = self.client.post(
133
+ reverse('django_spire:auth:admin:login'),
134
+ data={'username': 'testuser', 'password': 'pässwörd'}
135
+ )
136
+ assert response.status_code == 200
137
+
138
+ def test_login_get_request_does_not_authenticate(self) -> None:
139
+ response = self.client.get(
140
+ reverse('django_spire:auth:admin:login'),
141
+ data={'username': 'testuser', 'password': 'testpassword123'}
142
+ )
143
+ user = get_user(self.client)
144
+ assert not user.is_authenticated
145
+ assert response.status_code == 200
146
+
147
+ def test_login_with_null_bytes(self) -> None:
148
+ response = self.client.post(
149
+ reverse('django_spire:auth:admin:login'),
150
+ data={'username': 'test\x00user', 'password': 'password'}
151
+ )
152
+ assert response.status_code == 200
153
+
154
+ def test_login_with_newline_in_username(self) -> None:
155
+ response = self.client.post(
156
+ reverse('django_spire:auth:admin:login'),
157
+ data={'username': 'test\nuser', 'password': 'password'}
158
+ )
159
+ assert response.status_code == 200
160
+
161
+ def test_login_with_tab_in_username(self) -> None:
162
+ response = self.client.post(
163
+ reverse('django_spire:auth:admin:login'),
164
+ data={'username': 'test\tuser', 'password': 'password'}
165
+ )
166
+ assert response.status_code == 200
167
+
168
+
169
+ class PasswordChangeViewTestCase(BaseTestCase):
170
+ def setUp(self) -> None:
171
+ super().setUp()
172
+
173
+ self.user = create_user(username='testuser')
174
+ self.user.set_password('oldpassword123')
175
+ self.user.save()
176
+ self.client.force_login(self.user)
177
+
178
+ def test_password_change_page_loads(self) -> None:
179
+ response = self.client.get(reverse('django_spire:auth:admin:password_change'))
180
+ assert response.status_code == 200
181
+
182
+ def test_password_change_success(self) -> None:
183
+ response = self.client.post(
184
+ reverse('django_spire:auth:admin:password_change'),
185
+ data={
186
+ 'old_password': 'oldpassword123',
187
+ 'new_password1': 'newpassword456',
188
+ 'new_password2': 'newpassword456'
189
+ }
190
+ )
191
+ assert response.status_code == 302
192
+ self.user.refresh_from_db()
193
+ assert self.user.check_password('newpassword456')
194
+
195
+ def test_password_change_wrong_old_password(self) -> None:
196
+ response = self.client.post(
197
+ reverse('django_spire:auth:admin:password_change'),
198
+ data={
199
+ 'old_password': 'wrongoldpassword',
200
+ 'new_password1': 'newpassword456',
201
+ 'new_password2': 'newpassword456'
202
+ }
203
+ )
204
+ assert response.status_code == 200
205
+ self.user.refresh_from_db()
206
+ assert self.user.check_password('oldpassword123')
207
+
208
+ def test_password_change_mismatched_passwords(self) -> None:
209
+ response = self.client.post(
210
+ reverse('django_spire:auth:admin:password_change'),
211
+ data={
212
+ 'old_password': 'oldpassword123',
213
+ 'new_password1': 'newpassword456',
214
+ 'new_password2': 'differentpassword789'
215
+ }
216
+ )
217
+ assert response.status_code == 200
218
+
219
+ def test_password_change_empty_new_password(self) -> None:
220
+ response = self.client.post(
221
+ reverse('django_spire:auth:admin:password_change'),
222
+ data={
223
+ 'old_password': 'oldpassword123',
224
+ 'new_password1': '',
225
+ 'new_password2': ''
226
+ }
227
+ )
228
+ assert response.status_code == 200
229
+
230
+ def test_password_change_requires_login(self) -> None:
231
+ self.client.logout()
232
+ response = self.client.get(reverse('django_spire:auth:admin:password_change'))
233
+ assert response.status_code == 302
234
+
235
+ def test_password_change_same_as_old(self) -> None:
236
+ response = self.client.post(
237
+ reverse('django_spire:auth:admin:password_change'),
238
+ data={
239
+ 'old_password': 'oldpassword123',
240
+ 'new_password1': 'oldpassword123',
241
+ 'new_password2': 'oldpassword123'
242
+ }
243
+ )
244
+ assert response.status_code in (200, 302)
245
+
246
+ def test_password_change_with_unicode(self) -> None:
247
+ response = self.client.post(
248
+ reverse('django_spire:auth:admin:password_change'),
249
+ data={
250
+ 'old_password': 'oldpassword123',
251
+ 'new_password1': 'nëwpässwörd456',
252
+ 'new_password2': 'nëwpässwörd456'
253
+ }
254
+ )
255
+ assert response.status_code == 302
256
+ self.user.refresh_from_db()
257
+ assert self.user.check_password('nëwpässwörd456')
258
+
259
+ def test_password_change_very_long_password(self) -> None:
260
+ long_password = 'a' * 128
261
+ response = self.client.post(
262
+ reverse('django_spire:auth:admin:password_change'),
263
+ data={
264
+ 'old_password': 'oldpassword123',
265
+ 'new_password1': long_password,
266
+ 'new_password2': long_password
267
+ }
268
+ )
269
+ assert response.status_code == 302
270
+
271
+ def test_password_change_with_spaces(self) -> None:
272
+ response = self.client.post(
273
+ reverse('django_spire:auth:admin:password_change'),
274
+ data={
275
+ 'old_password': 'oldpassword123',
276
+ 'new_password1': 'new password 456',
277
+ 'new_password2': 'new password 456'
278
+ }
279
+ )
280
+ assert response.status_code == 302
281
+ self.user.refresh_from_db()
282
+ assert self.user.check_password('new password 456')
283
+
284
+ def test_password_change_preserves_session(self) -> None:
285
+ self.client.post(
286
+ reverse('django_spire:auth:admin:password_change'),
287
+ data={
288
+ 'old_password': 'oldpassword123',
289
+ 'new_password1': 'newpassword456',
290
+ 'new_password2': 'newpassword456'
291
+ }
292
+ )
293
+ user = get_user(self.client)
294
+ assert user.is_authenticated
295
+
296
+
297
+ class PasswordChangeDoneViewTestCase(BaseTestCase):
298
+ def test_password_change_done_page_loads(self) -> None:
299
+ response = self.client.get(reverse('django_spire:auth:admin:password_change_done'))
300
+ assert response.status_code == 200
301
+
302
+
303
+ class PasswordResetViewTestCase(BaseTestCase):
304
+ def setUp(self) -> None:
305
+ super().setUp()
306
+
307
+ self.user = create_user(username='testuser', email='test@example.com')
308
+
309
+ def test_password_reset_page_loads(self) -> None:
310
+ response = self.client.get(reverse('django_spire:auth:admin:password_reset'))
311
+ assert response.status_code == 200
312
+
313
+ def test_password_reset_with_valid_email(self) -> None:
314
+ response = self.client.post(
315
+ reverse('django_spire:auth:admin:password_reset'),
316
+ data={'email': 'test@example.com'}
317
+ )
318
+ assert response.status_code == 302
319
+
320
+ def test_password_reset_with_nonexistent_email(self) -> None:
321
+ response = self.client.post(
322
+ reverse('django_spire:auth:admin:password_reset'),
323
+ data={'email': 'nonexistent@example.com'}
324
+ )
325
+ assert response.status_code == 302
326
+
327
+ def test_password_reset_with_empty_email(self) -> None:
328
+ response = self.client.post(
329
+ reverse('django_spire:auth:admin:password_reset'),
330
+ data={'email': ''}
331
+ )
332
+ assert response.status_code == 200
333
+
334
+ def test_password_reset_with_invalid_email(self) -> None:
335
+ response = self.client.post(
336
+ reverse('django_spire:auth:admin:password_reset'),
337
+ data={'email': 'invalid-email'}
338
+ )
339
+ assert response.status_code == 200
340
+
341
+ def test_password_reset_with_uppercase_email(self) -> None:
342
+ response = self.client.post(
343
+ reverse('django_spire:auth:admin:password_reset'),
344
+ data={'email': 'TEST@EXAMPLE.COM'}
345
+ )
346
+ assert response.status_code == 302
347
+
348
+ def test_password_reset_with_mixed_case_email(self) -> None:
349
+ response = self.client.post(
350
+ reverse('django_spire:auth:admin:password_reset'),
351
+ data={'email': 'Test@Example.Com'}
352
+ )
353
+ assert response.status_code == 302
354
+
355
+ def test_password_reset_with_whitespace_email(self) -> None:
356
+ response = self.client.post(
357
+ reverse('django_spire:auth:admin:password_reset'),
358
+ data={'email': ' test@example.com '}
359
+ )
360
+ assert response.status_code in (200, 302)
361
+
362
+ def test_password_reset_inactive_user(self) -> None:
363
+ self.user.is_active = False
364
+ self.user.save()
365
+ response = self.client.post(
366
+ reverse('django_spire:auth:admin:password_reset'),
367
+ data={'email': 'test@example.com'}
368
+ )
369
+ assert response.status_code == 302
370
+
371
+
372
+ class PasswordResetDoneViewTestCase(BaseTestCase):
373
+ def test_password_reset_done_page_loads(self) -> None:
374
+ response = self.client.get(reverse('django_spire:auth:admin:password_reset_done'))
375
+ assert response.status_code == 200
376
+
377
+
378
+ class LogoutRedirectViewTestCase(BaseTestCase):
379
+ def test_logout_redirects(self) -> None:
380
+ response = self.client.get(reverse('django_spire:auth:redirect:logout'))
381
+ assert response.status_code == 302
382
+
383
+ def test_logout_logs_out_user(self) -> None:
384
+ self.client.get(reverse('django_spire:auth:redirect:logout'))
385
+ response = self.client.get(reverse('django_spire:auth:admin:password_change'))
386
+ assert response.status_code != 200
387
+
388
+ def test_logout_redirects_to_login(self) -> None:
389
+ response = self.client.get(reverse('django_spire:auth:redirect:logout'))
390
+ assert 'login' in response.url.lower()
391
+
392
+ def test_logout_clears_session(self) -> None:
393
+ self.client.get(reverse('django_spire:auth:redirect:logout'))
394
+ user = get_user(self.client)
395
+ assert not user.is_authenticated
396
+
397
+
398
+ class LoginRedirectViewTestCase(BaseTestCase):
399
+ def test_login_redirect_for_authenticated_user(self) -> None:
400
+ response = self.client.get(reverse('django_spire:auth:redirect:login'))
401
+ assert response.status_code == 302
402
+
403
+ def test_login_redirect_for_unauthenticated_user(self) -> None:
404
+ self.client.logout()
405
+ response = self.client.get(reverse('django_spire:auth:redirect:login'))
406
+ assert response.status_code == 302
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from django.urls import path
2
4
 
3
5
  from django_spire.auth.views import admin_views
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from django.urls import path
2
4
 
3
5
  from django_spire.auth.views import redirect_views
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from django.apps import AppConfig
2
4
 
3
5
  from django_spire.utils import check_required_apps
@@ -69,6 +69,15 @@ class EditUserForm(forms.ModelForm):
69
69
 
70
70
 
71
71
  class RegisterUserForm(forms.ModelForm):
72
+ def clean_email(self) -> str:
73
+ email = self.cleaned_data.get('email')
74
+
75
+ if not email:
76
+ message = 'Email is required.'
77
+ raise forms.ValidationError(message)
78
+
79
+ return email
80
+
72
81
  def clean_password(self) -> str:
73
82
  password = self.cleaned_data.get('password')
74
83
 
@@ -14,7 +14,7 @@ class AuthUser(User, ActivityMixin):
14
14
  @classmethod
15
15
  def base_breadcrumb(cls) -> Breadcrumbs:
16
16
  crumbs = Breadcrumbs()
17
- crumbs.add_breadcrumb('Users', reverse('django_spire:auth:user:list'))
17
+ crumbs.add_breadcrumb('Users', reverse('django_spire:auth:user:page:list'))
18
18
  return crumbs
19
19
 
20
20
  def breadcrumbs(self) -> Breadcrumbs:
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  from typing import TYPE_CHECKING
3
4
 
4
5
  from django_spire.contrib.service import BaseDjangoModelService
@@ -7,7 +7,7 @@ def create_user(
7
7
  username: str,
8
8
  **kwargs
9
9
  ) -> AuthUser:
10
- user, created = AuthUser.objects.get_or_create(
10
+ user, _ = AuthUser.objects.get_or_create(
11
11
  username=username,
12
12
  defaults={
13
13
  **kwargs
@@ -18,20 +18,21 @@ def create_user(
18
18
 
19
19
 
20
20
  def create_super_user(password: str = 'stratus', **kwargs) -> AuthUser:
21
- user, created = AuthUser.objects.get_or_create(
21
+ defaults = {
22
+ 'email': 'bobert@stratusadv.com',
23
+ 'first_name': 'Bob',
24
+ 'last_name': 'Robertson',
25
+ 'is_superuser': True,
26
+ 'is_staff': True,
27
+ **kwargs
28
+ }
29
+
30
+ user, _ = AuthUser.objects.update_or_create(
22
31
  username='stratus',
23
- defaults={
24
- 'email': 'bobert@stratusadv.com',
25
- 'first_name': 'Bob',
26
- 'last_name': 'Robertson',
27
- 'is_superuser': True,
28
- 'is_staff': True,
29
- **kwargs
30
- }
32
+ defaults=defaults
31
33
  )
32
34
 
33
- if created:
34
- user.set_password(password)
35
- user.save()
35
+ user.set_password(password)
36
+ user.save()
36
37
 
37
38
  return user
@@ -1,7 +1,171 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from django_spire.auth.user.models import AuthUser
4
+ from django_spire.auth.user.tests.factories import create_super_user, create_user
3
5
  from django_spire.core.tests.test_cases import BaseTestCase
4
6
 
5
7
 
6
- class UserAccountFactoriesUnitTestCase(BaseTestCase):
7
- pass
8
+ class CreateUserTestCase(BaseTestCase):
9
+ def test_creates_user(self) -> None:
10
+ user = create_user(username='testuser')
11
+ assert isinstance(user, AuthUser)
12
+ assert AuthUser.objects.filter(username='testuser').exists()
13
+
14
+ def test_creates_user_with_username(self) -> None:
15
+ user = create_user(username='myuser')
16
+ assert user.username == 'myuser'
17
+
18
+ def test_creates_user_with_kwargs(self) -> None:
19
+ user = create_user(
20
+ username='testuser',
21
+ first_name='Test',
22
+ last_name='User',
23
+ email='test@example.com'
24
+ )
25
+ assert user.first_name == 'Test'
26
+ assert user.last_name == 'User'
27
+ assert user.email == 'test@example.com'
28
+
29
+ def test_returns_existing_user_if_exists(self) -> None:
30
+ user1 = create_user(username='existinguser')
31
+ user2 = create_user(username='existinguser')
32
+ assert user1.pk == user2.pk
33
+
34
+ def test_get_or_create_behavior(self) -> None:
35
+ user1 = create_user(username='getorcreate', first_name='First')
36
+ user2 = create_user(username='getorcreate', first_name='Different')
37
+ assert user1.pk == user2.pk
38
+ assert user1.first_name == 'First'
39
+
40
+ def test_creates_with_is_active_true(self) -> None:
41
+ user = create_user(username='activeuser', is_active=True)
42
+ assert user.is_active
43
+
44
+ def test_creates_with_is_active_false(self) -> None:
45
+ user = create_user(username='inactiveuser', is_active=False)
46
+ assert not user.is_active
47
+
48
+ def test_user_has_pk(self) -> None:
49
+ user = create_user(username='pkuser')
50
+ assert user.pk is not None
51
+
52
+ def test_creates_unique_users(self) -> None:
53
+ user1 = create_user(username='user1')
54
+ user2 = create_user(username='user2')
55
+ assert user1.pk != user2.pk
56
+
57
+ def test_creates_with_unicode_username(self) -> None:
58
+ user = create_user(username='tëstüsér')
59
+ assert user.username == 'tëstüsér'
60
+
61
+ def test_creates_with_email(self) -> None:
62
+ user = create_user(username='emailuser', email='email@example.com')
63
+ assert user.email == 'email@example.com'
64
+
65
+ def test_creates_with_empty_email(self) -> None:
66
+ user = create_user(username='noemail', email='')
67
+ assert user.email == ''
68
+
69
+ def test_creates_with_staff_status(self) -> None:
70
+ user = create_user(username='staffuser', is_staff=True)
71
+ assert user.is_staff
72
+
73
+ def test_creates_without_staff_status(self) -> None:
74
+ user = create_user(username='nonstaffuser', is_staff=False)
75
+ assert not user.is_staff
76
+
77
+ def test_creates_with_superuser_status(self) -> None:
78
+ user = create_user(username='superuser', is_superuser=True)
79
+ assert user.is_superuser
80
+
81
+ def test_creates_without_superuser_status(self) -> None:
82
+ user = create_user(username='normaluser', is_superuser=False)
83
+ assert not user.is_superuser
84
+
85
+ def test_user_is_persisted(self) -> None:
86
+ user = create_user(username='persisteduser')
87
+ assert AuthUser.objects.filter(pk=user.pk).exists()
88
+
89
+ def test_creates_with_long_username(self) -> None:
90
+ long_username = 'a' * 150
91
+ user = create_user(username=long_username)
92
+ assert user.username == long_username
93
+
94
+ def test_creates_with_special_characters_username(self) -> None:
95
+ user = create_user(username='test.user@example.com')
96
+ assert user.username == 'test.user@example.com'
97
+
98
+
99
+ class CreateSuperUserTestCase(BaseTestCase):
100
+ def test_creates_superuser(self) -> None:
101
+ user = create_super_user()
102
+ assert isinstance(user, AuthUser)
103
+ assert user.is_superuser
104
+
105
+ def test_superuser_is_staff(self) -> None:
106
+ user = create_super_user()
107
+ assert user.is_staff
108
+
109
+ def test_superuser_default_username(self) -> None:
110
+ user = create_super_user()
111
+ assert user.username == 'stratus'
112
+
113
+ def test_superuser_default_email(self) -> None:
114
+ user = create_super_user()
115
+ assert user.email == 'bobert@stratusadv.com'
116
+
117
+ def test_superuser_default_names(self) -> None:
118
+ user = create_super_user()
119
+ assert user.first_name == 'Bob'
120
+ assert user.last_name == 'Robertson'
121
+
122
+ def test_superuser_default_password(self) -> None:
123
+ user = create_super_user()
124
+ assert user.check_password('stratus')
125
+
126
+ def test_superuser_custom_password(self) -> None:
127
+ user = create_super_user(password='custompassword') # noqa: S106
128
+ assert user.check_password('custompassword')
129
+
130
+ def test_superuser_returns_existing(self) -> None:
131
+ user1 = create_super_user()
132
+ user2 = create_super_user()
133
+ assert user1.pk == user2.pk
134
+
135
+ def test_superuser_has_pk(self) -> None:
136
+ user = create_super_user()
137
+ assert user.pk is not None
138
+
139
+ def test_superuser_is_active(self) -> None:
140
+ user = create_super_user()
141
+ assert user.is_active
142
+
143
+ def test_superuser_has_all_permissions(self) -> None:
144
+ user = create_super_user()
145
+ assert user.has_perm('any.permission')
146
+
147
+ def test_superuser_with_custom_kwargs(self) -> None:
148
+ user = create_super_user(first_name='Custom', last_name='Name')
149
+ assert user.first_name in ('Bob', 'Custom')
150
+
151
+ def test_superuser_is_persisted(self) -> None:
152
+ user = create_super_user()
153
+ assert AuthUser.objects.filter(pk=user.pk).exists()
154
+
155
+ def test_superuser_can_authenticate(self) -> None:
156
+ user = create_super_user()
157
+ assert user.check_password('stratus')
158
+
159
+ def test_superuser_password_is_hashed(self) -> None:
160
+ user = create_super_user()
161
+ assert user.password != 'stratus' # noqa: S105
162
+
163
+ def test_multiple_calls_same_user(self) -> None:
164
+ users = [create_super_user() for _ in range(5)]
165
+ pks = [u.pk for u in users]
166
+ assert len(set(pks)) == 1
167
+
168
+ def test_superuser_get_full_name(self) -> None:
169
+ user = create_super_user()
170
+ full_name = user.get_full_name()
171
+ assert 'Bob' in full_name or 'Robertson' in full_name