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
|
@@ -3,6 +3,7 @@ Model test cases
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import re
|
|
6
|
+
from unittest import TestCase
|
|
6
7
|
|
|
7
8
|
from django.contrib.contenttypes.models import ContentType
|
|
8
9
|
from django.core.validators import ValidationError
|
|
@@ -376,3 +377,17 @@ class UniqueValidationRuleModelTestCase(ValidationRuleModelTestCases.ValidationR
|
|
|
376
377
|
)
|
|
377
378
|
|
|
378
379
|
rule.clean()
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
class ValidationRuleModelMixinTestCase(TestCase):
|
|
383
|
+
"""
|
|
384
|
+
Validate ValidationRuleModelMixin is working as intended.
|
|
385
|
+
"""
|
|
386
|
+
|
|
387
|
+
def test_is_data_compliance_model(self):
|
|
388
|
+
"""Validate is_data_compliance_model is set correctly on models using ValidationRuleModelMixin."""
|
|
389
|
+
# These models should have is_data_compliance_model = False
|
|
390
|
+
self.assertFalse(MinMaxValidationRule.is_data_compliance_model)
|
|
391
|
+
self.assertFalse(RegularExpressionValidationRule.is_data_compliance_model)
|
|
392
|
+
self.assertFalse(RequiredValidationRule.is_data_compliance_model)
|
|
393
|
+
self.assertFalse(UniqueValidationRule.is_data_compliance_model)
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
"""Unit tests for data_validation views."""
|
|
2
2
|
|
|
3
|
-
from
|
|
4
|
-
|
|
3
|
+
from constance import config
|
|
4
|
+
from django.contrib.auth import get_user_model
|
|
5
5
|
from django.contrib.contenttypes.models import ContentType
|
|
6
|
-
from django.
|
|
6
|
+
from django.core.cache import caches
|
|
7
|
+
from django.test import override_settings
|
|
8
|
+
from django.urls import reverse
|
|
7
9
|
|
|
10
|
+
from nautobot.core import settings
|
|
8
11
|
from nautobot.core.testing import TestCase, ViewTestCases
|
|
9
12
|
from nautobot.data_validation.models import (
|
|
10
13
|
DataCompliance,
|
|
@@ -13,13 +16,17 @@ from nautobot.data_validation.models import (
|
|
|
13
16
|
RequiredValidationRule,
|
|
14
17
|
UniqueValidationRule,
|
|
15
18
|
)
|
|
16
|
-
from nautobot.data_validation.tables import DataComplianceTableTab
|
|
17
19
|
from nautobot.data_validation.tests import ValidationRuleTestCaseMixin
|
|
18
|
-
from nautobot.data_validation.tests.test_data_compliance_rules import
|
|
19
|
-
|
|
20
|
+
from nautobot.data_validation.tests.test_data_compliance_rules import (
|
|
21
|
+
TestFailedDataComplianceRule,
|
|
22
|
+
TestFailedDataComplianceRuleAlt,
|
|
23
|
+
)
|
|
24
|
+
from nautobot.dcim.choices import DeviceUniquenessChoices
|
|
20
25
|
from nautobot.dcim.models import Device, Location, LocationType, PowerFeed
|
|
21
26
|
from nautobot.extras.models import Status
|
|
22
27
|
|
|
28
|
+
User = get_user_model()
|
|
29
|
+
|
|
23
30
|
|
|
24
31
|
class RegularExpressionValidationRuleTestCase(ValidationRuleTestCaseMixin, ViewTestCases.PrimaryObjectViewTestCase):
|
|
25
32
|
"""View test cases for the RegularExpressionValidationRule model."""
|
|
@@ -247,31 +254,182 @@ class DataComplianceObjectTestCase(TestCase):
|
|
|
247
254
|
"""Test cases for DataComplianceObjectView."""
|
|
248
255
|
|
|
249
256
|
def setUp(self):
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
name="Test Location 1",
|
|
254
|
-
location_type=LocationType.objects.get_by_natural_key("Region"),
|
|
255
|
-
status=Status.objects.get_by_natural_key("Active"),
|
|
256
|
-
)
|
|
257
|
-
s.save()
|
|
258
|
-
t = TestFailedDataComplianceRule(s)
|
|
257
|
+
self.device = Device.objects.first()
|
|
258
|
+
|
|
259
|
+
t = TestFailedDataComplianceRuleAlt(self.device)
|
|
259
260
|
t.clean()
|
|
261
|
+
self.user = User.objects.create_user(username="testuser", is_superuser=True)
|
|
262
|
+
|
|
263
|
+
def test_data_compliance_action(self):
|
|
264
|
+
self.add_permissions("data_validation.view_datacompliance")
|
|
265
|
+
self.client.force_login(self.user)
|
|
266
|
+
url = reverse("dcim:device_data-compliance", kwargs={"pk": self.device.pk})
|
|
267
|
+
response = self.client.get(url)
|
|
268
|
+
self.assertEqual(response.status_code, 200)
|
|
269
|
+
self.assertIn("active_tab", response.context)
|
|
270
|
+
self.assertEqual(response.context["active_tab"], "data_compliance")
|
|
271
|
+
self.assertBodyContains(response, "The tenant is wrong")
|
|
272
|
+
self.assertBodyContains(response, "The name is wrong")
|
|
273
|
+
self.assertBodyContains(response, "The status is wrong")
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class DeviceConstraintsViewTest(TestCase):
|
|
277
|
+
"""Tests for the DeviceConstraintsView."""
|
|
260
278
|
|
|
261
|
-
def
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
self.
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
279
|
+
def setUp(self):
|
|
280
|
+
self.initial_setting = config.DEVICE_UNIQUENESS
|
|
281
|
+
self.url = reverse("data_validation:device-constraints")
|
|
282
|
+
self.device_ct = ContentType.objects.get_for_model(Device)
|
|
283
|
+
|
|
284
|
+
def tearDown(self):
|
|
285
|
+
"""Reset Constance config and clear cache."""
|
|
286
|
+
config.DEVICE_UNIQUENESS = self.initial_setting
|
|
287
|
+
cache = caches[settings.CONSTANCE_DATABASE_CACHE_BACKEND]
|
|
288
|
+
cache.clear()
|
|
289
|
+
|
|
290
|
+
@override_settings(
|
|
291
|
+
DEVICE_UNIQUENESS=DeviceUniquenessChoices.NONE,
|
|
292
|
+
)
|
|
293
|
+
def test_page_reflects_correct_settings_for_non_staff(self):
|
|
294
|
+
"""Non-staff view still shows the actual configuration, but in disabled form."""
|
|
295
|
+
user = get_user_model().objects.create_user(username="testuser")
|
|
296
|
+
self.client.force_login(user)
|
|
297
|
+
response = self.client.get(self.url)
|
|
298
|
+
|
|
299
|
+
self.assertEqual(response.status_code, 200)
|
|
300
|
+
|
|
301
|
+
# Ensure fields are disabled but reflect correct config values
|
|
302
|
+
self.assertContains(response, f'value="{DeviceUniquenessChoices.NONE}" selected')
|
|
303
|
+
self.assertContains(response, "disabled")
|
|
304
|
+
self.assertContains(response, "You do not have permission to modify these settings.")
|
|
305
|
+
|
|
306
|
+
# DEVICE_NAME_REQUIRED should NOT be checked, because RequiredValidationRule not exist
|
|
307
|
+
device_ct = ContentType.objects.get_for_model(Device)
|
|
308
|
+
self.assertFalse(RequiredValidationRule.objects.filter(content_type=device_ct, field="name").exists())
|
|
309
|
+
self.assertNotContains(response, 'name="DEVICE_NAME_REQUIRED" checked')
|
|
310
|
+
|
|
311
|
+
# Footer buttons should NOT be rendered
|
|
312
|
+
self.assertNotContains(response, '<button type="submit"')
|
|
313
|
+
self.assertNotContains(response, "-->Update")
|
|
314
|
+
self.assertNotContains(response, "-->Cancel")
|
|
315
|
+
|
|
316
|
+
@override_settings(
|
|
317
|
+
DEVICE_UNIQUENESS=DeviceUniquenessChoices.NAME,
|
|
318
|
+
)
|
|
319
|
+
def test_page_reflects_correct_settings_for_staff(self):
|
|
320
|
+
"""Page should reflect the true values of DEVICE_UNIQUENESS and DEVICE_NAME_REQUIRED."""
|
|
321
|
+
# Create RequiredValidationRule to check proper value of DEVICE_NAME_REQUIRED
|
|
322
|
+
device_ct = ContentType.objects.get_for_model(Device)
|
|
323
|
+
RequiredValidationRule.objects.create(
|
|
324
|
+
name="Required Name rule",
|
|
325
|
+
content_type=device_ct,
|
|
326
|
+
field="name",
|
|
327
|
+
)
|
|
328
|
+
user = get_user_model().objects.create_user(username="testuser", is_staff=True)
|
|
329
|
+
self.client.force_login(user)
|
|
330
|
+
response = self.client.get(self.url)
|
|
331
|
+
|
|
332
|
+
self.assertEqual(response.status_code, 200)
|
|
333
|
+
self.assertTemplateUsed(response, "data_validation/device_constraints.html")
|
|
334
|
+
|
|
335
|
+
self.assertIn("form", response.context)
|
|
336
|
+
self.assertContains(response, "Device Constraints")
|
|
337
|
+
|
|
338
|
+
# Check that the correct DEVICE_UNIQUENESS option is selected
|
|
339
|
+
self.assertContains(response, f'value="{DeviceUniquenessChoices.NAME}" selected')
|
|
340
|
+
|
|
341
|
+
self.assertNotContains(response, "disabled")
|
|
342
|
+
self.assertNotContains(response, "You do not have permission to modify these settings.")
|
|
343
|
+
|
|
344
|
+
# Check that DEVICE_NAME_REQUIRED checkbox is checked when RequiredValidationRule exist
|
|
345
|
+
self.assertTrue(RequiredValidationRule.objects.filter(content_type=device_ct, field="name").exists())
|
|
346
|
+
self.assertContains(response, 'id="id_DEVICE_NAME_REQUIRED" checked')
|
|
347
|
+
|
|
348
|
+
# Footer buttons should be rendered
|
|
349
|
+
self.assertContains(response, '<button type="submit"')
|
|
350
|
+
self.assertContains(response, "-->Update")
|
|
351
|
+
self.assertContains(response, "-->Cancel")
|
|
352
|
+
|
|
353
|
+
def test_post_as_non_admin_denied(self):
|
|
354
|
+
"""POST by non-admin should be denied."""
|
|
355
|
+
user = get_user_model().objects.create_user(username="normaluser")
|
|
356
|
+
self.client.force_login(user)
|
|
357
|
+
|
|
358
|
+
response = self.client.post(
|
|
359
|
+
self.url,
|
|
360
|
+
data={
|
|
361
|
+
"DEVICE_UNIQUENESS": DeviceUniquenessChoices.LOCATION_TENANT_NAME,
|
|
362
|
+
"DEVICE_NAME_REQUIRED": True,
|
|
363
|
+
},
|
|
364
|
+
follow=True,
|
|
365
|
+
)
|
|
366
|
+
self.assertEqual(response.status_code, 403)
|
|
367
|
+
|
|
368
|
+
# No rule should be created
|
|
369
|
+
self.assertFalse(RequiredValidationRule.objects.filter(content_type=self.device_ct, field="name").exists())
|
|
370
|
+
self.assertEqual(config.DEVICE_UNIQUENESS, self.initial_setting)
|
|
371
|
+
|
|
372
|
+
def test_post_updates_device_uniqueness_and_creates_required_rule(self):
|
|
373
|
+
"""POST with DEVICE_NAME_REQUIRED=True should create a RequiredValidationRule."""
|
|
374
|
+
user = get_user_model().objects.create_user(username="testuser", is_staff=True)
|
|
375
|
+
self.client.force_login(user)
|
|
376
|
+
response = self.client.post(
|
|
377
|
+
self.url,
|
|
378
|
+
{
|
|
379
|
+
"DEVICE_UNIQUENESS": DeviceUniquenessChoices.NAME,
|
|
380
|
+
"DEVICE_NAME_REQUIRED": True,
|
|
381
|
+
},
|
|
382
|
+
follow=True,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
self.assertRedirects(response, self.url)
|
|
386
|
+
self.assertEqual(config.DEVICE_UNIQUENESS, "name")
|
|
387
|
+
|
|
388
|
+
rule_exists = RequiredValidationRule.objects.filter(
|
|
389
|
+
content_type=self.device_ct,
|
|
390
|
+
field="name",
|
|
391
|
+
).exists()
|
|
392
|
+
self.assertTrue(rule_exists)
|
|
393
|
+
|
|
394
|
+
def test_post_disables_required_rule(self):
|
|
395
|
+
"""POST with DEVICE_NAME_REQUIRED=False should delete the RequiredValidationRule."""
|
|
396
|
+
user = get_user_model().objects.create_user(username="testuser", is_staff=True)
|
|
397
|
+
self.client.force_login(user)
|
|
398
|
+
RequiredValidationRule.objects.create(
|
|
399
|
+
name="Required Name rule",
|
|
400
|
+
content_type=self.device_ct,
|
|
401
|
+
field="name",
|
|
402
|
+
)
|
|
403
|
+
self.assertTrue(RequiredValidationRule.objects.filter(content_type=self.device_ct, field="name").exists())
|
|
404
|
+
|
|
405
|
+
response = self.client.post(
|
|
406
|
+
self.url,
|
|
407
|
+
{
|
|
408
|
+
"DEVICE_UNIQUENESS": DeviceUniquenessChoices.LOCATION_TENANT_NAME,
|
|
409
|
+
"DEVICE_NAME_REQUIRED": False,
|
|
410
|
+
},
|
|
411
|
+
follow=True,
|
|
412
|
+
)
|
|
413
|
+
self.assertRedirects(response, self.url)
|
|
414
|
+
self.assertEqual(config.DEVICE_UNIQUENESS, DeviceUniquenessChoices.LOCATION_TENANT_NAME)
|
|
415
|
+
|
|
416
|
+
self.assertFalse(RequiredValidationRule.objects.filter(content_type=self.device_ct, field="name").exists())
|
|
417
|
+
|
|
418
|
+
def test_invalid_post_rerenders_form(self):
|
|
419
|
+
"""If form is invalid, the view should re-render without redirect for multiple invalid inputs."""
|
|
420
|
+
user = get_user_model().objects.create_user(username="testuser", is_staff=True)
|
|
421
|
+
self.client.force_login(user)
|
|
422
|
+
|
|
423
|
+
invalid_inputs = [
|
|
424
|
+
{"DEVICE_UNIQUENESS": ""},
|
|
425
|
+
{"DEVICE_UNIQUENESS": "invalid_value"},
|
|
426
|
+
]
|
|
427
|
+
|
|
428
|
+
for post_data in invalid_inputs:
|
|
429
|
+
with self.subTest(post_data=post_data):
|
|
430
|
+
response = self.client.post(self.url, post_data)
|
|
431
|
+
self.assertEqual(response.status_code, 200)
|
|
432
|
+
self.assertTemplateUsed(response, "data_validation/device_constraints.html")
|
|
433
|
+
self.assertIn("form", response.context)
|
|
434
|
+
self.assertTrue(response.context["form"].errors)
|
|
435
|
+
self.assertEqual(config.DEVICE_UNIQUENESS, self.initial_setting)
|
nautobot/data_validation/urls.py
CHANGED
|
@@ -15,10 +15,7 @@ router.register("required-rules", views.RequiredValidationRuleUIViewSet)
|
|
|
15
15
|
router.register("unique-rules", views.UniqueValidationRuleUIViewSet)
|
|
16
16
|
|
|
17
17
|
urlpatterns = [
|
|
18
|
-
path(
|
|
19
|
-
"data-compliance/<model>/<uuid:id>/",
|
|
20
|
-
views.DataComplianceObjectView.as_view(),
|
|
21
|
-
name="data-compliance-tab",
|
|
22
|
-
),
|
|
18
|
+
path("device-constraints/", views.DeviceConstraintsView.as_view(), name="device-constraints"),
|
|
23
19
|
]
|
|
20
|
+
|
|
24
21
|
urlpatterns += router.urls
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
"""Views for data_validation."""
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from constance import config
|
|
4
|
+
from django.contrib import messages
|
|
4
5
|
from django.contrib.contenttypes.models import ContentType
|
|
5
|
-
from
|
|
6
|
+
from django.shortcuts import redirect, render
|
|
6
7
|
|
|
8
|
+
from nautobot.apps.ui import Breadcrumbs, Titles, ViewNameBreadcrumbItem
|
|
7
9
|
from nautobot.core.ui.choices import SectionChoices
|
|
8
10
|
from nautobot.core.ui.object_detail import (
|
|
9
11
|
ObjectDetailContent,
|
|
10
12
|
ObjectFieldsPanel,
|
|
11
13
|
)
|
|
12
|
-
from nautobot.core.views.generic import
|
|
14
|
+
from nautobot.core.views.generic import GenericView
|
|
13
15
|
from nautobot.core.views.mixins import (
|
|
14
16
|
ObjectBulkDestroyViewMixin,
|
|
15
17
|
ObjectChangeLogViewMixin,
|
|
@@ -18,18 +20,10 @@ from nautobot.core.views.mixins import (
|
|
|
18
20
|
ObjectListViewMixin,
|
|
19
21
|
ObjectNotesViewMixin,
|
|
20
22
|
)
|
|
21
|
-
from nautobot.core.views.paginator import EnhancedPaginator, get_paginate_count
|
|
22
23
|
from nautobot.core.views.viewsets import NautobotUIViewSet
|
|
23
|
-
from nautobot.data_validation import filters, forms, tables
|
|
24
|
+
from nautobot.data_validation import filters, forms, models, tables
|
|
24
25
|
from nautobot.data_validation.api import serializers
|
|
25
|
-
from nautobot.
|
|
26
|
-
DataCompliance,
|
|
27
|
-
MinMaxValidationRule,
|
|
28
|
-
RegularExpressionValidationRule,
|
|
29
|
-
RequiredValidationRule,
|
|
30
|
-
UniqueValidationRule,
|
|
31
|
-
)
|
|
32
|
-
from nautobot.extras.utils import get_base_template
|
|
26
|
+
from nautobot.dcim.models import Device
|
|
33
27
|
|
|
34
28
|
#
|
|
35
29
|
# RegularExpressionValidationRules
|
|
@@ -43,7 +37,7 @@ class RegularExpressionValidationRuleUIViewSet(NautobotUIViewSet):
|
|
|
43
37
|
filterset_class = filters.RegularExpressionValidationRuleFilterSet
|
|
44
38
|
filterset_form_class = forms.RegularExpressionValidationRuleFilterForm
|
|
45
39
|
form_class = forms.RegularExpressionValidationRuleForm
|
|
46
|
-
queryset = RegularExpressionValidationRule.objects.all()
|
|
40
|
+
queryset = models.RegularExpressionValidationRule.objects.all()
|
|
47
41
|
serializer_class = serializers.RegularExpressionValidationRuleSerializer
|
|
48
42
|
table_class = tables.RegularExpressionValidationRuleTable
|
|
49
43
|
object_detail_content = ObjectDetailContent(
|
|
@@ -69,7 +63,7 @@ class MinMaxValidationRuleUIViewSet(NautobotUIViewSet):
|
|
|
69
63
|
filterset_class = filters.MinMaxValidationRuleFilterSet
|
|
70
64
|
filterset_form_class = forms.MinMaxValidationRuleFilterForm
|
|
71
65
|
form_class = forms.MinMaxValidationRuleForm
|
|
72
|
-
queryset = MinMaxValidationRule.objects.all()
|
|
66
|
+
queryset = models.MinMaxValidationRule.objects.all()
|
|
73
67
|
serializer_class = serializers.MinMaxValidationRuleSerializer
|
|
74
68
|
table_class = tables.MinMaxValidationRuleTable
|
|
75
69
|
object_detail_content = ObjectDetailContent(
|
|
@@ -95,7 +89,7 @@ class RequiredValidationRuleUIViewSet(NautobotUIViewSet):
|
|
|
95
89
|
filterset_class = filters.RequiredValidationRuleFilterSet
|
|
96
90
|
filterset_form_class = forms.RequiredValidationRuleFilterForm
|
|
97
91
|
form_class = forms.RequiredValidationRuleForm
|
|
98
|
-
queryset = RequiredValidationRule.objects.all()
|
|
92
|
+
queryset = models.RequiredValidationRule.objects.all()
|
|
99
93
|
serializer_class = serializers.RequiredValidationRuleSerializer
|
|
100
94
|
table_class = tables.RequiredValidationRuleTable
|
|
101
95
|
object_detail_content = ObjectDetailContent(
|
|
@@ -121,7 +115,7 @@ class UniqueValidationRuleUIViewSet(NautobotUIViewSet):
|
|
|
121
115
|
filterset_class = filters.UniqueValidationRuleFilterSet
|
|
122
116
|
filterset_form_class = forms.UniqueValidationRuleFilterForm
|
|
123
117
|
form_class = forms.UniqueValidationRuleForm
|
|
124
|
-
queryset = UniqueValidationRule.objects.all()
|
|
118
|
+
queryset = models.UniqueValidationRule.objects.all()
|
|
125
119
|
serializer_class = serializers.UniqueValidationRuleSerializer
|
|
126
120
|
table_class = tables.UniqueValidationRuleTable
|
|
127
121
|
object_detail_content = ObjectDetailContent(
|
|
@@ -151,7 +145,7 @@ class DataComplianceUIViewSet( # pylint: disable=W0223
|
|
|
151
145
|
"""Views for the DataComplianceUIViewSet model."""
|
|
152
146
|
|
|
153
147
|
lookup_field = "pk"
|
|
154
|
-
queryset = DataCompliance.objects.all()
|
|
148
|
+
queryset = models.DataCompliance.objects.all()
|
|
155
149
|
table_class = tables.DataComplianceTable
|
|
156
150
|
filterset_class = filters.DataComplianceFilterSet
|
|
157
151
|
filterset_form_class = forms.DataComplianceFilterForm
|
|
@@ -162,33 +156,72 @@ class DataComplianceUIViewSet( # pylint: disable=W0223
|
|
|
162
156
|
ObjectFieldsPanel(
|
|
163
157
|
section=SectionChoices.LEFT_HALF,
|
|
164
158
|
weight=100,
|
|
165
|
-
fields=
|
|
159
|
+
fields=[
|
|
160
|
+
"content_type",
|
|
161
|
+
"compliance_class_name",
|
|
162
|
+
"last_validation_date",
|
|
163
|
+
"validated_object",
|
|
164
|
+
"validated_attribute",
|
|
165
|
+
"validated_attribute_value",
|
|
166
|
+
"valid",
|
|
167
|
+
"message",
|
|
168
|
+
],
|
|
166
169
|
),
|
|
167
170
|
)
|
|
168
171
|
)
|
|
169
172
|
|
|
170
173
|
|
|
171
|
-
class
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
self.queryset = global_apps.get_model(model).objects.all()
|
|
182
|
-
return super().dispatch(request, *args, **kwargs)
|
|
174
|
+
class DeviceConstraintsView(GenericView):
|
|
175
|
+
template_name = "data_validation/device_constraints.html"
|
|
176
|
+
view_titles = Titles(titles={"*": "Device Constraints"})
|
|
177
|
+
breadcrumbs = Breadcrumbs(
|
|
178
|
+
items={
|
|
179
|
+
"*": [
|
|
180
|
+
ViewNameBreadcrumbItem(view_name="data_validation:device-constraints", label="Device Constraints"),
|
|
181
|
+
],
|
|
182
|
+
},
|
|
183
|
+
)
|
|
183
184
|
|
|
184
|
-
def
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
185
|
+
def get(self, request):
|
|
186
|
+
form = forms.DeviceConstraintsForm(user=request.user)
|
|
187
|
+
return render(
|
|
188
|
+
request,
|
|
189
|
+
self.template_name,
|
|
190
|
+
{
|
|
191
|
+
"form": form,
|
|
192
|
+
"view_titles": self.get_view_titles(),
|
|
193
|
+
"breadcrumbs": self.get_breadcrumbs(),
|
|
194
|
+
},
|
|
188
195
|
)
|
|
189
|
-
compliance_table = tables.DataComplianceTableTab(compliance_objects)
|
|
190
|
-
base_template = get_base_template(None, instance)
|
|
191
196
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
197
|
+
def post(self, request):
|
|
198
|
+
if not request.user.is_staff:
|
|
199
|
+
return self.handle_no_permission()
|
|
200
|
+
form = forms.DeviceConstraintsForm(request.POST)
|
|
201
|
+
if form.is_valid():
|
|
202
|
+
config.DEVICE_UNIQUENESS = form.cleaned_data["DEVICE_UNIQUENESS"]
|
|
203
|
+
device_ct = ContentType.objects.get_for_model(Device)
|
|
204
|
+
if form.cleaned_data["DEVICE_NAME_REQUIRED"]:
|
|
205
|
+
models.RequiredValidationRule.objects.get_or_create(
|
|
206
|
+
content_type=device_ct,
|
|
207
|
+
field="name",
|
|
208
|
+
defaults={"name": "Require Device Name"},
|
|
209
|
+
)
|
|
210
|
+
else:
|
|
211
|
+
models.RequiredValidationRule.objects.filter(
|
|
212
|
+
content_type=device_ct,
|
|
213
|
+
field="name",
|
|
214
|
+
).delete()
|
|
215
|
+
|
|
216
|
+
messages.success(request, "Device constraints have been updated successfully.")
|
|
217
|
+
return redirect("data_validation:device-constraints")
|
|
218
|
+
|
|
219
|
+
return render(
|
|
220
|
+
request,
|
|
221
|
+
self.template_name,
|
|
222
|
+
{
|
|
223
|
+
"form": form,
|
|
224
|
+
"view_titles": self.get_view_titles(),
|
|
225
|
+
"breadcrumbs": self.get_breadcrumbs(),
|
|
226
|
+
},
|
|
227
|
+
)
|
nautobot/dcim/api/serializers.py
CHANGED
|
@@ -23,7 +23,6 @@ from nautobot.core.api.utils import (
|
|
|
23
23
|
)
|
|
24
24
|
from nautobot.core.models.utils import get_all_concrete_models
|
|
25
25
|
from nautobot.core.utils.config import get_settings_or_config
|
|
26
|
-
from nautobot.core.utils.deprecation import class_deprecated_in_favor_of
|
|
27
26
|
from nautobot.dcim.choices import (
|
|
28
27
|
CableLengthUnitChoices,
|
|
29
28
|
CableTypeChoices,
|
|
@@ -137,12 +136,6 @@ class CableTerminationModelSerializerMixin(serializers.ModelSerializer):
|
|
|
137
136
|
return None
|
|
138
137
|
|
|
139
138
|
|
|
140
|
-
# TODO: remove in 2.2
|
|
141
|
-
@class_deprecated_in_favor_of(CableTerminationModelSerializerMixin)
|
|
142
|
-
class CableTerminationSerializer(CableTerminationModelSerializerMixin):
|
|
143
|
-
pass
|
|
144
|
-
|
|
145
|
-
|
|
146
139
|
class PathEndpointModelSerializerMixin(ValidatedModelSerializer):
|
|
147
140
|
connected_endpoint_type = serializers.SerializerMethodField(read_only=True)
|
|
148
141
|
connected_endpoint = serializers.SerializerMethodField(read_only=True)
|
|
@@ -183,12 +176,6 @@ class PathEndpointModelSerializerMixin(ValidatedModelSerializer):
|
|
|
183
176
|
return None
|
|
184
177
|
|
|
185
178
|
|
|
186
|
-
# TODO: remove in 2.2
|
|
187
|
-
@class_deprecated_in_favor_of(PathEndpointModelSerializerMixin)
|
|
188
|
-
class ConnectedEndpointSerializer(PathEndpointModelSerializerMixin):
|
|
189
|
-
pass
|
|
190
|
-
|
|
191
|
-
|
|
192
179
|
class ModularDeviceComponentTemplateSerializerMixin:
|
|
193
180
|
def validate(self, data):
|
|
194
181
|
"""Validate device_type and module_type field constraints for modular device component templates."""
|
nautobot/dcim/apps.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from nautobot.core.apps import NautobotConfig
|
|
2
|
+
from nautobot.extras.plugins import register_custom_validators
|
|
2
3
|
|
|
3
4
|
|
|
4
5
|
class DCIMConfig(NautobotConfig):
|
|
@@ -26,4 +27,7 @@ class DCIMConfig(NautobotConfig):
|
|
|
26
27
|
|
|
27
28
|
def ready(self):
|
|
28
29
|
super().ready()
|
|
30
|
+
from nautobot.dcim.custom_validators import custom_validators
|
|
31
|
+
|
|
32
|
+
register_custom_validators(custom_validators)
|
|
29
33
|
import nautobot.dcim.signals # noqa: F401 # unused-import -- but this import installs the signals
|
nautobot/dcim/choices.py
CHANGED
|
@@ -159,6 +159,20 @@ class DeviceStatusChoices(ChoiceSet):
|
|
|
159
159
|
)
|
|
160
160
|
|
|
161
161
|
|
|
162
|
+
class DeviceUniquenessChoices(ChoiceSet):
|
|
163
|
+
LOCATION_TENANT_NAME = "location_tenant_name"
|
|
164
|
+
NAME = "name"
|
|
165
|
+
NONE = "none"
|
|
166
|
+
|
|
167
|
+
DEFAULT = LOCATION_TENANT_NAME
|
|
168
|
+
|
|
169
|
+
CHOICES = [
|
|
170
|
+
(LOCATION_TENANT_NAME, "Location + Tenant + Name"),
|
|
171
|
+
(NAME, "Device name must be globally unique"),
|
|
172
|
+
(NONE, "No enforced uniqueness"),
|
|
173
|
+
]
|
|
174
|
+
|
|
175
|
+
|
|
162
176
|
#
|
|
163
177
|
# ConsolePorts
|
|
164
178
|
#
|
|
@@ -764,6 +778,7 @@ class InterfaceTypeChoices(ChoiceSet):
|
|
|
764
778
|
TYPE_VIRTUAL = "virtual"
|
|
765
779
|
TYPE_BRIDGE = "bridge"
|
|
766
780
|
TYPE_LAG = "lag"
|
|
781
|
+
TYPE_TUNNEL = "tunnel"
|
|
767
782
|
|
|
768
783
|
# Ethernet
|
|
769
784
|
TYPE_100ME_FX = "100base-fx"
|
|
@@ -932,6 +947,7 @@ class InterfaceTypeChoices(ChoiceSet):
|
|
|
932
947
|
(TYPE_VIRTUAL, "Virtual"),
|
|
933
948
|
(TYPE_BRIDGE, "Bridge"),
|
|
934
949
|
(TYPE_LAG, "Link Aggregation Group (LAG)"),
|
|
950
|
+
(TYPE_TUNNEL, "Tunnel"),
|
|
935
951
|
),
|
|
936
952
|
),
|
|
937
953
|
(
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from django.contrib.contenttypes.models import ContentType
|
|
2
|
+
|
|
3
|
+
from nautobot.apps.models import CustomValidator
|
|
4
|
+
from nautobot.core.utils.config import get_settings_or_config
|
|
5
|
+
from nautobot.data_validation.models import RequiredValidationRule
|
|
6
|
+
from nautobot.dcim.choices import DeviceUniquenessChoices
|
|
7
|
+
from nautobot.dcim.models import Device
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DeviceUniquenessValidator(CustomValidator):
|
|
11
|
+
"""Custom validator enforcing device uniqueness based on DEVICE_UNIQUENESS setting."""
|
|
12
|
+
|
|
13
|
+
model = "dcim.device"
|
|
14
|
+
|
|
15
|
+
def clean(self):
|
|
16
|
+
obj = self.context["object"]
|
|
17
|
+
try:
|
|
18
|
+
uniqueness_mode = get_settings_or_config("DEVICE_UNIQUENESS", fallback=DeviceUniquenessChoices.DEFAULT)
|
|
19
|
+
except AttributeError:
|
|
20
|
+
uniqueness_mode = DeviceUniquenessChoices.DEFAULT
|
|
21
|
+
device_name_required = RequiredValidationRule.objects.filter(
|
|
22
|
+
content_type=ContentType.objects.get_for_model(Device), field="name"
|
|
23
|
+
).exists()
|
|
24
|
+
|
|
25
|
+
# Rule 1: If we don't set DEVICE_NAME_REQUIRED then it's acceptable for any number of devices to be "unnamed",
|
|
26
|
+
# regardless of the DEVICE_UNIQUENESS setting. Note that we consider both `None` and `""` to be "unnamed".
|
|
27
|
+
if not obj.name and not device_name_required:
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
# If not obj.name and device_name_required, this will be detected by RequiredValidationRule.
|
|
31
|
+
|
|
32
|
+
if uniqueness_mode == DeviceUniquenessChoices.LOCATION_TENANT_NAME:
|
|
33
|
+
# Rule 2: name is not None, tenant is None, given location --> no duplicates
|
|
34
|
+
# Rule 3: tenant is None, name is duplicated --> error
|
|
35
|
+
if obj.tenant is None:
|
|
36
|
+
duplicates = Device.objects.filter(
|
|
37
|
+
name=obj.name,
|
|
38
|
+
tenant__isnull=True,
|
|
39
|
+
location=obj.location,
|
|
40
|
+
).exclude(pk=obj.pk)
|
|
41
|
+
if duplicates.exists():
|
|
42
|
+
self.validation_error(
|
|
43
|
+
{
|
|
44
|
+
"__all__": (
|
|
45
|
+
f"A device named '{obj.name}' with no tenant already exists in this location: {obj.location}. "
|
|
46
|
+
"Device names must be unique when tenant is None and DEVICE_UNIQUENESS='location_tenant_name'."
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
else:
|
|
51
|
+
# When tenant is set, enforce uniqueness per (location, tenant, name)
|
|
52
|
+
duplicates = Device.objects.filter(
|
|
53
|
+
name=obj.name,
|
|
54
|
+
tenant=obj.tenant,
|
|
55
|
+
location=obj.location,
|
|
56
|
+
).exclude(pk=obj.pk)
|
|
57
|
+
if duplicates.exists():
|
|
58
|
+
self.validation_error(
|
|
59
|
+
{
|
|
60
|
+
"__all__": (
|
|
61
|
+
f"A device named '{obj.name}' already exists in this location: {obj.location} and tenant: {obj.tenant}. "
|
|
62
|
+
"Device names must be unique per (Location, Tenant) when DEVICE_UNIQUENESS='location_tenant_name'."
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
elif uniqueness_mode == DeviceUniquenessChoices.NAME:
|
|
68
|
+
duplicates = Device.objects.filter(name=obj.name).exclude(pk=obj.pk)
|
|
69
|
+
if duplicates.exists():
|
|
70
|
+
self.validation_error(
|
|
71
|
+
{
|
|
72
|
+
"name": (
|
|
73
|
+
f"At least one other device named '{obj.name}' already exists. "
|
|
74
|
+
"Device names must be globally unique when DEVICE_UNIQUENESS='name'."
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
elif uniqueness_mode == "none":
|
|
80
|
+
# Explicitly no uniqueness enforcement
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
custom_validators = [DeviceUniquenessValidator]
|