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
django_spire/ai/sms/views.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
3
5
|
from django.http import HttpResponse, HttpResponseForbidden
|
|
4
6
|
from django.views.decorators.csrf import csrf_exempt
|
|
5
7
|
from django.views.decorators.http import require_POST
|
|
@@ -9,11 +11,14 @@ from django_spire.ai.sms.decorators import twilio_auth_required
|
|
|
9
11
|
from django_spire.ai.sms.intelligence.workflows.sms_conversation_workflow import sms_conversation_workflow
|
|
10
12
|
from django_spire.ai.sms.models import SmsConversation
|
|
11
13
|
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from django.core.handlers.wsgi import WSGIRequest
|
|
16
|
+
|
|
12
17
|
|
|
13
18
|
@csrf_exempt
|
|
14
19
|
@require_POST
|
|
15
20
|
@twilio_auth_required
|
|
16
|
-
def webhook_view(request):
|
|
21
|
+
def webhook_view(request: WSGIRequest):
|
|
17
22
|
from_number = request.POST.get('From', '')
|
|
18
23
|
|
|
19
24
|
if len(from_number) < 11:
|
|
@@ -32,30 +37,24 @@ def webhook_view(request):
|
|
|
32
37
|
twilio_sid=message_sid,
|
|
33
38
|
)
|
|
34
39
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
twiml_response = MessagingResponse()
|
|
44
|
-
twiml_response.message(sms_intel.body)
|
|
45
|
-
|
|
46
|
-
conversation.add_message(
|
|
47
|
-
body=sms_intel.body,
|
|
48
|
-
is_inbound=False,
|
|
49
|
-
twilio_sid=message_sid,
|
|
50
|
-
is_processed=True
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
message.is_processed = True
|
|
54
|
-
message.save()
|
|
40
|
+
sms_intel = sms_conversation_workflow(
|
|
41
|
+
request=request,
|
|
42
|
+
user_input=body,
|
|
43
|
+
message_history=conversation.generate_message_history(),
|
|
44
|
+
actor=from_number,
|
|
45
|
+
)
|
|
55
46
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
raise
|
|
47
|
+
twiml_response = MessagingResponse()
|
|
48
|
+
twiml_response.message(sms_intel.body)
|
|
59
49
|
|
|
50
|
+
conversation.add_message(
|
|
51
|
+
body=sms_intel.body,
|
|
52
|
+
is_inbound=False,
|
|
53
|
+
twilio_sid=message_sid,
|
|
54
|
+
is_processed=True
|
|
55
|
+
)
|
|
60
56
|
|
|
57
|
+
message.is_processed = True
|
|
58
|
+
message.save()
|
|
61
59
|
|
|
60
|
+
return HttpResponse(twiml_response)
|
django_spire/ai/tests/test_ai.py
CHANGED
|
@@ -3,16 +3,20 @@ from __future__ import annotations
|
|
|
3
3
|
from dandy import BaseIntel, Bot, recorder_to_html_file
|
|
4
4
|
|
|
5
5
|
from django_spire.ai.decorators import log_ai_interaction_from_recorder
|
|
6
|
+
from django_spire.ai.models import AiInteraction, AiUsage
|
|
6
7
|
from django_spire.core.tests.test_cases import BaseTestCase
|
|
7
8
|
|
|
8
9
|
|
|
9
|
-
class
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
class HorseIntel(BaseIntel):
|
|
11
|
+
breed: str
|
|
12
|
+
color: str
|
|
13
|
+
first_name: str
|
|
14
|
+
has_cone_taped_to_head: bool
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AiDecoratorTestCase(BaseTestCase):
|
|
18
|
+
def test_ai_interaction_decorator_creates_usage_record(self) -> None:
|
|
19
|
+
initial_count = AiUsage.objects.count()
|
|
16
20
|
|
|
17
21
|
@log_ai_interaction_from_recorder(self.super_user, 'horse')
|
|
18
22
|
@recorder_to_html_file('horse')
|
|
@@ -26,3 +30,123 @@ class AiTestCase(BaseTestCase):
|
|
|
26
30
|
horse_intel = generate_horse_intel('Make me a magical horse that grants wishes!')
|
|
27
31
|
|
|
28
32
|
assert horse_intel.first_name != ''
|
|
33
|
+
assert AiUsage.objects.count() >= initial_count
|
|
34
|
+
|
|
35
|
+
def test_ai_interaction_decorator_creates_interaction_record(self) -> None:
|
|
36
|
+
initial_count = AiInteraction.objects.count()
|
|
37
|
+
|
|
38
|
+
@log_ai_interaction_from_recorder(self.super_user, 'test_actor')
|
|
39
|
+
@recorder_to_html_file('test_interaction')
|
|
40
|
+
def generate_horse_intel(user_input: str) -> HorseIntel:
|
|
41
|
+
bot = Bot()
|
|
42
|
+
return bot.llm.prompt_to_intel(
|
|
43
|
+
prompt=user_input,
|
|
44
|
+
intel_class=HorseIntel,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
generate_horse_intel('Create a horse')
|
|
48
|
+
|
|
49
|
+
assert AiInteraction.objects.count() > initial_count
|
|
50
|
+
|
|
51
|
+
def test_ai_interaction_decorator_requires_user_or_actor(self) -> None:
|
|
52
|
+
try:
|
|
53
|
+
@log_ai_interaction_from_recorder()
|
|
54
|
+
def dummy_func() -> None:
|
|
55
|
+
pass
|
|
56
|
+
except ValueError as e:
|
|
57
|
+
assert 'user or actor must be provided' in str(e)
|
|
58
|
+
else:
|
|
59
|
+
assert False, 'Expected ValueError'
|
|
60
|
+
|
|
61
|
+
def test_ai_interaction_decorator_with_actor_only(self) -> None:
|
|
62
|
+
@log_ai_interaction_from_recorder(actor='test_actor_only')
|
|
63
|
+
@recorder_to_html_file('test_actor_only')
|
|
64
|
+
def generate_horse_intel(user_input: str) -> HorseIntel:
|
|
65
|
+
bot = Bot()
|
|
66
|
+
return bot.llm.prompt_to_intel(
|
|
67
|
+
prompt=user_input,
|
|
68
|
+
intel_class=HorseIntel,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
horse_intel = generate_horse_intel('Create a horse')
|
|
72
|
+
|
|
73
|
+
assert horse_intel is not None
|
|
74
|
+
|
|
75
|
+
interaction = AiInteraction.objects.filter(actor='test_actor_only').first()
|
|
76
|
+
assert interaction is not None
|
|
77
|
+
assert interaction.actor == 'test_actor_only'
|
|
78
|
+
|
|
79
|
+
def test_ai_interaction_decorator_records_module_and_callable(self) -> None:
|
|
80
|
+
@log_ai_interaction_from_recorder(self.super_user, 'module_test')
|
|
81
|
+
@recorder_to_html_file('module_test')
|
|
82
|
+
def test_callable(user_input: str) -> HorseIntel:
|
|
83
|
+
bot = Bot()
|
|
84
|
+
return bot.llm.prompt_to_intel(
|
|
85
|
+
prompt=user_input,
|
|
86
|
+
intel_class=HorseIntel,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
test_callable('Test input')
|
|
90
|
+
|
|
91
|
+
interaction = AiInteraction.objects.filter(actor='module_test').first()
|
|
92
|
+
assert interaction is not None
|
|
93
|
+
assert 'test_callable' in interaction.callable_name
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class AiUsageModelTestCase(BaseTestCase):
|
|
97
|
+
def test_ai_usage_str(self) -> None:
|
|
98
|
+
ai_usage = AiUsage.objects.create()
|
|
99
|
+
|
|
100
|
+
assert 'ai usage' in str(ai_usage)
|
|
101
|
+
|
|
102
|
+
def test_ai_usage_default_values(self) -> None:
|
|
103
|
+
ai_usage = AiUsage.objects.create()
|
|
104
|
+
|
|
105
|
+
assert ai_usage.event_count == 0
|
|
106
|
+
assert ai_usage.token_usage == 0
|
|
107
|
+
assert ai_usage.run_time_seconds == 0.0
|
|
108
|
+
assert ai_usage.was_successful is True
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class AiInteractionModelTestCase(BaseTestCase):
|
|
112
|
+
def test_ai_interaction_str(self) -> None:
|
|
113
|
+
ai_usage = AiUsage.objects.create()
|
|
114
|
+
ai_interaction = AiInteraction.objects.create(
|
|
115
|
+
ai_usage=ai_usage,
|
|
116
|
+
actor='test_actor',
|
|
117
|
+
module_name='test_module',
|
|
118
|
+
callable_name='test_callable',
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
assert 'test_actor' in str(ai_interaction)
|
|
122
|
+
assert 'interaction' in str(ai_interaction)
|
|
123
|
+
|
|
124
|
+
def test_ai_interaction_saves_user_info(self) -> None:
|
|
125
|
+
ai_usage = AiUsage.objects.create()
|
|
126
|
+
ai_interaction = AiInteraction.objects.create(
|
|
127
|
+
ai_usage=ai_usage,
|
|
128
|
+
user=self.super_user,
|
|
129
|
+
actor=None,
|
|
130
|
+
module_name='test_module',
|
|
131
|
+
callable_name='test_callable',
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
assert ai_interaction.user_email == self.super_user.email
|
|
135
|
+
assert ai_interaction.user_first_name == self.super_user.first_name
|
|
136
|
+
assert ai_interaction.user_last_name == self.super_user.last_name
|
|
137
|
+
|
|
138
|
+
def test_ai_interaction_default_values(self) -> None:
|
|
139
|
+
ai_usage = AiUsage.objects.create()
|
|
140
|
+
ai_interaction = AiInteraction.objects.create(
|
|
141
|
+
ai_usage=ai_usage,
|
|
142
|
+
actor='test',
|
|
143
|
+
module_name='test',
|
|
144
|
+
callable_name='test',
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
assert ai_interaction.event_count == 0
|
|
148
|
+
assert ai_interaction.token_usage == 0
|
|
149
|
+
assert ai_interaction.run_time_seconds == 0.0
|
|
150
|
+
assert ai_interaction.was_successful is True
|
|
151
|
+
assert ai_interaction.exception is None
|
|
152
|
+
assert ai_interaction.stack_trace is None
|
django_spire/auth/apps.py
CHANGED
|
@@ -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
|
|
@@ -9,9 +11,9 @@ class AuthConfig(AppConfig):
|
|
|
9
11
|
name = 'django_spire.auth'
|
|
10
12
|
|
|
11
13
|
REQUIRED_APPS = ('django_spire_core',)
|
|
12
|
-
|
|
14
|
+
|
|
13
15
|
URLPATTERNS_INCLUDE = 'django_spire.auth.urls'
|
|
14
16
|
URLPATTERNS_NAMESPACE = 'auth'
|
|
15
17
|
|
|
16
18
|
def ready(self) -> None:
|
|
17
|
-
check_required_apps(self.label)
|
|
19
|
+
check_required_apps(self.label)
|
|
@@ -1,23 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import functools
|
|
2
4
|
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
3
7
|
from django.core.exceptions import PermissionDenied
|
|
4
|
-
from django.core.handlers.wsgi import WSGIRequest
|
|
5
8
|
|
|
9
|
+
from django_spire.auth.controller.exceptions import (
|
|
10
|
+
AuthControllerNotFoundError,
|
|
11
|
+
AuthControllerRequestError
|
|
12
|
+
)
|
|
6
13
|
from django_spire.auth.permissions.decorators import permission_required_decorator_function
|
|
14
|
+
from django_spire.conf import settings
|
|
7
15
|
from django_spire.core.utils import get_object_from_module_string
|
|
8
16
|
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from typing import Any, Callable
|
|
19
|
+
|
|
20
|
+
from django.core.handlers.wsgi import WSGIRequest
|
|
21
|
+
|
|
9
22
|
|
|
10
23
|
class BaseAuthController:
|
|
11
|
-
def __init__(
|
|
12
|
-
self,
|
|
13
|
-
request: WSGIRequest | None = None
|
|
14
|
-
):
|
|
24
|
+
def __init__(self, request: WSGIRequest | None = None):
|
|
15
25
|
self._request = request
|
|
16
26
|
|
|
17
27
|
@property
|
|
18
28
|
def request(self):
|
|
19
29
|
if self._request is None:
|
|
20
|
-
|
|
30
|
+
message = 'AuthController.request is None'
|
|
31
|
+
raise AuthControllerRequestError(message)
|
|
21
32
|
|
|
22
33
|
return self._request
|
|
23
34
|
|
|
@@ -26,12 +37,11 @@ class BaseAuthController:
|
|
|
26
37
|
self._request = value
|
|
27
38
|
|
|
28
39
|
def permission_required(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
40
|
+
self,
|
|
41
|
+
*permissions: str,
|
|
42
|
+
all_required: bool = True
|
|
32
43
|
):
|
|
33
|
-
|
|
34
|
-
def decorator(method):
|
|
44
|
+
def decorator(method: Callable[..., Any]):
|
|
35
45
|
@functools.wraps(method)
|
|
36
46
|
def wrapper(request: WSGIRequest, *args, **kwargs):
|
|
37
47
|
self.request = request
|
|
@@ -40,12 +50,16 @@ class BaseAuthController:
|
|
|
40
50
|
|
|
41
51
|
for perm in permissions:
|
|
42
52
|
callable_permission = (
|
|
43
|
-
getattr(self, perm)
|
|
53
|
+
getattr(self, perm)
|
|
54
|
+
if hasattr(self, perm)
|
|
55
|
+
else perm
|
|
44
56
|
)
|
|
57
|
+
|
|
45
58
|
if callable(callable_permission):
|
|
46
59
|
if not all_required and callable_permission():
|
|
47
60
|
return method(request, *args, **kwargs)
|
|
48
|
-
|
|
61
|
+
|
|
62
|
+
if not callable_permission():
|
|
49
63
|
raise PermissionDenied
|
|
50
64
|
|
|
51
65
|
else:
|
|
@@ -67,18 +81,17 @@ class BaseAuthController:
|
|
|
67
81
|
|
|
68
82
|
class AppAuthController:
|
|
69
83
|
def __new__(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
84
|
+
cls,
|
|
85
|
+
app_name: str,
|
|
86
|
+
request: WSGIRequest | None = None,
|
|
87
|
+
**kwargs: dict[str, Any]
|
|
74
88
|
):
|
|
75
|
-
from django_spire.conf import settings
|
|
76
|
-
|
|
77
89
|
if app_name not in settings.DJANGO_SPIRE_AUTH_CONTROLLERS:
|
|
78
|
-
|
|
90
|
+
message = f'Controller {app_name} not found in settings.AUTH_CONTROLLERS'
|
|
91
|
+
raise AuthControllerNotFoundError(message)
|
|
79
92
|
|
|
80
93
|
try:
|
|
81
94
|
return get_object_from_module_string(settings.DJANGO_SPIRE_AUTH_CONTROLLERS[app_name])(request)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
raise
|
|
95
|
+
except ModuleNotFoundError as err:
|
|
96
|
+
message = f'Auth Controller for {app_name} not found'
|
|
97
|
+
raise AuthControllerNotFoundError(message) from err
|
django_spire/auth/group/admin.py
CHANGED
django_spire/auth/group/apps.py
CHANGED
|
@@ -3,13 +3,22 @@ from __future__ import annotations
|
|
|
3
3
|
from django_spire.auth.group.models import AuthGroup
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
def bulk_create_groups_from_names(names: list[str]):
|
|
7
|
-
existing_groups = AuthGroup.objects.all().values_list('name', flat=True)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
def bulk_create_groups_from_names(names: list[str]) -> list[AuthGroup]:
|
|
7
|
+
existing_groups = set(AuthGroup.objects.all().values_list('name', flat=True))
|
|
8
|
+
seen_names = set()
|
|
9
|
+
|
|
10
|
+
new_groups = []
|
|
11
|
+
|
|
12
|
+
for name in names:
|
|
13
|
+
if name in existing_groups or name in seen_names:
|
|
14
|
+
continue
|
|
15
|
+
|
|
16
|
+
sanitized_name = name.replace('\x00', '')
|
|
17
|
+
|
|
18
|
+
if sanitized_name in existing_groups or sanitized_name in seen_names:
|
|
19
|
+
continue
|
|
20
|
+
|
|
21
|
+
seen_names.add(sanitized_name)
|
|
22
|
+
new_groups.append(AuthGroup(name=sanitized_name))
|
|
14
23
|
|
|
15
24
|
return AuthGroup.objects.bulk_create(new_groups)
|
django_spire/auth/group/forms.py
CHANGED
|
@@ -12,6 +12,12 @@ class GroupNamesField(forms.CharField):
|
|
|
12
12
|
"""Receives a list of group names as a json string"""
|
|
13
13
|
|
|
14
14
|
def clean(self, value) -> list[str]:
|
|
15
|
+
value = super().clean(value)
|
|
16
|
+
|
|
17
|
+
if value in (None, ''):
|
|
18
|
+
message = 'This field is required.'
|
|
19
|
+
raise forms.ValidationError(message)
|
|
20
|
+
|
|
15
21
|
return json.loads(value)
|
|
16
22
|
|
|
17
23
|
|
|
@@ -31,6 +37,7 @@ class GroupForm(forms.ModelForm):
|
|
|
31
37
|
raise forms.ValidationError(message)
|
|
32
38
|
|
|
33
39
|
return name
|
|
40
|
+
|
|
34
41
|
class Meta:
|
|
35
42
|
model = Group
|
|
36
43
|
exclude = ['permissions']
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from django_spire.auth.group.factories import bulk_create_groups_from_names
|
|
4
|
+
from django_spire.auth.group.models import AuthGroup
|
|
5
|
+
from django_spire.core.tests.test_cases import BaseTestCase
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BulkCreateGroupsFromNamesTestCase(BaseTestCase):
|
|
9
|
+
def test_creates_new_groups(self) -> None:
|
|
10
|
+
names = ['Group A', 'Group B', 'Group C']
|
|
11
|
+
groups = bulk_create_groups_from_names(names)
|
|
12
|
+
assert len(groups) == 3
|
|
13
|
+
assert AuthGroup.objects.filter(name__in=names).count() == 3
|
|
14
|
+
|
|
15
|
+
def test_skips_existing_groups(self) -> None:
|
|
16
|
+
AuthGroup.objects.create(name='Existing Group')
|
|
17
|
+
names = ['Existing Group', 'New Group']
|
|
18
|
+
groups = bulk_create_groups_from_names(names)
|
|
19
|
+
assert len(groups) == 1
|
|
20
|
+
assert groups[0].name == 'New Group'
|
|
21
|
+
|
|
22
|
+
def test_empty_list(self) -> None:
|
|
23
|
+
groups = bulk_create_groups_from_names([])
|
|
24
|
+
assert len(groups) == 0
|
|
25
|
+
|
|
26
|
+
def test_all_existing_groups(self) -> None:
|
|
27
|
+
AuthGroup.objects.create(name='Group A')
|
|
28
|
+
AuthGroup.objects.create(name='Group B')
|
|
29
|
+
groups = bulk_create_groups_from_names(['Group A', 'Group B'])
|
|
30
|
+
assert len(groups) == 0
|
|
31
|
+
|
|
32
|
+
def test_single_group(self) -> None:
|
|
33
|
+
groups = bulk_create_groups_from_names(['Single Group'])
|
|
34
|
+
assert len(groups) == 1
|
|
35
|
+
assert groups[0].name == 'Single Group'
|
|
36
|
+
|
|
37
|
+
def test_unique_names_only(self) -> None:
|
|
38
|
+
groups = bulk_create_groups_from_names(['Group A', 'Group B'])
|
|
39
|
+
assert len(groups) == 2
|
|
40
|
+
|
|
41
|
+
def test_groups_are_persisted(self) -> None:
|
|
42
|
+
bulk_create_groups_from_names(['Persisted Group'])
|
|
43
|
+
assert AuthGroup.objects.filter(name='Persisted Group').exists()
|
|
44
|
+
|
|
45
|
+
def test_mixed_existing_and_new(self) -> None:
|
|
46
|
+
AuthGroup.objects.create(name='Existing 1')
|
|
47
|
+
AuthGroup.objects.create(name='Existing 2')
|
|
48
|
+
names = ['Existing 1', 'New 1', 'Existing 2', 'New 2']
|
|
49
|
+
groups = bulk_create_groups_from_names(names)
|
|
50
|
+
assert len(groups) == 2
|
|
51
|
+
group_names = [g.name for g in groups]
|
|
52
|
+
assert 'New 1' in group_names
|
|
53
|
+
assert 'New 2' in group_names
|
|
54
|
+
|
|
55
|
+
def test_special_characters_in_names(self) -> None:
|
|
56
|
+
names = ['Group & Co', 'Test <Group>', 'Group "Name"']
|
|
57
|
+
groups = bulk_create_groups_from_names(names)
|
|
58
|
+
assert len(groups) == 3
|
|
59
|
+
|
|
60
|
+
def test_unicode_names(self) -> None:
|
|
61
|
+
names = ['Tëst Grøup', '日本語グループ', 'Группа']
|
|
62
|
+
groups = bulk_create_groups_from_names(names)
|
|
63
|
+
assert len(groups) == 3
|
|
64
|
+
|
|
65
|
+
def test_whitespace_names(self) -> None:
|
|
66
|
+
names = [' Spaces ', 'Tab\tGroup']
|
|
67
|
+
groups = bulk_create_groups_from_names(names)
|
|
68
|
+
assert len(groups) == 2
|
|
69
|
+
|
|
70
|
+
def test_many_groups(self) -> None:
|
|
71
|
+
names = [f'Group {i}' for i in range(50)]
|
|
72
|
+
groups = bulk_create_groups_from_names(names)
|
|
73
|
+
assert len(groups) == 50
|
|
74
|
+
|
|
75
|
+
def test_returns_group_instances(self) -> None:
|
|
76
|
+
groups = bulk_create_groups_from_names(['Test Group'])
|
|
77
|
+
assert isinstance(groups[0], AuthGroup)
|
|
78
|
+
|
|
79
|
+
def test_groups_have_primary_keys(self) -> None:
|
|
80
|
+
groups = bulk_create_groups_from_names(['PK Test Group'])
|
|
81
|
+
for group in groups:
|
|
82
|
+
assert group.pk is not None
|
|
83
|
+
|
|
84
|
+
def test_duplicate_names_in_input_list(self) -> None:
|
|
85
|
+
names = ['Duplicate', 'Duplicate', 'Duplicate']
|
|
86
|
+
groups = bulk_create_groups_from_names(names)
|
|
87
|
+
assert AuthGroup.objects.filter(name='Duplicate').count() >= 1
|
|
88
|
+
|
|
89
|
+
def test_empty_string_name(self) -> None:
|
|
90
|
+
names = ['', 'Valid Group']
|
|
91
|
+
groups = bulk_create_groups_from_names(names)
|
|
92
|
+
assert len(groups) >= 1
|
|
93
|
+
|
|
94
|
+
def test_very_long_name(self) -> None:
|
|
95
|
+
long_name = 'A' * 150
|
|
96
|
+
groups = bulk_create_groups_from_names([long_name])
|
|
97
|
+
assert len(groups) == 1
|
|
98
|
+
assert groups[0].name == long_name
|
|
99
|
+
|
|
100
|
+
def test_name_with_only_whitespace(self) -> None:
|
|
101
|
+
names = [' ', '\t\t', '\n\n']
|
|
102
|
+
groups = bulk_create_groups_from_names(names)
|
|
103
|
+
assert len(groups) == 3
|
|
104
|
+
|
|
105
|
+
def test_name_with_leading_trailing_whitespace(self) -> None:
|
|
106
|
+
names = [' Leading', 'Trailing ', ' Both ']
|
|
107
|
+
groups = bulk_create_groups_from_names(names)
|
|
108
|
+
assert len(groups) == 3
|
|
109
|
+
|
|
110
|
+
def test_case_sensitive_names(self) -> None:
|
|
111
|
+
names = ['group', 'Group', 'GROUP']
|
|
112
|
+
groups = bulk_create_groups_from_names(names)
|
|
113
|
+
assert len(groups) == 3
|
|
114
|
+
|
|
115
|
+
def test_numeric_names(self) -> None:
|
|
116
|
+
names = ['123', '456', '789']
|
|
117
|
+
groups = bulk_create_groups_from_names(names)
|
|
118
|
+
assert len(groups) == 3
|
|
119
|
+
|
|
120
|
+
def test_mixed_alphanumeric_names(self) -> None:
|
|
121
|
+
names = ['Group1', '2Group', 'Gr0up3']
|
|
122
|
+
groups = bulk_create_groups_from_names(names)
|
|
123
|
+
assert len(groups) == 3
|
|
124
|
+
|
|
125
|
+
def test_names_with_newlines(self) -> None:
|
|
126
|
+
names = ['Group\nWith\nNewlines']
|
|
127
|
+
groups = bulk_create_groups_from_names(names)
|
|
128
|
+
assert len(groups) == 1
|
|
129
|
+
|
|
130
|
+
def test_names_with_null_characters(self) -> None:
|
|
131
|
+
names = ['Group\x00Name']
|
|
132
|
+
groups = bulk_create_groups_from_names(names)
|
|
133
|
+
assert len(groups) == 1
|
|
134
|
+
|
|
135
|
+
def test_preserves_order_of_creation(self) -> None:
|
|
136
|
+
names = ['First', 'Second', 'Third']
|
|
137
|
+
groups = bulk_create_groups_from_names(names)
|
|
138
|
+
group_names = [g.name for g in groups]
|
|
139
|
+
assert group_names == names
|
|
140
|
+
|
|
141
|
+
def test_does_not_modify_existing_groups(self) -> None:
|
|
142
|
+
existing = AuthGroup.objects.create(name='Existing')
|
|
143
|
+
original_pk = existing.pk
|
|
144
|
+
bulk_create_groups_from_names(['Existing', 'New'])
|
|
145
|
+
existing.refresh_from_db()
|
|
146
|
+
assert existing.pk == original_pk
|