django-spire 0.23.6__py3-none-any.whl → 0.23.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_spire/ai/admin.py +11 -11
- django_spire/ai/chat/apps.py +1 -0
- django_spire/ai/chat/templates/django_spire/ai/chat/widget/dialog_widget.html +1 -1
- django_spire/ai/chat/tests/factories.py +15 -0
- django_spire/ai/chat/tests/test_controller.py +45 -0
- django_spire/ai/chat/tests/test_models.py +301 -0
- django_spire/ai/chat/tests/test_prompts.py +48 -0
- django_spire/ai/chat/tests/test_responses.py +208 -0
- django_spire/ai/chat/tests/test_router/test_base_chat_router.py +66 -6
- django_spire/ai/chat/tests/test_router/test_chat_workflow.py +73 -3
- django_spire/ai/chat/tests/test_router/test_integration.py +86 -6
- django_spire/ai/chat/tests/test_router/test_intent_decoder.py +93 -1
- django_spire/ai/chat/tests/test_router/test_message_intel.py +60 -1
- django_spire/ai/chat/tests/test_router/test_spire_chat_router.py +110 -0
- django_spire/ai/chat/tests/test_urls/test_json_urls.py +202 -1
- django_spire/ai/context/tests/__init__.py +0 -0
- django_spire/ai/context/tests/test_context.py +188 -0
- django_spire/ai/decorators.py +7 -6
- django_spire/ai/prompt/tests/test_bots.py +100 -10
- django_spire/ai/prompt/tests/test_prompt_intel.py +83 -0
- django_spire/ai/prompt/tests/test_prompt_tuning.py +126 -0
- django_spire/ai/sms/decorators.py +8 -2
- django_spire/ai/sms/tests/test_sms.py +240 -16
- django_spire/ai/sms/tests/test_sms_intel.py +42 -0
- django_spire/ai/sms/tests/test_webhook.py +155 -7
- django_spire/ai/sms/views.py +23 -24
- django_spire/ai/tests/test_ai.py +131 -7
- django_spire/auth/apps.py +4 -2
- django_spire/auth/controller/controller.py +36 -23
- django_spire/auth/controller/exceptions.py +9 -0
- django_spire/auth/group/admin.py +1 -0
- django_spire/auth/group/apps.py +2 -0
- django_spire/auth/group/factories.py +17 -8
- django_spire/auth/group/forms.py +7 -0
- django_spire/auth/group/tests/test_factories.py +146 -0
- django_spire/auth/group/tests/test_forms.py +282 -0
- django_spire/auth/group/tests/test_models.py +192 -0
- django_spire/auth/group/tests/test_querysets.py +98 -0
- django_spire/auth/group/tests/test_utils.py +341 -0
- django_spire/auth/group/tests/test_views.py +377 -0
- django_spire/auth/group/urls/__init__.py +3 -1
- django_spire/auth/group/urls/form_urls.py +2 -0
- django_spire/auth/group/urls/json_urls.py +3 -0
- django_spire/auth/group/urls/page_urls.py +2 -0
- django_spire/auth/group/utils.py +6 -2
- django_spire/auth/group/views/form_views.py +6 -3
- django_spire/auth/group/views/json_views.py +6 -2
- django_spire/auth/mfa/admin.py +2 -0
- django_spire/auth/mfa/apps.py +2 -0
- django_spire/auth/mfa/forms.py +1 -0
- django_spire/auth/mfa/querysets.py +9 -2
- django_spire/auth/mfa/tests/test_models.py +233 -0
- django_spire/auth/mfa/tests/test_utils.py +106 -0
- django_spire/auth/mfa/urls/__init__.py +2 -0
- django_spire/auth/mfa/urls/page_urls.py +2 -0
- django_spire/auth/mfa/urls/redirect_urls.py +2 -0
- django_spire/auth/mfa/views/page_views.py +2 -1
- django_spire/auth/permissions/consts.py +2 -2
- django_spire/auth/permissions/decorators.py +8 -8
- django_spire/auth/permissions/permissions.py +28 -35
- django_spire/auth/permissions/tests/test_decorators.py +333 -0
- django_spire/auth/permissions/tests/test_permissions.py +337 -0
- django_spire/auth/permissions/tests/test_tools.py +305 -0
- django_spire/auth/permissions/tools.py +21 -15
- django_spire/auth/seeding/seed.py +3 -0
- django_spire/auth/seeding/seeder.py +2 -0
- django_spire/auth/tests/test_controller.py +323 -0
- django_spire/auth/tests/test_url_endpoints.py +9 -9
- django_spire/auth/tests/test_views.py +406 -0
- django_spire/auth/urls/admin_urls.py +2 -0
- django_spire/auth/urls/redirect_urls.py +2 -0
- django_spire/auth/user/apps.py +2 -0
- django_spire/auth/user/forms.py +9 -0
- django_spire/auth/user/models.py +1 -1
- django_spire/auth/user/services/services.py +1 -0
- django_spire/auth/user/tests/factories.py +14 -13
- django_spire/auth/user/tests/test_factories.py +166 -2
- django_spire/auth/user/tests/test_forms.py +573 -0
- django_spire/auth/user/tests/test_models.py +257 -0
- django_spire/auth/user/tests/test_services.py +200 -0
- django_spire/auth/user/tests/test_tools.py +153 -0
- django_spire/auth/user/tests/test_user_factories.py +139 -0
- django_spire/auth/user/tests/test_views.py +363 -0
- django_spire/auth/user/tools.py +7 -1
- django_spire/auth/user/urls/form_urls.py +3 -0
- django_spire/auth/user/urls/page_urls.py +3 -0
- django_spire/auth/user/views/form_views.py +19 -10
- django_spire/auth/user/views/page_views.py +8 -2
- django_spire/auth/views/redirect_views.py +14 -9
- django_spire/comment/admin.py +2 -0
- django_spire/comment/apps.py +2 -0
- django_spire/comment/templatetags/comment_tags.py +1 -0
- django_spire/comment/tests/test_forms.py +27 -0
- django_spire/comment/tests/test_models.py +215 -0
- django_spire/comment/tests/test_querysets.py +101 -0
- django_spire/comment/tests/test_utils.py +90 -0
- django_spire/comment/urls.py +2 -0
- django_spire/comment/utils.py +22 -13
- django_spire/comment/views.py +1 -1
- django_spire/conf.py +8 -6
- django_spire/consts.py +1 -1
- django_spire/contrib/breadcrumb/apps.py +2 -0
- django_spire/contrib/breadcrumb/breadcrumbs.py +18 -18
- django_spire/contrib/breadcrumb/tests/test_breadcrumbs.py +198 -0
- django_spire/contrib/constructor/__init__.py +3 -3
- django_spire/contrib/constructor/constructor.py +15 -15
- django_spire/contrib/constructor/django_model_constructor.py +5 -4
- django_spire/contrib/constructor/exceptions.py +5 -3
- django_spire/contrib/constructor/tests/__init__.py +0 -0
- django_spire/contrib/constructor/tests/test_constructor.py +193 -0
- django_spire/contrib/form/tests/__init__.py +0 -0
- django_spire/contrib/form/tests/test_forms.py +203 -0
- django_spire/contrib/generic_views/modal_views.py +2 -1
- django_spire/contrib/generic_views/portal_views.py +20 -19
- django_spire/contrib/generic_views/tests/__init__.py +0 -0
- django_spire/contrib/generic_views/tests/test_views.py +459 -0
- django_spire/contrib/help/apps.py +2 -0
- django_spire/contrib/help/templatetags/help.py +1 -0
- django_spire/contrib/help/tests/__init__.py +0 -0
- django_spire/contrib/help/tests/test_templatetags.py +100 -0
- django_spire/contrib/options/mixins.py +6 -5
- django_spire/contrib/options/tests/factories.py +5 -1
- django_spire/contrib/options/tests/test_options.py +234 -0
- django_spire/contrib/ordering/exceptions.py +7 -3
- django_spire/contrib/ordering/mixins.py +2 -0
- django_spire/contrib/ordering/querysets.py +3 -1
- django_spire/contrib/ordering/services/processor_service.py +8 -4
- django_spire/contrib/ordering/services/service.py +1 -2
- django_spire/contrib/ordering/tests/__init__.py +0 -0
- django_spire/contrib/ordering/tests/test_ordering.py +165 -0
- django_spire/contrib/ordering/validators.py +6 -6
- django_spire/contrib/pagination/templatetags/pagination_tags.py +12 -5
- django_spire/contrib/pagination/tests/__init__.py +0 -0
- django_spire/contrib/pagination/tests/test_pagination.py +179 -0
- django_spire/contrib/performance/decorators.py +16 -6
- django_spire/contrib/performance/tests/__init__.py +0 -0
- django_spire/contrib/performance/tests/test_performance.py +107 -0
- django_spire/contrib/progress/__init__.py +1 -3
- django_spire/contrib/progress/static/django_spire/js/contrib/progress/progress.js +38 -82
- django_spire/contrib/queryset/enums.py +3 -1
- django_spire/contrib/queryset/filter_tools.py +10 -5
- django_spire/contrib/queryset/mixins.py +16 -16
- django_spire/contrib/queryset/tests/__init__.py +0 -0
- django_spire/contrib/queryset/tests/test_queryset.py +137 -0
- django_spire/contrib/seeding/field/base.py +13 -7
- django_spire/contrib/seeding/field/callable.py +8 -1
- django_spire/contrib/seeding/field/cleaners.py +5 -5
- django_spire/contrib/seeding/field/custom.py +20 -10
- django_spire/contrib/seeding/field/django/seeder.py +8 -6
- django_spire/contrib/seeding/field/enums.py +7 -5
- django_spire/contrib/seeding/field/override.py +16 -6
- django_spire/contrib/seeding/field/static.py +9 -2
- django_spire/contrib/seeding/field/tests/test_base.py +18 -14
- django_spire/contrib/seeding/field/tests/test_callable.py +13 -9
- django_spire/contrib/seeding/field/tests/test_cleaners.py +51 -38
- django_spire/contrib/seeding/field/tests/test_static.py +13 -9
- django_spire/contrib/seeding/intelligence/bots/seeder_generator_bot.py +2 -0
- django_spire/contrib/seeding/intelligence/intel.py +5 -1
- django_spire/contrib/seeding/intelligence/prompts/factory.py +6 -1
- django_spire/contrib/seeding/intelligence/prompts/foreign_key_selection_prompt.py +6 -1
- django_spire/contrib/seeding/intelligence/prompts/generate_django_model_seeder_prompts.py +2 -0
- django_spire/contrib/seeding/intelligence/prompts/generic_relationship_selection_prompt.py +7 -1
- django_spire/contrib/seeding/intelligence/prompts/hierarchical_selection_prompt.py +6 -2
- django_spire/contrib/seeding/intelligence/prompts/model_field_choices_prompt.py +8 -2
- django_spire/contrib/seeding/intelligence/prompts/negation_prompt.py +2 -0
- django_spire/contrib/seeding/intelligence/prompts/objective_prompt.py +6 -1
- django_spire/contrib/seeding/management/commands/seeding.py +9 -3
- django_spire/contrib/seeding/management/example.py +2 -0
- django_spire/contrib/seeding/model/base.py +16 -7
- django_spire/contrib/seeding/model/config.py +31 -15
- django_spire/contrib/seeding/model/django/config.py +13 -13
- django_spire/contrib/seeding/model/django/seeder.py +4 -4
- django_spire/contrib/seeding/model/django/tests/test_seeder.py +34 -23
- django_spire/contrib/seeding/model/enums.py +2 -0
- django_spire/contrib/seeding/tests/test_config.py +71 -0
- django_spire/contrib/seeding/tests/test_custom.py +35 -0
- django_spire/contrib/seeding/tests/test_enums.py +40 -0
- django_spire/contrib/seeding/tests/test_intel.py +32 -0
- django_spire/contrib/seeding/tests/test_override.py +63 -0
- django_spire/contrib/service/__init__.py +2 -2
- django_spire/contrib/service/django_model_service.py +16 -15
- django_spire/contrib/service/exceptions.py +5 -3
- django_spire/contrib/service/tests/__init__.py +0 -0
- django_spire/contrib/service/tests/test_service.py +153 -0
- django_spire/contrib/session/apps.py +2 -0
- django_spire/contrib/session/controller.py +48 -42
- django_spire/contrib/session/templatetags/session_tags.py +11 -2
- django_spire/contrib/session/tests/test_session_controller.py +117 -53
- django_spire/contrib/tests/__init__.py +0 -0
- django_spire/contrib/tests/test_utils.py +37 -0
- django_spire/contrib/utils.py +4 -1
- django_spire/core/apps.py +2 -0
- django_spire/core/converters/tests/test_to_data.py +353 -0
- django_spire/core/converters/tests/test_to_enums.py +61 -41
- django_spire/core/converters/tests/test_to_pydantic.py +138 -109
- django_spire/core/converters/to_data.py +29 -10
- django_spire/core/converters/to_enums.py +4 -2
- django_spire/core/converters/to_pydantic.py +22 -22
- django_spire/core/decorators.py +19 -6
- django_spire/core/forms/widgets.py +4 -0
- django_spire/core/maps.py +3 -1
- django_spire/core/middleware/maintenance.py +3 -3
- django_spire/core/middleware.py +8 -6
- django_spire/core/redirect/__init__.py +5 -0
- django_spire/core/redirect/generic_redirect.py +1 -2
- django_spire/core/redirect/tests/__init__.py +0 -0
- django_spire/core/redirect/tests/test_generic_redirect.py +34 -0
- django_spire/core/{tests/tests_redirect.py → redirect/tests/test_safe_redirect.py} +55 -81
- django_spire/core/shortcuts.py +3 -3
- django_spire/core/static/django_spire/css/app-layout.css +1 -1
- django_spire/core/static/django_spire/css/app-navigation.css +3 -3
- django_spire/core/static/django_spire/css/bootstrap-override.css +4 -0
- django_spire/core/tag/admin.py +12 -0
- django_spire/core/tag/intelligence/tag_set_bot.py +2 -0
- django_spire/core/tag/mixins.py +2 -0
- django_spire/core/tag/models.py +2 -0
- django_spire/core/tag/querysets.py +2 -0
- django_spire/core/tag/service/tag_service.py +6 -3
- django_spire/core/tag/tests/test_intelligence.py +9 -9
- django_spire/core/tag/tests/test_tags.py +44 -54
- django_spire/core/tag/tests/test_tools.py +191 -0
- django_spire/core/tag/tools.py +3 -0
- django_spire/core/templates/django_spire/card/card.html +5 -2
- django_spire/core/templates/django_spire/card/title_card.html +9 -4
- django_spire/core/templates/django_spire/infinite_scroll/base.html +1 -0
- django_spire/core/templates/django_spire/navigation/side_navigation.html +19 -24
- django_spire/core/templates/django_spire/page/full_page.html +46 -16
- django_spire/core/templates/django_spire/table/base.html +4 -2
- django_spire/core/templatetags/json.py +6 -2
- django_spire/core/templatetags/message.py +13 -8
- django_spire/core/templatetags/string_formating.py +8 -5
- django_spire/core/templatetags/tests/__init__.py +0 -0
- django_spire/core/templatetags/tests/test_templatetags.py +427 -0
- django_spire/core/templatetags/variable_types.py +17 -9
- django_spire/core/tests/test_cases.py +1 -1
- django_spire/core/tests/test_conf.py +43 -0
- django_spire/core/tests/test_consts.py +28 -0
- django_spire/core/tests/test_context_processors.py +93 -0
- django_spire/core/tests/test_decorators.py +95 -0
- django_spire/core/tests/test_django_spire_utils.py +56 -0
- django_spire/core/tests/test_exceptions.py +37 -0
- django_spire/core/tests/test_models.py +54 -0
- django_spire/core/tests/test_settings.py +45 -0
- django_spire/core/tests/test_shortcuts.py +74 -0
- django_spire/core/tests/test_urls.py +16 -0
- django_spire/core/tests/test_utils.py +58 -0
- django_spire/core/urls.py +4 -1
- django_spire/core/utils.py +12 -8
- django_spire/exceptions.py +16 -1
- django_spire/file/admin.py +4 -2
- django_spire/file/apps.py +8 -10
- django_spire/file/fields.py +7 -7
- django_spire/file/forms.py +1 -1
- django_spire/file/interfaces.py +15 -15
- django_spire/file/mixins.py +1 -4
- django_spire/file/models.py +3 -5
- django_spire/file/tests/factories.py +59 -0
- django_spire/file/tests/test_admin.py +69 -0
- django_spire/file/tests/test_apps.py +24 -0
- django_spire/file/tests/test_fields.py +114 -0
- django_spire/file/tests/test_forms.py +20 -0
- django_spire/file/tests/test_interfaces.py +183 -0
- django_spire/file/tests/test_models.py +82 -0
- django_spire/file/tests/test_querysets.py +102 -0
- django_spire/file/tests/test_utils.py +32 -0
- django_spire/file/tests/test_views.py +145 -0
- django_spire/file/tests/test_widgets.py +82 -0
- django_spire/file/tools.py +8 -2
- django_spire/file/views.py +7 -3
- django_spire/file/widgets.py +12 -12
- django_spire/help_desk/admin.py +15 -0
- django_spire/help_desk/apps.py +2 -0
- django_spire/help_desk/auth/controller.py +2 -0
- django_spire/help_desk/choices.py +2 -0
- django_spire/help_desk/enums.py +2 -0
- django_spire/help_desk/exceptions.py +31 -3
- django_spire/help_desk/forms.py +2 -0
- django_spire/help_desk/models.py +2 -0
- django_spire/help_desk/querysets.py +4 -1
- django_spire/help_desk/services/notification_service.py +26 -27
- django_spire/help_desk/services/service.py +2 -3
- django_spire/help_desk/tests/factories.py +8 -3
- django_spire/help_desk/tests/test_admin.py +41 -0
- django_spire/help_desk/tests/test_apps.py +41 -0
- django_spire/help_desk/tests/test_choices.py +50 -0
- django_spire/help_desk/tests/test_controller.py +87 -0
- django_spire/help_desk/tests/test_enums.py +18 -0
- django_spire/help_desk/tests/test_exceptions.py +37 -0
- django_spire/help_desk/tests/test_forms.py +89 -0
- django_spire/help_desk/tests/test_models.py +59 -0
- django_spire/help_desk/tests/test_querysets.py +38 -0
- django_spire/help_desk/tests/test_services/test_notification_service.py +15 -8
- django_spire/help_desk/tests/test_services/test_service.py +92 -0
- django_spire/help_desk/tests/test_urls/test_form_urls.py +6 -6
- django_spire/help_desk/tests/test_urls/test_page_urls.py +8 -9
- django_spire/help_desk/tests/test_views/test_form_views.py +46 -19
- django_spire/help_desk/tests/test_views/test_page_views.py +32 -9
- django_spire/help_desk/urls/__init__.py +4 -1
- django_spire/help_desk/urls/form_urls.py +3 -0
- django_spire/help_desk/urls/page_urls.py +3 -0
- django_spire/help_desk/views/form_views.py +13 -5
- django_spire/help_desk/views/page_views.py +11 -3
- django_spire/history/activity/admin.py +2 -0
- django_spire/history/activity/apps.py +3 -1
- django_spire/history/activity/mixins.py +13 -7
- django_spire/history/activity/models.py +6 -5
- django_spire/history/activity/querysets.py +2 -0
- django_spire/history/activity/tests/__init__.py +0 -0
- django_spire/history/activity/tests/test_activity.py +176 -0
- django_spire/history/admin.py +9 -2
- django_spire/history/choices.py +3 -0
- django_spire/history/models.py +5 -5
- django_spire/history/tests/test_admin.py +93 -0
- django_spire/history/tests/test_history.py +101 -0
- django_spire/history/tests/test_mixins.py +84 -0
- django_spire/history/viewed/admin.py +3 -1
- django_spire/history/viewed/apps.py +3 -1
- django_spire/history/viewed/models.py +2 -0
- django_spire/history/viewed/tests/__init__.py +0 -0
- django_spire/history/viewed/tests/test_viewed.py +46 -0
- django_spire/knowledge/auth/tests/__init__.py +0 -0
- django_spire/knowledge/auth/tests/test_controller.py +116 -0
- django_spire/knowledge/collection/admin.py +5 -1
- django_spire/knowledge/collection/models.py +3 -1
- django_spire/knowledge/collection/seeding/seed.py +1 -0
- django_spire/knowledge/collection/services/factory_service.py +10 -11
- django_spire/knowledge/collection/services/ordering_service.py +1 -2
- django_spire/knowledge/collection/services/service.py +5 -10
- django_spire/knowledge/collection/services/tag_service.py +5 -2
- django_spire/knowledge/collection/tests/factories.py +28 -1
- django_spire/knowledge/collection/tests/test_models.py +48 -0
- django_spire/knowledge/collection/tests/test_querysets.py +93 -0
- django_spire/knowledge/collection/tests/test_services/test_factory_service.py +100 -0
- django_spire/knowledge/collection/tests/test_services/test_services.py +160 -0
- django_spire/knowledge/collection/tests/test_urls/test_form_urls.py +21 -3
- django_spire/knowledge/collection/tests/test_urls/test_json_urls.py +39 -1
- django_spire/knowledge/collection/tests/test_urls/test_page_urls.py +12 -4
- django_spire/knowledge/collection/urls/__init__.py +3 -0
- django_spire/knowledge/collection/urls/form_urls.py +2 -0
- django_spire/knowledge/collection/urls/json_urls.py +2 -0
- django_spire/knowledge/collection/urls/page_urls.py +2 -0
- django_spire/knowledge/collection/views/form_views.py +4 -4
- django_spire/knowledge/collection/views/json_views.py +5 -1
- django_spire/knowledge/collection/views/page_views.py +5 -2
- django_spire/knowledge/entry/admin.py +7 -1
- django_spire/knowledge/entry/forms.py +2 -0
- django_spire/knowledge/entry/models.py +2 -0
- django_spire/knowledge/entry/seeding/seed.py +3 -0
- django_spire/knowledge/entry/services/automation_service.py +5 -4
- django_spire/knowledge/entry/services/factory_service.py +7 -5
- django_spire/knowledge/entry/services/service.py +4 -7
- django_spire/knowledge/entry/services/tag_service.py +0 -1
- django_spire/knowledge/entry/services/tool_service.py +1 -0
- django_spire/knowledge/entry/services/transformation_services.py +1 -5
- django_spire/knowledge/entry/tests/factories.py +1 -2
- django_spire/knowledge/entry/tests/test_factory_service.py +20 -0
- django_spire/knowledge/entry/tests/test_models.py +41 -0
- django_spire/knowledge/entry/tests/test_querysets.py +71 -0
- django_spire/knowledge/entry/tests/test_services.py +94 -0
- django_spire/knowledge/entry/tests/test_urls/test_form_urls.py +9 -14
- django_spire/knowledge/entry/tests/test_urls/test_json_urls.py +48 -5
- django_spire/knowledge/entry/tests/test_urls/test_page_urls.py +6 -8
- django_spire/knowledge/entry/tests/test_urls/test_template_urls.py +40 -0
- django_spire/knowledge/entry/urls/form_urls.py +2 -0
- django_spire/knowledge/entry/urls/json_urls.py +2 -0
- django_spire/knowledge/entry/urls/page_urls.py +2 -0
- django_spire/knowledge/entry/urls/template_urls.py +2 -0
- django_spire/knowledge/entry/version/block/choices.py +2 -0
- django_spire/knowledge/entry/version/block/data/data.py +1 -0
- django_spire/knowledge/entry/version/block/data/list/data.py +8 -13
- django_spire/knowledge/entry/version/block/data/list/maps.py +3 -0
- django_spire/knowledge/entry/version/block/data/list/meta.py +1 -2
- django_spire/knowledge/entry/version/block/data/list/tests/__init__.py +0 -0
- django_spire/knowledge/entry/version/block/data/list/tests/test_maps.py +32 -0
- django_spire/knowledge/entry/version/block/data/list/tests/test_meta.py +58 -0
- django_spire/knowledge/entry/version/block/data/maps.py +3 -6
- django_spire/knowledge/entry/version/block/models.py +7 -5
- django_spire/knowledge/entry/version/block/seeding/constants.py +5 -4
- django_spire/knowledge/entry/version/block/services/service.py +2 -3
- django_spire/knowledge/entry/version/block/tests/factories.py +4 -10
- django_spire/knowledge/entry/version/block/tests/test_choices.py +56 -0
- django_spire/knowledge/entry/version/block/tests/test_data.py +90 -0
- django_spire/knowledge/entry/version/block/tests/test_maps.py +37 -0
- django_spire/knowledge/entry/version/block/tests/test_models.py +55 -0
- django_spire/knowledge/entry/version/block/tests/test_querysets.py +35 -0
- django_spire/knowledge/entry/version/block/tests/test_services.py +65 -0
- django_spire/knowledge/entry/version/choices.py +2 -0
- django_spire/knowledge/entry/version/converters/converter.py +1 -1
- django_spire/knowledge/entry/version/converters/docx_converter.py +4 -7
- django_spire/knowledge/entry/version/converters/markdown_converter.py +20 -20
- django_spire/knowledge/entry/version/maps.py +4 -5
- django_spire/knowledge/entry/version/querysets.py +1 -1
- django_spire/knowledge/entry/version/seeding/seeder.py +1 -2
- django_spire/knowledge/entry/version/services/processor_service.py +5 -4
- django_spire/knowledge/entry/version/services/service.py +1 -2
- django_spire/knowledge/entry/version/tests/factories.py +2 -2
- django_spire/knowledge/entry/version/tests/test_choices.py +18 -0
- django_spire/knowledge/entry/version/tests/test_converters/test_docx_converter.py +56 -8
- django_spire/knowledge/entry/version/tests/test_converters/test_markdown_converter.py +78 -0
- django_spire/knowledge/entry/version/tests/test_maps.py +58 -0
- django_spire/knowledge/entry/version/tests/test_models.py +23 -0
- django_spire/knowledge/entry/version/tests/test_querysets.py +26 -0
- django_spire/knowledge/entry/version/tests/test_services.py +62 -0
- django_spire/knowledge/entry/version/tests/test_urls/test_json_urls.py +27 -8
- django_spire/knowledge/entry/version/tests/test_urls/test_page_urls.py +15 -8
- django_spire/knowledge/entry/version/tests/test_urls/test_redirect_urls.py +38 -0
- django_spire/knowledge/entry/version/urls/__init__.py +3 -0
- django_spire/knowledge/entry/version/urls/json_urls.py +2 -1
- django_spire/knowledge/entry/version/urls/page_urls.py +2 -0
- django_spire/knowledge/entry/version/urls/redirect_urls.py +2 -0
- django_spire/knowledge/entry/version/views/json_views.py +5 -1
- django_spire/knowledge/entry/version/views/page_views.py +10 -3
- django_spire/knowledge/entry/version/views/redirect_views.py +5 -1
- django_spire/knowledge/entry/views/form_views.py +16 -8
- django_spire/knowledge/entry/views/json_views.py +3 -1
- django_spire/knowledge/entry/views/page_views.py +8 -2
- django_spire/knowledge/entry/views/template_views.py +7 -1
- django_spire/knowledge/exceptions.py +2 -1
- django_spire/knowledge/intelligence/intel/answer_intel.py +2 -1
- django_spire/knowledge/intelligence/intel/entry_intel.py +0 -1
- django_spire/knowledge/intelligence/workflows/knowledge_workflow.py +4 -5
- django_spire/knowledge/models.py +1 -2
- django_spire/knowledge/tests/__init__.py +0 -0
- django_spire/knowledge/tests/test_templatetags.py +40 -0
- django_spire/knowledge/tests/test_urls/__init__.py +0 -0
- django_spire/knowledge/tests/test_urls/test_page_urls.py +24 -0
- django_spire/knowledge/urls/__init__.py +2 -0
- django_spire/knowledge/urls/page_urls.py +2 -0
- django_spire/knowledge/views/page_views.py +8 -3
- django_spire/notification/admin.py +3 -1
- django_spire/notification/app/admin.py +2 -0
- django_spire/notification/app/apps.py +3 -1
- django_spire/notification/app/exceptions.py +9 -2
- django_spire/notification/app/models.py +8 -4
- django_spire/notification/app/processor.py +22 -26
- django_spire/notification/app/querysets.py +2 -0
- django_spire/notification/app/tests/__init__.py +0 -0
- django_spire/notification/app/tests/factories.py +34 -0
- django_spire/notification/app/tests/test_apps.py +24 -0
- django_spire/notification/app/tests/test_models.py +72 -0
- django_spire/notification/app/tests/test_processor.py +111 -0
- django_spire/notification/app/tests/test_querysets.py +90 -0
- django_spire/notification/app/tests/test_views/__init__.py +0 -0
- django_spire/notification/app/tests/test_views/test_json_views.py +48 -0
- django_spire/notification/app/tests/test_views/test_page_views.py +19 -0
- django_spire/notification/app/urls/__init__.py +3 -1
- django_spire/notification/app/urls/json_urls.py +6 -4
- django_spire/notification/app/urls/page_urls.py +4 -3
- django_spire/notification/app/urls/template_urls.py +4 -2
- django_spire/notification/apps.py +4 -1
- django_spire/notification/email/admin.py +5 -1
- django_spire/notification/email/apps.py +3 -1
- django_spire/notification/email/exceptions.py +4 -2
- django_spire/notification/email/helper.py +5 -3
- django_spire/notification/email/models.py +4 -0
- django_spire/notification/email/processor.py +19 -15
- django_spire/notification/email/querysets.py +3 -0
- django_spire/notification/email/tests/__init__.py +0 -0
- django_spire/notification/email/tests/factories.py +35 -0
- django_spire/notification/email/tests/test_apps.py +24 -0
- django_spire/notification/email/tests/test_models.py +52 -0
- django_spire/notification/email/tests/test_processor.py +92 -0
- django_spire/notification/email/tests/test_querysets.py +43 -0
- django_spire/notification/exceptions.py +17 -2
- django_spire/notification/managers.py +7 -1
- django_spire/notification/maps.py +4 -1
- django_spire/notification/mixins.py +2 -0
- django_spire/notification/models.py +3 -1
- django_spire/notification/processors/notification.py +12 -5
- django_spire/notification/processors/processor.py +2 -0
- django_spire/notification/processors/tests/__init__.py +0 -0
- django_spire/notification/processors/tests/test_notification.py +106 -0
- django_spire/notification/push/admin.py +10 -1
- django_spire/notification/push/apps.py +3 -1
- django_spire/notification/push/models.py +2 -3
- django_spire/notification/push/tests/__init__.py +0 -0
- django_spire/notification/push/tests/test_apps.py +24 -0
- django_spire/notification/push/tests/test_models.py +28 -0
- django_spire/notification/querysets.py +7 -1
- django_spire/notification/sms/admin.py +2 -0
- django_spire/notification/sms/apps.py +4 -1
- django_spire/notification/sms/automations.py +2 -0
- django_spire/notification/sms/choices.py +2 -0
- django_spire/notification/sms/exceptions.py +19 -5
- django_spire/notification/sms/helper.py +33 -23
- django_spire/notification/sms/models.py +5 -1
- django_spire/notification/sms/processor.py +20 -20
- django_spire/notification/sms/querysets.py +2 -0
- django_spire/notification/sms/tests/factories.py +33 -0
- django_spire/notification/sms/tests/test_apps.py +24 -0
- django_spire/notification/sms/tests/test_automation.py +38 -0
- django_spire/notification/sms/tests/test_choices.py +15 -0
- django_spire/notification/sms/tests/test_consts.py +17 -0
- django_spire/notification/sms/tests/test_exceptions.py +27 -0
- django_spire/notification/sms/tests/test_helper.py +50 -0
- django_spire/notification/sms/tests/test_models.py +81 -0
- django_spire/notification/sms/tests/test_processor.py +107 -0
- django_spire/notification/sms/tests/test_tools.py +25 -11
- django_spire/notification/sms/tools.py +16 -5
- django_spire/notification/sms/urls/__init__.py +3 -1
- django_spire/notification/sms/urls/media_urls.py +2 -0
- django_spire/notification/sms/views/media_views.py +14 -4
- django_spire/notification/tests/__init__.py +0 -0
- django_spire/notification/tests/factories.py +26 -0
- django_spire/notification/tests/test_admin.py +55 -0
- django_spire/notification/tests/test_apps.py +30 -0
- django_spire/notification/tests/test_automation.py +18 -0
- django_spire/notification/tests/test_choices.py +59 -0
- django_spire/notification/tests/test_exceptions.py +58 -0
- django_spire/notification/tests/test_managers.py +100 -0
- django_spire/notification/tests/test_maps.py +31 -0
- django_spire/notification/tests/test_models.py +76 -0
- django_spire/notification/tests/test_querysets.py +184 -0
- django_spire/notification/tests/test_utils.py +23 -0
- django_spire/notification/urls.py +3 -1
- django_spire/notification/utils.py +3 -1
- django_spire/settings.py +3 -0
- django_spire/theme/tests/test_context_processor.py +15 -13
- django_spire/theme/tests/test_enums.py +2 -2
- django_spire/theme/tests/test_filesystem.py +2 -5
- django_spire/theme/tests/test_integration.py +12 -12
- django_spire/theme/tests/test_model.py +40 -38
- django_spire/theme/tests/test_views/test_json_views.py +33 -33
- django_spire/theme/urls/json_urls.py +3 -0
- django_spire/theme/urls/page_urls.py +3 -0
- django_spire/urls.py +19 -15
- django_spire/utils.py +13 -4
- {django_spire-0.23.6.dist-info → django_spire-0.23.8.dist-info}/METADATA +1 -1
- {django_spire-0.23.6.dist-info → django_spire-0.23.8.dist-info}/RECORD +532 -361
- django_spire/contrib/options/tests/test_unit.py +0 -148
- django_spire/contrib/progress/views.py +0 -64
- django_spire/contrib/seeding/tests/test_seeding.py +0 -25
- django_spire/core/tests/test_templatetags.py +0 -117
- django_spire/core/tests/tests_shortcuts.py +0 -73
- django_spire/history/activity/tests.py +0 -3
- django_spire/history/activity/views.py +0 -3
- django_spire/knowledge/collection/tests/test_services/test_transformation_service.py +0 -71
- django_spire/notification/app/tests.py +0 -3
- {django_spire-0.23.6.dist-info → django_spire-0.23.8.dist-info}/WHEEL +0 -0
- {django_spire-0.23.6.dist-info → django_spire-0.23.8.dist-info}/licenses/LICENSE.md +0 -0
- {django_spire-0.23.6.dist-info → django_spire-0.23.8.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from django_spire.auth.user.tests.factories import create_user
|
|
4
|
+
from django_spire.core.tests.test_cases import BaseTestCase
|
|
5
|
+
from django_spire.notification.choices import NotificationStatusChoices
|
|
6
|
+
from django_spire.notification.email.models import EmailNotification
|
|
7
|
+
from django_spire.notification.email.tests.factories import create_test_email_notification
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class EmailNotificationQuerySetTests(BaseTestCase):
|
|
11
|
+
def setUp(self):
|
|
12
|
+
super().setUp()
|
|
13
|
+
self.user = create_user(username='test_email_queryset_user')
|
|
14
|
+
self.email_notification = create_test_email_notification(
|
|
15
|
+
user=self.user,
|
|
16
|
+
status=NotificationStatusChoices.SENT
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
def test_by_user(self):
|
|
20
|
+
other_user = create_user(username='other_user')
|
|
21
|
+
other_notification = create_test_email_notification(user=other_user)
|
|
22
|
+
|
|
23
|
+
result = EmailNotification.objects.by_user(self.user)
|
|
24
|
+
assert self.email_notification in result
|
|
25
|
+
assert other_notification not in result
|
|
26
|
+
|
|
27
|
+
def test_by_users(self):
|
|
28
|
+
other_user = create_user(username='other_user')
|
|
29
|
+
other_notification = create_test_email_notification(user=other_user)
|
|
30
|
+
|
|
31
|
+
result = EmailNotification.objects.by_users([self.user, other_user])
|
|
32
|
+
assert self.email_notification in result
|
|
33
|
+
assert other_notification in result
|
|
34
|
+
|
|
35
|
+
def test_is_sent(self):
|
|
36
|
+
pending_notification = create_test_email_notification(
|
|
37
|
+
user=self.user,
|
|
38
|
+
status=NotificationStatusChoices.PENDING
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
result = EmailNotification.objects.is_sent()
|
|
42
|
+
assert self.email_notification in result
|
|
43
|
+
assert pending_notification not in result
|
|
@@ -1,5 +1,20 @@
|
|
|
1
|
-
from
|
|
1
|
+
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
from django_spire.exceptions import DjangoSpireError
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from django_spire.notification.choices import NotificationTypeChoices
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class NotificationError(DjangoSpireError):
|
|
5
12
|
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class InvalidNotificationTypeError(NotificationError):
|
|
16
|
+
def __init__(self, expected_type: NotificationTypeChoices, actual_type: NotificationTypeChoices) -> None:
|
|
17
|
+
super().__init__(
|
|
18
|
+
f'Expected notification type {expected_type}, '
|
|
19
|
+
f'but received {actual_type}'
|
|
20
|
+
)
|
|
@@ -1,9 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
1
5
|
from django_spire.notification.app.processor import AppNotificationProcessor
|
|
2
6
|
from django_spire.notification.email.processor import EmailNotificationProcessor
|
|
3
|
-
from django_spire.notification.models import Notification
|
|
4
7
|
from django_spire.notification.processors.notification import NotificationProcessor
|
|
5
8
|
from django_spire.notification.sms.processor import SMSNotificationProcessor
|
|
6
9
|
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from django_spire.notification.models import Notification
|
|
12
|
+
|
|
7
13
|
|
|
8
14
|
class NotificationManager:
|
|
9
15
|
@staticmethod
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from django_spire.notification.app.models import AppNotification
|
|
2
4
|
from django_spire.notification.choices import NotificationTypeChoices
|
|
3
5
|
from django_spire.notification.email.models import EmailNotification
|
|
4
6
|
from django_spire.notification.push.models import PushNotification
|
|
5
7
|
from django_spire.notification.sms.models import SmsNotification
|
|
6
8
|
|
|
9
|
+
|
|
7
10
|
NOTIFICATION_TYPE_CHOICE_TO_MODEL_MAP: dict[NotificationTypeChoices, type] = {
|
|
8
11
|
NotificationTypeChoices.APP: AppNotification,
|
|
9
12
|
NotificationTypeChoices.EMAIL: EmailNotification,
|
|
10
13
|
NotificationTypeChoices.PUSH: PushNotification,
|
|
11
14
|
NotificationTypeChoices.SMS: SmsNotification
|
|
12
|
-
}
|
|
15
|
+
}
|
|
@@ -8,7 +8,9 @@ from django.contrib.contenttypes.fields import GenericForeignKey
|
|
|
8
8
|
|
|
9
9
|
from django_spire.history.mixins import HistoryModelMixin
|
|
10
10
|
from django_spire.notification.choices import (
|
|
11
|
-
|
|
11
|
+
NotificationPriorityChoices,
|
|
12
|
+
NotificationStatusChoices,
|
|
13
|
+
NotificationTypeChoices
|
|
12
14
|
)
|
|
13
15
|
from django_spire.notification.querysets import NotificationQuerySet
|
|
14
16
|
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from collections import defaultdict
|
|
2
4
|
|
|
3
5
|
from django_spire.notification.app.processor import AppNotificationProcessor
|
|
4
|
-
from django_spire.notification.choices import
|
|
6
|
+
from django_spire.notification.choices import (
|
|
7
|
+
NotificationStatusChoices,
|
|
5
8
|
NotificationTypeChoices
|
|
9
|
+
)
|
|
6
10
|
from django_spire.notification.email.processor import EmailNotificationProcessor
|
|
7
|
-
from django_spire.notification.exceptions import
|
|
11
|
+
from django_spire.notification.exceptions import NotificationError
|
|
8
12
|
from django_spire.notification.models import Notification
|
|
9
13
|
from django_spire.notification.processors.processor import BaseNotificationProcessor
|
|
10
14
|
from django_spire.notification.sms.processor import SMSNotificationProcessor
|
|
@@ -13,10 +17,13 @@ from django_spire.notification.sms.processor import SMSNotificationProcessor
|
|
|
13
17
|
class NotificationProcessor(BaseNotificationProcessor):
|
|
14
18
|
def process(self, notification: Notification):
|
|
15
19
|
processor = self._get_processor(notification.type)
|
|
20
|
+
|
|
16
21
|
if processor is None:
|
|
17
22
|
notification.status = NotificationStatusChoices.FAILED
|
|
18
23
|
notification.save()
|
|
19
|
-
|
|
24
|
+
|
|
25
|
+
message = f'Unknown notification type: {notification.type}'
|
|
26
|
+
raise NotificationError(message)
|
|
20
27
|
|
|
21
28
|
processor().process(notification)
|
|
22
29
|
|
|
@@ -45,10 +52,10 @@ class NotificationProcessor(BaseNotificationProcessor):
|
|
|
45
52
|
if notification_type == NotificationTypeChoices.APP:
|
|
46
53
|
return AppNotificationProcessor
|
|
47
54
|
|
|
48
|
-
|
|
55
|
+
if notification_type == NotificationTypeChoices.EMAIL:
|
|
49
56
|
return EmailNotificationProcessor
|
|
50
57
|
|
|
51
|
-
|
|
58
|
+
if notification_type == NotificationTypeChoices.SMS:
|
|
52
59
|
return SMSNotificationProcessor
|
|
53
60
|
|
|
54
61
|
return None
|
|
File without changes
|
|
@@ -0,0 +1,106 @@
|
|
|
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.app.models import AppNotification
|
|
10
|
+
from django_spire.notification.choices import (
|
|
11
|
+
NotificationStatusChoices,
|
|
12
|
+
NotificationTypeChoices,
|
|
13
|
+
)
|
|
14
|
+
from django_spire.notification.exceptions import NotificationError
|
|
15
|
+
from django_spire.notification.models import Notification
|
|
16
|
+
from django_spire.notification.processors.notification import NotificationProcessor
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class NotificationProcessorTests(BaseTestCase):
|
|
20
|
+
def setUp(self):
|
|
21
|
+
super().setUp()
|
|
22
|
+
|
|
23
|
+
self.user = create_user(username='test_processor_user')
|
|
24
|
+
self.processor = NotificationProcessor()
|
|
25
|
+
|
|
26
|
+
def _create_notification(self, **kwargs) -> Notification:
|
|
27
|
+
data = {
|
|
28
|
+
'user': self.user,
|
|
29
|
+
'type': NotificationTypeChoices.APP,
|
|
30
|
+
'title': 'Test Notification',
|
|
31
|
+
'body': 'Test body',
|
|
32
|
+
'status': NotificationStatusChoices.PENDING,
|
|
33
|
+
}
|
|
34
|
+
data.update(kwargs)
|
|
35
|
+
notification = Notification.objects.create(**data)
|
|
36
|
+
|
|
37
|
+
if data['type'] == NotificationTypeChoices.APP:
|
|
38
|
+
AppNotification.objects.create(notification=notification)
|
|
39
|
+
|
|
40
|
+
return notification
|
|
41
|
+
|
|
42
|
+
@patch('django_spire.notification.processors.notification.AppNotificationProcessor')
|
|
43
|
+
def test_process_app_notification(self, mock_processor_class: MagicMock):
|
|
44
|
+
mock_processor = MagicMock()
|
|
45
|
+
mock_processor_class.return_value = mock_processor
|
|
46
|
+
|
|
47
|
+
notification = self._create_notification(type=NotificationTypeChoices.APP)
|
|
48
|
+
|
|
49
|
+
self.processor.process(notification)
|
|
50
|
+
|
|
51
|
+
mock_processor.process.assert_called_once_with(notification)
|
|
52
|
+
|
|
53
|
+
@patch('django_spire.notification.processors.notification.EmailNotificationProcessor')
|
|
54
|
+
def test_process_email_notification(self, mock_processor_class: MagicMock):
|
|
55
|
+
mock_processor = MagicMock()
|
|
56
|
+
mock_processor_class.return_value = mock_processor
|
|
57
|
+
|
|
58
|
+
notification = self._create_notification(type=NotificationTypeChoices.EMAIL)
|
|
59
|
+
|
|
60
|
+
self.processor.process(notification)
|
|
61
|
+
|
|
62
|
+
mock_processor.process.assert_called_once_with(notification)
|
|
63
|
+
|
|
64
|
+
@patch('django_spire.notification.processors.notification.SMSNotificationProcessor')
|
|
65
|
+
def test_process_sms_notification(self, mock_processor_class: MagicMock):
|
|
66
|
+
mock_processor = MagicMock()
|
|
67
|
+
mock_processor_class.return_value = mock_processor
|
|
68
|
+
|
|
69
|
+
notification = self._create_notification(type=NotificationTypeChoices.SMS)
|
|
70
|
+
|
|
71
|
+
self.processor.process(notification)
|
|
72
|
+
|
|
73
|
+
mock_processor.process.assert_called_once_with(notification)
|
|
74
|
+
|
|
75
|
+
def test_process_unknown_type_raises_error(self):
|
|
76
|
+
notification = self._create_notification()
|
|
77
|
+
notification.type = 'unknown'
|
|
78
|
+
|
|
79
|
+
with pytest.raises(NotificationError):
|
|
80
|
+
self.processor.process(notification)
|
|
81
|
+
|
|
82
|
+
def test_process_unknown_type_sets_status_failed(self):
|
|
83
|
+
notification = self._create_notification()
|
|
84
|
+
notification.type = 'unknown'
|
|
85
|
+
|
|
86
|
+
with pytest.raises(NotificationError):
|
|
87
|
+
self.processor.process(notification)
|
|
88
|
+
|
|
89
|
+
notification.refresh_from_db()
|
|
90
|
+
assert notification.status == NotificationStatusChoices.FAILED
|
|
91
|
+
|
|
92
|
+
@patch('django_spire.notification.processors.notification.AppNotificationProcessor')
|
|
93
|
+
def test_process_list_groups_by_type(self, mock_processor_class: MagicMock):
|
|
94
|
+
mock_processor = MagicMock()
|
|
95
|
+
mock_processor_class.return_value = mock_processor
|
|
96
|
+
|
|
97
|
+
notifications = [
|
|
98
|
+
self._create_notification(type=NotificationTypeChoices.APP),
|
|
99
|
+
self._create_notification(type=NotificationTypeChoices.APP),
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
self.processor.process_list(notifications)
|
|
103
|
+
|
|
104
|
+
mock_processor.process_list.assert_called_once()
|
|
105
|
+
call_args = mock_processor.process_list.call_args[0][0]
|
|
106
|
+
assert len(call_args) == 2
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from django.contrib import admin
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
from django_spire.notification.push.models import PushNotification
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@admin.register(PushNotification)
|
|
9
|
+
class PushNotificationAdmin(admin.ModelAdmin):
|
|
10
|
+
list_display = ('id', 'created_datetime', 'is_active', 'is_deleted')
|
|
11
|
+
list_filter = ('is_active', 'is_deleted')
|
|
12
|
+
ordering = ('-created_datetime',)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from django.apps import AppConfig
|
|
2
4
|
|
|
3
5
|
from django_spire.utils import check_required_apps
|
|
@@ -11,4 +13,4 @@ class NotificationPushConfig(AppConfig):
|
|
|
11
13
|
REQUIRED_APPS = ('django_spire_core', 'django_spire_notification')
|
|
12
14
|
|
|
13
15
|
def ready(self) -> None:
|
|
14
|
-
check_required_apps(self.label)
|
|
16
|
+
check_required_apps(self.label)
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
from
|
|
1
|
+
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from django_spire.history.mixins import HistoryModelMixin
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class PushNotification(HistoryModelMixin):
|
|
7
|
-
|
|
8
7
|
class Meta:
|
|
9
8
|
db_table = 'django_spire_notification_push'
|
|
10
9
|
verbose_name = 'Push Notification'
|
|
11
|
-
verbose_name_plural = 'Push Notifications'
|
|
10
|
+
verbose_name_plural = 'Push Notifications'
|
|
File without changes
|
|
@@ -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.push.apps import NotificationPushConfig
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class NotificationPushConfigTests(BaseTestCase):
|
|
10
|
+
def test_app_name(self):
|
|
11
|
+
assert NotificationPushConfig.name == 'django_spire.notification.push'
|
|
12
|
+
|
|
13
|
+
def test_app_label(self):
|
|
14
|
+
assert NotificationPushConfig.label == 'django_spire_notification_push'
|
|
15
|
+
|
|
16
|
+
def test_default_auto_field(self):
|
|
17
|
+
assert NotificationPushConfig.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 == NotificationPushConfig.REQUIRED_APPS
|
|
22
|
+
|
|
23
|
+
def test_app_is_installed(self):
|
|
24
|
+
assert apps.is_installed('django_spire.notification.push')
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from django_spire.core.tests.test_cases import BaseTestCase
|
|
4
|
+
from django_spire.notification.push.models import PushNotification
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PushNotificationModelTests(BaseTestCase):
|
|
8
|
+
def setUp(self):
|
|
9
|
+
super().setUp()
|
|
10
|
+
self.push_notification = PushNotification.objects.create()
|
|
11
|
+
|
|
12
|
+
def test_created_datetime_auto_set(self):
|
|
13
|
+
assert self.push_notification.created_datetime is not None
|
|
14
|
+
|
|
15
|
+
def test_is_active_default_true(self):
|
|
16
|
+
assert self.push_notification.is_active is True
|
|
17
|
+
|
|
18
|
+
def test_is_deleted_default_false(self):
|
|
19
|
+
assert self.push_notification.is_deleted is False
|
|
20
|
+
|
|
21
|
+
def test_meta_verbose_name(self):
|
|
22
|
+
assert PushNotification._meta.verbose_name == 'Push Notification'
|
|
23
|
+
|
|
24
|
+
def test_meta_verbose_name_plural(self):
|
|
25
|
+
assert PushNotification._meta.verbose_name_plural == 'Push Notifications'
|
|
26
|
+
|
|
27
|
+
def test_meta_db_table(self):
|
|
28
|
+
assert PushNotification._meta.db_table == 'django_spire_notification_push'
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from collections import defaultdict
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
2
5
|
|
|
3
6
|
from django.contrib.admin.options import get_content_type_for_model
|
|
4
|
-
from django.contrib.auth.models import User
|
|
5
7
|
from django.db.models import QuerySet, Q, Model
|
|
6
8
|
from django.utils.timezone import now
|
|
7
9
|
|
|
@@ -12,6 +14,9 @@ from django_spire.notification.choices import (
|
|
|
12
14
|
NotificationPriorityChoices,
|
|
13
15
|
)
|
|
14
16
|
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from django.contrib.auth.models import User
|
|
19
|
+
|
|
15
20
|
|
|
16
21
|
class NotificationQuerySet(HistoryQuerySet):
|
|
17
22
|
def app_notifications(self):
|
|
@@ -108,6 +113,7 @@ class NotificationContentObjectQuerySet(QuerySet):
|
|
|
108
113
|
].append(content_object.pk)
|
|
109
114
|
|
|
110
115
|
queries = Q()
|
|
116
|
+
|
|
111
117
|
for content_type, object_ids in content_type_to_object_ids.items():
|
|
112
118
|
queries |= Q(
|
|
113
119
|
notification__content_type=content_type,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from django.apps import AppConfig
|
|
2
4
|
from django.conf import settings
|
|
3
5
|
|
|
@@ -14,6 +16,7 @@ class NotificationSmsConfig(AppConfig):
|
|
|
14
16
|
|
|
15
17
|
def ready(self) -> None:
|
|
16
18
|
if not isinstance(getattr(settings, TWILIO_SMS_BATCH_SIZE_NAME), int):
|
|
17
|
-
|
|
19
|
+
message = f'"{TWILIO_SMS_BATCH_SIZE_NAME}" must be set in the django settings when using "{self.label}".'
|
|
20
|
+
raise TypeError(message)
|
|
18
21
|
|
|
19
22
|
check_required_apps(self.label)
|
|
@@ -1,17 +1,31 @@
|
|
|
1
|
-
from
|
|
1
|
+
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from django_spire.exceptions import DjangoSpireError
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
|
|
6
|
+
class SmsNotificationError(DjangoSpireError):
|
|
5
7
|
pass
|
|
6
8
|
|
|
7
9
|
|
|
8
|
-
class
|
|
10
|
+
class SmsTemporaryMediaError(SmsNotificationError):
|
|
9
11
|
pass
|
|
10
12
|
|
|
11
13
|
|
|
12
|
-
class
|
|
14
|
+
class TwilioError(Exception):
|
|
13
15
|
pass
|
|
14
16
|
|
|
15
17
|
|
|
16
|
-
class
|
|
18
|
+
class InvalidPhoneNumberError(TwilioError):
|
|
19
|
+
def __init__(self, phone_number: str) -> None:
|
|
20
|
+
super().__init__(f'Invalid phone number format: {phone_number}')
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TwilioResponseError(TwilioError):
|
|
24
|
+
def __init__(self, error_code: int | None, error_message: str | None) -> None:
|
|
25
|
+
super().__init__(
|
|
26
|
+
f'Twilio Error: code={error_code}, message={error_message}'
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TwilioAPIConcurrentError(TwilioError):
|
|
17
31
|
pass
|
|
@@ -1,17 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import time
|
|
4
|
+
|
|
2
5
|
from collections import defaultdict
|
|
3
6
|
|
|
4
7
|
from django.utils.timezone import now
|
|
5
|
-
from twilio.rest.api.v2010.account.message import MessageInstance
|
|
6
|
-
from twilio.rest import Client
|
|
7
8
|
|
|
8
9
|
from django.conf import settings
|
|
9
10
|
|
|
10
11
|
from django_spire.notification.choices import NotificationStatusChoices
|
|
11
|
-
from django_spire.notification.models import Notification
|
|
12
12
|
from django_spire.notification.sms.consts import TWILIO_UNSUCCESSFUL_STATUSES
|
|
13
|
-
from django_spire.notification.sms.exceptions import
|
|
14
|
-
|
|
13
|
+
from django_spire.notification.sms.exceptions import (
|
|
14
|
+
InvalidPhoneNumberError,
|
|
15
|
+
TwilioAPIConcurrentError,
|
|
16
|
+
TwilioError,
|
|
17
|
+
TwilioResponseError
|
|
18
|
+
)
|
|
19
|
+
from typing import TYPE_CHECKING
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from django_spire.notification.models import Notification
|
|
23
|
+
from twilio.rest.api.v2010.account.message import MessageInstance
|
|
24
|
+
from twilio.rest import Client
|
|
15
25
|
|
|
16
26
|
|
|
17
27
|
class BulkTwilioSMSHelper:
|
|
@@ -31,6 +41,7 @@ class BulkTwilioSMSHelper:
|
|
|
31
41
|
self._find_notification_segments()
|
|
32
42
|
|
|
33
43
|
sent_segments = 0
|
|
44
|
+
|
|
34
45
|
for notification, segments in self.notification_segments.items():
|
|
35
46
|
try:
|
|
36
47
|
TwilioSMSHelper(notification, self.client).send()
|
|
@@ -44,18 +55,17 @@ class BulkTwilioSMSHelper:
|
|
|
44
55
|
sent_segments = 0
|
|
45
56
|
|
|
46
57
|
except Exception as e:
|
|
47
|
-
if isinstance(e,
|
|
58
|
+
if isinstance(e, TwilioAPIConcurrentError):
|
|
48
59
|
notification.status = NotificationStatusChoices.PENDING
|
|
49
60
|
|
|
50
|
-
if isinstance(e,
|
|
61
|
+
if isinstance(e, TwilioError):
|
|
51
62
|
notification.status_message = str(e)
|
|
52
63
|
notification.status = NotificationStatusChoices.ERRORED
|
|
53
|
-
raise
|
|
64
|
+
raise
|
|
54
65
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
raise e
|
|
66
|
+
notification.status_message = str(e)
|
|
67
|
+
notification.status = NotificationStatusChoices.FAILED
|
|
68
|
+
raise
|
|
59
69
|
|
|
60
70
|
|
|
61
71
|
class TwilioSMSHelper:
|
|
@@ -69,14 +79,17 @@ class TwilioSMSHelper:
|
|
|
69
79
|
|
|
70
80
|
def _attempt_send(self) -> MessageInstance:
|
|
71
81
|
try:
|
|
82
|
+
media_url = self.notification.sms.media_url
|
|
83
|
+
|
|
72
84
|
return self.client.messages.create(
|
|
73
85
|
to=self.to_phone_number,
|
|
74
86
|
from_=settings.TWILIO_PHONE_NUMBER,
|
|
75
87
|
body=self.message,
|
|
76
|
-
media_url=[
|
|
88
|
+
media_url=[media_url] if media_url else [],
|
|
77
89
|
)
|
|
78
90
|
except Exception as e:
|
|
79
|
-
|
|
91
|
+
message = f'Twilio Error: {e!s}'
|
|
92
|
+
raise TwilioError(message) from e
|
|
80
93
|
|
|
81
94
|
def send(self):
|
|
82
95
|
response = self._attempt_send()
|
|
@@ -84,16 +97,15 @@ class TwilioSMSHelper:
|
|
|
84
97
|
|
|
85
98
|
def _handle_response(self, response: MessageInstance):
|
|
86
99
|
if response.error_code == 429:
|
|
87
|
-
raise
|
|
88
|
-
|
|
89
|
-
)
|
|
100
|
+
raise TwilioAPIConcurrentError
|
|
101
|
+
|
|
90
102
|
if response.status in TWILIO_UNSUCCESSFUL_STATUSES:
|
|
91
103
|
retry_response = self._attempt_send()
|
|
92
104
|
|
|
93
105
|
if retry_response.status in TWILIO_UNSUCCESSFUL_STATUSES:
|
|
94
|
-
raise
|
|
95
|
-
|
|
96
|
-
|
|
106
|
+
raise TwilioResponseError(
|
|
107
|
+
retry_response.error_code,
|
|
108
|
+
retry_response.error_message
|
|
97
109
|
)
|
|
98
110
|
|
|
99
111
|
@staticmethod
|
|
@@ -105,8 +117,6 @@ class TwilioSMSHelper:
|
|
|
105
117
|
elif len(cleaned_number) == 11 and cleaned_number.startswith('1'):
|
|
106
118
|
formatted_number = cleaned_number
|
|
107
119
|
else:
|
|
108
|
-
raise
|
|
109
|
-
f'Invalid phone number format: {phone_number}.'
|
|
110
|
-
)
|
|
120
|
+
raise InvalidPhoneNumberError(phone_number)
|
|
111
121
|
|
|
112
122
|
return '+' + formatted_number
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from django.contrib.sites.models import Site
|
|
2
4
|
from django.db import models
|
|
3
5
|
from django.urls import reverse
|
|
@@ -5,8 +7,10 @@ from django.utils.timezone import now
|
|
|
5
7
|
|
|
6
8
|
from django_spire.notification.models import Notification
|
|
7
9
|
from django_spire.notification.sms.choices import SmsMediaContentTypeChoices
|
|
8
|
-
from django_spire.notification.sms.querysets import
|
|
10
|
+
from django_spire.notification.sms.querysets import (
|
|
11
|
+
SmsNotificationQuerySet,
|
|
9
12
|
SmsTemporaryMediaQuerySet
|
|
13
|
+
)
|
|
10
14
|
|
|
11
15
|
|
|
12
16
|
class SmsTemporaryMedia(models.Model):
|