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
|
@@ -284,9 +284,7 @@ class LocationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
|
284
284
|
"data": post_data(test_form_data),
|
|
285
285
|
}
|
|
286
286
|
response = self.client.post(**request)
|
|
287
|
-
self.
|
|
288
|
-
response_body = response.content.decode(response.charset)
|
|
289
|
-
self.assertIn("“Generic Site” is not a valid UUID.", response_body)
|
|
287
|
+
self.assertBodyContains(response, "“Generic Site” is not a valid UUID.")
|
|
290
288
|
test_form_data["parent"] = site_1.pk
|
|
291
289
|
request["data"] = post_data(test_form_data)
|
|
292
290
|
self.assertHttpStatus(self.client.post(**request), 302)
|
|
@@ -1172,8 +1170,7 @@ module-bays:
|
|
|
1172
1170
|
}
|
|
1173
1171
|
|
|
1174
1172
|
response = self.client.post(url, data)
|
|
1175
|
-
self.
|
|
1176
|
-
self.assertIn("failed validation", response.content.decode(response.charset))
|
|
1173
|
+
self.assertBodyContains(response, "failed validation")
|
|
1177
1174
|
|
|
1178
1175
|
|
|
1179
1176
|
class ModuleTypeTestCase(
|
|
@@ -2279,12 +2276,8 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
|
2279
2276
|
|
|
2280
2277
|
url = reverse("dcim:device_interfaces", kwargs={"pk": device.pk})
|
|
2281
2278
|
response = self.client.get(url)
|
|
2282
|
-
self.assertHttpStatus(response, 200)
|
|
2283
|
-
response_body = response.content.decode(response.charset)
|
|
2284
|
-
# Count the number of occurrences of "Add IP address" in the response_body
|
|
2285
|
-
count = response_body.count("Add IP address")
|
|
2286
2279
|
# Assert that "Add IP address" appears for each of the three interfaces
|
|
2287
|
-
self.
|
|
2280
|
+
self.assertBodyContains(response, "Add IP address", count=3)
|
|
2288
2281
|
|
|
2289
2282
|
def test_device_interface_assign_ipaddress(self):
|
|
2290
2283
|
device = Device.objects.first()
|
|
@@ -2322,29 +2315,19 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
|
2322
2315
|
"data": post_data(assign_ip_form_data),
|
|
2323
2316
|
}
|
|
2324
2317
|
|
|
2325
|
-
with self.subTest("Assert
|
|
2318
|
+
with self.subTest("Assert Cannot assign IPAddress('Add New') without permission"):
|
|
2326
2319
|
# Assert Add new IPAddress
|
|
2327
2320
|
response = self.client.post(**add_new_ip_request, follow=True)
|
|
2328
|
-
|
|
2329
|
-
self.assertHttpStatus(response, 200)
|
|
2321
|
+
self.assertBodyContains(response, f"Interface with id "{self.interfaces[0].pk}" not found")
|
|
2330
2322
|
self.interfaces[0].refresh_from_db()
|
|
2331
2323
|
self.assertEqual(self.interfaces[0].ip_addresses.all().count(), 0)
|
|
2332
|
-
self.assertIn(
|
|
2333
|
-
f"Interface with id "{self.interfaces[0].pk}" not found",
|
|
2334
|
-
response_body,
|
|
2335
|
-
)
|
|
2336
2324
|
|
|
2337
|
-
with self.subTest("Assert
|
|
2325
|
+
with self.subTest("Assert Cannot assign IPAddress(Existing IP) without permission"):
|
|
2338
2326
|
# Assert Assign Exsisting IPAddress
|
|
2339
2327
|
response = self.client.post(**assign_ip_request, follow=True)
|
|
2340
|
-
|
|
2341
|
-
self.assertHttpStatus(response, 200)
|
|
2328
|
+
self.assertBodyContains(response, f"Interface with id "{self.interfaces[1].pk}" not found")
|
|
2342
2329
|
self.interfaces[1].refresh_from_db()
|
|
2343
2330
|
self.assertEqual(self.interfaces[1].ip_addresses.all().count(), 0)
|
|
2344
|
-
self.assertIn(
|
|
2345
|
-
f"Interface with id "{self.interfaces[1].pk}" not found",
|
|
2346
|
-
response_body,
|
|
2347
|
-
)
|
|
2348
2331
|
|
|
2349
2332
|
self.add_permissions("dcim.change_interface", "ipam.view_ipaddress")
|
|
2350
2333
|
|
|
@@ -2375,10 +2358,7 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
|
2375
2358
|
"data": post_data(assign_ip_form_data),
|
|
2376
2359
|
}
|
|
2377
2360
|
response = self.client.post(**assign_ip_request, follow=True)
|
|
2378
|
-
self.
|
|
2379
|
-
self.assertIn(
|
|
2380
|
-
"Please select at least one IP Address from the table.", response.content.decode(response.charset)
|
|
2381
|
-
)
|
|
2361
|
+
self.assertBodyContains(response, "Please select at least one IP Address from the table.")
|
|
2382
2362
|
|
|
2383
2363
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
2384
2364
|
def test_device_rearports(self):
|
|
@@ -2683,12 +2663,8 @@ class ModuleTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
|
2683
2663
|
|
|
2684
2664
|
url = reverse("dcim:module_interfaces", kwargs={"pk": module.pk})
|
|
2685
2665
|
response = self.client.get(url)
|
|
2686
|
-
self.assertHttpStatus(response, 200)
|
|
2687
|
-
response_body = response.content.decode(response.charset)
|
|
2688
|
-
# Count the number of occurrences of "Add IP address" in the response_body
|
|
2689
|
-
count = response_body.count("Add IP address")
|
|
2690
2666
|
# Assert that "Add IP address" appears for each of the three interfaces
|
|
2691
|
-
self.
|
|
2667
|
+
self.assertBodyContains(response, "Add IP address", count=3)
|
|
2692
2668
|
|
|
2693
2669
|
def test_module_interface_assign_ipaddress(self):
|
|
2694
2670
|
module = Module.objects.first()
|
|
@@ -2726,29 +2702,19 @@ class ModuleTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
|
2726
2702
|
"data": post_data(assign_ip_form_data),
|
|
2727
2703
|
}
|
|
2728
2704
|
|
|
2729
|
-
with self.subTest("Assert
|
|
2705
|
+
with self.subTest("Assert Cannot assign IPAddress('Add New') without permission"):
|
|
2730
2706
|
# Assert Add new IPAddress
|
|
2731
2707
|
response = self.client.post(**add_new_ip_request, follow=True)
|
|
2732
|
-
|
|
2733
|
-
self.assertHttpStatus(response, 200)
|
|
2708
|
+
self.assertBodyContains(response, f"Interface with id "{self.interfaces[0].pk}" not found")
|
|
2734
2709
|
self.interfaces[0].refresh_from_db()
|
|
2735
2710
|
self.assertEqual(self.interfaces[0].ip_addresses.all().count(), 0)
|
|
2736
|
-
self.assertIn(
|
|
2737
|
-
f"Interface with id "{self.interfaces[0].pk}" not found",
|
|
2738
|
-
response_body,
|
|
2739
|
-
)
|
|
2740
2711
|
|
|
2741
|
-
with self.subTest("Assert
|
|
2712
|
+
with self.subTest("Assert Cannot assign IPAddress(Existing IP) without permission"):
|
|
2742
2713
|
# Assert Assign Exsisting IPAddress
|
|
2743
2714
|
response = self.client.post(**assign_ip_request, follow=True)
|
|
2744
|
-
|
|
2745
|
-
self.assertHttpStatus(response, 200)
|
|
2715
|
+
self.assertBodyContains(response, f"Interface with id "{self.interfaces[1].pk}" not found")
|
|
2746
2716
|
self.interfaces[1].refresh_from_db()
|
|
2747
2717
|
self.assertEqual(self.interfaces[1].ip_addresses.all().count(), 0)
|
|
2748
|
-
self.assertIn(
|
|
2749
|
-
f"Interface with id "{self.interfaces[1].pk}" not found",
|
|
2750
|
-
response_body,
|
|
2751
|
-
)
|
|
2752
2718
|
|
|
2753
2719
|
self.add_permissions("dcim.change_interface", "ipam.view_ipaddress")
|
|
2754
2720
|
|
|
@@ -3228,8 +3194,8 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
|
|
3228
3194
|
invalid_ipaddress_link = reverse("ipam:ipaddress_edit", args=(ipaddress.pk,))
|
|
3229
3195
|
valid_ipaddress_link = ipaddress.get_absolute_url()
|
|
3230
3196
|
response = self.client.get(interface.get_absolute_url() + "?tab=main")
|
|
3231
|
-
|
|
3232
|
-
|
|
3197
|
+
self.assertBodyContains(response, valid_ipaddress_link)
|
|
3198
|
+
response_content = extract_page_body(response.content.decode(response.charset))
|
|
3233
3199
|
self.assertNotIn(invalid_ipaddress_link, response_content)
|
|
3234
3200
|
|
|
3235
3201
|
|
|
@@ -4113,7 +4079,6 @@ class InterfaceConnectionsTestCase(ViewTestCases.ListObjectsViewTestCase):
|
|
|
4113
4079
|
response = self.client.get(f"{self._get_url('list')}?id={instance1.pk}")
|
|
4114
4080
|
self.assertHttpStatus(response, 200)
|
|
4115
4081
|
content = extract_page_body(response.content.decode(response.charset))
|
|
4116
|
-
# TODO: it'd make test failures more readable if we strip the page headers/footers from the content
|
|
4117
4082
|
if hasattr(self.model, "name"):
|
|
4118
4083
|
self.assertIn(instance1.name, content, msg=content)
|
|
4119
4084
|
self.assertNotIn(instance2.name, content, msg=content)
|
|
@@ -4209,11 +4174,11 @@ class VirtualChassisTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
|
4209
4174
|
Interface.objects.create(device=self.devices[2], name="device 2 interface 1", status=interface_status)
|
|
4210
4175
|
Interface.objects.create(device=self.devices[2], name="device 2 interface 2", status=interface_status)
|
|
4211
4176
|
response = self.client.get(reverse("dcim:device_interfaces", kwargs={"pk": self.devices[0].pk}))
|
|
4212
|
-
self.
|
|
4213
|
-
self.
|
|
4214
|
-
self.
|
|
4215
|
-
self.
|
|
4216
|
-
self.
|
|
4177
|
+
self.assertBodyContains(response, 'Interfaces <span class="badge">6</span>')
|
|
4178
|
+
self.assertBodyContains(response, "device 1 interface 1")
|
|
4179
|
+
self.assertBodyContains(response, "device 1 interface 2")
|
|
4180
|
+
self.assertBodyContains(response, "device 2 interface 1")
|
|
4181
|
+
self.assertBodyContains(response, "device 2 interface 2")
|
|
4217
4182
|
|
|
4218
4183
|
def test_device_column_visible(self):
|
|
4219
4184
|
"""
|
|
@@ -4226,7 +4191,7 @@ class VirtualChassisTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
|
4226
4191
|
Interface.objects.create(device=self.devices[0], name="eth0", status=interface_status)
|
|
4227
4192
|
Interface.objects.create(device=self.devices[0], name="eth1", status=interface_status)
|
|
4228
4193
|
response = self.client.get(reverse("dcim:device_interfaces", kwargs={"pk": self.devices[0].pk}))
|
|
4229
|
-
self.
|
|
4194
|
+
self.assertBodyContains(response, "<th>Device</th>", html=True)
|
|
4230
4195
|
|
|
4231
4196
|
def test_device_column_not_visible(self):
|
|
4232
4197
|
"""
|
|
@@ -4239,9 +4204,9 @@ class VirtualChassisTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
|
4239
4204
|
Interface.objects.create(device=self.devices[1], name="eth2", status=interface_status)
|
|
4240
4205
|
Interface.objects.create(device=self.devices[1], name="eth3", status=interface_status)
|
|
4241
4206
|
response = self.client.get(reverse("dcim:device_interfaces", kwargs={"pk": self.devices[1].pk}))
|
|
4242
|
-
self.assertNotIn("<th >Device</th>",
|
|
4207
|
+
self.assertNotIn("<th >Device</th>", extract_page_body(response.content.decode(response.charset)))
|
|
4243
4208
|
# Sanity check:
|
|
4244
|
-
self.
|
|
4209
|
+
self.assertBodyContains(response, "<th>Name</th>", html=True)
|
|
4245
4210
|
|
|
4246
4211
|
|
|
4247
4212
|
class PowerPanelTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
@@ -4408,9 +4373,7 @@ class PathTraceViewTestCase(ModelViewTestCase):
|
|
|
4408
4373
|
url = reverse("dcim:rearport_trace", args=[obj.pk])
|
|
4409
4374
|
cablepath_id = CablePath.objects.first().id
|
|
4410
4375
|
response = self.client.get(url + f"?cablepath_id={cablepath_id}")
|
|
4411
|
-
self.
|
|
4412
|
-
content = extract_page_body(response.content.decode(response.charset))
|
|
4413
|
-
self.assertInHTML("<h1>Cable Trace for Rear Port Rear Port 1</h1>", content)
|
|
4376
|
+
self.assertBodyContains(response, "<h1>Cable Trace for Rear Port Rear Port 1</h1>", html=True)
|
|
4414
4377
|
|
|
4415
4378
|
|
|
4416
4379
|
class DeviceRedundancyGroupTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
@@ -4621,10 +4584,8 @@ class SoftwareImageFileTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
|
4621
4584
|
"_confirm": True, # Form button
|
|
4622
4585
|
}
|
|
4623
4586
|
response = self.client.post(self._get_url("bulk_delete"), data, follow=True)
|
|
4624
|
-
self.assertHttpStatus(response, 200)
|
|
4625
|
-
response_body = response.content.decode(response.charset)
|
|
4626
4587
|
# Assert protected error message included in the response body
|
|
4627
|
-
self.
|
|
4588
|
+
self.assertBodyContains(response, f"<span>{device_type_to_software_image_file}</span>", html=True)
|
|
4628
4589
|
|
|
4629
4590
|
|
|
4630
4591
|
class SoftwareVersionTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
@@ -4701,10 +4662,8 @@ class SoftwareVersionTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
|
4701
4662
|
"_confirm": True, # Form button
|
|
4702
4663
|
}
|
|
4703
4664
|
response = self.client.post(self._get_url("bulk_delete"), data, follow=True)
|
|
4704
|
-
self.assertHttpStatus(response, 200)
|
|
4705
|
-
response_body = response.content.decode(response.charset)
|
|
4706
4665
|
# Assert protected error message included in the response body
|
|
4707
|
-
self.
|
|
4666
|
+
self.assertBodyContains(response, f"<span>{device_type_to_software_image_file}</span>", html=True)
|
|
4708
4667
|
|
|
4709
4668
|
|
|
4710
4669
|
class ControllerTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
@@ -177,7 +177,12 @@ def ensure_git_repository(repository_record, logger=None, head=None): # pylint:
|
|
|
177
177
|
if logger:
|
|
178
178
|
if changed:
|
|
179
179
|
logger.info("Repository successfully refreshed")
|
|
180
|
-
logger.info(
|
|
180
|
+
logger.info(
|
|
181
|
+
'%s: the current Git repository hash is "%s"',
|
|
182
|
+
repository_record.name,
|
|
183
|
+
repository_record.current_head,
|
|
184
|
+
extra={"object": repository_record},
|
|
185
|
+
)
|
|
181
186
|
|
|
182
187
|
return changed
|
|
183
188
|
|
|
@@ -10,6 +10,9 @@ def set_dynamic_group_group_types(apps, schema_editor):
|
|
|
10
10
|
# The group_type field defaults to TYPE_DYNAMIC_FILTER
|
|
11
11
|
# There are no preexisting TYPE_STATIC groups as that's a new feature
|
|
12
12
|
# Any group that has children should be converted to TYPE_DYNAMIC_SET
|
|
13
|
+
# NOTE: The below is actually incorrect (see migration 0116) as for some reason, during migrations ONLY,
|
|
14
|
+
# Django somehow swaps the `parent` and `children` relations on DynamicGroup such that the below actually detects
|
|
15
|
+
# the opposite set of groups from what would be expected.
|
|
13
16
|
DynamicGroup.objects.filter(children__isnull=False).distinct().update(
|
|
14
17
|
group_type=DynamicGroupTypeChoices.TYPE_DYNAMIC_SET
|
|
15
18
|
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from django.db import migrations
|
|
2
|
+
|
|
3
|
+
from nautobot.extras.utils import fixup_dynamic_group_group_types
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
dependencies = [
|
|
8
|
+
("extras", "0115_scheduledjob_time_zone"),
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
operations = [
|
|
12
|
+
migrations.RunPython(
|
|
13
|
+
code=fixup_dynamic_group_group_types,
|
|
14
|
+
reverse_code=migrations.operations.special.RunPython.noop,
|
|
15
|
+
),
|
|
16
|
+
]
|
|
@@ -15,7 +15,7 @@ from nautobot.core.models.fields import slugify_dashes_to_underscores
|
|
|
15
15
|
from nautobot.core.tables import CustomFieldColumn
|
|
16
16
|
from nautobot.core.testing import APITestCase, TestCase, TransactionTestCase
|
|
17
17
|
from nautobot.core.testing.models import ModelTestCases
|
|
18
|
-
from nautobot.core.testing.utils import post_data
|
|
18
|
+
from nautobot.core.testing.utils import extract_page_body, post_data
|
|
19
19
|
from nautobot.core.utils.lookup import get_changes_for_model
|
|
20
20
|
from nautobot.dcim.filters import LocationFilterSet
|
|
21
21
|
from nautobot.dcim.forms import RackFilterForm
|
|
@@ -995,18 +995,15 @@ class CustomFieldDataAPITest(APITestCase):
|
|
|
995
995
|
},
|
|
996
996
|
}
|
|
997
997
|
response = self.client.post(self.list_url, data, format="json", **self.header)
|
|
998
|
-
self.
|
|
999
|
-
self.assertIn("Value must be a string", str(response.content))
|
|
998
|
+
self.assertContains(response, "Value must be a string", status_code=status.HTTP_400_BAD_REQUEST)
|
|
1000
999
|
|
|
1001
1000
|
data["custom_fields"].update({self.cf_text.key: 2})
|
|
1002
1001
|
response = self.client.post(self.list_url, data, format="json", **self.header)
|
|
1003
|
-
self.
|
|
1004
|
-
self.assertIn("Value must be a string", str(response.content))
|
|
1002
|
+
self.assertContains(response, "Value must be a string", status_code=status.HTTP_400_BAD_REQUEST)
|
|
1005
1003
|
|
|
1006
1004
|
data["custom_fields"].update({self.cf_text.key: True})
|
|
1007
1005
|
response = self.client.post(self.list_url, data, format="json", **self.header)
|
|
1008
|
-
self.
|
|
1009
|
-
self.assertIn("Value must be a string", str(response.content))
|
|
1006
|
+
self.assertContains(response, "Value must be a string", status_code=status.HTTP_400_BAD_REQUEST)
|
|
1010
1007
|
|
|
1011
1008
|
def test_create_without_required_field(self):
|
|
1012
1009
|
self.cf_text.default = None
|
|
@@ -1019,8 +1016,7 @@ class CustomFieldDataAPITest(APITestCase):
|
|
|
1019
1016
|
"status": self.statuses[0].pk,
|
|
1020
1017
|
}
|
|
1021
1018
|
response = self.client.post(self.list_url, data, format="json", **self.header)
|
|
1022
|
-
self.
|
|
1023
|
-
self.assertIn("Required field cannot be empty", str(response.content))
|
|
1019
|
+
self.assertContains(response, "Required field cannot be empty", status_code=status.HTTP_400_BAD_REQUEST)
|
|
1024
1020
|
|
|
1025
1021
|
# Try in CSV format too
|
|
1026
1022
|
csvdata = "\n".join(
|
|
@@ -1030,8 +1026,7 @@ class CustomFieldDataAPITest(APITestCase):
|
|
|
1030
1026
|
]
|
|
1031
1027
|
)
|
|
1032
1028
|
response = self.client.post(self.list_url, csvdata, content_type="text/csv", **self.header)
|
|
1033
|
-
self.
|
|
1034
|
-
self.assertIn("Required field cannot be empty", str(response.content))
|
|
1029
|
+
self.assertContains(response, "Required field cannot be empty", status_code=status.HTTP_400_BAD_REQUEST)
|
|
1035
1030
|
|
|
1036
1031
|
def test_create_invalid_select_choice(self):
|
|
1037
1032
|
data = {
|
|
@@ -1043,8 +1038,7 @@ class CustomFieldDataAPITest(APITestCase):
|
|
|
1043
1038
|
},
|
|
1044
1039
|
}
|
|
1045
1040
|
response = self.client.post(self.list_url, data, format="json", **self.header)
|
|
1046
|
-
self.
|
|
1047
|
-
self.assertIn("Invalid choice", str(response.content))
|
|
1041
|
+
self.assertContains(response, "Invalid choice", status_code=status.HTTP_400_BAD_REQUEST)
|
|
1048
1042
|
|
|
1049
1043
|
# Try in CSV format too
|
|
1050
1044
|
csvdata = "\n".join(
|
|
@@ -1054,8 +1048,7 @@ class CustomFieldDataAPITest(APITestCase):
|
|
|
1054
1048
|
]
|
|
1055
1049
|
)
|
|
1056
1050
|
response = self.client.post(self.list_url, csvdata, content_type="text/csv", **self.header)
|
|
1057
|
-
self.
|
|
1058
|
-
self.assertIn("Invalid choice", str(response.content))
|
|
1051
|
+
self.assertContains(response, "Invalid choice", status_code=status.HTTP_400_BAD_REQUEST)
|
|
1059
1052
|
|
|
1060
1053
|
|
|
1061
1054
|
class CustomFieldImportTest(TestCase):
|
|
@@ -1157,7 +1150,7 @@ class CustomFieldImportTest(TestCase):
|
|
|
1157
1150
|
try:
|
|
1158
1151
|
location1 = Location.objects.get(name="Location 1")
|
|
1159
1152
|
except Location.DoesNotExist:
|
|
1160
|
-
self.fail(
|
|
1153
|
+
self.fail(extract_page_body(response.content.decode(response.charset)))
|
|
1161
1154
|
self.assertEqual(len(location1.cf), 8)
|
|
1162
1155
|
self.assertEqual(location1.cf["text"], "ABC")
|
|
1163
1156
|
self.assertEqual(location1.cf["integer"], 123)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import random
|
|
2
2
|
|
|
3
|
+
from django.apps import apps
|
|
3
4
|
from django.contrib.contenttypes.models import ContentType
|
|
4
5
|
from django.core.exceptions import ValidationError
|
|
5
6
|
from django.db.models import ProtectedError, QuerySet
|
|
@@ -40,6 +41,7 @@ from nautobot.extras.models import (
|
|
|
40
41
|
Status,
|
|
41
42
|
Tag,
|
|
42
43
|
)
|
|
44
|
+
from nautobot.extras.utils import fixup_dynamic_group_group_types
|
|
43
45
|
from nautobot.ipam.models import IPAddress, Prefix
|
|
44
46
|
from nautobot.ipam.querysets import PrefixQuerySet
|
|
45
47
|
from nautobot.tenancy.models import Tenant
|
|
@@ -1224,3 +1226,117 @@ class DynamicGroupMembershipFilterTest(DynamicGroupTestBase, FilterTestCases.Fil
|
|
|
1224
1226
|
for value, cnt in tests.items():
|
|
1225
1227
|
params = {"q": value}
|
|
1226
1228
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), cnt)
|
|
1229
|
+
|
|
1230
|
+
|
|
1231
|
+
class DynamicGroupFixupTestCase(TestCase):
|
|
1232
|
+
"""Check for the correct functioning of the fixup_dynamic_group_group_types() data migration helper function."""
|
|
1233
|
+
|
|
1234
|
+
def test_fixup_dynamic_group_group_types(self):
|
|
1235
|
+
device_ct = ContentType.objects.get_for_model(Device)
|
|
1236
|
+
|
|
1237
|
+
good_grandparent_group = DynamicGroup.objects.create(
|
|
1238
|
+
name="Good Grandparent",
|
|
1239
|
+
group_type=DynamicGroupTypeChoices.TYPE_DYNAMIC_SET,
|
|
1240
|
+
content_type=device_ct,
|
|
1241
|
+
)
|
|
1242
|
+
bad_grandparent_group = DynamicGroup.objects.create(
|
|
1243
|
+
name="Bad Grandparent",
|
|
1244
|
+
group_type=DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER, # wrong, but possible due to #6329
|
|
1245
|
+
content_type=device_ct,
|
|
1246
|
+
)
|
|
1247
|
+
good_parent_group = DynamicGroup.objects.create(
|
|
1248
|
+
name="Good Parent",
|
|
1249
|
+
group_type=DynamicGroupTypeChoices.TYPE_DYNAMIC_SET,
|
|
1250
|
+
content_type=device_ct,
|
|
1251
|
+
)
|
|
1252
|
+
bad_parent_group = DynamicGroup.objects.create(
|
|
1253
|
+
name="Bad Parent",
|
|
1254
|
+
group_type=DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER, # wrong, #6329 again
|
|
1255
|
+
content_type=device_ct,
|
|
1256
|
+
)
|
|
1257
|
+
good_child_group = DynamicGroup.objects.create(
|
|
1258
|
+
name="Good Child",
|
|
1259
|
+
group_type=DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER,
|
|
1260
|
+
content_type=device_ct,
|
|
1261
|
+
filter={"status": [Status.objects.get_for_model(Device).first().name]},
|
|
1262
|
+
)
|
|
1263
|
+
bad_child_group = DynamicGroup.objects.create(
|
|
1264
|
+
name="Bad Child",
|
|
1265
|
+
group_type=DynamicGroupTypeChoices.TYPE_DYNAMIC_SET, # wrong, #6329 again
|
|
1266
|
+
content_type=device_ct,
|
|
1267
|
+
filter={"status": [Status.objects.get_for_model(Device).first().name]},
|
|
1268
|
+
)
|
|
1269
|
+
|
|
1270
|
+
DynamicGroupMembership.objects.create(
|
|
1271
|
+
parent_group=good_grandparent_group,
|
|
1272
|
+
group=good_parent_group,
|
|
1273
|
+
weight=10,
|
|
1274
|
+
operator=DynamicGroupOperatorChoices.OPERATOR_INTERSECTION,
|
|
1275
|
+
)
|
|
1276
|
+
DynamicGroupMembership.objects.create(
|
|
1277
|
+
parent_group=bad_grandparent_group,
|
|
1278
|
+
group=bad_parent_group,
|
|
1279
|
+
weight=10,
|
|
1280
|
+
operator=DynamicGroupOperatorChoices.OPERATOR_INTERSECTION,
|
|
1281
|
+
)
|
|
1282
|
+
DynamicGroupMembership.objects.create(
|
|
1283
|
+
parent_group=good_parent_group,
|
|
1284
|
+
group=good_child_group,
|
|
1285
|
+
weight=10,
|
|
1286
|
+
operator=DynamicGroupOperatorChoices.OPERATOR_INTERSECTION,
|
|
1287
|
+
)
|
|
1288
|
+
DynamicGroupMembership.objects.create(
|
|
1289
|
+
parent_group=bad_parent_group,
|
|
1290
|
+
group=bad_child_group,
|
|
1291
|
+
weight=10,
|
|
1292
|
+
operator=DynamicGroupOperatorChoices.OPERATOR_INTERSECTION,
|
|
1293
|
+
)
|
|
1294
|
+
|
|
1295
|
+
good_standalone_group_1 = DynamicGroup.objects.create(
|
|
1296
|
+
name="Good Standalone Group 1",
|
|
1297
|
+
group_type=DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER,
|
|
1298
|
+
content_type=device_ct,
|
|
1299
|
+
# empty filter - this is OK!
|
|
1300
|
+
)
|
|
1301
|
+
good_standalone_group_2 = DynamicGroup.objects.create(
|
|
1302
|
+
name="Good Standalone Group 2",
|
|
1303
|
+
group_type=DynamicGroupTypeChoices.TYPE_DYNAMIC_SET,
|
|
1304
|
+
content_type=device_ct,
|
|
1305
|
+
)
|
|
1306
|
+
bad_standalone_group = DynamicGroup.objects.create(
|
|
1307
|
+
name="Bad Standalone Group",
|
|
1308
|
+
group_type=DynamicGroupTypeChoices.TYPE_DYNAMIC_SET,
|
|
1309
|
+
content_type=device_ct,
|
|
1310
|
+
filter={"status": [Status.objects.get_for_model(Device).first().name]},
|
|
1311
|
+
)
|
|
1312
|
+
|
|
1313
|
+
# DynamicGroupMembership.save() will actually auto-fixup the type on bad_parent_group and bad_grandparent_group.
|
|
1314
|
+
# Make them wrong again:
|
|
1315
|
+
bad_grandparent_group.group_type = DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER
|
|
1316
|
+
bad_grandparent_group.save()
|
|
1317
|
+
bad_parent_group.group_type = DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER
|
|
1318
|
+
bad_parent_group.save()
|
|
1319
|
+
|
|
1320
|
+
count_1, count_2 = fixup_dynamic_group_group_types(apps)
|
|
1321
|
+
|
|
1322
|
+
self.assertEqual(count_1, 2) # bad_grandparent_group, bad_parent_group
|
|
1323
|
+
self.assertEqual(count_2, 2) # bad_child_group, bad_standalone_group
|
|
1324
|
+
|
|
1325
|
+
good_grandparent_group.refresh_from_db()
|
|
1326
|
+
self.assertEqual(good_grandparent_group.group_type, DynamicGroupTypeChoices.TYPE_DYNAMIC_SET) # unchanged
|
|
1327
|
+
bad_grandparent_group.refresh_from_db()
|
|
1328
|
+
self.assertEqual(bad_grandparent_group.group_type, DynamicGroupTypeChoices.TYPE_DYNAMIC_SET) # fixed
|
|
1329
|
+
good_parent_group.refresh_from_db()
|
|
1330
|
+
self.assertEqual(good_parent_group.group_type, DynamicGroupTypeChoices.TYPE_DYNAMIC_SET) # unchanged
|
|
1331
|
+
bad_parent_group.refresh_from_db()
|
|
1332
|
+
self.assertEqual(bad_parent_group.group_type, DynamicGroupTypeChoices.TYPE_DYNAMIC_SET) # fixed
|
|
1333
|
+
good_child_group.refresh_from_db()
|
|
1334
|
+
self.assertEqual(good_child_group.group_type, DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER) # unchanged
|
|
1335
|
+
bad_child_group.refresh_from_db()
|
|
1336
|
+
self.assertEqual(bad_child_group.group_type, DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER) # fixed
|
|
1337
|
+
good_standalone_group_1.refresh_from_db()
|
|
1338
|
+
self.assertEqual(good_standalone_group_1.group_type, DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER) # unchanged
|
|
1339
|
+
good_standalone_group_2.refresh_from_db()
|
|
1340
|
+
self.assertEqual(good_standalone_group_2.group_type, DynamicGroupTypeChoices.TYPE_DYNAMIC_SET) # unchanged
|
|
1341
|
+
bad_standalone_group.refresh_from_db()
|
|
1342
|
+
self.assertEqual(bad_standalone_group.group_type, DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER) # fixed
|
|
@@ -694,17 +694,15 @@ class TestAppCoreViewOverrides(TestCase):
|
|
|
694
694
|
|
|
695
695
|
def test_views_are_overridden(self):
|
|
696
696
|
response = self.client.get(reverse("plugins:example_app:view_to_be_overridden"))
|
|
697
|
-
self.assertEqual(
|
|
697
|
+
self.assertEqual("Hello world! I'm an overridden view.", response.content.decode(response.charset))
|
|
698
698
|
|
|
699
699
|
response = self.client.get(
|
|
700
700
|
f'{reverse("plugins:plugin_detail", kwargs={"plugin": "example_app_with_view_override"})}'
|
|
701
701
|
)
|
|
702
702
|
self.assertIn(
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
),
|
|
707
|
-
response.content,
|
|
703
|
+
"plugins:example_app:view_to_be_overridden <code>"
|
|
704
|
+
"example_app_with_view_override.views.ViewOverride</code>",
|
|
705
|
+
extract_page_body(response.content.decode(response.charset)),
|
|
708
706
|
)
|
|
709
707
|
|
|
710
708
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from unittest import mock
|
|
2
2
|
|
|
3
|
+
from django.core.cache import cache
|
|
4
|
+
|
|
3
5
|
from nautobot.core.testing import TestCase
|
|
4
6
|
from nautobot.extras.registry import registry
|
|
5
7
|
from nautobot.extras.utils import get_celery_queues, get_worker_count, populate_model_features_registry
|
|
@@ -17,6 +19,7 @@ class UtilsTestCase(TestCase):
|
|
|
17
19
|
self.assertDictEqual(get_celery_queues(), {"queue1": 1})
|
|
18
20
|
|
|
19
21
|
with self.subTest("2 workers 2 shared queues"):
|
|
22
|
+
cache.clear()
|
|
20
23
|
mock_active_queues.return_value = {
|
|
21
24
|
"celery@worker1": [{"name": "queue1"}, {"name": "queue2"}],
|
|
22
25
|
"celery@worker2": [{"name": "queue1"}, {"name": "queue2"}],
|
|
@@ -24,6 +27,7 @@ class UtilsTestCase(TestCase):
|
|
|
24
27
|
self.assertDictEqual(get_celery_queues(), {"queue1": 2, "queue2": 2})
|
|
25
28
|
|
|
26
29
|
with self.subTest("2 workers 2 individual queues"):
|
|
30
|
+
cache.clear()
|
|
27
31
|
mock_active_queues.return_value = {
|
|
28
32
|
"celery@worker1": [{"name": "queue1"}],
|
|
29
33
|
"celery@worker2": [{"name": "queue2"}],
|
|
@@ -31,6 +35,7 @@ class UtilsTestCase(TestCase):
|
|
|
31
35
|
self.assertDictEqual(get_celery_queues(), {"queue1": 1, "queue2": 1})
|
|
32
36
|
|
|
33
37
|
with self.subTest("2 workers 3 queues"):
|
|
38
|
+
cache.clear()
|
|
34
39
|
mock_active_queues.return_value = {
|
|
35
40
|
"celery@worker1": [{"name": "queue1"}, {"name": "queue3"}],
|
|
36
41
|
"celery@worker2": [{"name": "queue2"}],
|