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,90 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from django.contrib.contenttypes.models import ContentType
|
|
4
|
+
from django.utils.timezone import now
|
|
5
|
+
|
|
6
|
+
from django_spire.auth.user.tests.factories import create_user
|
|
7
|
+
from django_spire.core.tests.test_cases import BaseTestCase
|
|
8
|
+
from django_spire.history.viewed.models import Viewed
|
|
9
|
+
from django_spire.notification.app.models import AppNotification
|
|
10
|
+
from django_spire.notification.app.tests.factories import create_test_app_notification
|
|
11
|
+
from django_spire.notification.choices import NotificationStatusChoices
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AppNotificationQuerySetTests(BaseTestCase):
|
|
15
|
+
def setUp(self):
|
|
16
|
+
super().setUp()
|
|
17
|
+
self.user = create_user(username='test_app_notification_user')
|
|
18
|
+
self.app_notification = create_test_app_notification(user=self.user)
|
|
19
|
+
|
|
20
|
+
def _create_viewed_for_notification(self, app_notification: AppNotification, user):
|
|
21
|
+
content_type = ContentType.objects.get_for_model(AppNotification)
|
|
22
|
+
Viewed.objects.create(
|
|
23
|
+
user=user,
|
|
24
|
+
object_id=app_notification.id,
|
|
25
|
+
content_type=content_type
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def test_by_user(self):
|
|
29
|
+
other_user = create_user(username='other_user')
|
|
30
|
+
other_notification = create_test_app_notification(user=other_user)
|
|
31
|
+
|
|
32
|
+
result = AppNotification.objects.by_user(self.user)
|
|
33
|
+
assert self.app_notification in result
|
|
34
|
+
assert other_notification not in result
|
|
35
|
+
|
|
36
|
+
def test_by_users(self):
|
|
37
|
+
other_user = create_user(username='other_user')
|
|
38
|
+
other_notification = create_test_app_notification(user=other_user)
|
|
39
|
+
|
|
40
|
+
result = AppNotification.objects.by_users([self.user, other_user])
|
|
41
|
+
assert self.app_notification in result
|
|
42
|
+
assert other_notification in result
|
|
43
|
+
|
|
44
|
+
def test_is_sent(self):
|
|
45
|
+
pending_notification = create_test_app_notification(
|
|
46
|
+
user=self.user,
|
|
47
|
+
status=NotificationStatusChoices.PENDING
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
result = AppNotification.objects.is_sent()
|
|
51
|
+
assert self.app_notification in result
|
|
52
|
+
assert pending_notification not in result
|
|
53
|
+
|
|
54
|
+
def test_exclude_viewed_by_user(self):
|
|
55
|
+
self._create_viewed_for_notification(self.app_notification, self.user)
|
|
56
|
+
|
|
57
|
+
result = AppNotification.objects.exclude_viewed_by_user(self.user)
|
|
58
|
+
assert self.app_notification not in result
|
|
59
|
+
|
|
60
|
+
def test_annotate_is_viewed_by_user_false(self):
|
|
61
|
+
result = AppNotification.objects.annotate_is_viewed_by_user(self.user).first()
|
|
62
|
+
assert result.viewed is False
|
|
63
|
+
|
|
64
|
+
def test_annotate_is_viewed_by_user_true(self):
|
|
65
|
+
self._create_viewed_for_notification(self.app_notification, self.user)
|
|
66
|
+
|
|
67
|
+
result = AppNotification.objects.annotate_is_viewed_by_user(self.user).first()
|
|
68
|
+
assert result.viewed is True
|
|
69
|
+
|
|
70
|
+
def test_ordered_by_priority_and_sent_datetime(self):
|
|
71
|
+
same_time = now()
|
|
72
|
+
high_priority = create_test_app_notification(
|
|
73
|
+
user=self.user,
|
|
74
|
+
priority='high',
|
|
75
|
+
sent_datetime=same_time
|
|
76
|
+
)
|
|
77
|
+
low_priority = create_test_app_notification(
|
|
78
|
+
user=self.user,
|
|
79
|
+
priority='low',
|
|
80
|
+
sent_datetime=same_time
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
result = list(
|
|
84
|
+
AppNotification.objects
|
|
85
|
+
.filter(pk__in=[high_priority.pk, low_priority.pk])
|
|
86
|
+
.ordered_by_priority_and_sent_datetime()
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
assert result[0].pk == high_priority.pk
|
|
90
|
+
assert result[1].pk == low_priority.pk
|
|
File without changes
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from django.urls import reverse
|
|
4
|
+
|
|
5
|
+
from django_spire.core.tests.test_cases import BaseTestCase
|
|
6
|
+
from django_spire.notification.app.tests.factories import create_test_app_notification
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AppNotificationJsonViewsTests(BaseTestCase):
|
|
10
|
+
def setUp(self):
|
|
11
|
+
super().setUp()
|
|
12
|
+
self.app_notification = create_test_app_notification(user=self.super_user)
|
|
13
|
+
|
|
14
|
+
def test_check_new_notifications_ajax_view(self):
|
|
15
|
+
response = self.client.get(
|
|
16
|
+
reverse('django_spire:notification:app:json:check_new')
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
assert response.status_code == 200
|
|
20
|
+
assert 'has_new_notifications' in response.json()
|
|
21
|
+
|
|
22
|
+
def test_check_new_notifications_returns_true_when_unviewed(self):
|
|
23
|
+
response = self.client.get(
|
|
24
|
+
reverse('django_spire:notification:app:json:check_new')
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
data = response.json()
|
|
28
|
+
assert data['has_new_notifications'] is True
|
|
29
|
+
|
|
30
|
+
def test_set_notifications_as_viewed_ajax_view(self):
|
|
31
|
+
response = self.client.get(
|
|
32
|
+
reverse('django_spire:notification:app:json:set_viewed')
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
assert response.status_code == 200
|
|
36
|
+
assert response.json()['status'] == 200
|
|
37
|
+
|
|
38
|
+
def test_set_notifications_as_viewed_marks_as_viewed(self):
|
|
39
|
+
self.client.get(
|
|
40
|
+
reverse('django_spire:notification:app:json:set_viewed')
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
check_response = self.client.get(
|
|
44
|
+
reverse('django_spire:notification:app:json:check_new')
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
data = check_response.json()
|
|
48
|
+
assert data['has_new_notifications'] is False
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from django.urls import reverse
|
|
4
|
+
|
|
5
|
+
from django_spire.core.tests.test_cases import BaseTestCase
|
|
6
|
+
from django_spire.notification.app.tests.factories import create_test_app_notification
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AppNotificationPageViewsTests(BaseTestCase):
|
|
10
|
+
def setUp(self):
|
|
11
|
+
super().setUp()
|
|
12
|
+
self.app_notification = create_test_app_notification(user=self.super_user)
|
|
13
|
+
|
|
14
|
+
def test_app_notification_list_view_status_code(self):
|
|
15
|
+
response = self.client.get(
|
|
16
|
+
reverse('django_spire:notification:app:page:list')
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
assert response.status_code == 200
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from django.urls import path
|
|
2
4
|
|
|
3
5
|
from django_spire.notification.app.views import json_views
|
|
@@ -7,10 +9,10 @@ app_name = 'django_spire_notification'
|
|
|
7
9
|
|
|
8
10
|
urlpatterns = [
|
|
9
11
|
path('check/notification/ajax/',
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
json_views.check_new_notifications_ajax_view,
|
|
13
|
+
name='check_new'),
|
|
12
14
|
|
|
13
15
|
path('set_viewed/notification/ajax/',
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
json_views.set_notifications_as_viewed_ajax,
|
|
17
|
+
name='set_viewed'),
|
|
16
18
|
]
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from django.urls import path
|
|
2
4
|
|
|
3
5
|
from django_spire.notification.app.views import page_views
|
|
@@ -7,7 +9,6 @@ app_name = 'django_spire_notification'
|
|
|
7
9
|
|
|
8
10
|
urlpatterns = [
|
|
9
11
|
path('django_spire/notification/list/',
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
)
|
|
12
|
+
view=page_views.app_notification_list_view,
|
|
13
|
+
name='list')
|
|
13
14
|
]
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from django.urls import path
|
|
2
4
|
|
|
3
5
|
from django_spire.notification.app.views import template_views
|
|
@@ -7,6 +9,6 @@ app_name = 'django_spire_notification'
|
|
|
7
9
|
|
|
8
10
|
urlpatterns = [
|
|
9
11
|
path('notficiation/dropdown/template/',
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
template_views.notification_dropdown_template_view,
|
|
13
|
+
name='notification_dropdown')
|
|
12
14
|
]
|
|
@@ -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
|
|
|
@@ -17,6 +19,7 @@ class NotificationConfig(AppConfig):
|
|
|
17
19
|
|
|
18
20
|
def ready(self) -> None:
|
|
19
21
|
if not isinstance(getattr(settings, NOTIFICATION_THROTTLE_RATE_PER_MINUTE_SETTINGS_NAME), int):
|
|
20
|
-
|
|
22
|
+
message = f'"{NOTIFICATION_THROTTLE_RATE_PER_MINUTE_SETTINGS_NAME}" must be set in the django settings when using "{self.label}".'
|
|
23
|
+
raise TypeError(message)
|
|
21
24
|
|
|
22
25
|
check_required_apps(self.label)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from django.contrib import admin
|
|
2
4
|
|
|
3
5
|
from django_spire.notification.email.models import EmailNotification
|
|
@@ -6,5 +8,7 @@ from django_spire.notification.email.models import EmailNotification
|
|
|
6
8
|
@admin.register(EmailNotification)
|
|
7
9
|
class EmailNotificationAdmin(admin.ModelAdmin):
|
|
8
10
|
list_display = (
|
|
9
|
-
'id',
|
|
11
|
+
'id',
|
|
12
|
+
'notification',
|
|
13
|
+
'to_email_address'
|
|
10
14
|
)
|
|
@@ -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 NotificationEmailConfig(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,5 +1,7 @@
|
|
|
1
|
-
from
|
|
1
|
+
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from django_spire.exceptions import DjangoSpireError
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
|
|
6
|
+
class EmailNotificationError(DjangoSpireError):
|
|
5
7
|
pass
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
3
5
|
from django.conf import settings
|
|
4
6
|
from django.core.mail import EmailMessage
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
from django_spire.notification.models import Notification
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from django_spire.notification.models import Notification
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
class EmailHelper:
|
|
@@ -29,6 +31,7 @@ class EmailHelper:
|
|
|
29
31
|
self.fail_silently = fail_silently
|
|
30
32
|
|
|
31
33
|
self.attachments = []
|
|
34
|
+
|
|
32
35
|
for attachment in list(email.attachments.all()):
|
|
33
36
|
with attachment.file.open('rb') as f:
|
|
34
37
|
self.attachments.append((attachment.name, f.read(), attachment.type))
|
|
@@ -44,7 +47,6 @@ class SendGridEmailHelper(EmailHelper):
|
|
|
44
47
|
|
|
45
48
|
if notification.email.template_id == '':
|
|
46
49
|
self.template_id = settings.SENDGRID_TEMPLATE_ID
|
|
47
|
-
|
|
48
50
|
else:
|
|
49
51
|
self.template_id = notification.email.template_id
|
|
50
52
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from django.db import models
|
|
2
4
|
|
|
3
5
|
from django_spire.file.models import File
|
|
@@ -9,7 +11,9 @@ class EmailNotification(models.Model):
|
|
|
9
11
|
"""
|
|
10
12
|
It is important to note size limits for email content contained in this model. E.g., Sendgrid has a hard total email
|
|
11
13
|
limit of 30mb (and a recommended limit of 10mb for attachments): https://www.twilio.com/docs/sendgrid/ui/sending-email/attachments-with-digioh#-Limitations
|
|
14
|
+
|
|
12
15
|
"""
|
|
16
|
+
|
|
13
17
|
notification = models.OneToOneField(
|
|
14
18
|
Notification,
|
|
15
19
|
editable=False,
|
|
@@ -1,25 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from django.utils.timezone import now
|
|
4
|
+
|
|
2
5
|
from sendgrid import SendGridException
|
|
3
6
|
|
|
4
|
-
from django_spire.notification.choices import
|
|
5
|
-
NotificationStatusChoices
|
|
7
|
+
from django_spire.notification.choices import (
|
|
8
|
+
NotificationStatusChoices,
|
|
9
|
+
NotificationTypeChoices
|
|
10
|
+
)
|
|
6
11
|
from django_spire.notification.email.helper import SendGridEmailHelper
|
|
7
|
-
from django_spire.notification.exceptions import
|
|
12
|
+
from django_spire.notification.exceptions import InvalidNotificationTypeError
|
|
8
13
|
from django_spire.notification.models import Notification
|
|
9
14
|
from django_spire.notification.processors.processor import BaseNotificationProcessor
|
|
10
15
|
|
|
11
16
|
|
|
12
17
|
class EmailNotificationProcessor(BaseNotificationProcessor):
|
|
18
|
+
def _validate_notification_type(self, notification: Notification):
|
|
19
|
+
if notification.type != NotificationTypeChoices.EMAIL:
|
|
20
|
+
raise InvalidNotificationTypeError(NotificationTypeChoices.EMAIL, notification.type)
|
|
21
|
+
|
|
13
22
|
def process(self, notification: Notification):
|
|
14
23
|
notification.status = NotificationStatusChoices.PROCESSING
|
|
15
24
|
notification.save()
|
|
16
25
|
|
|
17
26
|
try:
|
|
18
|
-
|
|
19
|
-
raise NotificationException(
|
|
20
|
-
f"EmailNotificationProcessor only processes "
|
|
21
|
-
f"Email notifications. Was provided {notification.type}"
|
|
22
|
-
)
|
|
27
|
+
self._validate_notification_type(notification)
|
|
23
28
|
|
|
24
29
|
SendGridEmailHelper(notification).send()
|
|
25
30
|
|
|
@@ -27,11 +32,12 @@ class EmailNotificationProcessor(BaseNotificationProcessor):
|
|
|
27
32
|
notification.sent_datetime = now()
|
|
28
33
|
except Exception as e:
|
|
29
34
|
notification.status_message = str(e)
|
|
35
|
+
|
|
30
36
|
if isinstance(e, SendGridException):
|
|
31
37
|
notification.status = NotificationStatusChoices.ERRORED
|
|
32
38
|
else:
|
|
33
39
|
notification.status = NotificationStatusChoices.FAILED
|
|
34
|
-
raise
|
|
40
|
+
raise
|
|
35
41
|
finally:
|
|
36
42
|
notification.save()
|
|
37
43
|
|
|
@@ -40,11 +46,7 @@ class EmailNotificationProcessor(BaseNotificationProcessor):
|
|
|
40
46
|
|
|
41
47
|
for notification in notifications:
|
|
42
48
|
try:
|
|
43
|
-
|
|
44
|
-
raise NotificationException(
|
|
45
|
-
f"EmailNotificationProcessor only processes "
|
|
46
|
-
f"Email notifications. Was provided {notification.type}"
|
|
47
|
-
)
|
|
49
|
+
self._validate_notification_type(notification)
|
|
48
50
|
|
|
49
51
|
SendGridEmailHelper(notification).send()
|
|
50
52
|
|
|
@@ -56,11 +58,13 @@ class EmailNotificationProcessor(BaseNotificationProcessor):
|
|
|
56
58
|
notification.status = NotificationStatusChoices.ERRORED
|
|
57
59
|
else:
|
|
58
60
|
notification.status = NotificationStatusChoices.FAILED
|
|
61
|
+
|
|
59
62
|
Notification.objects.bulk_update(
|
|
60
63
|
notifications,
|
|
61
64
|
['status', 'sent_datetime', 'status_message']
|
|
62
65
|
)
|
|
63
|
-
|
|
66
|
+
|
|
67
|
+
raise
|
|
64
68
|
|
|
65
69
|
Notification.objects.bulk_update(
|
|
66
70
|
notifications,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
3
5
|
from django.db.models import QuerySet, Value, When, Case, BooleanField
|
|
4
6
|
|
|
5
7
|
from django_spire.history.querysets import HistoryQuerySet
|
|
@@ -9,6 +11,7 @@ from django_spire.notification.querysets import NotificationContentObjectQuerySe
|
|
|
9
11
|
if TYPE_CHECKING:
|
|
10
12
|
from django.contrib.auth.models import User
|
|
11
13
|
|
|
14
|
+
|
|
12
15
|
class EmailNotificationQuerySet(HistoryQuerySet, NotificationContentObjectQuerySet):
|
|
13
16
|
def annotate_is_viewed_by_user(self, user: User) -> QuerySet:
|
|
14
17
|
return self.by_user(user=user).annotate(viewed=Case(
|
|
File without changes
|
|
@@ -0,0 +1,35 @@
|
|
|
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.email.models import EmailNotification
|
|
10
|
+
from django_spire.notification.models import Notification
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def create_test_email_notification(**kwargs) -> EmailNotification:
|
|
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.EMAIL,
|
|
19
|
+
title=kwargs.pop('title', 'Test Email Notification'),
|
|
20
|
+
body=kwargs.pop('body', 'This is a test email notification.'),
|
|
21
|
+
url=kwargs.pop('url', 'https://example.com'),
|
|
22
|
+
status=kwargs.pop('status', NotificationStatusChoices.PENDING),
|
|
23
|
+
priority=kwargs.pop('priority', 'low'),
|
|
24
|
+
)
|
|
25
|
+
kwargs['notification'] = notification
|
|
26
|
+
|
|
27
|
+
data = {
|
|
28
|
+
'to_email_address': 'test@example.com',
|
|
29
|
+
'template_id': '',
|
|
30
|
+
'context_data': {},
|
|
31
|
+
'cc': [],
|
|
32
|
+
'bcc': [],
|
|
33
|
+
}
|
|
34
|
+
data.update(kwargs)
|
|
35
|
+
return EmailNotification.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.email.apps import NotificationEmailConfig
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class NotificationEmailConfigTests(BaseTestCase):
|
|
10
|
+
def test_app_name(self):
|
|
11
|
+
assert NotificationEmailConfig.name == 'django_spire.notification.email'
|
|
12
|
+
|
|
13
|
+
def test_app_label(self):
|
|
14
|
+
assert NotificationEmailConfig.label == 'django_spire_notification_email'
|
|
15
|
+
|
|
16
|
+
def test_default_auto_field(self):
|
|
17
|
+
assert NotificationEmailConfig.default_auto_field == 'django.db.models.BigAutoField'
|
|
18
|
+
|
|
19
|
+
def test_required_apps(self):
|
|
20
|
+
expected = ('django_spire_core', 'django_spire_notification')
|
|
21
|
+
assert NotificationEmailConfig.REQUIRED_APPS == expected
|
|
22
|
+
|
|
23
|
+
def test_app_is_installed(self):
|
|
24
|
+
assert apps.is_installed('django_spire.notification.email')
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from django_spire.core.tests.test_cases import BaseTestCase
|
|
4
|
+
from django_spire.notification.email.tests.factories import create_test_email_notification
|
|
5
|
+
from django_spire.notification.models import Notification
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class EmailNotificationModelTests(BaseTestCase):
|
|
9
|
+
def setUp(self):
|
|
10
|
+
super().setUp()
|
|
11
|
+
self.email_notification = create_test_email_notification()
|
|
12
|
+
|
|
13
|
+
def test_str(self):
|
|
14
|
+
expected = f'{self.email_notification.notification.title} - {self.email_notification.to_email_address}'
|
|
15
|
+
assert str(self.email_notification) == expected
|
|
16
|
+
|
|
17
|
+
def test_notification_relationship(self):
|
|
18
|
+
assert self.email_notification.notification is not None
|
|
19
|
+
assert isinstance(self.email_notification.notification, Notification)
|
|
20
|
+
|
|
21
|
+
def test_default_template_id(self):
|
|
22
|
+
assert self.email_notification.template_id == ''
|
|
23
|
+
|
|
24
|
+
def test_default_context_data(self):
|
|
25
|
+
assert self.email_notification.context_data == {}
|
|
26
|
+
|
|
27
|
+
def test_default_cc(self):
|
|
28
|
+
assert self.email_notification.cc == []
|
|
29
|
+
|
|
30
|
+
def test_default_bcc(self):
|
|
31
|
+
assert self.email_notification.bcc == []
|
|
32
|
+
|
|
33
|
+
def test_to_email_address(self):
|
|
34
|
+
assert self.email_notification.to_email_address == 'test@example.com'
|
|
35
|
+
|
|
36
|
+
def test_with_cc(self):
|
|
37
|
+
email_notification = create_test_email_notification(
|
|
38
|
+
cc=['cc1@example.com', 'cc2@example.com']
|
|
39
|
+
)
|
|
40
|
+
assert email_notification.cc == ['cc1@example.com', 'cc2@example.com']
|
|
41
|
+
|
|
42
|
+
def test_with_bcc(self):
|
|
43
|
+
email_notification = create_test_email_notification(
|
|
44
|
+
bcc=['bcc1@example.com', 'bcc2@example.com']
|
|
45
|
+
)
|
|
46
|
+
assert email_notification.bcc == ['bcc1@example.com', 'bcc2@example.com']
|
|
47
|
+
|
|
48
|
+
def test_with_context_data(self):
|
|
49
|
+
email_notification = create_test_email_notification(
|
|
50
|
+
context_data={'key': 'value'}
|
|
51
|
+
)
|
|
52
|
+
assert email_notification.context_data == {'key': 'value'}
|
|
@@ -0,0 +1,92 @@
|
|
|
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.email.models import EmailNotification
|
|
14
|
+
from django_spire.notification.email.processor import EmailNotificationProcessor
|
|
15
|
+
from django_spire.notification.email.tests.factories import create_test_email_notification
|
|
16
|
+
from django_spire.notification.exceptions import NotificationError
|
|
17
|
+
from django_spire.notification.models import Notification
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class EmailNotificationProcessorTests(BaseTestCase):
|
|
21
|
+
def setUp(self):
|
|
22
|
+
super().setUp()
|
|
23
|
+
self.user = create_user(username='test_email_processor_user')
|
|
24
|
+
self.processor = EmailNotificationProcessor()
|
|
25
|
+
|
|
26
|
+
@patch('django_spire.notification.email.processor.SendGridEmailHelper')
|
|
27
|
+
def test_process_sets_status_to_sent(self, mock_helper_class: MagicMock):
|
|
28
|
+
mock_helper = MagicMock()
|
|
29
|
+
mock_helper_class.return_value = mock_helper
|
|
30
|
+
|
|
31
|
+
email_notification = create_test_email_notification(user=self.user)
|
|
32
|
+
notification = email_notification.notification
|
|
33
|
+
|
|
34
|
+
self.processor.process(notification)
|
|
35
|
+
|
|
36
|
+
notification.refresh_from_db()
|
|
37
|
+
assert notification.status == NotificationStatusChoices.SENT
|
|
38
|
+
|
|
39
|
+
@patch('django_spire.notification.email.processor.SendGridEmailHelper')
|
|
40
|
+
def test_process_sets_sent_datetime(self, mock_helper_class: MagicMock):
|
|
41
|
+
mock_helper = MagicMock()
|
|
42
|
+
mock_helper_class.return_value = mock_helper
|
|
43
|
+
|
|
44
|
+
email_notification = create_test_email_notification(user=self.user)
|
|
45
|
+
notification = email_notification.notification
|
|
46
|
+
|
|
47
|
+
self.processor.process(notification)
|
|
48
|
+
|
|
49
|
+
notification.refresh_from_db()
|
|
50
|
+
assert notification.sent_datetime is not None
|
|
51
|
+
|
|
52
|
+
@patch('django_spire.notification.email.processor.SendGridEmailHelper')
|
|
53
|
+
def test_process_calls_send(self, mock_helper_class: MagicMock):
|
|
54
|
+
mock_helper = MagicMock()
|
|
55
|
+
mock_helper_class.return_value = mock_helper
|
|
56
|
+
|
|
57
|
+
email_notification = create_test_email_notification(user=self.user)
|
|
58
|
+
notification = email_notification.notification
|
|
59
|
+
|
|
60
|
+
self.processor.process(notification)
|
|
61
|
+
|
|
62
|
+
mock_helper.send.assert_called_once()
|
|
63
|
+
|
|
64
|
+
def test_process_raises_error_for_wrong_type(self):
|
|
65
|
+
notification = Notification.objects.create(
|
|
66
|
+
user=self.user,
|
|
67
|
+
type=NotificationTypeChoices.APP,
|
|
68
|
+
title='Test',
|
|
69
|
+
body='Test',
|
|
70
|
+
status=NotificationStatusChoices.PENDING,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
with pytest.raises(NotificationError):
|
|
74
|
+
self.processor.process(notification)
|
|
75
|
+
|
|
76
|
+
@patch('django_spire.notification.email.processor.SendGridEmailHelper')
|
|
77
|
+
def test_process_list(self, mock_helper_class: MagicMock):
|
|
78
|
+
mock_helper = MagicMock()
|
|
79
|
+
mock_helper_class.return_value = mock_helper
|
|
80
|
+
|
|
81
|
+
notifications = [
|
|
82
|
+
create_test_email_notification(user=self.user).notification
|
|
83
|
+
for _ in range(3)
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
self.processor.process_list(notifications)
|
|
87
|
+
|
|
88
|
+
for notification in notifications:
|
|
89
|
+
notification.refresh_from_db()
|
|
90
|
+
assert notification.status == NotificationStatusChoices.SENT
|
|
91
|
+
|
|
92
|
+
assert mock_helper.send.call_count == 3
|