nautobot 2.3.12__py3-none-any.whl → 2.3.14__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/circuits/tables.py +2 -1
- nautobot/core/api/serializers.py +1 -0
- nautobot/core/celery/log.py +4 -4
- nautobot/core/filters.py +2 -0
- nautobot/core/jobs/cleanup.py +47 -11
- nautobot/core/models/tree_queries.py +5 -2
- nautobot/core/settings.py +1 -1
- nautobot/core/tables.py +60 -10
- nautobot/core/templates/search.html +7 -0
- nautobot/core/templatetags/helpers.py +7 -1
- nautobot/core/testing/api.py +5 -1
- nautobot/core/testing/filters.py +20 -5
- nautobot/core/tests/test_api.py +20 -0
- nautobot/core/tests/test_csv.py +25 -3
- nautobot/core/tests/test_utils.py +8 -0
- nautobot/core/utils/lookup.py +11 -8
- nautobot/dcim/api/views.py +3 -0
- nautobot/dcim/filters/__init__.py +26 -1
- nautobot/dcim/forms.py +10 -5
- nautobot/dcim/models/devices.py +1 -0
- nautobot/dcim/tests/test_filters.py +33 -0
- nautobot/dcim/tests/test_forms.py +51 -2
- nautobot/dcim/tests/test_views.py +6 -0
- nautobot/extras/api/serializers.py +1 -0
- nautobot/extras/api/views.py +2 -0
- nautobot/extras/forms/forms.py +2 -0
- nautobot/extras/forms/mixins.py +10 -2
- nautobot/extras/group_sync.py +3 -3
- nautobot/extras/jobs.py +6 -4
- nautobot/extras/plugins/__init__.py +13 -2
- nautobot/extras/tests/test_views.py +2 -0
- nautobot/ipam/lookups.py +101 -62
- nautobot/ipam/models.py +62 -11
- nautobot/ipam/tables.py +20 -6
- nautobot/ipam/tests/test_api.py +68 -1
- nautobot/ipam/tests/test_models.py +41 -0
- nautobot/ipam/tests/test_querysets.py +49 -1
- nautobot/ipam/utils/__init__.py +24 -0
- nautobot/ipam/views.py +61 -68
- 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 +7 -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 +197 -5
- nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +2 -2
- 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 +16 -2
- 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 +23 -4
- 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 +154 -140
- 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 +142 -118
- nautobot/project-static/docs/development/jobs/migration/from-v1.html +1 -1
- nautobot/project-static/docs/index.html +1 -1
- nautobot/project-static/docs/objects.inv +0 -0
- 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 +614 -179
- 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 +553 -200
- 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 +3 -3
- 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/users/api/serializers.py +1 -0
- nautobot/virtualization/filters.py +19 -2
- nautobot/virtualization/tests/test_filters.py +9 -0
- {nautobot-2.3.12.dist-info → nautobot-2.3.14.dist-info}/METADATA +3 -3
- {nautobot-2.3.12.dist-info → nautobot-2.3.14.dist-info}/RECORD +324 -324
- {nautobot-2.3.12.dist-info → nautobot-2.3.14.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.3.12.dist-info → nautobot-2.3.14.dist-info}/NOTICE +0 -0
- {nautobot-2.3.12.dist-info → nautobot-2.3.14.dist-info}/WHEEL +0 -0
- {nautobot-2.3.12.dist-info → nautobot-2.3.14.dist-info}/entry_points.txt +0 -0
nautobot/circuits/tables.py
CHANGED
|
@@ -110,6 +110,7 @@ class CircuitTable(StatusTableMixin, BaseTable):
|
|
|
110
110
|
pk = ToggleColumn()
|
|
111
111
|
cid = tables.LinkColumn(verbose_name="ID")
|
|
112
112
|
provider = tables.Column(linkify=True)
|
|
113
|
+
circuit_type = tables.Column(linkify=True)
|
|
113
114
|
tenant = TenantColumn()
|
|
114
115
|
tags = TagColumn(url_name="circuits:circuit_list")
|
|
115
116
|
|
|
@@ -146,7 +147,7 @@ class CircuitTable(StatusTableMixin, BaseTable):
|
|
|
146
147
|
"pk",
|
|
147
148
|
"cid",
|
|
148
149
|
"provider",
|
|
149
|
-
"
|
|
150
|
+
"circuit_type",
|
|
150
151
|
"status",
|
|
151
152
|
"tenant",
|
|
152
153
|
"circuit_termination_a",
|
nautobot/core/api/serializers.py
CHANGED
|
@@ -134,6 +134,7 @@ class BaseModelSerializer(OptInFieldsMixin, serializers.HyperlinkedModelSerializ
|
|
|
134
134
|
|
|
135
135
|
serializer_related_field = NautobotHyperlinkedRelatedField
|
|
136
136
|
|
|
137
|
+
id = serializers.UUIDField(read_only=False, default=serializers.CreateOnlyDefault(uuid.uuid4))
|
|
137
138
|
display = serializers.SerializerMethodField(read_only=True, help_text="Human friendly display value")
|
|
138
139
|
object_type = ObjectTypeField()
|
|
139
140
|
# composite_key = serializers.SerializerMethodField() # TODO: Revisit if we reintroduce composite keys
|
nautobot/core/celery/log.py
CHANGED
|
@@ -11,6 +11,10 @@ class NautobotDatabaseHandler(logging.Handler):
|
|
|
11
11
|
if current_task is None:
|
|
12
12
|
return
|
|
13
13
|
|
|
14
|
+
# Skip recording the log entry if it has been marked as such
|
|
15
|
+
if getattr(record, "skip_db_logging", False):
|
|
16
|
+
return
|
|
17
|
+
|
|
14
18
|
from nautobot.extras.models.jobs import JobResult
|
|
15
19
|
|
|
16
20
|
try:
|
|
@@ -24,10 +28,6 @@ class NautobotDatabaseHandler(logging.Handler):
|
|
|
24
28
|
# JobResult.DoesNotExist - because we might not have a JobResult with that ID
|
|
25
29
|
return
|
|
26
30
|
|
|
27
|
-
# Skip recording the log entry if it has been marked as such
|
|
28
|
-
if getattr(record, "skip_db_logging", False):
|
|
29
|
-
return
|
|
30
|
-
|
|
31
31
|
job_result.log(
|
|
32
32
|
message=record.message,
|
|
33
33
|
level_choice=record.levelname.lower(),
|
nautobot/core/filters.py
CHANGED
|
@@ -618,6 +618,7 @@ class BaseFilterSet(django_filters.FilterSet):
|
|
|
618
618
|
@staticmethod
|
|
619
619
|
def _get_filter_lookup_dict(existing_filter):
|
|
620
620
|
# Choose the lookup expression map based on the filter type
|
|
621
|
+
|
|
621
622
|
if isinstance(
|
|
622
623
|
existing_filter,
|
|
623
624
|
(
|
|
@@ -637,6 +638,7 @@ class BaseFilterSet(django_filters.FilterSet):
|
|
|
637
638
|
(
|
|
638
639
|
django_filters.ModelChoiceFilter,
|
|
639
640
|
django_filters.ModelMultipleChoiceFilter,
|
|
641
|
+
MultiValueUUIDFilter,
|
|
640
642
|
TagFilter,
|
|
641
643
|
TreeNodeMultipleChoiceFilter,
|
|
642
644
|
),
|
nautobot/core/jobs/cleanup.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from datetime import timedelta
|
|
2
2
|
|
|
3
3
|
from django.core.exceptions import PermissionDenied
|
|
4
|
+
from django.db.models import CASCADE
|
|
4
5
|
from django.db.models.signals import pre_delete
|
|
5
6
|
from django.utils import timezone
|
|
6
7
|
|
|
@@ -48,6 +49,27 @@ class LogsCleanup(Job):
|
|
|
48
49
|
description = "Delete ObjectChange and/or JobResult/JobLogEntry records older than a specified cutoff."
|
|
49
50
|
has_sensitive_variables = False
|
|
50
51
|
|
|
52
|
+
def recursive_delete_with_cascade(self, queryset, deletion_summary):
|
|
53
|
+
"""
|
|
54
|
+
Recursively deletes all related objects with CASCADE for a given queryset.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
queryset (QuerySet): The queryset of objects to delete.
|
|
58
|
+
deletion_summary (dict): A dictionary to store the count of deleted objects for each model.
|
|
59
|
+
"""
|
|
60
|
+
related_objects = queryset.model._meta.related_objects
|
|
61
|
+
queryset = queryset.only("id")
|
|
62
|
+
|
|
63
|
+
for related_object in related_objects:
|
|
64
|
+
if related_object.on_delete is CASCADE:
|
|
65
|
+
related_model = related_object.related_model
|
|
66
|
+
related_field_name = related_object.field.name
|
|
67
|
+
cascade_queryset = related_model.objects.filter(**{f"{related_field_name}__id__in": queryset})
|
|
68
|
+
self.recursive_delete_with_cascade(cascade_queryset, deletion_summary)
|
|
69
|
+
_, deleted_dict = queryset.delete()
|
|
70
|
+
deletion_summary.update(deleted_dict)
|
|
71
|
+
return deletion_summary
|
|
72
|
+
|
|
51
73
|
def run(self, *, cleanup_types, max_age=None):
|
|
52
74
|
if max_age in (None, ""):
|
|
53
75
|
max_age = get_settings_or_config("CHANGELOG_RETENTION")
|
|
@@ -77,22 +99,36 @@ class LogsCleanup(Job):
|
|
|
77
99
|
|
|
78
100
|
if CleanupTypes.JOB_RESULT in cleanup_types:
|
|
79
101
|
self.logger.info("Deleting JobResult records prior to %s", cutoff)
|
|
80
|
-
|
|
102
|
+
queryset = JobResult.objects.restrict(self.user, "delete").filter(date_done__lt=cutoff)
|
|
103
|
+
deletion_summary = {}
|
|
104
|
+
self.recursive_delete_with_cascade(queryset, deletion_summary)
|
|
81
105
|
result.setdefault("extras.JobResult", 0)
|
|
82
106
|
result.setdefault("extras.JobLogEntry", 0)
|
|
83
|
-
result.update(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
107
|
+
result.update(deletion_summary)
|
|
108
|
+
|
|
109
|
+
for modelname, count in deletion_summary.items():
|
|
110
|
+
self.logger.info(
|
|
111
|
+
"As part of deleting %d JobResult records, also deleted %d related %s records",
|
|
112
|
+
result["extras.JobResult"],
|
|
113
|
+
count,
|
|
114
|
+
modelname,
|
|
115
|
+
)
|
|
89
116
|
|
|
90
117
|
if CleanupTypes.OBJECT_CHANGE in cleanup_types:
|
|
91
118
|
self.logger.info("Deleting ObjectChange records prior to %s", cutoff)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
119
|
+
queryset = ObjectChange.objects.restrict(self.user, "delete").filter(time__lt=cutoff)
|
|
120
|
+
deletion_summary = {}
|
|
121
|
+
self.recursive_delete_with_cascade(queryset, deletion_summary)
|
|
122
|
+
result.setdefault("extras.ObjectChange", 0)
|
|
123
|
+
result.update(deletion_summary)
|
|
124
|
+
|
|
125
|
+
for modelname, count in deletion_summary.items():
|
|
126
|
+
self.logger.info(
|
|
127
|
+
"As part of deleting %d ObjectChange records, also deleted %d related %s records",
|
|
128
|
+
result["extras.ObjectChange"],
|
|
129
|
+
count,
|
|
130
|
+
modelname,
|
|
131
|
+
)
|
|
96
132
|
return result
|
|
97
133
|
finally:
|
|
98
134
|
# Be sure to clean up after ourselves!
|
|
@@ -111,8 +111,11 @@ class TreeModel(TreeNode):
|
|
|
111
111
|
if display_str:
|
|
112
112
|
return display_str
|
|
113
113
|
try:
|
|
114
|
-
if self.
|
|
115
|
-
|
|
114
|
+
if self.parent_id is not None:
|
|
115
|
+
parent_display_str = cache.get(cache_key.replace(str(self.id), str(self.parent_id)), "")
|
|
116
|
+
if not parent_display_str:
|
|
117
|
+
parent_display_str = self.parent.display
|
|
118
|
+
display_str = parent_display_str + " → "
|
|
116
119
|
except self.DoesNotExist:
|
|
117
120
|
# Expected to occur at times during bulk-delete operations
|
|
118
121
|
pass
|
nautobot/core/settings.py
CHANGED
|
@@ -225,7 +225,7 @@ if SSO_ENABLE_GROUP_SYNC:
|
|
|
225
225
|
"social_core.pipeline.social_auth.associate_user",
|
|
226
226
|
"social_core.pipeline.social_auth.load_extra_data",
|
|
227
227
|
"social_core.pipeline.user.user_details",
|
|
228
|
-
"nautobot.extras.group_sync",
|
|
228
|
+
"nautobot.extras.group_sync.group_sync",
|
|
229
229
|
)
|
|
230
230
|
# OAuth2/OIDC claim where the list of groups the authenticating user is a part of
|
|
231
231
|
SSO_CLAIMS_GROUP = os.getenv("NAUTOBOT_SSO_CLAIMS_GROUP", "groups")
|
nautobot/core/tables.py
CHANGED
|
@@ -14,7 +14,8 @@ from django.utils.http import urlencode
|
|
|
14
14
|
from django.utils.safestring import mark_safe
|
|
15
15
|
from django.utils.text import Truncator
|
|
16
16
|
import django_tables2
|
|
17
|
-
from django_tables2.data import TableQuerysetData
|
|
17
|
+
from django_tables2.data import TableData, TableQuerysetData
|
|
18
|
+
from django_tables2.rows import BoundRows
|
|
18
19
|
from django_tables2.utils import Accessor, OrderBy, OrderByTuple
|
|
19
20
|
from tree_queries.models import TreeNode
|
|
20
21
|
|
|
@@ -46,8 +47,24 @@ class BaseTable(django_tables2.Table):
|
|
|
46
47
|
user=None,
|
|
47
48
|
hide_hierarchy_ui=False,
|
|
48
49
|
order_by=None,
|
|
50
|
+
data_transform_callback=None,
|
|
49
51
|
**kwargs,
|
|
50
52
|
):
|
|
53
|
+
"""
|
|
54
|
+
Instantiate a BaseTable.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
*args (list, optional): Passed through to django_tables2.Table
|
|
58
|
+
table_changes_pending (bool): TODO
|
|
59
|
+
saved_view (SavedView, optional): TODO
|
|
60
|
+
user (User, optional): TODO
|
|
61
|
+
hide_hierarchy_ui (bool): Whether to display or hide hierarchy indentation of nested objects.
|
|
62
|
+
order_by (list, optional): Field(s) to sort by
|
|
63
|
+
data_transform_callback (function, optional): A function that takes the given `data` as an input and
|
|
64
|
+
returns new data. Runs after all of the queryset auto-optimization performed by this class.
|
|
65
|
+
Used for example in IPAM views to inject "fake" records for "available" Prefixes, IPAddresses, or VLANs.
|
|
66
|
+
**kwargs (dict, optional): Passed through to django_tables2.Table
|
|
67
|
+
"""
|
|
51
68
|
# Add custom field columns
|
|
52
69
|
model = self._meta.model
|
|
53
70
|
|
|
@@ -89,6 +106,14 @@ class BaseTable(django_tables2.Table):
|
|
|
89
106
|
# Init table
|
|
90
107
|
super().__init__(*args, order_by=order_by, **kwargs)
|
|
91
108
|
|
|
109
|
+
if not isinstance(self.data, TableQuerysetData):
|
|
110
|
+
# LinkedCountColumns don't work properly if the data is a list of dicts instead of a queryset,
|
|
111
|
+
# as they rely on a `queryset.annotate()` call to gather the data.
|
|
112
|
+
self.exclude = [
|
|
113
|
+
*self.exclude,
|
|
114
|
+
*[column.name for column in self.columns if isinstance(column.column, LinkedCountColumn)],
|
|
115
|
+
]
|
|
116
|
+
|
|
92
117
|
# Don't show hierarchy if we're sorted
|
|
93
118
|
if order_by is not None and hide_hierarchy_ui is None:
|
|
94
119
|
hide_hierarchy_ui = True
|
|
@@ -122,7 +147,7 @@ class BaseTable(django_tables2.Table):
|
|
|
122
147
|
columns = user.get_config(f"tables.{self.__class__.__name__}.columns")
|
|
123
148
|
if columns:
|
|
124
149
|
for name, column in self.base_columns.items():
|
|
125
|
-
if name in columns:
|
|
150
|
+
if name in columns and name not in self.exclude:
|
|
126
151
|
self.columns.show(name)
|
|
127
152
|
else:
|
|
128
153
|
self.columns.hide(name)
|
|
@@ -161,7 +186,8 @@ class BaseTable(django_tables2.Table):
|
|
|
161
186
|
logger.error("Couldn't find model for %s", column.column.viewname)
|
|
162
187
|
continue
|
|
163
188
|
reverse_lookup = column.column.reverse_lookup or next(iter(column.column.url_params.keys()))
|
|
164
|
-
|
|
189
|
+
distinct = column.column.distinct
|
|
190
|
+
count_fields.append((column.name, column_model, reverse_lookup, distinct))
|
|
165
191
|
try:
|
|
166
192
|
lookup = column.column.lookup or get_related_field_for_models(model, column_model).name
|
|
167
193
|
# For some reason get_related_field_for_models(Tag, DynamicGroup) gives a M2M with the name
|
|
@@ -173,7 +199,8 @@ class BaseTable(django_tables2.Table):
|
|
|
173
199
|
if lookup is not None:
|
|
174
200
|
# Also attempt to prefetch the first matching record for display - see LinkedCountColumn
|
|
175
201
|
prefetch_fields.append(
|
|
176
|
-
|
|
202
|
+
# Use order_by() because we don't care about ordering here and it's potentially expensive
|
|
203
|
+
Prefetch(lookup, column_model.objects.order_by()[:1], to_attr=f"{lookup}_list")
|
|
177
204
|
)
|
|
178
205
|
continue
|
|
179
206
|
|
|
@@ -256,24 +283,34 @@ class BaseTable(django_tables2.Table):
|
|
|
256
283
|
)
|
|
257
284
|
|
|
258
285
|
if count_fields:
|
|
259
|
-
for column_name, column_model, lookup_name in count_fields:
|
|
286
|
+
for column_name, column_model, lookup_name, distinct in count_fields:
|
|
260
287
|
if hasattr(queryset.first(), column_name):
|
|
261
288
|
continue
|
|
262
289
|
try:
|
|
263
290
|
logger.debug(
|
|
264
|
-
"Applying .annotate(%s=count_related(%s, %r) to %s QuerySet",
|
|
291
|
+
"Applying .annotate(%s=count_related(%s, %r, distinct=%s) to %s QuerySet",
|
|
265
292
|
column_name,
|
|
266
293
|
column_model.__name__,
|
|
267
294
|
lookup_name,
|
|
295
|
+
distinct,
|
|
268
296
|
model.__name__,
|
|
269
297
|
)
|
|
270
|
-
queryset = queryset.annotate(
|
|
298
|
+
queryset = queryset.annotate(
|
|
299
|
+
**{column_name: count_related(column_model, lookup_name, distinct=distinct)}
|
|
300
|
+
)
|
|
271
301
|
except FieldError:
|
|
272
302
|
# No error message logged here as the above is *very much* best-effort
|
|
273
303
|
pass
|
|
274
304
|
|
|
275
305
|
self.data.data = queryset
|
|
276
306
|
|
|
307
|
+
# TODO: it would be better if we could apply this transformation and the above queryset optimizations
|
|
308
|
+
# **before** calling super().__init__(), but the current implementation works for now, though inelegant.
|
|
309
|
+
if data_transform_callback is not None:
|
|
310
|
+
self.data = TableData.from_data(data_transform_callback(self.data.data))
|
|
311
|
+
self.data.set_table(self)
|
|
312
|
+
self.rows = BoundRows(data=self.data, table=self, pinned_data=self.pinned_data)
|
|
313
|
+
|
|
277
314
|
@property
|
|
278
315
|
def configurable_columns(self):
|
|
279
316
|
selected_columns = [
|
|
@@ -288,7 +325,7 @@ class BaseTable(django_tables2.Table):
|
|
|
288
325
|
|
|
289
326
|
@property
|
|
290
327
|
def visible_columns(self):
|
|
291
|
-
return [name for name in self.sequence if self.columns[name].visible]
|
|
328
|
+
return [name for name in self.sequence if self.columns[name].visible and name not in self.exclude]
|
|
292
329
|
|
|
293
330
|
@property
|
|
294
331
|
def order_by(self):
|
|
@@ -490,6 +527,7 @@ class LinkedCountColumn(django_tables2.Column):
|
|
|
490
527
|
TODO: this currently does *not* support nested lookups via `__`. That may be solvable in the future.
|
|
491
528
|
reverse_lookup (str, optional): The reverse lookup parameter to use to derive the count.
|
|
492
529
|
If not specified, the first key in `url_params` will be implicitly used as the `reverse_lookup` value.
|
|
530
|
+
distinct (bool, optional): Parameter passed through to `count_related()`.
|
|
493
531
|
**kwargs (dict, optional): As the parent Column class.
|
|
494
532
|
|
|
495
533
|
Examples:
|
|
@@ -514,21 +552,33 @@ class LinkedCountColumn(django_tables2.Column):
|
|
|
514
552
|
# We'd like to do the below but this module isn't currently smart enough to build the right Prefetch()
|
|
515
553
|
# for a nested lookup:
|
|
516
554
|
# lookup="circuit_terminations__circuit",
|
|
517
|
-
# For the count,
|
|
555
|
+
# For the count,
|
|
556
|
+
# .annotate(circuit_count=count_related(Circuit, "circuit_terminations__cloud_network", distinct=True))
|
|
518
557
|
reverse_lookup="circuit_terminations__cloud_network",
|
|
558
|
+
distinct=True,
|
|
519
559
|
verbose_name="Circuits",
|
|
520
560
|
)
|
|
521
561
|
```
|
|
522
562
|
"""
|
|
523
563
|
|
|
524
564
|
def __init__(
|
|
525
|
-
self,
|
|
565
|
+
self,
|
|
566
|
+
viewname,
|
|
567
|
+
*args,
|
|
568
|
+
view_kwargs=None,
|
|
569
|
+
url_params=None,
|
|
570
|
+
lookup=None,
|
|
571
|
+
reverse_lookup=None,
|
|
572
|
+
distinct=False,
|
|
573
|
+
default=None,
|
|
574
|
+
**kwargs,
|
|
526
575
|
):
|
|
527
576
|
self.viewname = viewname
|
|
528
577
|
self.lookup = lookup
|
|
529
578
|
self.view_kwargs = view_kwargs or {}
|
|
530
579
|
self.url_params = url_params
|
|
531
580
|
self.reverse_lookup = reverse_lookup or next(iter(url_params.keys()))
|
|
581
|
+
self.distinct = distinct
|
|
532
582
|
self.model = get_model_for_view_name(self.viewname)
|
|
533
583
|
super().__init__(*args, default=default, **kwargs)
|
|
534
584
|
|
|
@@ -795,7 +795,13 @@ def saved_view_modal(
|
|
|
795
795
|
"clear_view",
|
|
796
796
|
]
|
|
797
797
|
|
|
798
|
-
|
|
798
|
+
view_class = lookup.get_view_for_model(model, "List")
|
|
799
|
+
table_name = None
|
|
800
|
+
if hasattr(view_class, "table"):
|
|
801
|
+
table_name = view_class.table.__name__
|
|
802
|
+
if hasattr(view_class, "table_class"):
|
|
803
|
+
table_name = view_class.table_class.__name__
|
|
804
|
+
|
|
799
805
|
for param in non_filter_params:
|
|
800
806
|
if param == "saved_view":
|
|
801
807
|
current_saved_view_pk = filters_applied.pop(param, None)
|
nautobot/core/testing/api.py
CHANGED
|
@@ -707,7 +707,11 @@ class APIViewTestCases:
|
|
|
707
707
|
self.assertHttpStatus(response, status.HTTP_201_CREATED, csv_data)
|
|
708
708
|
# Note that create via CSV is always treated as a bulk-create, and so the response is always a list of dicts
|
|
709
709
|
new_instance = self._get_queryset().get(pk=response.data[0]["id"])
|
|
710
|
-
|
|
710
|
+
if isinstance(orig_pk, int):
|
|
711
|
+
self.assertNotEqual(new_instance.pk, orig_pk)
|
|
712
|
+
else:
|
|
713
|
+
# for our non-integer PKs, we're expecting the creation to respect the requested PK
|
|
714
|
+
self.assertEqual(new_instance.pk, orig_pk)
|
|
711
715
|
|
|
712
716
|
new_serializer = serializer_class(new_instance, context={"request": None})
|
|
713
717
|
new_data = new_serializer.data
|
nautobot/core/testing/filters.py
CHANGED
|
@@ -88,11 +88,26 @@ class FilterTestCases:
|
|
|
88
88
|
return self.filterset.declared_filters["q"].filter_predicates
|
|
89
89
|
|
|
90
90
|
def test_id(self):
|
|
91
|
-
"""Verify that the filterset supports filtering by id
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
91
|
+
"""Verify that the filterset supports filtering by id with only lookup `__n`."""
|
|
92
|
+
with self.subTest("Assert `id`"):
|
|
93
|
+
params = {"id": list(self.queryset.values_list("pk", flat=True)[:2])}
|
|
94
|
+
expected_queryset = self.queryset.filter(id__in=params["id"])
|
|
95
|
+
filterset = self.filterset(params, self.queryset)
|
|
96
|
+
self.assertTrue(filterset.is_valid())
|
|
97
|
+
self.assertQuerysetEqualAndNotEmpty(filterset.qs.order_by("id"), expected_queryset.order_by("id"))
|
|
98
|
+
|
|
99
|
+
with self.subTest("Assert negate lookup"):
|
|
100
|
+
params = {"id__n": list(self.queryset.values_list("pk", flat=True)[:2])}
|
|
101
|
+
expected_queryset = self.queryset.exclude(id__in=params["id__n"])
|
|
102
|
+
filterset = self.filterset(params, self.queryset)
|
|
103
|
+
self.assertTrue(filterset.is_valid())
|
|
104
|
+
self.assertQuerysetEqualAndNotEmpty(filterset.qs.order_by("id"), expected_queryset.order_by("id"))
|
|
105
|
+
|
|
106
|
+
with self.subTest("Assert invalid lookup"):
|
|
107
|
+
params = {"id__in": list(self.queryset.values_list("pk", flat=True)[:2])}
|
|
108
|
+
filterset = self.filterset(params, self.queryset)
|
|
109
|
+
self.assertFalse(filterset.is_valid())
|
|
110
|
+
self.assertIn("Unknown filter field", filterset.errors.as_text())
|
|
96
111
|
|
|
97
112
|
def test_invalid_filter(self):
|
|
98
113
|
"""Verify that the filterset reports as invalid when initialized with an unsupported filter parameter."""
|
nautobot/core/tests/test_api.py
CHANGED
|
@@ -3,6 +3,7 @@ from io import BytesIO, StringIO
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
5
|
from unittest import skip
|
|
6
|
+
import uuid
|
|
6
7
|
|
|
7
8
|
from constance import config
|
|
8
9
|
from constance.test import override_config
|
|
@@ -754,6 +755,25 @@ class WritableNestedSerializerTest(testing.APITestCase):
|
|
|
754
755
|
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
755
756
|
self.assertEqual(ipam_models.VLAN.objects.filter(name="Test VLAN 100").count(), 0)
|
|
756
757
|
|
|
758
|
+
def test_create_with_specified_id(self):
|
|
759
|
+
data = {
|
|
760
|
+
"id": str(uuid.uuid4()),
|
|
761
|
+
"vid": 400,
|
|
762
|
+
"name": "Test VLAN 400",
|
|
763
|
+
"status": self.statuses.first().pk,
|
|
764
|
+
"vlan_group": self.vlan_group1.pk,
|
|
765
|
+
}
|
|
766
|
+
url = reverse("ipam-api:vlan-list")
|
|
767
|
+
self.add_permissions("ipam.add_vlan")
|
|
768
|
+
|
|
769
|
+
response = self.client.post(url, data, format="json", **self.header)
|
|
770
|
+
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
|
771
|
+
self.assertEqual(str(response.data["vlan_group"]["url"]), self.absolute_api_url(self.vlan_group1))
|
|
772
|
+
self.assertEqual(str(response.data["id"]), data["id"])
|
|
773
|
+
vlan = ipam_models.VLAN.objects.get(pk=response.data["id"])
|
|
774
|
+
self.assertEqual(vlan.status, self.statuses.first())
|
|
775
|
+
self.assertEqual(vlan.vlan_group, self.vlan_group1)
|
|
776
|
+
|
|
757
777
|
|
|
758
778
|
class APIOrderingTestCase(testing.APITestCase):
|
|
759
779
|
"""
|
nautobot/core/tests/test_csv.py
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
import io
|
|
3
|
+
|
|
1
4
|
from django.contrib.contenttypes.models import ContentType
|
|
2
5
|
from django.test import override_settings, RequestFactory, TestCase
|
|
3
6
|
from django.urls import reverse
|
|
@@ -261,9 +264,28 @@ class CSVParsingRelatedTestCase(TestCase):
|
|
|
261
264
|
self.assertEqual(response.status_code, 200)
|
|
262
265
|
response_data = response.content.decode(response.charset)
|
|
263
266
|
|
|
264
|
-
#
|
|
265
|
-
|
|
266
|
-
|
|
267
|
+
# parse the csv data
|
|
268
|
+
csv_reader = csv.DictReader(response_data.splitlines())
|
|
269
|
+
# remove the 'id' column so that all the items are imported new
|
|
270
|
+
fieldnames = [field for field in csv_reader.fieldnames if field != "id"]
|
|
271
|
+
# read all entries into a list
|
|
272
|
+
response_csv = list(csv_reader)
|
|
273
|
+
|
|
274
|
+
# mutate our data for testing purposes
|
|
275
|
+
for row in response_csv:
|
|
276
|
+
if row["name"] == "TestDevice1":
|
|
277
|
+
row["name"] = "TestDevice3"
|
|
278
|
+
elif row["name"] == "TestDevice2":
|
|
279
|
+
row["name"] = ""
|
|
280
|
+
|
|
281
|
+
# prep our data to write out
|
|
282
|
+
with io.StringIO() as import_csv:
|
|
283
|
+
writer = csv.DictWriter(import_csv, fieldnames=fieldnames)
|
|
284
|
+
writer.writeheader()
|
|
285
|
+
for row in response_csv:
|
|
286
|
+
filtered_row = {key: row[key] for key in fieldnames}
|
|
287
|
+
writer.writerow(filtered_row)
|
|
288
|
+
data = {"csv_data": import_csv.getvalue()}
|
|
267
289
|
url = reverse("dcim:device_import")
|
|
268
290
|
response = self.client.post(url, data)
|
|
269
291
|
self.assertEqual(response.status_code, 200)
|
|
@@ -268,6 +268,14 @@ class GetFooForModelTest(TestCase):
|
|
|
268
268
|
lookup.get_model_for_view_name("unknown:plugins:example_app:examplemodel_list")
|
|
269
269
|
self.assertEqual(str(err.exception), "Unexpected View Name: unknown:plugins:example_app:examplemodel_list")
|
|
270
270
|
|
|
271
|
+
def test_get_table_class_string_from_view_name(self):
|
|
272
|
+
# Testing UIViewSet
|
|
273
|
+
self.assertEqual(lookup.get_table_class_string_from_view_name("circuits:circuit_list"), "CircuitTable")
|
|
274
|
+
# Testing Legacy View
|
|
275
|
+
self.assertEqual(lookup.get_table_class_string_from_view_name("dcim:location_list"), "LocationTable")
|
|
276
|
+
# Testing unconventional table name
|
|
277
|
+
self.assertEqual(lookup.get_table_class_string_from_view_name("ipam:prefix_list"), "PrefixDetailTable")
|
|
278
|
+
|
|
271
279
|
|
|
272
280
|
class IsTaggableTest(TestCase):
|
|
273
281
|
def test_is_taggable_true(self):
|
nautobot/core/utils/lookup.py
CHANGED
|
@@ -8,7 +8,7 @@ from django.conf import settings
|
|
|
8
8
|
from django.contrib.auth.models import Group
|
|
9
9
|
from django.contrib.contenttypes.models import ContentType
|
|
10
10
|
from django.db.models import Model
|
|
11
|
-
from django.urls import get_resolver, URLPattern, URLResolver
|
|
11
|
+
from django.urls import get_resolver, resolve, reverse, URLPattern, URLResolver
|
|
12
12
|
from django.utils.module_loading import import_string
|
|
13
13
|
|
|
14
14
|
|
|
@@ -212,7 +212,7 @@ def get_related_field_for_models(from_model, to_model):
|
|
|
212
212
|
return matching_field
|
|
213
213
|
|
|
214
214
|
|
|
215
|
-
def get_table_for_model(model):
|
|
215
|
+
def get_table_for_model(model, suffix=None):
|
|
216
216
|
"""Return the `Table` class associated with a given `model`.
|
|
217
217
|
|
|
218
218
|
The `Table` class is expected to be in the `tables` module within the application
|
|
@@ -222,11 +222,12 @@ def get_table_for_model(model):
|
|
|
222
222
|
|
|
223
223
|
Args:
|
|
224
224
|
model (BaseModel): A model class
|
|
225
|
+
suffix (str): A replacement suffix for the table name (e.g. `DetailTable`, such as to retrieve `FooDetailTable`)
|
|
225
226
|
|
|
226
227
|
Returns:
|
|
227
228
|
(Union[Table, None]): Either the `Table` class or `None`
|
|
228
229
|
"""
|
|
229
|
-
return get_related_class_for_model(model, module_name="tables", object_suffix="Table")
|
|
230
|
+
return get_related_class_for_model(model, module_name="tables", object_suffix=suffix or "Table")
|
|
230
231
|
|
|
231
232
|
|
|
232
233
|
def get_view_for_model(model, view_type=""):
|
|
@@ -275,11 +276,13 @@ def get_table_class_string_from_view_name(view_name):
|
|
|
275
276
|
Returns:
|
|
276
277
|
table_class_name (String): The name of the model table class or None e.g. LocationTable, CircuitTable
|
|
277
278
|
"""
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
279
|
+
|
|
280
|
+
view_func = resolve(reverse(view_name)).func
|
|
281
|
+
view_class = getattr(view_func, "cls", getattr(view_func, "view_class", None))
|
|
282
|
+
if hasattr(view_class, "table_class"):
|
|
283
|
+
return view_class.table_class.__name__
|
|
284
|
+
if hasattr(view_class, "table"):
|
|
285
|
+
return view_class.table.__name__
|
|
283
286
|
return None
|
|
284
287
|
|
|
285
288
|
|
nautobot/dcim/api/views.py
CHANGED
|
@@ -11,6 +11,7 @@ from drf_spectacular.types import OpenApiTypes
|
|
|
11
11
|
from drf_spectacular.utils import extend_schema, OpenApiParameter
|
|
12
12
|
from rest_framework.decorators import action
|
|
13
13
|
from rest_framework.mixins import ListModelMixin
|
|
14
|
+
from rest_framework.parsers import JSONParser, MultiPartParser
|
|
14
15
|
from rest_framework.permissions import IsAuthenticated
|
|
15
16
|
from rest_framework.response import Response
|
|
16
17
|
from rest_framework.viewsets import GenericViewSet, ViewSet
|
|
@@ -18,6 +19,7 @@ from rest_framework.viewsets import GenericViewSet, ViewSet
|
|
|
18
19
|
from nautobot.circuits.models import Circuit
|
|
19
20
|
from nautobot.cloud.models import CloudAccount
|
|
20
21
|
from nautobot.core.api.exceptions import ServiceUnavailable
|
|
22
|
+
from nautobot.core.api.parsers import NautobotCSVParser
|
|
21
23
|
from nautobot.core.api.utils import get_serializer_for_model
|
|
22
24
|
from nautobot.core.api.views import ModelViewSet
|
|
23
25
|
from nautobot.core.models.querysets import count_related
|
|
@@ -300,6 +302,7 @@ class DeviceTypeViewSet(NautobotModelViewSet):
|
|
|
300
302
|
)
|
|
301
303
|
serializer_class = serializers.DeviceTypeSerializer
|
|
302
304
|
filterset_class = filters.DeviceTypeFilterSet
|
|
305
|
+
parser_classes = [JSONParser, NautobotCSVParser, MultiPartParser]
|
|
303
306
|
|
|
304
307
|
|
|
305
308
|
#
|
|
@@ -903,6 +903,19 @@ class DeviceFilterSet(
|
|
|
903
903
|
to_field_name="version",
|
|
904
904
|
label="Software version (version or ID)",
|
|
905
905
|
)
|
|
906
|
+
ip_addresses = MultiValueCharFilter(
|
|
907
|
+
method="filter_ip_addresses",
|
|
908
|
+
label="IP addresses (address or ID)",
|
|
909
|
+
distinct=True,
|
|
910
|
+
)
|
|
911
|
+
has_ip_addresses = RelatedMembershipBooleanFilter(field_name="interfaces__ip_addresses", label="Has IP addresses")
|
|
912
|
+
|
|
913
|
+
def filter_ip_addresses(self, queryset, name, value):
|
|
914
|
+
pk_values = set(item for item in value if is_uuid(item))
|
|
915
|
+
addresses = set(item for item in value if item not in pk_values)
|
|
916
|
+
|
|
917
|
+
ip_queryset = IPAddress.objects.filter_address_or_pk_in(addresses, pk_values)
|
|
918
|
+
return queryset.filter(interfaces__ip_addresses__in=ip_queryset).distinct()
|
|
906
919
|
|
|
907
920
|
class Meta:
|
|
908
921
|
model = Device
|
|
@@ -1130,6 +1143,19 @@ class InterfaceFilterSet(
|
|
|
1130
1143
|
queryset=InterfaceRedundancyGroup.objects.all(),
|
|
1131
1144
|
to_field_name="name",
|
|
1132
1145
|
)
|
|
1146
|
+
ip_addresses = MultiValueCharFilter(
|
|
1147
|
+
method="filter_ip_addresses",
|
|
1148
|
+
label="IP addresses (address or ID)",
|
|
1149
|
+
distinct=True,
|
|
1150
|
+
)
|
|
1151
|
+
has_ip_addresses = RelatedMembershipBooleanFilter(field_name="ip_addresses", label="Has IP addresses")
|
|
1152
|
+
|
|
1153
|
+
def filter_ip_addresses(self, queryset, name, value):
|
|
1154
|
+
pk_values = set(item for item in value if is_uuid(item))
|
|
1155
|
+
addresses = set(item for item in value if item not in pk_values)
|
|
1156
|
+
|
|
1157
|
+
ip_queryset = IPAddress.objects.filter_address_or_pk_in(addresses, pk_values)
|
|
1158
|
+
return queryset.filter(ip_addresses__in=ip_queryset).distinct()
|
|
1133
1159
|
|
|
1134
1160
|
class Meta:
|
|
1135
1161
|
model = Interface
|
|
@@ -1144,7 +1170,6 @@ class InterfaceFilterSet(
|
|
|
1144
1170
|
"description",
|
|
1145
1171
|
"label",
|
|
1146
1172
|
"tags",
|
|
1147
|
-
"interface_redundancy_groups",
|
|
1148
1173
|
]
|
|
1149
1174
|
|
|
1150
1175
|
def generate_query_filter_device(self, value):
|