nautobot 2.4.21__py3-none-any.whl → 3.0.0a3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of nautobot might be problematic. Click here for more details.
- nautobot/apps/choices.py +2 -2
- nautobot/apps/filters.py +9 -9
- nautobot/apps/forms.py +2 -0
- nautobot/apps/models.py +7 -2
- nautobot/apps/ui.py +20 -1
- nautobot/apps/utils.py +2 -3
- nautobot/apps/views.py +7 -1
- nautobot/circuits/filters.py +8 -23
- nautobot/circuits/navigation.py +3 -1
- nautobot/circuits/templates/circuits/circuit_create.html +9 -9
- nautobot/circuits/templates/circuits/circuit_terminations_swap.html +2 -2
- nautobot/circuits/templates/circuits/circuittermination_create.html +24 -33
- nautobot/circuits/templates/circuits/inc/circuit_termination.html +10 -10
- nautobot/circuits/templates/circuits/inc/circuit_termination_cable_fragment.html +13 -13
- nautobot/circuits/templates/circuits/inc/circuit_termination_header_extra_content.html +6 -6
- nautobot/circuits/templates/circuits/inc/circuit_termination_speed_fragment.html +3 -3
- nautobot/circuits/templates/circuits/inc/speed_widget.html +13 -13
- nautobot/circuits/templates/circuits/provider_create.html +9 -9
- nautobot/circuits/tests/integration/test_circuit.py +19 -19
- nautobot/circuits/tests/integration/test_circuits_bulk_operations.py +3 -0
- nautobot/circuits/tests/integration/test_relationships.py +4 -12
- nautobot/circuits/views.py +0 -2
- nautobot/cloud/filters.py +1 -13
- nautobot/cloud/navigation.py +3 -1
- nautobot/cloud/templates/cloud/cloudnetwork_update.html +9 -9
- nautobot/cloud/templates/cloud/cloudservice_update.html +6 -6
- nautobot/core/api/fields.py +30 -2
- nautobot/core/api/schema.py +1 -1
- nautobot/core/api/serializers.py +9 -2
- nautobot/core/api/urls.py +2 -0
- nautobot/core/api/views.py +58 -37
- nautobot/core/apps/__init__.py +6 -12
- nautobot/core/branching.py +83 -0
- nautobot/core/celery/__init__.py +11 -6
- nautobot/core/celery/backends.py +2 -0
- nautobot/core/celery/encoders.py +7 -0
- nautobot/core/celery/task.py +44 -0
- nautobot/core/checks.py +60 -0
- nautobot/core/cli/bootstrap_v3_to_v5.py +776 -0
- nautobot/core/constants.py +9 -0
- nautobot/core/context_processors.py +84 -0
- nautobot/core/filters.py +131 -2
- nautobot/core/forms/__init__.py +4 -2
- nautobot/core/forms/fields.py +10 -8
- nautobot/core/forms/forms.py +21 -9
- nautobot/core/forms/search.py +0 -15
- nautobot/core/forms/widgets.py +3 -2
- nautobot/core/graphql/__init__.py +8 -26
- nautobot/core/graphql/generators.py +16 -6
- nautobot/core/graphql/schema.py +1 -1
- nautobot/core/graphql/schema_init.py +1 -2
- nautobot/core/graphql/utils.py +7 -9
- nautobot/core/jobs/__init__.py +158 -0
- nautobot/core/management/commands/generate_test_data.py +28 -9
- nautobot/core/models/__init__.py +17 -2
- nautobot/core/models/fields.py +3 -2
- nautobot/core/models/generics.py +9 -1
- nautobot/core/models/name_color_content_types.py +1 -1
- nautobot/core/models/ordering.py +7 -5
- nautobot/core/models/querysets.py +77 -2
- nautobot/core/models/tree_queries.py +6 -4
- nautobot/core/settings.py +30 -16
- nautobot/core/settings.yaml +13 -7
- nautobot/core/tables.py +114 -44
- nautobot/core/templates/403.html +1 -1
- nautobot/core/templates/403_csrf_failure.html +1 -1
- nautobot/core/templates/404.html +1 -1
- nautobot/core/templates/40x.html +8 -8
- nautobot/core/templates/500.html +10 -10
- nautobot/core/templates/about.html +13 -12
- nautobot/core/templates/admin/actions.html +1 -1
- nautobot/core/templates/admin/app_index.html +3 -3
- nautobot/core/templates/admin/base.html +45 -52
- nautobot/core/templates/admin/base_site.html +0 -9
- nautobot/core/templates/admin/change_form.html +5 -5
- nautobot/core/templates/admin/change_list.html +8 -12
- nautobot/core/templates/admin/change_list_results.html +3 -3
- nautobot/core/templates/admin/config/config.html +24 -24
- nautobot/core/templates/admin/delete_confirmation.html +5 -5
- nautobot/core/templates/admin/edit_inline/stacked.html +5 -5
- nautobot/core/templates/admin/edit_inline/tabular.html +3 -3
- nautobot/core/templates/admin/includes/fieldset.html +15 -15
- nautobot/core/templates/admin/index.html +8 -8
- nautobot/core/templates/admin/submit_line.html +5 -5
- nautobot/core/templates/base_django.html +36 -32
- nautobot/core/templates/buttons/add.html +1 -1
- nautobot/core/templates/buttons/consolidated_detail_view_action_buttons.html +2 -2
- nautobot/core/templates/buttons/export.html +17 -18
- nautobot/core/templates/buttons/job_import.html +2 -2
- nautobot/core/templates/components/breadcrumbs.html +19 -17
- nautobot/core/templates/components/button/dropdown.html +7 -5
- nautobot/core/templates/components/echarts.html +2 -0
- nautobot/core/templates/components/layout/one_over_two.html +3 -3
- nautobot/core/templates/components/layout/two_over_one.html +3 -3
- nautobot/core/templates/components/panel/body_content_data_table.html +2 -2
- nautobot/core/templates/components/panel/body_content_tags.html +1 -1
- nautobot/core/templates/components/panel/body_wrapper_generic.html +4 -2
- nautobot/core/templates/components/panel/body_wrapper_generic_table.html +1 -1
- nautobot/core/templates/components/panel/body_wrapper_key_value_table.html +5 -3
- nautobot/core/templates/components/panel/body_wrapper_table.html +4 -2
- nautobot/core/templates/components/panel/footer_contacts_table.html +4 -4
- nautobot/core/templates/components/panel/footer_content_table.html +3 -3
- nautobot/core/templates/components/panel/grouping_toggle.html +12 -11
- nautobot/core/templates/components/panel/header_extra_content_table.html +2 -11
- nautobot/core/templates/components/panel/panel.html +6 -3
- nautobot/core/templates/components/panel/stats_panel_body.html +9 -7
- nautobot/core/templates/components/tab/content_wrapper.html +29 -1
- nautobot/core/templates/components/tab/label_wrapper.html +10 -2
- nautobot/core/templates/components/tab/label_wrapper_distinct_view.html +11 -4
- nautobot/core/templates/echarts/echarts.html +20 -0
- nautobot/core/templates/exceptions/import_error.html +2 -2
- nautobot/core/templates/exceptions/permission_error.html +1 -1
- nautobot/core/templates/exceptions/programming_error.html +2 -2
- nautobot/core/templates/generic/object_bulk_add_component.html +29 -20
- nautobot/core/templates/generic/object_bulk_create.html +87 -75
- nautobot/core/templates/generic/object_bulk_destroy.html +35 -37
- nautobot/core/templates/generic/object_bulk_remove.html +30 -26
- nautobot/core/templates/generic/object_bulk_rename.html +53 -40
- nautobot/core/templates/generic/object_bulk_update.html +36 -29
- nautobot/core/templates/generic/object_create.html +40 -27
- nautobot/core/templates/generic/object_import.html +36 -24
- nautobot/core/templates/generic/object_list.html +279 -215
- nautobot/core/templates/generic/object_notes.html +21 -11
- nautobot/core/templates/generic/object_retrieve.html +161 -213
- nautobot/core/templates/graphene/graphiql.html +113 -60
- nautobot/core/templates/home.html +164 -87
- nautobot/core/templates/import_success.html +3 -2
- nautobot/core/templates/inc/ajax_loader.html +1 -1
- nautobot/core/templates/inc/computed_fields/panel_data.html +25 -13
- nautobot/core/templates/inc/created_updated.html +12 -7
- nautobot/core/templates/inc/custom_fields/panel_data.html +28 -16
- nautobot/core/templates/inc/custom_fields_panel.html +3 -3
- nautobot/core/templates/inc/dynamic_groups_panel.html +3 -3
- nautobot/core/templates/inc/extras_features_edit_form_fields.html +15 -15
- nautobot/core/templates/inc/footer.html +90 -40
- nautobot/core/templates/inc/form_static_field.html +6 -0
- nautobot/core/templates/inc/header.html +75 -0
- nautobot/core/templates/inc/header_banners.html +17 -0
- nautobot/core/templates/inc/header_messages.html +6 -0
- nautobot/core/templates/inc/image_attachments.html +9 -9
- nautobot/core/templates/inc/javascript.html +7 -24
- nautobot/core/templates/inc/media.html +4 -29
- nautobot/core/templates/inc/modal.html +2 -2
- nautobot/core/templates/inc/nav_favorites.html +27 -0
- nautobot/core/templates/inc/nav_menu.html +150 -108
- nautobot/core/templates/inc/object_details_advanced_panel.html +84 -71
- nautobot/core/templates/inc/page_title.html +23 -0
- nautobot/core/templates/inc/paginator.html +39 -28
- nautobot/core/templates/inc/relationships/panel_override.html +3 -3
- nautobot/core/templates/inc/relationships_panel.html +3 -3
- nautobot/core/templates/inc/relationships_table_rows.html +1 -1
- nautobot/core/templates/inc/search_panel.html +22 -16
- nautobot/core/templates/inc/table.html +61 -36
- nautobot/core/templates/inc/tenancy_form_panel.html +3 -3
- nautobot/core/templates/login.html +17 -59
- nautobot/core/templates/modals/modal_theme.html +12 -23
- nautobot/core/templates/nautobot_config.py.j2 +6 -5
- nautobot/core/templates/panel_table.html +8 -12
- nautobot/core/templates/redoc_ui.html +80 -0
- nautobot/core/templates/rest_framework/api.html +43 -21
- nautobot/core/templates/search.html +12 -13
- nautobot/core/templates/swagger_ui.html +19 -4
- nautobot/core/templates/system_jobs/import_objects.html +70 -58
- nautobot/core/templates/template.css +0 -6
- nautobot/core/templates/utilities/comment_form.html +34 -0
- nautobot/core/templates/utilities/confirmation_form.html +17 -9
- nautobot/core/templates/utilities/obj_table.html +19 -11
- nautobot/core/templates/utilities/render_field.html +27 -21
- nautobot/core/templates/utilities/render_jinja2.html +22 -25
- nautobot/core/templates/utilities/templatetags/advanced_filter_indicator.html +8 -0
- nautobot/core/templates/utilities/templatetags/badge.html +1 -1
- nautobot/core/templates/utilities/templatetags/dynamic_group_assignment_modal.html +2 -3
- nautobot/core/templates/utilities/templatetags/filter_form_drawer.html +482 -0
- nautobot/core/templates/utilities/templatetags/modal_form_as_dialog.html +14 -18
- nautobot/core/templates/utilities/templatetags/saved_view_modal.html +11 -11
- nautobot/core/templates/utilities/templatetags/table_config_form.html +51 -24
- nautobot/core/templates/utilities/templatetags/tag.html +1 -1
- nautobot/core/templates/utilities/templatetags/utilization_graph.html +3 -3
- nautobot/core/templates/utilities/theme_preview.html +829 -566
- nautobot/core/templates/utilities/worker_status.html +42 -41
- nautobot/core/templates/widgets/selectwithdisabled_option.html +3 -1
- nautobot/core/templates/widgets/sluginput.html +2 -2
- nautobot/core/templatetags/buttons.py +38 -40
- nautobot/core/templatetags/helpers.py +105 -28
- nautobot/core/templatetags/ui_framework.py +17 -0
- nautobot/core/testing/api.py +76 -12
- nautobot/core/testing/filters.py +11 -27
- nautobot/core/testing/integration.py +128 -10
- nautobot/core/testing/mixins.py +7 -4
- nautobot/core/testing/utils.py +28 -5
- nautobot/core/testing/views.py +125 -27
- nautobot/core/tests/integration/test_app_home.py +39 -35
- nautobot/core/tests/integration/test_app_navbar.py +60 -67
- nautobot/core/tests/integration/test_filters.py +123 -55
- nautobot/core/tests/integration/test_general_functionality.py +1 -1
- nautobot/core/tests/integration/test_home.py +10 -18
- nautobot/core/tests/integration/test_import_objects_ui.py +2 -9
- nautobot/core/tests/integration/test_navbar.py +41 -16
- nautobot/core/tests/integration/test_swagger.py +1 -7
- nautobot/core/tests/integration/test_theme.py +3 -0
- nautobot/core/tests/nautobot_config_without_example_apps.py +4 -0
- nautobot/core/tests/runner.py +6 -1
- nautobot/core/tests/test_api.py +5 -3
- nautobot/core/tests/test_branching.py +154 -0
- nautobot/core/tests/test_breadcrumbs.py +7 -8
- nautobot/core/tests/test_checks.py +28 -0
- nautobot/core/tests/test_commands.py +0 -41
- nautobot/core/tests/test_config.py +2 -1
- nautobot/core/tests/test_csv.py +4 -7
- nautobot/core/tests/test_filters.py +326 -318
- nautobot/core/tests/test_forms.py +19 -30
- nautobot/core/tests/test_graphql.py +67 -57
- nautobot/core/tests/test_models.py +1 -1
- nautobot/core/tests/test_nautobot_server.py +2 -0
- nautobot/core/tests/test_navigations.py +78 -10
- nautobot/core/tests/test_tables.py +3 -1
- nautobot/core/tests/test_templatetags_helpers.py +61 -21
- nautobot/core/tests/test_templatetags_ui_framework.py +36 -18
- nautobot/core/tests/test_ui.py +207 -2
- nautobot/core/tests/test_utils.py +147 -2
- nautobot/core/tests/test_views.py +201 -64
- nautobot/core/tests/test_views_utils.py +1 -1
- nautobot/core/ui/breadcrumbs.py +2 -12
- nautobot/core/ui/choices.py +190 -0
- nautobot/core/ui/constants.py +86 -0
- nautobot/core/ui/echarts.py +474 -0
- nautobot/core/ui/nav.py +5 -1
- nautobot/core/ui/object_detail.py +180 -16
- nautobot/core/urls.py +13 -1
- nautobot/core/utils/cache.py +71 -0
- nautobot/core/utils/data.py +8 -5
- nautobot/core/utils/filtering.py +8 -2
- nautobot/core/utils/git.py +3 -3
- nautobot/core/utils/lookup.py +87 -13
- nautobot/core/utils/migrations.py +22 -0
- nautobot/core/utils/module_loading.py +26 -0
- nautobot/core/utils/permissions.py +9 -5
- nautobot/core/views/__init__.py +114 -63
- nautobot/core/views/generic.py +34 -27
- nautobot/core/views/mixins.py +49 -27
- nautobot/core/views/renderers.py +3 -5
- nautobot/core/views/utils.py +10 -5
- nautobot/core/views/viewsets.py +2 -1
- nautobot/data_validation/__init__.py +0 -0
- nautobot/data_validation/api/__init__.py +1 -0
- nautobot/data_validation/api/serializers.py +80 -0
- nautobot/data_validation/api/urls.py +20 -0
- nautobot/data_validation/api/views.py +44 -0
- nautobot/data_validation/apps.py +18 -0
- nautobot/data_validation/custom_validators.py +330 -0
- nautobot/data_validation/filters.py +133 -0
- nautobot/data_validation/form_mixin.py +25 -0
- nautobot/data_validation/forms.py +342 -0
- nautobot/data_validation/migrations/0001_initial.py +224 -0
- nautobot/data_validation/migrations/0002_data_migration_from_app.py +324 -0
- nautobot/data_validation/migrations/__init__.py +0 -0
- nautobot/data_validation/models.py +361 -0
- nautobot/data_validation/navigation.py +74 -0
- nautobot/data_validation/signals.py +30 -0
- nautobot/data_validation/tables.py +259 -0
- nautobot/data_validation/templates/data_validation/datacompliance_retrieve.html +1 -0
- nautobot/data_validation/templates/data_validation/datacompliance_tab.html +11 -0
- nautobot/data_validation/templates/data_validation/device_constraints.html +61 -0
- nautobot/data_validation/tests/__init__.py +20 -0
- nautobot/data_validation/tests/migrations/__init__.py +0 -0
- nautobot/data_validation/tests/migrations/test_migrations.py +489 -0
- nautobot/data_validation/tests/test_api.py +238 -0
- nautobot/data_validation/tests/test_custom_validators.py +423 -0
- nautobot/data_validation/tests/test_data_compliance_rules.py +85 -0
- nautobot/data_validation/tests/test_filters.py +240 -0
- nautobot/data_validation/tests/test_form_mixin.py +115 -0
- nautobot/data_validation/tests/test_models.py +393 -0
- nautobot/data_validation/tests/test_views.py +435 -0
- nautobot/data_validation/urls.py +21 -0
- nautobot/data_validation/views.py +227 -0
- nautobot/dcim/api/serializers.py +10 -13
- nautobot/dcim/api/urls.py +2 -0
- nautobot/dcim/api/views.py +7 -0
- nautobot/dcim/apps.py +4 -0
- nautobot/dcim/choices.py +16 -0
- nautobot/dcim/custom_validators.py +84 -0
- nautobot/dcim/filter_mixins.py +353 -4
- nautobot/dcim/{filters/__init__.py → filters.py} +70 -157
- nautobot/dcim/forms.py +12 -6
- nautobot/dcim/graphql/types.py +1 -0
- nautobot/dcim/migrations/0075_add_deviceclusterassignment.py +52 -0
- nautobot/dcim/migrations/0076_device_cluster_to_clusters_data_migration.py +40 -0
- nautobot/dcim/migrations/0077_remove_device_cluster.py +14 -0
- nautobot/dcim/migrations/0078_remove_device_location_tenant_name_uniqueness.py +16 -0
- nautobot/dcim/migrations/0079_device_name_data_migration.py +59 -0
- nautobot/dcim/models/__init__.py +2 -0
- nautobot/dcim/models/device_components.py +3 -1
- nautobot/dcim/models/devices.py +115 -51
- nautobot/dcim/navigation.py +7 -3
- nautobot/dcim/querysets.py +6 -0
- nautobot/dcim/signals.py +19 -0
- nautobot/dcim/tables/devices.py +9 -5
- nautobot/dcim/tables/template_code.py +191 -102
- nautobot/dcim/templates/dcim/cable.html +1 -1
- nautobot/dcim/templates/dcim/cable_connect.html +62 -146
- nautobot/dcim/templates/dcim/cable_retrieve.html +10 -10
- nautobot/dcim/templates/dcim/cable_trace.html +15 -17
- nautobot/dcim/templates/dcim/console_port_connection_list.html +2 -2
- nautobot/dcim/templates/dcim/consoleport.html +18 -17
- nautobot/dcim/templates/dcim/consoleserverport.html +18 -17
- nautobot/dcim/templates/dcim/controller_create.html +12 -8
- nautobot/dcim/templates/dcim/controller_wirelessnetworks.html +1 -1
- nautobot/dcim/templates/dcim/controllermanageddevicegroup_create.html +6 -6
- nautobot/dcim/templates/dcim/controllermanageddevicegroup_retrieve.html +1 -1
- nautobot/dcim/templates/dcim/device/config.html +17 -19
- nautobot/dcim/templates/dcim/device/lldp_neighbors.html +4 -4
- nautobot/dcim/templates/dcim/device/status.html +20 -20
- nautobot/dcim/templates/dcim/device_component_add.html +24 -15
- nautobot/dcim/templates/dcim/device_create.html +120 -120
- nautobot/dcim/templates/dcim/device_list.html +75 -12
- nautobot/dcim/templates/dcim/devicebay.html +7 -7
- nautobot/dcim/templates/dcim/devicebay_populate.html +29 -23
- nautobot/dcim/templates/dcim/deviceredundancygroup_create.html +6 -6
- nautobot/dcim/templates/dcim/devicetype.html +1 -1
- nautobot/dcim/templates/dcim/devicetype_component_add.html +25 -19
- nautobot/dcim/templates/dcim/devicetype_list.html +4 -4
- nautobot/dcim/templates/dcim/devicetype_update.html +9 -9
- nautobot/dcim/templates/dcim/footer_convert_to_contact_or_team_record.html +3 -3
- nautobot/dcim/templates/dcim/frontport.html +21 -20
- nautobot/dcim/templates/dcim/inc/cable_form.html +7 -7
- nautobot/dcim/templates/dcim/inc/cable_termination.html +1 -1
- nautobot/dcim/templates/dcim/inc/cable_toggle_buttons.html +18 -9
- nautobot/dcim/templates/dcim/inc/detail_softwareversion_softwareimagefile_rows.html +1 -1
- nautobot/dcim/templates/dcim/inc/device_interface_filter.html +1 -1
- nautobot/dcim/templates/dcim/inc/devicetype_component_table.html +10 -10
- nautobot/dcim/templates/dcim/inc/edit_form_softwareversion_js.html +2 -2
- nautobot/dcim/templates/dcim/inc/homepage_connections.html +2 -2
- nautobot/dcim/templates/dcim/inc/moduletype_component_table.html +10 -10
- nautobot/dcim/templates/dcim/inc/rack_elevation.html +2 -2
- nautobot/dcim/templates/dcim/interface.html +42 -22
- nautobot/dcim/templates/dcim/interface_connection_list.html +2 -2
- nautobot/dcim/templates/dcim/interface_edit.html +26 -11
- nautobot/dcim/templates/dcim/interfaceredundancygroupassociation_create.html +3 -3
- nautobot/dcim/templates/dcim/inventoryitem.html +3 -3
- nautobot/dcim/templates/dcim/inventoryitem_add.html +21 -10
- nautobot/dcim/templates/dcim/inventoryitem_bulk_delete.html +1 -1
- nautobot/dcim/templates/dcim/inventoryitem_edit.html +6 -4
- nautobot/dcim/templates/dcim/location.html +1 -1
- nautobot/dcim/templates/dcim/location_migrate_data_to_contact.html +24 -18
- nautobot/dcim/templates/dcim/location_retrieve.html +1 -1
- nautobot/dcim/templates/dcim/location_update.html +9 -9
- nautobot/dcim/templates/dcim/locationtype.html +0 -1
- nautobot/dcim/templates/dcim/module/base.html +67 -27
- nautobot/dcim/templates/dcim/module_consoleports.html +13 -15
- nautobot/dcim/templates/dcim/module_consoleserverports.html +13 -15
- nautobot/dcim/templates/dcim/module_frontports.html +13 -15
- nautobot/dcim/templates/dcim/module_interfaces.html +14 -16
- nautobot/dcim/templates/dcim/module_list.html +59 -10
- nautobot/dcim/templates/dcim/module_modulebays.html +12 -14
- nautobot/dcim/templates/dcim/module_poweroutlets.html +13 -15
- nautobot/dcim/templates/dcim/module_powerports.html +13 -15
- nautobot/dcim/templates/dcim/module_rearports.html +13 -15
- nautobot/dcim/templates/dcim/module_retrieve.html +3 -3
- nautobot/dcim/templates/dcim/module_update.html +15 -9
- nautobot/dcim/templates/dcim/modulebay_retrieve.html +0 -93
- nautobot/dcim/templates/dcim/modulefamily_retrieve.html +7 -7
- nautobot/dcim/templates/dcim/moduletype_list.html +2 -2
- nautobot/dcim/templates/dcim/moduletype_retrieve.html +74 -35
- nautobot/dcim/templates/dcim/platform_create.html +9 -9
- nautobot/dcim/templates/dcim/power_port_connection_list.html +3 -3
- nautobot/dcim/templates/dcim/powerfeed.html +1 -1
- nautobot/dcim/templates/dcim/powerfeed_edit.html +15 -15
- nautobot/dcim/templates/dcim/poweroutlet.html +13 -13
- nautobot/dcim/templates/dcim/powerpanel.html +1 -1
- nautobot/dcim/templates/dcim/powerport.html +17 -16
- nautobot/dcim/templates/dcim/rack.html +1 -1
- nautobot/dcim/templates/dcim/rack_elevation.html +2 -2
- nautobot/dcim/templates/dcim/rack_elevation_list.html +21 -9
- nautobot/dcim/templates/dcim/rack_retrieve.html +75 -57
- nautobot/dcim/templates/dcim/rack_update.html +14 -14
- nautobot/dcim/templates/dcim/rackreservation.html +1 -1
- nautobot/dcim/templates/dcim/rackreservation_edit.html +6 -6
- nautobot/dcim/templates/dcim/rearport.html +19 -18
- nautobot/dcim/templates/dcim/trace/cable.html +1 -1
- nautobot/dcim/templates/dcim/trace/circuit.html +1 -1
- nautobot/dcim/templates/dcim/trace/device.html +1 -1
- nautobot/dcim/templates/dcim/trace/powerpanel.html +1 -1
- nautobot/dcim/templates/dcim/trace/termination.html +1 -1
- nautobot/dcim/templates/dcim/virtualchassis.html +1 -1
- nautobot/dcim/templates/dcim/virtualchassis_add_member.html +25 -16
- nautobot/dcim/templates/dcim/virtualchassis_create.html +6 -6
- nautobot/dcim/templates/dcim/virtualchassis_edit.html +1 -1
- nautobot/dcim/templates/dcim/virtualchassis_retrieve.html +1 -1
- nautobot/dcim/templates/dcim/virtualchassis_update.html +36 -22
- nautobot/dcim/templates/dcim/virtualdevicecontext_update.html +9 -9
- nautobot/dcim/tests/integration/test_controller.py +6 -6
- nautobot/dcim/tests/integration/test_controller_managed_device_group.py +7 -7
- nautobot/dcim/tests/integration/test_create_device.py +9 -9
- nautobot/dcim/tests/integration/test_device_bulk_operations.py +7 -2
- nautobot/dcim/tests/integration/test_fileinputpicker.py +5 -7
- nautobot/dcim/tests/integration/test_location_bulk_operations.py +2 -0
- nautobot/dcim/tests/integration/test_module_bay_position.py +4 -1
- nautobot/dcim/tests/test_api.py +86 -6
- nautobot/dcim/tests/test_custom_validators.py +229 -0
- nautobot/dcim/tests/test_filters.py +159 -110
- nautobot/dcim/tests/test_graphql.py +32 -36
- nautobot/dcim/tests/test_jobs.py +1 -1
- nautobot/dcim/tests/test_models.py +229 -1
- nautobot/dcim/tests/test_views.py +31 -20
- nautobot/dcim/utils.py +3 -3
- nautobot/dcim/views.py +77 -41
- nautobot/extras/api/serializers.py +83 -19
- nautobot/extras/api/urls.py +7 -0
- nautobot/extras/api/views.py +243 -140
- nautobot/extras/choices.py +34 -13
- nautobot/extras/constants.py +1 -1
- nautobot/extras/context_managers.py +26 -26
- nautobot/extras/datasources/git.py +22 -0
- nautobot/extras/datasources/registry.py +3 -0
- nautobot/extras/exceptions.py +5 -0
- nautobot/extras/factory.py +11 -1
- nautobot/extras/{filters/mixins.py → filter_mixins.py} +4 -3
- nautobot/extras/{filters/__init__.py → filters.py} +203 -58
- nautobot/extras/forms/base.py +2 -1
- nautobot/extras/forms/forms.py +225 -20
- nautobot/extras/forms/mixins.py +0 -41
- nautobot/extras/homepage.py +21 -2
- nautobot/extras/jobs.py +2 -8
- nautobot/extras/jobs_ui.py +2 -2
- nautobot/extras/management/__init__.py +9 -0
- nautobot/extras/managers.py +31 -22
- nautobot/extras/migrations/0126_approval_workflow_pre_check.py +58 -0
- nautobot/extras/migrations/0127_approval_workflow_models.py +266 -0
- nautobot/extras/migrations/0128_remove_job_approval_required_and_more.py +29 -0
- nautobot/extras/migrations/0129_jobresult_debug_log_count_jobresult_error_log_count_and_more.py +37 -0
- nautobot/extras/migrations/0130_jobresult_generate_log_entry_counts.py +42 -0
- nautobot/extras/models/__init__.py +14 -3
- nautobot/extras/models/approvals.py +556 -0
- nautobot/extras/models/change_logging.py +1 -0
- nautobot/extras/models/contacts.py +2 -0
- nautobot/extras/models/customfields.py +57 -22
- nautobot/extras/models/datasources.py +21 -0
- nautobot/extras/models/groups.py +2 -0
- nautobot/extras/models/jobs.py +122 -39
- nautobot/extras/models/metadata.py +2 -3
- nautobot/extras/models/mixins.py +129 -1
- nautobot/extras/models/models.py +22 -14
- nautobot/extras/models/relationships.py +47 -10
- nautobot/extras/models/secrets.py +1 -0
- nautobot/extras/models/statuses.py +0 -15
- nautobot/extras/models/tags.py +1 -1
- nautobot/extras/navigation.py +42 -15
- nautobot/extras/plugins/__init__.py +33 -55
- nautobot/extras/plugins/marketplace_manifest.yml +1 -23
- nautobot/extras/plugins/tables.py +8 -6
- nautobot/extras/plugins/urls.py +2 -21
- nautobot/extras/plugins/utils.py +1 -33
- nautobot/extras/plugins/validators.py +10 -10
- nautobot/extras/plugins/views.py +1 -5
- nautobot/extras/querysets.py +17 -21
- nautobot/extras/signals.py +23 -8
- nautobot/extras/tables.py +420 -99
- nautobot/extras/templates/extras/approval_dashboard.html +15 -0
- nautobot/extras/templates/extras/approval_workflow/approve.html +11 -0
- nautobot/extras/templates/extras/approval_workflow/comment.html +9 -0
- nautobot/extras/templates/extras/approval_workflow/deny.html +10 -0
- nautobot/extras/templates/extras/approvalworkflowdefinition_update.html +77 -0
- nautobot/extras/templates/extras/approvalworkflowstage_retrieve.html +29 -0
- nautobot/extras/templates/extras/configcontext_update.html +12 -12
- nautobot/extras/templates/extras/configcontextschema.html +1 -1
- nautobot/extras/templates/extras/configcontextschema_retrieve.html +9 -9
- nautobot/extras/templates/extras/configcontextschema_update.html +6 -6
- nautobot/extras/templates/extras/configcontextschema_validation.html +2 -2
- nautobot/extras/templates/extras/customfield_update.html +12 -12
- nautobot/extras/templates/extras/dynamicgroup.html +1 -1
- nautobot/extras/templates/extras/dynamicgroup_edit.html +1 -1
- nautobot/extras/templates/extras/dynamicgroup_retrieve.html +17 -17
- nautobot/extras/templates/extras/dynamicgroup_update.html +24 -24
- nautobot/extras/templates/extras/externalintegration_update.html +6 -6
- nautobot/extras/templates/extras/gitrepository.html +1 -1
- nautobot/extras/templates/extras/gitrepository_object_edit.html +1 -1
- nautobot/extras/templates/extras/gitrepository_result.html +1 -1
- nautobot/extras/templates/extras/gitrepository_retrieve.html +12 -12
- nautobot/extras/templates/extras/gitrepository_update.html +25 -7
- nautobot/extras/templates/extras/graphqlquery_retrieve.html +1 -1
- nautobot/extras/templates/extras/inc/approval_buttons_column.html +38 -0
- nautobot/extras/templates/extras/inc/bulk_edit_overridable_field.html +14 -13
- nautobot/extras/templates/extras/inc/configcontext_format.html +11 -4
- nautobot/extras/templates/extras/inc/graphqlquery_execute.html +7 -7
- nautobot/extras/templates/extras/inc/job_label.html +5 -5
- nautobot/extras/templates/extras/inc/job_table.html +23 -10
- nautobot/extras/templates/extras/inc/job_tiles.html +33 -21
- nautobot/extras/templates/extras/inc/jobresult.html +6 -6
- nautobot/extras/templates/extras/inc/json_format.html +11 -4
- nautobot/extras/templates/extras/inc/object_contact_header.html +6 -6
- nautobot/extras/templates/extras/inc/overridable_field.html +16 -15
- nautobot/extras/templates/extras/inc/panel_approvalworkflowstage.html +34 -0
- nautobot/extras/templates/extras/inc/panel_changelog.html +9 -9
- nautobot/extras/templates/extras/inc/panel_jobhistory.html +8 -6
- nautobot/extras/templates/extras/inc/tags_panel.html +3 -3
- nautobot/extras/templates/extras/job.html +154 -155
- nautobot/extras/templates/extras/job_approval_confirmation.html +4 -27
- nautobot/extras/templates/extras/job_bulk_edit.html +18 -1
- nautobot/extras/templates/extras/job_detail.html +1 -1
- nautobot/extras/templates/extras/job_edit.html +69 -64
- nautobot/extras/templates/extras/job_list.html +37 -60
- nautobot/extras/templates/extras/jobresult.html +1 -1
- nautobot/extras/templates/extras/jobresult_retrieve.html +17 -17
- nautobot/extras/templates/extras/marketplace.html +62 -71
- nautobot/extras/templates/extras/metadatatype_create.html +9 -9
- nautobot/extras/templates/extras/note.html +1 -1
- nautobot/extras/templates/extras/object_approvalworkflow.html +36 -0
- nautobot/extras/templates/extras/object_assign_contact_or_team.html +16 -7
- nautobot/extras/templates/extras/object_configcontext.html +20 -20
- nautobot/extras/templates/extras/object_new_contact.html +6 -6
- nautobot/extras/templates/extras/object_new_team.html +6 -6
- nautobot/extras/templates/extras/objectchange.html +1 -1
- nautobot/extras/templates/extras/objectchange_retrieve.html +37 -56
- nautobot/extras/templates/extras/plugin_detail.html +40 -41
- nautobot/extras/templates/extras/plugins_list.html +23 -38
- nautobot/extras/templates/extras/plugins_tiles.html +28 -28
- nautobot/extras/templates/extras/role_retrieve.html +112 -48
- nautobot/extras/templates/extras/scheduledjob.html +25 -28
- nautobot/extras/templates/extras/secret_create.html +11 -11
- nautobot/extras/templates/extras/secretsgroup_update.html +6 -6
- nautobot/extras/templates/extras/staticgroupassociation_retrieve.html +3 -3
- nautobot/extras/templates/extras/status.html +1 -1
- nautobot/extras/templates/extras/tag.html +1 -1
- nautobot/extras/templates/extras/tag_update.html +3 -3
- nautobot/extras/templates/extras/templatetags/log_level.html +1 -1
- nautobot/extras/templates/extras/templatetags/plugin_object_detail_tabs.html +2 -2
- nautobot/extras/templates/extras/webhook.html +12 -12
- nautobot/extras/templatetags/approvals.py +19 -0
- 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 +5 -8
- nautobot/extras/tests/integration/test_configcontextschema.py +43 -48
- nautobot/extras/tests/integration/test_customfields.py +33 -33
- nautobot/extras/tests/integration/test_dynamicgroups.py +5 -10
- nautobot/extras/tests/integration/test_jobs.py +2 -4
- nautobot/extras/tests/integration/test_notes.py +3 -9
- nautobot/extras/tests/integration/test_plugin_banner.py +3 -0
- nautobot/extras/tests/integration/test_plugins.py +35 -27
- nautobot/extras/tests/integration/test_relationships.py +7 -11
- nautobot/extras/tests/integration/test_tagfilter.py +3 -11
- nautobot/extras/tests/test_api.py +786 -242
- nautobot/extras/tests/test_approvals.py +715 -0
- nautobot/extras/tests/test_changelog.py +18 -14
- nautobot/extras/tests/test_customfields.py +14 -13
- nautobot/extras/tests/test_datasources.py +1 -1
- nautobot/extras/tests/test_dynamicgroups.py +9 -4
- nautobot/extras/tests/test_filters.py +443 -13
- nautobot/extras/tests/test_forms.py +18 -57
- nautobot/extras/tests/test_jobs.py +25 -4
- nautobot/extras/tests/test_migrations.py +81 -1
- nautobot/extras/tests/test_models.py +378 -47
- nautobot/extras/tests/test_plugins.py +47 -13
- nautobot/extras/tests/test_relationships.py +7 -2
- nautobot/extras/tests/test_utils.py +2 -0
- nautobot/extras/tests/test_views.py +780 -493
- nautobot/extras/urls.py +36 -12
- nautobot/extras/utils.py +58 -12
- nautobot/extras/views.py +668 -209
- nautobot/ipam/factory.py +7 -0
- nautobot/ipam/filter_mixins.py +38 -0
- nautobot/ipam/filters.py +35 -71
- 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 -1
- nautobot/ipam/querysets.py +1 -2
- nautobot/ipam/tables.py +26 -17
- nautobot/ipam/templates/ipam/inc/ipadress_edit_header.html +6 -6
- nautobot/ipam/templates/ipam/inc/service.html +8 -8
- nautobot/ipam/templates/ipam/inc/toggle_available.html +10 -10
- nautobot/ipam/templates/ipam/inc/vlangroup_header.html +3 -2
- nautobot/ipam/templates/ipam/ipaddress.html +27 -13
- nautobot/ipam/templates/ipam/ipaddress_assign.html +31 -24
- nautobot/ipam/templates/ipam/ipaddress_bulk_add.html +3 -3
- nautobot/ipam/templates/ipam/ipaddress_edit.html +9 -9
- nautobot/ipam/templates/ipam/ipaddress_interfaces.html +7 -9
- nautobot/ipam/templates/ipam/ipaddress_merge.html +195 -186
- nautobot/ipam/templates/ipam/ipaddress_vm_interfaces.html +7 -9
- nautobot/ipam/templates/ipam/ipaddresstointerface_retrieve.html +7 -5
- 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_create.html +9 -9
- nautobot/ipam/templates/ipam/prefix_list.html +15 -14
- nautobot/ipam/templates/ipam/prefix_retrieve.html +0 -1
- nautobot/ipam/templates/ipam/vlan.html +1 -1
- nautobot/ipam/templates/ipam/vlan_interfaces.html +1 -1
- nautobot/ipam/templates/ipam/vlan_update.html +6 -6
- nautobot/ipam/templates/ipam/vlan_vminterfaces.html +1 -1
- nautobot/ipam/templates/ipam/vrf_edit.html +15 -15
- nautobot/ipam/tests/integration/test_prefixes.py +5 -13
- nautobot/ipam/tests/migration/test_migrations.py +89 -0
- nautobot/ipam/tests/test_api.py +20 -7
- nautobot/ipam/tests/test_filters.py +10 -0
- nautobot/ipam/tests/test_forms.py +1 -1
- nautobot/ipam/tests/test_models.py +1 -1
- nautobot/ipam/tests/test_tables.py +1 -2
- nautobot/ipam/tests/test_utils.py +1 -1
- nautobot/ipam/tests/test_views.py +24 -21
- nautobot/ipam/ui.py +0 -17
- nautobot/ipam/utils/migrations.py +16 -2
- nautobot/ipam/utils/testing.py +9 -3
- nautobot/ipam/views.py +49 -7
- nautobot/project-static/dist/css/graphql-libraries.css +655 -0
- nautobot/project-static/dist/css/graphql-libraries.css.map +1 -0
- nautobot/project-static/dist/css/materialdesignicons.css +3 -0
- nautobot/project-static/dist/css/materialdesignicons.css.map +1 -0
- nautobot/project-static/dist/css/nautobot.css +13 -0
- nautobot/project-static/dist/css/nautobot.css.map +1 -0
- nautobot/project-static/dist/js/graphql-libraries.js +3 -0
- nautobot/project-static/dist/js/graphql-libraries.js.LICENSE.txt +62 -0
- nautobot/project-static/dist/js/graphql-libraries.js.map +1 -0
- nautobot/project-static/dist/js/libraries.js +3 -0
- nautobot/project-static/dist/js/libraries.js.LICENSE.txt +65 -0
- nautobot/project-static/dist/js/libraries.js.map +1 -0
- nautobot/project-static/dist/js/materialdesignicons.js +0 -0
- nautobot/project-static/dist/js/nautobot-graphiql.js +2 -0
- nautobot/project-static/dist/js/nautobot-graphiql.js.map +1 -0
- nautobot/project-static/dist/js/nautobot.js +2 -0
- nautobot/project-static/dist/js/nautobot.js.map +1 -0
- nautobot/project-static/fonts/Montserrat-v30-Bold.woff2 +0 -0
- nautobot/project-static/fonts/Montserrat-v30-Light.woff2 +0 -0
- nautobot/project-static/fonts/Montserrat-v30-Regular.woff2 +0 -0
- nautobot/project-static/fonts/Roboto-v48-Bold.woff2 +0 -0
- nautobot/project-static/fonts/Roboto-v48-Light.woff2 +0 -0
- nautobot/project-static/fonts/Roboto-v48-Regular.woff2 +0 -0
- nautobot/project-static/img/jinja_logo.svg +21 -92
- nautobot/project-static/js/cabletrace.js +1 -1
- nautobot/project-static/js/editor.js +4 -4
- nautobot/project-static/js/forms.js +67 -717
- nautobot/project-static/js/job_result.js +2 -2
- nautobot/project-static/nautobot-icons/360-degrees.svg +3 -0
- nautobot/project-static/nautobot-icons/arrow-decision.svg +3 -0
- nautobot/project-static/nautobot-icons/arrows-expand-rec.svg +3 -0
- nautobot/project-static/nautobot-icons/arrows-move-2-rec.svg +3 -0
- nautobot/project-static/nautobot-icons/arrows-move-rec.svg +3 -0
- nautobot/project-static/nautobot-icons/atom.svg +3 -0
- nautobot/project-static/nautobot-icons/battery-3.svg +3 -0
- nautobot/project-static/nautobot-icons/branch.svg +3 -0
- nautobot/project-static/nautobot-icons/briefcase-2.svg +3 -0
- nautobot/project-static/nautobot-icons/cable-data-2.svg +3 -0
- nautobot/project-static/nautobot-icons/cable-data.svg +3 -0
- nautobot/project-static/nautobot-icons/cast.svg +3 -0
- nautobot/project-static/nautobot-icons/check-circle.svg +3 -0
- nautobot/project-static/nautobot-icons/checkbox-circle.svg +3 -0
- nautobot/project-static/nautobot-icons/checkbox-rec.svg +3 -0
- nautobot/project-static/nautobot-icons/cloud-check.svg +3 -0
- nautobot/project-static/nautobot-icons/cloud-lightning.svg +3 -0
- nautobot/project-static/nautobot-icons/cloud-upload.svg +3 -0
- nautobot/project-static/nautobot-icons/cloud.svg +3 -0
- nautobot/project-static/nautobot-icons/compass.svg +3 -0
- nautobot/project-static/nautobot-icons/control-panel.svg +3 -0
- nautobot/project-static/nautobot-icons/credit-card.svg +3 -0
- nautobot/project-static/nautobot-icons/device-lifecycle.svg +3 -0
- nautobot/project-static/nautobot-icons/direction.svg +3 -0
- nautobot/project-static/nautobot-icons/elements.svg +3 -0
- nautobot/project-static/nautobot-icons/extensibility.svg +3 -0
- nautobot/project-static/nautobot-icons/globe-2.svg +3 -0
- nautobot/project-static/nautobot-icons/globe.svg +3 -0
- nautobot/project-static/nautobot-icons/hammer.svg +3 -0
- nautobot/project-static/nautobot-icons/history.svg +3 -0
- nautobot/project-static/nautobot-icons/ip.svg +3 -0
- nautobot/project-static/nautobot-icons/laptop.svg +3 -0
- nautobot/project-static/nautobot-icons/lightning.svg +3 -0
- nautobot/project-static/nautobot-icons/list-unordered.svg +3 -0
- nautobot/project-static/nautobot-icons/map-view.svg +3 -0
- nautobot/project-static/nautobot-icons/organization.svg +3 -0
- nautobot/project-static/nautobot-icons/pin-2.svg +3 -0
- nautobot/project-static/nautobot-icons/pin-3.svg +3 -0
- nautobot/project-static/nautobot-icons/plug.svg +3 -0
- nautobot/project-static/nautobot-icons/refresh-cw.svg +3 -0
- nautobot/project-static/nautobot-icons/rocket-2.svg +3 -0
- nautobot/project-static/nautobot-icons/rotate-cw.svg +3 -0
- nautobot/project-static/nautobot-icons/route.svg +3 -0
- nautobot/project-static/nautobot-icons/secrets.svg +3 -0
- nautobot/project-static/nautobot-icons/security.svg +3 -0
- nautobot/project-static/nautobot-icons/server-2.svg +3 -0
- nautobot/project-static/nautobot-icons/server.svg +3 -0
- nautobot/project-static/nautobot-icons/share.svg +3 -0
- nautobot/project-static/nautobot-icons/shield-check.svg +3 -0
- nautobot/project-static/nautobot-icons/sitemap-outline.svg +3 -0
- nautobot/project-static/nautobot-icons/sliders-vert-2.svg +3 -0
- nautobot/project-static/nautobot-icons/sliders-vert.svg +3 -0
- nautobot/project-static/nautobot-icons/star-filled.svg +3 -0
- nautobot/project-static/nautobot-icons/star.svg +3 -0
- nautobot/project-static/nautobot-icons/transform.svg +3 -0
- nautobot/project-static/nautobot-icons/wifi.svg +3 -0
- 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/templates/tenancy/tenant_create.html +6 -6
- nautobot/tenancy/tests/test_filters.py +0 -2
- nautobot/tenancy/views.py +2 -1
- nautobot/ui/.gitignore +137 -0
- nautobot/ui/.node-version +1 -0
- nautobot/ui/.prettierignore +3 -0
- nautobot/ui/eslint.config.js +33 -0
- nautobot/ui/package-lock.json +6594 -0
- nautobot/ui/package.json +67 -0
- nautobot/ui/prettier.config.js +9 -0
- nautobot/ui/src/js/collapse.js +69 -0
- nautobot/ui/src/js/cookie.js +31 -0
- nautobot/ui/src/js/draggable.js +101 -0
- nautobot/ui/src/js/drawer.js +106 -0
- nautobot/ui/src/js/form.js +23 -0
- nautobot/ui/src/js/history.js +51 -0
- nautobot/ui/src/js/nautobot-graphiql.js +19 -0
- nautobot/ui/src/js/nautobot.js +128 -0
- nautobot/ui/src/js/search.js +274 -0
- nautobot/ui/src/js/select2.js +318 -0
- nautobot/ui/src/js/sidenav.js +87 -0
- nautobot/ui/src/js/tabs.js +139 -0
- nautobot/ui/src/js/theme.js +104 -0
- nautobot/ui/src/js/utils.js +54 -0
- nautobot/ui/src/scss/colors.scss +58 -0
- nautobot/ui/src/scss/nautobot.scss +2471 -0
- nautobot/ui/webpack.config.js +148 -0
- nautobot/users/apps.py +3 -0
- nautobot/users/filters.py +7 -11
- nautobot/users/forms.py +10 -0
- nautobot/users/models.py +8 -0
- nautobot/users/templates/users/advanced_settings_edit.html +31 -21
- nautobot/users/templates/users/api_tokens.html +61 -51
- nautobot/users/templates/users/base.html +23 -31
- nautobot/users/templates/users/change_password.html +29 -19
- nautobot/users/templates/users/preferences.html +55 -45
- nautobot/users/templates/users/profile.html +45 -14
- nautobot/users/tests/test_api.py +4 -0
- nautobot/users/urls.py +2 -0
- nautobot/users/views.py +70 -2
- nautobot/virtualization/api/views.py +1 -1
- nautobot/virtualization/filters.py +18 -32
- nautobot/virtualization/forms.py +22 -59
- nautobot/virtualization/models.py +1 -19
- nautobot/virtualization/navigation.py +3 -1
- nautobot/virtualization/tables.py +10 -6
- nautobot/virtualization/templates/virtualization/cluster.html +13 -13
- nautobot/virtualization/templates/virtualization/cluster_edit.html +6 -6
- nautobot/virtualization/templates/virtualization/inc/virtualmachine_vminterface_filter.html +1 -1
- nautobot/virtualization/templates/virtualization/virtualmachine.html +1 -1
- nautobot/virtualization/templates/virtualization/virtualmachine_component_add.html +24 -16
- nautobot/virtualization/templates/virtualization/virtualmachine_edit.html +1 -1
- nautobot/virtualization/templates/virtualization/virtualmachine_list.html +4 -4
- nautobot/virtualization/templates/virtualization/virtualmachine_update.html +27 -25
- nautobot/virtualization/templates/virtualization/vminterface.html +5 -5
- nautobot/virtualization/templates/virtualization/vminterface_edit.html +27 -11
- nautobot/virtualization/tests/test_api.py +3 -0
- nautobot/virtualization/tests/test_models.py +20 -5
- nautobot/virtualization/tests/test_views.py +3 -5
- nautobot/virtualization/urls.py +0 -11
- nautobot/virtualization/views.py +5 -122
- nautobot/vpn/__init__.py +0 -0
- nautobot/vpn/api/serializers.py +113 -0
- nautobot/vpn/api/urls.py +19 -0
- nautobot/vpn/api/views.py +70 -0
- nautobot/vpn/apps.py +8 -0
- nautobot/vpn/choices.py +171 -0
- nautobot/vpn/factory.py +209 -0
- nautobot/vpn/filters.py +233 -0
- nautobot/vpn/forms.py +486 -0
- nautobot/vpn/homepage.py +19 -0
- nautobot/vpn/migrations/0001_initial.py +541 -0
- nautobot/vpn/migrations/0002_populate_defaults.py +199 -0
- nautobot/vpn/migrations/__init__.py +0 -0
- nautobot/vpn/models.py +527 -0
- nautobot/vpn/navigation.py +98 -0
- nautobot/vpn/tables.py +380 -0
- nautobot/vpn/templates/vpn/vpnprofile.html +2 -0
- nautobot/vpn/templates/vpn/vpnprofile_create.html +150 -0
- nautobot/vpn/tests/__init__.py +0 -0
- nautobot/vpn/tests/test_api.py +341 -0
- nautobot/vpn/tests/test_filters.py +139 -0
- nautobot/vpn/tests/test_forms.py +294 -0
- nautobot/vpn/tests/test_models.py +97 -0
- nautobot/vpn/tests/test_views.py +281 -0
- nautobot/vpn/urls.py +16 -0
- nautobot/vpn/views.py +437 -0
- nautobot/wireless/filters.py +0 -8
- nautobot/wireless/navigation.py +3 -1
- nautobot/wireless/templates/wireless/wirelessnetwork_create.html +6 -6
- nautobot/wireless/tests/integration/test_radio_profile.py +3 -7
- nautobot/wireless/tests/test_api.py +1 -1
- {nautobot-2.4.21.dist-info → nautobot-3.0.0a3.dist-info}/METADATA +5 -4
- {nautobot-2.4.21.dist-info → nautobot-3.0.0a3.dist-info}/RECORD +802 -707
- {nautobot-2.4.21.dist-info → nautobot-3.0.0a3.dist-info}/entry_points.txt +1 -0
- nautobot/core/management/commands/check_job_approval_status.py +0 -47
- nautobot/core/templates/search_form.html +0 -9
- nautobot/core/templates/utilities/templatetags/filter_form_modal.html +0 -87
- nautobot/dcim/filters/mixins.py +0 -354
- nautobot/extras/templates/extras/job_approval_request.html +0 -134
- nautobot/extras/templates/extras/scheduled_jobs_approval_queue_list.html +0 -28
- nautobot/ipam/mixins.py +0 -32
- nautobot/ipam/templates/ipam/inc/prefix_header_extra_content_table.html +0 -4
- nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap-theme.css +0 -587
- nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap-theme.css.map +0 -1
- nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap-theme.min.css +0 -6
- nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap-theme.min.css.map +0 -1
- nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap.css +0 -6865
- nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap.css.map +0 -1
- nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap.min.css +0 -6
- nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap.min.css.map +0 -1
- nautobot/project-static/bootstrap-3.4.1-dist/fonts/glyphicons-halflings-regular.eot +0 -0
- nautobot/project-static/bootstrap-3.4.1-dist/fonts/glyphicons-halflings-regular.svg +0 -288
- nautobot/project-static/bootstrap-3.4.1-dist/fonts/glyphicons-halflings-regular.ttf +0 -0
- nautobot/project-static/bootstrap-3.4.1-dist/fonts/glyphicons-halflings-regular.woff +0 -0
- nautobot/project-static/bootstrap-3.4.1-dist/fonts/glyphicons-halflings-regular.woff2 +0 -0
- nautobot/project-static/bootstrap-3.4.1-dist/js/bootstrap.js +0 -2580
- nautobot/project-static/bootstrap-3.4.1-dist/js/bootstrap.min.js +0 -6
- nautobot/project-static/bootstrap-3.4.1-dist/js/npm.js +0 -13
- nautobot/project-static/clipboard.js-2.0.9/clipboard.min.js +0 -7
- nautobot/project-static/css/base.css +0 -1040
- nautobot/project-static/css/dark.css +0 -282
- nautobot/project-static/flatpickr-4.6.9/flatpickr.min.js +0 -2
- nautobot/project-static/flatpickr-4.6.9/themes/light.min.css +0 -1
- nautobot/project-static/graphiql-1.5.16/graphiql.min.css +0 -12
- nautobot/project-static/graphiql-1.5.16/graphiql.min.js +0 -11
- nautobot/project-static/highlight.js-11.9.0/github-dark.min.css +0 -10
- nautobot/project-static/highlight.js-11.9.0/github.min.css +0 -10
- nautobot/project-static/highlight.js-11.9.0/highlight.min.js +0 -378
- nautobot/project-static/jquery/jquery-3.7.1.min.js +0 -2
- nautobot/project-static/jquery-ui-1.13.2/images/ui-icons_444444_256x240.png +0 -0
- nautobot/project-static/jquery-ui-1.13.2/images/ui-icons_555555_256x240.png +0 -0
- nautobot/project-static/jquery-ui-1.13.2/images/ui-icons_777620_256x240.png +0 -0
- nautobot/project-static/jquery-ui-1.13.2/images/ui-icons_777777_256x240.png +0 -0
- nautobot/project-static/jquery-ui-1.13.2/images/ui-icons_cc0000_256x240.png +0 -0
- nautobot/project-static/jquery-ui-1.13.2/images/ui-icons_ffffff_256x240.png +0 -0
- nautobot/project-static/jquery-ui-1.13.2/jquery-ui.min.css +0 -7
- nautobot/project-static/jquery-ui-1.13.2/jquery-ui.min.js +0 -6
- nautobot/project-static/jquery-ui-1.13.2/jquery-ui.structure.min.css +0 -5
- nautobot/project-static/jquery-ui-1.13.2/jquery-ui.theme.min.css +0 -5
- nautobot/project-static/js/homepage_layout.js +0 -182
- nautobot/project-static/js/nav_menu.js +0 -250
- nautobot/project-static/js/theme.js +0 -133
- nautobot/project-static/materialdesignicons-7.4.47/LICENSE +0 -20
- nautobot/project-static/materialdesignicons-7.4.47/css/materialdesignicons.min.css +0 -3
- nautobot/project-static/react-16.14.0/react.production.min.js +0 -32
- nautobot/project-static/react-dom-16.14.0/react-dom.production.min.js +0 -239
- nautobot/project-static/select2-4.0.13/i18n/af.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/ar.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/az.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/bg.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/bn.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/bs.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/ca.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/cs.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/da.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/de.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/dsb.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/el.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/en.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/es.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/et.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/eu.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/fa.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/fi.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/fr.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/gl.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/he.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/hi.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/hr.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/hsb.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/hu.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/hy.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/id.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/is.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/it.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/ja.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/ka.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/km.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/ko.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/lt.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/lv.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/mk.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/ms.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/nb.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/ne.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/nl.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/pl.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/ps.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/pt-BR.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/pt.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/ro.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/ru.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/sk.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/sl.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/sq.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/sr-Cyrl.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/sr.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/sv.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/th.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/tk.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/tr.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/uk.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/vi.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/zh-CN.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/zh-TW.js +0 -3
- nautobot/project-static/select2-4.0.13/select2.min.css +0 -1
- nautobot/project-static/select2-4.0.13/select2.min.js +0 -2
- nautobot/project-static/select2-bootstrap-0.1.0-beta.10/select2-bootstrap.min.css +0 -7
- nautobot/project-static/subscriptions-transport-ws-0.9.18/client.min.js +0 -8
- nautobot/project-static/whatwg-fetch-3.6.2/fetch.umd.min.js +0 -8
- nautobot/virtualization/templates/virtualization/cluster_add_devices.html +0 -37
- /nautobot/extras/{filters/customfields.py → filter_mixins_customfields.py} +0 -0
- /nautobot/project-static/{materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.ttf → dist/1fcc36272ea3e53d0031.ttf} +0 -0
- /nautobot/project-static/{materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.eot → dist/2146c3c82b553977abc7.eot} +0 -0
- /nautobot/project-static/{materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.woff → dist/e55a20c80650829ec5fd.woff} +0 -0
- /nautobot/project-static/{materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.woff2 → dist/ec024da790d2972da002.woff2} +0 -0
- /nautobot/tenancy/{filters/mixins.py → filter_mixins.py} +0 -0
- {nautobot-2.4.21.dist-info → nautobot-3.0.0a3.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.4.21.dist-info → nautobot-3.0.0a3.dist-info}/NOTICE +0 -0
- {nautobot-2.4.21.dist-info → nautobot-3.0.0a3.dist-info}/WHEEL +0 -0
nautobot/core/testing/views.py
CHANGED
|
@@ -6,16 +6,18 @@ from unittest import mock, skipIf
|
|
|
6
6
|
import uuid
|
|
7
7
|
|
|
8
8
|
from django.apps import apps
|
|
9
|
-
from django.conf import settings
|
|
9
|
+
from django.conf import global_settings, settings
|
|
10
10
|
from django.contrib.contenttypes.models import ContentType
|
|
11
11
|
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
|
12
12
|
from django.core.validators import URLValidator
|
|
13
13
|
from django.db import connection
|
|
14
14
|
from django.db.models import ManyToManyField, Model, QuerySet
|
|
15
|
+
from django.template.defaultfilters import date
|
|
15
16
|
from django.test import override_settings, tag, TestCase as _TestCase
|
|
17
|
+
from django.test.testcases import assert_and_parse_html
|
|
16
18
|
from django.test.utils import CaptureQueriesContext
|
|
17
19
|
from django.urls import NoReverseMatch, reverse
|
|
18
|
-
from django.utils.html import escape
|
|
20
|
+
from django.utils.html import escape, format_html
|
|
19
21
|
from django.utils.http import urlencode
|
|
20
22
|
from django.utils.text import slugify
|
|
21
23
|
from tree_queries.models import TreeNode
|
|
@@ -23,7 +25,7 @@ from tree_queries.models import TreeNode
|
|
|
23
25
|
from nautobot.core.jobs.bulk_actions import BulkEditObjects
|
|
24
26
|
from nautobot.core.models.generics import PrimaryModel
|
|
25
27
|
from nautobot.core.models.tree_queries import TreeModel
|
|
26
|
-
from nautobot.core.templatetags import helpers
|
|
28
|
+
from nautobot.core.templatetags import buttons, helpers
|
|
27
29
|
from nautobot.core.testing import mixins, utils
|
|
28
30
|
from nautobot.core.utils import lookup
|
|
29
31
|
from nautobot.dcim.models.device_components import ComponentModel
|
|
@@ -190,6 +192,10 @@ class ViewTestCases:
|
|
|
190
192
|
with CaptureQueriesContext(connection) as capture_queries_context:
|
|
191
193
|
response = self.client.get(instance.get_absolute_url())
|
|
192
194
|
# The object's display name or string representation should appear in the response body
|
|
195
|
+
# TODO: some models (e.g. JobResult) intentionally do NOT display the full `.display` in the detail view,
|
|
196
|
+
# but only use the `.name` or `str()`.
|
|
197
|
+
# This check will always pass in the case where the Example App is installed, because of the banner
|
|
198
|
+
# it adds, but can/should/may? fail otherwise.
|
|
193
199
|
self.assertBodyContains(response, escape(getattr(instance, "display", str(instance))))
|
|
194
200
|
|
|
195
201
|
# If any Relationships are defined, they should appear in the response
|
|
@@ -260,6 +266,90 @@ class ViewTestCases:
|
|
|
260
266
|
|
|
261
267
|
return response # for consumption by child test cases if desired
|
|
262
268
|
|
|
269
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
|
270
|
+
def test_has_timestamps_and_buttons(self):
|
|
271
|
+
instance = self._get_queryset().first()
|
|
272
|
+
|
|
273
|
+
# Add model-level permission
|
|
274
|
+
obj_perm = users_models.ObjectPermission(
|
|
275
|
+
name="Test permission", actions=["view", "add", "change", "delete"]
|
|
276
|
+
)
|
|
277
|
+
obj_perm.save()
|
|
278
|
+
obj_perm.users.add(self.user)
|
|
279
|
+
obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
|
|
280
|
+
|
|
281
|
+
response = self.client.get(instance.get_absolute_url())
|
|
282
|
+
|
|
283
|
+
if hasattr(instance, "created") and hasattr(instance, "last_updated"):
|
|
284
|
+
self.assertBodyContains(response, date(instance.created, global_settings.DATETIME_FORMAT), html=True)
|
|
285
|
+
# We don't assert the rendering of `last_updated` because it's relative time ("10 minutes ago") and
|
|
286
|
+
# therefore is subject to off-by-one timing failures.
|
|
287
|
+
|
|
288
|
+
object_edit_url = buttons.edit_button(instance)["url"]
|
|
289
|
+
object_delete_url = buttons.delete_button(instance)["url"]
|
|
290
|
+
object_clone_url = buttons.clone_button(instance)["url"]
|
|
291
|
+
render_edit_button = bool(object_edit_url)
|
|
292
|
+
render_delete_button = bool(object_delete_url)
|
|
293
|
+
render_clone_button = bool(hasattr(instance, "clone_fields") and object_clone_url)
|
|
294
|
+
action_buttons = []
|
|
295
|
+
if render_edit_button:
|
|
296
|
+
action_buttons.append(
|
|
297
|
+
format_html(
|
|
298
|
+
"""
|
|
299
|
+
<a id="edit-button" class="btn btn-warning" href="{}">
|
|
300
|
+
<span class="mdi mdi-pencil" aria-hidden="true"></span> Edit {}
|
|
301
|
+
</a>
|
|
302
|
+
""",
|
|
303
|
+
object_edit_url,
|
|
304
|
+
helpers.bettertitle(self.model._meta.verbose_name),
|
|
305
|
+
)
|
|
306
|
+
)
|
|
307
|
+
if render_delete_button:
|
|
308
|
+
action_buttons.append(
|
|
309
|
+
format_html(
|
|
310
|
+
"""
|
|
311
|
+
<a id="delete-button" class="dropdown-item text-danger" href="{}">
|
|
312
|
+
<span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> Delete {}
|
|
313
|
+
</a>
|
|
314
|
+
""",
|
|
315
|
+
object_delete_url,
|
|
316
|
+
helpers.bettertitle(self.model._meta.verbose_name),
|
|
317
|
+
)
|
|
318
|
+
)
|
|
319
|
+
if render_clone_button:
|
|
320
|
+
action_buttons.append(
|
|
321
|
+
format_html(
|
|
322
|
+
"""
|
|
323
|
+
<a id="clone-button" class="dropdown-item" href="{}">
|
|
324
|
+
<span class="mdi mdi-plus-thick text-secondary" aria-hidden="true"></span> Clone {}
|
|
325
|
+
</a>
|
|
326
|
+
""",
|
|
327
|
+
object_clone_url,
|
|
328
|
+
helpers.bettertitle(self.model._meta.verbose_name),
|
|
329
|
+
)
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
# Because we are looking for a hypothetical use of legacy button templates on the page here, we need some
|
|
333
|
+
# additional logic to know if this assertion should be made in the first place, and be handled in the `if`
|
|
334
|
+
# below, or fall back to standard button presence check in the `else` case otherwise.
|
|
335
|
+
content = assert_and_parse_html(
|
|
336
|
+
self,
|
|
337
|
+
utils.extract_page_body(response.content.decode(response.charset)),
|
|
338
|
+
None,
|
|
339
|
+
"Response's content is not valid HTML:",
|
|
340
|
+
)
|
|
341
|
+
for button in action_buttons:
|
|
342
|
+
button_parsed = assert_and_parse_html(self, button, None, "Button is not valid HTML:")
|
|
343
|
+
button_id_attribute = re.search(r"id=\"([A-Za-z]+[\w\-\:\.]*)\"", button).group(0)
|
|
344
|
+
real_count = content.count(button_parsed)
|
|
345
|
+
if (real_count is None or real_count == 0) and button_id_attribute in str(content):
|
|
346
|
+
self.fail(
|
|
347
|
+
f"Couldn't find {button} in response, but an element with `{button_id_attribute}` has been found. Is the page using legacy button template?\n{content}",
|
|
348
|
+
)
|
|
349
|
+
else:
|
|
350
|
+
# If it wasn't for the legacy button template check above, this would be the only thing needed here.
|
|
351
|
+
self.assertBodyContains(response, button, html=True)
|
|
352
|
+
|
|
263
353
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
|
264
354
|
def test_has_advanced_tab(self):
|
|
265
355
|
instance = self._get_queryset().first()
|
|
@@ -305,7 +395,8 @@ class ViewTestCases:
|
|
|
305
395
|
if getattr(obj, "is_contact_associable_model", False):
|
|
306
396
|
self.assertBodyContains(
|
|
307
397
|
response,
|
|
308
|
-
f'href="{obj.get_absolute_url()}#contacts" onclick="switch_tab(this.href)"',
|
|
398
|
+
f'<a aria-controls="contacts" class="nav-link" data-bs-toggle="tab" href="{obj.get_absolute_url()}#contacts" onclick="switch_tab(this.href)" role="tab">Contacts<span class="badge bg-primary">{obj.associated_contacts.count()}</span></a>',
|
|
399
|
+
html=True,
|
|
309
400
|
)
|
|
310
401
|
else:
|
|
311
402
|
self.assertNotContains(response, f"{obj.get_absolute_url()}#contacts")
|
|
@@ -327,7 +418,8 @@ class ViewTestCases:
|
|
|
327
418
|
if getattr(obj, "is_contact_associable_model", False):
|
|
328
419
|
self.assertBodyContains(
|
|
329
420
|
response,
|
|
330
|
-
f'href="{obj.get_absolute_url()}#contacts" onclick="switch_tab(this.href)"',
|
|
421
|
+
f'<a aria-controls="contacts" class="nav-link" data-bs-toggle="tab" href="{obj.get_absolute_url()}#contacts" onclick="switch_tab(this.href)" role="tab">Contacts<span class="badge bg-primary">{obj.associated_contacts.count()}</span></a>',
|
|
422
|
+
html=True,
|
|
331
423
|
)
|
|
332
424
|
else:
|
|
333
425
|
self.assertNotContains(response, f"{obj.get_absolute_url()}#contacts")
|
|
@@ -344,10 +436,12 @@ class ViewTestCases:
|
|
|
344
436
|
slugify_function = staticmethod(slugify)
|
|
345
437
|
slug_test_object = ""
|
|
346
438
|
expected_create_form_buttons = [
|
|
347
|
-
'<button type="submit" name="_create" class="btn btn-primary"
|
|
348
|
-
'<button type="submit" name="_addanother" class="btn btn-primary"
|
|
439
|
+
'<button type="submit" name="_create" class="btn btn-primary"><span aria-hidden="true" class="mdi mdi-check me-4"></span><!---->Create</button>',
|
|
440
|
+
'<button type="submit" name="_addanother" class="btn btn-primary"><span aria-hidden="true" class="mdi mdi-check me-4"></span><!---->Create and Add Another</button>',
|
|
441
|
+
]
|
|
442
|
+
expected_edit_form_buttons = [
|
|
443
|
+
'<button type="submit" name="_update" class="btn btn-primary"><span aria-hidden="true" class="mdi mdi-check me-4"></span><!---->Update</button>'
|
|
349
444
|
]
|
|
350
|
-
expected_edit_form_buttons = ['<button type="submit" name="_update" class="btn btn-primary">Update</button>']
|
|
351
445
|
|
|
352
446
|
def test_create_object_without_permission(self):
|
|
353
447
|
# Try GET without permission
|
|
@@ -375,10 +469,10 @@ class ViewTestCases:
|
|
|
375
469
|
self.assertHttpStatus(response, 200)
|
|
376
470
|
# The response content should contain the expected form buttons
|
|
377
471
|
for button in self.expected_create_form_buttons:
|
|
378
|
-
self.assertBodyContains(response, button)
|
|
472
|
+
self.assertBodyContains(response, button, html=True)
|
|
379
473
|
# The response content should not contain the expected form buttons
|
|
380
474
|
for button in self.expected_edit_form_buttons:
|
|
381
|
-
self.assertNotContains(response, button)
|
|
475
|
+
self.assertNotContains(response, button, html=True)
|
|
382
476
|
|
|
383
477
|
# Try POST with model-level permission
|
|
384
478
|
request = {
|
|
@@ -492,10 +586,12 @@ class ViewTestCases:
|
|
|
492
586
|
|
|
493
587
|
form_data = {}
|
|
494
588
|
update_data = {}
|
|
495
|
-
expected_edit_form_buttons = [
|
|
589
|
+
expected_edit_form_buttons = [
|
|
590
|
+
'<button type="submit" name="_update" class="btn btn-primary"><span aria-hidden="true" class="mdi mdi-check me-4"></span><!---->Update</button>',
|
|
591
|
+
]
|
|
496
592
|
expected_create_form_buttons = [
|
|
497
|
-
'<button type="submit" name="_create" class="btn btn-primary"
|
|
498
|
-
'<button type="submit" name="_addanother" class="btn btn-primary"
|
|
593
|
+
'<button type="submit" name="_create" class="btn btn-primary"><span aria-hidden="true" class="mdi mdi-check me-4"></span><!---->Create</button>',
|
|
594
|
+
'<button type="submit" name="_addanother" class="btn btn-primary"><span aria-hidden="true" class="mdi mdi-check me-4"></span><!---->Create and Add Another</button>',
|
|
499
595
|
]
|
|
500
596
|
|
|
501
597
|
def test_edit_object_without_permission(self):
|
|
@@ -526,11 +622,11 @@ class ViewTestCases:
|
|
|
526
622
|
self.assertHttpStatus(response, 200)
|
|
527
623
|
# The response content should contain the expected form buttons
|
|
528
624
|
for button in self.expected_edit_form_buttons:
|
|
529
|
-
self.assertBodyContains(response, button)
|
|
625
|
+
self.assertBodyContains(response, button, html=True)
|
|
530
626
|
|
|
531
627
|
# The response content should not contain the unexpected form buttons
|
|
532
628
|
for button in self.expected_create_form_buttons:
|
|
533
|
-
self.assertNotContains(response, button)
|
|
629
|
+
self.assertNotContains(response, button, html=True)
|
|
534
630
|
|
|
535
631
|
# Try POST with model-level permission
|
|
536
632
|
update_data = self.update_data or self.form_data
|
|
@@ -842,8 +938,8 @@ class ViewTestCases:
|
|
|
842
938
|
response = self.client.get(self._get_url("list"))
|
|
843
939
|
self.assertHttpStatus(response, 200)
|
|
844
940
|
# There should be some rows
|
|
845
|
-
self.assertBodyContains(response, '
|
|
846
|
-
self.assertBodyContains(response, '
|
|
941
|
+
self.assertBodyContains(response, 'class="even')
|
|
942
|
+
self.assertBodyContains(response, 'class="odd')
|
|
847
943
|
|
|
848
944
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
849
945
|
def test_list_objects_filtered(self):
|
|
@@ -856,8 +952,7 @@ class ViewTestCases:
|
|
|
856
952
|
self.assertHttpStatus(response, 200)
|
|
857
953
|
content = utils.extract_page_body(response.content.decode(response.charset))
|
|
858
954
|
# There should be only one row in the table
|
|
859
|
-
self.
|
|
860
|
-
self.assertNotIn('<tr class="odd', content)
|
|
955
|
+
self.assertEqual(content.split("<main")[1].count("<tr "), 1)
|
|
861
956
|
if hasattr(self.model, "name"):
|
|
862
957
|
self.assertRegex(content, r">\s*" + re.escape(escape(instance1.name)) + r"\s*<", msg=content)
|
|
863
958
|
self.assertNotRegex(content, r">\s*" + re.escape(escape(instance2.name)) + r"\s*<", msg=content)
|
|
@@ -902,8 +997,7 @@ class ViewTestCases:
|
|
|
902
997
|
self.assertNotIn("Unknown filter field", content, msg=content)
|
|
903
998
|
self.assertIn("None", content, msg=content)
|
|
904
999
|
# There should be at least two rows in the table
|
|
905
|
-
self.
|
|
906
|
-
self.assertIn('<tr class="odd', content)
|
|
1000
|
+
self.assertGreaterEqual(content.split("<main")[1].count("<tr "), 2)
|
|
907
1001
|
if hasattr(self.model, "name"):
|
|
908
1002
|
self.assertRegex(content, r">\s*" + re.escape(escape(instance1.name)) + r"\s*<", msg=content)
|
|
909
1003
|
self.assertRegex(content, r">\s*" + re.escape(escape(instance2.name)) + r"\s*<", msg=content)
|
|
@@ -931,11 +1025,14 @@ class ViewTestCases:
|
|
|
931
1025
|
self.assertHttpStatus(response, 200)
|
|
932
1026
|
response_body = utils.extract_page_body(response.content.decode(response.charset))
|
|
933
1027
|
|
|
934
|
-
|
|
1028
|
+
# Check if title is rendered correctly
|
|
935
1029
|
title = self.get_title()
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
1030
|
+
expected_title = (
|
|
1031
|
+
'<h1 class="d-flex fs-2 gap-8 lh-sm py-6">'
|
|
1032
|
+
'<img alt="Nautobot chevron" class="align-self-start flex-grow-0 flex-shrink-0 my-n4" role="presentation" src="/static/img/nautobot_chevron.svg" style="width: 1.5rem;" />'
|
|
1033
|
+
f"{title}</h1>"
|
|
1034
|
+
)
|
|
1035
|
+
self.assertBodyContains(response, expected_title, html=True)
|
|
939
1036
|
|
|
940
1037
|
with self.subTest("Assert import-objects URL is absent due to user permissions"):
|
|
941
1038
|
self.assertNotIn(
|
|
@@ -1184,6 +1281,7 @@ class ViewTestCases:
|
|
|
1184
1281
|
sorted(passed_bulk_edit_data.get(key).values_list("pk", flat=True)), sorted(value)
|
|
1185
1282
|
)
|
|
1186
1283
|
else:
|
|
1284
|
+
self.assertIn(key, bulk_edit_form.fields)
|
|
1187
1285
|
self.assertEqual(passed_bulk_edit_data.get(key), bulk_edit_form.fields[key].clean(value))
|
|
1188
1286
|
|
|
1189
1287
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
@@ -1243,7 +1341,7 @@ class ViewTestCases:
|
|
|
1243
1341
|
self.assertHttpStatus(response, 200)
|
|
1244
1342
|
response_body = utils.extract_page_body(response.content.decode(response.charset))
|
|
1245
1343
|
# Assert the table which shows all the selected objects is not part of the html body in edit all case
|
|
1246
|
-
self.assertNotIn('<table class="table table-hover table-headings">', response_body)
|
|
1344
|
+
self.assertNotIn('<table class="table table-hover nb-table-headings">', response_body)
|
|
1247
1345
|
# Check if all the pks are passed into the BulkEditForm/BulkUpdateForm
|
|
1248
1346
|
for pk in pk_list:
|
|
1249
1347
|
self.assertNotIn(str(pk), response_body)
|
|
@@ -1397,7 +1495,7 @@ class ViewTestCases:
|
|
|
1397
1495
|
self.assertHttpStatus(response, 200)
|
|
1398
1496
|
response_body = utils.extract_page_body(response.content.decode(response.charset))
|
|
1399
1497
|
# Assert the table which shows all the selected objects is not part of the html body in delete all case
|
|
1400
|
-
self.assertNotIn('<table class="table table-hover table-headings">', response_body)
|
|
1498
|
+
self.assertNotIn('<table class="table table-hover nb-table-headings">', response_body)
|
|
1401
1499
|
# Assert none of the hidden input fields for each of the pks that would be deleted is part of the html body
|
|
1402
1500
|
for pk in self._get_queryset().values_list("pk", flat=True):
|
|
1403
1501
|
self.assertNotIn(str(pk), response_body)
|
|
@@ -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):
|
|
@@ -55,7 +59,7 @@ class AppHomeTestCase(SeleniumTestCase):
|
|
|
55
59
|
|
|
56
60
|
self.browser.visit(self.live_server_url)
|
|
57
61
|
|
|
58
|
-
columns_html = self.browser.
|
|
62
|
+
columns_html = self.browser.find_by_xpath("//div[@id='draggable-homepage-panels']")
|
|
59
63
|
for panel_name, panel_details in self.layout.items():
|
|
60
64
|
columns_html.first.find_by_xpath(f".//strong[text()='{panel_name}']")
|
|
61
65
|
for item_name, _ in panel_details.items():
|
|
@@ -71,7 +75,7 @@ class AppHomeTestCase(SeleniumTestCase):
|
|
|
71
75
|
|
|
72
76
|
self.browser.visit(self.live_server_url)
|
|
73
77
|
|
|
74
|
-
columns_html = self.browser.
|
|
78
|
+
columns_html = self.browser.find_by_xpath("//div[@id='draggable-homepage-panels']")
|
|
75
79
|
for panel_name, panel_details in self.layout.items():
|
|
76
80
|
columns_html.first.find_by_xpath(f".//strong[text()='{panel_name}']")
|
|
77
81
|
for item_name, item_details in panel_details.items():
|
|
@@ -88,7 +92,7 @@ class AppHomeTestCase(SeleniumTestCase):
|
|
|
88
92
|
"""
|
|
89
93
|
self.browser.visit(self.live_server_url)
|
|
90
94
|
|
|
91
|
-
columns_html = self.browser.
|
|
95
|
+
columns_html = self.browser.find_by_xpath("//div[@id='draggable-homepage-panels']")
|
|
92
96
|
for panel_name, panel_details in self.layout.items():
|
|
93
97
|
columns_html.first.find_by_xpath(f".//strong[text()='{panel_name}']")
|
|
94
98
|
for item_name, _ in panel_details.items():
|
|
@@ -105,7 +109,7 @@ class AppHomeTestCase(SeleniumTestCase):
|
|
|
105
109
|
|
|
106
110
|
self.browser.visit(self.live_server_url)
|
|
107
111
|
|
|
108
|
-
columns_html = self.browser.
|
|
112
|
+
columns_html = self.browser.find_by_xpath("//div[@id='draggable-homepage-panels']")
|
|
109
113
|
columns_html.first.find_by_xpath(f".//strong[text()='{self.custom_panel_examplemodel['name']}']")
|
|
110
114
|
|
|
111
115
|
for item_name in self.custom_panel_examplemodel["items"]:
|
|
@@ -122,7 +126,7 @@ class AppHomeTestCase(SeleniumTestCase):
|
|
|
122
126
|
|
|
123
127
|
self.browser.visit(self.live_server_url)
|
|
124
128
|
|
|
125
|
-
columns_html = self.browser.
|
|
129
|
+
columns_html = self.browser.find_by_xpath("//div[@id='draggable-homepage-panels']")
|
|
126
130
|
for panel_name, panel_details in self.layout.items():
|
|
127
131
|
columns_html.first.find_by_xpath(f".//*[contains(text(), '{panel_name}')]")
|
|
128
132
|
for item_name, item_details in panel_details.items():
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
from
|
|
1
|
+
from django.test import tag
|
|
2
|
+
|
|
2
3
|
from nautobot.core.testing.integration import SeleniumTestCase
|
|
3
4
|
|
|
4
5
|
|
|
6
|
+
@tag("example_app")
|
|
5
7
|
class AppNavBarTestCase(SeleniumTestCase):
|
|
6
8
|
"""Integration test the navigation menu."""
|
|
7
9
|
|
|
@@ -40,93 +42,84 @@ class AppNavBarTestCase(SeleniumTestCase):
|
|
|
40
42
|
|
|
41
43
|
def setUp(self):
|
|
42
44
|
super().setUp()
|
|
43
|
-
self.
|
|
44
|
-
|
|
45
|
-
def tearDown(self):
|
|
46
|
-
self.logout()
|
|
47
|
-
super().tearDown()
|
|
45
|
+
self.login_as_superuser()
|
|
46
|
+
self.browser.visit(self.live_server_url)
|
|
48
47
|
|
|
49
48
|
def test_app_navbar_new_tab(self):
|
|
50
49
|
"""
|
|
51
50
|
Verify that a new menu tab defined and populated by the example app is rendered properly.
|
|
52
51
|
"""
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
self.
|
|
56
|
-
|
|
57
|
-
# Retrieve home page
|
|
58
|
-
self.browser.visit(self.live_server_url)
|
|
52
|
+
sidenav_section = self.find_sidenav_section("Example Menu")
|
|
53
|
+
sidenav_section.toggle()
|
|
54
|
+
self.assertTrue(sidenav_section.is_expanded)
|
|
59
55
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
self.assertEqual(
|
|
56
|
+
group = sidenav_section.flyout.find_by_xpath(
|
|
57
|
+
"//li[@class='nb-sidenav-link-group' and normalize-space()='Example Group 1']"
|
|
58
|
+
)
|
|
59
|
+
self.assertEqual(len(group), 1)
|
|
64
60
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
item_xpath = f"{tab_xpath}/following-sibling::ul//li[.//a[normalize-space()='Example Models']]"
|
|
68
|
-
group.find_by_xpath(item_xpath)
|
|
61
|
+
link = sidenav_section.find_link("Example Models")
|
|
62
|
+
self.assertEqual(len(link), 1)
|
|
69
63
|
|
|
70
64
|
def test_app_navbar_modify_circuits(self):
|
|
71
65
|
"""
|
|
72
66
|
Verify that the example app is able to add a new group and items to an existing menu tab.
|
|
73
67
|
"""
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
self.
|
|
77
|
-
|
|
78
|
-
# Retrieve home page
|
|
79
|
-
self.browser.visit(self.live_server_url)
|
|
80
|
-
|
|
81
|
-
tab_xpath = "//*[@id='navbar']//*[normalize-space()='Circuits']"
|
|
82
|
-
tab = self.browser.find_by_xpath(tab_xpath)
|
|
83
|
-
tab.click()
|
|
84
|
-
self.assertEqual(tab["aria-expanded"], "true")
|
|
68
|
+
sidenav_section = self.find_sidenav_section("Circuits")
|
|
69
|
+
sidenav_section.toggle()
|
|
70
|
+
self.assertTrue(sidenav_section.is_expanded)
|
|
85
71
|
|
|
86
72
|
for group_name, items in self.navbar["Circuits"].items():
|
|
87
|
-
group =
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
73
|
+
group = sidenav_section.flyout.find_by_xpath(
|
|
74
|
+
f"//li[@class='nb-sidenav-link-group' and normalize-space()='{group_name}']"
|
|
75
|
+
)
|
|
76
|
+
self.assertEqual(len(group), 1)
|
|
91
77
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
self.assertIn(button_class, button["class"])
|
|
96
|
-
if button_icon := getattr(ButtonActionIconChoices, button_name.upper(), None):
|
|
97
|
-
icon = button.find_by_xpath(f"{item_xpath}/div//a[@data-original-title='{button_name}']/i")
|
|
98
|
-
self.assertIn(button_icon, icon["class"])
|
|
78
|
+
for item_name, _ in items.items():
|
|
79
|
+
link = sidenav_section.find_link(item_name)
|
|
80
|
+
self.assertEqual(len(link), 1)
|
|
99
81
|
|
|
100
|
-
|
|
82
|
+
sidenav_section.toggle()
|
|
101
83
|
|
|
102
84
|
def test_app_navbar_apps_tab(self):
|
|
103
85
|
"""
|
|
104
86
|
Test that old-style app menu definitions are correctly rendered to the Apps menu tab.
|
|
105
87
|
"""
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
self.
|
|
88
|
+
sidenav_section = self.find_sidenav_section("Apps")
|
|
89
|
+
sidenav_section.toggle()
|
|
90
|
+
self.assertTrue(sidenav_section.is_expanded)
|
|
109
91
|
|
|
110
|
-
|
|
111
|
-
|
|
92
|
+
for group_name, items in self.navbar["Apps"].items():
|
|
93
|
+
group = sidenav_section.flyout.find_by_xpath(
|
|
94
|
+
f"//li[@class='nb-sidenav-link-group' and normalize-space()='{group_name}']"
|
|
95
|
+
)
|
|
96
|
+
self.assertEqual(len(group), 1)
|
|
112
97
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
self.assertEqual(tab["aria-expanded"], "true")
|
|
98
|
+
for item_name, _ in items.items():
|
|
99
|
+
link = sidenav_section.find_link(item_name)
|
|
100
|
+
self.assertEqual(len(link), 1)
|
|
117
101
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
102
|
+
sidenav_section.toggle()
|
|
103
|
+
|
|
104
|
+
def test_app_navbar_state_persistence(self):
|
|
105
|
+
"""
|
|
106
|
+
Verify that menu expanded/collapse state is persistent and does not reset after page refresh.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
def get_toggler(aria_expanded):
|
|
110
|
+
return self.browser.find_by_xpath(
|
|
111
|
+
f"//*[@id='sidenav']//button[contains(@class, 'nb-sidenav-toggler') and @aria-expanded='{aria_expanded}']"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
toggler = get_toggler("true") # Get toggler, expect sidenav to be expanded by default.
|
|
115
|
+
self.assertTrue(toggler)
|
|
116
|
+
|
|
117
|
+
toggler.click() # Collapse sidenav.
|
|
118
|
+
self.browser.reload()
|
|
119
|
+
toggler = get_toggler("false") # Get toggler, expect sidenav to stay collapsed after full document reload.
|
|
120
|
+
self.assertTrue(toggler)
|
|
121
|
+
|
|
122
|
+
toggler.click() # Expand sidenav.
|
|
123
|
+
self.browser.reload()
|
|
124
|
+
toggler = get_toggler("true") # Get toggler, expect sidenav to be expanded again.
|
|
125
|
+
self.assertTrue(toggler)
|