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
|
@@ -31,6 +31,23 @@ def render_components(context, components):
|
|
|
31
31
|
return ""
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
@register.simple_tag(takes_context=True)
|
|
35
|
+
def render_table_config_forms(context, tabs):
|
|
36
|
+
if tabs is not None:
|
|
37
|
+
return format_html_join(
|
|
38
|
+
"\n",
|
|
39
|
+
"{}",
|
|
40
|
+
(
|
|
41
|
+
[panel.render_table_config_form(context)]
|
|
42
|
+
for tab in tabs
|
|
43
|
+
if tab.should_render_content(context)
|
|
44
|
+
for panel in tab.panels
|
|
45
|
+
if hasattr(panel, "render_table_config_form")
|
|
46
|
+
),
|
|
47
|
+
)
|
|
48
|
+
return ""
|
|
49
|
+
|
|
50
|
+
|
|
34
51
|
@register.simple_tag(takes_context=True)
|
|
35
52
|
def render_title(context, mode="plain"):
|
|
36
53
|
"""
|
nautobot/core/testing/api.py
CHANGED
|
@@ -3,6 +3,7 @@ from io import StringIO
|
|
|
3
3
|
from typing import Optional, Sequence, Union
|
|
4
4
|
|
|
5
5
|
from django.conf import settings
|
|
6
|
+
from django.contrib.auth.models import Group
|
|
6
7
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
|
7
8
|
from django.contrib.contenttypes.models import ContentType
|
|
8
9
|
from django.db import connections, DEFAULT_DB_ALIAS
|
|
@@ -16,6 +17,7 @@ from rest_framework import serializers, status
|
|
|
16
17
|
from rest_framework.relations import ManyRelatedField
|
|
17
18
|
from rest_framework.test import APITransactionTestCase as _APITransactionTestCase
|
|
18
19
|
|
|
20
|
+
from nautobot.core import constants
|
|
19
21
|
from nautobot.core.api.utils import get_serializer_for_model
|
|
20
22
|
from nautobot.core.models import fields as core_fields
|
|
21
23
|
from nautobot.core.models.tree_queries import TreeModel
|
|
@@ -253,7 +255,7 @@ class APIViewTestCases:
|
|
|
253
255
|
if not field.name.startswith("_"):
|
|
254
256
|
if isinstance(field, (ForeignKey, GenericForeignKey, ManyToManyField, core_fields.TagsField)) and (
|
|
255
257
|
# we represent content-types as "app_label.modelname" rather than as FKs
|
|
256
|
-
field.related_model
|
|
258
|
+
field.related_model not in [ContentType, Group]
|
|
257
259
|
# user is a model field on Token but not a field on TokenSerializer
|
|
258
260
|
and not (field.name == "user" and self.model == users_models.Token)
|
|
259
261
|
):
|
|
@@ -293,8 +295,9 @@ class APIViewTestCases:
|
|
|
293
295
|
m2m_fields = self.get_m2m_fields()
|
|
294
296
|
self.add_permissions(f"{self.model._meta.app_label}.view_{self.model._meta.model_name}")
|
|
295
297
|
list_url = f"{self._get_list_url()}?depth=0"
|
|
298
|
+
# With exclude_m2m query parameter set to False
|
|
296
299
|
with CaptureQueriesContext(connections[DEFAULT_DB_ALIAS]) as cqc:
|
|
297
|
-
response = self.client.get(list_url, **self.header)
|
|
300
|
+
response = self.client.get(list_url + "&exclude_m2m=false", **self.header)
|
|
298
301
|
base_num_queries = len(cqc)
|
|
299
302
|
|
|
300
303
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
@@ -339,9 +342,9 @@ class APIViewTestCases:
|
|
|
339
342
|
app_label, model_name = object_type.split(".")
|
|
340
343
|
ContentType.objects.get(app_label=app_label, model=model_name)
|
|
341
344
|
|
|
342
|
-
|
|
345
|
+
# With exclude_m2m query parameter set to True
|
|
343
346
|
with CaptureQueriesContext(connections[DEFAULT_DB_ALIAS]) as cqc:
|
|
344
|
-
response = self.client.get(list_url, **self.header)
|
|
347
|
+
response = self.client.get(list_url + "&exclude_m2m=true", **self.header)
|
|
345
348
|
|
|
346
349
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
347
350
|
self.assertIsInstance(response.data, dict)
|
|
@@ -386,8 +389,9 @@ class APIViewTestCases:
|
|
|
386
389
|
m2m_fields = self.get_m2m_fields()
|
|
387
390
|
self.add_permissions(f"{self.model._meta.app_label}.view_{self.model._meta.model_name}")
|
|
388
391
|
list_url = f"{self._get_list_url()}?depth=1"
|
|
392
|
+
# With exclude_m2m query parameter set to False
|
|
389
393
|
with CaptureQueriesContext(connections[DEFAULT_DB_ALIAS]) as cqc:
|
|
390
|
-
response = self.client.get(list_url, **self.header)
|
|
394
|
+
response = self.client.get(list_url + "&exclude_m2m=false", **self.header)
|
|
391
395
|
base_num_queries = len(cqc)
|
|
392
396
|
|
|
393
397
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
@@ -419,9 +423,9 @@ class APIViewTestCases:
|
|
|
419
423
|
self.assertTrue(is_uuid(response_data[field]["id"]))
|
|
420
424
|
self.assertGreater(len(response_data[field].keys()), 3, response_data[field])
|
|
421
425
|
|
|
422
|
-
|
|
426
|
+
# With exclude_m2m query parameter set to True
|
|
423
427
|
with CaptureQueriesContext(connections[DEFAULT_DB_ALIAS]) as cqc:
|
|
424
|
-
response = self.client.get(list_url, **self.header)
|
|
428
|
+
response = self.client.get(list_url + "&exclude_m2m=true", **self.header)
|
|
425
429
|
|
|
426
430
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
427
431
|
self.assertIsInstance(response.data, dict)
|
|
@@ -457,6 +461,54 @@ class APIViewTestCases:
|
|
|
457
461
|
self.assertNotIn(field, response_data)
|
|
458
462
|
# TODO: we should assert that all other fields are still present, but there's a few corner cases...
|
|
459
463
|
|
|
464
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
|
465
|
+
def test_list_objects_exclude_m2m(self):
|
|
466
|
+
"""
|
|
467
|
+
GET a list of objects with or without the "exclude_m2m" parameter.
|
|
468
|
+
|
|
469
|
+
With exclude_m2m query parameter set to True, we should see no many-to-many fields.
|
|
470
|
+
With exclude_m2m query parameter set to False, we should see all many-to-many fields.
|
|
471
|
+
With exclude_m2m query parameter not set, we should only see the default many-to-many fields.
|
|
472
|
+
"""
|
|
473
|
+
m2m_fields = self.get_m2m_fields()
|
|
474
|
+
if not m2m_fields:
|
|
475
|
+
self.skipTest("No many-to-many fields to test")
|
|
476
|
+
self.add_permissions(f"{self.model._meta.app_label}.view_{self.model._meta.model_name}")
|
|
477
|
+
list_url = f"{self._get_list_url()}"
|
|
478
|
+
|
|
479
|
+
# With exclude_m2m query parameter not set
|
|
480
|
+
response = self.client.get(list_url, **self.header)
|
|
481
|
+
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
482
|
+
self.assertIsInstance(response.data, dict)
|
|
483
|
+
self.assertIn("results", response.data)
|
|
484
|
+
|
|
485
|
+
for response_data in response.data["results"]:
|
|
486
|
+
for field in m2m_fields:
|
|
487
|
+
if field in constants.DEFAULT_M2M_FIELDS:
|
|
488
|
+
self.assertIn(field, response_data)
|
|
489
|
+
self.assertIsInstance(response_data[field], list)
|
|
490
|
+
else:
|
|
491
|
+
self.assertNotIn(field, response_data)
|
|
492
|
+
|
|
493
|
+
# With exclude_m2m query parameter set to True
|
|
494
|
+
response = self.client.get(list_url + "?exclude_m2m=true", **self.header)
|
|
495
|
+
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
496
|
+
self.assertIsInstance(response.data, dict)
|
|
497
|
+
self.assertIn("results", response.data)
|
|
498
|
+
for response_data in response.data["results"]:
|
|
499
|
+
for field in m2m_fields:
|
|
500
|
+
self.assertNotIn(field, response_data)
|
|
501
|
+
|
|
502
|
+
# With exclude_m2m query parameter set to False
|
|
503
|
+
response = self.client.get(list_url + "?exclude_m2m=false", **self.header)
|
|
504
|
+
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
505
|
+
self.assertIsInstance(response.data, dict)
|
|
506
|
+
self.assertIn("results", response.data)
|
|
507
|
+
for response_data in response.data["results"]:
|
|
508
|
+
for field in m2m_fields:
|
|
509
|
+
self.assertIn(field, response_data)
|
|
510
|
+
self.assertIsInstance(response_data[field], list)
|
|
511
|
+
|
|
460
512
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
|
461
513
|
def test_list_objects_without_permission(self):
|
|
462
514
|
"""
|
|
@@ -745,8 +797,12 @@ class APIViewTestCases:
|
|
|
745
797
|
# Verify ObjectChange creation
|
|
746
798
|
if hasattr(self.model, "to_objectchange"):
|
|
747
799
|
objectchanges = lookup.get_changes_for_model(instance)
|
|
748
|
-
|
|
749
|
-
|
|
800
|
+
# For models with ManyToMany relationships (like Device with clusters), the creation
|
|
801
|
+
# process may generate both CREATE and UPDATE records. We just need to ensure at least
|
|
802
|
+
# one ObjectChange record exists.
|
|
803
|
+
self.assertGreaterEqual(len(objectchanges), 1)
|
|
804
|
+
# Verify that at least one ObjectChange record is for this instance
|
|
805
|
+
self.assertTrue(any(oc.changed_object == instance for oc in objectchanges))
|
|
750
806
|
|
|
751
807
|
# TODO: The override_settings here is a temporary workaround for not breaking any app tests
|
|
752
808
|
# long term fix should be using appropriate object permissions instead of the blanket override
|
|
@@ -1071,12 +1127,20 @@ class APIViewTestCases:
|
|
|
1071
1127
|
|
|
1072
1128
|
self.assertIn("actions", data)
|
|
1073
1129
|
|
|
1074
|
-
# Grab any field that has choices defined (fields with enums)
|
|
1130
|
+
# Grab any field that has choices defined (fields with enums including child fields with enums)
|
|
1075
1131
|
field_choices = {}
|
|
1076
1132
|
if "POST" in data["actions"]:
|
|
1077
|
-
field_choices = {
|
|
1133
|
+
field_choices = {
|
|
1134
|
+
k
|
|
1135
|
+
for k, v in data["actions"]["POST"].items()
|
|
1136
|
+
if "choices" in v or ("child" in v and "choices" in v["child"])
|
|
1137
|
+
}
|
|
1078
1138
|
elif "PUT" in data["actions"]:
|
|
1079
|
-
field_choices = {
|
|
1139
|
+
field_choices = {
|
|
1140
|
+
k
|
|
1141
|
+
for k, v in data["actions"]["PUT"].items()
|
|
1142
|
+
if "choices" in v or ("child" in v and "choices" in v["child"])
|
|
1143
|
+
}
|
|
1080
1144
|
else:
|
|
1081
1145
|
self.fail(f"Neither PUT nor POST are available actions in: {data['actions']}")
|
|
1082
1146
|
|
nautobot/core/testing/filters.py
CHANGED
|
@@ -21,7 +21,6 @@ from nautobot.core.filters import (
|
|
|
21
21
|
)
|
|
22
22
|
from nautobot.core.models.generics import PrimaryModel
|
|
23
23
|
from nautobot.core.testing import views
|
|
24
|
-
from nautobot.core.utils.deprecation import class_deprecated_in_favor_of
|
|
25
24
|
from nautobot.extras.models import Contact, ContactAssociation, Role, Status, Tag, Team
|
|
26
25
|
from nautobot.tenancy import models
|
|
27
26
|
|
|
@@ -215,7 +214,9 @@ class FilterTestCases:
|
|
|
215
214
|
with self.subTest(f"{self.filterset.__name__} filter {filter_name} ({field_name})"):
|
|
216
215
|
test_data = self.get_filterset_test_values(field_name)
|
|
217
216
|
params = {filter_name: test_data}
|
|
218
|
-
|
|
217
|
+
filterset = self.filterset(params, self.queryset) # pylint: disable=not-callable
|
|
218
|
+
self.assertIn(filter_name, list(filterset.filters.keys()))
|
|
219
|
+
filterset_result = filterset.qs
|
|
219
220
|
qs_result = self.queryset.filter(**{f"{field_name}__in": test_data}).distinct()
|
|
220
221
|
self.assertQuerysetEqualAndNotEmpty(filterset_result, qs_result, ordered=False)
|
|
221
222
|
|
|
@@ -237,8 +238,8 @@ class FilterTestCases:
|
|
|
237
238
|
"""Test all `RelatedMembershipBooleanFilter` filters found in `self.filterset.filters`
|
|
238
239
|
except for the ones with custom filter logic defined in its `method` attribute.
|
|
239
240
|
|
|
240
|
-
This test asserts that `filter=True` matches `self.queryset.filter(field__isnull
|
|
241
|
-
that `filter=False` matches `self.queryset.
|
|
241
|
+
This test asserts that `filter=True` matches `self.queryset.filter(field__isnull=...)` and
|
|
242
|
+
that `filter=False` matches `self.queryset.exclude(field__isnull=...)`.
|
|
242
243
|
"""
|
|
243
244
|
self.assertIsNotNone(self.filterset)
|
|
244
245
|
for filter_name, filter_object in self.filterset().filters.items(): # pylint: disable=not-callable
|
|
@@ -425,31 +426,14 @@ class FilterTestCases:
|
|
|
425
426
|
# This field is not part of the Filterset.
|
|
426
427
|
continue
|
|
427
428
|
self.assertIsInstance(
|
|
428
|
-
filter_field,
|
|
429
|
+
filter_field,
|
|
430
|
+
(
|
|
431
|
+
ContentTypeFilter,
|
|
432
|
+
ContentTypeMultipleChoiceFilter,
|
|
433
|
+
ContentTypeChoiceFilter,
|
|
434
|
+
),
|
|
429
435
|
)
|
|
430
436
|
|
|
431
|
-
# Test cases should just explicitly include `name` as a generic_filter_tests entry
|
|
432
|
-
@class_deprecated_in_favor_of(FilterTestCase) # pylint: disable=undefined-variable
|
|
433
|
-
class NameOnlyFilterTestCase(FilterTestCase):
|
|
434
|
-
"""Add simple tests for filtering by name."""
|
|
435
|
-
|
|
436
|
-
def test_filters_generic(self):
|
|
437
|
-
if not any(test[0] == "name" for test in self.generic_filter_tests):
|
|
438
|
-
self.generic_filter_tests = (["name"], *self.generic_filter_tests)
|
|
439
|
-
super().test_filters_generic()
|
|
440
|
-
|
|
441
|
-
# Test cases should just explicitly include `name` and `slug` as generic_filter_tests entries
|
|
442
|
-
@class_deprecated_in_favor_of(FilterTestCase) # pylint: disable=undefined-variable
|
|
443
|
-
class NameSlugFilterTestCase(FilterTestCase):
|
|
444
|
-
"""Add simple tests for filtering by name and by slug."""
|
|
445
|
-
|
|
446
|
-
def test_filters_generic(self):
|
|
447
|
-
if not any(test[0] == "slug" for test in self.generic_filter_tests):
|
|
448
|
-
self.generic_filter_tests = (["slug"], *self.generic_filter_tests)
|
|
449
|
-
if not any(test[0] == "name" for test in self.generic_filter_tests):
|
|
450
|
-
self.generic_filter_tests = (["name"], *self.generic_filter_tests)
|
|
451
|
-
super().test_filters_generic()
|
|
452
|
-
|
|
453
437
|
class TenancyFilterTestCaseMixin(views.TestCase):
|
|
454
438
|
"""Add test cases for tenant and tenant-group filters."""
|
|
455
439
|
|
|
@@ -7,7 +7,9 @@ from django.db.models import Model
|
|
|
7
7
|
from django.test import override_settings, tag
|
|
8
8
|
from django.urls import reverse
|
|
9
9
|
from django.utils.functional import classproperty
|
|
10
|
+
from selenium.webdriver.common.by import By
|
|
10
11
|
from selenium.webdriver.common.keys import Keys
|
|
12
|
+
from selenium.webdriver.support.expected_conditions import element_to_be_clickable
|
|
11
13
|
from selenium.webdriver.support.wait import WebDriverWait
|
|
12
14
|
from splinter.browser import Browser
|
|
13
15
|
from splinter.exceptions import ElementDoesNotExist
|
|
@@ -90,6 +92,12 @@ class ObjectsListMixin:
|
|
|
90
92
|
"""
|
|
91
93
|
self.click_button('#select_all_box button[name="_edit"]')
|
|
92
94
|
|
|
95
|
+
def click_add_item(self):
|
|
96
|
+
"""
|
|
97
|
+
Click add item button on top of the items table list.
|
|
98
|
+
"""
|
|
99
|
+
self.click_button("#add-button")
|
|
100
|
+
|
|
93
101
|
def click_table_link(self, row=1, column=2):
|
|
94
102
|
"""By default, tries to click column next to checkbox to go to the details page."""
|
|
95
103
|
self.browser.find_by_xpath(f'//*[@id="object_list_form"]//tbody/tr[{row}]/td[{column}]/a').click()
|
|
@@ -123,7 +131,7 @@ class ObjectDetailsMixin:
|
|
|
123
131
|
By default, it's not using the exact match, because on the UI we're often adding
|
|
124
132
|
additional tags, relationships or units.
|
|
125
133
|
"""
|
|
126
|
-
panel_xpath = f'//*[@id="main"]//div[@class
|
|
134
|
+
panel_xpath = f'//*[@id="main"]//div[contains(@class, "card-header") and contains(normalize-space(), "{panel_label}")]/following-sibling::div[contains(@class, "collapse")]/table'
|
|
127
135
|
value = self.browser.find_by_xpath(f'{panel_xpath}//td[text()="{field_label}"]/following-sibling::td[1]').text
|
|
128
136
|
|
|
129
137
|
if exact_match:
|
|
@@ -131,6 +139,52 @@ class ObjectDetailsMixin:
|
|
|
131
139
|
else:
|
|
132
140
|
self.assertIn(str(expected_value), value)
|
|
133
141
|
|
|
142
|
+
def find_and_open_panel_v2(self, panel_label):
|
|
143
|
+
"""
|
|
144
|
+
Finds panel with given title and expand it if needed. Works with v2 template panels.
|
|
145
|
+
|
|
146
|
+
TODO: remove after all panels will be moved to UI Components Framework or new Bootstrap 5 templates.
|
|
147
|
+
"""
|
|
148
|
+
panel_xpath = f'//*[@id="main"]//div[@class="card-header"][contains(normalize-space(), "{panel_label}")]'
|
|
149
|
+
expand_button_xpath = f"{panel_xpath}/button[normalize-space()='Expand All Groups']"
|
|
150
|
+
expand_button = self.browser.find_by_xpath(expand_button_xpath)
|
|
151
|
+
if not expand_button.is_empty():
|
|
152
|
+
expand_button.click()
|
|
153
|
+
return self.browser.find_by_xpath(f"{panel_xpath}/../table")
|
|
154
|
+
|
|
155
|
+
def switch_tab(self, tab_name):
|
|
156
|
+
"""Finds and click tab based on tab name from the tab link."""
|
|
157
|
+
self.get_tab_link(tab_name).click()
|
|
158
|
+
|
|
159
|
+
def get_tab_link(self, tab_name):
|
|
160
|
+
"""
|
|
161
|
+
Finds tab link either in dropdown menu, cloned menu or standard tabs list
|
|
162
|
+
depending on the browser size and tabs count.
|
|
163
|
+
|
|
164
|
+
Please note, that if tab will be placed in dropdown menu this function will left this menu open.
|
|
165
|
+
"""
|
|
166
|
+
tabs_container_xpath = '//div[@data-nb-tests-id="object-details-header-tabs"]'
|
|
167
|
+
toggle_button_xpath = f'{tabs_container_xpath}//button[@data-bs-toggle="dropdown"]'
|
|
168
|
+
toggle_button = self.browser.find_by_xpath(toggle_button_xpath, wait_time=5)
|
|
169
|
+
if toggle_button:
|
|
170
|
+
# Our tab might be hidden
|
|
171
|
+
tab_xpath = (
|
|
172
|
+
f'{tabs_container_xpath}//ul[@data-clone="true"]/li/a[contains(normalize-space(), "{tab_name}")]'
|
|
173
|
+
)
|
|
174
|
+
visible_tab = self.browser.find_by_xpath(tab_xpath, wait_time=5)
|
|
175
|
+
if visible_tab:
|
|
176
|
+
return visible_tab
|
|
177
|
+
|
|
178
|
+
# If hidden, click toggle to show the dropdown menu
|
|
179
|
+
toggle_button.click()
|
|
180
|
+
tab_xpath = f'{toggle_button_xpath}/following-sibling::ul//a[contains(normalize-space(), "{tab_name}")]'
|
|
181
|
+
return self.browser.find_by_xpath(tab_xpath, wait_time=5)
|
|
182
|
+
|
|
183
|
+
tab_xpath = (
|
|
184
|
+
f'//ul[@data-nb-tests-id="object-details-header-tabs-ul"]//a[contains(normalize-space(), "{tab_name}")]'
|
|
185
|
+
)
|
|
186
|
+
return self.browser.find_by_xpath(tab_xpath, wait_time=5)
|
|
187
|
+
|
|
134
188
|
|
|
135
189
|
class BulkOperationsMixin:
|
|
136
190
|
def confirm_bulk_delete_operation(self):
|
|
@@ -173,7 +227,7 @@ class BulkOperationsMixin:
|
|
|
173
227
|
if is_select:
|
|
174
228
|
self.fill_select2_field(field_name, value)
|
|
175
229
|
else:
|
|
176
|
-
self.
|
|
230
|
+
self.fill_input(field_name, value)
|
|
177
231
|
|
|
178
232
|
def assertBulkDeleteConfirmMessageIsValid(self, expected_count):
|
|
179
233
|
"""
|
|
@@ -207,6 +261,44 @@ class BulkOperationsMixin:
|
|
|
207
261
|
self.assertEqual(job_status, "Completed")
|
|
208
262
|
|
|
209
263
|
|
|
264
|
+
class SidenavSection:
|
|
265
|
+
"""
|
|
266
|
+
Helper class which represents a Sidenav section and simplify moving around sidenav, expanding section and clicking
|
|
267
|
+
proper navigation link.
|
|
268
|
+
"""
|
|
269
|
+
|
|
270
|
+
def __init__(self, browser, section_name):
|
|
271
|
+
self.browser = browser
|
|
272
|
+
self.section_name = section_name
|
|
273
|
+
self.section_xpath = f"//*[@id='sidenav']//li[@data-section-name='{self.section_name}']"
|
|
274
|
+
self.section = self.browser.find_by_xpath(self.section_xpath)
|
|
275
|
+
|
|
276
|
+
@property
|
|
277
|
+
def button(self):
|
|
278
|
+
return self.section.find_by_xpath(f"{self.section_xpath}/button")
|
|
279
|
+
|
|
280
|
+
@property
|
|
281
|
+
def flyout(self):
|
|
282
|
+
return self.section.find_by_xpath(f"{self.section_xpath}/div[@class='nb-sidenav-flyout']")
|
|
283
|
+
|
|
284
|
+
@property
|
|
285
|
+
def is_expanded(self):
|
|
286
|
+
return self.button["aria-expanded"] == "true"
|
|
287
|
+
|
|
288
|
+
def toggle(self):
|
|
289
|
+
if not self.is_expanded:
|
|
290
|
+
self.button.click()
|
|
291
|
+
|
|
292
|
+
def find_link(self, link_name):
|
|
293
|
+
return self.section.find_by_xpath(
|
|
294
|
+
f"{self.section_xpath}/div[@class='nb-sidenav-flyout']//a[@class='nb-sidenav-link' and normalize-space()='{link_name}']"
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
def click_link(self, link_name):
|
|
298
|
+
link = self.find_link(link_name)
|
|
299
|
+
link.click()
|
|
300
|
+
|
|
301
|
+
|
|
210
302
|
# In CI, sometimes the FQDN of SELENIUM_HOST gets used, other times it seems to be just the hostname?
|
|
211
303
|
@override_settings(ALLOWED_HOSTS=["nautobot.example.com", SELENIUM_HOST, SELENIUM_HOST.split(".")[0]])
|
|
212
304
|
@tag("integration")
|
|
@@ -265,23 +357,31 @@ class SeleniumTestCase(StaticLiveServerTestCase, testing.NautobotTestCaseMixin):
|
|
|
265
357
|
if self.browser.is_text_present("Please enter a correct username and password."):
|
|
266
358
|
self.fail(f"Unable to login in with username {username}")
|
|
267
359
|
|
|
360
|
+
self.logged_in = True
|
|
361
|
+
|
|
268
362
|
def logout(self):
|
|
269
363
|
self.browser.visit(f"{self.live_server_url}/logout")
|
|
270
364
|
|
|
365
|
+
def find_sidenav_section(self, tab_name):
|
|
366
|
+
"""
|
|
367
|
+
Helper function to find sidenav section known as "tab_name". In `nav_menu` we still have tabs/groups naming.
|
|
368
|
+
"""
|
|
369
|
+
return SidenavSection(self.browser, tab_name)
|
|
370
|
+
|
|
271
371
|
def click_navbar_entry(self, parent_menu_name, child_menu_name):
|
|
272
372
|
"""
|
|
273
373
|
Helper function to click on a parent menu and child menu in the navigation bar.
|
|
274
374
|
"""
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
if not
|
|
278
|
-
|
|
279
|
-
child_menu_xpath = f"{
|
|
375
|
+
section_xpath = f"//*[@id='sidenav']//li[@data-section-name='{parent_menu_name}']"
|
|
376
|
+
sidenav_button = self.browser.find_by_xpath(f"{section_xpath}/button", wait_time=5)
|
|
377
|
+
if not sidenav_button["aria-expanded"] == "true":
|
|
378
|
+
sidenav_button.click()
|
|
379
|
+
child_menu_xpath = f"{section_xpath}/div[@class='nb-sidenav-flyout']//a[contains(@class, 'nb-sidenav-link') and normalize-space()='{child_menu_name}']"
|
|
280
380
|
child_menu = self.browser.find_by_xpath(child_menu_xpath, wait_time=5)
|
|
281
381
|
old_url = self.browser.url
|
|
282
382
|
child_menu.click()
|
|
283
383
|
|
|
284
|
-
WebDriverWait(self.browser,
|
|
384
|
+
WebDriverWait(self.browser, 30).until(lambda driver: driver.url != old_url)
|
|
285
385
|
# Wait for body element to appear
|
|
286
386
|
self.assertTrue(self.browser.is_element_present_by_tag("body", wait_time=5), "Page failed to load")
|
|
287
387
|
|
|
@@ -357,11 +457,29 @@ class SeleniumTestCase(StaticLiveServerTestCase, testing.NautobotTestCaseMixin):
|
|
|
357
457
|
search_box.first.type(Keys.ENTER)
|
|
358
458
|
|
|
359
459
|
def click_button(self, query_selector):
|
|
360
|
-
|
|
460
|
+
self.browser.is_element_present_by_css(query_selector, wait_time=5)
|
|
361
461
|
# Button might be visible but on the edge and then impossible to click due to vertical/horizontal scrolls
|
|
362
|
-
self.browser.execute_script(
|
|
462
|
+
self.browser.execute_script(
|
|
463
|
+
f"document.querySelector('{query_selector}').scrollIntoView({{ behavior: 'instant', block: 'start' }});"
|
|
464
|
+
)
|
|
465
|
+
# Scrolling may be asynchronous, wait until it's actually clickable.
|
|
466
|
+
WebDriverWait(self.browser.driver, 30).until(element_to_be_clickable((By.CSS_SELECTOR, query_selector)))
|
|
467
|
+
btn = self.browser.find_by_css(query_selector)
|
|
363
468
|
btn.click()
|
|
364
469
|
|
|
470
|
+
def fill_input(self, input_name, input_value):
|
|
471
|
+
"""
|
|
472
|
+
Helper function to fill an input field. Solves issue with element could not be scrolled into view for some pages.
|
|
473
|
+
"""
|
|
474
|
+
self.browser.is_element_present_by_name(input_name, wait_time=5)
|
|
475
|
+
element = self.browser.find_by_name(input_name)
|
|
476
|
+
self.browser.execute_script(
|
|
477
|
+
"arguments[0].scrollIntoView({ behavior: 'instant', block: 'start' });", element.first._element
|
|
478
|
+
)
|
|
479
|
+
element.is_visible(wait_time=5)
|
|
480
|
+
self.browser.execute_script("arguments[0].focus();", element.first._element)
|
|
481
|
+
self.browser.fill(input_name, input_value)
|
|
482
|
+
|
|
365
483
|
def login_as_superuser(self):
|
|
366
484
|
self.user.is_superuser = True
|
|
367
485
|
self.user.save()
|
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
|
"""
|
|
@@ -220,10 +222,11 @@ class NautobotTestCaseMixin:
|
|
|
220
222
|
Compare a model instance to a dictionary, checking that its attribute values match those specified
|
|
221
223
|
in the dictionary.
|
|
222
224
|
|
|
223
|
-
:
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
225
|
+
Args:
|
|
226
|
+
instance (Model): Django model instance
|
|
227
|
+
data (dict): Dictionary of test data used to define the instance
|
|
228
|
+
exclude (Optional[List[str]]): List of fields to exclude from comparison (e.g. passwords, which get hashed)
|
|
229
|
+
api (bool): Set to True is the data is a JSON representation of the instance
|
|
227
230
|
"""
|
|
228
231
|
if exclude is None:
|
|
229
232
|
exclude = []
|
nautobot/core/testing/utils.py
CHANGED
|
@@ -10,6 +10,8 @@ from django.db.models import Q
|
|
|
10
10
|
from django.db.models.deletion import PROTECT
|
|
11
11
|
from tree_queries.models import TreeNodeForeignKey
|
|
12
12
|
|
|
13
|
+
from nautobot.core.templatetags.helpers import bettertitle
|
|
14
|
+
|
|
13
15
|
# Use the proper swappable User model
|
|
14
16
|
User = get_user_model()
|
|
15
17
|
|
|
@@ -70,17 +72,20 @@ def extract_page_body(content):
|
|
|
70
72
|
<html>
|
|
71
73
|
<head>...</head>
|
|
72
74
|
<body>
|
|
73
|
-
<nav>...</nav
|
|
74
|
-
<
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
<nav id="sidenav">...</nav><!-- BEGIN -->
|
|
76
|
+
<header><nav><!-- breadcrumbs --></nav></header>
|
|
77
|
+
<main class="container-fluid wrapper" id="main-content" tabindex="-1">
|
|
78
|
+
...
|
|
79
|
+
</div><!-- END -->
|
|
77
80
|
<footer class="footer">...</footer>
|
|
78
81
|
...
|
|
79
82
|
</body>
|
|
80
83
|
</html>
|
|
81
84
|
"""
|
|
82
85
|
try:
|
|
83
|
-
return re.findall(
|
|
86
|
+
return re.findall(
|
|
87
|
+
r"<nav id=\"sidenav\"[^>]*>.*?</nav>(.*?)(?=<footer)", content, flags=(re.MULTILINE | re.DOTALL)
|
|
88
|
+
)[0]
|
|
84
89
|
except IndexError:
|
|
85
90
|
return content
|
|
86
91
|
|
|
@@ -117,3 +122,21 @@ def generate_random_device_asset_tag_of_specified_size(size):
|
|
|
117
122
|
"""
|
|
118
123
|
asset_tag = "".join(random.choices(string.ascii_letters + string.digits, k=size)) # noqa: S311 # suspicious-non-cryptographic-random-usage
|
|
119
124
|
return asset_tag
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def get_expected_menu_item_name(view_model) -> str:
|
|
128
|
+
"""Return the expected menu item name for a given model."""
|
|
129
|
+
name_map = {
|
|
130
|
+
"VM Interfaces": "Interfaces",
|
|
131
|
+
"Object Changes": "Change Log",
|
|
132
|
+
"Controller Managed Device Groups": "Device Groups",
|
|
133
|
+
"Min Max Validation Rules": "Min/Max Rules",
|
|
134
|
+
"Regular Expression Validation Rules": "Regex Rules",
|
|
135
|
+
"Required Validation Rules": "Required Rules",
|
|
136
|
+
"Unique Validation Rules": "Unique Rules",
|
|
137
|
+
"Approval Workflow Definitions": "Workflow Definitions",
|
|
138
|
+
"Approval Workflow Stages": "Approval Dashboard",
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
expected = bettertitle(view_model._meta.verbose_name_plural)
|
|
142
|
+
return name_map.get(expected, expected)
|