nautobot 2.3.6__py3-none-any.whl → 2.3.7__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/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/tests/test_views.py +26 -67
- 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/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/tests/test_api.py +18 -12
- nautobot/ipam/tests/test_views.py +6 -15
- 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 +247 -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.7.dist-info}/METADATA +2 -1
- {nautobot-2.3.6.dist-info → nautobot-2.3.7.dist-info}/RECORD +312 -310
- {nautobot-2.3.6.dist-info → nautobot-2.3.7.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.7.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.3.6.dist-info → nautobot-2.3.7.dist-info}/NOTICE +0 -0
- {nautobot-2.3.6.dist-info → nautobot-2.3.7.dist-info}/entry_points.txt +0 -0
nautobot/core/testing/views.py
CHANGED
|
@@ -144,6 +144,7 @@ class ViewTestCases:
|
|
|
144
144
|
self.client.logout()
|
|
145
145
|
response = self.client.get(self._get_queryset().first().get_absolute_url())
|
|
146
146
|
self.assertHttpStatus(response, 200)
|
|
147
|
+
# TODO: all this is doing is checking that a login link appears somewhere on the page (i.e. in the nav).
|
|
147
148
|
response_body = response.content.decode(response.charset)
|
|
148
149
|
self.assertIn(
|
|
149
150
|
"/login/?next=" + self._get_queryset().first().get_absolute_url(), response_body, msg=response_body
|
|
@@ -151,8 +152,7 @@ class ViewTestCases:
|
|
|
151
152
|
|
|
152
153
|
# The "Change Log" tab should appear in the response since we have all exempt permissions
|
|
153
154
|
if issubclass(self.model, extras_models.ChangeLoggedModel):
|
|
154
|
-
|
|
155
|
-
self.assertIn("Change Log", response_body, msg=response_body)
|
|
155
|
+
self.assertBodyContains(response, "Change Log")
|
|
156
156
|
|
|
157
157
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
|
158
158
|
def test_get_object_without_permission(self):
|
|
@@ -177,41 +177,33 @@ class ViewTestCases:
|
|
|
177
177
|
|
|
178
178
|
# Try GET with model-level permission
|
|
179
179
|
response = self.client.get(instance.get_absolute_url())
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
response_body = utils.extract_page_body(response.content.decode(response.charset))
|
|
183
|
-
|
|
184
|
-
# The object's display name or string representation should appear in the response
|
|
185
|
-
self.assertIn(escape(getattr(instance, "display", str(instance))), response_body, msg=response_body)
|
|
180
|
+
# The object's display name or string representation should appear in the response body
|
|
181
|
+
self.assertBodyContains(response, escape(getattr(instance, "display", str(instance))))
|
|
186
182
|
|
|
187
183
|
# If any Relationships are defined, they should appear in the response
|
|
188
184
|
if self.relationships is not None:
|
|
189
185
|
for relationship in self.relationships: # false positive pylint: disable=not-an-iterable
|
|
190
186
|
content_type = ContentType.objects.get_for_model(instance)
|
|
191
187
|
if content_type == relationship.source_type:
|
|
192
|
-
self.
|
|
188
|
+
self.assertBodyContains(
|
|
189
|
+
response,
|
|
193
190
|
escape(relationship.get_label(extras_choices.RelationshipSideChoices.SIDE_SOURCE)),
|
|
194
|
-
response_body,
|
|
195
|
-
msg=response_body,
|
|
196
191
|
)
|
|
197
192
|
if content_type == relationship.destination_type:
|
|
198
|
-
self.
|
|
193
|
+
self.assertBodyContains(
|
|
194
|
+
response,
|
|
199
195
|
escape(relationship.get_label(extras_choices.RelationshipSideChoices.SIDE_DESTINATION)),
|
|
200
|
-
response_body,
|
|
201
|
-
msg=response_body,
|
|
202
196
|
)
|
|
203
197
|
|
|
204
198
|
# If any Custom Fields are defined, they should appear in the response
|
|
205
199
|
if self.custom_fields is not None:
|
|
206
200
|
for custom_field in self.custom_fields: # false positive pylint: disable=not-an-iterable
|
|
207
|
-
self.
|
|
201
|
+
self.assertBodyContains(response, escape(str(custom_field)))
|
|
208
202
|
if custom_field.type == extras_choices.CustomFieldTypeChoices.TYPE_MULTISELECT:
|
|
209
203
|
for value in instance.cf.get(custom_field.key):
|
|
210
|
-
self.
|
|
204
|
+
self.assertBodyContains(response, escape(str(value)))
|
|
211
205
|
else:
|
|
212
|
-
self.
|
|
213
|
-
escape(str(instance.cf.get(custom_field.key) or "")), response_body, msg=response_body
|
|
214
|
-
)
|
|
206
|
+
self.assertBodyContains(response, escape(str(instance.cf.get(custom_field.key) or "")))
|
|
215
207
|
|
|
216
208
|
return response # for consumption by child test cases if desired
|
|
217
209
|
|
|
@@ -251,11 +243,8 @@ class ViewTestCases:
|
|
|
251
243
|
obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
|
|
252
244
|
|
|
253
245
|
response = self.client.get(instance.get_absolute_url())
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
self.assertIn(advanced_tab_href, response_body)
|
|
258
|
-
self.assertIn("Advanced", response_body)
|
|
246
|
+
self.assertBodyContains(response, f"{instance.get_absolute_url()}#advanced")
|
|
247
|
+
self.assertBodyContains(response, "Advanced")
|
|
259
248
|
|
|
260
249
|
class GetObjectChangelogViewTestCase(ModelViewTestCase):
|
|
261
250
|
"""
|
|
@@ -268,12 +257,16 @@ class ViewTestCases:
|
|
|
268
257
|
url = self._get_url("changelog", obj)
|
|
269
258
|
response = self.client.get(url)
|
|
270
259
|
self.assertHttpStatus(response, 200)
|
|
271
|
-
|
|
260
|
+
|
|
261
|
+
# Test for https://github.com/nautobot/nautobot/issues/5214
|
|
272
262
|
if getattr(obj, "is_contact_associable_model", False):
|
|
273
|
-
self.
|
|
263
|
+
self.assertBodyContains(
|
|
264
|
+
response,
|
|
274
265
|
f'<a href="{obj.get_absolute_url()}#contacts" onclick="switch_tab(this.href)" aria-controls="contacts" role="tab" data-toggle="tab">Contacts</a>',
|
|
275
|
-
|
|
266
|
+
html=True,
|
|
276
267
|
)
|
|
268
|
+
else:
|
|
269
|
+
self.assertNotContains(response, f"{obj.get_absolute_url()}#contacts")
|
|
277
270
|
|
|
278
271
|
class GetObjectNotesViewTestCase(ModelViewTestCase):
|
|
279
272
|
"""
|
|
@@ -287,12 +280,16 @@ class ViewTestCases:
|
|
|
287
280
|
url = self._get_url("notes", obj)
|
|
288
281
|
response = self.client.get(url)
|
|
289
282
|
self.assertHttpStatus(response, 200)
|
|
290
|
-
|
|
283
|
+
|
|
284
|
+
# Test for https://github.com/nautobot/nautobot/issues/5214
|
|
291
285
|
if getattr(obj, "is_contact_associable_model", False):
|
|
292
|
-
self.
|
|
286
|
+
self.assertBodyContains(
|
|
287
|
+
response,
|
|
293
288
|
f'<a href="{obj.get_absolute_url()}#contacts" onclick="switch_tab(this.href)" aria-controls="contacts" role="tab" data-toggle="tab">Contacts</a>',
|
|
294
|
-
|
|
289
|
+
html=True,
|
|
295
290
|
)
|
|
291
|
+
else:
|
|
292
|
+
self.assertNotContains(response, f"{obj.get_absolute_url()}#contacts")
|
|
296
293
|
|
|
297
294
|
class CreateObjectViewTestCase(ModelViewTestCase):
|
|
298
295
|
"""
|
|
@@ -366,11 +363,9 @@ class ViewTestCases:
|
|
|
366
363
|
detail_url = instance.get_absolute_url()
|
|
367
364
|
validate(detail_url)
|
|
368
365
|
response = self.client.get(detail_url)
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
self.
|
|
372
|
-
self.assertIn("<td>Created By</td>", response_body)
|
|
373
|
-
self.assertIn("<td>nautobotuser</td>", response_body)
|
|
366
|
+
self.assertBodyContains(response, f"{detail_url}#advanced")
|
|
367
|
+
self.assertBodyContains(response, "<td>Created By</td>", html=True)
|
|
368
|
+
self.assertBodyContains(response, f"<td>{self.user.username}</td>", html=True)
|
|
374
369
|
except (AttributeError, ValidationError):
|
|
375
370
|
# Instance does not have a valid detail view, do nothing here.
|
|
376
371
|
pass
|
|
@@ -522,11 +517,9 @@ class ViewTestCases:
|
|
|
522
517
|
detail_url = instance.get_absolute_url()
|
|
523
518
|
validate(detail_url)
|
|
524
519
|
response = self.client.get(detail_url)
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
self.
|
|
528
|
-
self.assertIn("<td>Last Updated By</td>", response_body)
|
|
529
|
-
self.assertIn("<td>nautobotuser</td>", response_body)
|
|
520
|
+
self.assertBodyContains(response, f"{detail_url}#advanced")
|
|
521
|
+
self.assertBodyContains(response, "<td>Last Updated By</td>", html=True)
|
|
522
|
+
self.assertBodyContains(response, f"<td>{self.user.username}</td>", html=True)
|
|
530
523
|
except (AttributeError, ValidationError):
|
|
531
524
|
# Instance does not have a valid detail view, do nothing here.
|
|
532
525
|
pass
|
|
@@ -763,8 +756,7 @@ class ViewTestCases:
|
|
|
763
756
|
|
|
764
757
|
with self.subTest("Assert indentation is present"):
|
|
765
758
|
response = self.client.get(f"{self._get_url('list')}")
|
|
766
|
-
|
|
767
|
-
self.assertInHTML('<i class="mdi mdi-circle-small"></i>', response_body)
|
|
759
|
+
self.assertBodyContains(response, '<i class="mdi mdi-circle-small"></i>', html=True)
|
|
768
760
|
|
|
769
761
|
with self.subTest("Assert indentation is removed on filter"):
|
|
770
762
|
queryset = (
|
|
@@ -786,6 +778,7 @@ class ViewTestCases:
|
|
|
786
778
|
self.client.logout()
|
|
787
779
|
response = self.client.get(self._get_url("list"))
|
|
788
780
|
self.assertHttpStatus(response, 200)
|
|
781
|
+
# TODO: all this is doing is checking that a login link appears somewhere on the page (i.e. in the nav).
|
|
789
782
|
response_body = response.content.decode(response.charset)
|
|
790
783
|
self.assertIn("/login/?next=" + self._get_url("list"), response_body, msg=response_body)
|
|
791
784
|
|
|
@@ -799,7 +792,6 @@ class ViewTestCases:
|
|
|
799
792
|
response = self.client.get(f"{self._get_url('list')}?id={instance1.pk}")
|
|
800
793
|
self.assertHttpStatus(response, 200)
|
|
801
794
|
content = utils.extract_page_body(response.content.decode(response.charset))
|
|
802
|
-
# TODO: it'd make test failures more readable if we strip the page headers/footers from the content
|
|
803
795
|
if hasattr(self.model, "name"):
|
|
804
796
|
self.assertRegex(content, r">\s*" + re.escape(escape(instance1.name)) + r"\s*<", msg=content)
|
|
805
797
|
self.assertNotRegex(content, r">\s*" + re.escape(escape(instance2.name)) + r"\s*<", msg=content)
|
|
@@ -810,12 +802,9 @@ class ViewTestCases:
|
|
|
810
802
|
def test_list_objects_unknown_filter_strict_filtering(self):
|
|
811
803
|
"""Verify that with STRICT_FILTERING, an unknown filter results in an error message and no matches."""
|
|
812
804
|
response = self.client.get(f"{self._get_url('list')}?ice_cream_flavor=chocolate")
|
|
813
|
-
self.
|
|
814
|
-
content = utils.extract_page_body(response.content.decode(response.charset))
|
|
815
|
-
# TODO: it'd make test failures more readable if we strip the page headers/footers from the content
|
|
816
|
-
self.assertIn("Unknown filter field", content, msg=content)
|
|
805
|
+
self.assertBodyContains(response, "Unknown filter field")
|
|
817
806
|
# There should be no table rows displayed except for the empty results row
|
|
818
|
-
self.
|
|
807
|
+
self.assertBodyContains(response, "None")
|
|
819
808
|
|
|
820
809
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"], STRICT_FILTERING=False)
|
|
821
810
|
def test_list_objects_unknown_filter_no_strict_filtering(self):
|
|
@@ -842,7 +831,6 @@ class ViewTestCases:
|
|
|
842
831
|
)
|
|
843
832
|
self.assertHttpStatus(response, 200)
|
|
844
833
|
content = utils.extract_page_body(response.content.decode(response.charset))
|
|
845
|
-
# TODO: it'd make test failures more readable if we strip the page headers/footers from the content
|
|
846
834
|
self.assertNotIn("Unknown filter field", content, msg=content)
|
|
847
835
|
self.assertIn("None", content, msg=content)
|
|
848
836
|
if hasattr(self.model, "name"):
|
|
@@ -871,16 +859,13 @@ class ViewTestCases:
|
|
|
871
859
|
# Try GET with model-level permission
|
|
872
860
|
response = self.client.get(self._get_url("list"))
|
|
873
861
|
self.assertHttpStatus(response, 200)
|
|
874
|
-
response_body = response.content.decode(response.charset)
|
|
862
|
+
response_body = utils.extract_page_body(response.content.decode(response.charset))
|
|
875
863
|
|
|
876
864
|
list_url = self.get_list_url()
|
|
877
865
|
title = self.get_title()
|
|
878
866
|
|
|
879
867
|
# Check if breadcrumb is rendered correctly
|
|
880
|
-
self.
|
|
881
|
-
f'<a href="{list_url}">{title}</a>',
|
|
882
|
-
response_body,
|
|
883
|
-
)
|
|
868
|
+
self.assertBodyContains(response, f'<a href="{list_url}">{title}</a>', html=True)
|
|
884
869
|
|
|
885
870
|
# Check if import button is absent due to user permissions
|
|
886
871
|
self.assertNotIn(
|
|
@@ -909,7 +894,6 @@ class ViewTestCases:
|
|
|
909
894
|
response = self.client.get(self._get_url("list"))
|
|
910
895
|
self.assertHttpStatus(response, 200)
|
|
911
896
|
content = utils.extract_page_body(response.content.decode(response.charset))
|
|
912
|
-
# TODO: it'd make test failures more readable if we strip the page headers/footers from the content
|
|
913
897
|
if hasattr(self.model, "name"):
|
|
914
898
|
self.assertRegex(content, r">\s*" + re.escape(escape(instance1.name)) + r"\s*<", msg=content)
|
|
915
899
|
self.assertNotRegex(content, r">\s*" + re.escape(escape(instance2.name)) + r"\s*<", msg=content)
|
|
@@ -953,12 +937,12 @@ class ViewTestCases:
|
|
|
953
937
|
|
|
954
938
|
# Try GET with model-level permission
|
|
955
939
|
response = self.client.get(self._get_url("list"))
|
|
956
|
-
self.assertHttpStatus(response, 200)
|
|
957
|
-
response_body = response.content.decode(response.charset)
|
|
958
940
|
|
|
959
941
|
# Check app banner is rendered correctly
|
|
960
|
-
self.
|
|
961
|
-
|
|
942
|
+
self.assertBodyContains(
|
|
943
|
+
response,
|
|
944
|
+
f"<div>You are viewing a table of {self.model._meta.verbose_name_plural}</div>",
|
|
945
|
+
html=True,
|
|
962
946
|
)
|
|
963
947
|
|
|
964
948
|
class CreateMultipleObjectsViewTestCase(ModelViewTestCase):
|
|
@@ -1596,7 +1580,7 @@ class ViewTestCases:
|
|
|
1596
1580
|
f"Renaming {len(objects)} {helpers.bettertitle(verbose_name_plural)} "
|
|
1597
1581
|
f"on {self.selected_objects_parent_name}"
|
|
1598
1582
|
)
|
|
1599
|
-
self.
|
|
1583
|
+
self.assertBodyContains(response, message)
|
|
1600
1584
|
|
|
1601
1585
|
with self.subTest("Assert update successfully"):
|
|
1602
1586
|
data["_apply"] = True # Form Apply button
|
|
@@ -1611,4 +1595,4 @@ class ViewTestCases:
|
|
|
1611
1595
|
data["pk"] = values
|
|
1612
1596
|
response = self.client.post(self._get_url("bulk_rename"), data, follow=True)
|
|
1613
1597
|
expected_message = f"No valid {verbose_name_plural} were selected."
|
|
1614
|
-
self.
|
|
1598
|
+
self.assertBodyContains(response, expected_message)
|
nautobot/core/tests/runner.py
CHANGED
|
@@ -157,8 +157,9 @@ class NautobotTestRunner(DiscoverRunner):
|
|
|
157
157
|
suffix=str(index + 1),
|
|
158
158
|
verbosity=self.verbosity,
|
|
159
159
|
keepdb=self.keepdb
|
|
160
|
-
# Extra
|
|
161
|
-
and self.reusedb
|
|
160
|
+
# Extra checks added for Nautobot:
|
|
161
|
+
and self.reusedb
|
|
162
|
+
and not settings.TEST_USE_FACTORIES, # w/ factory data, clones can't be reused
|
|
162
163
|
)
|
|
163
164
|
|
|
164
165
|
# Configure all other connections as mirrors of the first one
|
|
@@ -188,7 +189,9 @@ class NautobotTestRunner(DiscoverRunner):
|
|
|
188
189
|
connection.creation.destroy_test_db(
|
|
189
190
|
suffix=str(index + 1),
|
|
190
191
|
verbosity=self.verbosity,
|
|
191
|
-
keepdb=self.keepdb
|
|
192
|
+
keepdb=self.keepdb
|
|
193
|
+
# Extra check added for Nautobot
|
|
194
|
+
and not settings.TEST_USE_FACTORIES, # with factory data, clones cannot be reused
|
|
192
195
|
)
|
|
193
196
|
|
|
194
197
|
# Extra block added for Nautobot
|
|
@@ -8,6 +8,7 @@ from django.urls import reverse
|
|
|
8
8
|
|
|
9
9
|
from nautobot.circuits import models as circuits_models
|
|
10
10
|
from nautobot.core import testing
|
|
11
|
+
from nautobot.core.testing.utils import extract_page_body
|
|
11
12
|
from nautobot.core.views import paginator
|
|
12
13
|
from nautobot.dcim import models as dcim_models
|
|
13
14
|
from nautobot.extras import models as extras_models
|
|
@@ -75,7 +76,7 @@ class PaginatorTestCase(testing.TestCase):
|
|
|
75
76
|
warning_message = (
|
|
76
77
|
"Requested "per_page" is too large. No more than 10 items may be displayed at a time."
|
|
77
78
|
)
|
|
78
|
-
self.assertIn(warning_message, response.content.decode(response.charset))
|
|
79
|
+
self.assertIn(warning_message, extract_page_body(response.content.decode(response.charset)))
|
|
79
80
|
with self.subTest("query parameter per_page=5 returns 5 rows"):
|
|
80
81
|
response = self.client.get(url, {"per_page": 5})
|
|
81
82
|
self.assertHttpStatus(response, 200)
|
|
@@ -97,7 +98,7 @@ class PaginatorTestCase(testing.TestCase):
|
|
|
97
98
|
warning_message = (
|
|
98
99
|
"Requested "per_page" is too large. No more than 10 items may be displayed at a time."
|
|
99
100
|
)
|
|
100
|
-
self.assertIn(warning_message, response.content.decode(response.charset)
|
|
101
|
+
self.assertIn(warning_message, extract_page_body(response.content.decode(response.charset)))
|
|
101
102
|
|
|
102
103
|
@override_settings(MAX_PAGE_SIZE=0)
|
|
103
104
|
def test_error_warning_not_shown_when_max_page_size_is_0(self):
|
|
@@ -118,4 +119,4 @@ class PaginatorTestCase(testing.TestCase):
|
|
|
118
119
|
self.assertEqual(response.context["paginator"].per_page, 20)
|
|
119
120
|
self.assertEqual(len(response.context["table"].page), 20)
|
|
120
121
|
warning_message = "Requested "per_page" is too large."
|
|
121
|
-
self.assertNotIn(warning_message, response.content.decode(response.charset))
|
|
122
|
+
self.assertNotIn(warning_message, extract_page_body(response.content.decode(response.charset)))
|
|
@@ -14,6 +14,7 @@ from prometheus_client.parser import text_string_to_metric_families
|
|
|
14
14
|
from nautobot.core.constants import GLOBAL_SEARCH_EXCLUDE_LIST
|
|
15
15
|
from nautobot.core.testing import TestCase
|
|
16
16
|
from nautobot.core.testing.api import APITestCase
|
|
17
|
+
from nautobot.core.testing.utils import extract_page_body
|
|
17
18
|
from nautobot.core.utils.permissions import get_permission_for_model
|
|
18
19
|
from nautobot.core.views import NautobotMetricsView
|
|
19
20
|
from nautobot.core.views.mixins import GetReturnURLMixin
|
|
@@ -154,16 +155,15 @@ class HomeViewTestCase(TestCase):
|
|
|
154
155
|
BANNER_BOTTOM="[info](https://nautobot.com)",
|
|
155
156
|
):
|
|
156
157
|
response = self.client.get(url)
|
|
157
|
-
self.
|
|
158
|
-
self.
|
|
159
|
-
'<a href="https://nautobot.com" rel="noopener noreferrer">info</a>',
|
|
160
|
-
response.content.decode(response.charset),
|
|
158
|
+
self.assertBodyContains(response, "<h1>Hello world</h1>", html=True)
|
|
159
|
+
self.assertBodyContains(
|
|
160
|
+
response, '<a href="https://nautobot.com" rel="noopener noreferrer">info</a>', html=True
|
|
161
161
|
)
|
|
162
162
|
|
|
163
163
|
with override_settings(BANNER_LOGIN="_Welcome to Nautobot!_"):
|
|
164
164
|
self.client.logout()
|
|
165
165
|
response = self.client.get(reverse("login"))
|
|
166
|
-
self.
|
|
166
|
+
self.assertBodyContains(response, "<em>Welcome to Nautobot!</em>", html=True)
|
|
167
167
|
|
|
168
168
|
def test_banners_no_xss(self):
|
|
169
169
|
url = reverse("home")
|
|
@@ -195,21 +195,24 @@ class SearchFieldsTestCase(TestCase):
|
|
|
195
195
|
|
|
196
196
|
# Assert model search bar present in list UI
|
|
197
197
|
response = self.client.get(reverse("dcim:location_list"))
|
|
198
|
-
self.
|
|
198
|
+
self.assertBodyContains(
|
|
199
|
+
response,
|
|
199
200
|
'<input type="text" name="q" class="form-control" required placeholder="Search Locations" id="id_q">',
|
|
200
|
-
|
|
201
|
+
html=True,
|
|
201
202
|
)
|
|
202
203
|
|
|
203
204
|
response = self.client.get(reverse("dcim:device_list"))
|
|
204
|
-
self.
|
|
205
|
+
self.assertBodyContains(
|
|
206
|
+
response,
|
|
205
207
|
'<input type="text" name="q" class="form-control" required placeholder="Search Devices" id="id_q">',
|
|
206
|
-
|
|
208
|
+
html=True,
|
|
207
209
|
)
|
|
208
210
|
|
|
209
211
|
# Assert global search bar present in UI
|
|
210
|
-
self.
|
|
212
|
+
self.assertContains( # not using assertBodyContains because this is in the nav
|
|
213
|
+
response,
|
|
211
214
|
'<input type="text" name="q" class="form-control" placeholder="Search Nautobot">',
|
|
212
|
-
|
|
215
|
+
html=True,
|
|
213
216
|
)
|
|
214
217
|
|
|
215
218
|
|
|
@@ -233,16 +236,10 @@ class FilterFormsTestCase(TestCase):
|
|
|
233
236
|
"""
|
|
234
237
|
|
|
235
238
|
response = self.client.get(reverse("dcim:location_list"))
|
|
236
|
-
self.
|
|
237
|
-
filter_tabs,
|
|
238
|
-
response.content.decode(response.charset),
|
|
239
|
-
)
|
|
239
|
+
self.assertBodyContains(response, filter_tabs, html=True)
|
|
240
240
|
|
|
241
241
|
response = self.client.get(reverse("circuits:circuit_list"))
|
|
242
|
-
self.
|
|
243
|
-
filter_tabs,
|
|
244
|
-
response.content.decode(response.charset),
|
|
245
|
-
)
|
|
242
|
+
self.assertBodyContains(response, filter_tabs, html=True)
|
|
246
243
|
|
|
247
244
|
def test_filtering_on_custom_select_filter_field(self):
|
|
248
245
|
"""Assert CustomField select and multiple select fields can be filtered using multiple entries"""
|
|
@@ -271,10 +268,8 @@ class FilterFormsTestCase(TestCase):
|
|
|
271
268
|
)
|
|
272
269
|
url = reverse("dcim:location_list") + query_param
|
|
273
270
|
response = self.client.get(url)
|
|
274
|
-
self.
|
|
275
|
-
|
|
276
|
-
self.assertInHTML(locations[0].name, response_content)
|
|
277
|
-
self.assertInHTML(locations[1].name, response_content)
|
|
271
|
+
self.assertBodyContains(response, locations[0].name, html=True)
|
|
272
|
+
self.assertBodyContains(response, locations[1].name, html=True)
|
|
278
273
|
|
|
279
274
|
def test_filtering_crafted_query_params(self):
|
|
280
275
|
"""Test for reflected-XSS vulnerability GHSA-jxgr-gcj5-cqqg."""
|
|
@@ -282,17 +277,16 @@ class FilterFormsTestCase(TestCase):
|
|
|
282
277
|
query_param = "?location_type=1 onmouseover=alert('hi') foo=bar"
|
|
283
278
|
url = reverse("dcim:location_list") + query_param
|
|
284
279
|
response = self.client.get(url)
|
|
285
|
-
self.assertHttpStatus(response, 200)
|
|
286
|
-
response_content = response.content.decode(response.charset)
|
|
287
280
|
# The important thing here is that the data-field-parent and data-field-value are correctly quoted
|
|
288
|
-
self.
|
|
281
|
+
self.assertBodyContains(
|
|
282
|
+
response,
|
|
289
283
|
"""
|
|
290
284
|
<span class="filter-selection-choice-remove remove-filter-param"
|
|
291
285
|
data-field-type="child"
|
|
292
286
|
data-field-parent="location_type"
|
|
293
287
|
data-field-value="1 onmouseover=alert('hi') foo=bar"
|
|
294
288
|
>×</span>""", # noqa: RUF001 - ambiguous-unicode-character-string
|
|
295
|
-
|
|
289
|
+
html=True,
|
|
296
290
|
)
|
|
297
291
|
|
|
298
292
|
|
|
@@ -327,21 +321,18 @@ class NavAppsUITestCase(TestCase):
|
|
|
327
321
|
self.url = reverse("apps:apps_list")
|
|
328
322
|
self.item_weight = 100 # TODO: not easy to introspect from the nav menu struct, so hard-code it here for now
|
|
329
323
|
|
|
330
|
-
def make_request(self):
|
|
331
|
-
response = self.client.get(reverse("home"))
|
|
332
|
-
return response.content.decode(response.charset)
|
|
333
|
-
|
|
334
324
|
def test_installed_apps_visible(self):
|
|
335
325
|
"""The "Installed Apps" menu item should be available to an authenticated user regardless of permissions."""
|
|
336
|
-
|
|
337
|
-
self.
|
|
326
|
+
response = self.client.get(reverse("home"))
|
|
327
|
+
self.assertContains(
|
|
328
|
+
response,
|
|
338
329
|
f"""
|
|
339
330
|
<a href="{self.url}"
|
|
340
331
|
data-item-weight="{self.item_weight}">
|
|
341
332
|
Installed Apps
|
|
342
333
|
</a>
|
|
343
334
|
""",
|
|
344
|
-
|
|
335
|
+
html=True,
|
|
345
336
|
)
|
|
346
337
|
|
|
347
338
|
|
|
@@ -361,7 +352,7 @@ class LoginUITestCase(TestCase):
|
|
|
361
352
|
def make_request(self):
|
|
362
353
|
response = self.client.get(reverse("login"))
|
|
363
354
|
sso_login_pattern = re.compile('<a href=".*">Continue with SSO</a>')
|
|
364
|
-
return sso_login_pattern.search(response.content.decode(response.charset))
|
|
355
|
+
return sso_login_pattern.search(extract_page_body(response.content.decode(response.charset)))
|
|
365
356
|
|
|
366
357
|
def test_sso_login_button_not_visible(self):
|
|
367
358
|
"""Test Continue with SSO button not visible if SSO is enabled"""
|
|
@@ -458,13 +449,13 @@ class ErrorPagesTestCase(TestCase):
|
|
|
458
449
|
"""Nautobot's custom 404 page should be used and should include a default support message."""
|
|
459
450
|
with self.assertTemplateUsed("404.html"):
|
|
460
451
|
response = self.client.get("/foo/bar")
|
|
461
|
-
self.
|
|
462
|
-
|
|
463
|
-
self.assertInHTML(
|
|
452
|
+
self.assertBodyContains(
|
|
453
|
+
response,
|
|
464
454
|
"If further assistance is required, please join the <code>#nautobot</code> channel on "
|
|
465
455
|
'<a href="https://slack.networktocode.com/" rel="noopener noreferrer">Network to Code\'s '
|
|
466
456
|
"Slack community</a> and post your question.",
|
|
467
|
-
|
|
457
|
+
html=True,
|
|
458
|
+
status_code=404,
|
|
468
459
|
)
|
|
469
460
|
|
|
470
461
|
@override_settings(DEBUG=False, SUPPORT_MESSAGE="Hello world!")
|
|
@@ -473,8 +464,7 @@ class ErrorPagesTestCase(TestCase):
|
|
|
473
464
|
with self.assertTemplateUsed("404.html"):
|
|
474
465
|
response = self.client.get("/foo/bar")
|
|
475
466
|
self.assertNotContains(response, "Network to Code", status_code=404)
|
|
476
|
-
|
|
477
|
-
self.assertInHTML("Hello world!", response_content)
|
|
467
|
+
self.assertBodyContains(response, "Hello world!", status_code=404)
|
|
478
468
|
|
|
479
469
|
@override_settings(DEBUG=False)
|
|
480
470
|
@mock.patch("nautobot.core.views.HomeView.get", side_effect=Exception)
|
|
@@ -484,13 +474,13 @@ class ErrorPagesTestCase(TestCase):
|
|
|
484
474
|
self.client.raise_request_exception = False
|
|
485
475
|
response = self.client.get(url)
|
|
486
476
|
self.assertTemplateUsed(response, "500.html")
|
|
487
|
-
self.
|
|
488
|
-
|
|
489
|
-
self.assertInHTML(
|
|
477
|
+
self.assertBodyContains(
|
|
478
|
+
response,
|
|
490
479
|
"If further assistance is required, please join the <code>#nautobot</code> channel on "
|
|
491
480
|
'<a href="https://slack.networktocode.com/" rel="noopener noreferrer">Network to Code\'s '
|
|
492
481
|
"Slack community</a> and post your question.",
|
|
493
|
-
|
|
482
|
+
html=True,
|
|
483
|
+
status_code=500,
|
|
494
484
|
)
|
|
495
485
|
|
|
496
486
|
@override_settings(DEBUG=False, SUPPORT_MESSAGE="Hello world!")
|
|
@@ -502,8 +492,7 @@ class ErrorPagesTestCase(TestCase):
|
|
|
502
492
|
response = self.client.get(url)
|
|
503
493
|
self.assertTemplateUsed(response, "500.html")
|
|
504
494
|
self.assertNotContains(response, "Network to Code", status_code=500)
|
|
505
|
-
|
|
506
|
-
self.assertInHTML("Hello world!", response_content)
|
|
495
|
+
self.assertBodyContains(response, "Hello world!", status_code=500)
|
|
507
496
|
|
|
508
497
|
|
|
509
498
|
class DBFileStorageViewTestCase(TestCase):
|
|
@@ -591,23 +580,17 @@ class ExampleViewWithCustomPermissionsTest(TestCase):
|
|
|
591
580
|
self.client.logout()
|
|
592
581
|
url = reverse("plugins:example_app:view_with_custom_permissions")
|
|
593
582
|
response = self.client.get(url, follow=True)
|
|
594
|
-
self.assertHttpStatus(response, 200)
|
|
595
|
-
response_body = response.content.decode(response.charset)
|
|
596
583
|
# check if the user is redirected to the login page
|
|
597
|
-
self.
|
|
584
|
+
self.assertBodyContains(response, f'<input type="hidden" name="next" value="{url}" />', html=True)
|
|
598
585
|
|
|
599
586
|
# Test IsAdmin permission
|
|
600
587
|
self.client.force_login(self.user)
|
|
601
588
|
response = self.client.get(url, follow=True)
|
|
602
|
-
self.assertHttpStatus(response, 403)
|
|
603
|
-
response_body = response.content.decode(response.charset)
|
|
604
589
|
# check if the users have to have the permission to access the page
|
|
605
|
-
self.
|
|
590
|
+
self.assertBodyContains(response, "You do not have permission to access this page", status_code=403)
|
|
606
591
|
|
|
607
592
|
# View should be successfully accessed
|
|
608
593
|
self.user.is_staff = True
|
|
609
594
|
self.user.save()
|
|
610
595
|
response = self.client.get(url)
|
|
611
|
-
self.
|
|
612
|
-
response_body = response.content.decode(response.charset)
|
|
613
|
-
self.assertIn("You are viewing a table of example models", response_body)
|
|
596
|
+
self.assertBodyContains(response, "You are viewing a table of example models")
|
nautobot/core/views/__init__.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import logging
|
|
1
3
|
import os
|
|
2
4
|
import platform
|
|
3
5
|
import re
|
|
@@ -32,6 +34,7 @@ from prometheus_client import (
|
|
|
32
34
|
)
|
|
33
35
|
from prometheus_client.metrics_core import GaugeMetricFamily
|
|
34
36
|
from prometheus_client.registry import Collector
|
|
37
|
+
import redis.exceptions
|
|
35
38
|
from rest_framework.permissions import AllowAny, IsAuthenticated
|
|
36
39
|
from rest_framework.renderers import BaseRenderer
|
|
37
40
|
from rest_framework.response import Response
|
|
@@ -48,6 +51,8 @@ from nautobot.extras.forms import GraphQLQueryForm
|
|
|
48
51
|
from nautobot.extras.models import FileProxy, GraphQLQuery, Status
|
|
49
52
|
from nautobot.extras.registry import registry
|
|
50
53
|
|
|
54
|
+
logger = logging.getLogger(__name__)
|
|
55
|
+
|
|
51
56
|
|
|
52
57
|
class HomeView(AccessMixin, TemplateView):
|
|
53
58
|
template_name = "home.html"
|
|
@@ -154,26 +159,37 @@ class WorkerStatusView(UserPassesTestMixin, TemplateView):
|
|
|
154
159
|
timeout = max_timeout
|
|
155
160
|
else:
|
|
156
161
|
timeout = int(timeout)
|
|
157
|
-
celery_inspect = app.control.inspect(timeout=timeout)
|
|
158
162
|
|
|
159
|
-
|
|
160
|
-
worker_stats = celery_inspect.stats()
|
|
163
|
+
celery_inspect = app.control.inspect(timeout=timeout)
|
|
161
164
|
|
|
165
|
+
try:
|
|
166
|
+
# stats() returns a dict of {worker_name: stats_dict}
|
|
167
|
+
worker_stats = celery_inspect.stats()
|
|
168
|
+
except redis.exceptions.ConnectionError:
|
|
169
|
+
# Celery seems to be not smart enough to auto-retry on intermittent failures, so let's do it ourselves:
|
|
170
|
+
try:
|
|
171
|
+
worker_stats = celery_inspect.stats()
|
|
172
|
+
except redis.exceptions.ConnectionError as err:
|
|
173
|
+
logger.error("Repeated ConnectionError from Celery/Redis: %s", err)
|
|
174
|
+
worker_stats = None
|
|
175
|
+
|
|
176
|
+
active_tasks = reserved_tasks = active_queues = {}
|
|
162
177
|
if worker_stats:
|
|
163
178
|
# Set explicit list of workers to speed up subsequent queries
|
|
164
179
|
celery_inspect = app.control.inspect(list(worker_stats.keys()), timeout=5.0)
|
|
165
180
|
|
|
166
|
-
|
|
167
|
-
|
|
181
|
+
with contextlib.suppress(redis.exceptions.ConnectionError):
|
|
182
|
+
# active() returns a dict of {worker_name: [task_dict, task_dict, ...]}
|
|
183
|
+
active_tasks = celery_inspect.active() or {}
|
|
168
184
|
|
|
169
|
-
|
|
170
|
-
|
|
185
|
+
# reserved() returns a dict of {worker_name: [task_dict, task_dict, ...]}
|
|
186
|
+
reserved_tasks = celery_inspect.reserved() or {}
|
|
171
187
|
|
|
172
|
-
|
|
173
|
-
|
|
188
|
+
# active_queues() returns a dict of {worker_name: [queue_dict, queue_dict, ...]}
|
|
189
|
+
active_queues = celery_inspect.active_queues() or {}
|
|
174
190
|
else:
|
|
175
|
-
# No workers were found
|
|
176
|
-
worker_stats =
|
|
191
|
+
# No workers were found
|
|
192
|
+
worker_stats = {}
|
|
177
193
|
|
|
178
194
|
workers = []
|
|
179
195
|
for worker_name in sorted(worker_stats, key=sort_workers):
|