nautobot 2.3.6__py3-none-any.whl → 2.3.8__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 +4 -2
- nautobot/circuits/tests/test_views.py +4 -5
- nautobot/core/api/views.py +15 -3
- nautobot/core/templates/inc/javascript.html +2 -0
- nautobot/core/templates/inc/nav_menu.html +0 -251
- nautobot/core/templates/inc/paginator.html +3 -0
- nautobot/core/templates/utilities/obj_table.html +1 -1
- nautobot/core/testing/mixins.py +59 -2
- nautobot/core/testing/views.py +45 -61
- nautobot/core/tests/runner.py +6 -3
- nautobot/core/tests/test_paginator.py +4 -3
- nautobot/core/tests/test_views.py +39 -56
- nautobot/core/views/__init__.py +27 -11
- nautobot/dcim/api/serializers.py +10 -5
- nautobot/dcim/forms.py +11 -7
- nautobot/dcim/models/device_components.py +7 -4
- nautobot/dcim/tests/test_api.py +28 -0
- nautobot/dcim/tests/test_forms.py +17 -1
- nautobot/dcim/tests/test_models.py +42 -4
- nautobot/dcim/tests/test_views.py +26 -67
- nautobot/dcim/utils.py +9 -6
- nautobot/extras/datasources/git.py +6 -1
- nautobot/extras/migrations/0112_dynamic_group_group_type_data_migration.py +3 -0
- nautobot/extras/migrations/0116_fix_dynamic_group_group_type_data_migration.py +16 -0
- nautobot/extras/plugins/views.py +18 -3
- nautobot/extras/tests/test_customfields.py +9 -16
- nautobot/extras/tests/test_dynamicgroups.py +116 -0
- nautobot/extras/tests/test_plugins.py +4 -6
- nautobot/extras/tests/test_utils.py +5 -0
- nautobot/extras/tests/test_views.py +61 -159
- nautobot/extras/utils.py +50 -11
- nautobot/ipam/filters.py +2 -2
- nautobot/ipam/models.py +29 -2
- nautobot/ipam/templates/ipam/ipaddress.html +2 -2
- nautobot/ipam/templates/ipam/ipaddress_interfaces.html +3 -0
- nautobot/ipam/templates/ipam/ipaddress_vm_interfaces.html +3 -0
- nautobot/ipam/templates/ipam/prefix.html +3 -3
- nautobot/ipam/templates/ipam/routetarget.html +2 -2
- nautobot/ipam/templates/ipam/vlan.html +3 -0
- nautobot/ipam/templates/ipam/vrf.html +7 -4
- nautobot/ipam/tests/test_api.py +18 -12
- nautobot/ipam/tests/test_models.py +68 -12
- nautobot/ipam/tests/test_views.py +6 -15
- nautobot/ipam/views.py +43 -0
- nautobot/project-static/docs/404.html +3 -3
- nautobot/project-static/docs/apps/index.html +3 -3
- nautobot/project-static/docs/apps/nautobot-apps.html +3 -3
- nautobot/project-static/docs/assets/javascripts/bundle.525ec568.min.js +16 -0
- nautobot/project-static/docs/assets/javascripts/{bundle.56dfad97.min.js.map → bundle.525ec568.min.js.map} +4 -4
- nautobot/project-static/docs/assets/stylesheets/main.8c3ca2c6.min.css +1 -0
- nautobot/project-static/docs/assets/stylesheets/main.8c3ca2c6.min.css.map +1 -0
- nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/api.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/config.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/models.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +124 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/views.html +3 -3
- nautobot/project-static/docs/development/apps/api/configuration-view.html +3 -3
- nautobot/project-static/docs/development/apps/api/database-backend-config.html +3 -3
- nautobot/project-static/docs/development/apps/api/models/django-admin.html +3 -3
- nautobot/project-static/docs/development/apps/api/models/global-search.html +3 -3
- nautobot/project-static/docs/development/apps/api/models/graphql.html +3 -3
- nautobot/project-static/docs/development/apps/api/models/index.html +3 -3
- nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +3 -3
- nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +3 -3
- nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +3 -3
- nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +3 -3
- nautobot/project-static/docs/development/apps/api/platform-features/index.html +3 -3
- nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +3 -3
- nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +3 -3
- nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +3 -3
- nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +3 -3
- nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +3 -3
- nautobot/project-static/docs/development/apps/api/prometheus.html +3 -3
- nautobot/project-static/docs/development/apps/api/setup.html +3 -3
- nautobot/project-static/docs/development/apps/api/testing.html +3 -3
- nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +3 -3
- nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +3 -3
- nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +3 -3
- nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +3 -3
- nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +3 -3
- nautobot/project-static/docs/development/apps/api/views/base-template.html +3 -3
- nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +3 -3
- nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +3 -3
- nautobot/project-static/docs/development/apps/api/views/help-documentation.html +3 -3
- nautobot/project-static/docs/development/apps/api/views/index.html +3 -3
- nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +3 -3
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +3 -3
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +3 -3
- nautobot/project-static/docs/development/apps/api/views/notes.html +3 -3
- nautobot/project-static/docs/development/apps/api/views/rest-api.html +3 -3
- nautobot/project-static/docs/development/apps/api/views/urls.html +3 -3
- nautobot/project-static/docs/development/apps/index.html +3 -3
- nautobot/project-static/docs/development/apps/migration/code-updates.html +3 -3
- nautobot/project-static/docs/development/apps/migration/dependency-updates.html +3 -3
- nautobot/project-static/docs/development/apps/migration/from-v1.html +3 -3
- nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +3 -3
- nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +3 -3
- nautobot/project-static/docs/development/apps/migration/model-updates/global.html +3 -3
- nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +3 -3
- nautobot/project-static/docs/development/apps/porting-from-netbox.html +3 -3
- nautobot/project-static/docs/development/core/application-registry.html +3 -3
- nautobot/project-static/docs/development/core/best-practices.html +3 -3
- nautobot/project-static/docs/development/core/bootstrap-ui.html +3 -3
- nautobot/project-static/docs/development/core/caching.html +3 -3
- nautobot/project-static/docs/development/core/controllers.html +3 -3
- nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +3 -3
- nautobot/project-static/docs/development/core/generic-views.html +3 -3
- nautobot/project-static/docs/development/core/getting-started.html +3 -3
- nautobot/project-static/docs/development/core/homepage.html +3 -3
- nautobot/project-static/docs/development/core/index.html +3 -3
- nautobot/project-static/docs/development/core/model-checklist.html +3 -3
- nautobot/project-static/docs/development/core/model-features.html +3 -3
- nautobot/project-static/docs/development/core/natural-keys.html +3 -3
- nautobot/project-static/docs/development/core/navigation-menu.html +3 -3
- nautobot/project-static/docs/development/core/release-checklist.html +3 -3
- nautobot/project-static/docs/development/core/role-internals.html +3 -3
- nautobot/project-static/docs/development/core/settings.html +3 -3
- nautobot/project-static/docs/development/core/style-guide.html +3 -3
- nautobot/project-static/docs/development/core/templates.html +3 -3
- nautobot/project-static/docs/development/core/testing.html +3 -3
- nautobot/project-static/docs/development/core/user-preferences.html +3 -3
- nautobot/project-static/docs/development/index.html +3 -3
- nautobot/project-static/docs/development/jobs/index.html +3 -3
- nautobot/project-static/docs/development/jobs/migration/from-v1.html +3 -3
- nautobot/project-static/docs/index.html +3 -3
- nautobot/project-static/docs/objects.inv +0 -0
- nautobot/project-static/docs/overview/application_stack.html +3 -3
- nautobot/project-static/docs/overview/design_philosophy.html +3 -3
- nautobot/project-static/docs/release-notes/index.html +3 -3
- nautobot/project-static/docs/release-notes/version-1.0.html +3 -3
- nautobot/project-static/docs/release-notes/version-1.1.html +3 -3
- nautobot/project-static/docs/release-notes/version-1.2.html +3 -3
- nautobot/project-static/docs/release-notes/version-1.3.html +3 -3
- nautobot/project-static/docs/release-notes/version-1.4.html +3 -3
- nautobot/project-static/docs/release-notes/version-1.5.html +3 -3
- nautobot/project-static/docs/release-notes/version-1.6.html +3 -3
- nautobot/project-static/docs/release-notes/version-2.0.html +3 -3
- nautobot/project-static/docs/release-notes/version-2.1.html +3 -3
- nautobot/project-static/docs/release-notes/version-2.2.html +3 -3
- nautobot/project-static/docs/release-notes/version-2.3.html +304 -96
- nautobot/project-static/docs/requirements.txt +1 -1
- nautobot/project-static/docs/search/search_index.json +1 -1
- nautobot/project-static/docs/sitemap.xml +269 -269
- nautobot/project-static/docs/sitemap.xml.gz +0 -0
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +3 -3
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +3 -3
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +3 -3
- nautobot/project-static/docs/user-guide/administration/configuration/index.html +3 -3
- nautobot/project-static/docs/user-guide/administration/configuration/redis.html +3 -3
- nautobot/project-static/docs/user-guide/administration/configuration/settings.html +3 -3
- nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +3 -3
- nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +3 -3
- nautobot/project-static/docs/user-guide/administration/guides/docker.html +3 -3
- nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +3 -3
- nautobot/project-static/docs/user-guide/administration/guides/permissions.html +3 -3
- nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +3 -3
- nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +3 -3
- nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +3 -3
- nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +3 -3
- nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +3 -3
- nautobot/project-static/docs/user-guide/administration/installation/app-install.html +3 -3
- nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +3 -3
- nautobot/project-static/docs/user-guide/administration/installation/http-server.html +3 -3
- nautobot/project-static/docs/user-guide/administration/installation/index.html +3 -3
- nautobot/project-static/docs/user-guide/administration/installation/install_system.html +3 -3
- nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +3 -3
- nautobot/project-static/docs/user-guide/administration/installation/services.html +3 -3
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +3 -3
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +3 -3
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +3 -3
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +3 -3
- nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +3 -3
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +3 -3
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +3 -3
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +3 -3
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +3 -3
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +3 -3
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +3 -3
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +3 -3
- nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/graphql.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/relationships.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +3 -3
- nautobot/project-static/docs/user-guide/index.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/note.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/role.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/secret.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/status.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/tag.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +3 -3
- nautobot/project-static/js/nav_menu.js +249 -0
- nautobot/tenancy/templates/tenancy/tenant.html +1 -1
- nautobot/users/tests/test_views.py +9 -11
- nautobot/virtualization/tests/test_views.py +3 -5
- {nautobot-2.3.6.dist-info → nautobot-2.3.8.dist-info}/METADATA +2 -1
- {nautobot-2.3.6.dist-info → nautobot-2.3.8.dist-info}/RECORD +333 -331
- {nautobot-2.3.6.dist-info → nautobot-2.3.8.dist-info}/WHEEL +1 -1
- nautobot/project-static/docs/assets/javascripts/bundle.56dfad97.min.js +0 -16
- nautobot/project-static/docs/assets/stylesheets/main.35f28582.min.css +0 -1
- nautobot/project-static/docs/assets/stylesheets/main.35f28582.min.css.map +0 -1
- {nautobot-2.3.6.dist-info → nautobot-2.3.8.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.3.6.dist-info → nautobot-2.3.8.dist-info}/NOTICE +0 -0
- {nautobot-2.3.6.dist-info → nautobot-2.3.8.dist-info}/entry_points.txt +0 -0
|
@@ -864,11 +864,9 @@ class DynamicGroupTestCase(
|
|
|
864
864
|
url = reverse("dcim:device_dynamicgroups", kwargs={"pk": Device.objects.first().pk})
|
|
865
865
|
self.add_permissions("dcim.view_device", "extras.view_dynamicgroup")
|
|
866
866
|
response = self.client.get(url)
|
|
867
|
-
self.
|
|
868
|
-
|
|
869
|
-
self.
|
|
870
|
-
self.assertIn("DG 2", response_body, msg=response_body)
|
|
871
|
-
self.assertIn("DG 3", response_body, msg=response_body)
|
|
867
|
+
self.assertBodyContains(response, "DG 1")
|
|
868
|
+
self.assertBodyContains(response, "DG 2")
|
|
869
|
+
self.assertBodyContains(response, "DG 3")
|
|
872
870
|
|
|
873
871
|
def test_get_object_dynamic_groups_with_constrained_permission(self):
|
|
874
872
|
obj_perm = ObjectPermission(
|
|
@@ -891,7 +889,7 @@ class DynamicGroupTestCase(
|
|
|
891
889
|
url = reverse("dcim:device_dynamicgroups", kwargs={"pk": Device.objects.first().pk})
|
|
892
890
|
response = self.client.get(url)
|
|
893
891
|
self.assertHttpStatus(response, 200)
|
|
894
|
-
response_body = response.content.decode(response.charset)
|
|
892
|
+
response_body = extract_page_body(response.content.decode(response.charset))
|
|
895
893
|
self.assertIn("DG 1", response_body, msg=response_body)
|
|
896
894
|
self.assertNotIn("DG 2", response_body, msg=response_body)
|
|
897
895
|
self.assertNotIn("DG 3", response_body, msg=response_body)
|
|
@@ -1334,18 +1332,14 @@ class SavedViewTest(ModelViewTestCase):
|
|
|
1334
1332
|
# Try GET with model-level permission
|
|
1335
1333
|
# SavedView detail view should redirect to the View from which it is derived
|
|
1336
1334
|
response = self.client.get(instance.get_absolute_url(), follow=True)
|
|
1337
|
-
self.
|
|
1338
|
-
response_body = extract_page_body(response.content.decode(response.charset))
|
|
1339
|
-
self.assertIn(escape(instance.name), response_body, msg=response_body)
|
|
1335
|
+
self.assertBodyContains(response, escape(instance.name))
|
|
1340
1336
|
|
|
1341
1337
|
query_strings = ["&table_changes_pending=true", "&per_page=1234", "&status=active", "&sort=name"]
|
|
1342
1338
|
for string in query_strings:
|
|
1343
1339
|
view_url = self.get_view_url_for_saved_view(instance) + string
|
|
1344
1340
|
response = self.client.get(view_url)
|
|
1345
|
-
self.assertHttpStatus(response, 200)
|
|
1346
|
-
response_body = extract_page_body(response.content.decode(response.charset))
|
|
1347
1341
|
# Assert that the star sign is rendered on the page since there are unsaved changes
|
|
1348
|
-
self.
|
|
1342
|
+
self.assertBodyContains(response, '<i title="Pending changes not saved">')
|
|
1349
1343
|
|
|
1350
1344
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
|
1351
1345
|
def test_get_object_with_constrained_permission(self):
|
|
@@ -1383,12 +1377,9 @@ class SavedViewTest(ModelViewTestCase):
|
|
|
1383
1377
|
# Try update the saved view with a different user from the owner of the saved view
|
|
1384
1378
|
self.client.force_login(different_user)
|
|
1385
1379
|
response = self.client.get(update_url, follow=True)
|
|
1386
|
-
self.
|
|
1387
|
-
|
|
1388
|
-
self.assertIn(
|
|
1380
|
+
self.assertBodyContains(
|
|
1381
|
+
response,
|
|
1389
1382
|
f"You do not have the required permission to modify this Saved View owned by {instance.owner}",
|
|
1390
|
-
response_body,
|
|
1391
|
-
msg=response_body,
|
|
1392
1383
|
)
|
|
1393
1384
|
|
|
1394
1385
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
@@ -1424,12 +1415,9 @@ class SavedViewTest(ModelViewTestCase):
|
|
|
1424
1415
|
# Try delete the saved view with a different user from the owner of the saved view
|
|
1425
1416
|
self.client.force_login(different_user)
|
|
1426
1417
|
response = self.client.post(delete_url, follow=True)
|
|
1427
|
-
self.
|
|
1428
|
-
|
|
1429
|
-
self.assertIn(
|
|
1418
|
+
self.assertBodyContains(
|
|
1419
|
+
response,
|
|
1430
1420
|
f"You do not have the required permission to delete this Saved View owned by {instance.owner}",
|
|
1431
|
-
response_body,
|
|
1432
|
-
msg=response_body,
|
|
1433
1421
|
)
|
|
1434
1422
|
|
|
1435
1423
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
@@ -1450,13 +1438,7 @@ class SavedViewTest(ModelViewTestCase):
|
|
|
1450
1438
|
instance.owner.save()
|
|
1451
1439
|
self.client.force_login(instance.owner)
|
|
1452
1440
|
response = self.client.post(delete_url, follow=True)
|
|
1453
|
-
self.
|
|
1454
|
-
response_body = extract_page_body(response.content.decode(response.charset))
|
|
1455
|
-
self.assertIn(
|
|
1456
|
-
"Are you sure you want to delete saved view",
|
|
1457
|
-
response_body,
|
|
1458
|
-
msg=response_body,
|
|
1459
|
-
)
|
|
1441
|
+
self.assertBodyContains(response, "Are you sure you want to delete saved view")
|
|
1460
1442
|
|
|
1461
1443
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1462
1444
|
def test_create_saved_view(self):
|
|
@@ -1499,16 +1481,7 @@ class SavedViewTest(ModelViewTestCase):
|
|
|
1499
1481
|
)
|
|
1500
1482
|
response = self.client.get(reverse(view_name), follow=True)
|
|
1501
1483
|
# Assert that Location List View got redirected to Saved View set as global default
|
|
1502
|
-
self.
|
|
1503
|
-
response_body = extract_page_body(response.content.decode(response.charset))
|
|
1504
|
-
self.assertInHTML(
|
|
1505
|
-
"""
|
|
1506
|
-
<strong>
|
|
1507
|
-
Global Location Default View
|
|
1508
|
-
</strong>
|
|
1509
|
-
""",
|
|
1510
|
-
response_body,
|
|
1511
|
-
)
|
|
1484
|
+
self.assertBodyContains(response, "<strong>Global Location Default View</strong>", html=True)
|
|
1512
1485
|
|
|
1513
1486
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1514
1487
|
def test_user_default(self):
|
|
@@ -1522,16 +1495,7 @@ class SavedViewTest(ModelViewTestCase):
|
|
|
1522
1495
|
UserSavedViewAssociation.objects.create(user=self.user, saved_view=sv, view_name=sv.view)
|
|
1523
1496
|
response = self.client.get(reverse(view_name), follow=True)
|
|
1524
1497
|
# Assert that Location List View got redirected to Saved View set as user default
|
|
1525
|
-
self.
|
|
1526
|
-
response_body = extract_page_body(response.content.decode(response.charset))
|
|
1527
|
-
self.assertInHTML(
|
|
1528
|
-
"""
|
|
1529
|
-
<strong>
|
|
1530
|
-
User Location Default View
|
|
1531
|
-
</strong>
|
|
1532
|
-
""",
|
|
1533
|
-
response_body,
|
|
1534
|
-
)
|
|
1498
|
+
self.assertBodyContains(response, "<strong>User Location Default View</strong>", html=True)
|
|
1535
1499
|
|
|
1536
1500
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1537
1501
|
def test_user_default_precedes_global_default(self):
|
|
@@ -1550,16 +1514,7 @@ class SavedViewTest(ModelViewTestCase):
|
|
|
1550
1514
|
UserSavedViewAssociation.objects.create(user=self.user, saved_view=sv, view_name=sv.view)
|
|
1551
1515
|
response = self.client.get(reverse(view_name), follow=True)
|
|
1552
1516
|
# Assert that Location List View got redirected to Saved View set as user default
|
|
1553
|
-
self.
|
|
1554
|
-
response_body = extract_page_body(response.content.decode(response.charset))
|
|
1555
|
-
self.assertInHTML(
|
|
1556
|
-
"""
|
|
1557
|
-
<strong>
|
|
1558
|
-
User Location Default View
|
|
1559
|
-
</strong>
|
|
1560
|
-
""",
|
|
1561
|
-
response_body,
|
|
1562
|
-
)
|
|
1517
|
+
self.assertBodyContains(response, "<strong>User Location Default View</strong>", html=True)
|
|
1563
1518
|
|
|
1564
1519
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
|
1565
1520
|
def test_is_shared(self):
|
|
@@ -1914,12 +1869,8 @@ class ApprovalQueueTestCase(
|
|
|
1914
1869
|
|
|
1915
1870
|
# Try GET with model-level permission
|
|
1916
1871
|
response = self.client.get(self._get_url("view", instance))
|
|
1917
|
-
self.assertHttpStatus(response, 200)
|
|
1918
|
-
|
|
1919
|
-
response_body = extract_page_body(response.content.decode(response.charset))
|
|
1920
|
-
|
|
1921
1872
|
# The object's display name or string representation should appear in the response
|
|
1922
|
-
self.
|
|
1873
|
+
self.assertBodyContains(response, getattr(instance, "display", str(instance)))
|
|
1923
1874
|
|
|
1924
1875
|
# skip GetObjectViewTestCase checks for Relationships and Custom Fields since this isn't actually a detail view
|
|
1925
1876
|
|
|
@@ -1954,9 +1905,7 @@ class ApprovalQueueTestCase(
|
|
|
1954
1905
|
"""Anonymous users may not take any action with regard to job approval requests."""
|
|
1955
1906
|
self.client.logout()
|
|
1956
1907
|
response = self.client.post(self._get_url("view", self._get_queryset().first()))
|
|
1957
|
-
self.
|
|
1958
|
-
response_body = extract_page_body(response.content.decode(response.charset))
|
|
1959
|
-
self.assertIn("You do not have permission to run jobs", response_body)
|
|
1908
|
+
self.assertBodyContains(response, "You do not have permission to run jobs")
|
|
1960
1909
|
# No job was submitted
|
|
1961
1910
|
self.assertFalse(JobResult.objects.filter(name=self.job_model.name).exists())
|
|
1962
1911
|
|
|
@@ -1968,9 +1917,7 @@ class ApprovalQueueTestCase(
|
|
|
1968
1917
|
data = {"_dry_run": True}
|
|
1969
1918
|
|
|
1970
1919
|
response = self.client.post(self._get_url("view", instance), data)
|
|
1971
|
-
self.
|
|
1972
|
-
response_body = extract_page_body(response.content.decode(response.charset))
|
|
1973
|
-
self.assertIn("This job cannot be run at this time", response_body)
|
|
1920
|
+
self.assertBodyContains(response, "This job cannot be run at this time")
|
|
1974
1921
|
# No job was submitted
|
|
1975
1922
|
self.assertFalse(JobResult.objects.filter(name=instance.job_model.name).exists())
|
|
1976
1923
|
|
|
@@ -1984,9 +1931,7 @@ class ApprovalQueueTestCase(
|
|
|
1984
1931
|
data = {"_dry_run": True}
|
|
1985
1932
|
|
|
1986
1933
|
response = self.client.post(self._get_url("view", instance), data)
|
|
1987
|
-
self.
|
|
1988
|
-
response_body = extract_page_body(response.content.decode(response.charset))
|
|
1989
|
-
self.assertIn("You do not have permission to run this job", response_body)
|
|
1934
|
+
self.assertBodyContains(response, "You do not have permission to run this job")
|
|
1990
1935
|
# No job was submitted
|
|
1991
1936
|
self.assertFalse(JobResult.objects.filter(name=instance.job_model.name).exists())
|
|
1992
1937
|
|
|
@@ -2006,9 +1951,7 @@ class ApprovalQueueTestCase(
|
|
|
2006
1951
|
instance2.job_model.save()
|
|
2007
1952
|
|
|
2008
1953
|
response = self.client.post(self._get_url("view", instance2), data)
|
|
2009
|
-
self.
|
|
2010
|
-
response_body = extract_page_body(response.content.decode(response.charset))
|
|
2011
|
-
self.assertIn("You do not have permission to run this job", response_body)
|
|
1954
|
+
self.assertBodyContains(response, "You do not have permission to run this job")
|
|
2012
1955
|
# No job was submitted
|
|
2013
1956
|
job_names = [instance1.job_model.name, instance2.job_model.name]
|
|
2014
1957
|
self.assertFalse(JobResult.objects.filter(name__in=job_names).exists())
|
|
@@ -2085,9 +2028,7 @@ class ApprovalQueueTestCase(
|
|
|
2085
2028
|
for user in (user1, user2):
|
|
2086
2029
|
self.client.force_login(user)
|
|
2087
2030
|
response = self.client.post(self._get_url("view", instance), data)
|
|
2088
|
-
self.
|
|
2089
|
-
response_body = extract_page_body(response.content.decode(response.charset))
|
|
2090
|
-
self.assertIn("You do not have permission", response_body, msg=str(user))
|
|
2031
|
+
self.assertBodyContains(response, "You do not have permission")
|
|
2091
2032
|
# Request was not deleted
|
|
2092
2033
|
self.assertEqual(1, len(ScheduledJob.objects.filter(pk=instance.pk)), msg=str(user))
|
|
2093
2034
|
|
|
@@ -2120,9 +2061,7 @@ class ApprovalQueueTestCase(
|
|
|
2120
2061
|
# Check object-based permissions are enforced for a different instance
|
|
2121
2062
|
instance = self._get_queryset().first()
|
|
2122
2063
|
response = self.client.post(self._get_url("view", instance), data)
|
|
2123
|
-
self.
|
|
2124
|
-
response_body = extract_page_body(response.content.decode(response.charset))
|
|
2125
|
-
self.assertIn("You do not have permission", response_body, msg=str(user))
|
|
2064
|
+
self.assertBodyContains(response, "You do not have permission")
|
|
2126
2065
|
# Request was not deleted
|
|
2127
2066
|
self.assertEqual(1, len(ScheduledJob.objects.filter(pk=instance.pk)), msg=str(user))
|
|
2128
2067
|
|
|
@@ -2134,9 +2073,7 @@ class ApprovalQueueTestCase(
|
|
|
2134
2073
|
data = {"_approve": True}
|
|
2135
2074
|
|
|
2136
2075
|
response = self.client.post(self._get_url("view", instance), data)
|
|
2137
|
-
self.
|
|
2138
|
-
response_body = extract_page_body(response.content.decode(response.charset))
|
|
2139
|
-
self.assertIn("You cannot approve your own job request", response_body)
|
|
2076
|
+
self.assertBodyContains(response, "You cannot approve your own job request")
|
|
2140
2077
|
# Job was not approved
|
|
2141
2078
|
instance.refresh_from_db()
|
|
2142
2079
|
self.assertIsNone(instance.approved_by_user)
|
|
@@ -2171,9 +2108,7 @@ class ApprovalQueueTestCase(
|
|
|
2171
2108
|
for user in (user1, user2):
|
|
2172
2109
|
self.client.force_login(user)
|
|
2173
2110
|
response = self.client.post(self._get_url("view", instance), data)
|
|
2174
|
-
self.
|
|
2175
|
-
response_body = extract_page_body(response.content.decode(response.charset))
|
|
2176
|
-
self.assertIn("You do not have permission", response_body, msg=str(user))
|
|
2111
|
+
self.assertBodyContains(response, "You do not have permission")
|
|
2177
2112
|
# Job was not approved
|
|
2178
2113
|
instance.refresh_from_db()
|
|
2179
2114
|
self.assertIsNone(instance.approved_by_user)
|
|
@@ -2208,9 +2143,7 @@ class ApprovalQueueTestCase(
|
|
|
2208
2143
|
# Check object-based permissions are enforced for a different instance
|
|
2209
2144
|
instance = self._get_queryset().last()
|
|
2210
2145
|
response = self.client.post(self._get_url("view", instance), data)
|
|
2211
|
-
self.
|
|
2212
|
-
response_body = extract_page_body(response.content.decode(response.charset))
|
|
2213
|
-
self.assertIn("You do not have permission", response_body, msg=str(user))
|
|
2146
|
+
self.assertBodyContains(response, "You do not have permission")
|
|
2214
2147
|
# Job was not scheduled
|
|
2215
2148
|
instance.refresh_from_db()
|
|
2216
2149
|
self.assertIsNone(instance.approved_by_user)
|
|
@@ -2251,9 +2184,7 @@ class JobResultTestCase(
|
|
|
2251
2184
|
url = reverse("extras:jobresult_log-table", kwargs={"pk": JobResult.objects.first().pk})
|
|
2252
2185
|
self.add_permissions("extras.view_jobresult", "extras.view_joblogentry")
|
|
2253
2186
|
response = self.client.get(url)
|
|
2254
|
-
self.
|
|
2255
|
-
response_body = response.content.decode(response.charset)
|
|
2256
|
-
self.assertIn("This is a test", response_body)
|
|
2187
|
+
self.assertBodyContains(response, "This is a test")
|
|
2257
2188
|
|
|
2258
2189
|
# TODO test with constrained permissions on both JobResult and JobLogEntry records
|
|
2259
2190
|
|
|
@@ -2394,18 +2325,18 @@ class JobTestCase(
|
|
|
2394
2325
|
# Try delete with delete job permission
|
|
2395
2326
|
self.add_permissions("extras.delete_job")
|
|
2396
2327
|
response = self.client.post(**request, follow=True)
|
|
2397
|
-
self.
|
|
2398
|
-
|
|
2399
|
-
|
|
2328
|
+
self.assertBodyContains(
|
|
2329
|
+
response, f"Unable to delete Job {instance}. System Job cannot be deleted", status_code=403
|
|
2330
|
+
)
|
|
2400
2331
|
# assert Job still exists
|
|
2401
2332
|
self.assertTrue(self._get_queryset().filter(name=job_name).exists())
|
|
2402
2333
|
|
|
2403
2334
|
# Try delete as a superuser
|
|
2404
2335
|
self.user.is_superuser = True
|
|
2405
2336
|
response = self.client.post(**request, follow=True)
|
|
2406
|
-
self.
|
|
2407
|
-
|
|
2408
|
-
|
|
2337
|
+
self.assertBodyContains(
|
|
2338
|
+
response, f"Unable to delete Job {instance}. System Job cannot be deleted", status_code=403
|
|
2339
|
+
)
|
|
2409
2340
|
# assert Job still exists
|
|
2410
2341
|
self.assertTrue(self._get_queryset().filter(name=job_name).exists())
|
|
2411
2342
|
|
|
@@ -2421,22 +2352,22 @@ class JobTestCase(
|
|
|
2421
2352
|
# Try bulk delete with delete job permission
|
|
2422
2353
|
self.add_permissions("extras.delete_job")
|
|
2423
2354
|
response = self.client.post(self._get_url("bulk_delete"), data, follow=True)
|
|
2424
|
-
self.
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
f"Unable to delete Job {system_job_queryset.first()}. System Job cannot be deleted", response_body
|
|
2355
|
+
self.assertBodyContains(
|
|
2356
|
+
response,
|
|
2357
|
+
f"Unable to delete Job {system_job_queryset.first()}. System Job cannot be deleted",
|
|
2358
|
+
status_code=403,
|
|
2429
2359
|
)
|
|
2360
|
+
self.assertEqual(self._get_queryset().count(), initial_count)
|
|
2430
2361
|
|
|
2431
2362
|
# Try bulk delete as a superuser
|
|
2432
2363
|
self.user.is_superuser = True
|
|
2433
2364
|
response = self.client.post(self._get_url("bulk_delete"), data, follow=True)
|
|
2434
|
-
self.
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
f"Unable to delete Job {system_job_queryset.first()}. System Job cannot be deleted", response_body
|
|
2365
|
+
self.assertBodyContains(
|
|
2366
|
+
response,
|
|
2367
|
+
f"Unable to delete Job {system_job_queryset.first()}. System Job cannot be deleted",
|
|
2368
|
+
status_code=403,
|
|
2439
2369
|
)
|
|
2370
|
+
self.assertEqual(self._get_queryset().count(), initial_count)
|
|
2440
2371
|
|
|
2441
2372
|
def validate_job_data_after_bulk_edit(self, pk_list, old_data):
|
|
2442
2373
|
# Name is bulk-editable
|
|
@@ -2502,10 +2433,7 @@ class JobTestCase(
|
|
|
2502
2433
|
self.add_permissions("extras.run_job")
|
|
2503
2434
|
for run_url in self.run_urls:
|
|
2504
2435
|
response = self.client.get(run_url)
|
|
2505
|
-
self.
|
|
2506
|
-
|
|
2507
|
-
response_body = extract_page_body(response.content.decode(response.charset))
|
|
2508
|
-
self.assertIn("TestPass", response_body)
|
|
2436
|
+
self.assertBodyContains(response, "TestPass")
|
|
2509
2437
|
|
|
2510
2438
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
|
2511
2439
|
def test_get_run_with_constrained_permission(self):
|
|
@@ -2548,10 +2476,7 @@ class JobTestCase(
|
|
|
2548
2476
|
|
|
2549
2477
|
for run_url in self.run_urls:
|
|
2550
2478
|
response = self.client.post(run_url, self.data_run_immediately)
|
|
2551
|
-
self.
|
|
2552
|
-
|
|
2553
|
-
content = extract_page_body(response.content.decode(response.charset))
|
|
2554
|
-
self.assertIn("Celery worker process not running.", content)
|
|
2479
|
+
self.assertBodyContains(response, "Celery worker process not running.")
|
|
2555
2480
|
|
|
2556
2481
|
@mock.patch("nautobot.extras.views.get_worker_count", return_value=1)
|
|
2557
2482
|
def test_run_now(self, _):
|
|
@@ -2598,9 +2523,7 @@ class JobTestCase(
|
|
|
2598
2523
|
reverse("extras:job_run", kwargs={"pk": self.test_not_installed.pk}),
|
|
2599
2524
|
):
|
|
2600
2525
|
response = self.client.post(run_url, self.data_run_immediately)
|
|
2601
|
-
self.
|
|
2602
|
-
response_body = extract_page_body(response.content.decode(response.charset))
|
|
2603
|
-
self.assertIn("Job is not presently installed", response_body)
|
|
2526
|
+
self.assertBodyContains(response, "Job is not presently installed")
|
|
2604
2527
|
|
|
2605
2528
|
self.assertFalse(JobResult.objects.filter(name=self.test_not_installed.name).exists())
|
|
2606
2529
|
|
|
@@ -2613,9 +2536,7 @@ class JobTestCase(
|
|
|
2613
2536
|
reverse("extras:job_run", kwargs={"pk": Job.objects.get(job_class_name="TestFail").pk}),
|
|
2614
2537
|
):
|
|
2615
2538
|
response = self.client.post(run_url, self.data_run_immediately)
|
|
2616
|
-
self.
|
|
2617
|
-
response_body = extract_page_body(response.content.decode(response.charset))
|
|
2618
|
-
self.assertIn("Job is not enabled to be run", response_body)
|
|
2539
|
+
self.assertBodyContains(response, "Job is not enabled to be run")
|
|
2619
2540
|
self.assertFalse(JobResult.objects.filter(name="fail.TestFail").exists())
|
|
2620
2541
|
|
|
2621
2542
|
def test_run_now_missing_args(self):
|
|
@@ -2773,10 +2694,7 @@ class JobTestCase(
|
|
|
2773
2694
|
for i, run_url in enumerate(self.run_urls):
|
|
2774
2695
|
data["_schedule_name"] = f"test {i}"
|
|
2775
2696
|
response = self.client.post(run_url, data)
|
|
2776
|
-
self.
|
|
2777
|
-
|
|
2778
|
-
content = extract_page_body(response.content.decode(response.charset))
|
|
2779
|
-
self.assertIn("Unable to schedule job: Job may have sensitive input variables.", content)
|
|
2697
|
+
self.assertBodyContains(response, "Unable to schedule job: Job may have sensitive input variables.")
|
|
2780
2698
|
|
|
2781
2699
|
@mock.patch("nautobot.extras.views.get_worker_count", return_value=1)
|
|
2782
2700
|
def test_run_job_with_invalid_task_queue(self, _):
|
|
@@ -2817,31 +2735,28 @@ class JobTestCase(
|
|
|
2817
2735
|
for run_url in self.run_urls:
|
|
2818
2736
|
# Assert warning message shows in get
|
|
2819
2737
|
response = self.client.get(run_url)
|
|
2820
|
-
|
|
2821
|
-
|
|
2738
|
+
self.assertBodyContains(
|
|
2739
|
+
response,
|
|
2822
2740
|
"This job is flagged as possibly having sensitive variables but is also flagged as requiring approval.",
|
|
2823
|
-
content,
|
|
2824
2741
|
)
|
|
2825
2742
|
|
|
2826
2743
|
# Assert run button is disabled
|
|
2827
|
-
self.
|
|
2744
|
+
self.assertBodyContains(
|
|
2745
|
+
response,
|
|
2828
2746
|
"""
|
|
2829
2747
|
<button type="submit" name="_run" id="id__run" class="btn btn-primary" disabled="disabled">
|
|
2830
2748
|
<i class="mdi mdi-play"></i> Run Job Now
|
|
2831
2749
|
</button>
|
|
2832
2750
|
""",
|
|
2833
|
-
|
|
2751
|
+
html=True,
|
|
2834
2752
|
)
|
|
2835
2753
|
# Assert error message shows after post
|
|
2836
2754
|
response = self.client.post(run_url, data)
|
|
2837
|
-
self.
|
|
2838
|
-
|
|
2839
|
-
content = extract_page_body(response.content.decode(response.charset))
|
|
2840
|
-
self.assertIn(
|
|
2755
|
+
self.assertBodyContains(
|
|
2756
|
+
response,
|
|
2841
2757
|
"Unable to run or schedule job: "
|
|
2842
2758
|
"This job is flagged as possibly having sensitive variables but is also flagged as requiring approval."
|
|
2843
2759
|
"One of these two flags must be removed before this job can be scheduled or run.",
|
|
2844
|
-
content,
|
|
2845
2760
|
)
|
|
2846
2761
|
|
|
2847
2762
|
def test_job_object_change_log_view(self):
|
|
@@ -2849,10 +2764,7 @@ class JobTestCase(
|
|
|
2849
2764
|
instance = self.test_pass
|
|
2850
2765
|
self.add_permissions("extras.view_objectchange", "extras.view_job")
|
|
2851
2766
|
response = self.client.get(instance.get_changelog_url())
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
self.assertHttpStatus(response, 200)
|
|
2855
|
-
self.assertIn(f"{instance.name} - Change Log", content)
|
|
2767
|
+
self.assertBodyContains(response, f"{instance.name} - Change Log")
|
|
2856
2768
|
|
|
2857
2769
|
|
|
2858
2770
|
class JobButtonTestCase(
|
|
@@ -2949,10 +2861,8 @@ class JobButtonRenderingTestCase(TestCase):
|
|
|
2949
2861
|
def test_view_object_with_job_button(self):
|
|
2950
2862
|
"""Ensure that the job button is rendered."""
|
|
2951
2863
|
response = self.client.get(self.location_type.get_absolute_url(), follow=True)
|
|
2952
|
-
self.
|
|
2953
|
-
|
|
2954
|
-
self.assertIn(f"JobButton {self.location_type.name}", content, content)
|
|
2955
|
-
self.assertIn("Click me!", content, content)
|
|
2864
|
+
self.assertBodyContains(response, f"JobButton {self.location_type.name}")
|
|
2865
|
+
self.assertBodyContains(response, "Click me!")
|
|
2956
2866
|
|
|
2957
2867
|
def test_task_queue_hidden_input_is_present(self):
|
|
2958
2868
|
"""
|
|
@@ -2962,16 +2872,12 @@ class JobButtonRenderingTestCase(TestCase):
|
|
|
2962
2872
|
self.job.task_queues = ["overriden_queue", "default", "priority"]
|
|
2963
2873
|
self.job.save()
|
|
2964
2874
|
response = self.client.get(self.location_type.get_absolute_url(), follow=True)
|
|
2965
|
-
self.
|
|
2966
|
-
content = extract_page_body(response.content.decode(response.charset))
|
|
2967
|
-
self.assertIn(f'<input type="hidden" name="_task_queue" value="{self.job.task_queues[0]}">', content, content)
|
|
2875
|
+
self.assertBodyContains(response, f'<input type="hidden" name="_task_queue" value="{self.job.task_queues[0]}">')
|
|
2968
2876
|
self.job.task_queues_override = False
|
|
2969
2877
|
self.job.save()
|
|
2970
2878
|
response = self.client.get(self.location_type.get_absolute_url(), follow=True)
|
|
2971
|
-
self.
|
|
2972
|
-
|
|
2973
|
-
self.assertIn(
|
|
2974
|
-
f'<input type="hidden" name="_task_queue" value="{settings.CELERY_TASK_DEFAULT_QUEUE}">', content, content
|
|
2879
|
+
self.assertBodyContains(
|
|
2880
|
+
response, f'<input type="hidden" name="_task_queue" value="{settings.CELERY_TASK_DEFAULT_QUEUE}">'
|
|
2975
2881
|
)
|
|
2976
2882
|
|
|
2977
2883
|
def test_view_object_with_unsafe_text(self):
|
|
@@ -3426,7 +3332,6 @@ class RelationshipAssociationTestCase(
|
|
|
3426
3332
|
response = self.client.get(self._get_url("list"))
|
|
3427
3333
|
self.assertHttpStatus(response, 200)
|
|
3428
3334
|
content = extract_page_body(response.content.decode(response.charset))
|
|
3429
|
-
# TODO: it'd make test failures more readable if we strip the page headers/footers from the content
|
|
3430
3335
|
self.assertIn(instance1.source.name, content, msg=content)
|
|
3431
3336
|
self.assertIn(instance1.destination.name, content, msg=content)
|
|
3432
3337
|
self.assertNotIn(instance2.source.name, content, msg=content)
|
|
@@ -3470,10 +3375,7 @@ class StaticGroupAssociationTestCase(
|
|
|
3470
3375
|
|
|
3471
3376
|
self.add_permissions("extras.view_staticgroupassociation")
|
|
3472
3377
|
response = self.client.get(f"{self._get_url('list')}?dynamic_group={sga1.dynamic_group.pk}")
|
|
3473
|
-
self.
|
|
3474
|
-
content = extract_page_body(response.content.decode(response.charset))
|
|
3475
|
-
|
|
3476
|
-
self.assertIn(sga1.get_absolute_url(), content, msg=content)
|
|
3378
|
+
self.assertBodyContains(response, sga1.get_absolute_url())
|
|
3477
3379
|
|
|
3478
3380
|
|
|
3479
3381
|
class StatusTestCase(
|
|
@@ -3610,7 +3512,7 @@ class TagTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|
|
3610
3512
|
response = self.client.post(**request)
|
|
3611
3513
|
tag = Tag.objects.filter(name=self.form_data["name"])
|
|
3612
3514
|
self.assertFalse(tag.exists())
|
|
3613
|
-
self.
|
|
3515
|
+
self.assertBodyContains(response, "content_types: Select a valid choice")
|
|
3614
3516
|
|
|
3615
3517
|
def test_update_tags_remove_content_type(self):
|
|
3616
3518
|
"""Test removing a tag content_type that is been tagged to a model"""
|
nautobot/extras/utils.py
CHANGED
|
@@ -21,7 +21,7 @@ from nautobot.core.choices import ColorChoices
|
|
|
21
21
|
from nautobot.core.constants import CHARFIELD_MAX_LENGTH
|
|
22
22
|
from nautobot.core.models.managers import TagsManager
|
|
23
23
|
from nautobot.core.models.utils import find_models_with_matching_fields
|
|
24
|
-
from nautobot.extras.choices import ObjectChangeActionChoices
|
|
24
|
+
from nautobot.extras.choices import DynamicGroupTypeChoices, ObjectChangeActionChoices
|
|
25
25
|
from nautobot.extras.constants import (
|
|
26
26
|
CHANGELOG_MAX_CHANGE_CONTEXT_DETAIL,
|
|
27
27
|
EXTRAS_FEATURES,
|
|
@@ -366,17 +366,30 @@ def get_celery_queues():
|
|
|
366
366
|
"""
|
|
367
367
|
from nautobot.core.celery import app # prevent circular import
|
|
368
368
|
|
|
369
|
-
celery_queues =
|
|
369
|
+
celery_queues = None
|
|
370
|
+
with contextlib.suppress(redis.exceptions.ConnectionError):
|
|
371
|
+
celery_queues = cache.get("nautobot.extras.utils.get_celery_queues")
|
|
370
372
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
373
|
+
if celery_queues is None:
|
|
374
|
+
celery_queues = {}
|
|
375
|
+
celery_inspect = app.control.inspect()
|
|
376
|
+
try:
|
|
377
|
+
active_queues = celery_inspect.active_queues()
|
|
378
|
+
except redis.exceptions.ConnectionError:
|
|
379
|
+
# Celery seems to be not smart enough to auto-retry on intermittent failures, so let's do it ourselves:
|
|
380
|
+
try:
|
|
381
|
+
active_queues = celery_inspect.active_queues()
|
|
382
|
+
except redis.exceptions.ConnectionError as err:
|
|
383
|
+
logger.error("Repeated ConnectionError from Celery/Redis: %s", err)
|
|
384
|
+
active_queues = None
|
|
385
|
+
if active_queues is None:
|
|
386
|
+
return celery_queues
|
|
387
|
+
for task_queue_list in active_queues.values():
|
|
388
|
+
distinct_queues = {q["name"] for q in task_queue_list}
|
|
389
|
+
for queue in distinct_queues:
|
|
390
|
+
celery_queues[queue] = celery_queues.get(queue, 0) + 1
|
|
391
|
+
with contextlib.suppress(redis.exceptions.ConnectionError):
|
|
392
|
+
cache.set("nautobot.extras.utils.get_celery_queues", celery_queues, timeout=5)
|
|
380
393
|
|
|
381
394
|
return celery_queues
|
|
382
395
|
|
|
@@ -584,6 +597,32 @@ def fixup_null_statuses(*, model, model_contenttype, status_model):
|
|
|
584
597
|
print(f" Found and fixed {updated_count} instances of {model.__name__} that had null 'status' fields.")
|
|
585
598
|
|
|
586
599
|
|
|
600
|
+
def fixup_dynamic_group_group_types(apps, *args, **kwargs): # pylint: disable=redefined-outer-name
|
|
601
|
+
"""Set dynamic group group_type values correctly."""
|
|
602
|
+
DynamicGroup = apps.get_model("extras", "DynamicGroup")
|
|
603
|
+
DynamicGroupMembership = apps.get_model("extras", "DynamicGroupMembership")
|
|
604
|
+
count_1 = count_2 = 0
|
|
605
|
+
# See note in migration 0112 - for some reason, if we were to do the "intuitive" thing, and call
|
|
606
|
+
# `DynamicGroup.objects.filter(children__isnull=False)`, we would unexpectedly get those groups for which their
|
|
607
|
+
# *parent* is non-null. The below is an alternate approach that should remain correct even if that issue gets fixed.
|
|
608
|
+
parent_group_names = set(DynamicGroupMembership.objects.values_list("parent_group__name", flat=True))
|
|
609
|
+
parent_groups_with_wrong_type = DynamicGroup.objects.filter(name__in=parent_group_names).exclude(
|
|
610
|
+
group_type=DynamicGroupTypeChoices.TYPE_DYNAMIC_SET
|
|
611
|
+
)
|
|
612
|
+
if parent_groups_with_wrong_type.exists():
|
|
613
|
+
count_1 = parent_groups_with_wrong_type.update(group_type=DynamicGroupTypeChoices.TYPE_DYNAMIC_SET)
|
|
614
|
+
print(f'\n Found and fixed {count_1} DynamicGroup(s) that should be typed as "Group of groups".')
|
|
615
|
+
|
|
616
|
+
filter_groups_with_wrong_type = DynamicGroup.objects.exclude(filter__exact={}).exclude(
|
|
617
|
+
group_type=DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER
|
|
618
|
+
)
|
|
619
|
+
if filter_groups_with_wrong_type.exists():
|
|
620
|
+
count_2 = filter_groups_with_wrong_type.update(group_type=DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER)
|
|
621
|
+
print(f'\n Found and fixed {count_2} DynamicGroup(s) that should be typed as "Filter-defined".')
|
|
622
|
+
|
|
623
|
+
return count_1, count_2
|
|
624
|
+
|
|
625
|
+
|
|
587
626
|
def migrate_role_data(
|
|
588
627
|
model_to_migrate,
|
|
589
628
|
*,
|
nautobot/ipam/filters.py
CHANGED
|
@@ -586,13 +586,13 @@ class VLANFilterSet(
|
|
|
586
586
|
fields = ["id", "name", "tags", "vid"]
|
|
587
587
|
|
|
588
588
|
def get_for_device(self, queryset, name, value):
|
|
589
|
-
# TODO: after Location model replaced Site, which was not a hierarchical model, should we consider to include
|
|
590
|
-
# VLANs that belong to the parent/child locations of the `device.location`?
|
|
591
589
|
"""Return all VLANs available to the specified Device(value)."""
|
|
592
590
|
devices = Device.objects.select_related("location").filter(**{f"{name}__in": value})
|
|
593
591
|
if not devices.exists():
|
|
594
592
|
return queryset.none()
|
|
595
593
|
location_ids = list(devices.values_list("location__id", flat=True))
|
|
594
|
+
for location in Location.objects.filter(pk__in=location_ids):
|
|
595
|
+
location_ids.extend([ancestor.id for ancestor in location.ancestors()])
|
|
596
596
|
return queryset.filter(Q(locations__isnull=True) | Q(locations__in=location_ids))
|
|
597
597
|
|
|
598
598
|
|
nautobot/ipam/models.py
CHANGED
|
@@ -890,12 +890,39 @@ class Prefix(PrimaryModel):
|
|
|
890
890
|
)
|
|
891
891
|
return available_ips
|
|
892
892
|
|
|
893
|
+
def get_child_ips(self):
|
|
894
|
+
"""
|
|
895
|
+
Return IP addresses with this prefix as their *direct* parent.
|
|
896
|
+
|
|
897
|
+
Does *not* include IPs that descend from a descendant prefix; if those are desired, use get_all_ips() instead.
|
|
898
|
+
|
|
899
|
+
```
|
|
900
|
+
Prefix 10.0.0.0/16
|
|
901
|
+
IPAddress 10.0.0.1/24
|
|
902
|
+
Prefix 10.0.1.0/24
|
|
903
|
+
IPAddress 10.0.1.1/24
|
|
904
|
+
```
|
|
905
|
+
|
|
906
|
+
In the above example, `<Prefix 10.0.0.0/16>.get_child_ips()` will *only* return 10.0.0.1/24,
|
|
907
|
+
while `<Prefix 10.0.0.0/16>.get_all_ips()` will return *both* 10.0.0.1.24 and 10.0.1.1/24.
|
|
908
|
+
"""
|
|
909
|
+
return self.ip_addresses.all()
|
|
910
|
+
|
|
893
911
|
def get_all_ips(self):
|
|
894
912
|
"""
|
|
895
913
|
Return all IP addresses contained within this prefix, including child prefixes' IP addresses.
|
|
896
914
|
|
|
897
|
-
|
|
898
|
-
|
|
915
|
+
This is distinct from the behavior of `get_child_ips()` and in *most* cases is probably preferred.
|
|
916
|
+
|
|
917
|
+
```
|
|
918
|
+
Prefix 10.0.0.0/16
|
|
919
|
+
IPAddress 10.0.0.1/24
|
|
920
|
+
Prefix 10.0.1.0/24
|
|
921
|
+
IPAddress 10.0.1.1/24
|
|
922
|
+
```
|
|
923
|
+
|
|
924
|
+
In the above example, `<Prefix 10.0.0.0/16>.get_child_ips()` will *only* return 10.0.0.1/24,
|
|
925
|
+
while `<Prefix 10.0.0.0/16>.get_all_ips()` will return *both* 10.0.0.1.24 and 10.0.1.1/24.
|
|
899
926
|
"""
|
|
900
927
|
return IPAddress.objects.filter(
|
|
901
928
|
parent__namespace=self.namespace, host__gte=self.network, host__lte=self.broadcast
|
|
@@ -150,9 +150,9 @@
|
|
|
150
150
|
</tr>
|
|
151
151
|
</table>
|
|
152
152
|
</div>
|
|
153
|
-
{% include 'panel_table.html' with table=parent_prefixes_table heading='Parent Prefixes' %}
|
|
154
153
|
{% endblock content_right_page %}
|
|
155
154
|
|
|
156
155
|
{% block content_full_width_page %}
|
|
157
|
-
|
|
156
|
+
{% include 'utilities/obj_table.html' with table=parent_prefixes_table table_template='panel_table.html' heading='Parent Prefixes' %}
|
|
157
|
+
{% include 'utilities/obj_table.html' with table=related_ips_table table_template='panel_table.html' heading='Related IP Addresses' %}
|
|
158
158
|
{% endblock content_full_width_page %}
|
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
{% render_table interface_table 'inc/table.html' %}
|
|
22
22
|
</div>
|
|
23
23
|
</form>
|
|
24
|
+
{% if interface_table.paginator.num_pages > 1 %}
|
|
25
|
+
{% include "inc/paginator.html" with paginator=interface_table.paginator page=interface_table.page %}
|
|
26
|
+
{% endif %}
|
|
24
27
|
{% table_config_form interface_table %}
|
|
25
28
|
{% endblock content %}
|
|
26
29
|
|