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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (541) hide show
  1. django_spire/ai/admin.py +11 -11
  2. django_spire/ai/chat/apps.py +1 -0
  3. django_spire/ai/chat/templates/django_spire/ai/chat/widget/dialog_widget.html +1 -1
  4. django_spire/ai/chat/tests/factories.py +15 -0
  5. django_spire/ai/chat/tests/test_controller.py +45 -0
  6. django_spire/ai/chat/tests/test_models.py +301 -0
  7. django_spire/ai/chat/tests/test_prompts.py +48 -0
  8. django_spire/ai/chat/tests/test_responses.py +208 -0
  9. django_spire/ai/chat/tests/test_router/test_base_chat_router.py +66 -6
  10. django_spire/ai/chat/tests/test_router/test_chat_workflow.py +73 -3
  11. django_spire/ai/chat/tests/test_router/test_integration.py +86 -6
  12. django_spire/ai/chat/tests/test_router/test_intent_decoder.py +93 -1
  13. django_spire/ai/chat/tests/test_router/test_message_intel.py +60 -1
  14. django_spire/ai/chat/tests/test_router/test_spire_chat_router.py +110 -0
  15. django_spire/ai/chat/tests/test_urls/test_json_urls.py +202 -1
  16. django_spire/ai/context/tests/__init__.py +0 -0
  17. django_spire/ai/context/tests/test_context.py +188 -0
  18. django_spire/ai/decorators.py +7 -6
  19. django_spire/ai/prompt/tests/test_bots.py +100 -10
  20. django_spire/ai/prompt/tests/test_prompt_intel.py +83 -0
  21. django_spire/ai/prompt/tests/test_prompt_tuning.py +126 -0
  22. django_spire/ai/sms/decorators.py +8 -2
  23. django_spire/ai/sms/tests/test_sms.py +240 -16
  24. django_spire/ai/sms/tests/test_sms_intel.py +42 -0
  25. django_spire/ai/sms/tests/test_webhook.py +155 -7
  26. django_spire/ai/sms/views.py +23 -24
  27. django_spire/ai/tests/test_ai.py +131 -7
  28. django_spire/auth/apps.py +4 -2
  29. django_spire/auth/controller/controller.py +36 -23
  30. django_spire/auth/controller/exceptions.py +9 -0
  31. django_spire/auth/group/admin.py +1 -0
  32. django_spire/auth/group/apps.py +2 -0
  33. django_spire/auth/group/factories.py +17 -8
  34. django_spire/auth/group/forms.py +7 -0
  35. django_spire/auth/group/tests/test_factories.py +146 -0
  36. django_spire/auth/group/tests/test_forms.py +282 -0
  37. django_spire/auth/group/tests/test_models.py +192 -0
  38. django_spire/auth/group/tests/test_querysets.py +98 -0
  39. django_spire/auth/group/tests/test_utils.py +341 -0
  40. django_spire/auth/group/tests/test_views.py +377 -0
  41. django_spire/auth/group/urls/__init__.py +3 -1
  42. django_spire/auth/group/urls/form_urls.py +2 -0
  43. django_spire/auth/group/urls/json_urls.py +3 -0
  44. django_spire/auth/group/urls/page_urls.py +2 -0
  45. django_spire/auth/group/utils.py +6 -2
  46. django_spire/auth/group/views/form_views.py +6 -3
  47. django_spire/auth/group/views/json_views.py +6 -2
  48. django_spire/auth/mfa/admin.py +2 -0
  49. django_spire/auth/mfa/apps.py +2 -0
  50. django_spire/auth/mfa/forms.py +1 -0
  51. django_spire/auth/mfa/querysets.py +9 -2
  52. django_spire/auth/mfa/tests/test_models.py +233 -0
  53. django_spire/auth/mfa/tests/test_utils.py +106 -0
  54. django_spire/auth/mfa/urls/__init__.py +2 -0
  55. django_spire/auth/mfa/urls/page_urls.py +2 -0
  56. django_spire/auth/mfa/urls/redirect_urls.py +2 -0
  57. django_spire/auth/mfa/views/page_views.py +2 -1
  58. django_spire/auth/permissions/consts.py +2 -2
  59. django_spire/auth/permissions/decorators.py +8 -8
  60. django_spire/auth/permissions/permissions.py +28 -35
  61. django_spire/auth/permissions/tests/test_decorators.py +333 -0
  62. django_spire/auth/permissions/tests/test_permissions.py +337 -0
  63. django_spire/auth/permissions/tests/test_tools.py +305 -0
  64. django_spire/auth/permissions/tools.py +21 -15
  65. django_spire/auth/seeding/seed.py +3 -0
  66. django_spire/auth/seeding/seeder.py +2 -0
  67. django_spire/auth/tests/test_controller.py +323 -0
  68. django_spire/auth/tests/test_url_endpoints.py +9 -9
  69. django_spire/auth/tests/test_views.py +406 -0
  70. django_spire/auth/urls/admin_urls.py +2 -0
  71. django_spire/auth/urls/redirect_urls.py +2 -0
  72. django_spire/auth/user/apps.py +2 -0
  73. django_spire/auth/user/forms.py +9 -0
  74. django_spire/auth/user/models.py +1 -1
  75. django_spire/auth/user/services/services.py +1 -0
  76. django_spire/auth/user/tests/factories.py +14 -13
  77. django_spire/auth/user/tests/test_factories.py +166 -2
  78. django_spire/auth/user/tests/test_forms.py +573 -0
  79. django_spire/auth/user/tests/test_models.py +257 -0
  80. django_spire/auth/user/tests/test_services.py +200 -0
  81. django_spire/auth/user/tests/test_tools.py +153 -0
  82. django_spire/auth/user/tests/test_user_factories.py +139 -0
  83. django_spire/auth/user/tests/test_views.py +363 -0
  84. django_spire/auth/user/tools.py +7 -1
  85. django_spire/auth/user/urls/form_urls.py +3 -0
  86. django_spire/auth/user/urls/page_urls.py +3 -0
  87. django_spire/auth/user/views/form_views.py +19 -10
  88. django_spire/auth/user/views/page_views.py +8 -2
  89. django_spire/auth/views/redirect_views.py +14 -9
  90. django_spire/comment/admin.py +2 -0
  91. django_spire/comment/apps.py +2 -0
  92. django_spire/comment/templatetags/comment_tags.py +1 -0
  93. django_spire/comment/tests/test_forms.py +27 -0
  94. django_spire/comment/tests/test_models.py +215 -0
  95. django_spire/comment/tests/test_querysets.py +101 -0
  96. django_spire/comment/tests/test_utils.py +90 -0
  97. django_spire/comment/urls.py +2 -0
  98. django_spire/comment/utils.py +22 -13
  99. django_spire/comment/views.py +1 -1
  100. django_spire/conf.py +8 -6
  101. django_spire/consts.py +1 -1
  102. django_spire/contrib/breadcrumb/apps.py +2 -0
  103. django_spire/contrib/breadcrumb/breadcrumbs.py +18 -18
  104. django_spire/contrib/breadcrumb/tests/test_breadcrumbs.py +198 -0
  105. django_spire/contrib/constructor/__init__.py +3 -3
  106. django_spire/contrib/constructor/constructor.py +15 -15
  107. django_spire/contrib/constructor/django_model_constructor.py +5 -4
  108. django_spire/contrib/constructor/exceptions.py +5 -3
  109. django_spire/contrib/constructor/tests/__init__.py +0 -0
  110. django_spire/contrib/constructor/tests/test_constructor.py +193 -0
  111. django_spire/contrib/form/tests/__init__.py +0 -0
  112. django_spire/contrib/form/tests/test_forms.py +203 -0
  113. django_spire/contrib/generic_views/modal_views.py +2 -1
  114. django_spire/contrib/generic_views/portal_views.py +20 -19
  115. django_spire/contrib/generic_views/tests/__init__.py +0 -0
  116. django_spire/contrib/generic_views/tests/test_views.py +459 -0
  117. django_spire/contrib/help/apps.py +2 -0
  118. django_spire/contrib/help/templatetags/help.py +1 -0
  119. django_spire/contrib/help/tests/__init__.py +0 -0
  120. django_spire/contrib/help/tests/test_templatetags.py +100 -0
  121. django_spire/contrib/options/mixins.py +6 -5
  122. django_spire/contrib/options/tests/factories.py +5 -1
  123. django_spire/contrib/options/tests/test_options.py +234 -0
  124. django_spire/contrib/ordering/exceptions.py +7 -3
  125. django_spire/contrib/ordering/mixins.py +2 -0
  126. django_spire/contrib/ordering/querysets.py +3 -1
  127. django_spire/contrib/ordering/services/processor_service.py +8 -4
  128. django_spire/contrib/ordering/services/service.py +1 -2
  129. django_spire/contrib/ordering/tests/__init__.py +0 -0
  130. django_spire/contrib/ordering/tests/test_ordering.py +165 -0
  131. django_spire/contrib/ordering/validators.py +6 -6
  132. django_spire/contrib/pagination/templatetags/pagination_tags.py +12 -5
  133. django_spire/contrib/pagination/tests/__init__.py +0 -0
  134. django_spire/contrib/pagination/tests/test_pagination.py +179 -0
  135. django_spire/contrib/performance/decorators.py +16 -6
  136. django_spire/contrib/performance/tests/__init__.py +0 -0
  137. django_spire/contrib/performance/tests/test_performance.py +107 -0
  138. django_spire/contrib/progress/__init__.py +1 -3
  139. django_spire/contrib/progress/static/django_spire/js/contrib/progress/progress.js +38 -82
  140. django_spire/contrib/queryset/enums.py +3 -1
  141. django_spire/contrib/queryset/filter_tools.py +10 -5
  142. django_spire/contrib/queryset/mixins.py +16 -16
  143. django_spire/contrib/queryset/tests/__init__.py +0 -0
  144. django_spire/contrib/queryset/tests/test_queryset.py +137 -0
  145. django_spire/contrib/seeding/field/base.py +13 -7
  146. django_spire/contrib/seeding/field/callable.py +8 -1
  147. django_spire/contrib/seeding/field/cleaners.py +5 -5
  148. django_spire/contrib/seeding/field/custom.py +20 -10
  149. django_spire/contrib/seeding/field/django/seeder.py +8 -6
  150. django_spire/contrib/seeding/field/enums.py +7 -5
  151. django_spire/contrib/seeding/field/override.py +16 -6
  152. django_spire/contrib/seeding/field/static.py +9 -2
  153. django_spire/contrib/seeding/field/tests/test_base.py +18 -14
  154. django_spire/contrib/seeding/field/tests/test_callable.py +13 -9
  155. django_spire/contrib/seeding/field/tests/test_cleaners.py +51 -38
  156. django_spire/contrib/seeding/field/tests/test_static.py +13 -9
  157. django_spire/contrib/seeding/intelligence/bots/seeder_generator_bot.py +2 -0
  158. django_spire/contrib/seeding/intelligence/intel.py +5 -1
  159. django_spire/contrib/seeding/intelligence/prompts/factory.py +6 -1
  160. django_spire/contrib/seeding/intelligence/prompts/foreign_key_selection_prompt.py +6 -1
  161. django_spire/contrib/seeding/intelligence/prompts/generate_django_model_seeder_prompts.py +2 -0
  162. django_spire/contrib/seeding/intelligence/prompts/generic_relationship_selection_prompt.py +7 -1
  163. django_spire/contrib/seeding/intelligence/prompts/hierarchical_selection_prompt.py +6 -2
  164. django_spire/contrib/seeding/intelligence/prompts/model_field_choices_prompt.py +8 -2
  165. django_spire/contrib/seeding/intelligence/prompts/negation_prompt.py +2 -0
  166. django_spire/contrib/seeding/intelligence/prompts/objective_prompt.py +6 -1
  167. django_spire/contrib/seeding/management/commands/seeding.py +9 -3
  168. django_spire/contrib/seeding/management/example.py +2 -0
  169. django_spire/contrib/seeding/model/base.py +16 -7
  170. django_spire/contrib/seeding/model/config.py +31 -15
  171. django_spire/contrib/seeding/model/django/config.py +13 -13
  172. django_spire/contrib/seeding/model/django/seeder.py +4 -4
  173. django_spire/contrib/seeding/model/django/tests/test_seeder.py +34 -23
  174. django_spire/contrib/seeding/model/enums.py +2 -0
  175. django_spire/contrib/seeding/tests/test_config.py +71 -0
  176. django_spire/contrib/seeding/tests/test_custom.py +35 -0
  177. django_spire/contrib/seeding/tests/test_enums.py +40 -0
  178. django_spire/contrib/seeding/tests/test_intel.py +32 -0
  179. django_spire/contrib/seeding/tests/test_override.py +63 -0
  180. django_spire/contrib/service/__init__.py +2 -2
  181. django_spire/contrib/service/django_model_service.py +16 -15
  182. django_spire/contrib/service/exceptions.py +5 -3
  183. django_spire/contrib/service/tests/__init__.py +0 -0
  184. django_spire/contrib/service/tests/test_service.py +153 -0
  185. django_spire/contrib/session/apps.py +2 -0
  186. django_spire/contrib/session/controller.py +48 -42
  187. django_spire/contrib/session/templatetags/session_tags.py +11 -2
  188. django_spire/contrib/session/tests/test_session_controller.py +117 -53
  189. django_spire/contrib/tests/__init__.py +0 -0
  190. django_spire/contrib/tests/test_utils.py +37 -0
  191. django_spire/contrib/utils.py +4 -1
  192. django_spire/core/apps.py +2 -0
  193. django_spire/core/converters/tests/test_to_data.py +353 -0
  194. django_spire/core/converters/tests/test_to_enums.py +61 -41
  195. django_spire/core/converters/tests/test_to_pydantic.py +138 -109
  196. django_spire/core/converters/to_data.py +29 -10
  197. django_spire/core/converters/to_enums.py +4 -2
  198. django_spire/core/converters/to_pydantic.py +22 -22
  199. django_spire/core/decorators.py +19 -6
  200. django_spire/core/forms/widgets.py +4 -0
  201. django_spire/core/maps.py +3 -1
  202. django_spire/core/middleware/maintenance.py +3 -3
  203. django_spire/core/middleware.py +8 -6
  204. django_spire/core/redirect/__init__.py +5 -0
  205. django_spire/core/redirect/generic_redirect.py +1 -2
  206. django_spire/core/redirect/tests/__init__.py +0 -0
  207. django_spire/core/redirect/tests/test_generic_redirect.py +34 -0
  208. django_spire/core/{tests/tests_redirect.py → redirect/tests/test_safe_redirect.py} +55 -81
  209. django_spire/core/shortcuts.py +3 -3
  210. django_spire/core/static/django_spire/css/app-layout.css +1 -1
  211. django_spire/core/static/django_spire/css/app-navigation.css +3 -3
  212. django_spire/core/static/django_spire/css/bootstrap-override.css +4 -0
  213. django_spire/core/tag/admin.py +12 -0
  214. django_spire/core/tag/intelligence/tag_set_bot.py +2 -0
  215. django_spire/core/tag/mixins.py +2 -0
  216. django_spire/core/tag/models.py +2 -0
  217. django_spire/core/tag/querysets.py +2 -0
  218. django_spire/core/tag/service/tag_service.py +6 -3
  219. django_spire/core/tag/tests/test_intelligence.py +9 -9
  220. django_spire/core/tag/tests/test_tags.py +44 -54
  221. django_spire/core/tag/tests/test_tools.py +191 -0
  222. django_spire/core/tag/tools.py +3 -0
  223. django_spire/core/templates/django_spire/card/card.html +5 -2
  224. django_spire/core/templates/django_spire/card/title_card.html +9 -4
  225. django_spire/core/templates/django_spire/infinite_scroll/base.html +1 -0
  226. django_spire/core/templates/django_spire/navigation/side_navigation.html +19 -24
  227. django_spire/core/templates/django_spire/page/full_page.html +46 -16
  228. django_spire/core/templates/django_spire/table/base.html +4 -2
  229. django_spire/core/templatetags/json.py +6 -2
  230. django_spire/core/templatetags/message.py +13 -8
  231. django_spire/core/templatetags/string_formating.py +8 -5
  232. django_spire/core/templatetags/tests/__init__.py +0 -0
  233. django_spire/core/templatetags/tests/test_templatetags.py +427 -0
  234. django_spire/core/templatetags/variable_types.py +17 -9
  235. django_spire/core/tests/test_cases.py +1 -1
  236. django_spire/core/tests/test_conf.py +43 -0
  237. django_spire/core/tests/test_consts.py +28 -0
  238. django_spire/core/tests/test_context_processors.py +93 -0
  239. django_spire/core/tests/test_decorators.py +95 -0
  240. django_spire/core/tests/test_django_spire_utils.py +56 -0
  241. django_spire/core/tests/test_exceptions.py +37 -0
  242. django_spire/core/tests/test_models.py +54 -0
  243. django_spire/core/tests/test_settings.py +45 -0
  244. django_spire/core/tests/test_shortcuts.py +74 -0
  245. django_spire/core/tests/test_urls.py +16 -0
  246. django_spire/core/tests/test_utils.py +58 -0
  247. django_spire/core/urls.py +4 -1
  248. django_spire/core/utils.py +12 -8
  249. django_spire/exceptions.py +16 -1
  250. django_spire/file/admin.py +4 -2
  251. django_spire/file/apps.py +8 -10
  252. django_spire/file/fields.py +7 -7
  253. django_spire/file/forms.py +1 -1
  254. django_spire/file/interfaces.py +15 -15
  255. django_spire/file/mixins.py +1 -4
  256. django_spire/file/models.py +3 -5
  257. django_spire/file/tests/factories.py +59 -0
  258. django_spire/file/tests/test_admin.py +69 -0
  259. django_spire/file/tests/test_apps.py +24 -0
  260. django_spire/file/tests/test_fields.py +114 -0
  261. django_spire/file/tests/test_forms.py +20 -0
  262. django_spire/file/tests/test_interfaces.py +183 -0
  263. django_spire/file/tests/test_models.py +82 -0
  264. django_spire/file/tests/test_querysets.py +102 -0
  265. django_spire/file/tests/test_utils.py +32 -0
  266. django_spire/file/tests/test_views.py +145 -0
  267. django_spire/file/tests/test_widgets.py +82 -0
  268. django_spire/file/tools.py +8 -2
  269. django_spire/file/views.py +7 -3
  270. django_spire/file/widgets.py +12 -12
  271. django_spire/help_desk/admin.py +15 -0
  272. django_spire/help_desk/apps.py +2 -0
  273. django_spire/help_desk/auth/controller.py +2 -0
  274. django_spire/help_desk/choices.py +2 -0
  275. django_spire/help_desk/enums.py +2 -0
  276. django_spire/help_desk/exceptions.py +31 -3
  277. django_spire/help_desk/forms.py +2 -0
  278. django_spire/help_desk/models.py +2 -0
  279. django_spire/help_desk/querysets.py +4 -1
  280. django_spire/help_desk/services/notification_service.py +26 -27
  281. django_spire/help_desk/services/service.py +2 -3
  282. django_spire/help_desk/tests/factories.py +8 -3
  283. django_spire/help_desk/tests/test_admin.py +41 -0
  284. django_spire/help_desk/tests/test_apps.py +41 -0
  285. django_spire/help_desk/tests/test_choices.py +50 -0
  286. django_spire/help_desk/tests/test_controller.py +87 -0
  287. django_spire/help_desk/tests/test_enums.py +18 -0
  288. django_spire/help_desk/tests/test_exceptions.py +37 -0
  289. django_spire/help_desk/tests/test_forms.py +89 -0
  290. django_spire/help_desk/tests/test_models.py +59 -0
  291. django_spire/help_desk/tests/test_querysets.py +38 -0
  292. django_spire/help_desk/tests/test_services/test_notification_service.py +15 -8
  293. django_spire/help_desk/tests/test_services/test_service.py +92 -0
  294. django_spire/help_desk/tests/test_urls/test_form_urls.py +6 -6
  295. django_spire/help_desk/tests/test_urls/test_page_urls.py +8 -9
  296. django_spire/help_desk/tests/test_views/test_form_views.py +46 -19
  297. django_spire/help_desk/tests/test_views/test_page_views.py +32 -9
  298. django_spire/help_desk/urls/__init__.py +4 -1
  299. django_spire/help_desk/urls/form_urls.py +3 -0
  300. django_spire/help_desk/urls/page_urls.py +3 -0
  301. django_spire/help_desk/views/form_views.py +13 -5
  302. django_spire/help_desk/views/page_views.py +11 -3
  303. django_spire/history/activity/admin.py +2 -0
  304. django_spire/history/activity/apps.py +3 -1
  305. django_spire/history/activity/mixins.py +13 -7
  306. django_spire/history/activity/models.py +6 -5
  307. django_spire/history/activity/querysets.py +2 -0
  308. django_spire/history/activity/tests/__init__.py +0 -0
  309. django_spire/history/activity/tests/test_activity.py +176 -0
  310. django_spire/history/admin.py +9 -2
  311. django_spire/history/choices.py +3 -0
  312. django_spire/history/models.py +5 -5
  313. django_spire/history/tests/test_admin.py +93 -0
  314. django_spire/history/tests/test_history.py +101 -0
  315. django_spire/history/tests/test_mixins.py +84 -0
  316. django_spire/history/viewed/admin.py +3 -1
  317. django_spire/history/viewed/apps.py +3 -1
  318. django_spire/history/viewed/models.py +2 -0
  319. django_spire/history/viewed/tests/__init__.py +0 -0
  320. django_spire/history/viewed/tests/test_viewed.py +46 -0
  321. django_spire/knowledge/auth/tests/__init__.py +0 -0
  322. django_spire/knowledge/auth/tests/test_controller.py +116 -0
  323. django_spire/knowledge/collection/admin.py +5 -1
  324. django_spire/knowledge/collection/models.py +3 -1
  325. django_spire/knowledge/collection/seeding/seed.py +1 -0
  326. django_spire/knowledge/collection/services/factory_service.py +10 -11
  327. django_spire/knowledge/collection/services/ordering_service.py +1 -2
  328. django_spire/knowledge/collection/services/service.py +5 -10
  329. django_spire/knowledge/collection/services/tag_service.py +5 -2
  330. django_spire/knowledge/collection/tests/factories.py +28 -1
  331. django_spire/knowledge/collection/tests/test_models.py +48 -0
  332. django_spire/knowledge/collection/tests/test_querysets.py +93 -0
  333. django_spire/knowledge/collection/tests/test_services/test_factory_service.py +100 -0
  334. django_spire/knowledge/collection/tests/test_services/test_services.py +160 -0
  335. django_spire/knowledge/collection/tests/test_urls/test_form_urls.py +21 -3
  336. django_spire/knowledge/collection/tests/test_urls/test_json_urls.py +39 -1
  337. django_spire/knowledge/collection/tests/test_urls/test_page_urls.py +12 -4
  338. django_spire/knowledge/collection/urls/__init__.py +3 -0
  339. django_spire/knowledge/collection/urls/form_urls.py +2 -0
  340. django_spire/knowledge/collection/urls/json_urls.py +2 -0
  341. django_spire/knowledge/collection/urls/page_urls.py +2 -0
  342. django_spire/knowledge/collection/views/form_views.py +4 -4
  343. django_spire/knowledge/collection/views/json_views.py +5 -1
  344. django_spire/knowledge/collection/views/page_views.py +5 -2
  345. django_spire/knowledge/entry/admin.py +7 -1
  346. django_spire/knowledge/entry/forms.py +2 -0
  347. django_spire/knowledge/entry/models.py +2 -0
  348. django_spire/knowledge/entry/seeding/seed.py +3 -0
  349. django_spire/knowledge/entry/services/automation_service.py +5 -4
  350. django_spire/knowledge/entry/services/factory_service.py +7 -5
  351. django_spire/knowledge/entry/services/service.py +4 -7
  352. django_spire/knowledge/entry/services/tag_service.py +0 -1
  353. django_spire/knowledge/entry/services/tool_service.py +1 -0
  354. django_spire/knowledge/entry/services/transformation_services.py +1 -5
  355. django_spire/knowledge/entry/tests/factories.py +1 -2
  356. django_spire/knowledge/entry/tests/test_factory_service.py +20 -0
  357. django_spire/knowledge/entry/tests/test_models.py +41 -0
  358. django_spire/knowledge/entry/tests/test_querysets.py +71 -0
  359. django_spire/knowledge/entry/tests/test_services.py +94 -0
  360. django_spire/knowledge/entry/tests/test_urls/test_form_urls.py +9 -14
  361. django_spire/knowledge/entry/tests/test_urls/test_json_urls.py +48 -5
  362. django_spire/knowledge/entry/tests/test_urls/test_page_urls.py +6 -8
  363. django_spire/knowledge/entry/tests/test_urls/test_template_urls.py +40 -0
  364. django_spire/knowledge/entry/urls/form_urls.py +2 -0
  365. django_spire/knowledge/entry/urls/json_urls.py +2 -0
  366. django_spire/knowledge/entry/urls/page_urls.py +2 -0
  367. django_spire/knowledge/entry/urls/template_urls.py +2 -0
  368. django_spire/knowledge/entry/version/block/choices.py +2 -0
  369. django_spire/knowledge/entry/version/block/data/data.py +1 -0
  370. django_spire/knowledge/entry/version/block/data/list/data.py +8 -13
  371. django_spire/knowledge/entry/version/block/data/list/maps.py +3 -0
  372. django_spire/knowledge/entry/version/block/data/list/meta.py +1 -2
  373. django_spire/knowledge/entry/version/block/data/list/tests/__init__.py +0 -0
  374. django_spire/knowledge/entry/version/block/data/list/tests/test_maps.py +32 -0
  375. django_spire/knowledge/entry/version/block/data/list/tests/test_meta.py +58 -0
  376. django_spire/knowledge/entry/version/block/data/maps.py +3 -6
  377. django_spire/knowledge/entry/version/block/models.py +7 -5
  378. django_spire/knowledge/entry/version/block/seeding/constants.py +5 -4
  379. django_spire/knowledge/entry/version/block/services/service.py +2 -3
  380. django_spire/knowledge/entry/version/block/tests/factories.py +4 -10
  381. django_spire/knowledge/entry/version/block/tests/test_choices.py +56 -0
  382. django_spire/knowledge/entry/version/block/tests/test_data.py +90 -0
  383. django_spire/knowledge/entry/version/block/tests/test_maps.py +37 -0
  384. django_spire/knowledge/entry/version/block/tests/test_models.py +55 -0
  385. django_spire/knowledge/entry/version/block/tests/test_querysets.py +35 -0
  386. django_spire/knowledge/entry/version/block/tests/test_services.py +65 -0
  387. django_spire/knowledge/entry/version/choices.py +2 -0
  388. django_spire/knowledge/entry/version/converters/converter.py +1 -1
  389. django_spire/knowledge/entry/version/converters/docx_converter.py +4 -7
  390. django_spire/knowledge/entry/version/converters/markdown_converter.py +20 -20
  391. django_spire/knowledge/entry/version/maps.py +4 -5
  392. django_spire/knowledge/entry/version/querysets.py +1 -1
  393. django_spire/knowledge/entry/version/seeding/seeder.py +1 -2
  394. django_spire/knowledge/entry/version/services/processor_service.py +5 -4
  395. django_spire/knowledge/entry/version/services/service.py +1 -2
  396. django_spire/knowledge/entry/version/tests/factories.py +2 -2
  397. django_spire/knowledge/entry/version/tests/test_choices.py +18 -0
  398. django_spire/knowledge/entry/version/tests/test_converters/test_docx_converter.py +56 -8
  399. django_spire/knowledge/entry/version/tests/test_converters/test_markdown_converter.py +78 -0
  400. django_spire/knowledge/entry/version/tests/test_maps.py +58 -0
  401. django_spire/knowledge/entry/version/tests/test_models.py +23 -0
  402. django_spire/knowledge/entry/version/tests/test_querysets.py +26 -0
  403. django_spire/knowledge/entry/version/tests/test_services.py +62 -0
  404. django_spire/knowledge/entry/version/tests/test_urls/test_json_urls.py +27 -8
  405. django_spire/knowledge/entry/version/tests/test_urls/test_page_urls.py +15 -8
  406. django_spire/knowledge/entry/version/tests/test_urls/test_redirect_urls.py +38 -0
  407. django_spire/knowledge/entry/version/urls/__init__.py +3 -0
  408. django_spire/knowledge/entry/version/urls/json_urls.py +2 -1
  409. django_spire/knowledge/entry/version/urls/page_urls.py +2 -0
  410. django_spire/knowledge/entry/version/urls/redirect_urls.py +2 -0
  411. django_spire/knowledge/entry/version/views/json_views.py +5 -1
  412. django_spire/knowledge/entry/version/views/page_views.py +10 -3
  413. django_spire/knowledge/entry/version/views/redirect_views.py +5 -1
  414. django_spire/knowledge/entry/views/form_views.py +16 -8
  415. django_spire/knowledge/entry/views/json_views.py +3 -1
  416. django_spire/knowledge/entry/views/page_views.py +8 -2
  417. django_spire/knowledge/entry/views/template_views.py +7 -1
  418. django_spire/knowledge/exceptions.py +2 -1
  419. django_spire/knowledge/intelligence/intel/answer_intel.py +2 -1
  420. django_spire/knowledge/intelligence/intel/entry_intel.py +0 -1
  421. django_spire/knowledge/intelligence/workflows/knowledge_workflow.py +4 -5
  422. django_spire/knowledge/models.py +1 -2
  423. django_spire/knowledge/tests/__init__.py +0 -0
  424. django_spire/knowledge/tests/test_templatetags.py +40 -0
  425. django_spire/knowledge/tests/test_urls/__init__.py +0 -0
  426. django_spire/knowledge/tests/test_urls/test_page_urls.py +24 -0
  427. django_spire/knowledge/urls/__init__.py +2 -0
  428. django_spire/knowledge/urls/page_urls.py +2 -0
  429. django_spire/knowledge/views/page_views.py +8 -3
  430. django_spire/notification/admin.py +3 -1
  431. django_spire/notification/app/admin.py +2 -0
  432. django_spire/notification/app/apps.py +3 -1
  433. django_spire/notification/app/exceptions.py +9 -2
  434. django_spire/notification/app/models.py +8 -4
  435. django_spire/notification/app/processor.py +22 -26
  436. django_spire/notification/app/querysets.py +2 -0
  437. django_spire/notification/app/tests/__init__.py +0 -0
  438. django_spire/notification/app/tests/factories.py +34 -0
  439. django_spire/notification/app/tests/test_apps.py +24 -0
  440. django_spire/notification/app/tests/test_models.py +72 -0
  441. django_spire/notification/app/tests/test_processor.py +111 -0
  442. django_spire/notification/app/tests/test_querysets.py +90 -0
  443. django_spire/notification/app/tests/test_views/__init__.py +0 -0
  444. django_spire/notification/app/tests/test_views/test_json_views.py +48 -0
  445. django_spire/notification/app/tests/test_views/test_page_views.py +19 -0
  446. django_spire/notification/app/urls/__init__.py +3 -1
  447. django_spire/notification/app/urls/json_urls.py +6 -4
  448. django_spire/notification/app/urls/page_urls.py +4 -3
  449. django_spire/notification/app/urls/template_urls.py +4 -2
  450. django_spire/notification/apps.py +4 -1
  451. django_spire/notification/email/admin.py +5 -1
  452. django_spire/notification/email/apps.py +3 -1
  453. django_spire/notification/email/exceptions.py +4 -2
  454. django_spire/notification/email/helper.py +5 -3
  455. django_spire/notification/email/models.py +4 -0
  456. django_spire/notification/email/processor.py +19 -15
  457. django_spire/notification/email/querysets.py +3 -0
  458. django_spire/notification/email/tests/__init__.py +0 -0
  459. django_spire/notification/email/tests/factories.py +35 -0
  460. django_spire/notification/email/tests/test_apps.py +24 -0
  461. django_spire/notification/email/tests/test_models.py +52 -0
  462. django_spire/notification/email/tests/test_processor.py +92 -0
  463. django_spire/notification/email/tests/test_querysets.py +43 -0
  464. django_spire/notification/exceptions.py +17 -2
  465. django_spire/notification/managers.py +7 -1
  466. django_spire/notification/maps.py +4 -1
  467. django_spire/notification/mixins.py +2 -0
  468. django_spire/notification/models.py +3 -1
  469. django_spire/notification/processors/notification.py +12 -5
  470. django_spire/notification/processors/processor.py +2 -0
  471. django_spire/notification/processors/tests/__init__.py +0 -0
  472. django_spire/notification/processors/tests/test_notification.py +106 -0
  473. django_spire/notification/push/admin.py +10 -1
  474. django_spire/notification/push/apps.py +3 -1
  475. django_spire/notification/push/models.py +2 -3
  476. django_spire/notification/push/tests/__init__.py +0 -0
  477. django_spire/notification/push/tests/test_apps.py +24 -0
  478. django_spire/notification/push/tests/test_models.py +28 -0
  479. django_spire/notification/querysets.py +7 -1
  480. django_spire/notification/sms/admin.py +2 -0
  481. django_spire/notification/sms/apps.py +4 -1
  482. django_spire/notification/sms/automations.py +2 -0
  483. django_spire/notification/sms/choices.py +2 -0
  484. django_spire/notification/sms/exceptions.py +19 -5
  485. django_spire/notification/sms/helper.py +33 -23
  486. django_spire/notification/sms/models.py +5 -1
  487. django_spire/notification/sms/processor.py +20 -20
  488. django_spire/notification/sms/querysets.py +2 -0
  489. django_spire/notification/sms/tests/factories.py +33 -0
  490. django_spire/notification/sms/tests/test_apps.py +24 -0
  491. django_spire/notification/sms/tests/test_automation.py +38 -0
  492. django_spire/notification/sms/tests/test_choices.py +15 -0
  493. django_spire/notification/sms/tests/test_consts.py +17 -0
  494. django_spire/notification/sms/tests/test_exceptions.py +27 -0
  495. django_spire/notification/sms/tests/test_helper.py +50 -0
  496. django_spire/notification/sms/tests/test_models.py +81 -0
  497. django_spire/notification/sms/tests/test_processor.py +107 -0
  498. django_spire/notification/sms/tests/test_tools.py +25 -11
  499. django_spire/notification/sms/tools.py +16 -5
  500. django_spire/notification/sms/urls/__init__.py +3 -1
  501. django_spire/notification/sms/urls/media_urls.py +2 -0
  502. django_spire/notification/sms/views/media_views.py +14 -4
  503. django_spire/notification/tests/__init__.py +0 -0
  504. django_spire/notification/tests/factories.py +26 -0
  505. django_spire/notification/tests/test_admin.py +55 -0
  506. django_spire/notification/tests/test_apps.py +30 -0
  507. django_spire/notification/tests/test_automation.py +18 -0
  508. django_spire/notification/tests/test_choices.py +59 -0
  509. django_spire/notification/tests/test_exceptions.py +58 -0
  510. django_spire/notification/tests/test_managers.py +100 -0
  511. django_spire/notification/tests/test_maps.py +31 -0
  512. django_spire/notification/tests/test_models.py +76 -0
  513. django_spire/notification/tests/test_querysets.py +184 -0
  514. django_spire/notification/tests/test_utils.py +23 -0
  515. django_spire/notification/urls.py +3 -1
  516. django_spire/notification/utils.py +3 -1
  517. django_spire/settings.py +3 -0
  518. django_spire/theme/tests/test_context_processor.py +15 -13
  519. django_spire/theme/tests/test_enums.py +2 -2
  520. django_spire/theme/tests/test_filesystem.py +2 -5
  521. django_spire/theme/tests/test_integration.py +12 -12
  522. django_spire/theme/tests/test_model.py +40 -38
  523. django_spire/theme/tests/test_views/test_json_views.py +33 -33
  524. django_spire/theme/urls/json_urls.py +3 -0
  525. django_spire/theme/urls/page_urls.py +3 -0
  526. django_spire/urls.py +19 -15
  527. django_spire/utils.py +13 -4
  528. {django_spire-0.23.6.dist-info → django_spire-0.23.8.dist-info}/METADATA +1 -1
  529. {django_spire-0.23.6.dist-info → django_spire-0.23.8.dist-info}/RECORD +532 -361
  530. django_spire/contrib/options/tests/test_unit.py +0 -148
  531. django_spire/contrib/progress/views.py +0 -64
  532. django_spire/contrib/seeding/tests/test_seeding.py +0 -25
  533. django_spire/core/tests/test_templatetags.py +0 -117
  534. django_spire/core/tests/tests_shortcuts.py +0 -73
  535. django_spire/history/activity/tests.py +0 -3
  536. django_spire/history/activity/views.py +0 -3
  537. django_spire/knowledge/collection/tests/test_services/test_transformation_service.py +0 -71
  538. django_spire/notification/app/tests.py +0 -3
  539. {django_spire-0.23.6.dist-info → django_spire-0.23.8.dist-info}/WHEEL +0 -0
  540. {django_spire-0.23.6.dist-info → django_spire-0.23.8.dist-info}/licenses/LICENSE.md +0 -0
  541. {django_spire-0.23.6.dist-info → django_spire-0.23.8.dist-info}/top_level.txt +0 -0
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from typing import Sequence, Self
2
4
 
3
5
  from django.db import models
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from django.db.models import QuerySet
2
4
  from django.utils.text import slugify
3
5
 
@@ -1,7 +1,7 @@
1
- from abc import abstractmethod, ABC
2
- from typing import Generic
1
+ from __future__ import annotations
3
2
 
4
- from django.db.models import QuerySet
3
+ from abc import abstractmethod, ABC
4
+ from typing import Generic, TYPE_CHECKING
5
5
 
6
6
  from django_spire.contrib.constructor.django_model_constructor import TypeDjangoModel
7
7
  from django_spire.contrib.service import BaseDjangoModelService
@@ -9,6 +9,9 @@ from django_spire.core.tag import tools
9
9
  from django_spire.core.tag.models import Tag
10
10
  from django_spire.core.tag.tools import get_score_percentage_from_tag_set_weighted
11
11
 
12
+ if TYPE_CHECKING:
13
+ from django.db.models import QuerySet
14
+
12
15
 
13
16
  class BaseTagService(
14
17
  BaseDjangoModelService[TypeDjangoModel],
@@ -1,7 +1,10 @@
1
+ from __future__ import annotations
2
+
1
3
  from unittest import TestCase
2
4
 
3
5
  from django_spire.core.tag.intelligence.tag_set_bot import TagSetBot
4
6
 
7
+
5
8
  TEST_INPUT = """
6
9
  I love reading books about science fiction, fantasy, and adventure, especially ones
7
10
  that feature artificial intelligence, machine learning, and natural language processing.
@@ -13,16 +16,13 @@ TEST_INPUT = """
13
16
 
14
17
 
15
18
  class TestTagIntelligence(TestCase):
16
- def setUp(self):
17
- pass
18
-
19
19
  def test_tag_set_bot(self):
20
20
  tag_set = TagSetBot().process(TEST_INPUT)
21
21
 
22
- self.assertIn('science', tag_set)
23
- self.assertIn('artificial', tag_set)
24
- self.assertIn('fantasy', tag_set)
22
+ assert 'science' in tag_set
23
+ assert 'artificial' in tag_set
24
+ assert 'fantasy' in tag_set
25
25
 
26
- self.assertNotIn('camping', tag_set)
27
- self.assertNotIn('art', tag_set)
28
- self.assertNotIn('hate', tag_set)
26
+ assert 'camping' not in tag_set
27
+ assert 'art' not in tag_set
28
+ assert 'hate' not in tag_set
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from django.test import TestCase
2
4
 
3
5
  from django_spire.core.tag.models import Tag
@@ -5,9 +7,10 @@ from django_spire.knowledge.collection.models import Collection
5
7
 
6
8
 
7
9
  class TestTags(TestCase):
8
- def setUp(self):
10
+ def setUp(self) -> None:
9
11
  self.collection = Collection.objects.create(
10
- name='Test Collection', description='Test Collection Description'
12
+ name='Test Collection',
13
+ description='Test Collection Description'
11
14
  )
12
15
 
13
16
  self.tag_set_a = {'apple', 'banana', 'orange'}
@@ -26,77 +29,64 @@ class TestTags(TestCase):
26
29
 
27
30
  self.collection.services.tag.add_tags_from_tag_set(self.dirty_tag_set)
28
31
 
29
- def test_tag_removal(self):
30
- self.assertIn('apple', self.collection.tag_set)
31
-
32
- self.collection.services.tag.remove_tags_by_tag_set(self.mixed_tag_set)
33
-
34
- self.assertNotIn('lemon', self.collection.tag_set)
32
+ def test_tag_add_and_set(self) -> None:
33
+ self.collection.services.tag.set_tags_from_tag_set(self.mixed_tag_set)
35
34
 
36
- self.assertIn('kiwi', self.collection.tag_set)
35
+ assert 'lemon' in self.collection.tag_set
36
+ assert 'banana' not in self.collection.tag_set
37
37
 
38
- def test_tag_add_and_set(self):
39
- self.collection.services.tag.set_tags_from_tag_set(self.mixed_tag_set)
38
+ def test_tag_bool_methods(self) -> None:
39
+ self.collection.services.tag.remove_tags_by_tag_set(self.tag_set_b)
40
40
 
41
- self.assertIn('lemon', self.collection.tag_set)
41
+ assert self.collection.services.tag.has_tags_in_tag_set(self.tag_set_c) is True
42
+ assert self.collection.services.tag.has_tags_in_tag_set(self.mixed_tag_set) is False
42
43
 
43
- self.assertNotIn('banana', self.collection.tag_set)
44
+ def test_tag_count(self) -> None:
45
+ assert Tag.objects.count() == 12
46
+ assert self.collection.tags.count() == 12
44
47
 
45
- def test_tag_count(self):
46
- self.assertEqual(Tag.objects.count(), 12)
47
- self.assertEqual(self.collection.tags.count(), 12)
48
+ def test_tag_matching_methods(self) -> None:
49
+ assert len(self.collection.services.tag.get_matching_tags_from_tag_set(self.tag_set_a)) == 3
48
50
 
49
- def test_tag_removal(self):
50
51
  self.collection.services.tag.remove_tags_by_tag_set(self.tag_set_a)
51
52
 
52
- self.assertEqual(self.collection.tags.count(), 9)
53
+ assert len(self.collection.services.tag.get_matching_tags_from_tag_set(self.tag_set_a)) == 0
54
+ assert len(self.collection.services.tag.get_matching_tags_from_tag_set(self.tag_set_b)) == 3
55
+ assert len(self.collection.services.tag.get_matching_tags_from_tag_set(self.mixed_tag_set)) == 2
53
56
 
54
- def test_tag_bool_methods(self):
55
57
  self.collection.services.tag.remove_tags_by_tag_set(self.tag_set_b)
56
58
 
57
- self.assertTrue(self.collection.services.tag.has_tags_in_tag_set(self.tag_set_c))
58
- self.assertFalse(self.collection.services.tag.has_tags_in_tag_set(self.mixed_tag_set))
59
-
60
- def test_tag_matching_methods(self):
61
- self.assertEqual(len(self.collection.services.tag.get_matching_tags_from_tag_set(self.tag_set_a)), 3)
62
-
63
- self.collection.services.tag.remove_tags_by_tag_set(self.tag_set_a)
59
+ assert len(self.collection.services.tag.get_matching_tags_from_tag_set(self.tag_set_b)) == 0
60
+ assert len(self.collection.services.tag.get_matching_tags_from_tag_set(self.mixed_tag_set)) == 1
64
61
 
65
- self.assertEqual(len(self.collection.services.tag.get_matching_tags_from_tag_set(self.tag_set_a)), 0)
66
- self.assertEqual(len(self.collection.services.tag.get_matching_tags_from_tag_set(self.tag_set_b)), 3)
67
- self.assertEqual(len(self.collection.services.tag.get_matching_tags_from_tag_set(self.mixed_tag_set)), 2)
62
+ def test_tag_metric_methods(self) -> None:
63
+ assert abs(
64
+ self.collection.services.tag.get_matching_percentage_of_tag_set(self.tag_set_a) - 1.00
65
+ ) < 0.01
68
66
 
69
- self.collection.services.tag.remove_tags_by_tag_set(self.tag_set_b)
67
+ assert abs(
68
+ self.collection.services.tag.get_matching_percentage_of_model_tags_from_tag_set(self.tag_set_b) - 0.25
69
+ ) < 0.01
70
70
 
71
- self.assertEqual(len(self.collection.services.tag.get_matching_tags_from_tag_set(self.tag_set_b)), 0)
71
+ self.collection.services.tag.remove_tags_by_tag_set(self.mixed_tag_set)
72
72
 
73
- self.assertEqual(len(self.collection.services.tag.get_matching_tags_from_tag_set(self.mixed_tag_set)), 1)
73
+ assert abs(
74
+ self.collection.services.tag.get_matching_percentage_of_tag_set(self.tag_set_a) - 0.666
75
+ ) < 0.01
74
76
 
75
- def test_tag_metric_methods(self):
76
- self.assertAlmostEqual(
77
- self.collection.services.tag.get_matching_percentage_of_tag_set(self.tag_set_a),
78
- 1.00,
79
- 2
80
- )
77
+ assert abs(
78
+ self.collection.services.tag.get_matching_percentage_of_model_tags_from_tag_set(self.tag_set_b) - 0.222
79
+ ) < 0.01
81
80
 
82
- self.assertAlmostEqual(
83
- self.collection.services.tag.get_matching_percentage_of_model_tags_from_tag_set(self.tag_set_b),
84
- 0.25,
85
- 2
86
- )
81
+ def test_tag_removal(self) -> None:
82
+ assert 'apple' in self.collection.tag_set
87
83
 
88
84
  self.collection.services.tag.remove_tags_by_tag_set(self.mixed_tag_set)
89
85
 
90
- self.assertAlmostEqual(
91
- self.collection.services.tag.get_matching_percentage_of_tag_set(self.tag_set_a),
92
- 0.666,
93
- 2
94
- )
95
-
86
+ assert 'lemon' not in self.collection.tag_set
87
+ assert 'kiwi' in self.collection.tag_set
96
88
 
97
- self.assertAlmostEqual(
98
- self.collection.services.tag.get_matching_percentage_of_model_tags_from_tag_set(self.tag_set_b),
99
- 0.222,
100
- 2
101
- )
89
+ def test_tag_removal_count(self) -> None:
90
+ self.collection.services.tag.remove_tags_by_tag_set(self.tag_set_a)
102
91
 
92
+ assert self.collection.tags.count() == 9
@@ -0,0 +1,191 @@
1
+ from __future__ import annotations
2
+
3
+ from unittest import TestCase
4
+
5
+ from django_spire.core.tag.tools import (
6
+ get_matching_a_percentage_from_tag_sets,
7
+ get_matching_b_percentage_from_tag_sets,
8
+ get_matching_count_from_tag_sets,
9
+ get_matching_tag_set_from_tag_sets,
10
+ get_score_percentage_from_tag_set_weighted,
11
+ simplify_and_weight_tag_set_to_dict,
12
+ simplify_tag_set,
13
+ simplify_tag_set_to_list,
14
+ )
15
+
16
+
17
+ class TestGetMatchingTagSetFromTagSets(TestCase):
18
+ def test_full_intersection(self) -> None:
19
+ tag_set_a = {'apple', 'banana', 'orange'}
20
+ tag_set_b = {'apple', 'banana', 'orange'}
21
+
22
+ result = get_matching_tag_set_from_tag_sets(tag_set_a, tag_set_b)
23
+
24
+ assert result == {'apple', 'banana', 'orange'}
25
+
26
+ def test_no_intersection(self) -> None:
27
+ tag_set_a = {'apple', 'banana', 'orange'}
28
+ tag_set_b = {'grape', 'kiwi', 'pear'}
29
+
30
+ result = get_matching_tag_set_from_tag_sets(tag_set_a, tag_set_b)
31
+
32
+ assert result == set()
33
+
34
+ def test_partial_intersection(self) -> None:
35
+ tag_set_a = {'apple', 'banana', 'orange'}
36
+ tag_set_b = {'apple', 'grape', 'kiwi'}
37
+
38
+ result = get_matching_tag_set_from_tag_sets(tag_set_a, tag_set_b)
39
+
40
+ assert result == {'apple'}
41
+
42
+
43
+ class TestGetMatchingCountFromTagSets(TestCase):
44
+ def test_full_match(self) -> None:
45
+ tag_set_a = {'apple', 'banana'}
46
+ tag_set_b = {'apple', 'banana'}
47
+
48
+ assert get_matching_count_from_tag_sets(tag_set_a, tag_set_b) == 2
49
+
50
+ def test_no_match(self) -> None:
51
+ tag_set_a = {'apple', 'banana'}
52
+ tag_set_b = {'grape', 'kiwi'}
53
+
54
+ assert get_matching_count_from_tag_sets(tag_set_a, tag_set_b) == 0
55
+
56
+ def test_partial_match(self) -> None:
57
+ tag_set_a = {'apple', 'banana', 'orange'}
58
+ tag_set_b = {'apple', 'grape'}
59
+
60
+ assert get_matching_count_from_tag_sets(tag_set_a, tag_set_b) == 1
61
+
62
+
63
+ class TestGetMatchingAPercentageFromTagSets(TestCase):
64
+ def test_empty_tag_set_a(self) -> None:
65
+ tag_set_a = set()
66
+ tag_set_b = {'apple', 'banana'}
67
+
68
+ assert get_matching_a_percentage_from_tag_sets(tag_set_a, tag_set_b) == 0.0
69
+
70
+ def test_full_match(self) -> None:
71
+ tag_set_a = {'apple', 'banana'}
72
+ tag_set_b = {'apple', 'banana', 'orange'}
73
+
74
+ assert get_matching_a_percentage_from_tag_sets(tag_set_a, tag_set_b) == 1.0
75
+
76
+ def test_partial_match(self) -> None:
77
+ tag_set_a = {'apple', 'banana', 'orange'}
78
+ tag_set_b = {'apple'}
79
+
80
+ assert abs(get_matching_a_percentage_from_tag_sets(tag_set_a, tag_set_b) - 0.333) < 0.01
81
+
82
+
83
+ class TestGetMatchingBPercentageFromTagSets(TestCase):
84
+ def test_empty_tag_set_b(self) -> None:
85
+ tag_set_a = {'apple', 'banana'}
86
+ tag_set_b = set()
87
+
88
+ assert get_matching_b_percentage_from_tag_sets(tag_set_a, tag_set_b) == 0.0
89
+
90
+ def test_full_match(self) -> None:
91
+ tag_set_a = {'apple', 'banana', 'orange'}
92
+ tag_set_b = {'apple', 'banana'}
93
+
94
+ assert get_matching_b_percentage_from_tag_sets(tag_set_a, tag_set_b) == 1.0
95
+
96
+ def test_partial_match(self) -> None:
97
+ tag_set_a = {'apple'}
98
+ tag_set_b = {'apple', 'banana', 'orange'}
99
+
100
+ assert abs(get_matching_b_percentage_from_tag_sets(tag_set_a, tag_set_b) - 0.333) < 0.01
101
+
102
+
103
+ class TestSimplifyTagSet(TestCase):
104
+ def test_no_hyphens(self) -> None:
105
+ tag_set = {'apple', 'banana', 'orange'}
106
+
107
+ result = simplify_tag_set(tag_set)
108
+
109
+ assert result == {'apple', 'banana', 'orange'}
110
+
111
+ def test_with_hyphens(self) -> None:
112
+ tag_set = {'machine-learning', 'data-science'}
113
+
114
+ result = simplify_tag_set(tag_set)
115
+
116
+ assert 'machine' in result
117
+ assert 'learning' in result
118
+ assert 'data' in result
119
+ assert 'science' in result
120
+
121
+
122
+ class TestSimplifyTagSetToList(TestCase):
123
+ def test_mixed_tags(self) -> None:
124
+ tag_set = {'apple', 'machine-learning'}
125
+
126
+ result = simplify_tag_set_to_list(tag_set)
127
+
128
+ assert 'apple' in result
129
+ assert 'machine' in result
130
+ assert 'learning' in result
131
+
132
+ def test_with_hyphens(self) -> None:
133
+ tag_set = {'machine-learning'}
134
+
135
+ result = simplify_tag_set_to_list(tag_set)
136
+
137
+ assert result == ['machine', 'learning']
138
+
139
+
140
+ class TestSimplifyAndWeightTagSetToDict(TestCase):
141
+ def test_duplicate_words(self) -> None:
142
+ tag_set = {'machine-learning', 'deep-learning'}
143
+
144
+ result = simplify_and_weight_tag_set_to_dict(tag_set)
145
+
146
+ assert result['learning'] == 2
147
+ assert result['machine'] == 1
148
+ assert result['deep'] == 1
149
+
150
+ def test_simple_tags(self) -> None:
151
+ tag_set = {'apple', 'banana'}
152
+
153
+ result = simplify_and_weight_tag_set_to_dict(tag_set)
154
+
155
+ assert result['apple'] == 1
156
+ assert result['banana'] == 1
157
+
158
+ def test_sorted_by_weight(self) -> None:
159
+ tag_set = {'machine-learning', 'deep-learning', 'learning'}
160
+
161
+ result = simplify_and_weight_tag_set_to_dict(tag_set)
162
+ keys = list(result.keys())
163
+
164
+ assert keys[0] == 'learning'
165
+ assert result['learning'] == 3
166
+
167
+
168
+ class TestGetScorePercentageFromTagSetWeighted(TestCase):
169
+ def test_empty_reference(self) -> None:
170
+ tag_set_actual = {'apple', 'banana'}
171
+ tag_set_reference = set()
172
+
173
+ result = get_score_percentage_from_tag_set_weighted(tag_set_actual, tag_set_reference)
174
+
175
+ assert result == 0.0
176
+
177
+ def test_no_match(self) -> None:
178
+ tag_set_actual = {'grape', 'kiwi'}
179
+ tag_set_reference = {'apple', 'banana'}
180
+
181
+ result = get_score_percentage_from_tag_set_weighted(tag_set_actual, tag_set_reference)
182
+
183
+ assert result == 0.0
184
+
185
+ def test_with_weighted_match(self) -> None:
186
+ tag_set_actual = {'machine-learning', 'learning'}
187
+ tag_set_reference = {'machine-learning', 'deep-learning'}
188
+
189
+ result = get_score_percentage_from_tag_set_weighted(tag_set_actual, tag_set_reference)
190
+
191
+ assert result > 0
@@ -1,3 +1,6 @@
1
+ from __future__ import annotations
2
+
3
+
1
4
  def get_matching_tag_set_from_tag_sets(tag_set_a: set[str], tag_set_b: set[str]) -> set[str]:
2
5
  return tag_set_a.intersection(tag_set_b)
3
6
 
@@ -1,5 +1,8 @@
1
- <div class="card ms-0 shadow-sm rounded-3 bg-app-layer-two {% block card_class %}{% endblock %}">
2
- <div class="{% block card_padding %}p-3{% endblock %}">
1
+ <div
2
+ class="card ms-0 shadow-sm rounded-3 bg-app-layer-two {% block card_class %}{% endblock %}"
3
+ style="{% block card_style %}{% endblock %}"
4
+ >
5
+ <div class="{% block card_padding %}p-3{% endblock %}" style="{% block card_body_style %}{% endblock %}">
3
6
  {% block card_content %}{% endblock %}
4
7
  </div>
5
8
 
@@ -1,7 +1,12 @@
1
1
  {% extends 'django_spire/card/card.html' %}
2
2
 
3
+ {% block card_padding %}p-3 h-100{% endblock %}
4
+
5
+ {% block card_body_style %}display: flex; flex-direction: column; overflow: hidden;{% endblock %}
6
+
3
7
  {% block card_content %}
4
8
  <div
9
+ class="d-flex flex-column flex-grow-1 overflow-hidden"
5
10
  x-data="{
6
11
  card_title_dropdown: false,
7
12
  toggle_card_title_dropdown() {
@@ -9,8 +14,8 @@
9
14
  }
10
15
  }"
11
16
  >
12
- <div class="row justify-content-between align-items-center mb-2 pb-2 border-bottom">
13
- <div class="col">
17
+ <div class="row justify-content-between align-items-center mb-2 pb-2 border-bottom flex-shrink-0" style="min-height: 38px;">
18
+ <div class="col d-flex align-items-center">
14
19
  <div class="card-title text-uppercase mb-0">
15
20
  {% block card_title %}
16
21
  {% endblock %}
@@ -20,11 +25,11 @@
20
25
  {% block card_button %}{% endblock %}
21
26
  </div>
22
27
  </div>
23
- <div x-show="card_title_dropdown" x-cloak x-collapse>
28
+ <div x-show="card_title_dropdown" x-cloak x-collapse class="flex-shrink-0">
24
29
  {% block card_dropdown_content %}
25
30
  {% endblock %}
26
31
  </div>
27
- <div style="{% block card_title_content_style %}{% endblock %}">
32
+ <div class="flex-grow-1 overflow-hidden" style="{% block card_title_content_style %}{% endblock %}">
28
33
  {% block card_title_content %}
29
34
  {% endblock %}
30
35
  </div>
@@ -1,4 +1,5 @@
1
1
  <div
2
+ class="{% block scroll_wrapper_class %}{% endblock %}"
2
3
  x-data="{
3
4
  batch_size: parseInt('{{ batch_size|default:50 }}'),
4
5
  current_page: parseInt('{{ current_page|default:0 }}'),
@@ -1,8 +1,21 @@
1
1
  {% load static %}
2
2
 
3
3
  <div
4
- class="navbar sticky-top bg-app-side-navigation d-block"
5
- style="width: 100%; height: 100vh"
4
+ class="navbar sticky-top bg-app-side-navigation d-block w-100 vh-100"
5
+ x-data="{
6
+ show_top_fade: false,
7
+ show_bottom_fade: false,
8
+
9
+ check_scroll() {
10
+ let element = this.$refs.scroll_container;
11
+ let has_scroll = element.scrollHeight > element.clientHeight;
12
+ let is_at_top = element.scrollTop <= 20;
13
+ let is_at_bottom = element.scrollHeight - element.scrollTop <= element.clientHeight + 20;
14
+ this.show_top_fade = has_scroll && !is_at_top;
15
+ this.show_bottom_fade = has_scroll && !is_at_bottom;
16
+ }
17
+ }"
18
+ x-init="$nextTick(() => { check_scroll(); window.addEventListener('resize', () => check_scroll()); })"
6
19
  >
7
20
  <div class="text-center pt-2">
8
21
  {% block side_navigation_top_content %}
@@ -20,32 +33,15 @@
20
33
  {% endblock %}
21
34
  </div>
22
35
 
23
- <div
24
- class="position-relative"
25
- style="height: calc(100vh - var(--app-side-navigation-top-content-height));"
26
- x-data="{
27
- show_top_fade: false,
28
- show_bottom_fade: false,
29
- check_scroll() {
30
- let element = this.$refs.scroll_container;
31
- let has_scroll = element.scrollHeight > element.clientHeight;
32
- let is_at_top = element.scrollTop <= 20;
33
- let is_at_bottom = element.scrollHeight - element.scrollTop <= element.clientHeight + 20;
34
- this.show_top_fade = has_scroll && !is_at_top;
35
- this.show_bottom_fade = has_scroll && !is_at_bottom;
36
- }
37
- }"
38
- x-init="$nextTick(() => { check_scroll(); window.addEventListener('resize', () => check_scroll()); })"
39
- >
36
+ <div class="position-relative">
40
37
  <div
41
- class="position-absolute top-0 start-0 w-100 side-navigation-fade-top"
42
- style="z-index: 10;"
38
+ class="position-absolute top-0 start-0 w-100 z-1 side-navigation-fade-top"
43
39
  x-show="show_top_fade"
44
40
  >
45
41
  </div>
46
42
 
47
43
  <div
48
- class="navbar-nav-scroll scrollbar-hidden px-2 pb-4 h-100"
44
+ class="navbar-nav-scroll scrollbar-hidden px-2 px-1 h-100"
49
45
  x-ref="scroll_container"
50
46
  @scroll="check_scroll()"
51
47
  >
@@ -55,8 +51,7 @@
55
51
  </div>
56
52
 
57
53
  <div
58
- class="position-absolute bottom-0 start-0 w-100 side-navigation-fade-bottom"
59
- style="z-index: 10;"
54
+ class="position-absolute bottom-0 start-0 w-100 z-1 side-navigation-fade-bottom"
60
55
  x-show="show_bottom_fade"
61
56
  >
62
57
  </div>
@@ -37,11 +37,29 @@
37
37
  sub_nav_max_width: 600,
38
38
  sub_nav_min_width: 300,
39
39
  sub_nav_width: 300,
40
+ sub_toggle_left: 8,
40
41
 
41
42
  is_touch_device() {
42
43
  return window.matchMedia('(pointer: coarse)').matches || 'ontouchstart' in window;
43
44
  },
44
45
 
46
+ show_sub_toggle() {
47
+ return has_content(this.$refs.full_page_sub_navigation) && !this.show_sub_nav && !((this.is_mobile && this.is_narrow_viewport) && this.show_info_nav);
48
+ },
49
+
50
+ show_info_toggle() {
51
+ return has_content(this.$refs.full_page_info_navigation) && !this.show_info_nav && !((this.is_mobile && this.is_narrow_viewport) && this.show_sub_nav);
52
+ },
53
+
54
+ update_toggle_position() {
55
+ let side_nav = document.querySelector('.side-navigation');
56
+ if (side_nav && window.innerWidth >= 992) {
57
+ this.sub_toggle_left = side_nav.offsetWidth;
58
+ } else {
59
+ this.sub_toggle_left = 8;
60
+ }
61
+ },
62
+
45
63
  update_viewport_state() {
46
64
  const was_narrow = this.is_narrow_viewport;
47
65
  const is_now_narrow = window.innerWidth < 992;
@@ -67,6 +85,7 @@
67
85
  init() {
68
86
  this.is_mobile = this.is_touch_device();
69
87
  this.update_viewport_state();
88
+ this.update_toggle_position();
70
89
 
71
90
  const urlParams = new URLSearchParams(window.location.search);
72
91
 
@@ -82,6 +101,7 @@
82
101
 
83
102
  window.addEventListener('resize', () => {
84
103
  this.update_viewport_state();
104
+ this.update_toggle_position();
85
105
  })
86
106
  },
87
107
 
@@ -169,8 +189,10 @@
169
189
  {% block full_page_sub_navigation_buttons %}
170
190
  {% endblock %}
171
191
  </div>
172
- <div class="col-auto px-1">
173
- {% include 'django_spire/button/primary_button.html' with button_icon='bi bi-x-lg' x_button_click='close_sub()' %}
192
+ <div class="col-auto px-2">
193
+ <a role="button" class="link-secondary" @click="close_sub()">
194
+ <i class="bi bi-x-lg"></i>
195
+ </a>
174
196
  </div>
175
197
  </div>
176
198
 
@@ -195,14 +217,21 @@
195
217
  </div>
196
218
  </div>
197
219
 
220
+ <template x-if="show_sub_toggle()">
221
+ <div
222
+ class="panel-toggle-container"
223
+ :style="{ left: sub_toggle_left + 'px' }"
224
+ >
225
+ {% include 'django_spire/button/primary_button.html' with button_icon='bi bi-chevron-right' x_button_click='toggle_sub()' %}
226
+ </div>
227
+ </template>
228
+
198
229
  <div
199
- class="panel-toggle-container panel-toggle-container-left"
200
- x-show="has_content($refs.full_page_sub_navigation) && !show_sub_nav && !((is_mobile && is_narrow_viewport) && show_info_nav)"
230
+ class="col pb-2"
231
+ :class="{
232
+ 'ps-5': show_sub_toggle(),
233
+ }"
201
234
  >
202
- {% include 'django_spire/button/primary_button.html' with button_icon='bi bi-chevron-right' x_button_click='toggle_sub()' %}
203
- </div>
204
-
205
- <div class="col">
206
235
  {% block full_page_content %}
207
236
  {% endblock %}
208
237
 
@@ -210,12 +239,11 @@
210
239
  {% endblock %}
211
240
  </div>
212
241
 
213
- <div
214
- class="col-auto panel-toggle-container panel-toggle-container-right"
215
- x-show="has_content($refs.full_page_info_navigation) && !show_info_nav && !((is_mobile && is_narrow_viewport) && show_sub_nav)"
216
- >
217
- {% include 'django_spire/button/primary_button.html' with button_icon='bi bi-chevron-left' x_button_click='toggle_info()' %}
218
- </div>
242
+ <template x-if="show_info_toggle()">
243
+ <div class="panel-toggle-container panel-toggle-container-right">
244
+ {% include 'django_spire/button/primary_button.html' with button_icon='bi bi-chevron-left' x_button_click='toggle_info()' %}
245
+ </div>
246
+ </template>
219
247
 
220
248
  <div
221
249
  :class="(is_mobile && is_narrow_viewport) ? '' : 'position-relative col-auto'"
@@ -239,8 +267,10 @@
239
267
  {% endblock %}
240
268
  </div>
241
269
 
242
- <div class="col-auto px-1">
243
- {% include 'django_spire/button/primary_button.html' with button_icon='bi bi-x-lg' x_button_click="close_info()" %}
270
+ <div class="col-auto pe-4">
271
+ <a role="button" class="link-secondary" @click="close_info()">
272
+ <i class="bi bi-x-lg"></i>
273
+ </a>
244
274
  </div>
245
275
  </div>
246
276
 
@@ -1,5 +1,7 @@
1
1
  {% extends 'django_spire/infinite_scroll/base.html' %}
2
2
 
3
+ {% block scroll_wrapper_class %}d-flex flex-column{% if not table_height %} h-100{% endif %}{% endblock %}
4
+
3
5
  {% block scroll_xdata %}
4
6
  responsive_mode: '{{ responsive_mode|default:"collapse" }}',
5
7
  table_id: $id('table'),
@@ -138,8 +140,8 @@
138
140
 
139
141
  {% block scroll_container %}
140
142
  <div
141
- class="position-relative table-container"
142
- style="height: {{ table_height|default:'600px' }}; overflow-x: auto; overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch;"
143
+ class="position-relative table-container {% if not table_height %}flex-grow-1 min-height-0{% endif %}"
144
+ style="height: {{ table_height|default:'600px' }}; max-height: 100%; overflow-x: auto; overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch;"
143
145
  x-ref="scroll_container"
144
146
  :data-table-id="table_id"
145
147
  >