nautobot 3.0.0a2__py3-none-any.whl → 3.0.0rc1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nautobot/apps/choices.py +4 -2
- nautobot/apps/filters.py +7 -9
- nautobot/apps/models.py +2 -2
- nautobot/apps/ui.py +13 -1
- nautobot/apps/utils.py +8 -0
- nautobot/circuits/filters.py +3 -2
- nautobot/circuits/navigation.py +3 -2
- nautobot/circuits/templates/circuits/circuit_create.html +3 -3
- nautobot/circuits/templates/circuits/circuittermination_create.html +9 -24
- nautobot/circuits/templates/circuits/inc/circuit_termination_cable_fragment.html +6 -6
- nautobot/circuits/templates/circuits/inc/speed_widget.html +12 -12
- nautobot/circuits/tests/integration/test_circuit.py +10 -13
- nautobot/circuits/tests/integration/test_circuits_bulk_operations.py +0 -3
- nautobot/circuits/views.py +6 -2
- nautobot/cloud/filters.py +1 -1
- nautobot/cloud/navigation.py +3 -2
- nautobot/core/api/schema.py +1 -1
- nautobot/core/api/serializers.py +6 -1
- nautobot/core/api/urls.py +2 -0
- nautobot/core/api/views.py +12 -0
- nautobot/core/apps/__init__.py +11 -10
- nautobot/core/celery/__init__.py +3 -5
- nautobot/core/checks.py +46 -0
- nautobot/core/choices.py +1 -1
- nautobot/core/cli/bootstrap_v3_to_v5.py +105 -13
- nautobot/core/cli/migrate_deprecated_templates.py +227 -0
- nautobot/core/constants.py +3 -0
- nautobot/core/context_processors.py +9 -1
- nautobot/core/filters.py +4 -0
- nautobot/core/forms/__init__.py +2 -0
- nautobot/core/forms/forms.py +1 -1
- nautobot/core/forms/widgets.py +21 -2
- nautobot/core/jobs/__init__.py +62 -3
- nautobot/core/jobs/groups.py +31 -1
- nautobot/core/management/commands/generate_test_data.py +28 -9
- nautobot/core/models/__init__.py +11 -0
- nautobot/core/models/generics.py +9 -1
- nautobot/core/models/tree_queries.py +10 -5
- nautobot/core/models/utils.py +1 -1
- nautobot/core/settings.py +35 -19
- nautobot/core/settings.yaml +17 -33
- nautobot/core/signals.py +12 -1
- nautobot/core/tables.py +13 -6
- nautobot/core/templates/40x.html +1 -1
- nautobot/core/templates/500.html +2 -2
- nautobot/core/templates/admin/base.html +1 -2
- nautobot/core/templates/admin/change_list.html +9 -12
- nautobot/core/templates/admin/config/config.html +12 -12
- nautobot/core/templates/admin/index.html +3 -3
- nautobot/core/templates/base_django.html +1 -2
- nautobot/core/templates/buttons/export.html +1 -1
- nautobot/core/templates/components/button/dropdown.html +5 -3
- nautobot/core/templates/components/panel/body_wrapper_generic_table.html +1 -1
- nautobot/core/templates/components/panel/header_extra_content_table.html +1 -1
- nautobot/core/templates/components/panel/panel.html +3 -3
- nautobot/core/templates/components/tab/content_wrapper.html +6 -7
- nautobot/core/templates/components/tab/label_wrapper_distinct_view.html +1 -1
- nautobot/core/templates/echarts/echarts.html +22 -9
- nautobot/core/templates/generic/object_bulk_add_component.html +2 -1
- nautobot/core/templates/generic/object_bulk_create.html +6 -5
- nautobot/core/templates/generic/object_bulk_delete.html +1 -1
- nautobot/core/templates/generic/object_bulk_destroy.html +3 -3
- nautobot/core/templates/generic/object_bulk_edit.html +1 -1
- nautobot/core/templates/generic/object_bulk_import.html +1 -1
- nautobot/core/templates/generic/object_bulk_remove.html +2 -2
- nautobot/core/templates/generic/object_bulk_update.html +5 -4
- nautobot/core/templates/generic/object_create.html +5 -4
- nautobot/core/templates/generic/object_delete.html +1 -1
- nautobot/core/templates/generic/object_detail.html +1 -1
- nautobot/core/templates/generic/object_edit.html +1 -1
- nautobot/core/templates/generic/object_import.html +2 -1
- nautobot/core/templates/generic/object_list.html +12 -4
- nautobot/core/templates/generic/object_notes.html +5 -3
- nautobot/core/templates/generic/object_retrieve.html +4 -5
- nautobot/core/templates/graphene/graphiql.html +7 -8
- nautobot/core/templates/home.html +1 -1
- nautobot/core/templates/import_success.html +2 -1
- nautobot/core/templates/inc/computed_fields/panel_data.html +1 -1
- nautobot/core/templates/inc/created_updated.html +7 -3
- nautobot/core/templates/inc/custom_fields/panel_data.html +1 -1
- nautobot/core/templates/inc/footer.html +3 -1
- nautobot/core/templates/inc/form_static_field.html +6 -0
- nautobot/core/templates/inc/header.html +11 -1
- nautobot/core/templates/inc/image_attachments.html +2 -1
- nautobot/core/templates/inc/media.html +14 -0
- nautobot/core/templates/inc/nav_menu.html +3 -9
- nautobot/core/templates/inc/object_details_advanced_panel.html +2 -2
- nautobot/core/templates/inc/search_panel.html +4 -4
- nautobot/core/templates/login.html +4 -2
- nautobot/core/templates/nautobot_config.py.j2 +6 -11
- nautobot/core/templates/redoc_ui.html +7 -0
- nautobot/core/templates/rest_framework/api.html +103 -2
- nautobot/core/templates/search.html +1 -1
- nautobot/core/templates/swagger_ui.html +17 -3
- nautobot/core/templates/system_jobs/import_objects.html +1 -2
- nautobot/core/templates/utilities/confirmation_form.html +2 -2
- nautobot/core/templates/utilities/obj_table.html +10 -2
- nautobot/core/templates/utilities/render_field.html +7 -7
- nautobot/core/templates/utilities/render_jinja2.html +2 -2
- nautobot/core/templates/utilities/templatetags/filter_form_drawer.html +37 -4
- nautobot/core/templates/utilities/theme_preview.html +19 -3
- nautobot/core/templates/widgets/number_input_with_choices.html +44 -0
- nautobot/core/templates/widgets/selectwithdisabled_option.html +3 -1
- nautobot/core/templatetags/helpers.py +76 -18
- nautobot/core/testing/api.py +68 -9
- nautobot/core/testing/filters.py +0 -23
- nautobot/core/testing/integration.py +41 -17
- nautobot/core/testing/mixins.py +2 -0
- nautobot/core/testing/utils.py +18 -4
- nautobot/core/testing/views.py +104 -13
- nautobot/core/tests/integration/test_app_home.py +34 -30
- nautobot/core/tests/integration/test_app_navbar.py +3 -0
- nautobot/core/tests/integration/test_filters.py +48 -11
- nautobot/core/tests/integration/test_theme.py +22 -21
- nautobot/core/tests/nautobot_config.py +3 -0
- nautobot/core/tests/nautobot_config_without_example_apps.py +4 -0
- nautobot/core/tests/runner.py +8 -1
- nautobot/core/tests/test_api.py +5 -3
- nautobot/core/tests/test_breadcrumbs.py +27 -28
- nautobot/core/tests/test_checks.py +28 -0
- nautobot/core/tests/test_cli.py +40 -0
- nautobot/core/tests/test_config.py +2 -1
- nautobot/core/tests/test_forms.py +55 -13
- nautobot/core/tests/test_jobs.py +144 -3
- nautobot/core/tests/test_nautobot_server.py +2 -0
- nautobot/core/tests/test_navigations.py +76 -1
- nautobot/core/tests/test_patch_social_django.py +42 -0
- nautobot/core/tests/test_renderers.py +59 -0
- nautobot/core/tests/test_settings_schema.py +1 -0
- nautobot/core/tests/test_tables.py +3 -1
- nautobot/core/tests/test_templatetags_helpers.py +62 -13
- nautobot/core/tests/test_templatetags_ui_framework.py +4 -4
- nautobot/core/tests/test_titles.py +0 -16
- nautobot/core/tests/test_tree_queries.py +14 -1
- nautobot/core/tests/test_ui.py +123 -4
- nautobot/core/tests/test_utils.py +72 -5
- nautobot/core/tests/test_views.py +159 -31
- nautobot/core/ui/breadcrumbs.py +70 -29
- nautobot/core/ui/bulk_buttons.py +1 -1
- nautobot/core/ui/choices.py +143 -27
- nautobot/core/ui/constants.py +76 -12
- nautobot/core/ui/echarts.py +15 -20
- nautobot/core/ui/object_detail.py +143 -55
- nautobot/core/ui/titles.py +3 -6
- nautobot/core/urls.py +20 -9
- nautobot/core/utils/cache.py +2 -1
- nautobot/core/utils/filtering.py +28 -18
- nautobot/core/utils/lookup.py +49 -8
- nautobot/core/utils/module_loading.py +21 -0
- nautobot/core/utils/patch_social_django.py +128 -0
- nautobot/core/views/__init__.py +38 -1
- nautobot/core/views/generic.py +3 -3
- nautobot/core/views/mixins.py +45 -22
- nautobot/core/views/renderers.py +4 -3
- nautobot/core/views/viewsets.py +2 -1
- nautobot/data_validation/apps.py +1 -5
- nautobot/data_validation/custom_validators.py +4 -4
- nautobot/data_validation/filters.py +1 -1
- nautobot/data_validation/forms.py +40 -0
- nautobot/data_validation/migrations/0001_initial.py +0 -7
- nautobot/data_validation/migrations/0002_data_migration_from_app.py +3 -14
- nautobot/data_validation/models.py +16 -7
- nautobot/data_validation/navigation.py +8 -1
- nautobot/data_validation/tables.py +12 -5
- nautobot/data_validation/templates/data_validation/datacompliance_tab.html +1 -0
- nautobot/data_validation/templates/data_validation/device_constraints.html +61 -0
- nautobot/data_validation/tests/__init__.py +2 -2
- nautobot/data_validation/tests/migrations/test_migrations.py +83 -3
- nautobot/data_validation/tests/test_data_compliance_rules.py +12 -7
- nautobot/data_validation/tests/test_filters.py +8 -6
- nautobot/data_validation/tests/test_models.py +15 -0
- nautobot/data_validation/tests/test_views.py +190 -32
- nautobot/data_validation/urls.py +2 -5
- nautobot/data_validation/views.py +73 -40
- nautobot/dcim/api/serializers.py +3 -13
- nautobot/dcim/apps.py +4 -0
- nautobot/dcim/choices.py +65 -0
- nautobot/dcim/constants.py +7 -0
- nautobot/dcim/custom_validators.py +84 -0
- nautobot/dcim/factory.py +1 -1
- nautobot/dcim/filter_mixins.py +353 -4
- nautobot/dcim/{filters/__init__.py → filters.py} +15 -36
- nautobot/dcim/forms.py +90 -4
- nautobot/dcim/migrations/0075_interface_duplex_interface_speed_and_more.py +32 -0
- nautobot/dcim/migrations/{0075_add_deviceclusterassignment.py → 0076_add_deviceclusterassignment.py} +1 -1
- nautobot/dcim/migrations/{0076_device_cluster_to_clusters_data_migration.py → 0077_device_cluster_to_clusters_data_migration.py} +1 -1
- nautobot/dcim/migrations/{0077_remove_device_cluster.py → 0078_remove_device_cluster.py} +1 -1
- nautobot/dcim/migrations/0079_remove_device_location_tenant_name_uniqueness.py +16 -0
- nautobot/dcim/migrations/0080_device_name_data_migration.py +59 -0
- nautobot/dcim/migrations/0081_alter_device_device_redundancy_group_priority_and_more.py +25 -0
- nautobot/dcim/models/device_component_templates.py +33 -1
- nautobot/dcim/models/device_components.py +98 -64
- nautobot/dcim/models/devices.py +30 -20
- nautobot/dcim/navigation.py +7 -6
- nautobot/dcim/tables/devices.py +18 -0
- nautobot/dcim/tables/devicetypes.py +8 -1
- nautobot/dcim/tables/racks.py +0 -2
- nautobot/dcim/tables/template_code.py +15 -15
- nautobot/dcim/templates/dcim/cable_connect.html +28 -112
- nautobot/dcim/templates/dcim/cable_trace.html +0 -4
- nautobot/dcim/templates/dcim/{cable_edit.html → cable_update.html} +1 -1
- nautobot/dcim/templates/dcim/consoleport.html +7 -6
- nautobot/dcim/templates/dcim/consoleserverport.html +7 -6
- nautobot/dcim/templates/dcim/device/config.html +2 -2
- nautobot/dcim/templates/dcim/device/lldp_neighbors.html +1 -1
- nautobot/dcim/templates/dcim/device/status.html +8 -8
- nautobot/dcim/templates/dcim/device.html +1 -1
- nautobot/dcim/templates/dcim/device_component_add.html +2 -2
- nautobot/dcim/templates/dcim/device_create.html +5 -3
- nautobot/dcim/templates/dcim/device_interface_delete.html +1 -1
- nautobot/dcim/templates/dcim/device_list.html +73 -10
- nautobot/dcim/templates/dcim/devicebay.html +1 -1
- nautobot/dcim/templates/dcim/devicebay_populate.html +2 -2
- nautobot/dcim/templates/dcim/devicetype_component_add.html +2 -2
- nautobot/dcim/templates/dcim/footer_convert_to_contact_or_team_record.html +14 -0
- nautobot/dcim/templates/dcim/frontport.html +10 -9
- nautobot/dcim/templates/dcim/inc/devicetype_component_table.html +1 -1
- nautobot/dcim/templates/dcim/inc/edit_form_softwareversion_js.html +2 -2
- nautobot/dcim/templates/dcim/inc/moduletype_component_table.html +1 -1
- nautobot/dcim/templates/dcim/inc/rack_elevation.html +1 -1
- nautobot/dcim/templates/dcim/interface.html +35 -7
- nautobot/dcim/templates/dcim/interface_bulk_delete.html +1 -1
- nautobot/dcim/templates/dcim/interface_edit.html +2 -0
- nautobot/dcim/templates/dcim/inventoryitem.html +1 -1
- nautobot/dcim/templates/dcim/inventoryitem_add.html +3 -1
- nautobot/dcim/templates/dcim/inventoryitem_bulk_delete.html +1 -1
- nautobot/dcim/templates/dcim/inventoryitem_edit.html +3 -1
- nautobot/dcim/templates/dcim/module/base.html +49 -9
- nautobot/dcim/templates/dcim/module_consoleports.html +1 -1
- nautobot/dcim/templates/dcim/module_consoleserverports.html +1 -1
- nautobot/dcim/templates/dcim/module_frontports.html +1 -1
- nautobot/dcim/templates/dcim/module_interfaces.html +1 -1
- nautobot/dcim/templates/dcim/module_list.html +57 -8
- nautobot/dcim/templates/dcim/module_modulebays.html +1 -1
- nautobot/dcim/templates/dcim/module_poweroutlets.html +1 -1
- nautobot/dcim/templates/dcim/module_powerports.html +1 -1
- nautobot/dcim/templates/dcim/module_rearports.html +1 -1
- nautobot/dcim/templates/dcim/modulefamily_retrieve.html +1 -1
- nautobot/dcim/templates/dcim/moduletype_list.html +2 -2
- nautobot/dcim/templates/dcim/moduletype_retrieve.html +49 -9
- nautobot/dcim/templates/dcim/platform_create.html +1 -1
- nautobot/dcim/templates/dcim/poweroutlet.html +1 -1
- nautobot/dcim/templates/dcim/powerport.html +6 -5
- nautobot/dcim/templates/dcim/rack_elevation_list.html +17 -5
- nautobot/dcim/templates/dcim/rack_retrieve.html +22 -15
- nautobot/dcim/templates/dcim/rearport.html +8 -7
- nautobot/dcim/templates/dcim/trace/cable.html +1 -1
- nautobot/dcim/templates/dcim/virtualchassis_add_member.html +16 -14
- nautobot/dcim/templates/dcim/virtualchassis_update.html +15 -7
- nautobot/dcim/tests/integration/test_controller.py +4 -6
- nautobot/dcim/tests/integration/test_controller_managed_device_group.py +1 -5
- nautobot/dcim/tests/integration/test_create_device.py +0 -2
- nautobot/dcim/tests/integration/test_device_bulk_operations.py +1 -3
- nautobot/dcim/tests/integration/test_fileinputpicker.py +6 -10
- nautobot/dcim/tests/integration/test_location_bulk_operations.py +0 -2
- nautobot/dcim/tests/integration/test_module_bay_position.py +3 -4
- nautobot/dcim/tests/test_api.py +194 -6
- nautobot/dcim/tests/test_custom_validators.py +229 -0
- nautobot/dcim/tests/test_filters.py +55 -7
- nautobot/dcim/tests/test_forms.py +110 -8
- nautobot/dcim/tests/test_graphql.py +44 -1
- nautobot/dcim/tests/test_models.py +328 -4
- nautobot/dcim/tests/test_tables.py +160 -0
- nautobot/dcim/tests/test_views.py +132 -29
- nautobot/dcim/urls.py +64 -21
- nautobot/dcim/utils.py +3 -3
- nautobot/dcim/views.py +777 -397
- nautobot/extras/api/views.py +60 -45
- nautobot/extras/choices.py +2 -13
- nautobot/extras/datasources/git.py +3 -1
- nautobot/extras/{filters/mixins.py → filter_mixins.py} +1 -1
- nautobot/extras/{filters/customfields.py → filter_mixins_customfields.py} +42 -6
- nautobot/extras/{filters/__init__.py → filters.py} +33 -48
- nautobot/extras/forms/forms.py +14 -15
- nautobot/extras/forms/mixins.py +0 -41
- nautobot/extras/jobs.py +2 -0
- nautobot/extras/jobs_ui.py +4 -3
- nautobot/extras/management/__init__.py +11 -0
- nautobot/extras/management/commands/refresh_dynamic_group_member_caches.py +4 -1
- nautobot/extras/migrations/0127_approval_workflow_models.py +6 -6
- nautobot/extras/migrations/0129_jobresult_debug_log_count_jobresult_error_log_count_and_more.py +37 -0
- nautobot/extras/migrations/0130_jobresult_generate_log_entry_counts.py +42 -0
- nautobot/extras/migrations/0131_configcontext_device_families.py +18 -0
- nautobot/extras/models/__init__.py +1 -2
- nautobot/extras/models/approvals.py +33 -14
- nautobot/extras/models/change_logging.py +4 -0
- nautobot/extras/models/contacts.py +2 -0
- nautobot/extras/models/groups.py +44 -5
- nautobot/extras/models/jobs.py +60 -4
- nautobot/extras/models/mixins.py +28 -0
- nautobot/extras/models/models.py +23 -2
- nautobot/extras/models/secrets.py +1 -0
- nautobot/extras/models/statuses.py +0 -15
- nautobot/extras/navigation.py +13 -9
- nautobot/extras/plugins/__init__.py +33 -55
- nautobot/extras/plugins/marketplace_manifest.yml +49 -1
- nautobot/extras/plugins/tables.py +3 -3
- nautobot/extras/plugins/urls.py +2 -21
- nautobot/extras/plugins/utils.py +1 -33
- nautobot/extras/plugins/views.py +0 -9
- nautobot/extras/querysets.py +8 -0
- nautobot/extras/signals.py +20 -19
- nautobot/extras/tables.py +64 -68
- nautobot/extras/templates/django_ajax_tables/ajax_wrapper.html +2 -0
- nautobot/extras/templates/extras/approval_dashboard.html +7 -5
- nautobot/extras/templates/extras/approvalworkflowdefinition_update.html +4 -2
- nautobot/extras/templates/extras/approvalworkflowstage_retrieve.html +20 -12
- nautobot/extras/templates/extras/configcontext_update.html +1 -0
- nautobot/extras/templates/extras/configcontextschema_validation.html +2 -2
- nautobot/extras/templates/extras/dynamicgroup_retrieve.html +11 -5
- nautobot/extras/templates/extras/dynamicgroup_update.html +1 -1
- nautobot/extras/templates/extras/gitrepository_result.html +0 -2
- nautobot/extras/templates/extras/inc/approval_buttons_column.html +20 -6
- nautobot/extras/templates/extras/inc/bulk_edit_overridable_field.html +8 -7
- nautobot/extras/templates/extras/inc/configcontext_format.html +10 -3
- nautobot/extras/templates/extras/inc/graphqlquery_execute.html +71 -0
- nautobot/extras/templates/extras/inc/job_tiles.html +15 -3
- nautobot/extras/templates/extras/inc/json_format.html +10 -3
- nautobot/extras/templates/extras/inc/overridable_field.html +13 -12
- nautobot/extras/templates/extras/job.html +29 -12
- nautobot/extras/templates/extras/job_bulk_edit.html +18 -0
- nautobot/extras/templates/extras/job_edit.html +52 -46
- nautobot/extras/templates/extras/job_list.html +29 -25
- nautobot/extras/templates/extras/marketplace.html +5 -9
- nautobot/extras/templates/extras/object_configcontext.html +1 -1
- nautobot/extras/templates/extras/object_dynamicgroups.html +2 -2
- nautobot/extras/templates/extras/objectchange_retrieve.html +19 -39
- nautobot/extras/templates/extras/plugin_detail.html +29 -24
- nautobot/extras/templates/extras/plugins_list.html +16 -26
- nautobot/extras/templates/extras/role_retrieve.html +64 -0
- nautobot/extras/templates/extras/scheduledjob.html +4 -2
- nautobot/extras/templates/extras/secret_create.html +1 -1
- nautobot/extras/templatetags/custom_links.py +12 -12
- nautobot/extras/templatetags/job_buttons.py +14 -12
- nautobot/extras/test_jobs/invalid_import.py +9 -0
- nautobot/extras/test_jobs/log_counts_by_level.py +23 -0
- nautobot/extras/test_jobs/missing_import.py +11 -0
- nautobot/extras/tests/integration/test_computedfields.py +8 -9
- nautobot/extras/tests/integration/test_configcontextschema.py +27 -26
- nautobot/extras/tests/integration/test_customfields.py +9 -10
- nautobot/extras/tests/integration/test_dynamicgroups.py +12 -9
- nautobot/extras/tests/integration/test_plugin_banner.py +3 -0
- nautobot/extras/tests/integration/test_plugins.py +18 -6
- nautobot/extras/tests/integration/test_relationships.py +0 -2
- nautobot/extras/tests/test_api.py +90 -18
- nautobot/extras/tests/test_approvals.py +38 -38
- nautobot/extras/tests/test_changelog.py +59 -5
- nautobot/extras/tests/test_customfields.py +22 -13
- nautobot/extras/tests/test_customfields_filters.py +479 -0
- nautobot/extras/tests/test_dynamicgroups.py +39 -1
- nautobot/extras/tests/test_filters.py +57 -22
- nautobot/extras/tests/test_forms.py +18 -21
- nautobot/extras/tests/test_jobs.py +25 -4
- nautobot/extras/tests/test_migrations.py +1 -0
- nautobot/extras/tests/test_models.py +51 -33
- nautobot/extras/tests/test_plugins.py +36 -10
- nautobot/extras/tests/test_utils.py +3 -4
- nautobot/extras/tests/test_views.py +52 -112
- nautobot/extras/urls.py +0 -14
- nautobot/extras/views.py +164 -71
- nautobot/ipam/factory.py +7 -0
- nautobot/ipam/filter_mixins.py +38 -0
- nautobot/ipam/filters.py +53 -38
- nautobot/ipam/formfields.py +1 -1
- nautobot/ipam/forms.py +6 -3
- nautobot/ipam/migrations/0030_ipam__namespaces.py +13 -0
- nautobot/ipam/migrations/0031_ipam___data_migrations.py +4 -1
- nautobot/ipam/migrations/0054_namespace_tenant.py +25 -0
- nautobot/ipam/models.py +29 -2
- nautobot/ipam/navigation.py +3 -2
- nautobot/ipam/signals.py +71 -0
- nautobot/ipam/tables.py +19 -6
- nautobot/ipam/templates/ipam/inc/toggle_available.html +10 -10
- nautobot/ipam/templates/ipam/inc/vlangroup_header.html +1 -0
- nautobot/ipam/templates/ipam/ipaddress.html +14 -0
- nautobot/ipam/templates/ipam/ipaddress_merge.html +3 -3
- nautobot/ipam/templates/ipam/ipaddresstointerface_retrieve.html +1 -0
- nautobot/ipam/templates/ipam/namespace_ip_addresses.html +1 -1
- nautobot/ipam/templates/ipam/namespace_prefixes.html +1 -1
- nautobot/ipam/templates/ipam/namespace_update.html +15 -0
- nautobot/ipam/templates/ipam/namespace_vrfs.html +1 -1
- nautobot/ipam/templates/ipam/prefix_delete.html +1 -1
- nautobot/ipam/templates/ipam/prefix_list.html +14 -13
- nautobot/ipam/templates/ipam/vlan_interfaces.html +1 -1
- nautobot/ipam/templates/ipam/vlan_vminterfaces.html +1 -1
- nautobot/ipam/tests/migration/test_migrations.py +89 -0
- nautobot/ipam/tests/test_api.py +13 -6
- nautobot/ipam/tests/test_filters.py +36 -1
- nautobot/ipam/tests/test_forms.py +1 -1
- nautobot/ipam/tests/test_models.py +44 -2
- nautobot/ipam/tests/test_tables.py +1 -2
- nautobot/ipam/tests/test_utils.py +1 -1
- nautobot/ipam/tests/test_views.py +13 -14
- nautobot/ipam/ui.py +0 -17
- nautobot/ipam/utils/migrations.py +16 -2
- nautobot/ipam/utils/testing.py +9 -3
- nautobot/ipam/views.py +53 -11
- nautobot/load_balancers/__init__.py +0 -0
- nautobot/load_balancers/api/__init__.py +1 -0
- nautobot/load_balancers/api/serializers.py +75 -0
- nautobot/load_balancers/api/urls.py +23 -0
- nautobot/load_balancers/api/views.py +61 -0
- nautobot/load_balancers/apps.py +17 -0
- nautobot/load_balancers/choices.py +167 -0
- nautobot/load_balancers/filters.py +225 -0
- nautobot/load_balancers/forms.py +532 -0
- nautobot/load_balancers/management/commands/__init__.py +0 -0
- nautobot/load_balancers/management/commands/generate_load_balancer_models_test_data.py +38 -0
- nautobot/load_balancers/migrations/0001_initial.py +465 -0
- nautobot/load_balancers/migrations/0002_create_default_statuses_pool_members.py +31 -0
- nautobot/load_balancers/migrations/__init__.py +0 -0
- nautobot/load_balancers/models.py +423 -0
- nautobot/load_balancers/navigation.py +80 -0
- nautobot/load_balancers/tables.py +255 -0
- nautobot/load_balancers/tests/__init__.py +474 -0
- nautobot/load_balancers/tests/test_api.py +353 -0
- nautobot/load_balancers/tests/test_filters.py +134 -0
- nautobot/load_balancers/tests/test_forms.py +266 -0
- nautobot/load_balancers/tests/test_models.py +195 -0
- nautobot/load_balancers/tests/test_views.py +229 -0
- nautobot/load_balancers/urls.py +17 -0
- nautobot/load_balancers/views.py +248 -0
- nautobot/project-static/dist/css/github-dark.min.css +10 -0
- nautobot/project-static/dist/css/github.min.css +10 -0
- nautobot/project-static/dist/css/nautobot.css +1 -11
- nautobot/project-static/dist/css/nautobot.css.map +1 -1
- nautobot/project-static/dist/js/libraries.js +1 -1
- nautobot/project-static/dist/js/libraries.js.map +1 -1
- nautobot/project-static/dist/js/nautobot.js +1 -1
- nautobot/project-static/dist/js/nautobot.js.map +1 -1
- nautobot/project-static/js/cabletrace.js +1 -1
- nautobot/project-static/js/forms.js +13 -0
- nautobot/project-static/js/interface_filtering.js +20 -16
- nautobot/project-static/nautobot-icons/battery-3.svg +3 -0
- nautobot/project-static/nautobot-icons/bus-globe.svg +3 -0
- nautobot/project-static/nautobot-icons/bus-shield-check.svg +3 -0
- nautobot/project-static/nautobot-icons/bus-shield.svg +3 -0
- nautobot/project-static/nautobot-icons/cloud.svg +1 -1
- nautobot/project-static/nautobot-icons/control-panel.svg +1 -1
- nautobot/project-static/nautobot-icons/device-lifecycle.svg +1 -1
- nautobot/project-static/nautobot-icons/elements.svg +1 -1
- nautobot/project-static/nautobot-icons/extensibility.svg +3 -0
- nautobot/project-static/nautobot-icons/hammer.svg +1 -1
- nautobot/project-static/nautobot-icons/organization.svg +3 -0
- nautobot/project-static/nautobot-icons/secrets.svg +1 -1
- nautobot/project-static/nautobot-icons/security.svg +3 -0
- nautobot/project-static/nautobot-icons/server.svg +1 -1
- nautobot/project-static/nautobot-icons/star-filled.svg +1 -1
- nautobot/project-static/nautobot-icons/star.svg +1 -1
- nautobot/tenancy/api/serializers.py +1 -0
- nautobot/tenancy/api/views.py +2 -1
- nautobot/tenancy/{filters/__init__.py → filters.py} +2 -10
- nautobot/tenancy/navigation.py +3 -1
- nautobot/tenancy/tests/test_filters.py +0 -2
- nautobot/tenancy/views.py +2 -1
- nautobot/ui/package-lock.json +87 -4
- nautobot/ui/package.json +2 -1
- nautobot/ui/src/js/collapse.js +3 -3
- nautobot/ui/src/js/nautobot.js +16 -1
- nautobot/ui/src/js/select2.js +53 -2
- nautobot/ui/src/scss/colors.scss +1 -1
- nautobot/ui/src/scss/nautobot.scss +112 -30
- nautobot/ui/webpack.config.js +13 -0
- nautobot/users/templates/users/preferences.html +11 -2
- nautobot/users/templates/users/profile.html +45 -12
- nautobot/users/templates/users/sessionkey_delete.html +1 -1
- nautobot/users/tests/test_api.py +4 -0
- nautobot/users/views.py +4 -2
- nautobot/virtualization/filters.py +6 -1
- nautobot/virtualization/models.py +1 -68
- nautobot/virtualization/navigation.py +3 -2
- nautobot/virtualization/templates/virtualization/virtual_machine_vminterface_delete.html +1 -1
- nautobot/virtualization/templates/virtualization/virtualmachine_list.html +2 -2
- nautobot/virtualization/templates/virtualization/virtualmachine_update.html +3 -1
- nautobot/virtualization/tests/test_api.py +3 -0
- nautobot/virtualization/tests/test_filters.py +10 -1
- nautobot/virtualization/tests/test_models.py +45 -4
- nautobot/virtualization/views.py +4 -1
- nautobot/vpn/__init__.py +0 -0
- nautobot/vpn/api/serializers.py +113 -0
- nautobot/vpn/api/urls.py +19 -0
- nautobot/vpn/api/views.py +70 -0
- nautobot/vpn/apps.py +8 -0
- nautobot/vpn/choices.py +171 -0
- nautobot/vpn/factory.py +219 -0
- nautobot/vpn/filters.py +234 -0
- nautobot/vpn/forms.py +487 -0
- nautobot/vpn/homepage.py +19 -0
- nautobot/vpn/migrations/0001_initial.py +541 -0
- nautobot/vpn/migrations/0002_populate_defaults.py +199 -0
- nautobot/vpn/migrations/__init__.py +0 -0
- nautobot/vpn/models.py +535 -0
- nautobot/vpn/navigation.py +98 -0
- nautobot/vpn/tables.py +383 -0
- nautobot/vpn/templates/vpn/vpnprofile_create.html +150 -0
- nautobot/vpn/tests/__init__.py +0 -0
- nautobot/vpn/tests/test_api.py +336 -0
- nautobot/vpn/tests/test_filters.py +139 -0
- nautobot/vpn/tests/test_forms.py +293 -0
- nautobot/vpn/tests/test_models.py +147 -0
- nautobot/vpn/tests/test_views.py +300 -0
- nautobot/vpn/urls.py +16 -0
- nautobot/vpn/views.py +495 -0
- nautobot/wireless/navigation.py +3 -2
- nautobot/wireless/tests/integration/test_radio_profile.py +1 -5
- nautobot/wireless/tests/test_api.py +1 -1
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/METADATA +15 -15
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/RECORD +514 -572
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/entry_points.txt +1 -0
- nautobot/circuits/templates/circuits/circuit.html +0 -2
- nautobot/circuits/templates/circuits/circuit_edit.html +0 -2
- nautobot/circuits/templates/circuits/circuit_retrieve.html +0 -2
- nautobot/circuits/templates/circuits/circuit_update.html +0 -1
- nautobot/circuits/templates/circuits/circuittermination.html +0 -2
- nautobot/circuits/templates/circuits/circuittermination_edit.html +0 -2
- nautobot/circuits/templates/circuits/circuittermination_retrieve.html +0 -2
- nautobot/circuits/templates/circuits/circuittermination_update.html +0 -1
- nautobot/circuits/templates/circuits/circuittype.html +0 -2
- nautobot/circuits/templates/circuits/circuittype_retrieve.html +0 -2
- nautobot/circuits/templates/circuits/inc/circuit_termination.html +0 -85
- nautobot/circuits/templates/circuits/provider.html +0 -2
- nautobot/circuits/templates/circuits/provider_edit.html +0 -2
- nautobot/circuits/templates/circuits/provider_retrieve.html +0 -1
- nautobot/circuits/templates/circuits/provider_update.html +0 -1
- nautobot/circuits/templates/circuits/providernetwork.html +0 -2
- nautobot/circuits/templates/circuits/providernetwork_retrieve.html +0 -2
- nautobot/cloud/templates/cloud/cloudaccount_retrieve.html +0 -2
- nautobot/cloud/templates/cloud/cloudnetwork_retrieve.html +0 -2
- nautobot/cloud/templates/cloud/cloudresourcetype_retrieve.html +0 -2
- nautobot/cloud/templates/cloud/cloudservice_retrieve.html +0 -2
- nautobot/core/templates/buttons/import.html +0 -9
- nautobot/data_validation/template_content.py +0 -42
- nautobot/data_validation/templates/data_validation/datacompliance_retrieve.html +0 -1
- nautobot/dcim/filters/mixins.py +0 -354
- nautobot/dcim/templates/dcim/controller/base.html +0 -2
- nautobot/dcim/templates/dcim/controller_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/controller_wirelessnetworks.html +0 -2
- nautobot/dcim/templates/dcim/controllermanageddevicegroup_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/device/base.html +0 -2
- nautobot/dcim/templates/dcim/device/consoleports.html +0 -2
- nautobot/dcim/templates/dcim/device/consoleserverports.html +0 -2
- nautobot/dcim/templates/dcim/device/devicebays.html +0 -2
- nautobot/dcim/templates/dcim/device/frontports.html +0 -2
- nautobot/dcim/templates/dcim/device/interfaces.html +0 -2
- nautobot/dcim/templates/dcim/device/inventory.html +0 -2
- nautobot/dcim/templates/dcim/device/modulebays.html +0 -2
- nautobot/dcim/templates/dcim/device/poweroutlets.html +0 -2
- nautobot/dcim/templates/dcim/device/powerports.html +0 -2
- nautobot/dcim/templates/dcim/device/rearports.html +0 -2
- nautobot/dcim/templates/dcim/device/wireless.html +0 -2
- nautobot/dcim/templates/dcim/device_component.html +0 -2
- nautobot/dcim/templates/dcim/device_edit.html +0 -2
- nautobot/dcim/templates/dcim/devicefamily_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/deviceredundancygroup_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/devicetype.html +0 -2
- nautobot/dcim/templates/dcim/devicetype_edit.html +0 -2
- nautobot/dcim/templates/dcim/devicetype_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/inc/device_napalm_tabs.html +0 -1
- nautobot/dcim/templates/dcim/interfaceredundancygroup_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/location.html +0 -2
- nautobot/dcim/templates/dcim/location_edit.html +0 -2
- nautobot/dcim/templates/dcim/location_retrieve.html +0 -243
- nautobot/dcim/templates/dcim/locationtype.html +0 -2
- nautobot/dcim/templates/dcim/locationtype_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/manufacturer.html +0 -2
- nautobot/dcim/templates/dcim/modulebay_retrieve.html +0 -1
- nautobot/dcim/templates/dcim/platform.html +0 -2
- nautobot/dcim/templates/dcim/powerfeed.html +0 -2
- nautobot/dcim/templates/dcim/powerfeed_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/powerpanel.html +0 -2
- nautobot/dcim/templates/dcim/powerpanel_edit.html +0 -2
- nautobot/dcim/templates/dcim/powerpanel_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/rack.html +0 -2
- nautobot/dcim/templates/dcim/rack_edit.html +0 -2
- nautobot/dcim/templates/dcim/rackgroup.html +0 -2
- nautobot/dcim/templates/dcim/rackreservation.html +0 -2
- nautobot/dcim/templates/dcim/softwareimagefile_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/softwareversion_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/virtualchassis.html +0 -2
- nautobot/dcim/templates/dcim/virtualchassis_add.html +0 -2
- nautobot/dcim/templates/dcim/virtualchassis_edit.html +0 -2
- nautobot/dcim/templates/dcim/virtualchassis_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/virtualdevicecontext_retrieve.html +0 -2
- nautobot/dcim/ui.py +0 -29
- nautobot/extras/templates/extras/computedfield.html +0 -2
- nautobot/extras/templates/extras/computedfield_retrieve.html +0 -2
- nautobot/extras/templates/extras/configcontext.html +0 -2
- nautobot/extras/templates/extras/configcontext_edit.html +0 -2
- nautobot/extras/templates/extras/configcontext_retrieve.html +0 -2
- nautobot/extras/templates/extras/configcontextschema.html +0 -2
- nautobot/extras/templates/extras/configcontextschema_edit.html +0 -2
- nautobot/extras/templates/extras/contact_retrieve.html +0 -2
- nautobot/extras/templates/extras/customfield.html +0 -2
- nautobot/extras/templates/extras/customfield_edit.html +0 -2
- nautobot/extras/templates/extras/customfield_retrieve.html +0 -2
- nautobot/extras/templates/extras/customlink.html +0 -2
- nautobot/extras/templates/extras/dynamicgroup.html +0 -2
- nautobot/extras/templates/extras/dynamicgroup_edit.html +0 -2
- nautobot/extras/templates/extras/exporttemplate.html +0 -2
- nautobot/extras/templates/extras/gitrepository.html +0 -2
- nautobot/extras/templates/extras/gitrepository_object_edit.html +0 -2
- nautobot/extras/templates/extras/graphqlquery.html +0 -2
- nautobot/extras/templates/extras/graphqlquery_list.html +0 -1
- nautobot/extras/templates/extras/graphqlquery_retrieve.html +0 -97
- nautobot/extras/templates/extras/job_detail.html +0 -2
- nautobot/extras/templates/extras/jobbutton_retrieve.html +0 -2
- nautobot/extras/templates/extras/jobhook.html +0 -2
- nautobot/extras/templates/extras/jobqueue_retrieve.html +0 -2
- nautobot/extras/templates/extras/jobresult.html +0 -2
- nautobot/extras/templates/extras/metadatatype_retrieve.html +0 -2
- nautobot/extras/templates/extras/note.html +0 -2
- nautobot/extras/templates/extras/note_retrieve.html +0 -1
- nautobot/extras/templates/extras/object_changelog.html +0 -2
- nautobot/extras/templates/extras/object_notes.html +0 -2
- nautobot/extras/templates/extras/objectchange.html +0 -2
- nautobot/extras/templates/extras/objectchange_list.html +0 -3
- nautobot/extras/templates/extras/relationship.html +0 -1
- nautobot/extras/templates/extras/secret.html +0 -1
- nautobot/extras/templates/extras/secret_edit.html +0 -1
- nautobot/extras/templates/extras/secretsgroup.html +0 -2
- nautobot/extras/templates/extras/secretsgroup_edit.html +0 -2
- nautobot/extras/templates/extras/secretsgroup_retrieve.html +0 -2
- nautobot/extras/templates/extras/status.html +0 -2
- nautobot/extras/templates/extras/tag.html +0 -2
- nautobot/extras/templates/extras/tag_edit.html +0 -2
- nautobot/extras/templates/extras/tag_retrieve.html +0 -2
- nautobot/extras/templates/extras/team_retrieve.html +0 -2
- nautobot/ipam/templates/ipam/inc/prefix_header_extra_content_table.html +0 -4
- nautobot/ipam/templates/ipam/namespace_retrieve.html +0 -1
- nautobot/ipam/templates/ipam/prefix.html +0 -2
- nautobot/ipam/templates/ipam/prefix_edit.html +0 -1
- nautobot/ipam/templates/ipam/prefix_retrieve.html +0 -2
- nautobot/ipam/templates/ipam/rir.html +0 -2
- nautobot/ipam/templates/ipam/routetarget.html +0 -1
- nautobot/ipam/templates/ipam/service.html +0 -2
- nautobot/ipam/templates/ipam/service_edit.html +0 -2
- nautobot/ipam/templates/ipam/service_retrieve.html +0 -2
- nautobot/ipam/templates/ipam/vlan.html +0 -2
- nautobot/ipam/templates/ipam/vlan_edit.html +0 -2
- nautobot/ipam/templates/ipam/vlan_retrieve.html +0 -2
- nautobot/ipam/templates/ipam/vlangroup.html +0 -2
- nautobot/ipam/templates/ipam/vrf.html +0 -1
- nautobot/tenancy/templates/tenancy/tenant.html +0 -2
- nautobot/tenancy/templates/tenancy/tenant_edit.html +0 -2
- nautobot/tenancy/templates/tenancy/tenantgroup.html +0 -2
- nautobot/tenancy/templates/tenancy/tenantgroup_retrieve.html +0 -1
- nautobot/virtualization/templates/virtualization/clustergroup.html +0 -2
- nautobot/virtualization/templates/virtualization/clustertype.html +0 -2
- nautobot/virtualization/templates/virtualization/virtualmachine.html +0 -2
- nautobot/virtualization/templates/virtualization/virtualmachine_edit.html +0 -2
- nautobot/virtualization/templates/virtualization/virtualmachine_retrieve.html +0 -2
- nautobot/wireless/templates/wireless/radioprofile_retrieve.html +0 -2
- nautobot/wireless/templates/wireless/supporteddatarate_retrieve.html +0 -2
- nautobot/wireless/templates/wireless/wirelessnetwork_retrieve.html +0 -2
- /nautobot/dcim/templates/dcim/{cable.html → cable_retrieve.html} +0 -0
- /nautobot/tenancy/{filters/mixins.py → filter_mixins.py} +0 -0
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/LICENSE.txt +0 -0
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/NOTICE +0 -0
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/WHEEL +0 -0
nautobot/core/tests/test_ui.py
CHANGED
|
@@ -32,7 +32,9 @@ from nautobot.core.ui.object_detail import (
|
|
|
32
32
|
SectionChoices,
|
|
33
33
|
)
|
|
34
34
|
from nautobot.dcim.models import Device, DeviceRedundancyGroup, Location
|
|
35
|
+
from nautobot.dcim.tables import DeviceModuleInterfaceTable
|
|
35
36
|
from nautobot.dcim.tables.devices import DeviceTable
|
|
37
|
+
from nautobot.dcim.views import DeviceUIViewSet
|
|
36
38
|
from nautobot.ipam.models import Prefix
|
|
37
39
|
|
|
38
40
|
|
|
@@ -355,9 +357,80 @@ class EChartsBaseTests(TestCase):
|
|
|
355
357
|
self.assertEqual(config["series"][0]["name"], "S1")
|
|
356
358
|
self.assertEqual(config["series"][1]["name"], "S2")
|
|
357
359
|
|
|
358
|
-
def
|
|
359
|
-
|
|
360
|
-
|
|
360
|
+
def test_get_config_combined_charts_with_complex_data(self):
|
|
361
|
+
chart2 = EChartsBase(
|
|
362
|
+
chart_type=EChartsTypeChoices.LINE,
|
|
363
|
+
data={
|
|
364
|
+
"Compliant": {"aaa1": 5, "dns1": 12, "ntp1": 8},
|
|
365
|
+
"Non Compliant": {"aaa1": 10, "dns1": 20, "ntp1": 15},
|
|
366
|
+
},
|
|
367
|
+
)
|
|
368
|
+
chart1 = EChartsBase(
|
|
369
|
+
chart_type=EChartsTypeChoices.BAR,
|
|
370
|
+
data={
|
|
371
|
+
"Compliant": {"aaa": 5, "dns": 12, "ntp": 8},
|
|
372
|
+
"Non Compliant": {"aaa": 10, "dns": 20, "ntp": 15},
|
|
373
|
+
},
|
|
374
|
+
combined_with=chart2,
|
|
375
|
+
)
|
|
376
|
+
config = chart1.get_config()
|
|
377
|
+
self.assertEqual(len(config["series"]), 4)
|
|
378
|
+
|
|
379
|
+
self.assertEqual(config["series"][0], {"name": "Compliant", "type": "bar", "data": [5, 12, 8]})
|
|
380
|
+
self.assertEqual(config["series"][1], {"name": "Non Compliant", "type": "bar", "data": [10, 20, 15]})
|
|
381
|
+
self.assertEqual(
|
|
382
|
+
config["series"][2],
|
|
383
|
+
{"name": "Compliant", "type": "line", "data": [5, 12, 8], "smooth": False, "lineStyle": {}},
|
|
384
|
+
)
|
|
385
|
+
self.assertEqual(
|
|
386
|
+
config["series"][3],
|
|
387
|
+
{"name": "Non Compliant", "type": "line", "data": [10, 20, 15], "smooth": False, "lineStyle": {}},
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
def test_get_config_with_context_callable_and_combined_chart(self):
|
|
391
|
+
def main_data(ctx):
|
|
392
|
+
return {
|
|
393
|
+
"Compliant": {"aaa1": ctx.get("aaa1_count", 0), "dns1": 13, "ntp1": 8},
|
|
394
|
+
"Non Compliant": {"aaa1": 10, "dns1": 20, "ntp1": 15},
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
def combined_data(ctx):
|
|
398
|
+
return {
|
|
399
|
+
"Compliant": {"aaa": 5, "dns": ctx.get("dns_count", 0), "ntp1": 8},
|
|
400
|
+
"Non Compliant": {"aaa": 10, "dns": 20, "ntp1": 15},
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
ctx = Context({"aaa1_count": 5, "dns_count": 12})
|
|
404
|
+
combined_chart = EChartsBase(chart_type=EChartsTypeChoices.LINE, data=combined_data)
|
|
405
|
+
main_chart = EChartsBase(
|
|
406
|
+
chart_type=EChartsTypeChoices.BAR,
|
|
407
|
+
data=main_data,
|
|
408
|
+
combined_with=combined_chart,
|
|
409
|
+
)
|
|
410
|
+
config = main_chart.get_config(context=ctx)
|
|
411
|
+
self.assertEqual(len(config["series"]), 4)
|
|
412
|
+
|
|
413
|
+
self.assertEqual(config["series"][0]["data"][0], 5) # aaa1_count
|
|
414
|
+
self.assertEqual(config["series"][2]["data"][1], 12) # dns_count
|
|
415
|
+
|
|
416
|
+
def test_get_config_with_context_callable(self):
|
|
417
|
+
def dynamic_data(ctx):
|
|
418
|
+
return {"data": {"Devices": ctx.get("device_count", 0)}}
|
|
419
|
+
|
|
420
|
+
ctx = Context({"device_count": 42})
|
|
421
|
+
chart = EChartsBase(chart_type=EChartsTypeChoices.PIE, data=dynamic_data)
|
|
422
|
+
config = chart.get_config(context=ctx)
|
|
423
|
+
# check that after getting config data doesn't change it's still dynami_data function
|
|
424
|
+
self.assertEqual(chart.data, dynamic_data)
|
|
425
|
+
self.assertEqual(config["series"][0]["data"][0]["value"], 42)
|
|
426
|
+
ctx = Context({"device_count": 45})
|
|
427
|
+
config = chart.get_config(context=ctx)
|
|
428
|
+
self.assertEqual(config["series"][0]["data"][0]["value"], 45)
|
|
429
|
+
|
|
430
|
+
def test_get_config_with_context_ignored_when_data_is_not_callable(self):
|
|
431
|
+
ctx = Context({"some": "value"})
|
|
432
|
+
chart = EChartsBase(data=self.data_normalized)
|
|
433
|
+
config = chart.get_config(context=ctx)
|
|
361
434
|
self.assertEqual(config["series"][0]["data"], [1, 2])
|
|
362
435
|
|
|
363
436
|
|
|
@@ -446,7 +519,7 @@ class ObjectDetailContentExtraTabsTest(TestCase):
|
|
|
446
519
|
self.factory = RequestFactory()
|
|
447
520
|
self.request = self.factory.get("/")
|
|
448
521
|
self.request.user = self.user
|
|
449
|
-
self.default_tabs_id = ["main", "advanced", "contacts", "dynamic_groups", "object_metadata"]
|
|
522
|
+
self.default_tabs_id = ["main", "advanced", "contacts", "dynamic_groups", "object_metadata", "data_compliance"]
|
|
450
523
|
|
|
451
524
|
def test_default_extra_tabs_exist(self):
|
|
452
525
|
"""
|
|
@@ -492,6 +565,52 @@ class ObjectDetailContentExtraTabsTest(TestCase):
|
|
|
492
565
|
self.default_tabs_id.append("services")
|
|
493
566
|
self.assertListEqual(tab_ids, self.default_tabs_id)
|
|
494
567
|
|
|
568
|
+
def test_tab_id_url_as_action(self):
|
|
569
|
+
"""
|
|
570
|
+
Test that when you create a panel with a tab_id that matches a viewset action,
|
|
571
|
+
the return_url is constructed correctly.
|
|
572
|
+
"""
|
|
573
|
+
self.add_permissions("dcim.add_interface", "dcim.change_interface")
|
|
574
|
+
device_info = Device.objects.first()
|
|
575
|
+
|
|
576
|
+
panel = DeviceUIViewSet.DeviceInterfacesTablePanel(
|
|
577
|
+
weight=100,
|
|
578
|
+
section=SectionChoices.FULL_WIDTH,
|
|
579
|
+
table_title="Interfaces",
|
|
580
|
+
table_class=DeviceModuleInterfaceTable,
|
|
581
|
+
table_attribute="vc_interfaces",
|
|
582
|
+
related_field_name="device",
|
|
583
|
+
tab_id="interfaces",
|
|
584
|
+
)
|
|
585
|
+
context = {"request": self.request, "object": device_info}
|
|
586
|
+
panel_context = panel.get_extra_context(context)
|
|
587
|
+
|
|
588
|
+
return_url = f"/dcim/devices/{device_info.pk}/interfaces/"
|
|
589
|
+
self.assertTrue(panel_context["body_content_table_add_url"].endswith(return_url))
|
|
590
|
+
|
|
591
|
+
def test_tab_id_url_as_param(self):
|
|
592
|
+
"""
|
|
593
|
+
Test that when you create a panel with a tab_id that does NOT matches a viewset action,
|
|
594
|
+
the return_url is constructed correctly.
|
|
595
|
+
"""
|
|
596
|
+
self.add_permissions("dcim.add_interface", "dcim.change_interface")
|
|
597
|
+
device_info = Device.objects.first()
|
|
598
|
+
|
|
599
|
+
panel = DeviceUIViewSet.DeviceInterfacesTablePanel(
|
|
600
|
+
weight=100,
|
|
601
|
+
section=SectionChoices.FULL_WIDTH,
|
|
602
|
+
table_title="Interfaces",
|
|
603
|
+
table_class=DeviceModuleInterfaceTable,
|
|
604
|
+
table_attribute="vc_interfaces",
|
|
605
|
+
related_field_name="device",
|
|
606
|
+
tab_id="interfaces-not-exist",
|
|
607
|
+
)
|
|
608
|
+
context = {"request": self.request, "object": device_info}
|
|
609
|
+
panel_context = panel.get_extra_context(context)
|
|
610
|
+
|
|
611
|
+
return_url = f"&return_url=/dcim/devices/{device_info.pk}/?tab=interfaces-not-exist"
|
|
612
|
+
self.assertTrue(panel_context["body_content_table_add_url"].endswith(return_url))
|
|
613
|
+
|
|
495
614
|
def test_extra_tab_panel_context(self):
|
|
496
615
|
"""
|
|
497
616
|
Confirming that extra tab panels produce the correct context,
|
|
@@ -9,7 +9,7 @@ from django.contrib.contenttypes.models import ContentType
|
|
|
9
9
|
from django.core.exceptions import ValidationError
|
|
10
10
|
from django.db.models import Q
|
|
11
11
|
from django.http import QueryDict
|
|
12
|
-
from django.test import override_settings
|
|
12
|
+
from django.test import override_settings, tag
|
|
13
13
|
|
|
14
14
|
from nautobot.circuits import models as circuits_models
|
|
15
15
|
from nautobot.core import exceptions, forms, settings_funcs
|
|
@@ -20,9 +20,15 @@ from nautobot.core.testing import TestCase
|
|
|
20
20
|
from nautobot.core.utils import data as data_utils, filtering, lookup, querysets, requests
|
|
21
21
|
from nautobot.core.utils.cache import construct_cache_key
|
|
22
22
|
from nautobot.core.utils.migrations import update_object_change_ct_for_replaced_models
|
|
23
|
-
from nautobot.core.utils.module_loading import check_name_safe_to_import_privately
|
|
23
|
+
from nautobot.core.utils.module_loading import check_name_safe_to_import_privately, import_string_optional
|
|
24
24
|
from nautobot.data_validation import models as data_validation_models
|
|
25
|
-
from nautobot.dcim import
|
|
25
|
+
from nautobot.dcim import (
|
|
26
|
+
filters as dcim_filters,
|
|
27
|
+
forms as dcim_forms,
|
|
28
|
+
models as dcim_models,
|
|
29
|
+
tables,
|
|
30
|
+
views as dcim_views,
|
|
31
|
+
)
|
|
26
32
|
from nautobot.extras import models as extras_models, utils as extras_utils
|
|
27
33
|
from nautobot.extras.choices import ObjectChangeActionChoices, RelationshipTypeChoices
|
|
28
34
|
from nautobot.extras.filters import StatusFilterSet
|
|
@@ -30,8 +36,6 @@ from nautobot.extras.forms import StatusForm
|
|
|
30
36
|
from nautobot.extras.models import ObjectChange
|
|
31
37
|
from nautobot.ipam import models as ipam_models
|
|
32
38
|
|
|
33
|
-
from example_app.models import ExampleModel
|
|
34
|
-
|
|
35
39
|
|
|
36
40
|
class ConstructCacheKeyTest(TestCase):
|
|
37
41
|
"""
|
|
@@ -331,6 +335,26 @@ class GetFooForModelTest(TestCase):
|
|
|
331
335
|
instance = extras_models.GraphQLQuery.objects.create(name="FizzBuzz", query="{devices { name }}")
|
|
332
336
|
self.assertIsNone(lookup.get_user_from_instance(instance))
|
|
333
337
|
|
|
338
|
+
def test_get_breadcrumbs_for_model(self):
|
|
339
|
+
breadcrumbs = lookup.get_breadcrumbs_for_model(dcim_models.Device)
|
|
340
|
+
self.assertEqual(breadcrumbs.items, dcim_views.DeviceUIViewSet.get_breadcrumbs(dcim_models.Device).items)
|
|
341
|
+
breadcrumbs = lookup.get_breadcrumbs_for_model(dcim_models.Device, view_type="")
|
|
342
|
+
self.assertEqual(
|
|
343
|
+
breadcrumbs.items, dcim_views.DeviceUIViewSet.get_breadcrumbs(dcim_models.Device, view_type="").items
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
def test_get_detail_view_components_context_for_model(self):
|
|
347
|
+
context = lookup.get_detail_view_components_context_for_model(dcim_models.Device)
|
|
348
|
+
self.assertEqual(
|
|
349
|
+
context["breadcrumbs"].items, lookup.get_breadcrumbs_for_model(dcim_models.Device, view_type="").items
|
|
350
|
+
)
|
|
351
|
+
self.assertEqual(
|
|
352
|
+
context["object_detail_content"], lookup.get_object_detail_content_for_model(dcim_models.Device)
|
|
353
|
+
)
|
|
354
|
+
self.assertEqual(
|
|
355
|
+
context["view_titles"].titles, lookup.get_view_titles_for_model(dcim_models.Device, view_type="").titles
|
|
356
|
+
)
|
|
357
|
+
|
|
334
358
|
def test_get_filterset_for_model(self):
|
|
335
359
|
"""
|
|
336
360
|
Test that `get_filterset_for_model` returns the right FilterSet for various inputs.
|
|
@@ -353,6 +377,12 @@ class GetFooForModelTest(TestCase):
|
|
|
353
377
|
self.assertEqual(lookup.get_form_for_model("dcim.location"), dcim_forms.LocationForm)
|
|
354
378
|
self.assertEqual(lookup.get_form_for_model(dcim_models.Location), dcim_forms.LocationForm)
|
|
355
379
|
|
|
380
|
+
def test_get_object_detail_content_for_model(self):
|
|
381
|
+
self.assertEqual(
|
|
382
|
+
lookup.get_object_detail_content_for_model(dcim_models.Device),
|
|
383
|
+
dcim_views.DeviceUIViewSet.object_detail_content,
|
|
384
|
+
)
|
|
385
|
+
|
|
356
386
|
def test_get_related_field_for_models(self):
|
|
357
387
|
"""
|
|
358
388
|
Test that `get_related_field_for_models` returns the appropriate field for various inputs.
|
|
@@ -372,10 +402,13 @@ class GetFooForModelTest(TestCase):
|
|
|
372
402
|
# both primary_ip4 and primary_ip6 are candidates
|
|
373
403
|
lookup.get_related_field_for_models(dcim_models.Device, ipam_models.IPAddress)
|
|
374
404
|
|
|
405
|
+
@tag("example_app")
|
|
375
406
|
def test_get_route_for_model(self):
|
|
376
407
|
"""
|
|
377
408
|
Test that `get_route_for_model` returns the appropriate URL route name for various inputs.
|
|
378
409
|
"""
|
|
410
|
+
from example_app.models import ExampleModel
|
|
411
|
+
|
|
379
412
|
# UI
|
|
380
413
|
self.assertEqual(lookup.get_route_for_model("dcim.device", "list"), "dcim:device_list")
|
|
381
414
|
self.assertEqual(lookup.get_route_for_model(dcim_models.Device, "list"), "dcim:device_list")
|
|
@@ -423,10 +456,13 @@ class GetFooForModelTest(TestCase):
|
|
|
423
456
|
self.assertEqual(lookup.get_model_from_name("dcim.device"), dcim_models.Device)
|
|
424
457
|
self.assertEqual(lookup.get_model_from_name("dcim.location"), dcim_models.Location)
|
|
425
458
|
|
|
459
|
+
@tag("example_app")
|
|
426
460
|
def test_get_model_for_view_name(self):
|
|
427
461
|
"""
|
|
428
462
|
Test that `get_model_for_view_name` returns the appropriate Model, if the colon separated view name provided.
|
|
429
463
|
"""
|
|
464
|
+
from example_app.models import ExampleModel
|
|
465
|
+
|
|
430
466
|
with self.subTest("Test core UI view."):
|
|
431
467
|
self.assertEqual(lookup.get_model_for_view_name("dcim:device_list"), dcim_models.Device)
|
|
432
468
|
self.assertEqual(lookup.get_model_for_view_name("dcim:device"), dcim_models.Device)
|
|
@@ -459,6 +495,14 @@ class GetFooForModelTest(TestCase):
|
|
|
459
495
|
# Testing unconventional table name
|
|
460
496
|
self.assertEqual(lookup.get_table_class_string_from_view_name("ipam:prefix_list"), "PrefixDetailTable")
|
|
461
497
|
|
|
498
|
+
def test_get_view_titles_for_model(self):
|
|
499
|
+
view_titles = lookup.get_view_titles_for_model(dcim_models.Device)
|
|
500
|
+
self.assertEqual(view_titles.titles, dcim_views.DeviceUIViewSet.get_view_titles(dcim_models.Device).titles)
|
|
501
|
+
view_titles = lookup.get_view_titles_for_model(dcim_models.Device, view_type="")
|
|
502
|
+
self.assertEqual(
|
|
503
|
+
view_titles.titles, dcim_views.DeviceUIViewSet.get_view_titles(dcim_models.Device, view_type="").titles
|
|
504
|
+
)
|
|
505
|
+
|
|
462
506
|
|
|
463
507
|
class IsTaggableTest(TestCase):
|
|
464
508
|
def test_is_taggable_true(self):
|
|
@@ -1128,6 +1172,29 @@ class TestModuleLoadingUtils(TestCase):
|
|
|
1128
1172
|
self.assertFalse(permitted)
|
|
1129
1173
|
self.assertIsInstance(reason, str)
|
|
1130
1174
|
|
|
1175
|
+
def test_import_string_optional(self):
|
|
1176
|
+
with self.subTest("Nonexistent module should return None"):
|
|
1177
|
+
self.assertIsNone(import_string_optional("no_such_module.no_such_attribute"))
|
|
1178
|
+
self.assertIsNone(import_string_optional("no_such_module.no_such_submodule.no_such_attribute"))
|
|
1179
|
+
self.assertIsNone(import_string_optional("nautobot.no_such_submodule.no_such_attribute"))
|
|
1180
|
+
self.assertIsNone(import_string_optional("nautobot.core.no_such_submodule.no_such_attribute"))
|
|
1181
|
+
|
|
1182
|
+
with self.subTest("Existing module but nonexistent attribute should return None"):
|
|
1183
|
+
self.assertIsNone(import_string_optional("nautobot.core.no_such_attribute"))
|
|
1184
|
+
self.assertIsNone(import_string_optional("nautobot.core.no_such_attribute"))
|
|
1185
|
+
self.assertIsNone(import_string_optional("sys.no_such_attribute"))
|
|
1186
|
+
|
|
1187
|
+
with self.subTest("Other import errors should propagate upward still"):
|
|
1188
|
+
with self.assertRaises(ImportError):
|
|
1189
|
+
import_string_optional("nautobot.extras.test_jobs.invalid_import.MyJob")
|
|
1190
|
+
with self.assertRaises(ImportError):
|
|
1191
|
+
import_string_optional("nautobot.extras.test_jobs.missing_import.MyJob")
|
|
1192
|
+
|
|
1193
|
+
with self.subTest("Successful imports should succeed"):
|
|
1194
|
+
self.assertEqual(
|
|
1195
|
+
import_string_optional("nautobot.core.tests.test_utils.TestModuleLoadingUtils"), self.__class__
|
|
1196
|
+
)
|
|
1197
|
+
|
|
1131
1198
|
|
|
1132
1199
|
class TestQuerySetUtils(TestCase):
|
|
1133
1200
|
def test_maybe_select_related(self):
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import os
|
|
3
|
+
from pathlib import Path
|
|
3
4
|
import re
|
|
4
5
|
import tempfile
|
|
5
|
-
from unittest import mock
|
|
6
|
+
from unittest import mock
|
|
6
7
|
import urllib.parse
|
|
7
8
|
|
|
8
9
|
from django.apps import apps
|
|
9
|
-
from django.conf import settings
|
|
10
10
|
from django.contrib.contenttypes.models import ContentType
|
|
11
11
|
from django.core.cache import cache
|
|
12
12
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
|
@@ -110,38 +110,25 @@ class HomeViewTestCase(TestCase):
|
|
|
110
110
|
|
|
111
111
|
# Search bar in header
|
|
112
112
|
header_search_bar_pattern = re.compile(
|
|
113
|
-
'<header.*<form action="/search/" class="col text-center"
|
|
113
|
+
'<header.*<form action="/search/" class="col-4 text-center" id="header_search" method="get" role="search">.*</form>.*</header>'
|
|
114
114
|
)
|
|
115
115
|
header_search_bar_result = header_search_bar_pattern.search(
|
|
116
116
|
response.content.decode(response.charset).replace("\n", "")
|
|
117
117
|
)
|
|
118
118
|
|
|
119
|
-
|
|
120
|
-
body_search_bar_pattern = re.compile(
|
|
121
|
-
'<div class="container-fluid wrapper" id="main-content">.*<form action="/search/" method="get" class="form-inline">.*</form>.*</div>',
|
|
122
|
-
re.DOTALL,
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
body_search_bar_result = body_search_bar_pattern.search(
|
|
126
|
-
response.content.decode(response.charset).replace("\n", "")
|
|
127
|
-
)
|
|
128
|
-
|
|
129
|
-
return header_search_bar_result, body_search_bar_result
|
|
119
|
+
return header_search_bar_result
|
|
130
120
|
|
|
131
121
|
def test_search_bar_not_visible_if_user_not_authenticated(self):
|
|
132
122
|
self.client.logout()
|
|
133
123
|
|
|
134
|
-
header_search_bar_result
|
|
124
|
+
header_search_bar_result = self.make_request()
|
|
135
125
|
|
|
136
126
|
self.assertIsNone(header_search_bar_result)
|
|
137
|
-
self.assertIsNone(body_search_bar_result)
|
|
138
127
|
|
|
139
|
-
@tag("fix_in_v3")
|
|
140
128
|
def test_search_bar_visible_if_user_authenticated(self):
|
|
141
|
-
header_search_bar_result
|
|
129
|
+
header_search_bar_result = self.make_request()
|
|
142
130
|
|
|
143
131
|
self.assertIsNotNone(header_search_bar_result)
|
|
144
|
-
self.assertIsNotNone(body_search_bar_result)
|
|
145
132
|
|
|
146
133
|
@override_settings(VERSION="1.2.3")
|
|
147
134
|
def test_footer_version_visible_authenticated_users_only(self):
|
|
@@ -190,6 +177,132 @@ class HomeViewTestCase(TestCase):
|
|
|
190
177
|
self.assertNotIn("Welcome to Nautobot!", response.content.decode(response.charset))
|
|
191
178
|
|
|
192
179
|
|
|
180
|
+
class AppDocsViewTestCase(TestCase):
|
|
181
|
+
def setUp(self):
|
|
182
|
+
super().setUp()
|
|
183
|
+
self.test_app_label = "test_app"
|
|
184
|
+
self.test_base_url = "test-app"
|
|
185
|
+
|
|
186
|
+
# Create temp docs dir
|
|
187
|
+
# I use tearDown to clean up, so this is save
|
|
188
|
+
self.temp_dir = tempfile.TemporaryDirectory() # pylint: disable=consider-using-with
|
|
189
|
+
self.docs_path = Path(self.temp_dir.name) / "docs"
|
|
190
|
+
self.docs_path.mkdir(parents=True)
|
|
191
|
+
(self.docs_path / "index.html").write_text("<html>Test Index</html>")
|
|
192
|
+
(self.docs_path / "css/style.css").parent.mkdir(parents=True, exist_ok=True)
|
|
193
|
+
(self.docs_path / "css/style.css").write_text("body { background: #fff; }")
|
|
194
|
+
|
|
195
|
+
def tearDown(self):
|
|
196
|
+
self.temp_dir.cleanup()
|
|
197
|
+
super().tearDown()
|
|
198
|
+
|
|
199
|
+
def test_docs_index_redirect(self):
|
|
200
|
+
"""Ensure /docs/<base_url>/ redirects to /docs/<base_url>/index.html."""
|
|
201
|
+
url = reverse("docs_index_redirect", kwargs={"app_base_url": self.test_base_url})
|
|
202
|
+
response = self.client.get(url, follow=False)
|
|
203
|
+
self.assertEqual(response.status_code, 302)
|
|
204
|
+
self.assertEqual(response["Location"], f"/docs/{self.test_base_url}/index.html")
|
|
205
|
+
|
|
206
|
+
def test_docs_index_redirect_if_not_logged_in(self):
|
|
207
|
+
self.client.logout()
|
|
208
|
+
url = reverse("docs_index_redirect", kwargs={"app_base_url": self.test_base_url})
|
|
209
|
+
response = self.client.get(url, follow=False)
|
|
210
|
+
|
|
211
|
+
# First, the redirect to /docs/<base_url>/index.html
|
|
212
|
+
self.assertEqual(response.status_code, 302)
|
|
213
|
+
redirect_url = f"/docs/{self.test_base_url}/index.html"
|
|
214
|
+
self.assertEqual(response["Location"], redirect_url)
|
|
215
|
+
|
|
216
|
+
# Follow the redirect to AppDocsView, which should require login
|
|
217
|
+
response = self.client.get(redirect_url)
|
|
218
|
+
self.assertRedirects(
|
|
219
|
+
response,
|
|
220
|
+
expected_url=f"{reverse('login')}?next={redirect_url}",
|
|
221
|
+
status_code=302,
|
|
222
|
+
target_status_code=200,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
def test_docs_file_redirect_if_not_logged_in(self):
|
|
226
|
+
self.client.logout()
|
|
227
|
+
url = reverse("docs_file", kwargs={"app_base_url": self.test_base_url, "path": "css/style.css"})
|
|
228
|
+
response = self.client.get(url)
|
|
229
|
+
# LoginRequiredMixin redirects to /accounts/login/
|
|
230
|
+
self.assertRedirects(
|
|
231
|
+
response,
|
|
232
|
+
expected_url=f"{reverse('login')}?next={url}",
|
|
233
|
+
status_code=302,
|
|
234
|
+
target_status_code=200,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
@mock.patch.dict("nautobot.core.views.BASE_URL_TO_APP_LABEL", {"test-app": "test_app"})
|
|
238
|
+
@mock.patch("nautobot.core.views.resources.files")
|
|
239
|
+
def test_access_denied_path_traversal_attempts(self, mock_resources_files):
|
|
240
|
+
"""Ensure ../ or similar traversal patterns are rejected."""
|
|
241
|
+
mock_resources_files.return_value = Path(self.temp_dir.name)
|
|
242
|
+
|
|
243
|
+
malicious_paths = [
|
|
244
|
+
"../settings.py",
|
|
245
|
+
"../../etc/passwd",
|
|
246
|
+
"docs/../../secret.txt",
|
|
247
|
+
]
|
|
248
|
+
|
|
249
|
+
for path in malicious_paths:
|
|
250
|
+
with self.subTest(path=path):
|
|
251
|
+
url = reverse("docs_file", kwargs={"app_base_url": self.test_base_url, "path": path})
|
|
252
|
+
response = self.client.get(url)
|
|
253
|
+
self.assertEqual(response.status_code, 403)
|
|
254
|
+
|
|
255
|
+
@mock.patch.dict("nautobot.core.views.BASE_URL_TO_APP_LABEL", {"test-app": "test_app"})
|
|
256
|
+
@mock.patch("nautobot.core.views.resources.files")
|
|
257
|
+
def test_serve_index_html_logged_in(self, mock_resources_files):
|
|
258
|
+
mock_resources_files.return_value = Path(self.temp_dir.name)
|
|
259
|
+
url = reverse("docs_index_redirect", kwargs={"app_base_url": self.test_base_url, "path": "index.html"})
|
|
260
|
+
response = self.client.get(url, follow=True)
|
|
261
|
+
self.assertEqual(response.status_code, 200)
|
|
262
|
+
self.assertContains(response, "Test Index")
|
|
263
|
+
self.assertEqual(response["Content-Type"], "text/html")
|
|
264
|
+
|
|
265
|
+
@mock.patch.dict("nautobot.core.views.BASE_URL_TO_APP_LABEL", {"test-app": "test_app"})
|
|
266
|
+
@mock.patch("nautobot.core.views.resources.files")
|
|
267
|
+
def test_serve_css_logged_in(self, mock_resources_files):
|
|
268
|
+
mock_resources_files.return_value = Path(self.temp_dir.name)
|
|
269
|
+
url = reverse("docs_file", kwargs={"app_base_url": self.test_base_url, "path": "css/style.css"})
|
|
270
|
+
response = self.client.get(url)
|
|
271
|
+
self.assertEqual(response.status_code, 200)
|
|
272
|
+
self.assertContains(response, "background: #fff;")
|
|
273
|
+
self.assertEqual(response["Content-Type"], "text/css")
|
|
274
|
+
|
|
275
|
+
@mock.patch.dict("nautobot.core.views.BASE_URL_TO_APP_LABEL", {"test-app": "test_app"})
|
|
276
|
+
@mock.patch("nautobot.core.views.resources.files")
|
|
277
|
+
def test_docs_index_nonexistent_app(self, mock_resources_files):
|
|
278
|
+
mock_resources_files.return_value = Path(self.temp_dir.name)
|
|
279
|
+
url = reverse("docs_index_redirect", kwargs={"app_base_url": "nonexistent-app"})
|
|
280
|
+
response = self.client.get(url, follow=True)
|
|
281
|
+
self.assertEqual(response.status_code, 404)
|
|
282
|
+
self.assertJSONEqual(response.content, {"detail": "Unknown base_url 'nonexistent-app'."})
|
|
283
|
+
|
|
284
|
+
@mock.patch.dict("nautobot.core.views.BASE_URL_TO_APP_LABEL", {"test-app": "test_app"})
|
|
285
|
+
@mock.patch("nautobot.core.views.resources.files")
|
|
286
|
+
def test_docs_file_nonexistent_app(self, mock_resources_files):
|
|
287
|
+
mock_resources_files.return_value = Path(self.temp_dir.name)
|
|
288
|
+
url = reverse("docs_file", kwargs={"app_base_url": "nonexistent-app", "path": "css/style.css"})
|
|
289
|
+
response = self.client.get(url)
|
|
290
|
+
self.assertEqual(response.status_code, 404)
|
|
291
|
+
self.assertJSONEqual(response.content, {"detail": "Unknown base_url 'nonexistent-app'."})
|
|
292
|
+
|
|
293
|
+
@mock.patch.dict("nautobot.core.views.BASE_URL_TO_APP_LABEL", {"test-app": "test_app"})
|
|
294
|
+
@mock.patch("nautobot.core.views.resources.files")
|
|
295
|
+
def test_nonexistent_file(self, mock_resources_files):
|
|
296
|
+
mock_resources_files.return_value = Path(self.temp_dir.name)
|
|
297
|
+
test_cases = ["/../missing.html", "//../missing.html", "missing.html", "missing_dir/missing.html"]
|
|
298
|
+
for path in test_cases:
|
|
299
|
+
with self.subTest(path=path):
|
|
300
|
+
url = reverse("docs_file", kwargs={"app_base_url": self.test_base_url, "path": path})
|
|
301
|
+
response = self.client.get(url)
|
|
302
|
+
self.assertEqual(response.status_code, 404)
|
|
303
|
+
self.assertIn("File", response.json()["detail"])
|
|
304
|
+
|
|
305
|
+
|
|
193
306
|
class MediaViewTestCase(TestCase):
|
|
194
307
|
def test_media_unauthenticated(self):
|
|
195
308
|
"""
|
|
@@ -270,29 +383,45 @@ class SearchFieldsTestCase(TestCase):
|
|
|
270
383
|
# SearchForm will redirect the user to the login Page
|
|
271
384
|
self.assertEqual(response.status_code, 302)
|
|
272
385
|
|
|
273
|
-
|
|
274
|
-
def test_global_and_model_search_bar(self):
|
|
386
|
+
def test_global_search_bar_scoped_to_model(self):
|
|
275
387
|
self.add_permissions("dcim.view_location", "dcim.view_device")
|
|
276
388
|
|
|
277
389
|
# Assert model search bar present in list UI
|
|
278
390
|
response = self.client.get(reverse("dcim:location_list"))
|
|
279
391
|
self.assertBodyContains(
|
|
280
392
|
response,
|
|
281
|
-
'<input
|
|
393
|
+
'<input aria-placeholder="Press Ctrl+K to search" class="form-control nb-text-transparent" name="q" type="search" value="">',
|
|
394
|
+
html=True,
|
|
395
|
+
)
|
|
396
|
+
self.assertBodyContains(
|
|
397
|
+
response,
|
|
398
|
+
"""
|
|
399
|
+
<span class="badge border" data-nb-link="/dcim/locations/"><!--
|
|
400
|
+
-->in: Locations<!--
|
|
401
|
+
--><button tabindex="-1" type="button">
|
|
402
|
+
<span aria-hidden="true" class="mdi mdi-close"></span>
|
|
403
|
+
<span class="visually-hidden">Remove</span>
|
|
404
|
+
</button>
|
|
405
|
+
""",
|
|
282
406
|
html=True,
|
|
283
407
|
)
|
|
284
408
|
|
|
285
409
|
response = self.client.get(reverse("dcim:device_list"))
|
|
286
410
|
self.assertBodyContains(
|
|
287
411
|
response,
|
|
288
|
-
'<input
|
|
412
|
+
'<input aria-placeholder="Press Ctrl+K to search" class="form-control nb-text-transparent" name="q" type="search" value="">',
|
|
289
413
|
html=True,
|
|
290
414
|
)
|
|
291
|
-
|
|
292
|
-
# Assert global search bar present in UI
|
|
293
|
-
self.assertContains( # not using assertBodyContains because this is in the nav
|
|
415
|
+
self.assertBodyContains(
|
|
294
416
|
response,
|
|
295
|
-
|
|
417
|
+
"""
|
|
418
|
+
<span class="badge border" data-nb-link="/dcim/devices/"><!--
|
|
419
|
+
-->in: Devices<!--
|
|
420
|
+
--><button tabindex="-1" type="button">
|
|
421
|
+
<span aria-hidden="true" class="mdi mdi-close"></span>
|
|
422
|
+
<span class="visually-hidden">Remove</span>
|
|
423
|
+
</button>
|
|
424
|
+
""",
|
|
296
425
|
html=True,
|
|
297
426
|
)
|
|
298
427
|
|
|
@@ -560,6 +689,7 @@ class MetricsViewTestCase(TestCase):
|
|
|
560
689
|
page_content = response.content.decode(response.charset)
|
|
561
690
|
return text_string_to_metric_families(page_content)
|
|
562
691
|
|
|
692
|
+
@tag("example_app")
|
|
563
693
|
def test_metrics_extensibility(self):
|
|
564
694
|
"""Assert that the example metric from the Example App shows up _exactly_ when the app is enabled."""
|
|
565
695
|
test_metric_name = "nautobot_example_metric_count"
|
|
@@ -582,6 +712,7 @@ class MetricsViewTestCase(TestCase):
|
|
|
582
712
|
self.query_and_parse_metrics()
|
|
583
713
|
self.assertTrue(mock_generate_latest_with_cache.call_count == 0)
|
|
584
714
|
|
|
715
|
+
@tag("example_app")
|
|
585
716
|
@override_settings(METRICS_EXPERIMENTAL_CACHING_DURATION=30)
|
|
586
717
|
def test_enabled_metrics_cache_enabled(self):
|
|
587
718
|
"""Assert that multiple calls to metrics with caching returns expected response."""
|
|
@@ -762,10 +893,7 @@ class SilkUIAccessTestCase(TestCase):
|
|
|
762
893
|
|
|
763
894
|
|
|
764
895
|
class ExampleViewWithCustomPermissionsTest(TestCase):
|
|
765
|
-
@
|
|
766
|
-
"example_app" not in settings.PLUGINS,
|
|
767
|
-
"example_app not in settings.PLUGINS",
|
|
768
|
-
)
|
|
896
|
+
@tag("example_app")
|
|
769
897
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
|
770
898
|
def test_permission_classes_attribute_is_enforced(self):
|
|
771
899
|
"""
|