django-spire 0.23.7__py3-none-any.whl → 0.23.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_spire/ai/admin.py +11 -11
- django_spire/ai/chat/apps.py +1 -0
- django_spire/ai/chat/templates/django_spire/ai/chat/widget/dialog_widget.html +1 -1
- django_spire/ai/chat/tests/factories.py +15 -0
- django_spire/ai/chat/tests/test_controller.py +45 -0
- django_spire/ai/chat/tests/test_models.py +301 -0
- django_spire/ai/chat/tests/test_prompts.py +48 -0
- django_spire/ai/chat/tests/test_responses.py +208 -0
- django_spire/ai/chat/tests/test_router/test_base_chat_router.py +66 -6
- django_spire/ai/chat/tests/test_router/test_chat_workflow.py +73 -3
- django_spire/ai/chat/tests/test_router/test_integration.py +86 -6
- django_spire/ai/chat/tests/test_router/test_intent_decoder.py +93 -1
- django_spire/ai/chat/tests/test_router/test_message_intel.py +60 -1
- django_spire/ai/chat/tests/test_router/test_spire_chat_router.py +110 -0
- django_spire/ai/chat/tests/test_urls/test_json_urls.py +202 -1
- django_spire/ai/context/tests/__init__.py +0 -0
- django_spire/ai/context/tests/test_context.py +188 -0
- django_spire/ai/decorators.py +7 -6
- django_spire/ai/prompt/tests/test_bots.py +100 -10
- django_spire/ai/prompt/tests/test_prompt_intel.py +83 -0
- django_spire/ai/prompt/tests/test_prompt_tuning.py +126 -0
- django_spire/ai/sms/decorators.py +8 -2
- django_spire/ai/sms/tests/test_sms.py +240 -16
- django_spire/ai/sms/tests/test_sms_intel.py +42 -0
- django_spire/ai/sms/tests/test_webhook.py +155 -7
- django_spire/ai/sms/views.py +23 -24
- django_spire/ai/tests/test_ai.py +131 -7
- django_spire/auth/apps.py +4 -2
- django_spire/auth/controller/controller.py +36 -23
- django_spire/auth/controller/exceptions.py +9 -0
- django_spire/auth/group/admin.py +1 -0
- django_spire/auth/group/apps.py +2 -0
- django_spire/auth/group/factories.py +17 -8
- django_spire/auth/group/forms.py +7 -0
- django_spire/auth/group/tests/test_factories.py +146 -0
- django_spire/auth/group/tests/test_forms.py +282 -0
- django_spire/auth/group/tests/test_models.py +192 -0
- django_spire/auth/group/tests/test_querysets.py +98 -0
- django_spire/auth/group/tests/test_utils.py +341 -0
- django_spire/auth/group/tests/test_views.py +377 -0
- django_spire/auth/group/urls/__init__.py +3 -1
- django_spire/auth/group/urls/form_urls.py +2 -0
- django_spire/auth/group/urls/json_urls.py +3 -0
- django_spire/auth/group/urls/page_urls.py +2 -0
- django_spire/auth/group/utils.py +6 -2
- django_spire/auth/group/views/form_views.py +6 -3
- django_spire/auth/group/views/json_views.py +6 -2
- django_spire/auth/mfa/admin.py +2 -0
- django_spire/auth/mfa/apps.py +2 -0
- django_spire/auth/mfa/forms.py +1 -0
- django_spire/auth/mfa/querysets.py +9 -2
- django_spire/auth/mfa/tests/test_models.py +233 -0
- django_spire/auth/mfa/tests/test_utils.py +106 -0
- django_spire/auth/mfa/urls/__init__.py +2 -0
- django_spire/auth/mfa/urls/page_urls.py +2 -0
- django_spire/auth/mfa/urls/redirect_urls.py +2 -0
- django_spire/auth/mfa/views/page_views.py +2 -1
- django_spire/auth/permissions/consts.py +2 -2
- django_spire/auth/permissions/decorators.py +8 -8
- django_spire/auth/permissions/permissions.py +28 -35
- django_spire/auth/permissions/tests/test_decorators.py +333 -0
- django_spire/auth/permissions/tests/test_permissions.py +337 -0
- django_spire/auth/permissions/tests/test_tools.py +305 -0
- django_spire/auth/permissions/tools.py +21 -15
- django_spire/auth/seeding/seed.py +3 -0
- django_spire/auth/seeding/seeder.py +2 -0
- django_spire/auth/tests/test_controller.py +323 -0
- django_spire/auth/tests/test_url_endpoints.py +9 -9
- django_spire/auth/tests/test_views.py +406 -0
- django_spire/auth/urls/admin_urls.py +2 -0
- django_spire/auth/urls/redirect_urls.py +2 -0
- django_spire/auth/user/apps.py +2 -0
- django_spire/auth/user/forms.py +9 -0
- django_spire/auth/user/models.py +1 -1
- django_spire/auth/user/services/services.py +1 -0
- django_spire/auth/user/tests/factories.py +14 -13
- django_spire/auth/user/tests/test_factories.py +166 -2
- django_spire/auth/user/tests/test_forms.py +573 -0
- django_spire/auth/user/tests/test_models.py +257 -0
- django_spire/auth/user/tests/test_services.py +200 -0
- django_spire/auth/user/tests/test_tools.py +153 -0
- django_spire/auth/user/tests/test_user_factories.py +139 -0
- django_spire/auth/user/tests/test_views.py +363 -0
- django_spire/auth/user/tools.py +7 -1
- django_spire/auth/user/urls/form_urls.py +3 -0
- django_spire/auth/user/urls/page_urls.py +3 -0
- django_spire/auth/user/views/form_views.py +19 -10
- django_spire/auth/user/views/page_views.py +8 -2
- django_spire/auth/views/redirect_views.py +14 -9
- django_spire/comment/admin.py +2 -0
- django_spire/comment/apps.py +2 -0
- django_spire/comment/templatetags/comment_tags.py +1 -0
- django_spire/comment/tests/test_forms.py +27 -0
- django_spire/comment/tests/test_models.py +215 -0
- django_spire/comment/tests/test_querysets.py +101 -0
- django_spire/comment/tests/test_utils.py +90 -0
- django_spire/comment/urls.py +2 -0
- django_spire/comment/utils.py +22 -13
- django_spire/comment/views.py +1 -1
- django_spire/conf.py +8 -6
- django_spire/consts.py +1 -1
- django_spire/contrib/breadcrumb/apps.py +2 -0
- django_spire/contrib/breadcrumb/breadcrumbs.py +18 -18
- django_spire/contrib/breadcrumb/tests/test_breadcrumbs.py +198 -0
- django_spire/contrib/constructor/__init__.py +3 -3
- django_spire/contrib/constructor/constructor.py +15 -15
- django_spire/contrib/constructor/django_model_constructor.py +5 -4
- django_spire/contrib/constructor/exceptions.py +5 -3
- django_spire/contrib/constructor/tests/__init__.py +0 -0
- django_spire/contrib/constructor/tests/test_constructor.py +193 -0
- django_spire/contrib/form/tests/__init__.py +0 -0
- django_spire/contrib/form/tests/test_forms.py +203 -0
- django_spire/contrib/generic_views/modal_views.py +2 -1
- django_spire/contrib/generic_views/portal_views.py +20 -19
- django_spire/contrib/generic_views/tests/__init__.py +0 -0
- django_spire/contrib/generic_views/tests/test_views.py +459 -0
- django_spire/contrib/help/apps.py +2 -0
- django_spire/contrib/help/templatetags/help.py +1 -0
- django_spire/contrib/help/tests/__init__.py +0 -0
- django_spire/contrib/help/tests/test_templatetags.py +100 -0
- django_spire/contrib/options/mixins.py +6 -5
- django_spire/contrib/options/tests/factories.py +5 -1
- django_spire/contrib/options/tests/test_options.py +234 -0
- django_spire/contrib/ordering/exceptions.py +7 -3
- django_spire/contrib/ordering/mixins.py +2 -0
- django_spire/contrib/ordering/querysets.py +3 -1
- django_spire/contrib/ordering/services/processor_service.py +8 -4
- django_spire/contrib/ordering/services/service.py +1 -2
- django_spire/contrib/ordering/tests/__init__.py +0 -0
- django_spire/contrib/ordering/tests/test_ordering.py +165 -0
- django_spire/contrib/ordering/validators.py +6 -6
- django_spire/contrib/pagination/templatetags/pagination_tags.py +12 -5
- django_spire/contrib/pagination/tests/__init__.py +0 -0
- django_spire/contrib/pagination/tests/test_pagination.py +179 -0
- django_spire/contrib/performance/decorators.py +16 -6
- django_spire/contrib/performance/tests/__init__.py +0 -0
- django_spire/contrib/performance/tests/test_performance.py +107 -0
- django_spire/contrib/queryset/enums.py +3 -1
- django_spire/contrib/queryset/filter_tools.py +10 -5
- django_spire/contrib/queryset/mixins.py +16 -16
- django_spire/contrib/queryset/tests/__init__.py +0 -0
- django_spire/contrib/queryset/tests/test_queryset.py +137 -0
- django_spire/contrib/seeding/field/base.py +13 -7
- django_spire/contrib/seeding/field/callable.py +8 -1
- django_spire/contrib/seeding/field/cleaners.py +5 -5
- django_spire/contrib/seeding/field/custom.py +20 -10
- django_spire/contrib/seeding/field/django/seeder.py +8 -6
- django_spire/contrib/seeding/field/enums.py +7 -5
- django_spire/contrib/seeding/field/override.py +16 -6
- django_spire/contrib/seeding/field/static.py +9 -2
- django_spire/contrib/seeding/field/tests/test_base.py +18 -14
- django_spire/contrib/seeding/field/tests/test_callable.py +13 -9
- django_spire/contrib/seeding/field/tests/test_cleaners.py +51 -38
- django_spire/contrib/seeding/field/tests/test_static.py +13 -9
- django_spire/contrib/seeding/intelligence/bots/seeder_generator_bot.py +2 -0
- django_spire/contrib/seeding/intelligence/intel.py +5 -1
- django_spire/contrib/seeding/intelligence/prompts/factory.py +6 -1
- django_spire/contrib/seeding/intelligence/prompts/foreign_key_selection_prompt.py +6 -1
- django_spire/contrib/seeding/intelligence/prompts/generate_django_model_seeder_prompts.py +2 -0
- django_spire/contrib/seeding/intelligence/prompts/generic_relationship_selection_prompt.py +7 -1
- django_spire/contrib/seeding/intelligence/prompts/hierarchical_selection_prompt.py +6 -2
- django_spire/contrib/seeding/intelligence/prompts/model_field_choices_prompt.py +8 -2
- django_spire/contrib/seeding/intelligence/prompts/negation_prompt.py +2 -0
- django_spire/contrib/seeding/intelligence/prompts/objective_prompt.py +6 -1
- django_spire/contrib/seeding/management/commands/seeding.py +9 -3
- django_spire/contrib/seeding/management/example.py +2 -0
- django_spire/contrib/seeding/model/base.py +16 -7
- django_spire/contrib/seeding/model/config.py +31 -15
- django_spire/contrib/seeding/model/django/config.py +13 -13
- django_spire/contrib/seeding/model/django/seeder.py +4 -4
- django_spire/contrib/seeding/model/django/tests/test_seeder.py +34 -23
- django_spire/contrib/seeding/model/enums.py +2 -0
- django_spire/contrib/seeding/tests/test_config.py +71 -0
- django_spire/contrib/seeding/tests/test_custom.py +35 -0
- django_spire/contrib/seeding/tests/test_enums.py +40 -0
- django_spire/contrib/seeding/tests/test_intel.py +32 -0
- django_spire/contrib/seeding/tests/test_override.py +63 -0
- django_spire/contrib/service/__init__.py +2 -2
- django_spire/contrib/service/django_model_service.py +16 -15
- django_spire/contrib/service/exceptions.py +5 -3
- django_spire/contrib/service/tests/__init__.py +0 -0
- django_spire/contrib/service/tests/test_service.py +153 -0
- django_spire/contrib/session/apps.py +2 -0
- django_spire/contrib/session/controller.py +48 -42
- django_spire/contrib/session/templatetags/session_tags.py +11 -2
- django_spire/contrib/session/tests/test_session_controller.py +117 -53
- django_spire/contrib/tests/__init__.py +0 -0
- django_spire/contrib/tests/test_utils.py +37 -0
- django_spire/contrib/utils.py +4 -1
- django_spire/core/apps.py +2 -0
- django_spire/core/converters/tests/test_to_data.py +353 -0
- django_spire/core/converters/tests/test_to_enums.py +61 -41
- django_spire/core/converters/tests/test_to_pydantic.py +138 -109
- django_spire/core/converters/to_data.py +29 -10
- django_spire/core/converters/to_enums.py +4 -2
- django_spire/core/converters/to_pydantic.py +22 -22
- django_spire/core/decorators.py +19 -6
- django_spire/core/forms/widgets.py +4 -0
- django_spire/core/maps.py +3 -1
- django_spire/core/middleware/maintenance.py +3 -3
- django_spire/core/middleware.py +8 -6
- django_spire/core/redirect/__init__.py +5 -0
- django_spire/core/redirect/generic_redirect.py +1 -2
- django_spire/core/redirect/tests/__init__.py +0 -0
- django_spire/core/redirect/tests/test_generic_redirect.py +34 -0
- django_spire/core/{tests/tests_redirect.py → redirect/tests/test_safe_redirect.py} +55 -81
- django_spire/core/shortcuts.py +3 -3
- django_spire/core/static/django_spire/css/app-layout.css +1 -1
- django_spire/core/static/django_spire/css/app-navigation.css +3 -3
- django_spire/core/static/django_spire/css/bootstrap-override.css +4 -0
- django_spire/core/tag/admin.py +12 -0
- django_spire/core/tag/intelligence/tag_set_bot.py +2 -0
- django_spire/core/tag/mixins.py +2 -0
- django_spire/core/tag/models.py +2 -0
- django_spire/core/tag/querysets.py +2 -0
- django_spire/core/tag/service/tag_service.py +6 -3
- django_spire/core/tag/tests/test_intelligence.py +9 -9
- django_spire/core/tag/tests/test_tags.py +44 -54
- django_spire/core/tag/tests/test_tools.py +191 -0
- django_spire/core/tag/tools.py +3 -0
- django_spire/core/templates/django_spire/card/card.html +5 -2
- django_spire/core/templates/django_spire/card/title_card.html +9 -4
- django_spire/core/templates/django_spire/infinite_scroll/base.html +1 -0
- django_spire/core/templates/django_spire/navigation/side_navigation.html +19 -24
- django_spire/core/templates/django_spire/page/full_page.html +46 -16
- django_spire/core/templates/django_spire/table/base.html +4 -2
- django_spire/core/templatetags/json.py +6 -2
- django_spire/core/templatetags/message.py +13 -8
- django_spire/core/templatetags/string_formating.py +8 -5
- django_spire/core/templatetags/tests/__init__.py +0 -0
- django_spire/core/templatetags/tests/test_templatetags.py +427 -0
- django_spire/core/templatetags/variable_types.py +17 -9
- django_spire/core/tests/test_cases.py +1 -1
- django_spire/core/tests/test_conf.py +43 -0
- django_spire/core/tests/test_consts.py +28 -0
- django_spire/core/tests/test_context_processors.py +93 -0
- django_spire/core/tests/test_decorators.py +95 -0
- django_spire/core/tests/test_django_spire_utils.py +56 -0
- django_spire/core/tests/test_exceptions.py +37 -0
- django_spire/core/tests/test_models.py +54 -0
- django_spire/core/tests/test_settings.py +45 -0
- django_spire/core/tests/test_shortcuts.py +74 -0
- django_spire/core/tests/test_urls.py +16 -0
- django_spire/core/tests/test_utils.py +58 -0
- django_spire/core/urls.py +4 -1
- django_spire/core/utils.py +12 -8
- django_spire/exceptions.py +16 -1
- django_spire/file/admin.py +4 -2
- django_spire/file/apps.py +8 -10
- django_spire/file/fields.py +7 -7
- django_spire/file/forms.py +1 -1
- django_spire/file/interfaces.py +15 -15
- django_spire/file/mixins.py +1 -4
- django_spire/file/models.py +3 -5
- django_spire/file/tests/factories.py +59 -0
- django_spire/file/tests/test_admin.py +69 -0
- django_spire/file/tests/test_apps.py +24 -0
- django_spire/file/tests/test_fields.py +114 -0
- django_spire/file/tests/test_forms.py +20 -0
- django_spire/file/tests/test_interfaces.py +183 -0
- django_spire/file/tests/test_models.py +82 -0
- django_spire/file/tests/test_querysets.py +102 -0
- django_spire/file/tests/test_utils.py +32 -0
- django_spire/file/tests/test_views.py +145 -0
- django_spire/file/tests/test_widgets.py +82 -0
- django_spire/file/tools.py +8 -2
- django_spire/file/views.py +7 -3
- django_spire/file/widgets.py +12 -12
- django_spire/help_desk/admin.py +15 -0
- django_spire/help_desk/apps.py +2 -0
- django_spire/help_desk/auth/controller.py +2 -0
- django_spire/help_desk/choices.py +2 -0
- django_spire/help_desk/enums.py +2 -0
- django_spire/help_desk/exceptions.py +31 -3
- django_spire/help_desk/forms.py +2 -0
- django_spire/help_desk/models.py +2 -0
- django_spire/help_desk/querysets.py +4 -1
- django_spire/help_desk/services/notification_service.py +26 -27
- django_spire/help_desk/services/service.py +2 -3
- django_spire/help_desk/tests/factories.py +8 -3
- django_spire/help_desk/tests/test_admin.py +41 -0
- django_spire/help_desk/tests/test_apps.py +41 -0
- django_spire/help_desk/tests/test_choices.py +50 -0
- django_spire/help_desk/tests/test_controller.py +87 -0
- django_spire/help_desk/tests/test_enums.py +18 -0
- django_spire/help_desk/tests/test_exceptions.py +37 -0
- django_spire/help_desk/tests/test_forms.py +89 -0
- django_spire/help_desk/tests/test_models.py +59 -0
- django_spire/help_desk/tests/test_querysets.py +38 -0
- django_spire/help_desk/tests/test_services/test_notification_service.py +15 -8
- django_spire/help_desk/tests/test_services/test_service.py +92 -0
- django_spire/help_desk/tests/test_urls/test_form_urls.py +6 -6
- django_spire/help_desk/tests/test_urls/test_page_urls.py +8 -9
- django_spire/help_desk/tests/test_views/test_form_views.py +46 -19
- django_spire/help_desk/tests/test_views/test_page_views.py +32 -9
- django_spire/help_desk/urls/__init__.py +4 -1
- django_spire/help_desk/urls/form_urls.py +3 -0
- django_spire/help_desk/urls/page_urls.py +3 -0
- django_spire/help_desk/views/form_views.py +13 -5
- django_spire/help_desk/views/page_views.py +11 -3
- django_spire/history/activity/admin.py +2 -0
- django_spire/history/activity/apps.py +3 -1
- django_spire/history/activity/mixins.py +13 -7
- django_spire/history/activity/models.py +6 -5
- django_spire/history/activity/querysets.py +2 -0
- django_spire/history/activity/tests/__init__.py +0 -0
- django_spire/history/activity/tests/test_activity.py +176 -0
- django_spire/history/admin.py +9 -2
- django_spire/history/choices.py +3 -0
- django_spire/history/models.py +5 -5
- django_spire/history/tests/test_admin.py +93 -0
- django_spire/history/tests/test_history.py +101 -0
- django_spire/history/tests/test_mixins.py +84 -0
- django_spire/history/viewed/admin.py +3 -1
- django_spire/history/viewed/apps.py +3 -1
- django_spire/history/viewed/models.py +2 -0
- django_spire/history/viewed/tests/__init__.py +0 -0
- django_spire/history/viewed/tests/test_viewed.py +46 -0
- django_spire/knowledge/auth/tests/__init__.py +0 -0
- django_spire/knowledge/auth/tests/test_controller.py +116 -0
- django_spire/knowledge/collection/admin.py +5 -1
- django_spire/knowledge/collection/models.py +3 -1
- django_spire/knowledge/collection/seeding/seed.py +1 -0
- django_spire/knowledge/collection/services/factory_service.py +10 -11
- django_spire/knowledge/collection/services/ordering_service.py +1 -2
- django_spire/knowledge/collection/services/service.py +5 -10
- django_spire/knowledge/collection/services/tag_service.py +5 -2
- django_spire/knowledge/collection/tests/factories.py +28 -1
- django_spire/knowledge/collection/tests/test_models.py +48 -0
- django_spire/knowledge/collection/tests/test_querysets.py +93 -0
- django_spire/knowledge/collection/tests/test_services/test_factory_service.py +100 -0
- django_spire/knowledge/collection/tests/test_services/test_services.py +160 -0
- django_spire/knowledge/collection/tests/test_urls/test_form_urls.py +21 -3
- django_spire/knowledge/collection/tests/test_urls/test_json_urls.py +39 -1
- django_spire/knowledge/collection/tests/test_urls/test_page_urls.py +12 -4
- django_spire/knowledge/collection/urls/__init__.py +3 -0
- django_spire/knowledge/collection/urls/form_urls.py +2 -0
- django_spire/knowledge/collection/urls/json_urls.py +2 -0
- django_spire/knowledge/collection/urls/page_urls.py +2 -0
- django_spire/knowledge/collection/views/form_views.py +4 -4
- django_spire/knowledge/collection/views/json_views.py +5 -1
- django_spire/knowledge/collection/views/page_views.py +5 -2
- django_spire/knowledge/entry/admin.py +7 -1
- django_spire/knowledge/entry/forms.py +2 -0
- django_spire/knowledge/entry/models.py +2 -0
- django_spire/knowledge/entry/seeding/seed.py +3 -0
- django_spire/knowledge/entry/services/automation_service.py +5 -4
- django_spire/knowledge/entry/services/factory_service.py +7 -5
- django_spire/knowledge/entry/services/service.py +4 -7
- django_spire/knowledge/entry/services/tag_service.py +0 -1
- django_spire/knowledge/entry/services/tool_service.py +1 -0
- django_spire/knowledge/entry/services/transformation_services.py +1 -5
- django_spire/knowledge/entry/tests/factories.py +1 -2
- django_spire/knowledge/entry/tests/test_factory_service.py +20 -0
- django_spire/knowledge/entry/tests/test_models.py +41 -0
- django_spire/knowledge/entry/tests/test_querysets.py +71 -0
- django_spire/knowledge/entry/tests/test_services.py +94 -0
- django_spire/knowledge/entry/tests/test_urls/test_form_urls.py +9 -14
- django_spire/knowledge/entry/tests/test_urls/test_json_urls.py +48 -5
- django_spire/knowledge/entry/tests/test_urls/test_page_urls.py +6 -8
- django_spire/knowledge/entry/tests/test_urls/test_template_urls.py +40 -0
- django_spire/knowledge/entry/urls/form_urls.py +2 -0
- django_spire/knowledge/entry/urls/json_urls.py +2 -0
- django_spire/knowledge/entry/urls/page_urls.py +2 -0
- django_spire/knowledge/entry/urls/template_urls.py +2 -0
- django_spire/knowledge/entry/version/block/choices.py +2 -0
- django_spire/knowledge/entry/version/block/data/data.py +1 -0
- django_spire/knowledge/entry/version/block/data/list/data.py +8 -13
- django_spire/knowledge/entry/version/block/data/list/maps.py +3 -0
- django_spire/knowledge/entry/version/block/data/list/meta.py +1 -2
- django_spire/knowledge/entry/version/block/data/list/tests/__init__.py +0 -0
- django_spire/knowledge/entry/version/block/data/list/tests/test_maps.py +32 -0
- django_spire/knowledge/entry/version/block/data/list/tests/test_meta.py +58 -0
- django_spire/knowledge/entry/version/block/data/maps.py +3 -6
- django_spire/knowledge/entry/version/block/models.py +7 -5
- django_spire/knowledge/entry/version/block/seeding/constants.py +5 -4
- django_spire/knowledge/entry/version/block/services/service.py +2 -3
- django_spire/knowledge/entry/version/block/tests/factories.py +4 -10
- django_spire/knowledge/entry/version/block/tests/test_choices.py +56 -0
- django_spire/knowledge/entry/version/block/tests/test_data.py +90 -0
- django_spire/knowledge/entry/version/block/tests/test_maps.py +37 -0
- django_spire/knowledge/entry/version/block/tests/test_models.py +55 -0
- django_spire/knowledge/entry/version/block/tests/test_querysets.py +35 -0
- django_spire/knowledge/entry/version/block/tests/test_services.py +65 -0
- django_spire/knowledge/entry/version/choices.py +2 -0
- django_spire/knowledge/entry/version/converters/converter.py +1 -1
- django_spire/knowledge/entry/version/converters/docx_converter.py +4 -7
- django_spire/knowledge/entry/version/converters/markdown_converter.py +20 -20
- django_spire/knowledge/entry/version/maps.py +4 -5
- django_spire/knowledge/entry/version/querysets.py +1 -1
- django_spire/knowledge/entry/version/seeding/seeder.py +1 -2
- django_spire/knowledge/entry/version/services/processor_service.py +5 -4
- django_spire/knowledge/entry/version/services/service.py +1 -2
- django_spire/knowledge/entry/version/tests/factories.py +2 -2
- django_spire/knowledge/entry/version/tests/test_choices.py +18 -0
- django_spire/knowledge/entry/version/tests/test_converters/test_docx_converter.py +56 -8
- django_spire/knowledge/entry/version/tests/test_converters/test_markdown_converter.py +78 -0
- django_spire/knowledge/entry/version/tests/test_maps.py +58 -0
- django_spire/knowledge/entry/version/tests/test_models.py +23 -0
- django_spire/knowledge/entry/version/tests/test_querysets.py +26 -0
- django_spire/knowledge/entry/version/tests/test_services.py +62 -0
- django_spire/knowledge/entry/version/tests/test_urls/test_json_urls.py +27 -8
- django_spire/knowledge/entry/version/tests/test_urls/test_page_urls.py +15 -8
- django_spire/knowledge/entry/version/tests/test_urls/test_redirect_urls.py +38 -0
- django_spire/knowledge/entry/version/urls/__init__.py +3 -0
- django_spire/knowledge/entry/version/urls/json_urls.py +2 -1
- django_spire/knowledge/entry/version/urls/page_urls.py +2 -0
- django_spire/knowledge/entry/version/urls/redirect_urls.py +2 -0
- django_spire/knowledge/entry/version/views/json_views.py +5 -1
- django_spire/knowledge/entry/version/views/page_views.py +10 -3
- django_spire/knowledge/entry/version/views/redirect_views.py +5 -1
- django_spire/knowledge/entry/views/form_views.py +16 -8
- django_spire/knowledge/entry/views/json_views.py +3 -1
- django_spire/knowledge/entry/views/page_views.py +8 -2
- django_spire/knowledge/entry/views/template_views.py +7 -1
- django_spire/knowledge/exceptions.py +2 -1
- django_spire/knowledge/intelligence/intel/answer_intel.py +2 -1
- django_spire/knowledge/intelligence/intel/entry_intel.py +0 -1
- django_spire/knowledge/intelligence/workflows/knowledge_workflow.py +4 -5
- django_spire/knowledge/models.py +1 -2
- django_spire/knowledge/tests/__init__.py +0 -0
- django_spire/knowledge/tests/test_templatetags.py +40 -0
- django_spire/knowledge/tests/test_urls/__init__.py +0 -0
- django_spire/knowledge/tests/test_urls/test_page_urls.py +24 -0
- django_spire/knowledge/urls/__init__.py +2 -0
- django_spire/knowledge/urls/page_urls.py +2 -0
- django_spire/knowledge/views/page_views.py +8 -3
- django_spire/notification/admin.py +3 -1
- django_spire/notification/app/admin.py +2 -0
- django_spire/notification/app/apps.py +3 -1
- django_spire/notification/app/exceptions.py +9 -2
- django_spire/notification/app/models.py +8 -4
- django_spire/notification/app/processor.py +22 -26
- django_spire/notification/app/querysets.py +2 -0
- django_spire/notification/app/tests/__init__.py +0 -0
- django_spire/notification/app/tests/factories.py +34 -0
- django_spire/notification/app/tests/test_apps.py +24 -0
- django_spire/notification/app/tests/test_models.py +72 -0
- django_spire/notification/app/tests/test_processor.py +111 -0
- django_spire/notification/app/tests/test_querysets.py +90 -0
- django_spire/notification/app/tests/test_views/__init__.py +0 -0
- django_spire/notification/app/tests/test_views/test_json_views.py +48 -0
- django_spire/notification/app/tests/test_views/test_page_views.py +19 -0
- django_spire/notification/app/urls/__init__.py +3 -1
- django_spire/notification/app/urls/json_urls.py +6 -4
- django_spire/notification/app/urls/page_urls.py +4 -3
- django_spire/notification/app/urls/template_urls.py +4 -2
- django_spire/notification/apps.py +4 -1
- django_spire/notification/email/admin.py +5 -1
- django_spire/notification/email/apps.py +3 -1
- django_spire/notification/email/exceptions.py +4 -2
- django_spire/notification/email/helper.py +5 -3
- django_spire/notification/email/models.py +4 -0
- django_spire/notification/email/processor.py +19 -15
- django_spire/notification/email/querysets.py +3 -0
- django_spire/notification/email/tests/__init__.py +0 -0
- django_spire/notification/email/tests/factories.py +35 -0
- django_spire/notification/email/tests/test_apps.py +24 -0
- django_spire/notification/email/tests/test_models.py +52 -0
- django_spire/notification/email/tests/test_processor.py +92 -0
- django_spire/notification/email/tests/test_querysets.py +43 -0
- django_spire/notification/exceptions.py +17 -2
- django_spire/notification/managers.py +7 -1
- django_spire/notification/maps.py +4 -1
- django_spire/notification/mixins.py +2 -0
- django_spire/notification/models.py +3 -1
- django_spire/notification/processors/notification.py +12 -5
- django_spire/notification/processors/processor.py +2 -0
- django_spire/notification/processors/tests/__init__.py +0 -0
- django_spire/notification/processors/tests/test_notification.py +106 -0
- django_spire/notification/push/admin.py +10 -1
- django_spire/notification/push/apps.py +3 -1
- django_spire/notification/push/models.py +2 -3
- django_spire/notification/push/tests/__init__.py +0 -0
- django_spire/notification/push/tests/test_apps.py +24 -0
- django_spire/notification/push/tests/test_models.py +28 -0
- django_spire/notification/querysets.py +7 -1
- django_spire/notification/sms/admin.py +2 -0
- django_spire/notification/sms/apps.py +4 -1
- django_spire/notification/sms/automations.py +2 -0
- django_spire/notification/sms/choices.py +2 -0
- django_spire/notification/sms/exceptions.py +19 -5
- django_spire/notification/sms/helper.py +33 -23
- django_spire/notification/sms/models.py +5 -1
- django_spire/notification/sms/processor.py +20 -20
- django_spire/notification/sms/querysets.py +2 -0
- django_spire/notification/sms/tests/factories.py +33 -0
- django_spire/notification/sms/tests/test_apps.py +24 -0
- django_spire/notification/sms/tests/test_automation.py +38 -0
- django_spire/notification/sms/tests/test_choices.py +15 -0
- django_spire/notification/sms/tests/test_consts.py +17 -0
- django_spire/notification/sms/tests/test_exceptions.py +27 -0
- django_spire/notification/sms/tests/test_helper.py +50 -0
- django_spire/notification/sms/tests/test_models.py +81 -0
- django_spire/notification/sms/tests/test_processor.py +107 -0
- django_spire/notification/sms/tests/test_tools.py +25 -11
- django_spire/notification/sms/tools.py +16 -5
- django_spire/notification/sms/urls/__init__.py +3 -1
- django_spire/notification/sms/urls/media_urls.py +2 -0
- django_spire/notification/sms/views/media_views.py +14 -4
- django_spire/notification/tests/__init__.py +0 -0
- django_spire/notification/tests/factories.py +26 -0
- django_spire/notification/tests/test_admin.py +55 -0
- django_spire/notification/tests/test_apps.py +30 -0
- django_spire/notification/tests/test_automation.py +18 -0
- django_spire/notification/tests/test_choices.py +59 -0
- django_spire/notification/tests/test_exceptions.py +58 -0
- django_spire/notification/tests/test_managers.py +100 -0
- django_spire/notification/tests/test_maps.py +31 -0
- django_spire/notification/tests/test_models.py +76 -0
- django_spire/notification/tests/test_querysets.py +184 -0
- django_spire/notification/tests/test_utils.py +23 -0
- django_spire/notification/urls.py +3 -1
- django_spire/notification/utils.py +3 -1
- django_spire/settings.py +3 -0
- django_spire/theme/tests/test_context_processor.py +15 -13
- django_spire/theme/tests/test_enums.py +2 -2
- django_spire/theme/tests/test_filesystem.py +2 -5
- django_spire/theme/tests/test_integration.py +12 -12
- django_spire/theme/tests/test_model.py +40 -38
- django_spire/theme/tests/test_views/test_json_views.py +33 -33
- django_spire/theme/urls/json_urls.py +3 -0
- django_spire/theme/urls/page_urls.py +3 -0
- django_spire/urls.py +19 -15
- django_spire/utils.py +13 -4
- {django_spire-0.23.7.dist-info → django_spire-0.23.8.dist-info}/METADATA +1 -1
- {django_spire-0.23.7.dist-info → django_spire-0.23.8.dist-info}/RECORD +530 -358
- django_spire/contrib/options/tests/test_unit.py +0 -148
- django_spire/contrib/seeding/tests/test_seeding.py +0 -25
- django_spire/core/tests/test_templatetags.py +0 -117
- django_spire/core/tests/tests_shortcuts.py +0 -73
- django_spire/history/activity/tests.py +0 -3
- django_spire/history/activity/views.py +0 -3
- django_spire/knowledge/collection/tests/test_services/test_transformation_service.py +0 -71
- django_spire/notification/app/tests.py +0 -3
- {django_spire-0.23.7.dist-info → django_spire-0.23.8.dist-info}/WHEEL +0 -0
- {django_spire-0.23.7.dist-info → django_spire-0.23.8.dist-info}/licenses/LICENSE.md +0 -0
- {django_spire-0.23.7.dist-info → django_spire-0.23.8.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from django_spire.ai.prompt.tuning.prompts import (
|
|
4
|
+
duplication_removal_bot_instruction_prompt,
|
|
5
|
+
example_optimization_bot_instruction_prompt,
|
|
6
|
+
formatting_bot_instruction_prompt,
|
|
7
|
+
instruction_clarity_bot_instruction_prompt,
|
|
8
|
+
persona_bot_instruction_prompt,
|
|
9
|
+
prompt_tuning_input_prompt,
|
|
10
|
+
prompt_tuning_instruction_bot_prompt,
|
|
11
|
+
specialized_bot_input_prompt,
|
|
12
|
+
)
|
|
13
|
+
from django_spire.ai.prompt.tuning.choices import OutcomeRatingChoices
|
|
14
|
+
from django_spire.ai.prompt.tuning.intel import PromptTestingIntel, PromptTuningIntel
|
|
15
|
+
from django_spire.core.tests.test_cases import BaseTestCase
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class OutcomeRatingChoicesTests(BaseTestCase):
|
|
19
|
+
def test_terrible_rating(self) -> None:
|
|
20
|
+
assert OutcomeRatingChoices.TERRIBLE.value == 1
|
|
21
|
+
assert OutcomeRatingChoices.TERRIBLE.label == 'terrible'
|
|
22
|
+
|
|
23
|
+
def test_bad_rating(self) -> None:
|
|
24
|
+
assert OutcomeRatingChoices.BAD.value == 2
|
|
25
|
+
assert OutcomeRatingChoices.BAD.label == 'bad'
|
|
26
|
+
|
|
27
|
+
def test_ok_rating(self) -> None:
|
|
28
|
+
assert OutcomeRatingChoices.OK.value == 3
|
|
29
|
+
assert OutcomeRatingChoices.OK.label == 'ok'
|
|
30
|
+
|
|
31
|
+
def test_good_rating(self) -> None:
|
|
32
|
+
assert OutcomeRatingChoices.GOOD.value == 4
|
|
33
|
+
assert OutcomeRatingChoices.GOOD.label == 'good'
|
|
34
|
+
|
|
35
|
+
def test_great_rating(self) -> None:
|
|
36
|
+
assert OutcomeRatingChoices.GREAT.value == 5
|
|
37
|
+
assert OutcomeRatingChoices.GREAT.label == 'great'
|
|
38
|
+
|
|
39
|
+
def test_amazing_rating(self) -> None:
|
|
40
|
+
assert OutcomeRatingChoices.AMAZING.value == 6
|
|
41
|
+
assert OutcomeRatingChoices.AMAZING.label == 'amazing'
|
|
42
|
+
|
|
43
|
+
def test_all_ratings_count(self) -> None:
|
|
44
|
+
assert len(OutcomeRatingChoices) == 6
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class PromptTuningIntelTests(BaseTestCase):
|
|
48
|
+
def test_prompt_tuning_intel_creation(self) -> None:
|
|
49
|
+
intel = PromptTuningIntel(prompt='Test prompt')
|
|
50
|
+
|
|
51
|
+
assert intel.prompt == 'Test prompt'
|
|
52
|
+
|
|
53
|
+
def test_prompt_tuning_intel_empty_prompt(self) -> None:
|
|
54
|
+
intel = PromptTuningIntel(prompt='')
|
|
55
|
+
|
|
56
|
+
assert intel.prompt == ''
|
|
57
|
+
|
|
58
|
+
def test_prompt_tuning_intel_long_prompt(self) -> None:
|
|
59
|
+
long_prompt = 'A' * 10000
|
|
60
|
+
intel = PromptTuningIntel(prompt=long_prompt)
|
|
61
|
+
|
|
62
|
+
assert intel.prompt == long_prompt
|
|
63
|
+
assert len(intel.prompt) == 10000
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class PromptTestingIntelTests(BaseTestCase):
|
|
67
|
+
def test_prompt_testing_intel_creation(self) -> None:
|
|
68
|
+
intel = PromptTestingIntel(result='Test result')
|
|
69
|
+
|
|
70
|
+
assert intel.result == 'Test result'
|
|
71
|
+
|
|
72
|
+
def test_prompt_testing_intel_empty_result(self) -> None:
|
|
73
|
+
intel = PromptTestingIntel(result='')
|
|
74
|
+
|
|
75
|
+
assert intel.result == ''
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class PromptTuningPromptsTests(BaseTestCase):
|
|
79
|
+
def test_prompt_tuning_instruction_bot_prompt(self) -> None:
|
|
80
|
+
prompt = prompt_tuning_instruction_bot_prompt()
|
|
81
|
+
|
|
82
|
+
assert prompt is not None
|
|
83
|
+
assert 'System Prompt Tuning Expert' in prompt.to_str()
|
|
84
|
+
|
|
85
|
+
def test_prompt_tuning_input_prompt(self) -> None:
|
|
86
|
+
prompt = prompt_tuning_input_prompt('Test system prompt', 'Test feedback')
|
|
87
|
+
|
|
88
|
+
assert prompt is not None
|
|
89
|
+
assert 'Test system prompt' in prompt.to_str()
|
|
90
|
+
assert 'Test feedback' in prompt.to_str()
|
|
91
|
+
|
|
92
|
+
def test_formatting_bot_instruction_prompt(self) -> None:
|
|
93
|
+
prompt = formatting_bot_instruction_prompt()
|
|
94
|
+
|
|
95
|
+
assert prompt is not None
|
|
96
|
+
assert 'Prompt Formatting Expert' in prompt.to_str()
|
|
97
|
+
|
|
98
|
+
def test_instruction_clarity_bot_instruction_prompt(self) -> None:
|
|
99
|
+
prompt = instruction_clarity_bot_instruction_prompt()
|
|
100
|
+
|
|
101
|
+
assert prompt is not None
|
|
102
|
+
assert 'Instruction Clarity Expert' in prompt.to_str()
|
|
103
|
+
|
|
104
|
+
def test_persona_bot_instruction_prompt(self) -> None:
|
|
105
|
+
prompt = persona_bot_instruction_prompt()
|
|
106
|
+
|
|
107
|
+
assert prompt is not None
|
|
108
|
+
assert 'Persona Consistency Expert' in prompt.to_str()
|
|
109
|
+
|
|
110
|
+
def test_duplication_removal_bot_instruction_prompt(self) -> None:
|
|
111
|
+
prompt = duplication_removal_bot_instruction_prompt()
|
|
112
|
+
|
|
113
|
+
assert prompt is not None
|
|
114
|
+
assert 'Duplication Removal Expert' in prompt.to_str()
|
|
115
|
+
|
|
116
|
+
def test_example_optimization_bot_instruction_prompt(self) -> None:
|
|
117
|
+
prompt = example_optimization_bot_instruction_prompt()
|
|
118
|
+
|
|
119
|
+
assert prompt is not None
|
|
120
|
+
assert 'Example Optimization Expert' in prompt.to_str()
|
|
121
|
+
|
|
122
|
+
def test_specialized_bot_input_prompt(self) -> None:
|
|
123
|
+
prompt = specialized_bot_input_prompt('Test system prompt')
|
|
124
|
+
|
|
125
|
+
assert prompt is not None
|
|
126
|
+
assert 'Test system prompt' in prompt.to_str()
|
|
@@ -3,13 +3,19 @@ from __future__ import annotations
|
|
|
3
3
|
import functools
|
|
4
4
|
import os
|
|
5
5
|
|
|
6
|
+
from typing import TYPE_CHECKING, Callable
|
|
7
|
+
|
|
6
8
|
from django.http import HttpResponseForbidden
|
|
7
9
|
from twilio.request_validator import RequestValidator
|
|
8
10
|
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from django.core.handlers.wsgi import WSGIRequest
|
|
13
|
+
from django.http import HttpResponse
|
|
14
|
+
|
|
9
15
|
|
|
10
|
-
def twilio_auth_required(func):
|
|
16
|
+
def twilio_auth_required(func: Callable[..., HttpResponse]) -> Callable[..., HttpResponse]:
|
|
11
17
|
@functools.wraps(func)
|
|
12
|
-
def decorated_function(request, *args, **kwargs):
|
|
18
|
+
def decorated_function(request: WSGIRequest, *args, **kwargs) -> HttpResponse:
|
|
13
19
|
request_validator = RequestValidator(os.environ.get('TWILIO_AUTH_TOKEN', ''))
|
|
14
20
|
|
|
15
21
|
absolute_uri = request.build_absolute_uri()
|
|
@@ -1,30 +1,254 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from django.test import TestCase
|
|
2
4
|
|
|
3
|
-
from django_spire.ai.sms.models import SmsConversation
|
|
5
|
+
from django_spire.ai.sms.models import SmsConversation, SmsMessage
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SmsConversationModelTests(TestCase):
|
|
9
|
+
def test_conversation_creation(self) -> None:
|
|
10
|
+
conversation = SmsConversation.objects.create(
|
|
11
|
+
phone_number='+15551234567'
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
assert conversation.phone_number == '+15551234567'
|
|
15
|
+
assert conversation.messages.count() == 0
|
|
16
|
+
assert conversation.is_empty
|
|
17
|
+
|
|
18
|
+
def test_conversation_str(self) -> None:
|
|
19
|
+
conversation = SmsConversation.objects.create(
|
|
20
|
+
phone_number='+15551234567'
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
assert '+15551234567' in str(conversation)
|
|
24
|
+
assert 'SMS Conversation' in str(conversation)
|
|
25
|
+
|
|
26
|
+
def test_add_message(self) -> None:
|
|
27
|
+
conversation = SmsConversation.objects.create(
|
|
28
|
+
phone_number='+15551234567'
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
message = conversation.add_message(body='Hello', is_inbound=True, twilio_sid='')
|
|
32
|
+
|
|
33
|
+
assert message.body == 'Hello'
|
|
34
|
+
assert message.is_inbound
|
|
35
|
+
|
|
36
|
+
message = conversation.add_message(body='Hi there', is_inbound=False, twilio_sid='')
|
|
37
|
+
|
|
38
|
+
assert message.body == 'Hi there'
|
|
39
|
+
assert not message.is_inbound
|
|
40
|
+
|
|
41
|
+
assert conversation.messages.count() == 2
|
|
42
|
+
assert not conversation.is_empty
|
|
43
|
+
|
|
44
|
+
def test_add_message_updates_last_message_datetime(self) -> None:
|
|
45
|
+
conversation = SmsConversation.objects.create(
|
|
46
|
+
phone_number='+15551234567'
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
initial_datetime = conversation.last_message_datetime
|
|
50
|
+
|
|
51
|
+
conversation.add_message(body='Test', is_inbound=True, twilio_sid='')
|
|
52
|
+
conversation.refresh_from_db()
|
|
53
|
+
|
|
54
|
+
assert conversation.last_message_datetime >= initial_datetime
|
|
55
|
+
|
|
56
|
+
def test_generate_message_history(self) -> None:
|
|
57
|
+
conversation = SmsConversation.objects.create(
|
|
58
|
+
phone_number='+15551234567'
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
conversation.add_message(body='User message 1', is_inbound=True, twilio_sid='')
|
|
62
|
+
conversation.add_message(body='Assistant response 1', is_inbound=False, twilio_sid='')
|
|
63
|
+
conversation.add_message(body='User message 2', is_inbound=True, twilio_sid='')
|
|
4
64
|
|
|
65
|
+
message_history = conversation.generate_message_history(
|
|
66
|
+
message_count=20,
|
|
67
|
+
exclude_last_message=True
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
assert message_history is not None
|
|
5
71
|
|
|
6
|
-
|
|
7
|
-
def test_conversation_creation(self):
|
|
72
|
+
def test_generate_message_history_includes_last_message(self) -> None:
|
|
8
73
|
conversation = SmsConversation.objects.create(
|
|
9
74
|
phone_number='+15551234567'
|
|
10
75
|
)
|
|
11
|
-
self.assertEqual(conversation.phone_number, '+15551234567')
|
|
12
|
-
self.assertEqual(conversation.messages.count(), 0)
|
|
13
|
-
self.assertTrue(conversation.is_empty)
|
|
14
76
|
|
|
15
|
-
|
|
77
|
+
conversation.add_message(body='User message 1', is_inbound=True, twilio_sid='')
|
|
78
|
+
conversation.add_message(body='User message 2', is_inbound=True, twilio_sid='')
|
|
79
|
+
|
|
80
|
+
message_history = conversation.generate_message_history(
|
|
81
|
+
message_count=20,
|
|
82
|
+
exclude_last_message=False
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
assert message_history is not None
|
|
86
|
+
|
|
87
|
+
def test_is_empty_property(self) -> None:
|
|
88
|
+
conversation = SmsConversation.objects.create(
|
|
89
|
+
phone_number='+15551234567'
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
assert conversation.is_empty
|
|
93
|
+
|
|
94
|
+
conversation.add_message(body='Test', is_inbound=True, twilio_sid='')
|
|
95
|
+
|
|
96
|
+
assert not conversation.is_empty
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class SmsMessageModelTests(TestCase):
|
|
100
|
+
def test_message_creation(self) -> None:
|
|
101
|
+
conversation = SmsConversation.objects.create(
|
|
102
|
+
phone_number='+15551234567'
|
|
103
|
+
)
|
|
104
|
+
message = SmsMessage.objects.create(
|
|
105
|
+
conversation=conversation,
|
|
106
|
+
body='Test message',
|
|
107
|
+
is_inbound=True,
|
|
108
|
+
twilio_sid='SM123'
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
assert message.body == 'Test message'
|
|
112
|
+
assert message.is_inbound
|
|
113
|
+
assert message.twilio_sid == 'SM123'
|
|
114
|
+
|
|
115
|
+
def test_message_str_short(self) -> None:
|
|
116
|
+
conversation = SmsConversation.objects.create(
|
|
117
|
+
phone_number='+15551234567'
|
|
118
|
+
)
|
|
119
|
+
message = SmsMessage.objects.create(
|
|
120
|
+
conversation=conversation,
|
|
121
|
+
body='Short message',
|
|
122
|
+
is_inbound=True
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
assert 'Inbound' in str(message)
|
|
126
|
+
assert 'Short message' in str(message)
|
|
127
|
+
|
|
128
|
+
def test_message_str_long(self) -> None:
|
|
129
|
+
conversation = SmsConversation.objects.create(
|
|
130
|
+
phone_number='+15551234567'
|
|
131
|
+
)
|
|
132
|
+
long_body = 'A' * 100
|
|
133
|
+
message = SmsMessage.objects.create(
|
|
134
|
+
conversation=conversation,
|
|
135
|
+
body=long_body,
|
|
136
|
+
is_inbound=True
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
assert '...' in str(message)
|
|
140
|
+
|
|
141
|
+
def test_direction_property_inbound(self) -> None:
|
|
142
|
+
conversation = SmsConversation.objects.create(
|
|
143
|
+
phone_number='+15551234567'
|
|
144
|
+
)
|
|
145
|
+
message = SmsMessage.objects.create(
|
|
146
|
+
conversation=conversation,
|
|
147
|
+
body='Test',
|
|
148
|
+
is_inbound=True
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
assert message.direction == 'Inbound'
|
|
152
|
+
|
|
153
|
+
def test_direction_property_outbound(self) -> None:
|
|
154
|
+
conversation = SmsConversation.objects.create(
|
|
155
|
+
phone_number='+15551234567'
|
|
156
|
+
)
|
|
157
|
+
message = SmsMessage.objects.create(
|
|
158
|
+
conversation=conversation,
|
|
159
|
+
body='Test',
|
|
160
|
+
is_inbound=False
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
assert message.direction == 'Outbound'
|
|
164
|
+
|
|
165
|
+
def test_is_outbound_property(self) -> None:
|
|
166
|
+
conversation = SmsConversation.objects.create(
|
|
167
|
+
phone_number='+15551234567'
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
inbound_message = SmsMessage.objects.create(
|
|
171
|
+
conversation=conversation,
|
|
172
|
+
body='Test',
|
|
173
|
+
is_inbound=True
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
outbound_message = SmsMessage.objects.create(
|
|
177
|
+
conversation=conversation,
|
|
178
|
+
body='Test',
|
|
179
|
+
is_inbound=False
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
assert not inbound_message.is_outbound
|
|
183
|
+
assert outbound_message.is_outbound
|
|
184
|
+
|
|
185
|
+
def test_role_property_inbound(self) -> None:
|
|
186
|
+
conversation = SmsConversation.objects.create(
|
|
187
|
+
phone_number='+15551234567'
|
|
188
|
+
)
|
|
189
|
+
message = SmsMessage.objects.create(
|
|
190
|
+
conversation=conversation,
|
|
191
|
+
body='Test',
|
|
192
|
+
is_inbound=True
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
assert message.role == 'user'
|
|
196
|
+
|
|
197
|
+
def test_role_property_outbound(self) -> None:
|
|
198
|
+
conversation = SmsConversation.objects.create(
|
|
199
|
+
phone_number='+15551234567'
|
|
200
|
+
)
|
|
201
|
+
message = SmsMessage.objects.create(
|
|
202
|
+
conversation=conversation,
|
|
203
|
+
body='Test',
|
|
204
|
+
is_inbound=False
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
assert message.role == 'assistant'
|
|
208
|
+
|
|
209
|
+
def test_message_is_processed_default(self) -> None:
|
|
210
|
+
conversation = SmsConversation.objects.create(
|
|
211
|
+
phone_number='+15551234567'
|
|
212
|
+
)
|
|
213
|
+
message = SmsMessage.objects.create(
|
|
214
|
+
conversation=conversation,
|
|
215
|
+
body='Test',
|
|
216
|
+
is_inbound=True
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
assert message.is_processed is False
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class SmsQuerySetTests(TestCase):
|
|
223
|
+
def test_by_phone_number(self) -> None:
|
|
224
|
+
SmsConversation.objects.create(phone_number='+15551234567')
|
|
225
|
+
SmsConversation.objects.create(phone_number='+15559876543')
|
|
226
|
+
|
|
227
|
+
result = SmsConversation.objects.by_phone_number('+15551234567')
|
|
228
|
+
|
|
229
|
+
assert result.count() == 1
|
|
230
|
+
assert result.first().phone_number == '+15551234567'
|
|
231
|
+
|
|
232
|
+
def test_newest_by_count(self) -> None:
|
|
233
|
+
conversation = SmsConversation.objects.create(
|
|
234
|
+
phone_number='+15551234567'
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
for i in range(25):
|
|
238
|
+
conversation.add_message(body=f'Message {i}', is_inbound=True, twilio_sid='')
|
|
239
|
+
|
|
240
|
+
messages = conversation.messages.newest_by_count(20)
|
|
241
|
+
|
|
242
|
+
assert len(messages) == 20
|
|
243
|
+
|
|
244
|
+
def test_newest_by_count_reversed(self) -> None:
|
|
16
245
|
conversation = SmsConversation.objects.create(
|
|
17
246
|
phone_number='+15551234567'
|
|
18
247
|
)
|
|
19
|
-
|
|
20
|
-
message = conversation.add_message(body="Hello", is_inbound=True, twilio_sid='')
|
|
21
|
-
self.assertEqual(message.body, "Hello")
|
|
22
|
-
self.assertTrue(message.is_inbound)
|
|
23
248
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
self.assertFalse(message.is_inbound)
|
|
249
|
+
for i in range(5):
|
|
250
|
+
conversation.add_message(body=f'Message {i}', is_inbound=True, twilio_sid='')
|
|
27
251
|
|
|
28
|
-
|
|
29
|
-
self.assertFalse(conversation.is_empty)
|
|
252
|
+
messages = conversation.messages.newest_by_count_reversed(5)
|
|
30
253
|
|
|
254
|
+
assert len(messages) == 5
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from django_spire.ai.sms.intel import SmsIntel
|
|
4
|
+
from django_spire.core.tests.test_cases import BaseTestCase
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SmsIntelTests(BaseTestCase):
|
|
8
|
+
def test_sms_intel_creation(self) -> None:
|
|
9
|
+
intel = SmsIntel(body='Test message')
|
|
10
|
+
|
|
11
|
+
assert intel.body == 'Test message'
|
|
12
|
+
|
|
13
|
+
def test_sms_intel_empty_body(self) -> None:
|
|
14
|
+
intel = SmsIntel(body='')
|
|
15
|
+
|
|
16
|
+
assert intel.body == ''
|
|
17
|
+
|
|
18
|
+
def test_sms_intel_long_body(self) -> None:
|
|
19
|
+
long_body = 'A' * 1000
|
|
20
|
+
intel = SmsIntel(body=long_body)
|
|
21
|
+
|
|
22
|
+
assert intel.body == long_body
|
|
23
|
+
assert len(intel.body) == 1000
|
|
24
|
+
|
|
25
|
+
def test_sms_intel_unicode_body(self) -> None:
|
|
26
|
+
unicode_body = 'Hello 世界 🌍'
|
|
27
|
+
intel = SmsIntel(body=unicode_body)
|
|
28
|
+
|
|
29
|
+
assert intel.body == unicode_body
|
|
30
|
+
|
|
31
|
+
def test_sms_intel_model_dump(self) -> None:
|
|
32
|
+
intel = SmsIntel(body='Test')
|
|
33
|
+
dump = intel.model_dump()
|
|
34
|
+
|
|
35
|
+
assert 'body' in dump
|
|
36
|
+
assert dump['body'] == 'Test'
|
|
37
|
+
|
|
38
|
+
def test_sms_intel_special_characters(self) -> None:
|
|
39
|
+
special_body = 'Line 1\nLine 2\tTabbed'
|
|
40
|
+
intel = SmsIntel(body=special_body)
|
|
41
|
+
|
|
42
|
+
assert intel.body == special_body
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from unittest.mock import patch
|
|
2
4
|
|
|
3
5
|
from django.urls import reverse
|
|
@@ -7,13 +9,13 @@ from django_spire.core.tests.test_cases import BaseTestCase
|
|
|
7
9
|
|
|
8
10
|
|
|
9
11
|
class SmsWebhookTests(BaseTestCase):
|
|
10
|
-
def setUp(self):
|
|
12
|
+
def setUp(self) -> None:
|
|
11
13
|
super().setUp()
|
|
12
14
|
|
|
13
15
|
self.webhook_url = reverse('django_spire:ai:sms:webhook')
|
|
14
16
|
|
|
15
17
|
@patch('twilio.request_validator.RequestValidator.validate')
|
|
16
|
-
def test_webhook_receives_message(self, mock_validate):
|
|
18
|
+
def test_webhook_receives_message(self, mock_validate) -> None:
|
|
17
19
|
mock_validate.return_value = True
|
|
18
20
|
|
|
19
21
|
response = self.client.post(
|
|
@@ -25,14 +27,160 @@ class SmsWebhookTests(BaseTestCase):
|
|
|
25
27
|
}
|
|
26
28
|
)
|
|
27
29
|
|
|
28
|
-
|
|
30
|
+
assert response.status_code == 200
|
|
29
31
|
|
|
30
32
|
conversation = SmsConversation.objects.get(phone_number='+15551234567')
|
|
31
|
-
|
|
33
|
+
assert conversation.messages.count() == 2
|
|
32
34
|
|
|
33
35
|
inbound_message = conversation.messages.filter(is_inbound=True).first()
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
assert inbound_message.body == 'Hello'
|
|
37
|
+
assert inbound_message.twilio_sid == 'SM123456789'
|
|
36
38
|
|
|
37
39
|
outbound_message = conversation.messages.filter(is_inbound=False).first()
|
|
38
|
-
|
|
40
|
+
assert len(outbound_message.body) > 0
|
|
41
|
+
|
|
42
|
+
@patch('twilio.request_validator.RequestValidator.validate')
|
|
43
|
+
def test_webhook_creates_conversation(self, mock_validate) -> None:
|
|
44
|
+
mock_validate.return_value = True
|
|
45
|
+
|
|
46
|
+
initial_count = SmsConversation.objects.count()
|
|
47
|
+
|
|
48
|
+
self.client.post(
|
|
49
|
+
self.webhook_url,
|
|
50
|
+
{
|
|
51
|
+
'From': '+15559999999',
|
|
52
|
+
'Body': 'New conversation',
|
|
53
|
+
'MessageSid': 'SM999999999'
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
assert SmsConversation.objects.count() == initial_count + 1
|
|
58
|
+
|
|
59
|
+
@patch('twilio.request_validator.RequestValidator.validate')
|
|
60
|
+
def test_webhook_reuses_existing_conversation(self, mock_validate) -> None:
|
|
61
|
+
mock_validate.return_value = True
|
|
62
|
+
|
|
63
|
+
SmsConversation.objects.create(phone_number='+15551234567')
|
|
64
|
+
|
|
65
|
+
initial_count = SmsConversation.objects.count()
|
|
66
|
+
|
|
67
|
+
self.client.post(
|
|
68
|
+
self.webhook_url,
|
|
69
|
+
{
|
|
70
|
+
'From': '+15551234567',
|
|
71
|
+
'Body': 'Another message',
|
|
72
|
+
'MessageSid': 'SM123456789'
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
assert SmsConversation.objects.count() == initial_count
|
|
77
|
+
|
|
78
|
+
@patch('twilio.request_validator.RequestValidator.validate')
|
|
79
|
+
def test_webhook_invalid_signature(self, mock_validate) -> None:
|
|
80
|
+
mock_validate.return_value = False
|
|
81
|
+
|
|
82
|
+
response = self.client.post(
|
|
83
|
+
self.webhook_url,
|
|
84
|
+
{
|
|
85
|
+
'From': '+15551234567',
|
|
86
|
+
'Body': 'Hello',
|
|
87
|
+
'MessageSid': 'SM123456789'
|
|
88
|
+
}
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
assert response.status_code == 403
|
|
92
|
+
|
|
93
|
+
@patch('twilio.request_validator.RequestValidator.validate')
|
|
94
|
+
def test_webhook_short_phone_number(self, mock_validate) -> None:
|
|
95
|
+
mock_validate.return_value = True
|
|
96
|
+
|
|
97
|
+
response = self.client.post(
|
|
98
|
+
self.webhook_url,
|
|
99
|
+
{
|
|
100
|
+
'From': '1234',
|
|
101
|
+
'Body': 'Hello',
|
|
102
|
+
'MessageSid': 'SM123456789'
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
assert response.status_code == 403
|
|
107
|
+
|
|
108
|
+
@patch('twilio.request_validator.RequestValidator.validate')
|
|
109
|
+
def test_webhook_empty_body(self, mock_validate) -> None:
|
|
110
|
+
mock_validate.return_value = True
|
|
111
|
+
|
|
112
|
+
response = self.client.post(
|
|
113
|
+
self.webhook_url,
|
|
114
|
+
{
|
|
115
|
+
'From': '+15551234567',
|
|
116
|
+
'Body': '',
|
|
117
|
+
'MessageSid': 'SM123456789'
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
assert response.status_code == 200
|
|
122
|
+
|
|
123
|
+
@patch('twilio.request_validator.RequestValidator.validate')
|
|
124
|
+
def test_webhook_marks_inbound_as_processed(self, mock_validate) -> None:
|
|
125
|
+
mock_validate.return_value = True
|
|
126
|
+
|
|
127
|
+
self.client.post(
|
|
128
|
+
self.webhook_url,
|
|
129
|
+
{
|
|
130
|
+
'From': '+15551234567',
|
|
131
|
+
'Body': 'Test message',
|
|
132
|
+
'MessageSid': 'SM123456789'
|
|
133
|
+
}
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
conversation = SmsConversation.objects.get(phone_number='+15551234567')
|
|
137
|
+
inbound_message = conversation.messages.filter(is_inbound=True).first()
|
|
138
|
+
|
|
139
|
+
assert inbound_message.is_processed
|
|
140
|
+
|
|
141
|
+
@patch('twilio.request_validator.RequestValidator.validate')
|
|
142
|
+
def test_webhook_response_is_twiml(self, mock_validate) -> None:
|
|
143
|
+
mock_validate.return_value = True
|
|
144
|
+
|
|
145
|
+
response = self.client.post(
|
|
146
|
+
self.webhook_url,
|
|
147
|
+
{
|
|
148
|
+
'From': '+15551234567',
|
|
149
|
+
'Body': 'Hello',
|
|
150
|
+
'MessageSid': 'SM123456789'
|
|
151
|
+
}
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
assert b'<Response>' in response.content or b'Response' in response.content
|
|
155
|
+
|
|
156
|
+
@patch('twilio.request_validator.RequestValidator.validate')
|
|
157
|
+
def test_webhook_missing_from(self, mock_validate) -> None:
|
|
158
|
+
mock_validate.return_value = True
|
|
159
|
+
|
|
160
|
+
response = self.client.post(
|
|
161
|
+
self.webhook_url,
|
|
162
|
+
{
|
|
163
|
+
'Body': 'Hello',
|
|
164
|
+
'MessageSid': 'SM123456789'
|
|
165
|
+
}
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
assert response.status_code == 403
|
|
169
|
+
|
|
170
|
+
@patch('twilio.request_validator.RequestValidator.validate')
|
|
171
|
+
def test_webhook_stores_twilio_sid(self, mock_validate) -> None:
|
|
172
|
+
mock_validate.return_value = True
|
|
173
|
+
|
|
174
|
+
self.client.post(
|
|
175
|
+
self.webhook_url,
|
|
176
|
+
{
|
|
177
|
+
'From': '+15551234567',
|
|
178
|
+
'Body': 'Hello',
|
|
179
|
+
'MessageSid': 'SM_TEST_SID_123'
|
|
180
|
+
}
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
conversation = SmsConversation.objects.get(phone_number='+15551234567')
|
|
184
|
+
inbound_message = conversation.messages.filter(is_inbound=True).first()
|
|
185
|
+
|
|
186
|
+
assert inbound_message.twilio_sid == 'SM_TEST_SID_123'
|