django-spire 0.23.7__py3-none-any.whl → 0.23.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_spire/ai/admin.py +11 -11
- django_spire/ai/chat/apps.py +1 -0
- django_spire/ai/chat/templates/django_spire/ai/chat/widget/dialog_widget.html +1 -1
- django_spire/ai/chat/tests/factories.py +15 -0
- django_spire/ai/chat/tests/test_controller.py +45 -0
- django_spire/ai/chat/tests/test_models.py +301 -0
- django_spire/ai/chat/tests/test_prompts.py +48 -0
- django_spire/ai/chat/tests/test_responses.py +208 -0
- django_spire/ai/chat/tests/test_router/test_base_chat_router.py +66 -6
- django_spire/ai/chat/tests/test_router/test_chat_workflow.py +73 -3
- django_spire/ai/chat/tests/test_router/test_integration.py +86 -6
- django_spire/ai/chat/tests/test_router/test_intent_decoder.py +93 -1
- django_spire/ai/chat/tests/test_router/test_message_intel.py +60 -1
- django_spire/ai/chat/tests/test_router/test_spire_chat_router.py +110 -0
- django_spire/ai/chat/tests/test_urls/test_json_urls.py +202 -1
- django_spire/ai/context/tests/__init__.py +0 -0
- django_spire/ai/context/tests/test_context.py +188 -0
- django_spire/ai/decorators.py +7 -6
- django_spire/ai/prompt/tests/test_bots.py +100 -10
- django_spire/ai/prompt/tests/test_prompt_intel.py +83 -0
- django_spire/ai/prompt/tests/test_prompt_tuning.py +126 -0
- django_spire/ai/sms/decorators.py +8 -2
- django_spire/ai/sms/tests/test_sms.py +240 -16
- django_spire/ai/sms/tests/test_sms_intel.py +42 -0
- django_spire/ai/sms/tests/test_webhook.py +155 -7
- django_spire/ai/sms/views.py +23 -24
- django_spire/ai/tests/test_ai.py +131 -7
- django_spire/auth/apps.py +4 -2
- django_spire/auth/controller/controller.py +36 -23
- django_spire/auth/controller/exceptions.py +9 -0
- django_spire/auth/group/admin.py +1 -0
- django_spire/auth/group/apps.py +2 -0
- django_spire/auth/group/factories.py +17 -8
- django_spire/auth/group/forms.py +7 -0
- django_spire/auth/group/tests/test_factories.py +146 -0
- django_spire/auth/group/tests/test_forms.py +282 -0
- django_spire/auth/group/tests/test_models.py +192 -0
- django_spire/auth/group/tests/test_querysets.py +98 -0
- django_spire/auth/group/tests/test_utils.py +341 -0
- django_spire/auth/group/tests/test_views.py +377 -0
- django_spire/auth/group/urls/__init__.py +3 -1
- django_spire/auth/group/urls/form_urls.py +2 -0
- django_spire/auth/group/urls/json_urls.py +3 -0
- django_spire/auth/group/urls/page_urls.py +2 -0
- django_spire/auth/group/utils.py +6 -2
- django_spire/auth/group/views/form_views.py +6 -3
- django_spire/auth/group/views/json_views.py +6 -2
- django_spire/auth/mfa/admin.py +2 -0
- django_spire/auth/mfa/apps.py +2 -0
- django_spire/auth/mfa/forms.py +1 -0
- django_spire/auth/mfa/querysets.py +9 -2
- django_spire/auth/mfa/tests/test_models.py +233 -0
- django_spire/auth/mfa/tests/test_utils.py +106 -0
- django_spire/auth/mfa/urls/__init__.py +2 -0
- django_spire/auth/mfa/urls/page_urls.py +2 -0
- django_spire/auth/mfa/urls/redirect_urls.py +2 -0
- django_spire/auth/mfa/views/page_views.py +2 -1
- django_spire/auth/permissions/consts.py +2 -2
- django_spire/auth/permissions/decorators.py +8 -8
- django_spire/auth/permissions/permissions.py +28 -35
- django_spire/auth/permissions/tests/test_decorators.py +333 -0
- django_spire/auth/permissions/tests/test_permissions.py +337 -0
- django_spire/auth/permissions/tests/test_tools.py +305 -0
- django_spire/auth/permissions/tools.py +21 -15
- django_spire/auth/seeding/seed.py +3 -0
- django_spire/auth/seeding/seeder.py +2 -0
- django_spire/auth/tests/test_controller.py +323 -0
- django_spire/auth/tests/test_url_endpoints.py +9 -9
- django_spire/auth/tests/test_views.py +406 -0
- django_spire/auth/urls/admin_urls.py +2 -0
- django_spire/auth/urls/redirect_urls.py +2 -0
- django_spire/auth/user/apps.py +2 -0
- django_spire/auth/user/forms.py +9 -0
- django_spire/auth/user/models.py +1 -1
- django_spire/auth/user/services/services.py +1 -0
- django_spire/auth/user/tests/factories.py +14 -13
- django_spire/auth/user/tests/test_factories.py +166 -2
- django_spire/auth/user/tests/test_forms.py +573 -0
- django_spire/auth/user/tests/test_models.py +257 -0
- django_spire/auth/user/tests/test_services.py +200 -0
- django_spire/auth/user/tests/test_tools.py +153 -0
- django_spire/auth/user/tests/test_user_factories.py +139 -0
- django_spire/auth/user/tests/test_views.py +363 -0
- django_spire/auth/user/tools.py +7 -1
- django_spire/auth/user/urls/form_urls.py +3 -0
- django_spire/auth/user/urls/page_urls.py +3 -0
- django_spire/auth/user/views/form_views.py +19 -10
- django_spire/auth/user/views/page_views.py +8 -2
- django_spire/auth/views/redirect_views.py +14 -9
- django_spire/comment/admin.py +2 -0
- django_spire/comment/apps.py +2 -0
- django_spire/comment/templatetags/comment_tags.py +1 -0
- django_spire/comment/tests/test_forms.py +27 -0
- django_spire/comment/tests/test_models.py +215 -0
- django_spire/comment/tests/test_querysets.py +101 -0
- django_spire/comment/tests/test_utils.py +90 -0
- django_spire/comment/urls.py +2 -0
- django_spire/comment/utils.py +22 -13
- django_spire/comment/views.py +1 -1
- django_spire/conf.py +8 -6
- django_spire/consts.py +1 -1
- django_spire/contrib/breadcrumb/apps.py +2 -0
- django_spire/contrib/breadcrumb/breadcrumbs.py +18 -18
- django_spire/contrib/breadcrumb/tests/test_breadcrumbs.py +198 -0
- django_spire/contrib/constructor/__init__.py +3 -3
- django_spire/contrib/constructor/constructor.py +15 -15
- django_spire/contrib/constructor/django_model_constructor.py +5 -4
- django_spire/contrib/constructor/exceptions.py +5 -3
- django_spire/contrib/constructor/tests/__init__.py +0 -0
- django_spire/contrib/constructor/tests/test_constructor.py +193 -0
- django_spire/contrib/form/tests/__init__.py +0 -0
- django_spire/contrib/form/tests/test_forms.py +203 -0
- django_spire/contrib/generic_views/modal_views.py +2 -1
- django_spire/contrib/generic_views/portal_views.py +20 -19
- django_spire/contrib/generic_views/tests/__init__.py +0 -0
- django_spire/contrib/generic_views/tests/test_views.py +459 -0
- django_spire/contrib/help/apps.py +2 -0
- django_spire/contrib/help/templatetags/help.py +1 -0
- django_spire/contrib/help/tests/__init__.py +0 -0
- django_spire/contrib/help/tests/test_templatetags.py +100 -0
- django_spire/contrib/options/mixins.py +6 -5
- django_spire/contrib/options/tests/factories.py +5 -1
- django_spire/contrib/options/tests/test_options.py +234 -0
- django_spire/contrib/ordering/exceptions.py +7 -3
- django_spire/contrib/ordering/mixins.py +2 -0
- django_spire/contrib/ordering/querysets.py +3 -1
- django_spire/contrib/ordering/services/processor_service.py +8 -4
- django_spire/contrib/ordering/services/service.py +1 -2
- django_spire/contrib/ordering/tests/__init__.py +0 -0
- django_spire/contrib/ordering/tests/test_ordering.py +165 -0
- django_spire/contrib/ordering/validators.py +6 -6
- django_spire/contrib/pagination/templatetags/pagination_tags.py +12 -5
- django_spire/contrib/pagination/tests/__init__.py +0 -0
- django_spire/contrib/pagination/tests/test_pagination.py +179 -0
- django_spire/contrib/performance/decorators.py +16 -6
- django_spire/contrib/performance/tests/__init__.py +0 -0
- django_spire/contrib/performance/tests/test_performance.py +107 -0
- django_spire/contrib/queryset/enums.py +3 -1
- django_spire/contrib/queryset/filter_tools.py +10 -5
- django_spire/contrib/queryset/mixins.py +16 -16
- django_spire/contrib/queryset/tests/__init__.py +0 -0
- django_spire/contrib/queryset/tests/test_queryset.py +137 -0
- django_spire/contrib/seeding/field/base.py +13 -7
- django_spire/contrib/seeding/field/callable.py +8 -1
- django_spire/contrib/seeding/field/cleaners.py +5 -5
- django_spire/contrib/seeding/field/custom.py +20 -10
- django_spire/contrib/seeding/field/django/seeder.py +8 -6
- django_spire/contrib/seeding/field/enums.py +7 -5
- django_spire/contrib/seeding/field/override.py +16 -6
- django_spire/contrib/seeding/field/static.py +9 -2
- django_spire/contrib/seeding/field/tests/test_base.py +18 -14
- django_spire/contrib/seeding/field/tests/test_callable.py +13 -9
- django_spire/contrib/seeding/field/tests/test_cleaners.py +51 -38
- django_spire/contrib/seeding/field/tests/test_static.py +13 -9
- django_spire/contrib/seeding/intelligence/bots/seeder_generator_bot.py +2 -0
- django_spire/contrib/seeding/intelligence/intel.py +5 -1
- django_spire/contrib/seeding/intelligence/prompts/factory.py +6 -1
- django_spire/contrib/seeding/intelligence/prompts/foreign_key_selection_prompt.py +6 -1
- django_spire/contrib/seeding/intelligence/prompts/generate_django_model_seeder_prompts.py +2 -0
- django_spire/contrib/seeding/intelligence/prompts/generic_relationship_selection_prompt.py +7 -1
- django_spire/contrib/seeding/intelligence/prompts/hierarchical_selection_prompt.py +6 -2
- django_spire/contrib/seeding/intelligence/prompts/model_field_choices_prompt.py +8 -2
- django_spire/contrib/seeding/intelligence/prompts/negation_prompt.py +2 -0
- django_spire/contrib/seeding/intelligence/prompts/objective_prompt.py +6 -1
- django_spire/contrib/seeding/management/commands/seeding.py +9 -3
- django_spire/contrib/seeding/management/example.py +2 -0
- django_spire/contrib/seeding/model/base.py +16 -7
- django_spire/contrib/seeding/model/config.py +31 -15
- django_spire/contrib/seeding/model/django/config.py +13 -13
- django_spire/contrib/seeding/model/django/seeder.py +4 -4
- django_spire/contrib/seeding/model/django/tests/test_seeder.py +34 -23
- django_spire/contrib/seeding/model/enums.py +2 -0
- django_spire/contrib/seeding/tests/test_config.py +71 -0
- django_spire/contrib/seeding/tests/test_custom.py +35 -0
- django_spire/contrib/seeding/tests/test_enums.py +40 -0
- django_spire/contrib/seeding/tests/test_intel.py +32 -0
- django_spire/contrib/seeding/tests/test_override.py +63 -0
- django_spire/contrib/service/__init__.py +2 -2
- django_spire/contrib/service/django_model_service.py +16 -15
- django_spire/contrib/service/exceptions.py +5 -3
- django_spire/contrib/service/tests/__init__.py +0 -0
- django_spire/contrib/service/tests/test_service.py +153 -0
- django_spire/contrib/session/apps.py +2 -0
- django_spire/contrib/session/controller.py +48 -42
- django_spire/contrib/session/templatetags/session_tags.py +11 -2
- django_spire/contrib/session/tests/test_session_controller.py +117 -53
- django_spire/contrib/tests/__init__.py +0 -0
- django_spire/contrib/tests/test_utils.py +37 -0
- django_spire/contrib/utils.py +4 -1
- django_spire/core/apps.py +2 -0
- django_spire/core/converters/tests/test_to_data.py +353 -0
- django_spire/core/converters/tests/test_to_enums.py +61 -41
- django_spire/core/converters/tests/test_to_pydantic.py +138 -109
- django_spire/core/converters/to_data.py +29 -10
- django_spire/core/converters/to_enums.py +4 -2
- django_spire/core/converters/to_pydantic.py +22 -22
- django_spire/core/decorators.py +19 -6
- django_spire/core/forms/widgets.py +4 -0
- django_spire/core/management/commands/spire_startapp_pkg/template/app/apps.py.template +2 -0
- django_spire/core/management/commands/spire_startapp_pkg/template/app/urls/__init__.py.template +2 -0
- django_spire/core/management/commands/spire_startapp_pkg/template/app/urls/form_urls.py.template +2 -0
- django_spire/core/management/commands/spire_startapp_pkg/template/app/urls/page_urls.py.template +2 -0
- django_spire/core/maps.py +3 -1
- django_spire/core/middleware/maintenance.py +3 -3
- django_spire/core/middleware.py +8 -6
- django_spire/core/redirect/__init__.py +5 -0
- django_spire/core/redirect/generic_redirect.py +1 -2
- django_spire/core/redirect/tests/__init__.py +0 -0
- django_spire/core/redirect/tests/test_generic_redirect.py +34 -0
- django_spire/core/{tests/tests_redirect.py → redirect/tests/test_safe_redirect.py} +55 -81
- django_spire/core/shortcuts.py +3 -3
- django_spire/core/static/django_spire/css/app-layout.css +1 -1
- django_spire/core/static/django_spire/css/app-navigation.css +3 -3
- django_spire/core/static/django_spire/css/bootstrap-override.css +4 -0
- django_spire/core/tag/admin.py +12 -0
- django_spire/core/tag/intelligence/tag_set_bot.py +2 -0
- django_spire/core/tag/mixins.py +2 -0
- django_spire/core/tag/models.py +2 -0
- django_spire/core/tag/querysets.py +2 -0
- django_spire/core/tag/service/tag_service.py +6 -3
- django_spire/core/tag/tests/test_intelligence.py +9 -9
- django_spire/core/tag/tests/test_tags.py +44 -54
- django_spire/core/tag/tests/test_tools.py +191 -0
- django_spire/core/tag/tools.py +3 -0
- django_spire/core/templates/django_spire/card/card.html +5 -2
- django_spire/core/templates/django_spire/card/title_card.html +12 -4
- django_spire/core/templates/django_spire/infinite_scroll/base.html +1 -0
- django_spire/core/templates/django_spire/navigation/side_navigation.html +19 -24
- django_spire/core/templates/django_spire/page/full_page.html +46 -16
- django_spire/core/templates/django_spire/table/base.html +5 -3
- django_spire/core/templatetags/json.py +6 -2
- django_spire/core/templatetags/message.py +13 -8
- django_spire/core/templatetags/string_formating.py +8 -5
- django_spire/core/templatetags/tests/__init__.py +0 -0
- django_spire/core/templatetags/tests/test_templatetags.py +427 -0
- django_spire/core/templatetags/variable_types.py +17 -9
- django_spire/core/tests/test_cases.py +1 -1
- django_spire/core/tests/test_conf.py +43 -0
- django_spire/core/tests/test_consts.py +28 -0
- django_spire/core/tests/test_context_processors.py +93 -0
- django_spire/core/tests/test_decorators.py +95 -0
- django_spire/core/tests/test_django_spire_utils.py +56 -0
- django_spire/core/tests/test_exceptions.py +37 -0
- django_spire/core/tests/test_models.py +54 -0
- django_spire/core/tests/test_settings.py +45 -0
- django_spire/core/tests/test_shortcuts.py +74 -0
- django_spire/core/tests/test_urls.py +16 -0
- django_spire/core/tests/test_utils.py +58 -0
- django_spire/core/urls.py +4 -1
- django_spire/core/utils.py +12 -8
- django_spire/exceptions.py +16 -1
- django_spire/file/admin.py +4 -2
- django_spire/file/apps.py +8 -10
- django_spire/file/fields.py +7 -7
- django_spire/file/forms.py +1 -1
- django_spire/file/interfaces.py +15 -15
- django_spire/file/mixins.py +1 -4
- django_spire/file/models.py +3 -5
- django_spire/file/tests/factories.py +59 -0
- django_spire/file/tests/test_admin.py +69 -0
- django_spire/file/tests/test_apps.py +24 -0
- django_spire/file/tests/test_fields.py +114 -0
- django_spire/file/tests/test_forms.py +20 -0
- django_spire/file/tests/test_interfaces.py +183 -0
- django_spire/file/tests/test_models.py +82 -0
- django_spire/file/tests/test_querysets.py +102 -0
- django_spire/file/tests/test_utils.py +32 -0
- django_spire/file/tests/test_views.py +145 -0
- django_spire/file/tests/test_widgets.py +82 -0
- django_spire/file/tools.py +8 -2
- django_spire/file/views.py +7 -3
- django_spire/file/widgets.py +12 -12
- django_spire/help_desk/admin.py +15 -0
- django_spire/help_desk/apps.py +2 -0
- django_spire/help_desk/auth/controller.py +2 -0
- django_spire/help_desk/choices.py +2 -0
- django_spire/help_desk/enums.py +2 -0
- django_spire/help_desk/exceptions.py +31 -3
- django_spire/help_desk/forms.py +2 -0
- django_spire/help_desk/models.py +2 -0
- django_spire/help_desk/querysets.py +4 -1
- django_spire/help_desk/services/notification_service.py +26 -27
- django_spire/help_desk/services/service.py +2 -3
- django_spire/help_desk/tests/factories.py +8 -3
- django_spire/help_desk/tests/test_admin.py +41 -0
- django_spire/help_desk/tests/test_apps.py +41 -0
- django_spire/help_desk/tests/test_choices.py +50 -0
- django_spire/help_desk/tests/test_controller.py +87 -0
- django_spire/help_desk/tests/test_enums.py +18 -0
- django_spire/help_desk/tests/test_exceptions.py +37 -0
- django_spire/help_desk/tests/test_forms.py +89 -0
- django_spire/help_desk/tests/test_models.py +59 -0
- django_spire/help_desk/tests/test_querysets.py +38 -0
- django_spire/help_desk/tests/test_services/test_notification_service.py +15 -8
- django_spire/help_desk/tests/test_services/test_service.py +92 -0
- django_spire/help_desk/tests/test_urls/test_form_urls.py +6 -6
- django_spire/help_desk/tests/test_urls/test_page_urls.py +8 -9
- django_spire/help_desk/tests/test_views/test_form_views.py +46 -19
- django_spire/help_desk/tests/test_views/test_page_views.py +32 -9
- django_spire/help_desk/urls/__init__.py +4 -1
- django_spire/help_desk/urls/form_urls.py +3 -0
- django_spire/help_desk/urls/page_urls.py +3 -0
- django_spire/help_desk/views/form_views.py +13 -5
- django_spire/help_desk/views/page_views.py +11 -3
- django_spire/history/activity/admin.py +2 -0
- django_spire/history/activity/apps.py +3 -1
- django_spire/history/activity/mixins.py +13 -7
- django_spire/history/activity/models.py +6 -5
- django_spire/history/activity/querysets.py +2 -0
- django_spire/history/activity/tests/__init__.py +0 -0
- django_spire/history/activity/tests/test_activity.py +176 -0
- django_spire/history/admin.py +9 -2
- django_spire/history/choices.py +3 -0
- django_spire/history/models.py +5 -5
- django_spire/history/tests/test_admin.py +93 -0
- django_spire/history/tests/test_history.py +101 -0
- django_spire/history/tests/test_mixins.py +84 -0
- django_spire/history/viewed/admin.py +3 -1
- django_spire/history/viewed/apps.py +3 -1
- django_spire/history/viewed/models.py +2 -0
- django_spire/history/viewed/tests/__init__.py +0 -0
- django_spire/history/viewed/tests/test_viewed.py +46 -0
- django_spire/knowledge/auth/tests/__init__.py +0 -0
- django_spire/knowledge/auth/tests/test_controller.py +116 -0
- django_spire/knowledge/collection/admin.py +5 -1
- django_spire/knowledge/collection/models.py +3 -1
- django_spire/knowledge/collection/seeding/seed.py +1 -0
- django_spire/knowledge/collection/services/factory_service.py +10 -11
- django_spire/knowledge/collection/services/ordering_service.py +1 -2
- django_spire/knowledge/collection/services/service.py +5 -10
- django_spire/knowledge/collection/services/tag_service.py +5 -2
- django_spire/knowledge/collection/tests/factories.py +28 -1
- django_spire/knowledge/collection/tests/test_models.py +48 -0
- django_spire/knowledge/collection/tests/test_querysets.py +93 -0
- django_spire/knowledge/collection/tests/test_services/test_factory_service.py +100 -0
- django_spire/knowledge/collection/tests/test_services/test_services.py +160 -0
- django_spire/knowledge/collection/tests/test_urls/test_form_urls.py +21 -3
- django_spire/knowledge/collection/tests/test_urls/test_json_urls.py +39 -1
- django_spire/knowledge/collection/tests/test_urls/test_page_urls.py +12 -4
- django_spire/knowledge/collection/urls/__init__.py +3 -0
- django_spire/knowledge/collection/urls/form_urls.py +2 -0
- django_spire/knowledge/collection/urls/json_urls.py +2 -0
- django_spire/knowledge/collection/urls/page_urls.py +2 -0
- django_spire/knowledge/collection/views/form_views.py +4 -4
- django_spire/knowledge/collection/views/json_views.py +5 -1
- django_spire/knowledge/collection/views/page_views.py +5 -2
- django_spire/knowledge/entry/admin.py +7 -1
- django_spire/knowledge/entry/forms.py +2 -0
- django_spire/knowledge/entry/models.py +2 -0
- django_spire/knowledge/entry/seeding/seed.py +3 -0
- django_spire/knowledge/entry/services/automation_service.py +5 -4
- django_spire/knowledge/entry/services/factory_service.py +7 -5
- django_spire/knowledge/entry/services/service.py +4 -7
- django_spire/knowledge/entry/services/tag_service.py +0 -1
- django_spire/knowledge/entry/services/tool_service.py +1 -0
- django_spire/knowledge/entry/services/transformation_services.py +1 -5
- django_spire/knowledge/entry/tests/factories.py +1 -2
- django_spire/knowledge/entry/tests/test_factory_service.py +20 -0
- django_spire/knowledge/entry/tests/test_models.py +41 -0
- django_spire/knowledge/entry/tests/test_querysets.py +71 -0
- django_spire/knowledge/entry/tests/test_services.py +94 -0
- django_spire/knowledge/entry/tests/test_urls/test_form_urls.py +9 -14
- django_spire/knowledge/entry/tests/test_urls/test_json_urls.py +48 -5
- django_spire/knowledge/entry/tests/test_urls/test_page_urls.py +6 -8
- django_spire/knowledge/entry/tests/test_urls/test_template_urls.py +40 -0
- django_spire/knowledge/entry/urls/form_urls.py +2 -0
- django_spire/knowledge/entry/urls/json_urls.py +2 -0
- django_spire/knowledge/entry/urls/page_urls.py +2 -0
- django_spire/knowledge/entry/urls/template_urls.py +2 -0
- django_spire/knowledge/entry/version/block/choices.py +2 -0
- django_spire/knowledge/entry/version/block/data/data.py +1 -0
- django_spire/knowledge/entry/version/block/data/list/data.py +8 -13
- django_spire/knowledge/entry/version/block/data/list/maps.py +3 -0
- django_spire/knowledge/entry/version/block/data/list/meta.py +1 -2
- django_spire/knowledge/entry/version/block/data/list/tests/__init__.py +0 -0
- django_spire/knowledge/entry/version/block/data/list/tests/test_maps.py +32 -0
- django_spire/knowledge/entry/version/block/data/list/tests/test_meta.py +58 -0
- django_spire/knowledge/entry/version/block/data/maps.py +3 -6
- django_spire/knowledge/entry/version/block/models.py +7 -5
- django_spire/knowledge/entry/version/block/seeding/constants.py +5 -4
- django_spire/knowledge/entry/version/block/services/service.py +2 -3
- django_spire/knowledge/entry/version/block/tests/factories.py +4 -10
- django_spire/knowledge/entry/version/block/tests/test_choices.py +56 -0
- django_spire/knowledge/entry/version/block/tests/test_data.py +90 -0
- django_spire/knowledge/entry/version/block/tests/test_maps.py +37 -0
- django_spire/knowledge/entry/version/block/tests/test_models.py +55 -0
- django_spire/knowledge/entry/version/block/tests/test_querysets.py +35 -0
- django_spire/knowledge/entry/version/block/tests/test_services.py +65 -0
- django_spire/knowledge/entry/version/choices.py +2 -0
- django_spire/knowledge/entry/version/converters/converter.py +1 -1
- django_spire/knowledge/entry/version/converters/docx_converter.py +4 -7
- django_spire/knowledge/entry/version/converters/markdown_converter.py +20 -20
- django_spire/knowledge/entry/version/maps.py +4 -5
- django_spire/knowledge/entry/version/querysets.py +1 -1
- django_spire/knowledge/entry/version/seeding/seeder.py +1 -2
- django_spire/knowledge/entry/version/services/processor_service.py +5 -4
- django_spire/knowledge/entry/version/services/service.py +1 -2
- django_spire/knowledge/entry/version/tests/factories.py +2 -2
- django_spire/knowledge/entry/version/tests/test_choices.py +18 -0
- django_spire/knowledge/entry/version/tests/test_converters/test_docx_converter.py +56 -8
- django_spire/knowledge/entry/version/tests/test_converters/test_markdown_converter.py +78 -0
- django_spire/knowledge/entry/version/tests/test_maps.py +58 -0
- django_spire/knowledge/entry/version/tests/test_models.py +23 -0
- django_spire/knowledge/entry/version/tests/test_querysets.py +26 -0
- django_spire/knowledge/entry/version/tests/test_services.py +62 -0
- django_spire/knowledge/entry/version/tests/test_urls/test_json_urls.py +27 -8
- django_spire/knowledge/entry/version/tests/test_urls/test_page_urls.py +15 -8
- django_spire/knowledge/entry/version/tests/test_urls/test_redirect_urls.py +38 -0
- django_spire/knowledge/entry/version/urls/__init__.py +3 -0
- django_spire/knowledge/entry/version/urls/json_urls.py +2 -1
- django_spire/knowledge/entry/version/urls/page_urls.py +2 -0
- django_spire/knowledge/entry/version/urls/redirect_urls.py +2 -0
- django_spire/knowledge/entry/version/views/json_views.py +5 -1
- django_spire/knowledge/entry/version/views/page_views.py +10 -3
- django_spire/knowledge/entry/version/views/redirect_views.py +5 -1
- django_spire/knowledge/entry/views/form_views.py +16 -8
- django_spire/knowledge/entry/views/json_views.py +3 -1
- django_spire/knowledge/entry/views/page_views.py +8 -2
- django_spire/knowledge/entry/views/template_views.py +7 -1
- django_spire/knowledge/exceptions.py +2 -1
- django_spire/knowledge/intelligence/intel/answer_intel.py +2 -1
- django_spire/knowledge/intelligence/intel/entry_intel.py +0 -1
- django_spire/knowledge/intelligence/workflows/knowledge_workflow.py +4 -5
- django_spire/knowledge/models.py +1 -2
- django_spire/knowledge/tests/__init__.py +0 -0
- django_spire/knowledge/tests/test_templatetags.py +40 -0
- django_spire/knowledge/tests/test_urls/__init__.py +0 -0
- django_spire/knowledge/tests/test_urls/test_page_urls.py +24 -0
- django_spire/knowledge/urls/__init__.py +2 -0
- django_spire/knowledge/urls/page_urls.py +2 -0
- django_spire/knowledge/views/page_views.py +8 -3
- django_spire/notification/admin.py +3 -1
- django_spire/notification/app/admin.py +2 -0
- django_spire/notification/app/apps.py +3 -1
- django_spire/notification/app/exceptions.py +9 -2
- django_spire/notification/app/models.py +8 -4
- django_spire/notification/app/processor.py +22 -26
- django_spire/notification/app/querysets.py +2 -0
- django_spire/notification/app/tests/__init__.py +0 -0
- django_spire/notification/app/tests/factories.py +34 -0
- django_spire/notification/app/tests/test_apps.py +24 -0
- django_spire/notification/app/tests/test_models.py +72 -0
- django_spire/notification/app/tests/test_processor.py +111 -0
- django_spire/notification/app/tests/test_querysets.py +90 -0
- django_spire/notification/app/tests/test_views/__init__.py +0 -0
- django_spire/notification/app/tests/test_views/test_json_views.py +48 -0
- django_spire/notification/app/tests/test_views/test_page_views.py +19 -0
- django_spire/notification/app/urls/__init__.py +3 -1
- django_spire/notification/app/urls/json_urls.py +6 -4
- django_spire/notification/app/urls/page_urls.py +4 -3
- django_spire/notification/app/urls/template_urls.py +4 -2
- django_spire/notification/apps.py +4 -1
- django_spire/notification/email/admin.py +5 -1
- django_spire/notification/email/apps.py +3 -1
- django_spire/notification/email/exceptions.py +4 -2
- django_spire/notification/email/helper.py +5 -3
- django_spire/notification/email/models.py +4 -0
- django_spire/notification/email/processor.py +19 -15
- django_spire/notification/email/querysets.py +3 -0
- django_spire/notification/email/tests/__init__.py +0 -0
- django_spire/notification/email/tests/factories.py +35 -0
- django_spire/notification/email/tests/test_apps.py +24 -0
- django_spire/notification/email/tests/test_models.py +52 -0
- django_spire/notification/email/tests/test_processor.py +92 -0
- django_spire/notification/email/tests/test_querysets.py +43 -0
- django_spire/notification/exceptions.py +17 -2
- django_spire/notification/managers.py +7 -1
- django_spire/notification/maps.py +4 -1
- django_spire/notification/mixins.py +2 -0
- django_spire/notification/models.py +3 -1
- django_spire/notification/processors/notification.py +12 -5
- django_spire/notification/processors/processor.py +2 -0
- django_spire/notification/processors/tests/__init__.py +0 -0
- django_spire/notification/processors/tests/test_notification.py +106 -0
- django_spire/notification/push/admin.py +10 -1
- django_spire/notification/push/apps.py +3 -1
- django_spire/notification/push/models.py +2 -3
- django_spire/notification/push/tests/__init__.py +0 -0
- django_spire/notification/push/tests/test_apps.py +24 -0
- django_spire/notification/push/tests/test_models.py +28 -0
- django_spire/notification/querysets.py +7 -1
- django_spire/notification/sms/admin.py +2 -0
- django_spire/notification/sms/apps.py +4 -1
- django_spire/notification/sms/automations.py +2 -0
- django_spire/notification/sms/choices.py +2 -0
- django_spire/notification/sms/exceptions.py +19 -5
- django_spire/notification/sms/helper.py +33 -23
- django_spire/notification/sms/models.py +5 -1
- django_spire/notification/sms/processor.py +20 -20
- django_spire/notification/sms/querysets.py +2 -0
- django_spire/notification/sms/tests/factories.py +33 -0
- django_spire/notification/sms/tests/test_apps.py +24 -0
- django_spire/notification/sms/tests/test_automation.py +38 -0
- django_spire/notification/sms/tests/test_choices.py +15 -0
- django_spire/notification/sms/tests/test_consts.py +17 -0
- django_spire/notification/sms/tests/test_exceptions.py +27 -0
- django_spire/notification/sms/tests/test_helper.py +50 -0
- django_spire/notification/sms/tests/test_models.py +81 -0
- django_spire/notification/sms/tests/test_processor.py +107 -0
- django_spire/notification/sms/tests/test_tools.py +25 -11
- django_spire/notification/sms/tools.py +16 -5
- django_spire/notification/sms/urls/__init__.py +3 -1
- django_spire/notification/sms/urls/media_urls.py +2 -0
- django_spire/notification/sms/views/media_views.py +14 -4
- django_spire/notification/tests/__init__.py +0 -0
- django_spire/notification/tests/factories.py +26 -0
- django_spire/notification/tests/test_admin.py +55 -0
- django_spire/notification/tests/test_apps.py +30 -0
- django_spire/notification/tests/test_automation.py +18 -0
- django_spire/notification/tests/test_choices.py +59 -0
- django_spire/notification/tests/test_exceptions.py +58 -0
- django_spire/notification/tests/test_managers.py +100 -0
- django_spire/notification/tests/test_maps.py +31 -0
- django_spire/notification/tests/test_models.py +76 -0
- django_spire/notification/tests/test_querysets.py +184 -0
- django_spire/notification/tests/test_utils.py +23 -0
- django_spire/notification/urls.py +3 -1
- django_spire/notification/utils.py +3 -1
- django_spire/settings.py +3 -0
- django_spire/theme/tests/test_context_processor.py +15 -13
- django_spire/theme/tests/test_enums.py +2 -2
- django_spire/theme/tests/test_filesystem.py +2 -5
- django_spire/theme/tests/test_integration.py +12 -12
- django_spire/theme/tests/test_model.py +40 -38
- django_spire/theme/tests/test_views/test_json_views.py +33 -33
- django_spire/theme/urls/json_urls.py +3 -0
- django_spire/theme/urls/page_urls.py +3 -0
- django_spire/urls.py +19 -15
- django_spire/utils.py +13 -4
- {django_spire-0.23.7.dist-info → django_spire-0.23.9.dist-info}/METADATA +2 -2
- {django_spire-0.23.7.dist-info → django_spire-0.23.9.dist-info}/RECORD +534 -362
- {django_spire-0.23.7.dist-info → django_spire-0.23.9.dist-info}/licenses/LICENSE.md +1 -1
- django_spire/contrib/options/tests/test_unit.py +0 -148
- django_spire/contrib/seeding/tests/test_seeding.py +0 -25
- django_spire/core/tests/test_templatetags.py +0 -117
- django_spire/core/tests/tests_shortcuts.py +0 -73
- django_spire/history/activity/tests.py +0 -3
- django_spire/history/activity/views.py +0 -3
- django_spire/knowledge/collection/tests/test_services/test_transformation_service.py +0 -71
- django_spire/notification/app/tests.py +0 -3
- {django_spire-0.23.7.dist-info → django_spire-0.23.9.dist-info}/WHEEL +0 -0
- {django_spire-0.23.7.dist-info → django_spire-0.23.9.dist-info}/top_level.txt +0 -0
|
@@ -1,29 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from django.utils.timezone import now
|
|
2
4
|
from django.conf import settings
|
|
3
5
|
|
|
4
6
|
from twilio.rest import Client
|
|
5
7
|
|
|
6
|
-
from django_spire.notification.choices import
|
|
8
|
+
from django_spire.notification.choices import (
|
|
9
|
+
NotificationTypeChoices,
|
|
7
10
|
NotificationStatusChoices
|
|
8
|
-
|
|
11
|
+
)
|
|
12
|
+
from django_spire.notification.exceptions import InvalidNotificationTypeError
|
|
9
13
|
from django_spire.notification.models import Notification
|
|
10
14
|
from django_spire.notification.processors.processor import BaseNotificationProcessor
|
|
11
|
-
from django_spire.notification.sms.exceptions import
|
|
12
|
-
|
|
15
|
+
from django_spire.notification.sms.exceptions import (
|
|
16
|
+
TwilioAPIConcurrentError,
|
|
17
|
+
TwilioError
|
|
18
|
+
)
|
|
13
19
|
from django_spire.notification.sms.helper import TwilioSMSHelper, BulkTwilioSMSHelper
|
|
14
20
|
|
|
15
21
|
|
|
16
22
|
class SMSNotificationProcessor(BaseNotificationProcessor):
|
|
23
|
+
def _validate_notification_type(self, notification: Notification):
|
|
24
|
+
if notification.type != NotificationTypeChoices.SMS:
|
|
25
|
+
raise InvalidNotificationTypeError(NotificationTypeChoices.SMS, notification.type)
|
|
26
|
+
|
|
17
27
|
def process(self, notification: Notification):
|
|
18
28
|
notification.status = NotificationStatusChoices.PROCESSING
|
|
19
29
|
notification.save()
|
|
20
30
|
|
|
21
31
|
try:
|
|
22
|
-
|
|
23
|
-
raise NotificationException(
|
|
24
|
-
f'SMSNotificationProcessor only processes '
|
|
25
|
-
f'SMS notifications. Was provided {notification.type}'
|
|
26
|
-
)
|
|
32
|
+
self._validate_notification_type(notification)
|
|
27
33
|
|
|
28
34
|
twilio_sms_client = Client(settings.TWILIO_ACCOUNT_SID, settings.TWILIO_AUTH_TOKEN)
|
|
29
35
|
TwilioSMSHelper(notification, twilio_sms_client).send()
|
|
@@ -31,35 +37,29 @@ class SMSNotificationProcessor(BaseNotificationProcessor):
|
|
|
31
37
|
notification.status = NotificationStatusChoices.SENT
|
|
32
38
|
notification.sent_datetime = now()
|
|
33
39
|
except Exception as e:
|
|
34
|
-
|
|
35
|
-
if isinstance(e, TwilioAPIConcurrentException):
|
|
40
|
+
if isinstance(e, TwilioAPIConcurrentError):
|
|
36
41
|
notification.status = NotificationStatusChoices.PENDING
|
|
37
42
|
notification.save()
|
|
38
43
|
return
|
|
39
44
|
|
|
40
45
|
notification.status_message = str(e)
|
|
41
46
|
|
|
42
|
-
if isinstance(e,
|
|
47
|
+
if isinstance(e, TwilioError):
|
|
43
48
|
notification.status = NotificationStatusChoices.ERRORED
|
|
44
49
|
else:
|
|
45
50
|
notification.status = NotificationStatusChoices.FAILED
|
|
46
|
-
raise
|
|
51
|
+
raise
|
|
47
52
|
finally:
|
|
48
53
|
notification.save()
|
|
49
54
|
|
|
50
55
|
def process_list(self, notifications: list):
|
|
51
56
|
twilio_sms_client = Client(settings.TWILIO_ACCOUNT_SID, settings.TWILIO_AUTH_TOKEN)
|
|
57
|
+
|
|
52
58
|
for notification in notifications:
|
|
53
|
-
|
|
54
|
-
raise NotificationException(
|
|
55
|
-
f'SMSNotificationProcessor only processes '
|
|
56
|
-
f'SMS notifications. Was provided {notification.type}'
|
|
57
|
-
)
|
|
59
|
+
self._validate_notification_type(notification)
|
|
58
60
|
|
|
59
61
|
try:
|
|
60
62
|
BulkTwilioSMSHelper(notifications, twilio_sms_client).send_notifications()
|
|
61
|
-
except Exception as e:
|
|
62
|
-
raise e
|
|
63
63
|
finally:
|
|
64
64
|
Notification.objects.bulk_update(
|
|
65
65
|
notifications,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from django.contrib.auth.models import User
|
|
4
|
+
|
|
5
|
+
from django_spire.notification.choices import (
|
|
6
|
+
NotificationStatusChoices,
|
|
7
|
+
NotificationTypeChoices,
|
|
8
|
+
)
|
|
9
|
+
from django_spire.notification.models import Notification
|
|
10
|
+
from django_spire.notification.sms.models import SmsNotification
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def create_test_sms_notification(**kwargs) -> SmsNotification:
|
|
14
|
+
if 'notification' not in kwargs:
|
|
15
|
+
user = kwargs.pop('user', None) or User.objects.first()
|
|
16
|
+
notification = Notification.objects.create(
|
|
17
|
+
user=user,
|
|
18
|
+
type=NotificationTypeChoices.SMS,
|
|
19
|
+
title=kwargs.pop('title', 'Test SMS Notification'),
|
|
20
|
+
body=kwargs.pop('body', 'This is a test SMS notification.'),
|
|
21
|
+
url=kwargs.pop('url', ''),
|
|
22
|
+
status=kwargs.pop('status', NotificationStatusChoices.PENDING),
|
|
23
|
+
priority=kwargs.pop('priority', 'low'),
|
|
24
|
+
)
|
|
25
|
+
kwargs['notification'] = notification
|
|
26
|
+
|
|
27
|
+
data = {
|
|
28
|
+
'to_phone_number': '5551234567',
|
|
29
|
+
'media_url': None,
|
|
30
|
+
'temporary_media': None,
|
|
31
|
+
}
|
|
32
|
+
data.update(kwargs)
|
|
33
|
+
return SmsNotification.objects.create(**data)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from django.apps import apps
|
|
4
|
+
|
|
5
|
+
from django_spire.core.tests.test_cases import BaseTestCase
|
|
6
|
+
from django_spire.notification.sms.apps import NotificationSmsConfig
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class NotificationSmsConfigTests(BaseTestCase):
|
|
10
|
+
def test_app_name(self):
|
|
11
|
+
assert NotificationSmsConfig.name == 'django_spire.notification.sms'
|
|
12
|
+
|
|
13
|
+
def test_app_label(self):
|
|
14
|
+
assert NotificationSmsConfig.label == 'django_spire_notification_sms'
|
|
15
|
+
|
|
16
|
+
def test_default_auto_field(self):
|
|
17
|
+
assert NotificationSmsConfig.default_auto_field == 'django.db.models.BigAutoField'
|
|
18
|
+
|
|
19
|
+
def test_required_apps(self):
|
|
20
|
+
expected = ('django_spire_core', 'django_spire_notification')
|
|
21
|
+
assert expected == NotificationSmsConfig.REQUIRED_APPS
|
|
22
|
+
|
|
23
|
+
def test_app_is_installed(self):
|
|
24
|
+
assert apps.is_installed('django_spire.notification.sms')
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
from django.utils.timezone import now, timedelta
|
|
6
|
+
|
|
7
|
+
from django_spire.core.tests.test_cases import BaseTestCase
|
|
8
|
+
from django_spire.notification.sms.choices import SmsMediaContentTypeChoices
|
|
9
|
+
from django_spire.notification.sms.models import SmsTemporaryMedia
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SmsTemporaryMediaTests(BaseTestCase):
|
|
13
|
+
def setUp(self):
|
|
14
|
+
super().setUp()
|
|
15
|
+
self.expired_media = SmsTemporaryMedia.objects.create(
|
|
16
|
+
content='base64content',
|
|
17
|
+
content_type=SmsMediaContentTypeChoices.PNG,
|
|
18
|
+
name='expired.png',
|
|
19
|
+
expire_datetime=now() - timedelta(hours=1),
|
|
20
|
+
external_access_key=uuid.uuid4(),
|
|
21
|
+
)
|
|
22
|
+
self.valid_media = SmsTemporaryMedia.objects.create(
|
|
23
|
+
content='base64content',
|
|
24
|
+
content_type=SmsMediaContentTypeChoices.PNG,
|
|
25
|
+
name='valid.png',
|
|
26
|
+
expire_datetime=now() + timedelta(hours=1),
|
|
27
|
+
external_access_key=uuid.uuid4(),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def test_is_expired_true(self):
|
|
31
|
+
assert self.expired_media.is_expired() is True
|
|
32
|
+
|
|
33
|
+
def test_is_expired_false(self):
|
|
34
|
+
assert self.valid_media.is_expired() is False
|
|
35
|
+
|
|
36
|
+
def test_external_url_contains_access_key(self):
|
|
37
|
+
url = self.valid_media.external_url
|
|
38
|
+
assert str(self.valid_media.external_access_key) in url
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from django_spire.core.tests.test_cases import BaseTestCase
|
|
4
|
+
from django_spire.notification.sms.choices import SmsMediaContentTypeChoices
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SmsMediaContentTypeChoicesTests(BaseTestCase):
|
|
8
|
+
def test_png_value(self):
|
|
9
|
+
assert SmsMediaContentTypeChoices.PNG == 'image/png'
|
|
10
|
+
|
|
11
|
+
def test_jpeg_value(self):
|
|
12
|
+
assert SmsMediaContentTypeChoices.JPEG == 'image/jpeg'
|
|
13
|
+
|
|
14
|
+
def test_choices_count(self):
|
|
15
|
+
assert len(SmsMediaContentTypeChoices.choices) == 2
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from django_spire.core.tests.test_cases import BaseTestCase
|
|
4
|
+
from django_spire.notification.sms.consts import (
|
|
5
|
+
TWILIO_SMS_BATCH_SIZE_NAME,
|
|
6
|
+
TWILIO_UNSUCCESSFUL_STATUSES,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SmsConstsTests(BaseTestCase):
|
|
11
|
+
def test_twilio_unsuccessful_statuses(self):
|
|
12
|
+
assert 'failed' in TWILIO_UNSUCCESSFUL_STATUSES
|
|
13
|
+
assert 'undelivered' in TWILIO_UNSUCCESSFUL_STATUSES
|
|
14
|
+
assert len(TWILIO_UNSUCCESSFUL_STATUSES) == 2
|
|
15
|
+
|
|
16
|
+
def test_twilio_sms_batch_size_name(self):
|
|
17
|
+
assert TWILIO_SMS_BATCH_SIZE_NAME == 'TWILIO_SMS_BATCH_SIZE'
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from django_spire.core.tests.test_cases import BaseTestCase
|
|
4
|
+
from django_spire.notification.sms.exceptions import (
|
|
5
|
+
SmsNotificationError,
|
|
6
|
+
SmsTemporaryMediaError,
|
|
7
|
+
TwilioAPIConcurrentError,
|
|
8
|
+
TwilioError,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SmsExceptionsTests(BaseTestCase):
|
|
13
|
+
def test_sms_notification_error_message(self):
|
|
14
|
+
error = SmsNotificationError('SMS error')
|
|
15
|
+
assert str(error) == 'SMS error'
|
|
16
|
+
|
|
17
|
+
def test_sms_temporary_media_error_message(self):
|
|
18
|
+
error = SmsTemporaryMediaError('Media error')
|
|
19
|
+
assert str(error) == 'Media error'
|
|
20
|
+
|
|
21
|
+
def test_twilio_error_message(self):
|
|
22
|
+
error = TwilioError('Twilio error')
|
|
23
|
+
assert str(error) == 'Twilio error'
|
|
24
|
+
|
|
25
|
+
def test_twilio_api_concurrent_error_message(self):
|
|
26
|
+
error = TwilioAPIConcurrentError('Concurrent error')
|
|
27
|
+
assert str(error) == 'Concurrent error'
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from unittest.mock import MagicMock
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from django_spire.auth.user.tests.factories import create_user
|
|
8
|
+
from django_spire.core.tests.test_cases import BaseTestCase
|
|
9
|
+
from django_spire.notification.sms.exceptions import TwilioError
|
|
10
|
+
from django_spire.notification.sms.helper import TwilioSMSHelper
|
|
11
|
+
from django_spire.notification.sms.tests.factories import create_test_sms_notification
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TwilioSMSHelperTests(BaseTestCase):
|
|
15
|
+
def setUp(self):
|
|
16
|
+
super().setUp()
|
|
17
|
+
self.user = create_user(username='test_helper_user')
|
|
18
|
+
self.sms_notification = create_test_sms_notification(user=self.user)
|
|
19
|
+
self.mock_client = MagicMock()
|
|
20
|
+
|
|
21
|
+
def test_format_phone_number_10_digits(self):
|
|
22
|
+
result = TwilioSMSHelper._format_phone_number('5551234567')
|
|
23
|
+
assert result == '+15551234567'
|
|
24
|
+
|
|
25
|
+
def test_format_phone_number_11_digits_with_country_code(self):
|
|
26
|
+
result = TwilioSMSHelper._format_phone_number('15551234567')
|
|
27
|
+
assert result == '+15551234567'
|
|
28
|
+
|
|
29
|
+
def test_format_phone_number_invalid_raises_error(self):
|
|
30
|
+
with pytest.raises(TwilioError):
|
|
31
|
+
TwilioSMSHelper._format_phone_number('123')
|
|
32
|
+
|
|
33
|
+
def test_format_phone_number_too_long_raises_error(self):
|
|
34
|
+
with pytest.raises(TwilioError):
|
|
35
|
+
TwilioSMSHelper._format_phone_number('123456789012345')
|
|
36
|
+
|
|
37
|
+
def test_message_format(self):
|
|
38
|
+
helper = TwilioSMSHelper(
|
|
39
|
+
self.sms_notification.notification,
|
|
40
|
+
self.mock_client
|
|
41
|
+
)
|
|
42
|
+
expected = f'{self.sms_notification.notification.title}: {self.sms_notification.notification.body}'
|
|
43
|
+
assert helper.message == expected
|
|
44
|
+
|
|
45
|
+
def test_to_phone_number_formatted(self):
|
|
46
|
+
helper = TwilioSMSHelper(
|
|
47
|
+
self.sms_notification.notification,
|
|
48
|
+
self.mock_client
|
|
49
|
+
)
|
|
50
|
+
assert helper.to_phone_number.startswith('+1')
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
from django.utils.timezone import now, timedelta
|
|
6
|
+
|
|
7
|
+
from django_spire.core.tests.test_cases import BaseTestCase
|
|
8
|
+
from django_spire.notification.models import Notification
|
|
9
|
+
from django_spire.notification.sms.choices import SmsMediaContentTypeChoices
|
|
10
|
+
from django_spire.notification.sms.models import SmsTemporaryMedia
|
|
11
|
+
from django_spire.notification.sms.tests.factories import create_test_sms_notification
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SmsNotificationModelTests(BaseTestCase):
|
|
15
|
+
def setUp(self):
|
|
16
|
+
super().setUp()
|
|
17
|
+
self.sms_notification = create_test_sms_notification()
|
|
18
|
+
|
|
19
|
+
def test_str(self):
|
|
20
|
+
expected = f'{self.sms_notification.to_phone_number} - {self.sms_notification.notification.title}'
|
|
21
|
+
assert str(self.sms_notification) == expected
|
|
22
|
+
|
|
23
|
+
def test_notification_relationship(self):
|
|
24
|
+
assert self.sms_notification.notification is not None
|
|
25
|
+
assert isinstance(self.sms_notification.notification, Notification)
|
|
26
|
+
|
|
27
|
+
def test_to_phone_number(self):
|
|
28
|
+
assert self.sms_notification.to_phone_number == '5551234567'
|
|
29
|
+
|
|
30
|
+
def test_media_url_default_none(self):
|
|
31
|
+
assert self.sms_notification.media_url is None
|
|
32
|
+
|
|
33
|
+
def test_temporary_media_default_none(self):
|
|
34
|
+
assert self.sms_notification.temporary_media is None
|
|
35
|
+
|
|
36
|
+
def test_with_media_url(self):
|
|
37
|
+
sms_notification = create_test_sms_notification(
|
|
38
|
+
media_url='https://example.com/image.png'
|
|
39
|
+
)
|
|
40
|
+
assert sms_notification.media_url == 'https://example.com/image.png'
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class SmsTemporaryMediaModelTests(BaseTestCase):
|
|
44
|
+
def setUp(self):
|
|
45
|
+
super().setUp()
|
|
46
|
+
self.temporary_media = SmsTemporaryMedia.objects.create(
|
|
47
|
+
content='base64encodedcontent',
|
|
48
|
+
content_type=SmsMediaContentTypeChoices.PNG,
|
|
49
|
+
name='test_image.png',
|
|
50
|
+
expire_datetime=now() + timedelta(hours=1),
|
|
51
|
+
external_access_key=uuid.uuid4(),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def test_str(self):
|
|
55
|
+
expected = f'{self.temporary_media.name} - {self.temporary_media.content_type}'
|
|
56
|
+
assert str(self.temporary_media) == expected
|
|
57
|
+
|
|
58
|
+
def test_is_expired_false(self):
|
|
59
|
+
assert self.temporary_media.is_expired() is False
|
|
60
|
+
|
|
61
|
+
def test_is_expired_true(self):
|
|
62
|
+
self.temporary_media.expire_datetime = now() - timedelta(hours=1)
|
|
63
|
+
self.temporary_media.save()
|
|
64
|
+
assert self.temporary_media.is_expired() is True
|
|
65
|
+
|
|
66
|
+
def test_external_url(self):
|
|
67
|
+
url = self.temporary_media.external_url
|
|
68
|
+
assert str(self.temporary_media.external_access_key) in url
|
|
69
|
+
|
|
70
|
+
def test_content_type_png(self):
|
|
71
|
+
assert self.temporary_media.content_type == SmsMediaContentTypeChoices.PNG
|
|
72
|
+
|
|
73
|
+
def test_content_type_jpeg(self):
|
|
74
|
+
media = SmsTemporaryMedia.objects.create(
|
|
75
|
+
content='base64encodedcontent',
|
|
76
|
+
content_type=SmsMediaContentTypeChoices.JPEG,
|
|
77
|
+
name='test_image.jpg',
|
|
78
|
+
expire_datetime=now() + timedelta(hours=1),
|
|
79
|
+
external_access_key=uuid.uuid4(),
|
|
80
|
+
)
|
|
81
|
+
assert media.content_type == SmsMediaContentTypeChoices.JPEG
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from unittest.mock import MagicMock, patch
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from django_spire.auth.user.tests.factories import create_user
|
|
8
|
+
from django_spire.core.tests.test_cases import BaseTestCase
|
|
9
|
+
from django_spire.notification.choices import (
|
|
10
|
+
NotificationStatusChoices,
|
|
11
|
+
NotificationTypeChoices,
|
|
12
|
+
)
|
|
13
|
+
from django_spire.notification.exceptions import NotificationError
|
|
14
|
+
from django_spire.notification.models import Notification
|
|
15
|
+
from django_spire.notification.sms.processor import SMSNotificationProcessor
|
|
16
|
+
from django_spire.notification.sms.tests.factories import create_test_sms_notification
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SMSNotificationProcessorTests(BaseTestCase):
|
|
20
|
+
def setUp(self):
|
|
21
|
+
super().setUp()
|
|
22
|
+
self.user = create_user(username='test_sms_processor_user')
|
|
23
|
+
self.processor = SMSNotificationProcessor()
|
|
24
|
+
|
|
25
|
+
@patch('django_spire.notification.sms.processor.Client')
|
|
26
|
+
@patch('django_spire.notification.sms.processor.TwilioSMSHelper')
|
|
27
|
+
def test_process_sets_status_to_sent(
|
|
28
|
+
self,
|
|
29
|
+
mock_helper_class: MagicMock,
|
|
30
|
+
mock_client_class: MagicMock
|
|
31
|
+
):
|
|
32
|
+
mock_helper = MagicMock()
|
|
33
|
+
mock_helper_class.return_value = mock_helper
|
|
34
|
+
|
|
35
|
+
sms_notification = create_test_sms_notification(user=self.user)
|
|
36
|
+
notification = sms_notification.notification
|
|
37
|
+
|
|
38
|
+
self.processor.process(notification)
|
|
39
|
+
|
|
40
|
+
notification.refresh_from_db()
|
|
41
|
+
assert notification.status == NotificationStatusChoices.SENT
|
|
42
|
+
|
|
43
|
+
@patch('django_spire.notification.sms.processor.Client')
|
|
44
|
+
@patch('django_spire.notification.sms.processor.TwilioSMSHelper')
|
|
45
|
+
def test_process_sets_sent_datetime(
|
|
46
|
+
self,
|
|
47
|
+
mock_helper_class: MagicMock,
|
|
48
|
+
mock_client_class: MagicMock
|
|
49
|
+
):
|
|
50
|
+
mock_helper = MagicMock()
|
|
51
|
+
mock_helper_class.return_value = mock_helper
|
|
52
|
+
|
|
53
|
+
sms_notification = create_test_sms_notification(user=self.user)
|
|
54
|
+
notification = sms_notification.notification
|
|
55
|
+
|
|
56
|
+
self.processor.process(notification)
|
|
57
|
+
|
|
58
|
+
notification.refresh_from_db()
|
|
59
|
+
assert notification.sent_datetime is not None
|
|
60
|
+
|
|
61
|
+
@patch('django_spire.notification.sms.processor.Client')
|
|
62
|
+
@patch('django_spire.notification.sms.processor.TwilioSMSHelper')
|
|
63
|
+
def test_process_calls_send(
|
|
64
|
+
self,
|
|
65
|
+
mock_helper_class: MagicMock,
|
|
66
|
+
mock_client_class: MagicMock
|
|
67
|
+
):
|
|
68
|
+
mock_helper = MagicMock()
|
|
69
|
+
mock_helper_class.return_value = mock_helper
|
|
70
|
+
|
|
71
|
+
sms_notification = create_test_sms_notification(user=self.user)
|
|
72
|
+
notification = sms_notification.notification
|
|
73
|
+
|
|
74
|
+
self.processor.process(notification)
|
|
75
|
+
|
|
76
|
+
mock_helper.send.assert_called_once()
|
|
77
|
+
|
|
78
|
+
def test_process_raises_error_for_wrong_type(self):
|
|
79
|
+
notification = Notification.objects.create(
|
|
80
|
+
user=self.user,
|
|
81
|
+
type=NotificationTypeChoices.APP,
|
|
82
|
+
title='Test',
|
|
83
|
+
body='Test',
|
|
84
|
+
status=NotificationStatusChoices.PENDING,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
with pytest.raises(NotificationError):
|
|
88
|
+
self.processor.process(notification)
|
|
89
|
+
|
|
90
|
+
@patch('django_spire.notification.sms.processor.Client')
|
|
91
|
+
@patch('django_spire.notification.sms.processor.BulkTwilioSMSHelper')
|
|
92
|
+
def test_process_list(
|
|
93
|
+
self,
|
|
94
|
+
mock_helper_class: MagicMock,
|
|
95
|
+
mock_client_class: MagicMock
|
|
96
|
+
):
|
|
97
|
+
mock_helper = MagicMock()
|
|
98
|
+
mock_helper_class.return_value = mock_helper
|
|
99
|
+
|
|
100
|
+
notifications = [
|
|
101
|
+
create_test_sms_notification(user=self.user).notification
|
|
102
|
+
for _ in range(3)
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
self.processor.process_list(notifications)
|
|
106
|
+
|
|
107
|
+
mock_helper.send_notifications.assert_called_once()
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
1
5
|
from django_spire.core.tests.test_cases import BaseTestCase
|
|
2
6
|
from django_spire.notification.sms.tools import format_to_international_phone_number
|
|
3
7
|
|
|
4
8
|
|
|
5
9
|
class TestSMSTools(BaseTestCase):
|
|
6
|
-
|
|
7
10
|
def test_local_to_international_phone_number(self):
|
|
8
11
|
phone_numbers = (
|
|
9
12
|
'622 415 2736',
|
|
@@ -19,6 +22,7 @@ class TestSMSTools(BaseTestCase):
|
|
|
19
22
|
'368.447-9514',
|
|
20
23
|
'4563219876'
|
|
21
24
|
)
|
|
25
|
+
|
|
22
26
|
expected_phone_numbers = (
|
|
23
27
|
'+16224152736',
|
|
24
28
|
'+18815534599',
|
|
@@ -33,8 +37,12 @@ class TestSMSTools(BaseTestCase):
|
|
|
33
37
|
'+13684479514',
|
|
34
38
|
'+14563219876'
|
|
35
39
|
)
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
|
|
41
|
+
formatted_phone_numbers = tuple(
|
|
42
|
+
format_to_international_phone_number(phone_number)
|
|
43
|
+
for phone_number in phone_numbers
|
|
44
|
+
)
|
|
45
|
+
assert formatted_phone_numbers == expected_phone_numbers
|
|
38
46
|
|
|
39
47
|
def test_invalid_phone_number(self):
|
|
40
48
|
phone_numbers = (
|
|
@@ -43,13 +51,19 @@ class TestSMSTools(BaseTestCase):
|
|
|
43
51
|
'1',
|
|
44
52
|
'12345678901234567890',
|
|
45
53
|
'',
|
|
46
|
-
'32145698765',
|
|
47
|
-
'403000123',
|
|
48
|
-
'36800012',
|
|
54
|
+
'32145698765',
|
|
55
|
+
'403000123',
|
|
56
|
+
'36800012',
|
|
49
57
|
)
|
|
58
|
+
|
|
50
59
|
for phone_number in phone_numbers:
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
60
|
+
with pytest.raises(ValueError):
|
|
61
|
+
format_to_international_phone_number(phone_number)
|
|
62
|
+
|
|
63
|
+
def test_format_with_country_code(self):
|
|
64
|
+
result = format_to_international_phone_number('5551234567', country_code='1')
|
|
65
|
+
assert result == '+15551234567'
|
|
66
|
+
|
|
67
|
+
def test_format_already_has_country_code(self):
|
|
68
|
+
result = format_to_international_phone_number('15551234567', country_code='1')
|
|
69
|
+
assert result == '+15551234567'
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import re
|
|
2
4
|
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
3
7
|
from django_spire.notification.choices import NotificationStatusChoices
|
|
4
|
-
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from django_spire.notification.sms.models import SmsTemporaryMedia
|
|
5
11
|
|
|
6
12
|
|
|
7
13
|
def update_unsent_notification_status_for_deleted_temporary_media(
|
|
@@ -19,21 +25,26 @@ def format_to_international_phone_number(phone_number: str, country_code: str='1
|
|
|
19
25
|
Args: phone_number:
|
|
20
26
|
Returns: international phone number format
|
|
21
27
|
"""
|
|
28
|
+
|
|
22
29
|
if not phone_number:
|
|
23
|
-
|
|
30
|
+
message = f'No phone number provided: {phone_number}'
|
|
31
|
+
raise ValueError(message)
|
|
24
32
|
|
|
25
33
|
# Remove extension numbers
|
|
26
34
|
main_number = re.split(r'(?:ext\.?|x)\s*\d+', phone_number, flags=re.IGNORECASE)[0]
|
|
27
35
|
|
|
28
36
|
# Get all digit characters
|
|
29
37
|
digit_number = re.sub(r'\D', '', main_number)
|
|
38
|
+
|
|
30
39
|
if digit_number.startswith(country_code) and len(digit_number) == 10 + len(country_code):
|
|
31
40
|
digit_number = digit_number[len(country_code):]
|
|
32
41
|
|
|
33
42
|
# Check if the number is in local format or already in international format
|
|
34
43
|
if len(digit_number) == 10:
|
|
35
44
|
return f'+{country_code}{digit_number}'
|
|
36
|
-
|
|
45
|
+
|
|
46
|
+
if len(digit_number) == 10 + len(country_code) and digit_number.startswith(country_code):
|
|
37
47
|
return f'+{digit_number}'
|
|
38
|
-
|
|
39
|
-
|
|
48
|
+
|
|
49
|
+
message = f'Invalid phone number: {phone_number}'
|
|
50
|
+
raise ValueError(message)
|
|
@@ -1,29 +1,39 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import base64
|
|
2
|
-
|
|
4
|
+
|
|
3
5
|
from io import BytesIO
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
4
7
|
|
|
5
8
|
from PIL import Image
|
|
6
9
|
|
|
7
10
|
from django.http import HttpResponse
|
|
8
11
|
from django.views.decorators.csrf import csrf_exempt
|
|
9
12
|
|
|
10
|
-
from django_spire.notification.sms.exceptions import
|
|
13
|
+
from django_spire.notification.sms.exceptions import SmsTemporaryMediaError
|
|
11
14
|
from django_spire.notification.sms.models import SmsTemporaryMedia
|
|
12
15
|
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
import uuid
|
|
18
|
+
|
|
19
|
+
from django.core.handlers.wsgi import WSGIRequest
|
|
20
|
+
|
|
13
21
|
|
|
14
22
|
@csrf_exempt
|
|
15
|
-
def external_temporary_media_view(request, external_access_key: uuid.UUID) -> HttpResponse:
|
|
23
|
+
def external_temporary_media_view(request: WSGIRequest, external_access_key: uuid.UUID) -> HttpResponse:
|
|
16
24
|
try:
|
|
17
25
|
temporary_media = SmsTemporaryMedia.objects.get(external_access_key=external_access_key)
|
|
18
26
|
except SmsTemporaryMedia.DoesNotExist:
|
|
19
27
|
temporary_media = None
|
|
20
28
|
|
|
21
29
|
if temporary_media is None or temporary_media.content == '':
|
|
22
|
-
|
|
30
|
+
message = 'Content for Temporary Media cannot be empty'
|
|
31
|
+
raise SmsTemporaryMediaError(message)
|
|
23
32
|
|
|
24
33
|
image = Image.open(
|
|
25
34
|
BytesIO(base64.b64decode(temporary_media.content))
|
|
26
35
|
)
|
|
36
|
+
|
|
27
37
|
image = image.convert('P', palette=Image.ADAPTIVE, colors=32)
|
|
28
38
|
|
|
29
39
|
buffer = BytesIO()
|
|
File without changes
|