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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (538) hide show
  1. django_spire/ai/admin.py +11 -11
  2. django_spire/ai/chat/apps.py +1 -0
  3. django_spire/ai/chat/templates/django_spire/ai/chat/widget/dialog_widget.html +1 -1
  4. django_spire/ai/chat/tests/factories.py +15 -0
  5. django_spire/ai/chat/tests/test_controller.py +45 -0
  6. django_spire/ai/chat/tests/test_models.py +301 -0
  7. django_spire/ai/chat/tests/test_prompts.py +48 -0
  8. django_spire/ai/chat/tests/test_responses.py +208 -0
  9. django_spire/ai/chat/tests/test_router/test_base_chat_router.py +66 -6
  10. django_spire/ai/chat/tests/test_router/test_chat_workflow.py +73 -3
  11. django_spire/ai/chat/tests/test_router/test_integration.py +86 -6
  12. django_spire/ai/chat/tests/test_router/test_intent_decoder.py +93 -1
  13. django_spire/ai/chat/tests/test_router/test_message_intel.py +60 -1
  14. django_spire/ai/chat/tests/test_router/test_spire_chat_router.py +110 -0
  15. django_spire/ai/chat/tests/test_urls/test_json_urls.py +202 -1
  16. django_spire/ai/context/tests/__init__.py +0 -0
  17. django_spire/ai/context/tests/test_context.py +188 -0
  18. django_spire/ai/decorators.py +7 -6
  19. django_spire/ai/prompt/tests/test_bots.py +100 -10
  20. django_spire/ai/prompt/tests/test_prompt_intel.py +83 -0
  21. django_spire/ai/prompt/tests/test_prompt_tuning.py +126 -0
  22. django_spire/ai/sms/decorators.py +8 -2
  23. django_spire/ai/sms/tests/test_sms.py +240 -16
  24. django_spire/ai/sms/tests/test_sms_intel.py +42 -0
  25. django_spire/ai/sms/tests/test_webhook.py +155 -7
  26. django_spire/ai/sms/views.py +23 -24
  27. django_spire/ai/tests/test_ai.py +131 -7
  28. django_spire/auth/apps.py +4 -2
  29. django_spire/auth/controller/controller.py +36 -23
  30. django_spire/auth/controller/exceptions.py +9 -0
  31. django_spire/auth/group/admin.py +1 -0
  32. django_spire/auth/group/apps.py +2 -0
  33. django_spire/auth/group/factories.py +17 -8
  34. django_spire/auth/group/forms.py +7 -0
  35. django_spire/auth/group/tests/test_factories.py +146 -0
  36. django_spire/auth/group/tests/test_forms.py +282 -0
  37. django_spire/auth/group/tests/test_models.py +192 -0
  38. django_spire/auth/group/tests/test_querysets.py +98 -0
  39. django_spire/auth/group/tests/test_utils.py +341 -0
  40. django_spire/auth/group/tests/test_views.py +377 -0
  41. django_spire/auth/group/urls/__init__.py +3 -1
  42. django_spire/auth/group/urls/form_urls.py +2 -0
  43. django_spire/auth/group/urls/json_urls.py +3 -0
  44. django_spire/auth/group/urls/page_urls.py +2 -0
  45. django_spire/auth/group/utils.py +6 -2
  46. django_spire/auth/group/views/form_views.py +6 -3
  47. django_spire/auth/group/views/json_views.py +6 -2
  48. django_spire/auth/mfa/admin.py +2 -0
  49. django_spire/auth/mfa/apps.py +2 -0
  50. django_spire/auth/mfa/forms.py +1 -0
  51. django_spire/auth/mfa/querysets.py +9 -2
  52. django_spire/auth/mfa/tests/test_models.py +233 -0
  53. django_spire/auth/mfa/tests/test_utils.py +106 -0
  54. django_spire/auth/mfa/urls/__init__.py +2 -0
  55. django_spire/auth/mfa/urls/page_urls.py +2 -0
  56. django_spire/auth/mfa/urls/redirect_urls.py +2 -0
  57. django_spire/auth/mfa/views/page_views.py +2 -1
  58. django_spire/auth/permissions/consts.py +2 -2
  59. django_spire/auth/permissions/decorators.py +8 -8
  60. django_spire/auth/permissions/permissions.py +28 -35
  61. django_spire/auth/permissions/tests/test_decorators.py +333 -0
  62. django_spire/auth/permissions/tests/test_permissions.py +337 -0
  63. django_spire/auth/permissions/tests/test_tools.py +305 -0
  64. django_spire/auth/permissions/tools.py +21 -15
  65. django_spire/auth/seeding/seed.py +3 -0
  66. django_spire/auth/seeding/seeder.py +2 -0
  67. django_spire/auth/tests/test_controller.py +323 -0
  68. django_spire/auth/tests/test_url_endpoints.py +9 -9
  69. django_spire/auth/tests/test_views.py +406 -0
  70. django_spire/auth/urls/admin_urls.py +2 -0
  71. django_spire/auth/urls/redirect_urls.py +2 -0
  72. django_spire/auth/user/apps.py +2 -0
  73. django_spire/auth/user/forms.py +9 -0
  74. django_spire/auth/user/models.py +1 -1
  75. django_spire/auth/user/services/services.py +1 -0
  76. django_spire/auth/user/tests/factories.py +14 -13
  77. django_spire/auth/user/tests/test_factories.py +166 -2
  78. django_spire/auth/user/tests/test_forms.py +573 -0
  79. django_spire/auth/user/tests/test_models.py +257 -0
  80. django_spire/auth/user/tests/test_services.py +200 -0
  81. django_spire/auth/user/tests/test_tools.py +153 -0
  82. django_spire/auth/user/tests/test_user_factories.py +139 -0
  83. django_spire/auth/user/tests/test_views.py +363 -0
  84. django_spire/auth/user/tools.py +7 -1
  85. django_spire/auth/user/urls/form_urls.py +3 -0
  86. django_spire/auth/user/urls/page_urls.py +3 -0
  87. django_spire/auth/user/views/form_views.py +19 -10
  88. django_spire/auth/user/views/page_views.py +8 -2
  89. django_spire/auth/views/redirect_views.py +14 -9
  90. django_spire/comment/admin.py +2 -0
  91. django_spire/comment/apps.py +2 -0
  92. django_spire/comment/templatetags/comment_tags.py +1 -0
  93. django_spire/comment/tests/test_forms.py +27 -0
  94. django_spire/comment/tests/test_models.py +215 -0
  95. django_spire/comment/tests/test_querysets.py +101 -0
  96. django_spire/comment/tests/test_utils.py +90 -0
  97. django_spire/comment/urls.py +2 -0
  98. django_spire/comment/utils.py +22 -13
  99. django_spire/comment/views.py +1 -1
  100. django_spire/conf.py +8 -6
  101. django_spire/consts.py +1 -1
  102. django_spire/contrib/breadcrumb/apps.py +2 -0
  103. django_spire/contrib/breadcrumb/breadcrumbs.py +18 -18
  104. django_spire/contrib/breadcrumb/tests/test_breadcrumbs.py +198 -0
  105. django_spire/contrib/constructor/__init__.py +3 -3
  106. django_spire/contrib/constructor/constructor.py +15 -15
  107. django_spire/contrib/constructor/django_model_constructor.py +5 -4
  108. django_spire/contrib/constructor/exceptions.py +5 -3
  109. django_spire/contrib/constructor/tests/__init__.py +0 -0
  110. django_spire/contrib/constructor/tests/test_constructor.py +193 -0
  111. django_spire/contrib/form/tests/__init__.py +0 -0
  112. django_spire/contrib/form/tests/test_forms.py +203 -0
  113. django_spire/contrib/generic_views/modal_views.py +2 -1
  114. django_spire/contrib/generic_views/portal_views.py +20 -19
  115. django_spire/contrib/generic_views/tests/__init__.py +0 -0
  116. django_spire/contrib/generic_views/tests/test_views.py +459 -0
  117. django_spire/contrib/help/apps.py +2 -0
  118. django_spire/contrib/help/templatetags/help.py +1 -0
  119. django_spire/contrib/help/tests/__init__.py +0 -0
  120. django_spire/contrib/help/tests/test_templatetags.py +100 -0
  121. django_spire/contrib/options/mixins.py +6 -5
  122. django_spire/contrib/options/tests/factories.py +5 -1
  123. django_spire/contrib/options/tests/test_options.py +234 -0
  124. django_spire/contrib/ordering/exceptions.py +7 -3
  125. django_spire/contrib/ordering/mixins.py +2 -0
  126. django_spire/contrib/ordering/querysets.py +3 -1
  127. django_spire/contrib/ordering/services/processor_service.py +8 -4
  128. django_spire/contrib/ordering/services/service.py +1 -2
  129. django_spire/contrib/ordering/tests/__init__.py +0 -0
  130. django_spire/contrib/ordering/tests/test_ordering.py +165 -0
  131. django_spire/contrib/ordering/validators.py +6 -6
  132. django_spire/contrib/pagination/templatetags/pagination_tags.py +12 -5
  133. django_spire/contrib/pagination/tests/__init__.py +0 -0
  134. django_spire/contrib/pagination/tests/test_pagination.py +179 -0
  135. django_spire/contrib/performance/decorators.py +16 -6
  136. django_spire/contrib/performance/tests/__init__.py +0 -0
  137. django_spire/contrib/performance/tests/test_performance.py +107 -0
  138. django_spire/contrib/queryset/enums.py +3 -1
  139. django_spire/contrib/queryset/filter_tools.py +10 -5
  140. django_spire/contrib/queryset/mixins.py +16 -16
  141. django_spire/contrib/queryset/tests/__init__.py +0 -0
  142. django_spire/contrib/queryset/tests/test_queryset.py +137 -0
  143. django_spire/contrib/seeding/field/base.py +13 -7
  144. django_spire/contrib/seeding/field/callable.py +8 -1
  145. django_spire/contrib/seeding/field/cleaners.py +5 -5
  146. django_spire/contrib/seeding/field/custom.py +20 -10
  147. django_spire/contrib/seeding/field/django/seeder.py +8 -6
  148. django_spire/contrib/seeding/field/enums.py +7 -5
  149. django_spire/contrib/seeding/field/override.py +16 -6
  150. django_spire/contrib/seeding/field/static.py +9 -2
  151. django_spire/contrib/seeding/field/tests/test_base.py +18 -14
  152. django_spire/contrib/seeding/field/tests/test_callable.py +13 -9
  153. django_spire/contrib/seeding/field/tests/test_cleaners.py +51 -38
  154. django_spire/contrib/seeding/field/tests/test_static.py +13 -9
  155. django_spire/contrib/seeding/intelligence/bots/seeder_generator_bot.py +2 -0
  156. django_spire/contrib/seeding/intelligence/intel.py +5 -1
  157. django_spire/contrib/seeding/intelligence/prompts/factory.py +6 -1
  158. django_spire/contrib/seeding/intelligence/prompts/foreign_key_selection_prompt.py +6 -1
  159. django_spire/contrib/seeding/intelligence/prompts/generate_django_model_seeder_prompts.py +2 -0
  160. django_spire/contrib/seeding/intelligence/prompts/generic_relationship_selection_prompt.py +7 -1
  161. django_spire/contrib/seeding/intelligence/prompts/hierarchical_selection_prompt.py +6 -2
  162. django_spire/contrib/seeding/intelligence/prompts/model_field_choices_prompt.py +8 -2
  163. django_spire/contrib/seeding/intelligence/prompts/negation_prompt.py +2 -0
  164. django_spire/contrib/seeding/intelligence/prompts/objective_prompt.py +6 -1
  165. django_spire/contrib/seeding/management/commands/seeding.py +9 -3
  166. django_spire/contrib/seeding/management/example.py +2 -0
  167. django_spire/contrib/seeding/model/base.py +16 -7
  168. django_spire/contrib/seeding/model/config.py +31 -15
  169. django_spire/contrib/seeding/model/django/config.py +13 -13
  170. django_spire/contrib/seeding/model/django/seeder.py +4 -4
  171. django_spire/contrib/seeding/model/django/tests/test_seeder.py +34 -23
  172. django_spire/contrib/seeding/model/enums.py +2 -0
  173. django_spire/contrib/seeding/tests/test_config.py +71 -0
  174. django_spire/contrib/seeding/tests/test_custom.py +35 -0
  175. django_spire/contrib/seeding/tests/test_enums.py +40 -0
  176. django_spire/contrib/seeding/tests/test_intel.py +32 -0
  177. django_spire/contrib/seeding/tests/test_override.py +63 -0
  178. django_spire/contrib/service/__init__.py +2 -2
  179. django_spire/contrib/service/django_model_service.py +16 -15
  180. django_spire/contrib/service/exceptions.py +5 -3
  181. django_spire/contrib/service/tests/__init__.py +0 -0
  182. django_spire/contrib/service/tests/test_service.py +153 -0
  183. django_spire/contrib/session/apps.py +2 -0
  184. django_spire/contrib/session/controller.py +48 -42
  185. django_spire/contrib/session/templatetags/session_tags.py +11 -2
  186. django_spire/contrib/session/tests/test_session_controller.py +117 -53
  187. django_spire/contrib/tests/__init__.py +0 -0
  188. django_spire/contrib/tests/test_utils.py +37 -0
  189. django_spire/contrib/utils.py +4 -1
  190. django_spire/core/apps.py +2 -0
  191. django_spire/core/converters/tests/test_to_data.py +353 -0
  192. django_spire/core/converters/tests/test_to_enums.py +61 -41
  193. django_spire/core/converters/tests/test_to_pydantic.py +138 -109
  194. django_spire/core/converters/to_data.py +29 -10
  195. django_spire/core/converters/to_enums.py +4 -2
  196. django_spire/core/converters/to_pydantic.py +22 -22
  197. django_spire/core/decorators.py +19 -6
  198. django_spire/core/forms/widgets.py +4 -0
  199. django_spire/core/maps.py +3 -1
  200. django_spire/core/middleware/maintenance.py +3 -3
  201. django_spire/core/middleware.py +8 -6
  202. django_spire/core/redirect/__init__.py +5 -0
  203. django_spire/core/redirect/generic_redirect.py +1 -2
  204. django_spire/core/redirect/tests/__init__.py +0 -0
  205. django_spire/core/redirect/tests/test_generic_redirect.py +34 -0
  206. django_spire/core/{tests/tests_redirect.py → redirect/tests/test_safe_redirect.py} +55 -81
  207. django_spire/core/shortcuts.py +3 -3
  208. django_spire/core/static/django_spire/css/app-layout.css +1 -1
  209. django_spire/core/static/django_spire/css/app-navigation.css +3 -3
  210. django_spire/core/static/django_spire/css/bootstrap-override.css +4 -0
  211. django_spire/core/tag/admin.py +12 -0
  212. django_spire/core/tag/intelligence/tag_set_bot.py +2 -0
  213. django_spire/core/tag/mixins.py +2 -0
  214. django_spire/core/tag/models.py +2 -0
  215. django_spire/core/tag/querysets.py +2 -0
  216. django_spire/core/tag/service/tag_service.py +6 -3
  217. django_spire/core/tag/tests/test_intelligence.py +9 -9
  218. django_spire/core/tag/tests/test_tags.py +44 -54
  219. django_spire/core/tag/tests/test_tools.py +191 -0
  220. django_spire/core/tag/tools.py +3 -0
  221. django_spire/core/templates/django_spire/card/card.html +5 -2
  222. django_spire/core/templates/django_spire/card/title_card.html +9 -4
  223. django_spire/core/templates/django_spire/infinite_scroll/base.html +1 -0
  224. django_spire/core/templates/django_spire/navigation/side_navigation.html +19 -24
  225. django_spire/core/templates/django_spire/page/full_page.html +46 -16
  226. django_spire/core/templates/django_spire/table/base.html +4 -2
  227. django_spire/core/templatetags/json.py +6 -2
  228. django_spire/core/templatetags/message.py +13 -8
  229. django_spire/core/templatetags/string_formating.py +8 -5
  230. django_spire/core/templatetags/tests/__init__.py +0 -0
  231. django_spire/core/templatetags/tests/test_templatetags.py +427 -0
  232. django_spire/core/templatetags/variable_types.py +17 -9
  233. django_spire/core/tests/test_cases.py +1 -1
  234. django_spire/core/tests/test_conf.py +43 -0
  235. django_spire/core/tests/test_consts.py +28 -0
  236. django_spire/core/tests/test_context_processors.py +93 -0
  237. django_spire/core/tests/test_decorators.py +95 -0
  238. django_spire/core/tests/test_django_spire_utils.py +56 -0
  239. django_spire/core/tests/test_exceptions.py +37 -0
  240. django_spire/core/tests/test_models.py +54 -0
  241. django_spire/core/tests/test_settings.py +45 -0
  242. django_spire/core/tests/test_shortcuts.py +74 -0
  243. django_spire/core/tests/test_urls.py +16 -0
  244. django_spire/core/tests/test_utils.py +58 -0
  245. django_spire/core/urls.py +4 -1
  246. django_spire/core/utils.py +12 -8
  247. django_spire/exceptions.py +16 -1
  248. django_spire/file/admin.py +4 -2
  249. django_spire/file/apps.py +8 -10
  250. django_spire/file/fields.py +7 -7
  251. django_spire/file/forms.py +1 -1
  252. django_spire/file/interfaces.py +15 -15
  253. django_spire/file/mixins.py +1 -4
  254. django_spire/file/models.py +3 -5
  255. django_spire/file/tests/factories.py +59 -0
  256. django_spire/file/tests/test_admin.py +69 -0
  257. django_spire/file/tests/test_apps.py +24 -0
  258. django_spire/file/tests/test_fields.py +114 -0
  259. django_spire/file/tests/test_forms.py +20 -0
  260. django_spire/file/tests/test_interfaces.py +183 -0
  261. django_spire/file/tests/test_models.py +82 -0
  262. django_spire/file/tests/test_querysets.py +102 -0
  263. django_spire/file/tests/test_utils.py +32 -0
  264. django_spire/file/tests/test_views.py +145 -0
  265. django_spire/file/tests/test_widgets.py +82 -0
  266. django_spire/file/tools.py +8 -2
  267. django_spire/file/views.py +7 -3
  268. django_spire/file/widgets.py +12 -12
  269. django_spire/help_desk/admin.py +15 -0
  270. django_spire/help_desk/apps.py +2 -0
  271. django_spire/help_desk/auth/controller.py +2 -0
  272. django_spire/help_desk/choices.py +2 -0
  273. django_spire/help_desk/enums.py +2 -0
  274. django_spire/help_desk/exceptions.py +31 -3
  275. django_spire/help_desk/forms.py +2 -0
  276. django_spire/help_desk/models.py +2 -0
  277. django_spire/help_desk/querysets.py +4 -1
  278. django_spire/help_desk/services/notification_service.py +26 -27
  279. django_spire/help_desk/services/service.py +2 -3
  280. django_spire/help_desk/tests/factories.py +8 -3
  281. django_spire/help_desk/tests/test_admin.py +41 -0
  282. django_spire/help_desk/tests/test_apps.py +41 -0
  283. django_spire/help_desk/tests/test_choices.py +50 -0
  284. django_spire/help_desk/tests/test_controller.py +87 -0
  285. django_spire/help_desk/tests/test_enums.py +18 -0
  286. django_spire/help_desk/tests/test_exceptions.py +37 -0
  287. django_spire/help_desk/tests/test_forms.py +89 -0
  288. django_spire/help_desk/tests/test_models.py +59 -0
  289. django_spire/help_desk/tests/test_querysets.py +38 -0
  290. django_spire/help_desk/tests/test_services/test_notification_service.py +15 -8
  291. django_spire/help_desk/tests/test_services/test_service.py +92 -0
  292. django_spire/help_desk/tests/test_urls/test_form_urls.py +6 -6
  293. django_spire/help_desk/tests/test_urls/test_page_urls.py +8 -9
  294. django_spire/help_desk/tests/test_views/test_form_views.py +46 -19
  295. django_spire/help_desk/tests/test_views/test_page_views.py +32 -9
  296. django_spire/help_desk/urls/__init__.py +4 -1
  297. django_spire/help_desk/urls/form_urls.py +3 -0
  298. django_spire/help_desk/urls/page_urls.py +3 -0
  299. django_spire/help_desk/views/form_views.py +13 -5
  300. django_spire/help_desk/views/page_views.py +11 -3
  301. django_spire/history/activity/admin.py +2 -0
  302. django_spire/history/activity/apps.py +3 -1
  303. django_spire/history/activity/mixins.py +13 -7
  304. django_spire/history/activity/models.py +6 -5
  305. django_spire/history/activity/querysets.py +2 -0
  306. django_spire/history/activity/tests/__init__.py +0 -0
  307. django_spire/history/activity/tests/test_activity.py +176 -0
  308. django_spire/history/admin.py +9 -2
  309. django_spire/history/choices.py +3 -0
  310. django_spire/history/models.py +5 -5
  311. django_spire/history/tests/test_admin.py +93 -0
  312. django_spire/history/tests/test_history.py +101 -0
  313. django_spire/history/tests/test_mixins.py +84 -0
  314. django_spire/history/viewed/admin.py +3 -1
  315. django_spire/history/viewed/apps.py +3 -1
  316. django_spire/history/viewed/models.py +2 -0
  317. django_spire/history/viewed/tests/__init__.py +0 -0
  318. django_spire/history/viewed/tests/test_viewed.py +46 -0
  319. django_spire/knowledge/auth/tests/__init__.py +0 -0
  320. django_spire/knowledge/auth/tests/test_controller.py +116 -0
  321. django_spire/knowledge/collection/admin.py +5 -1
  322. django_spire/knowledge/collection/models.py +3 -1
  323. django_spire/knowledge/collection/seeding/seed.py +1 -0
  324. django_spire/knowledge/collection/services/factory_service.py +10 -11
  325. django_spire/knowledge/collection/services/ordering_service.py +1 -2
  326. django_spire/knowledge/collection/services/service.py +5 -10
  327. django_spire/knowledge/collection/services/tag_service.py +5 -2
  328. django_spire/knowledge/collection/tests/factories.py +28 -1
  329. django_spire/knowledge/collection/tests/test_models.py +48 -0
  330. django_spire/knowledge/collection/tests/test_querysets.py +93 -0
  331. django_spire/knowledge/collection/tests/test_services/test_factory_service.py +100 -0
  332. django_spire/knowledge/collection/tests/test_services/test_services.py +160 -0
  333. django_spire/knowledge/collection/tests/test_urls/test_form_urls.py +21 -3
  334. django_spire/knowledge/collection/tests/test_urls/test_json_urls.py +39 -1
  335. django_spire/knowledge/collection/tests/test_urls/test_page_urls.py +12 -4
  336. django_spire/knowledge/collection/urls/__init__.py +3 -0
  337. django_spire/knowledge/collection/urls/form_urls.py +2 -0
  338. django_spire/knowledge/collection/urls/json_urls.py +2 -0
  339. django_spire/knowledge/collection/urls/page_urls.py +2 -0
  340. django_spire/knowledge/collection/views/form_views.py +4 -4
  341. django_spire/knowledge/collection/views/json_views.py +5 -1
  342. django_spire/knowledge/collection/views/page_views.py +5 -2
  343. django_spire/knowledge/entry/admin.py +7 -1
  344. django_spire/knowledge/entry/forms.py +2 -0
  345. django_spire/knowledge/entry/models.py +2 -0
  346. django_spire/knowledge/entry/seeding/seed.py +3 -0
  347. django_spire/knowledge/entry/services/automation_service.py +5 -4
  348. django_spire/knowledge/entry/services/factory_service.py +7 -5
  349. django_spire/knowledge/entry/services/service.py +4 -7
  350. django_spire/knowledge/entry/services/tag_service.py +0 -1
  351. django_spire/knowledge/entry/services/tool_service.py +1 -0
  352. django_spire/knowledge/entry/services/transformation_services.py +1 -5
  353. django_spire/knowledge/entry/tests/factories.py +1 -2
  354. django_spire/knowledge/entry/tests/test_factory_service.py +20 -0
  355. django_spire/knowledge/entry/tests/test_models.py +41 -0
  356. django_spire/knowledge/entry/tests/test_querysets.py +71 -0
  357. django_spire/knowledge/entry/tests/test_services.py +94 -0
  358. django_spire/knowledge/entry/tests/test_urls/test_form_urls.py +9 -14
  359. django_spire/knowledge/entry/tests/test_urls/test_json_urls.py +48 -5
  360. django_spire/knowledge/entry/tests/test_urls/test_page_urls.py +6 -8
  361. django_spire/knowledge/entry/tests/test_urls/test_template_urls.py +40 -0
  362. django_spire/knowledge/entry/urls/form_urls.py +2 -0
  363. django_spire/knowledge/entry/urls/json_urls.py +2 -0
  364. django_spire/knowledge/entry/urls/page_urls.py +2 -0
  365. django_spire/knowledge/entry/urls/template_urls.py +2 -0
  366. django_spire/knowledge/entry/version/block/choices.py +2 -0
  367. django_spire/knowledge/entry/version/block/data/data.py +1 -0
  368. django_spire/knowledge/entry/version/block/data/list/data.py +8 -13
  369. django_spire/knowledge/entry/version/block/data/list/maps.py +3 -0
  370. django_spire/knowledge/entry/version/block/data/list/meta.py +1 -2
  371. django_spire/knowledge/entry/version/block/data/list/tests/__init__.py +0 -0
  372. django_spire/knowledge/entry/version/block/data/list/tests/test_maps.py +32 -0
  373. django_spire/knowledge/entry/version/block/data/list/tests/test_meta.py +58 -0
  374. django_spire/knowledge/entry/version/block/data/maps.py +3 -6
  375. django_spire/knowledge/entry/version/block/models.py +7 -5
  376. django_spire/knowledge/entry/version/block/seeding/constants.py +5 -4
  377. django_spire/knowledge/entry/version/block/services/service.py +2 -3
  378. django_spire/knowledge/entry/version/block/tests/factories.py +4 -10
  379. django_spire/knowledge/entry/version/block/tests/test_choices.py +56 -0
  380. django_spire/knowledge/entry/version/block/tests/test_data.py +90 -0
  381. django_spire/knowledge/entry/version/block/tests/test_maps.py +37 -0
  382. django_spire/knowledge/entry/version/block/tests/test_models.py +55 -0
  383. django_spire/knowledge/entry/version/block/tests/test_querysets.py +35 -0
  384. django_spire/knowledge/entry/version/block/tests/test_services.py +65 -0
  385. django_spire/knowledge/entry/version/choices.py +2 -0
  386. django_spire/knowledge/entry/version/converters/converter.py +1 -1
  387. django_spire/knowledge/entry/version/converters/docx_converter.py +4 -7
  388. django_spire/knowledge/entry/version/converters/markdown_converter.py +20 -20
  389. django_spire/knowledge/entry/version/maps.py +4 -5
  390. django_spire/knowledge/entry/version/querysets.py +1 -1
  391. django_spire/knowledge/entry/version/seeding/seeder.py +1 -2
  392. django_spire/knowledge/entry/version/services/processor_service.py +5 -4
  393. django_spire/knowledge/entry/version/services/service.py +1 -2
  394. django_spire/knowledge/entry/version/tests/factories.py +2 -2
  395. django_spire/knowledge/entry/version/tests/test_choices.py +18 -0
  396. django_spire/knowledge/entry/version/tests/test_converters/test_docx_converter.py +56 -8
  397. django_spire/knowledge/entry/version/tests/test_converters/test_markdown_converter.py +78 -0
  398. django_spire/knowledge/entry/version/tests/test_maps.py +58 -0
  399. django_spire/knowledge/entry/version/tests/test_models.py +23 -0
  400. django_spire/knowledge/entry/version/tests/test_querysets.py +26 -0
  401. django_spire/knowledge/entry/version/tests/test_services.py +62 -0
  402. django_spire/knowledge/entry/version/tests/test_urls/test_json_urls.py +27 -8
  403. django_spire/knowledge/entry/version/tests/test_urls/test_page_urls.py +15 -8
  404. django_spire/knowledge/entry/version/tests/test_urls/test_redirect_urls.py +38 -0
  405. django_spire/knowledge/entry/version/urls/__init__.py +3 -0
  406. django_spire/knowledge/entry/version/urls/json_urls.py +2 -1
  407. django_spire/knowledge/entry/version/urls/page_urls.py +2 -0
  408. django_spire/knowledge/entry/version/urls/redirect_urls.py +2 -0
  409. django_spire/knowledge/entry/version/views/json_views.py +5 -1
  410. django_spire/knowledge/entry/version/views/page_views.py +10 -3
  411. django_spire/knowledge/entry/version/views/redirect_views.py +5 -1
  412. django_spire/knowledge/entry/views/form_views.py +16 -8
  413. django_spire/knowledge/entry/views/json_views.py +3 -1
  414. django_spire/knowledge/entry/views/page_views.py +8 -2
  415. django_spire/knowledge/entry/views/template_views.py +7 -1
  416. django_spire/knowledge/exceptions.py +2 -1
  417. django_spire/knowledge/intelligence/intel/answer_intel.py +2 -1
  418. django_spire/knowledge/intelligence/intel/entry_intel.py +0 -1
  419. django_spire/knowledge/intelligence/workflows/knowledge_workflow.py +4 -5
  420. django_spire/knowledge/models.py +1 -2
  421. django_spire/knowledge/tests/__init__.py +0 -0
  422. django_spire/knowledge/tests/test_templatetags.py +40 -0
  423. django_spire/knowledge/tests/test_urls/__init__.py +0 -0
  424. django_spire/knowledge/tests/test_urls/test_page_urls.py +24 -0
  425. django_spire/knowledge/urls/__init__.py +2 -0
  426. django_spire/knowledge/urls/page_urls.py +2 -0
  427. django_spire/knowledge/views/page_views.py +8 -3
  428. django_spire/notification/admin.py +3 -1
  429. django_spire/notification/app/admin.py +2 -0
  430. django_spire/notification/app/apps.py +3 -1
  431. django_spire/notification/app/exceptions.py +9 -2
  432. django_spire/notification/app/models.py +8 -4
  433. django_spire/notification/app/processor.py +22 -26
  434. django_spire/notification/app/querysets.py +2 -0
  435. django_spire/notification/app/tests/__init__.py +0 -0
  436. django_spire/notification/app/tests/factories.py +34 -0
  437. django_spire/notification/app/tests/test_apps.py +24 -0
  438. django_spire/notification/app/tests/test_models.py +72 -0
  439. django_spire/notification/app/tests/test_processor.py +111 -0
  440. django_spire/notification/app/tests/test_querysets.py +90 -0
  441. django_spire/notification/app/tests/test_views/__init__.py +0 -0
  442. django_spire/notification/app/tests/test_views/test_json_views.py +48 -0
  443. django_spire/notification/app/tests/test_views/test_page_views.py +19 -0
  444. django_spire/notification/app/urls/__init__.py +3 -1
  445. django_spire/notification/app/urls/json_urls.py +6 -4
  446. django_spire/notification/app/urls/page_urls.py +4 -3
  447. django_spire/notification/app/urls/template_urls.py +4 -2
  448. django_spire/notification/apps.py +4 -1
  449. django_spire/notification/email/admin.py +5 -1
  450. django_spire/notification/email/apps.py +3 -1
  451. django_spire/notification/email/exceptions.py +4 -2
  452. django_spire/notification/email/helper.py +5 -3
  453. django_spire/notification/email/models.py +4 -0
  454. django_spire/notification/email/processor.py +19 -15
  455. django_spire/notification/email/querysets.py +3 -0
  456. django_spire/notification/email/tests/__init__.py +0 -0
  457. django_spire/notification/email/tests/factories.py +35 -0
  458. django_spire/notification/email/tests/test_apps.py +24 -0
  459. django_spire/notification/email/tests/test_models.py +52 -0
  460. django_spire/notification/email/tests/test_processor.py +92 -0
  461. django_spire/notification/email/tests/test_querysets.py +43 -0
  462. django_spire/notification/exceptions.py +17 -2
  463. django_spire/notification/managers.py +7 -1
  464. django_spire/notification/maps.py +4 -1
  465. django_spire/notification/mixins.py +2 -0
  466. django_spire/notification/models.py +3 -1
  467. django_spire/notification/processors/notification.py +12 -5
  468. django_spire/notification/processors/processor.py +2 -0
  469. django_spire/notification/processors/tests/__init__.py +0 -0
  470. django_spire/notification/processors/tests/test_notification.py +106 -0
  471. django_spire/notification/push/admin.py +10 -1
  472. django_spire/notification/push/apps.py +3 -1
  473. django_spire/notification/push/models.py +2 -3
  474. django_spire/notification/push/tests/__init__.py +0 -0
  475. django_spire/notification/push/tests/test_apps.py +24 -0
  476. django_spire/notification/push/tests/test_models.py +28 -0
  477. django_spire/notification/querysets.py +7 -1
  478. django_spire/notification/sms/admin.py +2 -0
  479. django_spire/notification/sms/apps.py +4 -1
  480. django_spire/notification/sms/automations.py +2 -0
  481. django_spire/notification/sms/choices.py +2 -0
  482. django_spire/notification/sms/exceptions.py +19 -5
  483. django_spire/notification/sms/helper.py +33 -23
  484. django_spire/notification/sms/models.py +5 -1
  485. django_spire/notification/sms/processor.py +20 -20
  486. django_spire/notification/sms/querysets.py +2 -0
  487. django_spire/notification/sms/tests/factories.py +33 -0
  488. django_spire/notification/sms/tests/test_apps.py +24 -0
  489. django_spire/notification/sms/tests/test_automation.py +38 -0
  490. django_spire/notification/sms/tests/test_choices.py +15 -0
  491. django_spire/notification/sms/tests/test_consts.py +17 -0
  492. django_spire/notification/sms/tests/test_exceptions.py +27 -0
  493. django_spire/notification/sms/tests/test_helper.py +50 -0
  494. django_spire/notification/sms/tests/test_models.py +81 -0
  495. django_spire/notification/sms/tests/test_processor.py +107 -0
  496. django_spire/notification/sms/tests/test_tools.py +25 -11
  497. django_spire/notification/sms/tools.py +16 -5
  498. django_spire/notification/sms/urls/__init__.py +3 -1
  499. django_spire/notification/sms/urls/media_urls.py +2 -0
  500. django_spire/notification/sms/views/media_views.py +14 -4
  501. django_spire/notification/tests/__init__.py +0 -0
  502. django_spire/notification/tests/factories.py +26 -0
  503. django_spire/notification/tests/test_admin.py +55 -0
  504. django_spire/notification/tests/test_apps.py +30 -0
  505. django_spire/notification/tests/test_automation.py +18 -0
  506. django_spire/notification/tests/test_choices.py +59 -0
  507. django_spire/notification/tests/test_exceptions.py +58 -0
  508. django_spire/notification/tests/test_managers.py +100 -0
  509. django_spire/notification/tests/test_maps.py +31 -0
  510. django_spire/notification/tests/test_models.py +76 -0
  511. django_spire/notification/tests/test_querysets.py +184 -0
  512. django_spire/notification/tests/test_utils.py +23 -0
  513. django_spire/notification/urls.py +3 -1
  514. django_spire/notification/utils.py +3 -1
  515. django_spire/settings.py +3 -0
  516. django_spire/theme/tests/test_context_processor.py +15 -13
  517. django_spire/theme/tests/test_enums.py +2 -2
  518. django_spire/theme/tests/test_filesystem.py +2 -5
  519. django_spire/theme/tests/test_integration.py +12 -12
  520. django_spire/theme/tests/test_model.py +40 -38
  521. django_spire/theme/tests/test_views/test_json_views.py +33 -33
  522. django_spire/theme/urls/json_urls.py +3 -0
  523. django_spire/theme/urls/page_urls.py +3 -0
  524. django_spire/urls.py +19 -15
  525. django_spire/utils.py +13 -4
  526. {django_spire-0.23.7.dist-info → django_spire-0.23.8.dist-info}/METADATA +1 -1
  527. {django_spire-0.23.7.dist-info → django_spire-0.23.8.dist-info}/RECORD +530 -358
  528. django_spire/contrib/options/tests/test_unit.py +0 -148
  529. django_spire/contrib/seeding/tests/test_seeding.py +0 -25
  530. django_spire/core/tests/test_templatetags.py +0 -117
  531. django_spire/core/tests/tests_shortcuts.py +0 -73
  532. django_spire/history/activity/tests.py +0 -3
  533. django_spire/history/activity/views.py +0 -3
  534. django_spire/knowledge/collection/tests/test_services/test_transformation_service.py +0 -71
  535. django_spire/notification/app/tests.py +0 -3
  536. {django_spire-0.23.7.dist-info → django_spire-0.23.8.dist-info}/WHEEL +0 -0
  537. {django_spire-0.23.7.dist-info → django_spire-0.23.8.dist-info}/licenses/LICENSE.md +0 -0
  538. {django_spire-0.23.7.dist-info → django_spire-0.23.8.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TypedDict
3
+ from typing import Any, TypedDict
4
4
 
5
5
 
6
6
  class BreadcrumbDict(TypedDict):
@@ -9,7 +9,7 @@ class BreadcrumbDict(TypedDict):
9
9
 
10
10
 
11
11
  class BreadcrumbItem:
12
- def __init__(self, name: str, href: str | None = None):
12
+ def __init__(self, name: str, href: str | None = None) -> None:
13
13
  self.name = name
14
14
  self.href = href
15
15
 
@@ -18,21 +18,18 @@ class BreadcrumbItem:
18
18
 
19
19
 
20
20
  class Breadcrumbs:
21
- def __init__(self, branch_slug: str | None = None):
21
+ def __init__(self) -> None:
22
22
  self.data: list[BreadcrumbItem] = []
23
23
  self.index: int = 0
24
24
 
25
- def __add__(self, other):
25
+ def __add__(self, other: Breadcrumbs) -> Breadcrumbs:
26
26
  self.data += other.data
27
27
  return self
28
28
 
29
- def __iter__(self):
29
+ def __iter__(self) -> Breadcrumbs:
30
30
  self.index = 0
31
31
  return self
32
32
 
33
- def __str__(self) -> str:
34
- return str(self.data)
35
-
36
33
  def __len__(self) -> int:
37
34
  return len(self.data)
38
35
 
@@ -44,7 +41,10 @@ class Breadcrumbs:
44
41
  self.index += 1
45
42
  return breadcrumb_item.to_dict()
46
43
 
47
- def add_base_breadcrumb(self, model) -> None:
44
+ def __str__(self) -> str:
45
+ return str(self.data)
46
+
47
+ def add_base_breadcrumb(self, model: Any) -> None:
48
48
  if hasattr(model, 'base_breadcrumb'):
49
49
  self += model.base_breadcrumb()
50
50
 
@@ -52,13 +52,7 @@ class Breadcrumbs:
52
52
  breadcrumb_item = BreadcrumbItem(name=name, href=href)
53
53
  self.data.append(breadcrumb_item)
54
54
 
55
- def add_obj_breadcrumbs(self, obj) -> None:
56
- # Expects a breadcrumb object to be returned from the object
57
- if hasattr(obj, 'breadcrumbs'):
58
- object_breadcrumbs = obj.breadcrumbs()
59
- self.data += object_breadcrumbs.data
60
-
61
- def add_form_breadcrumbs(self, obj) -> None:
55
+ def add_form_breadcrumbs(self, obj: Any) -> None:
62
56
  self.add_obj_breadcrumbs(obj)
63
57
 
64
58
  if obj.pk is None:
@@ -67,10 +61,16 @@ class Breadcrumbs:
67
61
  else:
68
62
  self.add_breadcrumb(name='Edit')
69
63
 
70
- def remove(self, index: int):
64
+ def add_obj_breadcrumbs(self, obj: Any) -> None:
65
+ # It expects a breadcrumb object to be returned from the object
66
+ if hasattr(obj, 'breadcrumbs'):
67
+ object_breadcrumbs = obj.breadcrumbs()
68
+ self.data += object_breadcrumbs.data
69
+
70
+ def remove(self, index: int) -> Breadcrumbs:
71
71
  del self.data[index]
72
72
  return self
73
73
 
74
- def reverse(self):
74
+ def reverse(self) -> Breadcrumbs:
75
75
  self.data.reverse()
76
76
  return self
@@ -0,0 +1,198 @@
1
+ from __future__ import annotations
2
+
3
+ from unittest.mock import MagicMock
4
+
5
+ from django.test import TestCase
6
+
7
+ from django_spire.contrib.breadcrumb.breadcrumbs import BreadcrumbItem, Breadcrumbs
8
+
9
+
10
+ class TestBreadcrumbItem(TestCase):
11
+ def test_create_with_href(self) -> None:
12
+ item = BreadcrumbItem(name='Home', href='/home/')
13
+
14
+ assert item.name == 'Home'
15
+ assert item.href == '/home/'
16
+
17
+ def test_create_without_href(self) -> None:
18
+ item = BreadcrumbItem(name='Current Page')
19
+
20
+ assert item.name == 'Current Page'
21
+ assert item.href is None
22
+
23
+ def test_to_dict(self) -> None:
24
+ item = BreadcrumbItem(name='Home', href='/home/')
25
+ result = item.to_dict()
26
+
27
+ assert result == {'name': 'Home', 'href': '/home/'}
28
+
29
+ def test_to_dict_without_href(self) -> None:
30
+ item = BreadcrumbItem(name='Current Page')
31
+ result = item.to_dict()
32
+
33
+ assert result == {'name': 'Current Page', 'href': None}
34
+
35
+
36
+ class TestBreadcrumbs(TestCase):
37
+ def test_add_base_breadcrumb(self) -> None:
38
+ breadcrumbs = Breadcrumbs()
39
+
40
+ model = MagicMock()
41
+ model_breadcrumbs = Breadcrumbs()
42
+ model_breadcrumbs.add_breadcrumb('Base', '/base/')
43
+ model.base_breadcrumb.return_value = model_breadcrumbs
44
+
45
+ breadcrumbs.add_base_breadcrumb(model)
46
+
47
+ assert len(breadcrumbs) == 1
48
+
49
+ def test_add_base_breadcrumb_no_method(self) -> None:
50
+ breadcrumbs = Breadcrumbs()
51
+
52
+ model = MagicMock(spec=[])
53
+
54
+ breadcrumbs.add_base_breadcrumb(model)
55
+
56
+ assert len(breadcrumbs) == 0
57
+
58
+ def test_add_breadcrumb(self) -> None:
59
+ breadcrumbs = Breadcrumbs()
60
+ breadcrumbs.add_breadcrumb('Home', '/home/')
61
+
62
+ assert len(breadcrumbs) == 1
63
+ assert breadcrumbs.data[0].name == 'Home'
64
+ assert breadcrumbs.data[0].href == '/home/'
65
+
66
+ def test_add_breadcrumb_without_href(self) -> None:
67
+ breadcrumbs = Breadcrumbs()
68
+ breadcrumbs.add_breadcrumb('Current Page')
69
+
70
+ assert len(breadcrumbs) == 1
71
+ assert breadcrumbs.data[0].name == 'Current Page'
72
+ assert breadcrumbs.data[0].href is None
73
+
74
+ def test_add_form_breadcrumbs_create(self) -> None:
75
+ breadcrumbs = Breadcrumbs()
76
+
77
+ obj = MagicMock()
78
+ obj.pk = None
79
+ obj._meta.model._meta.verbose_name = 'Test Model'
80
+ del obj.breadcrumbs
81
+
82
+ breadcrumbs.add_form_breadcrumbs(obj)
83
+
84
+ assert len(breadcrumbs) == 2
85
+ assert breadcrumbs.data[0].name == 'Test Model'
86
+ assert breadcrumbs.data[1].name == 'Create'
87
+
88
+ def test_add_form_breadcrumbs_edit(self) -> None:
89
+ breadcrumbs = Breadcrumbs()
90
+
91
+ obj = MagicMock()
92
+ obj.pk = 1
93
+ del obj.breadcrumbs
94
+
95
+ breadcrumbs.add_form_breadcrumbs(obj)
96
+
97
+ assert len(breadcrumbs) == 1
98
+ assert breadcrumbs.data[0].name == 'Edit'
99
+
100
+ def test_add_obj_breadcrumbs(self) -> None:
101
+ breadcrumbs = Breadcrumbs()
102
+
103
+ obj_breadcrumbs = Breadcrumbs()
104
+ obj_breadcrumbs.add_breadcrumb('Object', '/object/')
105
+
106
+ obj = MagicMock()
107
+ obj.breadcrumbs.return_value = obj_breadcrumbs
108
+
109
+ breadcrumbs.add_obj_breadcrumbs(obj)
110
+
111
+ assert len(breadcrumbs) == 1
112
+ assert breadcrumbs.data[0].name == 'Object'
113
+
114
+ def test_add_obj_breadcrumbs_no_method(self) -> None:
115
+ breadcrumbs = Breadcrumbs()
116
+
117
+ obj = MagicMock(spec=[])
118
+
119
+ breadcrumbs.add_obj_breadcrumbs(obj)
120
+
121
+ assert len(breadcrumbs) == 0
122
+
123
+ def test_add_operator(self) -> None:
124
+ breadcrumbs1 = Breadcrumbs()
125
+ breadcrumbs1.add_breadcrumb('Home', '/home/')
126
+
127
+ breadcrumbs2 = Breadcrumbs()
128
+ breadcrumbs2.add_breadcrumb('Page', '/page/')
129
+
130
+ result = breadcrumbs1 + breadcrumbs2
131
+
132
+ assert len(result) == 2
133
+ assert result.data[0].name == 'Home'
134
+ assert result.data[1].name == 'Page'
135
+
136
+ def test_empty_breadcrumbs(self) -> None:
137
+ breadcrumbs = Breadcrumbs()
138
+
139
+ assert len(breadcrumbs) == 0
140
+
141
+ def test_iteration(self) -> None:
142
+ breadcrumbs = Breadcrumbs()
143
+ breadcrumbs.add_breadcrumb('Home', '/home/')
144
+ breadcrumbs.add_breadcrumb('Page', '/page/')
145
+
146
+ items = list(breadcrumbs)
147
+
148
+ assert len(items) == 2
149
+ assert items[0] == {'name': 'Home', 'href': '/home/'}
150
+ assert items[1] == {'name': 'Page', 'href': '/page/'}
151
+
152
+ def test_iteration_empty(self) -> None:
153
+ breadcrumbs = Breadcrumbs()
154
+
155
+ items = list(breadcrumbs)
156
+
157
+ assert items == []
158
+
159
+ def test_len(self) -> None:
160
+ breadcrumbs = Breadcrumbs()
161
+ breadcrumbs.add_breadcrumb('Home', '/home/')
162
+ breadcrumbs.add_breadcrumb('Page', '/page/')
163
+
164
+ assert len(breadcrumbs) == 2
165
+
166
+ def test_remove(self) -> None:
167
+ breadcrumbs = Breadcrumbs()
168
+ breadcrumbs.add_breadcrumb('Home', '/home/')
169
+ breadcrumbs.add_breadcrumb('Page', '/page/')
170
+ breadcrumbs.add_breadcrumb('Current', None)
171
+
172
+ result = breadcrumbs.remove(1)
173
+
174
+ assert len(breadcrumbs) == 2
175
+ assert breadcrumbs.data[0].name == 'Home'
176
+ assert breadcrumbs.data[1].name == 'Current'
177
+ assert result is breadcrumbs
178
+
179
+ def test_reverse(self) -> None:
180
+ breadcrumbs = Breadcrumbs()
181
+ breadcrumbs.add_breadcrumb('First', '/first/')
182
+ breadcrumbs.add_breadcrumb('Second', '/second/')
183
+ breadcrumbs.add_breadcrumb('Third', '/third/')
184
+
185
+ result = breadcrumbs.reverse()
186
+
187
+ assert breadcrumbs.data[0].name == 'Third'
188
+ assert breadcrumbs.data[1].name == 'Second'
189
+ assert breadcrumbs.data[2].name == 'First'
190
+ assert result is breadcrumbs
191
+
192
+ def test_str(self) -> None:
193
+ breadcrumbs = Breadcrumbs()
194
+ breadcrumbs.add_breadcrumb('Home', '/home/')
195
+
196
+ result = str(breadcrumbs)
197
+
198
+ assert 'BreadcrumbItem' in result or '[' in result
@@ -1,9 +1,9 @@
1
- from django_spire.contrib.constructor.exceptions import ConstructorException
1
+ from django_spire.contrib.constructor.exceptions import ConstructorError
2
2
  from django_spire.contrib.constructor.django_model_constructor import BaseDjangoModelConstructor
3
3
  from django_spire.contrib.constructor.constructor import BaseConstructor
4
4
 
5
5
  __all__ = [
6
6
  'BaseConstructor',
7
7
  'BaseDjangoModelConstructor',
8
- 'ConstructorException'
9
- ]
8
+ 'ConstructorError'
9
+ ]
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  from abc import ABC
4
4
  from typing import Any, Generic, TypeVar
5
5
 
6
- from django_spire.contrib.constructor.exceptions import ConstructorException
6
+ from django_spire.contrib.constructor.exceptions import ConstructorError
7
7
 
8
8
 
9
9
  TypeAny = TypeVar('TypeAny', bound=Any, covariant=True)
@@ -15,7 +15,7 @@ class BaseConstructor(
15
15
  ):
16
16
  def __init__(self, obj: Any = None):
17
17
  self._obj_type_name: str = str(
18
- list(self.__class__.__annotations__.values())[0]
18
+ next(iter(self.__class__.__annotations__.values()))
19
19
  ).split('.')[-1]
20
20
 
21
21
  if obj is None:
@@ -23,22 +23,22 @@ class BaseConstructor(
23
23
 
24
24
  self._obj_mro_type_names = [cls.__name__ for cls in obj.__class__.__mro__]
25
25
 
26
- if not self._obj_type_name in self._obj_mro_type_names:
27
- raise ConstructorException(
28
- f'{self.__class__.__name__} was instantiated with obj type "{obj.__class__.__name__}" and failed as it was expecting "{self._obj_type_name}".'
29
- )
26
+ if self._obj_type_name not in self._obj_mro_type_names:
27
+ message = f'{self.__class__.__name__} was instantiated with obj type "{obj.__class__.__name__}" and failed as it was expecting "{self._obj_type_name}".'
28
+ raise ConstructorError(message)
30
29
 
31
30
  self._obj_type: type[TypeAny] = obj.__class__
32
31
 
33
32
  if self._obj_type is None or self._obj_type is ...:
34
- raise ConstructorException(
35
- f'{self.__class__.__name__} top class attribute must have an annotated type.')
33
+ message = f'{self.__class__.__name__} top class attribute must have an annotated type.'
34
+ raise ConstructorError(message)
36
35
 
37
36
  self.obj: TypeAny = obj
38
37
 
39
38
  if ABC not in self.__class__.__bases__:
40
39
  if not self._obj_is_valid:
41
- raise ConstructorException(f'{self._obj_type_name} failed to validate on {self.__class__.__name__}')
40
+ message = f'{self._obj_type_name} failed to validate on {self.__class__.__name__}'
41
+ raise ConstructorError(message)
42
42
 
43
43
  self.__post_init__()
44
44
 
@@ -47,8 +47,8 @@ class BaseConstructor(
47
47
 
48
48
  if ABC not in cls.__bases__:
49
49
  if 'obj' not in cls.__annotations__:
50
- raise ConstructorException(
51
- f'{cls.__name__} must have an "obj" attribute annotated with a type.')
50
+ message = f'{cls.__name__} must have an "obj" attribute annotated with a type.'
51
+ raise ConstructorError(message)
52
52
 
53
53
  # Typing Does not work properly for services if you override __get__ in the BaseService class.
54
54
  # This is a workaround and should be fixed in future versions of the python lsp.
@@ -63,11 +63,11 @@ class BaseConstructor(
63
63
 
64
64
  self._validate_base_service_target_or_error(target)
65
65
 
66
- return cls(getattr(target, 'obj'))
66
+ return cls(target.obj)
67
67
 
68
68
  return cls(target)
69
69
 
70
- setattr(cls, '__get__', __get__)
70
+ cls.__get__ = __get__
71
71
 
72
72
  def __post_init__(self):
73
73
  pass
@@ -82,5 +82,5 @@ class BaseConstructor(
82
82
 
83
83
  def _validate_base_service_target_or_error(self, target: BaseConstructor):
84
84
  if self._obj_type_name not in target._obj_mro_type_names:
85
- raise ConstructorException(
86
- f'{target.__class__.__name__} must use the same obj type as {self.__class__.__name__}. {self._obj_type_name} is not in {target._obj_mro_type_names}')
85
+ message = f'{target.__class__.__name__} must use the same obj type as {self.__class__.__name__}. {self._obj_type_name} is not in {target._obj_mro_type_names}'
86
+ raise ConstructorError(message)
@@ -1,12 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from abc import ABC
4
- from typing import Type, Generic, TypeVar
4
+ from typing import Generic, TypeVar
5
5
 
6
6
  from django.db.models import Model
7
7
 
8
8
  from django_spire.contrib.constructor.constructor import BaseConstructor
9
9
 
10
+
10
11
  TypeDjangoModel = TypeVar('TypeDjangoModel', bound=Model, covariant=True)
11
12
 
12
13
 
@@ -17,11 +18,11 @@ class BaseDjangoModelConstructor(
17
18
  ):
18
19
  @property
19
20
  def _model_obj_id_is_empty(self) -> bool:
20
- return self.obj.id is None or self.obj.id == 0 or self.obj.id == ''
21
+ return self.obj.id is None or self.obj.id in {0, ''}
21
22
 
22
23
  @property
23
24
  def _model_obj_pk_is_empty(self) -> bool:
24
- return self.obj.pk is None or self.obj.pk == 0 or self.obj.pk == ''
25
+ return self.obj.pk is None or self.obj.pk in {0, ''}
25
26
 
26
27
  @property
27
28
  def model_obj_is_created(self) -> bool:
@@ -32,7 +33,7 @@ class BaseDjangoModelConstructor(
32
33
  return self._model_obj_id_is_empty or self._model_obj_pk_is_empty
33
34
 
34
35
  @property
35
- def obj_class(self) -> Type[TypeDjangoModel]:
36
+ def obj_class(self) -> type[TypeDjangoModel]:
36
37
  return super().obj_class
37
38
 
38
39
  @property
@@ -1,5 +1,7 @@
1
- from django_spire.exceptions import DjangoSpireException
1
+ from __future__ import annotations
2
2
 
3
+ from django_spire.exceptions import DjangoSpireError
3
4
 
4
- class ConstructorException(DjangoSpireException):
5
- pass
5
+
6
+ class ConstructorError(DjangoSpireError):
7
+ pass
File without changes
@@ -0,0 +1,193 @@
1
+ from __future__ import annotations
2
+
3
+ import pytest
4
+
5
+ from abc import ABC
6
+
7
+ from django.contrib.auth.models import User
8
+ from django.test import TestCase
9
+
10
+ from django_spire.contrib.constructor import (
11
+ BaseConstructor,
12
+ BaseDjangoModelConstructor,
13
+ ConstructorError,
14
+ )
15
+
16
+
17
+ class TestBaseConstructor(TestCase):
18
+ def test_abc_subclass_does_not_require_obj_annotation(self) -> None:
19
+ class AbstractConstructor(BaseConstructor, ABC):
20
+ pass
21
+
22
+ # Should not raise
23
+ assert AbstractConstructor is not None
24
+
25
+ def test_constructor_with_none_obj(self) -> None:
26
+ class StringConstructor(BaseConstructor[str]):
27
+ obj: str
28
+
29
+ constructor = StringConstructor(None)
30
+
31
+ assert not hasattr(constructor, 'obj')
32
+
33
+ def test_constructor_with_valid_obj(self) -> None:
34
+ class StringConstructor(BaseConstructor[str]):
35
+ obj: str
36
+
37
+ constructor = StringConstructor('test')
38
+
39
+ assert constructor.obj == 'test'
40
+
41
+ def test_constructor_with_wrong_type_raises_error(self) -> None:
42
+ class StringConstructor(BaseConstructor[str]):
43
+ obj: str
44
+
45
+ with pytest.raises(ConstructorError):
46
+ StringConstructor(123)
47
+
48
+ def test_obj_class_property(self) -> None:
49
+ class StringConstructor(BaseConstructor[str]):
50
+ obj: str
51
+
52
+ constructor = StringConstructor('test')
53
+
54
+ assert constructor.obj_class is str
55
+
56
+ def test_obj_is_valid_property(self) -> None:
57
+ class StringConstructor(BaseConstructor[str]):
58
+ obj: str
59
+
60
+ constructor = StringConstructor('test')
61
+
62
+ assert constructor._obj_is_valid is True
63
+
64
+ def test_post_init_called(self) -> None:
65
+ class StringConstructor(BaseConstructor[str]):
66
+ obj: str
67
+ post_init_called: bool = False
68
+
69
+ def __post_init__(self):
70
+ self.post_init_called = True
71
+
72
+ constructor = StringConstructor('test')
73
+
74
+ assert constructor.post_init_called is True
75
+
76
+ def test_subclass_without_obj_annotation_raises_error(self) -> None:
77
+ with pytest.raises(ConstructorError, match='must have an "obj" attribute'):
78
+ class InvalidConstructor(BaseConstructor[str]):
79
+ pass
80
+
81
+
82
+ class TestBaseDjangoModelConstructor(TestCase):
83
+ def setUp(self) -> None:
84
+ self.user = User.objects.create_user(
85
+ username='testuser',
86
+ password='testpass' # noqa: S106
87
+ )
88
+
89
+ def test_constructor_with_django_model(self) -> None:
90
+ class UserConstructor(BaseDjangoModelConstructor[User]):
91
+ obj: User
92
+
93
+ constructor = UserConstructor(self.user)
94
+
95
+ assert constructor.obj == self.user
96
+
97
+ def test_model_obj_is_created_true(self) -> None:
98
+ class UserConstructor(BaseDjangoModelConstructor[User]):
99
+ obj: User
100
+
101
+ constructor = UserConstructor(self.user)
102
+
103
+ assert constructor.model_obj_is_created is True
104
+
105
+ def test_model_obj_is_created_false_for_new(self) -> None:
106
+ class UserConstructor(BaseDjangoModelConstructor[User]):
107
+ obj: User
108
+
109
+ new_user = User(username='newuser')
110
+ constructor = UserConstructor(new_user)
111
+
112
+ assert constructor.model_obj_is_created is False
113
+
114
+ def test_model_obj_is_new_false(self) -> None:
115
+ class UserConstructor(BaseDjangoModelConstructor[User]):
116
+ obj: User
117
+
118
+ constructor = UserConstructor(self.user)
119
+
120
+ assert constructor.model_obj_is_new is False
121
+
122
+ def test_model_obj_is_new_true(self) -> None:
123
+ class UserConstructor(BaseDjangoModelConstructor[User]):
124
+ obj: User
125
+
126
+ new_user = User(username='newuser')
127
+ constructor = UserConstructor(new_user)
128
+
129
+ assert constructor.model_obj_is_new is True
130
+
131
+ def test_model_obj_pk_is_empty_true(self) -> None:
132
+ class UserConstructor(BaseDjangoModelConstructor[User]):
133
+ obj: User
134
+
135
+ new_user = User(username='newuser')
136
+ constructor = UserConstructor(new_user)
137
+
138
+ assert constructor._model_obj_pk_is_empty is True
139
+
140
+ def test_model_obj_pk_is_empty_false(self) -> None:
141
+ class UserConstructor(BaseDjangoModelConstructor[User]):
142
+ obj: User
143
+
144
+ constructor = UserConstructor(self.user)
145
+
146
+ assert constructor._model_obj_pk_is_empty is False
147
+
148
+ def test_obj_class_returns_model_class(self) -> None:
149
+ class UserConstructor(BaseDjangoModelConstructor[User]):
150
+ obj: User
151
+
152
+ constructor = UserConstructor(self.user)
153
+
154
+ assert constructor.obj_class is User
155
+
156
+ def test_obj_is_valid_true(self) -> None:
157
+ class UserConstructor(BaseDjangoModelConstructor[User]):
158
+ obj: User
159
+
160
+ constructor = UserConstructor(self.user)
161
+
162
+ assert constructor._obj_is_valid is True
163
+
164
+
165
+ class TestConstructorError(TestCase):
166
+ def test_constructor_error_is_exception(self) -> None:
167
+ assert issubclass(ConstructorError, Exception)
168
+
169
+ def test_constructor_error_message(self) -> None:
170
+ error = ConstructorError('Test error message')
171
+
172
+ assert str(error) == 'Test error message'
173
+
174
+ def test_constructor_error_can_be_raised(self) -> None:
175
+ with pytest.raises(ConstructorError):
176
+ message = 'Test'
177
+ raise ConstructorError(message)
178
+
179
+
180
+ class TestConstructorDescriptor(TestCase):
181
+ def setUp(self) -> None:
182
+ self.user = User.objects.create_user(
183
+ username='testuser',
184
+ password='testpass' # noqa: S106
185
+ )
186
+
187
+ def test_descriptor_get_from_model_instance(self) -> None:
188
+ class UserConstructor(BaseDjangoModelConstructor[User]):
189
+ obj: User
190
+
191
+ constructor = UserConstructor(self.user)
192
+
193
+ assert constructor.obj == self.user
File without changes