django-spire 0.23.6__py3-none-any.whl → 0.23.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_spire/ai/admin.py +11 -11
- django_spire/ai/chat/apps.py +1 -0
- django_spire/ai/chat/templates/django_spire/ai/chat/widget/dialog_widget.html +1 -1
- django_spire/ai/chat/tests/factories.py +15 -0
- django_spire/ai/chat/tests/test_controller.py +45 -0
- django_spire/ai/chat/tests/test_models.py +301 -0
- django_spire/ai/chat/tests/test_prompts.py +48 -0
- django_spire/ai/chat/tests/test_responses.py +208 -0
- django_spire/ai/chat/tests/test_router/test_base_chat_router.py +66 -6
- django_spire/ai/chat/tests/test_router/test_chat_workflow.py +73 -3
- django_spire/ai/chat/tests/test_router/test_integration.py +86 -6
- django_spire/ai/chat/tests/test_router/test_intent_decoder.py +93 -1
- django_spire/ai/chat/tests/test_router/test_message_intel.py +60 -1
- django_spire/ai/chat/tests/test_router/test_spire_chat_router.py +110 -0
- django_spire/ai/chat/tests/test_urls/test_json_urls.py +202 -1
- django_spire/ai/context/tests/__init__.py +0 -0
- django_spire/ai/context/tests/test_context.py +188 -0
- django_spire/ai/decorators.py +7 -6
- django_spire/ai/prompt/tests/test_bots.py +100 -10
- django_spire/ai/prompt/tests/test_prompt_intel.py +83 -0
- django_spire/ai/prompt/tests/test_prompt_tuning.py +126 -0
- django_spire/ai/sms/decorators.py +8 -2
- django_spire/ai/sms/tests/test_sms.py +240 -16
- django_spire/ai/sms/tests/test_sms_intel.py +42 -0
- django_spire/ai/sms/tests/test_webhook.py +155 -7
- django_spire/ai/sms/views.py +23 -24
- django_spire/ai/tests/test_ai.py +131 -7
- django_spire/auth/apps.py +4 -2
- django_spire/auth/controller/controller.py +36 -23
- django_spire/auth/controller/exceptions.py +9 -0
- django_spire/auth/group/admin.py +1 -0
- django_spire/auth/group/apps.py +2 -0
- django_spire/auth/group/factories.py +17 -8
- django_spire/auth/group/forms.py +7 -0
- django_spire/auth/group/tests/test_factories.py +146 -0
- django_spire/auth/group/tests/test_forms.py +282 -0
- django_spire/auth/group/tests/test_models.py +192 -0
- django_spire/auth/group/tests/test_querysets.py +98 -0
- django_spire/auth/group/tests/test_utils.py +341 -0
- django_spire/auth/group/tests/test_views.py +377 -0
- django_spire/auth/group/urls/__init__.py +3 -1
- django_spire/auth/group/urls/form_urls.py +2 -0
- django_spire/auth/group/urls/json_urls.py +3 -0
- django_spire/auth/group/urls/page_urls.py +2 -0
- django_spire/auth/group/utils.py +6 -2
- django_spire/auth/group/views/form_views.py +6 -3
- django_spire/auth/group/views/json_views.py +6 -2
- django_spire/auth/mfa/admin.py +2 -0
- django_spire/auth/mfa/apps.py +2 -0
- django_spire/auth/mfa/forms.py +1 -0
- django_spire/auth/mfa/querysets.py +9 -2
- django_spire/auth/mfa/tests/test_models.py +233 -0
- django_spire/auth/mfa/tests/test_utils.py +106 -0
- django_spire/auth/mfa/urls/__init__.py +2 -0
- django_spire/auth/mfa/urls/page_urls.py +2 -0
- django_spire/auth/mfa/urls/redirect_urls.py +2 -0
- django_spire/auth/mfa/views/page_views.py +2 -1
- django_spire/auth/permissions/consts.py +2 -2
- django_spire/auth/permissions/decorators.py +8 -8
- django_spire/auth/permissions/permissions.py +28 -35
- django_spire/auth/permissions/tests/test_decorators.py +333 -0
- django_spire/auth/permissions/tests/test_permissions.py +337 -0
- django_spire/auth/permissions/tests/test_tools.py +305 -0
- django_spire/auth/permissions/tools.py +21 -15
- django_spire/auth/seeding/seed.py +3 -0
- django_spire/auth/seeding/seeder.py +2 -0
- django_spire/auth/tests/test_controller.py +323 -0
- django_spire/auth/tests/test_url_endpoints.py +9 -9
- django_spire/auth/tests/test_views.py +406 -0
- django_spire/auth/urls/admin_urls.py +2 -0
- django_spire/auth/urls/redirect_urls.py +2 -0
- django_spire/auth/user/apps.py +2 -0
- django_spire/auth/user/forms.py +9 -0
- django_spire/auth/user/models.py +1 -1
- django_spire/auth/user/services/services.py +1 -0
- django_spire/auth/user/tests/factories.py +14 -13
- django_spire/auth/user/tests/test_factories.py +166 -2
- django_spire/auth/user/tests/test_forms.py +573 -0
- django_spire/auth/user/tests/test_models.py +257 -0
- django_spire/auth/user/tests/test_services.py +200 -0
- django_spire/auth/user/tests/test_tools.py +153 -0
- django_spire/auth/user/tests/test_user_factories.py +139 -0
- django_spire/auth/user/tests/test_views.py +363 -0
- django_spire/auth/user/tools.py +7 -1
- django_spire/auth/user/urls/form_urls.py +3 -0
- django_spire/auth/user/urls/page_urls.py +3 -0
- django_spire/auth/user/views/form_views.py +19 -10
- django_spire/auth/user/views/page_views.py +8 -2
- django_spire/auth/views/redirect_views.py +14 -9
- django_spire/comment/admin.py +2 -0
- django_spire/comment/apps.py +2 -0
- django_spire/comment/templatetags/comment_tags.py +1 -0
- django_spire/comment/tests/test_forms.py +27 -0
- django_spire/comment/tests/test_models.py +215 -0
- django_spire/comment/tests/test_querysets.py +101 -0
- django_spire/comment/tests/test_utils.py +90 -0
- django_spire/comment/urls.py +2 -0
- django_spire/comment/utils.py +22 -13
- django_spire/comment/views.py +1 -1
- django_spire/conf.py +8 -6
- django_spire/consts.py +1 -1
- django_spire/contrib/breadcrumb/apps.py +2 -0
- django_spire/contrib/breadcrumb/breadcrumbs.py +18 -18
- django_spire/contrib/breadcrumb/tests/test_breadcrumbs.py +198 -0
- django_spire/contrib/constructor/__init__.py +3 -3
- django_spire/contrib/constructor/constructor.py +15 -15
- django_spire/contrib/constructor/django_model_constructor.py +5 -4
- django_spire/contrib/constructor/exceptions.py +5 -3
- django_spire/contrib/constructor/tests/__init__.py +0 -0
- django_spire/contrib/constructor/tests/test_constructor.py +193 -0
- django_spire/contrib/form/tests/__init__.py +0 -0
- django_spire/contrib/form/tests/test_forms.py +203 -0
- django_spire/contrib/generic_views/modal_views.py +2 -1
- django_spire/contrib/generic_views/portal_views.py +20 -19
- django_spire/contrib/generic_views/tests/__init__.py +0 -0
- django_spire/contrib/generic_views/tests/test_views.py +459 -0
- django_spire/contrib/help/apps.py +2 -0
- django_spire/contrib/help/templatetags/help.py +1 -0
- django_spire/contrib/help/tests/__init__.py +0 -0
- django_spire/contrib/help/tests/test_templatetags.py +100 -0
- django_spire/contrib/options/mixins.py +6 -5
- django_spire/contrib/options/tests/factories.py +5 -1
- django_spire/contrib/options/tests/test_options.py +234 -0
- django_spire/contrib/ordering/exceptions.py +7 -3
- django_spire/contrib/ordering/mixins.py +2 -0
- django_spire/contrib/ordering/querysets.py +3 -1
- django_spire/contrib/ordering/services/processor_service.py +8 -4
- django_spire/contrib/ordering/services/service.py +1 -2
- django_spire/contrib/ordering/tests/__init__.py +0 -0
- django_spire/contrib/ordering/tests/test_ordering.py +165 -0
- django_spire/contrib/ordering/validators.py +6 -6
- django_spire/contrib/pagination/templatetags/pagination_tags.py +12 -5
- django_spire/contrib/pagination/tests/__init__.py +0 -0
- django_spire/contrib/pagination/tests/test_pagination.py +179 -0
- django_spire/contrib/performance/decorators.py +16 -6
- django_spire/contrib/performance/tests/__init__.py +0 -0
- django_spire/contrib/performance/tests/test_performance.py +107 -0
- django_spire/contrib/progress/__init__.py +1 -3
- django_spire/contrib/progress/static/django_spire/js/contrib/progress/progress.js +38 -82
- django_spire/contrib/queryset/enums.py +3 -1
- django_spire/contrib/queryset/filter_tools.py +10 -5
- django_spire/contrib/queryset/mixins.py +16 -16
- django_spire/contrib/queryset/tests/__init__.py +0 -0
- django_spire/contrib/queryset/tests/test_queryset.py +137 -0
- django_spire/contrib/seeding/field/base.py +13 -7
- django_spire/contrib/seeding/field/callable.py +8 -1
- django_spire/contrib/seeding/field/cleaners.py +5 -5
- django_spire/contrib/seeding/field/custom.py +20 -10
- django_spire/contrib/seeding/field/django/seeder.py +8 -6
- django_spire/contrib/seeding/field/enums.py +7 -5
- django_spire/contrib/seeding/field/override.py +16 -6
- django_spire/contrib/seeding/field/static.py +9 -2
- django_spire/contrib/seeding/field/tests/test_base.py +18 -14
- django_spire/contrib/seeding/field/tests/test_callable.py +13 -9
- django_spire/contrib/seeding/field/tests/test_cleaners.py +51 -38
- django_spire/contrib/seeding/field/tests/test_static.py +13 -9
- django_spire/contrib/seeding/intelligence/bots/seeder_generator_bot.py +2 -0
- django_spire/contrib/seeding/intelligence/intel.py +5 -1
- django_spire/contrib/seeding/intelligence/prompts/factory.py +6 -1
- django_spire/contrib/seeding/intelligence/prompts/foreign_key_selection_prompt.py +6 -1
- django_spire/contrib/seeding/intelligence/prompts/generate_django_model_seeder_prompts.py +2 -0
- django_spire/contrib/seeding/intelligence/prompts/generic_relationship_selection_prompt.py +7 -1
- django_spire/contrib/seeding/intelligence/prompts/hierarchical_selection_prompt.py +6 -2
- django_spire/contrib/seeding/intelligence/prompts/model_field_choices_prompt.py +8 -2
- django_spire/contrib/seeding/intelligence/prompts/negation_prompt.py +2 -0
- django_spire/contrib/seeding/intelligence/prompts/objective_prompt.py +6 -1
- django_spire/contrib/seeding/management/commands/seeding.py +9 -3
- django_spire/contrib/seeding/management/example.py +2 -0
- django_spire/contrib/seeding/model/base.py +16 -7
- django_spire/contrib/seeding/model/config.py +31 -15
- django_spire/contrib/seeding/model/django/config.py +13 -13
- django_spire/contrib/seeding/model/django/seeder.py +4 -4
- django_spire/contrib/seeding/model/django/tests/test_seeder.py +34 -23
- django_spire/contrib/seeding/model/enums.py +2 -0
- django_spire/contrib/seeding/tests/test_config.py +71 -0
- django_spire/contrib/seeding/tests/test_custom.py +35 -0
- django_spire/contrib/seeding/tests/test_enums.py +40 -0
- django_spire/contrib/seeding/tests/test_intel.py +32 -0
- django_spire/contrib/seeding/tests/test_override.py +63 -0
- django_spire/contrib/service/__init__.py +2 -2
- django_spire/contrib/service/django_model_service.py +16 -15
- django_spire/contrib/service/exceptions.py +5 -3
- django_spire/contrib/service/tests/__init__.py +0 -0
- django_spire/contrib/service/tests/test_service.py +153 -0
- django_spire/contrib/session/apps.py +2 -0
- django_spire/contrib/session/controller.py +48 -42
- django_spire/contrib/session/templatetags/session_tags.py +11 -2
- django_spire/contrib/session/tests/test_session_controller.py +117 -53
- django_spire/contrib/tests/__init__.py +0 -0
- django_spire/contrib/tests/test_utils.py +37 -0
- django_spire/contrib/utils.py +4 -1
- django_spire/core/apps.py +2 -0
- django_spire/core/converters/tests/test_to_data.py +353 -0
- django_spire/core/converters/tests/test_to_enums.py +61 -41
- django_spire/core/converters/tests/test_to_pydantic.py +138 -109
- django_spire/core/converters/to_data.py +29 -10
- django_spire/core/converters/to_enums.py +4 -2
- django_spire/core/converters/to_pydantic.py +22 -22
- django_spire/core/decorators.py +19 -6
- django_spire/core/forms/widgets.py +4 -0
- django_spire/core/maps.py +3 -1
- django_spire/core/middleware/maintenance.py +3 -3
- django_spire/core/middleware.py +8 -6
- django_spire/core/redirect/__init__.py +5 -0
- django_spire/core/redirect/generic_redirect.py +1 -2
- django_spire/core/redirect/tests/__init__.py +0 -0
- django_spire/core/redirect/tests/test_generic_redirect.py +34 -0
- django_spire/core/{tests/tests_redirect.py → redirect/tests/test_safe_redirect.py} +55 -81
- django_spire/core/shortcuts.py +3 -3
- django_spire/core/static/django_spire/css/app-layout.css +1 -1
- django_spire/core/static/django_spire/css/app-navigation.css +3 -3
- django_spire/core/static/django_spire/css/bootstrap-override.css +4 -0
- django_spire/core/tag/admin.py +12 -0
- django_spire/core/tag/intelligence/tag_set_bot.py +2 -0
- django_spire/core/tag/mixins.py +2 -0
- django_spire/core/tag/models.py +2 -0
- django_spire/core/tag/querysets.py +2 -0
- django_spire/core/tag/service/tag_service.py +6 -3
- django_spire/core/tag/tests/test_intelligence.py +9 -9
- django_spire/core/tag/tests/test_tags.py +44 -54
- django_spire/core/tag/tests/test_tools.py +191 -0
- django_spire/core/tag/tools.py +3 -0
- django_spire/core/templates/django_spire/card/card.html +5 -2
- django_spire/core/templates/django_spire/card/title_card.html +9 -4
- django_spire/core/templates/django_spire/infinite_scroll/base.html +1 -0
- django_spire/core/templates/django_spire/navigation/side_navigation.html +19 -24
- django_spire/core/templates/django_spire/page/full_page.html +46 -16
- django_spire/core/templates/django_spire/table/base.html +4 -2
- django_spire/core/templatetags/json.py +6 -2
- django_spire/core/templatetags/message.py +13 -8
- django_spire/core/templatetags/string_formating.py +8 -5
- django_spire/core/templatetags/tests/__init__.py +0 -0
- django_spire/core/templatetags/tests/test_templatetags.py +427 -0
- django_spire/core/templatetags/variable_types.py +17 -9
- django_spire/core/tests/test_cases.py +1 -1
- django_spire/core/tests/test_conf.py +43 -0
- django_spire/core/tests/test_consts.py +28 -0
- django_spire/core/tests/test_context_processors.py +93 -0
- django_spire/core/tests/test_decorators.py +95 -0
- django_spire/core/tests/test_django_spire_utils.py +56 -0
- django_spire/core/tests/test_exceptions.py +37 -0
- django_spire/core/tests/test_models.py +54 -0
- django_spire/core/tests/test_settings.py +45 -0
- django_spire/core/tests/test_shortcuts.py +74 -0
- django_spire/core/tests/test_urls.py +16 -0
- django_spire/core/tests/test_utils.py +58 -0
- django_spire/core/urls.py +4 -1
- django_spire/core/utils.py +12 -8
- django_spire/exceptions.py +16 -1
- django_spire/file/admin.py +4 -2
- django_spire/file/apps.py +8 -10
- django_spire/file/fields.py +7 -7
- django_spire/file/forms.py +1 -1
- django_spire/file/interfaces.py +15 -15
- django_spire/file/mixins.py +1 -4
- django_spire/file/models.py +3 -5
- django_spire/file/tests/factories.py +59 -0
- django_spire/file/tests/test_admin.py +69 -0
- django_spire/file/tests/test_apps.py +24 -0
- django_spire/file/tests/test_fields.py +114 -0
- django_spire/file/tests/test_forms.py +20 -0
- django_spire/file/tests/test_interfaces.py +183 -0
- django_spire/file/tests/test_models.py +82 -0
- django_spire/file/tests/test_querysets.py +102 -0
- django_spire/file/tests/test_utils.py +32 -0
- django_spire/file/tests/test_views.py +145 -0
- django_spire/file/tests/test_widgets.py +82 -0
- django_spire/file/tools.py +8 -2
- django_spire/file/views.py +7 -3
- django_spire/file/widgets.py +12 -12
- django_spire/help_desk/admin.py +15 -0
- django_spire/help_desk/apps.py +2 -0
- django_spire/help_desk/auth/controller.py +2 -0
- django_spire/help_desk/choices.py +2 -0
- django_spire/help_desk/enums.py +2 -0
- django_spire/help_desk/exceptions.py +31 -3
- django_spire/help_desk/forms.py +2 -0
- django_spire/help_desk/models.py +2 -0
- django_spire/help_desk/querysets.py +4 -1
- django_spire/help_desk/services/notification_service.py +26 -27
- django_spire/help_desk/services/service.py +2 -3
- django_spire/help_desk/tests/factories.py +8 -3
- django_spire/help_desk/tests/test_admin.py +41 -0
- django_spire/help_desk/tests/test_apps.py +41 -0
- django_spire/help_desk/tests/test_choices.py +50 -0
- django_spire/help_desk/tests/test_controller.py +87 -0
- django_spire/help_desk/tests/test_enums.py +18 -0
- django_spire/help_desk/tests/test_exceptions.py +37 -0
- django_spire/help_desk/tests/test_forms.py +89 -0
- django_spire/help_desk/tests/test_models.py +59 -0
- django_spire/help_desk/tests/test_querysets.py +38 -0
- django_spire/help_desk/tests/test_services/test_notification_service.py +15 -8
- django_spire/help_desk/tests/test_services/test_service.py +92 -0
- django_spire/help_desk/tests/test_urls/test_form_urls.py +6 -6
- django_spire/help_desk/tests/test_urls/test_page_urls.py +8 -9
- django_spire/help_desk/tests/test_views/test_form_views.py +46 -19
- django_spire/help_desk/tests/test_views/test_page_views.py +32 -9
- django_spire/help_desk/urls/__init__.py +4 -1
- django_spire/help_desk/urls/form_urls.py +3 -0
- django_spire/help_desk/urls/page_urls.py +3 -0
- django_spire/help_desk/views/form_views.py +13 -5
- django_spire/help_desk/views/page_views.py +11 -3
- django_spire/history/activity/admin.py +2 -0
- django_spire/history/activity/apps.py +3 -1
- django_spire/history/activity/mixins.py +13 -7
- django_spire/history/activity/models.py +6 -5
- django_spire/history/activity/querysets.py +2 -0
- django_spire/history/activity/tests/__init__.py +0 -0
- django_spire/history/activity/tests/test_activity.py +176 -0
- django_spire/history/admin.py +9 -2
- django_spire/history/choices.py +3 -0
- django_spire/history/models.py +5 -5
- django_spire/history/tests/test_admin.py +93 -0
- django_spire/history/tests/test_history.py +101 -0
- django_spire/history/tests/test_mixins.py +84 -0
- django_spire/history/viewed/admin.py +3 -1
- django_spire/history/viewed/apps.py +3 -1
- django_spire/history/viewed/models.py +2 -0
- django_spire/history/viewed/tests/__init__.py +0 -0
- django_spire/history/viewed/tests/test_viewed.py +46 -0
- django_spire/knowledge/auth/tests/__init__.py +0 -0
- django_spire/knowledge/auth/tests/test_controller.py +116 -0
- django_spire/knowledge/collection/admin.py +5 -1
- django_spire/knowledge/collection/models.py +3 -1
- django_spire/knowledge/collection/seeding/seed.py +1 -0
- django_spire/knowledge/collection/services/factory_service.py +10 -11
- django_spire/knowledge/collection/services/ordering_service.py +1 -2
- django_spire/knowledge/collection/services/service.py +5 -10
- django_spire/knowledge/collection/services/tag_service.py +5 -2
- django_spire/knowledge/collection/tests/factories.py +28 -1
- django_spire/knowledge/collection/tests/test_models.py +48 -0
- django_spire/knowledge/collection/tests/test_querysets.py +93 -0
- django_spire/knowledge/collection/tests/test_services/test_factory_service.py +100 -0
- django_spire/knowledge/collection/tests/test_services/test_services.py +160 -0
- django_spire/knowledge/collection/tests/test_urls/test_form_urls.py +21 -3
- django_spire/knowledge/collection/tests/test_urls/test_json_urls.py +39 -1
- django_spire/knowledge/collection/tests/test_urls/test_page_urls.py +12 -4
- django_spire/knowledge/collection/urls/__init__.py +3 -0
- django_spire/knowledge/collection/urls/form_urls.py +2 -0
- django_spire/knowledge/collection/urls/json_urls.py +2 -0
- django_spire/knowledge/collection/urls/page_urls.py +2 -0
- django_spire/knowledge/collection/views/form_views.py +4 -4
- django_spire/knowledge/collection/views/json_views.py +5 -1
- django_spire/knowledge/collection/views/page_views.py +5 -2
- django_spire/knowledge/entry/admin.py +7 -1
- django_spire/knowledge/entry/forms.py +2 -0
- django_spire/knowledge/entry/models.py +2 -0
- django_spire/knowledge/entry/seeding/seed.py +3 -0
- django_spire/knowledge/entry/services/automation_service.py +5 -4
- django_spire/knowledge/entry/services/factory_service.py +7 -5
- django_spire/knowledge/entry/services/service.py +4 -7
- django_spire/knowledge/entry/services/tag_service.py +0 -1
- django_spire/knowledge/entry/services/tool_service.py +1 -0
- django_spire/knowledge/entry/services/transformation_services.py +1 -5
- django_spire/knowledge/entry/tests/factories.py +1 -2
- django_spire/knowledge/entry/tests/test_factory_service.py +20 -0
- django_spire/knowledge/entry/tests/test_models.py +41 -0
- django_spire/knowledge/entry/tests/test_querysets.py +71 -0
- django_spire/knowledge/entry/tests/test_services.py +94 -0
- django_spire/knowledge/entry/tests/test_urls/test_form_urls.py +9 -14
- django_spire/knowledge/entry/tests/test_urls/test_json_urls.py +48 -5
- django_spire/knowledge/entry/tests/test_urls/test_page_urls.py +6 -8
- django_spire/knowledge/entry/tests/test_urls/test_template_urls.py +40 -0
- django_spire/knowledge/entry/urls/form_urls.py +2 -0
- django_spire/knowledge/entry/urls/json_urls.py +2 -0
- django_spire/knowledge/entry/urls/page_urls.py +2 -0
- django_spire/knowledge/entry/urls/template_urls.py +2 -0
- django_spire/knowledge/entry/version/block/choices.py +2 -0
- django_spire/knowledge/entry/version/block/data/data.py +1 -0
- django_spire/knowledge/entry/version/block/data/list/data.py +8 -13
- django_spire/knowledge/entry/version/block/data/list/maps.py +3 -0
- django_spire/knowledge/entry/version/block/data/list/meta.py +1 -2
- django_spire/knowledge/entry/version/block/data/list/tests/__init__.py +0 -0
- django_spire/knowledge/entry/version/block/data/list/tests/test_maps.py +32 -0
- django_spire/knowledge/entry/version/block/data/list/tests/test_meta.py +58 -0
- django_spire/knowledge/entry/version/block/data/maps.py +3 -6
- django_spire/knowledge/entry/version/block/models.py +7 -5
- django_spire/knowledge/entry/version/block/seeding/constants.py +5 -4
- django_spire/knowledge/entry/version/block/services/service.py +2 -3
- django_spire/knowledge/entry/version/block/tests/factories.py +4 -10
- django_spire/knowledge/entry/version/block/tests/test_choices.py +56 -0
- django_spire/knowledge/entry/version/block/tests/test_data.py +90 -0
- django_spire/knowledge/entry/version/block/tests/test_maps.py +37 -0
- django_spire/knowledge/entry/version/block/tests/test_models.py +55 -0
- django_spire/knowledge/entry/version/block/tests/test_querysets.py +35 -0
- django_spire/knowledge/entry/version/block/tests/test_services.py +65 -0
- django_spire/knowledge/entry/version/choices.py +2 -0
- django_spire/knowledge/entry/version/converters/converter.py +1 -1
- django_spire/knowledge/entry/version/converters/docx_converter.py +4 -7
- django_spire/knowledge/entry/version/converters/markdown_converter.py +20 -20
- django_spire/knowledge/entry/version/maps.py +4 -5
- django_spire/knowledge/entry/version/querysets.py +1 -1
- django_spire/knowledge/entry/version/seeding/seeder.py +1 -2
- django_spire/knowledge/entry/version/services/processor_service.py +5 -4
- django_spire/knowledge/entry/version/services/service.py +1 -2
- django_spire/knowledge/entry/version/tests/factories.py +2 -2
- django_spire/knowledge/entry/version/tests/test_choices.py +18 -0
- django_spire/knowledge/entry/version/tests/test_converters/test_docx_converter.py +56 -8
- django_spire/knowledge/entry/version/tests/test_converters/test_markdown_converter.py +78 -0
- django_spire/knowledge/entry/version/tests/test_maps.py +58 -0
- django_spire/knowledge/entry/version/tests/test_models.py +23 -0
- django_spire/knowledge/entry/version/tests/test_querysets.py +26 -0
- django_spire/knowledge/entry/version/tests/test_services.py +62 -0
- django_spire/knowledge/entry/version/tests/test_urls/test_json_urls.py +27 -8
- django_spire/knowledge/entry/version/tests/test_urls/test_page_urls.py +15 -8
- django_spire/knowledge/entry/version/tests/test_urls/test_redirect_urls.py +38 -0
- django_spire/knowledge/entry/version/urls/__init__.py +3 -0
- django_spire/knowledge/entry/version/urls/json_urls.py +2 -1
- django_spire/knowledge/entry/version/urls/page_urls.py +2 -0
- django_spire/knowledge/entry/version/urls/redirect_urls.py +2 -0
- django_spire/knowledge/entry/version/views/json_views.py +5 -1
- django_spire/knowledge/entry/version/views/page_views.py +10 -3
- django_spire/knowledge/entry/version/views/redirect_views.py +5 -1
- django_spire/knowledge/entry/views/form_views.py +16 -8
- django_spire/knowledge/entry/views/json_views.py +3 -1
- django_spire/knowledge/entry/views/page_views.py +8 -2
- django_spire/knowledge/entry/views/template_views.py +7 -1
- django_spire/knowledge/exceptions.py +2 -1
- django_spire/knowledge/intelligence/intel/answer_intel.py +2 -1
- django_spire/knowledge/intelligence/intel/entry_intel.py +0 -1
- django_spire/knowledge/intelligence/workflows/knowledge_workflow.py +4 -5
- django_spire/knowledge/models.py +1 -2
- django_spire/knowledge/tests/__init__.py +0 -0
- django_spire/knowledge/tests/test_templatetags.py +40 -0
- django_spire/knowledge/tests/test_urls/__init__.py +0 -0
- django_spire/knowledge/tests/test_urls/test_page_urls.py +24 -0
- django_spire/knowledge/urls/__init__.py +2 -0
- django_spire/knowledge/urls/page_urls.py +2 -0
- django_spire/knowledge/views/page_views.py +8 -3
- django_spire/notification/admin.py +3 -1
- django_spire/notification/app/admin.py +2 -0
- django_spire/notification/app/apps.py +3 -1
- django_spire/notification/app/exceptions.py +9 -2
- django_spire/notification/app/models.py +8 -4
- django_spire/notification/app/processor.py +22 -26
- django_spire/notification/app/querysets.py +2 -0
- django_spire/notification/app/tests/__init__.py +0 -0
- django_spire/notification/app/tests/factories.py +34 -0
- django_spire/notification/app/tests/test_apps.py +24 -0
- django_spire/notification/app/tests/test_models.py +72 -0
- django_spire/notification/app/tests/test_processor.py +111 -0
- django_spire/notification/app/tests/test_querysets.py +90 -0
- django_spire/notification/app/tests/test_views/__init__.py +0 -0
- django_spire/notification/app/tests/test_views/test_json_views.py +48 -0
- django_spire/notification/app/tests/test_views/test_page_views.py +19 -0
- django_spire/notification/app/urls/__init__.py +3 -1
- django_spire/notification/app/urls/json_urls.py +6 -4
- django_spire/notification/app/urls/page_urls.py +4 -3
- django_spire/notification/app/urls/template_urls.py +4 -2
- django_spire/notification/apps.py +4 -1
- django_spire/notification/email/admin.py +5 -1
- django_spire/notification/email/apps.py +3 -1
- django_spire/notification/email/exceptions.py +4 -2
- django_spire/notification/email/helper.py +5 -3
- django_spire/notification/email/models.py +4 -0
- django_spire/notification/email/processor.py +19 -15
- django_spire/notification/email/querysets.py +3 -0
- django_spire/notification/email/tests/__init__.py +0 -0
- django_spire/notification/email/tests/factories.py +35 -0
- django_spire/notification/email/tests/test_apps.py +24 -0
- django_spire/notification/email/tests/test_models.py +52 -0
- django_spire/notification/email/tests/test_processor.py +92 -0
- django_spire/notification/email/tests/test_querysets.py +43 -0
- django_spire/notification/exceptions.py +17 -2
- django_spire/notification/managers.py +7 -1
- django_spire/notification/maps.py +4 -1
- django_spire/notification/mixins.py +2 -0
- django_spire/notification/models.py +3 -1
- django_spire/notification/processors/notification.py +12 -5
- django_spire/notification/processors/processor.py +2 -0
- django_spire/notification/processors/tests/__init__.py +0 -0
- django_spire/notification/processors/tests/test_notification.py +106 -0
- django_spire/notification/push/admin.py +10 -1
- django_spire/notification/push/apps.py +3 -1
- django_spire/notification/push/models.py +2 -3
- django_spire/notification/push/tests/__init__.py +0 -0
- django_spire/notification/push/tests/test_apps.py +24 -0
- django_spire/notification/push/tests/test_models.py +28 -0
- django_spire/notification/querysets.py +7 -1
- django_spire/notification/sms/admin.py +2 -0
- django_spire/notification/sms/apps.py +4 -1
- django_spire/notification/sms/automations.py +2 -0
- django_spire/notification/sms/choices.py +2 -0
- django_spire/notification/sms/exceptions.py +19 -5
- django_spire/notification/sms/helper.py +33 -23
- django_spire/notification/sms/models.py +5 -1
- django_spire/notification/sms/processor.py +20 -20
- django_spire/notification/sms/querysets.py +2 -0
- django_spire/notification/sms/tests/factories.py +33 -0
- django_spire/notification/sms/tests/test_apps.py +24 -0
- django_spire/notification/sms/tests/test_automation.py +38 -0
- django_spire/notification/sms/tests/test_choices.py +15 -0
- django_spire/notification/sms/tests/test_consts.py +17 -0
- django_spire/notification/sms/tests/test_exceptions.py +27 -0
- django_spire/notification/sms/tests/test_helper.py +50 -0
- django_spire/notification/sms/tests/test_models.py +81 -0
- django_spire/notification/sms/tests/test_processor.py +107 -0
- django_spire/notification/sms/tests/test_tools.py +25 -11
- django_spire/notification/sms/tools.py +16 -5
- django_spire/notification/sms/urls/__init__.py +3 -1
- django_spire/notification/sms/urls/media_urls.py +2 -0
- django_spire/notification/sms/views/media_views.py +14 -4
- django_spire/notification/tests/__init__.py +0 -0
- django_spire/notification/tests/factories.py +26 -0
- django_spire/notification/tests/test_admin.py +55 -0
- django_spire/notification/tests/test_apps.py +30 -0
- django_spire/notification/tests/test_automation.py +18 -0
- django_spire/notification/tests/test_choices.py +59 -0
- django_spire/notification/tests/test_exceptions.py +58 -0
- django_spire/notification/tests/test_managers.py +100 -0
- django_spire/notification/tests/test_maps.py +31 -0
- django_spire/notification/tests/test_models.py +76 -0
- django_spire/notification/tests/test_querysets.py +184 -0
- django_spire/notification/tests/test_utils.py +23 -0
- django_spire/notification/urls.py +3 -1
- django_spire/notification/utils.py +3 -1
- django_spire/settings.py +3 -0
- django_spire/theme/tests/test_context_processor.py +15 -13
- django_spire/theme/tests/test_enums.py +2 -2
- django_spire/theme/tests/test_filesystem.py +2 -5
- django_spire/theme/tests/test_integration.py +12 -12
- django_spire/theme/tests/test_model.py +40 -38
- django_spire/theme/tests/test_views/test_json_views.py +33 -33
- django_spire/theme/urls/json_urls.py +3 -0
- django_spire/theme/urls/page_urls.py +3 -0
- django_spire/urls.py +19 -15
- django_spire/utils.py +13 -4
- {django_spire-0.23.6.dist-info → django_spire-0.23.8.dist-info}/METADATA +1 -1
- {django_spire-0.23.6.dist-info → django_spire-0.23.8.dist-info}/RECORD +532 -361
- django_spire/contrib/options/tests/test_unit.py +0 -148
- django_spire/contrib/progress/views.py +0 -64
- django_spire/contrib/seeding/tests/test_seeding.py +0 -25
- django_spire/core/tests/test_templatetags.py +0 -117
- django_spire/core/tests/tests_shortcuts.py +0 -73
- django_spire/history/activity/tests.py +0 -3
- django_spire/history/activity/views.py +0 -3
- django_spire/knowledge/collection/tests/test_services/test_transformation_service.py +0 -71
- django_spire/notification/app/tests.py +0 -3
- {django_spire-0.23.6.dist-info → django_spire-0.23.8.dist-info}/WHEEL +0 -0
- {django_spire-0.23.6.dist-info → django_spire-0.23.8.dist-info}/licenses/LICENSE.md +0 -0
- {django_spire-0.23.6.dist-info → django_spire-0.23.8.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dateutil import relativedelta
|
|
4
|
+
from django.utils.timezone import localtime
|
|
5
|
+
|
|
6
|
+
from django_spire.auth.mfa.models import MfaCode
|
|
7
|
+
from django_spire.auth.user.tests.factories import create_user
|
|
8
|
+
from django_spire.core.tests.test_cases import BaseTestCase
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MfaCodeModelTestCase(BaseTestCase):
|
|
12
|
+
def setUp(self) -> None:
|
|
13
|
+
super().setUp()
|
|
14
|
+
|
|
15
|
+
self.user = create_user(username='testuser')
|
|
16
|
+
|
|
17
|
+
def test_generate_code_creates_valid_code(self) -> None:
|
|
18
|
+
mfa_code = MfaCode.generate_code(self.user)
|
|
19
|
+
code_value = int(mfa_code.code)
|
|
20
|
+
assert mfa_code.user == self.user
|
|
21
|
+
assert 0 <= code_value <= 999999
|
|
22
|
+
|
|
23
|
+
def test_generate_code_sets_expiration(self) -> None:
|
|
24
|
+
mfa_code = MfaCode.generate_code(self.user)
|
|
25
|
+
assert mfa_code.expiration_datetime > localtime()
|
|
26
|
+
|
|
27
|
+
def test_generate_code_expiration_is_five_minutes(self) -> None:
|
|
28
|
+
before = localtime()
|
|
29
|
+
mfa_code = MfaCode.generate_code(self.user)
|
|
30
|
+
after = localtime()
|
|
31
|
+
expected_min = before + relativedelta.relativedelta(minutes=5)
|
|
32
|
+
expected_max = after + relativedelta.relativedelta(minutes=5, seconds=1)
|
|
33
|
+
assert mfa_code.expiration_datetime >= expected_min
|
|
34
|
+
assert mfa_code.expiration_datetime <= expected_max
|
|
35
|
+
|
|
36
|
+
def test_is_valid_returns_true_for_unexpired(self) -> None:
|
|
37
|
+
mfa_code = MfaCode.generate_code(self.user)
|
|
38
|
+
assert mfa_code.is_valid()
|
|
39
|
+
|
|
40
|
+
def test_is_valid_returns_false_for_expired(self) -> None:
|
|
41
|
+
mfa_code = MfaCode.objects.create(
|
|
42
|
+
user=self.user,
|
|
43
|
+
code='123456',
|
|
44
|
+
expiration_datetime=localtime() - relativedelta.relativedelta(minutes=1)
|
|
45
|
+
)
|
|
46
|
+
assert not mfa_code.is_valid()
|
|
47
|
+
|
|
48
|
+
def test_is_valid_boundary_expired(self) -> None:
|
|
49
|
+
mfa_code = MfaCode.objects.create(
|
|
50
|
+
user=self.user,
|
|
51
|
+
code='123456',
|
|
52
|
+
expiration_datetime=localtime() - relativedelta.relativedelta(seconds=1)
|
|
53
|
+
)
|
|
54
|
+
assert not mfa_code.is_valid()
|
|
55
|
+
|
|
56
|
+
def test_set_expired(self) -> None:
|
|
57
|
+
mfa_code = MfaCode.generate_code(self.user)
|
|
58
|
+
assert mfa_code.is_valid()
|
|
59
|
+
mfa_code.set_expired()
|
|
60
|
+
assert not mfa_code.is_valid()
|
|
61
|
+
|
|
62
|
+
def test_set_expired_persists(self) -> None:
|
|
63
|
+
mfa_code = MfaCode.generate_code(self.user)
|
|
64
|
+
mfa_code.set_expired()
|
|
65
|
+
mfa_code.refresh_from_db()
|
|
66
|
+
assert not mfa_code.is_valid()
|
|
67
|
+
|
|
68
|
+
def test_str_representation(self) -> None:
|
|
69
|
+
mfa_code = MfaCode.generate_code(self.user)
|
|
70
|
+
expected = f'{mfa_code.expiration_datetime} - {mfa_code.code}'
|
|
71
|
+
assert str(mfa_code) == expected
|
|
72
|
+
|
|
73
|
+
def test_code_is_valid_numeric(self) -> None:
|
|
74
|
+
for _ in range(10):
|
|
75
|
+
mfa_code = MfaCode.generate_code(self.user)
|
|
76
|
+
code_value = int(mfa_code.code)
|
|
77
|
+
assert 0 <= code_value <= 999999
|
|
78
|
+
mfa_code.delete()
|
|
79
|
+
|
|
80
|
+
def test_multiple_codes_for_same_user(self) -> None:
|
|
81
|
+
code1 = MfaCode.generate_code(self.user)
|
|
82
|
+
code1.set_expired()
|
|
83
|
+
code2 = MfaCode.generate_code(self.user)
|
|
84
|
+
assert code1.pk != code2.pk
|
|
85
|
+
|
|
86
|
+
def test_codes_for_different_users(self) -> None:
|
|
87
|
+
user2 = create_user(username='testuser2')
|
|
88
|
+
code1 = MfaCode.generate_code(self.user)
|
|
89
|
+
code2 = MfaCode.generate_code(user2)
|
|
90
|
+
assert code1.user != code2.user
|
|
91
|
+
|
|
92
|
+
def test_code_user_relationship(self) -> None:
|
|
93
|
+
mfa_code = MfaCode.generate_code(self.user)
|
|
94
|
+
assert mfa_code.user.pk == self.user.pk
|
|
95
|
+
|
|
96
|
+
def test_code_deletion_with_user(self) -> None:
|
|
97
|
+
mfa_code = MfaCode.generate_code(self.user)
|
|
98
|
+
code_pk = mfa_code.pk
|
|
99
|
+
self.user.delete()
|
|
100
|
+
assert not MfaCode.objects.filter(pk=code_pk).exists()
|
|
101
|
+
|
|
102
|
+
def test_meta_db_table(self) -> None:
|
|
103
|
+
assert MfaCode._meta.db_table == 'django_spire_authentication_mfa_code'
|
|
104
|
+
|
|
105
|
+
def test_meta_verbose_name(self) -> None:
|
|
106
|
+
assert MfaCode._meta.verbose_name == 'MFA Code'
|
|
107
|
+
assert MfaCode._meta.verbose_name_plural == 'MFA Codes'
|
|
108
|
+
|
|
109
|
+
def test_code_length_is_six(self) -> None:
|
|
110
|
+
mfa_code = MfaCode.generate_code(self.user)
|
|
111
|
+
assert len(str(mfa_code.code)) <= 6
|
|
112
|
+
assert int(mfa_code.code) <= 999999
|
|
113
|
+
|
|
114
|
+
def test_code_is_string(self) -> None:
|
|
115
|
+
mfa_code = MfaCode.generate_code(self.user)
|
|
116
|
+
assert isinstance(mfa_code.code, (str, int))
|
|
117
|
+
|
|
118
|
+
def test_code_field_max_length(self) -> None:
|
|
119
|
+
max_length = MfaCode._meta.get_field('code').max_length
|
|
120
|
+
assert max_length == 6
|
|
121
|
+
|
|
122
|
+
def test_code_field_is_unique(self) -> None:
|
|
123
|
+
assert MfaCode._meta.get_field('code').unique
|
|
124
|
+
|
|
125
|
+
def test_code_field_is_not_editable(self) -> None:
|
|
126
|
+
assert not MfaCode._meta.get_field('code').editable
|
|
127
|
+
|
|
128
|
+
def test_expiration_field_is_not_editable(self) -> None:
|
|
129
|
+
assert not MfaCode._meta.get_field('expiration_datetime').editable
|
|
130
|
+
|
|
131
|
+
def test_user_foreign_key(self) -> None:
|
|
132
|
+
mfa_code = MfaCode.generate_code(self.user)
|
|
133
|
+
assert mfa_code.user_id == self.user.pk
|
|
134
|
+
|
|
135
|
+
def test_user_related_name(self) -> None:
|
|
136
|
+
mfa_code = MfaCode.generate_code(self.user)
|
|
137
|
+
assert mfa_code in self.user.mfa_codes.all()
|
|
138
|
+
|
|
139
|
+
def test_generate_code_creates_unique_codes(self) -> None:
|
|
140
|
+
codes = set()
|
|
141
|
+
for _ in range(100):
|
|
142
|
+
mfa_code = MfaCode.generate_code(self.user)
|
|
143
|
+
codes.add(mfa_code.code)
|
|
144
|
+
mfa_code.delete()
|
|
145
|
+
assert len(codes) > 90
|
|
146
|
+
|
|
147
|
+
def test_is_valid_exact_expiration_time(self) -> None:
|
|
148
|
+
mfa_code = MfaCode.objects.create(
|
|
149
|
+
user=self.user,
|
|
150
|
+
code='654321',
|
|
151
|
+
expiration_datetime=localtime()
|
|
152
|
+
)
|
|
153
|
+
assert not mfa_code.is_valid()
|
|
154
|
+
|
|
155
|
+
def test_code_with_leading_zeros(self) -> None:
|
|
156
|
+
mfa_code = MfaCode.objects.create(
|
|
157
|
+
user=self.user,
|
|
158
|
+
code='000123',
|
|
159
|
+
expiration_datetime=localtime() + relativedelta.relativedelta(minutes=5)
|
|
160
|
+
)
|
|
161
|
+
assert mfa_code.code == '000123'
|
|
162
|
+
assert len(mfa_code.code) == 6
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class MfaCodeQuerySetTestCase(BaseTestCase):
|
|
166
|
+
def setUp(self) -> None:
|
|
167
|
+
super().setUp()
|
|
168
|
+
|
|
169
|
+
self.user = create_user(username='testuser')
|
|
170
|
+
|
|
171
|
+
def test_valid_code_returns_unexpired(self) -> None:
|
|
172
|
+
mfa_code = MfaCode.generate_code(self.user)
|
|
173
|
+
result = MfaCode.objects.valid_code(self.user)
|
|
174
|
+
assert result == mfa_code
|
|
175
|
+
|
|
176
|
+
def test_valid_code_returns_none_for_expired(self) -> None:
|
|
177
|
+
MfaCode.objects.create(
|
|
178
|
+
user=self.user,
|
|
179
|
+
code='123456',
|
|
180
|
+
expiration_datetime=localtime() - relativedelta.relativedelta(minutes=1)
|
|
181
|
+
)
|
|
182
|
+
result = MfaCode.objects.valid_code(self.user)
|
|
183
|
+
assert result is None
|
|
184
|
+
|
|
185
|
+
def test_valid_code_returns_none_for_different_user(self) -> None:
|
|
186
|
+
other_user = create_user(username='otheruser')
|
|
187
|
+
MfaCode.generate_code(other_user)
|
|
188
|
+
result = MfaCode.objects.valid_code(self.user)
|
|
189
|
+
assert result is None
|
|
190
|
+
|
|
191
|
+
def test_valid_code_returns_none_when_no_codes(self) -> None:
|
|
192
|
+
result = MfaCode.objects.valid_code(self.user)
|
|
193
|
+
assert result is None
|
|
194
|
+
|
|
195
|
+
def test_valid_code_returns_first_valid(self) -> None:
|
|
196
|
+
MfaCode.objects.create(
|
|
197
|
+
user=self.user,
|
|
198
|
+
code='111111',
|
|
199
|
+
expiration_datetime=localtime() - relativedelta.relativedelta(minutes=1)
|
|
200
|
+
)
|
|
201
|
+
valid_code = MfaCode.generate_code(self.user)
|
|
202
|
+
result = MfaCode.objects.valid_code(self.user)
|
|
203
|
+
assert result == valid_code
|
|
204
|
+
|
|
205
|
+
def test_valid_code_with_multiple_users(self) -> None:
|
|
206
|
+
user2 = create_user(username='testuser2')
|
|
207
|
+
code1 = MfaCode.generate_code(self.user)
|
|
208
|
+
MfaCode.generate_code(user2)
|
|
209
|
+
result = MfaCode.objects.valid_code(self.user)
|
|
210
|
+
assert result == code1
|
|
211
|
+
|
|
212
|
+
def test_valid_code_ignores_other_users_codes(self) -> None:
|
|
213
|
+
user2 = create_user(username='testuser2')
|
|
214
|
+
MfaCode.generate_code(user2)
|
|
215
|
+
result = MfaCode.objects.valid_code(self.user)
|
|
216
|
+
assert result is None
|
|
217
|
+
|
|
218
|
+
def test_valid_code_after_expiring_previous(self) -> None:
|
|
219
|
+
code1 = MfaCode.generate_code(self.user)
|
|
220
|
+
code1.set_expired()
|
|
221
|
+
code2 = MfaCode.generate_code(self.user)
|
|
222
|
+
result = MfaCode.objects.valid_code(self.user)
|
|
223
|
+
assert result == code2
|
|
224
|
+
|
|
225
|
+
def test_valid_code_multiple_valid_returns_first(self) -> None:
|
|
226
|
+
code1 = MfaCode.generate_code(self.user)
|
|
227
|
+
MfaCode.objects.create(
|
|
228
|
+
user=self.user,
|
|
229
|
+
code='999999',
|
|
230
|
+
expiration_datetime=localtime() + relativedelta.relativedelta(minutes=10)
|
|
231
|
+
)
|
|
232
|
+
result = MfaCode.objects.valid_code(self.user)
|
|
233
|
+
assert result == code1
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from django_spire.auth.mfa.models import MfaCode
|
|
4
|
+
from django_spire.auth.mfa.utils import get_or_generate_user_mfa_code
|
|
5
|
+
from django_spire.auth.user.tests.factories import create_user
|
|
6
|
+
from django_spire.core.tests.test_cases import BaseTestCase
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class GetOrGenerateUserMfaCodeTestCase(BaseTestCase):
|
|
10
|
+
def setUp(self) -> None:
|
|
11
|
+
super().setUp()
|
|
12
|
+
|
|
13
|
+
self.user = create_user(username='testuser')
|
|
14
|
+
|
|
15
|
+
def test_generates_new_code_when_none_exists(self) -> None:
|
|
16
|
+
mfa_code = get_or_generate_user_mfa_code(self.user)
|
|
17
|
+
assert mfa_code is not None
|
|
18
|
+
assert mfa_code.user == self.user
|
|
19
|
+
assert mfa_code.is_valid()
|
|
20
|
+
|
|
21
|
+
def test_returns_existing_valid_code(self) -> None:
|
|
22
|
+
existing_code = MfaCode.generate_code(self.user)
|
|
23
|
+
result = get_or_generate_user_mfa_code(self.user)
|
|
24
|
+
assert result.pk == existing_code.pk
|
|
25
|
+
|
|
26
|
+
def test_generates_new_code_when_existing_expired(self) -> None:
|
|
27
|
+
existing_code = MfaCode.generate_code(self.user)
|
|
28
|
+
existing_code.set_expired()
|
|
29
|
+
|
|
30
|
+
new_code = get_or_generate_user_mfa_code(self.user)
|
|
31
|
+
assert new_code.pk != existing_code.pk
|
|
32
|
+
assert new_code.is_valid()
|
|
33
|
+
|
|
34
|
+
def test_returns_same_code_on_multiple_calls(self) -> None:
|
|
35
|
+
code1 = get_or_generate_user_mfa_code(self.user)
|
|
36
|
+
code2 = get_or_generate_user_mfa_code(self.user)
|
|
37
|
+
assert code1.pk == code2.pk
|
|
38
|
+
|
|
39
|
+
def test_different_users_get_different_codes(self) -> None:
|
|
40
|
+
user2 = create_user(username='testuser2')
|
|
41
|
+
code1 = get_or_generate_user_mfa_code(self.user)
|
|
42
|
+
code2 = get_or_generate_user_mfa_code(user2)
|
|
43
|
+
assert code1.pk != code2.pk
|
|
44
|
+
assert code1.user != code2.user
|
|
45
|
+
|
|
46
|
+
def test_generated_code_is_persisted(self) -> None:
|
|
47
|
+
mfa_code = get_or_generate_user_mfa_code(self.user)
|
|
48
|
+
assert MfaCode.objects.filter(pk=mfa_code.pk).exists()
|
|
49
|
+
|
|
50
|
+
def test_generates_valid_code_after_expiration(self) -> None:
|
|
51
|
+
code1 = get_or_generate_user_mfa_code(self.user)
|
|
52
|
+
code1.set_expired()
|
|
53
|
+
code2 = get_or_generate_user_mfa_code(self.user)
|
|
54
|
+
assert code2.is_valid()
|
|
55
|
+
|
|
56
|
+
def test_code_belongs_to_correct_user(self) -> None:
|
|
57
|
+
mfa_code = get_or_generate_user_mfa_code(self.user)
|
|
58
|
+
assert mfa_code.user.pk == self.user.pk
|
|
59
|
+
assert mfa_code.user.username == self.user.username
|
|
60
|
+
|
|
61
|
+
def test_returns_mfa_code_instance(self) -> None:
|
|
62
|
+
mfa_code = get_or_generate_user_mfa_code(self.user)
|
|
63
|
+
assert isinstance(mfa_code, MfaCode)
|
|
64
|
+
|
|
65
|
+
def test_code_has_valid_expiration(self) -> None:
|
|
66
|
+
mfa_code = get_or_generate_user_mfa_code(self.user)
|
|
67
|
+
assert mfa_code.expiration_datetime is not None
|
|
68
|
+
|
|
69
|
+
def test_code_has_six_digits(self) -> None:
|
|
70
|
+
mfa_code = get_or_generate_user_mfa_code(self.user)
|
|
71
|
+
assert len(str(mfa_code.code)) <= 6
|
|
72
|
+
assert int(mfa_code.code) <= 999999
|
|
73
|
+
|
|
74
|
+
def test_code_is_numeric(self) -> None:
|
|
75
|
+
mfa_code = get_or_generate_user_mfa_code(self.user)
|
|
76
|
+
assert str(mfa_code.code).isdigit()
|
|
77
|
+
|
|
78
|
+
def test_multiple_expired_codes_generates_new(self) -> None:
|
|
79
|
+
code1 = get_or_generate_user_mfa_code(self.user)
|
|
80
|
+
code1.set_expired()
|
|
81
|
+
code2 = get_or_generate_user_mfa_code(self.user)
|
|
82
|
+
code2.set_expired()
|
|
83
|
+
code3 = get_or_generate_user_mfa_code(self.user)
|
|
84
|
+
assert code3.is_valid()
|
|
85
|
+
assert code3.pk != code1.pk
|
|
86
|
+
assert code3.pk != code2.pk
|
|
87
|
+
|
|
88
|
+
def test_does_not_affect_other_users_codes(self) -> None:
|
|
89
|
+
user2 = create_user(username='testuser2')
|
|
90
|
+
code1 = get_or_generate_user_mfa_code(self.user)
|
|
91
|
+
code2 = get_or_generate_user_mfa_code(user2)
|
|
92
|
+
code1.set_expired()
|
|
93
|
+
result = get_or_generate_user_mfa_code(user2)
|
|
94
|
+
assert result.pk == code2.pk
|
|
95
|
+
|
|
96
|
+
def test_new_code_after_delete(self) -> None:
|
|
97
|
+
code1 = get_or_generate_user_mfa_code(self.user)
|
|
98
|
+
code1_pk = code1.pk
|
|
99
|
+
code1.delete()
|
|
100
|
+
code2 = get_or_generate_user_mfa_code(self.user)
|
|
101
|
+
assert code2.pk != code1_pk
|
|
102
|
+
|
|
103
|
+
def test_code_user_relationship_intact(self) -> None:
|
|
104
|
+
mfa_code = get_or_generate_user_mfa_code(self.user)
|
|
105
|
+
assert mfa_code.user == self.user
|
|
106
|
+
assert mfa_code in self.user.mfa_codes.all()
|
|
@@ -5,8 +5,8 @@ from django.http import HttpResponseRedirect
|
|
|
5
5
|
from django.template.response import TemplateResponse
|
|
6
6
|
from django.urls import reverse
|
|
7
7
|
|
|
8
|
-
from django_spire.auth.mfa.utils import get_or_generate_user_mfa_code
|
|
9
8
|
from django_spire.auth.mfa import forms
|
|
9
|
+
from django_spire.auth.mfa.utils import get_or_generate_user_mfa_code
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def mfa_form_view(request):
|
|
@@ -15,6 +15,7 @@ def mfa_form_view(request):
|
|
|
15
15
|
|
|
16
16
|
if request.method == 'POST':
|
|
17
17
|
form = forms.MFAForm(mfa_code, request.POST)
|
|
18
|
+
|
|
18
19
|
if form.is_valid():
|
|
19
20
|
mfa_code.set_expired()
|
|
20
21
|
profile.set_mfa_grace_period()
|
|
@@ -2,8 +2,8 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import Literal
|
|
4
4
|
|
|
5
|
-
from django_spire.auth.group.models import AuthGroup
|
|
6
|
-
from django_spire.auth.user.models import AuthUser
|
|
5
|
+
# from django_spire.auth.group.models import AuthGroup
|
|
6
|
+
# from django_spire.auth.user.models import AuthUser
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
PERMISSIONS_LEVEL_CHOICES = (
|
|
@@ -13,12 +13,12 @@ if TYPE_CHECKING:
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
def permission_required_decorator_function(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
permissions: str | Sequence[str],
|
|
17
|
+
method,
|
|
18
|
+
request: WSGIRequest,
|
|
19
|
+
*args,
|
|
20
|
+
all_required: bool = True,
|
|
21
|
+
**kwargs
|
|
22
22
|
):
|
|
23
23
|
if not request.user.is_authenticated:
|
|
24
24
|
return HttpResponseRedirect(reverse('django_spire:auth:admin:login'))
|
|
@@ -40,8 +40,8 @@ def permission_required_decorator_function(
|
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
def permission_required(
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
*permissions: str,
|
|
44
|
+
all_required: bool = True
|
|
45
45
|
):
|
|
46
46
|
def decorator(method):
|
|
47
47
|
@functools.wraps(method)
|
|
@@ -10,20 +10,21 @@ from django_spire.auth.group.utils import (
|
|
|
10
10
|
codename_to_perm_level,
|
|
11
11
|
perm_level_to_string
|
|
12
12
|
)
|
|
13
|
-
from django_spire.auth.permissions.consts import VALID_PERMISSION_LEVELS
|
|
14
13
|
from django_spire.core.utils import get_object_from_module_string
|
|
15
14
|
|
|
16
15
|
if TYPE_CHECKING:
|
|
17
|
-
from django.db.models import
|
|
16
|
+
from django.db.models import Model, QuerySet
|
|
17
|
+
|
|
18
|
+
from django_spire.auth.permissions.consts import VALID_PERMISSION_LEVELS
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
class ModelPermission:
|
|
21
22
|
def __init__(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
self,
|
|
24
|
+
name: str,
|
|
25
|
+
model_class_path: str,
|
|
26
|
+
is_proxy_model: bool,
|
|
27
|
+
verbose_name: str | None = None
|
|
27
28
|
):
|
|
28
29
|
self.name = name
|
|
29
30
|
self.model_class_path = model_class_path
|
|
@@ -57,13 +58,11 @@ class ModelPermissions:
|
|
|
57
58
|
self,
|
|
58
59
|
perm_level: VALID_PERMISSION_LEVELS | None
|
|
59
60
|
) -> list[Permission]:
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if codename_to_perm_level(perm.codename) <= perm_level
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return permission_list
|
|
61
|
+
return [
|
|
62
|
+
perm
|
|
63
|
+
for perm in self.permissions
|
|
64
|
+
if codename_to_perm_level(perm.codename) <= perm_level
|
|
65
|
+
]
|
|
67
66
|
|
|
68
67
|
def get_special_role(self, codename: str) -> Permission | None:
|
|
69
68
|
for perm in self.permissions:
|
|
@@ -90,9 +89,8 @@ class ModelPermissions:
|
|
|
90
89
|
|
|
91
90
|
class GroupPermissions:
|
|
92
91
|
def __init__(self, group: Group, model_permission: ModelPermission):
|
|
93
|
-
"""
|
|
94
|
-
|
|
95
|
-
"""
|
|
92
|
+
"""Helper to use Django Groups as a cascading permission structure."""
|
|
93
|
+
|
|
96
94
|
self.group = group
|
|
97
95
|
self.model_permissions = ModelPermissions(model_permission)
|
|
98
96
|
self.group_perms = self.group.permissions.all()
|
|
@@ -105,18 +103,14 @@ class GroupPermissions:
|
|
|
105
103
|
self.set_group_perms()
|
|
106
104
|
|
|
107
105
|
def has_special_role(self, codename: str) -> bool:
|
|
108
|
-
for perm in self.group_perms
|
|
109
|
-
if perm.codename == codename:
|
|
110
|
-
return True
|
|
111
|
-
|
|
112
|
-
return False
|
|
106
|
+
return any(perm.codename == codename for perm in self.group_perms)
|
|
113
107
|
|
|
114
108
|
def perm_level(self) -> VALID_PERMISSION_LEVELS:
|
|
115
|
-
codename_list = [
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if perm.codename.split('_')[-1] == self.model_permissions.model_name
|
|
119
|
-
|
|
109
|
+
codename_list = [
|
|
110
|
+
perm.codename
|
|
111
|
+
for perm in self.group_perms
|
|
112
|
+
if perm.codename.split('_')[-1] == self.model_permissions.model_name
|
|
113
|
+
]
|
|
120
114
|
|
|
121
115
|
return codename_list_to_perm_level(codename_list)
|
|
122
116
|
|
|
@@ -169,9 +163,8 @@ class GroupPermissions:
|
|
|
169
163
|
|
|
170
164
|
class UserPermissionHelper:
|
|
171
165
|
def __init__(self, user: User, model_permission: ModelPermission):
|
|
172
|
-
"""
|
|
173
|
-
|
|
174
|
-
"""
|
|
166
|
+
"""Helper to query user's current cascading permissions"""
|
|
167
|
+
|
|
175
168
|
self.user = user
|
|
176
169
|
self.model_permissions = ModelPermissions(model_permission)
|
|
177
170
|
self.user_perms = self.get_user_perms()
|
|
@@ -180,11 +173,11 @@ class UserPermissionHelper:
|
|
|
180
173
|
return Permission.objects.filter(group__user=self.user).distinct()
|
|
181
174
|
|
|
182
175
|
def perm_level(self) -> VALID_PERMISSION_LEVELS:
|
|
183
|
-
codename_list = [
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
if perm.codename.split('_')[-1] == self.model_permissions.model_name
|
|
187
|
-
|
|
176
|
+
codename_list = [
|
|
177
|
+
perm.codename
|
|
178
|
+
for perm in self.user_perms
|
|
179
|
+
if perm.codename.split('_')[-1] == self.model_permissions.model_name
|
|
180
|
+
]
|
|
188
181
|
|
|
189
182
|
return codename_list_to_perm_level(codename_list)
|
|
190
183
|
|