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
|
@@ -132,14 +132,14 @@ class ApprovalWorkflowDefinitionViewTestCase(
|
|
|
132
132
|
ApprovalWorkflowDefinition.objects.create(
|
|
133
133
|
name=f"Test Approval Workflow {i}",
|
|
134
134
|
model_content_type=cls.scheduledjob_ct,
|
|
135
|
-
|
|
135
|
+
weight=i,
|
|
136
136
|
)
|
|
137
137
|
|
|
138
138
|
cls.form_data = {
|
|
139
139
|
"name": "Test Approval Workflow Definition 5",
|
|
140
140
|
"model_content_type": cls.scheduledjob_ct.pk,
|
|
141
141
|
"model_constraints": '{"name": "Bulk Delete Objects"}',
|
|
142
|
-
"
|
|
142
|
+
"weight": 5,
|
|
143
143
|
# These are the "management_form" fields required by the dynamic CustomFieldChoice formsets.
|
|
144
144
|
"approval_workflow_stage_definitions-TOTAL_FORMS": "0", # Set to 0 so validation succeeds until we need it
|
|
145
145
|
"approval_workflow_stage_definitions-INITIAL_FORMS": "1",
|
|
@@ -161,14 +161,14 @@ class ApprovalWorkflowStageDefinitionViewTestCase(ViewTestCases.PrimaryObjectVie
|
|
|
161
161
|
cls.approval_workflow_definition = ApprovalWorkflowDefinition.objects.create(
|
|
162
162
|
name="Test Approval Workflow Definition 1",
|
|
163
163
|
model_content_type=cls.scheduledjob_ct,
|
|
164
|
-
|
|
164
|
+
weight=10,
|
|
165
165
|
)
|
|
166
166
|
cls.approver_group = Group.objects.create(name="Test Group 1")
|
|
167
167
|
cls.updated_approver_group = Group.objects.create(name="Test Group 2")
|
|
168
168
|
# Deletable objects
|
|
169
169
|
ApprovalWorkflowStageDefinition.objects.create(
|
|
170
170
|
approval_workflow_definition=cls.approval_workflow_definition,
|
|
171
|
-
|
|
171
|
+
sequence=100,
|
|
172
172
|
name="Test Approval Workflow 1 Stage 1 Definition",
|
|
173
173
|
min_approvers=2,
|
|
174
174
|
denial_message="Stage 1 Denial Message",
|
|
@@ -176,7 +176,7 @@ class ApprovalWorkflowStageDefinitionViewTestCase(ViewTestCases.PrimaryObjectVie
|
|
|
176
176
|
)
|
|
177
177
|
ApprovalWorkflowStageDefinition.objects.create(
|
|
178
178
|
approval_workflow_definition=cls.approval_workflow_definition,
|
|
179
|
-
|
|
179
|
+
sequence=200,
|
|
180
180
|
name="Test Approval Workflow 1 Stage 2 Definition",
|
|
181
181
|
min_approvers=3,
|
|
182
182
|
denial_message="Stage 2 Denial Message",
|
|
@@ -184,7 +184,7 @@ class ApprovalWorkflowStageDefinitionViewTestCase(ViewTestCases.PrimaryObjectVie
|
|
|
184
184
|
)
|
|
185
185
|
ApprovalWorkflowStageDefinition.objects.create(
|
|
186
186
|
approval_workflow_definition=cls.approval_workflow_definition,
|
|
187
|
-
|
|
187
|
+
sequence=300,
|
|
188
188
|
name="Test Approval Workflow 1 Stage 3 Definition",
|
|
189
189
|
min_approvers=4,
|
|
190
190
|
denial_message="Stage 3 Denial Message",
|
|
@@ -192,7 +192,7 @@ class ApprovalWorkflowStageDefinitionViewTestCase(ViewTestCases.PrimaryObjectVie
|
|
|
192
192
|
)
|
|
193
193
|
ApprovalWorkflowStageDefinition.objects.create(
|
|
194
194
|
approval_workflow_definition=cls.approval_workflow_definition,
|
|
195
|
-
|
|
195
|
+
sequence=400,
|
|
196
196
|
name="Test Approval Workflow 1 Stage 4 Definition",
|
|
197
197
|
min_approvers=4,
|
|
198
198
|
denial_message="Stage 4 Denial Message",
|
|
@@ -200,7 +200,7 @@ class ApprovalWorkflowStageDefinitionViewTestCase(ViewTestCases.PrimaryObjectVie
|
|
|
200
200
|
)
|
|
201
201
|
ApprovalWorkflowStageDefinition.objects.create(
|
|
202
202
|
approval_workflow_definition=cls.approval_workflow_definition,
|
|
203
|
-
|
|
203
|
+
sequence=500,
|
|
204
204
|
name="Test Approval Workflow 1 Stage 5 Definition",
|
|
205
205
|
min_approvers=4,
|
|
206
206
|
denial_message="Stage 5 Denial Message",
|
|
@@ -209,7 +209,7 @@ class ApprovalWorkflowStageDefinitionViewTestCase(ViewTestCases.PrimaryObjectVie
|
|
|
209
209
|
|
|
210
210
|
cls.form_data = {
|
|
211
211
|
"approval_workflow_definition": cls.approval_workflow_definition.pk,
|
|
212
|
-
"
|
|
212
|
+
"sequence": 600,
|
|
213
213
|
"name": "Approval Workflow Stage 1 Definition",
|
|
214
214
|
"min_approvers": 2,
|
|
215
215
|
"denial_message": "Stage 1 is denied",
|
|
@@ -218,7 +218,7 @@ class ApprovalWorkflowStageDefinitionViewTestCase(ViewTestCases.PrimaryObjectVie
|
|
|
218
218
|
|
|
219
219
|
cls.update_data = {
|
|
220
220
|
"approval_workflow_definition": cls.approval_workflow_definition.pk,
|
|
221
|
-
"
|
|
221
|
+
"sequence": 700,
|
|
222
222
|
"name": "Updated approval workflow stage 1",
|
|
223
223
|
"min_approvers": 3,
|
|
224
224
|
"denial_message": "updated message",
|
|
@@ -226,7 +226,7 @@ class ApprovalWorkflowStageDefinitionViewTestCase(ViewTestCases.PrimaryObjectVie
|
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
cls.bulk_edit_data = {
|
|
229
|
-
"
|
|
229
|
+
"sequence": 800,
|
|
230
230
|
"min_approvers": 5,
|
|
231
231
|
"denial_message": "updated denial message",
|
|
232
232
|
}
|
|
@@ -264,7 +264,7 @@ class ApprovalWorkflowViewTestCase(
|
|
|
264
264
|
]
|
|
265
265
|
approval_workflow_definitions = [
|
|
266
266
|
ApprovalWorkflowDefinition.objects.create(
|
|
267
|
-
name=f"Test Approval Workflow {i}", model_content_type=cls.scheduledjob_ct,
|
|
267
|
+
name=f"Test Approval Workflow {i}", model_content_type=cls.scheduledjob_ct, weight=i
|
|
268
268
|
)
|
|
269
269
|
for i in range(5)
|
|
270
270
|
]
|
|
@@ -332,7 +332,7 @@ class ApprovalWorkflowStageViewTestCase(
|
|
|
332
332
|
ApprovalWorkflowDefinition.objects.create(
|
|
333
333
|
name=f"Test Approval Workflow {i}",
|
|
334
334
|
model_content_type=cls.scheduledjob_ct,
|
|
335
|
-
|
|
335
|
+
weight=i,
|
|
336
336
|
)
|
|
337
337
|
for i in range(5)
|
|
338
338
|
]
|
|
@@ -342,7 +342,7 @@ class ApprovalWorkflowStageViewTestCase(
|
|
|
342
342
|
cls.approval_workflow_stage_definitions.append(
|
|
343
343
|
ApprovalWorkflowStageDefinition.objects.create(
|
|
344
344
|
approval_workflow_definition=approval_workflow_definition,
|
|
345
|
-
|
|
345
|
+
sequence=i * 100,
|
|
346
346
|
name=f"Test Approval Workflow Stage {i} Definition",
|
|
347
347
|
min_approvers=i + 1,
|
|
348
348
|
denial_message=f"Stage {i} Denial Message",
|
|
@@ -516,7 +516,7 @@ class ApprovalWorkflowStageResponseViewTestCase(
|
|
|
516
516
|
ApprovalWorkflowDefinition.objects.create(
|
|
517
517
|
name=f"Test Approval Workflow {i} Definition",
|
|
518
518
|
model_content_type=cls.scheduledjob_ct,
|
|
519
|
-
|
|
519
|
+
weight=i,
|
|
520
520
|
)
|
|
521
521
|
for i in range(5)
|
|
522
522
|
]
|
|
@@ -526,7 +526,7 @@ class ApprovalWorkflowStageResponseViewTestCase(
|
|
|
526
526
|
cls.approval_workflow_stage_definitions.append(
|
|
527
527
|
ApprovalWorkflowStageDefinition.objects.create(
|
|
528
528
|
approval_workflow_definition=approval_workflow_definition,
|
|
529
|
-
|
|
529
|
+
sequence=i * 100,
|
|
530
530
|
name=f"Test Approval Workflow Stage {i} Definition",
|
|
531
531
|
min_approvers=i + 1,
|
|
532
532
|
denial_message=f"Stage {i} Denial Message",
|
|
@@ -2346,6 +2346,7 @@ class SavedViewTest(ModelViewTestCase):
|
|
|
2346
2346
|
self.assertIn(str(sv_shared.pk), response_body, msg=response_body)
|
|
2347
2347
|
self.assertNotIn(str(sv_not_shared.pk), response_body, msg=response_body)
|
|
2348
2348
|
|
|
2349
|
+
@tag("example_app")
|
|
2349
2350
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
2350
2351
|
def test_create_saved_views_contain_boolean_filter_params(self):
|
|
2351
2352
|
"""
|
|
@@ -3461,7 +3462,7 @@ class JobTestCase(
|
|
|
3461
3462
|
ApprovalWorkflowDefinition.objects.create(
|
|
3462
3463
|
name="Test Approval Workflow Definition 1",
|
|
3463
3464
|
model_content_type=ContentType.objects.get_for_model(ScheduledJob),
|
|
3464
|
-
|
|
3465
|
+
weight=0,
|
|
3465
3466
|
)
|
|
3466
3467
|
|
|
3467
3468
|
self.add_permissions("extras.run_job")
|
|
@@ -3491,7 +3492,7 @@ class JobTestCase(
|
|
|
3491
3492
|
ApprovalWorkflowDefinition.objects.create(
|
|
3492
3493
|
name="Approval Definition",
|
|
3493
3494
|
model_content_type=ContentType.objects.get_for_model(ScheduledJob),
|
|
3494
|
-
|
|
3495
|
+
weight=0,
|
|
3495
3496
|
)
|
|
3496
3497
|
data = {
|
|
3497
3498
|
"_schedule_type": "immediately",
|
|
@@ -3513,7 +3514,7 @@ class JobTestCase(
|
|
|
3513
3514
|
ApprovalWorkflowDefinition.objects.create(
|
|
3514
3515
|
name="Approval Definition",
|
|
3515
3516
|
model_content_type=ContentType.objects.get_for_model(ScheduledJob),
|
|
3516
|
-
|
|
3517
|
+
weight=0,
|
|
3517
3518
|
)
|
|
3518
3519
|
data = {
|
|
3519
3520
|
"_schedule_type": "future",
|
|
@@ -3574,7 +3575,7 @@ class JobTestCase(
|
|
|
3574
3575
|
ApprovalWorkflowDefinition.objects.create(
|
|
3575
3576
|
name="Approval Definition",
|
|
3576
3577
|
model_content_type=ContentType.objects.get_for_model(ScheduledJob),
|
|
3577
|
-
|
|
3578
|
+
weight=0,
|
|
3578
3579
|
)
|
|
3579
3580
|
|
|
3580
3581
|
data = {
|
|
@@ -3599,7 +3600,7 @@ class JobTestCase(
|
|
|
3599
3600
|
ApprovalWorkflowDefinition.objects.create(
|
|
3600
3601
|
name="Approval Definition",
|
|
3601
3602
|
model_content_type=ContentType.objects.get_for_model(ScheduledJob),
|
|
3602
|
-
|
|
3603
|
+
weight=0,
|
|
3603
3604
|
)
|
|
3604
3605
|
|
|
3605
3606
|
data = {
|
|
@@ -3625,7 +3626,7 @@ class JobTestCase(
|
|
|
3625
3626
|
ApprovalWorkflowDefinition.objects.create(
|
|
3626
3627
|
name="Approval Definition",
|
|
3627
3628
|
model_content_type=ContentType.objects.get_for_model(ScheduledJob),
|
|
3628
|
-
|
|
3629
|
+
weight=0,
|
|
3629
3630
|
)
|
|
3630
3631
|
data = {
|
|
3631
3632
|
"_schedule_type": "future",
|
|
@@ -3765,14 +3766,12 @@ class JobButtonRenderingTestCase(TestCase):
|
|
|
3765
3766
|
|
|
3766
3767
|
self.location_type = LocationType.objects.get(name="Campus")
|
|
3767
3768
|
|
|
3768
|
-
@tag("fix_in_v3")
|
|
3769
3769
|
def test_view_object_with_job_button(self):
|
|
3770
3770
|
"""Ensure that the job button is rendered."""
|
|
3771
3771
|
response = self.client.get(self.location_type.get_absolute_url(), follow=True)
|
|
3772
3772
|
self.assertBodyContains(response, f"JobButton {self.location_type.name}")
|
|
3773
3773
|
self.assertBodyContains(response, "Click me!")
|
|
3774
3774
|
|
|
3775
|
-
@tag("fix_in_v3")
|
|
3776
3775
|
def test_task_queue_hidden_input_is_present(self):
|
|
3777
3776
|
"""
|
|
3778
3777
|
Ensure that the job button respects the job class' task_queues and the job class default job queue is passed as a hidden form input.
|
|
@@ -3800,7 +3799,6 @@ class JobButtonRenderingTestCase(TestCase):
|
|
|
3800
3799
|
f'<input type="hidden" name="_job_queue" value="{self.job.default_job_queue.pk}">', content, content
|
|
3801
3800
|
)
|
|
3802
3801
|
|
|
3803
|
-
@tag("fix_in_v3")
|
|
3804
3802
|
def test_view_object_with_unsafe_text(self):
|
|
3805
3803
|
"""Ensure that JobButton text can't be used as a vector for XSS."""
|
|
3806
3804
|
self.job_button_1.text = '<script>alert("Hello world!")</script>'
|
|
@@ -3820,7 +3818,6 @@ class JobButtonRenderingTestCase(TestCase):
|
|
|
3820
3818
|
self.assertNotIn("<script>alert", content, content)
|
|
3821
3819
|
self.assertIn("<script>alert", content, content)
|
|
3822
3820
|
|
|
3823
|
-
@tag("fix_in_v3")
|
|
3824
3821
|
def test_view_object_with_unsafe_name(self):
|
|
3825
3822
|
"""Ensure that JobButton names can't be used as a vector for XSS."""
|
|
3826
3823
|
self.job_button_1.text = "JobButton {{ obj"
|
|
@@ -3832,7 +3829,6 @@ class JobButtonRenderingTestCase(TestCase):
|
|
|
3832
3829
|
self.assertNotIn("<script>alert", content, content)
|
|
3833
3830
|
self.assertIn("<script>alert", content, content)
|
|
3834
3831
|
|
|
3835
|
-
@tag("fix_in_v3")
|
|
3836
3832
|
def test_render_constrained_run_permissions(self):
|
|
3837
3833
|
obj_perm = ObjectPermission(
|
|
3838
3834
|
name="Test permission",
|
|
@@ -3851,8 +3847,9 @@ class JobButtonRenderingTestCase(TestCase):
|
|
|
3851
3847
|
NO_CONFIRM_BUTTON.format(
|
|
3852
3848
|
button_id=self.job_button_1.pk,
|
|
3853
3849
|
button_text=f"JobButton {self.location_type.name}",
|
|
3854
|
-
button_class=self.job_button_1.
|
|
3850
|
+
button_class=self.job_button_1.button_class_css_class,
|
|
3855
3851
|
disabled="",
|
|
3852
|
+
menu_item="",
|
|
3856
3853
|
),
|
|
3857
3854
|
content,
|
|
3858
3855
|
)
|
|
@@ -3860,8 +3857,9 @@ class JobButtonRenderingTestCase(TestCase):
|
|
|
3860
3857
|
NO_CONFIRM_BUTTON.format(
|
|
3861
3858
|
button_id=self.job_button_2.pk,
|
|
3862
3859
|
button_text="Click me!",
|
|
3863
|
-
button_class=self.job_button_2.
|
|
3860
|
+
button_class=self.job_button_2.button_class_css_class,
|
|
3864
3861
|
disabled="disabled",
|
|
3862
|
+
menu_item="",
|
|
3865
3863
|
),
|
|
3866
3864
|
content,
|
|
3867
3865
|
)
|
|
@@ -3882,6 +3880,7 @@ class JobButtonRenderingTestCase(TestCase):
|
|
|
3882
3880
|
button_text=f"JobButton {self.location_type.name}",
|
|
3883
3881
|
button_class="link",
|
|
3884
3882
|
disabled="",
|
|
3883
|
+
menu_item="dropdown-item",
|
|
3885
3884
|
)
|
|
3886
3885
|
+ "</li>",
|
|
3887
3886
|
content,
|
|
@@ -3893,12 +3892,14 @@ class JobButtonRenderingTestCase(TestCase):
|
|
|
3893
3892
|
button_text="Click me!",
|
|
3894
3893
|
button_class="link",
|
|
3895
3894
|
disabled="disabled",
|
|
3895
|
+
menu_item="dropdown-item",
|
|
3896
3896
|
)
|
|
3897
3897
|
+ "</li>",
|
|
3898
3898
|
content,
|
|
3899
3899
|
)
|
|
3900
3900
|
|
|
3901
3901
|
|
|
3902
|
+
@tag("example_app")
|
|
3902
3903
|
class JobCustomTemplateTestCase(TestCase):
|
|
3903
3904
|
@classmethod
|
|
3904
3905
|
def setUpTestData(cls):
|
|
@@ -4611,7 +4612,7 @@ class RoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase, ViewTestCases
|
|
|
4611
4612
|
if result == "Contact Associations":
|
|
4612
4613
|
# AssociationContact Table in the contact tab should be there.
|
|
4613
4614
|
self.assertInHTML(
|
|
4614
|
-
f'<strong>{result}</strong><div class="
|
|
4615
|
+
f'<strong>{result}</strong><div class="float-end d-print-none">',
|
|
4615
4616
|
response_body,
|
|
4616
4617
|
)
|
|
4617
4618
|
# ContactAssociationTable related to this role instances should not be there.
|
nautobot/extras/views.py
CHANGED
|
@@ -24,6 +24,7 @@ from django_tables2 import RequestConfig
|
|
|
24
24
|
from jsonschema.validators import Draft7Validator
|
|
25
25
|
from rest_framework.decorators import action
|
|
26
26
|
from rest_framework.permissions import IsAuthenticated
|
|
27
|
+
from rest_framework.response import Response
|
|
27
28
|
|
|
28
29
|
from nautobot.core.choices import ButtonActionColorChoices
|
|
29
30
|
from nautobot.core.constants import PAGINATE_COUNT_DEFAULT
|
|
@@ -90,6 +91,8 @@ from nautobot.ipam.models import IPAddress, Prefix, VLAN
|
|
|
90
91
|
from nautobot.ipam.tables import IPAddressTable, PrefixTable, VLANTable
|
|
91
92
|
from nautobot.virtualization.models import VirtualMachine, VMInterface
|
|
92
93
|
from nautobot.virtualization.tables import VirtualMachineTable, VMInterfaceTable
|
|
94
|
+
from nautobot.vpn.models import VPN, VPNProfile, VPNTunnel, VPNTunnelEndpoint
|
|
95
|
+
from nautobot.vpn.tables import VPNProfileTable, VPNTable, VPNTunnelEndpointTable, VPNTunnelTable
|
|
93
96
|
|
|
94
97
|
from . import filters, forms, jobs_ui, tables
|
|
95
98
|
from .api import serializers
|
|
@@ -1262,12 +1265,7 @@ class DynamicGroupUIViewSet(NautobotUIViewSet):
|
|
|
1262
1265
|
elif self.action == "retrieve":
|
|
1263
1266
|
model = instance.model
|
|
1264
1267
|
table_class = get_table_for_model(model)
|
|
1265
|
-
|
|
1266
|
-
# Ensure that members cache is up-to-date for this specific group
|
|
1267
|
-
members = instance.update_cached_members()
|
|
1268
|
-
messages.success(request, f"Refreshed cached members list for {instance}")
|
|
1269
|
-
else:
|
|
1270
|
-
members = instance.members
|
|
1268
|
+
members = instance.members
|
|
1271
1269
|
if table_class is not None:
|
|
1272
1270
|
if hasattr(members, "without_tree_fields"):
|
|
1273
1271
|
members = members.without_tree_fields()
|
|
@@ -1322,7 +1320,7 @@ class DynamicGroupUIViewSet(NautobotUIViewSet):
|
|
|
1322
1320
|
|
|
1323
1321
|
return context
|
|
1324
1322
|
|
|
1325
|
-
def form_save(self, form, **kwargs):
|
|
1323
|
+
def form_save(self, form, commit=True, **kwargs):
|
|
1326
1324
|
obj = form.save(commit=False)
|
|
1327
1325
|
context = self.get_extra_context(self.request, obj)
|
|
1328
1326
|
|
|
@@ -1341,10 +1339,18 @@ class DynamicGroupUIViewSet(NautobotUIViewSet):
|
|
|
1341
1339
|
form.add_error(None, msg)
|
|
1342
1340
|
raise
|
|
1343
1341
|
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1342
|
+
if commit:
|
|
1343
|
+
# After filters have been set, now we save the object to the database.
|
|
1344
|
+
obj.save(update_cached_members=False)
|
|
1345
|
+
# Save m2m fields, such as Tags https://docs.djangoproject.com/en/3.2/topics/forms/modelforms/#the-save-method
|
|
1346
|
+
form.save_m2m()
|
|
1347
|
+
|
|
1348
|
+
if obj.group_type != DynamicGroupTypeChoices.TYPE_STATIC:
|
|
1349
|
+
messages.warning(
|
|
1350
|
+
self.request,
|
|
1351
|
+
"Dynamic Group membership is not automatically recalculated after creating/editing the group, "
|
|
1352
|
+
'as it may take some time to complete. You can use the "Refresh Members" button when ready.',
|
|
1353
|
+
)
|
|
1348
1354
|
|
|
1349
1355
|
# Process the formsets for children
|
|
1350
1356
|
children = context.get("children")
|
|
@@ -1359,7 +1365,7 @@ class DynamicGroupUIViewSet(NautobotUIViewSet):
|
|
|
1359
1365
|
added_errors.add(msg)
|
|
1360
1366
|
raise ValidationError("invalid DynamicGroupMembershipFormSet")
|
|
1361
1367
|
|
|
1362
|
-
if children:
|
|
1368
|
+
if commit and children:
|
|
1363
1369
|
children.save()
|
|
1364
1370
|
|
|
1365
1371
|
return obj
|
|
@@ -1658,6 +1664,7 @@ class GitRepositoryUIViewSet(NautobotUIViewSet):
|
|
|
1658
1664
|
filterset_class = filters.GitRepositoryFilterSet
|
|
1659
1665
|
serializer_class = serializers.GitRepositorySerializer
|
|
1660
1666
|
table_class = tables.GitRepositoryTable
|
|
1667
|
+
view_titles = Titles(titles={"result": "{{ object.display|default:object }} - Synchronization Status"})
|
|
1661
1668
|
|
|
1662
1669
|
def get_extra_context(self, request, instance=None):
|
|
1663
1670
|
context = super().get_extra_context(request, instance)
|
|
@@ -1701,14 +1708,18 @@ class GitRepositoryUIViewSet(NautobotUIViewSet):
|
|
|
1701
1708
|
job_result = instance.get_latest_sync()
|
|
1702
1709
|
|
|
1703
1710
|
context = {
|
|
1711
|
+
**super().get_extra_context(request, instance),
|
|
1704
1712
|
"result": job_result or {},
|
|
1705
|
-
"base_template": "extras/
|
|
1713
|
+
"base_template": "extras/gitrepository_retrieve.html",
|
|
1706
1714
|
"object": instance,
|
|
1707
1715
|
"active_tab": "result",
|
|
1708
1716
|
"verbose_name": instance._meta.verbose_name,
|
|
1709
1717
|
}
|
|
1710
1718
|
|
|
1711
|
-
return
|
|
1719
|
+
return Response(
|
|
1720
|
+
context,
|
|
1721
|
+
template_name="extras/gitrepository_result.html",
|
|
1722
|
+
)
|
|
1712
1723
|
|
|
1713
1724
|
@action(
|
|
1714
1725
|
detail=True,
|
|
@@ -1755,6 +1766,27 @@ class GraphQLQueryUIViewSet(
|
|
|
1755
1766
|
table_class = tables.GraphQLQueryTable
|
|
1756
1767
|
action_buttons = ("add",)
|
|
1757
1768
|
|
|
1769
|
+
object_detail_content = object_detail.ObjectDetailContent(
|
|
1770
|
+
panels=(
|
|
1771
|
+
object_detail.ObjectFieldsPanel(
|
|
1772
|
+
label="Query",
|
|
1773
|
+
weight=100,
|
|
1774
|
+
section=SectionChoices.LEFT_HALF,
|
|
1775
|
+
fields=["name", "query", "variables"],
|
|
1776
|
+
value_transforms={
|
|
1777
|
+
"query": [lambda val: format_html('<pre><code class="language-graphql">{}</code></pre>', val)],
|
|
1778
|
+
"variables": [lambda val: helpers.render_json(val, syntax_highlight=True, pretty_print=True)],
|
|
1779
|
+
},
|
|
1780
|
+
),
|
|
1781
|
+
object_detail.Panel(
|
|
1782
|
+
weight=100,
|
|
1783
|
+
section=object_detail.SectionChoices.RIGHT_HALF,
|
|
1784
|
+
body_content_template_path="extras/inc/graphqlquery_execute.html",
|
|
1785
|
+
body_wrapper_template_path="components/panel/body_wrapper_table.html",
|
|
1786
|
+
),
|
|
1787
|
+
)
|
|
1788
|
+
)
|
|
1789
|
+
|
|
1758
1790
|
|
|
1759
1791
|
#
|
|
1760
1792
|
# Image attachments
|
|
@@ -2086,7 +2118,7 @@ class JobRunView(ObjectPermissionRequiredMixin, View):
|
|
|
2086
2118
|
|
|
2087
2119
|
class JobView(generic.ObjectView):
|
|
2088
2120
|
queryset = JobModel.objects.all()
|
|
2089
|
-
template_name = "
|
|
2121
|
+
template_name = "generic/object_retrieve.html"
|
|
2090
2122
|
object_detail_content = object_detail.ObjectDetailContent(
|
|
2091
2123
|
panels=[
|
|
2092
2124
|
object_detail.ObjectFieldsPanel(
|
|
@@ -2701,6 +2733,12 @@ class ObjectChangeUIViewSet(ObjectDetailViewMixin, ObjectListViewMixin):
|
|
|
2701
2733
|
table_class = tables.ObjectChangeTable
|
|
2702
2734
|
action_buttons = ("export",)
|
|
2703
2735
|
|
|
2736
|
+
def __init__(self, *args, **kwargs):
|
|
2737
|
+
super().__init__(*args, **kwargs)
|
|
2738
|
+
self.object_detail_content = object_detail.ObjectDetailContent()
|
|
2739
|
+
# Remove "Advanced" tab while keeping the main.
|
|
2740
|
+
self.object_detail_content.tabs = self.object_detail_content.tabs[:1]
|
|
2741
|
+
|
|
2704
2742
|
# 2.0 TODO: Remove this remapping and solve it at the `BaseFilterSet` as it is addressing a breaking change.
|
|
2705
2743
|
def get(self, request, *args, **kwargs):
|
|
2706
2744
|
# Remappings below allow previous queries of time_before and time_after to use
|
|
@@ -2789,7 +2827,7 @@ class ObjectChangeLogView(generic.GenericView):
|
|
|
2789
2827
|
|
|
2790
2828
|
return render(
|
|
2791
2829
|
request,
|
|
2792
|
-
"
|
|
2830
|
+
"generic/object_changelog.html",
|
|
2793
2831
|
{
|
|
2794
2832
|
"object": obj,
|
|
2795
2833
|
"verbose_name": obj._meta.verbose_name,
|
|
@@ -2996,7 +3034,7 @@ class ObjectNotesView(generic.GenericView):
|
|
|
2996
3034
|
|
|
2997
3035
|
return render(
|
|
2998
3036
|
request,
|
|
2999
|
-
"
|
|
3037
|
+
"generic/object_notes.html",
|
|
3000
3038
|
{
|
|
3001
3039
|
"object": obj,
|
|
3002
3040
|
"verbose_name": obj._meta.verbose_name,
|
|
@@ -3171,6 +3209,30 @@ class RoleUIViewSet(viewsets.NautobotUIViewSet):
|
|
|
3171
3209
|
vdc_table.columns.hide("role")
|
|
3172
3210
|
RequestConfig(request, paginate).configure(vdc_table)
|
|
3173
3211
|
context["vdc_table"] = vdc_table
|
|
3212
|
+
if ContentType.objects.get_for_model(VPN) in context["content_types"]:
|
|
3213
|
+
vpns = instance.vpns.restrict(request.user, "view")
|
|
3214
|
+
vpn_table = VPNTable(vpns)
|
|
3215
|
+
vpn_table.columns.hide("role")
|
|
3216
|
+
RequestConfig(request, paginate).configure(vpn_table)
|
|
3217
|
+
context["vpn_table"] = vpn_table
|
|
3218
|
+
if ContentType.objects.get_for_model(VPNProfile) in context["content_types"]:
|
|
3219
|
+
vpn_profiles = instance.vpn_profiles.restrict(request.user, "view")
|
|
3220
|
+
vpn_profile_table = VPNProfileTable(vpn_profiles)
|
|
3221
|
+
vpn_profile_table.columns.hide("role")
|
|
3222
|
+
RequestConfig(request, paginate).configure(vpn_profile_table)
|
|
3223
|
+
context["vpn_profile_table"] = vpn_profile_table
|
|
3224
|
+
if ContentType.objects.get_for_model(VPNTunnel) in context["content_types"]:
|
|
3225
|
+
vpn_tunnels = instance.vpn_tunnels.restrict(request.user, "view")
|
|
3226
|
+
vpn_tunnel_table = VPNTunnelTable(vpn_tunnels)
|
|
3227
|
+
vpn_tunnel_table.columns.hide("role")
|
|
3228
|
+
RequestConfig(request, paginate).configure(vpn_tunnel_table)
|
|
3229
|
+
context["vpn_tunnel_table"] = vpn_tunnel_table
|
|
3230
|
+
if ContentType.objects.get_for_model(VPNTunnelEndpoint) in context["content_types"]:
|
|
3231
|
+
vpn_tunnel_endpoints = instance.vpn_tunnel_endpoints.restrict(request.user, "view")
|
|
3232
|
+
vpn_tunnel_endpoint_table = VPNTunnelEndpointTable(vpn_tunnel_endpoints)
|
|
3233
|
+
vpn_tunnel_endpoint_table.columns.hide("role")
|
|
3234
|
+
RequestConfig(request, paginate).configure(vpn_tunnel_endpoint_table)
|
|
3235
|
+
context["vpn_tunnel_endpoint_table"] = vpn_tunnel_endpoint_table
|
|
3174
3236
|
return context
|
|
3175
3237
|
|
|
3176
3238
|
|
|
@@ -3490,8 +3552,8 @@ class WebhookUIViewSet(NautobotUIViewSet):
|
|
|
3490
3552
|
|
|
3491
3553
|
|
|
3492
3554
|
class JobObjectChangeLogView(ObjectChangeLogView):
|
|
3493
|
-
base_template = "
|
|
3555
|
+
base_template = "generic/object_retrieve.html"
|
|
3494
3556
|
|
|
3495
3557
|
|
|
3496
3558
|
class JobObjectNotesView(ObjectNotesView):
|
|
3497
|
-
base_template = "
|
|
3559
|
+
base_template = "generic/object_retrieve.html"
|
nautobot/ipam/factory.py
CHANGED
|
@@ -278,9 +278,16 @@ class NamespaceFactory(PrimaryModelFactory):
|
|
|
278
278
|
|
|
279
279
|
class Meta:
|
|
280
280
|
model = Namespace
|
|
281
|
+
exclude = ("has_tenant", "has_description")
|
|
281
282
|
|
|
282
283
|
name = UniqueFaker("text", max_nb_chars=20) # could be up to CHARFIELD_MAX_LENGTH but that gets unwieldy fast
|
|
283
284
|
|
|
285
|
+
has_tenant = NautobotBoolIterator()
|
|
286
|
+
tenant = factory.Maybe("has_tenant", random_instance(Tenant))
|
|
287
|
+
|
|
288
|
+
has_description = NautobotBoolIterator()
|
|
289
|
+
description = factory.Maybe("has_description", factory.Faker("text", max_nb_chars=CHARFIELD_MAX_LENGTH), "")
|
|
290
|
+
|
|
284
291
|
|
|
285
292
|
class PrefixFactory(PrimaryModelFactory):
|
|
286
293
|
"""Create random Prefix objects with randomized data.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
|
|
3
|
+
from nautobot.core.filters import NaturalKeyOrPKMultipleChoiceFilter
|
|
4
|
+
from nautobot.ipam import formfields
|
|
5
|
+
from nautobot.ipam.models import Prefix
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PrefixFilter(NaturalKeyOrPKMultipleChoiceFilter):
|
|
9
|
+
"""
|
|
10
|
+
Filter that supports filtering a foreign key to Prefix by either its PK or by a literal `prefix` string.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
field_class = formfields.PrefixFilterFormField
|
|
14
|
+
|
|
15
|
+
def __init__(self, *args, **kwargs):
|
|
16
|
+
kwargs.setdefault("to_field_name", "pk")
|
|
17
|
+
kwargs.setdefault("label", "Prefix (ID or prefix string)")
|
|
18
|
+
kwargs.setdefault("queryset", Prefix.objects.all())
|
|
19
|
+
super().__init__(*args, **kwargs)
|
|
20
|
+
|
|
21
|
+
def get_filter_predicate(self, v):
|
|
22
|
+
# Null value filtering
|
|
23
|
+
if v is None:
|
|
24
|
+
return {f"{self.field_name}__isnull": True}
|
|
25
|
+
|
|
26
|
+
# If value is a model instance, stringify it to a pk.
|
|
27
|
+
if isinstance(v, Prefix):
|
|
28
|
+
v = v.pk
|
|
29
|
+
|
|
30
|
+
# Try to cast the value to a UUID to distinguish between PKs and prefix strings
|
|
31
|
+
v = str(v)
|
|
32
|
+
try:
|
|
33
|
+
uuid.UUID(v)
|
|
34
|
+
return {self.field_name: v}
|
|
35
|
+
except (AttributeError, TypeError, ValueError):
|
|
36
|
+
# It's a prefix string
|
|
37
|
+
prefixes_queryset = Prefix.objects.net_equals(v)
|
|
38
|
+
return {f"{self.field_name}__in": prefixes_queryset.values_list("pk", flat=True)}
|
nautobot/ipam/filters.py
CHANGED
|
@@ -19,12 +19,15 @@ from nautobot.core.filters import (
|
|
|
19
19
|
SearchFilter,
|
|
20
20
|
TreeNodeMultipleChoiceFilter,
|
|
21
21
|
)
|
|
22
|
+
from nautobot.core.utils.data import is_uuid
|
|
22
23
|
from nautobot.dcim.filters import LocatableModelFilterSetMixin
|
|
23
24
|
from nautobot.dcim.models import Device, Interface, Location, VirtualDeviceContext
|
|
24
25
|
from nautobot.extras.filters import NautobotFilterSet, RoleModelFilterSetMixin, StatusModelFilterSetMixin
|
|
25
|
-
from nautobot.ipam import choices
|
|
26
|
-
from nautobot.
|
|
26
|
+
from nautobot.ipam import choices
|
|
27
|
+
from nautobot.ipam.filter_mixins import PrefixFilter
|
|
28
|
+
from nautobot.tenancy.filter_mixins import TenancyModelFilterSetMixin
|
|
27
29
|
from nautobot.virtualization.models import VirtualMachine, VMInterface
|
|
30
|
+
from nautobot.vpn.models import VPNTunnelEndpoint
|
|
28
31
|
|
|
29
32
|
from .models import (
|
|
30
33
|
IPAddress,
|
|
@@ -57,40 +60,7 @@ __all__ = (
|
|
|
57
60
|
)
|
|
58
61
|
|
|
59
62
|
|
|
60
|
-
class
|
|
61
|
-
"""
|
|
62
|
-
Filter that supports filtering a foreign key to Prefix by either its PK or by a literal `prefix` string.
|
|
63
|
-
"""
|
|
64
|
-
|
|
65
|
-
field_class = formfields.PrefixFilterFormField
|
|
66
|
-
|
|
67
|
-
def __init__(self, *args, **kwargs):
|
|
68
|
-
kwargs.setdefault("to_field_name", "pk")
|
|
69
|
-
kwargs.setdefault("label", "Prefix (ID or prefix string)")
|
|
70
|
-
kwargs.setdefault("queryset", Prefix.objects.all())
|
|
71
|
-
super().__init__(*args, **kwargs)
|
|
72
|
-
|
|
73
|
-
def get_filter_predicate(self, v):
|
|
74
|
-
# Null value filtering
|
|
75
|
-
if v is None:
|
|
76
|
-
return {f"{self.field_name}__isnull": True}
|
|
77
|
-
|
|
78
|
-
# If value is a model instance, stringify it to a pk.
|
|
79
|
-
if isinstance(v, Prefix):
|
|
80
|
-
v = v.pk
|
|
81
|
-
|
|
82
|
-
# Try to cast the value to a UUID to distinguish between PKs and prefix strings
|
|
83
|
-
v = str(v)
|
|
84
|
-
try:
|
|
85
|
-
uuid.UUID(v)
|
|
86
|
-
return {self.field_name: v}
|
|
87
|
-
except (AttributeError, TypeError, ValueError):
|
|
88
|
-
# It's a prefix string
|
|
89
|
-
prefixes_queryset = Prefix.objects.net_equals(v)
|
|
90
|
-
return {f"{self.field_name}__in": prefixes_queryset.values_list("pk", flat=True)}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
class NamespaceFilterSet(NautobotFilterSet):
|
|
63
|
+
class NamespaceFilterSet(NautobotFilterSet, TenancyModelFilterSetMixin):
|
|
94
64
|
q = SearchFilter(
|
|
95
65
|
filter_predicates={
|
|
96
66
|
"name": "icontains",
|
|
@@ -320,13 +290,25 @@ class PrefixFilterSet(
|
|
|
320
290
|
queryset=CloudNetwork.objects.all(),
|
|
321
291
|
to_field_name="name",
|
|
322
292
|
)
|
|
293
|
+
vpn_tunnel_endpoints = NaturalKeyOrPKMultipleChoiceFilter(
|
|
294
|
+
queryset=VPNTunnelEndpoint.objects.all(),
|
|
295
|
+
to_field_name="pk",
|
|
296
|
+
label="VPN Tunnel Endpoint ID",
|
|
297
|
+
)
|
|
323
298
|
|
|
324
299
|
class Meta:
|
|
325
300
|
model = Prefix
|
|
326
301
|
fields = ["date_allocated", "id", "prefix_length", "tags"]
|
|
327
302
|
|
|
328
303
|
def _strip_values(self, values):
|
|
329
|
-
|
|
304
|
+
result = []
|
|
305
|
+
for value in values:
|
|
306
|
+
value = value.strip()
|
|
307
|
+
if is_uuid(value):
|
|
308
|
+
result.append(Prefix.objects.get(pk=value).prefix)
|
|
309
|
+
elif value:
|
|
310
|
+
result.append(value)
|
|
311
|
+
return result
|
|
330
312
|
|
|
331
313
|
def filter_prefix(self, queryset, name, value):
|
|
332
314
|
prefixes = self._strip_values(value)
|
|
@@ -505,7 +487,14 @@ class IPAddressFilterSet(
|
|
|
505
487
|
return queryset.filter(params)
|
|
506
488
|
|
|
507
489
|
def search_by_prefix(self, queryset, name, value):
|
|
508
|
-
prefixes = [
|
|
490
|
+
prefixes = []
|
|
491
|
+
for prefix in value:
|
|
492
|
+
prefix = prefix.strip()
|
|
493
|
+
if is_uuid(prefix):
|
|
494
|
+
prefixes.append(Prefix.objects.get(pk=prefix).prefix)
|
|
495
|
+
elif prefix:
|
|
496
|
+
prefixes.append(prefix)
|
|
497
|
+
|
|
509
498
|
return queryset.net_host_contained(*prefixes)
|
|
510
499
|
|
|
511
500
|
def filter_address(self, queryset, name, value):
|
nautobot/ipam/formfields.py
CHANGED
|
@@ -67,7 +67,7 @@ class IPNetworkFormField(forms.Field):
|
|
|
67
67
|
class PrefixFilterFormField(MultiMatchModelMultipleChoiceField):
|
|
68
68
|
@property
|
|
69
69
|
def filter(self):
|
|
70
|
-
from nautobot.ipam.
|
|
70
|
+
from nautobot.ipam.filter_mixins import PrefixFilter # avoid circular definition
|
|
71
71
|
|
|
72
72
|
return PrefixFilter
|
|
73
73
|
|