nautobot 3.0.0a3__py3-none-any.whl → 3.0.0rc1__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.
- nautobot/apps/choices.py +4 -0
- nautobot/apps/ui.py +4 -0
- nautobot/apps/utils.py +8 -0
- nautobot/circuits/tests/integration/test_circuits_bulk_operations.py +0 -3
- nautobot/circuits/views.py +6 -2
- nautobot/core/api/serializers.py +1 -1
- nautobot/core/api/urls.py +1 -0
- nautobot/core/api/views.py +4 -0
- nautobot/core/choices.py +1 -1
- nautobot/core/cli/bootstrap_v3_to_v5.py +36 -13
- nautobot/core/cli/migrate_deprecated_templates.py +36 -9
- nautobot/core/filters.py +4 -0
- nautobot/core/forms/__init__.py +2 -0
- nautobot/core/forms/widgets.py +21 -2
- nautobot/core/jobs/__init__.py +56 -0
- nautobot/core/management/commands/generate_test_data.py +3 -3
- nautobot/core/models/__init__.py +11 -0
- nautobot/core/models/utils.py +1 -1
- nautobot/core/settings.py +17 -7
- nautobot/core/settings.yaml +4 -26
- nautobot/core/templates/admin/base.html +1 -2
- nautobot/core/templates/admin/change_list.html +9 -12
- nautobot/core/templates/base_django.html +1 -2
- nautobot/core/templates/components/panel/header_extra_content_table.html +1 -1
- nautobot/core/templates/components/tab/content_wrapper.html +4 -4
- nautobot/core/templates/echarts/echarts.html +21 -8
- nautobot/core/templates/generic/object_bulk_create.html +2 -2
- nautobot/core/templates/generic/object_bulk_delete.html +1 -1
- nautobot/core/templates/generic/object_bulk_edit.html +1 -1
- nautobot/core/templates/generic/object_bulk_import.html +1 -1
- nautobot/core/templates/generic/object_delete.html +1 -1
- nautobot/core/templates/generic/object_detail.html +1 -1
- nautobot/core/templates/generic/object_edit.html +1 -1
- nautobot/core/templates/generic/object_retrieve.html +2 -2
- nautobot/core/templates/graphene/graphiql.html +0 -1
- nautobot/core/templates/inc/footer.html +3 -1
- nautobot/core/templates/inc/header.html +10 -0
- nautobot/core/templates/inc/media.html +14 -0
- nautobot/core/templates/inc/nav_menu.html +1 -8
- nautobot/core/templates/inc/object_details_advanced_panel.html +2 -2
- nautobot/core/templates/nautobot_config.py.j2 +0 -6
- nautobot/core/templates/rest_framework/api.html +103 -2
- nautobot/core/templates/utilities/templatetags/filter_form_drawer.html +33 -0
- nautobot/core/templates/utilities/theme_preview.html +3 -0
- nautobot/core/templates/widgets/number_input_with_choices.html +44 -0
- nautobot/core/templatetags/helpers.py +24 -12
- nautobot/core/testing/integration.py +24 -13
- nautobot/core/testing/utils.py +18 -4
- nautobot/core/testing/views.py +104 -17
- nautobot/core/tests/integration/test_filters.py +48 -11
- nautobot/core/tests/integration/test_theme.py +22 -21
- nautobot/core/tests/nautobot_config.py +3 -0
- nautobot/core/tests/runner.py +1 -2
- nautobot/core/tests/test_breadcrumbs.py +21 -21
- nautobot/core/tests/test_jobs.py +73 -6
- nautobot/core/tests/test_renderers.py +59 -0
- nautobot/core/tests/test_settings_schema.py +1 -0
- nautobot/core/tests/test_templatetags_helpers.py +9 -0
- nautobot/core/tests/test_titles.py +0 -16
- nautobot/core/tests/test_ui.py +122 -3
- nautobot/core/tests/test_utils.py +41 -1
- nautobot/core/ui/breadcrumbs.py +68 -17
- nautobot/core/ui/bulk_buttons.py +1 -1
- nautobot/core/ui/choices.py +49 -65
- nautobot/core/ui/echarts.py +15 -20
- nautobot/core/ui/object_detail.py +54 -46
- nautobot/core/ui/titles.py +3 -6
- nautobot/core/urls.py +8 -8
- nautobot/core/utils/filtering.py +11 -1
- nautobot/core/utils/lookup.py +46 -0
- nautobot/core/views/mixins.py +31 -20
- nautobot/core/views/renderers.py +2 -3
- nautobot/data_validation/migrations/0002_data_migration_from_app.py +3 -2
- nautobot/dcim/api/serializers.py +3 -0
- nautobot/dcim/choices.py +49 -0
- nautobot/dcim/constants.py +7 -0
- nautobot/dcim/factory.py +1 -1
- nautobot/dcim/filters.py +13 -1
- nautobot/dcim/forms.py +89 -3
- nautobot/dcim/migrations/0075_interface_duplex_interface_speed_and_more.py +32 -0
- nautobot/dcim/migrations/{0075_add_deviceclusterassignment.py → 0076_add_deviceclusterassignment.py} +1 -1
- nautobot/dcim/migrations/{0076_device_cluster_to_clusters_data_migration.py → 0077_device_cluster_to_clusters_data_migration.py} +1 -1
- nautobot/dcim/migrations/{0077_remove_device_cluster.py → 0078_remove_device_cluster.py} +1 -1
- nautobot/dcim/migrations/{0078_remove_device_location_tenant_name_uniqueness.py → 0079_remove_device_location_tenant_name_uniqueness.py} +1 -1
- nautobot/dcim/migrations/{0079_device_name_data_migration.py → 0080_device_name_data_migration.py} +1 -1
- nautobot/dcim/migrations/0081_alter_device_device_redundancy_group_priority_and_more.py +25 -0
- nautobot/dcim/models/device_component_templates.py +33 -1
- nautobot/dcim/models/device_components.py +22 -1
- nautobot/dcim/models/devices.py +17 -4
- nautobot/dcim/tables/devices.py +15 -0
- nautobot/dcim/tables/devicetypes.py +8 -1
- nautobot/dcim/tables/racks.py +0 -2
- nautobot/dcim/tables/template_code.py +1 -1
- nautobot/dcim/templates/dcim/cable_trace.html +0 -2
- nautobot/dcim/templates/dcim/consoleport.html +1 -1
- nautobot/dcim/templates/dcim/consoleserverport.html +1 -1
- nautobot/dcim/templates/dcim/devicebay.html +1 -1
- nautobot/dcim/templates/dcim/frontport.html +1 -1
- nautobot/dcim/templates/dcim/inc/devicetype_component_table.html +1 -1
- nautobot/dcim/templates/dcim/inc/moduletype_component_table.html +1 -1
- nautobot/dcim/templates/dcim/inc/rack_elevation.html +1 -1
- nautobot/dcim/templates/dcim/interface.html +9 -1
- nautobot/dcim/templates/dcim/interface_edit.html +2 -0
- nautobot/dcim/templates/dcim/inventoryitem.html +1 -1
- nautobot/dcim/templates/dcim/module_consoleports.html +1 -1
- nautobot/dcim/templates/dcim/module_consoleserverports.html +1 -1
- nautobot/dcim/templates/dcim/module_frontports.html +1 -1
- nautobot/dcim/templates/dcim/module_interfaces.html +1 -1
- nautobot/dcim/templates/dcim/module_modulebays.html +1 -1
- nautobot/dcim/templates/dcim/module_poweroutlets.html +1 -1
- nautobot/dcim/templates/dcim/module_powerports.html +1 -1
- nautobot/dcim/templates/dcim/module_rearports.html +1 -1
- nautobot/dcim/templates/dcim/moduletype_list.html +2 -2
- nautobot/dcim/templates/dcim/poweroutlet.html +1 -1
- nautobot/dcim/templates/dcim/powerport.html +1 -1
- nautobot/dcim/templates/dcim/rack_elevation_list.html +1 -1
- nautobot/dcim/templates/dcim/rack_retrieve.html +0 -11
- nautobot/dcim/templates/dcim/rearport.html +1 -1
- nautobot/dcim/templates/dcim/trace/cable.html +1 -1
- nautobot/dcim/templates/dcim/virtualchassis_update.html +1 -1
- nautobot/dcim/tests/integration/test_controller.py +3 -6
- nautobot/dcim/tests/integration/test_controller_managed_device_group.py +1 -5
- nautobot/dcim/tests/integration/test_create_device.py +0 -2
- nautobot/dcim/tests/integration/test_device_bulk_operations.py +1 -3
- nautobot/dcim/tests/integration/test_fileinputpicker.py +6 -10
- nautobot/dcim/tests/integration/test_location_bulk_operations.py +0 -2
- nautobot/dcim/tests/integration/test_module_bay_position.py +3 -4
- nautobot/dcim/tests/test_api.py +186 -6
- nautobot/dcim/tests/test_filters.py +43 -1
- nautobot/dcim/tests/test_forms.py +110 -8
- nautobot/dcim/tests/test_graphql.py +44 -1
- nautobot/dcim/tests/test_models.py +265 -0
- nautobot/dcim/tests/test_tables.py +160 -0
- nautobot/dcim/tests/test_views.py +69 -7
- nautobot/dcim/views.py +232 -126
- nautobot/extras/api/views.py +51 -44
- nautobot/extras/datasources/git.py +3 -1
- nautobot/extras/filters.py +19 -2
- nautobot/extras/forms/forms.py +9 -2
- nautobot/extras/jobs.py +2 -0
- nautobot/extras/jobs_ui.py +4 -3
- nautobot/extras/management/__init__.py +2 -0
- nautobot/extras/management/commands/refresh_dynamic_group_member_caches.py +4 -1
- nautobot/extras/migrations/0131_configcontext_device_families.py +18 -0
- nautobot/extras/models/approvals.py +11 -1
- nautobot/extras/models/change_logging.py +4 -0
- nautobot/extras/models/jobs.py +1 -3
- nautobot/extras/models/models.py +10 -2
- nautobot/extras/plugins/marketplace_manifest.yml +49 -1
- nautobot/extras/plugins/views.py +0 -5
- nautobot/extras/querysets.py +8 -0
- nautobot/extras/tables.py +12 -0
- nautobot/extras/templates/django_ajax_tables/ajax_wrapper.html +2 -0
- nautobot/extras/templates/extras/configcontext_update.html +1 -0
- nautobot/extras/templates/extras/dynamicgroup_update.html +1 -1
- nautobot/extras/templates/extras/objectchange_retrieve.html +0 -2
- nautobot/extras/templates/extras/plugin_detail.html +3 -3
- nautobot/extras/templates/extras/secret_create.html +1 -1
- nautobot/extras/tests/integration/test_computedfields.py +8 -9
- nautobot/extras/tests/integration/test_customfields.py +1 -3
- nautobot/extras/tests/integration/test_dynamicgroups.py +7 -8
- nautobot/extras/tests/integration/test_relationships.py +0 -2
- nautobot/extras/tests/test_api.py +63 -0
- nautobot/extras/tests/test_changelog.py +24 -2
- nautobot/extras/tests/test_filters.py +36 -3
- nautobot/extras/tests/test_models.py +38 -2
- nautobot/extras/tests/test_utils.py +3 -4
- nautobot/extras/tests/test_views.py +22 -83
- nautobot/extras/urls.py +0 -14
- nautobot/extras/views.py +83 -52
- nautobot/ipam/filters.py +26 -0
- nautobot/ipam/tables.py +6 -0
- nautobot/ipam/templates/ipam/namespace_ip_addresses.html +1 -1
- nautobot/ipam/templates/ipam/namespace_prefixes.html +1 -1
- nautobot/ipam/templates/ipam/namespace_vrfs.html +1 -1
- nautobot/ipam/tests/test_filters.py +26 -1
- nautobot/ipam/tests/test_models.py +1 -1
- nautobot/ipam/views.py +9 -7
- nautobot/load_balancers/__init__.py +0 -0
- nautobot/load_balancers/api/__init__.py +1 -0
- nautobot/load_balancers/api/serializers.py +75 -0
- nautobot/load_balancers/api/urls.py +23 -0
- nautobot/load_balancers/api/views.py +61 -0
- nautobot/load_balancers/apps.py +17 -0
- nautobot/load_balancers/choices.py +167 -0
- nautobot/load_balancers/filters.py +225 -0
- nautobot/load_balancers/forms.py +532 -0
- nautobot/load_balancers/management/commands/__init__.py +0 -0
- nautobot/load_balancers/management/commands/generate_load_balancer_models_test_data.py +38 -0
- nautobot/load_balancers/migrations/0001_initial.py +465 -0
- nautobot/load_balancers/migrations/0002_create_default_statuses_pool_members.py +31 -0
- nautobot/load_balancers/migrations/__init__.py +0 -0
- nautobot/load_balancers/models.py +423 -0
- nautobot/load_balancers/navigation.py +80 -0
- nautobot/load_balancers/tables.py +255 -0
- nautobot/load_balancers/tests/__init__.py +474 -0
- nautobot/load_balancers/tests/test_api.py +353 -0
- nautobot/load_balancers/tests/test_filters.py +134 -0
- nautobot/load_balancers/tests/test_forms.py +266 -0
- nautobot/load_balancers/tests/test_models.py +195 -0
- nautobot/load_balancers/tests/test_views.py +229 -0
- nautobot/load_balancers/urls.py +17 -0
- nautobot/load_balancers/views.py +248 -0
- nautobot/project-static/dist/css/github-dark.min.css +10 -0
- nautobot/project-static/dist/css/github.min.css +10 -0
- nautobot/project-static/dist/css/nautobot.css +1 -11
- nautobot/project-static/dist/css/nautobot.css.map +1 -1
- nautobot/project-static/dist/js/libraries.js +1 -1
- nautobot/project-static/dist/js/libraries.js.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/forms.js +13 -0
- nautobot/project-static/nautobot-icons/bus-globe.svg +3 -0
- nautobot/project-static/nautobot-icons/bus-shield-check.svg +3 -0
- nautobot/project-static/nautobot-icons/bus-shield.svg +3 -0
- nautobot/ui/package-lock.json +87 -4
- nautobot/ui/package.json +2 -1
- nautobot/ui/src/js/nautobot.js +0 -1
- nautobot/ui/src/js/select2.js +53 -2
- nautobot/ui/src/scss/nautobot.scss +51 -2
- nautobot/ui/webpack.config.js +13 -0
- nautobot/users/templates/users/preferences.html +11 -2
- nautobot/virtualization/filters.py +6 -1
- nautobot/virtualization/tests/test_filters.py +10 -1
- nautobot/virtualization/tests/test_models.py +1 -0
- nautobot/virtualization/views.py +4 -1
- nautobot/vpn/factory.py +25 -15
- nautobot/vpn/filters.py +1 -0
- nautobot/vpn/forms.py +1 -0
- nautobot/vpn/migrations/0001_initial.py +1 -1
- nautobot/vpn/models.py +16 -8
- nautobot/vpn/tables.py +5 -2
- nautobot/vpn/tests/test_api.py +0 -5
- nautobot/vpn/tests/test_forms.py +1 -2
- nautobot/vpn/tests/test_models.py +57 -7
- nautobot/vpn/tests/test_views.py +22 -3
- nautobot/vpn/views.py +78 -20
- {nautobot-3.0.0a3.dist-info → nautobot-3.0.0rc1.dist-info}/METADATA +4 -4
- {nautobot-3.0.0a3.dist-info → nautobot-3.0.0rc1.dist-info}/RECORD +243 -352
- nautobot/circuits/templates/circuits/circuit.html +0 -2
- nautobot/circuits/templates/circuits/circuit_edit.html +0 -2
- nautobot/circuits/templates/circuits/circuit_retrieve.html +0 -2
- nautobot/circuits/templates/circuits/circuit_update.html +0 -1
- nautobot/circuits/templates/circuits/circuittermination.html +0 -2
- nautobot/circuits/templates/circuits/circuittermination_edit.html +0 -2
- nautobot/circuits/templates/circuits/circuittermination_retrieve.html +0 -2
- nautobot/circuits/templates/circuits/circuittermination_update.html +0 -1
- nautobot/circuits/templates/circuits/circuittype.html +0 -2
- nautobot/circuits/templates/circuits/circuittype_retrieve.html +0 -2
- nautobot/circuits/templates/circuits/inc/circuit_termination.html +0 -85
- nautobot/circuits/templates/circuits/provider.html +0 -2
- nautobot/circuits/templates/circuits/provider_edit.html +0 -2
- nautobot/circuits/templates/circuits/provider_retrieve.html +0 -1
- nautobot/circuits/templates/circuits/provider_update.html +0 -1
- nautobot/circuits/templates/circuits/providernetwork.html +0 -2
- nautobot/circuits/templates/circuits/providernetwork_retrieve.html +0 -2
- nautobot/cloud/templates/cloud/cloudaccount_retrieve.html +0 -2
- nautobot/cloud/templates/cloud/cloudnetwork_retrieve.html +0 -2
- nautobot/cloud/templates/cloud/cloudresourcetype_retrieve.html +0 -2
- nautobot/cloud/templates/cloud/cloudservice_retrieve.html +0 -2
- nautobot/core/templates/buttons/import.html +0 -9
- nautobot/data_validation/templates/data_validation/datacompliance_retrieve.html +0 -1
- nautobot/dcim/templates/dcim/cable.html +0 -2
- nautobot/dcim/templates/dcim/cable_edit.html +0 -2
- nautobot/dcim/templates/dcim/controller/base.html +0 -2
- nautobot/dcim/templates/dcim/controller_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/controller_wirelessnetworks.html +0 -2
- nautobot/dcim/templates/dcim/controllermanageddevicegroup_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/device/base.html +0 -2
- nautobot/dcim/templates/dcim/device/consoleports.html +0 -2
- nautobot/dcim/templates/dcim/device/consoleserverports.html +0 -2
- nautobot/dcim/templates/dcim/device/devicebays.html +0 -2
- nautobot/dcim/templates/dcim/device/frontports.html +0 -2
- nautobot/dcim/templates/dcim/device/interfaces.html +0 -2
- nautobot/dcim/templates/dcim/device/inventory.html +0 -2
- nautobot/dcim/templates/dcim/device/modulebays.html +0 -2
- nautobot/dcim/templates/dcim/device/poweroutlets.html +0 -2
- nautobot/dcim/templates/dcim/device/powerports.html +0 -2
- nautobot/dcim/templates/dcim/device/rearports.html +0 -2
- nautobot/dcim/templates/dcim/device/wireless.html +0 -2
- nautobot/dcim/templates/dcim/device_component.html +0 -2
- nautobot/dcim/templates/dcim/device_edit.html +0 -2
- nautobot/dcim/templates/dcim/devicefamily_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/deviceredundancygroup_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/devicetype.html +0 -2
- nautobot/dcim/templates/dcim/devicetype_edit.html +0 -2
- nautobot/dcim/templates/dcim/devicetype_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/inc/device_napalm_tabs.html +0 -1
- nautobot/dcim/templates/dcim/interfaceredundancygroup_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/location.html +0 -2
- nautobot/dcim/templates/dcim/location_edit.html +0 -2
- nautobot/dcim/templates/dcim/location_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/locationtype.html +0 -2
- nautobot/dcim/templates/dcim/locationtype_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/manufacturer.html +0 -2
- nautobot/dcim/templates/dcim/modulebay_retrieve.html +0 -1
- nautobot/dcim/templates/dcim/platform.html +0 -2
- nautobot/dcim/templates/dcim/powerfeed.html +0 -2
- nautobot/dcim/templates/dcim/powerfeed_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/powerpanel.html +0 -2
- nautobot/dcim/templates/dcim/powerpanel_edit.html +0 -2
- nautobot/dcim/templates/dcim/powerpanel_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/rack.html +0 -2
- nautobot/dcim/templates/dcim/rack_edit.html +0 -2
- nautobot/dcim/templates/dcim/rackgroup.html +0 -2
- nautobot/dcim/templates/dcim/rackreservation.html +0 -2
- nautobot/dcim/templates/dcim/softwareimagefile_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/softwareversion_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/virtualchassis.html +0 -2
- nautobot/dcim/templates/dcim/virtualchassis_add.html +0 -2
- nautobot/dcim/templates/dcim/virtualchassis_edit.html +0 -2
- nautobot/dcim/templates/dcim/virtualchassis_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/virtualdevicecontext_retrieve.html +0 -2
- nautobot/dcim/ui.py +0 -29
- nautobot/extras/templates/extras/computedfield.html +0 -2
- nautobot/extras/templates/extras/computedfield_retrieve.html +0 -2
- nautobot/extras/templates/extras/configcontext.html +0 -2
- nautobot/extras/templates/extras/configcontext_edit.html +0 -2
- nautobot/extras/templates/extras/configcontext_retrieve.html +0 -2
- nautobot/extras/templates/extras/configcontextschema.html +0 -2
- nautobot/extras/templates/extras/configcontextschema_edit.html +0 -2
- nautobot/extras/templates/extras/contact_retrieve.html +0 -2
- nautobot/extras/templates/extras/customfield.html +0 -2
- nautobot/extras/templates/extras/customfield_edit.html +0 -2
- nautobot/extras/templates/extras/customfield_retrieve.html +0 -2
- nautobot/extras/templates/extras/customlink.html +0 -2
- nautobot/extras/templates/extras/dynamicgroup.html +0 -2
- nautobot/extras/templates/extras/dynamicgroup_edit.html +0 -2
- nautobot/extras/templates/extras/exporttemplate.html +0 -2
- nautobot/extras/templates/extras/gitrepository.html +0 -2
- nautobot/extras/templates/extras/gitrepository_object_edit.html +0 -2
- nautobot/extras/templates/extras/graphqlquery.html +0 -2
- nautobot/extras/templates/extras/graphqlquery_list.html +0 -1
- nautobot/extras/templates/extras/graphqlquery_retrieve.html +0 -2
- nautobot/extras/templates/extras/job_detail.html +0 -2
- nautobot/extras/templates/extras/jobbutton_retrieve.html +0 -2
- nautobot/extras/templates/extras/jobhook.html +0 -2
- nautobot/extras/templates/extras/jobqueue_retrieve.html +0 -2
- nautobot/extras/templates/extras/jobresult.html +0 -2
- nautobot/extras/templates/extras/metadatatype_retrieve.html +0 -2
- nautobot/extras/templates/extras/note.html +0 -2
- nautobot/extras/templates/extras/note_retrieve.html +0 -1
- nautobot/extras/templates/extras/object_changelog.html +0 -2
- nautobot/extras/templates/extras/object_notes.html +0 -2
- nautobot/extras/templates/extras/objectchange.html +0 -2
- nautobot/extras/templates/extras/objectchange_list.html +0 -3
- nautobot/extras/templates/extras/relationship.html +0 -1
- nautobot/extras/templates/extras/secret.html +0 -1
- nautobot/extras/templates/extras/secret_edit.html +0 -1
- nautobot/extras/templates/extras/secretsgroup.html +0 -2
- nautobot/extras/templates/extras/secretsgroup_edit.html +0 -2
- nautobot/extras/templates/extras/secretsgroup_retrieve.html +0 -2
- nautobot/extras/templates/extras/status.html +0 -2
- nautobot/extras/templates/extras/tag.html +0 -2
- nautobot/extras/templates/extras/tag_edit.html +0 -2
- nautobot/extras/templates/extras/tag_retrieve.html +0 -2
- nautobot/extras/templates/extras/team_retrieve.html +0 -2
- nautobot/ipam/templates/ipam/namespace_retrieve.html +0 -1
- nautobot/ipam/templates/ipam/prefix.html +0 -2
- nautobot/ipam/templates/ipam/prefix_edit.html +0 -1
- nautobot/ipam/templates/ipam/prefix_retrieve.html +0 -2
- nautobot/ipam/templates/ipam/rir.html +0 -2
- nautobot/ipam/templates/ipam/routetarget.html +0 -1
- nautobot/ipam/templates/ipam/service.html +0 -2
- nautobot/ipam/templates/ipam/service_edit.html +0 -2
- nautobot/ipam/templates/ipam/service_retrieve.html +0 -2
- nautobot/ipam/templates/ipam/vlan.html +0 -2
- nautobot/ipam/templates/ipam/vlan_edit.html +0 -2
- nautobot/ipam/templates/ipam/vlan_retrieve.html +0 -2
- nautobot/ipam/templates/ipam/vlangroup.html +0 -2
- nautobot/ipam/templates/ipam/vrf.html +0 -1
- nautobot/tenancy/templates/tenancy/tenant.html +0 -2
- nautobot/tenancy/templates/tenancy/tenant_edit.html +0 -2
- nautobot/tenancy/templates/tenancy/tenantgroup.html +0 -2
- nautobot/tenancy/templates/tenancy/tenantgroup_retrieve.html +0 -1
- nautobot/virtualization/templates/virtualization/clustergroup.html +0 -2
- nautobot/virtualization/templates/virtualization/clustertype.html +0 -2
- nautobot/virtualization/templates/virtualization/virtualmachine.html +0 -2
- nautobot/virtualization/templates/virtualization/virtualmachine_edit.html +0 -2
- nautobot/virtualization/templates/virtualization/virtualmachine_retrieve.html +0 -2
- nautobot/vpn/templates/vpn/vpnprofile.html +0 -2
- nautobot/wireless/templates/wireless/radioprofile_retrieve.html +0 -2
- nautobot/wireless/templates/wireless/supporteddatarate_retrieve.html +0 -2
- nautobot/wireless/templates/wireless/wirelessnetwork_retrieve.html +0 -2
- {nautobot-3.0.0a3.dist-info → nautobot-3.0.0rc1.dist-info}/LICENSE.txt +0 -0
- {nautobot-3.0.0a3.dist-info → nautobot-3.0.0rc1.dist-info}/NOTICE +0 -0
- {nautobot-3.0.0a3.dist-info → nautobot-3.0.0rc1.dist-info}/WHEEL +0 -0
- {nautobot-3.0.0a3.dist-info → nautobot-3.0.0rc1.dist-info}/entry_points.txt +0 -0
|
@@ -1139,6 +1139,9 @@ This:
|
|
|
1139
1139
|
<div class="icon-preview"><img alt="power icon" src="{% static 'nautobot-icons/battery-3.svg' %}">battery-3</div>
|
|
1140
1140
|
<div class="icon-preview"><img alt="branch icon" src="{% static 'nautobot-icons/branch.svg' %}">branch</div>
|
|
1141
1141
|
<div class="icon-preview"><img alt="briefcase-2 icon" src="{% static 'nautobot-icons/briefcase-2.svg' %}">briefcase-2</div>
|
|
1142
|
+
<div class="icon-preview"><img alt="bus-globe icon" src="{% static 'nautobot-icons/bus-globe.svg' %}">bus-globe</div>
|
|
1143
|
+
<div class="icon-preview"><img alt="bus-shield icon" src="{% static 'nautobot-icons/bus-shield.svg' %}">bus-shield</div>
|
|
1144
|
+
<div class="icon-preview"><img alt="bus-shield-check icon" src="{% static 'nautobot-icons/bus-shield-check.svg' %}">bus-shield-check</div>
|
|
1142
1145
|
<div class="icon-preview"><img alt="cable-data icon" src="{% static 'nautobot-icons/cable-data.svg' %}">cable-data</div>
|
|
1143
1146
|
<div class="icon-preview"><img alt="cable-data-2 icon" src="{% static 'nautobot-icons/cable-data-2.svg' %}">cable-data-2</div>
|
|
1144
1147
|
<div class="icon-preview"><img alt="cast icon" src="{% static 'nautobot-icons/cast.svg' %}">cast</div>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<div class="input-group">
|
|
2
|
+
{% include 'django/forms/widgets/number.html' %}
|
|
3
|
+
{% if widget.choices %}
|
|
4
|
+
<span class="input-group-btn">
|
|
5
|
+
<button type="button" class="btn btn-secondary dropdown-toggle" data-bs-toggle="dropdown">
|
|
6
|
+
<span class="mdi mdi-chevron-down"></span>
|
|
7
|
+
</button>
|
|
8
|
+
<ul class="dropdown-menu dropdown-menu-end">
|
|
9
|
+
{% for value, label in widget.choices %}
|
|
10
|
+
<li><a href="#" data-name="{{ widget.name }}" data-value="{{ value }}" class="set_value dropdown-item">{{ label }}</a></li>
|
|
11
|
+
{% endfor %}
|
|
12
|
+
</ul>
|
|
13
|
+
</span>
|
|
14
|
+
{% endif %}
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
{% if widget.choices %}
|
|
18
|
+
<script type="text/javascript">
|
|
19
|
+
(function() {
|
|
20
|
+
if (window.__nbNumberWithSelectWidgetBound) return;
|
|
21
|
+
window.__nbNumberWithSelectWidgetBound = true;
|
|
22
|
+
function bindNumberWithSelectHandler() {
|
|
23
|
+
document.addEventListener("click", function(e) {
|
|
24
|
+
if (!e.target) return;
|
|
25
|
+
var link = e.target.closest && e.target.closest("a.set_value");
|
|
26
|
+
if (!link) return;
|
|
27
|
+
e.preventDefault();
|
|
28
|
+
var container = link.closest(".input-group");
|
|
29
|
+
var name = link.getAttribute("data-name");
|
|
30
|
+
var value = link.getAttribute("data-value");
|
|
31
|
+
var input = container && name ? container.querySelector('input[name="' + name + '"]') : null;
|
|
32
|
+
if (input) {
|
|
33
|
+
input.value = value;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
if (document.readyState === 'loading') {
|
|
38
|
+
document.addEventListener('DOMContentLoaded', bindNumberWithSelectHandler);
|
|
39
|
+
} else {
|
|
40
|
+
bindNumberWithSelectHandler();
|
|
41
|
+
}
|
|
42
|
+
})();
|
|
43
|
+
</script>
|
|
44
|
+
{% endif %}
|
|
@@ -126,23 +126,29 @@ def placeholder(value):
|
|
|
126
126
|
|
|
127
127
|
@library.filter()
|
|
128
128
|
@register.filter()
|
|
129
|
-
def pre_tag(value):
|
|
129
|
+
def pre_tag(value, format_empty_value=True):
|
|
130
130
|
"""Render a value within `<pre></pre>` tags to enable formatting.
|
|
131
131
|
|
|
132
132
|
Args:
|
|
133
133
|
value (any): Input value, can be any variable.
|
|
134
|
+
format_empty_value (bool): Whether format empty value or render placeholder.
|
|
134
135
|
|
|
135
136
|
Returns:
|
|
136
|
-
(str): Value wrapped in `<pre></pre>` tags
|
|
137
|
+
(str): Value wrapped in `<pre></pre>` tags or placeholder if None or format_empty_values=False and empty
|
|
137
138
|
|
|
138
139
|
Example:
|
|
139
140
|
>>> pre_tag("")
|
|
140
141
|
'<pre></pre>'
|
|
141
142
|
>>> pre_tag("hello")
|
|
142
143
|
'<pre>hello</pre>'
|
|
144
|
+
>>> pre_tag("", format_empty_value=False)
|
|
145
|
+
'<span class="text-secondary">—</span>'
|
|
143
146
|
"""
|
|
144
|
-
if value is not None:
|
|
147
|
+
if format_empty_value and value is not None:
|
|
148
|
+
return format_html("<pre>{}</pre>", value)
|
|
149
|
+
elif value:
|
|
145
150
|
return format_html("<pre>{}</pre>", value)
|
|
151
|
+
|
|
146
152
|
return HTML_NONE
|
|
147
153
|
|
|
148
154
|
|
|
@@ -395,17 +401,19 @@ def humanize_speed(speed):
|
|
|
395
401
|
1544 => "1.544 Mbps"
|
|
396
402
|
100000 => "100 Mbps"
|
|
397
403
|
10000000 => "10 Gbps"
|
|
404
|
+
1000000000 => "1 Tbps"
|
|
405
|
+
1600000000 => "1.6 Tbps"
|
|
406
|
+
10000000000 => "10 Tbps"
|
|
398
407
|
"""
|
|
399
408
|
if not speed:
|
|
400
409
|
return ""
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
return f"{int(speed / 1000)} Mbps"
|
|
410
|
+
|
|
411
|
+
if speed >= 1000000000:
|
|
412
|
+
return f"{speed / 1000000000:g} Tbps"
|
|
413
|
+
elif speed >= 1000000:
|
|
414
|
+
return f"{speed / 1000000:g} Gbps"
|
|
407
415
|
elif speed >= 1000:
|
|
408
|
-
return f"{
|
|
416
|
+
return f"{speed / 1000:g} Mbps"
|
|
409
417
|
else:
|
|
410
418
|
return f"{speed} Kbps"
|
|
411
419
|
|
|
@@ -507,7 +515,9 @@ def get_docs_url(model):
|
|
|
507
515
|
|
|
508
516
|
Example:
|
|
509
517
|
>>> get_docs_url(location_instance)
|
|
510
|
-
"static/docs/
|
|
518
|
+
"static/docs/user-guide/core-data-model/dcim/location.html"
|
|
519
|
+
>>> get_docs_url(virtual_server_instance)
|
|
520
|
+
"static/docs/user-guide/core-data-model/load-balancers/virtualserver.html"
|
|
511
521
|
>>> get_docs_url(example_model)
|
|
512
522
|
"/docs/example-app/models/examplemodel.html"
|
|
513
523
|
"""
|
|
@@ -532,7 +542,9 @@ def get_docs_url(model):
|
|
|
532
542
|
elif model._meta.app_label == "extras":
|
|
533
543
|
path = f"docs/user-guide/platform-functionality/{model._meta.model_name}.html"
|
|
534
544
|
else:
|
|
535
|
-
path =
|
|
545
|
+
path = (
|
|
546
|
+
f"docs/user-guide/core-data-model/{model._meta.app_label.replace('_', '-')}/{model._meta.model_name}.html"
|
|
547
|
+
)
|
|
536
548
|
|
|
537
549
|
# Check to see if documentation exists in any of the static paths.
|
|
538
550
|
if find(path):
|
|
@@ -66,13 +66,14 @@ class ObjectsListMixin:
|
|
|
66
66
|
"""
|
|
67
67
|
Click bulk delete from dropdown menu on bottom of the items table list.
|
|
68
68
|
"""
|
|
69
|
-
self.
|
|
70
|
-
"document.querySelector('#bulk-action-buttons button[type=\"submit\"]').scrollIntoView()"
|
|
71
|
-
)
|
|
69
|
+
self.scroll_element_into_view(css='#bulk-action-buttons button[type="submit"]')
|
|
72
70
|
self.browser.find_by_xpath(
|
|
73
71
|
'//*[@id="bulk-action-buttons"]//button[@type="submit"]/following-sibling::button[1]'
|
|
74
72
|
).click()
|
|
75
|
-
self.browser.find_by_css('#bulk-action-buttons button[name="_delete"]')
|
|
73
|
+
bulk_delete_button = self.browser.find_by_css('#bulk-action-buttons button[name="_delete"]')
|
|
74
|
+
bulk_delete_button.is_visible(wait_time=5)
|
|
75
|
+
self.scroll_element_into_view(element=bulk_delete_button)
|
|
76
|
+
bulk_delete_button.click()
|
|
76
77
|
|
|
77
78
|
def click_bulk_delete_all(self):
|
|
78
79
|
"""
|
|
@@ -110,7 +111,7 @@ class ObjectsListMixin:
|
|
|
110
111
|
objects_table_container = self.browser.find_by_xpath('//*[@id="object_list_form"]')
|
|
111
112
|
try:
|
|
112
113
|
objects_table = objects_table_container.find_by_tag("tbody")
|
|
113
|
-
return len(objects_table.
|
|
114
|
+
return len(objects_table.find_by_xpath(".//tr[not(count(td[@colspan])=1)]"))
|
|
114
115
|
except ElementDoesNotExist:
|
|
115
116
|
return 0
|
|
116
117
|
|
|
@@ -238,7 +239,7 @@ class BulkOperationsMixin:
|
|
|
238
239
|
button_text = self.browser.find_by_xpath('//button[@name="_confirm" and @type="submit"]').text
|
|
239
240
|
self.assertIn(f"Delete these {expected_count}", button_text)
|
|
240
241
|
|
|
241
|
-
message_text = self.browser.find_by_id("confirm-bulk-deletion").find_by_xpath('//div[@class="
|
|
242
|
+
message_text = self.browser.find_by_id("confirm-bulk-deletion").find_by_xpath('//div[@class="card-body"]').text
|
|
242
243
|
self.assertIn(f"The following operation will delete {expected_count}", message_text)
|
|
243
244
|
|
|
244
245
|
def assertIsBulkDeleteJob(self):
|
|
@@ -413,7 +414,7 @@ class SeleniumTestCase(StaticLiveServerTestCase, testing.NautobotTestCaseMixin):
|
|
|
413
414
|
search_box_class = "select2-search select2-search--dropdown"
|
|
414
415
|
|
|
415
416
|
self.browser.find_by_xpath(f"//select[@id='id_{field_name}']//following-sibling::span").click()
|
|
416
|
-
self.
|
|
417
|
+
self.scroll_element_into_view(css=f"#id_{field_name}")
|
|
417
418
|
search_box = self.browser.find_by_xpath(f"//*[@class='{search_box_class}']//input", wait_time=5)
|
|
418
419
|
for _ in search_box.first.type(value, slowly=True):
|
|
419
420
|
pass
|
|
@@ -459,9 +460,7 @@ class SeleniumTestCase(StaticLiveServerTestCase, testing.NautobotTestCaseMixin):
|
|
|
459
460
|
def click_button(self, query_selector):
|
|
460
461
|
self.browser.is_element_present_by_css(query_selector, wait_time=5)
|
|
461
462
|
# Button might be visible but on the edge and then impossible to click due to vertical/horizontal scrolls
|
|
462
|
-
self.
|
|
463
|
-
f"document.querySelector('{query_selector}').scrollIntoView({{ behavior: 'instant', block: 'start' }});"
|
|
464
|
-
)
|
|
463
|
+
self.scroll_element_into_view(css=query_selector)
|
|
465
464
|
# Scrolling may be asynchronous, wait until it's actually clickable.
|
|
466
465
|
WebDriverWait(self.browser.driver, 30).until(element_to_be_clickable((By.CSS_SELECTOR, query_selector)))
|
|
467
466
|
btn = self.browser.find_by_css(query_selector)
|
|
@@ -473,9 +472,7 @@ class SeleniumTestCase(StaticLiveServerTestCase, testing.NautobotTestCaseMixin):
|
|
|
473
472
|
"""
|
|
474
473
|
self.browser.is_element_present_by_name(input_name, wait_time=5)
|
|
475
474
|
element = self.browser.find_by_name(input_name)
|
|
476
|
-
self.
|
|
477
|
-
"arguments[0].scrollIntoView({ behavior: 'instant', block: 'start' });", element.first._element
|
|
478
|
-
)
|
|
475
|
+
self.scroll_element_into_view(element=element)
|
|
479
476
|
element.is_visible(wait_time=5)
|
|
480
477
|
self.browser.execute_script("arguments[0].focus();", element.first._element)
|
|
481
478
|
self.browser.fill(input_name, input_value)
|
|
@@ -486,6 +483,20 @@ class SeleniumTestCase(StaticLiveServerTestCase, testing.NautobotTestCaseMixin):
|
|
|
486
483
|
self.login(self.user.username, self.password)
|
|
487
484
|
self.logged_in = True
|
|
488
485
|
|
|
486
|
+
def scroll_element_into_view(self, element=None, css=None, xpath=None, block="start"):
|
|
487
|
+
"""
|
|
488
|
+
Scroll element into view. Element can be expressed either as Splinter `ElementList`, `ElementAPI`, CSS query selector or XPath.
|
|
489
|
+
"""
|
|
490
|
+
if css:
|
|
491
|
+
element = self.browser.find_by_css(css)
|
|
492
|
+
elif xpath:
|
|
493
|
+
element = self.browser.find_by_xpath(xpath)
|
|
494
|
+
|
|
495
|
+
self.browser.execute_script(
|
|
496
|
+
f"arguments[0].scrollIntoView({{ behavior: 'instant', block: '{block}' }});",
|
|
497
|
+
element.first._element if hasattr(element, "__iter__") else element._element,
|
|
498
|
+
)
|
|
499
|
+
|
|
489
500
|
|
|
490
501
|
class BulkOperationsTestCases:
|
|
491
502
|
"""
|
nautobot/core/testing/utils.py
CHANGED
|
@@ -90,6 +90,20 @@ def extract_page_body(content):
|
|
|
90
90
|
return content
|
|
91
91
|
|
|
92
92
|
|
|
93
|
+
def extract_page_title(content):
|
|
94
|
+
"""
|
|
95
|
+
Given raw HTML content from an HTTP response, extract the page title section only.
|
|
96
|
+
|
|
97
|
+
<div id="page-title" ...>...</header>
|
|
98
|
+
"""
|
|
99
|
+
try:
|
|
100
|
+
return re.findall(
|
|
101
|
+
r"<div class=\"col-4\" id=\"page-title\">(.*?)(?=<\/header)", content, flags=(re.MULTILINE | re.DOTALL)
|
|
102
|
+
)[0]
|
|
103
|
+
except IndexError:
|
|
104
|
+
return content
|
|
105
|
+
|
|
106
|
+
|
|
93
107
|
@contextmanager
|
|
94
108
|
def disable_warnings(logger_name):
|
|
95
109
|
"""
|
|
@@ -127,15 +141,15 @@ def generate_random_device_asset_tag_of_specified_size(size):
|
|
|
127
141
|
def get_expected_menu_item_name(view_model) -> str:
|
|
128
142
|
"""Return the expected menu item name for a given model."""
|
|
129
143
|
name_map = {
|
|
130
|
-
"
|
|
131
|
-
"
|
|
144
|
+
"Approval Workflow Definitions": "Workflow Definitions",
|
|
145
|
+
"Approval Workflow Stages": "Approval Dashboard",
|
|
132
146
|
"Controller Managed Device Groups": "Device Groups",
|
|
147
|
+
"Object Changes": "Change Log",
|
|
133
148
|
"Min Max Validation Rules": "Min/Max Rules",
|
|
134
149
|
"Regular Expression Validation Rules": "Regex Rules",
|
|
135
150
|
"Required Validation Rules": "Required Rules",
|
|
136
151
|
"Unique Validation Rules": "Unique Rules",
|
|
137
|
-
"
|
|
138
|
-
"Approval Workflow Stages": "Approval Dashboard",
|
|
152
|
+
"VM Interfaces": "Interfaces",
|
|
139
153
|
}
|
|
140
154
|
|
|
141
155
|
expected = bettertitle(view_model._meta.verbose_name_plural)
|
nautobot/core/testing/views.py
CHANGED
|
@@ -27,7 +27,10 @@ from nautobot.core.models.generics import PrimaryModel
|
|
|
27
27
|
from nautobot.core.models.tree_queries import TreeModel
|
|
28
28
|
from nautobot.core.templatetags import buttons, helpers
|
|
29
29
|
from nautobot.core.testing import mixins, utils
|
|
30
|
+
from nautobot.core.testing.utils import extract_page_title
|
|
31
|
+
from nautobot.core.ui.object_detail import ObjectsTablePanel
|
|
30
32
|
from nautobot.core.utils import lookup
|
|
33
|
+
from nautobot.core.views.mixins import NautobotViewSetMixin, PERMISSIONS_ACTION_MAP
|
|
31
34
|
from nautobot.dcim.models.device_components import ComponentModel
|
|
32
35
|
from nautobot.extras import choices as extras_choices, models as extras_models, querysets as extras_querysets
|
|
33
36
|
from nautobot.extras.forms import CustomFieldModelFormMixin, RelationshipModelFormMixin
|
|
@@ -191,12 +194,10 @@ class ViewTestCases:
|
|
|
191
194
|
# Try GET with model-level permission
|
|
192
195
|
with CaptureQueriesContext(connection) as capture_queries_context:
|
|
193
196
|
response = self.client.get(instance.get_absolute_url())
|
|
194
|
-
|
|
195
|
-
#
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
# it adds, but can/should/may? fail otherwise.
|
|
199
|
-
self.assertBodyContains(response, escape(getattr(instance, "display", str(instance))))
|
|
197
|
+
|
|
198
|
+
# The object's display name or string representation should appear in the header
|
|
199
|
+
expected_title = escape(getattr(instance, "page_title", str(instance)))
|
|
200
|
+
self.assertInHTML(expected_title, extract_page_title(response.content.decode(response.charset)))
|
|
200
201
|
|
|
201
202
|
# If any Relationships are defined, they should appear in the response
|
|
202
203
|
if self.relationships is not None:
|
|
@@ -366,18 +367,104 @@ class ViewTestCases:
|
|
|
366
367
|
|
|
367
368
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
|
368
369
|
def test_custom_actions(self):
|
|
370
|
+
base_view = lookup.get_view_for_model(self.model)
|
|
371
|
+
if not issubclass(base_view, NautobotViewSetMixin):
|
|
372
|
+
self.skipTest(f"View {base_view} is not using NautobotUIViewSet")
|
|
373
|
+
|
|
374
|
+
instance = self._get_queryset().first()
|
|
375
|
+
for action_func in base_view.get_extra_actions():
|
|
376
|
+
if not action_func.detail:
|
|
377
|
+
continue
|
|
378
|
+
if "get" not in action_func.mapping:
|
|
379
|
+
continue
|
|
380
|
+
if action_func.url_name == "data-compliance" and not getattr(base_view, "object_detail_content", None):
|
|
381
|
+
continue
|
|
382
|
+
with self.subTest(action=action_func.url_name):
|
|
383
|
+
if action_func.url_name in self.custom_action_required_permissions:
|
|
384
|
+
required_permissions = self.custom_action_required_permissions[action_func.url_name]
|
|
385
|
+
else:
|
|
386
|
+
base_action = action_func.kwargs.get("custom_view_base_action")
|
|
387
|
+
if base_action is None:
|
|
388
|
+
if action_func.__name__ not in PERMISSIONS_ACTION_MAP:
|
|
389
|
+
self.fail(f"Missing custom_view_base_action for action {action_func.__name__}")
|
|
390
|
+
base_action = PERMISSIONS_ACTION_MAP[action_func.__name__]
|
|
391
|
+
|
|
392
|
+
required_permissions = [
|
|
393
|
+
f"{self.model._meta.app_label}.{base_action}_{self.model._meta.model_name}"
|
|
394
|
+
]
|
|
395
|
+
required_permissions += action_func.kwargs.get("custom_view_additional_permissions", [])
|
|
396
|
+
|
|
397
|
+
try:
|
|
398
|
+
url = self._get_url(action_func.url_name, instance)
|
|
399
|
+
self.assertHttpStatus(self.client.get(url), [403, 404])
|
|
400
|
+
for permission in required_permissions[:-1]:
|
|
401
|
+
self.add_permissions(permission)
|
|
402
|
+
self.assertHttpStatus(self.client.get(url), [403, 404])
|
|
403
|
+
|
|
404
|
+
self.add_permissions(required_permissions[-1])
|
|
405
|
+
self.assertHttpStatus(self.client.get(url), 200)
|
|
406
|
+
finally:
|
|
407
|
+
# delete the permissions here so that we start from a clean slate on the next loop
|
|
408
|
+
self.remove_permissions(*required_permissions)
|
|
409
|
+
|
|
410
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
|
411
|
+
def test_body_content_table_list_url(self):
|
|
412
|
+
"""
|
|
413
|
+
Testing that the badge links on related object panels are working as expected.
|
|
414
|
+
"""
|
|
415
|
+
self.user.is_superuser = True
|
|
416
|
+
self.user.save()
|
|
369
417
|
instance = self._get_queryset().first()
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
self.
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
self.
|
|
418
|
+
if not instance:
|
|
419
|
+
# We should have a better mechanism to test against an empty instance, but this will remove blocker for now.
|
|
420
|
+
self.skipTest("No instances to test against.")
|
|
421
|
+
errors = []
|
|
422
|
+
model_name = self.model._meta.model_name
|
|
423
|
+
|
|
424
|
+
response = self.client.get(instance.get_absolute_url())
|
|
425
|
+
self.assertHttpStatus(response, 200)
|
|
426
|
+
context = response.context
|
|
427
|
+
if not context.get("object_detail_content"):
|
|
428
|
+
self.skipTest("Model is not using UIViewSet")
|
|
429
|
+
for tab in context["object_detail_content"].tabs:
|
|
430
|
+
if not tab.should_render(context):
|
|
431
|
+
continue
|
|
432
|
+
tab_label = f"'{tab.label}'" if tab.label else "main"
|
|
433
|
+
for panel in tab.panels:
|
|
434
|
+
if not isinstance(panel, ObjectsTablePanel) or panel.context_table_key:
|
|
435
|
+
continue
|
|
436
|
+
extra_context = panel.get_extra_context(context)
|
|
437
|
+
list_url = extra_context.get("body_content_table_list_url")
|
|
438
|
+
table_title = panel.label or extra_context.get("body_content_table_verbose_name_plural")
|
|
439
|
+
if not list_url:
|
|
440
|
+
# If `header_extra_content_template_path` is not set,
|
|
441
|
+
# we don't render the badge in the header nor the link
|
|
442
|
+
if not panel.header_extra_content_template_path or not panel.enable_related_link:
|
|
443
|
+
continue
|
|
444
|
+
errors.append(
|
|
445
|
+
(
|
|
446
|
+
f"Error on {model_name} {tab_label} tab: panel '{table_title}' badge link does not exist."
|
|
447
|
+
" Please ensure the related model has a list view, or override with a custom list URL via 'related_list_url_name=app:model_list'."
|
|
448
|
+
" If the link should not be enabled, you must explicitly set 'enable_related_link=False' on the ObjectsTablePanel."
|
|
449
|
+
)
|
|
450
|
+
)
|
|
451
|
+
continue
|
|
452
|
+
try:
|
|
453
|
+
list_response = self.client.get(list_url)
|
|
454
|
+
except Exception as e:
|
|
455
|
+
errors.append(
|
|
456
|
+
f"Error on {model_name} {tab_label} tab: panel '{table_title}' badge link '{list_url}': {e}"
|
|
457
|
+
)
|
|
458
|
+
else:
|
|
459
|
+
self.assertHttpStatus(list_response, 200)
|
|
460
|
+
for error in list_response.context["errors"]:
|
|
461
|
+
errors.append(
|
|
462
|
+
(
|
|
463
|
+
f"Error on {model_name} {tab_label} tab: panel '{table_title}' badge link '{list_url}': {error}."
|
|
464
|
+
)
|
|
465
|
+
)
|
|
466
|
+
if errors:
|
|
467
|
+
self.fail("\n".join(errors))
|
|
381
468
|
|
|
382
469
|
class GetObjectChangelogViewTestCase(ModelViewTestCase):
|
|
383
470
|
"""
|
|
@@ -69,7 +69,7 @@ class ListViewFilterTestCase(SeleniumTestCase):
|
|
|
69
69
|
filter_button.click()
|
|
70
70
|
|
|
71
71
|
# assert the filter drawer has appeared
|
|
72
|
-
self.assertTrue(filter_drawer.
|
|
72
|
+
self.assertTrue(filter_drawer.is_visible(wait_time=10))
|
|
73
73
|
|
|
74
74
|
# start typing a parent into select2
|
|
75
75
|
location_type = LocationType.objects.filter(parent__isnull=True).first()
|
|
@@ -170,9 +170,7 @@ class ListViewFilterTestCase(SeleniumTestCase):
|
|
|
170
170
|
|
|
171
171
|
# Open the filter drawer, configure filter and apply filter
|
|
172
172
|
self.browser.find_by_id("id__filterbtn").click()
|
|
173
|
-
self.
|
|
174
|
-
f"document.querySelector('[name={text_field_name}]').scrollIntoView({{ behavior: 'instant', block: 'end' }})"
|
|
175
|
-
)
|
|
173
|
+
self.scroll_element_into_view(css=f"[name={text_field_name}]", block="end")
|
|
176
174
|
self.change_field_value(text_field_name, "example-text")
|
|
177
175
|
self.change_field_value(integer_field_name, 4356)
|
|
178
176
|
self.change_field_value(select_field_name, "SingleSelect Option A", field_type="select")
|
|
@@ -188,9 +186,7 @@ class ListViewFilterTestCase(SeleniumTestCase):
|
|
|
188
186
|
|
|
189
187
|
# Assert on update of field in Default Filter the update is replicated on Advanced Filter
|
|
190
188
|
self.browser.find_by_xpath("//a[@href='#default-filter']").click() # Go back to Basic tab
|
|
191
|
-
self.
|
|
192
|
-
f"document.querySelector('[name={text_field_name}]').scrollIntoView({{ behavior: 'instant', block: 'end' }})"
|
|
193
|
-
)
|
|
189
|
+
self.scroll_element_into_view(css=f"[name={text_field_name}]", block="end")
|
|
194
190
|
self.change_field_value(text_field_name, "test new")
|
|
195
191
|
self.change_field_value(integer_field_name, 1111)
|
|
196
192
|
self.change_field_value(select_field_name, "SingleSelect Option B", field_type="select")
|
|
@@ -260,9 +256,7 @@ class ListViewFilterTestCase(SeleniumTestCase):
|
|
|
260
256
|
)
|
|
261
257
|
dynamic_filter_add_button.click()
|
|
262
258
|
self.browser.find_by_xpath("//a[@href='#default-filter']").click()
|
|
263
|
-
self.
|
|
264
|
-
f"document.querySelector('[name={text_field_name}]').scrollIntoView({{ behavior: 'instant', block: 'end' }})"
|
|
265
|
-
)
|
|
259
|
+
self.scroll_element_into_view(css=f"[name={text_field_name}]", block="end")
|
|
266
260
|
self.assertEqual(self.browser.find_by_name(text_field_name)[0].value, "test new update")
|
|
267
261
|
self.assertEqual(self.browser.find_by_name(integer_field_name)[0].value, "8888")
|
|
268
262
|
custom_select_values = self.browser.find_by_name(select_field_name)[0].find_by_tag("option")
|
|
@@ -325,7 +319,7 @@ class ListViewFilterTestCase(SeleniumTestCase):
|
|
|
325
319
|
self.browser.find_by_xpath(apply_btn_xpath).click()
|
|
326
320
|
filter_drawer = self.browser.find_by_id("FilterForm_drawer", wait_time=10)
|
|
327
321
|
# Drawer is kept open
|
|
328
|
-
self.assertTrue(filter_drawer.
|
|
322
|
+
self.assertTrue(filter_drawer.is_visible(wait_time=10))
|
|
329
323
|
# Assert the choice is applied
|
|
330
324
|
self.browser.find_by_xpath(
|
|
331
325
|
f"//span[@class='badge' and @data-nb-value='{tag_object.name}' and contains(text(),{tag_object.name})]"
|
|
@@ -334,3 +328,46 @@ class ListViewFilterTestCase(SeleniumTestCase):
|
|
|
334
328
|
self.browser.find_by_xpath(
|
|
335
329
|
"//a[@href='#advanced-filter']//span[contains(@class,'nb-btn-indicator') and contains(text(),'Some of the applied filters can only be viewed in Advanced')]"
|
|
336
330
|
)
|
|
331
|
+
|
|
332
|
+
def test_selected_advanced_filter_automatic_application(self):
|
|
333
|
+
"""Assert that selected advanced filter is still used even if not manually applied by user."""
|
|
334
|
+
# Go to the location list view
|
|
335
|
+
self.browser.visit(f"{self.live_server_url}{reverse('dcim:location_list')}")
|
|
336
|
+
|
|
337
|
+
# Open the filter drawer
|
|
338
|
+
self.browser.find_by_id("id__filterbtn").click()
|
|
339
|
+
# Go to advanced Tab
|
|
340
|
+
self.browser.find_by_xpath("//a[@href='#advanced-filter']").click()
|
|
341
|
+
|
|
342
|
+
# Click on the first column lookup field and select ASN
|
|
343
|
+
lookup_field_container = self.browser.find_by_id("select2-id_form-0-lookup_field-container")
|
|
344
|
+
self.assertTrue(lookup_field_container.is_visible(wait_time=10))
|
|
345
|
+
lookup_field_container.click()
|
|
346
|
+
self.browser.find_by_xpath(
|
|
347
|
+
"//ul[@id='select2-id_form-0-lookup_field-results']/li[contains(@class,'select2-results__option') "
|
|
348
|
+
"and contains(text(),'ASN')]"
|
|
349
|
+
).click()
|
|
350
|
+
|
|
351
|
+
# Click on the second column lookup type and select exact
|
|
352
|
+
self.browser.find_by_id("select2-id_form-0-lookup_type-container").click()
|
|
353
|
+
self.browser.find_by_xpath(
|
|
354
|
+
"//ul[@id='select2-id_form-0-lookup_type-results']/li[contains(@class,'select2-results__option') "
|
|
355
|
+
"and contains(text(),'exact')]"
|
|
356
|
+
).click()
|
|
357
|
+
|
|
358
|
+
# Fill ASN input value with "65001"
|
|
359
|
+
self.browser.find_by_xpath("//input[@id='id_for_asn']").fill("65001")
|
|
360
|
+
|
|
361
|
+
# Click "Apply Specified" button
|
|
362
|
+
self.browser.find_by_xpath("//form[@id='dynamic-filter-form']//button[@type='submit']").click()
|
|
363
|
+
|
|
364
|
+
# Wait for filters button indicator to appear, meaning that the page was reloaded and selected filters applied.
|
|
365
|
+
self.assertTrue(
|
|
366
|
+
self.browser.is_element_present_by_xpath(
|
|
367
|
+
"//button[@id='id__filterbtn']//span[@class='nb-btn-indicator']", wait_time=10
|
|
368
|
+
)
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
# Assert that the filter has been successfully applied to the URL, despite not being previously added to the
|
|
372
|
+
# selected filters list with "Add Filter" button.
|
|
373
|
+
self.assertEqual(self.browser.url, f"{self.live_server_url}{reverse('dcim:location_list')}" + "?asn=65001")
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
from django.test import tag
|
|
2
|
-
|
|
3
1
|
from nautobot.core.testing.integration import SeleniumTestCase
|
|
4
2
|
|
|
5
3
|
|
|
@@ -24,7 +22,6 @@ class ThemeTestCase(SeleniumTestCase):
|
|
|
24
22
|
# Validate modal is not visible
|
|
25
23
|
self.assertFalse(theme_modal[0].visible)
|
|
26
24
|
|
|
27
|
-
@tag("fix_in_v3")
|
|
28
25
|
def test_modal_rendered(self):
|
|
29
26
|
"""Modal should render when selecting the 'theme' button in the footer."""
|
|
30
27
|
|
|
@@ -35,30 +32,34 @@ class ThemeTestCase(SeleniumTestCase):
|
|
|
35
32
|
self.assertEqual(len(self.browser.find_by_xpath("//div[@class[contains(., 'modal-backdrop')]]")), 1)
|
|
36
33
|
|
|
37
34
|
# Validate modal is visible
|
|
38
|
-
theme_modal = self.browser.find_by_xpath("//div[@id
|
|
39
|
-
self.assertTrue(theme_modal[0].
|
|
35
|
+
theme_modal = self.browser.find_by_xpath("//div[@id='theme_modal']")
|
|
36
|
+
self.assertTrue(theme_modal[0].is_visible(wait_time=5))
|
|
40
37
|
|
|
41
38
|
# Validate 3 themes available to select
|
|
42
|
-
self.
|
|
43
|
-
len(self.browser.find_by_xpath("//div[@class[contains(., 'modal-body')]]//tbody/tr")), 1
|
|
44
|
-
) # 1 row
|
|
45
|
-
|
|
46
|
-
columns = self.browser.find_by_xpath("//div[@class[contains(., 'modal-body')]]//tbody/tr/td")
|
|
39
|
+
columns = self.browser.find_by_xpath("//div[@class[contains(., 'modal-body')]]//dl/dt")
|
|
47
40
|
self.assertEqual(len(columns), 3) # 3 columns (light, dark, system)
|
|
48
41
|
|
|
49
42
|
# Validate 3 modes in order are light, dark, and system
|
|
50
|
-
self.assertIn("
|
|
51
|
-
self.assertIn("
|
|
52
|
-
self.assertIn("
|
|
43
|
+
self.assertIn("Light", columns[0].html)
|
|
44
|
+
self.assertIn("Dark", columns[1].html)
|
|
45
|
+
self.assertIn("System", columns[2].html)
|
|
53
46
|
|
|
54
47
|
# Validate only System theme is selected by default
|
|
55
|
-
|
|
56
|
-
self.assertFalse(
|
|
57
|
-
|
|
58
|
-
self.
|
|
59
|
-
|
|
60
|
-
self.
|
|
48
|
+
light_theme = self.browser.find_by_xpath(".//dd/button[@data-nb-theme='light']")
|
|
49
|
+
self.assertFalse(light_theme[0].has_class("border"))
|
|
50
|
+
self.assertFalse(light_theme[0].has_class("border-primary"))
|
|
51
|
+
dark_theme = self.browser.find_by_xpath(".//dd/button[@data-nb-theme='dark']")
|
|
52
|
+
self.assertFalse(dark_theme[0].has_class("border"))
|
|
53
|
+
self.assertFalse(dark_theme[0].has_class("border-primary"))
|
|
54
|
+
system_theme = self.browser.find_by_xpath(".//dd/button[@data-nb-theme='system']")
|
|
55
|
+
self.assertTrue(system_theme[0].has_class("border"))
|
|
56
|
+
self.assertTrue(system_theme[0].has_class("border-primary"))
|
|
57
|
+
|
|
58
|
+
# Why is it required to click the cancel button twice? I honestly don't know, but for some reason Selenium seems
|
|
59
|
+
# to have troubles here. The first press only focuses the cancel button, and only after clicking it for the
|
|
60
|
+
# second time, the modal closes successfully.
|
|
61
|
+
self.browser.find_by_xpath(".//button[@id='dismiss-modal-theme']").click()
|
|
62
|
+
self.browser.find_by_xpath(".//button[@id='dismiss-modal-theme']").click()
|
|
61
63
|
|
|
62
64
|
# Validate Modal closes when cancel button clicked
|
|
63
|
-
self.
|
|
64
|
-
self.assertFalse(theme_modal[0].visible)
|
|
65
|
+
self.assertTrue(theme_modal[0].is_not_visible(wait_time=5))
|
|
@@ -10,6 +10,9 @@ from nautobot.core.settings_funcs import parse_redis_connection
|
|
|
10
10
|
|
|
11
11
|
ALLOWED_HOSTS = ["nautobot.example.com"]
|
|
12
12
|
|
|
13
|
+
# Do *not* send anonymized install metrics when migration or post_upgrade management commands are run while testing
|
|
14
|
+
INSTALLATION_METRICS_ENABLED = False
|
|
15
|
+
|
|
13
16
|
# Discover test jobs from within the Nautobot source code
|
|
14
17
|
JOBS_ROOT = os.path.join(
|
|
15
18
|
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "extras", "test_jobs"
|
nautobot/core/tests/runner.py
CHANGED
|
@@ -162,8 +162,7 @@ class NautobotTestRunner(DiscoverRunner):
|
|
|
162
162
|
db_command = [*command, "--database", alias]
|
|
163
163
|
call_command(*db_command)
|
|
164
164
|
|
|
165
|
-
|
|
166
|
-
call_command("refresh_dynamic_group_member_caches")
|
|
165
|
+
call_command("post_upgrade")
|
|
167
166
|
|
|
168
167
|
if self.parallel > 1:
|
|
169
168
|
for index in range(self.parallel):
|