django-spire 0.23.7__py3-none-any.whl → 0.23.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_spire/ai/admin.py +11 -11
- django_spire/ai/chat/apps.py +1 -0
- django_spire/ai/chat/templates/django_spire/ai/chat/widget/dialog_widget.html +1 -1
- django_spire/ai/chat/tests/factories.py +15 -0
- django_spire/ai/chat/tests/test_controller.py +45 -0
- django_spire/ai/chat/tests/test_models.py +301 -0
- django_spire/ai/chat/tests/test_prompts.py +48 -0
- django_spire/ai/chat/tests/test_responses.py +208 -0
- django_spire/ai/chat/tests/test_router/test_base_chat_router.py +66 -6
- django_spire/ai/chat/tests/test_router/test_chat_workflow.py +73 -3
- django_spire/ai/chat/tests/test_router/test_integration.py +86 -6
- django_spire/ai/chat/tests/test_router/test_intent_decoder.py +93 -1
- django_spire/ai/chat/tests/test_router/test_message_intel.py +60 -1
- django_spire/ai/chat/tests/test_router/test_spire_chat_router.py +110 -0
- django_spire/ai/chat/tests/test_urls/test_json_urls.py +202 -1
- django_spire/ai/context/tests/__init__.py +0 -0
- django_spire/ai/context/tests/test_context.py +188 -0
- django_spire/ai/decorators.py +7 -6
- django_spire/ai/prompt/tests/test_bots.py +100 -10
- django_spire/ai/prompt/tests/test_prompt_intel.py +83 -0
- django_spire/ai/prompt/tests/test_prompt_tuning.py +126 -0
- django_spire/ai/sms/decorators.py +8 -2
- django_spire/ai/sms/tests/test_sms.py +240 -16
- django_spire/ai/sms/tests/test_sms_intel.py +42 -0
- django_spire/ai/sms/tests/test_webhook.py +155 -7
- django_spire/ai/sms/views.py +23 -24
- django_spire/ai/tests/test_ai.py +131 -7
- django_spire/auth/apps.py +4 -2
- django_spire/auth/controller/controller.py +36 -23
- django_spire/auth/controller/exceptions.py +9 -0
- django_spire/auth/group/admin.py +1 -0
- django_spire/auth/group/apps.py +2 -0
- django_spire/auth/group/factories.py +17 -8
- django_spire/auth/group/forms.py +7 -0
- django_spire/auth/group/tests/test_factories.py +146 -0
- django_spire/auth/group/tests/test_forms.py +282 -0
- django_spire/auth/group/tests/test_models.py +192 -0
- django_spire/auth/group/tests/test_querysets.py +98 -0
- django_spire/auth/group/tests/test_utils.py +341 -0
- django_spire/auth/group/tests/test_views.py +377 -0
- django_spire/auth/group/urls/__init__.py +3 -1
- django_spire/auth/group/urls/form_urls.py +2 -0
- django_spire/auth/group/urls/json_urls.py +3 -0
- django_spire/auth/group/urls/page_urls.py +2 -0
- django_spire/auth/group/utils.py +6 -2
- django_spire/auth/group/views/form_views.py +6 -3
- django_spire/auth/group/views/json_views.py +6 -2
- django_spire/auth/mfa/admin.py +2 -0
- django_spire/auth/mfa/apps.py +2 -0
- django_spire/auth/mfa/forms.py +1 -0
- django_spire/auth/mfa/querysets.py +9 -2
- django_spire/auth/mfa/tests/test_models.py +233 -0
- django_spire/auth/mfa/tests/test_utils.py +106 -0
- django_spire/auth/mfa/urls/__init__.py +2 -0
- django_spire/auth/mfa/urls/page_urls.py +2 -0
- django_spire/auth/mfa/urls/redirect_urls.py +2 -0
- django_spire/auth/mfa/views/page_views.py +2 -1
- django_spire/auth/permissions/consts.py +2 -2
- django_spire/auth/permissions/decorators.py +8 -8
- django_spire/auth/permissions/permissions.py +28 -35
- django_spire/auth/permissions/tests/test_decorators.py +333 -0
- django_spire/auth/permissions/tests/test_permissions.py +337 -0
- django_spire/auth/permissions/tests/test_tools.py +305 -0
- django_spire/auth/permissions/tools.py +21 -15
- django_spire/auth/seeding/seed.py +3 -0
- django_spire/auth/seeding/seeder.py +2 -0
- django_spire/auth/tests/test_controller.py +323 -0
- django_spire/auth/tests/test_url_endpoints.py +9 -9
- django_spire/auth/tests/test_views.py +406 -0
- django_spire/auth/urls/admin_urls.py +2 -0
- django_spire/auth/urls/redirect_urls.py +2 -0
- django_spire/auth/user/apps.py +2 -0
- django_spire/auth/user/forms.py +9 -0
- django_spire/auth/user/models.py +1 -1
- django_spire/auth/user/services/services.py +1 -0
- django_spire/auth/user/tests/factories.py +14 -13
- django_spire/auth/user/tests/test_factories.py +166 -2
- django_spire/auth/user/tests/test_forms.py +573 -0
- django_spire/auth/user/tests/test_models.py +257 -0
- django_spire/auth/user/tests/test_services.py +200 -0
- django_spire/auth/user/tests/test_tools.py +153 -0
- django_spire/auth/user/tests/test_user_factories.py +139 -0
- django_spire/auth/user/tests/test_views.py +363 -0
- django_spire/auth/user/tools.py +7 -1
- django_spire/auth/user/urls/form_urls.py +3 -0
- django_spire/auth/user/urls/page_urls.py +3 -0
- django_spire/auth/user/views/form_views.py +19 -10
- django_spire/auth/user/views/page_views.py +8 -2
- django_spire/auth/views/redirect_views.py +14 -9
- django_spire/comment/admin.py +2 -0
- django_spire/comment/apps.py +2 -0
- django_spire/comment/templatetags/comment_tags.py +1 -0
- django_spire/comment/tests/test_forms.py +27 -0
- django_spire/comment/tests/test_models.py +215 -0
- django_spire/comment/tests/test_querysets.py +101 -0
- django_spire/comment/tests/test_utils.py +90 -0
- django_spire/comment/urls.py +2 -0
- django_spire/comment/utils.py +22 -13
- django_spire/comment/views.py +1 -1
- django_spire/conf.py +8 -6
- django_spire/consts.py +1 -1
- django_spire/contrib/breadcrumb/apps.py +2 -0
- django_spire/contrib/breadcrumb/breadcrumbs.py +18 -18
- django_spire/contrib/breadcrumb/tests/test_breadcrumbs.py +198 -0
- django_spire/contrib/constructor/__init__.py +3 -3
- django_spire/contrib/constructor/constructor.py +15 -15
- django_spire/contrib/constructor/django_model_constructor.py +5 -4
- django_spire/contrib/constructor/exceptions.py +5 -3
- django_spire/contrib/constructor/tests/__init__.py +0 -0
- django_spire/contrib/constructor/tests/test_constructor.py +193 -0
- django_spire/contrib/form/tests/__init__.py +0 -0
- django_spire/contrib/form/tests/test_forms.py +203 -0
- django_spire/contrib/generic_views/modal_views.py +2 -1
- django_spire/contrib/generic_views/portal_views.py +20 -19
- django_spire/contrib/generic_views/tests/__init__.py +0 -0
- django_spire/contrib/generic_views/tests/test_views.py +459 -0
- django_spire/contrib/help/apps.py +2 -0
- django_spire/contrib/help/templatetags/help.py +1 -0
- django_spire/contrib/help/tests/__init__.py +0 -0
- django_spire/contrib/help/tests/test_templatetags.py +100 -0
- django_spire/contrib/options/mixins.py +6 -5
- django_spire/contrib/options/tests/factories.py +5 -1
- django_spire/contrib/options/tests/test_options.py +234 -0
- django_spire/contrib/ordering/exceptions.py +7 -3
- django_spire/contrib/ordering/mixins.py +2 -0
- django_spire/contrib/ordering/querysets.py +3 -1
- django_spire/contrib/ordering/services/processor_service.py +8 -4
- django_spire/contrib/ordering/services/service.py +1 -2
- django_spire/contrib/ordering/tests/__init__.py +0 -0
- django_spire/contrib/ordering/tests/test_ordering.py +165 -0
- django_spire/contrib/ordering/validators.py +6 -6
- django_spire/contrib/pagination/templatetags/pagination_tags.py +12 -5
- django_spire/contrib/pagination/tests/__init__.py +0 -0
- django_spire/contrib/pagination/tests/test_pagination.py +179 -0
- django_spire/contrib/performance/decorators.py +16 -6
- django_spire/contrib/performance/tests/__init__.py +0 -0
- django_spire/contrib/performance/tests/test_performance.py +107 -0
- django_spire/contrib/queryset/enums.py +3 -1
- django_spire/contrib/queryset/filter_tools.py +10 -5
- django_spire/contrib/queryset/mixins.py +16 -16
- django_spire/contrib/queryset/tests/__init__.py +0 -0
- django_spire/contrib/queryset/tests/test_queryset.py +137 -0
- django_spire/contrib/seeding/field/base.py +13 -7
- django_spire/contrib/seeding/field/callable.py +8 -1
- django_spire/contrib/seeding/field/cleaners.py +5 -5
- django_spire/contrib/seeding/field/custom.py +20 -10
- django_spire/contrib/seeding/field/django/seeder.py +8 -6
- django_spire/contrib/seeding/field/enums.py +7 -5
- django_spire/contrib/seeding/field/override.py +16 -6
- django_spire/contrib/seeding/field/static.py +9 -2
- django_spire/contrib/seeding/field/tests/test_base.py +18 -14
- django_spire/contrib/seeding/field/tests/test_callable.py +13 -9
- django_spire/contrib/seeding/field/tests/test_cleaners.py +51 -38
- django_spire/contrib/seeding/field/tests/test_static.py +13 -9
- django_spire/contrib/seeding/intelligence/bots/seeder_generator_bot.py +2 -0
- django_spire/contrib/seeding/intelligence/intel.py +5 -1
- django_spire/contrib/seeding/intelligence/prompts/factory.py +6 -1
- django_spire/contrib/seeding/intelligence/prompts/foreign_key_selection_prompt.py +6 -1
- django_spire/contrib/seeding/intelligence/prompts/generate_django_model_seeder_prompts.py +2 -0
- django_spire/contrib/seeding/intelligence/prompts/generic_relationship_selection_prompt.py +7 -1
- django_spire/contrib/seeding/intelligence/prompts/hierarchical_selection_prompt.py +6 -2
- django_spire/contrib/seeding/intelligence/prompts/model_field_choices_prompt.py +8 -2
- django_spire/contrib/seeding/intelligence/prompts/negation_prompt.py +2 -0
- django_spire/contrib/seeding/intelligence/prompts/objective_prompt.py +6 -1
- django_spire/contrib/seeding/management/commands/seeding.py +9 -3
- django_spire/contrib/seeding/management/example.py +2 -0
- django_spire/contrib/seeding/model/base.py +16 -7
- django_spire/contrib/seeding/model/config.py +31 -15
- django_spire/contrib/seeding/model/django/config.py +13 -13
- django_spire/contrib/seeding/model/django/seeder.py +4 -4
- django_spire/contrib/seeding/model/django/tests/test_seeder.py +34 -23
- django_spire/contrib/seeding/model/enums.py +2 -0
- django_spire/contrib/seeding/tests/test_config.py +71 -0
- django_spire/contrib/seeding/tests/test_custom.py +35 -0
- django_spire/contrib/seeding/tests/test_enums.py +40 -0
- django_spire/contrib/seeding/tests/test_intel.py +32 -0
- django_spire/contrib/seeding/tests/test_override.py +63 -0
- django_spire/contrib/service/__init__.py +2 -2
- django_spire/contrib/service/django_model_service.py +16 -15
- django_spire/contrib/service/exceptions.py +5 -3
- django_spire/contrib/service/tests/__init__.py +0 -0
- django_spire/contrib/service/tests/test_service.py +153 -0
- django_spire/contrib/session/apps.py +2 -0
- django_spire/contrib/session/controller.py +48 -42
- django_spire/contrib/session/templatetags/session_tags.py +11 -2
- django_spire/contrib/session/tests/test_session_controller.py +117 -53
- django_spire/contrib/tests/__init__.py +0 -0
- django_spire/contrib/tests/test_utils.py +37 -0
- django_spire/contrib/utils.py +4 -1
- django_spire/core/apps.py +2 -0
- django_spire/core/converters/tests/test_to_data.py +353 -0
- django_spire/core/converters/tests/test_to_enums.py +61 -41
- django_spire/core/converters/tests/test_to_pydantic.py +138 -109
- django_spire/core/converters/to_data.py +29 -10
- django_spire/core/converters/to_enums.py +4 -2
- django_spire/core/converters/to_pydantic.py +22 -22
- django_spire/core/decorators.py +19 -6
- django_spire/core/forms/widgets.py +4 -0
- django_spire/core/management/commands/spire_startapp_pkg/template/app/apps.py.template +2 -0
- django_spire/core/management/commands/spire_startapp_pkg/template/app/urls/__init__.py.template +2 -0
- django_spire/core/management/commands/spire_startapp_pkg/template/app/urls/form_urls.py.template +2 -0
- django_spire/core/management/commands/spire_startapp_pkg/template/app/urls/page_urls.py.template +2 -0
- django_spire/core/maps.py +3 -1
- django_spire/core/middleware/maintenance.py +3 -3
- django_spire/core/middleware.py +8 -6
- django_spire/core/redirect/__init__.py +5 -0
- django_spire/core/redirect/generic_redirect.py +1 -2
- django_spire/core/redirect/tests/__init__.py +0 -0
- django_spire/core/redirect/tests/test_generic_redirect.py +34 -0
- django_spire/core/{tests/tests_redirect.py → redirect/tests/test_safe_redirect.py} +55 -81
- django_spire/core/shortcuts.py +3 -3
- django_spire/core/static/django_spire/css/app-layout.css +1 -1
- django_spire/core/static/django_spire/css/app-navigation.css +3 -3
- django_spire/core/static/django_spire/css/bootstrap-override.css +4 -0
- django_spire/core/tag/admin.py +12 -0
- django_spire/core/tag/intelligence/tag_set_bot.py +2 -0
- django_spire/core/tag/mixins.py +2 -0
- django_spire/core/tag/models.py +2 -0
- django_spire/core/tag/querysets.py +2 -0
- django_spire/core/tag/service/tag_service.py +6 -3
- django_spire/core/tag/tests/test_intelligence.py +9 -9
- django_spire/core/tag/tests/test_tags.py +44 -54
- django_spire/core/tag/tests/test_tools.py +191 -0
- django_spire/core/tag/tools.py +3 -0
- django_spire/core/templates/django_spire/card/card.html +5 -2
- django_spire/core/templates/django_spire/card/title_card.html +12 -4
- django_spire/core/templates/django_spire/infinite_scroll/base.html +1 -0
- django_spire/core/templates/django_spire/navigation/side_navigation.html +19 -24
- django_spire/core/templates/django_spire/page/full_page.html +46 -16
- django_spire/core/templates/django_spire/table/base.html +5 -3
- django_spire/core/templatetags/json.py +6 -2
- django_spire/core/templatetags/message.py +13 -8
- django_spire/core/templatetags/string_formating.py +8 -5
- django_spire/core/templatetags/tests/__init__.py +0 -0
- django_spire/core/templatetags/tests/test_templatetags.py +427 -0
- django_spire/core/templatetags/variable_types.py +17 -9
- django_spire/core/tests/test_cases.py +1 -1
- django_spire/core/tests/test_conf.py +43 -0
- django_spire/core/tests/test_consts.py +28 -0
- django_spire/core/tests/test_context_processors.py +93 -0
- django_spire/core/tests/test_decorators.py +95 -0
- django_spire/core/tests/test_django_spire_utils.py +56 -0
- django_spire/core/tests/test_exceptions.py +37 -0
- django_spire/core/tests/test_models.py +54 -0
- django_spire/core/tests/test_settings.py +45 -0
- django_spire/core/tests/test_shortcuts.py +74 -0
- django_spire/core/tests/test_urls.py +16 -0
- django_spire/core/tests/test_utils.py +58 -0
- django_spire/core/urls.py +4 -1
- django_spire/core/utils.py +12 -8
- django_spire/exceptions.py +16 -1
- django_spire/file/admin.py +4 -2
- django_spire/file/apps.py +8 -10
- django_spire/file/fields.py +7 -7
- django_spire/file/forms.py +1 -1
- django_spire/file/interfaces.py +15 -15
- django_spire/file/mixins.py +1 -4
- django_spire/file/models.py +3 -5
- django_spire/file/tests/factories.py +59 -0
- django_spire/file/tests/test_admin.py +69 -0
- django_spire/file/tests/test_apps.py +24 -0
- django_spire/file/tests/test_fields.py +114 -0
- django_spire/file/tests/test_forms.py +20 -0
- django_spire/file/tests/test_interfaces.py +183 -0
- django_spire/file/tests/test_models.py +82 -0
- django_spire/file/tests/test_querysets.py +102 -0
- django_spire/file/tests/test_utils.py +32 -0
- django_spire/file/tests/test_views.py +145 -0
- django_spire/file/tests/test_widgets.py +82 -0
- django_spire/file/tools.py +8 -2
- django_spire/file/views.py +7 -3
- django_spire/file/widgets.py +12 -12
- django_spire/help_desk/admin.py +15 -0
- django_spire/help_desk/apps.py +2 -0
- django_spire/help_desk/auth/controller.py +2 -0
- django_spire/help_desk/choices.py +2 -0
- django_spire/help_desk/enums.py +2 -0
- django_spire/help_desk/exceptions.py +31 -3
- django_spire/help_desk/forms.py +2 -0
- django_spire/help_desk/models.py +2 -0
- django_spire/help_desk/querysets.py +4 -1
- django_spire/help_desk/services/notification_service.py +26 -27
- django_spire/help_desk/services/service.py +2 -3
- django_spire/help_desk/tests/factories.py +8 -3
- django_spire/help_desk/tests/test_admin.py +41 -0
- django_spire/help_desk/tests/test_apps.py +41 -0
- django_spire/help_desk/tests/test_choices.py +50 -0
- django_spire/help_desk/tests/test_controller.py +87 -0
- django_spire/help_desk/tests/test_enums.py +18 -0
- django_spire/help_desk/tests/test_exceptions.py +37 -0
- django_spire/help_desk/tests/test_forms.py +89 -0
- django_spire/help_desk/tests/test_models.py +59 -0
- django_spire/help_desk/tests/test_querysets.py +38 -0
- django_spire/help_desk/tests/test_services/test_notification_service.py +15 -8
- django_spire/help_desk/tests/test_services/test_service.py +92 -0
- django_spire/help_desk/tests/test_urls/test_form_urls.py +6 -6
- django_spire/help_desk/tests/test_urls/test_page_urls.py +8 -9
- django_spire/help_desk/tests/test_views/test_form_views.py +46 -19
- django_spire/help_desk/tests/test_views/test_page_views.py +32 -9
- django_spire/help_desk/urls/__init__.py +4 -1
- django_spire/help_desk/urls/form_urls.py +3 -0
- django_spire/help_desk/urls/page_urls.py +3 -0
- django_spire/help_desk/views/form_views.py +13 -5
- django_spire/help_desk/views/page_views.py +11 -3
- django_spire/history/activity/admin.py +2 -0
- django_spire/history/activity/apps.py +3 -1
- django_spire/history/activity/mixins.py +13 -7
- django_spire/history/activity/models.py +6 -5
- django_spire/history/activity/querysets.py +2 -0
- django_spire/history/activity/tests/__init__.py +0 -0
- django_spire/history/activity/tests/test_activity.py +176 -0
- django_spire/history/admin.py +9 -2
- django_spire/history/choices.py +3 -0
- django_spire/history/models.py +5 -5
- django_spire/history/tests/test_admin.py +93 -0
- django_spire/history/tests/test_history.py +101 -0
- django_spire/history/tests/test_mixins.py +84 -0
- django_spire/history/viewed/admin.py +3 -1
- django_spire/history/viewed/apps.py +3 -1
- django_spire/history/viewed/models.py +2 -0
- django_spire/history/viewed/tests/__init__.py +0 -0
- django_spire/history/viewed/tests/test_viewed.py +46 -0
- django_spire/knowledge/auth/tests/__init__.py +0 -0
- django_spire/knowledge/auth/tests/test_controller.py +116 -0
- django_spire/knowledge/collection/admin.py +5 -1
- django_spire/knowledge/collection/models.py +3 -1
- django_spire/knowledge/collection/seeding/seed.py +1 -0
- django_spire/knowledge/collection/services/factory_service.py +10 -11
- django_spire/knowledge/collection/services/ordering_service.py +1 -2
- django_spire/knowledge/collection/services/service.py +5 -10
- django_spire/knowledge/collection/services/tag_service.py +5 -2
- django_spire/knowledge/collection/tests/factories.py +28 -1
- django_spire/knowledge/collection/tests/test_models.py +48 -0
- django_spire/knowledge/collection/tests/test_querysets.py +93 -0
- django_spire/knowledge/collection/tests/test_services/test_factory_service.py +100 -0
- django_spire/knowledge/collection/tests/test_services/test_services.py +160 -0
- django_spire/knowledge/collection/tests/test_urls/test_form_urls.py +21 -3
- django_spire/knowledge/collection/tests/test_urls/test_json_urls.py +39 -1
- django_spire/knowledge/collection/tests/test_urls/test_page_urls.py +12 -4
- django_spire/knowledge/collection/urls/__init__.py +3 -0
- django_spire/knowledge/collection/urls/form_urls.py +2 -0
- django_spire/knowledge/collection/urls/json_urls.py +2 -0
- django_spire/knowledge/collection/urls/page_urls.py +2 -0
- django_spire/knowledge/collection/views/form_views.py +4 -4
- django_spire/knowledge/collection/views/json_views.py +5 -1
- django_spire/knowledge/collection/views/page_views.py +5 -2
- django_spire/knowledge/entry/admin.py +7 -1
- django_spire/knowledge/entry/forms.py +2 -0
- django_spire/knowledge/entry/models.py +2 -0
- django_spire/knowledge/entry/seeding/seed.py +3 -0
- django_spire/knowledge/entry/services/automation_service.py +5 -4
- django_spire/knowledge/entry/services/factory_service.py +7 -5
- django_spire/knowledge/entry/services/service.py +4 -7
- django_spire/knowledge/entry/services/tag_service.py +0 -1
- django_spire/knowledge/entry/services/tool_service.py +1 -0
- django_spire/knowledge/entry/services/transformation_services.py +1 -5
- django_spire/knowledge/entry/tests/factories.py +1 -2
- django_spire/knowledge/entry/tests/test_factory_service.py +20 -0
- django_spire/knowledge/entry/tests/test_models.py +41 -0
- django_spire/knowledge/entry/tests/test_querysets.py +71 -0
- django_spire/knowledge/entry/tests/test_services.py +94 -0
- django_spire/knowledge/entry/tests/test_urls/test_form_urls.py +9 -14
- django_spire/knowledge/entry/tests/test_urls/test_json_urls.py +48 -5
- django_spire/knowledge/entry/tests/test_urls/test_page_urls.py +6 -8
- django_spire/knowledge/entry/tests/test_urls/test_template_urls.py +40 -0
- django_spire/knowledge/entry/urls/form_urls.py +2 -0
- django_spire/knowledge/entry/urls/json_urls.py +2 -0
- django_spire/knowledge/entry/urls/page_urls.py +2 -0
- django_spire/knowledge/entry/urls/template_urls.py +2 -0
- django_spire/knowledge/entry/version/block/choices.py +2 -0
- django_spire/knowledge/entry/version/block/data/data.py +1 -0
- django_spire/knowledge/entry/version/block/data/list/data.py +8 -13
- django_spire/knowledge/entry/version/block/data/list/maps.py +3 -0
- django_spire/knowledge/entry/version/block/data/list/meta.py +1 -2
- django_spire/knowledge/entry/version/block/data/list/tests/__init__.py +0 -0
- django_spire/knowledge/entry/version/block/data/list/tests/test_maps.py +32 -0
- django_spire/knowledge/entry/version/block/data/list/tests/test_meta.py +58 -0
- django_spire/knowledge/entry/version/block/data/maps.py +3 -6
- django_spire/knowledge/entry/version/block/models.py +7 -5
- django_spire/knowledge/entry/version/block/seeding/constants.py +5 -4
- django_spire/knowledge/entry/version/block/services/service.py +2 -3
- django_spire/knowledge/entry/version/block/tests/factories.py +4 -10
- django_spire/knowledge/entry/version/block/tests/test_choices.py +56 -0
- django_spire/knowledge/entry/version/block/tests/test_data.py +90 -0
- django_spire/knowledge/entry/version/block/tests/test_maps.py +37 -0
- django_spire/knowledge/entry/version/block/tests/test_models.py +55 -0
- django_spire/knowledge/entry/version/block/tests/test_querysets.py +35 -0
- django_spire/knowledge/entry/version/block/tests/test_services.py +65 -0
- django_spire/knowledge/entry/version/choices.py +2 -0
- django_spire/knowledge/entry/version/converters/converter.py +1 -1
- django_spire/knowledge/entry/version/converters/docx_converter.py +4 -7
- django_spire/knowledge/entry/version/converters/markdown_converter.py +20 -20
- django_spire/knowledge/entry/version/maps.py +4 -5
- django_spire/knowledge/entry/version/querysets.py +1 -1
- django_spire/knowledge/entry/version/seeding/seeder.py +1 -2
- django_spire/knowledge/entry/version/services/processor_service.py +5 -4
- django_spire/knowledge/entry/version/services/service.py +1 -2
- django_spire/knowledge/entry/version/tests/factories.py +2 -2
- django_spire/knowledge/entry/version/tests/test_choices.py +18 -0
- django_spire/knowledge/entry/version/tests/test_converters/test_docx_converter.py +56 -8
- django_spire/knowledge/entry/version/tests/test_converters/test_markdown_converter.py +78 -0
- django_spire/knowledge/entry/version/tests/test_maps.py +58 -0
- django_spire/knowledge/entry/version/tests/test_models.py +23 -0
- django_spire/knowledge/entry/version/tests/test_querysets.py +26 -0
- django_spire/knowledge/entry/version/tests/test_services.py +62 -0
- django_spire/knowledge/entry/version/tests/test_urls/test_json_urls.py +27 -8
- django_spire/knowledge/entry/version/tests/test_urls/test_page_urls.py +15 -8
- django_spire/knowledge/entry/version/tests/test_urls/test_redirect_urls.py +38 -0
- django_spire/knowledge/entry/version/urls/__init__.py +3 -0
- django_spire/knowledge/entry/version/urls/json_urls.py +2 -1
- django_spire/knowledge/entry/version/urls/page_urls.py +2 -0
- django_spire/knowledge/entry/version/urls/redirect_urls.py +2 -0
- django_spire/knowledge/entry/version/views/json_views.py +5 -1
- django_spire/knowledge/entry/version/views/page_views.py +10 -3
- django_spire/knowledge/entry/version/views/redirect_views.py +5 -1
- django_spire/knowledge/entry/views/form_views.py +16 -8
- django_spire/knowledge/entry/views/json_views.py +3 -1
- django_spire/knowledge/entry/views/page_views.py +8 -2
- django_spire/knowledge/entry/views/template_views.py +7 -1
- django_spire/knowledge/exceptions.py +2 -1
- django_spire/knowledge/intelligence/intel/answer_intel.py +2 -1
- django_spire/knowledge/intelligence/intel/entry_intel.py +0 -1
- django_spire/knowledge/intelligence/workflows/knowledge_workflow.py +4 -5
- django_spire/knowledge/models.py +1 -2
- django_spire/knowledge/tests/__init__.py +0 -0
- django_spire/knowledge/tests/test_templatetags.py +40 -0
- django_spire/knowledge/tests/test_urls/__init__.py +0 -0
- django_spire/knowledge/tests/test_urls/test_page_urls.py +24 -0
- django_spire/knowledge/urls/__init__.py +2 -0
- django_spire/knowledge/urls/page_urls.py +2 -0
- django_spire/knowledge/views/page_views.py +8 -3
- django_spire/notification/admin.py +3 -1
- django_spire/notification/app/admin.py +2 -0
- django_spire/notification/app/apps.py +3 -1
- django_spire/notification/app/exceptions.py +9 -2
- django_spire/notification/app/models.py +8 -4
- django_spire/notification/app/processor.py +22 -26
- django_spire/notification/app/querysets.py +2 -0
- django_spire/notification/app/tests/__init__.py +0 -0
- django_spire/notification/app/tests/factories.py +34 -0
- django_spire/notification/app/tests/test_apps.py +24 -0
- django_spire/notification/app/tests/test_models.py +72 -0
- django_spire/notification/app/tests/test_processor.py +111 -0
- django_spire/notification/app/tests/test_querysets.py +90 -0
- django_spire/notification/app/tests/test_views/__init__.py +0 -0
- django_spire/notification/app/tests/test_views/test_json_views.py +48 -0
- django_spire/notification/app/tests/test_views/test_page_views.py +19 -0
- django_spire/notification/app/urls/__init__.py +3 -1
- django_spire/notification/app/urls/json_urls.py +6 -4
- django_spire/notification/app/urls/page_urls.py +4 -3
- django_spire/notification/app/urls/template_urls.py +4 -2
- django_spire/notification/apps.py +4 -1
- django_spire/notification/email/admin.py +5 -1
- django_spire/notification/email/apps.py +3 -1
- django_spire/notification/email/exceptions.py +4 -2
- django_spire/notification/email/helper.py +5 -3
- django_spire/notification/email/models.py +4 -0
- django_spire/notification/email/processor.py +19 -15
- django_spire/notification/email/querysets.py +3 -0
- django_spire/notification/email/tests/__init__.py +0 -0
- django_spire/notification/email/tests/factories.py +35 -0
- django_spire/notification/email/tests/test_apps.py +24 -0
- django_spire/notification/email/tests/test_models.py +52 -0
- django_spire/notification/email/tests/test_processor.py +92 -0
- django_spire/notification/email/tests/test_querysets.py +43 -0
- django_spire/notification/exceptions.py +17 -2
- django_spire/notification/managers.py +7 -1
- django_spire/notification/maps.py +4 -1
- django_spire/notification/mixins.py +2 -0
- django_spire/notification/models.py +3 -1
- django_spire/notification/processors/notification.py +12 -5
- django_spire/notification/processors/processor.py +2 -0
- django_spire/notification/processors/tests/__init__.py +0 -0
- django_spire/notification/processors/tests/test_notification.py +106 -0
- django_spire/notification/push/admin.py +10 -1
- django_spire/notification/push/apps.py +3 -1
- django_spire/notification/push/models.py +2 -3
- django_spire/notification/push/tests/__init__.py +0 -0
- django_spire/notification/push/tests/test_apps.py +24 -0
- django_spire/notification/push/tests/test_models.py +28 -0
- django_spire/notification/querysets.py +7 -1
- django_spire/notification/sms/admin.py +2 -0
- django_spire/notification/sms/apps.py +4 -1
- django_spire/notification/sms/automations.py +2 -0
- django_spire/notification/sms/choices.py +2 -0
- django_spire/notification/sms/exceptions.py +19 -5
- django_spire/notification/sms/helper.py +33 -23
- django_spire/notification/sms/models.py +5 -1
- django_spire/notification/sms/processor.py +20 -20
- django_spire/notification/sms/querysets.py +2 -0
- django_spire/notification/sms/tests/factories.py +33 -0
- django_spire/notification/sms/tests/test_apps.py +24 -0
- django_spire/notification/sms/tests/test_automation.py +38 -0
- django_spire/notification/sms/tests/test_choices.py +15 -0
- django_spire/notification/sms/tests/test_consts.py +17 -0
- django_spire/notification/sms/tests/test_exceptions.py +27 -0
- django_spire/notification/sms/tests/test_helper.py +50 -0
- django_spire/notification/sms/tests/test_models.py +81 -0
- django_spire/notification/sms/tests/test_processor.py +107 -0
- django_spire/notification/sms/tests/test_tools.py +25 -11
- django_spire/notification/sms/tools.py +16 -5
- django_spire/notification/sms/urls/__init__.py +3 -1
- django_spire/notification/sms/urls/media_urls.py +2 -0
- django_spire/notification/sms/views/media_views.py +14 -4
- django_spire/notification/tests/__init__.py +0 -0
- django_spire/notification/tests/factories.py +26 -0
- django_spire/notification/tests/test_admin.py +55 -0
- django_spire/notification/tests/test_apps.py +30 -0
- django_spire/notification/tests/test_automation.py +18 -0
- django_spire/notification/tests/test_choices.py +59 -0
- django_spire/notification/tests/test_exceptions.py +58 -0
- django_spire/notification/tests/test_managers.py +100 -0
- django_spire/notification/tests/test_maps.py +31 -0
- django_spire/notification/tests/test_models.py +76 -0
- django_spire/notification/tests/test_querysets.py +184 -0
- django_spire/notification/tests/test_utils.py +23 -0
- django_spire/notification/urls.py +3 -1
- django_spire/notification/utils.py +3 -1
- django_spire/settings.py +3 -0
- django_spire/theme/tests/test_context_processor.py +15 -13
- django_spire/theme/tests/test_enums.py +2 -2
- django_spire/theme/tests/test_filesystem.py +2 -5
- django_spire/theme/tests/test_integration.py +12 -12
- django_spire/theme/tests/test_model.py +40 -38
- django_spire/theme/tests/test_views/test_json_views.py +33 -33
- django_spire/theme/urls/json_urls.py +3 -0
- django_spire/theme/urls/page_urls.py +3 -0
- django_spire/urls.py +19 -15
- django_spire/utils.py +13 -4
- {django_spire-0.23.7.dist-info → django_spire-0.23.9.dist-info}/METADATA +2 -2
- {django_spire-0.23.7.dist-info → django_spire-0.23.9.dist-info}/RECORD +534 -362
- {django_spire-0.23.7.dist-info → django_spire-0.23.9.dist-info}/licenses/LICENSE.md +1 -1
- django_spire/contrib/options/tests/test_unit.py +0 -148
- django_spire/contrib/seeding/tests/test_seeding.py +0 -25
- django_spire/core/tests/test_templatetags.py +0 -117
- django_spire/core/tests/tests_shortcuts.py +0 -73
- django_spire/history/activity/tests.py +0 -3
- django_spire/history/activity/views.py +0 -3
- django_spire/knowledge/collection/tests/test_services/test_transformation_service.py +0 -71
- django_spire/notification/app/tests.py +0 -3
- {django_spire-0.23.7.dist-info → django_spire-0.23.9.dist-info}/WHEEL +0 -0
- {django_spire-0.23.7.dist-info → django_spire-0.23.9.dist-info}/top_level.txt +0 -0
|
@@ -1,15 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
1
5
|
from django_spire.contrib.seeding.field.cleaners import normalize_seeder_fields
|
|
2
6
|
from django_spire.contrib.seeding.model.enums import ModelSeederDefaultsEnum
|
|
3
7
|
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from typing import Any, Self
|
|
10
|
+
|
|
4
11
|
|
|
5
12
|
class FieldsConfig:
|
|
6
13
|
def __init__(
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
):
|
|
14
|
+
self,
|
|
15
|
+
raw_fields: dict[str, Any],
|
|
16
|
+
field_names: list[str],
|
|
17
|
+
default_to: str,
|
|
18
|
+
model_class: Any
|
|
19
|
+
) -> None:
|
|
13
20
|
self.raw_fields = raw_fields or {}
|
|
14
21
|
self.default_to = default_to
|
|
15
22
|
self.field_names = set(field_names)
|
|
@@ -17,7 +24,7 @@ class FieldsConfig:
|
|
|
17
24
|
|
|
18
25
|
self._excluded = {
|
|
19
26
|
k for k, v in self.raw_fields.items()
|
|
20
|
-
if v
|
|
27
|
+
if v in ("exclude", ("exclude",))
|
|
21
28
|
}
|
|
22
29
|
|
|
23
30
|
self.fields = normalize_seeder_fields({
|
|
@@ -26,14 +33,18 @@ class FieldsConfig:
|
|
|
26
33
|
|
|
27
34
|
self._validate()
|
|
28
35
|
self._assign_defaults()
|
|
29
|
-
self._order_fields() # Fields need to be in order for caching hash
|
|
30
36
|
|
|
31
|
-
|
|
37
|
+
# Fields need to be in order for caching hash
|
|
38
|
+
self._order_fields()
|
|
39
|
+
|
|
40
|
+
def _validate(self) -> None:
|
|
32
41
|
unknown = set(self.fields.keys()) - self.field_names
|
|
42
|
+
|
|
33
43
|
if unknown:
|
|
34
|
-
|
|
44
|
+
message = f"Invalid field name(s): {', '.join(unknown)}"
|
|
45
|
+
raise ValueError(message)
|
|
35
46
|
|
|
36
|
-
def _assign_defaults(self):
|
|
47
|
+
def _assign_defaults(self) -> None:
|
|
37
48
|
if self.default_to == ModelSeederDefaultsEnum.INCLUDED:
|
|
38
49
|
return
|
|
39
50
|
|
|
@@ -43,7 +54,7 @@ class FieldsConfig:
|
|
|
43
54
|
if name not in self.fields and name not in self._excluded:
|
|
44
55
|
self.fields[name] = (method,)
|
|
45
56
|
|
|
46
|
-
def override(self, overrides: dict):
|
|
57
|
+
def override(self, overrides: dict) -> Self:
|
|
47
58
|
merged = {**self.fields, **normalize_seeder_fields(overrides)}
|
|
48
59
|
new_raw = {
|
|
49
60
|
**{k: v for k, v in self.raw_fields.items() if k in self._excluded},
|
|
@@ -56,9 +67,14 @@ class FieldsConfig:
|
|
|
56
67
|
model_class=self.model_class
|
|
57
68
|
)
|
|
58
69
|
|
|
59
|
-
def _order_fields(self):
|
|
60
|
-
self.fields =
|
|
70
|
+
def _order_fields(self) -> None:
|
|
71
|
+
self.fields = dict(
|
|
72
|
+
sorted(
|
|
73
|
+
self.fields.items(),
|
|
74
|
+
key=lambda x: x[0]
|
|
75
|
+
)
|
|
76
|
+
)
|
|
61
77
|
|
|
62
78
|
@property
|
|
63
|
-
def excluded(self):
|
|
79
|
+
def excluded(self) -> set:
|
|
64
80
|
return self._excluded
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from django_spire.contrib.seeding.model.config import FieldsConfig
|
|
2
4
|
|
|
3
5
|
|
|
4
6
|
class DjangoModelFieldsConfig(FieldsConfig):
|
|
5
|
-
|
|
6
|
-
def _validate(self):
|
|
7
|
+
def _validate(self) -> None:
|
|
7
8
|
valid_field_names = set(self.field_names)
|
|
8
9
|
|
|
9
10
|
valid_field_names.update(f.attname for f in self.model_class._meta.fields)
|
|
@@ -11,18 +12,18 @@ class DjangoModelFieldsConfig(FieldsConfig):
|
|
|
11
12
|
unknown = set(self.fields.keys()) - valid_field_names
|
|
12
13
|
|
|
13
14
|
if unknown:
|
|
14
|
-
|
|
15
|
+
message = f"Invalid field name(s): {', '.join(unknown)}"
|
|
16
|
+
raise ValueError(message)
|
|
15
17
|
|
|
16
18
|
# fk_fields = {
|
|
17
19
|
# f.name: f.attname # f.attname = "user_id"
|
|
18
20
|
# for f in self.model_class._meta.fields
|
|
19
21
|
# if isinstance(f, ForeignKey)
|
|
20
22
|
# }
|
|
21
|
-
|
|
23
|
+
|
|
22
24
|
# for name, value in self.fields.items():
|
|
23
25
|
# if name in fk_fields:
|
|
24
26
|
# valid_types = [int, None]
|
|
25
|
-
#
|
|
26
27
|
|
|
27
28
|
# def map_foreign_keys(self):
|
|
28
29
|
# fk_fields = {
|
|
@@ -30,19 +31,20 @@ class DjangoModelFieldsConfig(FieldsConfig):
|
|
|
30
31
|
# for f in self.model_class._meta.fields
|
|
31
32
|
# if isinstance(f, ForeignKey)
|
|
32
33
|
# }
|
|
33
|
-
|
|
34
|
+
|
|
34
35
|
# # If asset_id and asset are passed, should I raise an error?
|
|
35
|
-
|
|
36
|
+
|
|
36
37
|
# {
|
|
37
38
|
# 'asset' : ('custom', 'in_order', {'values': [a.id for a in assets]}),
|
|
38
39
|
# 'asset_id': ('custom', 'in_order', {'values': [a.id for a in assets]}),
|
|
39
40
|
# }
|
|
40
|
-
|
|
41
|
+
|
|
41
42
|
# translated_fields = {}
|
|
43
|
+
|
|
42
44
|
# for name, value in self.fields.items():
|
|
43
45
|
# if name in fk_fields:
|
|
44
46
|
# translated_name = fk_fields[name]
|
|
45
|
-
|
|
47
|
+
|
|
46
48
|
# if translated_name in self.field_names:
|
|
47
49
|
# # Do not want to override asset_id if the user has already pass it in.
|
|
48
50
|
# continue
|
|
@@ -50,9 +52,7 @@ class DjangoModelFieldsConfig(FieldsConfig):
|
|
|
50
52
|
# translated_fields[translated_name] = value
|
|
51
53
|
# else:
|
|
52
54
|
# translated_fields[name] = value
|
|
53
|
-
|
|
55
|
+
|
|
54
56
|
# return translated_fields
|
|
55
|
-
|
|
57
|
+
|
|
56
58
|
# # Update self.fields.
|
|
57
|
-
#
|
|
58
|
-
#
|
|
@@ -2,8 +2,8 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import TypeVar, TYPE_CHECKING
|
|
4
4
|
|
|
5
|
-
from django.db.models import ForeignKey
|
|
6
5
|
from django.db.models.base import Model
|
|
6
|
+
from django.db.models import ForeignKey
|
|
7
7
|
|
|
8
8
|
from django_spire.contrib.seeding.field.callable import CallableFieldSeeder
|
|
9
9
|
from django_spire.contrib.seeding.field.custom import CustomFieldSeeder
|
|
@@ -37,11 +37,11 @@ class DjangoModelSeeder(BaseModelSeeder):
|
|
|
37
37
|
super().__init_subclass__(**kwargs)
|
|
38
38
|
|
|
39
39
|
if cls.model_class is None:
|
|
40
|
-
message =
|
|
40
|
+
message = 'Seeds must have a model class'
|
|
41
41
|
raise ValueError(message)
|
|
42
42
|
|
|
43
43
|
if cls.fields is None:
|
|
44
|
-
message =
|
|
44
|
+
message = 'Seeds must have fields'
|
|
45
45
|
raise ValueError(message)
|
|
46
46
|
|
|
47
47
|
@classmethod
|
|
@@ -55,7 +55,7 @@ class DjangoModelSeeder(BaseModelSeeder):
|
|
|
55
55
|
@classmethod
|
|
56
56
|
def seed_database(
|
|
57
57
|
cls,
|
|
58
|
-
count=1,
|
|
58
|
+
count: int = 1,
|
|
59
59
|
fields: dict | None = None
|
|
60
60
|
) -> list[TypeModel]:
|
|
61
61
|
model_objects = cls.seed(count, fields)
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
2
5
|
from django.db import models
|
|
6
|
+
from django.test import TestCase
|
|
3
7
|
from django.utils.timezone import now
|
|
4
8
|
|
|
5
9
|
from django_spire.contrib.seeding import DjangoModelSeeder
|
|
@@ -15,39 +19,46 @@ class Product(models.Model):
|
|
|
15
19
|
updated_at = models.DateTimeField(null=True, blank=True)
|
|
16
20
|
|
|
17
21
|
class Meta:
|
|
18
|
-
app_label =
|
|
22
|
+
app_label = 'tests'
|
|
19
23
|
managed = False
|
|
20
24
|
|
|
25
|
+
def __str__(self) -> str:
|
|
26
|
+
return 'Product'
|
|
27
|
+
|
|
21
28
|
|
|
22
29
|
class ProductSeeder(DjangoModelSeeder):
|
|
23
30
|
model_class = Product
|
|
24
31
|
fields = {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
'name': ('llm', 'Clothing store'),
|
|
33
|
+
'description': ('static', 'A great product for testing.'),
|
|
34
|
+
'price': ('faker', 'pydecimal', {'left_digits': 2, 'right_digits': 2, 'positive': True}),
|
|
35
|
+
'in_stock': True,
|
|
36
|
+
'created_at': ('callable', now),
|
|
37
|
+
'updated_at': 'exclude',
|
|
31
38
|
}
|
|
32
39
|
|
|
33
40
|
|
|
34
|
-
class TestProductSeeder(
|
|
41
|
+
class TestProductSeeder(TestCase):
|
|
42
|
+
def test_assign_defaults(self) -> None:
|
|
43
|
+
assert 'sku' in ProductSeeder.resolved_fields()
|
|
44
|
+
|
|
45
|
+
def test_field_override_applies(self) -> None:
|
|
46
|
+
override_time = now()
|
|
47
|
+
|
|
48
|
+
result = ProductSeeder.seed(count=1, fields={'updated_at': ('static', override_time)})
|
|
35
49
|
|
|
36
|
-
|
|
37
|
-
self.assertIn('sku', ProductSeeder.resolved_fields().keys())
|
|
50
|
+
assert result[0].updated_at == override_time
|
|
38
51
|
|
|
39
|
-
def
|
|
52
|
+
def test_raises_for_invalid_field(self) -> None:
|
|
53
|
+
with pytest.raises(Exception) as exc_info:
|
|
54
|
+
ProductSeeder.seed(count=1, fields={'not_a_field': ('static', 'value')})
|
|
55
|
+
|
|
56
|
+
assert 'not_a_field' in str(exc_info.value)
|
|
57
|
+
|
|
58
|
+
def test_seeds_three_objects(self) -> None:
|
|
40
59
|
result = ProductSeeder.seed(count=3)
|
|
41
|
-
self.assertEqual(len(result), 3)
|
|
42
|
-
for obj in result:
|
|
43
|
-
self.assertIsInstance(obj, Product)
|
|
44
60
|
|
|
45
|
-
|
|
46
|
-
with self.assertRaises(Exception) as context:
|
|
47
|
-
ProductSeeder.seed(count=1, fields={"not_a_field": ("static", "value")})
|
|
48
|
-
self.assertIn("not_a_field", str(context.exception))
|
|
61
|
+
assert len(result) == 3
|
|
49
62
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
result = ProductSeeder.seed(count=1, fields={"updated_at": ("static", override_time)})
|
|
53
|
-
self.assertEqual(result[0].updated_at, override_time)
|
|
63
|
+
for obj in result:
|
|
64
|
+
assert isinstance(obj, Product)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from unittest.mock import MagicMock
|
|
6
|
+
|
|
7
|
+
from django.test import TestCase
|
|
8
|
+
|
|
9
|
+
from django_spire.contrib.seeding.model.config import FieldsConfig
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestFieldsConfig(TestCase):
|
|
13
|
+
def setUp(self) -> None:
|
|
14
|
+
self.model_class = MagicMock()
|
|
15
|
+
|
|
16
|
+
def test_assign_defaults_adds_missing_fields(self) -> None:
|
|
17
|
+
config = FieldsConfig(
|
|
18
|
+
raw_fields={'name': ('static', 'test')},
|
|
19
|
+
field_names=['name', 'email'],
|
|
20
|
+
default_to='llm',
|
|
21
|
+
model_class=self.model_class
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
assert 'email' in config.fields
|
|
25
|
+
assert config.fields['email'] == ('llm',)
|
|
26
|
+
|
|
27
|
+
def test_excluded_property(self) -> None:
|
|
28
|
+
config = FieldsConfig(
|
|
29
|
+
raw_fields={'name': 'exclude', 'email': ('exclude',)},
|
|
30
|
+
field_names=['name', 'email', 'status'],
|
|
31
|
+
default_to='llm',
|
|
32
|
+
model_class=self.model_class
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
assert 'name' in config.excluded
|
|
36
|
+
assert 'email' in config.excluded
|
|
37
|
+
assert 'status' not in config.excluded
|
|
38
|
+
|
|
39
|
+
def test_fields_are_ordered(self) -> None:
|
|
40
|
+
config = FieldsConfig(
|
|
41
|
+
raw_fields={'zebra': ('static', 'z'), 'apple': ('static', 'a')},
|
|
42
|
+
field_names=['zebra', 'apple'],
|
|
43
|
+
default_to='included',
|
|
44
|
+
model_class=self.model_class
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
keys = list(config.fields.keys())
|
|
48
|
+
|
|
49
|
+
assert keys == ['apple', 'zebra']
|
|
50
|
+
|
|
51
|
+
def test_override_creates_new_config(self) -> None:
|
|
52
|
+
config = FieldsConfig(
|
|
53
|
+
raw_fields={'name': ('static', 'original')},
|
|
54
|
+
field_names=['name', 'email'],
|
|
55
|
+
default_to='llm',
|
|
56
|
+
model_class=self.model_class
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
new_config = config.override({'name': ('static', 'overridden')})
|
|
60
|
+
|
|
61
|
+
assert new_config is not config
|
|
62
|
+
assert new_config.fields['name'] == ('static', 'overridden')
|
|
63
|
+
|
|
64
|
+
def test_validate_raises_for_invalid_fields(self) -> None:
|
|
65
|
+
with pytest.raises(ValueError, match='Invalid field name'):
|
|
66
|
+
FieldsConfig(
|
|
67
|
+
raw_fields={'invalid_field': ('static', 'test')},
|
|
68
|
+
field_names=['name', 'email'],
|
|
69
|
+
default_to='llm',
|
|
70
|
+
model_class=self.model_class
|
|
71
|
+
)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from django.test import TestCase
|
|
6
|
+
from django.utils import timezone
|
|
7
|
+
|
|
8
|
+
from django_spire.contrib.seeding.field.custom import CustomFieldSeeder
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestCustomFieldSeeder(TestCase):
|
|
12
|
+
def test_date_time_between_returns_aware_datetime(self) -> None:
|
|
13
|
+
seeder = CustomFieldSeeder(fields={})
|
|
14
|
+
|
|
15
|
+
result = seeder.date_time_between(start_date='-30d', end_date='now')
|
|
16
|
+
|
|
17
|
+
assert timezone.is_aware(result)
|
|
18
|
+
|
|
19
|
+
def test_in_order_cycles_through_values(self) -> None:
|
|
20
|
+
seeder = CustomFieldSeeder(fields={})
|
|
21
|
+
values = ['a', 'b', 'c']
|
|
22
|
+
|
|
23
|
+
assert seeder.in_order(values, index=0) == 'a'
|
|
24
|
+
assert seeder.in_order(values, index=1) == 'b'
|
|
25
|
+
assert seeder.in_order(values, index=2) == 'c'
|
|
26
|
+
assert seeder.in_order(values, index=3) == 'a'
|
|
27
|
+
|
|
28
|
+
def test_in_order_raises_for_empty_list(self) -> None:
|
|
29
|
+
seeder = CustomFieldSeeder(fields={})
|
|
30
|
+
|
|
31
|
+
with pytest.raises(ValueError, match='Cannot select from empty values list'):
|
|
32
|
+
seeder.in_order(values=[], index=0)
|
|
33
|
+
|
|
34
|
+
def test_keyword_is_custom(self) -> None:
|
|
35
|
+
assert CustomFieldSeeder.keyword == 'custom'
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from django.test import TestCase
|
|
4
|
+
|
|
5
|
+
from django_spire.contrib.seeding.field.enums import FieldSeederTypesEnum
|
|
6
|
+
from django_spire.contrib.seeding.model.enums import ModelSeederDefaultsEnum
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestFieldSeederTypesEnum(TestCase):
|
|
10
|
+
def test_callable_value(self) -> None:
|
|
11
|
+
assert FieldSeederTypesEnum.CALLABLE == 'callable'
|
|
12
|
+
|
|
13
|
+
def test_custom_value(self) -> None:
|
|
14
|
+
assert FieldSeederTypesEnum.CUSTOM == 'custom'
|
|
15
|
+
|
|
16
|
+
def test_faker_value(self) -> None:
|
|
17
|
+
assert FieldSeederTypesEnum.FAKER == 'faker'
|
|
18
|
+
|
|
19
|
+
def test_is_str_enum(self) -> None:
|
|
20
|
+
assert isinstance(FieldSeederTypesEnum.LLM, str)
|
|
21
|
+
|
|
22
|
+
def test_llm_value(self) -> None:
|
|
23
|
+
assert FieldSeederTypesEnum.LLM == 'llm'
|
|
24
|
+
|
|
25
|
+
def test_static_value(self) -> None:
|
|
26
|
+
assert FieldSeederTypesEnum.STATIC == 'static'
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TestModelSeederDefaultsEnum(TestCase):
|
|
30
|
+
def test_faker_value(self) -> None:
|
|
31
|
+
assert ModelSeederDefaultsEnum.FAKER == 'faker'
|
|
32
|
+
|
|
33
|
+
def test_included_value(self) -> None:
|
|
34
|
+
assert ModelSeederDefaultsEnum.INCLUDED == 'included'
|
|
35
|
+
|
|
36
|
+
def test_is_str_enum(self) -> None:
|
|
37
|
+
assert isinstance(ModelSeederDefaultsEnum.LLM, str)
|
|
38
|
+
|
|
39
|
+
def test_llm_value(self) -> None:
|
|
40
|
+
assert ModelSeederDefaultsEnum.LLM == 'llm'
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from django.test import TestCase
|
|
4
|
+
|
|
5
|
+
from django_spire.contrib.seeding.intelligence.intel import SeedingIntel, SourceIntel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestSeedingIntel(TestCase):
|
|
9
|
+
def test_is_iterable(self) -> None:
|
|
10
|
+
intel = SeedingIntel(items=[{'name': 'test1'}, {'name': 'test2'}])
|
|
11
|
+
|
|
12
|
+
result = list(intel)
|
|
13
|
+
|
|
14
|
+
assert result == [{'name': 'test1'}, {'name': 'test2'}]
|
|
15
|
+
|
|
16
|
+
def test_items_attribute(self) -> None:
|
|
17
|
+
items = [{'name': 'test'}]
|
|
18
|
+
intel = SeedingIntel(items=items)
|
|
19
|
+
|
|
20
|
+
assert intel.items == items
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TestSourceIntel(TestCase):
|
|
24
|
+
def test_file_name_attribute(self) -> None:
|
|
25
|
+
intel = SourceIntel(file_name='test.py', python_source_code='print("hello")')
|
|
26
|
+
|
|
27
|
+
assert intel.file_name == 'test.py'
|
|
28
|
+
|
|
29
|
+
def test_python_source_code_attribute(self) -> None:
|
|
30
|
+
intel = SourceIntel(file_name='test.py', python_source_code='print("hello")')
|
|
31
|
+
|
|
32
|
+
assert intel.python_source_code == 'print("hello")'
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from unittest.mock import MagicMock
|
|
4
|
+
|
|
5
|
+
from django.test import TestCase
|
|
6
|
+
|
|
7
|
+
from django_spire.contrib.seeding.field.override import FieldOverride
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestFieldOverride(TestCase):
|
|
11
|
+
def setUp(self) -> None:
|
|
12
|
+
self.seeder_class = MagicMock()
|
|
13
|
+
self.seeder_class.seed.return_value = [{'name': 'test'}]
|
|
14
|
+
|
|
15
|
+
def test_filter_returns_self(self) -> None:
|
|
16
|
+
override = FieldOverride(self.seeder_class)
|
|
17
|
+
|
|
18
|
+
result = override.filter(name='test')
|
|
19
|
+
|
|
20
|
+
assert result is override
|
|
21
|
+
|
|
22
|
+
def test_filter_updates_overrides(self) -> None:
|
|
23
|
+
override = FieldOverride(self.seeder_class)
|
|
24
|
+
|
|
25
|
+
override.filter(name='test', status='active')
|
|
26
|
+
|
|
27
|
+
assert override.overrides == {'name': 'test', 'status': 'active'}
|
|
28
|
+
|
|
29
|
+
def test_getattr_delegates_to_seeder_class(self) -> None:
|
|
30
|
+
self.seeder_class.some_attribute = 'value'
|
|
31
|
+
override = FieldOverride(self.seeder_class)
|
|
32
|
+
|
|
33
|
+
result = override.some_attribute
|
|
34
|
+
|
|
35
|
+
assert result == 'value'
|
|
36
|
+
|
|
37
|
+
def test_init_sets_seeder_class(self) -> None:
|
|
38
|
+
override = FieldOverride(self.seeder_class)
|
|
39
|
+
|
|
40
|
+
assert override.seeder_class is self.seeder_class
|
|
41
|
+
|
|
42
|
+
def test_init_sets_empty_overrides(self) -> None:
|
|
43
|
+
override = FieldOverride(self.seeder_class)
|
|
44
|
+
|
|
45
|
+
assert override.overrides == {}
|
|
46
|
+
|
|
47
|
+
def test_seed_calls_seeder_class_seed(self) -> None:
|
|
48
|
+
override = FieldOverride(self.seeder_class)
|
|
49
|
+
override.filter(name='override_value')
|
|
50
|
+
|
|
51
|
+
override.seed(count=2)
|
|
52
|
+
|
|
53
|
+
self.seeder_class.seed.assert_called_once_with(
|
|
54
|
+
count=2,
|
|
55
|
+
fields={'name': 'override_value'}
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def test_seed_returns_seeder_class_result(self) -> None:
|
|
59
|
+
override = FieldOverride(self.seeder_class)
|
|
60
|
+
|
|
61
|
+
result = override.seed(count=1)
|
|
62
|
+
|
|
63
|
+
assert result == [{'name': 'test'}]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
from django_spire.contrib.service.exceptions import
|
|
1
|
+
from django_spire.contrib.service.exceptions import ServiceError
|
|
2
2
|
from django_spire.contrib.service.django_model_service import BaseDjangoModelService
|
|
3
3
|
|
|
4
4
|
__all__ = [
|
|
5
5
|
'BaseDjangoModelService',
|
|
6
|
-
'
|
|
6
|
+
'ServiceError',
|
|
7
7
|
]
|
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
+
|
|
4
5
|
from abc import ABC
|
|
5
|
-
from typing import
|
|
6
|
+
from typing import Generic, TypeVar
|
|
6
7
|
|
|
7
8
|
from django.db import transaction
|
|
8
9
|
from django.db.models import Model
|
|
9
10
|
|
|
10
11
|
from django_spire.contrib.constructor.django_model_constructor import BaseDjangoModelConstructor
|
|
11
|
-
from django_spire.contrib.service.exceptions import
|
|
12
|
+
from django_spire.contrib.service.exceptions import ServiceError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
log = logging.getLogger(__name__)
|
|
12
16
|
|
|
13
|
-
|
|
17
|
+
TypeDjangoModel_co = TypeVar('TypeDjangoModel_co', bound=Model, covariant=True)
|
|
14
18
|
|
|
15
19
|
|
|
16
20
|
class BaseDjangoModelService(
|
|
17
|
-
BaseDjangoModelConstructor[
|
|
21
|
+
BaseDjangoModelConstructor[TypeDjangoModel_co],
|
|
18
22
|
ABC,
|
|
19
|
-
Generic[
|
|
23
|
+
Generic[TypeDjangoModel_co]
|
|
20
24
|
):
|
|
21
25
|
def _get_concrete_fields(self) -> dict:
|
|
22
26
|
return {
|
|
@@ -33,7 +37,7 @@ class BaseDjangoModelService(
|
|
|
33
37
|
|
|
34
38
|
for field, value in field_data.items():
|
|
35
39
|
if field not in allowed:
|
|
36
|
-
|
|
40
|
+
log.warning(f'Field {field!r} is not valid for {self.obj.__class__.__name__}')
|
|
37
41
|
continue
|
|
38
42
|
|
|
39
43
|
model_field = concrete_fields.get(field.removesuffix("_id"), None)
|
|
@@ -51,12 +55,8 @@ class BaseDjangoModelService(
|
|
|
51
55
|
concrete_fields = self._get_concrete_fields()
|
|
52
56
|
touched_fields = self._get_touched_fields(concrete_fields, **field_data)
|
|
53
57
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
exclude=[field for field in concrete_fields if field not in touched_fields]
|
|
57
|
-
)
|
|
58
|
-
except:
|
|
59
|
-
raise
|
|
58
|
+
exclude = [field for field in concrete_fields if field not in touched_fields]
|
|
59
|
+
self.obj.full_clean(exclude=exclude)
|
|
60
60
|
|
|
61
61
|
return touched_fields
|
|
62
62
|
|
|
@@ -65,7 +65,8 @@ class BaseDjangoModelService(
|
|
|
65
65
|
new_model_obj_was_created = False
|
|
66
66
|
|
|
67
67
|
if not field_data:
|
|
68
|
-
|
|
68
|
+
message = f'Field data is required to save on {self.obj.__class__.__name__}'
|
|
69
|
+
raise ServiceError(message)
|
|
69
70
|
|
|
70
71
|
touched_fields = self.validate_model_obj(**field_data)
|
|
71
72
|
|
|
@@ -77,7 +78,7 @@ class BaseDjangoModelService(
|
|
|
77
78
|
self.obj.save(update_fields=touched_fields)
|
|
78
79
|
|
|
79
80
|
else:
|
|
80
|
-
|
|
81
|
-
|
|
81
|
+
message = f'{self.obj.__class__.__name__} is not a new object or there was no touched fields to update.'
|
|
82
|
+
log.warning(message)
|
|
82
83
|
|
|
83
84
|
return self.obj, new_model_obj_was_created
|
|
File without changes
|