nautobot 3.0.0a2__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 +0 -2
- nautobot/apps/filters.py +7 -9
- nautobot/apps/models.py +2 -2
- nautobot/apps/ui.py +9 -1
- nautobot/circuits/filters.py +3 -2
- nautobot/circuits/navigation.py +3 -2
- nautobot/circuits/templates/circuits/circuit.html +1 -1
- nautobot/circuits/templates/circuits/circuit_create.html +3 -3
- nautobot/circuits/templates/circuits/circuittermination.html +1 -1
- nautobot/circuits/templates/circuits/circuittermination_create.html +9 -24
- nautobot/circuits/templates/circuits/circuittype.html +1 -1
- nautobot/circuits/templates/circuits/inc/circuit_termination_cable_fragment.html +6 -6
- nautobot/circuits/templates/circuits/inc/speed_widget.html +12 -12
- nautobot/circuits/templates/circuits/providernetwork.html +1 -1
- nautobot/circuits/tests/integration/test_circuit.py +10 -13
- 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 +1 -0
- nautobot/core/api/views.py +8 -0
- nautobot/core/apps/__init__.py +11 -10
- nautobot/core/celery/__init__.py +3 -5
- nautobot/core/checks.py +46 -0
- nautobot/core/cli/bootstrap_v3_to_v5.py +70 -1
- nautobot/core/cli/migrate_deprecated_templates.py +200 -0
- nautobot/core/constants.py +3 -0
- nautobot/core/context_processors.py +9 -1
- nautobot/core/forms/forms.py +1 -1
- nautobot/core/jobs/__init__.py +6 -3
- nautobot/core/jobs/groups.py +31 -1
- nautobot/core/management/commands/generate_test_data.py +28 -9
- nautobot/core/models/generics.py +9 -1
- nautobot/core/models/tree_queries.py +10 -5
- nautobot/core/settings.py +18 -12
- nautobot/core/settings.yaml +13 -7
- 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/config/config.html +12 -12
- nautobot/core/templates/admin/index.html +3 -3
- 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/panel.html +3 -3
- nautobot/core/templates/components/tab/content_wrapper.html +2 -3
- nautobot/core/templates/components/tab/label_wrapper_distinct_view.html +1 -1
- nautobot/core/templates/echarts/echarts.html +1 -1
- nautobot/core/templates/generic/object_bulk_add_component.html +2 -1
- nautobot/core/templates/generic/object_bulk_create.html +4 -3
- nautobot/core/templates/generic/object_bulk_destroy.html +3 -3
- 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_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 +2 -3
- nautobot/core/templates/graphene/graphiql.html +7 -7
- 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/form_static_field.html +6 -0
- nautobot/core/templates/inc/header.html +1 -1
- nautobot/core/templates/inc/image_attachments.html +2 -1
- nautobot/core/templates/inc/nav_menu.html +2 -1
- nautobot/core/templates/inc/search_panel.html +4 -4
- nautobot/core/templates/login.html +4 -2
- nautobot/core/templates/nautobot_config.py.j2 +6 -5
- nautobot/core/templates/redoc_ui.html +7 -0
- 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 +4 -4
- nautobot/core/templates/utilities/theme_preview.html +16 -3
- nautobot/core/templates/widgets/selectwithdisabled_option.html +3 -1
- nautobot/core/templatetags/helpers.py +52 -6
- nautobot/core/testing/api.py +68 -9
- nautobot/core/testing/filters.py +0 -23
- nautobot/core/testing/integration.py +23 -10
- nautobot/core/testing/mixins.py +2 -0
- nautobot/core/testing/views.py +4 -0
- nautobot/core/tests/integration/test_app_home.py +34 -30
- nautobot/core/tests/integration/test_app_navbar.py +3 -0
- nautobot/core/tests/nautobot_config_without_example_apps.py +4 -0
- nautobot/core/tests/runner.py +9 -1
- nautobot/core/tests/test_api.py +5 -3
- nautobot/core/tests/test_breadcrumbs.py +6 -7
- 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 +75 -1
- 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_tables.py +3 -1
- nautobot/core/tests/test_templatetags_helpers.py +53 -13
- nautobot/core/tests/test_templatetags_ui_framework.py +4 -4
- nautobot/core/tests/test_tree_queries.py +14 -1
- nautobot/core/tests/test_ui.py +1 -1
- nautobot/core/tests/test_utils.py +31 -4
- nautobot/core/tests/test_views.py +159 -31
- nautobot/core/ui/breadcrumbs.py +2 -12
- nautobot/core/ui/choices.py +142 -10
- nautobot/core/ui/constants.py +76 -12
- nautobot/core/ui/object_detail.py +92 -12
- nautobot/core/urls.py +12 -1
- nautobot/core/utils/cache.py +2 -1
- nautobot/core/utils/filtering.py +17 -17
- nautobot/core/utils/lookup.py +3 -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 +15 -3
- nautobot/core/views/renderers.py +2 -0
- 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 +0 -12
- 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 +0 -13
- 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} +2 -35
- nautobot/dcim/forms.py +1 -1
- 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/device_components.py +81 -68
- nautobot/dcim/models/devices.py +13 -16
- nautobot/dcim/navigation.py +7 -6
- nautobot/dcim/tables/devices.py +3 -0
- nautobot/dcim/tables/template_code.py +14 -14
- nautobot/dcim/templates/dcim/cable.html +2 -61
- nautobot/dcim/templates/dcim/cable_connect.html +28 -112
- nautobot/dcim/templates/dcim/cable_edit.html +2 -5
- nautobot/dcim/templates/dcim/cable_retrieve.html +61 -0
- nautobot/dcim/templates/dcim/cable_trace.html +1 -3
- nautobot/dcim/templates/dcim/cable_update.html +5 -0
- nautobot/dcim/templates/dcim/consoleport.html +6 -5
- nautobot/dcim/templates/dcim/consoleserverport.html +6 -5
- nautobot/dcim/templates/dcim/device/config.html +2 -2
- nautobot/dcim/templates/dcim/device/consoleports.html +1 -1
- nautobot/dcim/templates/dcim/device/consoleserverports.html +1 -1
- nautobot/dcim/templates/dcim/device/devicebays.html +1 -1
- nautobot/dcim/templates/dcim/device/frontports.html +1 -1
- nautobot/dcim/templates/dcim/device/interfaces.html +1 -1
- nautobot/dcim/templates/dcim/device/inventory.html +1 -1
- nautobot/dcim/templates/dcim/device/lldp_neighbors.html +1 -1
- nautobot/dcim/templates/dcim/device/modulebays.html +1 -1
- nautobot/dcim/templates/dcim/device/poweroutlets.html +1 -1
- nautobot/dcim/templates/dcim/device/powerports.html +1 -1
- nautobot/dcim/templates/dcim/device/rearports.html +1 -1
- nautobot/dcim/templates/dcim/device/status.html +8 -8
- nautobot/dcim/templates/dcim/device/wireless.html +1 -1
- 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_populate.html +2 -2
- nautobot/dcim/templates/dcim/devicetype.html +1 -1
- 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 +9 -8
- nautobot/dcim/templates/dcim/inc/edit_form_softwareversion_js.html +2 -2
- nautobot/dcim/templates/dcim/interface.html +26 -6
- nautobot/dcim/templates/dcim/interface_bulk_delete.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/location_retrieve.html +1 -242
- nautobot/dcim/templates/dcim/module/base.html +49 -9
- nautobot/dcim/templates/dcim/module_list.html +57 -8
- nautobot/dcim/templates/dcim/modulefamily_retrieve.html +1 -1
- nautobot/dcim/templates/dcim/moduletype_retrieve.html +49 -9
- nautobot/dcim/templates/dcim/platform_create.html +1 -1
- nautobot/dcim/templates/dcim/powerfeed.html +1 -1
- nautobot/dcim/templates/dcim/powerpanel.html +1 -1
- nautobot/dcim/templates/dcim/powerport.html +5 -4
- nautobot/dcim/templates/dcim/rack_elevation_list.html +16 -4
- nautobot/dcim/templates/dcim/rack_retrieve.html +33 -15
- nautobot/dcim/templates/dcim/rearport.html +7 -6
- nautobot/dcim/templates/dcim/virtualchassis.html +1 -1
- nautobot/dcim/templates/dcim/virtualchassis_add_member.html +16 -14
- nautobot/dcim/templates/dcim/virtualchassis_update.html +14 -6
- nautobot/dcim/tests/integration/test_controller.py +1 -0
- nautobot/dcim/tests/test_api.py +8 -0
- nautobot/dcim/tests/test_custom_validators.py +229 -0
- nautobot/dcim/tests/test_filters.py +12 -6
- nautobot/dcim/tests/test_models.py +63 -4
- nautobot/dcim/tests/test_views.py +63 -22
- nautobot/dcim/urls.py +64 -21
- nautobot/dcim/utils.py +3 -3
- nautobot/dcim/views.py +547 -273
- nautobot/extras/api/views.py +9 -1
- nautobot/extras/choices.py +2 -13
- 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} +14 -46
- nautobot/extras/forms/forms.py +5 -13
- nautobot/extras/forms/mixins.py +0 -41
- nautobot/extras/management/__init__.py +9 -0
- 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/models/__init__.py +1 -2
- nautobot/extras/models/approvals.py +22 -13
- nautobot/extras/models/contacts.py +2 -0
- nautobot/extras/models/groups.py +44 -5
- nautobot/extras/models/jobs.py +59 -1
- nautobot/extras/models/mixins.py +28 -0
- nautobot/extras/models/models.py +13 -0
- 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/tables.py +3 -3
- nautobot/extras/plugins/urls.py +2 -21
- nautobot/extras/plugins/utils.py +1 -33
- nautobot/extras/plugins/views.py +0 -4
- nautobot/extras/signals.py +20 -19
- nautobot/extras/tables.py +52 -68
- 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/computedfield.html +1 -1
- nautobot/extras/templates/extras/configcontext.html +1 -1
- nautobot/extras/templates/extras/configcontextschema_validation.html +2 -2
- nautobot/extras/templates/extras/customfield.html +1 -1
- 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/graphqlquery_retrieve.html +1 -96
- 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 -37
- nautobot/extras/templates/extras/plugin_detail.html +26 -21
- 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/secretsgroup.html +1 -1
- nautobot/extras/templates/extras/tag.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_configcontextschema.py +27 -26
- nautobot/extras/tests/integration/test_customfields.py +8 -7
- nautobot/extras/tests/integration/test_dynamicgroups.py +5 -1
- nautobot/extras/tests/integration/test_plugin_banner.py +3 -0
- nautobot/extras/tests/integration/test_plugins.py +18 -6
- nautobot/extras/tests/test_api.py +27 -18
- nautobot/extras/tests/test_approvals.py +38 -38
- nautobot/extras/tests/test_changelog.py +35 -3
- 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 +21 -19
- 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 +13 -31
- nautobot/extras/tests/test_plugins.py +36 -10
- nautobot/extras/tests/test_views.py +31 -30
- nautobot/extras/views.py +81 -19
- nautobot/ipam/factory.py +7 -0
- nautobot/ipam/filter_mixins.py +38 -0
- nautobot/ipam/filters.py +27 -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 +13 -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_update.html +15 -0
- nautobot/ipam/templates/ipam/prefix_delete.html +1 -1
- nautobot/ipam/templates/ipam/prefix_list.html +14 -13
- nautobot/ipam/templates/ipam/service.html +1 -1
- nautobot/ipam/templates/ipam/vlan.html +1 -1
- 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 +10 -0
- nautobot/ipam/tests/test_forms.py +1 -1
- nautobot/ipam/tests/test_models.py +43 -1
- 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 +46 -6
- nautobot/project-static/dist/css/nautobot.css +1 -1
- nautobot/project-static/dist/css/nautobot.css.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/interface_filtering.js +20 -16
- nautobot/project-static/nautobot-icons/battery-3.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/src/js/collapse.js +3 -3
- nautobot/ui/src/js/nautobot.js +16 -0
- nautobot/ui/src/scss/colors.scss +1 -1
- nautobot/ui/src/scss/nautobot.scss +61 -28
- 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/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.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_models.py +44 -4
- 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/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.0a3.dist-info}/METADATA +14 -14
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/RECORD +417 -366
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/entry_points.txt +1 -0
- nautobot/data_validation/template_content.py +0 -42
- nautobot/dcim/filters/mixins.py +0 -354
- nautobot/ipam/templates/ipam/inc/prefix_header_extra_content_table.html +0 -4
- /nautobot/tenancy/{filters/mixins.py → filter_mixins.py} +0 -0
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/LICENSE.txt +0 -0
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/NOTICE +0 -0
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/WHEEL +0 -0
|
@@ -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,7 +20,7 @@ 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
25
|
from nautobot.dcim import filters as dcim_filters, forms as dcim_forms, models as dcim_models, tables
|
|
26
26
|
from nautobot.extras import models as extras_models, utils as extras_utils
|
|
@@ -30,8 +30,6 @@ from nautobot.extras.forms import StatusForm
|
|
|
30
30
|
from nautobot.extras.models import ObjectChange
|
|
31
31
|
from nautobot.ipam import models as ipam_models
|
|
32
32
|
|
|
33
|
-
from example_app.models import ExampleModel
|
|
34
|
-
|
|
35
33
|
|
|
36
34
|
class ConstructCacheKeyTest(TestCase):
|
|
37
35
|
"""
|
|
@@ -372,10 +370,13 @@ class GetFooForModelTest(TestCase):
|
|
|
372
370
|
# both primary_ip4 and primary_ip6 are candidates
|
|
373
371
|
lookup.get_related_field_for_models(dcim_models.Device, ipam_models.IPAddress)
|
|
374
372
|
|
|
373
|
+
@tag("example_app")
|
|
375
374
|
def test_get_route_for_model(self):
|
|
376
375
|
"""
|
|
377
376
|
Test that `get_route_for_model` returns the appropriate URL route name for various inputs.
|
|
378
377
|
"""
|
|
378
|
+
from example_app.models import ExampleModel
|
|
379
|
+
|
|
379
380
|
# UI
|
|
380
381
|
self.assertEqual(lookup.get_route_for_model("dcim.device", "list"), "dcim:device_list")
|
|
381
382
|
self.assertEqual(lookup.get_route_for_model(dcim_models.Device, "list"), "dcim:device_list")
|
|
@@ -423,10 +424,13 @@ class GetFooForModelTest(TestCase):
|
|
|
423
424
|
self.assertEqual(lookup.get_model_from_name("dcim.device"), dcim_models.Device)
|
|
424
425
|
self.assertEqual(lookup.get_model_from_name("dcim.location"), dcim_models.Location)
|
|
425
426
|
|
|
427
|
+
@tag("example_app")
|
|
426
428
|
def test_get_model_for_view_name(self):
|
|
427
429
|
"""
|
|
428
430
|
Test that `get_model_for_view_name` returns the appropriate Model, if the colon separated view name provided.
|
|
429
431
|
"""
|
|
432
|
+
from example_app.models import ExampleModel
|
|
433
|
+
|
|
430
434
|
with self.subTest("Test core UI view."):
|
|
431
435
|
self.assertEqual(lookup.get_model_for_view_name("dcim:device_list"), dcim_models.Device)
|
|
432
436
|
self.assertEqual(lookup.get_model_for_view_name("dcim:device"), dcim_models.Device)
|
|
@@ -1128,6 +1132,29 @@ class TestModuleLoadingUtils(TestCase):
|
|
|
1128
1132
|
self.assertFalse(permitted)
|
|
1129
1133
|
self.assertIsInstance(reason, str)
|
|
1130
1134
|
|
|
1135
|
+
def test_import_string_optional(self):
|
|
1136
|
+
with self.subTest("Nonexistent module should return None"):
|
|
1137
|
+
self.assertIsNone(import_string_optional("no_such_module.no_such_attribute"))
|
|
1138
|
+
self.assertIsNone(import_string_optional("no_such_module.no_such_submodule.no_such_attribute"))
|
|
1139
|
+
self.assertIsNone(import_string_optional("nautobot.no_such_submodule.no_such_attribute"))
|
|
1140
|
+
self.assertIsNone(import_string_optional("nautobot.core.no_such_submodule.no_such_attribute"))
|
|
1141
|
+
|
|
1142
|
+
with self.subTest("Existing module but nonexistent attribute should return None"):
|
|
1143
|
+
self.assertIsNone(import_string_optional("nautobot.core.no_such_attribute"))
|
|
1144
|
+
self.assertIsNone(import_string_optional("nautobot.core.no_such_attribute"))
|
|
1145
|
+
self.assertIsNone(import_string_optional("sys.no_such_attribute"))
|
|
1146
|
+
|
|
1147
|
+
with self.subTest("Other import errors should propagate upward still"):
|
|
1148
|
+
with self.assertRaises(ImportError):
|
|
1149
|
+
import_string_optional("nautobot.extras.test_jobs.invalid_import.MyJob")
|
|
1150
|
+
with self.assertRaises(ImportError):
|
|
1151
|
+
import_string_optional("nautobot.extras.test_jobs.missing_import.MyJob")
|
|
1152
|
+
|
|
1153
|
+
with self.subTest("Successful imports should succeed"):
|
|
1154
|
+
self.assertEqual(
|
|
1155
|
+
import_string_optional("nautobot.core.tests.test_utils.TestModuleLoadingUtils"), self.__class__
|
|
1156
|
+
)
|
|
1157
|
+
|
|
1131
1158
|
|
|
1132
1159
|
class TestQuerySetUtils(TestCase):
|
|
1133
1160
|
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
|
"""
|
nautobot/core/ui/breadcrumbs.py
CHANGED
|
@@ -507,14 +507,7 @@ class Breadcrumbs:
|
|
|
507
507
|
and there is `context['detail'] = True` set in context
|
|
508
508
|
|
|
509
509
|
!!! important
|
|
510
|
-
|
|
511
|
-
- `InstanceBreadcrumbItem` at the end of `detail` breadcrumbs
|
|
512
|
-
- `ModelBreadcrumbItem` at the beginning of `list` and `detail` breadcrumbs
|
|
513
|
-
|
|
514
|
-
You can override this behavior by subclassing this class and updating
|
|
515
|
-
the `list_breadcrumb_item` or `detail_breadcrumb_item` attributes.
|
|
516
|
-
|
|
517
|
-
If you're using custom action other than `list` / `detail` you need to remember to add above breadcrumbs
|
|
510
|
+
If you're using custom action other than `list` / `detail` you need to remember to add list view breadcrumb manually
|
|
518
511
|
if you need them in your custom action.
|
|
519
512
|
|
|
520
513
|
Attributes:
|
|
@@ -553,7 +546,7 @@ class Breadcrumbs:
|
|
|
553
546
|
|
|
554
547
|
# Set the default breadcrumbs
|
|
555
548
|
self.items = {
|
|
556
|
-
"list": [
|
|
549
|
+
"list": [],
|
|
557
550
|
"detail": [*self.breadcrumb_items],
|
|
558
551
|
}
|
|
559
552
|
|
|
@@ -561,9 +554,6 @@ class Breadcrumbs:
|
|
|
561
554
|
if items:
|
|
562
555
|
self.items = {**self.items, **items}
|
|
563
556
|
|
|
564
|
-
# Built-in feature: always add the instance details at the end of breadcrumbs path
|
|
565
|
-
self.items["detail"].append(InstanceBreadcrumbItem(label=detail_item_label))
|
|
566
|
-
|
|
567
557
|
def get_breadcrumbs_items(self, context: Context) -> list[tuple[str, str]]:
|
|
568
558
|
"""
|
|
569
559
|
Compute the list of breadcrumb items for the given context.
|
nautobot/core/ui/choices.py
CHANGED
|
@@ -86,17 +86,149 @@ class EChartsTypeTheme(ChoiceSet):
|
|
|
86
86
|
|
|
87
87
|
COLORS = {
|
|
88
88
|
LIGHT: [
|
|
89
|
-
UI_COLORS["blue
|
|
90
|
-
UI_COLORS["
|
|
91
|
-
UI_COLORS["
|
|
92
|
-
UI_COLORS["
|
|
93
|
-
UI_COLORS["
|
|
89
|
+
UI_COLORS["blue"]["light"],
|
|
90
|
+
UI_COLORS["purple"]["light"],
|
|
91
|
+
UI_COLORS["turquoise"]["light"],
|
|
92
|
+
UI_COLORS["orange"]["light"],
|
|
93
|
+
UI_COLORS["green"]["light"],
|
|
94
|
+
UI_COLORS["red"]["light"],
|
|
95
|
+
UI_COLORS["gray"]["light"],
|
|
96
|
+
UI_COLORS["blue-lighter"]["light"],
|
|
97
|
+
UI_COLORS["purple-lighter"]["light"],
|
|
98
|
+
UI_COLORS["turquoise-lighter"]["light"],
|
|
99
|
+
UI_COLORS["orange-lighter"]["light"],
|
|
100
|
+
UI_COLORS["green-lighter"]["light"],
|
|
101
|
+
UI_COLORS["red-lighter"]["light"],
|
|
102
|
+
UI_COLORS["gray-lighter"]["light"],
|
|
103
|
+
UI_COLORS["blue-darker"]["light"],
|
|
104
|
+
UI_COLORS["purple-darker"]["light"],
|
|
105
|
+
UI_COLORS["turquoise-darker"]["light"],
|
|
106
|
+
UI_COLORS["orange-darker"]["light"],
|
|
107
|
+
UI_COLORS["green-darker"]["light"],
|
|
108
|
+
UI_COLORS["red-darker"]["light"],
|
|
109
|
+
UI_COLORS["gray-darker"]["light"],
|
|
94
110
|
],
|
|
95
111
|
DARK: [
|
|
96
|
-
UI_COLORS["blue
|
|
97
|
-
UI_COLORS["
|
|
98
|
-
UI_COLORS["
|
|
99
|
-
UI_COLORS["
|
|
100
|
-
UI_COLORS["
|
|
112
|
+
UI_COLORS["blue"]["dark"],
|
|
113
|
+
UI_COLORS["purple"]["dark"],
|
|
114
|
+
UI_COLORS["turquoise"]["dark"],
|
|
115
|
+
UI_COLORS["orange"]["dark"],
|
|
116
|
+
UI_COLORS["green"]["dark"],
|
|
117
|
+
UI_COLORS["red"]["dark"],
|
|
118
|
+
UI_COLORS["gray"]["dark"],
|
|
119
|
+
UI_COLORS["blue-lighter"]["dark"],
|
|
120
|
+
UI_COLORS["purple-lighter"]["dark"],
|
|
121
|
+
UI_COLORS["turquoise-lighter"]["dark"],
|
|
122
|
+
UI_COLORS["orange-lighter"]["dark"],
|
|
123
|
+
UI_COLORS["green-lighter"]["dark"],
|
|
124
|
+
UI_COLORS["red-lighter"]["dark"],
|
|
125
|
+
UI_COLORS["gray-lighter"]["dark"],
|
|
126
|
+
UI_COLORS["blue-darker"]["dark"],
|
|
127
|
+
UI_COLORS["purple-darker"]["dark"],
|
|
128
|
+
UI_COLORS["turquoise-darker"]["dark"],
|
|
129
|
+
UI_COLORS["orange-darker"]["dark"],
|
|
130
|
+
UI_COLORS["green-darker"]["dark"],
|
|
131
|
+
UI_COLORS["red-darker"]["dark"],
|
|
132
|
+
UI_COLORS["gray-darker"]["dark"],
|
|
101
133
|
],
|
|
102
134
|
}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class NavigationIconChoices(ChoiceSet):
|
|
138
|
+
"""Navigation icons for major Nautobot sections."""
|
|
139
|
+
|
|
140
|
+
DEVICES = "server"
|
|
141
|
+
IPAM = "sitemap-outline"
|
|
142
|
+
ORGANIZATION = "organization"
|
|
143
|
+
CIRCUITS = "cable-data"
|
|
144
|
+
VPN = "atom"
|
|
145
|
+
ROUTING = "route"
|
|
146
|
+
POWER = "battery-3"
|
|
147
|
+
WIRELESS = "wifi"
|
|
148
|
+
SECRETS = "secrets"
|
|
149
|
+
SECURITY = "security"
|
|
150
|
+
LOAD_BALANCERS = "arrow-decision"
|
|
151
|
+
VIRTUALIZATION = "cloud-upload"
|
|
152
|
+
CLOUD = "cloud"
|
|
153
|
+
DESIGN = "hammer"
|
|
154
|
+
APPROVAL_WORKFLOWS = "checkbox-circle"
|
|
155
|
+
EXTENSIBILITY = "extensibility"
|
|
156
|
+
GOLDEN_CONFIG = "sliders-vert-2"
|
|
157
|
+
JOBS = "share"
|
|
158
|
+
APPS = "elements"
|
|
159
|
+
|
|
160
|
+
CHOICES = (
|
|
161
|
+
(DEVICES, "Devices"),
|
|
162
|
+
(IPAM, "IPAM"),
|
|
163
|
+
(ORGANIZATION, "Organization"),
|
|
164
|
+
(CIRCUITS, "Circuits"),
|
|
165
|
+
(VPN, "VPN"),
|
|
166
|
+
(ROUTING, "Routing"),
|
|
167
|
+
(POWER, "Power"),
|
|
168
|
+
(WIRELESS, "Wireless"),
|
|
169
|
+
(SECRETS, "Secrets"),
|
|
170
|
+
(SECURITY, "Security"),
|
|
171
|
+
(LOAD_BALANCERS, "Load Balancers"),
|
|
172
|
+
(VIRTUALIZATION, "Virtualization"),
|
|
173
|
+
(CLOUD, "Cloud"),
|
|
174
|
+
(DESIGN, "Design"),
|
|
175
|
+
(APPROVAL_WORKFLOWS, "Approval Workflows"),
|
|
176
|
+
(EXTENSIBILITY, "Extensibility"),
|
|
177
|
+
(GOLDEN_CONFIG, "Golden Config"),
|
|
178
|
+
(JOBS, "Jobs"),
|
|
179
|
+
(APPS, "Apps"),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class NavigationWeightChoices(ChoiceSet):
|
|
184
|
+
"""Navigation weights for major Nautobot sections."""
|
|
185
|
+
|
|
186
|
+
# In general we are looking to:
|
|
187
|
+
# - Keep data models before the default weight of 1000
|
|
188
|
+
# - Leave a gap between the default location of 1000
|
|
189
|
+
# - Keep non-model items after the default weight of 1000
|
|
190
|
+
# - Keep key items of GC, Jobs, and Apps at the end for easy access
|
|
191
|
+
DEVICES = 100
|
|
192
|
+
IPAM = 200
|
|
193
|
+
ORGANIZATION = 300
|
|
194
|
+
CIRCUITS = 400
|
|
195
|
+
VPN = 450
|
|
196
|
+
ROUTING = 500
|
|
197
|
+
POWER = 550
|
|
198
|
+
WIRELESS = 600
|
|
199
|
+
SECRETS = 650
|
|
200
|
+
SECURITY = 700
|
|
201
|
+
LOAD_BALANCERS = 750
|
|
202
|
+
VIRTUALIZATION = 800
|
|
203
|
+
CLOUD = 850
|
|
204
|
+
# We leave a gap here to allow for future expansion and don't use 1000
|
|
205
|
+
# since it the default weight for NavMenuTab if none is specified.
|
|
206
|
+
DESIGN = 1100
|
|
207
|
+
APPROVAL_WORKFLOWS = 1200
|
|
208
|
+
EXTENSIBILITY = 1300
|
|
209
|
+
# look to keep these last few items at the end of the nav for easy access
|
|
210
|
+
GOLDEN_CONFIG = 2000
|
|
211
|
+
JOBS = 2100
|
|
212
|
+
APPS = 2200
|
|
213
|
+
|
|
214
|
+
CHOICES = (
|
|
215
|
+
(DEVICES, "Devices"),
|
|
216
|
+
(IPAM, "IPAM"),
|
|
217
|
+
(ORGANIZATION, "Organization"),
|
|
218
|
+
(CIRCUITS, "Circuits"),
|
|
219
|
+
(VPN, "VPN"),
|
|
220
|
+
(ROUTING, "Routing"),
|
|
221
|
+
(POWER, "Power"),
|
|
222
|
+
(WIRELESS, "Wireless"),
|
|
223
|
+
(SECRETS, "Secrets"),
|
|
224
|
+
(SECURITY, "Security"),
|
|
225
|
+
(VIRTUALIZATION, "Virtualization"),
|
|
226
|
+
(LOAD_BALANCERS, "Load Balancers"),
|
|
227
|
+
(CLOUD, "Cloud"),
|
|
228
|
+
(DESIGN, "Design"),
|
|
229
|
+
(APPROVAL_WORKFLOWS, "Approval Workflows"),
|
|
230
|
+
(EXTENSIBILITY, "Extensibility"),
|
|
231
|
+
(GOLDEN_CONFIG, "Golden Config"),
|
|
232
|
+
(JOBS, "Jobs"),
|
|
233
|
+
(APPS, "Apps"),
|
|
234
|
+
)
|
nautobot/core/ui/constants.py
CHANGED
|
@@ -1,22 +1,86 @@
|
|
|
1
1
|
UI_COLORS = {
|
|
2
|
-
"blue
|
|
2
|
+
"blue": {
|
|
3
3
|
"light": "#007dff",
|
|
4
|
-
"dark": "#
|
|
4
|
+
"dark": "#339dff",
|
|
5
5
|
},
|
|
6
|
-
"
|
|
7
|
-
"light": "#
|
|
8
|
-
"dark": "#
|
|
6
|
+
"blue-darker": {
|
|
7
|
+
"light": "#0e4781",
|
|
8
|
+
"dark": "#255581",
|
|
9
|
+
},
|
|
10
|
+
"blue-lighter": {
|
|
11
|
+
"light": "#8cc5ff",
|
|
12
|
+
"dark": "#a3d3ff",
|
|
13
|
+
},
|
|
14
|
+
"gray": {
|
|
15
|
+
"light": "#b0b0b0",
|
|
16
|
+
"dark": "#828282",
|
|
17
|
+
},
|
|
18
|
+
"gray-darker": {
|
|
19
|
+
"light": "#5e5e5e",
|
|
20
|
+
"dark": "#494949",
|
|
21
|
+
},
|
|
22
|
+
"gray-lighter": {
|
|
23
|
+
"light": "#dbdbdb",
|
|
24
|
+
"dark": "#c7c7c7",
|
|
9
25
|
},
|
|
10
|
-
"green
|
|
26
|
+
"green": {
|
|
11
27
|
"light": "#1ca92a",
|
|
12
|
-
"dark": "#
|
|
28
|
+
"dark": "#2ecc40",
|
|
29
|
+
},
|
|
30
|
+
"green-darker": {
|
|
31
|
+
"light": "#1b5a21",
|
|
32
|
+
"dark": "#236a2b",
|
|
33
|
+
},
|
|
34
|
+
"green-lighter": {
|
|
35
|
+
"light": "#99d89f",
|
|
36
|
+
"dark": "#a1e8a9",
|
|
37
|
+
},
|
|
38
|
+
"orange": {
|
|
39
|
+
"light": "#e07807",
|
|
40
|
+
"dark": "#ff9933",
|
|
13
41
|
},
|
|
14
|
-
"
|
|
42
|
+
"orange-darker": {
|
|
43
|
+
"light": "#734411",
|
|
44
|
+
"dark": "#815325",
|
|
45
|
+
},
|
|
46
|
+
"orange-lighter": {
|
|
47
|
+
"light": "#f1c28f",
|
|
48
|
+
"dark": "#ffd1a3",
|
|
49
|
+
},
|
|
50
|
+
"purple": {
|
|
51
|
+
"light": "#7b61ff",
|
|
52
|
+
"dark": "#a394ff",
|
|
53
|
+
},
|
|
54
|
+
"purple-darker": {
|
|
55
|
+
"light": "#463a81",
|
|
56
|
+
"dark": "#585181",
|
|
57
|
+
},
|
|
58
|
+
"purple-lighter": {
|
|
59
|
+
"light": "#c4b8ff",
|
|
60
|
+
"dark": "#d6cfff",
|
|
61
|
+
},
|
|
62
|
+
"red": {
|
|
15
63
|
"light": "#e01f1f",
|
|
16
|
-
"dark": "#
|
|
64
|
+
"dark": "#ff4c4c",
|
|
65
|
+
},
|
|
66
|
+
"red-darker": {
|
|
67
|
+
"light": "#731c1c",
|
|
68
|
+
"dark": "#813131",
|
|
69
|
+
},
|
|
70
|
+
"red-lighter": {
|
|
71
|
+
"light": "#f19a9a",
|
|
72
|
+
"dark": "#ffaeae",
|
|
73
|
+
},
|
|
74
|
+
"turquoise": {
|
|
75
|
+
"light": "#00bfa5",
|
|
76
|
+
"dark": "#26d7c5",
|
|
77
|
+
},
|
|
78
|
+
"turquoise-darker": {
|
|
79
|
+
"light": "#0e6459",
|
|
80
|
+
"dark": "#1f6f67",
|
|
17
81
|
},
|
|
18
|
-
"
|
|
19
|
-
"light": "#
|
|
20
|
-
"dark": "#
|
|
82
|
+
"turquoise-lighter": {
|
|
83
|
+
"light": "#8ce2d7",
|
|
84
|
+
"dark": "#9dede5",
|
|
21
85
|
},
|
|
22
86
|
}
|