django-spire 0.23.7__py3-none-any.whl → 0.23.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_spire/ai/admin.py +11 -11
- django_spire/ai/chat/apps.py +1 -0
- django_spire/ai/chat/templates/django_spire/ai/chat/widget/dialog_widget.html +1 -1
- django_spire/ai/chat/tests/factories.py +15 -0
- django_spire/ai/chat/tests/test_controller.py +45 -0
- django_spire/ai/chat/tests/test_models.py +301 -0
- django_spire/ai/chat/tests/test_prompts.py +48 -0
- django_spire/ai/chat/tests/test_responses.py +208 -0
- django_spire/ai/chat/tests/test_router/test_base_chat_router.py +66 -6
- django_spire/ai/chat/tests/test_router/test_chat_workflow.py +73 -3
- django_spire/ai/chat/tests/test_router/test_integration.py +86 -6
- django_spire/ai/chat/tests/test_router/test_intent_decoder.py +93 -1
- django_spire/ai/chat/tests/test_router/test_message_intel.py +60 -1
- django_spire/ai/chat/tests/test_router/test_spire_chat_router.py +110 -0
- django_spire/ai/chat/tests/test_urls/test_json_urls.py +202 -1
- django_spire/ai/context/tests/__init__.py +0 -0
- django_spire/ai/context/tests/test_context.py +188 -0
- django_spire/ai/decorators.py +7 -6
- django_spire/ai/prompt/tests/test_bots.py +100 -10
- django_spire/ai/prompt/tests/test_prompt_intel.py +83 -0
- django_spire/ai/prompt/tests/test_prompt_tuning.py +126 -0
- django_spire/ai/sms/decorators.py +8 -2
- django_spire/ai/sms/tests/test_sms.py +240 -16
- django_spire/ai/sms/tests/test_sms_intel.py +42 -0
- django_spire/ai/sms/tests/test_webhook.py +155 -7
- django_spire/ai/sms/views.py +23 -24
- django_spire/ai/tests/test_ai.py +131 -7
- django_spire/auth/apps.py +4 -2
- django_spire/auth/controller/controller.py +36 -23
- django_spire/auth/controller/exceptions.py +9 -0
- django_spire/auth/group/admin.py +1 -0
- django_spire/auth/group/apps.py +2 -0
- django_spire/auth/group/factories.py +17 -8
- django_spire/auth/group/forms.py +7 -0
- django_spire/auth/group/tests/test_factories.py +146 -0
- django_spire/auth/group/tests/test_forms.py +282 -0
- django_spire/auth/group/tests/test_models.py +192 -0
- django_spire/auth/group/tests/test_querysets.py +98 -0
- django_spire/auth/group/tests/test_utils.py +341 -0
- django_spire/auth/group/tests/test_views.py +377 -0
- django_spire/auth/group/urls/__init__.py +3 -1
- django_spire/auth/group/urls/form_urls.py +2 -0
- django_spire/auth/group/urls/json_urls.py +3 -0
- django_spire/auth/group/urls/page_urls.py +2 -0
- django_spire/auth/group/utils.py +6 -2
- django_spire/auth/group/views/form_views.py +6 -3
- django_spire/auth/group/views/json_views.py +6 -2
- django_spire/auth/mfa/admin.py +2 -0
- django_spire/auth/mfa/apps.py +2 -0
- django_spire/auth/mfa/forms.py +1 -0
- django_spire/auth/mfa/querysets.py +9 -2
- django_spire/auth/mfa/tests/test_models.py +233 -0
- django_spire/auth/mfa/tests/test_utils.py +106 -0
- django_spire/auth/mfa/urls/__init__.py +2 -0
- django_spire/auth/mfa/urls/page_urls.py +2 -0
- django_spire/auth/mfa/urls/redirect_urls.py +2 -0
- django_spire/auth/mfa/views/page_views.py +2 -1
- django_spire/auth/permissions/consts.py +2 -2
- django_spire/auth/permissions/decorators.py +8 -8
- django_spire/auth/permissions/permissions.py +28 -35
- django_spire/auth/permissions/tests/test_decorators.py +333 -0
- django_spire/auth/permissions/tests/test_permissions.py +337 -0
- django_spire/auth/permissions/tests/test_tools.py +305 -0
- django_spire/auth/permissions/tools.py +21 -15
- django_spire/auth/seeding/seed.py +3 -0
- django_spire/auth/seeding/seeder.py +2 -0
- django_spire/auth/tests/test_controller.py +323 -0
- django_spire/auth/tests/test_url_endpoints.py +9 -9
- django_spire/auth/tests/test_views.py +406 -0
- django_spire/auth/urls/admin_urls.py +2 -0
- django_spire/auth/urls/redirect_urls.py +2 -0
- django_spire/auth/user/apps.py +2 -0
- django_spire/auth/user/forms.py +9 -0
- django_spire/auth/user/models.py +1 -1
- django_spire/auth/user/services/services.py +1 -0
- django_spire/auth/user/tests/factories.py +14 -13
- django_spire/auth/user/tests/test_factories.py +166 -2
- django_spire/auth/user/tests/test_forms.py +573 -0
- django_spire/auth/user/tests/test_models.py +257 -0
- django_spire/auth/user/tests/test_services.py +200 -0
- django_spire/auth/user/tests/test_tools.py +153 -0
- django_spire/auth/user/tests/test_user_factories.py +139 -0
- django_spire/auth/user/tests/test_views.py +363 -0
- django_spire/auth/user/tools.py +7 -1
- django_spire/auth/user/urls/form_urls.py +3 -0
- django_spire/auth/user/urls/page_urls.py +3 -0
- django_spire/auth/user/views/form_views.py +19 -10
- django_spire/auth/user/views/page_views.py +8 -2
- django_spire/auth/views/redirect_views.py +14 -9
- django_spire/comment/admin.py +2 -0
- django_spire/comment/apps.py +2 -0
- django_spire/comment/templatetags/comment_tags.py +1 -0
- django_spire/comment/tests/test_forms.py +27 -0
- django_spire/comment/tests/test_models.py +215 -0
- django_spire/comment/tests/test_querysets.py +101 -0
- django_spire/comment/tests/test_utils.py +90 -0
- django_spire/comment/urls.py +2 -0
- django_spire/comment/utils.py +22 -13
- django_spire/comment/views.py +1 -1
- django_spire/conf.py +8 -6
- django_spire/consts.py +1 -1
- django_spire/contrib/breadcrumb/apps.py +2 -0
- django_spire/contrib/breadcrumb/breadcrumbs.py +18 -18
- django_spire/contrib/breadcrumb/tests/test_breadcrumbs.py +198 -0
- django_spire/contrib/constructor/__init__.py +3 -3
- django_spire/contrib/constructor/constructor.py +15 -15
- django_spire/contrib/constructor/django_model_constructor.py +5 -4
- django_spire/contrib/constructor/exceptions.py +5 -3
- django_spire/contrib/constructor/tests/__init__.py +0 -0
- django_spire/contrib/constructor/tests/test_constructor.py +193 -0
- django_spire/contrib/form/tests/__init__.py +0 -0
- django_spire/contrib/form/tests/test_forms.py +203 -0
- django_spire/contrib/generic_views/modal_views.py +2 -1
- django_spire/contrib/generic_views/portal_views.py +20 -19
- django_spire/contrib/generic_views/tests/__init__.py +0 -0
- django_spire/contrib/generic_views/tests/test_views.py +459 -0
- django_spire/contrib/help/apps.py +2 -0
- django_spire/contrib/help/templatetags/help.py +1 -0
- django_spire/contrib/help/tests/__init__.py +0 -0
- django_spire/contrib/help/tests/test_templatetags.py +100 -0
- django_spire/contrib/options/mixins.py +6 -5
- django_spire/contrib/options/tests/factories.py +5 -1
- django_spire/contrib/options/tests/test_options.py +234 -0
- django_spire/contrib/ordering/exceptions.py +7 -3
- django_spire/contrib/ordering/mixins.py +2 -0
- django_spire/contrib/ordering/querysets.py +3 -1
- django_spire/contrib/ordering/services/processor_service.py +8 -4
- django_spire/contrib/ordering/services/service.py +1 -2
- django_spire/contrib/ordering/tests/__init__.py +0 -0
- django_spire/contrib/ordering/tests/test_ordering.py +165 -0
- django_spire/contrib/ordering/validators.py +6 -6
- django_spire/contrib/pagination/templatetags/pagination_tags.py +12 -5
- django_spire/contrib/pagination/tests/__init__.py +0 -0
- django_spire/contrib/pagination/tests/test_pagination.py +179 -0
- django_spire/contrib/performance/decorators.py +16 -6
- django_spire/contrib/performance/tests/__init__.py +0 -0
- django_spire/contrib/performance/tests/test_performance.py +107 -0
- django_spire/contrib/queryset/enums.py +3 -1
- django_spire/contrib/queryset/filter_tools.py +10 -5
- django_spire/contrib/queryset/mixins.py +16 -16
- django_spire/contrib/queryset/tests/__init__.py +0 -0
- django_spire/contrib/queryset/tests/test_queryset.py +137 -0
- django_spire/contrib/seeding/field/base.py +13 -7
- django_spire/contrib/seeding/field/callable.py +8 -1
- django_spire/contrib/seeding/field/cleaners.py +5 -5
- django_spire/contrib/seeding/field/custom.py +20 -10
- django_spire/contrib/seeding/field/django/seeder.py +8 -6
- django_spire/contrib/seeding/field/enums.py +7 -5
- django_spire/contrib/seeding/field/override.py +16 -6
- django_spire/contrib/seeding/field/static.py +9 -2
- django_spire/contrib/seeding/field/tests/test_base.py +18 -14
- django_spire/contrib/seeding/field/tests/test_callable.py +13 -9
- django_spire/contrib/seeding/field/tests/test_cleaners.py +51 -38
- django_spire/contrib/seeding/field/tests/test_static.py +13 -9
- django_spire/contrib/seeding/intelligence/bots/seeder_generator_bot.py +2 -0
- django_spire/contrib/seeding/intelligence/intel.py +5 -1
- django_spire/contrib/seeding/intelligence/prompts/factory.py +6 -1
- django_spire/contrib/seeding/intelligence/prompts/foreign_key_selection_prompt.py +6 -1
- django_spire/contrib/seeding/intelligence/prompts/generate_django_model_seeder_prompts.py +2 -0
- django_spire/contrib/seeding/intelligence/prompts/generic_relationship_selection_prompt.py +7 -1
- django_spire/contrib/seeding/intelligence/prompts/hierarchical_selection_prompt.py +6 -2
- django_spire/contrib/seeding/intelligence/prompts/model_field_choices_prompt.py +8 -2
- django_spire/contrib/seeding/intelligence/prompts/negation_prompt.py +2 -0
- django_spire/contrib/seeding/intelligence/prompts/objective_prompt.py +6 -1
- django_spire/contrib/seeding/management/commands/seeding.py +9 -3
- django_spire/contrib/seeding/management/example.py +2 -0
- django_spire/contrib/seeding/model/base.py +16 -7
- django_spire/contrib/seeding/model/config.py +31 -15
- django_spire/contrib/seeding/model/django/config.py +13 -13
- django_spire/contrib/seeding/model/django/seeder.py +4 -4
- django_spire/contrib/seeding/model/django/tests/test_seeder.py +34 -23
- django_spire/contrib/seeding/model/enums.py +2 -0
- django_spire/contrib/seeding/tests/test_config.py +71 -0
- django_spire/contrib/seeding/tests/test_custom.py +35 -0
- django_spire/contrib/seeding/tests/test_enums.py +40 -0
- django_spire/contrib/seeding/tests/test_intel.py +32 -0
- django_spire/contrib/seeding/tests/test_override.py +63 -0
- django_spire/contrib/service/__init__.py +2 -2
- django_spire/contrib/service/django_model_service.py +16 -15
- django_spire/contrib/service/exceptions.py +5 -3
- django_spire/contrib/service/tests/__init__.py +0 -0
- django_spire/contrib/service/tests/test_service.py +153 -0
- django_spire/contrib/session/apps.py +2 -0
- django_spire/contrib/session/controller.py +48 -42
- django_spire/contrib/session/templatetags/session_tags.py +11 -2
- django_spire/contrib/session/tests/test_session_controller.py +117 -53
- django_spire/contrib/tests/__init__.py +0 -0
- django_spire/contrib/tests/test_utils.py +37 -0
- django_spire/contrib/utils.py +4 -1
- django_spire/core/apps.py +2 -0
- django_spire/core/converters/tests/test_to_data.py +353 -0
- django_spire/core/converters/tests/test_to_enums.py +61 -41
- django_spire/core/converters/tests/test_to_pydantic.py +138 -109
- django_spire/core/converters/to_data.py +29 -10
- django_spire/core/converters/to_enums.py +4 -2
- django_spire/core/converters/to_pydantic.py +22 -22
- django_spire/core/decorators.py +19 -6
- django_spire/core/forms/widgets.py +4 -0
- django_spire/core/maps.py +3 -1
- django_spire/core/middleware/maintenance.py +3 -3
- django_spire/core/middleware.py +8 -6
- django_spire/core/redirect/__init__.py +5 -0
- django_spire/core/redirect/generic_redirect.py +1 -2
- django_spire/core/redirect/tests/__init__.py +0 -0
- django_spire/core/redirect/tests/test_generic_redirect.py +34 -0
- django_spire/core/{tests/tests_redirect.py → redirect/tests/test_safe_redirect.py} +55 -81
- django_spire/core/shortcuts.py +3 -3
- django_spire/core/static/django_spire/css/app-layout.css +1 -1
- django_spire/core/static/django_spire/css/app-navigation.css +3 -3
- django_spire/core/static/django_spire/css/bootstrap-override.css +4 -0
- django_spire/core/tag/admin.py +12 -0
- django_spire/core/tag/intelligence/tag_set_bot.py +2 -0
- django_spire/core/tag/mixins.py +2 -0
- django_spire/core/tag/models.py +2 -0
- django_spire/core/tag/querysets.py +2 -0
- django_spire/core/tag/service/tag_service.py +6 -3
- django_spire/core/tag/tests/test_intelligence.py +9 -9
- django_spire/core/tag/tests/test_tags.py +44 -54
- django_spire/core/tag/tests/test_tools.py +191 -0
- django_spire/core/tag/tools.py +3 -0
- django_spire/core/templates/django_spire/card/card.html +5 -2
- django_spire/core/templates/django_spire/card/title_card.html +9 -4
- django_spire/core/templates/django_spire/infinite_scroll/base.html +1 -0
- django_spire/core/templates/django_spire/navigation/side_navigation.html +19 -24
- django_spire/core/templates/django_spire/page/full_page.html +46 -16
- django_spire/core/templates/django_spire/table/base.html +4 -2
- django_spire/core/templatetags/json.py +6 -2
- django_spire/core/templatetags/message.py +13 -8
- django_spire/core/templatetags/string_formating.py +8 -5
- django_spire/core/templatetags/tests/__init__.py +0 -0
- django_spire/core/templatetags/tests/test_templatetags.py +427 -0
- django_spire/core/templatetags/variable_types.py +17 -9
- django_spire/core/tests/test_cases.py +1 -1
- django_spire/core/tests/test_conf.py +43 -0
- django_spire/core/tests/test_consts.py +28 -0
- django_spire/core/tests/test_context_processors.py +93 -0
- django_spire/core/tests/test_decorators.py +95 -0
- django_spire/core/tests/test_django_spire_utils.py +56 -0
- django_spire/core/tests/test_exceptions.py +37 -0
- django_spire/core/tests/test_models.py +54 -0
- django_spire/core/tests/test_settings.py +45 -0
- django_spire/core/tests/test_shortcuts.py +74 -0
- django_spire/core/tests/test_urls.py +16 -0
- django_spire/core/tests/test_utils.py +58 -0
- django_spire/core/urls.py +4 -1
- django_spire/core/utils.py +12 -8
- django_spire/exceptions.py +16 -1
- django_spire/file/admin.py +4 -2
- django_spire/file/apps.py +8 -10
- django_spire/file/fields.py +7 -7
- django_spire/file/forms.py +1 -1
- django_spire/file/interfaces.py +15 -15
- django_spire/file/mixins.py +1 -4
- django_spire/file/models.py +3 -5
- django_spire/file/tests/factories.py +59 -0
- django_spire/file/tests/test_admin.py +69 -0
- django_spire/file/tests/test_apps.py +24 -0
- django_spire/file/tests/test_fields.py +114 -0
- django_spire/file/tests/test_forms.py +20 -0
- django_spire/file/tests/test_interfaces.py +183 -0
- django_spire/file/tests/test_models.py +82 -0
- django_spire/file/tests/test_querysets.py +102 -0
- django_spire/file/tests/test_utils.py +32 -0
- django_spire/file/tests/test_views.py +145 -0
- django_spire/file/tests/test_widgets.py +82 -0
- django_spire/file/tools.py +8 -2
- django_spire/file/views.py +7 -3
- django_spire/file/widgets.py +12 -12
- django_spire/help_desk/admin.py +15 -0
- django_spire/help_desk/apps.py +2 -0
- django_spire/help_desk/auth/controller.py +2 -0
- django_spire/help_desk/choices.py +2 -0
- django_spire/help_desk/enums.py +2 -0
- django_spire/help_desk/exceptions.py +31 -3
- django_spire/help_desk/forms.py +2 -0
- django_spire/help_desk/models.py +2 -0
- django_spire/help_desk/querysets.py +4 -1
- django_spire/help_desk/services/notification_service.py +26 -27
- django_spire/help_desk/services/service.py +2 -3
- django_spire/help_desk/tests/factories.py +8 -3
- django_spire/help_desk/tests/test_admin.py +41 -0
- django_spire/help_desk/tests/test_apps.py +41 -0
- django_spire/help_desk/tests/test_choices.py +50 -0
- django_spire/help_desk/tests/test_controller.py +87 -0
- django_spire/help_desk/tests/test_enums.py +18 -0
- django_spire/help_desk/tests/test_exceptions.py +37 -0
- django_spire/help_desk/tests/test_forms.py +89 -0
- django_spire/help_desk/tests/test_models.py +59 -0
- django_spire/help_desk/tests/test_querysets.py +38 -0
- django_spire/help_desk/tests/test_services/test_notification_service.py +15 -8
- django_spire/help_desk/tests/test_services/test_service.py +92 -0
- django_spire/help_desk/tests/test_urls/test_form_urls.py +6 -6
- django_spire/help_desk/tests/test_urls/test_page_urls.py +8 -9
- django_spire/help_desk/tests/test_views/test_form_views.py +46 -19
- django_spire/help_desk/tests/test_views/test_page_views.py +32 -9
- django_spire/help_desk/urls/__init__.py +4 -1
- django_spire/help_desk/urls/form_urls.py +3 -0
- django_spire/help_desk/urls/page_urls.py +3 -0
- django_spire/help_desk/views/form_views.py +13 -5
- django_spire/help_desk/views/page_views.py +11 -3
- django_spire/history/activity/admin.py +2 -0
- django_spire/history/activity/apps.py +3 -1
- django_spire/history/activity/mixins.py +13 -7
- django_spire/history/activity/models.py +6 -5
- django_spire/history/activity/querysets.py +2 -0
- django_spire/history/activity/tests/__init__.py +0 -0
- django_spire/history/activity/tests/test_activity.py +176 -0
- django_spire/history/admin.py +9 -2
- django_spire/history/choices.py +3 -0
- django_spire/history/models.py +5 -5
- django_spire/history/tests/test_admin.py +93 -0
- django_spire/history/tests/test_history.py +101 -0
- django_spire/history/tests/test_mixins.py +84 -0
- django_spire/history/viewed/admin.py +3 -1
- django_spire/history/viewed/apps.py +3 -1
- django_spire/history/viewed/models.py +2 -0
- django_spire/history/viewed/tests/__init__.py +0 -0
- django_spire/history/viewed/tests/test_viewed.py +46 -0
- django_spire/knowledge/auth/tests/__init__.py +0 -0
- django_spire/knowledge/auth/tests/test_controller.py +116 -0
- django_spire/knowledge/collection/admin.py +5 -1
- django_spire/knowledge/collection/models.py +3 -1
- django_spire/knowledge/collection/seeding/seed.py +1 -0
- django_spire/knowledge/collection/services/factory_service.py +10 -11
- django_spire/knowledge/collection/services/ordering_service.py +1 -2
- django_spire/knowledge/collection/services/service.py +5 -10
- django_spire/knowledge/collection/services/tag_service.py +5 -2
- django_spire/knowledge/collection/tests/factories.py +28 -1
- django_spire/knowledge/collection/tests/test_models.py +48 -0
- django_spire/knowledge/collection/tests/test_querysets.py +93 -0
- django_spire/knowledge/collection/tests/test_services/test_factory_service.py +100 -0
- django_spire/knowledge/collection/tests/test_services/test_services.py +160 -0
- django_spire/knowledge/collection/tests/test_urls/test_form_urls.py +21 -3
- django_spire/knowledge/collection/tests/test_urls/test_json_urls.py +39 -1
- django_spire/knowledge/collection/tests/test_urls/test_page_urls.py +12 -4
- django_spire/knowledge/collection/urls/__init__.py +3 -0
- django_spire/knowledge/collection/urls/form_urls.py +2 -0
- django_spire/knowledge/collection/urls/json_urls.py +2 -0
- django_spire/knowledge/collection/urls/page_urls.py +2 -0
- django_spire/knowledge/collection/views/form_views.py +4 -4
- django_spire/knowledge/collection/views/json_views.py +5 -1
- django_spire/knowledge/collection/views/page_views.py +5 -2
- django_spire/knowledge/entry/admin.py +7 -1
- django_spire/knowledge/entry/forms.py +2 -0
- django_spire/knowledge/entry/models.py +2 -0
- django_spire/knowledge/entry/seeding/seed.py +3 -0
- django_spire/knowledge/entry/services/automation_service.py +5 -4
- django_spire/knowledge/entry/services/factory_service.py +7 -5
- django_spire/knowledge/entry/services/service.py +4 -7
- django_spire/knowledge/entry/services/tag_service.py +0 -1
- django_spire/knowledge/entry/services/tool_service.py +1 -0
- django_spire/knowledge/entry/services/transformation_services.py +1 -5
- django_spire/knowledge/entry/tests/factories.py +1 -2
- django_spire/knowledge/entry/tests/test_factory_service.py +20 -0
- django_spire/knowledge/entry/tests/test_models.py +41 -0
- django_spire/knowledge/entry/tests/test_querysets.py +71 -0
- django_spire/knowledge/entry/tests/test_services.py +94 -0
- django_spire/knowledge/entry/tests/test_urls/test_form_urls.py +9 -14
- django_spire/knowledge/entry/tests/test_urls/test_json_urls.py +48 -5
- django_spire/knowledge/entry/tests/test_urls/test_page_urls.py +6 -8
- django_spire/knowledge/entry/tests/test_urls/test_template_urls.py +40 -0
- django_spire/knowledge/entry/urls/form_urls.py +2 -0
- django_spire/knowledge/entry/urls/json_urls.py +2 -0
- django_spire/knowledge/entry/urls/page_urls.py +2 -0
- django_spire/knowledge/entry/urls/template_urls.py +2 -0
- django_spire/knowledge/entry/version/block/choices.py +2 -0
- django_spire/knowledge/entry/version/block/data/data.py +1 -0
- django_spire/knowledge/entry/version/block/data/list/data.py +8 -13
- django_spire/knowledge/entry/version/block/data/list/maps.py +3 -0
- django_spire/knowledge/entry/version/block/data/list/meta.py +1 -2
- django_spire/knowledge/entry/version/block/data/list/tests/__init__.py +0 -0
- django_spire/knowledge/entry/version/block/data/list/tests/test_maps.py +32 -0
- django_spire/knowledge/entry/version/block/data/list/tests/test_meta.py +58 -0
- django_spire/knowledge/entry/version/block/data/maps.py +3 -6
- django_spire/knowledge/entry/version/block/models.py +7 -5
- django_spire/knowledge/entry/version/block/seeding/constants.py +5 -4
- django_spire/knowledge/entry/version/block/services/service.py +2 -3
- django_spire/knowledge/entry/version/block/tests/factories.py +4 -10
- django_spire/knowledge/entry/version/block/tests/test_choices.py +56 -0
- django_spire/knowledge/entry/version/block/tests/test_data.py +90 -0
- django_spire/knowledge/entry/version/block/tests/test_maps.py +37 -0
- django_spire/knowledge/entry/version/block/tests/test_models.py +55 -0
- django_spire/knowledge/entry/version/block/tests/test_querysets.py +35 -0
- django_spire/knowledge/entry/version/block/tests/test_services.py +65 -0
- django_spire/knowledge/entry/version/choices.py +2 -0
- django_spire/knowledge/entry/version/converters/converter.py +1 -1
- django_spire/knowledge/entry/version/converters/docx_converter.py +4 -7
- django_spire/knowledge/entry/version/converters/markdown_converter.py +20 -20
- django_spire/knowledge/entry/version/maps.py +4 -5
- django_spire/knowledge/entry/version/querysets.py +1 -1
- django_spire/knowledge/entry/version/seeding/seeder.py +1 -2
- django_spire/knowledge/entry/version/services/processor_service.py +5 -4
- django_spire/knowledge/entry/version/services/service.py +1 -2
- django_spire/knowledge/entry/version/tests/factories.py +2 -2
- django_spire/knowledge/entry/version/tests/test_choices.py +18 -0
- django_spire/knowledge/entry/version/tests/test_converters/test_docx_converter.py +56 -8
- django_spire/knowledge/entry/version/tests/test_converters/test_markdown_converter.py +78 -0
- django_spire/knowledge/entry/version/tests/test_maps.py +58 -0
- django_spire/knowledge/entry/version/tests/test_models.py +23 -0
- django_spire/knowledge/entry/version/tests/test_querysets.py +26 -0
- django_spire/knowledge/entry/version/tests/test_services.py +62 -0
- django_spire/knowledge/entry/version/tests/test_urls/test_json_urls.py +27 -8
- django_spire/knowledge/entry/version/tests/test_urls/test_page_urls.py +15 -8
- django_spire/knowledge/entry/version/tests/test_urls/test_redirect_urls.py +38 -0
- django_spire/knowledge/entry/version/urls/__init__.py +3 -0
- django_spire/knowledge/entry/version/urls/json_urls.py +2 -1
- django_spire/knowledge/entry/version/urls/page_urls.py +2 -0
- django_spire/knowledge/entry/version/urls/redirect_urls.py +2 -0
- django_spire/knowledge/entry/version/views/json_views.py +5 -1
- django_spire/knowledge/entry/version/views/page_views.py +10 -3
- django_spire/knowledge/entry/version/views/redirect_views.py +5 -1
- django_spire/knowledge/entry/views/form_views.py +16 -8
- django_spire/knowledge/entry/views/json_views.py +3 -1
- django_spire/knowledge/entry/views/page_views.py +8 -2
- django_spire/knowledge/entry/views/template_views.py +7 -1
- django_spire/knowledge/exceptions.py +2 -1
- django_spire/knowledge/intelligence/intel/answer_intel.py +2 -1
- django_spire/knowledge/intelligence/intel/entry_intel.py +0 -1
- django_spire/knowledge/intelligence/workflows/knowledge_workflow.py +4 -5
- django_spire/knowledge/models.py +1 -2
- django_spire/knowledge/tests/__init__.py +0 -0
- django_spire/knowledge/tests/test_templatetags.py +40 -0
- django_spire/knowledge/tests/test_urls/__init__.py +0 -0
- django_spire/knowledge/tests/test_urls/test_page_urls.py +24 -0
- django_spire/knowledge/urls/__init__.py +2 -0
- django_spire/knowledge/urls/page_urls.py +2 -0
- django_spire/knowledge/views/page_views.py +8 -3
- django_spire/notification/admin.py +3 -1
- django_spire/notification/app/admin.py +2 -0
- django_spire/notification/app/apps.py +3 -1
- django_spire/notification/app/exceptions.py +9 -2
- django_spire/notification/app/models.py +8 -4
- django_spire/notification/app/processor.py +22 -26
- django_spire/notification/app/querysets.py +2 -0
- django_spire/notification/app/tests/__init__.py +0 -0
- django_spire/notification/app/tests/factories.py +34 -0
- django_spire/notification/app/tests/test_apps.py +24 -0
- django_spire/notification/app/tests/test_models.py +72 -0
- django_spire/notification/app/tests/test_processor.py +111 -0
- django_spire/notification/app/tests/test_querysets.py +90 -0
- django_spire/notification/app/tests/test_views/__init__.py +0 -0
- django_spire/notification/app/tests/test_views/test_json_views.py +48 -0
- django_spire/notification/app/tests/test_views/test_page_views.py +19 -0
- django_spire/notification/app/urls/__init__.py +3 -1
- django_spire/notification/app/urls/json_urls.py +6 -4
- django_spire/notification/app/urls/page_urls.py +4 -3
- django_spire/notification/app/urls/template_urls.py +4 -2
- django_spire/notification/apps.py +4 -1
- django_spire/notification/email/admin.py +5 -1
- django_spire/notification/email/apps.py +3 -1
- django_spire/notification/email/exceptions.py +4 -2
- django_spire/notification/email/helper.py +5 -3
- django_spire/notification/email/models.py +4 -0
- django_spire/notification/email/processor.py +19 -15
- django_spire/notification/email/querysets.py +3 -0
- django_spire/notification/email/tests/__init__.py +0 -0
- django_spire/notification/email/tests/factories.py +35 -0
- django_spire/notification/email/tests/test_apps.py +24 -0
- django_spire/notification/email/tests/test_models.py +52 -0
- django_spire/notification/email/tests/test_processor.py +92 -0
- django_spire/notification/email/tests/test_querysets.py +43 -0
- django_spire/notification/exceptions.py +17 -2
- django_spire/notification/managers.py +7 -1
- django_spire/notification/maps.py +4 -1
- django_spire/notification/mixins.py +2 -0
- django_spire/notification/models.py +3 -1
- django_spire/notification/processors/notification.py +12 -5
- django_spire/notification/processors/processor.py +2 -0
- django_spire/notification/processors/tests/__init__.py +0 -0
- django_spire/notification/processors/tests/test_notification.py +106 -0
- django_spire/notification/push/admin.py +10 -1
- django_spire/notification/push/apps.py +3 -1
- django_spire/notification/push/models.py +2 -3
- django_spire/notification/push/tests/__init__.py +0 -0
- django_spire/notification/push/tests/test_apps.py +24 -0
- django_spire/notification/push/tests/test_models.py +28 -0
- django_spire/notification/querysets.py +7 -1
- django_spire/notification/sms/admin.py +2 -0
- django_spire/notification/sms/apps.py +4 -1
- django_spire/notification/sms/automations.py +2 -0
- django_spire/notification/sms/choices.py +2 -0
- django_spire/notification/sms/exceptions.py +19 -5
- django_spire/notification/sms/helper.py +33 -23
- django_spire/notification/sms/models.py +5 -1
- django_spire/notification/sms/processor.py +20 -20
- django_spire/notification/sms/querysets.py +2 -0
- django_spire/notification/sms/tests/factories.py +33 -0
- django_spire/notification/sms/tests/test_apps.py +24 -0
- django_spire/notification/sms/tests/test_automation.py +38 -0
- django_spire/notification/sms/tests/test_choices.py +15 -0
- django_spire/notification/sms/tests/test_consts.py +17 -0
- django_spire/notification/sms/tests/test_exceptions.py +27 -0
- django_spire/notification/sms/tests/test_helper.py +50 -0
- django_spire/notification/sms/tests/test_models.py +81 -0
- django_spire/notification/sms/tests/test_processor.py +107 -0
- django_spire/notification/sms/tests/test_tools.py +25 -11
- django_spire/notification/sms/tools.py +16 -5
- django_spire/notification/sms/urls/__init__.py +3 -1
- django_spire/notification/sms/urls/media_urls.py +2 -0
- django_spire/notification/sms/views/media_views.py +14 -4
- django_spire/notification/tests/__init__.py +0 -0
- django_spire/notification/tests/factories.py +26 -0
- django_spire/notification/tests/test_admin.py +55 -0
- django_spire/notification/tests/test_apps.py +30 -0
- django_spire/notification/tests/test_automation.py +18 -0
- django_spire/notification/tests/test_choices.py +59 -0
- django_spire/notification/tests/test_exceptions.py +58 -0
- django_spire/notification/tests/test_managers.py +100 -0
- django_spire/notification/tests/test_maps.py +31 -0
- django_spire/notification/tests/test_models.py +76 -0
- django_spire/notification/tests/test_querysets.py +184 -0
- django_spire/notification/tests/test_utils.py +23 -0
- django_spire/notification/urls.py +3 -1
- django_spire/notification/utils.py +3 -1
- django_spire/settings.py +3 -0
- django_spire/theme/tests/test_context_processor.py +15 -13
- django_spire/theme/tests/test_enums.py +2 -2
- django_spire/theme/tests/test_filesystem.py +2 -5
- django_spire/theme/tests/test_integration.py +12 -12
- django_spire/theme/tests/test_model.py +40 -38
- django_spire/theme/tests/test_views/test_json_views.py +33 -33
- django_spire/theme/urls/json_urls.py +3 -0
- django_spire/theme/urls/page_urls.py +3 -0
- django_spire/urls.py +19 -15
- django_spire/utils.py +13 -4
- {django_spire-0.23.7.dist-info → django_spire-0.23.8.dist-info}/METADATA +1 -1
- {django_spire-0.23.7.dist-info → django_spire-0.23.8.dist-info}/RECORD +530 -358
- django_spire/contrib/options/tests/test_unit.py +0 -148
- django_spire/contrib/seeding/tests/test_seeding.py +0 -25
- django_spire/core/tests/test_templatetags.py +0 -117
- django_spire/core/tests/tests_shortcuts.py +0 -73
- django_spire/history/activity/tests.py +0 -3
- django_spire/history/activity/views.py +0 -3
- django_spire/knowledge/collection/tests/test_services/test_transformation_service.py +0 -71
- django_spire/notification/app/tests.py +0 -3
- {django_spire-0.23.7.dist-info → django_spire-0.23.8.dist-info}/WHEEL +0 -0
- {django_spire-0.23.7.dist-info → django_spire-0.23.8.dist-info}/licenses/LICENSE.md +0 -0
- {django_spire-0.23.7.dist-info → django_spire-0.23.8.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import TypedDict
|
|
3
|
+
from typing import Any, TypedDict
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class BreadcrumbDict(TypedDict):
|
|
@@ -9,7 +9,7 @@ class BreadcrumbDict(TypedDict):
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class BreadcrumbItem:
|
|
12
|
-
def __init__(self, name: str, href: str | None = None):
|
|
12
|
+
def __init__(self, name: str, href: str | None = None) -> None:
|
|
13
13
|
self.name = name
|
|
14
14
|
self.href = href
|
|
15
15
|
|
|
@@ -18,21 +18,18 @@ class BreadcrumbItem:
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
class Breadcrumbs:
|
|
21
|
-
def __init__(self
|
|
21
|
+
def __init__(self) -> None:
|
|
22
22
|
self.data: list[BreadcrumbItem] = []
|
|
23
23
|
self.index: int = 0
|
|
24
24
|
|
|
25
|
-
def __add__(self, other):
|
|
25
|
+
def __add__(self, other: Breadcrumbs) -> Breadcrumbs:
|
|
26
26
|
self.data += other.data
|
|
27
27
|
return self
|
|
28
28
|
|
|
29
|
-
def __iter__(self):
|
|
29
|
+
def __iter__(self) -> Breadcrumbs:
|
|
30
30
|
self.index = 0
|
|
31
31
|
return self
|
|
32
32
|
|
|
33
|
-
def __str__(self) -> str:
|
|
34
|
-
return str(self.data)
|
|
35
|
-
|
|
36
33
|
def __len__(self) -> int:
|
|
37
34
|
return len(self.data)
|
|
38
35
|
|
|
@@ -44,7 +41,10 @@ class Breadcrumbs:
|
|
|
44
41
|
self.index += 1
|
|
45
42
|
return breadcrumb_item.to_dict()
|
|
46
43
|
|
|
47
|
-
def
|
|
44
|
+
def __str__(self) -> str:
|
|
45
|
+
return str(self.data)
|
|
46
|
+
|
|
47
|
+
def add_base_breadcrumb(self, model: Any) -> None:
|
|
48
48
|
if hasattr(model, 'base_breadcrumb'):
|
|
49
49
|
self += model.base_breadcrumb()
|
|
50
50
|
|
|
@@ -52,13 +52,7 @@ class Breadcrumbs:
|
|
|
52
52
|
breadcrumb_item = BreadcrumbItem(name=name, href=href)
|
|
53
53
|
self.data.append(breadcrumb_item)
|
|
54
54
|
|
|
55
|
-
def
|
|
56
|
-
# Expects a breadcrumb object to be returned from the object
|
|
57
|
-
if hasattr(obj, 'breadcrumbs'):
|
|
58
|
-
object_breadcrumbs = obj.breadcrumbs()
|
|
59
|
-
self.data += object_breadcrumbs.data
|
|
60
|
-
|
|
61
|
-
def add_form_breadcrumbs(self, obj) -> None:
|
|
55
|
+
def add_form_breadcrumbs(self, obj: Any) -> None:
|
|
62
56
|
self.add_obj_breadcrumbs(obj)
|
|
63
57
|
|
|
64
58
|
if obj.pk is None:
|
|
@@ -67,10 +61,16 @@ class Breadcrumbs:
|
|
|
67
61
|
else:
|
|
68
62
|
self.add_breadcrumb(name='Edit')
|
|
69
63
|
|
|
70
|
-
def
|
|
64
|
+
def add_obj_breadcrumbs(self, obj: Any) -> None:
|
|
65
|
+
# It expects a breadcrumb object to be returned from the object
|
|
66
|
+
if hasattr(obj, 'breadcrumbs'):
|
|
67
|
+
object_breadcrumbs = obj.breadcrumbs()
|
|
68
|
+
self.data += object_breadcrumbs.data
|
|
69
|
+
|
|
70
|
+
def remove(self, index: int) -> Breadcrumbs:
|
|
71
71
|
del self.data[index]
|
|
72
72
|
return self
|
|
73
73
|
|
|
74
|
-
def reverse(self):
|
|
74
|
+
def reverse(self) -> Breadcrumbs:
|
|
75
75
|
self.data.reverse()
|
|
76
76
|
return self
|
|
@@ -0,0 +1,198 @@
|
|
|
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.breadcrumb.breadcrumbs import BreadcrumbItem, Breadcrumbs
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestBreadcrumbItem(TestCase):
|
|
11
|
+
def test_create_with_href(self) -> None:
|
|
12
|
+
item = BreadcrumbItem(name='Home', href='/home/')
|
|
13
|
+
|
|
14
|
+
assert item.name == 'Home'
|
|
15
|
+
assert item.href == '/home/'
|
|
16
|
+
|
|
17
|
+
def test_create_without_href(self) -> None:
|
|
18
|
+
item = BreadcrumbItem(name='Current Page')
|
|
19
|
+
|
|
20
|
+
assert item.name == 'Current Page'
|
|
21
|
+
assert item.href is None
|
|
22
|
+
|
|
23
|
+
def test_to_dict(self) -> None:
|
|
24
|
+
item = BreadcrumbItem(name='Home', href='/home/')
|
|
25
|
+
result = item.to_dict()
|
|
26
|
+
|
|
27
|
+
assert result == {'name': 'Home', 'href': '/home/'}
|
|
28
|
+
|
|
29
|
+
def test_to_dict_without_href(self) -> None:
|
|
30
|
+
item = BreadcrumbItem(name='Current Page')
|
|
31
|
+
result = item.to_dict()
|
|
32
|
+
|
|
33
|
+
assert result == {'name': 'Current Page', 'href': None}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TestBreadcrumbs(TestCase):
|
|
37
|
+
def test_add_base_breadcrumb(self) -> None:
|
|
38
|
+
breadcrumbs = Breadcrumbs()
|
|
39
|
+
|
|
40
|
+
model = MagicMock()
|
|
41
|
+
model_breadcrumbs = Breadcrumbs()
|
|
42
|
+
model_breadcrumbs.add_breadcrumb('Base', '/base/')
|
|
43
|
+
model.base_breadcrumb.return_value = model_breadcrumbs
|
|
44
|
+
|
|
45
|
+
breadcrumbs.add_base_breadcrumb(model)
|
|
46
|
+
|
|
47
|
+
assert len(breadcrumbs) == 1
|
|
48
|
+
|
|
49
|
+
def test_add_base_breadcrumb_no_method(self) -> None:
|
|
50
|
+
breadcrumbs = Breadcrumbs()
|
|
51
|
+
|
|
52
|
+
model = MagicMock(spec=[])
|
|
53
|
+
|
|
54
|
+
breadcrumbs.add_base_breadcrumb(model)
|
|
55
|
+
|
|
56
|
+
assert len(breadcrumbs) == 0
|
|
57
|
+
|
|
58
|
+
def test_add_breadcrumb(self) -> None:
|
|
59
|
+
breadcrumbs = Breadcrumbs()
|
|
60
|
+
breadcrumbs.add_breadcrumb('Home', '/home/')
|
|
61
|
+
|
|
62
|
+
assert len(breadcrumbs) == 1
|
|
63
|
+
assert breadcrumbs.data[0].name == 'Home'
|
|
64
|
+
assert breadcrumbs.data[0].href == '/home/'
|
|
65
|
+
|
|
66
|
+
def test_add_breadcrumb_without_href(self) -> None:
|
|
67
|
+
breadcrumbs = Breadcrumbs()
|
|
68
|
+
breadcrumbs.add_breadcrumb('Current Page')
|
|
69
|
+
|
|
70
|
+
assert len(breadcrumbs) == 1
|
|
71
|
+
assert breadcrumbs.data[0].name == 'Current Page'
|
|
72
|
+
assert breadcrumbs.data[0].href is None
|
|
73
|
+
|
|
74
|
+
def test_add_form_breadcrumbs_create(self) -> None:
|
|
75
|
+
breadcrumbs = Breadcrumbs()
|
|
76
|
+
|
|
77
|
+
obj = MagicMock()
|
|
78
|
+
obj.pk = None
|
|
79
|
+
obj._meta.model._meta.verbose_name = 'Test Model'
|
|
80
|
+
del obj.breadcrumbs
|
|
81
|
+
|
|
82
|
+
breadcrumbs.add_form_breadcrumbs(obj)
|
|
83
|
+
|
|
84
|
+
assert len(breadcrumbs) == 2
|
|
85
|
+
assert breadcrumbs.data[0].name == 'Test Model'
|
|
86
|
+
assert breadcrumbs.data[1].name == 'Create'
|
|
87
|
+
|
|
88
|
+
def test_add_form_breadcrumbs_edit(self) -> None:
|
|
89
|
+
breadcrumbs = Breadcrumbs()
|
|
90
|
+
|
|
91
|
+
obj = MagicMock()
|
|
92
|
+
obj.pk = 1
|
|
93
|
+
del obj.breadcrumbs
|
|
94
|
+
|
|
95
|
+
breadcrumbs.add_form_breadcrumbs(obj)
|
|
96
|
+
|
|
97
|
+
assert len(breadcrumbs) == 1
|
|
98
|
+
assert breadcrumbs.data[0].name == 'Edit'
|
|
99
|
+
|
|
100
|
+
def test_add_obj_breadcrumbs(self) -> None:
|
|
101
|
+
breadcrumbs = Breadcrumbs()
|
|
102
|
+
|
|
103
|
+
obj_breadcrumbs = Breadcrumbs()
|
|
104
|
+
obj_breadcrumbs.add_breadcrumb('Object', '/object/')
|
|
105
|
+
|
|
106
|
+
obj = MagicMock()
|
|
107
|
+
obj.breadcrumbs.return_value = obj_breadcrumbs
|
|
108
|
+
|
|
109
|
+
breadcrumbs.add_obj_breadcrumbs(obj)
|
|
110
|
+
|
|
111
|
+
assert len(breadcrumbs) == 1
|
|
112
|
+
assert breadcrumbs.data[0].name == 'Object'
|
|
113
|
+
|
|
114
|
+
def test_add_obj_breadcrumbs_no_method(self) -> None:
|
|
115
|
+
breadcrumbs = Breadcrumbs()
|
|
116
|
+
|
|
117
|
+
obj = MagicMock(spec=[])
|
|
118
|
+
|
|
119
|
+
breadcrumbs.add_obj_breadcrumbs(obj)
|
|
120
|
+
|
|
121
|
+
assert len(breadcrumbs) == 0
|
|
122
|
+
|
|
123
|
+
def test_add_operator(self) -> None:
|
|
124
|
+
breadcrumbs1 = Breadcrumbs()
|
|
125
|
+
breadcrumbs1.add_breadcrumb('Home', '/home/')
|
|
126
|
+
|
|
127
|
+
breadcrumbs2 = Breadcrumbs()
|
|
128
|
+
breadcrumbs2.add_breadcrumb('Page', '/page/')
|
|
129
|
+
|
|
130
|
+
result = breadcrumbs1 + breadcrumbs2
|
|
131
|
+
|
|
132
|
+
assert len(result) == 2
|
|
133
|
+
assert result.data[0].name == 'Home'
|
|
134
|
+
assert result.data[1].name == 'Page'
|
|
135
|
+
|
|
136
|
+
def test_empty_breadcrumbs(self) -> None:
|
|
137
|
+
breadcrumbs = Breadcrumbs()
|
|
138
|
+
|
|
139
|
+
assert len(breadcrumbs) == 0
|
|
140
|
+
|
|
141
|
+
def test_iteration(self) -> None:
|
|
142
|
+
breadcrumbs = Breadcrumbs()
|
|
143
|
+
breadcrumbs.add_breadcrumb('Home', '/home/')
|
|
144
|
+
breadcrumbs.add_breadcrumb('Page', '/page/')
|
|
145
|
+
|
|
146
|
+
items = list(breadcrumbs)
|
|
147
|
+
|
|
148
|
+
assert len(items) == 2
|
|
149
|
+
assert items[0] == {'name': 'Home', 'href': '/home/'}
|
|
150
|
+
assert items[1] == {'name': 'Page', 'href': '/page/'}
|
|
151
|
+
|
|
152
|
+
def test_iteration_empty(self) -> None:
|
|
153
|
+
breadcrumbs = Breadcrumbs()
|
|
154
|
+
|
|
155
|
+
items = list(breadcrumbs)
|
|
156
|
+
|
|
157
|
+
assert items == []
|
|
158
|
+
|
|
159
|
+
def test_len(self) -> None:
|
|
160
|
+
breadcrumbs = Breadcrumbs()
|
|
161
|
+
breadcrumbs.add_breadcrumb('Home', '/home/')
|
|
162
|
+
breadcrumbs.add_breadcrumb('Page', '/page/')
|
|
163
|
+
|
|
164
|
+
assert len(breadcrumbs) == 2
|
|
165
|
+
|
|
166
|
+
def test_remove(self) -> None:
|
|
167
|
+
breadcrumbs = Breadcrumbs()
|
|
168
|
+
breadcrumbs.add_breadcrumb('Home', '/home/')
|
|
169
|
+
breadcrumbs.add_breadcrumb('Page', '/page/')
|
|
170
|
+
breadcrumbs.add_breadcrumb('Current', None)
|
|
171
|
+
|
|
172
|
+
result = breadcrumbs.remove(1)
|
|
173
|
+
|
|
174
|
+
assert len(breadcrumbs) == 2
|
|
175
|
+
assert breadcrumbs.data[0].name == 'Home'
|
|
176
|
+
assert breadcrumbs.data[1].name == 'Current'
|
|
177
|
+
assert result is breadcrumbs
|
|
178
|
+
|
|
179
|
+
def test_reverse(self) -> None:
|
|
180
|
+
breadcrumbs = Breadcrumbs()
|
|
181
|
+
breadcrumbs.add_breadcrumb('First', '/first/')
|
|
182
|
+
breadcrumbs.add_breadcrumb('Second', '/second/')
|
|
183
|
+
breadcrumbs.add_breadcrumb('Third', '/third/')
|
|
184
|
+
|
|
185
|
+
result = breadcrumbs.reverse()
|
|
186
|
+
|
|
187
|
+
assert breadcrumbs.data[0].name == 'Third'
|
|
188
|
+
assert breadcrumbs.data[1].name == 'Second'
|
|
189
|
+
assert breadcrumbs.data[2].name == 'First'
|
|
190
|
+
assert result is breadcrumbs
|
|
191
|
+
|
|
192
|
+
def test_str(self) -> None:
|
|
193
|
+
breadcrumbs = Breadcrumbs()
|
|
194
|
+
breadcrumbs.add_breadcrumb('Home', '/home/')
|
|
195
|
+
|
|
196
|
+
result = str(breadcrumbs)
|
|
197
|
+
|
|
198
|
+
assert 'BreadcrumbItem' in result or '[' in result
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
from django_spire.contrib.constructor.exceptions import
|
|
1
|
+
from django_spire.contrib.constructor.exceptions import ConstructorError
|
|
2
2
|
from django_spire.contrib.constructor.django_model_constructor import BaseDjangoModelConstructor
|
|
3
3
|
from django_spire.contrib.constructor.constructor import BaseConstructor
|
|
4
4
|
|
|
5
5
|
__all__ = [
|
|
6
6
|
'BaseConstructor',
|
|
7
7
|
'BaseDjangoModelConstructor',
|
|
8
|
-
'
|
|
9
|
-
]
|
|
8
|
+
'ConstructorError'
|
|
9
|
+
]
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from abc import ABC
|
|
4
4
|
from typing import Any, Generic, TypeVar
|
|
5
5
|
|
|
6
|
-
from django_spire.contrib.constructor.exceptions import
|
|
6
|
+
from django_spire.contrib.constructor.exceptions import ConstructorError
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
TypeAny = TypeVar('TypeAny', bound=Any, covariant=True)
|
|
@@ -15,7 +15,7 @@ class BaseConstructor(
|
|
|
15
15
|
):
|
|
16
16
|
def __init__(self, obj: Any = None):
|
|
17
17
|
self._obj_type_name: str = str(
|
|
18
|
-
|
|
18
|
+
next(iter(self.__class__.__annotations__.values()))
|
|
19
19
|
).split('.')[-1]
|
|
20
20
|
|
|
21
21
|
if obj is None:
|
|
@@ -23,22 +23,22 @@ class BaseConstructor(
|
|
|
23
23
|
|
|
24
24
|
self._obj_mro_type_names = [cls.__name__ for cls in obj.__class__.__mro__]
|
|
25
25
|
|
|
26
|
-
if
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
)
|
|
26
|
+
if self._obj_type_name not in self._obj_mro_type_names:
|
|
27
|
+
message = f'{self.__class__.__name__} was instantiated with obj type "{obj.__class__.__name__}" and failed as it was expecting "{self._obj_type_name}".'
|
|
28
|
+
raise ConstructorError(message)
|
|
30
29
|
|
|
31
30
|
self._obj_type: type[TypeAny] = obj.__class__
|
|
32
31
|
|
|
33
32
|
if self._obj_type is None or self._obj_type is ...:
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
message = f'{self.__class__.__name__} top class attribute must have an annotated type.'
|
|
34
|
+
raise ConstructorError(message)
|
|
36
35
|
|
|
37
36
|
self.obj: TypeAny = obj
|
|
38
37
|
|
|
39
38
|
if ABC not in self.__class__.__bases__:
|
|
40
39
|
if not self._obj_is_valid:
|
|
41
|
-
|
|
40
|
+
message = f'{self._obj_type_name} failed to validate on {self.__class__.__name__}'
|
|
41
|
+
raise ConstructorError(message)
|
|
42
42
|
|
|
43
43
|
self.__post_init__()
|
|
44
44
|
|
|
@@ -47,8 +47,8 @@ class BaseConstructor(
|
|
|
47
47
|
|
|
48
48
|
if ABC not in cls.__bases__:
|
|
49
49
|
if 'obj' not in cls.__annotations__:
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
message = f'{cls.__name__} must have an "obj" attribute annotated with a type.'
|
|
51
|
+
raise ConstructorError(message)
|
|
52
52
|
|
|
53
53
|
# Typing Does not work properly for services if you override __get__ in the BaseService class.
|
|
54
54
|
# This is a workaround and should be fixed in future versions of the python lsp.
|
|
@@ -63,11 +63,11 @@ class BaseConstructor(
|
|
|
63
63
|
|
|
64
64
|
self._validate_base_service_target_or_error(target)
|
|
65
65
|
|
|
66
|
-
return cls(
|
|
66
|
+
return cls(target.obj)
|
|
67
67
|
|
|
68
68
|
return cls(target)
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
cls.__get__ = __get__
|
|
71
71
|
|
|
72
72
|
def __post_init__(self):
|
|
73
73
|
pass
|
|
@@ -82,5 +82,5 @@ class BaseConstructor(
|
|
|
82
82
|
|
|
83
83
|
def _validate_base_service_target_or_error(self, target: BaseConstructor):
|
|
84
84
|
if self._obj_type_name not in target._obj_mro_type_names:
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
message = f'{target.__class__.__name__} must use the same obj type as {self.__class__.__name__}. {self._obj_type_name} is not in {target._obj_mro_type_names}'
|
|
86
|
+
raise ConstructorError(message)
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from abc import ABC
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import Generic, TypeVar
|
|
5
5
|
|
|
6
6
|
from django.db.models import Model
|
|
7
7
|
|
|
8
8
|
from django_spire.contrib.constructor.constructor import BaseConstructor
|
|
9
9
|
|
|
10
|
+
|
|
10
11
|
TypeDjangoModel = TypeVar('TypeDjangoModel', bound=Model, covariant=True)
|
|
11
12
|
|
|
12
13
|
|
|
@@ -17,11 +18,11 @@ class BaseDjangoModelConstructor(
|
|
|
17
18
|
):
|
|
18
19
|
@property
|
|
19
20
|
def _model_obj_id_is_empty(self) -> bool:
|
|
20
|
-
return self.obj.id is None or self.obj.id
|
|
21
|
+
return self.obj.id is None or self.obj.id in {0, ''}
|
|
21
22
|
|
|
22
23
|
@property
|
|
23
24
|
def _model_obj_pk_is_empty(self) -> bool:
|
|
24
|
-
return self.obj.pk is None or self.obj.pk
|
|
25
|
+
return self.obj.pk is None or self.obj.pk in {0, ''}
|
|
25
26
|
|
|
26
27
|
@property
|
|
27
28
|
def model_obj_is_created(self) -> bool:
|
|
@@ -32,7 +33,7 @@ class BaseDjangoModelConstructor(
|
|
|
32
33
|
return self._model_obj_id_is_empty or self._model_obj_pk_is_empty
|
|
33
34
|
|
|
34
35
|
@property
|
|
35
|
-
def obj_class(self) ->
|
|
36
|
+
def obj_class(self) -> type[TypeDjangoModel]:
|
|
36
37
|
return super().obj_class
|
|
37
38
|
|
|
38
39
|
@property
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
from
|
|
1
|
+
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from django_spire.exceptions import DjangoSpireError
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
class ConstructorError(DjangoSpireError):
|
|
7
|
+
pass
|
|
File without changes
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from abc import ABC
|
|
6
|
+
|
|
7
|
+
from django.contrib.auth.models import User
|
|
8
|
+
from django.test import TestCase
|
|
9
|
+
|
|
10
|
+
from django_spire.contrib.constructor import (
|
|
11
|
+
BaseConstructor,
|
|
12
|
+
BaseDjangoModelConstructor,
|
|
13
|
+
ConstructorError,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TestBaseConstructor(TestCase):
|
|
18
|
+
def test_abc_subclass_does_not_require_obj_annotation(self) -> None:
|
|
19
|
+
class AbstractConstructor(BaseConstructor, ABC):
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
# Should not raise
|
|
23
|
+
assert AbstractConstructor is not None
|
|
24
|
+
|
|
25
|
+
def test_constructor_with_none_obj(self) -> None:
|
|
26
|
+
class StringConstructor(BaseConstructor[str]):
|
|
27
|
+
obj: str
|
|
28
|
+
|
|
29
|
+
constructor = StringConstructor(None)
|
|
30
|
+
|
|
31
|
+
assert not hasattr(constructor, 'obj')
|
|
32
|
+
|
|
33
|
+
def test_constructor_with_valid_obj(self) -> None:
|
|
34
|
+
class StringConstructor(BaseConstructor[str]):
|
|
35
|
+
obj: str
|
|
36
|
+
|
|
37
|
+
constructor = StringConstructor('test')
|
|
38
|
+
|
|
39
|
+
assert constructor.obj == 'test'
|
|
40
|
+
|
|
41
|
+
def test_constructor_with_wrong_type_raises_error(self) -> None:
|
|
42
|
+
class StringConstructor(BaseConstructor[str]):
|
|
43
|
+
obj: str
|
|
44
|
+
|
|
45
|
+
with pytest.raises(ConstructorError):
|
|
46
|
+
StringConstructor(123)
|
|
47
|
+
|
|
48
|
+
def test_obj_class_property(self) -> None:
|
|
49
|
+
class StringConstructor(BaseConstructor[str]):
|
|
50
|
+
obj: str
|
|
51
|
+
|
|
52
|
+
constructor = StringConstructor('test')
|
|
53
|
+
|
|
54
|
+
assert constructor.obj_class is str
|
|
55
|
+
|
|
56
|
+
def test_obj_is_valid_property(self) -> None:
|
|
57
|
+
class StringConstructor(BaseConstructor[str]):
|
|
58
|
+
obj: str
|
|
59
|
+
|
|
60
|
+
constructor = StringConstructor('test')
|
|
61
|
+
|
|
62
|
+
assert constructor._obj_is_valid is True
|
|
63
|
+
|
|
64
|
+
def test_post_init_called(self) -> None:
|
|
65
|
+
class StringConstructor(BaseConstructor[str]):
|
|
66
|
+
obj: str
|
|
67
|
+
post_init_called: bool = False
|
|
68
|
+
|
|
69
|
+
def __post_init__(self):
|
|
70
|
+
self.post_init_called = True
|
|
71
|
+
|
|
72
|
+
constructor = StringConstructor('test')
|
|
73
|
+
|
|
74
|
+
assert constructor.post_init_called is True
|
|
75
|
+
|
|
76
|
+
def test_subclass_without_obj_annotation_raises_error(self) -> None:
|
|
77
|
+
with pytest.raises(ConstructorError, match='must have an "obj" attribute'):
|
|
78
|
+
class InvalidConstructor(BaseConstructor[str]):
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class TestBaseDjangoModelConstructor(TestCase):
|
|
83
|
+
def setUp(self) -> None:
|
|
84
|
+
self.user = User.objects.create_user(
|
|
85
|
+
username='testuser',
|
|
86
|
+
password='testpass' # noqa: S106
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def test_constructor_with_django_model(self) -> None:
|
|
90
|
+
class UserConstructor(BaseDjangoModelConstructor[User]):
|
|
91
|
+
obj: User
|
|
92
|
+
|
|
93
|
+
constructor = UserConstructor(self.user)
|
|
94
|
+
|
|
95
|
+
assert constructor.obj == self.user
|
|
96
|
+
|
|
97
|
+
def test_model_obj_is_created_true(self) -> None:
|
|
98
|
+
class UserConstructor(BaseDjangoModelConstructor[User]):
|
|
99
|
+
obj: User
|
|
100
|
+
|
|
101
|
+
constructor = UserConstructor(self.user)
|
|
102
|
+
|
|
103
|
+
assert constructor.model_obj_is_created is True
|
|
104
|
+
|
|
105
|
+
def test_model_obj_is_created_false_for_new(self) -> None:
|
|
106
|
+
class UserConstructor(BaseDjangoModelConstructor[User]):
|
|
107
|
+
obj: User
|
|
108
|
+
|
|
109
|
+
new_user = User(username='newuser')
|
|
110
|
+
constructor = UserConstructor(new_user)
|
|
111
|
+
|
|
112
|
+
assert constructor.model_obj_is_created is False
|
|
113
|
+
|
|
114
|
+
def test_model_obj_is_new_false(self) -> None:
|
|
115
|
+
class UserConstructor(BaseDjangoModelConstructor[User]):
|
|
116
|
+
obj: User
|
|
117
|
+
|
|
118
|
+
constructor = UserConstructor(self.user)
|
|
119
|
+
|
|
120
|
+
assert constructor.model_obj_is_new is False
|
|
121
|
+
|
|
122
|
+
def test_model_obj_is_new_true(self) -> None:
|
|
123
|
+
class UserConstructor(BaseDjangoModelConstructor[User]):
|
|
124
|
+
obj: User
|
|
125
|
+
|
|
126
|
+
new_user = User(username='newuser')
|
|
127
|
+
constructor = UserConstructor(new_user)
|
|
128
|
+
|
|
129
|
+
assert constructor.model_obj_is_new is True
|
|
130
|
+
|
|
131
|
+
def test_model_obj_pk_is_empty_true(self) -> None:
|
|
132
|
+
class UserConstructor(BaseDjangoModelConstructor[User]):
|
|
133
|
+
obj: User
|
|
134
|
+
|
|
135
|
+
new_user = User(username='newuser')
|
|
136
|
+
constructor = UserConstructor(new_user)
|
|
137
|
+
|
|
138
|
+
assert constructor._model_obj_pk_is_empty is True
|
|
139
|
+
|
|
140
|
+
def test_model_obj_pk_is_empty_false(self) -> None:
|
|
141
|
+
class UserConstructor(BaseDjangoModelConstructor[User]):
|
|
142
|
+
obj: User
|
|
143
|
+
|
|
144
|
+
constructor = UserConstructor(self.user)
|
|
145
|
+
|
|
146
|
+
assert constructor._model_obj_pk_is_empty is False
|
|
147
|
+
|
|
148
|
+
def test_obj_class_returns_model_class(self) -> None:
|
|
149
|
+
class UserConstructor(BaseDjangoModelConstructor[User]):
|
|
150
|
+
obj: User
|
|
151
|
+
|
|
152
|
+
constructor = UserConstructor(self.user)
|
|
153
|
+
|
|
154
|
+
assert constructor.obj_class is User
|
|
155
|
+
|
|
156
|
+
def test_obj_is_valid_true(self) -> None:
|
|
157
|
+
class UserConstructor(BaseDjangoModelConstructor[User]):
|
|
158
|
+
obj: User
|
|
159
|
+
|
|
160
|
+
constructor = UserConstructor(self.user)
|
|
161
|
+
|
|
162
|
+
assert constructor._obj_is_valid is True
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class TestConstructorError(TestCase):
|
|
166
|
+
def test_constructor_error_is_exception(self) -> None:
|
|
167
|
+
assert issubclass(ConstructorError, Exception)
|
|
168
|
+
|
|
169
|
+
def test_constructor_error_message(self) -> None:
|
|
170
|
+
error = ConstructorError('Test error message')
|
|
171
|
+
|
|
172
|
+
assert str(error) == 'Test error message'
|
|
173
|
+
|
|
174
|
+
def test_constructor_error_can_be_raised(self) -> None:
|
|
175
|
+
with pytest.raises(ConstructorError):
|
|
176
|
+
message = 'Test'
|
|
177
|
+
raise ConstructorError(message)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class TestConstructorDescriptor(TestCase):
|
|
181
|
+
def setUp(self) -> None:
|
|
182
|
+
self.user = User.objects.create_user(
|
|
183
|
+
username='testuser',
|
|
184
|
+
password='testpass' # noqa: S106
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
def test_descriptor_get_from_model_instance(self) -> None:
|
|
188
|
+
class UserConstructor(BaseDjangoModelConstructor[User]):
|
|
189
|
+
obj: User
|
|
190
|
+
|
|
191
|
+
constructor = UserConstructor(self.user)
|
|
192
|
+
|
|
193
|
+
assert constructor.obj == self.user
|
|
File without changes
|