nautobot 2.3.9__py3-none-any.whl → 2.3.10__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/models/query_functions.py +147 -1
- nautobot/core/tests/test_models_query_functions.py +108 -0
- nautobot/dcim/templates/dcim/modulebay_create.html +39 -0
- nautobot/dcim/templates/dcim/modulebay_update.html +39 -0
- nautobot/dcim/views.py +1 -1
- nautobot/extras/api/customfields.py +3 -10
- nautobot/extras/context_managers.py +23 -3
- nautobot/extras/jobs.py +20 -14
- nautobot/extras/models/customfields.py +12 -0
- nautobot/extras/signals.py +2 -0
- nautobot/extras/tasks.py +88 -69
- nautobot/extras/tests/test_context_managers.py +9 -4
- nautobot/extras/tests/test_webhooks.py +1 -1
- nautobot/extras/webhooks.py +16 -7
- nautobot/project-static/docs/404.html +1 -1
- nautobot/project-static/docs/apps/index.html +1 -1
- nautobot/project-static/docs/apps/nautobot-apps.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/api.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/config.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +62 -5
- nautobot/project-static/docs/code-reference/nautobot/apps/models.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/views.html +1 -1
- nautobot/project-static/docs/development/apps/api/configuration-view.html +1 -1
- nautobot/project-static/docs/development/apps/api/database-backend-config.html +1 -1
- nautobot/project-static/docs/development/apps/api/models/django-admin.html +1 -1
- nautobot/project-static/docs/development/apps/api/models/global-search.html +1 -1
- nautobot/project-static/docs/development/apps/api/models/graphql.html +1 -1
- nautobot/project-static/docs/development/apps/api/models/index.html +1 -1
- nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +1 -1
- nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +1 -1
- nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +1 -1
- nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +1 -1
- nautobot/project-static/docs/development/apps/api/platform-features/index.html +1 -1
- nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +1 -1
- nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +1 -1
- nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +1 -1
- nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +1 -1
- nautobot/project-static/docs/development/apps/api/platform-features/table-extensions.html +1 -1
- nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +1 -1
- nautobot/project-static/docs/development/apps/api/prometheus.html +1 -1
- nautobot/project-static/docs/development/apps/api/setup.html +1 -1
- nautobot/project-static/docs/development/apps/api/testing.html +1 -1
- nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +1 -1
- nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +1 -1
- nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +1 -1
- nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +1 -1
- nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +1 -1
- nautobot/project-static/docs/development/apps/api/views/base-template.html +1 -1
- nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +1 -1
- nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +1 -1
- nautobot/project-static/docs/development/apps/api/views/help-documentation.html +1 -1
- nautobot/project-static/docs/development/apps/api/views/index.html +1 -1
- nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +1 -1
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +1 -1
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +1 -1
- nautobot/project-static/docs/development/apps/api/views/notes.html +1 -1
- nautobot/project-static/docs/development/apps/api/views/rest-api.html +1 -1
- nautobot/project-static/docs/development/apps/api/views/urls.html +1 -1
- nautobot/project-static/docs/development/apps/index.html +1 -1
- nautobot/project-static/docs/development/apps/migration/code-updates.html +1 -1
- nautobot/project-static/docs/development/apps/migration/dependency-updates.html +1 -1
- nautobot/project-static/docs/development/apps/migration/from-v1.html +1 -1
- nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +1 -1
- nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +1 -1
- nautobot/project-static/docs/development/apps/migration/model-updates/global.html +1 -1
- nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +1 -1
- nautobot/project-static/docs/development/apps/porting-from-netbox.html +1 -1
- nautobot/project-static/docs/development/core/application-registry.html +1 -1
- nautobot/project-static/docs/development/core/best-practices.html +1 -1
- nautobot/project-static/docs/development/core/bootstrap-ui.html +1 -1
- nautobot/project-static/docs/development/core/caching.html +1 -1
- nautobot/project-static/docs/development/core/controllers.html +1 -1
- nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +1 -1
- nautobot/project-static/docs/development/core/generic-views.html +1 -1
- nautobot/project-static/docs/development/core/getting-started.html +1 -1
- nautobot/project-static/docs/development/core/homepage.html +1 -1
- nautobot/project-static/docs/development/core/index.html +1 -1
- nautobot/project-static/docs/development/core/model-checklist.html +1 -1
- nautobot/project-static/docs/development/core/model-features.html +1 -1
- nautobot/project-static/docs/development/core/natural-keys.html +1 -1
- nautobot/project-static/docs/development/core/navigation-menu.html +1 -1
- nautobot/project-static/docs/development/core/release-checklist.html +1 -1
- nautobot/project-static/docs/development/core/role-internals.html +1 -1
- nautobot/project-static/docs/development/core/settings.html +1 -1
- nautobot/project-static/docs/development/core/style-guide.html +1 -1
- nautobot/project-static/docs/development/core/templates.html +1 -1
- nautobot/project-static/docs/development/core/testing.html +1 -1
- nautobot/project-static/docs/development/core/user-preferences.html +1 -1
- nautobot/project-static/docs/development/index.html +1 -1
- nautobot/project-static/docs/development/jobs/index.html +1 -1
- nautobot/project-static/docs/development/jobs/migration/from-v1.html +1 -1
- nautobot/project-static/docs/index.html +1 -1
- nautobot/project-static/docs/overview/application_stack.html +1 -1
- nautobot/project-static/docs/overview/design_philosophy.html +1 -1
- nautobot/project-static/docs/release-notes/index.html +1 -1
- nautobot/project-static/docs/release-notes/version-1.0.html +1 -1
- nautobot/project-static/docs/release-notes/version-1.1.html +1 -1
- nautobot/project-static/docs/release-notes/version-1.2.html +1 -1
- nautobot/project-static/docs/release-notes/version-1.3.html +1 -1
- nautobot/project-static/docs/release-notes/version-1.4.html +1 -1
- nautobot/project-static/docs/release-notes/version-1.5.html +1 -1
- nautobot/project-static/docs/release-notes/version-1.6.html +1 -1
- nautobot/project-static/docs/release-notes/version-2.0.html +1 -1
- nautobot/project-static/docs/release-notes/version-2.1.html +1 -1
- nautobot/project-static/docs/release-notes/version-2.2.html +1 -1
- nautobot/project-static/docs/release-notes/version-2.3.html +268 -123
- nautobot/project-static/docs/requirements.txt +1 -1
- nautobot/project-static/docs/search/search_index.json +1 -1
- nautobot/project-static/docs/sitemap.xml +270 -270
- nautobot/project-static/docs/sitemap.xml.gz +0 -0
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +1 -1
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +1 -1
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +1 -1
- nautobot/project-static/docs/user-guide/administration/configuration/index.html +1 -1
- nautobot/project-static/docs/user-guide/administration/configuration/redis.html +1 -1
- nautobot/project-static/docs/user-guide/administration/configuration/settings.html +1 -1
- nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +1 -1
- nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +1 -1
- nautobot/project-static/docs/user-guide/administration/guides/docker.html +1 -1
- nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +1 -1
- nautobot/project-static/docs/user-guide/administration/guides/permissions.html +1 -1
- nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +1 -1
- nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +1 -1
- nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +1 -1
- nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +1 -1
- nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +1 -1
- nautobot/project-static/docs/user-guide/administration/installation/app-install.html +1 -1
- nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +1 -1
- nautobot/project-static/docs/user-guide/administration/installation/http-server.html +1 -1
- nautobot/project-static/docs/user-guide/administration/installation/index.html +1 -1
- nautobot/project-static/docs/user-guide/administration/installation/install_system.html +1 -1
- nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +1 -1
- nautobot/project-static/docs/user-guide/administration/installation/services.html +1 -1
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +1 -1
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +1 -1
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +1 -1
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +1 -1
- nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +1 -1
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +1 -1
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +1 -1
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +1 -1
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +1 -1
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +1 -1
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +1 -1
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +1 -1
- nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +1 -1
- nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +1 -1
- nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +1 -1
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +1 -1
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +1 -1
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +1 -1
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +1 -1
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +1 -1
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +1 -1
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +1 -1
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +1 -1
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +1 -1
- nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +1 -1
- nautobot/project-static/docs/user-guide/feature-guides/graphql.html +1 -1
- nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +1 -1
- nautobot/project-static/docs/user-guide/feature-guides/relationships.html +1 -1
- nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +1 -1
- nautobot/project-static/docs/user-guide/index.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/note.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/role.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/secret.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/status.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/tag.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +1 -1
- nautobot/project-static/js/forms.js +0 -38
- {nautobot-2.3.9.dist-info → nautobot-2.3.10.dist-info}/METADATA +2 -2
- {nautobot-2.3.9.dist-info → nautobot-2.3.10.dist-info}/RECORD +296 -293
- {nautobot-2.3.9.dist-info → nautobot-2.3.10.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.3.9.dist-info → nautobot-2.3.10.dist-info}/NOTICE +0 -0
- {nautobot-2.3.9.dist-info → nautobot-2.3.10.dist-info}/WHEEL +0 -0
- {nautobot-2.3.9.dist-info → nautobot-2.3.10.dist-info}/entry_points.txt +0 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from django.db import NotSupportedError
|
|
2
|
-
from django.db.models import Aggregate, Func, JSONField
|
|
2
|
+
from django.db.models import Aggregate, Func, JSONField, Value
|
|
3
|
+
from django.db.models.fields.json import compile_json_path
|
|
4
|
+
from django.db.models.functions import Cast
|
|
3
5
|
|
|
4
6
|
|
|
5
7
|
class CollateAsChar(Func):
|
|
@@ -26,6 +28,150 @@ class CollateAsChar(Func):
|
|
|
26
28
|
return super().as_sql(compiler, connection, function, template, arg_joiner, **extra_context)
|
|
27
29
|
|
|
28
30
|
|
|
31
|
+
class JSONSet(Func):
|
|
32
|
+
"""
|
|
33
|
+
Set or create the value of a single key in a JSONField.
|
|
34
|
+
|
|
35
|
+
Example:
|
|
36
|
+
model.objects.all().update(_custom_field_data=JSONSet("_custom_field_data", "cf_key", "new_value"))
|
|
37
|
+
|
|
38
|
+
Limitations:
|
|
39
|
+
- Postgres and MySQL only.
|
|
40
|
+
- Does *not* support nested lookups (`key1__key2`), only a single top-level key.
|
|
41
|
+
- Unlike the referenced Django PR, supports only a single key/value rather than an arbitrary number of them.
|
|
42
|
+
|
|
43
|
+
References:
|
|
44
|
+
- https://code.djangoproject.com/ticket/32519
|
|
45
|
+
- https://github.com/django/django/pull/18489/files
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
function = None
|
|
49
|
+
|
|
50
|
+
def __init__(self, expression, path, value, output_field=None):
|
|
51
|
+
self.path = path
|
|
52
|
+
self.value = value
|
|
53
|
+
super().__init__(expression, output_field=output_field)
|
|
54
|
+
|
|
55
|
+
def resolve_expression(self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False):
|
|
56
|
+
"""
|
|
57
|
+
Based on https://github.com/django/django/pull/18489/files.
|
|
58
|
+
|
|
59
|
+
Transforms and inserts self.path and self.value appropriately into the expression fields.
|
|
60
|
+
"""
|
|
61
|
+
c = super().resolve_expression(query, allow_joins, reuse, summarize, for_save)
|
|
62
|
+
# Resolve expressions in the JSON update values.
|
|
63
|
+
c.fields = {
|
|
64
|
+
self.path: (
|
|
65
|
+
self.value.resolve_expression(query, allow_joins, reuse, summarize, for_save)
|
|
66
|
+
if hasattr(self.value, "resolve_expression")
|
|
67
|
+
else self.value
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
return c
|
|
71
|
+
|
|
72
|
+
def as_sql(self, compiler, connection, function=None, **extra_context):
|
|
73
|
+
"""
|
|
74
|
+
MySQL implementation based on https://github.com/django/django/pull/18489/files.
|
|
75
|
+
|
|
76
|
+
Creates a copy of this object with the appropriately transformed self.path and self.value for MySQL JSON_SET().
|
|
77
|
+
"""
|
|
78
|
+
if connection.vendor != "mysql":
|
|
79
|
+
raise NotSupportedError(f"JSONSet is not implemented for database {connection.vendor}")
|
|
80
|
+
|
|
81
|
+
copy = self.copy()
|
|
82
|
+
new_source_expressions = copy.get_source_expressions()
|
|
83
|
+
|
|
84
|
+
path = compile_json_path([self.path])
|
|
85
|
+
value = self.value
|
|
86
|
+
if not hasattr(value, "resolve_expression"):
|
|
87
|
+
# Use Value to serialize the value to a string, then Cast to ensure it's treated as JSON.
|
|
88
|
+
value = Cast(Value(value, output_field=self.output_field), output_field=self.output_field)
|
|
89
|
+
|
|
90
|
+
new_source_expressions.extend((Value(path), value))
|
|
91
|
+
copy.set_source_expressions(new_source_expressions)
|
|
92
|
+
return super(JSONSet, copy).as_sql(compiler, connection, function="JSON_SET", **extra_context)
|
|
93
|
+
|
|
94
|
+
def as_postgresql(self, compiler, connection, function=None, **extra_context):
|
|
95
|
+
"""
|
|
96
|
+
PostgreSQL implementation based on https://github.com/django/django/pull/18489/files.
|
|
97
|
+
|
|
98
|
+
Creates a copy of this object with appropriately transformed self.path and self.value for Postgres JSONB_SET().
|
|
99
|
+
"""
|
|
100
|
+
copy = self.copy()
|
|
101
|
+
new_source_expressions = copy.get_source_expressions()
|
|
102
|
+
|
|
103
|
+
path = self.path
|
|
104
|
+
value = self.value
|
|
105
|
+
if not hasattr(value, "resolve_expression"):
|
|
106
|
+
# We don't need Cast() here because Value with a JSONFIeld is correctly handled as JSONB by Postgres
|
|
107
|
+
value = Value(value, output_field=self.output_field)
|
|
108
|
+
else:
|
|
109
|
+
|
|
110
|
+
class ToJSONB(Func):
|
|
111
|
+
function = "TO_JSONB"
|
|
112
|
+
|
|
113
|
+
value = ToJSONB(value, output_field=self.output_field)
|
|
114
|
+
|
|
115
|
+
new_source_expressions.extend((Value(f"{{{path}}}"), value))
|
|
116
|
+
copy.set_source_expressions(new_source_expressions)
|
|
117
|
+
return super(JSONSet, copy).as_sql(compiler, connection, function="JSONB_SET", **extra_context)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class JSONRemove(Func):
|
|
121
|
+
"""
|
|
122
|
+
Unset and remove a single key in a JSONField.
|
|
123
|
+
|
|
124
|
+
Example:
|
|
125
|
+
model.objects.all().update(_custom_field_data=JSONRemove("_custom_field_data", "cf_key"))
|
|
126
|
+
|
|
127
|
+
Limitations:
|
|
128
|
+
- Postgres and MySQL only.
|
|
129
|
+
- Does *not* support nested lookups (`key1__key2`), only a single top-level key.
|
|
130
|
+
- Unlike the referenced Django PR, supports only a single key, not N keys.
|
|
131
|
+
|
|
132
|
+
References:
|
|
133
|
+
- https://code.djangoproject.com/ticket/32519
|
|
134
|
+
- https://github.com/django/django/pull/18489/files
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
def __init__(self, expression, path):
|
|
138
|
+
self.path = path
|
|
139
|
+
super().__init__(expression)
|
|
140
|
+
|
|
141
|
+
def as_sql(self, compiler, connection, function=None, **extra_context):
|
|
142
|
+
"""
|
|
143
|
+
MySQL implementation based on https://github.com/django/django/pull/18489/files.
|
|
144
|
+
|
|
145
|
+
Creates a copy of this object with appropriately transformed self.path for MySQL JSON_REMOVE().
|
|
146
|
+
"""
|
|
147
|
+
if connection.vendor != "mysql":
|
|
148
|
+
raise NotSupportedError(f"JSONSet is not implemented for database {connection.vendor}")
|
|
149
|
+
|
|
150
|
+
copy = self.copy()
|
|
151
|
+
new_source_expressions = copy.get_source_expressions()
|
|
152
|
+
|
|
153
|
+
new_source_expressions.append(Value(compile_json_path([self.path])))
|
|
154
|
+
|
|
155
|
+
copy.set_source_expressions(new_source_expressions)
|
|
156
|
+
return super(JSONRemove, copy).as_sql(compiler, connection, function="JSON_REMOVE", **extra_context)
|
|
157
|
+
|
|
158
|
+
def as_postgresql(self, compiler, connection, function=None, **extra_context):
|
|
159
|
+
"""
|
|
160
|
+
PostgreSQL implementation based on https://github.com/django/django/pull/18489/files.
|
|
161
|
+
|
|
162
|
+
Creates a copy of this object with appropriately transformed self.path for Postgres `#-` operator.
|
|
163
|
+
"""
|
|
164
|
+
copy = self.copy()
|
|
165
|
+
new_source_expressions = copy.get_source_expressions()
|
|
166
|
+
|
|
167
|
+
new_source_expressions.append(Value(f"{{{self.path}}}"))
|
|
168
|
+
|
|
169
|
+
copy.set_source_expressions(new_source_expressions)
|
|
170
|
+
return super(JSONRemove, copy).as_sql(
|
|
171
|
+
compiler, connection, template="%(expressions)s", arg_joiner="#- ", **extra_context
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
29
175
|
class JSONBAgg(Aggregate):
|
|
30
176
|
"""
|
|
31
177
|
Like django.contrib.postgres.aggregates.JSONBAgg, but different.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from nautobot.core.models.query_functions import JSONRemove, JSONSet
|
|
2
|
+
from nautobot.core.testing import TestCase
|
|
3
|
+
from nautobot.dcim.models import Manufacturer
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class JSONFuncTests(TestCase):
|
|
7
|
+
"""Test JSONSet and JSONRemove functionality."""
|
|
8
|
+
|
|
9
|
+
def test_json_set(self):
|
|
10
|
+
# Setting a key/value should efficiently work
|
|
11
|
+
with self.assertNumQueries(1):
|
|
12
|
+
Manufacturer.objects.all().update(_custom_field_data=JSONSet("_custom_field_data", "a", 1))
|
|
13
|
+
for mfr in Manufacturer.objects.all():
|
|
14
|
+
self.assertIn("a", mfr._custom_field_data)
|
|
15
|
+
self.assertEqual(1, mfr._custom_field_data["a"])
|
|
16
|
+
|
|
17
|
+
# Setting a different key/value shouldn't overwrite other keys
|
|
18
|
+
with self.assertNumQueries(1):
|
|
19
|
+
Manufacturer.objects.all().update(_custom_field_data=JSONSet("_custom_field_data", "b", "text"))
|
|
20
|
+
for mfr in Manufacturer.objects.all():
|
|
21
|
+
self.assertIn("a", mfr._custom_field_data)
|
|
22
|
+
self.assertEqual(1, mfr._custom_field_data["a"])
|
|
23
|
+
self.assertIn("b", mfr._custom_field_data)
|
|
24
|
+
self.assertEqual("text", mfr._custom_field_data["b"])
|
|
25
|
+
|
|
26
|
+
# Setting a key/value again should overwrite that value only
|
|
27
|
+
with self.assertNumQueries(1):
|
|
28
|
+
Manufacturer.objects.all().update(_custom_field_data=JSONSet("_custom_field_data", "b", "more text"))
|
|
29
|
+
for mfr in Manufacturer.objects.all():
|
|
30
|
+
self.assertIn("a", mfr._custom_field_data)
|
|
31
|
+
self.assertEqual(1, mfr._custom_field_data["a"])
|
|
32
|
+
self.assertIn("b", mfr._custom_field_data)
|
|
33
|
+
self.assertEqual("more text", mfr._custom_field_data["b"])
|
|
34
|
+
|
|
35
|
+
# A filtered query should be updatable
|
|
36
|
+
with self.assertNumQueries(1):
|
|
37
|
+
Manufacturer.objects.filter(name__istartswith="a").update(
|
|
38
|
+
_custom_field_data=JSONSet("_custom_field_data", "a", None)
|
|
39
|
+
)
|
|
40
|
+
for mfr in Manufacturer.objects.filter(name__istartswith="a"):
|
|
41
|
+
self.assertIn("a", mfr._custom_field_data)
|
|
42
|
+
self.assertEqual(None, mfr._custom_field_data["a"])
|
|
43
|
+
for mfr in Manufacturer.objects.exclude(name__istartswith="a"):
|
|
44
|
+
self.assertIn("a", mfr._custom_field_data)
|
|
45
|
+
self.assertEqual(1, mfr._custom_field_data["a"])
|
|
46
|
+
for mfr in Manufacturer.objects.all():
|
|
47
|
+
self.assertIn("b", mfr._custom_field_data)
|
|
48
|
+
self.assertEqual("more text", mfr._custom_field_data["b"])
|
|
49
|
+
|
|
50
|
+
# Setting a value doesn't require all existing values to be homogeneous
|
|
51
|
+
with self.assertNumQueries(1):
|
|
52
|
+
Manufacturer.objects.all().update(_custom_field_data=JSONSet("_custom_field_data", "a", "hello"))
|
|
53
|
+
for mfr in Manufacturer.objects.all():
|
|
54
|
+
self.assertIn("a", mfr._custom_field_data)
|
|
55
|
+
self.assertEqual("hello", mfr._custom_field_data["a"])
|
|
56
|
+
self.assertIn("b", mfr._custom_field_data)
|
|
57
|
+
self.assertEqual("more text", mfr._custom_field_data["b"])
|
|
58
|
+
|
|
59
|
+
def test_json_remove(self):
|
|
60
|
+
Manufacturer.objects.all().update(_custom_field_data=JSONSet("_custom_field_data", "a", 1))
|
|
61
|
+
Manufacturer.objects.filter(name__istartswith="a").update(
|
|
62
|
+
_custom_field_data=JSONSet("_custom_field_data", "b", "hello")
|
|
63
|
+
)
|
|
64
|
+
Manufacturer.objects.exclude(name__istartswith="a").update(
|
|
65
|
+
_custom_field_data=JSONSet("_custom_field_data", "b", "world")
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Should be able to clear all values for a key without impacting other keys
|
|
69
|
+
with self.assertNumQueries(1):
|
|
70
|
+
Manufacturer.objects.all().update(_custom_field_data=JSONRemove("_custom_field_data", "a"))
|
|
71
|
+
for mfr in Manufacturer.objects.all():
|
|
72
|
+
self.assertNotIn("a", mfr._custom_field_data)
|
|
73
|
+
self.assertIn("b", mfr._custom_field_data)
|
|
74
|
+
for mfr in Manufacturer.objects.filter(name__istartswith="a"):
|
|
75
|
+
self.assertEqual("hello", mfr._custom_field_data["b"])
|
|
76
|
+
for mfr in Manufacturer.objects.exclude(name__istartswith="a"):
|
|
77
|
+
self.assertEqual("world", mfr._custom_field_data["b"])
|
|
78
|
+
|
|
79
|
+
# Clearing a value that doesn't exist should be safe
|
|
80
|
+
with self.assertNumQueries(1):
|
|
81
|
+
Manufacturer.objects.all().update(_custom_field_data=JSONRemove("_custom_field_data", "a"))
|
|
82
|
+
for mfr in Manufacturer.objects.all():
|
|
83
|
+
self.assertNotIn("a", mfr._custom_field_data)
|
|
84
|
+
self.assertIn("b", mfr._custom_field_data)
|
|
85
|
+
for mfr in Manufacturer.objects.filter(name__istartswith="a"):
|
|
86
|
+
self.assertEqual("hello", mfr._custom_field_data["b"])
|
|
87
|
+
for mfr in Manufacturer.objects.exclude(name__istartswith="a"):
|
|
88
|
+
self.assertEqual("world", mfr._custom_field_data["b"])
|
|
89
|
+
|
|
90
|
+
# Subsets should be updateable
|
|
91
|
+
with self.assertNumQueries(1):
|
|
92
|
+
Manufacturer.objects.filter(name__istartswith="a").update(
|
|
93
|
+
_custom_field_data=JSONRemove("_custom_field_data", "b")
|
|
94
|
+
)
|
|
95
|
+
for mfr in Manufacturer.objects.all():
|
|
96
|
+
self.assertNotIn("a", mfr._custom_field_data)
|
|
97
|
+
for mfr in Manufacturer.objects.filter(name__istartswith="a"):
|
|
98
|
+
self.assertNotIn("b", mfr._custom_field_data)
|
|
99
|
+
for mfr in Manufacturer.objects.exclude(name__istartswith="a"):
|
|
100
|
+
self.assertIn("b", mfr._custom_field_data)
|
|
101
|
+
self.assertEqual("world", mfr._custom_field_data["b"])
|
|
102
|
+
|
|
103
|
+
# Non-homogeneous data should be updatable
|
|
104
|
+
with self.assertNumQueries(1):
|
|
105
|
+
Manufacturer.objects.all().update(_custom_field_data=JSONRemove("_custom_field_data", "b"))
|
|
106
|
+
for mfr in Manufacturer.objects.all():
|
|
107
|
+
self.assertNotIn("a", mfr._custom_field_data)
|
|
108
|
+
self.assertNotIn("b", mfr._custom_field_data)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{% extends 'dcim/device_component_add.html' %}
|
|
2
|
+
|
|
3
|
+
{% block javascript %}
|
|
4
|
+
{{ block.super }}
|
|
5
|
+
<script>
|
|
6
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
7
|
+
var position_field = document.getElementById('id_position_pattern');
|
|
8
|
+
var source_arr = position_field.getAttribute('source').split(" ");
|
|
9
|
+
var length = position_field.getAttribute('maxlength');
|
|
10
|
+
position_field.setAttribute('_changed', Boolean(position_field.value))
|
|
11
|
+
position_field.addEventListener('change', function() {
|
|
12
|
+
position_field.setAttribute('_changed', Boolean(position_field.value))
|
|
13
|
+
});
|
|
14
|
+
function repopulate() {
|
|
15
|
+
let str = "";
|
|
16
|
+
for (source_str of source_arr) {
|
|
17
|
+
if (str != "") {
|
|
18
|
+
str += " ";
|
|
19
|
+
}
|
|
20
|
+
let source_id = 'id_' + source_str;
|
|
21
|
+
let source = document.getElementById(source_id)
|
|
22
|
+
str += source.value;
|
|
23
|
+
}
|
|
24
|
+
position_field.value = str.slice(0, length ? length : 255);
|
|
25
|
+
};
|
|
26
|
+
for (source_str of source_arr) {
|
|
27
|
+
let source_id = 'id_' + source_str;
|
|
28
|
+
let source = document.getElementById(source_id);
|
|
29
|
+
source.addEventListener('keyup', function() {
|
|
30
|
+
if (position_field && position_field.getAttribute('_changed')=="false") {
|
|
31
|
+
repopulate();
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
document.getElementsByClassName('reslugify')[0].addEventListener('click', repopulate);
|
|
36
|
+
document.getElementsByClassName('reslugify')[0].setAttribute('data-original-title', "Regenerate position");
|
|
37
|
+
});
|
|
38
|
+
</script>
|
|
39
|
+
{% endblock javascript %}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{% extends 'generic/object_create.html' %}
|
|
2
|
+
|
|
3
|
+
{% block javascript %}
|
|
4
|
+
{{ block.super }}
|
|
5
|
+
<script>
|
|
6
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
7
|
+
var position_field = document.getElementById('id_position');
|
|
8
|
+
var source_arr = position_field.getAttribute('source').split(" ");
|
|
9
|
+
var length = position_field.getAttribute('maxlength');
|
|
10
|
+
position_field.setAttribute('_changed', Boolean(position_field.value))
|
|
11
|
+
position_field.addEventListener('change', function() {
|
|
12
|
+
position_field.setAttribute('_changed', Boolean(position_field.value))
|
|
13
|
+
});
|
|
14
|
+
function repopulate() {
|
|
15
|
+
let str = "";
|
|
16
|
+
for (source_str of source_arr) {
|
|
17
|
+
if (str != "") {
|
|
18
|
+
str += " ";
|
|
19
|
+
}
|
|
20
|
+
let source_id = 'id_' + source_str;
|
|
21
|
+
let source = document.getElementById(source_id)
|
|
22
|
+
str += source.value;
|
|
23
|
+
}
|
|
24
|
+
position_field.value = str.slice(0, length ? length : 255);
|
|
25
|
+
};
|
|
26
|
+
for (source_str of source_arr) {
|
|
27
|
+
let source_id = 'id_' + source_str;
|
|
28
|
+
let source = document.getElementById(source_id);
|
|
29
|
+
source.addEventListener('keyup', function() {
|
|
30
|
+
if (position_field && position_field.getAttribute('_changed')=="false") {
|
|
31
|
+
repopulate();
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
document.getElementsByClassName('reslugify')[0].addEventListener('click', repopulate);
|
|
36
|
+
document.getElementsByClassName('reslugify')[0].setAttribute('data-original-title', "Regenerate position");
|
|
37
|
+
});
|
|
38
|
+
</script>
|
|
39
|
+
{% endblock javascript %}
|
nautobot/dcim/views.py
CHANGED
|
@@ -3241,7 +3241,7 @@ class ModuleBayUIViewSet(ModuleBayCommonViewSetMixin, NautobotUIViewSet):
|
|
|
3241
3241
|
model_form_class = forms.ModuleBayForm
|
|
3242
3242
|
serializer_class = serializers.ModuleBaySerializer
|
|
3243
3243
|
table_class = tables.ModuleBayTable
|
|
3244
|
-
create_template_name = "dcim/
|
|
3244
|
+
create_template_name = "dcim/modulebay_create.html"
|
|
3245
3245
|
|
|
3246
3246
|
def get_extra_context(self, request, instance):
|
|
3247
3247
|
if instance:
|
|
@@ -40,15 +40,7 @@ class CustomFieldDefaultValues:
|
|
|
40
40
|
class CustomFieldsDataField(Field):
|
|
41
41
|
@property
|
|
42
42
|
def custom_field_keys(self):
|
|
43
|
-
|
|
44
|
-
Cache CustomField keys assigned to this model to avoid redundant database queries
|
|
45
|
-
"""
|
|
46
|
-
if not hasattr(self, "_custom_field_keys"):
|
|
47
|
-
content_type = ContentType.objects.get_for_model(self.parent.Meta.model)
|
|
48
|
-
self._custom_field_keys = CustomField.objects.filter(content_types=content_type).values_list(
|
|
49
|
-
"key", flat=True
|
|
50
|
-
)
|
|
51
|
-
return self._custom_field_keys
|
|
43
|
+
return CustomField.objects.keys_for_model(self.parent.Meta.model)
|
|
52
44
|
|
|
53
45
|
def to_representation(self, obj):
|
|
54
46
|
return {key: obj.get(key) for key in self.custom_field_keys}
|
|
@@ -58,7 +50,8 @@ class CustomFieldsDataField(Field):
|
|
|
58
50
|
|
|
59
51
|
# Discard any entries in data that do not align with actual CustomFields - this matches the REST API behavior
|
|
60
52
|
# for top-level serializer fields that do not exist or are not writable
|
|
61
|
-
|
|
53
|
+
custom_field_keys = self.custom_field_keys
|
|
54
|
+
data = {key: value for key, value in data.items() if key in custom_field_keys}
|
|
62
55
|
|
|
63
56
|
# If updating an existing instance, start with existing _custom_field_data
|
|
64
57
|
if self.parent.instance:
|
|
@@ -204,14 +204,34 @@ def web_request_context(
|
|
|
204
204
|
yield request
|
|
205
205
|
finally:
|
|
206
206
|
jobs_reloaded = False
|
|
207
|
+
# In bulk operations, we are performing the same action (create/update/delete) on the same content-type.
|
|
208
|
+
# Save some repeated database queries by reusing the same evaluated querysets where applicable:
|
|
209
|
+
jobhook_queryset = None
|
|
210
|
+
webhook_queryset = None
|
|
211
|
+
last_action = None
|
|
212
|
+
last_content_type = None
|
|
207
213
|
# enqueue jobhooks and webhooks, use change_context.change_id in case change_id was not supplied
|
|
208
214
|
for object_change in (
|
|
209
|
-
ObjectChange.objects.
|
|
215
|
+
ObjectChange.objects.select_related("changed_object_type", "user")
|
|
216
|
+
.filter(request_id=change_context.change_id)
|
|
217
|
+
.order_by("time") # default ordering is -time but we want oldest first not newest first
|
|
218
|
+
.iterator()
|
|
210
219
|
):
|
|
220
|
+
if object_change.action != last_action or object_change.changed_object_type != last_content_type:
|
|
221
|
+
jobhook_queryset = None
|
|
222
|
+
webhook_queryset = None
|
|
223
|
+
|
|
211
224
|
if context != ObjectChangeEventContextChoices.CONTEXT_JOB_HOOK:
|
|
212
225
|
# Make sure JobHooks are up to date (only once) before calling them
|
|
213
|
-
|
|
214
|
-
|
|
226
|
+
did_reload_jobs, jobhook_queryset = enqueue_job_hooks(
|
|
227
|
+
object_change, may_reload_jobs=(not jobs_reloaded), jobhook_queryset=jobhook_queryset
|
|
228
|
+
)
|
|
229
|
+
if did_reload_jobs:
|
|
230
|
+
jobs_reloaded = True
|
|
231
|
+
|
|
232
|
+
webhook_queryset = enqueue_webhooks(object_change, webhook_queryset=webhook_queryset)
|
|
233
|
+
last_action = object_change.action
|
|
234
|
+
last_content_type = object_change.changed_object_type
|
|
215
235
|
|
|
216
236
|
|
|
217
237
|
@contextmanager
|
nautobot/extras/jobs.py
CHANGED
|
@@ -1144,41 +1144,47 @@ def run_job(self, job_class_path, *args, **kwargs):
|
|
|
1144
1144
|
raise
|
|
1145
1145
|
|
|
1146
1146
|
|
|
1147
|
-
def enqueue_job_hooks(object_change, may_reload_jobs=True):
|
|
1147
|
+
def enqueue_job_hooks(object_change, may_reload_jobs=True, jobhook_queryset=None):
|
|
1148
1148
|
"""
|
|
1149
1149
|
Find job hook(s) assigned to this changed object type + action and enqueue them to be processed.
|
|
1150
1150
|
|
|
1151
|
+
Args:
|
|
1152
|
+
object_change (ObjectChange): The change that may trigger JobHooks to execute.
|
|
1153
|
+
may_reload_jobs (bool): Whether to reload JobHook source code from disk to guarantee up-to-date code.
|
|
1154
|
+
jobhook_queryset (QuerySet): Previously retrieved set of JobHooks to potentially enqueue
|
|
1155
|
+
|
|
1151
1156
|
Returns:
|
|
1152
|
-
|
|
1157
|
+
result (tuple[bool, QuerySet]): whether Jobs were reloaded here, and the jobhooks that were considered
|
|
1153
1158
|
"""
|
|
1154
1159
|
jobs_reloaded = False
|
|
1155
1160
|
|
|
1156
1161
|
# Job hooks cannot trigger other job hooks
|
|
1157
1162
|
if object_change.change_context == ObjectChangeEventContextChoices.CONTEXT_JOB_HOOK:
|
|
1158
|
-
return jobs_reloaded
|
|
1163
|
+
return jobs_reloaded, jobhook_queryset
|
|
1159
1164
|
|
|
1160
1165
|
# Determine whether this type of object supports job hooks
|
|
1161
1166
|
content_type = object_change.changed_object_type
|
|
1162
1167
|
if content_type not in change_logged_models_queryset():
|
|
1163
|
-
return jobs_reloaded
|
|
1168
|
+
return jobs_reloaded, jobhook_queryset
|
|
1164
1169
|
|
|
1165
1170
|
# Retrieve any applicable job hooks
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1171
|
+
if jobhook_queryset is None:
|
|
1172
|
+
action_flag = {
|
|
1173
|
+
ObjectChangeActionChoices.ACTION_CREATE: "type_create",
|
|
1174
|
+
ObjectChangeActionChoices.ACTION_UPDATE: "type_update",
|
|
1175
|
+
ObjectChangeActionChoices.ACTION_DELETE: "type_delete",
|
|
1176
|
+
}[object_change.action]
|
|
1177
|
+
jobhook_queryset = JobHook.objects.filter(content_types=content_type, enabled=True, **{action_flag: True})
|
|
1172
1178
|
|
|
1173
|
-
if not
|
|
1174
|
-
return jobs_reloaded
|
|
1179
|
+
if not jobhook_queryset: # not .exists() as we *want* to populate the queryset cache
|
|
1180
|
+
return jobs_reloaded, jobhook_queryset
|
|
1175
1181
|
|
|
1176
1182
|
# Enqueue the jobs related to the job_hooks
|
|
1177
1183
|
if may_reload_jobs:
|
|
1178
1184
|
get_jobs(reload=True)
|
|
1179
1185
|
jobs_reloaded = True
|
|
1180
1186
|
|
|
1181
|
-
for job_hook in
|
|
1187
|
+
for job_hook in jobhook_queryset:
|
|
1182
1188
|
job_model = job_hook.job
|
|
1183
1189
|
if not job_model.installed or not job_model.enabled:
|
|
1184
1190
|
logger.warning(
|
|
@@ -1189,4 +1195,4 @@ def enqueue_job_hooks(object_change, may_reload_jobs=True):
|
|
|
1189
1195
|
else:
|
|
1190
1196
|
JobResult.enqueue_job(job_model, object_change.user, object_change=object_change.pk)
|
|
1191
1197
|
|
|
1192
|
-
return jobs_reloaded
|
|
1198
|
+
return jobs_reloaded, jobhook_queryset
|
|
@@ -391,6 +391,18 @@ class CustomFieldManager(BaseManager.from_queryset(RestrictedQuerySet)):
|
|
|
391
391
|
|
|
392
392
|
get_for_model.cache_key_prefix = "nautobot.extras.customfield.get_for_model"
|
|
393
393
|
|
|
394
|
+
def keys_for_model(self, model):
|
|
395
|
+
"""Return list of all keys for CustomFields assigned to the given model."""
|
|
396
|
+
concrete_model = model._meta.concrete_model
|
|
397
|
+
cache_key = f"{self.keys_for_model.cache_key_prefix}.{concrete_model._meta.label_lower}"
|
|
398
|
+
keys = cache.get(cache_key)
|
|
399
|
+
if keys is None:
|
|
400
|
+
keys = list(self.get_for_model(model).values_list("key", flat=True))
|
|
401
|
+
cache.set(cache_key, keys)
|
|
402
|
+
return keys
|
|
403
|
+
|
|
404
|
+
keys_for_model.cache_key_prefix = "nautobot.extras.customfield.keys_for_model"
|
|
405
|
+
|
|
394
406
|
|
|
395
407
|
@extras_features("webhooks")
|
|
396
408
|
class CustomField(
|
nautobot/extras/signals.py
CHANGED
|
@@ -91,6 +91,8 @@ def invalidate_models_cache(sender, **kwargs):
|
|
|
91
91
|
with contextlib.suppress(redis.exceptions.ConnectionError):
|
|
92
92
|
# TODO: *maybe* target more narrowly, e.g. only clear the cache for specific related content-types?
|
|
93
93
|
cache.delete_pattern(f"{manager.get_for_model.cache_key_prefix}.*")
|
|
94
|
+
if hasattr(manager, "keys_for_model"):
|
|
95
|
+
cache.delete_pattern(f"{manager.keys_for_model.cache_key_prefix}.*")
|
|
94
96
|
|
|
95
97
|
|
|
96
98
|
@receiver(post_save, sender=Relationship)
|