django-spire 0.23.7__py3-none-any.whl → 0.23.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_spire/ai/admin.py +11 -11
- django_spire/ai/chat/apps.py +1 -0
- django_spire/ai/chat/templates/django_spire/ai/chat/widget/dialog_widget.html +1 -1
- django_spire/ai/chat/tests/factories.py +15 -0
- django_spire/ai/chat/tests/test_controller.py +45 -0
- django_spire/ai/chat/tests/test_models.py +301 -0
- django_spire/ai/chat/tests/test_prompts.py +48 -0
- django_spire/ai/chat/tests/test_responses.py +208 -0
- django_spire/ai/chat/tests/test_router/test_base_chat_router.py +66 -6
- django_spire/ai/chat/tests/test_router/test_chat_workflow.py +73 -3
- django_spire/ai/chat/tests/test_router/test_integration.py +86 -6
- django_spire/ai/chat/tests/test_router/test_intent_decoder.py +93 -1
- django_spire/ai/chat/tests/test_router/test_message_intel.py +60 -1
- django_spire/ai/chat/tests/test_router/test_spire_chat_router.py +110 -0
- django_spire/ai/chat/tests/test_urls/test_json_urls.py +202 -1
- django_spire/ai/context/tests/__init__.py +0 -0
- django_spire/ai/context/tests/test_context.py +188 -0
- django_spire/ai/decorators.py +7 -6
- django_spire/ai/prompt/tests/test_bots.py +100 -10
- django_spire/ai/prompt/tests/test_prompt_intel.py +83 -0
- django_spire/ai/prompt/tests/test_prompt_tuning.py +126 -0
- django_spire/ai/sms/decorators.py +8 -2
- django_spire/ai/sms/tests/test_sms.py +240 -16
- django_spire/ai/sms/tests/test_sms_intel.py +42 -0
- django_spire/ai/sms/tests/test_webhook.py +155 -7
- django_spire/ai/sms/views.py +23 -24
- django_spire/ai/tests/test_ai.py +131 -7
- django_spire/auth/apps.py +4 -2
- django_spire/auth/controller/controller.py +36 -23
- django_spire/auth/controller/exceptions.py +9 -0
- django_spire/auth/group/admin.py +1 -0
- django_spire/auth/group/apps.py +2 -0
- django_spire/auth/group/factories.py +17 -8
- django_spire/auth/group/forms.py +7 -0
- django_spire/auth/group/tests/test_factories.py +146 -0
- django_spire/auth/group/tests/test_forms.py +282 -0
- django_spire/auth/group/tests/test_models.py +192 -0
- django_spire/auth/group/tests/test_querysets.py +98 -0
- django_spire/auth/group/tests/test_utils.py +341 -0
- django_spire/auth/group/tests/test_views.py +377 -0
- django_spire/auth/group/urls/__init__.py +3 -1
- django_spire/auth/group/urls/form_urls.py +2 -0
- django_spire/auth/group/urls/json_urls.py +3 -0
- django_spire/auth/group/urls/page_urls.py +2 -0
- django_spire/auth/group/utils.py +6 -2
- django_spire/auth/group/views/form_views.py +6 -3
- django_spire/auth/group/views/json_views.py +6 -2
- django_spire/auth/mfa/admin.py +2 -0
- django_spire/auth/mfa/apps.py +2 -0
- django_spire/auth/mfa/forms.py +1 -0
- django_spire/auth/mfa/querysets.py +9 -2
- django_spire/auth/mfa/tests/test_models.py +233 -0
- django_spire/auth/mfa/tests/test_utils.py +106 -0
- django_spire/auth/mfa/urls/__init__.py +2 -0
- django_spire/auth/mfa/urls/page_urls.py +2 -0
- django_spire/auth/mfa/urls/redirect_urls.py +2 -0
- django_spire/auth/mfa/views/page_views.py +2 -1
- django_spire/auth/permissions/consts.py +2 -2
- django_spire/auth/permissions/decorators.py +8 -8
- django_spire/auth/permissions/permissions.py +28 -35
- django_spire/auth/permissions/tests/test_decorators.py +333 -0
- django_spire/auth/permissions/tests/test_permissions.py +337 -0
- django_spire/auth/permissions/tests/test_tools.py +305 -0
- django_spire/auth/permissions/tools.py +21 -15
- django_spire/auth/seeding/seed.py +3 -0
- django_spire/auth/seeding/seeder.py +2 -0
- django_spire/auth/tests/test_controller.py +323 -0
- django_spire/auth/tests/test_url_endpoints.py +9 -9
- django_spire/auth/tests/test_views.py +406 -0
- django_spire/auth/urls/admin_urls.py +2 -0
- django_spire/auth/urls/redirect_urls.py +2 -0
- django_spire/auth/user/apps.py +2 -0
- django_spire/auth/user/forms.py +9 -0
- django_spire/auth/user/models.py +1 -1
- django_spire/auth/user/services/services.py +1 -0
- django_spire/auth/user/tests/factories.py +14 -13
- django_spire/auth/user/tests/test_factories.py +166 -2
- django_spire/auth/user/tests/test_forms.py +573 -0
- django_spire/auth/user/tests/test_models.py +257 -0
- django_spire/auth/user/tests/test_services.py +200 -0
- django_spire/auth/user/tests/test_tools.py +153 -0
- django_spire/auth/user/tests/test_user_factories.py +139 -0
- django_spire/auth/user/tests/test_views.py +363 -0
- django_spire/auth/user/tools.py +7 -1
- django_spire/auth/user/urls/form_urls.py +3 -0
- django_spire/auth/user/urls/page_urls.py +3 -0
- django_spire/auth/user/views/form_views.py +19 -10
- django_spire/auth/user/views/page_views.py +8 -2
- django_spire/auth/views/redirect_views.py +14 -9
- django_spire/comment/admin.py +2 -0
- django_spire/comment/apps.py +2 -0
- django_spire/comment/templatetags/comment_tags.py +1 -0
- django_spire/comment/tests/test_forms.py +27 -0
- django_spire/comment/tests/test_models.py +215 -0
- django_spire/comment/tests/test_querysets.py +101 -0
- django_spire/comment/tests/test_utils.py +90 -0
- django_spire/comment/urls.py +2 -0
- django_spire/comment/utils.py +22 -13
- django_spire/comment/views.py +1 -1
- django_spire/conf.py +8 -6
- django_spire/consts.py +1 -1
- django_spire/contrib/breadcrumb/apps.py +2 -0
- django_spire/contrib/breadcrumb/breadcrumbs.py +18 -18
- django_spire/contrib/breadcrumb/tests/test_breadcrumbs.py +198 -0
- django_spire/contrib/constructor/__init__.py +3 -3
- django_spire/contrib/constructor/constructor.py +15 -15
- django_spire/contrib/constructor/django_model_constructor.py +5 -4
- django_spire/contrib/constructor/exceptions.py +5 -3
- django_spire/contrib/constructor/tests/__init__.py +0 -0
- django_spire/contrib/constructor/tests/test_constructor.py +193 -0
- django_spire/contrib/form/tests/__init__.py +0 -0
- django_spire/contrib/form/tests/test_forms.py +203 -0
- django_spire/contrib/generic_views/modal_views.py +2 -1
- django_spire/contrib/generic_views/portal_views.py +20 -19
- django_spire/contrib/generic_views/tests/__init__.py +0 -0
- django_spire/contrib/generic_views/tests/test_views.py +459 -0
- django_spire/contrib/help/apps.py +2 -0
- django_spire/contrib/help/templatetags/help.py +1 -0
- django_spire/contrib/help/tests/__init__.py +0 -0
- django_spire/contrib/help/tests/test_templatetags.py +100 -0
- django_spire/contrib/options/mixins.py +6 -5
- django_spire/contrib/options/tests/factories.py +5 -1
- django_spire/contrib/options/tests/test_options.py +234 -0
- django_spire/contrib/ordering/exceptions.py +7 -3
- django_spire/contrib/ordering/mixins.py +2 -0
- django_spire/contrib/ordering/querysets.py +3 -1
- django_spire/contrib/ordering/services/processor_service.py +8 -4
- django_spire/contrib/ordering/services/service.py +1 -2
- django_spire/contrib/ordering/tests/__init__.py +0 -0
- django_spire/contrib/ordering/tests/test_ordering.py +165 -0
- django_spire/contrib/ordering/validators.py +6 -6
- django_spire/contrib/pagination/templatetags/pagination_tags.py +12 -5
- django_spire/contrib/pagination/tests/__init__.py +0 -0
- django_spire/contrib/pagination/tests/test_pagination.py +179 -0
- django_spire/contrib/performance/decorators.py +16 -6
- django_spire/contrib/performance/tests/__init__.py +0 -0
- django_spire/contrib/performance/tests/test_performance.py +107 -0
- django_spire/contrib/queryset/enums.py +3 -1
- django_spire/contrib/queryset/filter_tools.py +10 -5
- django_spire/contrib/queryset/mixins.py +16 -16
- django_spire/contrib/queryset/tests/__init__.py +0 -0
- django_spire/contrib/queryset/tests/test_queryset.py +137 -0
- django_spire/contrib/seeding/field/base.py +13 -7
- django_spire/contrib/seeding/field/callable.py +8 -1
- django_spire/contrib/seeding/field/cleaners.py +5 -5
- django_spire/contrib/seeding/field/custom.py +20 -10
- django_spire/contrib/seeding/field/django/seeder.py +8 -6
- django_spire/contrib/seeding/field/enums.py +7 -5
- django_spire/contrib/seeding/field/override.py +16 -6
- django_spire/contrib/seeding/field/static.py +9 -2
- django_spire/contrib/seeding/field/tests/test_base.py +18 -14
- django_spire/contrib/seeding/field/tests/test_callable.py +13 -9
- django_spire/contrib/seeding/field/tests/test_cleaners.py +51 -38
- django_spire/contrib/seeding/field/tests/test_static.py +13 -9
- django_spire/contrib/seeding/intelligence/bots/seeder_generator_bot.py +2 -0
- django_spire/contrib/seeding/intelligence/intel.py +5 -1
- django_spire/contrib/seeding/intelligence/prompts/factory.py +6 -1
- django_spire/contrib/seeding/intelligence/prompts/foreign_key_selection_prompt.py +6 -1
- django_spire/contrib/seeding/intelligence/prompts/generate_django_model_seeder_prompts.py +2 -0
- django_spire/contrib/seeding/intelligence/prompts/generic_relationship_selection_prompt.py +7 -1
- django_spire/contrib/seeding/intelligence/prompts/hierarchical_selection_prompt.py +6 -2
- django_spire/contrib/seeding/intelligence/prompts/model_field_choices_prompt.py +8 -2
- django_spire/contrib/seeding/intelligence/prompts/negation_prompt.py +2 -0
- django_spire/contrib/seeding/intelligence/prompts/objective_prompt.py +6 -1
- django_spire/contrib/seeding/management/commands/seeding.py +9 -3
- django_spire/contrib/seeding/management/example.py +2 -0
- django_spire/contrib/seeding/model/base.py +16 -7
- django_spire/contrib/seeding/model/config.py +31 -15
- django_spire/contrib/seeding/model/django/config.py +13 -13
- django_spire/contrib/seeding/model/django/seeder.py +4 -4
- django_spire/contrib/seeding/model/django/tests/test_seeder.py +34 -23
- django_spire/contrib/seeding/model/enums.py +2 -0
- django_spire/contrib/seeding/tests/test_config.py +71 -0
- django_spire/contrib/seeding/tests/test_custom.py +35 -0
- django_spire/contrib/seeding/tests/test_enums.py +40 -0
- django_spire/contrib/seeding/tests/test_intel.py +32 -0
- django_spire/contrib/seeding/tests/test_override.py +63 -0
- django_spire/contrib/service/__init__.py +2 -2
- django_spire/contrib/service/django_model_service.py +16 -15
- django_spire/contrib/service/exceptions.py +5 -3
- django_spire/contrib/service/tests/__init__.py +0 -0
- django_spire/contrib/service/tests/test_service.py +153 -0
- django_spire/contrib/session/apps.py +2 -0
- django_spire/contrib/session/controller.py +48 -42
- django_spire/contrib/session/templatetags/session_tags.py +11 -2
- django_spire/contrib/session/tests/test_session_controller.py +117 -53
- django_spire/contrib/tests/__init__.py +0 -0
- django_spire/contrib/tests/test_utils.py +37 -0
- django_spire/contrib/utils.py +4 -1
- django_spire/core/apps.py +2 -0
- django_spire/core/converters/tests/test_to_data.py +353 -0
- django_spire/core/converters/tests/test_to_enums.py +61 -41
- django_spire/core/converters/tests/test_to_pydantic.py +138 -109
- django_spire/core/converters/to_data.py +29 -10
- django_spire/core/converters/to_enums.py +4 -2
- django_spire/core/converters/to_pydantic.py +22 -22
- django_spire/core/decorators.py +19 -6
- django_spire/core/forms/widgets.py +4 -0
- django_spire/core/maps.py +3 -1
- django_spire/core/middleware/maintenance.py +3 -3
- django_spire/core/middleware.py +8 -6
- django_spire/core/redirect/__init__.py +5 -0
- django_spire/core/redirect/generic_redirect.py +1 -2
- django_spire/core/redirect/tests/__init__.py +0 -0
- django_spire/core/redirect/tests/test_generic_redirect.py +34 -0
- django_spire/core/{tests/tests_redirect.py → redirect/tests/test_safe_redirect.py} +55 -81
- django_spire/core/shortcuts.py +3 -3
- django_spire/core/static/django_spire/css/app-layout.css +1 -1
- django_spire/core/static/django_spire/css/app-navigation.css +3 -3
- django_spire/core/static/django_spire/css/bootstrap-override.css +4 -0
- django_spire/core/tag/admin.py +12 -0
- django_spire/core/tag/intelligence/tag_set_bot.py +2 -0
- django_spire/core/tag/mixins.py +2 -0
- django_spire/core/tag/models.py +2 -0
- django_spire/core/tag/querysets.py +2 -0
- django_spire/core/tag/service/tag_service.py +6 -3
- django_spire/core/tag/tests/test_intelligence.py +9 -9
- django_spire/core/tag/tests/test_tags.py +44 -54
- django_spire/core/tag/tests/test_tools.py +191 -0
- django_spire/core/tag/tools.py +3 -0
- django_spire/core/templates/django_spire/card/card.html +5 -2
- django_spire/core/templates/django_spire/card/title_card.html +9 -4
- django_spire/core/templates/django_spire/infinite_scroll/base.html +1 -0
- django_spire/core/templates/django_spire/navigation/side_navigation.html +19 -24
- django_spire/core/templates/django_spire/page/full_page.html +46 -16
- django_spire/core/templates/django_spire/table/base.html +4 -2
- django_spire/core/templatetags/json.py +6 -2
- django_spire/core/templatetags/message.py +13 -8
- django_spire/core/templatetags/string_formating.py +8 -5
- django_spire/core/templatetags/tests/__init__.py +0 -0
- django_spire/core/templatetags/tests/test_templatetags.py +427 -0
- django_spire/core/templatetags/variable_types.py +17 -9
- django_spire/core/tests/test_cases.py +1 -1
- django_spire/core/tests/test_conf.py +43 -0
- django_spire/core/tests/test_consts.py +28 -0
- django_spire/core/tests/test_context_processors.py +93 -0
- django_spire/core/tests/test_decorators.py +95 -0
- django_spire/core/tests/test_django_spire_utils.py +56 -0
- django_spire/core/tests/test_exceptions.py +37 -0
- django_spire/core/tests/test_models.py +54 -0
- django_spire/core/tests/test_settings.py +45 -0
- django_spire/core/tests/test_shortcuts.py +74 -0
- django_spire/core/tests/test_urls.py +16 -0
- django_spire/core/tests/test_utils.py +58 -0
- django_spire/core/urls.py +4 -1
- django_spire/core/utils.py +12 -8
- django_spire/exceptions.py +16 -1
- django_spire/file/admin.py +4 -2
- django_spire/file/apps.py +8 -10
- django_spire/file/fields.py +7 -7
- django_spire/file/forms.py +1 -1
- django_spire/file/interfaces.py +15 -15
- django_spire/file/mixins.py +1 -4
- django_spire/file/models.py +3 -5
- django_spire/file/tests/factories.py +59 -0
- django_spire/file/tests/test_admin.py +69 -0
- django_spire/file/tests/test_apps.py +24 -0
- django_spire/file/tests/test_fields.py +114 -0
- django_spire/file/tests/test_forms.py +20 -0
- django_spire/file/tests/test_interfaces.py +183 -0
- django_spire/file/tests/test_models.py +82 -0
- django_spire/file/tests/test_querysets.py +102 -0
- django_spire/file/tests/test_utils.py +32 -0
- django_spire/file/tests/test_views.py +145 -0
- django_spire/file/tests/test_widgets.py +82 -0
- django_spire/file/tools.py +8 -2
- django_spire/file/views.py +7 -3
- django_spire/file/widgets.py +12 -12
- django_spire/help_desk/admin.py +15 -0
- django_spire/help_desk/apps.py +2 -0
- django_spire/help_desk/auth/controller.py +2 -0
- django_spire/help_desk/choices.py +2 -0
- django_spire/help_desk/enums.py +2 -0
- django_spire/help_desk/exceptions.py +31 -3
- django_spire/help_desk/forms.py +2 -0
- django_spire/help_desk/models.py +2 -0
- django_spire/help_desk/querysets.py +4 -1
- django_spire/help_desk/services/notification_service.py +26 -27
- django_spire/help_desk/services/service.py +2 -3
- django_spire/help_desk/tests/factories.py +8 -3
- django_spire/help_desk/tests/test_admin.py +41 -0
- django_spire/help_desk/tests/test_apps.py +41 -0
- django_spire/help_desk/tests/test_choices.py +50 -0
- django_spire/help_desk/tests/test_controller.py +87 -0
- django_spire/help_desk/tests/test_enums.py +18 -0
- django_spire/help_desk/tests/test_exceptions.py +37 -0
- django_spire/help_desk/tests/test_forms.py +89 -0
- django_spire/help_desk/tests/test_models.py +59 -0
- django_spire/help_desk/tests/test_querysets.py +38 -0
- django_spire/help_desk/tests/test_services/test_notification_service.py +15 -8
- django_spire/help_desk/tests/test_services/test_service.py +92 -0
- django_spire/help_desk/tests/test_urls/test_form_urls.py +6 -6
- django_spire/help_desk/tests/test_urls/test_page_urls.py +8 -9
- django_spire/help_desk/tests/test_views/test_form_views.py +46 -19
- django_spire/help_desk/tests/test_views/test_page_views.py +32 -9
- django_spire/help_desk/urls/__init__.py +4 -1
- django_spire/help_desk/urls/form_urls.py +3 -0
- django_spire/help_desk/urls/page_urls.py +3 -0
- django_spire/help_desk/views/form_views.py +13 -5
- django_spire/help_desk/views/page_views.py +11 -3
- django_spire/history/activity/admin.py +2 -0
- django_spire/history/activity/apps.py +3 -1
- django_spire/history/activity/mixins.py +13 -7
- django_spire/history/activity/models.py +6 -5
- django_spire/history/activity/querysets.py +2 -0
- django_spire/history/activity/tests/__init__.py +0 -0
- django_spire/history/activity/tests/test_activity.py +176 -0
- django_spire/history/admin.py +9 -2
- django_spire/history/choices.py +3 -0
- django_spire/history/models.py +5 -5
- django_spire/history/tests/test_admin.py +93 -0
- django_spire/history/tests/test_history.py +101 -0
- django_spire/history/tests/test_mixins.py +84 -0
- django_spire/history/viewed/admin.py +3 -1
- django_spire/history/viewed/apps.py +3 -1
- django_spire/history/viewed/models.py +2 -0
- django_spire/history/viewed/tests/__init__.py +0 -0
- django_spire/history/viewed/tests/test_viewed.py +46 -0
- django_spire/knowledge/auth/tests/__init__.py +0 -0
- django_spire/knowledge/auth/tests/test_controller.py +116 -0
- django_spire/knowledge/collection/admin.py +5 -1
- django_spire/knowledge/collection/models.py +3 -1
- django_spire/knowledge/collection/seeding/seed.py +1 -0
- django_spire/knowledge/collection/services/factory_service.py +10 -11
- django_spire/knowledge/collection/services/ordering_service.py +1 -2
- django_spire/knowledge/collection/services/service.py +5 -10
- django_spire/knowledge/collection/services/tag_service.py +5 -2
- django_spire/knowledge/collection/tests/factories.py +28 -1
- django_spire/knowledge/collection/tests/test_models.py +48 -0
- django_spire/knowledge/collection/tests/test_querysets.py +93 -0
- django_spire/knowledge/collection/tests/test_services/test_factory_service.py +100 -0
- django_spire/knowledge/collection/tests/test_services/test_services.py +160 -0
- django_spire/knowledge/collection/tests/test_urls/test_form_urls.py +21 -3
- django_spire/knowledge/collection/tests/test_urls/test_json_urls.py +39 -1
- django_spire/knowledge/collection/tests/test_urls/test_page_urls.py +12 -4
- django_spire/knowledge/collection/urls/__init__.py +3 -0
- django_spire/knowledge/collection/urls/form_urls.py +2 -0
- django_spire/knowledge/collection/urls/json_urls.py +2 -0
- django_spire/knowledge/collection/urls/page_urls.py +2 -0
- django_spire/knowledge/collection/views/form_views.py +4 -4
- django_spire/knowledge/collection/views/json_views.py +5 -1
- django_spire/knowledge/collection/views/page_views.py +5 -2
- django_spire/knowledge/entry/admin.py +7 -1
- django_spire/knowledge/entry/forms.py +2 -0
- django_spire/knowledge/entry/models.py +2 -0
- django_spire/knowledge/entry/seeding/seed.py +3 -0
- django_spire/knowledge/entry/services/automation_service.py +5 -4
- django_spire/knowledge/entry/services/factory_service.py +7 -5
- django_spire/knowledge/entry/services/service.py +4 -7
- django_spire/knowledge/entry/services/tag_service.py +0 -1
- django_spire/knowledge/entry/services/tool_service.py +1 -0
- django_spire/knowledge/entry/services/transformation_services.py +1 -5
- django_spire/knowledge/entry/tests/factories.py +1 -2
- django_spire/knowledge/entry/tests/test_factory_service.py +20 -0
- django_spire/knowledge/entry/tests/test_models.py +41 -0
- django_spire/knowledge/entry/tests/test_querysets.py +71 -0
- django_spire/knowledge/entry/tests/test_services.py +94 -0
- django_spire/knowledge/entry/tests/test_urls/test_form_urls.py +9 -14
- django_spire/knowledge/entry/tests/test_urls/test_json_urls.py +48 -5
- django_spire/knowledge/entry/tests/test_urls/test_page_urls.py +6 -8
- django_spire/knowledge/entry/tests/test_urls/test_template_urls.py +40 -0
- django_spire/knowledge/entry/urls/form_urls.py +2 -0
- django_spire/knowledge/entry/urls/json_urls.py +2 -0
- django_spire/knowledge/entry/urls/page_urls.py +2 -0
- django_spire/knowledge/entry/urls/template_urls.py +2 -0
- django_spire/knowledge/entry/version/block/choices.py +2 -0
- django_spire/knowledge/entry/version/block/data/data.py +1 -0
- django_spire/knowledge/entry/version/block/data/list/data.py +8 -13
- django_spire/knowledge/entry/version/block/data/list/maps.py +3 -0
- django_spire/knowledge/entry/version/block/data/list/meta.py +1 -2
- django_spire/knowledge/entry/version/block/data/list/tests/__init__.py +0 -0
- django_spire/knowledge/entry/version/block/data/list/tests/test_maps.py +32 -0
- django_spire/knowledge/entry/version/block/data/list/tests/test_meta.py +58 -0
- django_spire/knowledge/entry/version/block/data/maps.py +3 -6
- django_spire/knowledge/entry/version/block/models.py +7 -5
- django_spire/knowledge/entry/version/block/seeding/constants.py +5 -4
- django_spire/knowledge/entry/version/block/services/service.py +2 -3
- django_spire/knowledge/entry/version/block/tests/factories.py +4 -10
- django_spire/knowledge/entry/version/block/tests/test_choices.py +56 -0
- django_spire/knowledge/entry/version/block/tests/test_data.py +90 -0
- django_spire/knowledge/entry/version/block/tests/test_maps.py +37 -0
- django_spire/knowledge/entry/version/block/tests/test_models.py +55 -0
- django_spire/knowledge/entry/version/block/tests/test_querysets.py +35 -0
- django_spire/knowledge/entry/version/block/tests/test_services.py +65 -0
- django_spire/knowledge/entry/version/choices.py +2 -0
- django_spire/knowledge/entry/version/converters/converter.py +1 -1
- django_spire/knowledge/entry/version/converters/docx_converter.py +4 -7
- django_spire/knowledge/entry/version/converters/markdown_converter.py +20 -20
- django_spire/knowledge/entry/version/maps.py +4 -5
- django_spire/knowledge/entry/version/querysets.py +1 -1
- django_spire/knowledge/entry/version/seeding/seeder.py +1 -2
- django_spire/knowledge/entry/version/services/processor_service.py +5 -4
- django_spire/knowledge/entry/version/services/service.py +1 -2
- django_spire/knowledge/entry/version/tests/factories.py +2 -2
- django_spire/knowledge/entry/version/tests/test_choices.py +18 -0
- django_spire/knowledge/entry/version/tests/test_converters/test_docx_converter.py +56 -8
- django_spire/knowledge/entry/version/tests/test_converters/test_markdown_converter.py +78 -0
- django_spire/knowledge/entry/version/tests/test_maps.py +58 -0
- django_spire/knowledge/entry/version/tests/test_models.py +23 -0
- django_spire/knowledge/entry/version/tests/test_querysets.py +26 -0
- django_spire/knowledge/entry/version/tests/test_services.py +62 -0
- django_spire/knowledge/entry/version/tests/test_urls/test_json_urls.py +27 -8
- django_spire/knowledge/entry/version/tests/test_urls/test_page_urls.py +15 -8
- django_spire/knowledge/entry/version/tests/test_urls/test_redirect_urls.py +38 -0
- django_spire/knowledge/entry/version/urls/__init__.py +3 -0
- django_spire/knowledge/entry/version/urls/json_urls.py +2 -1
- django_spire/knowledge/entry/version/urls/page_urls.py +2 -0
- django_spire/knowledge/entry/version/urls/redirect_urls.py +2 -0
- django_spire/knowledge/entry/version/views/json_views.py +5 -1
- django_spire/knowledge/entry/version/views/page_views.py +10 -3
- django_spire/knowledge/entry/version/views/redirect_views.py +5 -1
- django_spire/knowledge/entry/views/form_views.py +16 -8
- django_spire/knowledge/entry/views/json_views.py +3 -1
- django_spire/knowledge/entry/views/page_views.py +8 -2
- django_spire/knowledge/entry/views/template_views.py +7 -1
- django_spire/knowledge/exceptions.py +2 -1
- django_spire/knowledge/intelligence/intel/answer_intel.py +2 -1
- django_spire/knowledge/intelligence/intel/entry_intel.py +0 -1
- django_spire/knowledge/intelligence/workflows/knowledge_workflow.py +4 -5
- django_spire/knowledge/models.py +1 -2
- django_spire/knowledge/tests/__init__.py +0 -0
- django_spire/knowledge/tests/test_templatetags.py +40 -0
- django_spire/knowledge/tests/test_urls/__init__.py +0 -0
- django_spire/knowledge/tests/test_urls/test_page_urls.py +24 -0
- django_spire/knowledge/urls/__init__.py +2 -0
- django_spire/knowledge/urls/page_urls.py +2 -0
- django_spire/knowledge/views/page_views.py +8 -3
- django_spire/notification/admin.py +3 -1
- django_spire/notification/app/admin.py +2 -0
- django_spire/notification/app/apps.py +3 -1
- django_spire/notification/app/exceptions.py +9 -2
- django_spire/notification/app/models.py +8 -4
- django_spire/notification/app/processor.py +22 -26
- django_spire/notification/app/querysets.py +2 -0
- django_spire/notification/app/tests/__init__.py +0 -0
- django_spire/notification/app/tests/factories.py +34 -0
- django_spire/notification/app/tests/test_apps.py +24 -0
- django_spire/notification/app/tests/test_models.py +72 -0
- django_spire/notification/app/tests/test_processor.py +111 -0
- django_spire/notification/app/tests/test_querysets.py +90 -0
- django_spire/notification/app/tests/test_views/__init__.py +0 -0
- django_spire/notification/app/tests/test_views/test_json_views.py +48 -0
- django_spire/notification/app/tests/test_views/test_page_views.py +19 -0
- django_spire/notification/app/urls/__init__.py +3 -1
- django_spire/notification/app/urls/json_urls.py +6 -4
- django_spire/notification/app/urls/page_urls.py +4 -3
- django_spire/notification/app/urls/template_urls.py +4 -2
- django_spire/notification/apps.py +4 -1
- django_spire/notification/email/admin.py +5 -1
- django_spire/notification/email/apps.py +3 -1
- django_spire/notification/email/exceptions.py +4 -2
- django_spire/notification/email/helper.py +5 -3
- django_spire/notification/email/models.py +4 -0
- django_spire/notification/email/processor.py +19 -15
- django_spire/notification/email/querysets.py +3 -0
- django_spire/notification/email/tests/__init__.py +0 -0
- django_spire/notification/email/tests/factories.py +35 -0
- django_spire/notification/email/tests/test_apps.py +24 -0
- django_spire/notification/email/tests/test_models.py +52 -0
- django_spire/notification/email/tests/test_processor.py +92 -0
- django_spire/notification/email/tests/test_querysets.py +43 -0
- django_spire/notification/exceptions.py +17 -2
- django_spire/notification/managers.py +7 -1
- django_spire/notification/maps.py +4 -1
- django_spire/notification/mixins.py +2 -0
- django_spire/notification/models.py +3 -1
- django_spire/notification/processors/notification.py +12 -5
- django_spire/notification/processors/processor.py +2 -0
- django_spire/notification/processors/tests/__init__.py +0 -0
- django_spire/notification/processors/tests/test_notification.py +106 -0
- django_spire/notification/push/admin.py +10 -1
- django_spire/notification/push/apps.py +3 -1
- django_spire/notification/push/models.py +2 -3
- django_spire/notification/push/tests/__init__.py +0 -0
- django_spire/notification/push/tests/test_apps.py +24 -0
- django_spire/notification/push/tests/test_models.py +28 -0
- django_spire/notification/querysets.py +7 -1
- django_spire/notification/sms/admin.py +2 -0
- django_spire/notification/sms/apps.py +4 -1
- django_spire/notification/sms/automations.py +2 -0
- django_spire/notification/sms/choices.py +2 -0
- django_spire/notification/sms/exceptions.py +19 -5
- django_spire/notification/sms/helper.py +33 -23
- django_spire/notification/sms/models.py +5 -1
- django_spire/notification/sms/processor.py +20 -20
- django_spire/notification/sms/querysets.py +2 -0
- django_spire/notification/sms/tests/factories.py +33 -0
- django_spire/notification/sms/tests/test_apps.py +24 -0
- django_spire/notification/sms/tests/test_automation.py +38 -0
- django_spire/notification/sms/tests/test_choices.py +15 -0
- django_spire/notification/sms/tests/test_consts.py +17 -0
- django_spire/notification/sms/tests/test_exceptions.py +27 -0
- django_spire/notification/sms/tests/test_helper.py +50 -0
- django_spire/notification/sms/tests/test_models.py +81 -0
- django_spire/notification/sms/tests/test_processor.py +107 -0
- django_spire/notification/sms/tests/test_tools.py +25 -11
- django_spire/notification/sms/tools.py +16 -5
- django_spire/notification/sms/urls/__init__.py +3 -1
- django_spire/notification/sms/urls/media_urls.py +2 -0
- django_spire/notification/sms/views/media_views.py +14 -4
- django_spire/notification/tests/__init__.py +0 -0
- django_spire/notification/tests/factories.py +26 -0
- django_spire/notification/tests/test_admin.py +55 -0
- django_spire/notification/tests/test_apps.py +30 -0
- django_spire/notification/tests/test_automation.py +18 -0
- django_spire/notification/tests/test_choices.py +59 -0
- django_spire/notification/tests/test_exceptions.py +58 -0
- django_spire/notification/tests/test_managers.py +100 -0
- django_spire/notification/tests/test_maps.py +31 -0
- django_spire/notification/tests/test_models.py +76 -0
- django_spire/notification/tests/test_querysets.py +184 -0
- django_spire/notification/tests/test_utils.py +23 -0
- django_spire/notification/urls.py +3 -1
- django_spire/notification/utils.py +3 -1
- django_spire/settings.py +3 -0
- django_spire/theme/tests/test_context_processor.py +15 -13
- django_spire/theme/tests/test_enums.py +2 -2
- django_spire/theme/tests/test_filesystem.py +2 -5
- django_spire/theme/tests/test_integration.py +12 -12
- django_spire/theme/tests/test_model.py +40 -38
- django_spire/theme/tests/test_views/test_json_views.py +33 -33
- django_spire/theme/urls/json_urls.py +3 -0
- django_spire/theme/urls/page_urls.py +3 -0
- django_spire/urls.py +19 -15
- django_spire/utils.py +13 -4
- {django_spire-0.23.7.dist-info → django_spire-0.23.8.dist-info}/METADATA +1 -1
- {django_spire-0.23.7.dist-info → django_spire-0.23.8.dist-info}/RECORD +530 -358
- django_spire/contrib/options/tests/test_unit.py +0 -148
- django_spire/contrib/seeding/tests/test_seeding.py +0 -25
- django_spire/core/tests/test_templatetags.py +0 -117
- django_spire/core/tests/tests_shortcuts.py +0 -73
- django_spire/history/activity/tests.py +0 -3
- django_spire/history/activity/views.py +0 -3
- django_spire/knowledge/collection/tests/test_services/test_transformation_service.py +0 -71
- django_spire/notification/app/tests.py +0 -3
- {django_spire-0.23.7.dist-info → django_spire-0.23.8.dist-info}/WHEEL +0 -0
- {django_spire-0.23.7.dist-info → django_spire-0.23.8.dist-info}/licenses/LICENSE.md +0 -0
- {django_spire-0.23.7.dist-info → django_spire-0.23.8.dist-info}/top_level.txt +0 -0
django_spire/file/interfaces.py
CHANGED
|
@@ -5,16 +5,16 @@ from dataclasses import dataclass
|
|
|
5
5
|
from typing import TYPE_CHECKING
|
|
6
6
|
|
|
7
7
|
from django.conf import settings
|
|
8
|
-
from django.core.files.base import ContentFile
|
|
9
|
-
|
|
10
8
|
from django.contrib.contenttypes.models import ContentType
|
|
11
9
|
from django.core.exceptions import ObjectDoesNotExist
|
|
10
|
+
from django.core.files.base import ContentFile
|
|
12
11
|
|
|
13
12
|
from django_spire.file.models import File
|
|
14
13
|
from django_spire.file.utils import random_64_char_token
|
|
15
14
|
|
|
16
15
|
if TYPE_CHECKING:
|
|
17
16
|
from django.core.files.uploadedfile import InMemoryUploadedFile
|
|
17
|
+
|
|
18
18
|
from django_spire.file.mixins import FileModelMixin
|
|
19
19
|
|
|
20
20
|
|
|
@@ -26,7 +26,7 @@ class FileFormatter:
|
|
|
26
26
|
type: str = ...
|
|
27
27
|
app_name: str = 'Uncategorized'
|
|
28
28
|
|
|
29
|
-
def __post_init__(self):
|
|
29
|
+
def __post_init__(self) -> None:
|
|
30
30
|
self.name = self._format_name()
|
|
31
31
|
self.type = self._parse_type()
|
|
32
32
|
|
|
@@ -60,6 +60,10 @@ class FileFormatter:
|
|
|
60
60
|
|
|
61
61
|
return file_obj
|
|
62
62
|
|
|
63
|
+
def save_file(self, file_obj: File) -> None:
|
|
64
|
+
file_path = self.location + '.' + self.type
|
|
65
|
+
file_obj.file.save(file_path, ContentFile(self.file.read()), save=False)
|
|
66
|
+
|
|
63
67
|
def size_verbose(self) -> str:
|
|
64
68
|
if self.file.size < 512000:
|
|
65
69
|
value = round(self.file.size / 1000, 2)
|
|
@@ -76,10 +80,6 @@ class FileFormatter:
|
|
|
76
80
|
|
|
77
81
|
return str(value) + ext
|
|
78
82
|
|
|
79
|
-
def save_file(self, file_obj: File):
|
|
80
|
-
file_path = self.location + '.' + self.type
|
|
81
|
-
file_obj.file.save(file_path, ContentFile(self.file.read()), save=False)
|
|
82
|
-
|
|
83
83
|
|
|
84
84
|
@dataclass
|
|
85
85
|
class FileContentObjectFormatter(FileFormatter):
|
|
@@ -116,7 +116,7 @@ class FileUploader(ABC):
|
|
|
116
116
|
related_field: str | None
|
|
117
117
|
app_name: str = 'Uncategorized'
|
|
118
118
|
|
|
119
|
-
def null_file_obj(self, file):
|
|
119
|
+
def null_file_obj(self, file: InMemoryUploadedFile) -> File:
|
|
120
120
|
formatted_file = FileFormatter(
|
|
121
121
|
file=file,
|
|
122
122
|
related_field=self.related_field,
|
|
@@ -135,12 +135,7 @@ class FileUploader(ABC):
|
|
|
135
135
|
|
|
136
136
|
@dataclass
|
|
137
137
|
class SingleFileUploader(FileUploader):
|
|
138
|
-
def
|
|
139
|
-
file = self.null_file_obj(file)
|
|
140
|
-
file.save()
|
|
141
|
-
return file
|
|
142
|
-
|
|
143
|
-
def delete_old_files(self, content_object: FileModelMixin):
|
|
138
|
+
def delete_old_files(self, content_object: FileModelMixin) -> None:
|
|
144
139
|
old_files = content_object.files.active().related_field(self.related_field)
|
|
145
140
|
|
|
146
141
|
for file in old_files:
|
|
@@ -149,9 +144,14 @@ class SingleFileUploader(FileUploader):
|
|
|
149
144
|
|
|
150
145
|
File.objects.bulk_update(old_files, ['is_active', 'is_deleted'])
|
|
151
146
|
|
|
147
|
+
def upload(self, file: InMemoryUploadedFile) -> File:
|
|
148
|
+
file = self.null_file_obj(file)
|
|
149
|
+
file.save()
|
|
150
|
+
return file
|
|
151
|
+
|
|
152
152
|
def upload_from_form_field(
|
|
153
153
|
self,
|
|
154
|
-
form_data,
|
|
154
|
+
form_data: dict,
|
|
155
155
|
content_object: FileModelMixin
|
|
156
156
|
) -> File:
|
|
157
157
|
try:
|
django_spire/file/mixins.py
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import Any
|
|
4
|
-
|
|
5
3
|
from django.contrib.contenttypes.fields import GenericRelation
|
|
6
|
-
|
|
7
4
|
from django.db import models
|
|
8
5
|
|
|
9
6
|
from django_spire.file.models import File
|
|
@@ -13,7 +10,7 @@ from django_spire.file.tools import copy_files_from_source_to_target_model_objec
|
|
|
13
10
|
class FileModelMixin(models.Model):
|
|
14
11
|
files = GenericRelation(File, editable=False)
|
|
15
12
|
|
|
16
|
-
def copy_files_to_target_model_object(self, target:
|
|
13
|
+
def copy_files_to_target_model_object(self, target: models.Model) -> list[File]:
|
|
17
14
|
# TODO: Move to File Service
|
|
18
15
|
return copy_files_from_source_to_target_model_object(source=self, target=target)
|
|
19
16
|
|
django_spire/file/models.py
CHANGED
|
@@ -2,9 +2,8 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
|
|
5
|
-
from django.contrib.contenttypes.models import ContentType
|
|
6
|
-
|
|
7
5
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
|
6
|
+
from django.contrib.contenttypes.models import ContentType
|
|
8
7
|
from django.db import models
|
|
9
8
|
|
|
10
9
|
from django_spire.file.queryset import FileQuerySet
|
|
@@ -25,10 +24,10 @@ class File(HistoryModelMixin):
|
|
|
25
24
|
|
|
26
25
|
objects = FileQuerySet.as_manager()
|
|
27
26
|
|
|
28
|
-
def __str__(self):
|
|
27
|
+
def __str__(self) -> str:
|
|
29
28
|
return self.name
|
|
30
29
|
|
|
31
|
-
def to_dict(self) -> dict[str, str]:
|
|
30
|
+
def to_dict(self) -> dict[str, str | int]:
|
|
32
31
|
return {
|
|
33
32
|
'name': self.name,
|
|
34
33
|
'url': self.file.url,
|
|
@@ -45,4 +44,3 @@ class File(HistoryModelMixin):
|
|
|
45
44
|
indexes = [
|
|
46
45
|
models.Index(fields=['content_type', 'object_id']),
|
|
47
46
|
]
|
|
48
|
-
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from io import BytesIO
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from django.core.files.uploadedfile import InMemoryUploadedFile, SimpleUploadedFile
|
|
7
|
+
|
|
8
|
+
from django_spire.file.models import File
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from django.contrib.contenttypes.models import ContentType
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def create_test_file(
|
|
15
|
+
content_type: ContentType | None = None,
|
|
16
|
+
object_id: int | None = None,
|
|
17
|
+
name: str = 'test_file',
|
|
18
|
+
file_type: str = 'pdf',
|
|
19
|
+
size: str = '1.5 Mb',
|
|
20
|
+
related_field: str | None = None,
|
|
21
|
+
is_active: bool = True,
|
|
22
|
+
is_deleted: bool = False,
|
|
23
|
+
) -> File:
|
|
24
|
+
file_content = b'test content'
|
|
25
|
+
uploaded_file = SimpleUploadedFile(
|
|
26
|
+
name=f'{name}.{file_type}',
|
|
27
|
+
content=file_content,
|
|
28
|
+
content_type='application/octet-stream'
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
return File.objects.create(
|
|
32
|
+
content_type=content_type,
|
|
33
|
+
object_id=object_id,
|
|
34
|
+
file=uploaded_file,
|
|
35
|
+
name=name,
|
|
36
|
+
type=file_type,
|
|
37
|
+
size=size,
|
|
38
|
+
related_field=related_field,
|
|
39
|
+
is_active=is_active,
|
|
40
|
+
is_deleted=is_deleted,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def create_test_in_memory_uploaded_file(
|
|
45
|
+
name: str = 'test_file',
|
|
46
|
+
file_type: str = 'pdf',
|
|
47
|
+
content: bytes = b'test content',
|
|
48
|
+
) -> InMemoryUploadedFile:
|
|
49
|
+
file_io = BytesIO(content)
|
|
50
|
+
file_io.seek(0)
|
|
51
|
+
|
|
52
|
+
return InMemoryUploadedFile(
|
|
53
|
+
file=file_io,
|
|
54
|
+
field_name='file',
|
|
55
|
+
name=f'{name}.{file_type}',
|
|
56
|
+
content_type='application/octet-stream',
|
|
57
|
+
size=len(content),
|
|
58
|
+
charset=None,
|
|
59
|
+
)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from django.contrib.admin.sites import AdminSite
|
|
4
|
+
from django.contrib.contenttypes.models import ContentType
|
|
5
|
+
from django.test import override_settings
|
|
6
|
+
|
|
7
|
+
from django_spire.core.tests.test_cases import BaseTestCase
|
|
8
|
+
from django_spire.file.admin import FileAdmin
|
|
9
|
+
from django_spire.file.models import File
|
|
10
|
+
from django_spire.file.tests.factories import create_test_file
|
|
11
|
+
from django_spire.help_desk.tests.factories import create_test_helpdesk_ticket
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
STORAGES_OVERRIDE = {
|
|
15
|
+
'default': {
|
|
16
|
+
'BACKEND': 'django.core.files.storage.FileSystemStorage',
|
|
17
|
+
},
|
|
18
|
+
'staticfiles': {
|
|
19
|
+
'BACKEND': 'django.contrib.staticfiles.storage.StaticFilesStorage',
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@override_settings(STORAGES=STORAGES_OVERRIDE)
|
|
25
|
+
class FileAdminTests(BaseTestCase):
|
|
26
|
+
def setUp(self):
|
|
27
|
+
super().setUp()
|
|
28
|
+
|
|
29
|
+
self.site = AdminSite()
|
|
30
|
+
self.admin = FileAdmin(File, self.site)
|
|
31
|
+
self.file = create_test_file()
|
|
32
|
+
|
|
33
|
+
def test_list_display(self):
|
|
34
|
+
expected = ('id', 'name', 'type', 'size', 'content_object_link', 'file_link')
|
|
35
|
+
|
|
36
|
+
assert self.admin.list_display == expected
|
|
37
|
+
|
|
38
|
+
def test_list_filter(self):
|
|
39
|
+
assert self.admin.list_filter == ('type',)
|
|
40
|
+
|
|
41
|
+
def test_search_fields(self):
|
|
42
|
+
assert self.admin.search_fields == ('id', 'name', 'type')
|
|
43
|
+
|
|
44
|
+
def test_ordering(self):
|
|
45
|
+
assert self.admin.ordering == ('-id',)
|
|
46
|
+
|
|
47
|
+
def test_content_object_link_no_related_object(self):
|
|
48
|
+
result = self.admin.content_object_link(self.file)
|
|
49
|
+
|
|
50
|
+
assert result == 'No Related Object'
|
|
51
|
+
|
|
52
|
+
def test_content_object_link_with_related_object(self):
|
|
53
|
+
ticket = create_test_helpdesk_ticket()
|
|
54
|
+
content_type = ContentType.objects.get_for_model(ticket.__class__)
|
|
55
|
+
self.file.content_type = content_type
|
|
56
|
+
self.file.object_id = ticket.pk
|
|
57
|
+
self.file.save()
|
|
58
|
+
|
|
59
|
+
result = self.admin.content_object_link(self.file)
|
|
60
|
+
|
|
61
|
+
assert 'href=' in result
|
|
62
|
+
assert str(ticket) in result
|
|
63
|
+
|
|
64
|
+
def test_file_link(self):
|
|
65
|
+
result = self.admin.file_link(self.file)
|
|
66
|
+
|
|
67
|
+
assert f'href="{self.file.file.url}"' in result
|
|
68
|
+
assert self.file.name in result
|
|
69
|
+
assert 'download' in result
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from django_spire.core.tests.test_cases import BaseTestCase
|
|
4
|
+
from django_spire.file.apps import FileConfig
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class FileConfigTests(BaseTestCase):
|
|
8
|
+
def test_default_auto_field(self):
|
|
9
|
+
assert FileConfig.default_auto_field == 'django.db.models.BigAutoField'
|
|
10
|
+
|
|
11
|
+
def test_label(self):
|
|
12
|
+
assert FileConfig.label == 'django_spire_file'
|
|
13
|
+
|
|
14
|
+
def test_name(self):
|
|
15
|
+
assert FileConfig.name == 'django_spire.file'
|
|
16
|
+
|
|
17
|
+
def test_required_apps(self):
|
|
18
|
+
assert FileConfig.REQUIRED_APPS == ('django_spire_core',)
|
|
19
|
+
|
|
20
|
+
def test_urlpatterns_include(self):
|
|
21
|
+
assert FileConfig.URLPATTERNS_INCLUDE == 'django_spire.file.urls'
|
|
22
|
+
|
|
23
|
+
def test_urlpatterns_namespace(self):
|
|
24
|
+
assert FileConfig.URLPATTERNS_NAMESPACE == 'file'
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from django.test import override_settings
|
|
6
|
+
|
|
7
|
+
from django_spire.core.tests.test_cases import BaseTestCase
|
|
8
|
+
from django_spire.file.fields import MultipleFileField, SingleFileField
|
|
9
|
+
from django_spire.file.models import File
|
|
10
|
+
from django_spire.file.tests.factories import create_test_file
|
|
11
|
+
from django_spire.file.widgets import MultipleWidget, SingleFileWidget
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
STORAGES_OVERRIDE = {
|
|
15
|
+
'default': {
|
|
16
|
+
'BACKEND': 'django.core.files.storage.FileSystemStorage',
|
|
17
|
+
},
|
|
18
|
+
'staticfiles': {
|
|
19
|
+
'BACKEND': 'django.contrib.staticfiles.storage.StaticFilesStorage',
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@override_settings(STORAGES=STORAGES_OVERRIDE)
|
|
25
|
+
class MultipleFileFieldTests(BaseTestCase):
|
|
26
|
+
def setUp(self):
|
|
27
|
+
super().setUp()
|
|
28
|
+
|
|
29
|
+
self.field = MultipleFileField()
|
|
30
|
+
|
|
31
|
+
def test_widget_is_multiple_widget(self):
|
|
32
|
+
assert isinstance(self.field.widget, MultipleWidget)
|
|
33
|
+
|
|
34
|
+
def test_prepare_value_with_files(self):
|
|
35
|
+
file1 = create_test_file(name='file1')
|
|
36
|
+
file2 = create_test_file(name='file2')
|
|
37
|
+
|
|
38
|
+
result = self.field.prepare_value([file1, file2])
|
|
39
|
+
parsed = json.loads(result)
|
|
40
|
+
|
|
41
|
+
assert len(parsed) == 2
|
|
42
|
+
|
|
43
|
+
def test_prepare_value_with_none(self):
|
|
44
|
+
result = self.field.prepare_value(None)
|
|
45
|
+
|
|
46
|
+
assert result == '[]'
|
|
47
|
+
|
|
48
|
+
def test_prepare_value_with_empty_list(self):
|
|
49
|
+
result = self.field.prepare_value([])
|
|
50
|
+
|
|
51
|
+
assert result == '[]'
|
|
52
|
+
|
|
53
|
+
def test_prepare_value_contains_file_data(self):
|
|
54
|
+
file = create_test_file(name='test')
|
|
55
|
+
|
|
56
|
+
result = self.field.prepare_value([file])
|
|
57
|
+
parsed = json.loads(result)
|
|
58
|
+
|
|
59
|
+
assert parsed[0]['name'] == 'test'
|
|
60
|
+
assert 'id' in parsed[0]
|
|
61
|
+
assert 'url' in parsed[0]
|
|
62
|
+
|
|
63
|
+
def test_clean_returns_data(self):
|
|
64
|
+
data = {'test': 'value'}
|
|
65
|
+
|
|
66
|
+
result = self.field.clean(data)
|
|
67
|
+
|
|
68
|
+
assert result == data
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@override_settings(STORAGES=STORAGES_OVERRIDE)
|
|
72
|
+
class SingleFileFieldTests(BaseTestCase):
|
|
73
|
+
def setUp(self):
|
|
74
|
+
super().setUp()
|
|
75
|
+
|
|
76
|
+
self.field = SingleFileField()
|
|
77
|
+
|
|
78
|
+
def test_widget_is_single_file_widget(self):
|
|
79
|
+
assert isinstance(self.field.widget, SingleFileWidget)
|
|
80
|
+
|
|
81
|
+
def test_prepare_value_with_file(self):
|
|
82
|
+
file = create_test_file(name='test')
|
|
83
|
+
|
|
84
|
+
result = self.field.prepare_value(file)
|
|
85
|
+
parsed = json.loads(result)
|
|
86
|
+
|
|
87
|
+
assert parsed['name'] == 'test'
|
|
88
|
+
assert 'id' in parsed
|
|
89
|
+
assert 'url' in parsed
|
|
90
|
+
|
|
91
|
+
def test_prepare_value_with_none(self):
|
|
92
|
+
result = self.field.prepare_value(None)
|
|
93
|
+
|
|
94
|
+
assert result == 'null'
|
|
95
|
+
|
|
96
|
+
def test_prepare_value_with_queryset(self):
|
|
97
|
+
file = create_test_file(name='queryset_file')
|
|
98
|
+
|
|
99
|
+
result = self.field.prepare_value(File.objects.filter(pk=file.pk))
|
|
100
|
+
parsed = json.loads(result)
|
|
101
|
+
|
|
102
|
+
assert parsed['name'] == 'queryset_file'
|
|
103
|
+
|
|
104
|
+
def test_prepare_value_with_empty_queryset(self):
|
|
105
|
+
result = self.field.prepare_value(File.objects.none())
|
|
106
|
+
|
|
107
|
+
assert result == 'null'
|
|
108
|
+
|
|
109
|
+
def test_clean_returns_data(self):
|
|
110
|
+
data = {'test': 'value'}
|
|
111
|
+
|
|
112
|
+
result = self.field.clean(data)
|
|
113
|
+
|
|
114
|
+
assert result == data
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from django_spire.core.tests.test_cases import BaseTestCase
|
|
4
|
+
from django_spire.file.fields import MultipleFileField
|
|
5
|
+
from django_spire.file.forms import FileForm
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FileFormTests(BaseTestCase):
|
|
9
|
+
def test_has_files_field(self):
|
|
10
|
+
assert 'files' in FileForm().fields
|
|
11
|
+
|
|
12
|
+
def test_files_field_is_multiple_file_field(self):
|
|
13
|
+
form = FileForm()
|
|
14
|
+
|
|
15
|
+
assert isinstance(form.fields['files'], MultipleFileField)
|
|
16
|
+
|
|
17
|
+
def test_form_field_count(self):
|
|
18
|
+
form = FileForm()
|
|
19
|
+
|
|
20
|
+
assert len(form.fields) == 1
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from django.conf import settings
|
|
4
|
+
from django.test import override_settings
|
|
5
|
+
|
|
6
|
+
from django_spire.core.tests.test_cases import BaseTestCase
|
|
7
|
+
from django_spire.file.interfaces import (
|
|
8
|
+
FileFormatter,
|
|
9
|
+
MultiFileUploader,
|
|
10
|
+
SingleFileUploader,
|
|
11
|
+
)
|
|
12
|
+
from django_spire.file.models import File
|
|
13
|
+
from django_spire.file.tests.factories import create_test_in_memory_uploaded_file
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
STORAGES_OVERRIDE = {
|
|
17
|
+
'default': {
|
|
18
|
+
'BACKEND': 'django.core.files.storage.FileSystemStorage',
|
|
19
|
+
},
|
|
20
|
+
'staticfiles': {
|
|
21
|
+
'BACKEND': 'django.contrib.staticfiles.storage.StaticFilesStorage',
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class FileFormatterTests(BaseTestCase):
|
|
27
|
+
def setUp(self):
|
|
28
|
+
super().setUp()
|
|
29
|
+
|
|
30
|
+
self.uploaded_file = create_test_in_memory_uploaded_file(
|
|
31
|
+
name='document',
|
|
32
|
+
file_type='pdf',
|
|
33
|
+
content=b'test content',
|
|
34
|
+
)
|
|
35
|
+
self.formatter = FileFormatter(file=self.uploaded_file)
|
|
36
|
+
|
|
37
|
+
def test_format_name(self):
|
|
38
|
+
assert self.formatter.name == 'document'
|
|
39
|
+
|
|
40
|
+
def test_parse_type(self):
|
|
41
|
+
assert self.formatter.type == 'pdf'
|
|
42
|
+
|
|
43
|
+
def test_location_contains_base_folder(self):
|
|
44
|
+
assert self.formatter.location.startswith(settings.BASE_FOLDER_NAME + '/')
|
|
45
|
+
|
|
46
|
+
def test_location_contains_app_name(self):
|
|
47
|
+
assert '/Uncategorized/' in self.formatter.location
|
|
48
|
+
|
|
49
|
+
def test_location_contains_file_name(self):
|
|
50
|
+
assert self.formatter.location.endswith('document')
|
|
51
|
+
|
|
52
|
+
def test_location_with_related_field(self):
|
|
53
|
+
formatter = FileFormatter(
|
|
54
|
+
file=self.uploaded_file,
|
|
55
|
+
related_field='abc',
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
assert '/abc/' in formatter.location
|
|
59
|
+
|
|
60
|
+
def test_location_with_custom_app_name(self):
|
|
61
|
+
formatter = FileFormatter(
|
|
62
|
+
file=self.uploaded_file,
|
|
63
|
+
app_name='CustomApp',
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
assert '/CustomApp/' in formatter.location
|
|
67
|
+
|
|
68
|
+
def test_size_verbose_kb(self):
|
|
69
|
+
content = b'x' * 1000
|
|
70
|
+
uploaded_file = create_test_in_memory_uploaded_file(content=content)
|
|
71
|
+
formatter = FileFormatter(file=uploaded_file)
|
|
72
|
+
|
|
73
|
+
assert 'kb' in formatter.size_verbose()
|
|
74
|
+
|
|
75
|
+
def test_size_verbose_mb(self):
|
|
76
|
+
content = b'x' * 1000000
|
|
77
|
+
uploaded_file = create_test_in_memory_uploaded_file(content=content)
|
|
78
|
+
formatter = FileFormatter(file=uploaded_file)
|
|
79
|
+
|
|
80
|
+
assert 'Mb' in formatter.size_verbose()
|
|
81
|
+
|
|
82
|
+
@override_settings(STORAGES=STORAGES_OVERRIDE)
|
|
83
|
+
def test_null_file_obj_returns_file(self):
|
|
84
|
+
result = self.formatter.null_file_obj()
|
|
85
|
+
|
|
86
|
+
assert isinstance(result, File)
|
|
87
|
+
assert result.name == 'document'
|
|
88
|
+
assert result.type == 'pdf'
|
|
89
|
+
|
|
90
|
+
@override_settings(STORAGES=STORAGES_OVERRIDE)
|
|
91
|
+
def test_null_file_obj_not_saved(self):
|
|
92
|
+
result = self.formatter.null_file_obj()
|
|
93
|
+
|
|
94
|
+
assert result.pk is None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@override_settings(STORAGES=STORAGES_OVERRIDE)
|
|
98
|
+
class SingleFileUploaderTests(BaseTestCase):
|
|
99
|
+
def setUp(self):
|
|
100
|
+
super().setUp()
|
|
101
|
+
|
|
102
|
+
self.uploader = SingleFileUploader(related_field='abc')
|
|
103
|
+
self.uploaded_file = create_test_in_memory_uploaded_file()
|
|
104
|
+
|
|
105
|
+
def test_upload_creates_file(self):
|
|
106
|
+
initial_count = File.objects.count()
|
|
107
|
+
|
|
108
|
+
self.uploader.upload(self.uploaded_file)
|
|
109
|
+
|
|
110
|
+
assert File.objects.count() == initial_count + 1
|
|
111
|
+
|
|
112
|
+
def test_upload_returns_file(self):
|
|
113
|
+
result = self.uploader.upload(self.uploaded_file)
|
|
114
|
+
|
|
115
|
+
assert isinstance(result, File)
|
|
116
|
+
assert result.pk is not None
|
|
117
|
+
|
|
118
|
+
def test_upload_sets_name(self):
|
|
119
|
+
result = self.uploader.upload(self.uploaded_file)
|
|
120
|
+
|
|
121
|
+
assert result.name == 'test_file'
|
|
122
|
+
|
|
123
|
+
def test_upload_sets_type(self):
|
|
124
|
+
result = self.uploader.upload(self.uploaded_file)
|
|
125
|
+
|
|
126
|
+
assert result.type == 'pdf'
|
|
127
|
+
|
|
128
|
+
def test_null_file_obj(self):
|
|
129
|
+
result = self.uploader.null_file_obj(self.uploaded_file)
|
|
130
|
+
|
|
131
|
+
assert isinstance(result, File)
|
|
132
|
+
assert result.pk is None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@override_settings(STORAGES=STORAGES_OVERRIDE)
|
|
136
|
+
class MultiFileUploaderTests(BaseTestCase):
|
|
137
|
+
def setUp(self):
|
|
138
|
+
super().setUp()
|
|
139
|
+
|
|
140
|
+
self.uploader = MultiFileUploader(related_field='abc')
|
|
141
|
+
self.uploaded_files = [
|
|
142
|
+
create_test_in_memory_uploaded_file(name='file1'),
|
|
143
|
+
create_test_in_memory_uploaded_file(name='file2'),
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
def test_upload_creates_multiple_files(self):
|
|
147
|
+
initial_count = File.objects.count()
|
|
148
|
+
|
|
149
|
+
self.uploader.upload(self.uploaded_files)
|
|
150
|
+
|
|
151
|
+
assert File.objects.count() == initial_count + 2
|
|
152
|
+
|
|
153
|
+
def test_upload_returns_list_of_files(self):
|
|
154
|
+
result = self.uploader.upload(self.uploaded_files)
|
|
155
|
+
|
|
156
|
+
assert isinstance(result, list)
|
|
157
|
+
assert len(result) == 2
|
|
158
|
+
|
|
159
|
+
def test_upload_returns_file_instances(self):
|
|
160
|
+
result = self.uploader.upload(self.uploaded_files)
|
|
161
|
+
|
|
162
|
+
for file in result:
|
|
163
|
+
assert isinstance(file, File)
|
|
164
|
+
assert file.pk is not None
|
|
165
|
+
|
|
166
|
+
def test_upload_sets_names(self):
|
|
167
|
+
result = self.uploader.upload(self.uploaded_files)
|
|
168
|
+
names = [f.name for f in result]
|
|
169
|
+
|
|
170
|
+
assert 'file1' in names
|
|
171
|
+
assert 'file2' in names
|
|
172
|
+
|
|
173
|
+
def test_upload_empty_list(self):
|
|
174
|
+
result = self.uploader.upload([])
|
|
175
|
+
|
|
176
|
+
assert result == []
|
|
177
|
+
|
|
178
|
+
def test_null_file_obj(self):
|
|
179
|
+
uploaded_file = create_test_in_memory_uploaded_file()
|
|
180
|
+
result = self.uploader.null_file_obj(uploaded_file)
|
|
181
|
+
|
|
182
|
+
assert isinstance(result, File)
|
|
183
|
+
assert result.pk is None
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from django.test import override_settings
|
|
6
|
+
|
|
7
|
+
from django_spire.core.tests.test_cases import BaseTestCase
|
|
8
|
+
from django_spire.file.tests.factories import create_test_file
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
STORAGES_OVERRIDE = {
|
|
12
|
+
'default': {
|
|
13
|
+
'BACKEND': 'django.core.files.storage.FileSystemStorage',
|
|
14
|
+
},
|
|
15
|
+
'staticfiles': {
|
|
16
|
+
'BACKEND': 'django.contrib.staticfiles.storage.StaticFilesStorage',
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@override_settings(STORAGES=STORAGES_OVERRIDE)
|
|
22
|
+
class FileModelTests(BaseTestCase):
|
|
23
|
+
def setUp(self):
|
|
24
|
+
super().setUp()
|
|
25
|
+
|
|
26
|
+
self.file = create_test_file()
|
|
27
|
+
|
|
28
|
+
def test_str(self):
|
|
29
|
+
assert str(self.file) == self.file.name
|
|
30
|
+
|
|
31
|
+
def test_default_is_active(self):
|
|
32
|
+
assert self.file.is_active is True
|
|
33
|
+
|
|
34
|
+
def test_default_is_deleted(self):
|
|
35
|
+
assert self.file.is_deleted is False
|
|
36
|
+
|
|
37
|
+
def test_name_field(self):
|
|
38
|
+
assert self.file.name == 'test_file'
|
|
39
|
+
|
|
40
|
+
def test_type_field(self):
|
|
41
|
+
assert self.file.type == 'pdf'
|
|
42
|
+
|
|
43
|
+
def test_size_field(self):
|
|
44
|
+
assert self.file.size == '1.5 Mb'
|
|
45
|
+
|
|
46
|
+
def test_related_field_default(self):
|
|
47
|
+
assert self.file.related_field is None
|
|
48
|
+
|
|
49
|
+
def test_related_field_set(self):
|
|
50
|
+
file = create_test_file(related_field='abc')
|
|
51
|
+
assert file.related_field == 'abc'
|
|
52
|
+
|
|
53
|
+
def test_content_type_null(self):
|
|
54
|
+
assert self.file.content_type is None
|
|
55
|
+
|
|
56
|
+
def test_object_id_null(self):
|
|
57
|
+
assert self.file.object_id is None
|
|
58
|
+
|
|
59
|
+
def test_created_datetime_auto_set(self):
|
|
60
|
+
assert self.file.created_datetime is not None
|
|
61
|
+
|
|
62
|
+
def test_to_dict(self):
|
|
63
|
+
result = self.file.to_dict()
|
|
64
|
+
|
|
65
|
+
assert result['name'] == self.file.name
|
|
66
|
+
assert result['id'] == self.file.id
|
|
67
|
+
assert 'url' in result
|
|
68
|
+
|
|
69
|
+
def test_to_dict_keys(self):
|
|
70
|
+
result = self.file.to_dict()
|
|
71
|
+
|
|
72
|
+
assert set(result.keys()) == {'name', 'url', 'id'}
|
|
73
|
+
|
|
74
|
+
def test_to_json(self):
|
|
75
|
+
result = self.file.to_json()
|
|
76
|
+
parsed = json.loads(result)
|
|
77
|
+
|
|
78
|
+
assert parsed['name'] == self.file.name
|
|
79
|
+
assert parsed['id'] == self.file.id
|
|
80
|
+
|
|
81
|
+
def test_file_field_url(self):
|
|
82
|
+
assert self.file.file.url is not None
|