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,15 +1,22 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
1
5
  from django_spire.contrib.seeding.field.cleaners import normalize_seeder_fields
2
6
  from django_spire.contrib.seeding.model.enums import ModelSeederDefaultsEnum
3
7
 
8
+ if TYPE_CHECKING:
9
+ from typing import Any, Self
10
+
4
11
 
5
12
  class FieldsConfig:
6
13
  def __init__(
7
- self,
8
- raw_fields: dict,
9
- field_names: list[str],
10
- default_to: str,
11
- model_class
12
- ):
14
+ self,
15
+ raw_fields: dict[str, Any],
16
+ field_names: list[str],
17
+ default_to: str,
18
+ model_class: Any
19
+ ) -> None:
13
20
  self.raw_fields = raw_fields or {}
14
21
  self.default_to = default_to
15
22
  self.field_names = set(field_names)
@@ -17,7 +24,7 @@ class FieldsConfig:
17
24
 
18
25
  self._excluded = {
19
26
  k for k, v in self.raw_fields.items()
20
- if v == "exclude" or v == ("exclude",)
27
+ if v in ("exclude", ("exclude",))
21
28
  }
22
29
 
23
30
  self.fields = normalize_seeder_fields({
@@ -26,14 +33,18 @@ class FieldsConfig:
26
33
 
27
34
  self._validate()
28
35
  self._assign_defaults()
29
- self._order_fields() # Fields need to be in order for caching hash
30
36
 
31
- def _validate(self):
37
+ # Fields need to be in order for caching hash
38
+ self._order_fields()
39
+
40
+ def _validate(self) -> None:
32
41
  unknown = set(self.fields.keys()) - self.field_names
42
+
33
43
  if unknown:
34
- raise ValueError(f"Invalid field name(s): {', '.join(unknown)}")
44
+ message = f"Invalid field name(s): {', '.join(unknown)}"
45
+ raise ValueError(message)
35
46
 
36
- def _assign_defaults(self):
47
+ def _assign_defaults(self) -> None:
37
48
  if self.default_to == ModelSeederDefaultsEnum.INCLUDED:
38
49
  return
39
50
 
@@ -43,7 +54,7 @@ class FieldsConfig:
43
54
  if name not in self.fields and name not in self._excluded:
44
55
  self.fields[name] = (method,)
45
56
 
46
- def override(self, overrides: dict):
57
+ def override(self, overrides: dict) -> Self:
47
58
  merged = {**self.fields, **normalize_seeder_fields(overrides)}
48
59
  new_raw = {
49
60
  **{k: v for k, v in self.raw_fields.items() if k in self._excluded},
@@ -56,9 +67,14 @@ class FieldsConfig:
56
67
  model_class=self.model_class
57
68
  )
58
69
 
59
- def _order_fields(self):
60
- self.fields = {k: v for k, v in sorted(self.fields.items(), key=lambda x: x[0])}
70
+ def _order_fields(self) -> None:
71
+ self.fields = dict(
72
+ sorted(
73
+ self.fields.items(),
74
+ key=lambda x: x[0]
75
+ )
76
+ )
61
77
 
62
78
  @property
63
- def excluded(self):
79
+ def excluded(self) -> set:
64
80
  return self._excluded
@@ -1,9 +1,10 @@
1
+ from __future__ import annotations
2
+
1
3
  from django_spire.contrib.seeding.model.config import FieldsConfig
2
4
 
3
5
 
4
6
  class DjangoModelFieldsConfig(FieldsConfig):
5
-
6
- def _validate(self):
7
+ def _validate(self) -> None:
7
8
  valid_field_names = set(self.field_names)
8
9
 
9
10
  valid_field_names.update(f.attname for f in self.model_class._meta.fields)
@@ -11,18 +12,18 @@ class DjangoModelFieldsConfig(FieldsConfig):
11
12
  unknown = set(self.fields.keys()) - valid_field_names
12
13
 
13
14
  if unknown:
14
- raise ValueError(f"Invalid field name(s): {', '.join(unknown)}")
15
+ message = f"Invalid field name(s): {', '.join(unknown)}"
16
+ raise ValueError(message)
15
17
 
16
18
  # fk_fields = {
17
19
  # f.name: f.attname # f.attname = "user_id"
18
20
  # for f in self.model_class._meta.fields
19
21
  # if isinstance(f, ForeignKey)
20
22
  # }
21
- #
23
+
22
24
  # for name, value in self.fields.items():
23
25
  # if name in fk_fields:
24
26
  # valid_types = [int, None]
25
- #
26
27
 
27
28
  # def map_foreign_keys(self):
28
29
  # fk_fields = {
@@ -30,19 +31,20 @@ class DjangoModelFieldsConfig(FieldsConfig):
30
31
  # for f in self.model_class._meta.fields
31
32
  # if isinstance(f, ForeignKey)
32
33
  # }
33
- #
34
+
34
35
  # # If asset_id and asset are passed, should I raise an error?
35
- #
36
+
36
37
  # {
37
38
  # 'asset' : ('custom', 'in_order', {'values': [a.id for a in assets]}),
38
39
  # 'asset_id': ('custom', 'in_order', {'values': [a.id for a in assets]}),
39
40
  # }
40
- #
41
+
41
42
  # translated_fields = {}
43
+
42
44
  # for name, value in self.fields.items():
43
45
  # if name in fk_fields:
44
46
  # translated_name = fk_fields[name]
45
- #
47
+
46
48
  # if translated_name in self.field_names:
47
49
  # # Do not want to override asset_id if the user has already pass it in.
48
50
  # continue
@@ -50,9 +52,7 @@ class DjangoModelFieldsConfig(FieldsConfig):
50
52
  # translated_fields[translated_name] = value
51
53
  # else:
52
54
  # translated_fields[name] = value
53
- #
55
+
54
56
  # return translated_fields
55
- #
57
+
56
58
  # # Update self.fields.
57
- #
58
- #
@@ -2,8 +2,8 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TypeVar, TYPE_CHECKING
4
4
 
5
- from django.db.models import ForeignKey
6
5
  from django.db.models.base import Model
6
+ from django.db.models import ForeignKey
7
7
 
8
8
  from django_spire.contrib.seeding.field.callable import CallableFieldSeeder
9
9
  from django_spire.contrib.seeding.field.custom import CustomFieldSeeder
@@ -37,11 +37,11 @@ class DjangoModelSeeder(BaseModelSeeder):
37
37
  super().__init_subclass__(**kwargs)
38
38
 
39
39
  if cls.model_class is None:
40
- message = "Seeds must have a model class"
40
+ message = 'Seeds must have a model class'
41
41
  raise ValueError(message)
42
42
 
43
43
  if cls.fields is None:
44
- message = "Seeds must have fields"
44
+ message = 'Seeds must have fields'
45
45
  raise ValueError(message)
46
46
 
47
47
  @classmethod
@@ -55,7 +55,7 @@ class DjangoModelSeeder(BaseModelSeeder):
55
55
  @classmethod
56
56
  def seed_database(
57
57
  cls,
58
- count=1,
58
+ count: int = 1,
59
59
  fields: dict | None = None
60
60
  ) -> list[TypeModel]:
61
61
  model_objects = cls.seed(count, fields)
@@ -1,5 +1,9 @@
1
- import unittest
1
+ from __future__ import annotations
2
+
3
+ import pytest
4
+
2
5
  from django.db import models
6
+ from django.test import TestCase
3
7
  from django.utils.timezone import now
4
8
 
5
9
  from django_spire.contrib.seeding import DjangoModelSeeder
@@ -15,39 +19,46 @@ class Product(models.Model):
15
19
  updated_at = models.DateTimeField(null=True, blank=True)
16
20
 
17
21
  class Meta:
18
- app_label = "tests"
22
+ app_label = 'tests'
19
23
  managed = False
20
24
 
25
+ def __str__(self) -> str:
26
+ return 'Product'
27
+
21
28
 
22
29
  class ProductSeeder(DjangoModelSeeder):
23
30
  model_class = Product
24
31
  fields = {
25
- "name": ('llm', 'Clothing store'),
26
- "description": ('static', 'A great product for testing.'),
27
- "price": ('faker', 'pydecimal', {'left_digits': 2, 'right_digits': 2, 'positive': True}),
28
- "in_stock": True,
29
- "created_at": ("callable", now),
30
- "updated_at": 'exclude',
32
+ 'name': ('llm', 'Clothing store'),
33
+ 'description': ('static', 'A great product for testing.'),
34
+ 'price': ('faker', 'pydecimal', {'left_digits': 2, 'right_digits': 2, 'positive': True}),
35
+ 'in_stock': True,
36
+ 'created_at': ('callable', now),
37
+ 'updated_at': 'exclude',
31
38
  }
32
39
 
33
40
 
34
- class TestProductSeeder(unittest.TestCase):
41
+ class TestProductSeeder(TestCase):
42
+ def test_assign_defaults(self) -> None:
43
+ assert 'sku' in ProductSeeder.resolved_fields()
44
+
45
+ def test_field_override_applies(self) -> None:
46
+ override_time = now()
47
+
48
+ result = ProductSeeder.seed(count=1, fields={'updated_at': ('static', override_time)})
35
49
 
36
- def test_assign_defaults(self):
37
- self.assertIn('sku', ProductSeeder.resolved_fields().keys())
50
+ assert result[0].updated_at == override_time
38
51
 
39
- def test_seeds_three_objects(self):
52
+ def test_raises_for_invalid_field(self) -> None:
53
+ with pytest.raises(Exception) as exc_info:
54
+ ProductSeeder.seed(count=1, fields={'not_a_field': ('static', 'value')})
55
+
56
+ assert 'not_a_field' in str(exc_info.value)
57
+
58
+ def test_seeds_three_objects(self) -> None:
40
59
  result = ProductSeeder.seed(count=3)
41
- self.assertEqual(len(result), 3)
42
- for obj in result:
43
- self.assertIsInstance(obj, Product)
44
60
 
45
- def test_raises_for_invalid_field(self):
46
- with self.assertRaises(Exception) as context:
47
- ProductSeeder.seed(count=1, fields={"not_a_field": ("static", "value")})
48
- self.assertIn("not_a_field", str(context.exception))
61
+ assert len(result) == 3
49
62
 
50
- def test_field_override_applies(self):
51
- override_time = now()
52
- result = ProductSeeder.seed(count=1, fields={"updated_at": ("static", override_time)})
53
- self.assertEqual(result[0].updated_at, override_time)
63
+ for obj in result:
64
+ assert isinstance(obj, Product)
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from enum import Enum
2
4
 
3
5
 
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ import pytest
4
+
5
+ from unittest.mock import MagicMock
6
+
7
+ from django.test import TestCase
8
+
9
+ from django_spire.contrib.seeding.model.config import FieldsConfig
10
+
11
+
12
+ class TestFieldsConfig(TestCase):
13
+ def setUp(self) -> None:
14
+ self.model_class = MagicMock()
15
+
16
+ def test_assign_defaults_adds_missing_fields(self) -> None:
17
+ config = FieldsConfig(
18
+ raw_fields={'name': ('static', 'test')},
19
+ field_names=['name', 'email'],
20
+ default_to='llm',
21
+ model_class=self.model_class
22
+ )
23
+
24
+ assert 'email' in config.fields
25
+ assert config.fields['email'] == ('llm',)
26
+
27
+ def test_excluded_property(self) -> None:
28
+ config = FieldsConfig(
29
+ raw_fields={'name': 'exclude', 'email': ('exclude',)},
30
+ field_names=['name', 'email', 'status'],
31
+ default_to='llm',
32
+ model_class=self.model_class
33
+ )
34
+
35
+ assert 'name' in config.excluded
36
+ assert 'email' in config.excluded
37
+ assert 'status' not in config.excluded
38
+
39
+ def test_fields_are_ordered(self) -> None:
40
+ config = FieldsConfig(
41
+ raw_fields={'zebra': ('static', 'z'), 'apple': ('static', 'a')},
42
+ field_names=['zebra', 'apple'],
43
+ default_to='included',
44
+ model_class=self.model_class
45
+ )
46
+
47
+ keys = list(config.fields.keys())
48
+
49
+ assert keys == ['apple', 'zebra']
50
+
51
+ def test_override_creates_new_config(self) -> None:
52
+ config = FieldsConfig(
53
+ raw_fields={'name': ('static', 'original')},
54
+ field_names=['name', 'email'],
55
+ default_to='llm',
56
+ model_class=self.model_class
57
+ )
58
+
59
+ new_config = config.override({'name': ('static', 'overridden')})
60
+
61
+ assert new_config is not config
62
+ assert new_config.fields['name'] == ('static', 'overridden')
63
+
64
+ def test_validate_raises_for_invalid_fields(self) -> None:
65
+ with pytest.raises(ValueError, match='Invalid field name'):
66
+ FieldsConfig(
67
+ raw_fields={'invalid_field': ('static', 'test')},
68
+ field_names=['name', 'email'],
69
+ default_to='llm',
70
+ model_class=self.model_class
71
+ )
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ import pytest
4
+
5
+ from django.test import TestCase
6
+ from django.utils import timezone
7
+
8
+ from django_spire.contrib.seeding.field.custom import CustomFieldSeeder
9
+
10
+
11
+ class TestCustomFieldSeeder(TestCase):
12
+ def test_date_time_between_returns_aware_datetime(self) -> None:
13
+ seeder = CustomFieldSeeder(fields={})
14
+
15
+ result = seeder.date_time_between(start_date='-30d', end_date='now')
16
+
17
+ assert timezone.is_aware(result)
18
+
19
+ def test_in_order_cycles_through_values(self) -> None:
20
+ seeder = CustomFieldSeeder(fields={})
21
+ values = ['a', 'b', 'c']
22
+
23
+ assert seeder.in_order(values, index=0) == 'a'
24
+ assert seeder.in_order(values, index=1) == 'b'
25
+ assert seeder.in_order(values, index=2) == 'c'
26
+ assert seeder.in_order(values, index=3) == 'a'
27
+
28
+ def test_in_order_raises_for_empty_list(self) -> None:
29
+ seeder = CustomFieldSeeder(fields={})
30
+
31
+ with pytest.raises(ValueError, match='Cannot select from empty values list'):
32
+ seeder.in_order(values=[], index=0)
33
+
34
+ def test_keyword_is_custom(self) -> None:
35
+ assert CustomFieldSeeder.keyword == 'custom'
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ from django.test import TestCase
4
+
5
+ from django_spire.contrib.seeding.field.enums import FieldSeederTypesEnum
6
+ from django_spire.contrib.seeding.model.enums import ModelSeederDefaultsEnum
7
+
8
+
9
+ class TestFieldSeederTypesEnum(TestCase):
10
+ def test_callable_value(self) -> None:
11
+ assert FieldSeederTypesEnum.CALLABLE == 'callable'
12
+
13
+ def test_custom_value(self) -> None:
14
+ assert FieldSeederTypesEnum.CUSTOM == 'custom'
15
+
16
+ def test_faker_value(self) -> None:
17
+ assert FieldSeederTypesEnum.FAKER == 'faker'
18
+
19
+ def test_is_str_enum(self) -> None:
20
+ assert isinstance(FieldSeederTypesEnum.LLM, str)
21
+
22
+ def test_llm_value(self) -> None:
23
+ assert FieldSeederTypesEnum.LLM == 'llm'
24
+
25
+ def test_static_value(self) -> None:
26
+ assert FieldSeederTypesEnum.STATIC == 'static'
27
+
28
+
29
+ class TestModelSeederDefaultsEnum(TestCase):
30
+ def test_faker_value(self) -> None:
31
+ assert ModelSeederDefaultsEnum.FAKER == 'faker'
32
+
33
+ def test_included_value(self) -> None:
34
+ assert ModelSeederDefaultsEnum.INCLUDED == 'included'
35
+
36
+ def test_is_str_enum(self) -> None:
37
+ assert isinstance(ModelSeederDefaultsEnum.LLM, str)
38
+
39
+ def test_llm_value(self) -> None:
40
+ assert ModelSeederDefaultsEnum.LLM == 'llm'
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ from django.test import TestCase
4
+
5
+ from django_spire.contrib.seeding.intelligence.intel import SeedingIntel, SourceIntel
6
+
7
+
8
+ class TestSeedingIntel(TestCase):
9
+ def test_is_iterable(self) -> None:
10
+ intel = SeedingIntel(items=[{'name': 'test1'}, {'name': 'test2'}])
11
+
12
+ result = list(intel)
13
+
14
+ assert result == [{'name': 'test1'}, {'name': 'test2'}]
15
+
16
+ def test_items_attribute(self) -> None:
17
+ items = [{'name': 'test'}]
18
+ intel = SeedingIntel(items=items)
19
+
20
+ assert intel.items == items
21
+
22
+
23
+ class TestSourceIntel(TestCase):
24
+ def test_file_name_attribute(self) -> None:
25
+ intel = SourceIntel(file_name='test.py', python_source_code='print("hello")')
26
+
27
+ assert intel.file_name == 'test.py'
28
+
29
+ def test_python_source_code_attribute(self) -> None:
30
+ intel = SourceIntel(file_name='test.py', python_source_code='print("hello")')
31
+
32
+ assert intel.python_source_code == 'print("hello")'
@@ -0,0 +1,63 @@
1
+ from __future__ import annotations
2
+
3
+ from unittest.mock import MagicMock
4
+
5
+ from django.test import TestCase
6
+
7
+ from django_spire.contrib.seeding.field.override import FieldOverride
8
+
9
+
10
+ class TestFieldOverride(TestCase):
11
+ def setUp(self) -> None:
12
+ self.seeder_class = MagicMock()
13
+ self.seeder_class.seed.return_value = [{'name': 'test'}]
14
+
15
+ def test_filter_returns_self(self) -> None:
16
+ override = FieldOverride(self.seeder_class)
17
+
18
+ result = override.filter(name='test')
19
+
20
+ assert result is override
21
+
22
+ def test_filter_updates_overrides(self) -> None:
23
+ override = FieldOverride(self.seeder_class)
24
+
25
+ override.filter(name='test', status='active')
26
+
27
+ assert override.overrides == {'name': 'test', 'status': 'active'}
28
+
29
+ def test_getattr_delegates_to_seeder_class(self) -> None:
30
+ self.seeder_class.some_attribute = 'value'
31
+ override = FieldOverride(self.seeder_class)
32
+
33
+ result = override.some_attribute
34
+
35
+ assert result == 'value'
36
+
37
+ def test_init_sets_seeder_class(self) -> None:
38
+ override = FieldOverride(self.seeder_class)
39
+
40
+ assert override.seeder_class is self.seeder_class
41
+
42
+ def test_init_sets_empty_overrides(self) -> None:
43
+ override = FieldOverride(self.seeder_class)
44
+
45
+ assert override.overrides == {}
46
+
47
+ def test_seed_calls_seeder_class_seed(self) -> None:
48
+ override = FieldOverride(self.seeder_class)
49
+ override.filter(name='override_value')
50
+
51
+ override.seed(count=2)
52
+
53
+ self.seeder_class.seed.assert_called_once_with(
54
+ count=2,
55
+ fields={'name': 'override_value'}
56
+ )
57
+
58
+ def test_seed_returns_seeder_class_result(self) -> None:
59
+ override = FieldOverride(self.seeder_class)
60
+
61
+ result = override.seed(count=1)
62
+
63
+ assert result == [{'name': 'test'}]
@@ -1,7 +1,7 @@
1
- from django_spire.contrib.service.exceptions import ServiceException
1
+ from django_spire.contrib.service.exceptions import ServiceError
2
2
  from django_spire.contrib.service.django_model_service import BaseDjangoModelService
3
3
 
4
4
  __all__ = [
5
5
  'BaseDjangoModelService',
6
- 'ServiceException',
6
+ 'ServiceError',
7
7
  ]
@@ -1,22 +1,26 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
+
4
5
  from abc import ABC
5
- from typing import Type, Generic, TypeVar
6
+ from typing import Generic, TypeVar
6
7
 
7
8
  from django.db import transaction
8
9
  from django.db.models import Model
9
10
 
10
11
  from django_spire.contrib.constructor.django_model_constructor import BaseDjangoModelConstructor
11
- from django_spire.contrib.service.exceptions import ServiceException
12
+ from django_spire.contrib.service.exceptions import ServiceError
13
+
14
+
15
+ log = logging.getLogger(__name__)
12
16
 
13
- TypeDjangoModel = TypeVar('TypeDjangoModel', bound=Model, covariant=True)
17
+ TypeDjangoModel_co = TypeVar('TypeDjangoModel_co', bound=Model, covariant=True)
14
18
 
15
19
 
16
20
  class BaseDjangoModelService(
17
- BaseDjangoModelConstructor[TypeDjangoModel],
21
+ BaseDjangoModelConstructor[TypeDjangoModel_co],
18
22
  ABC,
19
- Generic[TypeDjangoModel]
23
+ Generic[TypeDjangoModel_co]
20
24
  ):
21
25
  def _get_concrete_fields(self) -> dict:
22
26
  return {
@@ -33,7 +37,7 @@ class BaseDjangoModelService(
33
37
 
34
38
  for field, value in field_data.items():
35
39
  if field not in allowed:
36
- logging.warning(f'Field {field!r} is not valid for {self.obj.__class__.__name__}')
40
+ log.warning(f'Field {field!r} is not valid for {self.obj.__class__.__name__}')
37
41
  continue
38
42
 
39
43
  model_field = concrete_fields.get(field.removesuffix("_id"), None)
@@ -51,12 +55,8 @@ class BaseDjangoModelService(
51
55
  concrete_fields = self._get_concrete_fields()
52
56
  touched_fields = self._get_touched_fields(concrete_fields, **field_data)
53
57
 
54
- try:
55
- self.obj.full_clean(
56
- exclude=[field for field in concrete_fields if field not in touched_fields]
57
- )
58
- except:
59
- raise
58
+ exclude = [field for field in concrete_fields if field not in touched_fields]
59
+ self.obj.full_clean(exclude=exclude)
60
60
 
61
61
  return touched_fields
62
62
 
@@ -65,7 +65,8 @@ class BaseDjangoModelService(
65
65
  new_model_obj_was_created = False
66
66
 
67
67
  if not field_data:
68
- raise ServiceException(f'Field data is required to save on {self.obj.__class__.__name__}')
68
+ message = f'Field data is required to save on {self.obj.__class__.__name__}'
69
+ raise ServiceError(message)
69
70
 
70
71
  touched_fields = self.validate_model_obj(**field_data)
71
72
 
@@ -77,7 +78,7 @@ class BaseDjangoModelService(
77
78
  self.obj.save(update_fields=touched_fields)
78
79
 
79
80
  else:
80
- logging.warning(
81
- f'{self.obj.__class__.__name__} is not a new object or there was no touched fields to update.')
81
+ message = f'{self.obj.__class__.__name__} is not a new object or there was no touched fields to update.'
82
+ log.warning(message)
82
83
 
83
84
  return self.obj, new_model_obj_was_created
@@ -1,5 +1,7 @@
1
- from django_spire.exceptions import DjangoSpireException
1
+ from __future__ import annotations
2
2
 
3
+ from django_spire.exceptions import DjangoSpireError
3
4
 
4
- class ServiceException(DjangoSpireException):
5
- pass
5
+
6
+ class ServiceError(DjangoSpireError):
7
+ pass
File without changes