django-spire 0.23.6__py3-none-any.whl → 0.23.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/progress/__init__.py +1 -3
- django_spire/contrib/progress/static/django_spire/js/contrib/progress/progress.js +38 -82
- 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.6.dist-info → django_spire-0.23.8.dist-info}/METADATA +1 -1
- {django_spire-0.23.6.dist-info → django_spire-0.23.8.dist-info}/RECORD +532 -361
- django_spire/contrib/options/tests/test_unit.py +0 -148
- django_spire/contrib/progress/views.py +0 -64
- 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.6.dist-info → django_spire-0.23.8.dist-info}/WHEEL +0 -0
- {django_spire-0.23.6.dist-info → django_spire-0.23.8.dist-info}/licenses/LICENSE.md +0 -0
- {django_spire-0.23.6.dist-info → django_spire-0.23.8.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from django.test import TestCase
|
|
4
|
+
|
|
5
|
+
from django_spire.contrib.seeding.intelligence.intel import SeedingIntel, SourceIntel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestSeedingIntel(TestCase):
|
|
9
|
+
def test_is_iterable(self) -> None:
|
|
10
|
+
intel = SeedingIntel(items=[{'name': 'test1'}, {'name': 'test2'}])
|
|
11
|
+
|
|
12
|
+
result = list(intel)
|
|
13
|
+
|
|
14
|
+
assert result == [{'name': 'test1'}, {'name': 'test2'}]
|
|
15
|
+
|
|
16
|
+
def test_items_attribute(self) -> None:
|
|
17
|
+
items = [{'name': 'test'}]
|
|
18
|
+
intel = SeedingIntel(items=items)
|
|
19
|
+
|
|
20
|
+
assert intel.items == items
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TestSourceIntel(TestCase):
|
|
24
|
+
def test_file_name_attribute(self) -> None:
|
|
25
|
+
intel = SourceIntel(file_name='test.py', python_source_code='print("hello")')
|
|
26
|
+
|
|
27
|
+
assert intel.file_name == 'test.py'
|
|
28
|
+
|
|
29
|
+
def test_python_source_code_attribute(self) -> None:
|
|
30
|
+
intel = SourceIntel(file_name='test.py', python_source_code='print("hello")')
|
|
31
|
+
|
|
32
|
+
assert intel.python_source_code == 'print("hello")'
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from unittest.mock import MagicMock
|
|
4
|
+
|
|
5
|
+
from django.test import TestCase
|
|
6
|
+
|
|
7
|
+
from django_spire.contrib.seeding.field.override import FieldOverride
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestFieldOverride(TestCase):
|
|
11
|
+
def setUp(self) -> None:
|
|
12
|
+
self.seeder_class = MagicMock()
|
|
13
|
+
self.seeder_class.seed.return_value = [{'name': 'test'}]
|
|
14
|
+
|
|
15
|
+
def test_filter_returns_self(self) -> None:
|
|
16
|
+
override = FieldOverride(self.seeder_class)
|
|
17
|
+
|
|
18
|
+
result = override.filter(name='test')
|
|
19
|
+
|
|
20
|
+
assert result is override
|
|
21
|
+
|
|
22
|
+
def test_filter_updates_overrides(self) -> None:
|
|
23
|
+
override = FieldOverride(self.seeder_class)
|
|
24
|
+
|
|
25
|
+
override.filter(name='test', status='active')
|
|
26
|
+
|
|
27
|
+
assert override.overrides == {'name': 'test', 'status': 'active'}
|
|
28
|
+
|
|
29
|
+
def test_getattr_delegates_to_seeder_class(self) -> None:
|
|
30
|
+
self.seeder_class.some_attribute = 'value'
|
|
31
|
+
override = FieldOverride(self.seeder_class)
|
|
32
|
+
|
|
33
|
+
result = override.some_attribute
|
|
34
|
+
|
|
35
|
+
assert result == 'value'
|
|
36
|
+
|
|
37
|
+
def test_init_sets_seeder_class(self) -> None:
|
|
38
|
+
override = FieldOverride(self.seeder_class)
|
|
39
|
+
|
|
40
|
+
assert override.seeder_class is self.seeder_class
|
|
41
|
+
|
|
42
|
+
def test_init_sets_empty_overrides(self) -> None:
|
|
43
|
+
override = FieldOverride(self.seeder_class)
|
|
44
|
+
|
|
45
|
+
assert override.overrides == {}
|
|
46
|
+
|
|
47
|
+
def test_seed_calls_seeder_class_seed(self) -> None:
|
|
48
|
+
override = FieldOverride(self.seeder_class)
|
|
49
|
+
override.filter(name='override_value')
|
|
50
|
+
|
|
51
|
+
override.seed(count=2)
|
|
52
|
+
|
|
53
|
+
self.seeder_class.seed.assert_called_once_with(
|
|
54
|
+
count=2,
|
|
55
|
+
fields={'name': 'override_value'}
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def test_seed_returns_seeder_class_result(self) -> None:
|
|
59
|
+
override = FieldOverride(self.seeder_class)
|
|
60
|
+
|
|
61
|
+
result = override.seed(count=1)
|
|
62
|
+
|
|
63
|
+
assert result == [{'name': 'test'}]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
from django_spire.contrib.service.exceptions import
|
|
1
|
+
from django_spire.contrib.service.exceptions import ServiceError
|
|
2
2
|
from django_spire.contrib.service.django_model_service import BaseDjangoModelService
|
|
3
3
|
|
|
4
4
|
__all__ = [
|
|
5
5
|
'BaseDjangoModelService',
|
|
6
|
-
'
|
|
6
|
+
'ServiceError',
|
|
7
7
|
]
|
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
+
|
|
4
5
|
from abc import ABC
|
|
5
|
-
from typing import
|
|
6
|
+
from typing import Generic, TypeVar
|
|
6
7
|
|
|
7
8
|
from django.db import transaction
|
|
8
9
|
from django.db.models import Model
|
|
9
10
|
|
|
10
11
|
from django_spire.contrib.constructor.django_model_constructor import BaseDjangoModelConstructor
|
|
11
|
-
from django_spire.contrib.service.exceptions import
|
|
12
|
+
from django_spire.contrib.service.exceptions import ServiceError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
log = logging.getLogger(__name__)
|
|
12
16
|
|
|
13
|
-
|
|
17
|
+
TypeDjangoModel_co = TypeVar('TypeDjangoModel_co', bound=Model, covariant=True)
|
|
14
18
|
|
|
15
19
|
|
|
16
20
|
class BaseDjangoModelService(
|
|
17
|
-
BaseDjangoModelConstructor[
|
|
21
|
+
BaseDjangoModelConstructor[TypeDjangoModel_co],
|
|
18
22
|
ABC,
|
|
19
|
-
Generic[
|
|
23
|
+
Generic[TypeDjangoModel_co]
|
|
20
24
|
):
|
|
21
25
|
def _get_concrete_fields(self) -> dict:
|
|
22
26
|
return {
|
|
@@ -33,7 +37,7 @@ class BaseDjangoModelService(
|
|
|
33
37
|
|
|
34
38
|
for field, value in field_data.items():
|
|
35
39
|
if field not in allowed:
|
|
36
|
-
|
|
40
|
+
log.warning(f'Field {field!r} is not valid for {self.obj.__class__.__name__}')
|
|
37
41
|
continue
|
|
38
42
|
|
|
39
43
|
model_field = concrete_fields.get(field.removesuffix("_id"), None)
|
|
@@ -51,12 +55,8 @@ class BaseDjangoModelService(
|
|
|
51
55
|
concrete_fields = self._get_concrete_fields()
|
|
52
56
|
touched_fields = self._get_touched_fields(concrete_fields, **field_data)
|
|
53
57
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
exclude=[field for field in concrete_fields if field not in touched_fields]
|
|
57
|
-
)
|
|
58
|
-
except:
|
|
59
|
-
raise
|
|
58
|
+
exclude = [field for field in concrete_fields if field not in touched_fields]
|
|
59
|
+
self.obj.full_clean(exclude=exclude)
|
|
60
60
|
|
|
61
61
|
return touched_fields
|
|
62
62
|
|
|
@@ -65,7 +65,8 @@ class BaseDjangoModelService(
|
|
|
65
65
|
new_model_obj_was_created = False
|
|
66
66
|
|
|
67
67
|
if not field_data:
|
|
68
|
-
|
|
68
|
+
message = f'Field data is required to save on {self.obj.__class__.__name__}'
|
|
69
|
+
raise ServiceError(message)
|
|
69
70
|
|
|
70
71
|
touched_fields = self.validate_model_obj(**field_data)
|
|
71
72
|
|
|
@@ -77,7 +78,7 @@ class BaseDjangoModelService(
|
|
|
77
78
|
self.obj.save(update_fields=touched_fields)
|
|
78
79
|
|
|
79
80
|
else:
|
|
80
|
-
|
|
81
|
-
|
|
81
|
+
message = f'{self.obj.__class__.__name__} is not a new object or there was no touched fields to update.'
|
|
82
|
+
log.warning(message)
|
|
82
83
|
|
|
83
84
|
return self.obj, new_model_obj_was_created
|
|
File without changes
|
|
@@ -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))
|