nautobot 2.2.6__py3-none-any.whl → 2.2.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of nautobot might be problematic. Click here for more details.
- nautobot/core/api/mixins.py +6 -1
- nautobot/core/api/schema.py +3 -1
- nautobot/core/celery/__init__.py +1 -1
- nautobot/core/graphql/generators.py +2 -2
- nautobot/core/graphql/schema.py +7 -4
- nautobot/core/tests/integration/test_general_functionality.py +36 -0
- nautobot/core/tests/runner.py +10 -0
- nautobot/core/tests/test_graphql.py +51 -5
- nautobot/dcim/models/devices.py +10 -3
- nautobot/dcim/tests/test_models.py +75 -0
- nautobot/extras/models/groups.py +9 -2
- nautobot/extras/models/relationships.py +12 -0
- nautobot/extras/tests/integration/test_dynamicgroups.py +1 -1
- nautobot/extras/tests/test_dynamicgroups.py +8 -1
- nautobot/extras/tests/test_relationships.py +221 -1
- nautobot/extras/views.py +1 -1
- nautobot/ipam/api/serializers.py +46 -16
- nautobot/ipam/api/views.py +29 -16
- nautobot/ipam/tests/test_api.py +15 -20
- 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.ad660dcc.min.js → bundle.fe8b6f2b.min.js} +4 -4
- nautobot/project-static/docs/assets/javascripts/{bundle.ad660dcc.min.js.map → bundle.fe8b6f2b.min.js.map} +3 -3
- nautobot/project-static/docs/assets/stylesheets/{main.6543a935.min.css → main.76a95c52.min.css} +1 -1
- nautobot/project-static/docs/assets/stylesheets/{main.6543a935.min.css.map → main.76a95c52.min.css.map} +1 -1
- 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 +3 -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 +5 -6
- 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 +16 -4
- 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/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/overview/index.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 +370 -99
- nautobot/project-static/docs/requirements.txt +2 -1
- nautobot/project-static/docs/search/search_index.json +1 -1
- nautobot/project-static/docs/sitemap.xml +256 -256
- 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/optional-settings.html +3 -3
- nautobot/project-static/docs/user-guide/administration/configuration/required-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/caching.html +3 -3
- nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +3 -3
- nautobot/project-static/docs/user-guide/administration/guides/healthcheck.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/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 +7 -7
- nautobot/project-static/docs/user-guide/administration/installation/index.html +3 -3
- nautobot/project-static/docs/user-guide/administration/installation/install_system.html +8 -4
- 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/installation-extras/docker.html +3 -3
- nautobot/project-static/docs/user-guide/administration/installation-extras/health-checks.html +3 -3
- nautobot/project-static/docs/user-guide/administration/installation-extras/selinux-troubleshooting.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/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/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/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/secret.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/connection_toggles.js +7 -6
- {nautobot-2.2.6.dist-info → nautobot-2.2.8.dist-info}/METADATA +4 -4
- {nautobot-2.2.6.dist-info → nautobot-2.2.8.dist-info}/RECORD +292 -291
- {nautobot-2.2.6.dist-info → nautobot-2.2.8.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.2.6.dist-info → nautobot-2.2.8.dist-info}/NOTICE +0 -0
- {nautobot-2.2.6.dist-info → nautobot-2.2.8.dist-info}/WHEEL +0 -0
- {nautobot-2.2.6.dist-info → nautobot-2.2.8.dist-info}/entry_points.txt +0 -0
nautobot/core/api/mixins.py
CHANGED
|
@@ -6,7 +6,7 @@ from django.core.exceptions import (
|
|
|
6
6
|
MultipleObjectsReturned,
|
|
7
7
|
ObjectDoesNotExist,
|
|
8
8
|
)
|
|
9
|
-
from django.db.models import AutoField
|
|
9
|
+
from django.db.models import AutoField, Model
|
|
10
10
|
from rest_framework.exceptions import ValidationError
|
|
11
11
|
|
|
12
12
|
from nautobot.core.api.utils import dict_to_filter_params
|
|
@@ -70,6 +70,11 @@ class WritableSerializerMixin:
|
|
|
70
70
|
# Strip the trailing slash and split on slashes, taking the last value as the PK.
|
|
71
71
|
data = data.rstrip("/").split("/")[-1]
|
|
72
72
|
|
|
73
|
+
# If we're passing the validated_data from one serializer as input to another serializer,
|
|
74
|
+
# data might already be a model instance:
|
|
75
|
+
if isinstance(data, Model):
|
|
76
|
+
return {"pk": data.pk}
|
|
77
|
+
|
|
73
78
|
try:
|
|
74
79
|
# The int case here is taking into account for the User model we inherit from django
|
|
75
80
|
pk = int(data) if isinstance(queryset.model._meta.pk, AutoField) else uuid.UUID(str(data))
|
nautobot/core/api/schema.py
CHANGED
|
@@ -4,7 +4,7 @@ import re
|
|
|
4
4
|
from drf_spectacular.contrib.django_filters import DjangoFilterExtension
|
|
5
5
|
from drf_spectacular.extensions import OpenApiSerializerFieldExtension
|
|
6
6
|
from drf_spectacular.openapi import AutoSchema
|
|
7
|
-
from drf_spectacular.plumbing import build_array_type, build_media_type_object, get_doc, is_serializer
|
|
7
|
+
from drf_spectacular.plumbing import build_array_type, build_media_type_object, get_doc, is_serializer, list_hash
|
|
8
8
|
from drf_spectacular.serializers import PolymorphicProxySerializerExtension
|
|
9
9
|
from rest_framework import serializers
|
|
10
10
|
from rest_framework.relations import ManyRelatedField
|
|
@@ -359,6 +359,8 @@ class ChoiceFieldFix(OpenApiSerializerFieldExtension):
|
|
|
359
359
|
return {
|
|
360
360
|
"type": value_type,
|
|
361
361
|
"enum": list(choices.keys()),
|
|
362
|
+
# Used to deduplicate enums with the same set of choices, since drf-spectacular 0.27.2
|
|
363
|
+
"x-spec-enum-id": list_hash([(k, v) for k, v in choices.items() if k not in ("", None)]),
|
|
362
364
|
}
|
|
363
365
|
else:
|
|
364
366
|
return {
|
nautobot/core/celery/__init__.py
CHANGED
|
@@ -109,7 +109,7 @@ def _import_jobs_from_git_repositories():
|
|
|
109
109
|
and not GitRepository.objects.filter(slug=filename).exists()
|
|
110
110
|
):
|
|
111
111
|
logger.warning("Deleting unmanaged (leftover?) Git repository clone at %s", filepath)
|
|
112
|
-
shutil.rmtree(filepath)
|
|
112
|
+
shutil.rmtree(filepath, ignore_errors=True)
|
|
113
113
|
|
|
114
114
|
# Make sure all GitRepository records that include Jobs have up-to-date git clones, and load their jobs
|
|
115
115
|
for repo in GitRepository.objects.filter(provided_contents__contains="extras.job"):
|
|
@@ -55,12 +55,12 @@ def generate_null_choices_resolver(name, resolver_name):
|
|
|
55
55
|
|
|
56
56
|
def generate_filter_resolver(schema_type, resolver_name, field_name):
|
|
57
57
|
"""
|
|
58
|
-
Generate function to resolve
|
|
58
|
+
Generate function to resolve filtering of ManyToOne and ManyToMany related objects.
|
|
59
59
|
|
|
60
60
|
Args:
|
|
61
61
|
schema_type (DjangoObjectType): DjangoObjectType for a given model
|
|
62
62
|
resolver_name (str): name of the resolver
|
|
63
|
-
field_name (str): name of
|
|
63
|
+
field_name (str): name of ManyToOneField, ManyToManyRel, or ManyToOneRel field to filter
|
|
64
64
|
"""
|
|
65
65
|
filterset_class = schema_type._meta.filterset_class
|
|
66
66
|
|
nautobot/core/graphql/schema.py
CHANGED
|
@@ -6,7 +6,8 @@ import logging
|
|
|
6
6
|
from django.conf import settings
|
|
7
7
|
from django.contrib.contenttypes.models import ContentType
|
|
8
8
|
from django.core.validators import ValidationError
|
|
9
|
-
from django.db.models
|
|
9
|
+
from django.db.models import ManyToManyField
|
|
10
|
+
from django.db.models.fields.reverse_related import ManyToManyRel, ManyToOneRel, OneToOneRel
|
|
10
11
|
import graphene
|
|
11
12
|
from graphene.types import generic
|
|
12
13
|
|
|
@@ -198,9 +199,11 @@ def extend_schema_type_filter(schema_type, model):
|
|
|
198
199
|
(DjangoObjectType): The extended schema_type object
|
|
199
200
|
"""
|
|
200
201
|
for field in model._meta.get_fields():
|
|
201
|
-
# Check attribute is a ManyToOne field
|
|
202
|
-
|
|
203
|
-
|
|
202
|
+
# Check whether attribute is a ManyToOne or ManyToMany field
|
|
203
|
+
if not isinstance(field, (ManyToManyField, ManyToManyRel, ManyToOneRel)):
|
|
204
|
+
continue
|
|
205
|
+
# OneToOneRel is a subclass of ManyToOneRel, but we don't want to treat it as a list
|
|
206
|
+
if isinstance(field, OneToOneRel):
|
|
204
207
|
continue
|
|
205
208
|
child_schema_type = registry["graphql_types"].get(field.related_model._meta.label_lower)
|
|
206
209
|
if child_schema_type:
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from django.urls import reverse
|
|
2
|
+
|
|
3
|
+
from nautobot.core.testing.integration import SeleniumTestCase
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class StaticMediaFailureTestCase(SeleniumTestCase):
|
|
7
|
+
"""Integration test to make sure no static media failures are encountered."""
|
|
8
|
+
|
|
9
|
+
def setUp(self):
|
|
10
|
+
super().setUp()
|
|
11
|
+
self.user.is_superuser = True
|
|
12
|
+
self.user.is_staff = True
|
|
13
|
+
self.user.save()
|
|
14
|
+
self.login(self.user.username, self.password)
|
|
15
|
+
|
|
16
|
+
def tearDown(self):
|
|
17
|
+
self.logout()
|
|
18
|
+
super().tearDown()
|
|
19
|
+
|
|
20
|
+
def test_for_static_media_failure(self):
|
|
21
|
+
test_urls = [
|
|
22
|
+
reverse("home"),
|
|
23
|
+
reverse("api-root"),
|
|
24
|
+
reverse("graphql"),
|
|
25
|
+
reverse("api_docs"),
|
|
26
|
+
"/admin/",
|
|
27
|
+
"/static/docs/overview/index.html",
|
|
28
|
+
]
|
|
29
|
+
for url in test_urls:
|
|
30
|
+
with self.subTest(test_url=url):
|
|
31
|
+
self.browser.visit(self.live_server_url + url)
|
|
32
|
+
# Wait for body element to appear
|
|
33
|
+
self.assertTrue(self.browser.is_element_present_by_tag("body", wait_time=10), "Page failed to load")
|
|
34
|
+
# Ensure we weren't redirected to another page
|
|
35
|
+
self.assertEqual(self.browser.url, self.live_server_url + url)
|
|
36
|
+
self.assertTrue(self.browser.is_text_not_present("Static Media Failure"))
|
nautobot/core/tests/runner.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import copy
|
|
2
|
+
import hashlib
|
|
2
3
|
|
|
3
4
|
from django.conf import settings
|
|
4
5
|
from django.core.management import call_command
|
|
5
6
|
from django.db import connections
|
|
7
|
+
from django.db.migrations.recorder import MigrationRecorder
|
|
6
8
|
from django.test.runner import _init_worker, DiscoverRunner, ParallelTestSuite
|
|
7
9
|
from django.test.utils import get_unique_databases_and_mirrors, NullTimeKeeper, override_settings
|
|
8
10
|
import yaml
|
|
@@ -113,6 +115,14 @@ class NautobotTestRunner(DiscoverRunner):
|
|
|
113
115
|
command += ["--seed", settings.TEST_FACTORY_SEED]
|
|
114
116
|
if self.cache_test_fixtures:
|
|
115
117
|
command += ["--cache-test-fixtures"]
|
|
118
|
+
# Use the list of applied migrations as a unique hash to keep fixtures from differing
|
|
119
|
+
# branches/releases of Nautobot in separate files.
|
|
120
|
+
hexdigest = hashlib.shake_128(
|
|
121
|
+
",".join(
|
|
122
|
+
sorted(f"{m.app}.{m.name}" for m in MigrationRecorder.Migration.objects.all())
|
|
123
|
+
).encode("utf-8")
|
|
124
|
+
).hexdigest(10)
|
|
125
|
+
command += ["--fixture-file", f"development/factory_dump.{hexdigest}.json"]
|
|
116
126
|
with time_keeper.timed(f' Pre-populating test database "{alias}" with factory data...'):
|
|
117
127
|
db_command = [*command, "--database", alias]
|
|
118
128
|
call_command(*db_command)
|
|
@@ -729,8 +729,8 @@ class GraphQLQueryTest(GraphQLTestCaseBase):
|
|
|
729
729
|
cls.location_type = LocationType.objects.get(name="Campus")
|
|
730
730
|
cls.location1 = Location.objects.filter(location_type=cls.location_type).first()
|
|
731
731
|
cls.location2 = Location.objects.filter(location_type=cls.location_type).last()
|
|
732
|
-
cls.location1.name = "Location-1"
|
|
733
|
-
cls.location2.name = "Location-2"
|
|
732
|
+
cls.location1.name = "Campus Location-1"
|
|
733
|
+
cls.location2.name = "Campus Location-2"
|
|
734
734
|
cls.location1.status = cls.location_statuses[0]
|
|
735
735
|
cls.location2.status = cls.location_statuses[1]
|
|
736
736
|
cls.location1.validated_save()
|
|
@@ -930,6 +930,7 @@ class GraphQLQueryTest(GraphQLTestCaseBase):
|
|
|
930
930
|
cls.prefix1 = Prefix.objects.create(
|
|
931
931
|
prefix="10.0.1.0/24", namespace=cls.namespace, status=cls.prefix_statuses[0]
|
|
932
932
|
)
|
|
933
|
+
cls.prefix1.locations.add(cls.location1, cls.location2)
|
|
933
934
|
cls.ipaddr1 = IPAddress.objects.create(
|
|
934
935
|
address="10.0.1.1/24", namespace=cls.namespace, status=cls.ip_statuses[0]
|
|
935
936
|
)
|
|
@@ -968,6 +969,7 @@ class GraphQLQueryTest(GraphQLTestCaseBase):
|
|
|
968
969
|
cls.prefix2 = Prefix.objects.create(
|
|
969
970
|
prefix="10.0.2.0/24", namespace=cls.namespace, status=cls.prefix_statuses[1]
|
|
970
971
|
)
|
|
972
|
+
cls.prefix2.locations.add(cls.location1, cls.location2)
|
|
971
973
|
cls.ipaddr2 = IPAddress.objects.create(
|
|
972
974
|
address="10.0.2.1/30", namespace=cls.namespace, status=cls.ip_statuses[1]
|
|
973
975
|
)
|
|
@@ -1507,9 +1509,9 @@ query {
|
|
|
1507
1509
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1508
1510
|
def test_query_locations_filter(self):
|
|
1509
1511
|
filters = (
|
|
1510
|
-
('name: "Location-1"', 1),
|
|
1511
|
-
('name: ["Location-1"]', 1),
|
|
1512
|
-
('name: ["Location-1", "Location-2"]', 2),
|
|
1512
|
+
('name: "Campus Location-1"', 1),
|
|
1513
|
+
('name: ["Campus Location-1"]', 1),
|
|
1514
|
+
('name: ["Campus Location-1", "Campus Location-2"]', 2),
|
|
1513
1515
|
('name__ic: "Location"', Location.objects.filter(name__icontains="Location").count()),
|
|
1514
1516
|
('name__ic: ["Location"]', Location.objects.filter(name__icontains="Location").count()),
|
|
1515
1517
|
('name__nic: "Location"', Location.objects.exclude(name__icontains="Location").count()),
|
|
@@ -1541,6 +1543,50 @@ query {
|
|
|
1541
1543
|
self.assertIsNone(result.errors)
|
|
1542
1544
|
self.assertEqual(len(result.data["locations"]), nbr_expected_results)
|
|
1543
1545
|
|
|
1546
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1547
|
+
def test_query_prefixes_nested_m2m_filter(self):
|
|
1548
|
+
"""
|
|
1549
|
+
Test functionality added to address https://github.com/nautobot/nautobot/issues/5906.
|
|
1550
|
+
|
|
1551
|
+
Prefix.locations is a ManyToManyField, which was not filterable in our GraphQL schema before this fix.
|
|
1552
|
+
"""
|
|
1553
|
+
query = 'query { prefixes (prefix_length__gte:16) { prefix locations (location_type:["Campus"]) { name } } }'
|
|
1554
|
+
result = self.execute_query(query)
|
|
1555
|
+
self.assertIsNone(result.errors)
|
|
1556
|
+
found_valid_location = False
|
|
1557
|
+
found_invalid_location = False
|
|
1558
|
+
for prefix_data in result.data["prefixes"]:
|
|
1559
|
+
for location_data in prefix_data["locations"]:
|
|
1560
|
+
if location_data["name"].startswith("Campus"):
|
|
1561
|
+
found_valid_location = True
|
|
1562
|
+
else:
|
|
1563
|
+
print(f"Found unexpected unfiltered location {location_data['name']} under {prefix_data['prefix']}")
|
|
1564
|
+
found_invalid_location = True
|
|
1565
|
+
self.assertTrue(found_valid_location)
|
|
1566
|
+
self.assertFalse(found_invalid_location)
|
|
1567
|
+
|
|
1568
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1569
|
+
def test_query_locations_nested_reverse_m2m_filter(self):
|
|
1570
|
+
"""
|
|
1571
|
+
Test functionality added to address https://github.com/nautobot/nautobot/issues/5906.
|
|
1572
|
+
|
|
1573
|
+
Location.prefixes is a (reverse) ManyToManyRel, which was not filterable in our GraphQL schema before this fix.
|
|
1574
|
+
"""
|
|
1575
|
+
query = "query { locations { name prefixes (prefix_length:24) { prefix } } }"
|
|
1576
|
+
result = self.execute_query(query)
|
|
1577
|
+
self.assertIsNone(result.errors)
|
|
1578
|
+
found_valid_prefix = False
|
|
1579
|
+
found_invalid_prefix = False
|
|
1580
|
+
for location_data in result.data["locations"]:
|
|
1581
|
+
for prefix_data in location_data["prefixes"]:
|
|
1582
|
+
if prefix_data["prefix"].endswith("/24"):
|
|
1583
|
+
found_valid_prefix = True
|
|
1584
|
+
else:
|
|
1585
|
+
print(f"Found unexpected unfiltered prefix {prefix_data['prefix']} under {location_data['name']}")
|
|
1586
|
+
found_invalid_prefix = True
|
|
1587
|
+
self.assertTrue(found_valid_prefix)
|
|
1588
|
+
self.assertFalse(found_invalid_prefix)
|
|
1589
|
+
|
|
1544
1590
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1545
1591
|
def test_query_devices_filter(self):
|
|
1546
1592
|
filterset_class = DeviceFilterSet
|
nautobot/dcim/models/devices.py
CHANGED
|
@@ -832,9 +832,16 @@ class Device(PrimaryModel, ConfigContextModel):
|
|
|
832
832
|
# Update Location and Rack assignment for any child Devices
|
|
833
833
|
devices = Device.objects.filter(parent_bay__device=self)
|
|
834
834
|
for device in devices:
|
|
835
|
-
|
|
836
|
-
device.
|
|
837
|
-
|
|
835
|
+
save_child_device = False
|
|
836
|
+
if device.location != self.location:
|
|
837
|
+
device.location = self.location
|
|
838
|
+
save_child_device = True
|
|
839
|
+
if device.rack != self.rack:
|
|
840
|
+
device.rack = self.rack
|
|
841
|
+
save_child_device = True
|
|
842
|
+
|
|
843
|
+
if save_child_device:
|
|
844
|
+
device.save()
|
|
838
845
|
|
|
839
846
|
def create_components(self):
|
|
840
847
|
"""Create device components from the device type definition."""
|
|
@@ -16,6 +16,7 @@ from nautobot.dcim.choices import (
|
|
|
16
16
|
InterfaceTypeChoices,
|
|
17
17
|
PortTypeChoices,
|
|
18
18
|
PowerOutletFeedLegChoices,
|
|
19
|
+
SubdeviceRoleChoices,
|
|
19
20
|
)
|
|
20
21
|
from nautobot.dcim.models import (
|
|
21
22
|
Cable,
|
|
@@ -886,6 +887,13 @@ class DeviceTestCase(ModelTestCases.BaseModelTestCase):
|
|
|
886
887
|
self.device_type = DeviceType.objects.create(
|
|
887
888
|
manufacturer=manufacturer,
|
|
888
889
|
model="Test Device Type 1",
|
|
890
|
+
subdevice_role=SubdeviceRoleChoices.ROLE_PARENT,
|
|
891
|
+
)
|
|
892
|
+
self.child_devicetype = DeviceType.objects.create(
|
|
893
|
+
model="Child Device Type 1",
|
|
894
|
+
manufacturer=manufacturer,
|
|
895
|
+
subdevice_role=SubdeviceRoleChoices.ROLE_CHILD,
|
|
896
|
+
u_height=0,
|
|
889
897
|
)
|
|
890
898
|
self.device_role = Role.objects.get_for_model(Device).first()
|
|
891
899
|
self.device_status = Status.objects.get_for_model(Device).first()
|
|
@@ -1188,6 +1196,73 @@ class DeviceTestCase(ModelTestCases.BaseModelTestCase):
|
|
|
1188
1196
|
self.device_type.software_image_files.add(software_image_file)
|
|
1189
1197
|
self.device.validated_save()
|
|
1190
1198
|
|
|
1199
|
+
def test_child_devices_are_not_saved_when_unnecessary(self):
|
|
1200
|
+
parent_device = Device.objects.create(
|
|
1201
|
+
name="Parent Device 1",
|
|
1202
|
+
location=self.location_3,
|
|
1203
|
+
device_type=self.device_type,
|
|
1204
|
+
role=self.device_role,
|
|
1205
|
+
status=self.device_status,
|
|
1206
|
+
)
|
|
1207
|
+
parent_device.validated_save()
|
|
1208
|
+
|
|
1209
|
+
child_device = Device.objects.create(
|
|
1210
|
+
name="Child Device 1",
|
|
1211
|
+
location=parent_device.location,
|
|
1212
|
+
device_type=self.child_devicetype,
|
|
1213
|
+
role=parent_device.role,
|
|
1214
|
+
status=self.device_status,
|
|
1215
|
+
)
|
|
1216
|
+
child_device.validated_save()
|
|
1217
|
+
child_mtime_before_parent_saved = str(child_device.last_updated)
|
|
1218
|
+
|
|
1219
|
+
devicebay = DeviceBay.objects.get(device=parent_device, name="Device Bay 1")
|
|
1220
|
+
devicebay.installed_device = child_device
|
|
1221
|
+
devicebay.validated_save()
|
|
1222
|
+
|
|
1223
|
+
#
|
|
1224
|
+
# Tests
|
|
1225
|
+
#
|
|
1226
|
+
|
|
1227
|
+
#
|
|
1228
|
+
# On a NOOP save, the child device shouldn't be updated
|
|
1229
|
+
parent_device.save()
|
|
1230
|
+
|
|
1231
|
+
child_mtime_after_parent_noop_save = str(Device.objects.get(name="Child Device 1").last_updated)
|
|
1232
|
+
|
|
1233
|
+
self.assertEqual(child_mtime_before_parent_saved, child_mtime_after_parent_noop_save)
|
|
1234
|
+
|
|
1235
|
+
#
|
|
1236
|
+
# On a serial number update, the child device shouldn't be updated
|
|
1237
|
+
parent_device.serial = "12345"
|
|
1238
|
+
parent_device.save()
|
|
1239
|
+
|
|
1240
|
+
child_mtime_after_parent_serial_update_save = str(Device.objects.get(name="Child Device 1").last_updated)
|
|
1241
|
+
|
|
1242
|
+
self.assertEqual(child_mtime_before_parent_saved, child_mtime_after_parent_serial_update_save)
|
|
1243
|
+
|
|
1244
|
+
#
|
|
1245
|
+
# If the parent rack updates, the child mtime should update.
|
|
1246
|
+
rack = Rack.objects.create(name="Rack 1", location=parent_device.location, status=self.device_status)
|
|
1247
|
+
parent_device.rack = rack
|
|
1248
|
+
parent_device.save()
|
|
1249
|
+
|
|
1250
|
+
child_mtime_after_parent_rack_update_save = str(Device.objects.get(name="Child Device 1").last_updated)
|
|
1251
|
+
|
|
1252
|
+
self.assertNotEqual(child_mtime_after_parent_noop_save, child_mtime_after_parent_rack_update_save)
|
|
1253
|
+
|
|
1254
|
+
#
|
|
1255
|
+
# If the parent site updates, the child mtime should update
|
|
1256
|
+
location = Location.objects.create(
|
|
1257
|
+
name="New Site 1", status=self.device_status, location_type=self.location_type_3
|
|
1258
|
+
)
|
|
1259
|
+
parent_device.location = location
|
|
1260
|
+
parent_device.save()
|
|
1261
|
+
|
|
1262
|
+
child_mtime_after_parent_site_update_save = str(Device.objects.get(name="Child Device 1").last_updated)
|
|
1263
|
+
|
|
1264
|
+
self.assertNotEqual(child_mtime_after_parent_rack_update_save, child_mtime_after_parent_site_update_save)
|
|
1265
|
+
|
|
1191
1266
|
|
|
1192
1267
|
class DeviceTypeToSoftwareImageFileTestCase(ModelTestCases.BaseModelTestCase):
|
|
1193
1268
|
model = DeviceTypeToSoftwareImageFile
|
nautobot/extras/models/groups.py
CHANGED
|
@@ -597,10 +597,17 @@ class DynamicGroup(OrganizationalModel):
|
|
|
597
597
|
query |= filter_field.generate_query(gq_value)
|
|
598
598
|
|
|
599
599
|
# For vanilla multiple-choice filters, we want all values in a set union (boolean OR)
|
|
600
|
-
# because we want ANY of the filter values to match.
|
|
600
|
+
# because we want ANY of the filter values to match. Unless the filter field is explicitly
|
|
601
|
+
# conjoining the values, in which case we want a set intersection (boolean AND). We know this isn't right
|
|
602
|
+
# since the resulting query actually does tag.name == tag_1 AND tag.name == tag_2, but django_filter does
|
|
603
|
+
# not use Q evaluation for conjoined filters. This function is only used for the display, and the display
|
|
604
|
+
# is good enough to get the point across.
|
|
601
605
|
elif isinstance(filter_field, django_filters.MultipleChoiceFilter):
|
|
602
606
|
for v in value:
|
|
603
|
-
|
|
607
|
+
if filter_field.conjoined:
|
|
608
|
+
query &= models.Q(**filter_field.get_filter_predicate(v))
|
|
609
|
+
else:
|
|
610
|
+
query |= models.Q(**filter_field.get_filter_predicate(v))
|
|
604
611
|
|
|
605
612
|
# The method `get_filter_predicate()` is only available on instances or subclasses
|
|
606
613
|
# of `MultipleChoiceFilter`, so we must construct a lookup if a filter is not
|
|
@@ -244,6 +244,18 @@ class RelationshipModel(models.Model):
|
|
|
244
244
|
if relation.skip_required(cls, opposite_side):
|
|
245
245
|
continue
|
|
246
246
|
|
|
247
|
+
if getattr(relation, f"{relation.required_on}_filter") and instance:
|
|
248
|
+
filterset = get_filterset_for_model(cls)
|
|
249
|
+
if filterset:
|
|
250
|
+
filter_params = getattr(relation, f"{relation.required_on}_filter")
|
|
251
|
+
# If the relationship is required on the model, but the object is not in the filter,
|
|
252
|
+
# we should allow the object to be saved, as the object is not part of the relationship.
|
|
253
|
+
# Example: We want a Device with a Role of Switch to be required to have a relationship
|
|
254
|
+
# with a Device that has a Role of Router. A Device with a Role of Printer should
|
|
255
|
+
# be exempt from the requirement.
|
|
256
|
+
if not filterset(filter_params, cls.objects.filter(id=instance.id)).qs.exists():
|
|
257
|
+
continue
|
|
258
|
+
|
|
247
259
|
if relation.has_many(opposite_side):
|
|
248
260
|
num_required_verbose = "at least one"
|
|
249
261
|
else:
|
|
@@ -74,7 +74,7 @@ class DynamicGroupTestCase(SeleniumTestCase):
|
|
|
74
74
|
|
|
75
75
|
# And just a cursory check to make sure that the filter worked.
|
|
76
76
|
group = DynamicGroup.objects.get(name=name)
|
|
77
|
-
self.assertEqual(group.count,
|
|
77
|
+
self.assertEqual(group.count, Device.objects.filter(status__name="Active").count())
|
|
78
78
|
self.assertEqual(group.filter, {"status": ["Active"]})
|
|
79
79
|
|
|
80
80
|
# Verify dynamic group shows up on device detail tab
|
|
@@ -646,7 +646,7 @@ class DynamicGroupModelTest(DynamicGroupTestBase): # TODO: BaseModelTestCase mi
|
|
|
646
646
|
|
|
647
647
|
queryset = group.get_queryset()
|
|
648
648
|
|
|
649
|
-
# Assert that both querysets
|
|
649
|
+
# Assert that both querysets return the same results
|
|
650
650
|
group_qs = queryset.filter(multi_query)
|
|
651
651
|
device_qs = Device.objects.filter(location__name__in=multi_value)
|
|
652
652
|
self.assertQuerySetEqual(group_qs, device_qs)
|
|
@@ -659,6 +659,13 @@ class DynamicGroupModelTest(DynamicGroupTestBase): # TODO: BaseModelTestCase mi
|
|
|
659
659
|
interface_qs = Device.objects.filter(interfaces__isnull=True)
|
|
660
660
|
self.assertQuerySetEqual(solo_qs, interface_qs)
|
|
661
661
|
|
|
662
|
+
# Tags are conjoined in the TagFilterSet, ensure that tags__name is using AND. We know this isn't right
|
|
663
|
+
# since the resulting query actually does tag.name == tag_1 AND tag.name == tag_2, but django_filter does
|
|
664
|
+
# not use Q evaluation for conjoined filters. This function is only used for the display, and the display
|
|
665
|
+
# is good enough to get the point across.
|
|
666
|
+
tags_query = group.generate_query_for_filter(filter_field=fs.filters["tags"], value=["tag_1", "tag_2"])
|
|
667
|
+
self.assertEqual(str(tags_query), "(AND: ('tags__name', 'tag_1'), ('tags__name', 'tag_2'))")
|
|
668
|
+
|
|
662
669
|
# Test that a nested field_name w/ `generate_query` works as expected. This is explicitly to
|
|
663
670
|
# test a regression w/ nested name-related values such as `DeviceFilterSet.manufacturer` which
|
|
664
671
|
# filters on `device_type__manufacturer`.
|