django-spire 0.23.7__py3-none-any.whl → 0.23.9__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/management/commands/spire_startapp_pkg/template/app/apps.py.template +2 -0
- django_spire/core/management/commands/spire_startapp_pkg/template/app/urls/__init__.py.template +2 -0
- django_spire/core/management/commands/spire_startapp_pkg/template/app/urls/form_urls.py.template +2 -0
- django_spire/core/management/commands/spire_startapp_pkg/template/app/urls/page_urls.py.template +2 -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 +12 -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 +5 -3
- 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.9.dist-info}/METADATA +2 -2
- {django_spire-0.23.7.dist-info → django_spire-0.23.9.dist-info}/RECORD +534 -362
- {django_spire-0.23.7.dist-info → django_spire-0.23.9.dist-info}/licenses/LICENSE.md +1 -1
- 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.9.dist-info}/WHEEL +0 -0
- {django_spire-0.23.7.dist-info → django_spire-0.23.9.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from unittest.mock import patch
|
|
6
|
+
|
|
7
|
+
from django.contrib.auth.models import User
|
|
8
|
+
from django.core.exceptions import ValidationError
|
|
9
|
+
from django.test import TestCase
|
|
10
|
+
|
|
11
|
+
from django_spire.contrib.service import BaseDjangoModelService, ServiceError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class UserService(BaseDjangoModelService[User]):
|
|
15
|
+
obj: User
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TestServiceError(TestCase):
|
|
19
|
+
def test_can_be_raised(self) -> None:
|
|
20
|
+
with pytest.raises(ServiceError):
|
|
21
|
+
message = 'Test'
|
|
22
|
+
raise ServiceError(message)
|
|
23
|
+
|
|
24
|
+
def test_is_exception(self) -> None:
|
|
25
|
+
assert issubclass(ServiceError, Exception)
|
|
26
|
+
|
|
27
|
+
def test_message(self) -> None:
|
|
28
|
+
error = ServiceError('Test error message')
|
|
29
|
+
|
|
30
|
+
assert str(error) == 'Test error message'
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TestBaseDjangoModelService(TestCase):
|
|
34
|
+
def setUp(self) -> None:
|
|
35
|
+
self.user = User.objects.create_user(
|
|
36
|
+
username='testuser',
|
|
37
|
+
password='testpass' # noqa: S106
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def test_get_concrete_fields_returns_dict(self) -> None:
|
|
41
|
+
service = UserService(self.user)
|
|
42
|
+
|
|
43
|
+
result = service._get_concrete_fields()
|
|
44
|
+
|
|
45
|
+
assert isinstance(result, dict)
|
|
46
|
+
assert 'username' in result
|
|
47
|
+
assert 'email' in result
|
|
48
|
+
|
|
49
|
+
def test_get_touched_fields_returns_list(self) -> None:
|
|
50
|
+
service = UserService(self.user)
|
|
51
|
+
concrete_fields = service._get_concrete_fields()
|
|
52
|
+
|
|
53
|
+
result = service._get_touched_fields(concrete_fields, username='newuser')
|
|
54
|
+
|
|
55
|
+
assert isinstance(result, list)
|
|
56
|
+
|
|
57
|
+
def test_get_touched_fields_sets_attribute(self) -> None:
|
|
58
|
+
service = UserService(self.user)
|
|
59
|
+
concrete_fields = service._get_concrete_fields()
|
|
60
|
+
|
|
61
|
+
service._get_touched_fields(concrete_fields, first_name='NewName')
|
|
62
|
+
|
|
63
|
+
assert service.obj.first_name == 'NewName'
|
|
64
|
+
|
|
65
|
+
def test_get_touched_fields_logs_warning_for_invalid_field(self) -> None:
|
|
66
|
+
service = UserService(self.user)
|
|
67
|
+
concrete_fields = service._get_concrete_fields()
|
|
68
|
+
|
|
69
|
+
with patch('django_spire.contrib.service.django_model_service.log') as mock_log:
|
|
70
|
+
service._get_touched_fields(concrete_fields, invalid_field='value')
|
|
71
|
+
|
|
72
|
+
mock_log.warning.assert_called_once()
|
|
73
|
+
|
|
74
|
+
def test_get_touched_fields_skips_auto_created_fields(self) -> None:
|
|
75
|
+
service = UserService(self.user)
|
|
76
|
+
concrete_fields = service._get_concrete_fields()
|
|
77
|
+
|
|
78
|
+
result = service._get_touched_fields(concrete_fields, id=999)
|
|
79
|
+
|
|
80
|
+
assert 'id' not in result
|
|
81
|
+
|
|
82
|
+
def test_has_get_concrete_fields_method(self) -> None:
|
|
83
|
+
assert hasattr(BaseDjangoModelService, '_get_concrete_fields')
|
|
84
|
+
|
|
85
|
+
def test_has_get_touched_fields_method(self) -> None:
|
|
86
|
+
assert hasattr(BaseDjangoModelService, '_get_touched_fields')
|
|
87
|
+
|
|
88
|
+
def test_has_save_model_obj_method(self) -> None:
|
|
89
|
+
assert hasattr(BaseDjangoModelService, 'save_model_obj')
|
|
90
|
+
|
|
91
|
+
def test_has_validate_model_obj_method(self) -> None:
|
|
92
|
+
assert hasattr(BaseDjangoModelService, 'validate_model_obj')
|
|
93
|
+
|
|
94
|
+
def test_save_model_obj_raises_error_without_field_data(self) -> None:
|
|
95
|
+
service = UserService(self.user)
|
|
96
|
+
|
|
97
|
+
with pytest.raises(ServiceError, match='Field data is required'):
|
|
98
|
+
service.save_model_obj()
|
|
99
|
+
|
|
100
|
+
def test_save_model_obj_returns_tuple(self) -> None:
|
|
101
|
+
service = UserService(self.user)
|
|
102
|
+
|
|
103
|
+
result = service.save_model_obj(first_name='Updated')
|
|
104
|
+
|
|
105
|
+
assert isinstance(result, tuple)
|
|
106
|
+
assert len(result) == 2
|
|
107
|
+
|
|
108
|
+
def test_save_model_obj_returns_model_and_bool(self) -> None:
|
|
109
|
+
service = UserService(self.user)
|
|
110
|
+
|
|
111
|
+
obj, created = service.save_model_obj(first_name='Updated')
|
|
112
|
+
|
|
113
|
+
assert isinstance(obj, User)
|
|
114
|
+
assert isinstance(created, bool)
|
|
115
|
+
|
|
116
|
+
def test_save_model_obj_updates_existing_model(self) -> None:
|
|
117
|
+
service = UserService(self.user)
|
|
118
|
+
|
|
119
|
+
obj, created = service.save_model_obj(first_name='UpdatedName')
|
|
120
|
+
|
|
121
|
+
assert created is False
|
|
122
|
+
assert obj.first_name == 'UpdatedName'
|
|
123
|
+
|
|
124
|
+
def test_save_model_obj_creates_new_model(self) -> None:
|
|
125
|
+
new_user = User(username='newuser', email='new@test.com')
|
|
126
|
+
service = UserService(new_user)
|
|
127
|
+
|
|
128
|
+
obj, created = service.save_model_obj(username='newuser', email='new@test.com')
|
|
129
|
+
|
|
130
|
+
assert created is True
|
|
131
|
+
assert obj.pk is not None
|
|
132
|
+
|
|
133
|
+
def test_save_model_obj_logs_warning_when_no_changes(self) -> None:
|
|
134
|
+
service = UserService(self.user)
|
|
135
|
+
|
|
136
|
+
with patch('django_spire.contrib.service.django_model_service.log') as mock_log:
|
|
137
|
+
service.save_model_obj(id=self.user.id)
|
|
138
|
+
|
|
139
|
+
mock_log.warning.assert_called_once()
|
|
140
|
+
|
|
141
|
+
def test_validate_model_obj_returns_touched_fields(self) -> None:
|
|
142
|
+
service = UserService(self.user)
|
|
143
|
+
|
|
144
|
+
result = service.validate_model_obj(first_name='Valid')
|
|
145
|
+
|
|
146
|
+
assert isinstance(result, list)
|
|
147
|
+
assert 'first_name' in result
|
|
148
|
+
|
|
149
|
+
def test_validate_model_obj_raises_validation_error(self) -> None:
|
|
150
|
+
service = UserService(self.user)
|
|
151
|
+
|
|
152
|
+
with pytest.raises(ValidationError):
|
|
153
|
+
service.validate_model_obj(email='invalid-email')
|
|
@@ -1,34 +1,40 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
|
|
4
5
|
from datetime import datetime, timedelta
|
|
5
|
-
from typing import
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
6
7
|
|
|
7
8
|
from django.core.serializers.json import DjangoJSONEncoder
|
|
8
|
-
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from django.http import HttpRequest
|
|
9
14
|
|
|
10
15
|
|
|
11
16
|
class SessionController:
|
|
12
17
|
"""
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
This class provides an interface for storing, retrieving, and managing session data
|
|
19
|
+
with automatic expiration. It handles session data under a specific key and supports
|
|
20
|
+
timeout-based cleanup of expired sessions.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
request (HttpRequest): The Django request object containing the session.
|
|
24
|
+
session_key (str): Unique key under which the session data will be stored.
|
|
25
|
+
seconds_till_expiry (int, optional): Number of seconds until session expiry.
|
|
26
|
+
Defaults to 300 seconds (5 minutes).
|
|
27
|
+
|
|
22
28
|
"""
|
|
23
29
|
|
|
24
30
|
_TIMEOUT_KEY = '_timeout_datestamp'
|
|
25
31
|
|
|
26
32
|
def __init__(
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
):
|
|
33
|
+
self,
|
|
34
|
+
request: HttpRequest,
|
|
35
|
+
session_key: str,
|
|
36
|
+
seconds_till_expiry: int = 60 * 5
|
|
37
|
+
) -> None:
|
|
32
38
|
self.request = request
|
|
33
39
|
self.session_key = session_key
|
|
34
40
|
self.seconds_till_expiry = seconds_till_expiry
|
|
@@ -41,28 +47,44 @@ class SessionController:
|
|
|
41
47
|
def __getitem__(self, key: str) -> Any:
|
|
42
48
|
return self.data[key]
|
|
43
49
|
|
|
44
|
-
def __setitem__(self, key: str, value: Any):
|
|
50
|
+
def __setitem__(self, key: str, value: Any) -> None:
|
|
45
51
|
self.add_data(key, value)
|
|
46
52
|
|
|
47
|
-
def
|
|
53
|
+
def _clean(self) -> None:
|
|
54
|
+
if self._TIMEOUT_KEY in self.data and self.is_expired:
|
|
55
|
+
self.request.session.pop(self.session_key)
|
|
56
|
+
self._set_modified()
|
|
57
|
+
|
|
58
|
+
def _set_modified(self) -> None:
|
|
59
|
+
self.request.session.modified = True
|
|
60
|
+
|
|
61
|
+
def _set_timeout_datestamp(self) -> None:
|
|
62
|
+
timeout_datetime = datetime.now() + timedelta(seconds=self.seconds_till_expiry)
|
|
63
|
+
self.data[self._TIMEOUT_KEY] = timeout_datetime.timestamp()
|
|
64
|
+
|
|
65
|
+
def add_data(self, key: str, data: Any) -> None:
|
|
48
66
|
self._session[key] = data
|
|
49
67
|
self._set_timeout_datestamp()
|
|
50
68
|
self._set_modified()
|
|
51
69
|
|
|
52
70
|
@property
|
|
53
|
-
def data(self):
|
|
71
|
+
def data(self) -> dict[str, Any]:
|
|
54
72
|
return self._session
|
|
55
73
|
|
|
74
|
+
@property
|
|
75
|
+
def has_data(self) -> bool:
|
|
76
|
+
return bool(self.data)
|
|
77
|
+
|
|
56
78
|
@property
|
|
57
79
|
def is_expired(self) -> bool:
|
|
58
80
|
current_timestamp = datetime.now().timestamp()
|
|
59
81
|
return self.timeout_datestamp < current_timestamp
|
|
60
82
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
83
|
+
def purge(self) -> None:
|
|
84
|
+
self.request.session.pop(self.session_key)
|
|
85
|
+
self._set_modified()
|
|
64
86
|
|
|
65
|
-
def remove_data(self, key: str):
|
|
87
|
+
def remove_data(self, key: str) -> None:
|
|
66
88
|
self.data.pop(key)
|
|
67
89
|
self._set_modified()
|
|
68
90
|
|
|
@@ -70,25 +92,9 @@ class SessionController:
|
|
|
70
92
|
if self._TIMEOUT_KEY in self.data and len(self.data.keys()) == 1:
|
|
71
93
|
self.data.pop(self._TIMEOUT_KEY)
|
|
72
94
|
|
|
73
|
-
def purge(self):
|
|
74
|
-
self.request.session.pop(self.session_key)
|
|
75
|
-
self._set_modified()
|
|
76
|
-
|
|
77
|
-
def _clean(self) -> None:
|
|
78
|
-
if self._TIMEOUT_KEY in self.data and self.is_expired:
|
|
79
|
-
self.request.session.pop(self.session_key)
|
|
80
|
-
self._set_modified()
|
|
81
|
-
|
|
82
|
-
def _set_modified(self):
|
|
83
|
-
self.request.session.modified = True
|
|
84
|
-
|
|
85
95
|
@property
|
|
86
|
-
def
|
|
87
|
-
return
|
|
88
|
-
|
|
89
|
-
def _set_timeout_datestamp(self):
|
|
90
|
-
timeout_datetime = datetime.now() + timedelta(seconds=self.seconds_till_expiry)
|
|
91
|
-
self.data[self._TIMEOUT_KEY] = timeout_datetime.timestamp()
|
|
96
|
+
def timeout_datestamp(self) -> float:
|
|
97
|
+
return self.data.get(self._TIMEOUT_KEY, 0)
|
|
92
98
|
|
|
93
|
-
def to_json(self):
|
|
99
|
+
def to_json(self) -> str:
|
|
94
100
|
return json.dumps(self.data, cls=DjangoJSONEncoder)
|
|
@@ -1,15 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
1
5
|
from django import template
|
|
2
6
|
from django.utils.html import escapejs
|
|
3
7
|
from django.utils.safestring import mark_safe
|
|
4
8
|
|
|
5
9
|
from django_spire.contrib.session.controller import SessionController
|
|
6
10
|
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from django.template import Context
|
|
13
|
+
from django.utils.safestring import SafeString
|
|
14
|
+
|
|
15
|
+
|
|
7
16
|
register = template.Library()
|
|
8
17
|
|
|
9
18
|
|
|
10
19
|
@register.simple_tag(takes_context=True)
|
|
11
|
-
def session_controller_to_json(context, key):
|
|
12
|
-
request = context.get(
|
|
20
|
+
def session_controller_to_json(context: Context, key: str) -> SafeString:
|
|
21
|
+
request = context.get('request')
|
|
13
22
|
controller = SessionController(request, key)
|
|
14
23
|
data = controller.to_json()
|
|
15
24
|
return mark_safe(escapejs(data))
|
|
@@ -1,18 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from datetime import datetime
|
|
4
|
+
from unittest.mock import MagicMock
|
|
2
5
|
|
|
3
|
-
from django.test import TestCase, RequestFactory
|
|
4
6
|
from django.contrib.sessions.middleware import SessionMiddleware
|
|
5
7
|
from django.contrib.sessions.models import Session
|
|
8
|
+
from django.test import RequestFactory, TestCase
|
|
6
9
|
|
|
7
10
|
from django_spire.contrib.session.controller import SessionController
|
|
11
|
+
from django_spire.contrib.session.templatetags.session_tags import session_controller_to_json
|
|
8
12
|
|
|
9
13
|
|
|
10
|
-
class
|
|
11
|
-
def setUp(self):
|
|
14
|
+
class TestSessionController(TestCase):
|
|
15
|
+
def setUp(self) -> None:
|
|
12
16
|
self.factory = RequestFactory()
|
|
13
17
|
self.request = self.factory.get('/')
|
|
14
18
|
|
|
15
|
-
# Attach real session
|
|
16
19
|
middleware = SessionMiddleware(lambda req: None)
|
|
17
20
|
middleware.process_request(self.request)
|
|
18
21
|
|
|
@@ -21,85 +24,146 @@ class BaseSessionTestCase(TestCase):
|
|
|
21
24
|
session_key='shopping_cart'
|
|
22
25
|
)
|
|
23
26
|
|
|
24
|
-
def
|
|
27
|
+
def test_add_data(self) -> None:
|
|
25
28
|
key = 'currency'
|
|
26
29
|
value = 'CAD'
|
|
27
30
|
|
|
28
31
|
self.shopping_cart_session.add_data(key, value)
|
|
29
|
-
self.assertIn(key, self.shopping_cart_session.data)
|
|
30
|
-
self.assertEqual(self.shopping_cart_session.data[key], value)
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
key
|
|
34
|
-
value = 'CAD'
|
|
33
|
+
assert key in self.shopping_cart_session.data
|
|
34
|
+
assert self.shopping_cart_session.data[key] == value
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
self.assertEqual(self.shopping_cart_session.data[key], value)
|
|
36
|
+
def test_data_property_returns_dict(self) -> None:
|
|
37
|
+
assert isinstance(self.shopping_cart_session.data, dict)
|
|
39
38
|
|
|
40
|
-
def
|
|
39
|
+
def test_getitem(self) -> None:
|
|
41
40
|
self.shopping_cart_session.add_data('currency', 'CAD')
|
|
42
|
-
self.shopping_cart_session.add_data('discount', None)
|
|
43
|
-
self.shopping_cart_session.remove_data('currency')
|
|
44
41
|
|
|
45
|
-
self.
|
|
46
|
-
self.assertIn('discount', self.shopping_cart_session.data)
|
|
42
|
+
assert self.shopping_cart_session['currency'] == 'CAD'
|
|
47
43
|
|
|
48
|
-
def
|
|
44
|
+
def test_has_data_false_when_empty(self) -> None:
|
|
45
|
+
assert self.shopping_cart_session.has_data is False
|
|
46
|
+
|
|
47
|
+
def test_has_data_true_when_data_exists(self) -> None:
|
|
49
48
|
self.shopping_cart_session.add_data('currency', 'CAD')
|
|
50
|
-
self.assertTrue(self.request.session.modified)
|
|
51
49
|
|
|
52
|
-
|
|
53
|
-
self.assertFalse(self.shopping_cart_session.has_data)
|
|
50
|
+
assert self.shopping_cart_session.has_data is True
|
|
54
51
|
|
|
52
|
+
def test_has_data_false_after_remove(self) -> None:
|
|
55
53
|
self.shopping_cart_session.add_data('currency', 'CAD')
|
|
56
|
-
self.
|
|
54
|
+
self.shopping_cart_session.remove_data('currency')
|
|
57
55
|
|
|
56
|
+
assert self.shopping_cart_session.has_data is False
|
|
58
57
|
|
|
59
|
-
|
|
60
|
-
self.
|
|
58
|
+
def test_is_expired_false_when_not_expired(self) -> None:
|
|
59
|
+
self.shopping_cart_session.add_data('currency', 'CAD')
|
|
61
60
|
|
|
62
|
-
|
|
61
|
+
assert self.shopping_cart_session.is_expired is False
|
|
62
|
+
|
|
63
|
+
def test_is_expired_true_when_expired(self) -> None:
|
|
63
64
|
self.shopping_cart_session.add_data('currency', 'CAD')
|
|
64
|
-
self.shopping_cart_session.
|
|
65
|
+
self.shopping_cart_session.seconds_till_expiry = -5
|
|
66
|
+
self.shopping_cart_session._set_timeout_datestamp()
|
|
65
67
|
|
|
66
|
-
self.
|
|
68
|
+
assert self.shopping_cart_session.is_expired is True
|
|
67
69
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
# with self.assertRaises(ValueError):
|
|
72
|
-
# self.shopping_cart_session.to_json()
|
|
70
|
+
def test_purge_removes_session_data(self) -> None:
|
|
71
|
+
self.shopping_cart_session.add_data('currency', 'CAD')
|
|
72
|
+
self.shopping_cart_session.purge()
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
self.shopping_cart_session._set_timeout_datestamp()
|
|
76
|
-
self.assertIn('_timeout_datestamp', self.shopping_cart_session.data)
|
|
77
|
-
self.assertTrue(datetime.now().timestamp() < self.shopping_cart_session['_timeout_datestamp'])
|
|
74
|
+
assert 'shopping_cart' not in self.request.session
|
|
78
75
|
|
|
79
|
-
def
|
|
76
|
+
def test_remove_data(self) -> None:
|
|
80
77
|
self.shopping_cart_session.add_data('currency', 'CAD')
|
|
81
|
-
self.
|
|
78
|
+
self.shopping_cart_session.add_data('discount', None)
|
|
79
|
+
self.shopping_cart_session.remove_data('currency')
|
|
82
80
|
|
|
83
|
-
self.shopping_cart_session.
|
|
84
|
-
self.shopping_cart_session.
|
|
85
|
-
self.assertTrue(self.shopping_cart_session.is_expired)
|
|
81
|
+
assert 'currency' not in self.shopping_cart_session.data
|
|
82
|
+
assert 'discount' in self.shopping_cart_session.data
|
|
86
83
|
|
|
87
|
-
def test_session_data_added_to_database(self):
|
|
88
|
-
|
|
89
|
-
self.shopping_cart_session.add_data('favorites', ['cool_shirt', 'fun_hat'] )
|
|
84
|
+
def test_session_data_added_to_database(self) -> None:
|
|
85
|
+
self.shopping_cart_session.add_data('favorites', ['cool_shirt', 'fun_hat'])
|
|
90
86
|
self.request.session.save()
|
|
91
87
|
|
|
92
|
-
# Get session from DB using session_key
|
|
93
88
|
session_key = self.request.session.session_key
|
|
94
89
|
session_in_db = Session.objects.get(session_key=session_key)
|
|
95
|
-
|
|
96
|
-
# Decode the session data
|
|
97
90
|
session_data = session_in_db.get_decoded()
|
|
98
91
|
|
|
99
|
-
self.
|
|
100
|
-
|
|
101
|
-
self.
|
|
102
|
-
|
|
103
|
-
|
|
92
|
+
assert self.shopping_cart_session.session_key in session_data
|
|
93
|
+
assert 'favorites' in session_data[self.shopping_cart_session.session_key]
|
|
94
|
+
assert session_data[self.shopping_cart_session.session_key]['favorites'] == ['cool_shirt', 'fun_hat']
|
|
95
|
+
|
|
96
|
+
def test_set_modified(self) -> None:
|
|
97
|
+
self.shopping_cart_session.add_data('currency', 'CAD')
|
|
98
|
+
|
|
99
|
+
assert self.request.session.modified is True
|
|
100
|
+
|
|
101
|
+
def test_set_timeout_datestamp(self) -> None:
|
|
102
|
+
self.shopping_cart_session._set_timeout_datestamp()
|
|
103
|
+
|
|
104
|
+
assert '_timeout_datestamp' in self.shopping_cart_session.data
|
|
105
|
+
assert datetime.now().timestamp() < self.shopping_cart_session['_timeout_datestamp']
|
|
106
|
+
|
|
107
|
+
def test_setitem(self) -> None:
|
|
108
|
+
key = 'currency'
|
|
109
|
+
value = 'CAD'
|
|
110
|
+
|
|
111
|
+
self.shopping_cart_session[key] = value
|
|
112
|
+
|
|
113
|
+
assert key in self.shopping_cart_session.data
|
|
114
|
+
assert self.shopping_cart_session.data[key] == value
|
|
115
|
+
|
|
116
|
+
def test_timeout_datestamp_returns_float(self) -> None:
|
|
117
|
+
self.shopping_cart_session._set_timeout_datestamp()
|
|
118
|
+
|
|
119
|
+
assert isinstance(self.shopping_cart_session.timeout_datestamp, float)
|
|
120
|
+
|
|
121
|
+
def test_timeout_datestamp_returns_zero_when_not_set(self) -> None:
|
|
122
|
+
controller = SessionController(
|
|
123
|
+
request=self.request,
|
|
124
|
+
session_key='new_session'
|
|
104
125
|
)
|
|
105
126
|
|
|
127
|
+
assert controller.timeout_datestamp == 0
|
|
128
|
+
|
|
129
|
+
def test_to_json_returns_string(self) -> None:
|
|
130
|
+
self.shopping_cart_session.add_data('currency', 'CAD')
|
|
131
|
+
self.shopping_cart_session.add_data('discount', None)
|
|
132
|
+
|
|
133
|
+
assert isinstance(self.shopping_cart_session.to_json(), str)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class TestSessionControllerToJson(TestCase):
|
|
137
|
+
def setUp(self) -> None:
|
|
138
|
+
self.factory = RequestFactory()
|
|
139
|
+
|
|
140
|
+
def _create_context_with_session(self) -> MagicMock:
|
|
141
|
+
request = self.factory.get('/')
|
|
142
|
+
middleware = SessionMiddleware(lambda req: None)
|
|
143
|
+
middleware.process_request(request)
|
|
144
|
+
|
|
145
|
+
context = MagicMock()
|
|
146
|
+
context.get.return_value = request
|
|
147
|
+
|
|
148
|
+
return context
|
|
149
|
+
|
|
150
|
+
def test_returns_escaped_json(self) -> None:
|
|
151
|
+
context = self._create_context_with_session()
|
|
152
|
+
|
|
153
|
+
result = session_controller_to_json(context, 'test_key')
|
|
154
|
+
|
|
155
|
+
assert isinstance(result, str)
|
|
156
|
+
|
|
157
|
+
def test_calls_context_get_with_request(self) -> None:
|
|
158
|
+
context = self._create_context_with_session()
|
|
159
|
+
|
|
160
|
+
session_controller_to_json(context, 'test_key')
|
|
161
|
+
|
|
162
|
+
context.get.assert_called_once_with('request')
|
|
163
|
+
|
|
164
|
+
def test_returns_safe_string(self) -> None:
|
|
165
|
+
context = self._create_context_with_session()
|
|
166
|
+
|
|
167
|
+
result = session_controller_to_json(context, 'test_key')
|
|
168
|
+
|
|
169
|
+
assert hasattr(result, '__html__')
|
|
File without changes
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from django.test import TestCase
|
|
4
|
+
|
|
5
|
+
from django_spire.contrib.utils import truncate_string
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestTruncateString(TestCase):
|
|
9
|
+
def test_returns_original_when_equal_to_length(self) -> None:
|
|
10
|
+
result = truncate_string('hello', 5)
|
|
11
|
+
|
|
12
|
+
assert result == 'hello'
|
|
13
|
+
|
|
14
|
+
def test_returns_original_when_shorter_than_length(self) -> None:
|
|
15
|
+
result = truncate_string('hi', 10)
|
|
16
|
+
|
|
17
|
+
assert result == 'hi'
|
|
18
|
+
|
|
19
|
+
def test_truncates_when_longer_than_length(self) -> None:
|
|
20
|
+
result = truncate_string('hello world', 8)
|
|
21
|
+
|
|
22
|
+
assert result == 'hello...'
|
|
23
|
+
|
|
24
|
+
def test_truncates_with_ellipsis(self) -> None:
|
|
25
|
+
result = truncate_string('this is a long string', 10)
|
|
26
|
+
|
|
27
|
+
assert result.endswith('...')
|
|
28
|
+
|
|
29
|
+
def test_truncated_length_equals_specified_length(self) -> None:
|
|
30
|
+
result = truncate_string('this is a long string', 10)
|
|
31
|
+
|
|
32
|
+
assert len(result) == 10
|
|
33
|
+
|
|
34
|
+
def test_empty_string(self) -> None:
|
|
35
|
+
result = truncate_string('', 5)
|
|
36
|
+
|
|
37
|
+
assert result == ''
|
django_spire/contrib/utils.py
CHANGED