nautobot 3.0.0a2__py3-none-any.whl → 3.0.0a3__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.
Potentially problematic release.
This version of nautobot might be problematic. Click here for more details.
- nautobot/apps/choices.py +0 -2
- nautobot/apps/filters.py +7 -9
- nautobot/apps/models.py +2 -2
- nautobot/apps/ui.py +9 -1
- nautobot/circuits/filters.py +3 -2
- nautobot/circuits/navigation.py +3 -2
- nautobot/circuits/templates/circuits/circuit.html +1 -1
- nautobot/circuits/templates/circuits/circuit_create.html +3 -3
- nautobot/circuits/templates/circuits/circuittermination.html +1 -1
- nautobot/circuits/templates/circuits/circuittermination_create.html +9 -24
- nautobot/circuits/templates/circuits/circuittype.html +1 -1
- nautobot/circuits/templates/circuits/inc/circuit_termination_cable_fragment.html +6 -6
- nautobot/circuits/templates/circuits/inc/speed_widget.html +12 -12
- nautobot/circuits/templates/circuits/providernetwork.html +1 -1
- nautobot/circuits/tests/integration/test_circuit.py +10 -13
- nautobot/cloud/filters.py +1 -1
- nautobot/cloud/navigation.py +3 -2
- nautobot/core/api/schema.py +1 -1
- nautobot/core/api/serializers.py +6 -1
- nautobot/core/api/urls.py +1 -0
- nautobot/core/api/views.py +8 -0
- nautobot/core/apps/__init__.py +11 -10
- nautobot/core/celery/__init__.py +3 -5
- nautobot/core/checks.py +46 -0
- nautobot/core/cli/bootstrap_v3_to_v5.py +70 -1
- nautobot/core/cli/migrate_deprecated_templates.py +200 -0
- nautobot/core/constants.py +3 -0
- nautobot/core/context_processors.py +9 -1
- nautobot/core/forms/forms.py +1 -1
- nautobot/core/jobs/__init__.py +6 -3
- nautobot/core/jobs/groups.py +31 -1
- nautobot/core/management/commands/generate_test_data.py +28 -9
- nautobot/core/models/generics.py +9 -1
- nautobot/core/models/tree_queries.py +10 -5
- nautobot/core/settings.py +18 -12
- nautobot/core/settings.yaml +13 -7
- nautobot/core/signals.py +12 -1
- nautobot/core/tables.py +13 -6
- nautobot/core/templates/40x.html +1 -1
- nautobot/core/templates/500.html +2 -2
- nautobot/core/templates/admin/config/config.html +12 -12
- nautobot/core/templates/admin/index.html +3 -3
- nautobot/core/templates/buttons/export.html +1 -1
- nautobot/core/templates/components/button/dropdown.html +5 -3
- nautobot/core/templates/components/panel/body_wrapper_generic_table.html +1 -1
- nautobot/core/templates/components/panel/panel.html +3 -3
- nautobot/core/templates/components/tab/content_wrapper.html +2 -3
- nautobot/core/templates/components/tab/label_wrapper_distinct_view.html +1 -1
- nautobot/core/templates/echarts/echarts.html +1 -1
- nautobot/core/templates/generic/object_bulk_add_component.html +2 -1
- nautobot/core/templates/generic/object_bulk_create.html +4 -3
- nautobot/core/templates/generic/object_bulk_destroy.html +3 -3
- nautobot/core/templates/generic/object_bulk_remove.html +2 -2
- nautobot/core/templates/generic/object_bulk_update.html +5 -4
- nautobot/core/templates/generic/object_create.html +5 -4
- nautobot/core/templates/generic/object_import.html +2 -1
- nautobot/core/templates/generic/object_list.html +12 -4
- nautobot/core/templates/generic/object_notes.html +5 -3
- nautobot/core/templates/generic/object_retrieve.html +2 -3
- nautobot/core/templates/graphene/graphiql.html +7 -7
- nautobot/core/templates/home.html +1 -1
- nautobot/core/templates/import_success.html +2 -1
- nautobot/core/templates/inc/computed_fields/panel_data.html +1 -1
- nautobot/core/templates/inc/created_updated.html +7 -3
- nautobot/core/templates/inc/custom_fields/panel_data.html +1 -1
- nautobot/core/templates/inc/form_static_field.html +6 -0
- nautobot/core/templates/inc/header.html +1 -1
- nautobot/core/templates/inc/image_attachments.html +2 -1
- nautobot/core/templates/inc/nav_menu.html +2 -1
- nautobot/core/templates/inc/search_panel.html +4 -4
- nautobot/core/templates/login.html +4 -2
- nautobot/core/templates/nautobot_config.py.j2 +6 -5
- nautobot/core/templates/redoc_ui.html +7 -0
- nautobot/core/templates/search.html +1 -1
- nautobot/core/templates/swagger_ui.html +17 -3
- nautobot/core/templates/system_jobs/import_objects.html +1 -2
- nautobot/core/templates/utilities/confirmation_form.html +2 -2
- nautobot/core/templates/utilities/obj_table.html +10 -2
- nautobot/core/templates/utilities/render_field.html +7 -7
- nautobot/core/templates/utilities/render_jinja2.html +2 -2
- nautobot/core/templates/utilities/templatetags/filter_form_drawer.html +4 -4
- nautobot/core/templates/utilities/theme_preview.html +16 -3
- nautobot/core/templates/widgets/selectwithdisabled_option.html +3 -1
- nautobot/core/templatetags/helpers.py +52 -6
- nautobot/core/testing/api.py +68 -9
- nautobot/core/testing/filters.py +0 -23
- nautobot/core/testing/integration.py +23 -10
- nautobot/core/testing/mixins.py +2 -0
- nautobot/core/testing/views.py +4 -0
- nautobot/core/tests/integration/test_app_home.py +34 -30
- nautobot/core/tests/integration/test_app_navbar.py +3 -0
- nautobot/core/tests/nautobot_config_without_example_apps.py +4 -0
- nautobot/core/tests/runner.py +9 -1
- nautobot/core/tests/test_api.py +5 -3
- nautobot/core/tests/test_breadcrumbs.py +6 -7
- nautobot/core/tests/test_checks.py +28 -0
- nautobot/core/tests/test_cli.py +40 -0
- nautobot/core/tests/test_config.py +2 -1
- nautobot/core/tests/test_forms.py +55 -13
- nautobot/core/tests/test_jobs.py +75 -1
- nautobot/core/tests/test_nautobot_server.py +2 -0
- nautobot/core/tests/test_navigations.py +76 -1
- nautobot/core/tests/test_patch_social_django.py +42 -0
- nautobot/core/tests/test_tables.py +3 -1
- nautobot/core/tests/test_templatetags_helpers.py +53 -13
- nautobot/core/tests/test_templatetags_ui_framework.py +4 -4
- nautobot/core/tests/test_tree_queries.py +14 -1
- nautobot/core/tests/test_ui.py +1 -1
- nautobot/core/tests/test_utils.py +31 -4
- nautobot/core/tests/test_views.py +159 -31
- nautobot/core/ui/breadcrumbs.py +2 -12
- nautobot/core/ui/choices.py +142 -10
- nautobot/core/ui/constants.py +76 -12
- nautobot/core/ui/object_detail.py +92 -12
- nautobot/core/urls.py +12 -1
- nautobot/core/utils/cache.py +2 -1
- nautobot/core/utils/filtering.py +17 -17
- nautobot/core/utils/lookup.py +3 -8
- nautobot/core/utils/module_loading.py +21 -0
- nautobot/core/utils/patch_social_django.py +128 -0
- nautobot/core/views/__init__.py +38 -1
- nautobot/core/views/generic.py +3 -3
- nautobot/core/views/mixins.py +15 -3
- nautobot/core/views/renderers.py +2 -0
- nautobot/core/views/viewsets.py +2 -1
- nautobot/data_validation/apps.py +1 -5
- nautobot/data_validation/custom_validators.py +4 -4
- nautobot/data_validation/filters.py +1 -1
- nautobot/data_validation/forms.py +40 -0
- nautobot/data_validation/migrations/0001_initial.py +0 -7
- nautobot/data_validation/migrations/0002_data_migration_from_app.py +0 -12
- nautobot/data_validation/models.py +16 -7
- nautobot/data_validation/navigation.py +8 -1
- nautobot/data_validation/tables.py +12 -5
- nautobot/data_validation/templates/data_validation/datacompliance_tab.html +1 -0
- nautobot/data_validation/templates/data_validation/device_constraints.html +61 -0
- nautobot/data_validation/tests/__init__.py +2 -2
- nautobot/data_validation/tests/migrations/test_migrations.py +83 -3
- nautobot/data_validation/tests/test_data_compliance_rules.py +12 -7
- nautobot/data_validation/tests/test_filters.py +8 -6
- nautobot/data_validation/tests/test_models.py +15 -0
- nautobot/data_validation/tests/test_views.py +190 -32
- nautobot/data_validation/urls.py +2 -5
- nautobot/data_validation/views.py +73 -40
- nautobot/dcim/api/serializers.py +0 -13
- nautobot/dcim/apps.py +4 -0
- nautobot/dcim/choices.py +16 -0
- nautobot/dcim/custom_validators.py +84 -0
- nautobot/dcim/filter_mixins.py +353 -4
- nautobot/dcim/{filters/__init__.py → filters.py} +2 -35
- nautobot/dcim/forms.py +1 -1
- nautobot/dcim/migrations/0078_remove_device_location_tenant_name_uniqueness.py +16 -0
- nautobot/dcim/migrations/0079_device_name_data_migration.py +59 -0
- nautobot/dcim/models/device_components.py +81 -68
- nautobot/dcim/models/devices.py +13 -16
- nautobot/dcim/navigation.py +7 -6
- nautobot/dcim/tables/devices.py +3 -0
- nautobot/dcim/tables/template_code.py +14 -14
- nautobot/dcim/templates/dcim/cable.html +2 -61
- nautobot/dcim/templates/dcim/cable_connect.html +28 -112
- nautobot/dcim/templates/dcim/cable_edit.html +2 -5
- nautobot/dcim/templates/dcim/cable_retrieve.html +61 -0
- nautobot/dcim/templates/dcim/cable_trace.html +1 -3
- nautobot/dcim/templates/dcim/cable_update.html +5 -0
- nautobot/dcim/templates/dcim/consoleport.html +6 -5
- nautobot/dcim/templates/dcim/consoleserverport.html +6 -5
- nautobot/dcim/templates/dcim/device/config.html +2 -2
- nautobot/dcim/templates/dcim/device/consoleports.html +1 -1
- nautobot/dcim/templates/dcim/device/consoleserverports.html +1 -1
- nautobot/dcim/templates/dcim/device/devicebays.html +1 -1
- nautobot/dcim/templates/dcim/device/frontports.html +1 -1
- nautobot/dcim/templates/dcim/device/interfaces.html +1 -1
- nautobot/dcim/templates/dcim/device/inventory.html +1 -1
- nautobot/dcim/templates/dcim/device/lldp_neighbors.html +1 -1
- nautobot/dcim/templates/dcim/device/modulebays.html +1 -1
- nautobot/dcim/templates/dcim/device/poweroutlets.html +1 -1
- nautobot/dcim/templates/dcim/device/powerports.html +1 -1
- nautobot/dcim/templates/dcim/device/rearports.html +1 -1
- nautobot/dcim/templates/dcim/device/status.html +8 -8
- nautobot/dcim/templates/dcim/device/wireless.html +1 -1
- nautobot/dcim/templates/dcim/device.html +1 -1
- nautobot/dcim/templates/dcim/device_component_add.html +2 -2
- nautobot/dcim/templates/dcim/device_create.html +5 -3
- nautobot/dcim/templates/dcim/device_interface_delete.html +1 -1
- nautobot/dcim/templates/dcim/device_list.html +73 -10
- nautobot/dcim/templates/dcim/devicebay_populate.html +2 -2
- nautobot/dcim/templates/dcim/devicetype.html +1 -1
- nautobot/dcim/templates/dcim/devicetype_component_add.html +2 -2
- nautobot/dcim/templates/dcim/footer_convert_to_contact_or_team_record.html +14 -0
- nautobot/dcim/templates/dcim/frontport.html +9 -8
- nautobot/dcim/templates/dcim/inc/edit_form_softwareversion_js.html +2 -2
- nautobot/dcim/templates/dcim/interface.html +26 -6
- nautobot/dcim/templates/dcim/interface_bulk_delete.html +1 -1
- nautobot/dcim/templates/dcim/inventoryitem_add.html +3 -1
- nautobot/dcim/templates/dcim/inventoryitem_bulk_delete.html +1 -1
- nautobot/dcim/templates/dcim/inventoryitem_edit.html +3 -1
- nautobot/dcim/templates/dcim/location_retrieve.html +1 -242
- nautobot/dcim/templates/dcim/module/base.html +49 -9
- nautobot/dcim/templates/dcim/module_list.html +57 -8
- nautobot/dcim/templates/dcim/modulefamily_retrieve.html +1 -1
- nautobot/dcim/templates/dcim/moduletype_retrieve.html +49 -9
- nautobot/dcim/templates/dcim/platform_create.html +1 -1
- nautobot/dcim/templates/dcim/powerfeed.html +1 -1
- nautobot/dcim/templates/dcim/powerpanel.html +1 -1
- nautobot/dcim/templates/dcim/powerport.html +5 -4
- nautobot/dcim/templates/dcim/rack_elevation_list.html +16 -4
- nautobot/dcim/templates/dcim/rack_retrieve.html +33 -15
- nautobot/dcim/templates/dcim/rearport.html +7 -6
- nautobot/dcim/templates/dcim/virtualchassis.html +1 -1
- nautobot/dcim/templates/dcim/virtualchassis_add_member.html +16 -14
- nautobot/dcim/templates/dcim/virtualchassis_update.html +14 -6
- nautobot/dcim/tests/integration/test_controller.py +1 -0
- nautobot/dcim/tests/test_api.py +8 -0
- nautobot/dcim/tests/test_custom_validators.py +229 -0
- nautobot/dcim/tests/test_filters.py +12 -6
- nautobot/dcim/tests/test_models.py +63 -4
- nautobot/dcim/tests/test_views.py +63 -22
- nautobot/dcim/urls.py +64 -21
- nautobot/dcim/utils.py +3 -3
- nautobot/dcim/views.py +547 -273
- nautobot/extras/api/views.py +9 -1
- nautobot/extras/choices.py +2 -13
- nautobot/extras/{filters/mixins.py → filter_mixins.py} +1 -1
- nautobot/extras/{filters/customfields.py → filter_mixins_customfields.py} +42 -6
- nautobot/extras/{filters/__init__.py → filters.py} +14 -46
- nautobot/extras/forms/forms.py +5 -13
- nautobot/extras/forms/mixins.py +0 -41
- nautobot/extras/management/__init__.py +9 -0
- nautobot/extras/migrations/0127_approval_workflow_models.py +6 -6
- nautobot/extras/migrations/0129_jobresult_debug_log_count_jobresult_error_log_count_and_more.py +37 -0
- nautobot/extras/migrations/0130_jobresult_generate_log_entry_counts.py +42 -0
- nautobot/extras/models/__init__.py +1 -2
- nautobot/extras/models/approvals.py +22 -13
- nautobot/extras/models/contacts.py +2 -0
- nautobot/extras/models/groups.py +44 -5
- nautobot/extras/models/jobs.py +59 -1
- nautobot/extras/models/mixins.py +28 -0
- nautobot/extras/models/models.py +13 -0
- nautobot/extras/models/secrets.py +1 -0
- nautobot/extras/models/statuses.py +0 -15
- nautobot/extras/navigation.py +13 -9
- nautobot/extras/plugins/__init__.py +33 -55
- nautobot/extras/plugins/tables.py +3 -3
- nautobot/extras/plugins/urls.py +2 -21
- nautobot/extras/plugins/utils.py +1 -33
- nautobot/extras/plugins/views.py +0 -4
- nautobot/extras/signals.py +20 -19
- nautobot/extras/tables.py +52 -68
- nautobot/extras/templates/extras/approval_dashboard.html +7 -5
- nautobot/extras/templates/extras/approvalworkflowdefinition_update.html +4 -2
- nautobot/extras/templates/extras/approvalworkflowstage_retrieve.html +20 -12
- nautobot/extras/templates/extras/computedfield.html +1 -1
- nautobot/extras/templates/extras/configcontext.html +1 -1
- nautobot/extras/templates/extras/configcontextschema_validation.html +2 -2
- nautobot/extras/templates/extras/customfield.html +1 -1
- nautobot/extras/templates/extras/dynamicgroup_retrieve.html +11 -5
- nautobot/extras/templates/extras/dynamicgroup_update.html +1 -1
- nautobot/extras/templates/extras/gitrepository_result.html +0 -2
- nautobot/extras/templates/extras/graphqlquery_retrieve.html +1 -96
- nautobot/extras/templates/extras/inc/approval_buttons_column.html +20 -6
- nautobot/extras/templates/extras/inc/bulk_edit_overridable_field.html +8 -7
- nautobot/extras/templates/extras/inc/configcontext_format.html +10 -3
- nautobot/extras/templates/extras/inc/graphqlquery_execute.html +71 -0
- nautobot/extras/templates/extras/inc/job_tiles.html +15 -3
- nautobot/extras/templates/extras/inc/json_format.html +10 -3
- nautobot/extras/templates/extras/inc/overridable_field.html +13 -12
- nautobot/extras/templates/extras/job.html +29 -12
- nautobot/extras/templates/extras/job_bulk_edit.html +18 -0
- nautobot/extras/templates/extras/job_edit.html +52 -46
- nautobot/extras/templates/extras/job_list.html +29 -25
- nautobot/extras/templates/extras/marketplace.html +5 -9
- nautobot/extras/templates/extras/object_configcontext.html +1 -1
- nautobot/extras/templates/extras/object_dynamicgroups.html +2 -2
- nautobot/extras/templates/extras/objectchange_retrieve.html +19 -37
- nautobot/extras/templates/extras/plugin_detail.html +26 -21
- nautobot/extras/templates/extras/plugins_list.html +16 -26
- nautobot/extras/templates/extras/role_retrieve.html +64 -0
- nautobot/extras/templates/extras/scheduledjob.html +4 -2
- nautobot/extras/templates/extras/secretsgroup.html +1 -1
- nautobot/extras/templates/extras/tag.html +1 -1
- nautobot/extras/templatetags/custom_links.py +12 -12
- nautobot/extras/templatetags/job_buttons.py +14 -12
- nautobot/extras/test_jobs/invalid_import.py +9 -0
- nautobot/extras/test_jobs/log_counts_by_level.py +23 -0
- nautobot/extras/test_jobs/missing_import.py +11 -0
- nautobot/extras/tests/integration/test_configcontextschema.py +27 -26
- nautobot/extras/tests/integration/test_customfields.py +8 -7
- nautobot/extras/tests/integration/test_dynamicgroups.py +5 -1
- nautobot/extras/tests/integration/test_plugin_banner.py +3 -0
- nautobot/extras/tests/integration/test_plugins.py +18 -6
- nautobot/extras/tests/test_api.py +27 -18
- nautobot/extras/tests/test_approvals.py +38 -38
- nautobot/extras/tests/test_changelog.py +35 -3
- nautobot/extras/tests/test_customfields.py +22 -13
- nautobot/extras/tests/test_customfields_filters.py +479 -0
- nautobot/extras/tests/test_dynamicgroups.py +39 -1
- nautobot/extras/tests/test_filters.py +21 -19
- nautobot/extras/tests/test_forms.py +18 -21
- nautobot/extras/tests/test_jobs.py +25 -4
- nautobot/extras/tests/test_migrations.py +1 -0
- nautobot/extras/tests/test_models.py +13 -31
- nautobot/extras/tests/test_plugins.py +36 -10
- nautobot/extras/tests/test_views.py +31 -30
- nautobot/extras/views.py +81 -19
- nautobot/ipam/factory.py +7 -0
- nautobot/ipam/filter_mixins.py +38 -0
- nautobot/ipam/filters.py +27 -38
- nautobot/ipam/formfields.py +1 -1
- nautobot/ipam/forms.py +6 -3
- nautobot/ipam/migrations/0030_ipam__namespaces.py +13 -0
- nautobot/ipam/migrations/0031_ipam___data_migrations.py +4 -1
- nautobot/ipam/migrations/0054_namespace_tenant.py +25 -0
- nautobot/ipam/models.py +29 -2
- nautobot/ipam/navigation.py +3 -2
- nautobot/ipam/signals.py +71 -0
- nautobot/ipam/tables.py +13 -6
- nautobot/ipam/templates/ipam/inc/toggle_available.html +10 -10
- nautobot/ipam/templates/ipam/inc/vlangroup_header.html +1 -0
- nautobot/ipam/templates/ipam/ipaddress.html +14 -0
- nautobot/ipam/templates/ipam/ipaddress_merge.html +3 -3
- nautobot/ipam/templates/ipam/ipaddresstointerface_retrieve.html +1 -0
- nautobot/ipam/templates/ipam/namespace_update.html +15 -0
- nautobot/ipam/templates/ipam/prefix_delete.html +1 -1
- nautobot/ipam/templates/ipam/prefix_list.html +14 -13
- nautobot/ipam/templates/ipam/service.html +1 -1
- nautobot/ipam/templates/ipam/vlan.html +1 -1
- nautobot/ipam/templates/ipam/vlan_interfaces.html +1 -1
- nautobot/ipam/templates/ipam/vlan_vminterfaces.html +1 -1
- nautobot/ipam/tests/migration/test_migrations.py +89 -0
- nautobot/ipam/tests/test_api.py +13 -6
- nautobot/ipam/tests/test_filters.py +10 -0
- nautobot/ipam/tests/test_forms.py +1 -1
- nautobot/ipam/tests/test_models.py +43 -1
- nautobot/ipam/tests/test_tables.py +1 -2
- nautobot/ipam/tests/test_utils.py +1 -1
- nautobot/ipam/tests/test_views.py +13 -14
- nautobot/ipam/ui.py +0 -17
- nautobot/ipam/utils/migrations.py +16 -2
- nautobot/ipam/utils/testing.py +9 -3
- nautobot/ipam/views.py +46 -6
- nautobot/project-static/dist/css/nautobot.css +1 -1
- nautobot/project-static/dist/css/nautobot.css.map +1 -1
- nautobot/project-static/dist/js/nautobot.js +1 -1
- nautobot/project-static/dist/js/nautobot.js.map +1 -1
- nautobot/project-static/js/cabletrace.js +1 -1
- nautobot/project-static/js/interface_filtering.js +20 -16
- nautobot/project-static/nautobot-icons/battery-3.svg +3 -0
- nautobot/project-static/nautobot-icons/cloud.svg +1 -1
- nautobot/project-static/nautobot-icons/control-panel.svg +1 -1
- nautobot/project-static/nautobot-icons/device-lifecycle.svg +1 -1
- nautobot/project-static/nautobot-icons/elements.svg +1 -1
- nautobot/project-static/nautobot-icons/extensibility.svg +3 -0
- nautobot/project-static/nautobot-icons/hammer.svg +1 -1
- nautobot/project-static/nautobot-icons/organization.svg +3 -0
- nautobot/project-static/nautobot-icons/secrets.svg +1 -1
- nautobot/project-static/nautobot-icons/security.svg +3 -0
- nautobot/project-static/nautobot-icons/server.svg +1 -1
- nautobot/project-static/nautobot-icons/star-filled.svg +1 -1
- nautobot/project-static/nautobot-icons/star.svg +1 -1
- nautobot/tenancy/api/serializers.py +1 -0
- nautobot/tenancy/api/views.py +2 -1
- nautobot/tenancy/{filters/__init__.py → filters.py} +2 -10
- nautobot/tenancy/navigation.py +3 -1
- nautobot/tenancy/tests/test_filters.py +0 -2
- nautobot/tenancy/views.py +2 -1
- nautobot/ui/src/js/collapse.js +3 -3
- nautobot/ui/src/js/nautobot.js +16 -0
- nautobot/ui/src/scss/colors.scss +1 -1
- nautobot/ui/src/scss/nautobot.scss +61 -28
- nautobot/users/templates/users/profile.html +45 -12
- nautobot/users/templates/users/sessionkey_delete.html +1 -1
- nautobot/users/tests/test_api.py +4 -0
- nautobot/users/views.py +4 -2
- nautobot/virtualization/models.py +1 -68
- nautobot/virtualization/navigation.py +3 -2
- nautobot/virtualization/templates/virtualization/virtual_machine_vminterface_delete.html +1 -1
- nautobot/virtualization/templates/virtualization/virtualmachine.html +1 -1
- nautobot/virtualization/templates/virtualization/virtualmachine_list.html +2 -2
- nautobot/virtualization/templates/virtualization/virtualmachine_update.html +3 -1
- nautobot/virtualization/tests/test_api.py +3 -0
- nautobot/virtualization/tests/test_models.py +44 -4
- nautobot/vpn/__init__.py +0 -0
- nautobot/vpn/api/serializers.py +113 -0
- nautobot/vpn/api/urls.py +19 -0
- nautobot/vpn/api/views.py +70 -0
- nautobot/vpn/apps.py +8 -0
- nautobot/vpn/choices.py +171 -0
- nautobot/vpn/factory.py +209 -0
- nautobot/vpn/filters.py +233 -0
- nautobot/vpn/forms.py +486 -0
- nautobot/vpn/homepage.py +19 -0
- nautobot/vpn/migrations/0001_initial.py +541 -0
- nautobot/vpn/migrations/0002_populate_defaults.py +199 -0
- nautobot/vpn/migrations/__init__.py +0 -0
- nautobot/vpn/models.py +527 -0
- nautobot/vpn/navigation.py +98 -0
- nautobot/vpn/tables.py +380 -0
- nautobot/vpn/templates/vpn/vpnprofile.html +2 -0
- nautobot/vpn/templates/vpn/vpnprofile_create.html +150 -0
- nautobot/vpn/tests/__init__.py +0 -0
- nautobot/vpn/tests/test_api.py +341 -0
- nautobot/vpn/tests/test_filters.py +139 -0
- nautobot/vpn/tests/test_forms.py +294 -0
- nautobot/vpn/tests/test_models.py +97 -0
- nautobot/vpn/tests/test_views.py +281 -0
- nautobot/vpn/urls.py +16 -0
- nautobot/vpn/views.py +437 -0
- nautobot/wireless/navigation.py +3 -2
- nautobot/wireless/tests/integration/test_radio_profile.py +1 -5
- nautobot/wireless/tests/test_api.py +1 -1
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/METADATA +14 -14
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/RECORD +417 -366
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/entry_points.txt +1 -0
- nautobot/data_validation/template_content.py +0 -42
- nautobot/dcim/filters/mixins.py +0 -354
- nautobot/ipam/templates/ipam/inc/prefix_header_extra_content_table.html +0 -4
- /nautobot/tenancy/{filters/mixins.py → filter_mixins.py} +0 -0
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/LICENSE.txt +0 -0
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/NOTICE +0 -0
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/WHEEL +0 -0
|
@@ -1,21 +1,26 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import sys
|
|
1
3
|
from unittest import mock
|
|
2
4
|
|
|
3
5
|
from django import forms as django_forms
|
|
6
|
+
from django.apps import apps as django_apps
|
|
4
7
|
from django.contrib.contenttypes.models import ContentType
|
|
5
8
|
from django.http import QueryDict
|
|
6
|
-
from django.test import
|
|
9
|
+
from django.test import tag
|
|
7
10
|
from django.urls import reverse
|
|
11
|
+
from django_filters.filterset import FilterSet
|
|
8
12
|
from netaddr import IPNetwork
|
|
9
13
|
|
|
10
14
|
from nautobot.core import filters, forms, testing
|
|
11
15
|
from nautobot.core.utils import requests
|
|
16
|
+
from nautobot.core.utils.filtering import get_filterset_parameter_form_field
|
|
12
17
|
from nautobot.dcim import filters as dcim_filters, forms as dcim_forms, models as dcim_models
|
|
13
18
|
from nautobot.dcim.tests import test_views
|
|
14
19
|
from nautobot.extras import filters as extras_filters, models as extras_models
|
|
15
20
|
from nautobot.ipam import forms as ipam_forms, models as ipam_models
|
|
16
21
|
|
|
17
22
|
|
|
18
|
-
class ExpandIPAddress(TestCase):
|
|
23
|
+
class ExpandIPAddress(testing.TestCase):
|
|
19
24
|
"""
|
|
20
25
|
Validate the operation of expand_ipaddress_pattern().
|
|
21
26
|
"""
|
|
@@ -198,7 +203,7 @@ class ExpandIPAddress(TestCase):
|
|
|
198
203
|
sorted(forms.expand_ipaddress_pattern("1.2.3.[4,,5]/32", 4))
|
|
199
204
|
|
|
200
205
|
|
|
201
|
-
class ExpandAlphanumeric(TestCase):
|
|
206
|
+
class ExpandAlphanumeric(testing.TestCase):
|
|
202
207
|
"""
|
|
203
208
|
Validate the operation of expand_alphanumeric_pattern().
|
|
204
209
|
"""
|
|
@@ -332,7 +337,7 @@ class ExpandAlphanumeric(TestCase):
|
|
|
332
337
|
sorted(forms.expand_alphanumeric_pattern("r[a,,b]a"))
|
|
333
338
|
|
|
334
339
|
|
|
335
|
-
class AddFieldToFormClassTest(TestCase):
|
|
340
|
+
class AddFieldToFormClassTest(testing.TestCase):
|
|
336
341
|
def test_field_added(self):
|
|
337
342
|
"""
|
|
338
343
|
Test adding of a new field to an existing form.
|
|
@@ -361,7 +366,7 @@ class AddFieldToFormClassTest(TestCase):
|
|
|
361
366
|
)
|
|
362
367
|
|
|
363
368
|
|
|
364
|
-
class DynamicModelChoiceFieldTest(TestCase):
|
|
369
|
+
class DynamicModelChoiceFieldTest(testing.TestCase):
|
|
365
370
|
"""Tests for DynamicModelChoiceField."""
|
|
366
371
|
|
|
367
372
|
def setUp(self):
|
|
@@ -397,7 +402,7 @@ class DynamicModelChoiceFieldTest(TestCase):
|
|
|
397
402
|
self.assertEqual(self.field_with_to_field_name.prepare_value(address), address.address)
|
|
398
403
|
|
|
399
404
|
|
|
400
|
-
class DynamicModelMultipleChoiceFieldTest(TestCase):
|
|
405
|
+
class DynamicModelMultipleChoiceFieldTest(testing.TestCase):
|
|
401
406
|
"""Tests for DynamicModelMultipleChoiceField."""
|
|
402
407
|
|
|
403
408
|
def setUp(self):
|
|
@@ -434,7 +439,7 @@ class DynamicModelMultipleChoiceFieldTest(TestCase):
|
|
|
434
439
|
)
|
|
435
440
|
|
|
436
441
|
|
|
437
|
-
class MultiValueCharFieldTest(TestCase):
|
|
442
|
+
class MultiValueCharFieldTest(testing.TestCase):
|
|
438
443
|
def setUp(self):
|
|
439
444
|
self.filter = filters.MultiValueCharFilter()
|
|
440
445
|
self.field = forms.MultiValueCharField()
|
|
@@ -467,7 +472,7 @@ class MultiValueCharFieldTest(TestCase):
|
|
|
467
472
|
)
|
|
468
473
|
|
|
469
474
|
|
|
470
|
-
class NumericArrayFieldTest(TestCase):
|
|
475
|
+
class NumericArrayFieldTest(testing.TestCase):
|
|
471
476
|
def setUp(self):
|
|
472
477
|
super().setUp()
|
|
473
478
|
# We need to use a field with required=False so we can test empty/None inputs
|
|
@@ -497,7 +502,7 @@ class NumericArrayFieldTest(TestCase):
|
|
|
497
502
|
self.field.clean(test)
|
|
498
503
|
|
|
499
504
|
|
|
500
|
-
class AddressFieldMixinTest(TestCase):
|
|
505
|
+
class AddressFieldMixinTest(testing.TestCase):
|
|
501
506
|
"""Test cases for the AddressFieldMixin."""
|
|
502
507
|
|
|
503
508
|
def setUp(self):
|
|
@@ -527,7 +532,7 @@ class AddressFieldMixinTest(TestCase):
|
|
|
527
532
|
mock_init.assert_called_with(initial=self.initial, instance=self.ip)
|
|
528
533
|
|
|
529
534
|
|
|
530
|
-
class PrefixFieldMixinTest(TestCase):
|
|
535
|
+
class PrefixFieldMixinTest(testing.TestCase):
|
|
531
536
|
"""Test cases for the PrefixFieldMixin."""
|
|
532
537
|
|
|
533
538
|
def setUp(self):
|
|
@@ -576,7 +581,7 @@ class JSONFieldTest(testing.TestCase):
|
|
|
576
581
|
self.assertEqual('"I am UTF-8! 😀"', forms.JSONField().prepare_value("I am UTF-8! 😀"))
|
|
577
582
|
|
|
578
583
|
|
|
579
|
-
class MultiMatchModelMultipleChoiceFieldTest(TestCase):
|
|
584
|
+
class MultiMatchModelMultipleChoiceFieldTest(testing.TestCase):
|
|
580
585
|
def test_clean(self):
|
|
581
586
|
field = forms.MultiMatchModelMultipleChoiceField(
|
|
582
587
|
queryset=ipam_models.VLANGroup.objects.all(), to_field_name="name"
|
|
@@ -604,20 +609,57 @@ class MultiMatchModelMultipleChoiceFieldTest(TestCase):
|
|
|
604
609
|
field.clean(value)
|
|
605
610
|
|
|
606
611
|
|
|
607
|
-
class WidgetsTest(TestCase):
|
|
612
|
+
class WidgetsTest(testing.TestCase):
|
|
608
613
|
def test_api_select_add_query_param_with_utf8(self):
|
|
609
614
|
widget = forms.APISelect()
|
|
610
615
|
widget.add_query_param("utf8", "I am UTF-8! 😀")
|
|
611
616
|
self.assertEqual('["I am UTF-8! 😀"]', widget.attrs["data-query-param-utf8"])
|
|
612
617
|
|
|
613
618
|
|
|
614
|
-
class DynamicFilterFormTest(TestCase):
|
|
619
|
+
class DynamicFilterFormTest(testing.TestCase):
|
|
620
|
+
def test_get_filterset_parameter_form_field_all_filters(self):
|
|
621
|
+
"""
|
|
622
|
+
Test every FilterSet to validate that Plural names are correctly mapped in get_filterset_parameter_form_field.
|
|
623
|
+
"""
|
|
624
|
+
filterset_classes = set()
|
|
625
|
+
for app_config in django_apps.get_app_configs():
|
|
626
|
+
try:
|
|
627
|
+
filters_mod = sys.modules.get(f"{app_config.name}.filters")
|
|
628
|
+
if not filters_mod:
|
|
629
|
+
continue
|
|
630
|
+
for _name, obj in inspect.getmembers(filters_mod):
|
|
631
|
+
if (
|
|
632
|
+
inspect.isclass(obj) # Check if obj is a class
|
|
633
|
+
and issubclass(obj, FilterSet) # Check if obj is a subclass of FilterSet
|
|
634
|
+
and obj is not FilterSet # Exclude the base FilterSet class itself
|
|
635
|
+
and getattr(getattr(obj, "_meta", None), "model", None)
|
|
636
|
+
is not None # Ensure the FilterSet has a model defined
|
|
637
|
+
):
|
|
638
|
+
filterset_classes.add(obj)
|
|
639
|
+
except Exception as e:
|
|
640
|
+
# This test might start failing if an app's filters.py gets a design change.
|
|
641
|
+
self.fail(f"Error processing app '{app_config.name}': {e}")
|
|
642
|
+
for filterset_class in filterset_classes:
|
|
643
|
+
filterset = filterset_class()
|
|
644
|
+
model = filterset._meta.model
|
|
645
|
+
for filter_name in filterset.filters.keys():
|
|
646
|
+
try:
|
|
647
|
+
field = get_filterset_parameter_form_field(model, filter_name, filterset=filterset)
|
|
648
|
+
self.assertIsNotNone(field, "Field was unexpectedly None")
|
|
649
|
+
except KeyError as e:
|
|
650
|
+
self.fail(
|
|
651
|
+
f"A filter failed to operate due to mismatched plural name:"
|
|
652
|
+
f" Check MODEL_VERBOSE_NAME_PLURAL_TO_FEATURE_NAME_MAPPING:"
|
|
653
|
+
f" FilterClass: {filterset_class.__name__} name: {filter_name}: {e}"
|
|
654
|
+
)
|
|
655
|
+
|
|
615
656
|
# TODO(timizuo): investigate why test fails on CI
|
|
616
657
|
# def test_dynamic_filter_form_with_missing_attr(self):
|
|
617
658
|
# with self.assertRaises(AttributeError) as err:
|
|
618
659
|
# DynamicFilterForm()
|
|
619
660
|
# self.assertEqual("'DynamicFilterForm' object requires `filterset_class` attribute", str(err.exception))
|
|
620
661
|
|
|
662
|
+
@tag("example_app")
|
|
621
663
|
def test_dynamic_filter_form(self):
|
|
622
664
|
form = forms.DynamicFilterForm(filterset=extras_filters.StatusFilterSet())
|
|
623
665
|
location_form = forms.DynamicFilterForm(filterset=dcim_filters.LocationFilterSet())
|
nautobot/core/tests/test_jobs.py
CHANGED
|
@@ -17,11 +17,12 @@ from nautobot.core.jobs.cleanup import CleanupTypes
|
|
|
17
17
|
from nautobot.core.testing import create_job_result_and_run_job, TransactionTestCase
|
|
18
18
|
from nautobot.core.testing.context import load_event_broker_override_settings
|
|
19
19
|
from nautobot.dcim.models import Device, DeviceType, Location, LocationType, Manufacturer
|
|
20
|
-
from nautobot.extras.choices import JobResultStatusChoices, LogLevelChoices
|
|
20
|
+
from nautobot.extras.choices import DynamicGroupTypeChoices, JobResultStatusChoices, LogLevelChoices
|
|
21
21
|
from nautobot.extras.factory import JobResultFactory, ObjectChangeFactory
|
|
22
22
|
from nautobot.extras.models import (
|
|
23
23
|
Contact,
|
|
24
24
|
ContactAssociation,
|
|
25
|
+
DynamicGroup,
|
|
25
26
|
ExportTemplate,
|
|
26
27
|
FileProxy,
|
|
27
28
|
JobLogEntry,
|
|
@@ -1248,3 +1249,76 @@ class BulkDeleteTestCase(TransactionTestCase):
|
|
|
1248
1249
|
saved_view_id=None,
|
|
1249
1250
|
)
|
|
1250
1251
|
self._common_no_error_test_assertion(Role, job_result, name__istartswith="Example Status")
|
|
1252
|
+
|
|
1253
|
+
|
|
1254
|
+
class RefreshDynamicGroupCacheJobButtonReceiverTestCase(TransactionTestCase):
|
|
1255
|
+
def setUp(self):
|
|
1256
|
+
super().setUp()
|
|
1257
|
+
self.job_module = "nautobot.core.jobs.groups"
|
|
1258
|
+
self.job_name = "RefreshDynamicGroupCacheJobButtonReceiver"
|
|
1259
|
+
|
|
1260
|
+
def test_successful_cache_refresh(self):
|
|
1261
|
+
LocationType.objects.create(name="DG Test LT 1")
|
|
1262
|
+
LocationType.objects.create(name="DG Test LT 2")
|
|
1263
|
+
LocationType.objects.create(name="DG Test LT 3")
|
|
1264
|
+
dg = DynamicGroup(
|
|
1265
|
+
name="Location Types",
|
|
1266
|
+
content_type=ContentType.objects.get_for_model(LocationType),
|
|
1267
|
+
group_type=DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER,
|
|
1268
|
+
filter={"name__isw": ["DG Test"]},
|
|
1269
|
+
)
|
|
1270
|
+
dg.clean()
|
|
1271
|
+
dg.save(update_cached_members=False)
|
|
1272
|
+
self.assertEqual(0, dg.count)
|
|
1273
|
+
|
|
1274
|
+
job_result = create_job_result_and_run_job(
|
|
1275
|
+
self.job_module,
|
|
1276
|
+
self.job_name,
|
|
1277
|
+
object_model_name="extras.dynamicgroup",
|
|
1278
|
+
object_pk=dg.pk,
|
|
1279
|
+
)
|
|
1280
|
+
self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_SUCCESS)
|
|
1281
|
+
self.assertEqual(3, dg.count)
|
|
1282
|
+
|
|
1283
|
+
dg.filter = {"name__iew": ["DG Test"]}
|
|
1284
|
+
dg.clean()
|
|
1285
|
+
dg.save(update_cached_members=False)
|
|
1286
|
+
self.assertEqual(3, dg.count)
|
|
1287
|
+
job_result = create_job_result_and_run_job(
|
|
1288
|
+
self.job_module,
|
|
1289
|
+
self.job_name,
|
|
1290
|
+
object_model_name="extras.dynamicgroup",
|
|
1291
|
+
object_pk=dg.pk,
|
|
1292
|
+
)
|
|
1293
|
+
self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_SUCCESS)
|
|
1294
|
+
self.assertEqual(0, dg.count)
|
|
1295
|
+
|
|
1296
|
+
def test_failure_on_non_dg(self):
|
|
1297
|
+
job_result = create_job_result_and_run_job(
|
|
1298
|
+
self.job_module,
|
|
1299
|
+
self.job_name,
|
|
1300
|
+
object_model_name="extras.status",
|
|
1301
|
+
object_pk=Status.objects.first().pk,
|
|
1302
|
+
)
|
|
1303
|
+
self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
|
|
1304
|
+
log_fail = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_FAILURE)
|
|
1305
|
+
self.assertEqual(log_fail.message, "This job button should only be used with Dynamic Group records.")
|
|
1306
|
+
|
|
1307
|
+
def test_failure_on_static_dg(self):
|
|
1308
|
+
dg = DynamicGroup.objects.create(
|
|
1309
|
+
name="Location Types",
|
|
1310
|
+
content_type=ContentType.objects.get_for_model(LocationType),
|
|
1311
|
+
group_type=DynamicGroupTypeChoices.TYPE_STATIC,
|
|
1312
|
+
)
|
|
1313
|
+
job_result = create_job_result_and_run_job(
|
|
1314
|
+
self.job_module,
|
|
1315
|
+
self.job_name,
|
|
1316
|
+
object_model_name="extras.dynamicgroup",
|
|
1317
|
+
object_pk=dg.pk,
|
|
1318
|
+
)
|
|
1319
|
+
self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
|
|
1320
|
+
log_fail = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_FAILURE)
|
|
1321
|
+
self.assertEqual(
|
|
1322
|
+
log_fail.message,
|
|
1323
|
+
"The members of this Dynamic Group are statically defined and do not need to be recalculated.",
|
|
1324
|
+
)
|
|
@@ -13,6 +13,7 @@ from unittest import mock, TestCase
|
|
|
13
13
|
|
|
14
14
|
from django import __version__ as django_version
|
|
15
15
|
from django.conf import settings
|
|
16
|
+
from django.test import tag
|
|
16
17
|
|
|
17
18
|
from nautobot import __version__ as nautobot_version
|
|
18
19
|
|
|
@@ -101,6 +102,7 @@ class NautobotServerTestCase(TestCase):
|
|
|
101
102
|
|
|
102
103
|
self.assertNotEqual(secret_key_1, secret_key_2)
|
|
103
104
|
|
|
105
|
+
@tag("example_app")
|
|
104
106
|
def test_settings_processing(self):
|
|
105
107
|
result = subprocess.run(
|
|
106
108
|
["nautobot-server", "--config", settings.SETTINGS_PATH, "print_settings"],
|
|
@@ -1,9 +1,15 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from django.apps import apps
|
|
1
4
|
from django.test import tag, TestCase
|
|
2
5
|
from django.urls import resolve
|
|
3
6
|
|
|
7
|
+
from nautobot.core.apps import NavMenuTab
|
|
4
8
|
from nautobot.core.choices import ButtonActionColorChoices, ButtonActionIconChoices
|
|
5
9
|
from nautobot.core.testing.utils import get_expected_menu_item_name
|
|
10
|
+
from nautobot.core.ui.choices import NavigationIconChoices, NavigationWeightChoices
|
|
6
11
|
from nautobot.core.utils.lookup import get_route_for_model
|
|
12
|
+
from nautobot.core.utils.module_loading import import_string_optional
|
|
7
13
|
from nautobot.core.utils.permissions import get_permission_for_model
|
|
8
14
|
from nautobot.extras.registry import registry
|
|
9
15
|
|
|
@@ -45,7 +51,8 @@ class NavMenuTestCase(TestCase):
|
|
|
45
51
|
except AttributeError:
|
|
46
52
|
# Not a model view?
|
|
47
53
|
self.assertIn(
|
|
48
|
-
item_details["name"],
|
|
54
|
+
item_details["name"],
|
|
55
|
+
{"Apps Marketplace", "Installed Apps", "Interface Connections", "Device Constraints"},
|
|
49
56
|
)
|
|
50
57
|
|
|
51
58
|
for button, button_details in item_details["buttons"].items():
|
|
@@ -81,3 +88,71 @@ class NavMenuTestCase(TestCase):
|
|
|
81
88
|
else:
|
|
82
89
|
expected_perms[tab_name] |= group_perms
|
|
83
90
|
self.assertEqual(expected_perms[tab_name], tab_details["permissions"])
|
|
91
|
+
|
|
92
|
+
def test_nav_menu_tabs_have_icon_and_weight(self):
|
|
93
|
+
"""Ensure each NavMenuTab in every navigation.py has an icon and weight set, and any duplicates by name match."""
|
|
94
|
+
tabs_by_name = {}
|
|
95
|
+
for app in apps.get_app_configs():
|
|
96
|
+
if not app.name.startswith("nautobot."):
|
|
97
|
+
continue
|
|
98
|
+
nav_path = f"{app.name}.navigation.menu_items"
|
|
99
|
+
menu_items = import_string_optional(nav_path)
|
|
100
|
+
if menu_items is None:
|
|
101
|
+
continue
|
|
102
|
+
for tab in menu_items:
|
|
103
|
+
if not isinstance(tab, NavMenuTab):
|
|
104
|
+
raise TypeError(f"Expected NavMenuTab instance in {nav_path}, got {type(tab)}")
|
|
105
|
+
tab_name = tab.name
|
|
106
|
+
icon = tab.icon
|
|
107
|
+
weight = tab.weight
|
|
108
|
+
with self.subTest(tab_name=tab_name, nav_path=nav_path):
|
|
109
|
+
self.assertIsNotNone(tab_name, f"Tab in {nav_path} missing 'name'")
|
|
110
|
+
self.assertIsNotNone(icon, f"Tab '{tab_name}' in {nav_path} missing 'icon'")
|
|
111
|
+
self.assertIsNotNone(weight, f"Tab '{tab_name}' in {nav_path} missing 'weight'")
|
|
112
|
+
if tab_name in tabs_by_name:
|
|
113
|
+
prev_icon, prev_weight, prev_path = tabs_by_name[tab_name]
|
|
114
|
+
self.assertEqual(
|
|
115
|
+
icon,
|
|
116
|
+
prev_icon,
|
|
117
|
+
f"Tab '{tab_name}' has inconsistent icons: '{icon}' in {nav_path} vs '{prev_icon}' in {prev_path}",
|
|
118
|
+
)
|
|
119
|
+
self.assertEqual(
|
|
120
|
+
weight,
|
|
121
|
+
prev_weight,
|
|
122
|
+
f"Tab '{tab_name}' has inconsistent weights: '{weight}' in {nav_path} vs '{prev_weight}' in {prev_path}",
|
|
123
|
+
)
|
|
124
|
+
else:
|
|
125
|
+
tabs_by_name[tab_name] = (icon, weight, nav_path)
|
|
126
|
+
|
|
127
|
+
def test_icon_and_weight_class_attributes_match(self):
|
|
128
|
+
"""
|
|
129
|
+
Ensure every class attribute in NavigationIconChoices is also in NavigationWeightChoices and vice versa.
|
|
130
|
+
If not, print the missing/extra attributes for easier debugging.
|
|
131
|
+
"""
|
|
132
|
+
icon_attrs = {attr for attr in dir(NavigationIconChoices) if attr.isupper()}
|
|
133
|
+
weight_attrs = {attr for attr in dir(NavigationWeightChoices) if attr.isupper()}
|
|
134
|
+
|
|
135
|
+
only_in_icons = sorted(icon_attrs - weight_attrs)
|
|
136
|
+
only_in_weights = sorted(weight_attrs - icon_attrs)
|
|
137
|
+
|
|
138
|
+
if only_in_icons or only_in_weights:
|
|
139
|
+
msg = []
|
|
140
|
+
if only_in_icons:
|
|
141
|
+
msg.append(f"Class attributes only in NavigationIconChoices: {only_in_icons}")
|
|
142
|
+
if only_in_weights:
|
|
143
|
+
msg.append(f"Class attributes only in NavigationWeightChoices: {only_in_weights}")
|
|
144
|
+
self.fail("\n".join(msg))
|
|
145
|
+
|
|
146
|
+
def test_navigation_icons_have_svg(self):
|
|
147
|
+
"""Ensure every NavigationIconChoices icon has a corresponding SVG file."""
|
|
148
|
+
missing = []
|
|
149
|
+
svg_dir = os.path.abspath(
|
|
150
|
+
os.path.join(os.path.dirname(__file__), "..", "..", "project-static", "nautobot-icons")
|
|
151
|
+
)
|
|
152
|
+
icon_attrs = [attr for attr in dir(NavigationIconChoices) if attr.isupper() and not attr == "CHOICES"]
|
|
153
|
+
for icon_attr in icon_attrs:
|
|
154
|
+
icon_name = getattr(NavigationIconChoices, icon_attr)
|
|
155
|
+
svg_path = os.path.join(svg_dir, f"{icon_name}.svg")
|
|
156
|
+
if not os.path.isfile(svg_path):
|
|
157
|
+
missing.append(svg_path)
|
|
158
|
+
self.assertFalse(missing, f"Missing SVG files for NavigationIconChoices: {missing}")
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test suite for social_django storage patch.
|
|
3
|
+
|
|
4
|
+
This tests that the monkeypatch correctly replaces the vulnerable create_user
|
|
5
|
+
method with the secure version that raises AuthAlreadyAssociated instead of
|
|
6
|
+
silently returning an existing user.
|
|
7
|
+
|
|
8
|
+
Please see nautobot/core/utils/patch_social_django.py for details on the patch.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from unittest.mock import MagicMock, patch
|
|
12
|
+
|
|
13
|
+
from nautobot.core.testing import TestCase
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PatchSocialDjangoTestCase(TestCase):
|
|
17
|
+
def test_django_storage_has_patch_at_import_time(self):
|
|
18
|
+
"""
|
|
19
|
+
Test that importing DjangoStorage gives us the patched version.
|
|
20
|
+
|
|
21
|
+
This verifies that the patch applied in CoreConfig.ready() persists
|
|
22
|
+
and affects all imports of DjangoStorage throughout the application.
|
|
23
|
+
"""
|
|
24
|
+
from django.db.utils import IntegrityError
|
|
25
|
+
from social_core.exceptions import AuthAlreadyAssociated
|
|
26
|
+
from social_django.models import DjangoStorage
|
|
27
|
+
|
|
28
|
+
# Mock user model to trigger IntegrityError
|
|
29
|
+
mock_user_model = MagicMock()
|
|
30
|
+
mock_manager = MagicMock()
|
|
31
|
+
mock_manager.create_user.side_effect = IntegrityError("duplicate key")
|
|
32
|
+
mock_user_model._default_manager = mock_manager
|
|
33
|
+
|
|
34
|
+
# Patch username_field and user_model methods to return our mock user model
|
|
35
|
+
with patch.object(DjangoStorage.user, "username_field", return_value="username"):
|
|
36
|
+
with patch.object(DjangoStorage.user, "user_model", return_value=mock_user_model):
|
|
37
|
+
# Should raise AuthAlreadyAssociated (patched behavior)
|
|
38
|
+
with self.assertRaises(AuthAlreadyAssociated):
|
|
39
|
+
DjangoStorage.user.create_user(username="test", email="test@example.com")
|
|
40
|
+
|
|
41
|
+
# Verify vulnerable get() not called
|
|
42
|
+
mock_manager.get.assert_not_called()
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from django.test import TestCase
|
|
1
|
+
from django.test import tag, TestCase
|
|
2
2
|
|
|
3
3
|
from nautobot.circuits.models import Circuit
|
|
4
4
|
from nautobot.circuits.tables import CircuitTable
|
|
@@ -32,6 +32,7 @@ class TableTestCase(TestCase):
|
|
|
32
32
|
)
|
|
33
33
|
self.assertEqual(list(table_queryset_data), list(sorted_queryset))
|
|
34
34
|
|
|
35
|
+
@tag("example_app")
|
|
35
36
|
def test_tree_model_table_orderable(self):
|
|
36
37
|
"""Assert TreeNode model table are orderable."""
|
|
37
38
|
location_type = LocationType.objects.get(name="Campus")
|
|
@@ -107,6 +108,7 @@ class TableTestCase(TestCase):
|
|
|
107
108
|
)
|
|
108
109
|
self.assertEqual(list(table_queryset_data), list(sorted_queryset))
|
|
109
110
|
|
|
111
|
+
@tag("example_app")
|
|
110
112
|
def test_base_table_apis(self):
|
|
111
113
|
"""
|
|
112
114
|
Test BaseTable APIs, specifically visible_columns and configurable_columns.
|
|
@@ -1,14 +1,16 @@
|
|
|
1
|
+
from unittest import mock
|
|
2
|
+
|
|
1
3
|
from constance.test import override_config
|
|
2
4
|
from django.conf import settings
|
|
5
|
+
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
|
|
3
6
|
from django.templatetags.static import static
|
|
4
|
-
from django.test import override_settings,
|
|
7
|
+
from django.test import override_settings, tag
|
|
5
8
|
|
|
6
9
|
from nautobot.core.templatetags import helpers
|
|
10
|
+
from nautobot.core.testing import TestCase
|
|
7
11
|
from nautobot.dcim import models
|
|
8
12
|
from nautobot.ipam.models import VLAN
|
|
9
13
|
|
|
10
|
-
from example_app.models import AnotherExampleModel, ExampleModel
|
|
11
|
-
|
|
12
14
|
|
|
13
15
|
class NautobotTemplatetagsHelperTest(TestCase):
|
|
14
16
|
def test_hyperlinked_object(self):
|
|
@@ -132,6 +134,7 @@ class NautobotTemplatetagsHelperTest(TestCase):
|
|
|
132
134
|
)
|
|
133
135
|
self.assertEqual("utf8:\n- 😀😀\n- 😀\n", helpers.render_yaml({"utf8": ["😀😀", "😀"]}, False))
|
|
134
136
|
|
|
137
|
+
@tag("example_app")
|
|
135
138
|
def test_meta(self):
|
|
136
139
|
location = models.Location.objects.first()
|
|
137
140
|
|
|
@@ -139,31 +142,42 @@ class NautobotTemplatetagsHelperTest(TestCase):
|
|
|
139
142
|
self.assertEqual(helpers.meta(models.Location, "app_label"), "dcim")
|
|
140
143
|
self.assertEqual(helpers.meta(location, "not_present"), "")
|
|
141
144
|
|
|
145
|
+
from example_app.models import ExampleModel
|
|
146
|
+
|
|
142
147
|
self.assertEqual(helpers.meta(ExampleModel, "app_label"), "example_app")
|
|
143
148
|
|
|
149
|
+
@tag("example_app")
|
|
144
150
|
def test_viewname(self):
|
|
145
151
|
location = models.Location.objects.first()
|
|
146
152
|
|
|
147
153
|
self.assertEqual(helpers.viewname(location, "edit"), "dcim:location_edit")
|
|
148
154
|
self.assertEqual(helpers.viewname(models.Location, "test"), "dcim:location_test")
|
|
149
155
|
|
|
156
|
+
from example_app.models import ExampleModel
|
|
157
|
+
|
|
150
158
|
self.assertEqual(helpers.viewname(ExampleModel, "edit"), "plugins:example_app:examplemodel_edit")
|
|
151
159
|
|
|
160
|
+
@tag("example_app")
|
|
152
161
|
def test_validated_viewname(self):
|
|
153
162
|
location = models.Location.objects.first()
|
|
154
163
|
|
|
155
164
|
self.assertEqual(helpers.validated_viewname(location, "list"), "dcim:location_list")
|
|
156
165
|
self.assertIsNone(helpers.validated_viewname(models.Location, "notvalid"))
|
|
157
166
|
|
|
167
|
+
from example_app.models import ExampleModel
|
|
168
|
+
|
|
158
169
|
self.assertEqual(helpers.validated_viewname(ExampleModel, "list"), "plugins:example_app:examplemodel_list")
|
|
159
170
|
self.assertIsNone(helpers.validated_viewname(ExampleModel, "notvalid"))
|
|
160
171
|
|
|
172
|
+
@tag("example_app")
|
|
161
173
|
def test_validated_api_viewname(self):
|
|
162
174
|
location = models.Location.objects.first()
|
|
163
175
|
|
|
164
176
|
self.assertEqual(helpers.validated_api_viewname(location, "list"), "dcim-api:location-list")
|
|
165
177
|
self.assertIsNone(helpers.validated_api_viewname(models.Location, "notvalid"))
|
|
166
178
|
|
|
179
|
+
from example_app.models import ExampleModel
|
|
180
|
+
|
|
167
181
|
self.assertEqual(
|
|
168
182
|
helpers.validated_api_viewname(ExampleModel, "list"), "plugins-api:example_app-api:examplemodel-list"
|
|
169
183
|
)
|
|
@@ -201,16 +215,6 @@ class NautobotTemplatetagsHelperTest(TestCase):
|
|
|
201
215
|
self.assertEqual(helpers.percentage(2, 10), 20)
|
|
202
216
|
self.assertEqual(helpers.percentage(10, 3), 333)
|
|
203
217
|
|
|
204
|
-
def test_get_docs_url(self):
|
|
205
|
-
self.assertTrue(callable(helpers.get_docs_url))
|
|
206
|
-
location = models.Location.objects.first()
|
|
207
|
-
self.assertEqual(helpers.get_docs_url(location), static("docs/user-guide/core-data-model/dcim/location.html"))
|
|
208
|
-
example_model = ExampleModel.objects.create(name="test", number=1)
|
|
209
|
-
self.assertEqual(helpers.get_docs_url(example_model), static("example_app/docs/models/examplemodel.html"))
|
|
210
|
-
# AnotherExampleModel does not have documentation.
|
|
211
|
-
another_model = AnotherExampleModel.objects.create(name="test", number=1)
|
|
212
|
-
self.assertIsNone(helpers.get_docs_url(another_model))
|
|
213
|
-
|
|
214
218
|
def test_has_perms(self):
|
|
215
219
|
self.assertTrue(callable(helpers.has_perms))
|
|
216
220
|
# TODO add unit tests for has_perms
|
|
@@ -265,6 +269,7 @@ class NautobotTemplatetagsHelperTest(TestCase):
|
|
|
265
269
|
# Assert when obj is None
|
|
266
270
|
self.assertEqual(helpers.hyperlinked_object_with_color(obj=None), '<span class="text-secondary">—</span>')
|
|
267
271
|
|
|
272
|
+
@tag("example_app")
|
|
268
273
|
@override_settings(BANNER_TOP="¡Hola, mundo!")
|
|
269
274
|
@override_config(example_app__SAMPLE_VARIABLE="Testing")
|
|
270
275
|
def test_settings_or_config(self):
|
|
@@ -338,3 +343,38 @@ class NautobotTemplatetagsHelperTest(TestCase):
|
|
|
338
343
|
"-85 dBm",
|
|
339
344
|
)
|
|
340
345
|
self.assertEqual(helpers.dbm(None), helpers.placeholder(None))
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
@tag("test")
|
|
349
|
+
class NautobotStaticDocsTestCase(StaticLiveServerTestCase):
|
|
350
|
+
@tag("example_app")
|
|
351
|
+
def test_get_docs_url(self):
|
|
352
|
+
self.assertTrue(callable(helpers.get_docs_url))
|
|
353
|
+
location_type = models.LocationType.objects.create(name="Some Location Type")
|
|
354
|
+
self.assertEqual(
|
|
355
|
+
helpers.get_docs_url(location_type), static("docs/user-guide/core-data-model/dcim/locationtype.html")
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
from example_app.models import AnotherExampleModel, ExampleModel
|
|
359
|
+
|
|
360
|
+
example_model = ExampleModel.objects.create(name="test", number=1)
|
|
361
|
+
self.assertEqual(helpers.get_docs_url(example_model), "/docs/example-app/models/examplemodel.html")
|
|
362
|
+
# AnotherExampleModel does not have documentation.
|
|
363
|
+
another_model = AnotherExampleModel.objects.create(name="test", number=1)
|
|
364
|
+
self.assertIsNone(helpers.get_docs_url(another_model))
|
|
365
|
+
|
|
366
|
+
@tag("example_app")
|
|
367
|
+
@mock.patch("nautobot.core.templatetags.helpers.find", return_value=False)
|
|
368
|
+
@mock.patch("nautobot.core.templatetags.helpers.resources.files", side_effect=ModuleNotFoundError)
|
|
369
|
+
def test_get_docs_url_module_not_found_and_no_static_file(self, mock_files, mock_find):
|
|
370
|
+
# Force `resources.files()` to raise ModuleNotFoundError to simulate a plugin
|
|
371
|
+
# that is listed in settings.PLUGINS but doesn't actually exist on disk.
|
|
372
|
+
# This ensures the `except ModuleNotFoundError` branch is covered.
|
|
373
|
+
from example_app.models import ExampleModel
|
|
374
|
+
|
|
375
|
+
example_model = ExampleModel.objects.create(name="test", number=1)
|
|
376
|
+
result = helpers.get_docs_url(example_model)
|
|
377
|
+
self.assertIsNone(result)
|
|
378
|
+
|
|
379
|
+
mock_files.assert_called_once()
|
|
380
|
+
mock_find.assert_called_once()
|
|
@@ -65,7 +65,7 @@ class NautobotTemplatetagsUIComponentsTest(TestCase):
|
|
|
65
65
|
{
|
|
66
66
|
"list_url": "home",
|
|
67
67
|
"title": "New Home",
|
|
68
|
-
"
|
|
68
|
+
"detail": True,
|
|
69
69
|
"breadcrumbs": Breadcrumbs(),
|
|
70
70
|
}
|
|
71
71
|
)
|
|
@@ -98,7 +98,7 @@ class NautobotTemplatetagsUIComponentsTest(TestCase):
|
|
|
98
98
|
{
|
|
99
99
|
"list_url": "home",
|
|
100
100
|
"title": "New Home",
|
|
101
|
-
"
|
|
101
|
+
"detail": True,
|
|
102
102
|
"breadcrumbs": Breadcrumbs(),
|
|
103
103
|
}
|
|
104
104
|
)
|
|
@@ -126,7 +126,7 @@ class NautobotTemplatetagsUIComponentsTest(TestCase):
|
|
|
126
126
|
{
|
|
127
127
|
"list_url": "home",
|
|
128
128
|
"title": "New Home",
|
|
129
|
-
"
|
|
129
|
+
"detail": True,
|
|
130
130
|
"breadcrumbs": Breadcrumbs(),
|
|
131
131
|
}
|
|
132
132
|
)
|
|
@@ -152,7 +152,7 @@ class NautobotTemplatetagsUIComponentsTest(TestCase):
|
|
|
152
152
|
{
|
|
153
153
|
"list_url": "home",
|
|
154
154
|
"title": "New Home",
|
|
155
|
-
"
|
|
155
|
+
"detail": True,
|
|
156
156
|
"breadcrumbs": Breadcrumbs(),
|
|
157
157
|
}
|
|
158
158
|
)
|
|
@@ -13,8 +13,21 @@ class TestInvalidateMaxTreeDepthSignal(TestCase):
|
|
|
13
13
|
# Ensure that the max_depth hasn't already been cached
|
|
14
14
|
Location.objects.__dict__.pop("max_depth", None)
|
|
15
15
|
location = Location.objects.first()
|
|
16
|
-
|
|
16
|
+
|
|
17
|
+
with CaptureQueriesContext(connection) as ctx:
|
|
17
18
|
location.save()
|
|
19
|
+
captured_tree_cte_queries = [
|
|
20
|
+
query["sql"] for query in ctx.captured_queries if "WITH RECURSIVE" in query["sql"]
|
|
21
|
+
]
|
|
22
|
+
allowed_number_of_tree_queries = 0 # We don't expect any tree queries to be run
|
|
23
|
+
_query_separator = "\n" + ("-" * 10) + "\n" + "NEXT QUERY" + "\n" + ("-" * 10)
|
|
24
|
+
self.assertEqual(
|
|
25
|
+
len(captured_tree_cte_queries),
|
|
26
|
+
allowed_number_of_tree_queries,
|
|
27
|
+
f"The CTE tree was calculated a different number of times ({len(captured_tree_cte_queries)})"
|
|
28
|
+
f" than allowed ({allowed_number_of_tree_queries})."
|
|
29
|
+
f" The following queries were used:\n{_query_separator.join(captured_tree_cte_queries)}",
|
|
30
|
+
)
|
|
18
31
|
|
|
19
32
|
|
|
20
33
|
class QuerySetAncestorTests(TestCase):
|
nautobot/core/tests/test_ui.py
CHANGED
|
@@ -446,7 +446,7 @@ class ObjectDetailContentExtraTabsTest(TestCase):
|
|
|
446
446
|
self.factory = RequestFactory()
|
|
447
447
|
self.request = self.factory.get("/")
|
|
448
448
|
self.request.user = self.user
|
|
449
|
-
self.default_tabs_id = ["main", "advanced", "contacts", "dynamic_groups", "object_metadata"]
|
|
449
|
+
self.default_tabs_id = ["main", "advanced", "contacts", "dynamic_groups", "object_metadata", "data_compliance"]
|
|
450
450
|
|
|
451
451
|
def test_default_extra_tabs_exist(self):
|
|
452
452
|
"""
|