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,233 @@
1
+ from __future__ import annotations
2
+
3
+ from dateutil import relativedelta
4
+ from django.utils.timezone import localtime
5
+
6
+ from django_spire.auth.mfa.models import MfaCode
7
+ from django_spire.auth.user.tests.factories import create_user
8
+ from django_spire.core.tests.test_cases import BaseTestCase
9
+
10
+
11
+ class MfaCodeModelTestCase(BaseTestCase):
12
+ def setUp(self) -> None:
13
+ super().setUp()
14
+
15
+ self.user = create_user(username='testuser')
16
+
17
+ def test_generate_code_creates_valid_code(self) -> None:
18
+ mfa_code = MfaCode.generate_code(self.user)
19
+ code_value = int(mfa_code.code)
20
+ assert mfa_code.user == self.user
21
+ assert 0 <= code_value <= 999999
22
+
23
+ def test_generate_code_sets_expiration(self) -> None:
24
+ mfa_code = MfaCode.generate_code(self.user)
25
+ assert mfa_code.expiration_datetime > localtime()
26
+
27
+ def test_generate_code_expiration_is_five_minutes(self) -> None:
28
+ before = localtime()
29
+ mfa_code = MfaCode.generate_code(self.user)
30
+ after = localtime()
31
+ expected_min = before + relativedelta.relativedelta(minutes=5)
32
+ expected_max = after + relativedelta.relativedelta(minutes=5, seconds=1)
33
+ assert mfa_code.expiration_datetime >= expected_min
34
+ assert mfa_code.expiration_datetime <= expected_max
35
+
36
+ def test_is_valid_returns_true_for_unexpired(self) -> None:
37
+ mfa_code = MfaCode.generate_code(self.user)
38
+ assert mfa_code.is_valid()
39
+
40
+ def test_is_valid_returns_false_for_expired(self) -> None:
41
+ mfa_code = MfaCode.objects.create(
42
+ user=self.user,
43
+ code='123456',
44
+ expiration_datetime=localtime() - relativedelta.relativedelta(minutes=1)
45
+ )
46
+ assert not mfa_code.is_valid()
47
+
48
+ def test_is_valid_boundary_expired(self) -> None:
49
+ mfa_code = MfaCode.objects.create(
50
+ user=self.user,
51
+ code='123456',
52
+ expiration_datetime=localtime() - relativedelta.relativedelta(seconds=1)
53
+ )
54
+ assert not mfa_code.is_valid()
55
+
56
+ def test_set_expired(self) -> None:
57
+ mfa_code = MfaCode.generate_code(self.user)
58
+ assert mfa_code.is_valid()
59
+ mfa_code.set_expired()
60
+ assert not mfa_code.is_valid()
61
+
62
+ def test_set_expired_persists(self) -> None:
63
+ mfa_code = MfaCode.generate_code(self.user)
64
+ mfa_code.set_expired()
65
+ mfa_code.refresh_from_db()
66
+ assert not mfa_code.is_valid()
67
+
68
+ def test_str_representation(self) -> None:
69
+ mfa_code = MfaCode.generate_code(self.user)
70
+ expected = f'{mfa_code.expiration_datetime} - {mfa_code.code}'
71
+ assert str(mfa_code) == expected
72
+
73
+ def test_code_is_valid_numeric(self) -> None:
74
+ for _ in range(10):
75
+ mfa_code = MfaCode.generate_code(self.user)
76
+ code_value = int(mfa_code.code)
77
+ assert 0 <= code_value <= 999999
78
+ mfa_code.delete()
79
+
80
+ def test_multiple_codes_for_same_user(self) -> None:
81
+ code1 = MfaCode.generate_code(self.user)
82
+ code1.set_expired()
83
+ code2 = MfaCode.generate_code(self.user)
84
+ assert code1.pk != code2.pk
85
+
86
+ def test_codes_for_different_users(self) -> None:
87
+ user2 = create_user(username='testuser2')
88
+ code1 = MfaCode.generate_code(self.user)
89
+ code2 = MfaCode.generate_code(user2)
90
+ assert code1.user != code2.user
91
+
92
+ def test_code_user_relationship(self) -> None:
93
+ mfa_code = MfaCode.generate_code(self.user)
94
+ assert mfa_code.user.pk == self.user.pk
95
+
96
+ def test_code_deletion_with_user(self) -> None:
97
+ mfa_code = MfaCode.generate_code(self.user)
98
+ code_pk = mfa_code.pk
99
+ self.user.delete()
100
+ assert not MfaCode.objects.filter(pk=code_pk).exists()
101
+
102
+ def test_meta_db_table(self) -> None:
103
+ assert MfaCode._meta.db_table == 'django_spire_authentication_mfa_code'
104
+
105
+ def test_meta_verbose_name(self) -> None:
106
+ assert MfaCode._meta.verbose_name == 'MFA Code'
107
+ assert MfaCode._meta.verbose_name_plural == 'MFA Codes'
108
+
109
+ def test_code_length_is_six(self) -> None:
110
+ mfa_code = MfaCode.generate_code(self.user)
111
+ assert len(str(mfa_code.code)) <= 6
112
+ assert int(mfa_code.code) <= 999999
113
+
114
+ def test_code_is_string(self) -> None:
115
+ mfa_code = MfaCode.generate_code(self.user)
116
+ assert isinstance(mfa_code.code, (str, int))
117
+
118
+ def test_code_field_max_length(self) -> None:
119
+ max_length = MfaCode._meta.get_field('code').max_length
120
+ assert max_length == 6
121
+
122
+ def test_code_field_is_unique(self) -> None:
123
+ assert MfaCode._meta.get_field('code').unique
124
+
125
+ def test_code_field_is_not_editable(self) -> None:
126
+ assert not MfaCode._meta.get_field('code').editable
127
+
128
+ def test_expiration_field_is_not_editable(self) -> None:
129
+ assert not MfaCode._meta.get_field('expiration_datetime').editable
130
+
131
+ def test_user_foreign_key(self) -> None:
132
+ mfa_code = MfaCode.generate_code(self.user)
133
+ assert mfa_code.user_id == self.user.pk
134
+
135
+ def test_user_related_name(self) -> None:
136
+ mfa_code = MfaCode.generate_code(self.user)
137
+ assert mfa_code in self.user.mfa_codes.all()
138
+
139
+ def test_generate_code_creates_unique_codes(self) -> None:
140
+ codes = set()
141
+ for _ in range(100):
142
+ mfa_code = MfaCode.generate_code(self.user)
143
+ codes.add(mfa_code.code)
144
+ mfa_code.delete()
145
+ assert len(codes) > 90
146
+
147
+ def test_is_valid_exact_expiration_time(self) -> None:
148
+ mfa_code = MfaCode.objects.create(
149
+ user=self.user,
150
+ code='654321',
151
+ expiration_datetime=localtime()
152
+ )
153
+ assert not mfa_code.is_valid()
154
+
155
+ def test_code_with_leading_zeros(self) -> None:
156
+ mfa_code = MfaCode.objects.create(
157
+ user=self.user,
158
+ code='000123',
159
+ expiration_datetime=localtime() + relativedelta.relativedelta(minutes=5)
160
+ )
161
+ assert mfa_code.code == '000123'
162
+ assert len(mfa_code.code) == 6
163
+
164
+
165
+ class MfaCodeQuerySetTestCase(BaseTestCase):
166
+ def setUp(self) -> None:
167
+ super().setUp()
168
+
169
+ self.user = create_user(username='testuser')
170
+
171
+ def test_valid_code_returns_unexpired(self) -> None:
172
+ mfa_code = MfaCode.generate_code(self.user)
173
+ result = MfaCode.objects.valid_code(self.user)
174
+ assert result == mfa_code
175
+
176
+ def test_valid_code_returns_none_for_expired(self) -> None:
177
+ MfaCode.objects.create(
178
+ user=self.user,
179
+ code='123456',
180
+ expiration_datetime=localtime() - relativedelta.relativedelta(minutes=1)
181
+ )
182
+ result = MfaCode.objects.valid_code(self.user)
183
+ assert result is None
184
+
185
+ def test_valid_code_returns_none_for_different_user(self) -> None:
186
+ other_user = create_user(username='otheruser')
187
+ MfaCode.generate_code(other_user)
188
+ result = MfaCode.objects.valid_code(self.user)
189
+ assert result is None
190
+
191
+ def test_valid_code_returns_none_when_no_codes(self) -> None:
192
+ result = MfaCode.objects.valid_code(self.user)
193
+ assert result is None
194
+
195
+ def test_valid_code_returns_first_valid(self) -> None:
196
+ MfaCode.objects.create(
197
+ user=self.user,
198
+ code='111111',
199
+ expiration_datetime=localtime() - relativedelta.relativedelta(minutes=1)
200
+ )
201
+ valid_code = MfaCode.generate_code(self.user)
202
+ result = MfaCode.objects.valid_code(self.user)
203
+ assert result == valid_code
204
+
205
+ def test_valid_code_with_multiple_users(self) -> None:
206
+ user2 = create_user(username='testuser2')
207
+ code1 = MfaCode.generate_code(self.user)
208
+ MfaCode.generate_code(user2)
209
+ result = MfaCode.objects.valid_code(self.user)
210
+ assert result == code1
211
+
212
+ def test_valid_code_ignores_other_users_codes(self) -> None:
213
+ user2 = create_user(username='testuser2')
214
+ MfaCode.generate_code(user2)
215
+ result = MfaCode.objects.valid_code(self.user)
216
+ assert result is None
217
+
218
+ def test_valid_code_after_expiring_previous(self) -> None:
219
+ code1 = MfaCode.generate_code(self.user)
220
+ code1.set_expired()
221
+ code2 = MfaCode.generate_code(self.user)
222
+ result = MfaCode.objects.valid_code(self.user)
223
+ assert result == code2
224
+
225
+ def test_valid_code_multiple_valid_returns_first(self) -> None:
226
+ code1 = MfaCode.generate_code(self.user)
227
+ MfaCode.objects.create(
228
+ user=self.user,
229
+ code='999999',
230
+ expiration_datetime=localtime() + relativedelta.relativedelta(minutes=10)
231
+ )
232
+ result = MfaCode.objects.valid_code(self.user)
233
+ assert result == code1
@@ -0,0 +1,106 @@
1
+ from __future__ import annotations
2
+
3
+ from django_spire.auth.mfa.models import MfaCode
4
+ from django_spire.auth.mfa.utils import get_or_generate_user_mfa_code
5
+ from django_spire.auth.user.tests.factories import create_user
6
+ from django_spire.core.tests.test_cases import BaseTestCase
7
+
8
+
9
+ class GetOrGenerateUserMfaCodeTestCase(BaseTestCase):
10
+ def setUp(self) -> None:
11
+ super().setUp()
12
+
13
+ self.user = create_user(username='testuser')
14
+
15
+ def test_generates_new_code_when_none_exists(self) -> None:
16
+ mfa_code = get_or_generate_user_mfa_code(self.user)
17
+ assert mfa_code is not None
18
+ assert mfa_code.user == self.user
19
+ assert mfa_code.is_valid()
20
+
21
+ def test_returns_existing_valid_code(self) -> None:
22
+ existing_code = MfaCode.generate_code(self.user)
23
+ result = get_or_generate_user_mfa_code(self.user)
24
+ assert result.pk == existing_code.pk
25
+
26
+ def test_generates_new_code_when_existing_expired(self) -> None:
27
+ existing_code = MfaCode.generate_code(self.user)
28
+ existing_code.set_expired()
29
+
30
+ new_code = get_or_generate_user_mfa_code(self.user)
31
+ assert new_code.pk != existing_code.pk
32
+ assert new_code.is_valid()
33
+
34
+ def test_returns_same_code_on_multiple_calls(self) -> None:
35
+ code1 = get_or_generate_user_mfa_code(self.user)
36
+ code2 = get_or_generate_user_mfa_code(self.user)
37
+ assert code1.pk == code2.pk
38
+
39
+ def test_different_users_get_different_codes(self) -> None:
40
+ user2 = create_user(username='testuser2')
41
+ code1 = get_or_generate_user_mfa_code(self.user)
42
+ code2 = get_or_generate_user_mfa_code(user2)
43
+ assert code1.pk != code2.pk
44
+ assert code1.user != code2.user
45
+
46
+ def test_generated_code_is_persisted(self) -> None:
47
+ mfa_code = get_or_generate_user_mfa_code(self.user)
48
+ assert MfaCode.objects.filter(pk=mfa_code.pk).exists()
49
+
50
+ def test_generates_valid_code_after_expiration(self) -> None:
51
+ code1 = get_or_generate_user_mfa_code(self.user)
52
+ code1.set_expired()
53
+ code2 = get_or_generate_user_mfa_code(self.user)
54
+ assert code2.is_valid()
55
+
56
+ def test_code_belongs_to_correct_user(self) -> None:
57
+ mfa_code = get_or_generate_user_mfa_code(self.user)
58
+ assert mfa_code.user.pk == self.user.pk
59
+ assert mfa_code.user.username == self.user.username
60
+
61
+ def test_returns_mfa_code_instance(self) -> None:
62
+ mfa_code = get_or_generate_user_mfa_code(self.user)
63
+ assert isinstance(mfa_code, MfaCode)
64
+
65
+ def test_code_has_valid_expiration(self) -> None:
66
+ mfa_code = get_or_generate_user_mfa_code(self.user)
67
+ assert mfa_code.expiration_datetime is not None
68
+
69
+ def test_code_has_six_digits(self) -> None:
70
+ mfa_code = get_or_generate_user_mfa_code(self.user)
71
+ assert len(str(mfa_code.code)) <= 6
72
+ assert int(mfa_code.code) <= 999999
73
+
74
+ def test_code_is_numeric(self) -> None:
75
+ mfa_code = get_or_generate_user_mfa_code(self.user)
76
+ assert str(mfa_code.code).isdigit()
77
+
78
+ def test_multiple_expired_codes_generates_new(self) -> None:
79
+ code1 = get_or_generate_user_mfa_code(self.user)
80
+ code1.set_expired()
81
+ code2 = get_or_generate_user_mfa_code(self.user)
82
+ code2.set_expired()
83
+ code3 = get_or_generate_user_mfa_code(self.user)
84
+ assert code3.is_valid()
85
+ assert code3.pk != code1.pk
86
+ assert code3.pk != code2.pk
87
+
88
+ def test_does_not_affect_other_users_codes(self) -> None:
89
+ user2 = create_user(username='testuser2')
90
+ code1 = get_or_generate_user_mfa_code(self.user)
91
+ code2 = get_or_generate_user_mfa_code(user2)
92
+ code1.set_expired()
93
+ result = get_or_generate_user_mfa_code(user2)
94
+ assert result.pk == code2.pk
95
+
96
+ def test_new_code_after_delete(self) -> None:
97
+ code1 = get_or_generate_user_mfa_code(self.user)
98
+ code1_pk = code1.pk
99
+ code1.delete()
100
+ code2 = get_or_generate_user_mfa_code(self.user)
101
+ assert code2.pk != code1_pk
102
+
103
+ def test_code_user_relationship_intact(self) -> None:
104
+ mfa_code = get_or_generate_user_mfa_code(self.user)
105
+ assert mfa_code.user == self.user
106
+ assert mfa_code in self.user.mfa_codes.all()
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from django.urls import include, path
2
4
 
3
5
 
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from django.urls import path
2
4
 
3
5
  from django_spire.auth.mfa.views import page_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.mfa.views import redirect_views
@@ -5,8 +5,8 @@ from django.http import HttpResponseRedirect
5
5
  from django.template.response import TemplateResponse
6
6
  from django.urls import reverse
7
7
 
8
- from django_spire.auth.mfa.utils import get_or_generate_user_mfa_code
9
8
  from django_spire.auth.mfa import forms
9
+ from django_spire.auth.mfa.utils import get_or_generate_user_mfa_code
10
10
 
11
11
 
12
12
  def mfa_form_view(request):
@@ -15,6 +15,7 @@ def mfa_form_view(request):
15
15
 
16
16
  if request.method == 'POST':
17
17
  form = forms.MFAForm(mfa_code, request.POST)
18
+
18
19
  if form.is_valid():
19
20
  mfa_code.set_expired()
20
21
  profile.set_mfa_grace_period()
@@ -2,8 +2,8 @@ from __future__ import annotations
2
2
 
3
3
  from typing import Literal
4
4
 
5
- from django_spire.auth.group.models import AuthGroup
6
- from django_spire.auth.user.models import AuthUser
5
+ # from django_spire.auth.group.models import AuthGroup
6
+ # from django_spire.auth.user.models import AuthUser
7
7
 
8
8
 
9
9
  PERMISSIONS_LEVEL_CHOICES = (
@@ -13,12 +13,12 @@ if TYPE_CHECKING:
13
13
 
14
14
 
15
15
  def permission_required_decorator_function(
16
- permissions: str | Sequence[str],
17
- method,
18
- request: WSGIRequest,
19
- *args,
20
- all_required: bool = True,
21
- **kwargs
16
+ permissions: str | Sequence[str],
17
+ method,
18
+ request: WSGIRequest,
19
+ *args,
20
+ all_required: bool = True,
21
+ **kwargs
22
22
  ):
23
23
  if not request.user.is_authenticated:
24
24
  return HttpResponseRedirect(reverse('django_spire:auth:admin:login'))
@@ -40,8 +40,8 @@ def permission_required_decorator_function(
40
40
 
41
41
 
42
42
  def permission_required(
43
- *permissions: str,
44
- all_required: bool = True
43
+ *permissions: str,
44
+ all_required: bool = True
45
45
  ):
46
46
  def decorator(method):
47
47
  @functools.wraps(method)
@@ -10,20 +10,21 @@ from django_spire.auth.group.utils import (
10
10
  codename_to_perm_level,
11
11
  perm_level_to_string
12
12
  )
13
- from django_spire.auth.permissions.consts import VALID_PERMISSION_LEVELS
14
13
  from django_spire.core.utils import get_object_from_module_string
15
14
 
16
15
  if TYPE_CHECKING:
17
- from django.db.models import QuerySet, Model
16
+ from django.db.models import Model, QuerySet
17
+
18
+ from django_spire.auth.permissions.consts import VALID_PERMISSION_LEVELS
18
19
 
19
20
 
20
21
  class ModelPermission:
21
22
  def __init__(
22
- self,
23
- name: str,
24
- model_class_path: str,
25
- is_proxy_model: bool,
26
- verbose_name: str | None = None,
23
+ self,
24
+ name: str,
25
+ model_class_path: str,
26
+ is_proxy_model: bool,
27
+ verbose_name: str | None = None
27
28
  ):
28
29
  self.name = name
29
30
  self.model_class_path = model_class_path
@@ -57,13 +58,11 @@ class ModelPermissions:
57
58
  self,
58
59
  perm_level: VALID_PERMISSION_LEVELS | None
59
60
  ) -> list[Permission]:
60
- permission_list = []
61
-
62
- for perm in self.permissions:
63
- if codename_to_perm_level(perm.codename) <= perm_level:
64
- permission_list.append(perm)
65
-
66
- return permission_list
61
+ return [
62
+ perm
63
+ for perm in self.permissions
64
+ if codename_to_perm_level(perm.codename) <= perm_level
65
+ ]
67
66
 
68
67
  def get_special_role(self, codename: str) -> Permission | None:
69
68
  for perm in self.permissions:
@@ -90,9 +89,8 @@ class ModelPermissions:
90
89
 
91
90
  class GroupPermissions:
92
91
  def __init__(self, group: Group, model_permission: ModelPermission):
93
- """
94
- Helper to use Django Groups as a cascading permission structure.
95
- """
92
+ """Helper to use Django Groups as a cascading permission structure."""
93
+
96
94
  self.group = group
97
95
  self.model_permissions = ModelPermissions(model_permission)
98
96
  self.group_perms = self.group.permissions.all()
@@ -105,18 +103,14 @@ class GroupPermissions:
105
103
  self.set_group_perms()
106
104
 
107
105
  def has_special_role(self, codename: str) -> bool:
108
- for perm in self.group_perms:
109
- if perm.codename == codename:
110
- return True
111
-
112
- return False
106
+ return any(perm.codename == codename for perm in self.group_perms)
113
107
 
114
108
  def perm_level(self) -> VALID_PERMISSION_LEVELS:
115
- codename_list = []
116
-
117
- for perm in self.group_perms:
118
- if perm.codename.split('_')[-1] == self.model_permissions.model_name:
119
- codename_list.append(perm.codename)
109
+ codename_list = [
110
+ perm.codename
111
+ for perm in self.group_perms
112
+ if perm.codename.split('_')[-1] == self.model_permissions.model_name
113
+ ]
120
114
 
121
115
  return codename_list_to_perm_level(codename_list)
122
116
 
@@ -169,9 +163,8 @@ class GroupPermissions:
169
163
 
170
164
  class UserPermissionHelper:
171
165
  def __init__(self, user: User, model_permission: ModelPermission):
172
- """
173
- Helper to query user's current cascading permissions
174
- """
166
+ """Helper to query user's current cascading permissions"""
167
+
175
168
  self.user = user
176
169
  self.model_permissions = ModelPermissions(model_permission)
177
170
  self.user_perms = self.get_user_perms()
@@ -180,11 +173,11 @@ class UserPermissionHelper:
180
173
  return Permission.objects.filter(group__user=self.user).distinct()
181
174
 
182
175
  def perm_level(self) -> VALID_PERMISSION_LEVELS:
183
- codename_list = []
184
-
185
- for perm in self.user_perms:
186
- if perm.codename.split('_')[-1] == self.model_permissions.model_name:
187
- codename_list.append(perm.codename)
176
+ codename_list = [
177
+ perm.codename
178
+ for perm in self.user_perms
179
+ if perm.codename.split('_')[-1] == self.model_permissions.model_name
180
+ ]
188
181
 
189
182
  return codename_list_to_perm_level(codename_list)
190
183