nautobot 3.0.0a2__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 -2
- nautobot/apps/filters.py +7 -9
- nautobot/apps/models.py +2 -2
- nautobot/apps/ui.py +13 -1
- nautobot/apps/utils.py +8 -0
- nautobot/circuits/filters.py +3 -2
- nautobot/circuits/navigation.py +3 -2
- nautobot/circuits/templates/circuits/circuit_create.html +3 -3
- nautobot/circuits/templates/circuits/circuittermination_create.html +9 -24
- nautobot/circuits/templates/circuits/inc/circuit_termination_cable_fragment.html +6 -6
- nautobot/circuits/templates/circuits/inc/speed_widget.html +12 -12
- nautobot/circuits/tests/integration/test_circuit.py +10 -13
- nautobot/circuits/tests/integration/test_circuits_bulk_operations.py +0 -3
- nautobot/circuits/views.py +6 -2
- 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 +2 -0
- nautobot/core/api/views.py +12 -0
- nautobot/core/apps/__init__.py +11 -10
- nautobot/core/celery/__init__.py +3 -5
- nautobot/core/checks.py +46 -0
- nautobot/core/choices.py +1 -1
- nautobot/core/cli/bootstrap_v3_to_v5.py +105 -13
- nautobot/core/cli/migrate_deprecated_templates.py +227 -0
- nautobot/core/constants.py +3 -0
- nautobot/core/context_processors.py +9 -1
- nautobot/core/filters.py +4 -0
- nautobot/core/forms/__init__.py +2 -0
- nautobot/core/forms/forms.py +1 -1
- nautobot/core/forms/widgets.py +21 -2
- nautobot/core/jobs/__init__.py +62 -3
- nautobot/core/jobs/groups.py +31 -1
- nautobot/core/management/commands/generate_test_data.py +28 -9
- nautobot/core/models/__init__.py +11 -0
- nautobot/core/models/generics.py +9 -1
- nautobot/core/models/tree_queries.py +10 -5
- nautobot/core/models/utils.py +1 -1
- nautobot/core/settings.py +35 -19
- nautobot/core/settings.yaml +17 -33
- 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/base.html +1 -2
- nautobot/core/templates/admin/change_list.html +9 -12
- nautobot/core/templates/admin/config/config.html +12 -12
- nautobot/core/templates/admin/index.html +3 -3
- nautobot/core/templates/base_django.html +1 -2
- 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/header_extra_content_table.html +1 -1
- nautobot/core/templates/components/panel/panel.html +3 -3
- nautobot/core/templates/components/tab/content_wrapper.html +6 -7
- nautobot/core/templates/components/tab/label_wrapper_distinct_view.html +1 -1
- nautobot/core/templates/echarts/echarts.html +22 -9
- nautobot/core/templates/generic/object_bulk_add_component.html +2 -1
- nautobot/core/templates/generic/object_bulk_create.html +6 -5
- nautobot/core/templates/generic/object_bulk_delete.html +1 -1
- nautobot/core/templates/generic/object_bulk_destroy.html +3 -3
- 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_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_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_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 +4 -5
- nautobot/core/templates/graphene/graphiql.html +7 -8
- 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/footer.html +3 -1
- nautobot/core/templates/inc/form_static_field.html +6 -0
- nautobot/core/templates/inc/header.html +11 -1
- nautobot/core/templates/inc/image_attachments.html +2 -1
- nautobot/core/templates/inc/media.html +14 -0
- nautobot/core/templates/inc/nav_menu.html +3 -9
- nautobot/core/templates/inc/object_details_advanced_panel.html +2 -2
- nautobot/core/templates/inc/search_panel.html +4 -4
- nautobot/core/templates/login.html +4 -2
- nautobot/core/templates/nautobot_config.py.j2 +6 -11
- nautobot/core/templates/redoc_ui.html +7 -0
- nautobot/core/templates/rest_framework/api.html +103 -2
- 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 +37 -4
- nautobot/core/templates/utilities/theme_preview.html +19 -3
- nautobot/core/templates/widgets/number_input_with_choices.html +44 -0
- nautobot/core/templates/widgets/selectwithdisabled_option.html +3 -1
- nautobot/core/templatetags/helpers.py +76 -18
- nautobot/core/testing/api.py +68 -9
- nautobot/core/testing/filters.py +0 -23
- nautobot/core/testing/integration.py +41 -17
- nautobot/core/testing/mixins.py +2 -0
- nautobot/core/testing/utils.py +18 -4
- nautobot/core/testing/views.py +104 -13
- nautobot/core/tests/integration/test_app_home.py +34 -30
- nautobot/core/tests/integration/test_app_navbar.py +3 -0
- 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/nautobot_config_without_example_apps.py +4 -0
- nautobot/core/tests/runner.py +8 -1
- nautobot/core/tests/test_api.py +5 -3
- nautobot/core/tests/test_breadcrumbs.py +27 -28
- 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 +144 -3
- 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_renderers.py +59 -0
- nautobot/core/tests/test_settings_schema.py +1 -0
- nautobot/core/tests/test_tables.py +3 -1
- nautobot/core/tests/test_templatetags_helpers.py +62 -13
- nautobot/core/tests/test_templatetags_ui_framework.py +4 -4
- nautobot/core/tests/test_titles.py +0 -16
- nautobot/core/tests/test_tree_queries.py +14 -1
- nautobot/core/tests/test_ui.py +123 -4
- nautobot/core/tests/test_utils.py +72 -5
- nautobot/core/tests/test_views.py +159 -31
- nautobot/core/ui/breadcrumbs.py +70 -29
- nautobot/core/ui/bulk_buttons.py +1 -1
- nautobot/core/ui/choices.py +143 -27
- nautobot/core/ui/constants.py +76 -12
- nautobot/core/ui/echarts.py +15 -20
- nautobot/core/ui/object_detail.py +143 -55
- nautobot/core/ui/titles.py +3 -6
- nautobot/core/urls.py +20 -9
- nautobot/core/utils/cache.py +2 -1
- nautobot/core/utils/filtering.py +28 -18
- nautobot/core/utils/lookup.py +49 -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 +45 -22
- nautobot/core/views/renderers.py +4 -3
- 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 +3 -14
- 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 +3 -13
- nautobot/dcim/apps.py +4 -0
- nautobot/dcim/choices.py +65 -0
- nautobot/dcim/constants.py +7 -0
- nautobot/dcim/custom_validators.py +84 -0
- nautobot/dcim/factory.py +1 -1
- nautobot/dcim/filter_mixins.py +353 -4
- nautobot/dcim/{filters/__init__.py → filters.py} +15 -36
- nautobot/dcim/forms.py +90 -4
- 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/0079_remove_device_location_tenant_name_uniqueness.py +16 -0
- nautobot/dcim/migrations/0080_device_name_data_migration.py +59 -0
- 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 +98 -64
- nautobot/dcim/models/devices.py +30 -20
- nautobot/dcim/navigation.py +7 -6
- nautobot/dcim/tables/devices.py +18 -0
- nautobot/dcim/tables/devicetypes.py +8 -1
- nautobot/dcim/tables/racks.py +0 -2
- nautobot/dcim/tables/template_code.py +15 -15
- nautobot/dcim/templates/dcim/cable_connect.html +28 -112
- nautobot/dcim/templates/dcim/cable_trace.html +0 -4
- nautobot/dcim/templates/dcim/{cable_edit.html → cable_update.html} +1 -1
- nautobot/dcim/templates/dcim/consoleport.html +7 -6
- nautobot/dcim/templates/dcim/consoleserverport.html +7 -6
- nautobot/dcim/templates/dcim/device/config.html +2 -2
- nautobot/dcim/templates/dcim/device/lldp_neighbors.html +1 -1
- nautobot/dcim/templates/dcim/device/status.html +8 -8
- 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.html +1 -1
- nautobot/dcim/templates/dcim/devicebay_populate.html +2 -2
- 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 +10 -9
- nautobot/dcim/templates/dcim/inc/devicetype_component_table.html +1 -1
- nautobot/dcim/templates/dcim/inc/edit_form_softwareversion_js.html +2 -2
- 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 +35 -7
- nautobot/dcim/templates/dcim/interface_bulk_delete.html +1 -1
- nautobot/dcim/templates/dcim/interface_edit.html +2 -0
- nautobot/dcim/templates/dcim/inventoryitem.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/module/base.html +49 -9
- 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_list.html +57 -8
- 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/modulefamily_retrieve.html +1 -1
- nautobot/dcim/templates/dcim/moduletype_list.html +2 -2
- nautobot/dcim/templates/dcim/moduletype_retrieve.html +49 -9
- nautobot/dcim/templates/dcim/platform_create.html +1 -1
- nautobot/dcim/templates/dcim/poweroutlet.html +1 -1
- nautobot/dcim/templates/dcim/powerport.html +6 -5
- nautobot/dcim/templates/dcim/rack_elevation_list.html +17 -5
- nautobot/dcim/templates/dcim/rack_retrieve.html +22 -15
- nautobot/dcim/templates/dcim/rearport.html +8 -7
- nautobot/dcim/templates/dcim/trace/cable.html +1 -1
- nautobot/dcim/templates/dcim/virtualchassis_add_member.html +16 -14
- nautobot/dcim/templates/dcim/virtualchassis_update.html +15 -7
- nautobot/dcim/tests/integration/test_controller.py +4 -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 +194 -6
- nautobot/dcim/tests/test_custom_validators.py +229 -0
- nautobot/dcim/tests/test_filters.py +55 -7
- nautobot/dcim/tests/test_forms.py +110 -8
- nautobot/dcim/tests/test_graphql.py +44 -1
- nautobot/dcim/tests/test_models.py +328 -4
- nautobot/dcim/tests/test_tables.py +160 -0
- nautobot/dcim/tests/test_views.py +132 -29
- nautobot/dcim/urls.py +64 -21
- nautobot/dcim/utils.py +3 -3
- nautobot/dcim/views.py +777 -397
- nautobot/extras/api/views.py +60 -45
- nautobot/extras/choices.py +2 -13
- nautobot/extras/datasources/git.py +3 -1
- 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} +33 -48
- nautobot/extras/forms/forms.py +14 -15
- nautobot/extras/forms/mixins.py +0 -41
- nautobot/extras/jobs.py +2 -0
- nautobot/extras/jobs_ui.py +4 -3
- nautobot/extras/management/__init__.py +11 -0
- nautobot/extras/management/commands/refresh_dynamic_group_member_caches.py +4 -1
- 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/migrations/0131_configcontext_device_families.py +18 -0
- nautobot/extras/models/__init__.py +1 -2
- nautobot/extras/models/approvals.py +33 -14
- nautobot/extras/models/change_logging.py +4 -0
- nautobot/extras/models/contacts.py +2 -0
- nautobot/extras/models/groups.py +44 -5
- nautobot/extras/models/jobs.py +60 -4
- nautobot/extras/models/mixins.py +28 -0
- nautobot/extras/models/models.py +23 -2
- 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/marketplace_manifest.yml +49 -1
- 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 -9
- nautobot/extras/querysets.py +8 -0
- nautobot/extras/signals.py +20 -19
- nautobot/extras/tables.py +64 -68
- nautobot/extras/templates/django_ajax_tables/ajax_wrapper.html +2 -0
- 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/configcontext_update.html +1 -0
- nautobot/extras/templates/extras/configcontextschema_validation.html +2 -2
- 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/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 -39
- nautobot/extras/templates/extras/plugin_detail.html +29 -24
- 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/secret_create.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_computedfields.py +8 -9
- nautobot/extras/tests/integration/test_configcontextschema.py +27 -26
- nautobot/extras/tests/integration/test_customfields.py +9 -10
- nautobot/extras/tests/integration/test_dynamicgroups.py +12 -9
- nautobot/extras/tests/integration/test_plugin_banner.py +3 -0
- nautobot/extras/tests/integration/test_plugins.py +18 -6
- nautobot/extras/tests/integration/test_relationships.py +0 -2
- nautobot/extras/tests/test_api.py +90 -18
- nautobot/extras/tests/test_approvals.py +38 -38
- nautobot/extras/tests/test_changelog.py +59 -5
- 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 +57 -22
- 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 +51 -33
- nautobot/extras/tests/test_plugins.py +36 -10
- nautobot/extras/tests/test_utils.py +3 -4
- nautobot/extras/tests/test_views.py +52 -112
- nautobot/extras/urls.py +0 -14
- nautobot/extras/views.py +164 -71
- nautobot/ipam/factory.py +7 -0
- nautobot/ipam/filter_mixins.py +38 -0
- nautobot/ipam/filters.py +53 -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 +19 -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_ip_addresses.html +1 -1
- nautobot/ipam/templates/ipam/namespace_prefixes.html +1 -1
- nautobot/ipam/templates/ipam/namespace_update.html +15 -0
- nautobot/ipam/templates/ipam/namespace_vrfs.html +1 -1
- nautobot/ipam/templates/ipam/prefix_delete.html +1 -1
- nautobot/ipam/templates/ipam/prefix_list.html +14 -13
- 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 +36 -1
- nautobot/ipam/tests/test_forms.py +1 -1
- nautobot/ipam/tests/test_models.py +44 -2
- 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 +53 -11
- 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/cabletrace.js +1 -1
- nautobot/project-static/js/forms.js +13 -0
- nautobot/project-static/js/interface_filtering.js +20 -16
- nautobot/project-static/nautobot-icons/battery-3.svg +3 -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/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/package-lock.json +87 -4
- nautobot/ui/package.json +2 -1
- nautobot/ui/src/js/collapse.js +3 -3
- nautobot/ui/src/js/nautobot.js +16 -1
- nautobot/ui/src/js/select2.js +53 -2
- nautobot/ui/src/scss/colors.scss +1 -1
- nautobot/ui/src/scss/nautobot.scss +112 -30
- nautobot/ui/webpack.config.js +13 -0
- nautobot/users/templates/users/preferences.html +11 -2
- 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/filters.py +6 -1
- 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_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_filters.py +10 -1
- nautobot/virtualization/tests/test_models.py +45 -4
- nautobot/virtualization/views.py +4 -1
- 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 +219 -0
- nautobot/vpn/filters.py +234 -0
- nautobot/vpn/forms.py +487 -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 +535 -0
- nautobot/vpn/navigation.py +98 -0
- nautobot/vpn/tables.py +383 -0
- nautobot/vpn/templates/vpn/vpnprofile_create.html +150 -0
- nautobot/vpn/tests/__init__.py +0 -0
- nautobot/vpn/tests/test_api.py +336 -0
- nautobot/vpn/tests/test_filters.py +139 -0
- nautobot/vpn/tests/test_forms.py +293 -0
- nautobot/vpn/tests/test_models.py +147 -0
- nautobot/vpn/tests/test_views.py +300 -0
- nautobot/vpn/urls.py +16 -0
- nautobot/vpn/views.py +495 -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.0rc1.dist-info}/METADATA +15 -15
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/RECORD +514 -572
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/entry_points.txt +1 -0
- 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/template_content.py +0 -42
- nautobot/data_validation/templates/data_validation/datacompliance_retrieve.html +0 -1
- nautobot/dcim/filters/mixins.py +0 -354
- 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 -243
- 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 -97
- 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/inc/prefix_header_extra_content_table.html +0 -4
- 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/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/dcim/templates/dcim/{cable.html → cable_retrieve.html} +0 -0
- /nautobot/tenancy/{filters/mixins.py → filter_mixins.py} +0 -0
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/LICENSE.txt +0 -0
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/NOTICE +0 -0
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/WHEEL +0 -0
|
@@ -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
|
|
@@ -64,13 +66,14 @@ class ObjectsListMixin:
|
|
|
64
66
|
"""
|
|
65
67
|
Click bulk delete from dropdown menu on bottom of the items table list.
|
|
66
68
|
"""
|
|
67
|
-
self.
|
|
68
|
-
"document.querySelector('#bulk-action-buttons button[type=\"submit\"]').scrollIntoView()"
|
|
69
|
-
)
|
|
69
|
+
self.scroll_element_into_view(css='#bulk-action-buttons button[type="submit"]')
|
|
70
70
|
self.browser.find_by_xpath(
|
|
71
71
|
'//*[@id="bulk-action-buttons"]//button[@type="submit"]/following-sibling::button[1]'
|
|
72
72
|
).click()
|
|
73
|
-
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()
|
|
74
77
|
|
|
75
78
|
def click_bulk_delete_all(self):
|
|
76
79
|
"""
|
|
@@ -94,7 +97,7 @@ class ObjectsListMixin:
|
|
|
94
97
|
"""
|
|
95
98
|
Click add item button on top of the items table list.
|
|
96
99
|
"""
|
|
97
|
-
self.click_button("#
|
|
100
|
+
self.click_button("#add-button")
|
|
98
101
|
|
|
99
102
|
def click_table_link(self, row=1, column=2):
|
|
100
103
|
"""By default, tries to click column next to checkbox to go to the details page."""
|
|
@@ -108,7 +111,7 @@ class ObjectsListMixin:
|
|
|
108
111
|
objects_table_container = self.browser.find_by_xpath('//*[@id="object_list_form"]')
|
|
109
112
|
try:
|
|
110
113
|
objects_table = objects_table_container.find_by_tag("tbody")
|
|
111
|
-
return len(objects_table.
|
|
114
|
+
return len(objects_table.find_by_xpath(".//tr[not(count(td[@colspan])=1)]"))
|
|
112
115
|
except ElementDoesNotExist:
|
|
113
116
|
return 0
|
|
114
117
|
|
|
@@ -129,7 +132,7 @@ class ObjectDetailsMixin:
|
|
|
129
132
|
By default, it's not using the exact match, because on the UI we're often adding
|
|
130
133
|
additional tags, relationships or units.
|
|
131
134
|
"""
|
|
132
|
-
panel_xpath = f'//*[@id="main"]//div[@class
|
|
135
|
+
panel_xpath = f'//*[@id="main"]//div[contains(@class, "card-header") and contains(normalize-space(), "{panel_label}")]/following-sibling::div[contains(@class, "collapse")]/table'
|
|
133
136
|
value = self.browser.find_by_xpath(f'{panel_xpath}//td[text()="{field_label}"]/following-sibling::td[1]').text
|
|
134
137
|
|
|
135
138
|
if exact_match:
|
|
@@ -144,7 +147,7 @@ class ObjectDetailsMixin:
|
|
|
144
147
|
TODO: remove after all panels will be moved to UI Components Framework or new Bootstrap 5 templates.
|
|
145
148
|
"""
|
|
146
149
|
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']"
|
|
150
|
+
expand_button_xpath = f"{panel_xpath}/button[normalize-space()='Expand All Groups']"
|
|
148
151
|
expand_button = self.browser.find_by_xpath(expand_button_xpath)
|
|
149
152
|
if not expand_button.is_empty():
|
|
150
153
|
expand_button.click()
|
|
@@ -236,7 +239,7 @@ class BulkOperationsMixin:
|
|
|
236
239
|
button_text = self.browser.find_by_xpath('//button[@name="_confirm" and @type="submit"]').text
|
|
237
240
|
self.assertIn(f"Delete these {expected_count}", button_text)
|
|
238
241
|
|
|
239
|
-
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
|
|
240
243
|
self.assertIn(f"The following operation will delete {expected_count}", message_text)
|
|
241
244
|
|
|
242
245
|
def assertIsBulkDeleteJob(self):
|
|
@@ -374,10 +377,12 @@ class SeleniumTestCase(StaticLiveServerTestCase, testing.NautobotTestCaseMixin):
|
|
|
374
377
|
sidenav_button = self.browser.find_by_xpath(f"{section_xpath}/button", wait_time=5)
|
|
375
378
|
if not sidenav_button["aria-expanded"] == "true":
|
|
376
379
|
sidenav_button.click()
|
|
377
|
-
child_menu_xpath = f"{section_xpath}/div[@class='nb-sidenav-flyout']//a[@class
|
|
380
|
+
child_menu_xpath = f"{section_xpath}/div[@class='nb-sidenav-flyout']//a[contains(@class, 'nb-sidenav-link') and normalize-space()='{child_menu_name}']"
|
|
378
381
|
child_menu = self.browser.find_by_xpath(child_menu_xpath, wait_time=5)
|
|
382
|
+
old_url = self.browser.url
|
|
379
383
|
child_menu.click()
|
|
380
384
|
|
|
385
|
+
WebDriverWait(self.browser, 30).until(lambda driver: driver.url != old_url)
|
|
381
386
|
# Wait for body element to appear
|
|
382
387
|
self.assertTrue(self.browser.is_element_present_by_tag("body", wait_time=5), "Page failed to load")
|
|
383
388
|
|
|
@@ -389,7 +394,7 @@ class SeleniumTestCase(StaticLiveServerTestCase, testing.NautobotTestCaseMixin):
|
|
|
389
394
|
add_button.click()
|
|
390
395
|
|
|
391
396
|
# Wait for body element to appear
|
|
392
|
-
self.assertTrue(self.browser.
|
|
397
|
+
self.assertTrue(self.browser.is_element_present_by_name("_create", wait_time=5), "Page failed to load")
|
|
393
398
|
|
|
394
399
|
def click_edit_form_create_button(self):
|
|
395
400
|
"""
|
|
@@ -399,7 +404,7 @@ class SeleniumTestCase(StaticLiveServerTestCase, testing.NautobotTestCaseMixin):
|
|
|
399
404
|
add_button.click()
|
|
400
405
|
|
|
401
406
|
# Wait for body element to appear
|
|
402
|
-
self.assertTrue(self.browser.
|
|
407
|
+
self.assertTrue(self.browser.is_element_present_by_css(".alert-success", wait_time=5), "Page failed to load")
|
|
403
408
|
|
|
404
409
|
def _fill_select2_field(self, field_name, value, search_box_class=None):
|
|
405
410
|
"""
|
|
@@ -409,7 +414,7 @@ class SeleniumTestCase(StaticLiveServerTestCase, testing.NautobotTestCaseMixin):
|
|
|
409
414
|
search_box_class = "select2-search select2-search--dropdown"
|
|
410
415
|
|
|
411
416
|
self.browser.find_by_xpath(f"//select[@id='id_{field_name}']//following-sibling::span").click()
|
|
412
|
-
self.
|
|
417
|
+
self.scroll_element_into_view(css=f"#id_{field_name}")
|
|
413
418
|
search_box = self.browser.find_by_xpath(f"//*[@class='{search_box_class}']//input", wait_time=5)
|
|
414
419
|
for _ in search_box.first.type(value, slowly=True):
|
|
415
420
|
pass
|
|
@@ -453,17 +458,22 @@ class SeleniumTestCase(StaticLiveServerTestCase, testing.NautobotTestCaseMixin):
|
|
|
453
458
|
search_box.first.type(Keys.ENTER)
|
|
454
459
|
|
|
455
460
|
def click_button(self, query_selector):
|
|
456
|
-
|
|
461
|
+
self.browser.is_element_present_by_css(query_selector, wait_time=5)
|
|
457
462
|
# Button might be visible but on the edge and then impossible to click due to vertical/horizontal scrolls
|
|
458
|
-
self.
|
|
463
|
+
self.scroll_element_into_view(css=query_selector)
|
|
464
|
+
# Scrolling may be asynchronous, wait until it's actually clickable.
|
|
465
|
+
WebDriverWait(self.browser.driver, 30).until(element_to_be_clickable((By.CSS_SELECTOR, query_selector)))
|
|
466
|
+
btn = self.browser.find_by_css(query_selector)
|
|
459
467
|
btn.click()
|
|
460
468
|
|
|
461
469
|
def fill_input(self, input_name, input_value):
|
|
462
470
|
"""
|
|
463
471
|
Helper function to fill an input field. Solves issue with element could not be scrolled into view for some pages.
|
|
464
472
|
"""
|
|
465
|
-
|
|
466
|
-
self.browser.
|
|
473
|
+
self.browser.is_element_present_by_name(input_name, wait_time=5)
|
|
474
|
+
element = self.browser.find_by_name(input_name)
|
|
475
|
+
self.scroll_element_into_view(element=element)
|
|
476
|
+
element.is_visible(wait_time=5)
|
|
467
477
|
self.browser.execute_script("arguments[0].focus();", element.first._element)
|
|
468
478
|
self.browser.fill(input_name, input_value)
|
|
469
479
|
|
|
@@ -473,6 +483,20 @@ class SeleniumTestCase(StaticLiveServerTestCase, testing.NautobotTestCaseMixin):
|
|
|
473
483
|
self.login(self.user.username, self.password)
|
|
474
484
|
self.logged_in = True
|
|
475
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
|
+
|
|
476
500
|
|
|
477
501
|
class BulkOperationsTestCases:
|
|
478
502
|
"""
|
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/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,8 +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
|
-
|
|
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)))
|
|
196
201
|
|
|
197
202
|
# If any Relationships are defined, they should appear in the response
|
|
198
203
|
if self.relationships is not None:
|
|
@@ -362,18 +367,104 @@ class ViewTestCases:
|
|
|
362
367
|
|
|
363
368
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
|
364
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()
|
|
365
417
|
instance = self._get_queryset().first()
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
self.
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
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))
|
|
377
468
|
|
|
378
469
|
class GetObjectChangelogViewTestCase(ModelViewTestCase):
|
|
379
470
|
"""
|
|
@@ -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):
|
|
@@ -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"
|