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
@@ -5,16 +5,16 @@ from dataclasses import dataclass
5
5
  from typing import TYPE_CHECKING
6
6
 
7
7
  from django.conf import settings
8
- from django.core.files.base import ContentFile
9
-
10
8
  from django.contrib.contenttypes.models import ContentType
11
9
  from django.core.exceptions import ObjectDoesNotExist
10
+ from django.core.files.base import ContentFile
12
11
 
13
12
  from django_spire.file.models import File
14
13
  from django_spire.file.utils import random_64_char_token
15
14
 
16
15
  if TYPE_CHECKING:
17
16
  from django.core.files.uploadedfile import InMemoryUploadedFile
17
+
18
18
  from django_spire.file.mixins import FileModelMixin
19
19
 
20
20
 
@@ -26,7 +26,7 @@ class FileFormatter:
26
26
  type: str = ...
27
27
  app_name: str = 'Uncategorized'
28
28
 
29
- def __post_init__(self):
29
+ def __post_init__(self) -> None:
30
30
  self.name = self._format_name()
31
31
  self.type = self._parse_type()
32
32
 
@@ -60,6 +60,10 @@ class FileFormatter:
60
60
 
61
61
  return file_obj
62
62
 
63
+ def save_file(self, file_obj: File) -> None:
64
+ file_path = self.location + '.' + self.type
65
+ file_obj.file.save(file_path, ContentFile(self.file.read()), save=False)
66
+
63
67
  def size_verbose(self) -> str:
64
68
  if self.file.size < 512000:
65
69
  value = round(self.file.size / 1000, 2)
@@ -76,10 +80,6 @@ class FileFormatter:
76
80
 
77
81
  return str(value) + ext
78
82
 
79
- def save_file(self, file_obj: File):
80
- file_path = self.location + '.' + self.type
81
- file_obj.file.save(file_path, ContentFile(self.file.read()), save=False)
82
-
83
83
 
84
84
  @dataclass
85
85
  class FileContentObjectFormatter(FileFormatter):
@@ -116,7 +116,7 @@ class FileUploader(ABC):
116
116
  related_field: str | None
117
117
  app_name: str = 'Uncategorized'
118
118
 
119
- def null_file_obj(self, file):
119
+ def null_file_obj(self, file: InMemoryUploadedFile) -> File:
120
120
  formatted_file = FileFormatter(
121
121
  file=file,
122
122
  related_field=self.related_field,
@@ -135,12 +135,7 @@ class FileUploader(ABC):
135
135
 
136
136
  @dataclass
137
137
  class SingleFileUploader(FileUploader):
138
- def upload(self, file: InMemoryUploadedFile) -> File:
139
- file = self.null_file_obj(file)
140
- file.save()
141
- return file
142
-
143
- def delete_old_files(self, content_object: FileModelMixin):
138
+ def delete_old_files(self, content_object: FileModelMixin) -> None:
144
139
  old_files = content_object.files.active().related_field(self.related_field)
145
140
 
146
141
  for file in old_files:
@@ -149,9 +144,14 @@ class SingleFileUploader(FileUploader):
149
144
 
150
145
  File.objects.bulk_update(old_files, ['is_active', 'is_deleted'])
151
146
 
147
+ def upload(self, file: InMemoryUploadedFile) -> File:
148
+ file = self.null_file_obj(file)
149
+ file.save()
150
+ return file
151
+
152
152
  def upload_from_form_field(
153
153
  self,
154
- form_data,
154
+ form_data: dict,
155
155
  content_object: FileModelMixin
156
156
  ) -> File:
157
157
  try:
@@ -1,9 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Any
4
-
5
3
  from django.contrib.contenttypes.fields import GenericRelation
6
-
7
4
  from django.db import models
8
5
 
9
6
  from django_spire.file.models import File
@@ -13,7 +10,7 @@ from django_spire.file.tools import copy_files_from_source_to_target_model_objec
13
10
  class FileModelMixin(models.Model):
14
11
  files = GenericRelation(File, editable=False)
15
12
 
16
- def copy_files_to_target_model_object(self, target: Any[models.Model]) -> File:
13
+ def copy_files_to_target_model_object(self, target: models.Model) -> list[File]:
17
14
  # TODO: Move to File Service
18
15
  return copy_files_from_source_to_target_model_object(source=self, target=target)
19
16
 
@@ -2,9 +2,8 @@ from __future__ import annotations
2
2
 
3
3
  import json
4
4
 
5
- from django.contrib.contenttypes.models import ContentType
6
-
7
5
  from django.contrib.contenttypes.fields import GenericForeignKey
6
+ from django.contrib.contenttypes.models import ContentType
8
7
  from django.db import models
9
8
 
10
9
  from django_spire.file.queryset import FileQuerySet
@@ -25,10 +24,10 @@ class File(HistoryModelMixin):
25
24
 
26
25
  objects = FileQuerySet.as_manager()
27
26
 
28
- def __str__(self):
27
+ def __str__(self) -> str:
29
28
  return self.name
30
29
 
31
- def to_dict(self) -> dict[str, str]:
30
+ def to_dict(self) -> dict[str, str | int]:
32
31
  return {
33
32
  'name': self.name,
34
33
  'url': self.file.url,
@@ -45,4 +44,3 @@ class File(HistoryModelMixin):
45
44
  indexes = [
46
45
  models.Index(fields=['content_type', 'object_id']),
47
46
  ]
48
-
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ from io import BytesIO
4
+ from typing import TYPE_CHECKING
5
+
6
+ from django.core.files.uploadedfile import InMemoryUploadedFile, SimpleUploadedFile
7
+
8
+ from django_spire.file.models import File
9
+
10
+ if TYPE_CHECKING:
11
+ from django.contrib.contenttypes.models import ContentType
12
+
13
+
14
+ def create_test_file(
15
+ content_type: ContentType | None = None,
16
+ object_id: int | None = None,
17
+ name: str = 'test_file',
18
+ file_type: str = 'pdf',
19
+ size: str = '1.5 Mb',
20
+ related_field: str | None = None,
21
+ is_active: bool = True,
22
+ is_deleted: bool = False,
23
+ ) -> File:
24
+ file_content = b'test content'
25
+ uploaded_file = SimpleUploadedFile(
26
+ name=f'{name}.{file_type}',
27
+ content=file_content,
28
+ content_type='application/octet-stream'
29
+ )
30
+
31
+ return File.objects.create(
32
+ content_type=content_type,
33
+ object_id=object_id,
34
+ file=uploaded_file,
35
+ name=name,
36
+ type=file_type,
37
+ size=size,
38
+ related_field=related_field,
39
+ is_active=is_active,
40
+ is_deleted=is_deleted,
41
+ )
42
+
43
+
44
+ def create_test_in_memory_uploaded_file(
45
+ name: str = 'test_file',
46
+ file_type: str = 'pdf',
47
+ content: bytes = b'test content',
48
+ ) -> InMemoryUploadedFile:
49
+ file_io = BytesIO(content)
50
+ file_io.seek(0)
51
+
52
+ return InMemoryUploadedFile(
53
+ file=file_io,
54
+ field_name='file',
55
+ name=f'{name}.{file_type}',
56
+ content_type='application/octet-stream',
57
+ size=len(content),
58
+ charset=None,
59
+ )
@@ -0,0 +1,69 @@
1
+ from __future__ import annotations
2
+
3
+ from django.contrib.admin.sites import AdminSite
4
+ from django.contrib.contenttypes.models import ContentType
5
+ from django.test import override_settings
6
+
7
+ from django_spire.core.tests.test_cases import BaseTestCase
8
+ from django_spire.file.admin import FileAdmin
9
+ from django_spire.file.models import File
10
+ from django_spire.file.tests.factories import create_test_file
11
+ from django_spire.help_desk.tests.factories import create_test_helpdesk_ticket
12
+
13
+
14
+ STORAGES_OVERRIDE = {
15
+ 'default': {
16
+ 'BACKEND': 'django.core.files.storage.FileSystemStorage',
17
+ },
18
+ 'staticfiles': {
19
+ 'BACKEND': 'django.contrib.staticfiles.storage.StaticFilesStorage',
20
+ },
21
+ }
22
+
23
+
24
+ @override_settings(STORAGES=STORAGES_OVERRIDE)
25
+ class FileAdminTests(BaseTestCase):
26
+ def setUp(self):
27
+ super().setUp()
28
+
29
+ self.site = AdminSite()
30
+ self.admin = FileAdmin(File, self.site)
31
+ self.file = create_test_file()
32
+
33
+ def test_list_display(self):
34
+ expected = ('id', 'name', 'type', 'size', 'content_object_link', 'file_link')
35
+
36
+ assert self.admin.list_display == expected
37
+
38
+ def test_list_filter(self):
39
+ assert self.admin.list_filter == ('type',)
40
+
41
+ def test_search_fields(self):
42
+ assert self.admin.search_fields == ('id', 'name', 'type')
43
+
44
+ def test_ordering(self):
45
+ assert self.admin.ordering == ('-id',)
46
+
47
+ def test_content_object_link_no_related_object(self):
48
+ result = self.admin.content_object_link(self.file)
49
+
50
+ assert result == 'No Related Object'
51
+
52
+ def test_content_object_link_with_related_object(self):
53
+ ticket = create_test_helpdesk_ticket()
54
+ content_type = ContentType.objects.get_for_model(ticket.__class__)
55
+ self.file.content_type = content_type
56
+ self.file.object_id = ticket.pk
57
+ self.file.save()
58
+
59
+ result = self.admin.content_object_link(self.file)
60
+
61
+ assert 'href=' in result
62
+ assert str(ticket) in result
63
+
64
+ def test_file_link(self):
65
+ result = self.admin.file_link(self.file)
66
+
67
+ assert f'href="{self.file.file.url}"' in result
68
+ assert self.file.name in result
69
+ assert 'download' in result
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from django_spire.core.tests.test_cases import BaseTestCase
4
+ from django_spire.file.apps import FileConfig
5
+
6
+
7
+ class FileConfigTests(BaseTestCase):
8
+ def test_default_auto_field(self):
9
+ assert FileConfig.default_auto_field == 'django.db.models.BigAutoField'
10
+
11
+ def test_label(self):
12
+ assert FileConfig.label == 'django_spire_file'
13
+
14
+ def test_name(self):
15
+ assert FileConfig.name == 'django_spire.file'
16
+
17
+ def test_required_apps(self):
18
+ assert FileConfig.REQUIRED_APPS == ('django_spire_core',)
19
+
20
+ def test_urlpatterns_include(self):
21
+ assert FileConfig.URLPATTERNS_INCLUDE == 'django_spire.file.urls'
22
+
23
+ def test_urlpatterns_namespace(self):
24
+ assert FileConfig.URLPATTERNS_NAMESPACE == 'file'
@@ -0,0 +1,114 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+
5
+ from django.test import override_settings
6
+
7
+ from django_spire.core.tests.test_cases import BaseTestCase
8
+ from django_spire.file.fields import MultipleFileField, SingleFileField
9
+ from django_spire.file.models import File
10
+ from django_spire.file.tests.factories import create_test_file
11
+ from django_spire.file.widgets import MultipleWidget, SingleFileWidget
12
+
13
+
14
+ STORAGES_OVERRIDE = {
15
+ 'default': {
16
+ 'BACKEND': 'django.core.files.storage.FileSystemStorage',
17
+ },
18
+ 'staticfiles': {
19
+ 'BACKEND': 'django.contrib.staticfiles.storage.StaticFilesStorage',
20
+ },
21
+ }
22
+
23
+
24
+ @override_settings(STORAGES=STORAGES_OVERRIDE)
25
+ class MultipleFileFieldTests(BaseTestCase):
26
+ def setUp(self):
27
+ super().setUp()
28
+
29
+ self.field = MultipleFileField()
30
+
31
+ def test_widget_is_multiple_widget(self):
32
+ assert isinstance(self.field.widget, MultipleWidget)
33
+
34
+ def test_prepare_value_with_files(self):
35
+ file1 = create_test_file(name='file1')
36
+ file2 = create_test_file(name='file2')
37
+
38
+ result = self.field.prepare_value([file1, file2])
39
+ parsed = json.loads(result)
40
+
41
+ assert len(parsed) == 2
42
+
43
+ def test_prepare_value_with_none(self):
44
+ result = self.field.prepare_value(None)
45
+
46
+ assert result == '[]'
47
+
48
+ def test_prepare_value_with_empty_list(self):
49
+ result = self.field.prepare_value([])
50
+
51
+ assert result == '[]'
52
+
53
+ def test_prepare_value_contains_file_data(self):
54
+ file = create_test_file(name='test')
55
+
56
+ result = self.field.prepare_value([file])
57
+ parsed = json.loads(result)
58
+
59
+ assert parsed[0]['name'] == 'test'
60
+ assert 'id' in parsed[0]
61
+ assert 'url' in parsed[0]
62
+
63
+ def test_clean_returns_data(self):
64
+ data = {'test': 'value'}
65
+
66
+ result = self.field.clean(data)
67
+
68
+ assert result == data
69
+
70
+
71
+ @override_settings(STORAGES=STORAGES_OVERRIDE)
72
+ class SingleFileFieldTests(BaseTestCase):
73
+ def setUp(self):
74
+ super().setUp()
75
+
76
+ self.field = SingleFileField()
77
+
78
+ def test_widget_is_single_file_widget(self):
79
+ assert isinstance(self.field.widget, SingleFileWidget)
80
+
81
+ def test_prepare_value_with_file(self):
82
+ file = create_test_file(name='test')
83
+
84
+ result = self.field.prepare_value(file)
85
+ parsed = json.loads(result)
86
+
87
+ assert parsed['name'] == 'test'
88
+ assert 'id' in parsed
89
+ assert 'url' in parsed
90
+
91
+ def test_prepare_value_with_none(self):
92
+ result = self.field.prepare_value(None)
93
+
94
+ assert result == 'null'
95
+
96
+ def test_prepare_value_with_queryset(self):
97
+ file = create_test_file(name='queryset_file')
98
+
99
+ result = self.field.prepare_value(File.objects.filter(pk=file.pk))
100
+ parsed = json.loads(result)
101
+
102
+ assert parsed['name'] == 'queryset_file'
103
+
104
+ def test_prepare_value_with_empty_queryset(self):
105
+ result = self.field.prepare_value(File.objects.none())
106
+
107
+ assert result == 'null'
108
+
109
+ def test_clean_returns_data(self):
110
+ data = {'test': 'value'}
111
+
112
+ result = self.field.clean(data)
113
+
114
+ assert result == data
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ from django_spire.core.tests.test_cases import BaseTestCase
4
+ from django_spire.file.fields import MultipleFileField
5
+ from django_spire.file.forms import FileForm
6
+
7
+
8
+ class FileFormTests(BaseTestCase):
9
+ def test_has_files_field(self):
10
+ assert 'files' in FileForm().fields
11
+
12
+ def test_files_field_is_multiple_file_field(self):
13
+ form = FileForm()
14
+
15
+ assert isinstance(form.fields['files'], MultipleFileField)
16
+
17
+ def test_form_field_count(self):
18
+ form = FileForm()
19
+
20
+ assert len(form.fields) == 1
@@ -0,0 +1,183 @@
1
+ from __future__ import annotations
2
+
3
+ from django.conf import settings
4
+ from django.test import override_settings
5
+
6
+ from django_spire.core.tests.test_cases import BaseTestCase
7
+ from django_spire.file.interfaces import (
8
+ FileFormatter,
9
+ MultiFileUploader,
10
+ SingleFileUploader,
11
+ )
12
+ from django_spire.file.models import File
13
+ from django_spire.file.tests.factories import create_test_in_memory_uploaded_file
14
+
15
+
16
+ STORAGES_OVERRIDE = {
17
+ 'default': {
18
+ 'BACKEND': 'django.core.files.storage.FileSystemStorage',
19
+ },
20
+ 'staticfiles': {
21
+ 'BACKEND': 'django.contrib.staticfiles.storage.StaticFilesStorage',
22
+ },
23
+ }
24
+
25
+
26
+ class FileFormatterTests(BaseTestCase):
27
+ def setUp(self):
28
+ super().setUp()
29
+
30
+ self.uploaded_file = create_test_in_memory_uploaded_file(
31
+ name='document',
32
+ file_type='pdf',
33
+ content=b'test content',
34
+ )
35
+ self.formatter = FileFormatter(file=self.uploaded_file)
36
+
37
+ def test_format_name(self):
38
+ assert self.formatter.name == 'document'
39
+
40
+ def test_parse_type(self):
41
+ assert self.formatter.type == 'pdf'
42
+
43
+ def test_location_contains_base_folder(self):
44
+ assert self.formatter.location.startswith(settings.BASE_FOLDER_NAME + '/')
45
+
46
+ def test_location_contains_app_name(self):
47
+ assert '/Uncategorized/' in self.formatter.location
48
+
49
+ def test_location_contains_file_name(self):
50
+ assert self.formatter.location.endswith('document')
51
+
52
+ def test_location_with_related_field(self):
53
+ formatter = FileFormatter(
54
+ file=self.uploaded_file,
55
+ related_field='abc',
56
+ )
57
+
58
+ assert '/abc/' in formatter.location
59
+
60
+ def test_location_with_custom_app_name(self):
61
+ formatter = FileFormatter(
62
+ file=self.uploaded_file,
63
+ app_name='CustomApp',
64
+ )
65
+
66
+ assert '/CustomApp/' in formatter.location
67
+
68
+ def test_size_verbose_kb(self):
69
+ content = b'x' * 1000
70
+ uploaded_file = create_test_in_memory_uploaded_file(content=content)
71
+ formatter = FileFormatter(file=uploaded_file)
72
+
73
+ assert 'kb' in formatter.size_verbose()
74
+
75
+ def test_size_verbose_mb(self):
76
+ content = b'x' * 1000000
77
+ uploaded_file = create_test_in_memory_uploaded_file(content=content)
78
+ formatter = FileFormatter(file=uploaded_file)
79
+
80
+ assert 'Mb' in formatter.size_verbose()
81
+
82
+ @override_settings(STORAGES=STORAGES_OVERRIDE)
83
+ def test_null_file_obj_returns_file(self):
84
+ result = self.formatter.null_file_obj()
85
+
86
+ assert isinstance(result, File)
87
+ assert result.name == 'document'
88
+ assert result.type == 'pdf'
89
+
90
+ @override_settings(STORAGES=STORAGES_OVERRIDE)
91
+ def test_null_file_obj_not_saved(self):
92
+ result = self.formatter.null_file_obj()
93
+
94
+ assert result.pk is None
95
+
96
+
97
+ @override_settings(STORAGES=STORAGES_OVERRIDE)
98
+ class SingleFileUploaderTests(BaseTestCase):
99
+ def setUp(self):
100
+ super().setUp()
101
+
102
+ self.uploader = SingleFileUploader(related_field='abc')
103
+ self.uploaded_file = create_test_in_memory_uploaded_file()
104
+
105
+ def test_upload_creates_file(self):
106
+ initial_count = File.objects.count()
107
+
108
+ self.uploader.upload(self.uploaded_file)
109
+
110
+ assert File.objects.count() == initial_count + 1
111
+
112
+ def test_upload_returns_file(self):
113
+ result = self.uploader.upload(self.uploaded_file)
114
+
115
+ assert isinstance(result, File)
116
+ assert result.pk is not None
117
+
118
+ def test_upload_sets_name(self):
119
+ result = self.uploader.upload(self.uploaded_file)
120
+
121
+ assert result.name == 'test_file'
122
+
123
+ def test_upload_sets_type(self):
124
+ result = self.uploader.upload(self.uploaded_file)
125
+
126
+ assert result.type == 'pdf'
127
+
128
+ def test_null_file_obj(self):
129
+ result = self.uploader.null_file_obj(self.uploaded_file)
130
+
131
+ assert isinstance(result, File)
132
+ assert result.pk is None
133
+
134
+
135
+ @override_settings(STORAGES=STORAGES_OVERRIDE)
136
+ class MultiFileUploaderTests(BaseTestCase):
137
+ def setUp(self):
138
+ super().setUp()
139
+
140
+ self.uploader = MultiFileUploader(related_field='abc')
141
+ self.uploaded_files = [
142
+ create_test_in_memory_uploaded_file(name='file1'),
143
+ create_test_in_memory_uploaded_file(name='file2'),
144
+ ]
145
+
146
+ def test_upload_creates_multiple_files(self):
147
+ initial_count = File.objects.count()
148
+
149
+ self.uploader.upload(self.uploaded_files)
150
+
151
+ assert File.objects.count() == initial_count + 2
152
+
153
+ def test_upload_returns_list_of_files(self):
154
+ result = self.uploader.upload(self.uploaded_files)
155
+
156
+ assert isinstance(result, list)
157
+ assert len(result) == 2
158
+
159
+ def test_upload_returns_file_instances(self):
160
+ result = self.uploader.upload(self.uploaded_files)
161
+
162
+ for file in result:
163
+ assert isinstance(file, File)
164
+ assert file.pk is not None
165
+
166
+ def test_upload_sets_names(self):
167
+ result = self.uploader.upload(self.uploaded_files)
168
+ names = [f.name for f in result]
169
+
170
+ assert 'file1' in names
171
+ assert 'file2' in names
172
+
173
+ def test_upload_empty_list(self):
174
+ result = self.uploader.upload([])
175
+
176
+ assert result == []
177
+
178
+ def test_null_file_obj(self):
179
+ uploaded_file = create_test_in_memory_uploaded_file()
180
+ result = self.uploader.null_file_obj(uploaded_file)
181
+
182
+ assert isinstance(result, File)
183
+ assert result.pk is None
@@ -0,0 +1,82 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+
5
+ from django.test import override_settings
6
+
7
+ from django_spire.core.tests.test_cases import BaseTestCase
8
+ from django_spire.file.tests.factories import create_test_file
9
+
10
+
11
+ STORAGES_OVERRIDE = {
12
+ 'default': {
13
+ 'BACKEND': 'django.core.files.storage.FileSystemStorage',
14
+ },
15
+ 'staticfiles': {
16
+ 'BACKEND': 'django.contrib.staticfiles.storage.StaticFilesStorage',
17
+ },
18
+ }
19
+
20
+
21
+ @override_settings(STORAGES=STORAGES_OVERRIDE)
22
+ class FileModelTests(BaseTestCase):
23
+ def setUp(self):
24
+ super().setUp()
25
+
26
+ self.file = create_test_file()
27
+
28
+ def test_str(self):
29
+ assert str(self.file) == self.file.name
30
+
31
+ def test_default_is_active(self):
32
+ assert self.file.is_active is True
33
+
34
+ def test_default_is_deleted(self):
35
+ assert self.file.is_deleted is False
36
+
37
+ def test_name_field(self):
38
+ assert self.file.name == 'test_file'
39
+
40
+ def test_type_field(self):
41
+ assert self.file.type == 'pdf'
42
+
43
+ def test_size_field(self):
44
+ assert self.file.size == '1.5 Mb'
45
+
46
+ def test_related_field_default(self):
47
+ assert self.file.related_field is None
48
+
49
+ def test_related_field_set(self):
50
+ file = create_test_file(related_field='abc')
51
+ assert file.related_field == 'abc'
52
+
53
+ def test_content_type_null(self):
54
+ assert self.file.content_type is None
55
+
56
+ def test_object_id_null(self):
57
+ assert self.file.object_id is None
58
+
59
+ def test_created_datetime_auto_set(self):
60
+ assert self.file.created_datetime is not None
61
+
62
+ def test_to_dict(self):
63
+ result = self.file.to_dict()
64
+
65
+ assert result['name'] == self.file.name
66
+ assert result['id'] == self.file.id
67
+ assert 'url' in result
68
+
69
+ def test_to_dict_keys(self):
70
+ result = self.file.to_dict()
71
+
72
+ assert set(result.keys()) == {'name', 'url', 'id'}
73
+
74
+ def test_to_json(self):
75
+ result = self.file.to_json()
76
+ parsed = json.loads(result)
77
+
78
+ assert parsed['name'] == self.file.name
79
+ assert parsed['id'] == self.file.id
80
+
81
+ def test_file_field_url(self):
82
+ assert self.file.file.url is not None