nautobot 2.3.9__py3-none-any.whl → 2.3.11__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/apps/utils.py +2 -0
- nautobot/cloud/tables.py +1 -0
- nautobot/core/forms/forms.py +5 -1
- nautobot/core/models/query_functions.py +147 -1
- nautobot/core/tables.py +88 -22
- nautobot/core/templates/generic/object_bulk_destroy.html +12 -3
- nautobot/core/templates/generic/object_bulk_update.html +4 -2
- nautobot/core/templates/generic/object_create.html +1 -1
- nautobot/core/templates/rest_framework/api.html +3 -0
- nautobot/core/testing/api.py +3 -1
- nautobot/core/testing/integration.py +64 -0
- nautobot/core/testing/views.py +33 -27
- nautobot/core/tests/integration/test_app_navbar.py +3 -3
- nautobot/core/tests/integration/test_navbar.py +1 -1
- nautobot/core/tests/test_csv.py +3 -0
- nautobot/core/tests/test_models_query_functions.py +108 -0
- nautobot/core/tests/test_utils.py +25 -5
- nautobot/core/utils/lookup.py +35 -0
- nautobot/core/views/generic.py +50 -39
- nautobot/core/views/mixins.py +97 -43
- nautobot/core/views/renderers.py +8 -5
- nautobot/dcim/tables/devices.py +3 -0
- nautobot/dcim/templates/dcim/device_component_add.html +8 -8
- nautobot/dcim/templates/dcim/modulebay_create.html +39 -0
- nautobot/dcim/templates/dcim/modulebay_update.html +39 -0
- nautobot/dcim/templates/dcim/virtualchassis_add_member.html +2 -2
- nautobot/dcim/templates/dcim/virtualchassis_edit.html +2 -2
- nautobot/dcim/tests/integration/test_create_device.py +86 -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_relationships.py +1 -0
- nautobot/extras/tests/test_webhooks.py +1 -1
- nautobot/extras/views.py +1 -0
- nautobot/extras/webhooks.py +16 -7
- nautobot/ipam/factory.py +3 -0
- nautobot/ipam/filters.py +5 -0
- nautobot/ipam/forms.py +17 -0
- nautobot/ipam/models.py +2 -1
- nautobot/ipam/signals.py +2 -2
- nautobot/ipam/tables.py +3 -3
- nautobot/ipam/templates/ipam/ipaddress_assign.html +2 -2
- nautobot/ipam/tests/test_models.py +113 -1
- nautobot/ipam/tests/test_views.py +39 -5
- 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 +132 -7
- nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +176 -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 +95 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/views.html +5 -5
- 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/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 +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 +440 -140
- 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 +40 -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/virtualization/forms.py +24 -0
- nautobot/virtualization/templates/virtualization/vminterface_edit.html +1 -0
- nautobot/virtualization/tests/test_views.py +7 -2
- {nautobot-2.3.9.dist-info → nautobot-2.3.11.dist-info}/METADATA +2 -2
- {nautobot-2.3.9.dist-info → nautobot-2.3.11.dist-info}/RECORD +335 -331
- {nautobot-2.3.9.dist-info → nautobot-2.3.11.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.3.9.dist-info → nautobot-2.3.11.dist-info}/NOTICE +0 -0
- {nautobot-2.3.9.dist-info → nautobot-2.3.11.dist-info}/WHEEL +0 -0
- {nautobot-2.3.9.dist-info → nautobot-2.3.11.dist-info}/entry_points.txt +0 -0
nautobot/core/testing/views.py
CHANGED
|
@@ -873,6 +873,8 @@ class ViewTestCases:
|
|
|
873
873
|
response_body,
|
|
874
874
|
)
|
|
875
875
|
|
|
876
|
+
return response
|
|
877
|
+
|
|
876
878
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
|
877
879
|
def test_list_objects_with_constrained_permission(self):
|
|
878
880
|
instance1, instance2 = self._get_queryset().all()[:2]
|
|
@@ -1112,13 +1114,10 @@ class ViewTestCases:
|
|
|
1112
1114
|
|
|
1113
1115
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1114
1116
|
def test_bulk_edit_form_contains_all_pks(self):
|
|
1115
|
-
# We are testing the intermediary step of bulk_edit
|
|
1117
|
+
# We are testing the intermediary step of all bulk_edit.
|
|
1116
1118
|
# i.e. "_all" passed in the form.
|
|
1117
1119
|
pk_list = self._get_queryset().values_list("pk", flat=True)
|
|
1118
|
-
# We only pass in one pk to test the functionality of "_all"
|
|
1119
|
-
# which should grab all instance pks regardless of "pk"
|
|
1120
1120
|
selected_data = {
|
|
1121
|
-
"pk": pk_list[:1],
|
|
1122
1121
|
"_all": "on",
|
|
1123
1122
|
}
|
|
1124
1123
|
# Assign model-level permission
|
|
@@ -1133,13 +1132,19 @@ class ViewTestCases:
|
|
|
1133
1132
|
# after pressing Edit Selected button.
|
|
1134
1133
|
self.assertHttpStatus(response, 200)
|
|
1135
1134
|
response_body = utils.extract_page_body(response.content.decode(response.charset))
|
|
1135
|
+
# Assert the table which shows all the selected objects is not part of the html body in edit all case
|
|
1136
|
+
self.assertNotIn('<table class="table table-hover table-headings">', response_body)
|
|
1136
1137
|
# Check if all the pks are passed into the BulkEditForm/BulkUpdateForm
|
|
1137
1138
|
for pk in pk_list:
|
|
1138
|
-
self.
|
|
1139
|
+
self.assertNotIn(str(pk), response_body)
|
|
1140
|
+
self.assertInHTML(
|
|
1141
|
+
'<input type="hidden" name="_all" value="True" class="form-control" required="required" placeholder="None" id="id__all">',
|
|
1142
|
+
response_body,
|
|
1143
|
+
)
|
|
1139
1144
|
|
|
1140
1145
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1141
1146
|
def test_bulk_edit_form_contains_all_filtered(self):
|
|
1142
|
-
# We are testing the intermediary step of
|
|
1147
|
+
# We are testing the intermediary step of bulk editing all filtered objects.
|
|
1143
1148
|
# i.e. "_all" passed in the form and filter using query params.
|
|
1144
1149
|
self.add_permissions(f"{self.model._meta.app_label}.change_{self.model._meta.model_name}")
|
|
1145
1150
|
|
|
@@ -1155,7 +1160,6 @@ class ViewTestCases:
|
|
|
1155
1160
|
|
|
1156
1161
|
# Open bulk update form with first two objects
|
|
1157
1162
|
selected_data = {
|
|
1158
|
-
"pk": third_pk, # This is ignored when filtering with "_all"
|
|
1159
1163
|
"_all": "on",
|
|
1160
1164
|
**post_data,
|
|
1161
1165
|
}
|
|
@@ -1164,12 +1168,15 @@ class ViewTestCases:
|
|
|
1164
1168
|
# Expect a 200 status cause we are only rendering the bulk edit table after pressing Edit Selected button.
|
|
1165
1169
|
self.assertHttpStatus(response, 200)
|
|
1166
1170
|
response_body = utils.extract_page_body(response.content.decode(response.charset))
|
|
1167
|
-
# Check if
|
|
1168
|
-
self.
|
|
1169
|
-
self.
|
|
1171
|
+
# Check if all pks is not part of the html.
|
|
1172
|
+
self.assertNotIn(str(first_pk), response_body)
|
|
1173
|
+
self.assertNotIn(str(second_pk), response_body)
|
|
1174
|
+
self.assertNotIn(str(third_pk), response_body)
|
|
1170
1175
|
self.assertIn("Editing 2 ", response_body)
|
|
1171
|
-
|
|
1172
|
-
|
|
1176
|
+
self.assertInHTML(
|
|
1177
|
+
'<input type="hidden" name="_all" value="True" class="form-control" required="required" placeholder="None" id="id__all">',
|
|
1178
|
+
response_body,
|
|
1179
|
+
)
|
|
1173
1180
|
|
|
1174
1181
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1175
1182
|
def test_bulk_edit_objects_with_constrained_permission(self):
|
|
@@ -1271,14 +1278,10 @@ class ViewTestCases:
|
|
|
1271
1278
|
self.assertEqual(self._get_queryset().count(), initial_count - len(pk_list))
|
|
1272
1279
|
|
|
1273
1280
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
|
1274
|
-
def
|
|
1275
|
-
# We are testing the intermediary step of bulk_delete
|
|
1281
|
+
def test_bulk_delete_form_contains_all_objects(self):
|
|
1282
|
+
# We are testing the intermediary step of bulk_delete all objects.
|
|
1276
1283
|
# i.e. "_all" passed in the form.
|
|
1277
|
-
pk_list = self._get_queryset().values_list("pk", flat=True)
|
|
1278
|
-
# We only pass in one pk to test the functionality of "_all"
|
|
1279
|
-
# which should grab all instance pks regardless of "pks".
|
|
1280
1284
|
selected_data = {
|
|
1281
|
-
"pk": pk_list[:1],
|
|
1282
1285
|
"confirm": True,
|
|
1283
1286
|
"_all": "on",
|
|
1284
1287
|
}
|
|
@@ -1293,13 +1296,16 @@ class ViewTestCases:
|
|
|
1293
1296
|
response = self.client.post(self._get_url("bulk_delete"), selected_data)
|
|
1294
1297
|
self.assertHttpStatus(response, 200)
|
|
1295
1298
|
response_body = utils.extract_page_body(response.content.decode(response.charset))
|
|
1296
|
-
#
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
+
# Assert the table which shows all the selected objects is not part of the html body in delete all case
|
|
1300
|
+
self.assertNotIn('<table class="table table-hover table-headings">', response_body)
|
|
1301
|
+
# Assert none of the hidden input fields for each of the pks that would be deleted is part of the html body
|
|
1302
|
+
for pk in self._get_queryset().values_list("pk", flat=True):
|
|
1303
|
+
self.assertNotIn(str(pk), response_body)
|
|
1304
|
+
self.assertInHTML('<input type="hidden" name="_all" value="true" />', response_body)
|
|
1299
1305
|
|
|
1300
1306
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1301
1307
|
def test_bulk_delete_form_contains_all_filtered(self):
|
|
1302
|
-
# We are testing the intermediary step of bulk_delete with
|
|
1308
|
+
# We are testing the intermediary step of bulk_delete all with additional filter.
|
|
1303
1309
|
# i.e. "_all" passed in the form and filter using query params.
|
|
1304
1310
|
self.add_permissions(f"{self.model._meta.app_label}.delete_{self.model._meta.model_name}")
|
|
1305
1311
|
|
|
@@ -1321,12 +1327,12 @@ class ViewTestCases:
|
|
|
1321
1327
|
# Expect a 200 status cause we are only rendering the bulk delete table after pressing Delete Selected button.
|
|
1322
1328
|
self.assertHttpStatus(response, 200)
|
|
1323
1329
|
response_body = utils.extract_page_body(response.content.decode(response.charset))
|
|
1324
|
-
# Check if
|
|
1325
|
-
self.
|
|
1326
|
-
self.
|
|
1330
|
+
# Check if all pks is not part of the html.
|
|
1331
|
+
self.assertNotIn(str(first_pk), response_body)
|
|
1332
|
+
self.assertNotIn(str(second_pk), response_body)
|
|
1333
|
+
self.assertNotIn(str(third_pk), response_body)
|
|
1327
1334
|
self.assertIn("<strong>Warning:</strong> The following operation will delete 2 ", response_body)
|
|
1328
|
-
|
|
1329
|
-
self.assertNotIn(f'<input type="hidden" name="pk" value="{third_pk}"', response_body)
|
|
1335
|
+
self.assertInHTML('<input type="hidden" name="_all" value="true" />', response_body)
|
|
1330
1336
|
|
|
1331
1337
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
|
1332
1338
|
def test_bulk_delete_objects_with_constrained_permission(self):
|
|
@@ -61,7 +61,7 @@ class AppNavBarTestCase(SeleniumTestCase):
|
|
|
61
61
|
tab_xpath = "//*[@id='navbar']//span[normalize-space()='Example Menu']/.."
|
|
62
62
|
tab = self.browser.find_by_xpath(tab_xpath)
|
|
63
63
|
tab.click()
|
|
64
|
-
self.
|
|
64
|
+
self.assertEqual(tab["aria-expanded"], "true")
|
|
65
65
|
|
|
66
66
|
group = tab.find_by_xpath(f"{tab_xpath}/following-sibling::ul//li[normalize-space()='Example Group 1']")
|
|
67
67
|
|
|
@@ -82,7 +82,7 @@ class AppNavBarTestCase(SeleniumTestCase):
|
|
|
82
82
|
tab_xpath = "//*[@id='navbar']//*[normalize-space()='Circuits']"
|
|
83
83
|
tab = self.browser.find_by_xpath(tab_xpath)
|
|
84
84
|
tab.click()
|
|
85
|
-
self.
|
|
85
|
+
self.assertEqual(tab["aria-expanded"], "true")
|
|
86
86
|
|
|
87
87
|
for group_name, items in self.navbar["Circuits"].items():
|
|
88
88
|
group = tab.find_by_xpath(f"{tab_xpath}/following-sibling::ul//li[normalize-space()='{group_name}']")
|
|
@@ -114,7 +114,7 @@ class AppNavBarTestCase(SeleniumTestCase):
|
|
|
114
114
|
tab_xpath = "//*[@id='navbar']//*[normalize-space()='Apps']"
|
|
115
115
|
tab = self.browser.find_by_xpath(tab_xpath)
|
|
116
116
|
tab.click()
|
|
117
|
-
self.
|
|
117
|
+
self.assertEqual(tab["aria-expanded"], "true")
|
|
118
118
|
|
|
119
119
|
for group_name, items in self.navbar["Apps"].items():
|
|
120
120
|
group = tab.find_by_xpath(f"{tab_xpath}/following-sibling::ul//li[normalize-space()='{group_name}']")
|
|
@@ -60,7 +60,7 @@ class NavBarTestCase(SeleniumTestCase):
|
|
|
60
60
|
tab_xpath = f"//*[@id='navbar']//span[normalize-space()='{tab_name}']/.."
|
|
61
61
|
tab = self.browser.find_by_xpath(tab_xpath)
|
|
62
62
|
tab.click()
|
|
63
|
-
self.
|
|
63
|
+
self.assertEqual(tab["aria-expanded"], "true")
|
|
64
64
|
|
|
65
65
|
for group_name, items in groups.items():
|
|
66
66
|
# Append onto tab xpath with group name search
|
nautobot/core/tests/test_csv.py
CHANGED
|
@@ -267,6 +267,9 @@ class CSVParsingRelatedTestCase(TestCase):
|
|
|
267
267
|
url = reverse("dcim:device_import")
|
|
268
268
|
response = self.client.post(url, data)
|
|
269
269
|
self.assertEqual(response.status_code, 200)
|
|
270
|
+
# uploading the CSV always returns a 200 code with a page with an error message on it
|
|
271
|
+
# ensure we don't have that error message
|
|
272
|
+
self.assertNotIn("FORM-ERROR", response.content.decode(response.charset))
|
|
270
273
|
self.assertEqual(Device.objects.count(), 4)
|
|
271
274
|
|
|
272
275
|
# Assert TestDevice3 got created with the right fields
|
|
@@ -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)
|
|
@@ -21,6 +21,7 @@ from nautobot.extras import models as extras_models, utils as extras_utils
|
|
|
21
21
|
from nautobot.extras.choices import ObjectChangeActionChoices, RelationshipTypeChoices
|
|
22
22
|
from nautobot.extras.models import ObjectChange
|
|
23
23
|
from nautobot.extras.registry import registry
|
|
24
|
+
from nautobot.ipam import models as ipam_models
|
|
24
25
|
|
|
25
26
|
from example_app.models import ExampleModel
|
|
26
27
|
|
|
@@ -164,7 +165,7 @@ class GetFooForModelTest(TestCase):
|
|
|
164
165
|
|
|
165
166
|
def test_get_filterset_for_model(self):
|
|
166
167
|
"""
|
|
167
|
-
Test
|
|
168
|
+
Test that `get_filterset_for_model` returns the right FilterSet for various inputs.
|
|
168
169
|
"""
|
|
169
170
|
self.assertEqual(lookup.get_filterset_for_model("dcim.device"), dcim_filters.DeviceFilterSet)
|
|
170
171
|
self.assertEqual(lookup.get_filterset_for_model(dcim_models.Device), dcim_filters.DeviceFilterSet)
|
|
@@ -173,7 +174,7 @@ class GetFooForModelTest(TestCase):
|
|
|
173
174
|
|
|
174
175
|
def test_get_form_for_model(self):
|
|
175
176
|
"""
|
|
176
|
-
Test
|
|
177
|
+
Test that `get_form_for_model` returns the right Form for various inputs.
|
|
177
178
|
"""
|
|
178
179
|
self.assertEqual(lookup.get_form_for_model("dcim.device", "Filter"), dcim_forms.DeviceFilterForm)
|
|
179
180
|
self.assertEqual(lookup.get_form_for_model(dcim_models.Device, "Filter"), dcim_forms.DeviceFilterForm)
|
|
@@ -184,9 +185,28 @@ class GetFooForModelTest(TestCase):
|
|
|
184
185
|
self.assertEqual(lookup.get_form_for_model("dcim.location"), dcim_forms.LocationForm)
|
|
185
186
|
self.assertEqual(lookup.get_form_for_model(dcim_models.Location), dcim_forms.LocationForm)
|
|
186
187
|
|
|
188
|
+
def test_get_related_field_for_models(self):
|
|
189
|
+
"""
|
|
190
|
+
Test that `get_related_field_for_models` returns the appropriate field for various inputs.
|
|
191
|
+
"""
|
|
192
|
+
# No direct relation found
|
|
193
|
+
self.assertIsNone(lookup.get_related_field_for_models(dcim_models.Device, dcim_models.LocationType))
|
|
194
|
+
# ForeignKey and reverse
|
|
195
|
+
self.assertEqual(lookup.get_related_field_for_models(dcim_models.Device, dcim_models.Location).name, "location")
|
|
196
|
+
self.assertEqual(lookup.get_related_field_for_models(dcim_models.Location, dcim_models.Device).name, "devices")
|
|
197
|
+
# ManyToMany and reverse
|
|
198
|
+
self.assertEqual(
|
|
199
|
+
lookup.get_related_field_for_models(ipam_models.Prefix, dcim_models.Location).name, "locations"
|
|
200
|
+
)
|
|
201
|
+
self.assertEqual(lookup.get_related_field_for_models(dcim_models.Location, ipam_models.Prefix).name, "prefixes")
|
|
202
|
+
# Multiple candidate fields
|
|
203
|
+
with self.assertRaises(AttributeError):
|
|
204
|
+
# both primary_ip4 and primary_ip6 are candidates
|
|
205
|
+
lookup.get_related_field_for_models(dcim_models.Device, ipam_models.IPAddress)
|
|
206
|
+
|
|
187
207
|
def test_get_route_for_model(self):
|
|
188
208
|
"""
|
|
189
|
-
Test
|
|
209
|
+
Test that `get_route_for_model` returns the appropriate URL route name for various inputs.
|
|
190
210
|
"""
|
|
191
211
|
# UI
|
|
192
212
|
self.assertEqual(lookup.get_route_for_model("dcim.device", "list"), "dcim:device_list")
|
|
@@ -221,7 +241,7 @@ class GetFooForModelTest(TestCase):
|
|
|
221
241
|
|
|
222
242
|
def test_get_table_for_model(self):
|
|
223
243
|
"""
|
|
224
|
-
Test
|
|
244
|
+
Test that `get_table_for_model` returns the appropriate Table for various inputs.
|
|
225
245
|
"""
|
|
226
246
|
self.assertEqual(lookup.get_table_for_model("dcim.device"), tables.DeviceTable)
|
|
227
247
|
self.assertEqual(lookup.get_table_for_model(dcim_models.Device), tables.DeviceTable)
|
|
@@ -237,7 +257,7 @@ class GetFooForModelTest(TestCase):
|
|
|
237
257
|
|
|
238
258
|
def test_get_model_for_view_name(self):
|
|
239
259
|
"""
|
|
240
|
-
Test
|
|
260
|
+
Test that `get_model_for_view_name` returns the appropriate Model, if the colon separated view name provided.
|
|
241
261
|
"""
|
|
242
262
|
with self.subTest("Test core view."):
|
|
243
263
|
self.assertEqual(lookup.get_model_for_view_name("dcim:device_list"), dcim_models.Device)
|
nautobot/core/utils/lookup.py
CHANGED
|
@@ -177,6 +177,41 @@ def get_form_for_model(model, form_prefix=""):
|
|
|
177
177
|
return get_related_class_for_model(model, module_name="forms", object_suffix=object_suffix)
|
|
178
178
|
|
|
179
179
|
|
|
180
|
+
def get_related_field_for_models(from_model, to_model):
|
|
181
|
+
"""
|
|
182
|
+
Find the field on `from_model` that is a relation to `to_model`.
|
|
183
|
+
|
|
184
|
+
If no such field is found, returns None.
|
|
185
|
+
If more than one such field is found, raises an AttributeError.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
from_model (BaseModel): The model class that should contain the relevant field or relation.
|
|
189
|
+
to_model (BaseModel): The model class that we're looking for as the destination.
|
|
190
|
+
|
|
191
|
+
Examples:
|
|
192
|
+
>>> get_related_field_for_models(Device, Location)
|
|
193
|
+
<django.db.models.fields.related.ForeignKey: location>
|
|
194
|
+
>>> get_related_field_for_models(Location, Device)
|
|
195
|
+
<ManyToOneRel: dcim.device>
|
|
196
|
+
>>> get_related_field_for_models(Prefix, Location)
|
|
197
|
+
<django.db.models.fields.related.ManyToManyField: locations>
|
|
198
|
+
>>> get_related_field_for_models(Location, Prefix)
|
|
199
|
+
<ManyToManyRel: ipam.prefix>
|
|
200
|
+
>>> get_related_field_for_models(Device, IPAddress)
|
|
201
|
+
AttributeError: Device has more than one relation to IPAddress: primary_ip4, primary_ip6
|
|
202
|
+
"""
|
|
203
|
+
matching_field = None
|
|
204
|
+
for field in from_model._meta.get_fields():
|
|
205
|
+
if hasattr(field, "remote_field") and field.remote_field and field.remote_field.model == to_model:
|
|
206
|
+
if matching_field is not None:
|
|
207
|
+
raise AttributeError(
|
|
208
|
+
f"{from_model.__name__} has more than one relation to {to_model.__name__}: "
|
|
209
|
+
f"{matching_field.name}, {field.name}"
|
|
210
|
+
)
|
|
211
|
+
matching_field = field
|
|
212
|
+
return matching_field
|
|
213
|
+
|
|
214
|
+
|
|
180
215
|
def get_table_for_model(model):
|
|
181
216
|
"""Return the `Table` class associated with a given `model`.
|
|
182
217
|
|
nautobot/core/views/generic.py
CHANGED
|
@@ -46,7 +46,7 @@ from nautobot.core.utils.requests import (
|
|
|
46
46
|
get_filterable_params_from_filter_params,
|
|
47
47
|
normalize_querydict,
|
|
48
48
|
)
|
|
49
|
-
from nautobot.core.views.mixins import GetReturnURLMixin, ObjectPermissionRequiredMixin
|
|
49
|
+
from nautobot.core.views.mixins import EditAndDeleteAllModelMixin, GetReturnURLMixin, ObjectPermissionRequiredMixin
|
|
50
50
|
from nautobot.core.views.paginator import EnhancedPaginator, get_paginate_count
|
|
51
51
|
from nautobot.core.views.utils import (
|
|
52
52
|
check_filter_for_display,
|
|
@@ -979,7 +979,7 @@ class BulkImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View): #
|
|
|
979
979
|
)
|
|
980
980
|
|
|
981
981
|
|
|
982
|
-
class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|
982
|
+
class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, EditAndDeleteAllModelMixin, View):
|
|
983
983
|
"""
|
|
984
984
|
Edit objects in bulk.
|
|
985
985
|
|
|
@@ -1013,18 +1013,18 @@ class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|
|
1013
1013
|
def post(self, request, **kwargs):
|
|
1014
1014
|
logger = logging.getLogger(__name__ + ".BulkEditView")
|
|
1015
1015
|
model = self.queryset.model
|
|
1016
|
+
edit_all = request.POST.get("_all")
|
|
1016
1017
|
|
|
1017
1018
|
# If we are editing *all* objects in the queryset, replace the PK list with all matched objects.
|
|
1018
|
-
if
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
else:
|
|
1022
|
-
pk_list = list(model.objects.all().values_list("pk", flat=True))
|
|
1019
|
+
if edit_all:
|
|
1020
|
+
pk_list = []
|
|
1021
|
+
queryset = self._get_bulk_edit_delete_all_queryset(request)
|
|
1023
1022
|
else:
|
|
1024
1023
|
pk_list = request.POST.getlist("pk")
|
|
1024
|
+
queryset = self.queryset.filter(pk__in=pk_list)
|
|
1025
1025
|
|
|
1026
1026
|
if "_apply" in request.POST:
|
|
1027
|
-
form = self.form(model, request.POST)
|
|
1027
|
+
form = self.form(model, request.POST, edit_all=edit_all)
|
|
1028
1028
|
restrict_form_fields(form, request.user)
|
|
1029
1029
|
|
|
1030
1030
|
if form.is_valid():
|
|
@@ -1041,7 +1041,8 @@ class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|
|
1041
1041
|
try:
|
|
1042
1042
|
with deferred_change_logging_for_bulk_operation():
|
|
1043
1043
|
updated_objects = []
|
|
1044
|
-
|
|
1044
|
+
queryset = queryset if edit_all else queryset.filter(pk__in=form.cleaned_data["pk"])
|
|
1045
|
+
for obj in queryset:
|
|
1045
1046
|
obj = self.alter_obj(obj, request, [], kwargs)
|
|
1046
1047
|
|
|
1047
1048
|
# Update standard fields. If a field is listed in _nullify, delete its value.
|
|
@@ -1130,23 +1131,26 @@ class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|
|
1130
1131
|
elif "device_type" in request.GET:
|
|
1131
1132
|
initial_data["device_type"] = request.GET.get("device_type")
|
|
1132
1133
|
|
|
1133
|
-
form = self.form(model, initial=initial_data)
|
|
1134
|
+
form = self.form(model, initial=initial_data, edit_all=edit_all)
|
|
1134
1135
|
restrict_form_fields(form, request.user)
|
|
1135
1136
|
|
|
1136
1137
|
# Retrieve objects being edited
|
|
1137
|
-
table =
|
|
1138
|
-
if not
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1138
|
+
table = None
|
|
1139
|
+
if not edit_all:
|
|
1140
|
+
table = self.table(queryset, orderable=False)
|
|
1141
|
+
if not table.rows:
|
|
1142
|
+
messages.warning(request, f"No {model._meta.verbose_name_plural} were selected.")
|
|
1143
|
+
return redirect(self.get_return_url(request))
|
|
1144
|
+
# Hide actions column if present
|
|
1145
|
+
if "actions" in table.columns:
|
|
1146
|
+
table.columns.hide("actions")
|
|
1144
1147
|
|
|
1145
1148
|
context = {
|
|
1146
1149
|
"form": form,
|
|
1147
1150
|
"table": table,
|
|
1148
1151
|
"obj_type_plural": model._meta.verbose_name_plural,
|
|
1149
1152
|
"return_url": self.get_return_url(request),
|
|
1153
|
+
"objs_count": queryset.count(),
|
|
1150
1154
|
}
|
|
1151
1155
|
context.update(self.extra_context())
|
|
1152
1156
|
return render(request, self.template_name, context)
|
|
@@ -1255,7 +1259,7 @@ class BulkRenameView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|
|
1255
1259
|
return ""
|
|
1256
1260
|
|
|
1257
1261
|
|
|
1258
|
-
class BulkDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|
1262
|
+
class BulkDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, EditAndDeleteAllModelMixin, View):
|
|
1259
1263
|
"""
|
|
1260
1264
|
Delete objects in bulk.
|
|
1261
1265
|
|
|
@@ -1278,18 +1282,37 @@ class BulkDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|
|
1278
1282
|
def get(self, request):
|
|
1279
1283
|
return redirect(self.get_return_url(request))
|
|
1280
1284
|
|
|
1281
|
-
def
|
|
1285
|
+
def _perform_delete_operation(self, request, queryset, model):
|
|
1282
1286
|
logger = logging.getLogger(__name__ + ".BulkDeleteView")
|
|
1287
|
+
self.perform_pre_delete(request, queryset)
|
|
1288
|
+
try:
|
|
1289
|
+
_, deleted_info = bulk_delete_with_bulk_change_logging(queryset)
|
|
1290
|
+
deleted_count = deleted_info[model._meta.label]
|
|
1291
|
+
except ProtectedError as e:
|
|
1292
|
+
logger.info("Caught ProtectedError while attempting to delete objects")
|
|
1293
|
+
handle_protectederror(queryset, request, e)
|
|
1294
|
+
return redirect(self.get_return_url(request))
|
|
1295
|
+
msg = f"Deleted {deleted_count} {model._meta.verbose_name_plural}"
|
|
1296
|
+
logger.info(msg)
|
|
1297
|
+
messages.success(request, msg)
|
|
1298
|
+
return redirect(self.get_return_url(request))
|
|
1299
|
+
|
|
1300
|
+
def post(self, request, **kwargs):
|
|
1301
|
+
logger = logging.getLogger(f"{__name__}.BulkDeleteView")
|
|
1283
1302
|
model = self.queryset.model
|
|
1284
1303
|
|
|
1285
1304
|
# Are we deleting *all* objects in the queryset or just a selected subset?
|
|
1286
1305
|
if request.POST.get("_all"):
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1306
|
+
queryset = self._get_bulk_edit_delete_all_queryset(request)
|
|
1307
|
+
|
|
1308
|
+
if "_confirm" in request.POST:
|
|
1309
|
+
return self._perform_delete_operation(request, queryset, model)
|
|
1310
|
+
|
|
1311
|
+
context = self._bulk_delete_all_context(request, queryset)
|
|
1312
|
+
context.update(self.extra_context())
|
|
1313
|
+
return render(request, self.template_name, context)
|
|
1314
|
+
|
|
1315
|
+
pk_list = request.POST.getlist("pk")
|
|
1293
1316
|
|
|
1294
1317
|
form_cls = self.get_form()
|
|
1295
1318
|
|
|
@@ -1300,20 +1323,7 @@ class BulkDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|
|
1300
1323
|
|
|
1301
1324
|
# Delete objects
|
|
1302
1325
|
queryset = self.queryset.filter(pk__in=pk_list)
|
|
1303
|
-
|
|
1304
|
-
self.perform_pre_delete(request, queryset)
|
|
1305
|
-
try:
|
|
1306
|
-
_, deleted_info = bulk_delete_with_bulk_change_logging(queryset)
|
|
1307
|
-
deleted_count = deleted_info[model._meta.label]
|
|
1308
|
-
except ProtectedError as e:
|
|
1309
|
-
logger.info("Caught ProtectedError while attempting to delete objects")
|
|
1310
|
-
handle_protectederror(queryset, request, e)
|
|
1311
|
-
return redirect(self.get_return_url(request))
|
|
1312
|
-
msg = f"Deleted {deleted_count} {model._meta.verbose_name_plural}"
|
|
1313
|
-
logger.info(msg)
|
|
1314
|
-
messages.success(request, msg)
|
|
1315
|
-
return redirect(self.get_return_url(request))
|
|
1316
|
-
|
|
1326
|
+
return self._perform_delete_operation(request, queryset, model)
|
|
1317
1327
|
else:
|
|
1318
1328
|
logger.debug("Form validation failed")
|
|
1319
1329
|
|
|
@@ -1342,6 +1352,7 @@ class BulkDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|
|
1342
1352
|
"obj_type_plural": model._meta.verbose_name_plural,
|
|
1343
1353
|
"table": table,
|
|
1344
1354
|
"return_url": self.get_return_url(request),
|
|
1355
|
+
"total_objs_to_delete": len(table.rows),
|
|
1345
1356
|
}
|
|
1346
1357
|
context.update(self.extra_context())
|
|
1347
1358
|
return render(request, self.template_name, context)
|