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
|
@@ -10,7 +10,7 @@ from django.contrib.auth import get_user_model
|
|
|
10
10
|
from django.contrib.auth.models import Group
|
|
11
11
|
from django.contrib.contenttypes.models import ContentType
|
|
12
12
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
|
13
|
-
from django.test import override_settings
|
|
13
|
+
from django.test import override_settings, tag
|
|
14
14
|
from django.urls import reverse
|
|
15
15
|
from django.utils.timezone import make_aware, now
|
|
16
16
|
from rest_framework import status
|
|
@@ -144,7 +144,7 @@ class ApprovalWorkflowStageTest(
|
|
|
144
144
|
|
|
145
145
|
cls.approval_workflow_definitions = [
|
|
146
146
|
ApprovalWorkflowDefinition.objects.create(
|
|
147
|
-
name=f"Test Approval Workflow {i}", model_content_type=cls.scheduledjob_ct,
|
|
147
|
+
name=f"Test Approval Workflow {i}", model_content_type=cls.scheduledjob_ct, weight=i
|
|
148
148
|
)
|
|
149
149
|
for i in range(4)
|
|
150
150
|
]
|
|
@@ -160,7 +160,7 @@ class ApprovalWorkflowStageTest(
|
|
|
160
160
|
cls.approval_workflow_stage_definitions = [
|
|
161
161
|
ApprovalWorkflowStageDefinition.objects.create(
|
|
162
162
|
approval_workflow_definition=cls.approval_workflow_definitions[i],
|
|
163
|
-
|
|
163
|
+
sequence=i * 100,
|
|
164
164
|
name=f"Test Approval Workflow Stage {i} Definition",
|
|
165
165
|
min_approvers=1,
|
|
166
166
|
denial_message="Stage Denial Message",
|
|
@@ -403,7 +403,7 @@ class ApprovalWorkflowStageTest(
|
|
|
403
403
|
|
|
404
404
|
approval_workflow_stage_definition_2 = ApprovalWorkflowStageDefinition.objects.create(
|
|
405
405
|
approval_workflow_definition=approval_workflow.approval_workflow_definition,
|
|
406
|
-
|
|
406
|
+
sequence=200,
|
|
407
407
|
name="Approval Workflow Stage Definition 2",
|
|
408
408
|
min_approvers=1,
|
|
409
409
|
denial_message="Stage 2 Denial Message",
|
|
@@ -496,7 +496,7 @@ class ApprovalWorkflowStageTest(
|
|
|
496
496
|
|
|
497
497
|
approval_workflow_stage_definition_2 = ApprovalWorkflowStageDefinition.objects.create(
|
|
498
498
|
approval_workflow_definition=approval_workflow.approval_workflow_definition,
|
|
499
|
-
|
|
499
|
+
sequence=200,
|
|
500
500
|
name="Approval Workflow Stage Definition 2",
|
|
501
501
|
min_approvers=1,
|
|
502
502
|
denial_message="Stage 2 Denial Message",
|
|
@@ -645,7 +645,7 @@ class ApprovalWorkflowStageTest(
|
|
|
645
645
|
approver_group_2 = Group.objects.create(name="Approver Group 2")
|
|
646
646
|
approval_workflow_stage_definition_approver_group_2 = ApprovalWorkflowStageDefinition.objects.create(
|
|
647
647
|
approval_workflow_definition=self.approval_workflow_definitions[3],
|
|
648
|
-
|
|
648
|
+
sequence=100,
|
|
649
649
|
name="Test Approval Workflow Stage 1 Definition",
|
|
650
650
|
min_approvers=1,
|
|
651
651
|
denial_message="Stage Denial Message",
|
|
@@ -986,6 +986,9 @@ class ContactTest(APIViewTestCases.APIViewTestCase):
|
|
|
986
986
|
bulk_update_data = {
|
|
987
987
|
"address": "Carnegie Hall, New York, NY",
|
|
988
988
|
}
|
|
989
|
+
validation_excluded_fields = [
|
|
990
|
+
"teams", # M2M field, excluded by default
|
|
991
|
+
]
|
|
989
992
|
|
|
990
993
|
@classmethod
|
|
991
994
|
def setUpTestData(cls):
|
|
@@ -1793,6 +1796,7 @@ class GitRepositoryTest(APIViewTestCases.APIViewTestCase):
|
|
|
1793
1796
|
self.assertEqual(response.data["message"], f"Repository {self.repos[0].name} sync job added to queue.")
|
|
1794
1797
|
self.assertIsInstance(response.data["job_result"], dict)
|
|
1795
1798
|
|
|
1799
|
+
@tag("example_app")
|
|
1796
1800
|
def test_create_with_app_provided_contents(self):
|
|
1797
1801
|
"""Test that `provided_contents` published by an App works."""
|
|
1798
1802
|
self.add_permissions("extras.add_gitrepository")
|
|
@@ -2215,7 +2219,7 @@ class JobTest(
|
|
|
2215
2219
|
ApprovalWorkflowDefinition.objects.create(
|
|
2216
2220
|
name="Test Approval Workflow Definition 1",
|
|
2217
2221
|
model_content_type=ContentType.objects.get_for_model(ScheduledJob),
|
|
2218
|
-
|
|
2222
|
+
weight=0,
|
|
2219
2223
|
)
|
|
2220
2224
|
|
|
2221
2225
|
# Do the stuff.
|
|
@@ -2481,13 +2485,14 @@ class JobTest(
|
|
|
2481
2485
|
"Unable to schedule job: Job may have sensitive input variables",
|
|
2482
2486
|
)
|
|
2483
2487
|
|
|
2488
|
+
@tag("example_app")
|
|
2484
2489
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
2485
2490
|
@mock.patch("nautobot.extras.api.views.get_worker_count")
|
|
2486
2491
|
def test_run_a_job_with_sensitive_variables_when_approval_workflow_defined(self, mock_get_worker_count):
|
|
2487
2492
|
ApprovalWorkflowDefinition.objects.create(
|
|
2488
2493
|
name="Test Approval Workflow Definition 1",
|
|
2489
2494
|
model_content_type=ContentType.objects.get_for_model(ScheduledJob),
|
|
2490
|
-
|
|
2495
|
+
weight=0,
|
|
2491
2496
|
)
|
|
2492
2497
|
|
|
2493
2498
|
mock_get_worker_count.return_value = 1
|
|
@@ -2702,7 +2707,7 @@ class JobTest(
|
|
|
2702
2707
|
ApprovalWorkflowDefinition.objects.create(
|
|
2703
2708
|
name="Test Approval Workflow Definition 1",
|
|
2704
2709
|
model_content_type=ContentType.objects.get_for_model(ScheduledJob),
|
|
2705
|
-
|
|
2710
|
+
weight=0,
|
|
2706
2711
|
)
|
|
2707
2712
|
|
|
2708
2713
|
mock_get_worker_count.return_value = 1
|
|
@@ -2802,7 +2807,7 @@ class JobTest(
|
|
|
2802
2807
|
ApprovalWorkflowDefinition.objects.create(
|
|
2803
2808
|
name="Approval Definition",
|
|
2804
2809
|
model_content_type=ContentType.objects.get_for_model(ScheduledJob),
|
|
2805
|
-
|
|
2810
|
+
weight=0,
|
|
2806
2811
|
)
|
|
2807
2812
|
|
|
2808
2813
|
start_time = now() + timedelta(minutes=1)
|
|
@@ -3274,6 +3279,7 @@ class UserSavedViewAssociationTest(APIViewTestCases.APIViewTestCase):
|
|
|
3274
3279
|
class ScheduledJobTest(
|
|
3275
3280
|
APIViewTestCases.GetObjectViewTestCase,
|
|
3276
3281
|
APIViewTestCases.ListObjectsViewTestCase,
|
|
3282
|
+
APIViewTestCases.DeleteObjectViewTestCase,
|
|
3277
3283
|
):
|
|
3278
3284
|
model = ScheduledJob
|
|
3279
3285
|
choices_fields = []
|
|
@@ -4744,9 +4750,9 @@ class TagTest(APIViewTestCases.APIViewTestCase):
|
|
|
4744
4750
|
data = {**self.create_data[0], "content_types": [Manufacturer._meta.label_lower]}
|
|
4745
4751
|
response = self.client.post(self._get_list_url(), data, format="json", **self.header)
|
|
4746
4752
|
|
|
4747
|
-
|
|
4753
|
+
tags = Tag.objects.filter(name=data["name"])
|
|
4748
4754
|
self.assertHttpStatus(response, 400)
|
|
4749
|
-
self.assertFalse(
|
|
4755
|
+
self.assertFalse(tags.exists())
|
|
4750
4756
|
self.assertIn(f"Invalid content type: {Manufacturer._meta.label_lower}", response.data["content_types"])
|
|
4751
4757
|
|
|
4752
4758
|
def test_create_tags_without_content_types(self):
|
|
@@ -4783,9 +4789,9 @@ class TagTest(APIViewTestCases.APIViewTestCase):
|
|
|
4783
4789
|
"""Test updating a tag without changing its content-types."""
|
|
4784
4790
|
self.add_permissions("extras.change_tag")
|
|
4785
4791
|
|
|
4786
|
-
|
|
4787
|
-
tag_content_types = list(
|
|
4788
|
-
url = self._get_detail_url(
|
|
4792
|
+
tag_instance = Tag.objects.exclude(content_types=ContentType.objects.get_for_model(Location)).first()
|
|
4793
|
+
tag_content_types = list(tag_instance.content_types.all())
|
|
4794
|
+
url = self._get_detail_url(tag_instance)
|
|
4789
4795
|
data = {"color": ColorChoices.COLOR_LIME}
|
|
4790
4796
|
|
|
4791
4797
|
response = self.client.patch(url, data, format="json", **self.header)
|
|
@@ -4795,9 +4801,9 @@ class TagTest(APIViewTestCases.APIViewTestCase):
|
|
|
4795
4801
|
sorted(response.data["content_types"]), sorted([f"{ct.app_label}.{ct.model}" for ct in tag_content_types])
|
|
4796
4802
|
)
|
|
4797
4803
|
|
|
4798
|
-
|
|
4799
|
-
self.assertEqual(
|
|
4800
|
-
self.assertEqual(list(
|
|
4804
|
+
tag_instance.refresh_from_db()
|
|
4805
|
+
self.assertEqual(tag_instance.color, ColorChoices.COLOR_LIME)
|
|
4806
|
+
self.assertEqual(list(tag_instance.content_types.all()), tag_content_types)
|
|
4801
4807
|
|
|
4802
4808
|
|
|
4803
4809
|
#
|
|
@@ -4810,6 +4816,9 @@ class TeamTest(APIViewTestCases.APIViewTestCase):
|
|
|
4810
4816
|
bulk_update_data = {
|
|
4811
4817
|
"address": "Carnegie Hall, New York, NY",
|
|
4812
4818
|
}
|
|
4819
|
+
validation_excluded_fields = [
|
|
4820
|
+
"contacts", # M2M field, excluded by default
|
|
4821
|
+
]
|
|
4813
4822
|
|
|
4814
4823
|
@classmethod
|
|
4815
4824
|
def setUpTestData(cls):
|
|
@@ -39,31 +39,31 @@ class ApprovalWorkflowTestMixin:
|
|
|
39
39
|
cls.approval_workflow_def_1 = models.ApprovalWorkflowDefinition.objects.create(
|
|
40
40
|
name="Test Approval Workflow Definition 1",
|
|
41
41
|
model_content_type=scheduled_job_ct,
|
|
42
|
-
|
|
42
|
+
weight=0,
|
|
43
43
|
)
|
|
44
44
|
cls.approval_workflow_def_2 = models.ApprovalWorkflowDefinition.objects.create(
|
|
45
45
|
name="Test Approval Workflow Definition 2",
|
|
46
46
|
model_content_type=scheduled_job_ct,
|
|
47
47
|
model_constraints={"name": "Bulk Delete Objects"},
|
|
48
|
-
|
|
48
|
+
weight=1,
|
|
49
49
|
)
|
|
50
50
|
cls.approval_workflow_def_3 = models.ApprovalWorkflowDefinition.objects.create(
|
|
51
51
|
name="Test Approval Workflow Definition 3",
|
|
52
52
|
model_content_type=scheduled_job_ct,
|
|
53
53
|
model_constraints={"name": "Bulk Delete Objects"},
|
|
54
|
-
|
|
54
|
+
weight=2,
|
|
55
55
|
)
|
|
56
56
|
cls.approval_workflow_def_4 = models.ApprovalWorkflowDefinition.objects.create(
|
|
57
57
|
name="Test Approval Workflow Definition 4",
|
|
58
58
|
model_content_type=scheduled_job_ct,
|
|
59
59
|
model_constraints={"name": "Bulk Delete Objects"},
|
|
60
|
-
|
|
60
|
+
weight=3,
|
|
61
61
|
)
|
|
62
62
|
cls.approval_workflow_def_5 = models.ApprovalWorkflowDefinition.objects.create(
|
|
63
63
|
name="Test Approval Workflow Definition 5",
|
|
64
64
|
model_content_type=scheduled_job_ct,
|
|
65
65
|
model_constraints={"name": "Bulk Delete Objects"},
|
|
66
|
-
|
|
66
|
+
weight=4,
|
|
67
67
|
)
|
|
68
68
|
cls.approval_workflow_1_instance_1 = models.ApprovalWorkflow.objects.create(
|
|
69
69
|
approval_workflow_definition=cls.approval_workflow_def_1,
|
|
@@ -97,7 +97,7 @@ class ApprovalWorkflowTestMixin:
|
|
|
97
97
|
)
|
|
98
98
|
cls.approval_workflow_1_stage_def_1 = models.ApprovalWorkflowStageDefinition.objects.create(
|
|
99
99
|
approval_workflow_definition=cls.approval_workflow_def_1,
|
|
100
|
-
|
|
100
|
+
sequence=100,
|
|
101
101
|
name="Test Approval Workflow 1 Stage 1",
|
|
102
102
|
min_approvers=2,
|
|
103
103
|
denial_message="Stage 1 Denial Message",
|
|
@@ -105,7 +105,7 @@ class ApprovalWorkflowTestMixin:
|
|
|
105
105
|
)
|
|
106
106
|
cls.approval_workflow_1_stage_def_2 = models.ApprovalWorkflowStageDefinition.objects.create(
|
|
107
107
|
approval_workflow_definition=cls.approval_workflow_def_1,
|
|
108
|
-
|
|
108
|
+
sequence=200,
|
|
109
109
|
name="Test Approval Workflow 1 Stage 2",
|
|
110
110
|
min_approvers=2,
|
|
111
111
|
denial_message="Stage 2 Denial Message",
|
|
@@ -113,7 +113,7 @@ class ApprovalWorkflowTestMixin:
|
|
|
113
113
|
)
|
|
114
114
|
cls.approval_workflow_1_stage_def_3 = models.ApprovalWorkflowStageDefinition.objects.create(
|
|
115
115
|
approval_workflow_definition=cls.approval_workflow_def_1,
|
|
116
|
-
|
|
116
|
+
sequence=300,
|
|
117
117
|
name="Test Approval Workflow 1 Stage 3",
|
|
118
118
|
min_approvers=2,
|
|
119
119
|
denial_message="Stage 3 Denial Message",
|
|
@@ -121,7 +121,7 @@ class ApprovalWorkflowTestMixin:
|
|
|
121
121
|
)
|
|
122
122
|
cls.approval_workflow_1_stage_def_4 = models.ApprovalWorkflowStageDefinition.objects.create(
|
|
123
123
|
approval_workflow_definition=cls.approval_workflow_def_1,
|
|
124
|
-
|
|
124
|
+
sequence=400,
|
|
125
125
|
name="Test Approval Workflow 1 Stage 4",
|
|
126
126
|
min_approvers=2,
|
|
127
127
|
denial_message="Stage 4 Denial Message",
|
|
@@ -129,7 +129,7 @@ class ApprovalWorkflowTestMixin:
|
|
|
129
129
|
)
|
|
130
130
|
cls.approval_workflow_1_stage_def_5 = models.ApprovalWorkflowStageDefinition.objects.create(
|
|
131
131
|
approval_workflow_definition=cls.approval_workflow_def_1,
|
|
132
|
-
|
|
132
|
+
sequence=500,
|
|
133
133
|
name="Test Approval Workflow 1 Stage 5",
|
|
134
134
|
min_approvers=2,
|
|
135
135
|
denial_message="Stage 5 Denial Message",
|
|
@@ -137,7 +137,7 @@ class ApprovalWorkflowTestMixin:
|
|
|
137
137
|
)
|
|
138
138
|
cls.approval_workflow_1_stage_def_6 = models.ApprovalWorkflowStageDefinition.objects.create(
|
|
139
139
|
approval_workflow_definition=cls.approval_workflow_def_1,
|
|
140
|
-
|
|
140
|
+
sequence=600,
|
|
141
141
|
name="Test Approval Workflow 1 Stage 6",
|
|
142
142
|
min_approvers=2,
|
|
143
143
|
denial_message="Stage 6 Denial Message",
|
|
@@ -145,7 +145,7 @@ class ApprovalWorkflowTestMixin:
|
|
|
145
145
|
)
|
|
146
146
|
cls.approval_workflow_2_stage_def_1 = models.ApprovalWorkflowStageDefinition.objects.create(
|
|
147
147
|
approval_workflow_definition=cls.approval_workflow_def_2,
|
|
148
|
-
|
|
148
|
+
sequence=100,
|
|
149
149
|
name="Test Approval Workflow 2 Stage 1",
|
|
150
150
|
min_approvers=2,
|
|
151
151
|
denial_message="Stage 1 Denial Message",
|
|
@@ -153,7 +153,7 @@ class ApprovalWorkflowTestMixin:
|
|
|
153
153
|
)
|
|
154
154
|
cls.approval_workflow_2_stage_def_2 = models.ApprovalWorkflowStageDefinition.objects.create(
|
|
155
155
|
approval_workflow_definition=cls.approval_workflow_def_2,
|
|
156
|
-
|
|
156
|
+
sequence=200,
|
|
157
157
|
name="Test Approval Workflow 2 Stage 2",
|
|
158
158
|
min_approvers=2,
|
|
159
159
|
denial_message="Stage 2 Denial Message",
|
|
@@ -161,7 +161,7 @@ class ApprovalWorkflowTestMixin:
|
|
|
161
161
|
)
|
|
162
162
|
cls.approval_workflow_2_stage_def_3 = models.ApprovalWorkflowStageDefinition.objects.create(
|
|
163
163
|
approval_workflow_definition=cls.approval_workflow_def_2,
|
|
164
|
-
|
|
164
|
+
sequence=300,
|
|
165
165
|
name="Test Approval Workflow 2 Stage 3",
|
|
166
166
|
min_approvers=2,
|
|
167
167
|
denial_message="Stage 3 Denial Message",
|
|
@@ -232,19 +232,19 @@ class ApprovalWorkflowDefinitionAPITest(ApprovalWorkflowTestMixin, APIViewTestCa
|
|
|
232
232
|
"name": "Approval Workflow Definition 1",
|
|
233
233
|
"model_content_type": "extras.scheduledjob",
|
|
234
234
|
"model_constraints": {"name": "Bulk Delete Objects"},
|
|
235
|
-
"
|
|
235
|
+
"weight": 5,
|
|
236
236
|
},
|
|
237
237
|
{
|
|
238
238
|
"name": "Approval Workflow Definition 2",
|
|
239
239
|
"model_content_type": "extras.scheduledjob",
|
|
240
240
|
"model_constraints": {},
|
|
241
|
-
"
|
|
241
|
+
"weight": 6,
|
|
242
242
|
},
|
|
243
243
|
{
|
|
244
244
|
"name": "Approval Workflow Definition 3",
|
|
245
245
|
"model_content_type": "extras.scheduledjob",
|
|
246
246
|
"model_constraints": {"name": "Bulk Delete Objects"},
|
|
247
|
-
"
|
|
247
|
+
"weight": 7,
|
|
248
248
|
},
|
|
249
249
|
]
|
|
250
250
|
|
|
@@ -271,18 +271,18 @@ class ApprovalWorkflowDefinitionManagerTest(TestCase):
|
|
|
271
271
|
models.ApprovalWorkflowDefinition.objects.create(
|
|
272
272
|
name=f"Test Approval Workflow Definition {i}",
|
|
273
273
|
model_content_type=scheduled_job_ct,
|
|
274
|
-
|
|
274
|
+
weight=len(range(4)) - 1 - i, # first with highest weight
|
|
275
275
|
)
|
|
276
276
|
for i in range(4)
|
|
277
277
|
]
|
|
278
278
|
|
|
279
279
|
def test_find_for_model(self):
|
|
280
|
-
"""Test that the workflow definition with the highest
|
|
280
|
+
"""Test that the workflow definition with the highest weight and no constraints is returned."""
|
|
281
281
|
self.assertEqual(
|
|
282
282
|
models.ApprovalWorkflowDefinition.objects.find_for_model(self.scheduled_job),
|
|
283
283
|
self.approval_workflow_defs[0],
|
|
284
284
|
)
|
|
285
|
-
self.assertEqual(self.approval_workflow_defs[0].
|
|
285
|
+
self.assertEqual(self.approval_workflow_defs[0].weight, 3)
|
|
286
286
|
|
|
287
287
|
def test_find_for_model_with_filter_match_constraints(self):
|
|
288
288
|
"""Test that a workflow definition with filter matching constraints is correctly returned."""
|
|
@@ -294,7 +294,7 @@ class ApprovalWorkflowDefinitionManagerTest(TestCase):
|
|
|
294
294
|
models.ApprovalWorkflowDefinition.objects.find_for_model(self.scheduled_job),
|
|
295
295
|
self.approval_workflow_defs[0],
|
|
296
296
|
)
|
|
297
|
-
self.assertEqual(self.approval_workflow_defs[0].
|
|
297
|
+
self.assertEqual(self.approval_workflow_defs[0].weight, 3)
|
|
298
298
|
|
|
299
299
|
export_job_model = models.Job.objects.get_for_class_path(ExportObjectList.class_path)
|
|
300
300
|
export_scheduled_job = models.ScheduledJob.objects.create(
|
|
@@ -309,7 +309,7 @@ class ApprovalWorkflowDefinitionManagerTest(TestCase):
|
|
|
309
309
|
models.ApprovalWorkflowDefinition.objects.find_for_model(export_scheduled_job),
|
|
310
310
|
self.approval_workflow_defs[0],
|
|
311
311
|
)
|
|
312
|
-
self.assertEqual(self.approval_workflow_defs[0].
|
|
312
|
+
self.assertEqual(self.approval_workflow_defs[0].weight, 3)
|
|
313
313
|
|
|
314
314
|
def test_find_for_model_with_exact_match_constraints(self):
|
|
315
315
|
"""Test that a workflow definition with exact matching constraints is correctly returned."""
|
|
@@ -319,12 +319,12 @@ class ApprovalWorkflowDefinitionManagerTest(TestCase):
|
|
|
319
319
|
models.ApprovalWorkflowDefinition.objects.find_for_model(self.scheduled_job),
|
|
320
320
|
self.approval_workflow_defs[0],
|
|
321
321
|
)
|
|
322
|
-
self.assertEqual(self.approval_workflow_defs[0].
|
|
322
|
+
self.assertEqual(self.approval_workflow_defs[0].weight, 3)
|
|
323
323
|
|
|
324
|
-
def
|
|
324
|
+
def test_find_for_model_returns_highest_weight_when_all_match(self):
|
|
325
325
|
"""
|
|
326
326
|
Test that when all workflow definitions match the model instance,
|
|
327
|
-
the one with the highest
|
|
327
|
+
the one with the highest weight is returned.
|
|
328
328
|
"""
|
|
329
329
|
# Set all definitions to have matching constraints
|
|
330
330
|
for definition in self.approval_workflow_defs:
|
|
@@ -335,14 +335,14 @@ class ApprovalWorkflowDefinitionManagerTest(TestCase):
|
|
|
335
335
|
models.ApprovalWorkflowDefinition.objects.find_for_model(self.scheduled_job),
|
|
336
336
|
self.approval_workflow_defs[0],
|
|
337
337
|
)
|
|
338
|
-
self.assertEqual(self.approval_workflow_defs[0].
|
|
338
|
+
self.assertEqual(self.approval_workflow_defs[0].weight, 3)
|
|
339
339
|
|
|
340
340
|
def test_find_for_model_skips_unmatched_constraints_and_returns_next_without_constraints(self):
|
|
341
341
|
"""
|
|
342
|
-
Test that if the highest
|
|
342
|
+
Test that if the highest weight workflow definition has unmatched constraints,
|
|
343
343
|
the method skips it and returns the next one with no constraints.
|
|
344
344
|
"""
|
|
345
|
-
# Set constraints on the highest
|
|
345
|
+
# Set constraints on the highest weight definition that do not match the instance
|
|
346
346
|
self.approval_workflow_defs[0].model_constraints = {"name": "Non Matching Name"}
|
|
347
347
|
self.approval_workflow_defs[0].save()
|
|
348
348
|
|
|
@@ -354,12 +354,12 @@ class ApprovalWorkflowDefinitionManagerTest(TestCase):
|
|
|
354
354
|
models.ApprovalWorkflowDefinition.objects.find_for_model(self.scheduled_job),
|
|
355
355
|
self.approval_workflow_defs[1],
|
|
356
356
|
)
|
|
357
|
-
self.assertEqual(self.approval_workflow_defs[1].
|
|
357
|
+
self.assertEqual(self.approval_workflow_defs[1].weight, 2)
|
|
358
358
|
|
|
359
|
-
def
|
|
359
|
+
def test_find_for_model_matches_lower_weight_if_higher_fails(self):
|
|
360
360
|
"""
|
|
361
|
-
Test that if the highest
|
|
362
|
-
the next matching lower-
|
|
361
|
+
Test that if the highest weight workflow definition's constraints don't match,
|
|
362
|
+
the next matching lower-weight definition is returned.
|
|
363
363
|
"""
|
|
364
364
|
self.approval_workflow_defs[0].model_constraints = {"name": "Non Matching Name"}
|
|
365
365
|
self.approval_workflow_defs[0].save()
|
|
@@ -371,7 +371,7 @@ class ApprovalWorkflowDefinitionManagerTest(TestCase):
|
|
|
371
371
|
models.ApprovalWorkflowDefinition.objects.find_for_model(self.scheduled_job),
|
|
372
372
|
self.approval_workflow_defs[1],
|
|
373
373
|
)
|
|
374
|
-
self.assertEqual(self.approval_workflow_defs[1].
|
|
374
|
+
self.assertEqual(self.approval_workflow_defs[1].weight, 2)
|
|
375
375
|
|
|
376
376
|
def test_find_for_model_ignores_constraints_matching_different_instance(self):
|
|
377
377
|
"""
|
|
@@ -412,7 +412,7 @@ class ApprovalWorkflowStageDefinitionAPITest(ApprovalWorkflowTestMixin, APIViewT
|
|
|
412
412
|
cls.create_data = [
|
|
413
413
|
{
|
|
414
414
|
"approval_workflow_definition": cls.approval_workflow_def_2.pk,
|
|
415
|
-
"
|
|
415
|
+
"sequence": 400,
|
|
416
416
|
"name": "Test Approval Workflow 2 Stage 4 Definition",
|
|
417
417
|
"min_approvers": 3,
|
|
418
418
|
"denial_message": "Stage 4 Denial Message",
|
|
@@ -420,7 +420,7 @@ class ApprovalWorkflowStageDefinitionAPITest(ApprovalWorkflowTestMixin, APIViewT
|
|
|
420
420
|
},
|
|
421
421
|
{
|
|
422
422
|
"approval_workflow_definition": cls.approval_workflow_def_2.pk,
|
|
423
|
-
"
|
|
423
|
+
"sequence": 500,
|
|
424
424
|
"name": "Test Approval Workflow 2 Stage 5 Definition",
|
|
425
425
|
"min_approvers": 2,
|
|
426
426
|
"denial_message": "Stage 5 Denial Message",
|
|
@@ -428,7 +428,7 @@ class ApprovalWorkflowStageDefinitionAPITest(ApprovalWorkflowTestMixin, APIViewT
|
|
|
428
428
|
},
|
|
429
429
|
{
|
|
430
430
|
"approval_workflow_definition": cls.approval_workflow_def_2.pk,
|
|
431
|
-
"
|
|
431
|
+
"sequence": 600,
|
|
432
432
|
"name": "Test Approval Workflow 2 Stage 6 Definition",
|
|
433
433
|
"min_approvers": 1,
|
|
434
434
|
"denial_message": "Stage 6 Denial Message",
|
|
@@ -437,7 +437,7 @@ class ApprovalWorkflowStageDefinitionAPITest(ApprovalWorkflowTestMixin, APIViewT
|
|
|
437
437
|
]
|
|
438
438
|
|
|
439
439
|
cls.update_data = {
|
|
440
|
-
"
|
|
440
|
+
"sequence": 700,
|
|
441
441
|
"approval_workflow_definition": cls.approval_workflow_def_2.pk,
|
|
442
442
|
"min_approvers": 4,
|
|
443
443
|
"denial_message": "Updated Denial Message",
|
|
@@ -505,7 +505,7 @@ class ApprovalWorkflowTriggerAPITest(APITestCase):
|
|
|
505
505
|
)
|
|
506
506
|
models.ApprovalWorkflowStageDefinition.objects.create(
|
|
507
507
|
approval_workflow_definition=self.approval_workflow_def,
|
|
508
|
-
|
|
508
|
+
sequence=100,
|
|
509
509
|
name="Test Approval Workflow Stage 1",
|
|
510
510
|
min_approvers=2,
|
|
511
511
|
denial_message="Stage 1 Denial Message",
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
|
|
1
3
|
from django.contrib.contenttypes.models import ContentType
|
|
2
|
-
from django.test import override_settings
|
|
4
|
+
from django.test import override_settings, tag
|
|
3
5
|
from django.urls import reverse
|
|
4
6
|
from django.utils.html import escape
|
|
5
7
|
from rest_framework import status
|
|
@@ -22,8 +24,6 @@ from nautobot.extras.models import CustomField, CustomFieldChoice, DynamicGroup,
|
|
|
22
24
|
from nautobot.ipam.models import VLAN, VLANGroup
|
|
23
25
|
from nautobot.virtualization.models import Cluster, ClusterType, VirtualMachine, VMInterface
|
|
24
26
|
|
|
25
|
-
from example_app.signals import EXAMPLE_APP_CUSTOM_FIELD_DEFAULT, EXAMPLE_APP_CUSTOM_FIELD_NAME
|
|
26
|
-
|
|
27
27
|
|
|
28
28
|
class ChangeLogViewTest(ModelViewTestCase):
|
|
29
29
|
model = Location
|
|
@@ -234,6 +234,32 @@ class ChangeLogViewTest(ModelViewTestCase):
|
|
|
234
234
|
self.assertContains(resp, escape('"description": "changed description2"'))
|
|
235
235
|
self.assertContains(resp, escape('"description": "changed description3"'))
|
|
236
236
|
|
|
237
|
+
def test_objectchange_skips_add_conditional_prefetch(self):
|
|
238
|
+
"""
|
|
239
|
+
Test that ObjectChange.objects.all() skips prefetch_related on ContentTypes without a model class.
|
|
240
|
+
"""
|
|
241
|
+
self.add_permissions("extras.view_objectchange")
|
|
242
|
+
|
|
243
|
+
ct = ContentType.objects.create(app_label="nonexistent_app", model="nonexistentmodel")
|
|
244
|
+
oc = ObjectChange.objects.create(
|
|
245
|
+
changed_object_type=ct,
|
|
246
|
+
changed_object_id=1,
|
|
247
|
+
object_repr="nonexistentobject",
|
|
248
|
+
action=ObjectChangeActionChoices.ACTION_CREATE,
|
|
249
|
+
user=self.user,
|
|
250
|
+
object_data={},
|
|
251
|
+
request_id=uuid.uuid4(),
|
|
252
|
+
)
|
|
253
|
+
url = reverse("extras:objectchange_list")
|
|
254
|
+
with self.assertLogs(level="WARNING") as cm:
|
|
255
|
+
response = self.client.get(url)
|
|
256
|
+
self.assertHttpStatus(response, 200)
|
|
257
|
+
self.assertContains(response, oc.object_repr)
|
|
258
|
+
self.assertIn(
|
|
259
|
+
("One or more ContentType entries in the database are invalid."),
|
|
260
|
+
cm.output[0],
|
|
261
|
+
)
|
|
262
|
+
|
|
237
263
|
|
|
238
264
|
class ChangeLogAPITest(APITestCase):
|
|
239
265
|
def setUp(self):
|
|
@@ -260,7 +286,10 @@ class ChangeLogAPITest(APITestCase):
|
|
|
260
286
|
self.tags = Tag.objects.get_for_model(Location)
|
|
261
287
|
self.statuses = Status.objects.get_for_model(Location)
|
|
262
288
|
|
|
289
|
+
@tag("example_app")
|
|
263
290
|
def test_create_object(self):
|
|
291
|
+
from example_app.signals import EXAMPLE_APP_CUSTOM_FIELD_DEFAULT, EXAMPLE_APP_CUSTOM_FIELD_NAME
|
|
292
|
+
|
|
264
293
|
location_type = LocationType.objects.get(name="Campus")
|
|
265
294
|
data = {
|
|
266
295
|
"name": "Test Location 1",
|
|
@@ -290,8 +319,11 @@ class ChangeLogAPITest(APITestCase):
|
|
|
290
319
|
self.assertEqual(oc.object_data["tags"], sorted([self.tags[0].name, self.tags[1].name]))
|
|
291
320
|
self.assertEqual(oc.user_id, self.user.pk)
|
|
292
321
|
|
|
322
|
+
@tag("example_app")
|
|
293
323
|
def test_update_object(self):
|
|
294
324
|
"""Test PUT with changelogs."""
|
|
325
|
+
from example_app.signals import EXAMPLE_APP_CUSTOM_FIELD_DEFAULT, EXAMPLE_APP_CUSTOM_FIELD_NAME
|
|
326
|
+
|
|
295
327
|
location_type = LocationType.objects.get(name="Campus")
|
|
296
328
|
location = Location.objects.create(
|
|
297
329
|
name="Test Location 1",
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import logging
|
|
3
3
|
|
|
4
|
-
from django.conf import settings
|
|
5
4
|
from django.contrib.contenttypes.models import ContentType
|
|
6
5
|
from django.core.exceptions import ValidationError
|
|
7
6
|
from django.db.models import ProtectedError, QuerySet
|
|
8
7
|
from django.forms import ChoiceField, IntegerField, NumberInput
|
|
8
|
+
from django.test import tag
|
|
9
9
|
from django.urls import reverse
|
|
10
10
|
from rest_framework import status
|
|
11
11
|
|
|
@@ -31,6 +31,7 @@ from nautobot.virtualization.models import VirtualMachine
|
|
|
31
31
|
# TODO: this needs to be both a BaseModelTestCase (as it tests the model class) and a (views) TestCase,
|
|
32
32
|
# (due to the test_multi_select_field_value_after_bulk_update() test).
|
|
33
33
|
# At some point we should probably split this into separate classes.
|
|
34
|
+
@tag("example_app")
|
|
34
35
|
class CustomFieldTest(ModelTestCases.BaseModelTestCase, TestCase):
|
|
35
36
|
model = CustomField
|
|
36
37
|
|
|
@@ -397,6 +398,7 @@ class CustomFieldTest(ModelTestCases.BaseModelTestCase, TestCase):
|
|
|
397
398
|
self.assertIsInstance(filter_field.widget, NumberInput)
|
|
398
399
|
|
|
399
400
|
|
|
401
|
+
@tag("example_app")
|
|
400
402
|
class CustomFieldManagerTest(TestCase):
|
|
401
403
|
def setUp(self):
|
|
402
404
|
self.content_type = ContentType.objects.get_for_model(Location)
|
|
@@ -569,6 +571,7 @@ class ComputedFieldManagerTestCase(TestCase):
|
|
|
569
571
|
self.assertQuerysetEqualAndNotEmpty(qs, listing)
|
|
570
572
|
|
|
571
573
|
|
|
574
|
+
@tag("example_app")
|
|
572
575
|
class CustomFieldDataAPITest(APITestCase):
|
|
573
576
|
"""
|
|
574
577
|
Check that object representations in the REST API include their custom field data.
|
|
@@ -693,9 +696,8 @@ class CustomFieldDataAPITest(APITestCase):
|
|
|
693
696
|
self.cf_json,
|
|
694
697
|
]
|
|
695
698
|
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
self.all_cfs.append(self.cf_plugin_field)
|
|
699
|
+
self.cf_plugin_field = CustomField.objects.get(key="example_app_auto_custom_field")
|
|
700
|
+
self.all_cfs.append(self.cf_plugin_field)
|
|
699
701
|
self.statuses = Status.objects.get_for_model(Location)
|
|
700
702
|
|
|
701
703
|
# Create some locations
|
|
@@ -717,8 +719,7 @@ class CustomFieldDataAPITest(APITestCase):
|
|
|
717
719
|
self.cf_markdown.key: "### Hello world!\n\n- Item 1\n- Item 2\n- Item 3",
|
|
718
720
|
self.cf_json.key: {"hello": "world"},
|
|
719
721
|
}
|
|
720
|
-
|
|
721
|
-
self.locations[1]._custom_field_data[self.cf_plugin_field.key] = "Custom value"
|
|
722
|
+
self.locations[1]._custom_field_data[self.cf_plugin_field.key] = "Custom value"
|
|
722
723
|
self.locations[1].validated_save()
|
|
723
724
|
self.list_url = reverse("dcim-api:location-list")
|
|
724
725
|
self.detail_url = reverse("dcim-api:location-detail", kwargs={"pk": self.locations[1].pk})
|
|
@@ -792,8 +793,7 @@ class CustomFieldDataAPITest(APITestCase):
|
|
|
792
793
|
self.cf_json.key: {"foo": "bar"},
|
|
793
794
|
},
|
|
794
795
|
}
|
|
795
|
-
|
|
796
|
-
data["custom_fields"]["example_app_auto_custom_field"] = "Custom value"
|
|
796
|
+
data["custom_fields"]["example_app_auto_custom_field"] = "Custom value"
|
|
797
797
|
response = self.client.post(self.list_url, data, format="json", **self.header)
|
|
798
798
|
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
|
799
799
|
|
|
@@ -865,9 +865,8 @@ class CustomFieldDataAPITest(APITestCase):
|
|
|
865
865
|
self.cf_markdown.key: "### Heading",
|
|
866
866
|
self.cf_json.key: {"dict1": {"dict2": {}}},
|
|
867
867
|
}
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
custom_field_data[self.cf_plugin_field.key] = "Custom Value"
|
|
868
|
+
self.cf_plugin_field = CustomField.objects.get(key="example_app_auto_custom_field")
|
|
869
|
+
custom_field_data[self.cf_plugin_field.key] = "Custom Value"
|
|
871
870
|
data = (
|
|
872
871
|
{
|
|
873
872
|
"name": "Location 3",
|
|
@@ -1169,6 +1168,7 @@ class CustomFieldDataAPITest(APITestCase):
|
|
|
1169
1168
|
self.assertContains(response, "Invalid choice", status_code=status.HTTP_400_BAD_REQUEST)
|
|
1170
1169
|
|
|
1171
1170
|
|
|
1171
|
+
@tag("example_app")
|
|
1172
1172
|
class CustomFieldImportTest(TestCase):
|
|
1173
1173
|
"""
|
|
1174
1174
|
Test importing object custom field data along with the object itself.
|
|
@@ -2009,20 +2009,29 @@ class CustomFieldFilterTest(TestCase):
|
|
|
2009
2009
|
)
|
|
2010
2010
|
|
|
2011
2011
|
def test_filter_multi_select(self):
|
|
2012
|
-
self.
|
|
2012
|
+
self.assertQuerysetEqualAndNotEmpty(
|
|
2013
2013
|
self.filterset({"cf_cf9": "Foo"}, self.queryset).qs,
|
|
2014
2014
|
self.queryset.filter(_custom_field_data__cf9__contains="Foo"),
|
|
2015
2015
|
)
|
|
2016
|
-
self.
|
|
2016
|
+
self.assertQuerysetEqualAndNotEmpty(
|
|
2017
2017
|
self.filterset({"cf_cf9": "Bar"}, self.queryset).qs,
|
|
2018
2018
|
self.queryset.filter(_custom_field_data__cf9__contains="Bar"),
|
|
2019
2019
|
)
|
|
2020
|
+
self.assertQuerysetEqualAndNotEmpty(
|
|
2021
|
+
self.filterset({"cf_cf9": ["Foo"]}, self.queryset).qs,
|
|
2022
|
+
self.queryset.filter(_custom_field_data__cf9__contains="Foo"),
|
|
2023
|
+
)
|
|
2024
|
+
self.assertQuerysetEqualAndNotEmpty(
|
|
2025
|
+
self.filterset({"cf_cf9": ["Bar"]}, self.queryset).qs,
|
|
2026
|
+
self.queryset.filter(_custom_field_data__cf9__contains="Bar"),
|
|
2027
|
+
)
|
|
2020
2028
|
self.assertQuerysetEqualAndNotEmpty( # https://github.com/nautobot/nautobot/issues/5009
|
|
2021
2029
|
self.filterset({"cf_cf9": str(self.multiselect_choices[0].pk)}, self.queryset).qs,
|
|
2022
2030
|
self.queryset.filter(_custom_field_data__cf9__contains=self.multiselect_choices[0].value),
|
|
2023
2031
|
)
|
|
2024
2032
|
|
|
2025
2033
|
|
|
2034
|
+
@tag("example_app")
|
|
2026
2035
|
class CustomFieldChoiceTest(ModelTestCases.BaseModelTestCase):
|
|
2027
2036
|
model = CustomFieldChoice
|
|
2028
2037
|
|