nautobot 2.3.5__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/utils.py +12 -2
- nautobot/core/api/views.py +15 -3
- nautobot/core/forms/fields.py +5 -2
- nautobot/core/forms/utils.py +31 -6
- nautobot/core/models/fields.py +56 -0
- 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_utils.py +83 -0
- nautobot/core/tests/test_views.py +39 -56
- nautobot/core/views/__init__.py +27 -11
- nautobot/dcim/tests/test_api.py +4 -1
- nautobot/dcim/tests/test_views.py +26 -67
- nautobot/extras/datasources/git.py +6 -1
- nautobot/extras/factory.py +2 -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/models/models.py +2 -0
- nautobot/extras/tests/test_api.py +3 -3
- nautobot/extras/tests/test_customfields.py +9 -16
- nautobot/extras/tests/test_dynamicgroups.py +116 -0
- nautobot/extras/tests/test_forms.py +2 -0
- nautobot/extras/tests/test_plugins.py +4 -6
- nautobot/extras/tests/test_utils.py +5 -0
- nautobot/extras/tests/test_views.py +63 -161
- nautobot/extras/utils.py +50 -11
- nautobot/ipam/api/serializers.py +30 -1
- nautobot/ipam/api/views.py +165 -3
- nautobot/ipam/filters.py +1 -1
- nautobot/ipam/forms.py +2 -0
- nautobot/ipam/migrations/0050_vlangroup_range.py +24 -0
- nautobot/ipam/models.py +51 -8
- nautobot/ipam/tables.py +4 -4
- nautobot/ipam/templates/ipam/vlangroup.html +4 -0
- nautobot/ipam/tests/test_api.py +192 -12
- nautobot/ipam/tests/test_models.py +35 -1
- nautobot/ipam/tests/test_utils.py +61 -0
- nautobot/ipam/tests/test_views.py +8 -15
- nautobot/ipam/utils/__init__.py +10 -17
- nautobot/ipam/views.py +1 -1
- 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 +5 -5
- 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 +5 -5
- 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 +392 -95
- 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 +308 -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.5.dist-info → nautobot-2.3.7.dist-info}/METADATA +2 -1
- {nautobot-2.3.5.dist-info → nautobot-2.3.7.dist-info}/RECORD +334 -330
- {nautobot-2.3.5.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.5.dist-info → nautobot-2.3.7.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.3.5.dist-info → nautobot-2.3.7.dist-info}/NOTICE +0 -0
- {nautobot-2.3.5.dist-info → nautobot-2.3.7.dist-info}/entry_points.txt +0 -0
nautobot/__init__.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from importlib import metadata
|
|
2
2
|
import logging
|
|
3
3
|
import os
|
|
4
|
+
import sys
|
|
4
5
|
|
|
5
6
|
import django
|
|
6
7
|
|
|
@@ -28,8 +29,9 @@ def setup(config_path=None):
|
|
|
28
29
|
# Point Django to our 'nautobot_config' pseudo-module that we'll load from the provided config path
|
|
29
30
|
os.environ["DJANGO_SETTINGS_MODULE"] = "nautobot_config"
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
if "nautobot_config" not in sys.modules:
|
|
33
|
+
load_settings(config_path)
|
|
32
34
|
django.setup()
|
|
33
35
|
|
|
34
|
-
logger.info("Nautobot initialized!")
|
|
36
|
+
logger.info("Nautobot %s initialized!", __version__)
|
|
35
37
|
__initialized = True
|
|
@@ -10,7 +10,7 @@ from nautobot.circuits.models import (
|
|
|
10
10
|
Provider,
|
|
11
11
|
ProviderNetwork,
|
|
12
12
|
)
|
|
13
|
-
from nautobot.core.testing import post_data, TestCase as NautobotTestCase, ViewTestCases
|
|
13
|
+
from nautobot.core.testing import post_data, TestCase as NautobotTestCase, utils, ViewTestCases
|
|
14
14
|
from nautobot.extras.models import Status, Tag
|
|
15
15
|
|
|
16
16
|
|
|
@@ -176,13 +176,12 @@ class CircuitTerminationTestCase(
|
|
|
176
176
|
|
|
177
177
|
# Visit the termination detail page and assert responses:
|
|
178
178
|
response = self.client.get(reverse("circuits:circuittermination", kwargs={"pk": termination.pk}))
|
|
179
|
-
self.
|
|
180
|
-
self.
|
|
181
|
-
self.assertNotIn("</span> Connect", str(response.content))
|
|
179
|
+
self.assertBodyContains(response, "Test Provider Network")
|
|
180
|
+
self.assertNotIn("</span> Connect", utils.extract_page_body(response.content.decode(response.charset)))
|
|
182
181
|
|
|
183
182
|
# Visit the circuit object detail page and check there is no connect button present:
|
|
184
183
|
response = self.client.get(reverse("circuits:circuit", kwargs={"pk": circuit.pk}))
|
|
185
|
-
self.assertNotIn("</span> Connect",
|
|
184
|
+
self.assertNotIn("</span> Connect", utils.extract_page_body(response.content.decode(response.charset)))
|
|
186
185
|
|
|
187
186
|
|
|
188
187
|
class CircuitSwapTerminationsTestCase(NautobotTestCase):
|
nautobot/core/api/utils.py
CHANGED
|
@@ -116,7 +116,7 @@ def get_serializer_for_model(model, prefix=""):
|
|
|
116
116
|
return dynamic_import(serializer_name)
|
|
117
117
|
except AttributeError as exc:
|
|
118
118
|
raise exceptions.SerializerNotFound(
|
|
119
|
-
f"
|
|
119
|
+
f"Serializer for {app_label}.{model_name} not found, expected it at {serializer_name}"
|
|
120
120
|
) from exc
|
|
121
121
|
|
|
122
122
|
|
|
@@ -129,16 +129,26 @@ def nested_serializers_for_models(models, prefix=""):
|
|
|
129
129
|
|
|
130
130
|
Used exclusively in OpenAPI schema generation.
|
|
131
131
|
"""
|
|
132
|
+
from nautobot.core.api.serializers import BaseModelSerializer # avoid circular import
|
|
133
|
+
|
|
132
134
|
serializer_classes = []
|
|
133
135
|
for model in models:
|
|
134
136
|
try:
|
|
135
137
|
serializer_classes.append(get_serializer_for_model(model, prefix=prefix))
|
|
136
138
|
except exceptions.SerializerNotFound as exc:
|
|
137
|
-
logger.
|
|
139
|
+
logger.warning("%s", exc)
|
|
138
140
|
continue
|
|
139
141
|
|
|
140
142
|
nested_serializer_classes = []
|
|
141
143
|
for serializer_class in serializer_classes:
|
|
144
|
+
if not issubclass(serializer_class, BaseModelSerializer):
|
|
145
|
+
logger.warning(
|
|
146
|
+
"Serializer class %s.%s does not inherit from nautobot.apps.api.BaseModelSerializer. "
|
|
147
|
+
"This should probably be corrected.",
|
|
148
|
+
serializer_class.__module__,
|
|
149
|
+
serializer_class.__name__,
|
|
150
|
+
)
|
|
151
|
+
continue
|
|
142
152
|
nested_serializer_name = f"Nested{serializer_class.__name__}"
|
|
143
153
|
if nested_serializer_name in NESTED_SERIALIZER_CACHE:
|
|
144
154
|
nested_serializer_classes.append(NESTED_SERIALIZER_CACHE[nested_serializer_name])
|
nautobot/core/api/views.py
CHANGED
|
@@ -24,6 +24,7 @@ from graphql import get_default_backend
|
|
|
24
24
|
from graphql.execution import ExecutionResult
|
|
25
25
|
from graphql.execution.middleware import MiddlewareManager
|
|
26
26
|
from graphql.type.schema import GraphQLSchema
|
|
27
|
+
import redis.exceptions
|
|
27
28
|
from rest_framework import routers, status
|
|
28
29
|
from rest_framework.exceptions import ParseError, PermissionDenied
|
|
29
30
|
from rest_framework.permissions import IsAuthenticated
|
|
@@ -58,6 +59,10 @@ HTTP_ACTIONS = {
|
|
|
58
59
|
"DELETE": "delete",
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
|
|
63
|
+
logger = logging.getLogger(__name__)
|
|
64
|
+
|
|
65
|
+
|
|
61
66
|
#
|
|
62
67
|
# Mixins
|
|
63
68
|
#
|
|
@@ -467,7 +472,16 @@ class StatusView(NautobotAPIVersionMixin, APIView):
|
|
|
467
472
|
nautobot_apps = dict(sorted(nautobot_apps.items()))
|
|
468
473
|
|
|
469
474
|
# Gather Celery workers
|
|
470
|
-
|
|
475
|
+
try:
|
|
476
|
+
workers = celery_app.control.inspect().active() # list or None
|
|
477
|
+
except redis.exceptions.ConnectionError:
|
|
478
|
+
# Celery seems to be not smart enough to auto-retry on intermittent failures, so let's do it ourselves:
|
|
479
|
+
try:
|
|
480
|
+
workers = celery_app.control.inspect().active() # list or None
|
|
481
|
+
except redis.exceptions.ConnectionError as err:
|
|
482
|
+
logger.error("Repeated ConnectionError from Celery/Redis: %s", err)
|
|
483
|
+
workers = None
|
|
484
|
+
|
|
471
485
|
worker_count = len(workers) if workers is not None else 0
|
|
472
486
|
|
|
473
487
|
return Response(
|
|
@@ -872,7 +886,6 @@ class GetObjectCountsView(NautobotAPIVersionMixin, APIView):
|
|
|
872
886
|
try:
|
|
873
887
|
data["url"] = django_reverse(get_route_for_model(model, "list"))
|
|
874
888
|
except NoReverseMatch:
|
|
875
|
-
logger = logging.getLogger(__name__)
|
|
876
889
|
route = get_route_for_model(model, "list")
|
|
877
890
|
logger.warning(f"Handled expected exception when generating filter field: {route}")
|
|
878
891
|
manager = model.objects
|
|
@@ -975,7 +988,6 @@ class GetFilterSetFieldDOMElementAPIView(NautobotAPIVersionMixin, APIView):
|
|
|
975
988
|
# Cant determine the exceptions to handle because any exception could be raised,
|
|
976
989
|
# e.g InterfaceForm would raise a ObjectDoesNotExist Error since no device was provided
|
|
977
990
|
# While other forms might raise other errors, also if model_form is None a TypeError would be raised.
|
|
978
|
-
logger = logging.getLogger(__name__)
|
|
979
991
|
logger.debug(f"Handled expected exception when generating filter field: {err}")
|
|
980
992
|
|
|
981
993
|
# Create a temporary form and get a BoundField for the specified field
|
nautobot/core/forms/fields.py
CHANGED
|
@@ -719,8 +719,11 @@ class NumericArrayField(SimpleArrayField):
|
|
|
719
719
|
|
|
720
720
|
def to_python(self, value):
|
|
721
721
|
try:
|
|
722
|
-
|
|
723
|
-
|
|
722
|
+
if not value:
|
|
723
|
+
value = ""
|
|
724
|
+
else:
|
|
725
|
+
value = ",".join([str(n) for n in forms.parse_numeric_range(value)])
|
|
726
|
+
except (TypeError, ValueError) as error:
|
|
724
727
|
raise ValidationError(error)
|
|
725
728
|
return super().to_python(value)
|
|
726
729
|
|
nautobot/core/forms/utils.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from itertools import groupby
|
|
1
2
|
import re
|
|
2
3
|
|
|
3
4
|
from django import forms as django_forms
|
|
@@ -18,24 +19,31 @@ __all__ = (
|
|
|
18
19
|
)
|
|
19
20
|
|
|
20
21
|
|
|
21
|
-
def parse_numeric_range(
|
|
22
|
+
def parse_numeric_range(input_string, base=10):
|
|
22
23
|
"""
|
|
23
|
-
Expand a numeric range (continuous or not) into a decimal or
|
|
24
|
+
Expand a numeric range (continuous or not) into a sorted decimal or
|
|
24
25
|
hexadecimal list, as specified by the base parameter
|
|
25
26
|
'0-3,5' => [0, 1, 2, 3, 5]
|
|
26
27
|
'2,8-b,d,f' => [2, 8, 9, a, b, d, f]
|
|
27
28
|
"""
|
|
29
|
+
if base not in [10, 16]:
|
|
30
|
+
raise TypeError("Invalid base value.")
|
|
31
|
+
|
|
32
|
+
if not isinstance(input_string, str) or not input_string:
|
|
33
|
+
raise TypeError("Input value must be a string using a range format.")
|
|
34
|
+
|
|
28
35
|
values = []
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
for dash_range in string.split(","):
|
|
36
|
+
|
|
37
|
+
for dash_range in input_string.split(","):
|
|
32
38
|
try:
|
|
33
39
|
begin, end = dash_range.split("-")
|
|
40
|
+
if begin == "" or end == "":
|
|
41
|
+
raise TypeError("Input value must be a string using a range format.")
|
|
34
42
|
except ValueError:
|
|
35
43
|
begin, end = dash_range, dash_range
|
|
36
44
|
begin, end = int(begin.strip(), base=base), int(end.strip(), base=base) + 1
|
|
37
45
|
values.extend(range(begin, end))
|
|
38
|
-
return list(set(values))
|
|
46
|
+
return sorted(list(set(values)))
|
|
39
47
|
|
|
40
48
|
|
|
41
49
|
def parse_alphanumeric_range(string):
|
|
@@ -150,3 +158,20 @@ def add_field_to_filter_form_class(form_class, field_name, field_obj):
|
|
|
150
158
|
f"There was a conflict with filter form field `{field_name}`, the custom filter form field was ignored."
|
|
151
159
|
)
|
|
152
160
|
form_class.base_fields[field_name] = field_obj
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def compress_range(iterable):
|
|
164
|
+
"""
|
|
165
|
+
Generates compressed range from an un-sorted expanded range.
|
|
166
|
+
For example:
|
|
167
|
+
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 100, 101, 102, 103, 104, 105, 1000, 1100, 1101, 1102, 1103, 1104, 1105, 1106]
|
|
168
|
+
=>
|
|
169
|
+
iter1: (1, 10)
|
|
170
|
+
iter2: (100, 105)
|
|
171
|
+
iter3: (1000, 1000)
|
|
172
|
+
iter4: (1100, 1106)
|
|
173
|
+
"""
|
|
174
|
+
iterable = sorted(set(iterable))
|
|
175
|
+
for _, grp in groupby(enumerate(iterable), lambda t: t[1] - t[0]):
|
|
176
|
+
grp = list(grp)
|
|
177
|
+
yield grp[0][1], grp[-1][1]
|
nautobot/core/models/fields.py
CHANGED
|
@@ -4,6 +4,7 @@ import re
|
|
|
4
4
|
from django.core import exceptions
|
|
5
5
|
from django.core.validators import MaxLengthValidator, RegexValidator
|
|
6
6
|
from django.db import models
|
|
7
|
+
from django.forms import TextInput
|
|
7
8
|
from django.utils.text import slugify
|
|
8
9
|
from django_extensions.db.fields import AutoSlugField as _AutoSlugField
|
|
9
10
|
from netaddr import AddrFormatError, EUI, mac_unix_expanded
|
|
@@ -11,6 +12,7 @@ from taggit.managers import TaggableManager
|
|
|
11
12
|
|
|
12
13
|
from nautobot.core.constants import CHARFIELD_MAX_LENGTH
|
|
13
14
|
from nautobot.core.forms import fields, widgets
|
|
15
|
+
from nautobot.core.forms.utils import compress_range, parse_numeric_range
|
|
14
16
|
from nautobot.core.models import ordering
|
|
15
17
|
from nautobot.core.models.managers import TagsManager
|
|
16
18
|
from nautobot.core.models.validators import EnhancedURLValidator
|
|
@@ -415,3 +417,57 @@ class TagsField(TaggableManager):
|
|
|
415
417
|
kwargs.setdefault("required", False)
|
|
416
418
|
kwargs.setdefault("query_params", {"content_types": self.model._meta.label_lower})
|
|
417
419
|
return super().formfield(form_class=form_class, **kwargs)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
class PositiveRangeNumberTextField(models.TextField):
|
|
423
|
+
default_error_messages = {
|
|
424
|
+
"invalid": "Invalid value. Specify a value using non-negative integers in a range format (i.e. '10-20').",
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
description = "A text based representation of positive number range."
|
|
428
|
+
|
|
429
|
+
def __init__(self, min_boundary=0, max_boundary=None, *args, **kwargs):
|
|
430
|
+
super().__init__(*args, **kwargs)
|
|
431
|
+
self.min_boundary = min_boundary
|
|
432
|
+
self.max_boundary = max_boundary
|
|
433
|
+
|
|
434
|
+
def to_python(self, value):
|
|
435
|
+
if value is None:
|
|
436
|
+
return None
|
|
437
|
+
|
|
438
|
+
try:
|
|
439
|
+
self.expanded = sorted(parse_numeric_range(value))
|
|
440
|
+
except (ValueError, AttributeError):
|
|
441
|
+
raise exceptions.ValidationError(
|
|
442
|
+
self.error_messages["invalid"],
|
|
443
|
+
code="invalid",
|
|
444
|
+
params={"value": value},
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
converted_ranges = compress_range(self.expanded)
|
|
448
|
+
normalized_range = ",".join([f"{x[0]}" if x[0] == x[1] else f"{x[0]}-{x[1]}" for x in converted_ranges])
|
|
449
|
+
|
|
450
|
+
return normalized_range
|
|
451
|
+
|
|
452
|
+
def validate(self, value, model_instance):
|
|
453
|
+
"""
|
|
454
|
+
Validate `value` and raise ValidationError if necessary.
|
|
455
|
+
"""
|
|
456
|
+
super().validate(value, model_instance)
|
|
457
|
+
|
|
458
|
+
if (self.min_boundary is not None and self.expanded[0] < self.min_boundary) or (
|
|
459
|
+
self.max_boundary is not None and self.expanded[-1] > self.max_boundary
|
|
460
|
+
):
|
|
461
|
+
raise exceptions.ValidationError(
|
|
462
|
+
message=f"Invalid value. Specify a range value between {self.min_boundary}-{self.max_boundary or 'unlimited'}",
|
|
463
|
+
code="outofrange",
|
|
464
|
+
params={"value": value},
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
def formfield(self, **kwargs):
|
|
468
|
+
return super().formfield(
|
|
469
|
+
**{
|
|
470
|
+
"widget": TextInput,
|
|
471
|
+
**kwargs,
|
|
472
|
+
}
|
|
473
|
+
)
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
onerror="window.location='{% url 'media_failure' %}?filename=highlight.js-11.9.0/highlight.min.js'"></script>
|
|
19
19
|
<script src="{% versioned_static 'js/forms.js' %}"
|
|
20
20
|
onerror="window.location='{% url 'media_failure' %}?filename=js/forms.js'"></script>
|
|
21
|
+
<script src="{% versioned_static 'js/nav_menu.js' %}"
|
|
22
|
+
onerror="window.location='{% url 'media_failure' %}?filename=js/nav_menu.js'"></script>
|
|
21
23
|
<script src="{% versioned_static 'js/theme.js' %}"
|
|
22
24
|
onerror="window.location='{% url 'media_failure' %}?filename=js/theme.js'"></script>
|
|
23
25
|
<script src="{% versioned_static 'js/table_sorting_indicator.js' %}"
|
|
@@ -117,254 +117,3 @@
|
|
|
117
117
|
<button type="button" class="btn btn-xs btn-warning navbar-toggler" aria-label="Collapse navbar">
|
|
118
118
|
<span class="mdi mdi-chevron-up mdi-rotate-270 navbar-toggler-arrow"></span>
|
|
119
119
|
</button>
|
|
120
|
-
|
|
121
|
-
<script>
|
|
122
|
-
document.addEventListener('DOMContentLoaded', function() {
|
|
123
|
-
const navbar = document.querySelector('.navbar-fixed-left');
|
|
124
|
-
const navbarHeader = document.querySelector('.navbar-header');
|
|
125
|
-
const mainContent = document.querySelector('#main-content');
|
|
126
|
-
const footer = document.querySelector('#footer');
|
|
127
|
-
const dropdownToggles = document.querySelectorAll('.navbar-fixed-left .navbar-nav > .dropdown > a[data-toggle="collapse"]');
|
|
128
|
-
const dropdowns = document.querySelectorAll('.navbar-fixed-left .navbar-nav .collapse');
|
|
129
|
-
const toggler = document.querySelector('.navbar-toggler');
|
|
130
|
-
const togglerIcon = toggler.querySelector('.navbar-toggler-arrow');
|
|
131
|
-
let lastDropdownId = sessionStorage.getItem('lastOpenedDropdown');
|
|
132
|
-
let savedScrollPosition = sessionStorage.getItem('navbarScrollPosition');
|
|
133
|
-
let activeLink = sessionStorage.getItem('activeLink');
|
|
134
|
-
let expandedByHover = false;
|
|
135
|
-
let manuallyToggled = sessionStorage.getItem('manuallyToggled') === 'true';
|
|
136
|
-
|
|
137
|
-
// Function to reset stored dropdown state information
|
|
138
|
-
function resetNavbarState() {
|
|
139
|
-
sessionStorage.removeItem('lastOpenedDropdown');
|
|
140
|
-
sessionStorage.removeItem('savedScrollPosition');
|
|
141
|
-
sessionStorage.removeItem('activeLink');
|
|
142
|
-
sessionStorage.removeItem('navbarCollapsed');
|
|
143
|
-
expandedByHover = false;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
toggler.addEventListener('click', function() {
|
|
147
|
-
let isNowCollapsed;
|
|
148
|
-
if (expandedByHover) {
|
|
149
|
-
expandedByHover = false;
|
|
150
|
-
isNowCollapsed = false;
|
|
151
|
-
} else {
|
|
152
|
-
isNowCollapsed = navbar.classList.toggle('collapsed');
|
|
153
|
-
}
|
|
154
|
-
sessionStorage.setItem('navbarCollapsed', isNowCollapsed ? 'true' : 'false');
|
|
155
|
-
// Set 'navbarManuallyToggled' to track any manual toggle
|
|
156
|
-
sessionStorage.setItem('navbarManuallyToggled', 'true');
|
|
157
|
-
// Track if the action was an expansion or a collapse
|
|
158
|
-
sessionStorage.setItem('navbarExpanded', !isNowCollapsed ? 'true' : 'false');
|
|
159
|
-
if (isNowCollapsed) {
|
|
160
|
-
togglerIcon.classList.add("mdi-rotate-90");
|
|
161
|
-
togglerIcon.classList.remove("mdi-rotate-270");
|
|
162
|
-
} else {
|
|
163
|
-
togglerIcon.classList.remove("mdi-rotate-90");
|
|
164
|
-
togglerIcon.classList.add("mdi-rotate-270");
|
|
165
|
-
}
|
|
166
|
-
adjustElementsForNavbarState(isNowCollapsed);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
// Retrieve the navbar collapsed state from session storage on page load
|
|
170
|
-
const navbarCollapsed = sessionStorage.getItem('navbarCollapsed') === 'true';
|
|
171
|
-
if (navbarCollapsed) {
|
|
172
|
-
navbar.classList.add('collapsed');
|
|
173
|
-
togglerIcon.classList.remove("mdi-rotate-270");
|
|
174
|
-
togglerIcon.classList.add("mdi-rotate-90");
|
|
175
|
-
adjustElementsForNavbarState(true);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function adjustElementsForNavbarState(isCollapsed) {
|
|
179
|
-
const marginLeftValue = isCollapsed ? '-240px' : '0px';
|
|
180
|
-
mainContent.style.marginLeft = marginLeftValue;
|
|
181
|
-
if(footer) footer.style.marginLeft = marginLeftValue;
|
|
182
|
-
toggler.style.left = isCollapsed ? '-5px' : '225px';
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Expand navbar when hovering near the left edge of the screen
|
|
186
|
-
document.addEventListener('mousemove', function(e) {
|
|
187
|
-
if (
|
|
188
|
-
e.clientX < 20 // 20px from the left edge
|
|
189
|
-
&& (e.clientY < 20 || e.clientY > 50) // not near the toggle button
|
|
190
|
-
&& navbar.classList.contains('collapsed')
|
|
191
|
-
) {
|
|
192
|
-
navbar.classList.remove('collapsed');
|
|
193
|
-
toggler.style.left = '225px';
|
|
194
|
-
expandedByHover = true; // Set flag when expanded by hover
|
|
195
|
-
} else if (expandedByHover && e.clientX > 240) {
|
|
196
|
-
navbar.classList.add('collapsed');
|
|
197
|
-
toggler.style.left = '-5px';
|
|
198
|
-
expandedByHover = false; // Reset flag after auto-collapse
|
|
199
|
-
}
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
function collapseNavbarIfNeeded() {
|
|
203
|
-
const windowWidth = window.innerWidth;
|
|
204
|
-
const navbarManuallyToggled = sessionStorage.getItem('navbarManuallyToggled') === 'true';
|
|
205
|
-
const navbarExpanded = sessionStorage.getItem('navbarExpanded') === 'true';
|
|
206
|
-
const isCollapsed = navbar.classList.contains('collapsed');
|
|
207
|
-
|
|
208
|
-
if (windowWidth < 1007) {
|
|
209
|
-
if (!isCollapsed) {
|
|
210
|
-
navbar.classList.add('collapsed');
|
|
211
|
-
togglerIcon.classList.remove("mdi-rotate-270");
|
|
212
|
-
togglerIcon.classList.add("mdi-rotate-90");
|
|
213
|
-
adjustElementsForNavbarState(true);
|
|
214
|
-
sessionStorage.setItem('navbarCollapsed', 'true');
|
|
215
|
-
}
|
|
216
|
-
} else if (windowWidth >= 1007) {
|
|
217
|
-
// Only expand automatically if it was not manually collapsed
|
|
218
|
-
if (isCollapsed && (navbarManuallyToggled && navbarExpanded)) {
|
|
219
|
-
navbar.classList.remove('collapsed');
|
|
220
|
-
togglerIcon.classList.add("mdi-rotate-270");
|
|
221
|
-
togglerIcon.classList.remove("mdi-rotate-90");
|
|
222
|
-
adjustElementsForNavbarState(false);
|
|
223
|
-
sessionStorage.setItem('navbarCollapsed', 'false');
|
|
224
|
-
}
|
|
225
|
-
// Do not automatically change the state if it was manually collapsed
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Update the window resize listener
|
|
230
|
-
function toggleNavbarOnResize() {
|
|
231
|
-
collapseNavbarIfNeeded(); // Use the new function to decide whether to collapse
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
let debouncedToggleNavbarOnResize = debounce(toggleNavbarOnResize, 50);
|
|
235
|
-
window.addEventListener('resize', debouncedToggleNavbarOnResize);
|
|
236
|
-
|
|
237
|
-
// Select the navbar dropdown elements
|
|
238
|
-
let navbarItems = document.querySelectorAll('.navbar-fixed-left .navbar-nav > .dropdown > .dropdown-toggle > #dropdown_title');
|
|
239
|
-
|
|
240
|
-
// Add a title attribute and tooltip, only if necessary
|
|
241
|
-
navbarItems.forEach(function(item) {
|
|
242
|
-
// Check if the text overflows
|
|
243
|
-
if (item.scrollWidth > item.clientWidth) {
|
|
244
|
-
// Set the title attribute
|
|
245
|
-
item.setAttribute('title', item.innerText);
|
|
246
|
-
|
|
247
|
-
// Reinitialize Bootstrap tooltip
|
|
248
|
-
$(item).tooltip();
|
|
249
|
-
}
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
// Add an event listener for the home link click
|
|
253
|
-
const homeLink = document.querySelector('.navbar-fixed-left .navbar-brand');
|
|
254
|
-
if (homeLink) {
|
|
255
|
-
homeLink.addEventListener('click', function() {
|
|
256
|
-
resetNavbarState();
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// Close all dropdowns except the one specified
|
|
261
|
-
function closeAllDropdownsExcept(exceptId) {
|
|
262
|
-
dropdowns.forEach(function(collapse) {
|
|
263
|
-
if (collapse.id !== exceptId && collapse.classList.contains('in')) {
|
|
264
|
-
$(collapse).collapse('hide');
|
|
265
|
-
}
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// Add click event listener to the dropdown links and save the clicked one
|
|
270
|
-
function addLinkClickListeners() {
|
|
271
|
-
const dropdownLinks = document.querySelectorAll('.navbar-fixed-left .navbar-nav > .dropdown > .nav-dropdown-menu > li > a');
|
|
272
|
-
|
|
273
|
-
dropdownLinks.forEach(function(link) {
|
|
274
|
-
link.addEventListener('click', function() {
|
|
275
|
-
sessionStorage.setItem('activeLink', link.getAttribute('href'));
|
|
276
|
-
});
|
|
277
|
-
});
|
|
278
|
-
collapseNavbarIfNeeded();
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// Close all dropdowns except the last opened one
|
|
282
|
-
dropdownToggles.forEach(function(toggle) {
|
|
283
|
-
toggle.addEventListener('click', function(event) {
|
|
284
|
-
event.preventDefault();
|
|
285
|
-
const collapseElement = document.getElementById(this.getAttribute('href').substring(1));
|
|
286
|
-
|
|
287
|
-
if (!collapseElement.classList.contains('in')) {
|
|
288
|
-
closeAllDropdownsExcept(collapseElement.id);
|
|
289
|
-
$(collapseElement).collapse('show');
|
|
290
|
-
sessionStorage.setItem('lastOpenedDropdown', collapseElement.id);
|
|
291
|
-
} else {
|
|
292
|
-
$(collapseElement).collapse('hide');
|
|
293
|
-
sessionStorage.removeItem('lastOpenedDropdown');
|
|
294
|
-
}
|
|
295
|
-
});
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
// Open the last opened dropdown
|
|
299
|
-
if (lastDropdownId) {
|
|
300
|
-
let lastDropdownMenu = document.getElementById(lastDropdownId);
|
|
301
|
-
if (lastDropdownMenu && !lastDropdownMenu.classList.contains('in')) {
|
|
302
|
-
$(lastDropdownMenu).collapse('show');
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// Restore the last saved scroll position
|
|
307
|
-
if (savedScrollPosition) {
|
|
308
|
-
navbar.scrollTop = savedScrollPosition;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// Function to adjust navbar header visibility based on scroll position and navbar collapsed state
|
|
312
|
-
function adjustNavbarHeaderVisibility() {
|
|
313
|
-
// Check if the navbar is collapsed and mainContent is defined
|
|
314
|
-
if (navbar.classList.contains('collapsed') && mainContent) {
|
|
315
|
-
const mainContentTop = mainContent.getBoundingClientRect().top;
|
|
316
|
-
// Show or hide the navbar header based on mainContent's position
|
|
317
|
-
if (mainContentTop < 0) {
|
|
318
|
-
// Main content top is out of view, hide navbar header
|
|
319
|
-
navbarHeader.style.top = '-60px'; // height of navbar header
|
|
320
|
-
} else {
|
|
321
|
-
// Main content top is in view, show navbar header
|
|
322
|
-
navbarHeader.style.top = '0';
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// Add scroll event listener to adjust navbar header visibility
|
|
328
|
-
window.addEventListener('scroll', adjustNavbarHeaderVisibility);
|
|
329
|
-
|
|
330
|
-
// Call the function initially to set the correct state when the page loads
|
|
331
|
-
adjustNavbarHeaderVisibility();
|
|
332
|
-
|
|
333
|
-
// Debounce function to limit the rate at which the handleScroll function is executed
|
|
334
|
-
function debounce(func, wait, immediate) {
|
|
335
|
-
let timeout;
|
|
336
|
-
return function() {
|
|
337
|
-
const context = this, args = arguments;
|
|
338
|
-
const later = function() {
|
|
339
|
-
timeout = null;
|
|
340
|
-
if (!immediate) func.apply(context, args);
|
|
341
|
-
};
|
|
342
|
-
const callNow = immediate && !timeout;
|
|
343
|
-
clearTimeout(timeout);
|
|
344
|
-
timeout = setTimeout(later, wait);
|
|
345
|
-
if (callNow) func.apply(context, args);
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// Save the scroll position when the navbar is scrolled
|
|
350
|
-
navbar.addEventListener('scroll', debounce(function() {
|
|
351
|
-
sessionStorage.setItem('navbarScrollPosition', navbar.scrollTop);
|
|
352
|
-
}, 250));
|
|
353
|
-
|
|
354
|
-
// Add click event listeners to dropdown links
|
|
355
|
-
addLinkClickListeners();
|
|
356
|
-
|
|
357
|
-
// Apply the 'active' class to the previously clicked link
|
|
358
|
-
if (activeLink) {
|
|
359
|
-
let previouslyClickedLink = document.querySelector('.navbar-fixed-left .navbar-nav > .dropdown > .nav-dropdown-menu > li > a[href="' + activeLink + '"]');
|
|
360
|
-
let currentLocation = window.location.pathname + window.location.search;
|
|
361
|
-
|
|
362
|
-
if (previouslyClickedLink && currentLocation.includes(previouslyClickedLink.getAttribute('href'))) {
|
|
363
|
-
previouslyClickedLink.parentElement.classList.add('active');
|
|
364
|
-
}
|
|
365
|
-
else {
|
|
366
|
-
sessionStorage.removeItem('activeLink');
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
});
|
|
370
|
-
</script>
|
nautobot/core/testing/mixins.py
CHANGED
|
@@ -9,6 +9,7 @@ from django.core.exceptions import FieldDoesNotExist
|
|
|
9
9
|
from django.db import connections, DEFAULT_DB_ALIAS
|
|
10
10
|
from django.db.models import JSONField, ManyToManyField, ManyToManyRel
|
|
11
11
|
from django.forms.models import model_to_dict
|
|
12
|
+
from django.test.testcases import assert_and_parse_html
|
|
12
13
|
from django.test.utils import CaptureQueriesContext
|
|
13
14
|
from netaddr import IPNetwork
|
|
14
15
|
from rest_framework.test import APIClient, APIRequestFactory
|
|
@@ -172,8 +173,12 @@ class NautobotTestCaseMixin:
|
|
|
172
173
|
# REST API response; pass the response data through directly
|
|
173
174
|
err_message += f"\n{response.data}"
|
|
174
175
|
# Attempt to extract form validation errors from the response HTML
|
|
175
|
-
form_errors
|
|
176
|
-
|
|
176
|
+
elif form_errors := utils.extract_form_failures(response.content.decode(response.charset)):
|
|
177
|
+
err_message += f"\n{form_errors}"
|
|
178
|
+
elif body_content := utils.extract_page_body(response.content.decode(response.charset)):
|
|
179
|
+
err_message += f"\n{body_content}"
|
|
180
|
+
else:
|
|
181
|
+
err_message += "No data"
|
|
177
182
|
if msg:
|
|
178
183
|
err_message = f"{msg}\n{err_message}"
|
|
179
184
|
self.assertIn(response.status_code, expected_status, err_message)
|
|
@@ -277,6 +282,58 @@ class NautobotTestCaseMixin:
|
|
|
277
282
|
|
|
278
283
|
return None
|
|
279
284
|
|
|
285
|
+
def assertBodyContains(self, response, text, count=None, status_code=200, msg_prefix="", html=False):
|
|
286
|
+
"""
|
|
287
|
+
Like Django's `assertContains`, but uses `extract_page_body` utility function to scope the check more narrowly.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
response (HttpResponse): The response to inspect
|
|
291
|
+
text (str): Plaintext or HTML to check for in the response body
|
|
292
|
+
count (int, optional): Number of times the `text` should occur, or None if we don't care as long as
|
|
293
|
+
it's present at all.
|
|
294
|
+
status_code (int): HTTP status code expected
|
|
295
|
+
html (bool): If True, handle `text` as HTML, ignoring whitespace etc, as in Django's `assertHTMLEqual()`.
|
|
296
|
+
"""
|
|
297
|
+
# The below is copied from SimpleTestCase._assert_contains and SimpleTestCase.assertContains
|
|
298
|
+
# If the response supports deferred rendering and hasn't been rendered
|
|
299
|
+
# yet, then ensure that it does get rendered before proceeding further.
|
|
300
|
+
if hasattr(response, "render") and callable(response.render) and not response.is_rendered:
|
|
301
|
+
response.render()
|
|
302
|
+
|
|
303
|
+
if msg_prefix:
|
|
304
|
+
msg_prefix += ": "
|
|
305
|
+
|
|
306
|
+
self.assertHttpStatus( # Nautobot-specific, original uses simple assertEqual()
|
|
307
|
+
response, status_code, msg_prefix
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
if response.streaming:
|
|
311
|
+
content = b"".join(response.streaming_content)
|
|
312
|
+
else:
|
|
313
|
+
content = response.content
|
|
314
|
+
|
|
315
|
+
if not isinstance(text, bytes) or html:
|
|
316
|
+
text = str(text)
|
|
317
|
+
content = content.decode(response.charset)
|
|
318
|
+
content = utils.extract_page_body(content) # Nautobot-specific
|
|
319
|
+
text_repr = f"'{text}'"
|
|
320
|
+
else:
|
|
321
|
+
text_repr = repr(text)
|
|
322
|
+
|
|
323
|
+
if html:
|
|
324
|
+
content = assert_and_parse_html(self, content, None, "Response's content is not valid HTML:")
|
|
325
|
+
text = assert_and_parse_html(self, text, None, "Second argument is not valid HTML:")
|
|
326
|
+
real_count = content.count(text)
|
|
327
|
+
|
|
328
|
+
if count is not None:
|
|
329
|
+
self.assertEqual(
|
|
330
|
+
real_count,
|
|
331
|
+
count,
|
|
332
|
+
msg_prefix + f"Found {real_count} instances of {text_repr} in response (expected {count}):\n{content}",
|
|
333
|
+
)
|
|
334
|
+
else:
|
|
335
|
+
self.assertTrue(real_count != 0, msg_prefix + f"Couldn't find {text_repr} in response:\n{content}")
|
|
336
|
+
|
|
280
337
|
#
|
|
281
338
|
# Convenience methods
|
|
282
339
|
#
|