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/core/tests/test_api.py
CHANGED
|
@@ -770,6 +770,7 @@ class APIOrderingTestCase(testing.APITestCase):
|
|
|
770
770
|
"TextField": "admin_contact",
|
|
771
771
|
"DateTimeField": "created",
|
|
772
772
|
}
|
|
773
|
+
cls.maxDiff = None
|
|
773
774
|
|
|
774
775
|
def _validate_sorted_response(self, response, queryset, field_name, is_fk_field=False):
|
|
775
776
|
self.assertHttpStatus(response, 200)
|
|
@@ -794,18 +795,24 @@ class APIOrderingTestCase(testing.APITestCase):
|
|
|
794
795
|
"""Tests that results are returned in the expected ascending order."""
|
|
795
796
|
|
|
796
797
|
for field_type, field_name in self.field_type_map.items():
|
|
797
|
-
with self.subTest(f"Testing {field_type}"):
|
|
798
|
-
|
|
799
|
-
|
|
798
|
+
with self.subTest(f"Testing {field_type} {field_name}"):
|
|
799
|
+
# Use `name` as a secondary sort as fields like `asn` and `admin_contact` may be null
|
|
800
|
+
response = self.client.get(f"{self.url}?sort={field_name},name&limit=10", **self.header)
|
|
801
|
+
self._validate_sorted_response(
|
|
802
|
+
response, Provider.objects.all().order_by(field_name, "name"), field_name
|
|
803
|
+
)
|
|
800
804
|
|
|
801
805
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
802
806
|
def test_descending_sort(self):
|
|
803
807
|
"""Tests that results are returned in the expected descending order."""
|
|
804
808
|
|
|
805
809
|
for field_type, field_name in self.field_type_map.items():
|
|
806
|
-
with self.subTest(f"Testing {field_type}"):
|
|
807
|
-
|
|
808
|
-
|
|
810
|
+
with self.subTest(f"Testing {field_type} {field_name}"):
|
|
811
|
+
# Use `name` as a secondary sort as fields like `asn` and `admin_contact` may be null
|
|
812
|
+
response = self.client.get(f"{self.url}?sort=-{field_name},name&limit=10", **self.header)
|
|
813
|
+
self._validate_sorted_response(
|
|
814
|
+
response, Provider.objects.all().order_by(f"-{field_name}", "name"), field_name
|
|
815
|
+
)
|
|
809
816
|
|
|
810
817
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
811
818
|
def test_sorting_tree_node_models(self):
|
nautobot/core/tests/test_csv.py
CHANGED
|
@@ -4,7 +4,7 @@ from django.urls import reverse
|
|
|
4
4
|
|
|
5
5
|
from nautobot.core.constants import CSV_NO_OBJECT, CSV_NULL_TYPE, VARBINARY_IP_FIELD_REPR_OF_CSV_NO_OBJECT
|
|
6
6
|
from nautobot.dcim.api.serializers import DeviceSerializer
|
|
7
|
-
from nautobot.dcim.models.devices import Device, DeviceType
|
|
7
|
+
from nautobot.dcim.models.devices import Controller, Device, DeviceType
|
|
8
8
|
from nautobot.dcim.models.locations import Location
|
|
9
9
|
from nautobot.extras.models.roles import Role
|
|
10
10
|
from nautobot.extras.models.statuses import Status
|
|
@@ -25,6 +25,7 @@ class CSVParsingRelatedTestCase(TestCase):
|
|
|
25
25
|
devicerole = Role.objects.get_for_model(Device).first()
|
|
26
26
|
device_status = Status.objects.get_for_model(Device).first()
|
|
27
27
|
tags = Tag.objects.get_for_model(Device).all()[:3]
|
|
28
|
+
Controller.objects.filter(controller_device__isnull=False).delete()
|
|
28
29
|
Device.objects.all().delete()
|
|
29
30
|
self.device = Device.objects.create(
|
|
30
31
|
device_type=devicetype,
|
|
@@ -92,7 +93,7 @@ class CSVParsingRelatedTestCase(TestCase):
|
|
|
92
93
|
"primary_ip6__host",
|
|
93
94
|
"cluster__name",
|
|
94
95
|
"virtual_chassis__name",
|
|
95
|
-
"
|
|
96
|
+
"controller_managed_device_group__name",
|
|
96
97
|
"device_redundancy_group__name",
|
|
97
98
|
"software_version__platform__name",
|
|
98
99
|
"software_version__version",
|
|
@@ -119,7 +120,7 @@ class CSVParsingRelatedTestCase(TestCase):
|
|
|
119
120
|
"primary_ip6",
|
|
120
121
|
"cluster",
|
|
121
122
|
"virtual_chassis",
|
|
122
|
-
"
|
|
123
|
+
"controller_managed_device_group",
|
|
123
124
|
"device_redundancy_group",
|
|
124
125
|
"secrets_group",
|
|
125
126
|
]
|
|
@@ -224,7 +225,7 @@ class CSVParsingRelatedTestCase(TestCase):
|
|
|
224
225
|
"primary_ip6__host": CSV_NO_OBJECT,
|
|
225
226
|
"cluster__name": CSV_NO_OBJECT,
|
|
226
227
|
"virtual_chassis__name": CSV_NO_OBJECT,
|
|
227
|
-
"
|
|
228
|
+
"controller_managed_device_group__name": CSV_NO_OBJECT,
|
|
228
229
|
"device_redundancy_group__name": CSV_NO_OBJECT,
|
|
229
230
|
"software_version__platform__name": CSV_NO_OBJECT,
|
|
230
231
|
"software_version__version": CSV_NO_OBJECT,
|
|
@@ -17,7 +17,7 @@ from nautobot.core.constants import CHARFIELD_MAX_LENGTH
|
|
|
17
17
|
from nautobot.core.models import fields as core_fields
|
|
18
18
|
from nautobot.core.utils import lookup
|
|
19
19
|
from nautobot.dcim import choices as dcim_choices, filters as dcim_filters, models as dcim_models
|
|
20
|
-
from nautobot.dcim.models import Device
|
|
20
|
+
from nautobot.dcim.models import Controller, Device
|
|
21
21
|
from nautobot.extras import models as extras_models
|
|
22
22
|
from nautobot.extras.utils import FeatureQuery
|
|
23
23
|
from nautobot.ipam import models as ipam_models
|
|
@@ -830,6 +830,7 @@ class DynamicFilterLookupExpressionTest(TestCase):
|
|
|
830
830
|
@classmethod
|
|
831
831
|
def setUpTestData(cls):
|
|
832
832
|
manufacturers = dcim_models.Manufacturer.objects.all()[:3]
|
|
833
|
+
Controller.objects.filter(controller_device__isnull=False).delete()
|
|
833
834
|
Device.objects.all().delete()
|
|
834
835
|
|
|
835
836
|
device_types = (
|
|
@@ -43,6 +43,7 @@ from nautobot.dcim.models import (
|
|
|
43
43
|
Cable,
|
|
44
44
|
ConsolePort,
|
|
45
45
|
ConsoleServerPort,
|
|
46
|
+
Controller,
|
|
46
47
|
Device,
|
|
47
48
|
DeviceType,
|
|
48
49
|
FrontPort,
|
|
@@ -630,21 +631,9 @@ class GraphQLAPIPermissionTest(GraphQLTestCaseBase):
|
|
|
630
631
|
self.assertEqual(names, ["Rack 1-1", "Rack 1-2", "Rack 2-1", "Rack 2-2"])
|
|
631
632
|
|
|
632
633
|
def test_graphql_api_no_token(self):
|
|
633
|
-
"""Validate unauthenticated users are not able to query anything
|
|
634
|
+
"""Validate unauthenticated users are not able to query anything."""
|
|
634
635
|
response = self.client.post(self.api_url, {"query": self.get_racks_query}, format="json")
|
|
635
|
-
self.assertEqual(response.status_code, status.
|
|
636
|
-
self.assertIsInstance(response.data["data"]["racks"], list)
|
|
637
|
-
names = [item["name"] for item in response.data["data"]["racks"]]
|
|
638
|
-
self.assertEqual(names, [])
|
|
639
|
-
|
|
640
|
-
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
641
|
-
def test_graphql_api_no_token_exempt(self):
|
|
642
|
-
"""Validate unauthenticated users are able to query based on the exempt permissions."""
|
|
643
|
-
response = self.client.post(self.api_url, {"query": self.get_racks_query}, format="json")
|
|
644
|
-
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
645
|
-
self.assertIsInstance(response.data["data"]["racks"], list)
|
|
646
|
-
names = [item["name"] for item in response.data["data"]["racks"]]
|
|
647
|
-
self.assertEqual(names, ["Rack 1-1", "Rack 1-2", "Rack 2-1", "Rack 2-2"])
|
|
636
|
+
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
|
648
637
|
|
|
649
638
|
def test_graphql_api_wrong_token(self):
|
|
650
639
|
"""Validate a wrong token return 403."""
|
|
@@ -721,6 +710,7 @@ class GraphQLQueryTest(GraphQLTestCaseBase):
|
|
|
721
710
|
|
|
722
711
|
# Remove random IPAddress and Device fixtures for this custom test
|
|
723
712
|
IPAddress.objects.all().delete()
|
|
713
|
+
Controller.objects.filter(controller_device__isnull=False).delete()
|
|
724
714
|
Device.objects.all().delete()
|
|
725
715
|
|
|
726
716
|
# Initialize fake request that will be required to execute GraphQL query
|
|
@@ -44,6 +44,8 @@ class NavMenuTestCase(TestCase):
|
|
|
44
44
|
expected_name = "Interfaces"
|
|
45
45
|
elif expected_name == "Object Changes":
|
|
46
46
|
expected_name = "Change Log"
|
|
47
|
+
elif expected_name == "Controller Managed Device Groups":
|
|
48
|
+
expected_name = "Managed Device Groups"
|
|
47
49
|
self.assertEqual(item_details["name"], expected_name)
|
|
48
50
|
if item_url == get_route_for_model(view_model, "list"):
|
|
49
51
|
# Not assertEqual as some menu items have additional permissions defined.
|
|
@@ -87,6 +89,7 @@ class NavMenuTestCase(TestCase):
|
|
|
87
89
|
self.assertEqual(expected_perms[tab_name], tab_details["permissions"])
|
|
88
90
|
|
|
89
91
|
|
|
92
|
+
@tag("unit")
|
|
90
93
|
class NewUINavTest(TestCase):
|
|
91
94
|
@patch.dict(registry, values={"new_ui_nav_menu": {}}, clear=True)
|
|
92
95
|
def test_build_new_ui_nav_menu(self):
|
|
@@ -2,6 +2,7 @@ import re
|
|
|
2
2
|
from unittest import mock
|
|
3
3
|
import urllib.parse
|
|
4
4
|
|
|
5
|
+
from django.apps import apps
|
|
5
6
|
from django.contrib.contenttypes.models import ContentType
|
|
6
7
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
|
7
8
|
from django.test import override_settings, RequestFactory
|
|
@@ -9,6 +10,7 @@ from django.test.utils import override_script_prefix
|
|
|
9
10
|
from django.urls import get_script_prefix, reverse
|
|
10
11
|
from prometheus_client.parser import text_string_to_metric_families
|
|
11
12
|
|
|
13
|
+
from nautobot.core.constants import GLOBAL_SEARCH_EXCLUDE_LIST
|
|
12
14
|
from nautobot.core.testing import TestCase
|
|
13
15
|
from nautobot.core.testing.api import APITestCase
|
|
14
16
|
from nautobot.core.utils.permissions import get_permission_for_model
|
|
@@ -71,6 +73,27 @@ class HomeViewTestCase(TestCase):
|
|
|
71
73
|
response = self.client.get(f"{url}?{urllib.parse.urlencode(params)}")
|
|
72
74
|
self.assertHttpStatus(response, 200)
|
|
73
75
|
|
|
76
|
+
def test_appropriate_models_included_in_global_search(self):
|
|
77
|
+
# Gather core app configs
|
|
78
|
+
existing_models = []
|
|
79
|
+
global_searchable_models = []
|
|
80
|
+
for app_name in ["circuits", "dcim", "extras", "ipam", "tenancy", "virtualization"]:
|
|
81
|
+
app_config = apps.get_app_config(app_name)
|
|
82
|
+
existing_models += [model._meta.model_name for model in app_config.get_models()]
|
|
83
|
+
global_searchable_models += app_config.searchable_models
|
|
84
|
+
|
|
85
|
+
# Remove those models that are not searchable
|
|
86
|
+
existing_models = [model for model in existing_models if model not in GLOBAL_SEARCH_EXCLUDE_LIST]
|
|
87
|
+
existing_models.sort()
|
|
88
|
+
|
|
89
|
+
# See if there are any models that are missing from global search
|
|
90
|
+
difference = [model for model in existing_models if model not in global_searchable_models]
|
|
91
|
+
if difference:
|
|
92
|
+
self.fail(
|
|
93
|
+
f'Existing model/models {",".join(difference)} are not included in the searchable_models attribute of the app config.\n'
|
|
94
|
+
'If you do not want the models to be searchable, please include them in the GLOBAL_SEARCH_EXCLUDE_LIST constant in nautobot.core.constants.'
|
|
95
|
+
)
|
|
96
|
+
|
|
74
97
|
def make_request(self):
|
|
75
98
|
url = reverse("home")
|
|
76
99
|
response = self.client.get(url)
|
|
@@ -305,25 +328,31 @@ class LoginUITestCase(TestCase):
|
|
|
305
328
|
sso_login_search_result = self.make_request()
|
|
306
329
|
self.assertIsNotNone(sso_login_search_result)
|
|
307
330
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
"""Assert that api docs and graphql redirects to login page if user is unauthenticated."""
|
|
331
|
+
def test_graphql_redirects_back_to_login_unauthenticated(self):
|
|
332
|
+
"""Assert that graphql redirects to login page if user is unauthenticated."""
|
|
311
333
|
self.client.logout()
|
|
312
334
|
headers = {"HTTP_ACCEPT": "text/html"}
|
|
313
|
-
|
|
335
|
+
url = reverse("graphql")
|
|
336
|
+
response = self.client.get(url, follow=True, **headers)
|
|
337
|
+
self.assertHttpStatus(response, 200)
|
|
338
|
+
self.assertRedirects(response, f"/login/?next={url}")
|
|
339
|
+
response_content = response.content.decode(response.charset).replace("\n", "")
|
|
340
|
+
for footer_text in self.footer_elements:
|
|
341
|
+
self.assertNotIn(footer_text, response_content)
|
|
342
|
+
|
|
343
|
+
def test_api_docs_403_unauthenticated(self):
|
|
344
|
+
"""Assert that api docs return a 403 Forbidden if user is unauthenticated."""
|
|
345
|
+
self.client.logout()
|
|
346
|
+
urls = [
|
|
347
|
+
reverse("api_docs"),
|
|
348
|
+
reverse("api_redocs"),
|
|
349
|
+
reverse("schema"),
|
|
350
|
+
reverse("schema_json"),
|
|
351
|
+
reverse("schema_yaml"),
|
|
352
|
+
]
|
|
314
353
|
for url in urls:
|
|
315
|
-
response = self.client.get(url
|
|
316
|
-
self.assertHttpStatus(response,
|
|
317
|
-
redirect_chain = [(f"/login/?next={url}", 302)]
|
|
318
|
-
self.assertEqual(response.redirect_chain, redirect_chain)
|
|
319
|
-
response_content = response.content.decode(response.charset).replace("\n", "")
|
|
320
|
-
# Assert Footer items(`self.footer_elements`), Banner and Banner Top is hidden
|
|
321
|
-
for footer_text in self.footer_elements:
|
|
322
|
-
self.assertNotIn(footer_text, response_content)
|
|
323
|
-
# Only API Docs implements BANNERS
|
|
324
|
-
if url == urls[0]:
|
|
325
|
-
self.assertNotIn("Hello, Banner Top", response_content)
|
|
326
|
-
self.assertNotIn("Hello, Banner Bottom", response_content)
|
|
354
|
+
response = self.client.get(url)
|
|
355
|
+
self.assertHttpStatus(response, 403)
|
|
327
356
|
|
|
328
357
|
|
|
329
358
|
class MetricsViewTestCase(TestCase):
|
nautobot/core/utils/data.py
CHANGED
nautobot/core/utils/lookup.py
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
"""Utilities for looking up related classes and information."""
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
|
+
import re
|
|
4
5
|
|
|
5
6
|
from django.apps import apps
|
|
6
7
|
from django.conf import settings
|
|
7
8
|
from django.contrib.auth.models import Group
|
|
8
9
|
from django.contrib.contenttypes.models import ContentType
|
|
9
10
|
from django.db.models import Model
|
|
11
|
+
from django.urls import get_resolver, URLPattern, URLResolver
|
|
10
12
|
from django.utils.module_loading import import_string
|
|
11
13
|
|
|
12
14
|
|
|
@@ -232,3 +234,127 @@ def get_created_and_last_updated_usernames_for_model(instance):
|
|
|
232
234
|
last_updated_by = last_updated_by_record.user_name
|
|
233
235
|
|
|
234
236
|
return created_by, last_updated_by
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def get_url_patterns(urlconf=None, patterns_list=None, base_path="/"):
|
|
240
|
+
"""
|
|
241
|
+
Recursively yield a list of registered URL patterns.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
urlconf (URLConf): Python module such as `nautobot.core.urls`.
|
|
245
|
+
Default if unspecified is the value of `settings.ROOT_URLCONF`, i.e. the `nautobot.core.urls` module.
|
|
246
|
+
patterns_list (list): Used in recursion. Generally can be omitted on initial call.
|
|
247
|
+
Default if unspecified is the `url_patterns` attribute of the given `urlconf` module.
|
|
248
|
+
base_path (str): String to prepend to all URL patterns yielded.
|
|
249
|
+
Default if unspecified is the string `"/"`.
|
|
250
|
+
|
|
251
|
+
Yields:
|
|
252
|
+
(str): Each URL pattern defined in the given urlconf and its descendants
|
|
253
|
+
|
|
254
|
+
Examples:
|
|
255
|
+
>>> generator = get_url_patterns()
|
|
256
|
+
>>> next(generator)
|
|
257
|
+
'/'
|
|
258
|
+
>>> next(generator)
|
|
259
|
+
'/search/'
|
|
260
|
+
>>> next(generator)
|
|
261
|
+
'/login/'
|
|
262
|
+
>>> next(generator)
|
|
263
|
+
'/logout/'
|
|
264
|
+
>>> next(generator)
|
|
265
|
+
'/circuits/circuits/<uuid:pk>/terminations/swap/'
|
|
266
|
+
|
|
267
|
+
>>> import example_plugin.urls as example_urls
|
|
268
|
+
>>> for url_pattern in get_url_patterns(example_urls, base_path="/plugins/example-app/"):
|
|
269
|
+
... print(url_pattern)
|
|
270
|
+
...
|
|
271
|
+
/plugins/example-app/
|
|
272
|
+
/plugins/example-app/config/
|
|
273
|
+
/plugins/example-app/models/<uuid:pk>/dynamic-groups/
|
|
274
|
+
/plugins/example-app/other-models/<uuid:pk>/dynamic-groups/
|
|
275
|
+
/plugins/example-app/docs/
|
|
276
|
+
/plugins/example-app/circuits/<uuid:pk>/example-app-tab/
|
|
277
|
+
/plugins/example-app/devices/<uuid:pk>/example-app-tab-1/
|
|
278
|
+
/plugins/example-app/devices/<uuid:pk>/example-app-tab-2/
|
|
279
|
+
/plugins/example-app/override-target/
|
|
280
|
+
/plugins/example-app/^models/$
|
|
281
|
+
/plugins/example-app/^models/add/$
|
|
282
|
+
/plugins/example-app/^models/import/$
|
|
283
|
+
/plugins/example-app/^models/edit/$
|
|
284
|
+
/plugins/example-app/^models/delete/$
|
|
285
|
+
/plugins/example-app/^models/all-names/$
|
|
286
|
+
/plugins/example-app/^models/(?P<pk>[^/.]+)/$
|
|
287
|
+
/plugins/example-app/^models/(?P<pk>[^/.]+)/delete/$
|
|
288
|
+
/plugins/example-app/^models/(?P<pk>[^/.]+)/edit/$
|
|
289
|
+
/plugins/example-app/^models/(?P<pk>[^/.]+)/changelog/$
|
|
290
|
+
/plugins/example-app/^models/(?P<pk>[^/.]+)/notes/$
|
|
291
|
+
/plugins/example-app/^other-models/$
|
|
292
|
+
/plugins/example-app/^other-models/add/$
|
|
293
|
+
/plugins/example-app/^other-models/edit/$
|
|
294
|
+
/plugins/example-app/^other-models/delete/$
|
|
295
|
+
/plugins/example-app/^other-models/(?P<pk>[^/.]+)/$
|
|
296
|
+
/plugins/example-app/^other-models/(?P<pk>[^/.]+)/delete/$
|
|
297
|
+
/plugins/example-app/^other-models/(?P<pk>[^/.]+)/edit/$
|
|
298
|
+
/plugins/example-app/^other-models/(?P<pk>[^/.]+)/changelog/$
|
|
299
|
+
/plugins/example-app/^other-models/(?P<pk>[^/.]+)/notes/$
|
|
300
|
+
"""
|
|
301
|
+
if urlconf is None:
|
|
302
|
+
urlconf = settings.ROOT_URLCONF
|
|
303
|
+
if patterns_list is None:
|
|
304
|
+
patterns_list = get_resolver(urlconf).url_patterns
|
|
305
|
+
|
|
306
|
+
for item in patterns_list:
|
|
307
|
+
if isinstance(item, URLPattern):
|
|
308
|
+
yield base_path + str(item.pattern)
|
|
309
|
+
elif isinstance(item, URLResolver):
|
|
310
|
+
# Recurse!
|
|
311
|
+
yield from get_url_patterns(urlconf, item.url_patterns, base_path + str(item.pattern))
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def get_url_for_url_pattern(url_pattern):
|
|
315
|
+
"""
|
|
316
|
+
Given a URL pattern, construct a URL string that would match that pattern.
|
|
317
|
+
|
|
318
|
+
Examples:
|
|
319
|
+
>>> get_url_for_url_pattern("/plugins/example-app/^models/(?P<pk>[^/.]+)/$")
|
|
320
|
+
'/plugins/example-app/models/00000000-0000-0000-0000-000000000000/'
|
|
321
|
+
>>> get_url_for_url_pattern("/circuits/circuit-terminations/<uuid:termination_a_id>/connect/<str:termination_b_type>/")
|
|
322
|
+
'/circuits/circuit-terminations/00000000-0000-0000-0000-000000000000/connect/string/'
|
|
323
|
+
"""
|
|
324
|
+
url = url_pattern
|
|
325
|
+
# Fixup tokens in path-style "classic" view URLs:
|
|
326
|
+
# "/admin/users/user/<id>/password/"
|
|
327
|
+
url = re.sub(r"<id>", "00000000-0000-0000-0000-000000000000", url)
|
|
328
|
+
# "/silk/request/<uuid:request_id>/profile/<int:profile_id>/"
|
|
329
|
+
url = re.sub(r"<int:\w+>", "1", url)
|
|
330
|
+
# "/admin/admin/logentry/<path:object_id>/"
|
|
331
|
+
url = re.sub(r"<path:\w+>", "1", url)
|
|
332
|
+
# "/dcim/sites/<slug:slug>/"
|
|
333
|
+
url = re.sub(r"<slug:\w+>", "slug", url)
|
|
334
|
+
# "/apps/installed-apps/<str:app>/"
|
|
335
|
+
url = re.sub(r"<str:\w+>", "string", url)
|
|
336
|
+
# "/dcim/locations/<uuid:pk>/"
|
|
337
|
+
url = re.sub(r"<uuid:\w+>", "00000000-0000-0000-0000-000000000000", url)
|
|
338
|
+
# "/api/circuits/<drf_format_suffix:format>"
|
|
339
|
+
url = re.sub(r"<drf_format_suffix:\w+>", ".json", url)
|
|
340
|
+
# tokens in regexp-style router urls, including REST and NautobotUIViewSet:
|
|
341
|
+
# "/extras/^external-integrations/(?P<pk>[^/.]+)/$"
|
|
342
|
+
# "/api/virtualization/^interfaces/(?P<pk>[^/.]+)/$"
|
|
343
|
+
# "/api/virtualization/^interfaces/(?P<pk>[^/.]+)\\.(?P<format>[a-z0-9]+)/?$"
|
|
344
|
+
url = re.sub(r"[$^]", "", url)
|
|
345
|
+
url = re.sub(r"/\?", "/", url)
|
|
346
|
+
url = re.sub(r"\(\?P<app_label>[^)]+\)", "users", url)
|
|
347
|
+
url = re.sub(r"\(\?P<class_path>[^)]+\)", "foo/bar/baz", url)
|
|
348
|
+
url = re.sub(r"\(\?P<format>[^)]+\)", "json", url)
|
|
349
|
+
url = re.sub(r"\(\?P<name>[^)]+\)", "string", url)
|
|
350
|
+
url = re.sub(r"\(\?P<pk>[^)]+\)", "00000000-0000-0000-0000-000000000000", url)
|
|
351
|
+
url = re.sub(r"\(\?P<slug>[^)]+\)", "string", url)
|
|
352
|
+
url = re.sub(r"\(\?P<url>[^)]+\)", "any", url)
|
|
353
|
+
# Fallthru for generic URL parameters
|
|
354
|
+
url = re.sub(r"\(\?P<\w+>[^)]+\)\??", "unknown", url)
|
|
355
|
+
url = re.sub(r"\\", "", url)
|
|
356
|
+
|
|
357
|
+
if any(char in url for char in "<>[]()?+^$"):
|
|
358
|
+
raise RuntimeError(f"Unhandled token in URL {url} derived from {url_pattern}")
|
|
359
|
+
|
|
360
|
+
return url
|
nautobot/core/views/__init__.py
CHANGED
|
@@ -11,7 +11,7 @@ from django.contrib.auth.decorators import permission_required
|
|
|
11
11
|
from django.contrib.auth.mixins import AccessMixin, LoginRequiredMixin
|
|
12
12
|
from django.contrib.contenttypes.models import ContentType
|
|
13
13
|
from django.http import HttpResponseForbidden, HttpResponseServerError, JsonResponse
|
|
14
|
-
from django.shortcuts import get_object_or_404,
|
|
14
|
+
from django.shortcuts import get_object_or_404, render
|
|
15
15
|
from django.template import loader, RequestContext, Template
|
|
16
16
|
from django.template.exceptions import TemplateDoesNotExist
|
|
17
17
|
from django.urls import resolve, reverse
|
|
@@ -210,7 +210,7 @@ class SearchView(AccessMixin, View):
|
|
|
210
210
|
)
|
|
211
211
|
|
|
212
212
|
|
|
213
|
-
class StaticMediaFailureView(View):
|
|
213
|
+
class StaticMediaFailureView(View): # NOT using LoginRequiredMixin here as this may happen even on the login page
|
|
214
214
|
"""
|
|
215
215
|
Display a user-friendly error message with troubleshooting tips when a static media file fails to load.
|
|
216
216
|
"""
|
|
@@ -265,12 +265,8 @@ def csrf_failure(request, reason="", template_name="403_csrf_failure.html"):
|
|
|
265
265
|
return HttpResponseForbidden(t.render(context), content_type="text/html")
|
|
266
266
|
|
|
267
267
|
|
|
268
|
-
class CustomGraphQLView(GraphQLView):
|
|
268
|
+
class CustomGraphQLView(LoginRequiredMixin, GraphQLView):
|
|
269
269
|
def render_graphiql(self, request, **data):
|
|
270
|
-
if not request.user.is_authenticated:
|
|
271
|
-
graphql_url = reverse("graphql")
|
|
272
|
-
login_url = reverse(settings.LOGIN_URL)
|
|
273
|
-
return redirect(f"{login_url}?next={graphql_url}")
|
|
274
270
|
query_name = request.GET.get("name")
|
|
275
271
|
if query_name:
|
|
276
272
|
data["obj"] = GraphQLQuery.objects.get(name=query_name)
|
nautobot/core/views/generic.py
CHANGED
|
@@ -4,6 +4,7 @@ import re
|
|
|
4
4
|
|
|
5
5
|
from django.conf import settings
|
|
6
6
|
from django.contrib import messages
|
|
7
|
+
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
7
8
|
from django.contrib.contenttypes.models import ContentType
|
|
8
9
|
from django.core.exceptions import (
|
|
9
10
|
FieldDoesNotExist,
|
|
@@ -53,9 +54,18 @@ from nautobot.core.views.utils import (
|
|
|
53
54
|
import_csv_helper,
|
|
54
55
|
prepare_cloned_fields,
|
|
55
56
|
)
|
|
57
|
+
from nautobot.extras.context_managers import deferred_change_logging_for_bulk_operation
|
|
56
58
|
from nautobot.extras.models import ContactAssociation, ExportTemplate
|
|
57
59
|
from nautobot.extras.tables import AssociatedContactsTable
|
|
58
|
-
from nautobot.extras.utils import remove_prefix_from_cf_key
|
|
60
|
+
from nautobot.extras.utils import bulk_delete_with_bulk_change_logging, remove_prefix_from_cf_key
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class GenericView(LoginRequiredMixin, View):
|
|
64
|
+
"""
|
|
65
|
+
Base class for non-object-related views.
|
|
66
|
+
|
|
67
|
+
Enforces authentication, which Django's base View does not by default.
|
|
68
|
+
"""
|
|
59
69
|
|
|
60
70
|
|
|
61
71
|
class ObjectView(ObjectPermissionRequiredMixin, View):
|
|
@@ -68,7 +78,6 @@ class ObjectView(ObjectPermissionRequiredMixin, View):
|
|
|
68
78
|
|
|
69
79
|
queryset = None
|
|
70
80
|
template_name = None
|
|
71
|
-
is_contact_associatable_model = True
|
|
72
81
|
|
|
73
82
|
def get_required_permission(self):
|
|
74
83
|
return get_permission_for_model(self.queryset.model, "view")
|
|
@@ -127,7 +136,6 @@ class ObjectView(ObjectPermissionRequiredMixin, View):
|
|
|
127
136
|
content_type = ContentType.objects.get_for_model(self.queryset.model)
|
|
128
137
|
context = {
|
|
129
138
|
"object": instance,
|
|
130
|
-
"is_contact_associatable_model": self.is_contact_associatable_model,
|
|
131
139
|
"content_type": content_type,
|
|
132
140
|
"verbose_name": self.queryset.model._meta.verbose_name,
|
|
133
141
|
"verbose_name_plural": self.queryset.model._meta.verbose_name_plural,
|
|
@@ -135,7 +143,7 @@ class ObjectView(ObjectPermissionRequiredMixin, View):
|
|
|
135
143
|
"last_updated_by": last_updated_by,
|
|
136
144
|
**self.get_extra_context(request, instance),
|
|
137
145
|
}
|
|
138
|
-
if
|
|
146
|
+
if instance.is_contact_associable_model:
|
|
139
147
|
paginate = {"paginator_class": EnhancedPaginator, "per_page": get_paginate_count(request)}
|
|
140
148
|
associations = (
|
|
141
149
|
ContactAssociation.objects.filter(
|
|
@@ -220,6 +228,7 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
|
|
|
220
228
|
display_filter_params = []
|
|
221
229
|
dynamic_filter_form = None
|
|
222
230
|
filter_form = None
|
|
231
|
+
hide_hierarchy_ui = False
|
|
223
232
|
|
|
224
233
|
if self.filterset:
|
|
225
234
|
filter_params = self.get_filter_params(request)
|
|
@@ -232,6 +241,12 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
|
|
|
232
241
|
)
|
|
233
242
|
self.queryset = self.queryset.none()
|
|
234
243
|
|
|
244
|
+
# If a valid filterset is applied, we have to hide the hierarchy indentation in the UI for tables that support hierarchy indentation.
|
|
245
|
+
# NOTE: An empty filterset query-param is also valid filterset and we dont want to hide hierarchy indentation if no filter query-param is provided
|
|
246
|
+
# hence `filterset.data`.
|
|
247
|
+
if filterset.is_valid() and filterset.data:
|
|
248
|
+
hide_hierarchy_ui = True
|
|
249
|
+
|
|
235
250
|
display_filter_params = [
|
|
236
251
|
check_filter_for_display(filterset.filters, field_name, values)
|
|
237
252
|
for field_name, values in filter_params.items()
|
|
@@ -283,9 +298,9 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
|
|
|
283
298
|
table_config_form = None
|
|
284
299
|
if self.table:
|
|
285
300
|
# Construct the objects table
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
table = self.table(self.queryset, user=request.user,
|
|
301
|
+
if self.request.GET.getlist("sort"):
|
|
302
|
+
hide_hierarchy_ui = True # hide tree hierarchy if custom sort is used
|
|
303
|
+
table = self.table(self.queryset, user=request.user, hide_hierarchy_ui=hide_hierarchy_ui)
|
|
289
304
|
if "pk" in table.base_columns and (permissions["change"] or permissions["delete"]):
|
|
290
305
|
table.columns.show("pk")
|
|
291
306
|
|
|
@@ -974,7 +989,7 @@ class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|
|
974
989
|
nullified_fields = request.POST.getlist("_nullify")
|
|
975
990
|
|
|
976
991
|
try:
|
|
977
|
-
with
|
|
992
|
+
with deferred_change_logging_for_bulk_operation():
|
|
978
993
|
updated_objects = []
|
|
979
994
|
for obj in self.queryset.filter(pk__in=form.cleaned_data["pk"]):
|
|
980
995
|
obj = self.alter_obj(obj, request, [], kwargs)
|
|
@@ -1238,13 +1253,12 @@ class BulkDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|
|
1238
1253
|
|
|
1239
1254
|
self.perform_pre_delete(request, queryset)
|
|
1240
1255
|
try:
|
|
1241
|
-
_, deleted_info = queryset
|
|
1256
|
+
_, deleted_info = bulk_delete_with_bulk_change_logging(queryset)
|
|
1242
1257
|
deleted_count = deleted_info[model._meta.label]
|
|
1243
1258
|
except ProtectedError as e:
|
|
1244
1259
|
logger.info("Caught ProtectedError while attempting to delete objects")
|
|
1245
1260
|
handle_protectederror(queryset, request, e)
|
|
1246
1261
|
return redirect(self.get_return_url(request))
|
|
1247
|
-
|
|
1248
1262
|
msg = f"Deleted {deleted_count} {model._meta.verbose_name_plural}"
|
|
1249
1263
|
logger.info(msg)
|
|
1250
1264
|
messages.success(request, msg)
|
nautobot/core/views/mixins.py
CHANGED
|
@@ -45,10 +45,11 @@ from nautobot.core.views.utils import (
|
|
|
45
45
|
import_csv_helper,
|
|
46
46
|
prepare_cloned_fields,
|
|
47
47
|
)
|
|
48
|
+
from nautobot.extras.context_managers import deferred_change_logging_for_bulk_operation
|
|
48
49
|
from nautobot.extras.forms import NoteForm
|
|
49
50
|
from nautobot.extras.models import ExportTemplate
|
|
50
51
|
from nautobot.extras.tables import NoteTable, ObjectChangeTable
|
|
51
|
-
from nautobot.extras.utils import remove_prefix_from_cf_key
|
|
52
|
+
from nautobot.extras.utils import bulk_delete_with_bulk_change_logging, remove_prefix_from_cf_key
|
|
52
53
|
|
|
53
54
|
PERMISSIONS_ACTION_MAP = {
|
|
54
55
|
"list": "view",
|
|
@@ -226,7 +227,6 @@ class NautobotViewSetMixin(GenericViewSet, AccessMixin, GetReturnURLMixin, FormV
|
|
|
226
227
|
create_form_class = None
|
|
227
228
|
update_form_class = None
|
|
228
229
|
parser_classes = [FormParser, MultiPartParser]
|
|
229
|
-
is_contact_associatable_model = True
|
|
230
230
|
queryset = None
|
|
231
231
|
# serializer_class has to be specified to eliminate the need to override retrieve() in the RetrieveModelMixin for now.
|
|
232
232
|
serializer_class = None
|
|
@@ -608,6 +608,7 @@ class ObjectListViewMixin(NautobotViewSetMixin, mixins.ListModelMixin):
|
|
|
608
608
|
action_buttons = ("add", "import", "export")
|
|
609
609
|
filterset_class = None
|
|
610
610
|
filterset_form_class = None
|
|
611
|
+
hide_hierarchy_ui = False
|
|
611
612
|
non_filter_params = (
|
|
612
613
|
"export", # trigger for CSV/export-template/YAML export # 3.0 TODO: remove, irrelevant after #4746
|
|
613
614
|
"page", # used by django-tables2.RequestConfig
|
|
@@ -629,6 +630,12 @@ class ObjectListViewMixin(NautobotViewSetMixin, mixins.ListModelMixin):
|
|
|
629
630
|
format_html("Invalid filters were specified: {}", self.filterset.errors),
|
|
630
631
|
)
|
|
631
632
|
queryset = queryset.none()
|
|
633
|
+
|
|
634
|
+
# If a valid filterset is applied, we have to hide the hierarchy indentation in the UI for tables that support hierarchy indentation.
|
|
635
|
+
# NOTE: An empty filterset query-param is also valid filterset and we dont want to hide hierarchy indentation if no filter query-param is provided
|
|
636
|
+
# hence `filterset.data`.
|
|
637
|
+
if self.filterset.is_valid() and self.filterset.data:
|
|
638
|
+
self.hide_hierarchy_ui = True
|
|
632
639
|
return queryset
|
|
633
640
|
|
|
634
641
|
# 3.0 TODO: remove, irrelevant after #4746
|
|
@@ -840,7 +847,7 @@ class ObjectBulkDestroyViewMixin(NautobotViewSetMixin, BulkDestroyModelMixin):
|
|
|
840
847
|
|
|
841
848
|
try:
|
|
842
849
|
with transaction.atomic():
|
|
843
|
-
deleted_count = queryset
|
|
850
|
+
deleted_count = bulk_delete_with_bulk_change_logging(queryset)[1][model._meta.label]
|
|
844
851
|
msg = f"Deleted {deleted_count} {model._meta.verbose_name_plural}"
|
|
845
852
|
self.logger.info(msg)
|
|
846
853
|
self.success_url = self.get_return_url(request)
|
|
@@ -972,7 +979,7 @@ class ObjectBulkUpdateViewMixin(NautobotViewSetMixin, BulkUpdateModelMixin):
|
|
|
972
979
|
if field not in form_custom_fields + form_relationships + ["pk"] + ["object_note"]
|
|
973
980
|
]
|
|
974
981
|
nullified_fields = request.POST.getlist("_nullify")
|
|
975
|
-
with
|
|
982
|
+
with deferred_change_logging_for_bulk_operation():
|
|
976
983
|
updated_objects = []
|
|
977
984
|
for obj in queryset.filter(pk__in=form.cleaned_data["pk"]):
|
|
978
985
|
self.obj = obj
|
nautobot/core/views/renderers.py
CHANGED
|
@@ -65,10 +65,13 @@ class NautobotHTMLRenderer(renderers.BrowsableAPIRenderer):
|
|
|
65
65
|
table_class = view.get_table_class()
|
|
66
66
|
request = kwargs.get("request", view.request)
|
|
67
67
|
queryset = view.alter_queryset(request)
|
|
68
|
+
|
|
68
69
|
if view.action in ["list", "notes", "changelog"]:
|
|
69
70
|
if view.action == "list":
|
|
70
71
|
permissions = kwargs.get("permissions", {})
|
|
71
|
-
|
|
72
|
+
if view.request.GET.getlist("sort"):
|
|
73
|
+
view.hide_hierarchy_ui = True # hide tree hierarchy if custom sort is used
|
|
74
|
+
table = table_class(queryset, user=request.user, hide_hierarchy_ui=view.hide_hierarchy_ui)
|
|
72
75
|
if "pk" in table.base_columns and (permissions["change"] or permissions["delete"]):
|
|
73
76
|
table.columns.show("pk")
|
|
74
77
|
elif view.action == "notes":
|
|
@@ -142,7 +145,6 @@ class NautobotHTMLRenderer(renderers.BrowsableAPIRenderer):
|
|
|
142
145
|
view = renderer_context["view"]
|
|
143
146
|
request = renderer_context["request"]
|
|
144
147
|
# Check if queryset attribute is set before doing anything
|
|
145
|
-
is_contact_associatable_model = view.is_contact_associatable_model
|
|
146
148
|
queryset = view.alter_queryset(request)
|
|
147
149
|
model = queryset.model
|
|
148
150
|
form_class = view.get_form_class()
|
|
@@ -219,7 +221,6 @@ class NautobotHTMLRenderer(renderers.BrowsableAPIRenderer):
|
|
|
219
221
|
|
|
220
222
|
context = {
|
|
221
223
|
"content_type": content_type,
|
|
222
|
-
"is_contact_associatable_model": is_contact_associatable_model,
|
|
223
224
|
"form": form,
|
|
224
225
|
"filter_form": filter_form,
|
|
225
226
|
"dynamic_filter_form": self.get_dynamic_filter_form(view, request, filterset_class=view.filterset_class),
|
|
@@ -241,9 +242,13 @@ class NautobotHTMLRenderer(renderers.BrowsableAPIRenderer):
|
|
|
241
242
|
|
|
242
243
|
context["created_by"] = created_by
|
|
243
244
|
context["last_updated_by"] = last_updated_by
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
245
|
+
if instance.is_contact_associable_model:
|
|
246
|
+
paginate = {"paginator_class": EnhancedPaginator, "per_page": get_paginate_count(request)}
|
|
247
|
+
associations = instance.associated_contacts.restrict(request.user, "view").order_by("role__name")
|
|
248
|
+
associations_table = AssociatedContactsTable(associations, orderable=False)
|
|
249
|
+
RequestConfig(request, paginate).configure(associations_table)
|
|
250
|
+
associations_table.columns.show("pk")
|
|
251
|
+
context["associated_contacts_table"] = associations_table
|
|
247
252
|
else:
|
|
248
253
|
context["associated_contacts_table"] = None
|
|
249
254
|
context.update(view.get_extra_context(request, instance))
|