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.
- django_spire/ai/admin.py +11 -11
- django_spire/ai/chat/apps.py +1 -0
- django_spire/ai/chat/templates/django_spire/ai/chat/widget/dialog_widget.html +1 -1
- django_spire/ai/chat/tests/factories.py +15 -0
- django_spire/ai/chat/tests/test_controller.py +45 -0
- django_spire/ai/chat/tests/test_models.py +301 -0
- django_spire/ai/chat/tests/test_prompts.py +48 -0
- django_spire/ai/chat/tests/test_responses.py +208 -0
- django_spire/ai/chat/tests/test_router/test_base_chat_router.py +66 -6
- django_spire/ai/chat/tests/test_router/test_chat_workflow.py +73 -3
- django_spire/ai/chat/tests/test_router/test_integration.py +86 -6
- django_spire/ai/chat/tests/test_router/test_intent_decoder.py +93 -1
- django_spire/ai/chat/tests/test_router/test_message_intel.py +60 -1
- django_spire/ai/chat/tests/test_router/test_spire_chat_router.py +110 -0
- django_spire/ai/chat/tests/test_urls/test_json_urls.py +202 -1
- django_spire/ai/context/tests/__init__.py +0 -0
- django_spire/ai/context/tests/test_context.py +188 -0
- django_spire/ai/decorators.py +7 -6
- django_spire/ai/prompt/tests/test_bots.py +100 -10
- django_spire/ai/prompt/tests/test_prompt_intel.py +83 -0
- django_spire/ai/prompt/tests/test_prompt_tuning.py +126 -0
- django_spire/ai/sms/decorators.py +8 -2
- django_spire/ai/sms/tests/test_sms.py +240 -16
- django_spire/ai/sms/tests/test_sms_intel.py +42 -0
- django_spire/ai/sms/tests/test_webhook.py +155 -7
- django_spire/ai/sms/views.py +23 -24
- django_spire/ai/tests/test_ai.py +131 -7
- django_spire/auth/apps.py +4 -2
- django_spire/auth/controller/controller.py +36 -23
- django_spire/auth/controller/exceptions.py +9 -0
- django_spire/auth/group/admin.py +1 -0
- django_spire/auth/group/apps.py +2 -0
- django_spire/auth/group/factories.py +17 -8
- django_spire/auth/group/forms.py +7 -0
- django_spire/auth/group/tests/test_factories.py +146 -0
- django_spire/auth/group/tests/test_forms.py +282 -0
- django_spire/auth/group/tests/test_models.py +192 -0
- django_spire/auth/group/tests/test_querysets.py +98 -0
- django_spire/auth/group/tests/test_utils.py +341 -0
- django_spire/auth/group/tests/test_views.py +377 -0
- django_spire/auth/group/urls/__init__.py +3 -1
- django_spire/auth/group/urls/form_urls.py +2 -0
- django_spire/auth/group/urls/json_urls.py +3 -0
- django_spire/auth/group/urls/page_urls.py +2 -0
- django_spire/auth/group/utils.py +6 -2
- django_spire/auth/group/views/form_views.py +6 -3
- django_spire/auth/group/views/json_views.py +6 -2
- django_spire/auth/mfa/admin.py +2 -0
- django_spire/auth/mfa/apps.py +2 -0
- django_spire/auth/mfa/forms.py +1 -0
- django_spire/auth/mfa/querysets.py +9 -2
- django_spire/auth/mfa/tests/test_models.py +233 -0
- django_spire/auth/mfa/tests/test_utils.py +106 -0
- django_spire/auth/mfa/urls/__init__.py +2 -0
- django_spire/auth/mfa/urls/page_urls.py +2 -0
- django_spire/auth/mfa/urls/redirect_urls.py +2 -0
- django_spire/auth/mfa/views/page_views.py +2 -1
- django_spire/auth/permissions/consts.py +2 -2
- django_spire/auth/permissions/decorators.py +8 -8
- django_spire/auth/permissions/permissions.py +28 -35
- django_spire/auth/permissions/tests/test_decorators.py +333 -0
- django_spire/auth/permissions/tests/test_permissions.py +337 -0
- django_spire/auth/permissions/tests/test_tools.py +305 -0
- django_spire/auth/permissions/tools.py +21 -15
- django_spire/auth/seeding/seed.py +3 -0
- django_spire/auth/seeding/seeder.py +2 -0
- django_spire/auth/tests/test_controller.py +323 -0
- django_spire/auth/tests/test_url_endpoints.py +9 -9
- django_spire/auth/tests/test_views.py +406 -0
- django_spire/auth/urls/admin_urls.py +2 -0
- django_spire/auth/urls/redirect_urls.py +2 -0
- django_spire/auth/user/apps.py +2 -0
- django_spire/auth/user/forms.py +9 -0
- django_spire/auth/user/models.py +1 -1
- django_spire/auth/user/services/services.py +1 -0
- django_spire/auth/user/tests/factories.py +14 -13
- django_spire/auth/user/tests/test_factories.py +166 -2
- django_spire/auth/user/tests/test_forms.py +573 -0
- django_spire/auth/user/tests/test_models.py +257 -0
- django_spire/auth/user/tests/test_services.py +200 -0
- django_spire/auth/user/tests/test_tools.py +153 -0
- django_spire/auth/user/tests/test_user_factories.py +139 -0
- django_spire/auth/user/tests/test_views.py +363 -0
- django_spire/auth/user/tools.py +7 -1
- django_spire/auth/user/urls/form_urls.py +3 -0
- django_spire/auth/user/urls/page_urls.py +3 -0
- django_spire/auth/user/views/form_views.py +19 -10
- django_spire/auth/user/views/page_views.py +8 -2
- django_spire/auth/views/redirect_views.py +14 -9
- django_spire/comment/admin.py +2 -0
- django_spire/comment/apps.py +2 -0
- django_spire/comment/templatetags/comment_tags.py +1 -0
- django_spire/comment/tests/test_forms.py +27 -0
- django_spire/comment/tests/test_models.py +215 -0
- django_spire/comment/tests/test_querysets.py +101 -0
- django_spire/comment/tests/test_utils.py +90 -0
- django_spire/comment/urls.py +2 -0
- django_spire/comment/utils.py +22 -13
- django_spire/comment/views.py +1 -1
- django_spire/conf.py +8 -6
- django_spire/consts.py +1 -1
- django_spire/contrib/breadcrumb/apps.py +2 -0
- django_spire/contrib/breadcrumb/breadcrumbs.py +18 -18
- django_spire/contrib/breadcrumb/tests/test_breadcrumbs.py +198 -0
- django_spire/contrib/constructor/__init__.py +3 -3
- django_spire/contrib/constructor/constructor.py +15 -15
- django_spire/contrib/constructor/django_model_constructor.py +5 -4
- django_spire/contrib/constructor/exceptions.py +5 -3
- django_spire/contrib/constructor/tests/__init__.py +0 -0
- django_spire/contrib/constructor/tests/test_constructor.py +193 -0
- django_spire/contrib/form/tests/__init__.py +0 -0
- django_spire/contrib/form/tests/test_forms.py +203 -0
- django_spire/contrib/generic_views/modal_views.py +2 -1
- django_spire/contrib/generic_views/portal_views.py +20 -19
- django_spire/contrib/generic_views/tests/__init__.py +0 -0
- django_spire/contrib/generic_views/tests/test_views.py +459 -0
- django_spire/contrib/help/apps.py +2 -0
- django_spire/contrib/help/templatetags/help.py +1 -0
- django_spire/contrib/help/tests/__init__.py +0 -0
- django_spire/contrib/help/tests/test_templatetags.py +100 -0
- django_spire/contrib/options/mixins.py +6 -5
- django_spire/contrib/options/tests/factories.py +5 -1
- django_spire/contrib/options/tests/test_options.py +234 -0
- django_spire/contrib/ordering/exceptions.py +7 -3
- django_spire/contrib/ordering/mixins.py +2 -0
- django_spire/contrib/ordering/querysets.py +3 -1
- django_spire/contrib/ordering/services/processor_service.py +8 -4
- django_spire/contrib/ordering/services/service.py +1 -2
- django_spire/contrib/ordering/tests/__init__.py +0 -0
- django_spire/contrib/ordering/tests/test_ordering.py +165 -0
- django_spire/contrib/ordering/validators.py +6 -6
- django_spire/contrib/pagination/templatetags/pagination_tags.py +12 -5
- django_spire/contrib/pagination/tests/__init__.py +0 -0
- django_spire/contrib/pagination/tests/test_pagination.py +179 -0
- django_spire/contrib/performance/decorators.py +16 -6
- django_spire/contrib/performance/tests/__init__.py +0 -0
- django_spire/contrib/performance/tests/test_performance.py +107 -0
- django_spire/contrib/queryset/enums.py +3 -1
- django_spire/contrib/queryset/filter_tools.py +10 -5
- django_spire/contrib/queryset/mixins.py +16 -16
- django_spire/contrib/queryset/tests/__init__.py +0 -0
- django_spire/contrib/queryset/tests/test_queryset.py +137 -0
- django_spire/contrib/seeding/field/base.py +13 -7
- django_spire/contrib/seeding/field/callable.py +8 -1
- django_spire/contrib/seeding/field/cleaners.py +5 -5
- django_spire/contrib/seeding/field/custom.py +20 -10
- django_spire/contrib/seeding/field/django/seeder.py +8 -6
- django_spire/contrib/seeding/field/enums.py +7 -5
- django_spire/contrib/seeding/field/override.py +16 -6
- django_spire/contrib/seeding/field/static.py +9 -2
- django_spire/contrib/seeding/field/tests/test_base.py +18 -14
- django_spire/contrib/seeding/field/tests/test_callable.py +13 -9
- django_spire/contrib/seeding/field/tests/test_cleaners.py +51 -38
- django_spire/contrib/seeding/field/tests/test_static.py +13 -9
- django_spire/contrib/seeding/intelligence/bots/seeder_generator_bot.py +2 -0
- django_spire/contrib/seeding/intelligence/intel.py +5 -1
- django_spire/contrib/seeding/intelligence/prompts/factory.py +6 -1
- django_spire/contrib/seeding/intelligence/prompts/foreign_key_selection_prompt.py +6 -1
- django_spire/contrib/seeding/intelligence/prompts/generate_django_model_seeder_prompts.py +2 -0
- django_spire/contrib/seeding/intelligence/prompts/generic_relationship_selection_prompt.py +7 -1
- django_spire/contrib/seeding/intelligence/prompts/hierarchical_selection_prompt.py +6 -2
- django_spire/contrib/seeding/intelligence/prompts/model_field_choices_prompt.py +8 -2
- django_spire/contrib/seeding/intelligence/prompts/negation_prompt.py +2 -0
- django_spire/contrib/seeding/intelligence/prompts/objective_prompt.py +6 -1
- django_spire/contrib/seeding/management/commands/seeding.py +9 -3
- django_spire/contrib/seeding/management/example.py +2 -0
- django_spire/contrib/seeding/model/base.py +16 -7
- django_spire/contrib/seeding/model/config.py +31 -15
- django_spire/contrib/seeding/model/django/config.py +13 -13
- django_spire/contrib/seeding/model/django/seeder.py +4 -4
- django_spire/contrib/seeding/model/django/tests/test_seeder.py +34 -23
- django_spire/contrib/seeding/model/enums.py +2 -0
- django_spire/contrib/seeding/tests/test_config.py +71 -0
- django_spire/contrib/seeding/tests/test_custom.py +35 -0
- django_spire/contrib/seeding/tests/test_enums.py +40 -0
- django_spire/contrib/seeding/tests/test_intel.py +32 -0
- django_spire/contrib/seeding/tests/test_override.py +63 -0
- django_spire/contrib/service/__init__.py +2 -2
- django_spire/contrib/service/django_model_service.py +16 -15
- django_spire/contrib/service/exceptions.py +5 -3
- django_spire/contrib/service/tests/__init__.py +0 -0
- django_spire/contrib/service/tests/test_service.py +153 -0
- django_spire/contrib/session/apps.py +2 -0
- django_spire/contrib/session/controller.py +48 -42
- django_spire/contrib/session/templatetags/session_tags.py +11 -2
- django_spire/contrib/session/tests/test_session_controller.py +117 -53
- django_spire/contrib/tests/__init__.py +0 -0
- django_spire/contrib/tests/test_utils.py +37 -0
- django_spire/contrib/utils.py +4 -1
- django_spire/core/apps.py +2 -0
- django_spire/core/converters/tests/test_to_data.py +353 -0
- django_spire/core/converters/tests/test_to_enums.py +61 -41
- django_spire/core/converters/tests/test_to_pydantic.py +138 -109
- django_spire/core/converters/to_data.py +29 -10
- django_spire/core/converters/to_enums.py +4 -2
- django_spire/core/converters/to_pydantic.py +22 -22
- django_spire/core/decorators.py +19 -6
- django_spire/core/forms/widgets.py +4 -0
- django_spire/core/maps.py +3 -1
- django_spire/core/middleware/maintenance.py +3 -3
- django_spire/core/middleware.py +8 -6
- django_spire/core/redirect/__init__.py +5 -0
- django_spire/core/redirect/generic_redirect.py +1 -2
- django_spire/core/redirect/tests/__init__.py +0 -0
- django_spire/core/redirect/tests/test_generic_redirect.py +34 -0
- django_spire/core/{tests/tests_redirect.py → redirect/tests/test_safe_redirect.py} +55 -81
- django_spire/core/shortcuts.py +3 -3
- django_spire/core/static/django_spire/css/app-layout.css +1 -1
- django_spire/core/static/django_spire/css/app-navigation.css +3 -3
- django_spire/core/static/django_spire/css/bootstrap-override.css +4 -0
- django_spire/core/tag/admin.py +12 -0
- django_spire/core/tag/intelligence/tag_set_bot.py +2 -0
- django_spire/core/tag/mixins.py +2 -0
- django_spire/core/tag/models.py +2 -0
- django_spire/core/tag/querysets.py +2 -0
- django_spire/core/tag/service/tag_service.py +6 -3
- django_spire/core/tag/tests/test_intelligence.py +9 -9
- django_spire/core/tag/tests/test_tags.py +44 -54
- django_spire/core/tag/tests/test_tools.py +191 -0
- django_spire/core/tag/tools.py +3 -0
- django_spire/core/templates/django_spire/card/card.html +5 -2
- django_spire/core/templates/django_spire/card/title_card.html +9 -4
- django_spire/core/templates/django_spire/infinite_scroll/base.html +1 -0
- django_spire/core/templates/django_spire/navigation/side_navigation.html +19 -24
- django_spire/core/templates/django_spire/page/full_page.html +46 -16
- django_spire/core/templates/django_spire/table/base.html +4 -2
- django_spire/core/templatetags/json.py +6 -2
- django_spire/core/templatetags/message.py +13 -8
- django_spire/core/templatetags/string_formating.py +8 -5
- django_spire/core/templatetags/tests/__init__.py +0 -0
- django_spire/core/templatetags/tests/test_templatetags.py +427 -0
- django_spire/core/templatetags/variable_types.py +17 -9
- django_spire/core/tests/test_cases.py +1 -1
- django_spire/core/tests/test_conf.py +43 -0
- django_spire/core/tests/test_consts.py +28 -0
- django_spire/core/tests/test_context_processors.py +93 -0
- django_spire/core/tests/test_decorators.py +95 -0
- django_spire/core/tests/test_django_spire_utils.py +56 -0
- django_spire/core/tests/test_exceptions.py +37 -0
- django_spire/core/tests/test_models.py +54 -0
- django_spire/core/tests/test_settings.py +45 -0
- django_spire/core/tests/test_shortcuts.py +74 -0
- django_spire/core/tests/test_urls.py +16 -0
- django_spire/core/tests/test_utils.py +58 -0
- django_spire/core/urls.py +4 -1
- django_spire/core/utils.py +12 -8
- django_spire/exceptions.py +16 -1
- django_spire/file/admin.py +4 -2
- django_spire/file/apps.py +8 -10
- django_spire/file/fields.py +7 -7
- django_spire/file/forms.py +1 -1
- django_spire/file/interfaces.py +15 -15
- django_spire/file/mixins.py +1 -4
- django_spire/file/models.py +3 -5
- django_spire/file/tests/factories.py +59 -0
- django_spire/file/tests/test_admin.py +69 -0
- django_spire/file/tests/test_apps.py +24 -0
- django_spire/file/tests/test_fields.py +114 -0
- django_spire/file/tests/test_forms.py +20 -0
- django_spire/file/tests/test_interfaces.py +183 -0
- django_spire/file/tests/test_models.py +82 -0
- django_spire/file/tests/test_querysets.py +102 -0
- django_spire/file/tests/test_utils.py +32 -0
- django_spire/file/tests/test_views.py +145 -0
- django_spire/file/tests/test_widgets.py +82 -0
- django_spire/file/tools.py +8 -2
- django_spire/file/views.py +7 -3
- django_spire/file/widgets.py +12 -12
- django_spire/help_desk/admin.py +15 -0
- django_spire/help_desk/apps.py +2 -0
- django_spire/help_desk/auth/controller.py +2 -0
- django_spire/help_desk/choices.py +2 -0
- django_spire/help_desk/enums.py +2 -0
- django_spire/help_desk/exceptions.py +31 -3
- django_spire/help_desk/forms.py +2 -0
- django_spire/help_desk/models.py +2 -0
- django_spire/help_desk/querysets.py +4 -1
- django_spire/help_desk/services/notification_service.py +26 -27
- django_spire/help_desk/services/service.py +2 -3
- django_spire/help_desk/tests/factories.py +8 -3
- django_spire/help_desk/tests/test_admin.py +41 -0
- django_spire/help_desk/tests/test_apps.py +41 -0
- django_spire/help_desk/tests/test_choices.py +50 -0
- django_spire/help_desk/tests/test_controller.py +87 -0
- django_spire/help_desk/tests/test_enums.py +18 -0
- django_spire/help_desk/tests/test_exceptions.py +37 -0
- django_spire/help_desk/tests/test_forms.py +89 -0
- django_spire/help_desk/tests/test_models.py +59 -0
- django_spire/help_desk/tests/test_querysets.py +38 -0
- django_spire/help_desk/tests/test_services/test_notification_service.py +15 -8
- django_spire/help_desk/tests/test_services/test_service.py +92 -0
- django_spire/help_desk/tests/test_urls/test_form_urls.py +6 -6
- django_spire/help_desk/tests/test_urls/test_page_urls.py +8 -9
- django_spire/help_desk/tests/test_views/test_form_views.py +46 -19
- django_spire/help_desk/tests/test_views/test_page_views.py +32 -9
- django_spire/help_desk/urls/__init__.py +4 -1
- django_spire/help_desk/urls/form_urls.py +3 -0
- django_spire/help_desk/urls/page_urls.py +3 -0
- django_spire/help_desk/views/form_views.py +13 -5
- django_spire/help_desk/views/page_views.py +11 -3
- django_spire/history/activity/admin.py +2 -0
- django_spire/history/activity/apps.py +3 -1
- django_spire/history/activity/mixins.py +13 -7
- django_spire/history/activity/models.py +6 -5
- django_spire/history/activity/querysets.py +2 -0
- django_spire/history/activity/tests/__init__.py +0 -0
- django_spire/history/activity/tests/test_activity.py +176 -0
- django_spire/history/admin.py +9 -2
- django_spire/history/choices.py +3 -0
- django_spire/history/models.py +5 -5
- django_spire/history/tests/test_admin.py +93 -0
- django_spire/history/tests/test_history.py +101 -0
- django_spire/history/tests/test_mixins.py +84 -0
- django_spire/history/viewed/admin.py +3 -1
- django_spire/history/viewed/apps.py +3 -1
- django_spire/history/viewed/models.py +2 -0
- django_spire/history/viewed/tests/__init__.py +0 -0
- django_spire/history/viewed/tests/test_viewed.py +46 -0
- django_spire/knowledge/auth/tests/__init__.py +0 -0
- django_spire/knowledge/auth/tests/test_controller.py +116 -0
- django_spire/knowledge/collection/admin.py +5 -1
- django_spire/knowledge/collection/models.py +3 -1
- django_spire/knowledge/collection/seeding/seed.py +1 -0
- django_spire/knowledge/collection/services/factory_service.py +10 -11
- django_spire/knowledge/collection/services/ordering_service.py +1 -2
- django_spire/knowledge/collection/services/service.py +5 -10
- django_spire/knowledge/collection/services/tag_service.py +5 -2
- django_spire/knowledge/collection/tests/factories.py +28 -1
- django_spire/knowledge/collection/tests/test_models.py +48 -0
- django_spire/knowledge/collection/tests/test_querysets.py +93 -0
- django_spire/knowledge/collection/tests/test_services/test_factory_service.py +100 -0
- django_spire/knowledge/collection/tests/test_services/test_services.py +160 -0
- django_spire/knowledge/collection/tests/test_urls/test_form_urls.py +21 -3
- django_spire/knowledge/collection/tests/test_urls/test_json_urls.py +39 -1
- django_spire/knowledge/collection/tests/test_urls/test_page_urls.py +12 -4
- django_spire/knowledge/collection/urls/__init__.py +3 -0
- django_spire/knowledge/collection/urls/form_urls.py +2 -0
- django_spire/knowledge/collection/urls/json_urls.py +2 -0
- django_spire/knowledge/collection/urls/page_urls.py +2 -0
- django_spire/knowledge/collection/views/form_views.py +4 -4
- django_spire/knowledge/collection/views/json_views.py +5 -1
- django_spire/knowledge/collection/views/page_views.py +5 -2
- django_spire/knowledge/entry/admin.py +7 -1
- django_spire/knowledge/entry/forms.py +2 -0
- django_spire/knowledge/entry/models.py +2 -0
- django_spire/knowledge/entry/seeding/seed.py +3 -0
- django_spire/knowledge/entry/services/automation_service.py +5 -4
- django_spire/knowledge/entry/services/factory_service.py +7 -5
- django_spire/knowledge/entry/services/service.py +4 -7
- django_spire/knowledge/entry/services/tag_service.py +0 -1
- django_spire/knowledge/entry/services/tool_service.py +1 -0
- django_spire/knowledge/entry/services/transformation_services.py +1 -5
- django_spire/knowledge/entry/tests/factories.py +1 -2
- django_spire/knowledge/entry/tests/test_factory_service.py +20 -0
- django_spire/knowledge/entry/tests/test_models.py +41 -0
- django_spire/knowledge/entry/tests/test_querysets.py +71 -0
- django_spire/knowledge/entry/tests/test_services.py +94 -0
- django_spire/knowledge/entry/tests/test_urls/test_form_urls.py +9 -14
- django_spire/knowledge/entry/tests/test_urls/test_json_urls.py +48 -5
- django_spire/knowledge/entry/tests/test_urls/test_page_urls.py +6 -8
- django_spire/knowledge/entry/tests/test_urls/test_template_urls.py +40 -0
- django_spire/knowledge/entry/urls/form_urls.py +2 -0
- django_spire/knowledge/entry/urls/json_urls.py +2 -0
- django_spire/knowledge/entry/urls/page_urls.py +2 -0
- django_spire/knowledge/entry/urls/template_urls.py +2 -0
- django_spire/knowledge/entry/version/block/choices.py +2 -0
- django_spire/knowledge/entry/version/block/data/data.py +1 -0
- django_spire/knowledge/entry/version/block/data/list/data.py +8 -13
- django_spire/knowledge/entry/version/block/data/list/maps.py +3 -0
- django_spire/knowledge/entry/version/block/data/list/meta.py +1 -2
- django_spire/knowledge/entry/version/block/data/list/tests/__init__.py +0 -0
- django_spire/knowledge/entry/version/block/data/list/tests/test_maps.py +32 -0
- django_spire/knowledge/entry/version/block/data/list/tests/test_meta.py +58 -0
- django_spire/knowledge/entry/version/block/data/maps.py +3 -6
- django_spire/knowledge/entry/version/block/models.py +7 -5
- django_spire/knowledge/entry/version/block/seeding/constants.py +5 -4
- django_spire/knowledge/entry/version/block/services/service.py +2 -3
- django_spire/knowledge/entry/version/block/tests/factories.py +4 -10
- django_spire/knowledge/entry/version/block/tests/test_choices.py +56 -0
- django_spire/knowledge/entry/version/block/tests/test_data.py +90 -0
- django_spire/knowledge/entry/version/block/tests/test_maps.py +37 -0
- django_spire/knowledge/entry/version/block/tests/test_models.py +55 -0
- django_spire/knowledge/entry/version/block/tests/test_querysets.py +35 -0
- django_spire/knowledge/entry/version/block/tests/test_services.py +65 -0
- django_spire/knowledge/entry/version/choices.py +2 -0
- django_spire/knowledge/entry/version/converters/converter.py +1 -1
- django_spire/knowledge/entry/version/converters/docx_converter.py +4 -7
- django_spire/knowledge/entry/version/converters/markdown_converter.py +20 -20
- django_spire/knowledge/entry/version/maps.py +4 -5
- django_spire/knowledge/entry/version/querysets.py +1 -1
- django_spire/knowledge/entry/version/seeding/seeder.py +1 -2
- django_spire/knowledge/entry/version/services/processor_service.py +5 -4
- django_spire/knowledge/entry/version/services/service.py +1 -2
- django_spire/knowledge/entry/version/tests/factories.py +2 -2
- django_spire/knowledge/entry/version/tests/test_choices.py +18 -0
- django_spire/knowledge/entry/version/tests/test_converters/test_docx_converter.py +56 -8
- django_spire/knowledge/entry/version/tests/test_converters/test_markdown_converter.py +78 -0
- django_spire/knowledge/entry/version/tests/test_maps.py +58 -0
- django_spire/knowledge/entry/version/tests/test_models.py +23 -0
- django_spire/knowledge/entry/version/tests/test_querysets.py +26 -0
- django_spire/knowledge/entry/version/tests/test_services.py +62 -0
- django_spire/knowledge/entry/version/tests/test_urls/test_json_urls.py +27 -8
- django_spire/knowledge/entry/version/tests/test_urls/test_page_urls.py +15 -8
- django_spire/knowledge/entry/version/tests/test_urls/test_redirect_urls.py +38 -0
- django_spire/knowledge/entry/version/urls/__init__.py +3 -0
- django_spire/knowledge/entry/version/urls/json_urls.py +2 -1
- django_spire/knowledge/entry/version/urls/page_urls.py +2 -0
- django_spire/knowledge/entry/version/urls/redirect_urls.py +2 -0
- django_spire/knowledge/entry/version/views/json_views.py +5 -1
- django_spire/knowledge/entry/version/views/page_views.py +10 -3
- django_spire/knowledge/entry/version/views/redirect_views.py +5 -1
- django_spire/knowledge/entry/views/form_views.py +16 -8
- django_spire/knowledge/entry/views/json_views.py +3 -1
- django_spire/knowledge/entry/views/page_views.py +8 -2
- django_spire/knowledge/entry/views/template_views.py +7 -1
- django_spire/knowledge/exceptions.py +2 -1
- django_spire/knowledge/intelligence/intel/answer_intel.py +2 -1
- django_spire/knowledge/intelligence/intel/entry_intel.py +0 -1
- django_spire/knowledge/intelligence/workflows/knowledge_workflow.py +4 -5
- django_spire/knowledge/models.py +1 -2
- django_spire/knowledge/tests/__init__.py +0 -0
- django_spire/knowledge/tests/test_templatetags.py +40 -0
- django_spire/knowledge/tests/test_urls/__init__.py +0 -0
- django_spire/knowledge/tests/test_urls/test_page_urls.py +24 -0
- django_spire/knowledge/urls/__init__.py +2 -0
- django_spire/knowledge/urls/page_urls.py +2 -0
- django_spire/knowledge/views/page_views.py +8 -3
- django_spire/notification/admin.py +3 -1
- django_spire/notification/app/admin.py +2 -0
- django_spire/notification/app/apps.py +3 -1
- django_spire/notification/app/exceptions.py +9 -2
- django_spire/notification/app/models.py +8 -4
- django_spire/notification/app/processor.py +22 -26
- django_spire/notification/app/querysets.py +2 -0
- django_spire/notification/app/tests/__init__.py +0 -0
- django_spire/notification/app/tests/factories.py +34 -0
- django_spire/notification/app/tests/test_apps.py +24 -0
- django_spire/notification/app/tests/test_models.py +72 -0
- django_spire/notification/app/tests/test_processor.py +111 -0
- django_spire/notification/app/tests/test_querysets.py +90 -0
- django_spire/notification/app/tests/test_views/__init__.py +0 -0
- django_spire/notification/app/tests/test_views/test_json_views.py +48 -0
- django_spire/notification/app/tests/test_views/test_page_views.py +19 -0
- django_spire/notification/app/urls/__init__.py +3 -1
- django_spire/notification/app/urls/json_urls.py +6 -4
- django_spire/notification/app/urls/page_urls.py +4 -3
- django_spire/notification/app/urls/template_urls.py +4 -2
- django_spire/notification/apps.py +4 -1
- django_spire/notification/email/admin.py +5 -1
- django_spire/notification/email/apps.py +3 -1
- django_spire/notification/email/exceptions.py +4 -2
- django_spire/notification/email/helper.py +5 -3
- django_spire/notification/email/models.py +4 -0
- django_spire/notification/email/processor.py +19 -15
- django_spire/notification/email/querysets.py +3 -0
- django_spire/notification/email/tests/__init__.py +0 -0
- django_spire/notification/email/tests/factories.py +35 -0
- django_spire/notification/email/tests/test_apps.py +24 -0
- django_spire/notification/email/tests/test_models.py +52 -0
- django_spire/notification/email/tests/test_processor.py +92 -0
- django_spire/notification/email/tests/test_querysets.py +43 -0
- django_spire/notification/exceptions.py +17 -2
- django_spire/notification/managers.py +7 -1
- django_spire/notification/maps.py +4 -1
- django_spire/notification/mixins.py +2 -0
- django_spire/notification/models.py +3 -1
- django_spire/notification/processors/notification.py +12 -5
- django_spire/notification/processors/processor.py +2 -0
- django_spire/notification/processors/tests/__init__.py +0 -0
- django_spire/notification/processors/tests/test_notification.py +106 -0
- django_spire/notification/push/admin.py +10 -1
- django_spire/notification/push/apps.py +3 -1
- django_spire/notification/push/models.py +2 -3
- django_spire/notification/push/tests/__init__.py +0 -0
- django_spire/notification/push/tests/test_apps.py +24 -0
- django_spire/notification/push/tests/test_models.py +28 -0
- django_spire/notification/querysets.py +7 -1
- django_spire/notification/sms/admin.py +2 -0
- django_spire/notification/sms/apps.py +4 -1
- django_spire/notification/sms/automations.py +2 -0
- django_spire/notification/sms/choices.py +2 -0
- django_spire/notification/sms/exceptions.py +19 -5
- django_spire/notification/sms/helper.py +33 -23
- django_spire/notification/sms/models.py +5 -1
- django_spire/notification/sms/processor.py +20 -20
- django_spire/notification/sms/querysets.py +2 -0
- django_spire/notification/sms/tests/factories.py +33 -0
- django_spire/notification/sms/tests/test_apps.py +24 -0
- django_spire/notification/sms/tests/test_automation.py +38 -0
- django_spire/notification/sms/tests/test_choices.py +15 -0
- django_spire/notification/sms/tests/test_consts.py +17 -0
- django_spire/notification/sms/tests/test_exceptions.py +27 -0
- django_spire/notification/sms/tests/test_helper.py +50 -0
- django_spire/notification/sms/tests/test_models.py +81 -0
- django_spire/notification/sms/tests/test_processor.py +107 -0
- django_spire/notification/sms/tests/test_tools.py +25 -11
- django_spire/notification/sms/tools.py +16 -5
- django_spire/notification/sms/urls/__init__.py +3 -1
- django_spire/notification/sms/urls/media_urls.py +2 -0
- django_spire/notification/sms/views/media_views.py +14 -4
- django_spire/notification/tests/__init__.py +0 -0
- django_spire/notification/tests/factories.py +26 -0
- django_spire/notification/tests/test_admin.py +55 -0
- django_spire/notification/tests/test_apps.py +30 -0
- django_spire/notification/tests/test_automation.py +18 -0
- django_spire/notification/tests/test_choices.py +59 -0
- django_spire/notification/tests/test_exceptions.py +58 -0
- django_spire/notification/tests/test_managers.py +100 -0
- django_spire/notification/tests/test_maps.py +31 -0
- django_spire/notification/tests/test_models.py +76 -0
- django_spire/notification/tests/test_querysets.py +184 -0
- django_spire/notification/tests/test_utils.py +23 -0
- django_spire/notification/urls.py +3 -1
- django_spire/notification/utils.py +3 -1
- django_spire/settings.py +3 -0
- django_spire/theme/tests/test_context_processor.py +15 -13
- django_spire/theme/tests/test_enums.py +2 -2
- django_spire/theme/tests/test_filesystem.py +2 -5
- django_spire/theme/tests/test_integration.py +12 -12
- django_spire/theme/tests/test_model.py +40 -38
- django_spire/theme/tests/test_views/test_json_views.py +33 -33
- django_spire/theme/urls/json_urls.py +3 -0
- django_spire/theme/urls/page_urls.py +3 -0
- django_spire/urls.py +19 -15
- django_spire/utils.py +13 -4
- {django_spire-0.23.7.dist-info → django_spire-0.23.8.dist-info}/METADATA +1 -1
- {django_spire-0.23.7.dist-info → django_spire-0.23.8.dist-info}/RECORD +530 -358
- django_spire/contrib/options/tests/test_unit.py +0 -148
- django_spire/contrib/seeding/tests/test_seeding.py +0 -25
- django_spire/core/tests/test_templatetags.py +0 -117
- django_spire/core/tests/tests_shortcuts.py +0 -73
- django_spire/history/activity/tests.py +0 -3
- django_spire/history/activity/views.py +0 -3
- django_spire/knowledge/collection/tests/test_services/test_transformation_service.py +0 -71
- django_spire/notification/app/tests.py +0 -3
- {django_spire-0.23.7.dist-info → django_spire-0.23.8.dist-info}/WHEEL +0 -0
- {django_spire-0.23.7.dist-info → django_spire-0.23.8.dist-info}/licenses/LICENSE.md +0 -0
- {django_spire-0.23.7.dist-info → django_spire-0.23.8.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from unittest.mock import MagicMock
|
|
4
|
+
|
|
5
|
+
from django.core.paginator import Page
|
|
6
|
+
from django.test import RequestFactory, TestCase
|
|
7
|
+
|
|
8
|
+
from django_spire.contrib.pagination.pagination import paginate_list
|
|
9
|
+
from django_spire.contrib.pagination.templatetags.pagination_tags import (
|
|
10
|
+
get_elided_page_range,
|
|
11
|
+
pagination_url,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestPaginateList(TestCase):
|
|
16
|
+
def setUp(self) -> None:
|
|
17
|
+
self.object_list = list(range(100))
|
|
18
|
+
|
|
19
|
+
def test_custom_page_number(self) -> None:
|
|
20
|
+
result = paginate_list(self.object_list, page_number=2)
|
|
21
|
+
|
|
22
|
+
assert result.number == 2
|
|
23
|
+
|
|
24
|
+
def test_custom_per_page(self) -> None:
|
|
25
|
+
result = paginate_list(self.object_list, per_page=10)
|
|
26
|
+
|
|
27
|
+
assert len(result.object_list) == 10
|
|
28
|
+
|
|
29
|
+
def test_default_page_number(self) -> None:
|
|
30
|
+
result = paginate_list(self.object_list)
|
|
31
|
+
|
|
32
|
+
assert result.number == 1
|
|
33
|
+
|
|
34
|
+
def test_default_per_page(self) -> None:
|
|
35
|
+
result = paginate_list(self.object_list)
|
|
36
|
+
|
|
37
|
+
assert len(result.object_list) == 50
|
|
38
|
+
|
|
39
|
+
def test_empty_list(self) -> None:
|
|
40
|
+
result = paginate_list([])
|
|
41
|
+
|
|
42
|
+
assert len(result.object_list) == 0
|
|
43
|
+
|
|
44
|
+
def test_has_next_page(self) -> None:
|
|
45
|
+
result = paginate_list(self.object_list, page_number=1)
|
|
46
|
+
|
|
47
|
+
assert result.has_next() is True
|
|
48
|
+
|
|
49
|
+
def test_has_previous_page(self) -> None:
|
|
50
|
+
result = paginate_list(self.object_list, page_number=2)
|
|
51
|
+
|
|
52
|
+
assert result.has_previous() is True
|
|
53
|
+
|
|
54
|
+
def test_last_page_has_no_next(self) -> None:
|
|
55
|
+
result = paginate_list(self.object_list, page_number=2)
|
|
56
|
+
|
|
57
|
+
assert result.has_next() is False
|
|
58
|
+
|
|
59
|
+
def test_page_number_exceeds_pages_returns_last_page(self) -> None:
|
|
60
|
+
result = paginate_list(self.object_list, page_number=999)
|
|
61
|
+
|
|
62
|
+
assert result.number == 2
|
|
63
|
+
|
|
64
|
+
def test_returns_page_object(self) -> None:
|
|
65
|
+
result = paginate_list(self.object_list)
|
|
66
|
+
|
|
67
|
+
assert isinstance(result, Page)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class TestPaginationUrl(TestCase):
|
|
71
|
+
def setUp(self) -> None:
|
|
72
|
+
self.factory = RequestFactory()
|
|
73
|
+
|
|
74
|
+
def _create_context(self, request) -> MagicMock:
|
|
75
|
+
context = MagicMock()
|
|
76
|
+
context.request = request
|
|
77
|
+
return context
|
|
78
|
+
|
|
79
|
+
def test_custom_page_name(self) -> None:
|
|
80
|
+
request = self.factory.get('/')
|
|
81
|
+
context = self._create_context(request)
|
|
82
|
+
|
|
83
|
+
result = pagination_url(context, page_number=2, page_name='p')
|
|
84
|
+
|
|
85
|
+
assert 'p=2' in result
|
|
86
|
+
|
|
87
|
+
def test_default_page_name(self) -> None:
|
|
88
|
+
request = self.factory.get('/')
|
|
89
|
+
context = self._create_context(request)
|
|
90
|
+
|
|
91
|
+
result = pagination_url(context, page_number=1)
|
|
92
|
+
|
|
93
|
+
assert 'page=1' in result
|
|
94
|
+
|
|
95
|
+
def test_multiple_query_params(self) -> None:
|
|
96
|
+
request = self.factory.get('/', {'filter': 'active', 'sort': 'name'})
|
|
97
|
+
context = self._create_context(request)
|
|
98
|
+
|
|
99
|
+
result = pagination_url(context, page_number=3)
|
|
100
|
+
|
|
101
|
+
assert 'page=3' in result
|
|
102
|
+
assert 'filter=active' in result
|
|
103
|
+
assert 'sort=name' in result
|
|
104
|
+
|
|
105
|
+
def test_preserves_existing_query_params(self) -> None:
|
|
106
|
+
request = self.factory.get('/', {'search': 'test'})
|
|
107
|
+
context = self._create_context(request)
|
|
108
|
+
|
|
109
|
+
result = pagination_url(context, page_number=2)
|
|
110
|
+
|
|
111
|
+
assert 'search=test' in result
|
|
112
|
+
assert 'page=2' in result
|
|
113
|
+
|
|
114
|
+
def test_replaces_space_with_plus(self) -> None:
|
|
115
|
+
request = self.factory.get('/', {'search': 'hello world'})
|
|
116
|
+
context = self._create_context(request)
|
|
117
|
+
|
|
118
|
+
result = pagination_url(context, page_number=1)
|
|
119
|
+
|
|
120
|
+
assert 'search=hello+world' in result
|
|
121
|
+
|
|
122
|
+
def test_returns_query_string(self) -> None:
|
|
123
|
+
request = self.factory.get('/')
|
|
124
|
+
context = self._create_context(request)
|
|
125
|
+
|
|
126
|
+
result = pagination_url(context, page_number=1)
|
|
127
|
+
|
|
128
|
+
assert result.startswith('?')
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class TestGetElidedPageRange(TestCase):
|
|
132
|
+
def test_custom_on_each_side(self) -> None:
|
|
133
|
+
page_obj = MagicMock()
|
|
134
|
+
page_obj.number = 5
|
|
135
|
+
page_obj.paginator.get_elided_page_range.return_value = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
|
136
|
+
|
|
137
|
+
get_elided_page_range(page_obj, on_each_side=3)
|
|
138
|
+
|
|
139
|
+
page_obj.paginator.get_elided_page_range.assert_called_once_with(
|
|
140
|
+
number=5,
|
|
141
|
+
on_each_side=3,
|
|
142
|
+
on_ends=2
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def test_custom_on_ends(self) -> None:
|
|
146
|
+
page_obj = MagicMock()
|
|
147
|
+
page_obj.number = 5
|
|
148
|
+
page_obj.paginator.get_elided_page_range.return_value = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
|
149
|
+
|
|
150
|
+
get_elided_page_range(page_obj, on_ends=3)
|
|
151
|
+
|
|
152
|
+
page_obj.paginator.get_elided_page_range.assert_called_once_with(
|
|
153
|
+
number=5,
|
|
154
|
+
on_each_side=2,
|
|
155
|
+
on_ends=3
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
def test_default_parameters(self) -> None:
|
|
159
|
+
page_obj = MagicMock()
|
|
160
|
+
page_obj.number = 1
|
|
161
|
+
page_obj.paginator.get_elided_page_range.return_value = [1, 2, 3]
|
|
162
|
+
|
|
163
|
+
get_elided_page_range(page_obj)
|
|
164
|
+
|
|
165
|
+
page_obj.paginator.get_elided_page_range.assert_called_once_with(
|
|
166
|
+
number=1,
|
|
167
|
+
on_each_side=2,
|
|
168
|
+
on_ends=2
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
def test_returns_elided_page_range(self) -> None:
|
|
172
|
+
page_obj = MagicMock()
|
|
173
|
+
page_obj.number = 1
|
|
174
|
+
expected_range = [1, 2, 3, '...', 10]
|
|
175
|
+
page_obj.paginator.get_elided_page_range.return_value = expected_range
|
|
176
|
+
|
|
177
|
+
result = get_elided_page_range(page_obj)
|
|
178
|
+
|
|
179
|
+
assert result == expected_range
|
|
@@ -1,21 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import logging
|
|
2
4
|
import time
|
|
3
5
|
|
|
6
|
+
from typing import Callable, ParamSpec, TypeVar
|
|
7
|
+
|
|
4
8
|
from django.conf import settings
|
|
5
9
|
|
|
6
10
|
|
|
7
|
-
|
|
8
|
-
|
|
11
|
+
P = ParamSpec('P')
|
|
12
|
+
R = TypeVar('R')
|
|
13
|
+
|
|
14
|
+
log = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def performance_timer(func: Callable[P, R]) -> Callable[P, R]:
|
|
18
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
9
19
|
if settings.DEBUG:
|
|
10
20
|
start_time = time.perf_counter()
|
|
11
21
|
result = func(*args, **kwargs)
|
|
12
22
|
end_time = time.perf_counter()
|
|
13
23
|
|
|
14
|
-
|
|
24
|
+
message = f'{end_time - start_time:.4f} seconds runtime for "{func.__module__}.{func.__qualname__}"'
|
|
25
|
+
log.warning(message)
|
|
15
26
|
|
|
16
27
|
return result
|
|
17
28
|
|
|
18
|
-
|
|
19
|
-
return func(*args, **kwargs)
|
|
29
|
+
return func(*args, **kwargs)
|
|
20
30
|
|
|
21
|
-
return wrapper
|
|
31
|
+
return wrapper
|
|
File without changes
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from unittest.mock import patch
|
|
4
|
+
|
|
5
|
+
from django.test import TestCase, override_settings
|
|
6
|
+
|
|
7
|
+
from django_spire.contrib.performance.decorators import performance_timer
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestPerformanceTimer(TestCase):
|
|
11
|
+
@override_settings(DEBUG=True)
|
|
12
|
+
def test_calls_function_when_debug_true(self) -> None:
|
|
13
|
+
@performance_timer
|
|
14
|
+
def sample_func() -> str:
|
|
15
|
+
return 'result'
|
|
16
|
+
|
|
17
|
+
result = sample_func()
|
|
18
|
+
|
|
19
|
+
assert result == 'result'
|
|
20
|
+
|
|
21
|
+
@override_settings(DEBUG=False)
|
|
22
|
+
def test_calls_function_when_debug_false(self) -> None:
|
|
23
|
+
@performance_timer
|
|
24
|
+
def sample_func() -> str:
|
|
25
|
+
return 'result'
|
|
26
|
+
|
|
27
|
+
result = sample_func()
|
|
28
|
+
|
|
29
|
+
assert result == 'result'
|
|
30
|
+
|
|
31
|
+
@override_settings(DEBUG=True)
|
|
32
|
+
def test_logs_warning_when_debug_true(self) -> None:
|
|
33
|
+
@performance_timer
|
|
34
|
+
def sample_func() -> str:
|
|
35
|
+
return 'result'
|
|
36
|
+
|
|
37
|
+
with patch('django_spire.contrib.performance.decorators.log.warning') as mock_warning:
|
|
38
|
+
sample_func()
|
|
39
|
+
|
|
40
|
+
mock_warning.assert_called_once()
|
|
41
|
+
|
|
42
|
+
@override_settings(DEBUG=False)
|
|
43
|
+
def test_no_logging_when_debug_false(self) -> None:
|
|
44
|
+
@performance_timer
|
|
45
|
+
def sample_func() -> str:
|
|
46
|
+
return 'result'
|
|
47
|
+
|
|
48
|
+
with patch('django_spire.contrib.performance.decorators.log.warning') as mock_warning:
|
|
49
|
+
sample_func()
|
|
50
|
+
|
|
51
|
+
mock_warning.assert_not_called()
|
|
52
|
+
|
|
53
|
+
@override_settings(DEBUG=True)
|
|
54
|
+
def test_log_message_contains_runtime(self) -> None:
|
|
55
|
+
@performance_timer
|
|
56
|
+
def sample_func() -> str:
|
|
57
|
+
return 'result'
|
|
58
|
+
|
|
59
|
+
with patch('django_spire.contrib.performance.decorators.log.warning') as mock_warning:
|
|
60
|
+
sample_func()
|
|
61
|
+
|
|
62
|
+
log_message = mock_warning.call_args[0][0]
|
|
63
|
+
|
|
64
|
+
assert 'seconds runtime' in log_message
|
|
65
|
+
|
|
66
|
+
@override_settings(DEBUG=True)
|
|
67
|
+
def test_log_message_contains_function_name(self) -> None:
|
|
68
|
+
@performance_timer
|
|
69
|
+
def sample_func() -> str:
|
|
70
|
+
return 'result'
|
|
71
|
+
|
|
72
|
+
with patch('django_spire.contrib.performance.decorators.log.warning') as mock_warning:
|
|
73
|
+
sample_func()
|
|
74
|
+
|
|
75
|
+
log_message = mock_warning.call_args[0][0]
|
|
76
|
+
|
|
77
|
+
assert 'sample_func' in log_message
|
|
78
|
+
|
|
79
|
+
@override_settings(DEBUG=True)
|
|
80
|
+
def test_passes_args_to_function(self) -> None:
|
|
81
|
+
@performance_timer
|
|
82
|
+
def sample_func(a: int, b: int) -> int:
|
|
83
|
+
return a + b
|
|
84
|
+
|
|
85
|
+
result = sample_func(2, 3)
|
|
86
|
+
|
|
87
|
+
assert result == 5
|
|
88
|
+
|
|
89
|
+
@override_settings(DEBUG=True)
|
|
90
|
+
def test_passes_kwargs_to_function(self) -> None:
|
|
91
|
+
@performance_timer
|
|
92
|
+
def sample_func(a: int, b: int = 10) -> int:
|
|
93
|
+
return a + b
|
|
94
|
+
|
|
95
|
+
result = sample_func(5, b=20)
|
|
96
|
+
|
|
97
|
+
assert result == 25
|
|
98
|
+
|
|
99
|
+
@override_settings(DEBUG=True)
|
|
100
|
+
def test_returns_none_when_function_returns_none(self) -> None:
|
|
101
|
+
@performance_timer
|
|
102
|
+
def sample_func() -> None:
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
result = sample_func()
|
|
106
|
+
|
|
107
|
+
assert result is None
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from django.db.models import QuerySet
|
|
4
7
|
|
|
5
8
|
|
|
6
9
|
def filter_by_lookup_map(
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
queryset: QuerySet,
|
|
11
|
+
lookup_map: dict,
|
|
12
|
+
data: dict,
|
|
13
|
+
extra_filters: dict | None = None
|
|
11
14
|
):
|
|
12
15
|
"""
|
|
13
16
|
Filters a given queryset based on a lookup map and provided data. Additional filters
|
|
@@ -23,7 +26,9 @@ def filter_by_lookup_map(
|
|
|
23
26
|
|
|
24
27
|
Returns:
|
|
25
28
|
QuerySet: The filtered queryset.
|
|
29
|
+
|
|
26
30
|
"""
|
|
31
|
+
|
|
27
32
|
if extra_filters is None:
|
|
28
33
|
extra_filters = {}
|
|
29
34
|
|
|
@@ -1,25 +1,27 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
|
|
4
5
|
from abc import abstractmethod
|
|
5
|
-
from typing import
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
6
7
|
|
|
7
|
-
from django.core.handlers.wsgi import WSGIRequest
|
|
8
8
|
from django.db.models import QuerySet
|
|
9
|
-
from django.forms import Form
|
|
10
9
|
from django_spire.contrib.form.utils import show_form_errors
|
|
11
10
|
from django_spire.contrib.queryset.enums import SessionFilterActionEnum
|
|
12
11
|
from django_spire.contrib.session.controller import SessionController
|
|
13
12
|
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from django.core.handlers.wsgi import WSGIRequest
|
|
15
|
+
from django.forms import Form
|
|
14
16
|
|
|
15
|
-
class SessionFilterQuerySetMixin(QuerySet):
|
|
16
17
|
|
|
18
|
+
class SessionFilterQuerySetMixin(QuerySet):
|
|
17
19
|
def process_session_filter(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
self,
|
|
21
|
+
request: WSGIRequest,
|
|
22
|
+
session_key: str,
|
|
23
|
+
form_class: type[Form],
|
|
24
|
+
is_from_body: bool = False
|
|
23
25
|
) -> QuerySet:
|
|
24
26
|
# Session keys must match to process new queryset data
|
|
25
27
|
|
|
@@ -41,10 +43,9 @@ class SessionFilterQuerySetMixin(QuerySet):
|
|
|
41
43
|
|
|
42
44
|
# Apply filters when the user submits the filter form
|
|
43
45
|
if (
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
action == SessionFilterActionEnum.FILTER
|
|
47
|
+
and session_key == data.get('session_filter_key')
|
|
46
48
|
):
|
|
47
|
-
|
|
48
49
|
# Update session data
|
|
49
50
|
for key, value in form.cleaned_data.items():
|
|
50
51
|
session.add_data(key, value)
|
|
@@ -56,9 +57,9 @@ class SessionFilterQuerySetMixin(QuerySet):
|
|
|
56
57
|
# When no new filter data is applied and session is NOT yet expired,
|
|
57
58
|
# return the original queryset
|
|
58
59
|
return self.bulk_filter(session.data)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
|
|
61
|
+
show_form_errors(request, form)
|
|
62
|
+
return self
|
|
62
63
|
|
|
63
64
|
@abstractmethod
|
|
64
65
|
def bulk_filter(self, filter_data: dict) -> QuerySet:
|
|
@@ -66,7 +67,6 @@ class SessionFilterQuerySetMixin(QuerySet):
|
|
|
66
67
|
|
|
67
68
|
|
|
68
69
|
class SearchQuerySetMixin(QuerySet):
|
|
69
|
-
|
|
70
70
|
@abstractmethod
|
|
71
71
|
def search(self, value: str | None) -> QuerySet:
|
|
72
72
|
pass
|
|
File without changes
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from unittest.mock import MagicMock
|
|
4
|
+
|
|
5
|
+
from django.contrib.sessions.middleware import SessionMiddleware
|
|
6
|
+
from django.db.models import QuerySet
|
|
7
|
+
from django.test import RequestFactory, TestCase
|
|
8
|
+
|
|
9
|
+
from django_spire.contrib.queryset.enums import SessionFilterActionEnum
|
|
10
|
+
from django_spire.contrib.queryset.filter_tools import filter_by_lookup_map
|
|
11
|
+
from django_spire.contrib.queryset.mixins import SearchQuerySetMixin, SessionFilterQuerySetMixin
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TestSessionFilterActionEnum(TestCase):
|
|
15
|
+
def test_clear_value(self) -> None:
|
|
16
|
+
assert SessionFilterActionEnum.CLEAR == 'Clear'
|
|
17
|
+
|
|
18
|
+
def test_filter_value(self) -> None:
|
|
19
|
+
assert SessionFilterActionEnum.FILTER == 'Filter'
|
|
20
|
+
|
|
21
|
+
def test_is_str_enum(self) -> None:
|
|
22
|
+
assert isinstance(SessionFilterActionEnum.CLEAR, str)
|
|
23
|
+
assert isinstance(SessionFilterActionEnum.FILTER, str)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TestFilterByLookupMap(TestCase):
|
|
27
|
+
def setUp(self) -> None:
|
|
28
|
+
self.queryset = MagicMock(spec=QuerySet)
|
|
29
|
+
self.queryset.filter.return_value = self.queryset
|
|
30
|
+
|
|
31
|
+
def test_applies_filter_from_lookup_map(self) -> None:
|
|
32
|
+
lookup_map = {'name': 'name__icontains'}
|
|
33
|
+
data = {'name': 'test'}
|
|
34
|
+
|
|
35
|
+
filter_by_lookup_map(self.queryset, lookup_map, data)
|
|
36
|
+
|
|
37
|
+
self.queryset.filter.assert_called_once_with(name__icontains='test')
|
|
38
|
+
|
|
39
|
+
def test_extra_filters_applied(self) -> None:
|
|
40
|
+
lookup_map = {'name': 'name__icontains'}
|
|
41
|
+
data = {'name': 'test'}
|
|
42
|
+
extra_filters = {'is_active': True}
|
|
43
|
+
|
|
44
|
+
filter_by_lookup_map(self.queryset, lookup_map, data, extra_filters)
|
|
45
|
+
|
|
46
|
+
self.queryset.filter.assert_called_once_with(
|
|
47
|
+
name__icontains='test',
|
|
48
|
+
is_active=True
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def test_extra_filters_defaults_to_empty_dict(self) -> None:
|
|
52
|
+
lookup_map = {}
|
|
53
|
+
data = {}
|
|
54
|
+
|
|
55
|
+
filter_by_lookup_map(self.queryset, lookup_map, data)
|
|
56
|
+
|
|
57
|
+
self.queryset.filter.assert_called_once_with()
|
|
58
|
+
|
|
59
|
+
def test_ignores_empty_string_values(self) -> None:
|
|
60
|
+
lookup_map = {'name': 'name__icontains', 'email': 'email__icontains'}
|
|
61
|
+
data = {'name': 'test', 'email': ''}
|
|
62
|
+
|
|
63
|
+
filter_by_lookup_map(self.queryset, lookup_map, data)
|
|
64
|
+
|
|
65
|
+
self.queryset.filter.assert_called_once_with(name__icontains='test')
|
|
66
|
+
|
|
67
|
+
def test_ignores_keys_not_in_lookup_map(self) -> None:
|
|
68
|
+
lookup_map = {'name': 'name__icontains'}
|
|
69
|
+
data = {'name': 'test', 'other': 'value'}
|
|
70
|
+
|
|
71
|
+
filter_by_lookup_map(self.queryset, lookup_map, data)
|
|
72
|
+
|
|
73
|
+
self.queryset.filter.assert_called_once_with(name__icontains='test')
|
|
74
|
+
|
|
75
|
+
def test_ignores_none_values(self) -> None:
|
|
76
|
+
lookup_map = {'name': 'name__icontains', 'email': 'email__icontains'}
|
|
77
|
+
data = {'name': 'test', 'email': None}
|
|
78
|
+
|
|
79
|
+
filter_by_lookup_map(self.queryset, lookup_map, data)
|
|
80
|
+
|
|
81
|
+
self.queryset.filter.assert_called_once_with(name__icontains='test')
|
|
82
|
+
|
|
83
|
+
def test_ignores_empty_list_values(self) -> None:
|
|
84
|
+
lookup_map = {'name': 'name__icontains', 'tags': 'tags__in'}
|
|
85
|
+
data = {'name': 'test', 'tags': []}
|
|
86
|
+
|
|
87
|
+
filter_by_lookup_map(self.queryset, lookup_map, data)
|
|
88
|
+
|
|
89
|
+
self.queryset.filter.assert_called_once_with(name__icontains='test')
|
|
90
|
+
|
|
91
|
+
def test_multiple_filters_applied(self) -> None:
|
|
92
|
+
lookup_map = {'name': 'name__icontains', 'status': 'status'}
|
|
93
|
+
data = {'name': 'test', 'status': 'active'}
|
|
94
|
+
|
|
95
|
+
filter_by_lookup_map(self.queryset, lookup_map, data)
|
|
96
|
+
|
|
97
|
+
self.queryset.filter.assert_called_once_with(
|
|
98
|
+
name__icontains='test',
|
|
99
|
+
status='active'
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def test_returns_filtered_queryset(self) -> None:
|
|
103
|
+
lookup_map = {'name': 'name__icontains'}
|
|
104
|
+
data = {'name': 'test'}
|
|
105
|
+
|
|
106
|
+
result = filter_by_lookup_map(self.queryset, lookup_map, data)
|
|
107
|
+
|
|
108
|
+
assert result == self.queryset
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class TestSessionFilterQuerySetMixin(TestCase):
|
|
112
|
+
def setUp(self) -> None:
|
|
113
|
+
self.factory = RequestFactory()
|
|
114
|
+
|
|
115
|
+
def _create_request_with_session(self, path: str = '/', data: dict | None = None):
|
|
116
|
+
request = self.factory.get(path, data or {})
|
|
117
|
+
middleware = SessionMiddleware(lambda req: None)
|
|
118
|
+
middleware.process_request(request)
|
|
119
|
+
request.session.save()
|
|
120
|
+
return request
|
|
121
|
+
|
|
122
|
+
def test_has_bulk_filter_abstract_method(self) -> None:
|
|
123
|
+
assert hasattr(SessionFilterQuerySetMixin, 'bulk_filter')
|
|
124
|
+
|
|
125
|
+
def test_has_process_session_filter_method(self) -> None:
|
|
126
|
+
assert hasattr(SessionFilterQuerySetMixin, 'process_session_filter')
|
|
127
|
+
|
|
128
|
+
def test_is_queryset_subclass(self) -> None:
|
|
129
|
+
assert issubclass(SessionFilterQuerySetMixin, QuerySet)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class TestSearchQuerySetMixin(TestCase):
|
|
133
|
+
def test_has_search_abstract_method(self) -> None:
|
|
134
|
+
assert hasattr(SearchQuerySetMixin, 'search')
|
|
135
|
+
|
|
136
|
+
def test_is_queryset_subclass(self) -> None:
|
|
137
|
+
assert issubclass(SearchQuerySetMixin, QuerySet)
|
|
@@ -1,24 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
2
5
|
|
|
3
6
|
from django_spire.contrib.seeding.field.cleaners import normalize_seeder_fields
|
|
4
7
|
from django_spire.contrib.seeding.field.enums import FieldSeederTypesEnum
|
|
5
8
|
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
6
12
|
|
|
7
13
|
class BaseFieldSeeder(ABC):
|
|
8
14
|
keyword: str = None
|
|
9
15
|
seed_keywords = FieldSeederTypesEnum._value2member_map_.keys()
|
|
10
16
|
|
|
11
17
|
def __init__(
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
):
|
|
18
|
+
self,
|
|
19
|
+
fields: dict | None = None,
|
|
20
|
+
default_to: str = 'llm'
|
|
21
|
+
) -> None:
|
|
16
22
|
|
|
17
23
|
self.fields = self._normalize_fields(fields or {})
|
|
18
24
|
self.default_to = default_to
|
|
19
25
|
|
|
20
26
|
|
|
21
|
-
def __init_subclass__(cls, **kwargs):
|
|
27
|
+
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
22
28
|
super().__init_subclass__(**kwargs)
|
|
23
29
|
|
|
24
30
|
if cls.keyword is None:
|
|
@@ -35,9 +41,9 @@ class BaseFieldSeeder(ABC):
|
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
@property
|
|
38
|
-
def seeder_fields(self):
|
|
44
|
+
def seeder_fields(self) -> dict:
|
|
39
45
|
return self.filter_fields(self.keyword)
|
|
40
46
|
|
|
41
47
|
@abstractmethod
|
|
42
|
-
def seed(self, model_seeder_cls, count: int):
|
|
48
|
+
def seed(self, model_seeder_cls: Any, count: int) -> list[dict]:
|
|
43
49
|
pass
|
|
@@ -1,11 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
1
5
|
from django_spire.contrib.seeding.field.base import BaseFieldSeeder
|
|
2
6
|
from django_spire.contrib.seeding.field.enums import FieldSeederTypesEnum
|
|
3
7
|
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
4
11
|
|
|
5
12
|
class CallableFieldSeeder(BaseFieldSeeder):
|
|
6
13
|
keyword = FieldSeederTypesEnum.CALLABLE
|
|
7
14
|
|
|
8
|
-
def seed(self, manager = None, count = 1) -> list[dict]:
|
|
15
|
+
def seed(self, manager: Any = None, count: int = 1) -> list[dict]:
|
|
9
16
|
return [
|
|
10
17
|
{field_name: func[1]() for field_name, func in self.seeder_fields.items()}
|
|
11
18
|
for _ in range(count)
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
from
|
|
1
|
+
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import copy
|
|
4
4
|
|
|
5
|
+
from django_spire.contrib.seeding.field.enums import FieldSeederTypesEnum
|
|
6
|
+
|
|
7
|
+
|
|
5
8
|
def normalize_seeder_fields(fields: dict) -> dict:
|
|
6
9
|
normalized = {}
|
|
7
10
|
|
|
@@ -21,14 +24,11 @@ def normalize_seeder_fields(fields: dict) -> dict:
|
|
|
21
24
|
else:
|
|
22
25
|
extra = (extra_value,)
|
|
23
26
|
|
|
24
|
-
normalized[k] = (v[0],
|
|
25
|
-
|
|
27
|
+
normalized[k] = (v[0], *v[1:2], *extra)
|
|
26
28
|
elif callable(v):
|
|
27
29
|
normalized[k] = ("callable", v)
|
|
28
|
-
|
|
29
30
|
elif isinstance(v, str) and v.lower() in FieldSeederTypesEnum._value2member_map_:
|
|
30
31
|
normalized[k] = (v.lower(),)
|
|
31
|
-
|
|
32
32
|
else:
|
|
33
33
|
normalized[k] = ("static", v)
|
|
34
34
|
|