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
nautobot/core/testing/api.py
CHANGED
|
@@ -17,6 +17,7 @@ from rest_framework import serializers, status
|
|
|
17
17
|
from rest_framework.relations import ManyRelatedField
|
|
18
18
|
from rest_framework.test import APITransactionTestCase as _APITransactionTestCase
|
|
19
19
|
|
|
20
|
+
from nautobot.core import constants
|
|
20
21
|
from nautobot.core.api.utils import get_serializer_for_model
|
|
21
22
|
from nautobot.core.models import fields as core_fields
|
|
22
23
|
from nautobot.core.models.tree_queries import TreeModel
|
|
@@ -294,8 +295,9 @@ class APIViewTestCases:
|
|
|
294
295
|
m2m_fields = self.get_m2m_fields()
|
|
295
296
|
self.add_permissions(f"{self.model._meta.app_label}.view_{self.model._meta.model_name}")
|
|
296
297
|
list_url = f"{self._get_list_url()}?depth=0"
|
|
298
|
+
# With exclude_m2m query parameter set to False
|
|
297
299
|
with CaptureQueriesContext(connections[DEFAULT_DB_ALIAS]) as cqc:
|
|
298
|
-
response = self.client.get(list_url, **self.header)
|
|
300
|
+
response = self.client.get(list_url + "&exclude_m2m=false", **self.header)
|
|
299
301
|
base_num_queries = len(cqc)
|
|
300
302
|
|
|
301
303
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
@@ -340,9 +342,9 @@ class APIViewTestCases:
|
|
|
340
342
|
app_label, model_name = object_type.split(".")
|
|
341
343
|
ContentType.objects.get(app_label=app_label, model=model_name)
|
|
342
344
|
|
|
343
|
-
|
|
345
|
+
# With exclude_m2m query parameter set to True
|
|
344
346
|
with CaptureQueriesContext(connections[DEFAULT_DB_ALIAS]) as cqc:
|
|
345
|
-
response = self.client.get(list_url, **self.header)
|
|
347
|
+
response = self.client.get(list_url + "&exclude_m2m=true", **self.header)
|
|
346
348
|
|
|
347
349
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
348
350
|
self.assertIsInstance(response.data, dict)
|
|
@@ -387,8 +389,9 @@ class APIViewTestCases:
|
|
|
387
389
|
m2m_fields = self.get_m2m_fields()
|
|
388
390
|
self.add_permissions(f"{self.model._meta.app_label}.view_{self.model._meta.model_name}")
|
|
389
391
|
list_url = f"{self._get_list_url()}?depth=1"
|
|
392
|
+
# With exclude_m2m query parameter set to False
|
|
390
393
|
with CaptureQueriesContext(connections[DEFAULT_DB_ALIAS]) as cqc:
|
|
391
|
-
response = self.client.get(list_url, **self.header)
|
|
394
|
+
response = self.client.get(list_url + "&exclude_m2m=false", **self.header)
|
|
392
395
|
base_num_queries = len(cqc)
|
|
393
396
|
|
|
394
397
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
@@ -420,9 +423,9 @@ class APIViewTestCases:
|
|
|
420
423
|
self.assertTrue(is_uuid(response_data[field]["id"]))
|
|
421
424
|
self.assertGreater(len(response_data[field].keys()), 3, response_data[field])
|
|
422
425
|
|
|
423
|
-
|
|
426
|
+
# With exclude_m2m query parameter set to True
|
|
424
427
|
with CaptureQueriesContext(connections[DEFAULT_DB_ALIAS]) as cqc:
|
|
425
|
-
response = self.client.get(list_url, **self.header)
|
|
428
|
+
response = self.client.get(list_url + "&exclude_m2m=true", **self.header)
|
|
426
429
|
|
|
427
430
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
428
431
|
self.assertIsInstance(response.data, dict)
|
|
@@ -458,6 +461,54 @@ class APIViewTestCases:
|
|
|
458
461
|
self.assertNotIn(field, response_data)
|
|
459
462
|
# TODO: we should assert that all other fields are still present, but there's a few corner cases...
|
|
460
463
|
|
|
464
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
|
465
|
+
def test_list_objects_exclude_m2m(self):
|
|
466
|
+
"""
|
|
467
|
+
GET a list of objects with or without the "exclude_m2m" parameter.
|
|
468
|
+
|
|
469
|
+
With exclude_m2m query parameter set to True, we should see no many-to-many fields.
|
|
470
|
+
With exclude_m2m query parameter set to False, we should see all many-to-many fields.
|
|
471
|
+
With exclude_m2m query parameter not set, we should only see the default many-to-many fields.
|
|
472
|
+
"""
|
|
473
|
+
m2m_fields = self.get_m2m_fields()
|
|
474
|
+
if not m2m_fields:
|
|
475
|
+
self.skipTest("No many-to-many fields to test")
|
|
476
|
+
self.add_permissions(f"{self.model._meta.app_label}.view_{self.model._meta.model_name}")
|
|
477
|
+
list_url = f"{self._get_list_url()}"
|
|
478
|
+
|
|
479
|
+
# With exclude_m2m query parameter not set
|
|
480
|
+
response = self.client.get(list_url, **self.header)
|
|
481
|
+
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
482
|
+
self.assertIsInstance(response.data, dict)
|
|
483
|
+
self.assertIn("results", response.data)
|
|
484
|
+
|
|
485
|
+
for response_data in response.data["results"]:
|
|
486
|
+
for field in m2m_fields:
|
|
487
|
+
if field in constants.DEFAULT_M2M_FIELDS:
|
|
488
|
+
self.assertIn(field, response_data)
|
|
489
|
+
self.assertIsInstance(response_data[field], list)
|
|
490
|
+
else:
|
|
491
|
+
self.assertNotIn(field, response_data)
|
|
492
|
+
|
|
493
|
+
# With exclude_m2m query parameter set to True
|
|
494
|
+
response = self.client.get(list_url + "?exclude_m2m=true", **self.header)
|
|
495
|
+
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
496
|
+
self.assertIsInstance(response.data, dict)
|
|
497
|
+
self.assertIn("results", response.data)
|
|
498
|
+
for response_data in response.data["results"]:
|
|
499
|
+
for field in m2m_fields:
|
|
500
|
+
self.assertNotIn(field, response_data)
|
|
501
|
+
|
|
502
|
+
# With exclude_m2m query parameter set to False
|
|
503
|
+
response = self.client.get(list_url + "?exclude_m2m=false", **self.header)
|
|
504
|
+
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
505
|
+
self.assertIsInstance(response.data, dict)
|
|
506
|
+
self.assertIn("results", response.data)
|
|
507
|
+
for response_data in response.data["results"]:
|
|
508
|
+
for field in m2m_fields:
|
|
509
|
+
self.assertIn(field, response_data)
|
|
510
|
+
self.assertIsInstance(response_data[field], list)
|
|
511
|
+
|
|
461
512
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
|
462
513
|
def test_list_objects_without_permission(self):
|
|
463
514
|
"""
|
|
@@ -1076,12 +1127,20 @@ class APIViewTestCases:
|
|
|
1076
1127
|
|
|
1077
1128
|
self.assertIn("actions", data)
|
|
1078
1129
|
|
|
1079
|
-
# Grab any field that has choices defined (fields with enums)
|
|
1130
|
+
# Grab any field that has choices defined (fields with enums including child fields with enums)
|
|
1080
1131
|
field_choices = {}
|
|
1081
1132
|
if "POST" in data["actions"]:
|
|
1082
|
-
field_choices = {
|
|
1133
|
+
field_choices = {
|
|
1134
|
+
k
|
|
1135
|
+
for k, v in data["actions"]["POST"].items()
|
|
1136
|
+
if "choices" in v or ("child" in v and "choices" in v["child"])
|
|
1137
|
+
}
|
|
1083
1138
|
elif "PUT" in data["actions"]:
|
|
1084
|
-
field_choices = {
|
|
1139
|
+
field_choices = {
|
|
1140
|
+
k
|
|
1141
|
+
for k, v in data["actions"]["PUT"].items()
|
|
1142
|
+
if "choices" in v or ("child" in v and "choices" in v["child"])
|
|
1143
|
+
}
|
|
1085
1144
|
else:
|
|
1086
1145
|
self.fail(f"Neither PUT nor POST are available actions in: {data['actions']}")
|
|
1087
1146
|
|
nautobot/core/testing/filters.py
CHANGED
|
@@ -21,7 +21,6 @@ from nautobot.core.filters import (
|
|
|
21
21
|
)
|
|
22
22
|
from nautobot.core.models.generics import PrimaryModel
|
|
23
23
|
from nautobot.core.testing import views
|
|
24
|
-
from nautobot.core.utils.deprecation import class_deprecated_in_favor_of
|
|
25
24
|
from nautobot.extras.models import Contact, ContactAssociation, Role, Status, Tag, Team
|
|
26
25
|
from nautobot.tenancy import models
|
|
27
26
|
|
|
@@ -435,28 +434,6 @@ class FilterTestCases:
|
|
|
435
434
|
),
|
|
436
435
|
)
|
|
437
436
|
|
|
438
|
-
# Test cases should just explicitly include `name` as a generic_filter_tests entry
|
|
439
|
-
@class_deprecated_in_favor_of(FilterTestCase) # pylint: disable=undefined-variable
|
|
440
|
-
class NameOnlyFilterTestCase(FilterTestCase):
|
|
441
|
-
"""Add simple tests for filtering by name."""
|
|
442
|
-
|
|
443
|
-
def test_filters_generic(self):
|
|
444
|
-
if not any(test[0] == "name" for test in self.generic_filter_tests):
|
|
445
|
-
self.generic_filter_tests = (["name"], *self.generic_filter_tests)
|
|
446
|
-
super().test_filters_generic()
|
|
447
|
-
|
|
448
|
-
# Test cases should just explicitly include `name` and `slug` as generic_filter_tests entries
|
|
449
|
-
@class_deprecated_in_favor_of(FilterTestCase) # pylint: disable=undefined-variable
|
|
450
|
-
class NameSlugFilterTestCase(FilterTestCase):
|
|
451
|
-
"""Add simple tests for filtering by name and by slug."""
|
|
452
|
-
|
|
453
|
-
def test_filters_generic(self):
|
|
454
|
-
if not any(test[0] == "slug" for test in self.generic_filter_tests):
|
|
455
|
-
self.generic_filter_tests = (["slug"], *self.generic_filter_tests)
|
|
456
|
-
if not any(test[0] == "name" for test in self.generic_filter_tests):
|
|
457
|
-
self.generic_filter_tests = (["name"], *self.generic_filter_tests)
|
|
458
|
-
super().test_filters_generic()
|
|
459
|
-
|
|
460
437
|
class TenancyFilterTestCaseMixin(views.TestCase):
|
|
461
438
|
"""Add test cases for tenant and tenant-group filters."""
|
|
462
439
|
|
|
@@ -7,7 +7,9 @@ from django.db.models import Model
|
|
|
7
7
|
from django.test import override_settings, tag
|
|
8
8
|
from django.urls import reverse
|
|
9
9
|
from django.utils.functional import classproperty
|
|
10
|
+
from selenium.webdriver.common.by import By
|
|
10
11
|
from selenium.webdriver.common.keys import Keys
|
|
12
|
+
from selenium.webdriver.support.expected_conditions import element_to_be_clickable
|
|
11
13
|
from selenium.webdriver.support.wait import WebDriverWait
|
|
12
14
|
from splinter.browser import Browser
|
|
13
15
|
from splinter.exceptions import ElementDoesNotExist
|
|
@@ -94,7 +96,7 @@ class ObjectsListMixin:
|
|
|
94
96
|
"""
|
|
95
97
|
Click add item button on top of the items table list.
|
|
96
98
|
"""
|
|
97
|
-
self.click_button("#
|
|
99
|
+
self.click_button("#add-button")
|
|
98
100
|
|
|
99
101
|
def click_table_link(self, row=1, column=2):
|
|
100
102
|
"""By default, tries to click column next to checkbox to go to the details page."""
|
|
@@ -129,7 +131,7 @@ class ObjectDetailsMixin:
|
|
|
129
131
|
By default, it's not using the exact match, because on the UI we're often adding
|
|
130
132
|
additional tags, relationships or units.
|
|
131
133
|
"""
|
|
132
|
-
panel_xpath = f'//*[@id="main"]//div[@class
|
|
134
|
+
panel_xpath = f'//*[@id="main"]//div[contains(@class, "card-header") and contains(normalize-space(), "{panel_label}")]/following-sibling::div[contains(@class, "collapse")]/table'
|
|
133
135
|
value = self.browser.find_by_xpath(f'{panel_xpath}//td[text()="{field_label}"]/following-sibling::td[1]').text
|
|
134
136
|
|
|
135
137
|
if exact_match:
|
|
@@ -144,7 +146,7 @@ class ObjectDetailsMixin:
|
|
|
144
146
|
TODO: remove after all panels will be moved to UI Components Framework or new Bootstrap 5 templates.
|
|
145
147
|
"""
|
|
146
148
|
panel_xpath = f'//*[@id="main"]//div[@class="card-header"][contains(normalize-space(), "{panel_label}")]'
|
|
147
|
-
expand_button_xpath = f"{panel_xpath}/button[normalize-space()='Expand All']"
|
|
149
|
+
expand_button_xpath = f"{panel_xpath}/button[normalize-space()='Expand All Groups']"
|
|
148
150
|
expand_button = self.browser.find_by_xpath(expand_button_xpath)
|
|
149
151
|
if not expand_button.is_empty():
|
|
150
152
|
expand_button.click()
|
|
@@ -374,10 +376,12 @@ class SeleniumTestCase(StaticLiveServerTestCase, testing.NautobotTestCaseMixin):
|
|
|
374
376
|
sidenav_button = self.browser.find_by_xpath(f"{section_xpath}/button", wait_time=5)
|
|
375
377
|
if not sidenav_button["aria-expanded"] == "true":
|
|
376
378
|
sidenav_button.click()
|
|
377
|
-
child_menu_xpath = f"{section_xpath}/div[@class='nb-sidenav-flyout']//a[@class
|
|
379
|
+
child_menu_xpath = f"{section_xpath}/div[@class='nb-sidenav-flyout']//a[contains(@class, 'nb-sidenav-link') and normalize-space()='{child_menu_name}']"
|
|
378
380
|
child_menu = self.browser.find_by_xpath(child_menu_xpath, wait_time=5)
|
|
381
|
+
old_url = self.browser.url
|
|
379
382
|
child_menu.click()
|
|
380
383
|
|
|
384
|
+
WebDriverWait(self.browser, 30).until(lambda driver: driver.url != old_url)
|
|
381
385
|
# Wait for body element to appear
|
|
382
386
|
self.assertTrue(self.browser.is_element_present_by_tag("body", wait_time=5), "Page failed to load")
|
|
383
387
|
|
|
@@ -389,7 +393,7 @@ class SeleniumTestCase(StaticLiveServerTestCase, testing.NautobotTestCaseMixin):
|
|
|
389
393
|
add_button.click()
|
|
390
394
|
|
|
391
395
|
# Wait for body element to appear
|
|
392
|
-
self.assertTrue(self.browser.
|
|
396
|
+
self.assertTrue(self.browser.is_element_present_by_name("_create", wait_time=5), "Page failed to load")
|
|
393
397
|
|
|
394
398
|
def click_edit_form_create_button(self):
|
|
395
399
|
"""
|
|
@@ -399,7 +403,7 @@ class SeleniumTestCase(StaticLiveServerTestCase, testing.NautobotTestCaseMixin):
|
|
|
399
403
|
add_button.click()
|
|
400
404
|
|
|
401
405
|
# Wait for body element to appear
|
|
402
|
-
self.assertTrue(self.browser.
|
|
406
|
+
self.assertTrue(self.browser.is_element_present_by_css(".alert-success", wait_time=5), "Page failed to load")
|
|
403
407
|
|
|
404
408
|
def _fill_select2_field(self, field_name, value, search_box_class=None):
|
|
405
409
|
"""
|
|
@@ -453,17 +457,26 @@ class SeleniumTestCase(StaticLiveServerTestCase, testing.NautobotTestCaseMixin):
|
|
|
453
457
|
search_box.first.type(Keys.ENTER)
|
|
454
458
|
|
|
455
459
|
def click_button(self, query_selector):
|
|
456
|
-
|
|
460
|
+
self.browser.is_element_present_by_css(query_selector, wait_time=5)
|
|
457
461
|
# Button might be visible but on the edge and then impossible to click due to vertical/horizontal scrolls
|
|
458
|
-
self.browser.execute_script(
|
|
462
|
+
self.browser.execute_script(
|
|
463
|
+
f"document.querySelector('{query_selector}').scrollIntoView({{ behavior: 'instant', block: 'start' }});"
|
|
464
|
+
)
|
|
465
|
+
# Scrolling may be asynchronous, wait until it's actually clickable.
|
|
466
|
+
WebDriverWait(self.browser.driver, 30).until(element_to_be_clickable((By.CSS_SELECTOR, query_selector)))
|
|
467
|
+
btn = self.browser.find_by_css(query_selector)
|
|
459
468
|
btn.click()
|
|
460
469
|
|
|
461
470
|
def fill_input(self, input_name, input_value):
|
|
462
471
|
"""
|
|
463
472
|
Helper function to fill an input field. Solves issue with element could not be scrolled into view for some pages.
|
|
464
473
|
"""
|
|
465
|
-
|
|
466
|
-
self.browser.
|
|
474
|
+
self.browser.is_element_present_by_name(input_name, wait_time=5)
|
|
475
|
+
element = self.browser.find_by_name(input_name)
|
|
476
|
+
self.browser.execute_script(
|
|
477
|
+
"arguments[0].scrollIntoView({ behavior: 'instant', block: 'start' });", element.first._element
|
|
478
|
+
)
|
|
479
|
+
element.is_visible(wait_time=5)
|
|
467
480
|
self.browser.execute_script("arguments[0].focus();", element.first._element)
|
|
468
481
|
self.browser.fill(input_name, input_value)
|
|
469
482
|
|
nautobot/core/testing/mixins.py
CHANGED
|
@@ -19,6 +19,7 @@ from nautobot.core.testing import utils
|
|
|
19
19
|
from nautobot.core.utils import permissions
|
|
20
20
|
from nautobot.extras import management, models as extras_models
|
|
21
21
|
from nautobot.extras.choices import JobResultStatusChoices
|
|
22
|
+
from nautobot.ipam.models import default_namespace_pk
|
|
22
23
|
from nautobot.users import models as users_models
|
|
23
24
|
|
|
24
25
|
# Use the proper swappable User model
|
|
@@ -81,6 +82,7 @@ class NautobotTestCaseMixin:
|
|
|
81
82
|
"""
|
|
82
83
|
super().tearDown()
|
|
83
84
|
cache.clear()
|
|
85
|
+
default_namespace_pk.set(None)
|
|
84
86
|
|
|
85
87
|
def prepare_instance(self, instance):
|
|
86
88
|
"""
|
nautobot/core/testing/views.py
CHANGED
|
@@ -192,6 +192,10 @@ class ViewTestCases:
|
|
|
192
192
|
with CaptureQueriesContext(connection) as capture_queries_context:
|
|
193
193
|
response = self.client.get(instance.get_absolute_url())
|
|
194
194
|
# The object's display name or string representation should appear in the response body
|
|
195
|
+
# TODO: some models (e.g. JobResult) intentionally do NOT display the full `.display` in the detail view,
|
|
196
|
+
# but only use the `.name` or `str()`.
|
|
197
|
+
# This check will always pass in the case where the Example App is installed, because of the banner
|
|
198
|
+
# it adds, but can/should/may? fail otherwise.
|
|
195
199
|
self.assertBodyContains(response, escape(getattr(instance, "display", str(instance))))
|
|
196
200
|
|
|
197
201
|
# If any Relationships are defined, they should appear in the response
|
|
@@ -1,44 +1,48 @@
|
|
|
1
|
+
from django.test import tag
|
|
2
|
+
|
|
1
3
|
from nautobot.circuits.models import Circuit, Provider
|
|
2
4
|
from nautobot.core.testing.integration import SeleniumTestCase
|
|
3
5
|
from nautobot.dcim.models import Location, PowerFeed, PowerPanel
|
|
4
6
|
from nautobot.tenancy.models import Tenant
|
|
5
7
|
|
|
6
|
-
from example_app.models import ExampleModel
|
|
7
|
-
|
|
8
8
|
|
|
9
|
+
@tag("example_app")
|
|
9
10
|
class AppHomeTestCase(SeleniumTestCase):
|
|
10
11
|
"""Integration test the Example App homepage extensions."""
|
|
11
12
|
|
|
12
|
-
layout = {
|
|
13
|
-
"Organization": {
|
|
14
|
-
"Locations": {"model": Location, "permission": "dcim.view_location"},
|
|
15
|
-
"Example Models": {"model": ExampleModel, "permission": "example_app.view_examplemodel"},
|
|
16
|
-
"Tenants": {"model": Tenant, "permission": "tenancy.view_tenant"},
|
|
17
|
-
},
|
|
18
|
-
"Example App Standard Panel": {
|
|
19
|
-
"Example App Custom Item": {"permission": "example_app.view_examplemodel"},
|
|
20
|
-
},
|
|
21
|
-
"Power": {
|
|
22
|
-
"Power Feeds": {"model": PowerFeed, "permission": "dcim.view_powerfeed"},
|
|
23
|
-
"Power Panel": {"model": PowerPanel, "permission": "dcim.view_powerpanel"},
|
|
24
|
-
},
|
|
25
|
-
"Circuits": {
|
|
26
|
-
"Providers": {"model": Provider, "permission": "circuits.view_provider"},
|
|
27
|
-
"Circuits": {"model": Circuit, "permission": "circuits.view_circuit"},
|
|
28
|
-
},
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
custom_panel_examplemodel = {
|
|
32
|
-
"name": "Example App Custom Panel",
|
|
33
|
-
"items": [
|
|
34
|
-
"Example 1",
|
|
35
|
-
"Example 2",
|
|
36
|
-
"Example 3",
|
|
37
|
-
],
|
|
38
|
-
}
|
|
39
|
-
|
|
40
13
|
def setUp(self):
|
|
41
14
|
super().setUp()
|
|
15
|
+
|
|
16
|
+
from example_app.models import ExampleModel
|
|
17
|
+
|
|
18
|
+
self.layout = {
|
|
19
|
+
"Organization": {
|
|
20
|
+
"Locations": {"model": Location, "permission": "dcim.view_location"},
|
|
21
|
+
"Example Models": {"model": ExampleModel, "permission": "example_app.view_examplemodel"},
|
|
22
|
+
"Tenants": {"model": Tenant, "permission": "tenancy.view_tenant"},
|
|
23
|
+
},
|
|
24
|
+
"Example App Standard Panel": {
|
|
25
|
+
"Example App Custom Item": {"permission": "example_app.view_examplemodel"},
|
|
26
|
+
},
|
|
27
|
+
"Power": {
|
|
28
|
+
"Power Feeds": {"model": PowerFeed, "permission": "dcim.view_powerfeed"},
|
|
29
|
+
"Power Panel": {"model": PowerPanel, "permission": "dcim.view_powerpanel"},
|
|
30
|
+
},
|
|
31
|
+
"Circuits": {
|
|
32
|
+
"Providers": {"model": Provider, "permission": "circuits.view_provider"},
|
|
33
|
+
"Circuits": {"model": Circuit, "permission": "circuits.view_circuit"},
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
self.custom_panel_examplemodel = {
|
|
38
|
+
"name": "Example App Custom Panel",
|
|
39
|
+
"items": [
|
|
40
|
+
"Example 1",
|
|
41
|
+
"Example 2",
|
|
42
|
+
"Example 3",
|
|
43
|
+
],
|
|
44
|
+
}
|
|
45
|
+
|
|
42
46
|
self.login(self.user.username, self.password)
|
|
43
47
|
|
|
44
48
|
def tearDown(self):
|
nautobot/core/tests/runner.py
CHANGED
|
@@ -39,11 +39,12 @@ class NautobotParallelTestSuite(ParallelTestSuite):
|
|
|
39
39
|
|
|
40
40
|
class NautobotTestRunner(DiscoverRunner):
|
|
41
41
|
"""
|
|
42
|
-
Custom test runner that excludes (slow) integration and migration tests by default.
|
|
42
|
+
Custom test runner that excludes (slow) integration and migration tests by default among others.
|
|
43
43
|
|
|
44
44
|
This test runner is aware of our use of the "integration" tag and only runs integration tests if
|
|
45
45
|
explicitly passed in with `nautobot-server test --tag integration`.
|
|
46
46
|
Similarly, it only runs migration tests if explicitly called with `--tag migration_test`.
|
|
47
|
+
Similarly, it only runs tests that require the example app(s) if those are present in settings.PLUGINS.
|
|
47
48
|
|
|
48
49
|
By Nautobot convention, integration tests must be tagged with "integration". The base
|
|
49
50
|
`nautobot.core.testing.integration.SeleniumTestCase` has this tag, therefore any test cases
|
|
@@ -58,6 +59,10 @@ class NautobotTestRunner(DiscoverRunner):
|
|
|
58
59
|
parallel_test_suite = NautobotParallelTestSuite
|
|
59
60
|
|
|
60
61
|
exclude_tags = ["integration", "migration_test"]
|
|
62
|
+
if "example_app" not in settings.PLUGINS:
|
|
63
|
+
exclude_tags.append("example_app")
|
|
64
|
+
if "example_app_with_view_override" not in settings.PLUGINS:
|
|
65
|
+
exclude_tags.append("example_app_with_view_override")
|
|
61
66
|
|
|
62
67
|
@classmethod
|
|
63
68
|
def add_arguments(cls, parser):
|
|
@@ -157,6 +162,9 @@ class NautobotTestRunner(DiscoverRunner):
|
|
|
157
162
|
db_command = [*command, "--database", alias]
|
|
158
163
|
call_command(*db_command)
|
|
159
164
|
|
|
165
|
+
# Calculate membership for the dynamic groups that were generated by the factories/fixtures
|
|
166
|
+
call_command("refresh_dynamic_group_member_caches")
|
|
167
|
+
|
|
160
168
|
if self.parallel > 1:
|
|
161
169
|
for index in range(self.parallel):
|
|
162
170
|
with time_keeper.timed(f" Cloning '{alias}'"):
|
nautobot/core/tests/test_api.py
CHANGED
|
@@ -533,10 +533,12 @@ class ModelViewSetMixinTest(testing.APITestCase):
|
|
|
533
533
|
self.user.is_superuser = True
|
|
534
534
|
self.user.save()
|
|
535
535
|
|
|
536
|
-
#
|
|
536
|
+
# With exclude_m2m query parameter set to False
|
|
537
537
|
view = self.SimpleIPAddressViewSet()
|
|
538
538
|
view.action_map = {"get": "list"}
|
|
539
|
-
request = APIRequestFactory().get(
|
|
539
|
+
request = APIRequestFactory().get(
|
|
540
|
+
reverse("ipam-api:ipaddress-list"), headers=self.header, data={"exclude_m2m": False}
|
|
541
|
+
)
|
|
540
542
|
force_authenticate(request, user=self.user)
|
|
541
543
|
request = view.initialize_request(request)
|
|
542
544
|
view.setup(request)
|
|
@@ -561,7 +563,7 @@ class ModelViewSetMixinTest(testing.APITestCase):
|
|
|
561
563
|
list(instance.vm_interfaces.all())
|
|
562
564
|
list(instance.tags.all())
|
|
563
565
|
|
|
564
|
-
# With exclude_m2m query parameter
|
|
566
|
+
# With exclude_m2m query parameter set to True
|
|
565
567
|
view = self.SimpleIPAddressViewSet()
|
|
566
568
|
view.action_map = {"get": "list"}
|
|
567
569
|
request = APIRequestFactory().get(
|
|
@@ -296,8 +296,8 @@ class BreadcrumbsTestCase(TestCase):
|
|
|
296
296
|
breadcrumbs = Breadcrumbs()
|
|
297
297
|
|
|
298
298
|
# Should have defaults for list and details
|
|
299
|
-
self.assertEqual(len(breadcrumbs.items["list"]),
|
|
300
|
-
self.assertEqual(len(breadcrumbs.items["detail"]),
|
|
299
|
+
self.assertEqual(len(breadcrumbs.items["list"]), 0)
|
|
300
|
+
self.assertEqual(len(breadcrumbs.items["detail"]), 2)
|
|
301
301
|
|
|
302
302
|
# Verify adding items
|
|
303
303
|
new_item = BaseBreadcrumbItem()
|
|
@@ -306,7 +306,7 @@ class BreadcrumbsTestCase(TestCase):
|
|
|
306
306
|
self.assertEqual(len(breadcrumbs.items["list"]), 1)
|
|
307
307
|
self.assertEqual(breadcrumbs.items["list"][0], new_item)
|
|
308
308
|
|
|
309
|
-
self.assertEqual(len(breadcrumbs.items["detail"]),
|
|
309
|
+
self.assertEqual(len(breadcrumbs.items["detail"]), 1)
|
|
310
310
|
self.assertEqual(breadcrumbs.items["detail"][0], new_item)
|
|
311
311
|
|
|
312
312
|
self.assertEqual(len(breadcrumbs.items["custom_action"]), 1)
|
|
@@ -325,7 +325,7 @@ class BreadcrumbsTestCase(TestCase):
|
|
|
325
325
|
|
|
326
326
|
# Other defaults should still exist
|
|
327
327
|
self.assertIn("detail", breadcrumbs.items)
|
|
328
|
-
self.assertEqual(len(breadcrumbs.items["detail"]),
|
|
328
|
+
self.assertEqual(len(breadcrumbs.items["detail"]), 2)
|
|
329
329
|
|
|
330
330
|
def test_get_items_from_action_static_method(self):
|
|
331
331
|
"""Test the _get_items_from_action static method."""
|
|
@@ -355,7 +355,6 @@ class BreadcrumbsTestCase(TestCase):
|
|
|
355
355
|
breadcrumbs = Breadcrumbs()
|
|
356
356
|
expected_items = [
|
|
357
357
|
("/dcim/location-types/", "Location Types"),
|
|
358
|
-
(f"/dcim/location-types/{self.location_type.pk}/", str(self.location_type)),
|
|
359
358
|
]
|
|
360
359
|
|
|
361
360
|
# Test with an action that doesn't exist but detail=True
|
|
@@ -366,13 +365,13 @@ class BreadcrumbsTestCase(TestCase):
|
|
|
366
365
|
items = breadcrumbs.get_breadcrumbs_items(context)
|
|
367
366
|
|
|
368
367
|
# Should get 2 items from detail fallback
|
|
369
|
-
self.assertEqual(len(items),
|
|
368
|
+
self.assertEqual(len(items), 1)
|
|
370
369
|
self.assertEqual(items, expected_items)
|
|
371
370
|
|
|
372
371
|
def test_render_method(self):
|
|
373
372
|
"""Test the render method."""
|
|
374
373
|
breadcrumbs = Breadcrumbs()
|
|
375
|
-
context = Context({"
|
|
374
|
+
context = Context({"detail": True, "model": Device})
|
|
376
375
|
|
|
377
376
|
html = breadcrumbs.render(context)
|
|
378
377
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from django.test import override_settings, TestCase
|
|
2
2
|
|
|
3
3
|
from nautobot.core import checks
|
|
4
|
+
from nautobot.dcim.choices import DeviceUniquenessChoices
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
class CheckCoreSettingsTest(TestCase):
|
|
@@ -42,3 +43,30 @@ class CheckCoreSettingsTest(TestCase):
|
|
|
42
43
|
def test_check_maintenance_mode(self):
|
|
43
44
|
"""Error if MAINTENANCE_MODE is set and yet SESSION_ENGINE is still storing sessions in the db."""
|
|
44
45
|
self.assertEqual(checks.check_maintenance_mode(None), [checks.E005])
|
|
46
|
+
|
|
47
|
+
@override_settings(
|
|
48
|
+
DEVICE_NAME_AS_NATURAL_KEY=True,
|
|
49
|
+
)
|
|
50
|
+
def test_check_deprecated_device_name_as_natural_key(self):
|
|
51
|
+
"""Warn if DEVICE_NAME_AS_NATURAL_KEY is defined in settings."""
|
|
52
|
+
self.assertEqual(
|
|
53
|
+
checks.check_deprecated_device_name_as_natural_key(None),
|
|
54
|
+
[checks.W006],
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
@override_settings(
|
|
58
|
+
DEVICE_UNIQUENESS="invalid_value",
|
|
59
|
+
)
|
|
60
|
+
def test_check_invalid_device_uniqueness_value(self):
|
|
61
|
+
"""Warn if DEVICE_UNIQUENESS is set to an invalid value."""
|
|
62
|
+
self.assertEqual(
|
|
63
|
+
checks.check_valid_value_for_device_uniqueness(None),
|
|
64
|
+
[checks.W007],
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
@override_settings(
|
|
68
|
+
DEVICE_UNIQUENESS=DeviceUniquenessChoices.NAME,
|
|
69
|
+
)
|
|
70
|
+
def test_check_valid_device_uniqueness_value(self):
|
|
71
|
+
"""No warning if DEVICE_UNIQUENESS is set to a valid value."""
|
|
72
|
+
self.assertEqual(checks.check_valid_value_for_device_uniqueness(None), [])
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from nautobot.core.cli import migrate_deprecated_templates
|
|
2
|
+
from nautobot.core.testing import TestCase
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TestMigrateTemplates(TestCase):
|
|
6
|
+
def test_template_replacements(self):
|
|
7
|
+
"""Verify that all old templates are replaced by a single new template."""
|
|
8
|
+
audit_dict = {}
|
|
9
|
+
for new_template, old_templates in migrate_deprecated_templates.TEMPLATE_REPLACEMENTS.items():
|
|
10
|
+
for old_template in old_templates:
|
|
11
|
+
self.assertNotIn(old_template, audit_dict)
|
|
12
|
+
audit_dict[old_template] = new_template
|
|
13
|
+
|
|
14
|
+
def test_replace_template_references_no_change(self):
|
|
15
|
+
content = """
|
|
16
|
+
{% extends "base.html" %}
|
|
17
|
+
{% block content %}
|
|
18
|
+
<h1>Hello, World!</h1>
|
|
19
|
+
{% endblock %}
|
|
20
|
+
"""
|
|
21
|
+
replaced_content, was_updated = migrate_deprecated_templates.replace_template_references(content)
|
|
22
|
+
self.assertFalse(was_updated)
|
|
23
|
+
self.assertEqual(replaced_content, content)
|
|
24
|
+
|
|
25
|
+
def test_replace_template_references(self):
|
|
26
|
+
original_content = """
|
|
27
|
+
{% extends "generic/object_bulk_import.html" %}
|
|
28
|
+
{% block content %}
|
|
29
|
+
<h1>Hello, World!</h1>
|
|
30
|
+
{% endblock %}
|
|
31
|
+
"""
|
|
32
|
+
new_content = """
|
|
33
|
+
{% extends "generic/object_bulk_create.html" %}
|
|
34
|
+
{% block content %}
|
|
35
|
+
<h1>Hello, World!</h1>
|
|
36
|
+
{% endblock %}
|
|
37
|
+
"""
|
|
38
|
+
replaced_content, was_updated = migrate_deprecated_templates.replace_template_references(original_content)
|
|
39
|
+
self.assertTrue(was_updated)
|
|
40
|
+
self.assertEqual(replaced_content, new_content)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Test cases for nautobot.core.config module."""
|
|
2
2
|
|
|
3
3
|
from constance.test import override_config
|
|
4
|
-
from django.test import override_settings, TestCase
|
|
4
|
+
from django.test import override_settings, tag, TestCase
|
|
5
5
|
|
|
6
6
|
from nautobot.apps import config as app_config
|
|
7
7
|
from nautobot.core.utils import config
|
|
@@ -37,6 +37,7 @@ class GetSettingsOrConfigTestCase(TestCase):
|
|
|
37
37
|
self.assertRaises(AttributeError, config.get_settings_or_config, "FAKE_SETTING")
|
|
38
38
|
|
|
39
39
|
|
|
40
|
+
@tag("example_app")
|
|
40
41
|
class GetAppSettingsOrConfigTestCase(TestCase):
|
|
41
42
|
"""Test the get_app_settings_or_config() helper function."""
|
|
42
43
|
|