nautobot 2.2.0b1__py3-none-any.whl → 2.2.2__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/__init__.py +31 -0
- nautobot/apps/api.py +1 -2
- nautobot/apps/utils.py +4 -0
- nautobot/apps/views.py +2 -0
- nautobot/circuits/api/urls.py +1 -2
- nautobot/circuits/api/views.py +0 -12
- nautobot/circuits/apps.py +1 -1
- nautobot/circuits/tests/test_filters.py +1 -1
- nautobot/core/api/routers.py +50 -3
- nautobot/core/api/utils.py +4 -0
- nautobot/core/api/views.py +21 -15
- nautobot/core/cli/__init__.py +18 -11
- nautobot/core/constants.py +85 -0
- nautobot/core/filters.py +7 -1
- nautobot/core/forms/widgets.py +1 -2
- nautobot/core/graphql/schema.py +1 -0
- nautobot/core/management/commands/generate_test_data.py +4 -4
- nautobot/core/models/__init__.py +1 -0
- nautobot/core/settings.py +24 -3
- nautobot/core/settings.yaml +20 -0
- nautobot/core/signals.py +1 -0
- nautobot/core/tables.py +2 -1
- nautobot/core/templates/admin/base.html +23 -94
- nautobot/core/templates/generic/object_retrieve.html +2 -2
- nautobot/core/templates/graphene/graphiql.html +18 -47
- nautobot/core/templates/inc/footer.html +5 -5
- nautobot/core/templates/inc/javascript.html +4 -4
- nautobot/core/templates/inc/media.html +2 -2
- nautobot/core/templates/inc/nav_menu.html +0 -7
- nautobot/core/templates/nautobot_config.py.j2 +14 -1
- nautobot/core/templates/rest_framework/api.html +12 -5
- nautobot/core/templatetags/helpers.py +2 -2
- nautobot/core/testing/__init__.py +1 -1
- nautobot/core/testing/filters.py +1 -1
- nautobot/core/testing/views.py +30 -0
- nautobot/core/tests/integration/test_view_authentication.py +68 -0
- nautobot/core/tests/test_api.py +13 -6
- nautobot/core/tests/test_csv.py +5 -4
- nautobot/core/tests/test_filters.py +2 -1
- nautobot/core/tests/test_graphql.py +4 -14
- nautobot/core/tests/test_navigations.py +3 -0
- nautobot/core/tests/test_views.py +45 -16
- nautobot/core/utils/data.py +1 -2
- nautobot/core/utils/lookup.py +126 -0
- nautobot/core/views/__init__.py +3 -7
- nautobot/core/views/generic.py +24 -10
- nautobot/core/views/mixins.py +11 -4
- nautobot/core/views/renderers.py +11 -6
- nautobot/core/wsgi.py +9 -2
- nautobot/dcim/api/serializers.py +4 -4
- nautobot/dcim/api/urls.py +2 -3
- nautobot/dcim/api/views.py +7 -18
- nautobot/dcim/apps.py +8 -4
- nautobot/dcim/elevations.py +5 -1
- nautobot/dcim/factory.py +7 -7
- nautobot/dcim/filters/__init__.py +16 -17
- nautobot/dcim/forms.py +69 -48
- nautobot/dcim/homepage.py +11 -3
- nautobot/dcim/management/commands/migrate_location_contacts.py +218 -0
- nautobot/dcim/migrations/0057_controller_models.py +11 -70
- nautobot/dcim/models/__init__.py +2 -2
- nautobot/dcim/models/devices.py +14 -16
- nautobot/dcim/models/racks.py +1 -3
- nautobot/dcim/navigation.py +23 -31
- nautobot/dcim/signals.py +6 -6
- nautobot/dcim/tables/__init__.py +2 -2
- nautobot/dcim/tables/devices.py +13 -16
- nautobot/dcim/tables/template_code.py +1 -1
- nautobot/dcim/templates/dcim/controller_create.html +70 -0
- nautobot/dcim/templates/dcim/controller_retrieve.html +35 -18
- nautobot/dcim/templates/dcim/controllermanageddevicegroup_create.html +88 -0
- nautobot/dcim/templates/dcim/device/lldp_neighbors.html +74 -42
- nautobot/dcim/templates/dcim/device.html +11 -3
- nautobot/dcim/templates/dcim/device_edit.html +1 -1
- nautobot/dcim/templates/dcim/devicefamily_retrieve.html +4 -0
- nautobot/dcim/templates/dcim/softwareimagefile_retrieve.html +1 -1
- nautobot/dcim/tests/test_api.py +47 -6
- nautobot/dcim/tests/test_filters.py +92 -81
- nautobot/dcim/tests/test_forms.py +49 -2
- nautobot/dcim/tests/test_graphql.py +11 -1
- nautobot/dcim/tests/test_models.py +15 -15
- nautobot/dcim/tests/test_signals.py +3 -1
- nautobot/dcim/tests/test_views.py +24 -12
- nautobot/dcim/urls.py +1 -1
- nautobot/dcim/views.py +25 -15
- nautobot/extras/api/serializers.py +20 -1
- nautobot/extras/api/urls.py +1 -2
- nautobot/extras/api/views.py +0 -10
- nautobot/extras/apps.py +7 -0
- nautobot/extras/context_managers.py +71 -4
- nautobot/extras/filters/__init__.py +53 -2
- nautobot/extras/filters/customfields.py +14 -9
- nautobot/extras/filters/mixins.py +6 -1
- nautobot/extras/forms/contacts.py +7 -0
- nautobot/extras/health_checks.py +1 -0
- nautobot/extras/jobs.py +1 -0
- nautobot/extras/managers.py +15 -2
- nautobot/extras/models/contacts.py +1 -0
- nautobot/extras/models/customfields.py +25 -2
- nautobot/extras/models/datasources.py +1 -0
- nautobot/extras/models/mixins.py +1 -0
- nautobot/extras/navigation.py +71 -65
- nautobot/extras/plugins/__init__.py +2 -1
- nautobot/extras/plugins/views.py +7 -11
- nautobot/extras/querysets.py +1 -2
- nautobot/extras/secrets/providers.py +1 -0
- nautobot/extras/signals.py +95 -51
- nautobot/extras/tasks.py +70 -17
- nautobot/extras/tests/test_api.py +2 -4
- nautobot/extras/tests/test_context_managers.py +98 -1
- nautobot/extras/tests/test_customfields.py +72 -9
- nautobot/extras/tests/test_dynamicgroups.py +2 -0
- nautobot/extras/tests/test_filters.py +89 -4
- nautobot/extras/tests/test_models.py +9 -0
- nautobot/extras/tests/test_relationships.py +10 -1
- nautobot/extras/tests/test_views.py +112 -1
- nautobot/extras/utils.py +37 -0
- nautobot/extras/views.py +18 -17
- nautobot/ipam/api/serializers.py +10 -0
- nautobot/ipam/api/urls.py +1 -2
- nautobot/ipam/api/views.py +0 -11
- nautobot/ipam/apps.py +3 -2
- nautobot/ipam/tables.py +3 -23
- nautobot/ipam/tests/test_graphql.py +2 -3
- nautobot/ipam/tests/test_tables.py +42 -0
- nautobot/ipam/tests/test_views.py +1 -0
- nautobot/ipam/views.py +9 -9
- nautobot/project-static/css/base.css +1 -0
- nautobot/project-static/docs/404.html +126 -73
- nautobot/project-static/docs/apps/index.html +127 -71
- nautobot/project-static/docs/apps/nautobot-apps.html +127 -71
- nautobot/project-static/docs/assets/javascripts/{bundle.8fd75fb4.min.js → bundle.bd41221c.min.js} +2 -2
- nautobot/project-static/docs/assets/javascripts/{bundle.8fd75fb4.min.js.map → bundle.bd41221c.min.js.map} +3 -3
- nautobot/project-static/docs/assets/stylesheets/main.bcfcd587.min.css +1 -0
- nautobot/project-static/docs/assets/stylesheets/main.bcfcd587.min.css.map +1 -0
- nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +127 -71
- nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +127 -71
- nautobot/project-static/docs/code-reference/nautobot/apps/api.html +167 -73
- nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +165 -72
- nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +127 -71
- nautobot/project-static/docs/code-reference/nautobot/apps/config.html +127 -71
- nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +127 -71
- nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +127 -71
- nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +127 -71
- nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +127 -71
- nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +127 -71
- nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +127 -71
- nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +127 -71
- nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +127 -71
- nautobot/project-static/docs/code-reference/nautobot/apps/models.html +127 -71
- nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +127 -71
- nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +127 -71
- nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +127 -71
- nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +128 -72
- nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +127 -71
- nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +127 -71
- nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +345 -71
- nautobot/project-static/docs/code-reference/nautobot/apps/views.html +172 -73
- nautobot/project-static/docs/development/apps/api/configuration-view.html +127 -71
- nautobot/project-static/docs/development/apps/api/database-backend-config.html +127 -71
- nautobot/project-static/docs/development/apps/api/models/django-admin.html +127 -71
- nautobot/project-static/docs/development/apps/api/models/global-search.html +127 -71
- nautobot/project-static/docs/development/apps/api/models/graphql.html +127 -71
- nautobot/project-static/docs/development/apps/api/models/index.html +127 -71
- nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +127 -71
- nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +127 -71
- nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +127 -71
- nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +127 -71
- nautobot/project-static/docs/development/apps/api/platform-features/index.html +127 -71
- nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +127 -71
- nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +127 -71
- nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +127 -71
- nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +127 -71
- nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +127 -71
- nautobot/project-static/docs/development/apps/api/prometheus.html +127 -71
- nautobot/project-static/docs/development/apps/api/setup.html +127 -71
- nautobot/project-static/docs/development/apps/api/testing.html +127 -71
- nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +127 -71
- nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +127 -71
- nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +127 -71
- nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +127 -71
- nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +127 -71
- nautobot/project-static/docs/development/apps/api/views/base-template.html +127 -71
- nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +141 -80
- nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +144 -83
- nautobot/project-static/docs/development/apps/api/views/help-documentation.html +127 -71
- nautobot/project-static/docs/development/apps/api/views/index.html +127 -71
- nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +127 -71
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +127 -71
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +127 -71
- nautobot/project-static/docs/development/apps/api/views/notes.html +127 -71
- nautobot/project-static/docs/development/apps/api/views/rest-api.html +127 -71
- nautobot/project-static/docs/development/apps/api/views/urls.html +127 -71
- nautobot/project-static/docs/development/apps/index.html +127 -71
- nautobot/project-static/docs/development/apps/migration/code-updates.html +127 -71
- nautobot/project-static/docs/development/apps/migration/dependency-updates.html +127 -71
- nautobot/project-static/docs/development/apps/migration/from-v1.html +127 -71
- nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +127 -71
- nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +127 -71
- nautobot/project-static/docs/development/apps/migration/model-updates/global.html +127 -71
- nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +127 -71
- nautobot/project-static/docs/development/apps/porting-from-netbox.html +127 -71
- nautobot/project-static/docs/development/core/application-registry.html +127 -71
- nautobot/project-static/docs/development/core/best-practices.html +145 -79
- nautobot/project-static/docs/development/core/bootstrap-ui.html +127 -71
- nautobot/project-static/docs/development/core/caching.html +127 -71
- nautobot/project-static/docs/development/core/controllers.html +141 -275
- nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +127 -71
- nautobot/project-static/docs/development/core/extending-models.html +13 -8166
- nautobot/project-static/docs/development/core/generic-views.html +142 -86
- nautobot/project-static/docs/development/core/getting-started.html +146 -81
- nautobot/project-static/docs/development/core/homepage.html +145 -89
- nautobot/project-static/docs/development/core/index.html +127 -71
- nautobot/project-static/docs/development/core/model-checklist.html +8354 -0
- nautobot/project-static/docs/development/core/model-features.html +130 -74
- nautobot/project-static/docs/development/core/natural-keys.html +127 -71
- nautobot/project-static/docs/development/core/navigation-menu.html +127 -71
- nautobot/project-static/docs/development/core/release-checklist.html +127 -71
- nautobot/project-static/docs/development/core/role-internals.html +127 -71
- nautobot/project-static/docs/development/core/settings.html +127 -71
- nautobot/project-static/docs/development/core/style-guide.html +127 -71
- nautobot/project-static/docs/development/core/templates.html +127 -71
- nautobot/project-static/docs/development/core/testing.html +127 -71
- nautobot/project-static/docs/development/core/user-preferences.html +127 -71
- nautobot/project-static/docs/development/extending-models.html +3 -3
- nautobot/project-static/docs/development/index.html +127 -71
- nautobot/project-static/docs/development/jobs/index.html +128 -72
- nautobot/project-static/docs/development/jobs/migration/from-v1.html +127 -71
- nautobot/project-static/docs/index.html +126 -73
- nautobot/project-static/docs/models/dcim/{controllerdevicegroup.html → controllermanageddevicegroup.html} +3 -3
- nautobot/project-static/docs/objects.inv +0 -0
- nautobot/project-static/docs/release-notes/index.html +127 -71
- nautobot/project-static/docs/release-notes/version-1.0.html +127 -71
- nautobot/project-static/docs/release-notes/version-1.1.html +127 -71
- nautobot/project-static/docs/release-notes/version-1.2.html +127 -71
- nautobot/project-static/docs/release-notes/version-1.3.html +127 -71
- nautobot/project-static/docs/release-notes/version-1.4.html +127 -71
- nautobot/project-static/docs/release-notes/version-1.5.html +127 -71
- nautobot/project-static/docs/release-notes/version-1.6.html +663 -304
- nautobot/project-static/docs/release-notes/version-2.0.html +127 -71
- nautobot/project-static/docs/release-notes/version-2.1.html +538 -254
- nautobot/project-static/docs/release-notes/version-2.2.html +711 -125
- nautobot/project-static/docs/requirements.txt +3 -3
- nautobot/project-static/docs/search/search_index.json +1 -1
- nautobot/project-static/docs/sitemap.xml +264 -259
- nautobot/project-static/docs/sitemap.xml.gz +0 -0
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +127 -71
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +127 -71
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +127 -71
- nautobot/project-static/docs/user-guide/administration/configuration/index.html +127 -71
- nautobot/project-static/docs/user-guide/administration/configuration/optional-settings.html +192 -71
- nautobot/project-static/docs/user-guide/administration/configuration/required-settings.html +127 -71
- nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +127 -71
- nautobot/project-static/docs/user-guide/administration/guides/caching.html +127 -71
- nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +127 -71
- nautobot/project-static/docs/user-guide/administration/guides/healthcheck.html +127 -71
- nautobot/project-static/docs/user-guide/administration/guides/permissions.html +127 -71
- nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +131 -71
- nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +127 -71
- nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +127 -71
- nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +130 -74
- nautobot/project-static/docs/user-guide/administration/installation/app-install.html +127 -71
- nautobot/project-static/docs/user-guide/administration/installation/docker.html +134 -74
- nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +127 -71
- nautobot/project-static/docs/user-guide/administration/installation/health-checks.html +8616 -0
- nautobot/project-static/docs/user-guide/administration/installation/http-server.html +127 -71
- nautobot/project-static/docs/user-guide/administration/installation/index.html +127 -71
- nautobot/project-static/docs/user-guide/administration/installation/install_system.html +127 -71
- nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +127 -71
- nautobot/project-static/docs/user-guide/administration/installation/selinux-troubleshooting.html +130 -74
- nautobot/project-static/docs/user-guide/administration/installation/services.html +127 -71
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +127 -71
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +127 -71
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +127 -71
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +127 -71
- nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +127 -71
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +127 -71
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +127 -71
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +127 -71
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +127 -71
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +127 -71
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +127 -71
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +127 -71
- nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +362 -79
- nautobot/project-static/docs/user-guide/core-data-model/dcim/{controllerdevicegroup.html → controllermanageddevicegroup.html} +210 -85
- nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +130 -74
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +138 -71
- nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +138 -71
- nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +127 -71
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +127 -71
- nautobot/project-static/docs/user-guide/feature-guides/{contact-and-team.html → contacts-and-teams.html} +128 -72
- nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +129 -73
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +127 -71
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +127 -71
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +127 -71
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +127 -71
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +127 -71
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +127 -71
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +129 -73
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +127 -71
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +127 -71
- nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +127 -71
- nautobot/project-static/docs/user-guide/feature-guides/graphql.html +127 -71
- nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +127 -71
- nautobot/project-static/docs/user-guide/feature-guides/relationships.html +127 -71
- nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +127 -71
- nautobot/project-static/docs/user-guide/index.html +127 -71
- nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +127 -71
- nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +127 -71
- nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +127 -71
- nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +127 -71
- nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +127 -71
- nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +127 -71
- nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +127 -71
- nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +127 -71
- nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +127 -71
- nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +127 -71
- nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +127 -71
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +127 -71
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +127 -71
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +127 -71
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +127 -71
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +127 -71
- nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +127 -71
- nautobot/project-static/docs/user-guide/platform-functionality/note.html +127 -71
- nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +127 -71
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +127 -71
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +127 -71
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +127 -71
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +127 -71
- nautobot/project-static/docs/user-guide/platform-functionality/role.html +127 -71
- nautobot/project-static/docs/user-guide/platform-functionality/secret.html +127 -71
- nautobot/project-static/docs/user-guide/platform-functionality/status.html +127 -71
- nautobot/project-static/docs/user-guide/platform-functionality/tag.html +127 -71
- nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +127 -71
- nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +127 -71
- nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +127 -71
- nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +127 -71
- nautobot/project-static/jquery/jquery-3.7.1.min.js +2 -0
- nautobot/project-static/{jquery-ui-1.13.1 → jquery-ui-1.13.2}/images/ui-icons_444444_256x240.png +0 -0
- nautobot/project-static/{jquery-ui-1.13.1 → jquery-ui-1.13.2}/images/ui-icons_555555_256x240.png +0 -0
- nautobot/project-static/{jquery-ui-1.13.1 → jquery-ui-1.13.2}/images/ui-icons_777620_256x240.png +0 -0
- nautobot/project-static/{jquery-ui-1.13.1 → jquery-ui-1.13.2}/images/ui-icons_777777_256x240.png +0 -0
- nautobot/project-static/{jquery-ui-1.13.1 → jquery-ui-1.13.2}/images/ui-icons_cc0000_256x240.png +0 -0
- nautobot/project-static/{jquery-ui-1.13.1 → jquery-ui-1.13.2}/images/ui-icons_ffffff_256x240.png +0 -0
- nautobot/project-static/jquery-ui-1.13.2/jquery-ui.min.css +7 -0
- nautobot/project-static/jquery-ui-1.13.2/jquery-ui.min.js +6 -0
- nautobot/project-static/jquery-ui-1.13.2/jquery-ui.structure.min.css +5 -0
- nautobot/project-static/{jquery-ui-1.13.1 → jquery-ui-1.13.2}/jquery-ui.theme.min.css +1 -1
- nautobot/tenancy/api/urls.py +1 -2
- nautobot/tenancy/api/views.py +0 -12
- nautobot/tenancy/tables.py +1 -1
- nautobot/tenancy/tests/test_views.py +1 -0
- nautobot/users/api/urls.py +1 -2
- nautobot/users/api/views.py +2 -65
- nautobot/users/views.py +8 -8
- nautobot/virtualization/api/urls.py +1 -2
- nautobot/virtualization/api/views.py +0 -12
- {nautobot-2.2.0b1.dist-info → nautobot-2.2.2.dist-info}/METADATA +24 -24
- {nautobot-2.2.0b1.dist-info → nautobot-2.2.2.dist-info}/RECORD +422 -416
- nautobot/dcim/templates/dcim/controllerdevicegroup_create.html +0 -43
- nautobot/project-static/docs/assets/stylesheets/main.f2e4d321.min.css +0 -1
- nautobot/project-static/docs/assets/stylesheets/main.f2e4d321.min.css.map +0 -1
- nautobot/project-static/jquery/jquery-3.6.0.min.js +0 -2
- nautobot/project-static/jquery-ui-1.13.1/jquery-ui.min.css +0 -7
- nautobot/project-static/jquery-ui-1.13.1/jquery-ui.min.js +0 -6
- nautobot/project-static/jquery-ui-1.13.1/jquery-ui.structure.min.css +0 -5
- /nautobot/dcim/templates/dcim/{controllerdevicegroup_retrieve.html → controllermanageddevicegroup_retrieve.html} +0 -0
- {nautobot-2.2.0b1.dist-info → nautobot-2.2.2.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.2.0b1.dist-info → nautobot-2.2.2.dist-info}/NOTICE +0 -0
- {nautobot-2.2.0b1.dist-info → nautobot-2.2.2.dist-info}/WHEEL +0 -0
- {nautobot-2.2.0b1.dist-info → nautobot-2.2.2.dist-info}/entry_points.txt +0 -0
nautobot/extras/tasks.py
CHANGED
|
@@ -14,7 +14,7 @@ logger = getLogger("nautobot.extras.tasks")
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
@nautobot_task
|
|
17
|
-
def update_custom_field_choice_data(field_id, old_value, new_value):
|
|
17
|
+
def update_custom_field_choice_data(field_id, old_value, new_value, change_context=None):
|
|
18
18
|
"""
|
|
19
19
|
Update the values for a custom field choice used in objects' _custom_field_data for the given field.
|
|
20
20
|
|
|
@@ -23,6 +23,8 @@ def update_custom_field_choice_data(field_id, old_value, new_value):
|
|
|
23
23
|
old_value (str): The existing value of the choice
|
|
24
24
|
new_value (str): The value which will be used as replacement
|
|
25
25
|
"""
|
|
26
|
+
# Circular Import
|
|
27
|
+
from nautobot.extras.context_managers import web_request_context
|
|
26
28
|
from nautobot.extras.models import CustomField
|
|
27
29
|
|
|
28
30
|
try:
|
|
@@ -35,19 +37,43 @@ def update_custom_field_choice_data(field_id, old_value, new_value):
|
|
|
35
37
|
# Loop through all field content types and search for values to update
|
|
36
38
|
for ct in field.content_types.all():
|
|
37
39
|
model = ct.model_class()
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
if change_context is not None:
|
|
41
|
+
with web_request_context(
|
|
42
|
+
user=change_context.get("user"),
|
|
43
|
+
change_id=change_context.get("change_id"),
|
|
44
|
+
context_detail=change_context.get("context_detail"),
|
|
45
|
+
context=change_context.get("context"),
|
|
46
|
+
):
|
|
47
|
+
for obj in model.objects.filter(**{f"_custom_field_data__{field.key}": old_value}):
|
|
48
|
+
obj._custom_field_data[field.key] = new_value
|
|
49
|
+
obj.save()
|
|
50
|
+
else:
|
|
51
|
+
for obj in model.objects.filter(**{f"_custom_field_data__{field.key}": old_value}):
|
|
52
|
+
obj._custom_field_data[field.key] = new_value
|
|
53
|
+
obj.save()
|
|
41
54
|
|
|
42
55
|
elif field.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
|
|
43
56
|
# Loop through all field content types and search for values to update
|
|
44
57
|
for ct in field.content_types.all():
|
|
45
58
|
model = ct.model_class()
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
59
|
+
if change_context is not None:
|
|
60
|
+
with web_request_context(
|
|
61
|
+
user=change_context.get("user"),
|
|
62
|
+
change_id=change_context.get("change_id"),
|
|
63
|
+
context_detail=change_context.get("context_detail"),
|
|
64
|
+
context=change_context.get("context"),
|
|
65
|
+
):
|
|
66
|
+
for obj in model.objects.filter(**{f"_custom_field_data__{field.key}__contains": old_value}):
|
|
67
|
+
old_list = obj._custom_field_data[field.key]
|
|
68
|
+
new_list = [new_value if e == old_value else e for e in old_list]
|
|
69
|
+
obj._custom_field_data[field.key] = new_list
|
|
70
|
+
obj.save()
|
|
71
|
+
else:
|
|
72
|
+
for obj in model.objects.filter(**{f"_custom_field_data__{field.key}__contains": old_value}):
|
|
73
|
+
old_list = obj._custom_field_data[field.key]
|
|
74
|
+
new_list = [new_value if e == old_value else e for e in old_list]
|
|
75
|
+
obj._custom_field_data[field.key] = new_list
|
|
76
|
+
obj.save()
|
|
51
77
|
|
|
52
78
|
else:
|
|
53
79
|
logger.error(f"Unknown field type, failing to act on choice data for this field {field.key}.")
|
|
@@ -57,7 +83,7 @@ def update_custom_field_choice_data(field_id, old_value, new_value):
|
|
|
57
83
|
|
|
58
84
|
|
|
59
85
|
@nautobot_task
|
|
60
|
-
def delete_custom_field_data(field_key, content_type_pk_set):
|
|
86
|
+
def delete_custom_field_data(field_key, content_type_pk_set, change_context=None):
|
|
61
87
|
"""
|
|
62
88
|
Delete the values for a custom field
|
|
63
89
|
|
|
@@ -65,16 +91,30 @@ def delete_custom_field_data(field_key, content_type_pk_set):
|
|
|
65
91
|
field_key (str): The key of the custom field which is being deleted
|
|
66
92
|
content_type_pk_set (list): List of PKs for content types to act upon
|
|
67
93
|
"""
|
|
94
|
+
# Circular Import
|
|
95
|
+
from nautobot.extras.context_managers import web_request_context
|
|
96
|
+
|
|
68
97
|
with transaction.atomic():
|
|
69
98
|
for ct in ContentType.objects.filter(pk__in=content_type_pk_set):
|
|
70
99
|
model = ct.model_class()
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
100
|
+
if change_context is not None:
|
|
101
|
+
with web_request_context(
|
|
102
|
+
user=change_context.get("user"),
|
|
103
|
+
change_id=change_context.get("change_id"),
|
|
104
|
+
context_detail=change_context.get("context_detail"),
|
|
105
|
+
context=change_context.get("context"),
|
|
106
|
+
):
|
|
107
|
+
for obj in model.objects.filter(**{f"_custom_field_data__{field_key}__isnull": False}):
|
|
108
|
+
del obj._custom_field_data[field_key]
|
|
109
|
+
obj.save()
|
|
110
|
+
else:
|
|
111
|
+
for obj in model.objects.filter(**{f"_custom_field_data__{field_key}__isnull": False}):
|
|
112
|
+
del obj._custom_field_data[field_key]
|
|
113
|
+
obj.save()
|
|
74
114
|
|
|
75
115
|
|
|
76
116
|
@nautobot_task
|
|
77
|
-
def provision_field(field_id, content_type_pk_set):
|
|
117
|
+
def provision_field(field_id, content_type_pk_set, change_context=None):
|
|
78
118
|
"""
|
|
79
119
|
Provision a new custom field on all relevant content type object instances.
|
|
80
120
|
|
|
@@ -82,6 +122,8 @@ def provision_field(field_id, content_type_pk_set):
|
|
|
82
122
|
field_id (uuid4): The PK of the custom field being provisioned
|
|
83
123
|
content_type_pk_set (list): List of PKs for content types to act upon
|
|
84
124
|
"""
|
|
125
|
+
# Circular Import
|
|
126
|
+
from nautobot.extras.context_managers import web_request_context
|
|
85
127
|
from nautobot.extras.models import CustomField
|
|
86
128
|
|
|
87
129
|
try:
|
|
@@ -93,9 +135,20 @@ def provision_field(field_id, content_type_pk_set):
|
|
|
93
135
|
with transaction.atomic():
|
|
94
136
|
for ct in ContentType.objects.filter(pk__in=content_type_pk_set):
|
|
95
137
|
model = ct.model_class()
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
138
|
+
if change_context is not None:
|
|
139
|
+
with web_request_context(
|
|
140
|
+
user=change_context.get("user"),
|
|
141
|
+
change_id=change_context.get("change_id"),
|
|
142
|
+
context_detail=change_context.get("context_detail"),
|
|
143
|
+
context=change_context.get("context"),
|
|
144
|
+
):
|
|
145
|
+
for obj in model.objects.all():
|
|
146
|
+
obj._custom_field_data.setdefault(field.key, field.default)
|
|
147
|
+
obj.save()
|
|
148
|
+
else:
|
|
149
|
+
for obj in model.objects.all():
|
|
150
|
+
obj._custom_field_data.setdefault(field.key, field.default)
|
|
151
|
+
obj.save()
|
|
99
152
|
|
|
100
153
|
return True
|
|
101
154
|
|
|
@@ -18,6 +18,7 @@ from nautobot.core.testing import APITestCase, APIViewTestCases
|
|
|
18
18
|
from nautobot.core.testing.utils import disable_warnings
|
|
19
19
|
from nautobot.core.utils.lookup import get_route_for_model
|
|
20
20
|
from nautobot.dcim.models import (
|
|
21
|
+
Controller,
|
|
21
22
|
Device,
|
|
22
23
|
DeviceType,
|
|
23
24
|
Location,
|
|
@@ -408,11 +409,9 @@ class ContactTest(APIViewTestCases.APIViewTestCase):
|
|
|
408
409
|
{
|
|
409
410
|
"name": "Contact 3",
|
|
410
411
|
"phone": "555-0123",
|
|
411
|
-
"email": "",
|
|
412
412
|
},
|
|
413
413
|
{
|
|
414
414
|
"name": "Contact 4",
|
|
415
|
-
"phone": "",
|
|
416
415
|
"email": "contact4@example.com",
|
|
417
416
|
},
|
|
418
417
|
]
|
|
@@ -2851,6 +2850,7 @@ class RelationshipTest(APIViewTestCases.APIViewTestCase, RequiredRelationshipTes
|
|
|
2851
2850
|
vlan_groups = VLANGroup.objects.all()[:2]
|
|
2852
2851
|
|
|
2853
2852
|
# Try deleting all devices and then creating 2 VLANs (fails):
|
|
2853
|
+
Controller.objects.filter(controller_device__isnull=False).delete()
|
|
2854
2854
|
Device.objects.all().delete()
|
|
2855
2855
|
response = send_bulk_data(
|
|
2856
2856
|
"post",
|
|
@@ -3611,11 +3611,9 @@ class TeamTest(APIViewTestCases.APIViewTestCase):
|
|
|
3611
3611
|
{
|
|
3612
3612
|
"name": "Team 3",
|
|
3613
3613
|
"phone": "555-0123",
|
|
3614
|
-
"email": "",
|
|
3615
3614
|
},
|
|
3616
3615
|
{
|
|
3617
3616
|
"name": "Team 4",
|
|
3618
|
-
"phone": "",
|
|
3619
3617
|
"email": "team4@example.com",
|
|
3620
3618
|
"address": "Rainbow Bridge, Central NJ",
|
|
3621
3619
|
},
|
|
@@ -7,7 +7,10 @@ from nautobot.core.testing import TransactionTestCase
|
|
|
7
7
|
from nautobot.core.utils.lookup import get_changes_for_model
|
|
8
8
|
from nautobot.dcim.models import Location, LocationType
|
|
9
9
|
from nautobot.extras.choices import ObjectChangeActionChoices, ObjectChangeEventContextChoices
|
|
10
|
-
from nautobot.extras.context_managers import
|
|
10
|
+
from nautobot.extras.context_managers import (
|
|
11
|
+
deferred_change_logging_for_bulk_operation,
|
|
12
|
+
web_request_context,
|
|
13
|
+
)
|
|
11
14
|
from nautobot.extras.models import Status, Webhook
|
|
12
15
|
|
|
13
16
|
# Use the proper swappable User model
|
|
@@ -193,3 +196,97 @@ class WebRequestContextTransactionTestCase(TransactionTestCase):
|
|
|
193
196
|
Status.objects.create(name="Test Status 2")
|
|
194
197
|
|
|
195
198
|
self.assertEqual(get_changes_for_model(Status).count(), 2)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class BulkEditDeleteChangeLogging(TestCase):
|
|
202
|
+
def setUp(self):
|
|
203
|
+
self.user = User.objects.create_user(
|
|
204
|
+
username="jacob",
|
|
205
|
+
email="jacob@example.com",
|
|
206
|
+
password="top_secret", # noqa: S106 # hardcoded-password-func-arg -- ok as this is test code only
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
def test_change_log_created(self):
|
|
210
|
+
location_type = LocationType.objects.get(name="Campus")
|
|
211
|
+
location_status = Status.objects.get_for_model(Location).first()
|
|
212
|
+
with web_request_context(self.user):
|
|
213
|
+
with deferred_change_logging_for_bulk_operation():
|
|
214
|
+
location = Location(name="Test Location 1", location_type=location_type, status=location_status)
|
|
215
|
+
location.save()
|
|
216
|
+
|
|
217
|
+
location = Location.objects.get(name="Test Location 1")
|
|
218
|
+
oc_list = get_changes_for_model(location).order_by("pk")
|
|
219
|
+
self.assertEqual(len(oc_list), 1)
|
|
220
|
+
self.assertEqual(oc_list[0].changed_object, location)
|
|
221
|
+
self.assertEqual(oc_list[0].action, ObjectChangeActionChoices.ACTION_CREATE)
|
|
222
|
+
|
|
223
|
+
def test_delete(self):
|
|
224
|
+
"""Test that deletes raise an exception"""
|
|
225
|
+
location_type = LocationType.objects.get(name="Campus")
|
|
226
|
+
location_status = Status.objects.get_for_model(Location).first()
|
|
227
|
+
with self.assertRaises(ValueError):
|
|
228
|
+
with web_request_context(self.user):
|
|
229
|
+
with deferred_change_logging_for_bulk_operation():
|
|
230
|
+
location = Location(name="Test Location 1", location_type=location_type, status=location_status)
|
|
231
|
+
location.save()
|
|
232
|
+
location.delete()
|
|
233
|
+
|
|
234
|
+
def test_create_then_update(self):
|
|
235
|
+
"""Test that a create followed by an update is logged as a single create"""
|
|
236
|
+
location_type = LocationType.objects.get(name="Campus")
|
|
237
|
+
location_status = Status.objects.get_for_model(Location).first()
|
|
238
|
+
with web_request_context(self.user):
|
|
239
|
+
with deferred_change_logging_for_bulk_operation():
|
|
240
|
+
location = Location(name="Test Location 1", location_type=location_type, status=location_status)
|
|
241
|
+
location.save()
|
|
242
|
+
location.description = "changed"
|
|
243
|
+
location.save()
|
|
244
|
+
|
|
245
|
+
oc_list = get_changes_for_model(location)
|
|
246
|
+
self.assertEqual(len(oc_list), 1)
|
|
247
|
+
self.assertEqual(oc_list[0].action, ObjectChangeActionChoices.ACTION_CREATE)
|
|
248
|
+
snapshots = oc_list[0].get_snapshots()
|
|
249
|
+
self.assertIsNone(snapshots["prechange"])
|
|
250
|
+
self.assertIsNotNone(snapshots["postchange"])
|
|
251
|
+
self.assertIsNone(snapshots["differences"]["removed"])
|
|
252
|
+
self.assertEqual(snapshots["differences"]["added"]["description"], "changed")
|
|
253
|
+
|
|
254
|
+
def test_bulk_edit(self):
|
|
255
|
+
"""Test that edits to multiple objects are correctly logged"""
|
|
256
|
+
location_type = LocationType.objects.get(name="Campus")
|
|
257
|
+
location_status = Status.objects.get_for_model(Location).first()
|
|
258
|
+
locations = [
|
|
259
|
+
Location(name=f"Test Location {i}", location_type=location_type, status=location_status)
|
|
260
|
+
for i in range(1, 4)
|
|
261
|
+
]
|
|
262
|
+
Location.objects.bulk_create(locations)
|
|
263
|
+
with web_request_context(self.user):
|
|
264
|
+
with deferred_change_logging_for_bulk_operation():
|
|
265
|
+
for location in locations:
|
|
266
|
+
location.description = "changed"
|
|
267
|
+
location.save()
|
|
268
|
+
|
|
269
|
+
oc_list = get_changes_for_model(Location)
|
|
270
|
+
self.assertEqual(len(oc_list), 3)
|
|
271
|
+
for oc in oc_list:
|
|
272
|
+
self.assertEqual(oc.action, ObjectChangeActionChoices.ACTION_UPDATE)
|
|
273
|
+
snapshots = oc.get_snapshots()
|
|
274
|
+
self.assertIsNone(snapshots["prechange"])
|
|
275
|
+
self.assertIsNotNone(snapshots["postchange"])
|
|
276
|
+
self.assertIsNone(snapshots["differences"]["removed"])
|
|
277
|
+
self.assertEqual(snapshots["differences"]["added"]["description"], "changed")
|
|
278
|
+
|
|
279
|
+
def test_change_log_context(self):
|
|
280
|
+
location_type = LocationType.objects.get(name="Campus")
|
|
281
|
+
location_status = Status.objects.get_for_model(Location).first()
|
|
282
|
+
with web_request_context(self.user, context_detail="test_change_log_context"):
|
|
283
|
+
with deferred_change_logging_for_bulk_operation():
|
|
284
|
+
location = Location(name="Test Location 1", location_type=location_type, status=location_status)
|
|
285
|
+
location.save()
|
|
286
|
+
|
|
287
|
+
location = Location.objects.get(name="Test Location 1")
|
|
288
|
+
oc_list = get_changes_for_model(location)
|
|
289
|
+
with self.subTest():
|
|
290
|
+
self.assertEqual(oc_list[0].change_context, ObjectChangeEventContextChoices.CONTEXT_ORM)
|
|
291
|
+
with self.subTest():
|
|
292
|
+
self.assertEqual(oc_list[0].change_context_detail, "test_change_log_context")
|
|
@@ -16,11 +16,13 @@ from nautobot.core.tables import CustomFieldColumn
|
|
|
16
16
|
from nautobot.core.testing import APITestCase, TestCase, TransactionTestCase
|
|
17
17
|
from nautobot.core.testing.models import ModelTestCases
|
|
18
18
|
from nautobot.core.testing.utils import post_data
|
|
19
|
+
from nautobot.core.utils.lookup import get_changes_for_model
|
|
19
20
|
from nautobot.dcim.filters import LocationFilterSet
|
|
20
21
|
from nautobot.dcim.forms import RackFilterForm
|
|
21
22
|
from nautobot.dcim.models import Device, Location, LocationType, Rack
|
|
22
23
|
from nautobot.dcim.tables import LocationTable
|
|
23
24
|
from nautobot.extras.choices import CustomFieldFilterLogicChoices, CustomFieldTypeChoices
|
|
25
|
+
from nautobot.extras.context_managers import web_request_context
|
|
24
26
|
from nautobot.extras.models import ComputedField, CustomField, CustomFieldChoice, Status
|
|
25
27
|
from nautobot.users.models import ObjectPermission
|
|
26
28
|
from nautobot.virtualization.models import VirtualMachine
|
|
@@ -1537,8 +1539,10 @@ class CustomFieldFilterTest(TestCase):
|
|
|
1537
1539
|
cf.save()
|
|
1538
1540
|
cf.content_types.set([obj_type])
|
|
1539
1541
|
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
+
cls.select_choices = (
|
|
1543
|
+
CustomFieldChoice.objects.create(custom_field=cf, value="Foo"),
|
|
1544
|
+
CustomFieldChoice.objects.create(custom_field=cf, value="Bar"),
|
|
1545
|
+
)
|
|
1542
1546
|
|
|
1543
1547
|
# Multi-select filtering
|
|
1544
1548
|
cf = CustomField(
|
|
@@ -1548,8 +1552,11 @@ class CustomFieldFilterTest(TestCase):
|
|
|
1548
1552
|
cf.save()
|
|
1549
1553
|
cf.content_types.set([obj_type])
|
|
1550
1554
|
|
|
1551
|
-
|
|
1552
|
-
|
|
1555
|
+
cls.multiselect_choices = (
|
|
1556
|
+
CustomFieldChoice.objects.create(custom_field=cf, value="Foo"),
|
|
1557
|
+
CustomFieldChoice.objects.create(custom_field=cf, value="Bar"),
|
|
1558
|
+
)
|
|
1559
|
+
|
|
1553
1560
|
cls.location_type = LocationType.objects.get(name="Campus")
|
|
1554
1561
|
location_status = Status.objects.get_for_model(Location).first()
|
|
1555
1562
|
Location.objects.create(
|
|
@@ -1844,6 +1851,10 @@ class CustomFieldFilterTest(TestCase):
|
|
|
1844
1851
|
self.filterset({"cf_cf8": ["Foo", "AR"]}, self.queryset).qs,
|
|
1845
1852
|
self.queryset.filter(_custom_field_data__cf8__in=["Foo", "AR"]),
|
|
1846
1853
|
)
|
|
1854
|
+
self.assertQuerysetEqualAndNotEmpty( # https://github.com/nautobot/nautobot/issues/5009
|
|
1855
|
+
self.filterset({"cf_cf8": [str(choice.pk) for choice in self.select_choices]}, self.queryset).qs,
|
|
1856
|
+
self.queryset.filter(_custom_field_data__cf8__in=[choice.value for choice in self.select_choices]),
|
|
1857
|
+
)
|
|
1847
1858
|
self.assertQuerysetEqual(
|
|
1848
1859
|
self.filterset({"cf_cf8__n": ["Foo"]}, self.queryset).qs,
|
|
1849
1860
|
self.queryset.exclude(_custom_field_data__cf8="Foo")
|
|
@@ -1913,6 +1924,10 @@ class CustomFieldFilterTest(TestCase):
|
|
|
1913
1924
|
self.filterset({"cf_cf9": "Bar"}, self.queryset).qs,
|
|
1914
1925
|
self.queryset.filter(_custom_field_data__cf9__contains="Bar"),
|
|
1915
1926
|
)
|
|
1927
|
+
self.assertQuerysetEqualAndNotEmpty( # https://github.com/nautobot/nautobot/issues/5009
|
|
1928
|
+
self.filterset({"cf_cf9": str(self.multiselect_choices[0].pk)}, self.queryset).qs,
|
|
1929
|
+
self.queryset.filter(_custom_field_data__cf9__contains=self.multiselect_choices[0].value),
|
|
1930
|
+
)
|
|
1916
1931
|
|
|
1917
1932
|
|
|
1918
1933
|
class CustomFieldChoiceTest(ModelTestCases.BaseModelTestCase):
|
|
@@ -2016,30 +2031,64 @@ class CustomFieldBackgroundTasks(TransactionTestCase):
|
|
|
2016
2031
|
|
|
2017
2032
|
self.assertEqual(location.cf["cf1"], "Foo")
|
|
2018
2033
|
|
|
2034
|
+
with web_request_context(self.user):
|
|
2035
|
+
cf = CustomField(label="CF2", type=CustomFieldTypeChoices.TYPE_TEXT, default="Bar")
|
|
2036
|
+
cf.save()
|
|
2037
|
+
cf.content_types.set([obj_type])
|
|
2038
|
+
|
|
2039
|
+
location.refresh_from_db()
|
|
2040
|
+
|
|
2041
|
+
self.assertEqual(location.cf["cf2"], "Bar")
|
|
2042
|
+
|
|
2043
|
+
oc_list = get_changes_for_model(location).order_by("pk")
|
|
2044
|
+
self.assertEqual(len(oc_list), 1)
|
|
2045
|
+
self.assertEqual(oc_list[0].changed_object, location)
|
|
2046
|
+
self.assertEqual(oc_list[0].change_context_detail, "provision custom field data for new content types")
|
|
2047
|
+
self.assertEqual(oc_list[0].user, self.user)
|
|
2048
|
+
|
|
2019
2049
|
def test_delete_custom_field_data_task(self):
|
|
2020
2050
|
obj_type = ContentType.objects.get_for_model(Location)
|
|
2021
|
-
|
|
2051
|
+
cf_1 = CustomField(
|
|
2022
2052
|
label="CF1",
|
|
2023
2053
|
type=CustomFieldTypeChoices.TYPE_TEXT,
|
|
2024
2054
|
)
|
|
2025
|
-
|
|
2026
|
-
|
|
2055
|
+
cf_1.save()
|
|
2056
|
+
cf_1.content_types.set([obj_type])
|
|
2057
|
+
cf_2 = CustomField(
|
|
2058
|
+
label="CF2",
|
|
2059
|
+
type=CustomFieldTypeChoices.TYPE_TEXT,
|
|
2060
|
+
)
|
|
2061
|
+
cf_2.save()
|
|
2062
|
+
cf_2.content_types.set([obj_type])
|
|
2027
2063
|
location_type = LocationType.objects.create(name="Root Type 2")
|
|
2028
2064
|
location_status = Status.objects.get_for_model(Location).first()
|
|
2029
2065
|
location = Location(
|
|
2030
2066
|
name="Location 1",
|
|
2031
2067
|
location_type=location_type,
|
|
2032
2068
|
status=location_status,
|
|
2033
|
-
_custom_field_data={"cf1": "foo"},
|
|
2069
|
+
_custom_field_data={"cf1": "foo", "cf2": "bar"},
|
|
2034
2070
|
)
|
|
2035
2071
|
location.save()
|
|
2036
2072
|
|
|
2037
|
-
|
|
2073
|
+
cf_1.delete()
|
|
2038
2074
|
|
|
2039
2075
|
location.refresh_from_db()
|
|
2040
2076
|
|
|
2041
2077
|
self.assertTrue("cf1" not in location.cf)
|
|
2042
2078
|
|
|
2079
|
+
with web_request_context(self.user):
|
|
2080
|
+
cf_2.delete()
|
|
2081
|
+
|
|
2082
|
+
location.refresh_from_db()
|
|
2083
|
+
|
|
2084
|
+
self.assertTrue("cf2" not in location.cf)
|
|
2085
|
+
|
|
2086
|
+
oc_list = get_changes_for_model(location).order_by("pk")
|
|
2087
|
+
self.assertEqual(len(oc_list), 1)
|
|
2088
|
+
self.assertEqual(oc_list[0].changed_object, location)
|
|
2089
|
+
self.assertEqual(oc_list[0].change_context_detail, "delete custom field data")
|
|
2090
|
+
self.assertEqual(oc_list[0].user, self.user)
|
|
2091
|
+
|
|
2043
2092
|
def test_update_custom_field_choice_data_task(self):
|
|
2044
2093
|
obj_type = ContentType.objects.get_for_model(Location)
|
|
2045
2094
|
cf = CustomField(
|
|
@@ -2068,6 +2117,20 @@ class CustomFieldBackgroundTasks(TransactionTestCase):
|
|
|
2068
2117
|
|
|
2069
2118
|
self.assertEqual(location.cf["cf1"], "Bar")
|
|
2070
2119
|
|
|
2120
|
+
with web_request_context(self.user):
|
|
2121
|
+
choice.value = "FizzBuzz"
|
|
2122
|
+
choice.save()
|
|
2123
|
+
|
|
2124
|
+
location.refresh_from_db()
|
|
2125
|
+
|
|
2126
|
+
self.assertEqual(location.cf["cf1"], "FizzBuzz")
|
|
2127
|
+
|
|
2128
|
+
oc_list = get_changes_for_model(location).order_by("pk")
|
|
2129
|
+
self.assertEqual(len(oc_list), 1)
|
|
2130
|
+
self.assertEqual(oc_list[0].changed_object, location)
|
|
2131
|
+
self.assertEqual(oc_list[0].change_context_detail, "update custom field choice data")
|
|
2132
|
+
self.assertEqual(oc_list[0].user, self.user)
|
|
2133
|
+
|
|
2071
2134
|
|
|
2072
2135
|
class CustomFieldTableTest(TestCase):
|
|
2073
2136
|
"""
|
|
@@ -16,6 +16,7 @@ from nautobot.dcim.choices import PortTypeChoices
|
|
|
16
16
|
from nautobot.dcim.filters import DeviceFilterSet
|
|
17
17
|
from nautobot.dcim.forms import DeviceFilterForm, DeviceForm
|
|
18
18
|
from nautobot.dcim.models import (
|
|
19
|
+
Controller,
|
|
19
20
|
Device,
|
|
20
21
|
DeviceType,
|
|
21
22
|
FrontPort,
|
|
@@ -48,6 +49,7 @@ from nautobot.tenancy.models import Tenant
|
|
|
48
49
|
class DynamicGroupTestBase(TestCase):
|
|
49
50
|
@classmethod
|
|
50
51
|
def setUpTestData(cls):
|
|
52
|
+
Controller.objects.filter(controller_device__isnull=False).delete()
|
|
51
53
|
Device.objects.all().delete()
|
|
52
54
|
cls.device_ct = ContentType.objects.get_for_model(Device)
|
|
53
55
|
cls.dynamicgroup_ct = ContentType.objects.get_for_model(DynamicGroup)
|
|
@@ -61,6 +61,7 @@ from nautobot.extras.models import (
|
|
|
61
61
|
ComputedField,
|
|
62
62
|
ConfigContext,
|
|
63
63
|
Contact,
|
|
64
|
+
ContactAssociation,
|
|
64
65
|
CustomField,
|
|
65
66
|
CustomFieldChoice,
|
|
66
67
|
CustomLink,
|
|
@@ -457,7 +458,91 @@ class ContentTypeFilterSetTestCase(FilterTestCases.FilterTestCase):
|
|
|
457
458
|
)
|
|
458
459
|
|
|
459
460
|
|
|
460
|
-
class
|
|
461
|
+
class ContactAndTeamFilterSetTestCaseMixin:
|
|
462
|
+
"""Mixin class to test common filters to both Contact and Team filter sets."""
|
|
463
|
+
|
|
464
|
+
def test_similar_to_location_data(self):
|
|
465
|
+
"""Complex test to test the complex `similar_to_location_data` method filter."""
|
|
466
|
+
ContactAssociation.objects.all().delete()
|
|
467
|
+
self.queryset.delete()
|
|
468
|
+
location_type = LocationType.objects.filter(parent__isnull=True).first()
|
|
469
|
+
location_status = Status.objects.get_for_model(Location).first()
|
|
470
|
+
test_locations = (
|
|
471
|
+
Location.objects.create(
|
|
472
|
+
location_type=location_type,
|
|
473
|
+
name="Filter Test Location 0",
|
|
474
|
+
status=location_status,
|
|
475
|
+
contact_name="match 0",
|
|
476
|
+
),
|
|
477
|
+
Location.objects.create(
|
|
478
|
+
location_type=location_type,
|
|
479
|
+
name="Filter Test Location 1",
|
|
480
|
+
status=location_status,
|
|
481
|
+
contact_email="Test email for location 1 and 2",
|
|
482
|
+
),
|
|
483
|
+
Location.objects.create(
|
|
484
|
+
location_type=location_type,
|
|
485
|
+
name="Filter Test Location 2",
|
|
486
|
+
status=location_status,
|
|
487
|
+
contact_email="TEST EMAIL FOR LOCATION 1 AND 2",
|
|
488
|
+
contact_phone="Test phone for location 2 and 3",
|
|
489
|
+
),
|
|
490
|
+
Location.objects.create(
|
|
491
|
+
location_type=location_type,
|
|
492
|
+
name="Filter Test Location 3",
|
|
493
|
+
status=location_status,
|
|
494
|
+
contact_phone="Test phone for location 2 and 3",
|
|
495
|
+
),
|
|
496
|
+
Location.objects.create(
|
|
497
|
+
location_type=location_type,
|
|
498
|
+
name="Filter Test Location 4",
|
|
499
|
+
status=location_status,
|
|
500
|
+
contact_name="Hopefully this doesn't match any random factory data",
|
|
501
|
+
contact_email="Hopefully this doesn't match any random factory data",
|
|
502
|
+
contact_phone="Hopefully this doesn't match any random factory data",
|
|
503
|
+
physical_address="Hopefully this doesn't match any random factory data",
|
|
504
|
+
shipping_address="Hopefully this doesn't match any random factory data",
|
|
505
|
+
),
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
self.queryset.create(name="match 0")
|
|
509
|
+
self.queryset.create(name="match 1 and 2", email="Test email for location 1 and 2")
|
|
510
|
+
self.queryset.create(name="match 2 and 3", phone="Test phone for location 2 and 3")
|
|
511
|
+
|
|
512
|
+
# These subtests are confusing because we're trying to test the NaturalKeyOrPKMultipleChoiceFilter
|
|
513
|
+
# behavior while also testing the `similar_to_location_data` method filter behavior.
|
|
514
|
+
with self.subTest("Test name match"):
|
|
515
|
+
params = {"similar_to_location_data": [test_locations[0].pk]}
|
|
516
|
+
self.assertQuerysetEqualAndNotEmpty(
|
|
517
|
+
self.filterset(params, self.queryset).qs,
|
|
518
|
+
self.queryset.filter(name__in=["match 0"]),
|
|
519
|
+
)
|
|
520
|
+
with self.subTest("Test email match"):
|
|
521
|
+
params = {"similar_to_location_data": [test_locations[1].pk]}
|
|
522
|
+
self.assertQuerysetEqualAndNotEmpty(
|
|
523
|
+
self.filterset(params, self.queryset).qs,
|
|
524
|
+
self.queryset.filter(name__in=["match 1 and 2"]),
|
|
525
|
+
)
|
|
526
|
+
with self.subTest("Test phone match"):
|
|
527
|
+
params = {"similar_to_location_data": [test_locations[2].pk]}
|
|
528
|
+
self.assertQuerysetEqualAndNotEmpty(
|
|
529
|
+
self.filterset(params, self.queryset).qs,
|
|
530
|
+
self.queryset.filter(name__in=["match 1 and 2", "match 2 and 3"]),
|
|
531
|
+
)
|
|
532
|
+
with self.subTest("Test email and phone match"):
|
|
533
|
+
params = {"similar_to_location_data": [test_locations[1].pk, test_locations[3].name]}
|
|
534
|
+
self.assertQuerysetEqualAndNotEmpty(
|
|
535
|
+
self.filterset(params, self.queryset).qs,
|
|
536
|
+
self.queryset.filter(name__in=["match 1 and 2", "match 2 and 3"]),
|
|
537
|
+
)
|
|
538
|
+
with self.subTest("Test no match"):
|
|
539
|
+
params = {"similar_to_location_data": [test_locations[4].pk]}
|
|
540
|
+
self.assertFalse(self.filterset(params, self.queryset).qs.exists())
|
|
541
|
+
params = {"similar_to_location_data": [test_locations[4].name]}
|
|
542
|
+
self.assertFalse(self.filterset(params, self.queryset).qs.exists())
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
class ContactFilterSetTestCase(ContactAndTeamFilterSetTestCaseMixin, FilterTestCases.FilterTestCase):
|
|
461
546
|
queryset = Contact.objects.all()
|
|
462
547
|
filterset = ContactFilterSet
|
|
463
548
|
|
|
@@ -1756,7 +1841,7 @@ class TagTestCase(FilterTestCases.NameOnlyFilterTestCase):
|
|
|
1756
1841
|
self.assertEqual(self.filterset(params, self.queryset).qs.values_list("pk", flat=True)[0], value)
|
|
1757
1842
|
|
|
1758
1843
|
|
|
1759
|
-
class TeamFilterSetTestCase(FilterTestCases.FilterTestCase):
|
|
1844
|
+
class TeamFilterSetTestCase(ContactAndTeamFilterSetTestCaseMixin, FilterTestCases.FilterTestCase):
|
|
1760
1845
|
queryset = Team.objects.all()
|
|
1761
1846
|
filterset = TeamFilterSet
|
|
1762
1847
|
|
|
@@ -1838,8 +1923,8 @@ class RoleTestCase(FilterTestCases.NameOnlyFilterTestCase):
|
|
|
1838
1923
|
def test_content_types(self):
|
|
1839
1924
|
device_ct = ContentType.objects.get_for_model(Device)
|
|
1840
1925
|
rack_ct = ContentType.objects.get_for_model(Rack)
|
|
1841
|
-
device_roles = self.queryset.filter(
|
|
1842
|
-
params = {"content_types": ["dcim.device"]}
|
|
1926
|
+
device_roles = self.queryset.filter(content_types__in=[device_ct, rack_ct]).distinct()
|
|
1927
|
+
params = {"content_types": ["dcim.device", "dcim.rack"]}
|
|
1843
1928
|
self.assertQuerysetEqualAndNotEmpty(self.filterset(params, self.queryset).qs, device_roles)
|
|
1844
1929
|
|
|
1845
1930
|
rack_roles = self.queryset.filter(content_types=rack_ct)
|
|
@@ -1810,6 +1810,15 @@ class JobResultTestCase(TestCase):
|
|
|
1810
1810
|
JobResult.objects.create(name="ExampleJob2", user=None, result=lambda: 1)
|
|
1811
1811
|
self.assertEqual(str(err.exception), "Object of type function is not JSON serializable")
|
|
1812
1812
|
|
|
1813
|
+
def test_get_task(self):
|
|
1814
|
+
"""Assert bug fix for `Cannot resolve keyword 'task_id' into field` #5440"""
|
|
1815
|
+
data = {
|
|
1816
|
+
"output": "valid data",
|
|
1817
|
+
}
|
|
1818
|
+
job_result = JobResult.objects.create(name="ExampleJob1", user=None, result=data)
|
|
1819
|
+
|
|
1820
|
+
self.assertEqual(JobResult.objects.get_task(job_result.pk), job_result)
|
|
1821
|
+
|
|
1813
1822
|
|
|
1814
1823
|
class WebhookTest(ModelTestCases.BaseModelTestCase):
|
|
1815
1824
|
model = Webhook
|
|
@@ -15,7 +15,15 @@ from nautobot.core.tables import RelationshipColumn
|
|
|
15
15
|
from nautobot.core.testing import TestCase
|
|
16
16
|
from nautobot.core.testing.models import ModelTestCases
|
|
17
17
|
from nautobot.core.utils.lookup import get_route_for_model
|
|
18
|
-
from nautobot.dcim.models import
|
|
18
|
+
from nautobot.dcim.models import (
|
|
19
|
+
Controller,
|
|
20
|
+
Device,
|
|
21
|
+
DeviceTypeToSoftwareImageFile,
|
|
22
|
+
Location,
|
|
23
|
+
LocationType,
|
|
24
|
+
Platform,
|
|
25
|
+
Rack,
|
|
26
|
+
)
|
|
19
27
|
from nautobot.dcim.tables import LocationTable
|
|
20
28
|
from nautobot.dcim.tests.test_views import create_test_device
|
|
21
29
|
from nautobot.extras.choices import RelationshipRequiredSideChoices, RelationshipSideChoices, RelationshipTypeChoices
|
|
@@ -1165,6 +1173,7 @@ class RequiredRelationshipTestMixin:
|
|
|
1165
1173
|
# Protected FK to SoftwareImageFile prevents deletion
|
|
1166
1174
|
DeviceTypeToSoftwareImageFile.objects.all().delete()
|
|
1167
1175
|
# Protected FK to SoftwareVersion prevents deletion
|
|
1176
|
+
Controller.objects.all().delete()
|
|
1168
1177
|
Device.objects.all().update(software_version=None)
|
|
1169
1178
|
|
|
1170
1179
|
# Create required relationships:
|