nautobot 2.1.7__py3-none-any.whl → 2.1.9__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/api.py +1 -2
- nautobot/apps/utils.py +4 -0
- nautobot/apps/views.py +2 -0
- nautobot/circuits/api/urls.py +1 -2
- nautobot/circuits/api/views.py +0 -12
- nautobot/circuits/tests/integration/test_relationships.py +0 -4
- nautobot/core/api/routers.py +25 -3
- nautobot/core/api/utils.py +4 -0
- nautobot/core/api/views.py +21 -15
- nautobot/core/celery/schedulers.py +13 -0
- nautobot/core/choices.py +0 -21
- nautobot/core/models/__init__.py +1 -1
- nautobot/core/models/tree_queries.py +29 -7
- nautobot/core/releases.py +1 -1
- nautobot/core/settings.py +9 -0
- nautobot/core/settings_funcs.py +0 -18
- nautobot/core/signals.py +5 -5
- nautobot/core/tasks.py +7 -3
- nautobot/core/templates/admin/base.html +23 -94
- nautobot/core/templates/generic/object_list.html +2 -0
- nautobot/core/templates/graphene/graphiql.html +18 -47
- nautobot/core/templates/inc/footer.html +5 -5
- nautobot/core/templates/inc/nav_menu.html +0 -7
- nautobot/core/templates/nautobot_config.py.j2 +6 -0
- nautobot/core/templates/rest_framework/api.html +12 -5
- nautobot/core/testing/mixins.py +13 -5
- nautobot/core/tests/integration/test_plugin_navbar.py +7 -21
- nautobot/core/tests/integration/test_view_authentication.py +67 -0
- nautobot/core/tests/runner.py +25 -2
- nautobot/core/tests/test_graphql.py +2 -14
- nautobot/core/tests/test_models.py +3 -3
- nautobot/core/tests/test_navigations.py +67 -10
- nautobot/core/tests/test_releases.py +9 -3
- nautobot/core/tests/test_views.py +23 -16
- nautobot/core/utils/lookup.py +124 -0
- nautobot/core/views/__init__.py +3 -7
- nautobot/core/views/generic.py +9 -0
- nautobot/dcim/api/urls.py +1 -2
- nautobot/dcim/api/views.py +1 -12
- nautobot/dcim/choices.py +56 -0
- nautobot/dcim/models/racks.py +1 -3
- nautobot/dcim/navigation.py +1 -1
- nautobot/dcim/templates/dcim/device/lldp_neighbors.html +67 -43
- nautobot/dcim/tests/test_api.py +3 -0
- nautobot/dcim/tests/test_filters.py +0 -28
- nautobot/dcim/views.py +5 -2
- nautobot/extras/api/urls.py +1 -2
- nautobot/extras/api/views.py +0 -10
- nautobot/extras/choices.py +14 -0
- nautobot/extras/models/customfields.py +93 -34
- nautobot/extras/models/groups.py +1 -1
- nautobot/extras/models/relationships.py +32 -19
- nautobot/extras/navigation.py +3 -2
- nautobot/extras/plugins/__init__.py +8 -0
- nautobot/extras/plugins/views.py +6 -9
- nautobot/extras/querysets.py +1 -1
- nautobot/extras/signals.py +12 -6
- nautobot/extras/templates/extras/customfield.html +22 -14
- nautobot/extras/templatetags/job_buttons.py +7 -0
- nautobot/extras/templatetags/plugins.py +5 -1
- nautobot/extras/tests/test_customfields.py +323 -287
- nautobot/extras/tests/test_dynamicgroups.py +1 -1
- nautobot/extras/tests/test_jobs.py +2 -2
- nautobot/extras/tests/test_plugins.py +41 -0
- nautobot/extras/tests/test_relationships.py +31 -14
- nautobot/extras/tests/test_views.py +124 -1
- nautobot/extras/utils.py +7 -3
- nautobot/extras/views.py +10 -10
- nautobot/ipam/api/urls.py +1 -2
- nautobot/ipam/api/views.py +6 -13
- nautobot/ipam/tables.py +0 -1
- nautobot/ipam/tests/test_graphql.py +2 -3
- nautobot/ipam/views.py +12 -10
- nautobot/project-static/css/base.css +1 -0
- nautobot/project-static/docs/404.html +30 -2
- nautobot/project-static/docs/apps/index.html +30 -2
- nautobot/project-static/docs/apps/nautobot-apps.html +30 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +30 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +30 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/api.html +410 -410
- nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +30 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +386 -358
- nautobot/project-static/docs/code-reference/nautobot/apps/config.html +30 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +30 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +30 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +30 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +45 -17
- nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +30 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +30 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +30 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/models.html +759 -602
- nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +30 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +30 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +30 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +528 -467
- nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +205 -109
- nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +30 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +1265 -785
- nautobot/project-static/docs/code-reference/nautobot/apps/views.html +1827 -1746
- nautobot/project-static/docs/development/apps/api/configuration-view.html +30 -2
- nautobot/project-static/docs/development/apps/api/database-backend-config.html +30 -2
- nautobot/project-static/docs/development/apps/api/models/django-admin.html +30 -2
- nautobot/project-static/docs/development/apps/api/models/global-search.html +30 -2
- nautobot/project-static/docs/development/apps/api/models/graphql.html +30 -2
- nautobot/project-static/docs/development/apps/api/models/index.html +30 -2
- nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +31 -3
- nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +30 -2
- nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +30 -2
- nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +30 -2
- nautobot/project-static/docs/development/apps/api/platform-features/index.html +30 -2
- nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +30 -2
- nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +30 -2
- nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +30 -2
- nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +30 -2
- nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +30 -2
- nautobot/project-static/docs/development/apps/api/prometheus.html +30 -2
- nautobot/project-static/docs/development/apps/api/setup.html +30 -2
- nautobot/project-static/docs/development/apps/api/testing.html +33 -5
- nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +30 -2
- nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +30 -2
- nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +30 -2
- nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +33 -5
- nautobot/project-static/docs/development/apps/api/ui-extensions/object-detail-views.html +13 -5559
- nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +5594 -0
- nautobot/project-static/docs/development/apps/api/ui-extensions/tabs.html +3 -3
- nautobot/project-static/docs/development/apps/api/views/base-template.html +30 -2
- nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +44 -11
- nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +47 -14
- nautobot/project-static/docs/development/apps/api/views/help-documentation.html +30 -2
- nautobot/project-static/docs/development/apps/api/views/index.html +30 -2
- nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +30 -2
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +30 -2
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +30 -2
- nautobot/project-static/docs/development/apps/api/views/notes.html +30 -2
- nautobot/project-static/docs/development/apps/api/views/rest-api.html +30 -2
- nautobot/project-static/docs/development/apps/api/views/urls.html +30 -2
- nautobot/project-static/docs/development/apps/index.html +30 -2
- nautobot/project-static/docs/development/apps/migration/code-updates.html +30 -2
- nautobot/project-static/docs/development/apps/migration/dependency-updates.html +30 -2
- nautobot/project-static/docs/development/apps/migration/from-v1.html +30 -2
- nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +30 -2
- nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +30 -2
- nautobot/project-static/docs/development/apps/migration/model-updates/global.html +30 -2
- nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +30 -2
- nautobot/project-static/docs/development/apps/porting-from-netbox.html +30 -2
- nautobot/project-static/docs/development/core/application-registry.html +30 -2
- nautobot/project-static/docs/development/core/best-practices.html +33 -5
- nautobot/project-static/docs/development/core/bootstrap-ui.html +30 -2
- nautobot/project-static/docs/development/core/caching.html +5481 -0
- nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +30 -2
- nautobot/project-static/docs/development/core/extending-models.html +33 -5
- nautobot/project-static/docs/development/core/generic-views.html +30 -2
- nautobot/project-static/docs/development/core/getting-started.html +49 -12
- nautobot/project-static/docs/development/core/homepage.html +30 -2
- nautobot/project-static/docs/development/core/index.html +30 -2
- nautobot/project-static/docs/development/core/model-features.html +30 -2
- nautobot/project-static/docs/development/core/natural-keys.html +30 -2
- nautobot/project-static/docs/development/core/navigation-menu.html +30 -2
- nautobot/project-static/docs/development/core/release-checklist.html +30 -2
- nautobot/project-static/docs/development/core/role-internals.html +30 -2
- nautobot/project-static/docs/development/core/style-guide.html +30 -2
- nautobot/project-static/docs/development/core/templates.html +30 -2
- nautobot/project-static/docs/development/core/testing.html +30 -2
- nautobot/project-static/docs/development/core/user-preferences.html +30 -2
- nautobot/project-static/docs/development/index.html +30 -2
- nautobot/project-static/docs/development/jobs/index.html +30 -2
- nautobot/project-static/docs/development/jobs/migration/from-v1.html +30 -2
- nautobot/project-static/docs/index.html +30 -2
- nautobot/project-static/docs/objects.inv +0 -0
- nautobot/project-static/docs/release-notes/index.html +30 -2
- nautobot/project-static/docs/release-notes/version-1.0.html +30 -2
- nautobot/project-static/docs/release-notes/version-1.1.html +30 -2
- nautobot/project-static/docs/release-notes/version-1.2.html +30 -2
- nautobot/project-static/docs/release-notes/version-1.3.html +30 -2
- nautobot/project-static/docs/release-notes/version-1.4.html +31 -3
- nautobot/project-static/docs/release-notes/version-1.5.html +30 -2
- nautobot/project-static/docs/release-notes/version-1.6.html +573 -134
- nautobot/project-static/docs/release-notes/version-2.0.html +30 -2
- nautobot/project-static/docs/release-notes/version-2.1.html +539 -170
- nautobot/project-static/docs/search/search_index.json +1 -1
- nautobot/project-static/docs/sitemap.xml +250 -240
- nautobot/project-static/docs/sitemap.xml.gz +0 -0
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +30 -2
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +30 -2
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +30 -2
- nautobot/project-static/docs/user-guide/administration/configuration/index.html +30 -2
- nautobot/project-static/docs/user-guide/administration/configuration/optional-settings.html +49 -2
- nautobot/project-static/docs/user-guide/administration/configuration/required-settings.html +30 -2
- nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +30 -2
- nautobot/project-static/docs/user-guide/administration/guides/caching.html +30 -2
- nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +30 -2
- nautobot/project-static/docs/user-guide/administration/guides/healthcheck.html +30 -2
- nautobot/project-static/docs/user-guide/administration/guides/permissions.html +30 -2
- nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +30 -2
- nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +30 -2
- nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +30 -2
- nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +30 -2
- nautobot/project-static/docs/user-guide/administration/installation/app-install.html +30 -2
- nautobot/project-static/docs/user-guide/administration/installation/docker.html +37 -5
- nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +30 -2
- nautobot/project-static/docs/user-guide/administration/installation/health-checks.html +6019 -0
- nautobot/project-static/docs/user-guide/administration/installation/http-server.html +30 -2
- nautobot/project-static/docs/user-guide/administration/installation/index.html +30 -2
- nautobot/project-static/docs/user-guide/administration/installation/install_system.html +30 -2
- nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +30 -2
- nautobot/project-static/docs/user-guide/administration/installation/selinux-troubleshooting.html +33 -5
- nautobot/project-static/docs/user-guide/administration/installation/services.html +30 -2
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +30 -2
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +30 -2
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +30 -2
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +30 -2
- nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +30 -2
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +30 -2
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +30 -2
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +30 -2
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +30 -2
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +30 -2
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +30 -2
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +30 -2
- nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +30 -2
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +30 -2
- nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +33 -5
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +30 -2
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +30 -2
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +30 -2
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +30 -2
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +30 -2
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +30 -2
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +30 -2
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +30 -2
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +30 -2
- nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +30 -2
- nautobot/project-static/docs/user-guide/feature-guides/graphql.html +30 -2
- nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +30 -2
- nautobot/project-static/docs/user-guide/feature-guides/relationships.html +30 -2
- nautobot/project-static/docs/user-guide/index.html +30 -2
- nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +30 -2
- nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +30 -2
- nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +111 -15
- nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +30 -2
- nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +30 -2
- nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +30 -2
- nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +30 -2
- nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +30 -2
- nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +30 -2
- nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +30 -2
- nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +30 -2
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +30 -2
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +30 -2
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +30 -2
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +30 -2
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +30 -2
- nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +30 -2
- nautobot/project-static/docs/user-guide/platform-functionality/note.html +30 -2
- nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +30 -2
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +30 -2
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +30 -2
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +30 -2
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +30 -2
- nautobot/project-static/docs/user-guide/platform-functionality/role.html +30 -2
- nautobot/project-static/docs/user-guide/platform-functionality/secret.html +30 -2
- nautobot/project-static/docs/user-guide/platform-functionality/status.html +30 -2
- nautobot/project-static/docs/user-guide/platform-functionality/tag.html +30 -2
- nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +30 -2
- nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +30 -2
- nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +30 -2
- nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +30 -2
- nautobot/tenancy/api/urls.py +1 -2
- nautobot/tenancy/api/views.py +0 -12
- nautobot/tenancy/navigation.py +1 -1
- nautobot/tenancy/tests/test_filters.py +0 -168
- nautobot/users/api/urls.py +1 -2
- nautobot/users/api/views.py +2 -65
- nautobot/users/views.py +8 -8
- nautobot/virtualization/api/urls.py +1 -2
- nautobot/virtualization/api/views.py +0 -12
- nautobot/virtualization/tests/test_filters.py +0 -28
- {nautobot-2.1.7.dist-info → nautobot-2.1.9.dist-info}/METADATA +2 -2
- {nautobot-2.1.7.dist-info → nautobot-2.1.9.dist-info}/RECORD +338 -334
- {nautobot-2.1.7.dist-info → nautobot-2.1.9.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.1.7.dist-info → nautobot-2.1.9.dist-info}/NOTICE +0 -0
- {nautobot-2.1.7.dist-info → nautobot-2.1.9.dist-info}/WHEEL +0 -0
- {nautobot-2.1.7.dist-info → nautobot-2.1.9.dist-info}/entry_points.txt +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import logging
|
|
2
3
|
|
|
3
4
|
from django.conf import settings
|
|
@@ -42,7 +43,6 @@ class CustomFieldTest(ModelTestCases.BaseModelTestCase, TestCase):
|
|
|
42
43
|
def test_immutable_fields(self):
|
|
43
44
|
"""Some fields may not be changed once set, due to the potential for complex downstream effects."""
|
|
44
45
|
instance = CustomField(
|
|
45
|
-
# 2.0 TODO: #824 remove name field
|
|
46
46
|
label="Custom Field",
|
|
47
47
|
key="custom_field",
|
|
48
48
|
type=CustomFieldTypeChoices.TYPE_TEXT,
|
|
@@ -96,6 +96,11 @@ class CustomFieldTest(ModelTestCases.BaseModelTestCase, TestCase):
|
|
|
96
96
|
"field_value": "http://example.com/",
|
|
97
97
|
"empty_value": "",
|
|
98
98
|
},
|
|
99
|
+
{
|
|
100
|
+
"field_type": CustomFieldTypeChoices.TYPE_MARKDOWN,
|
|
101
|
+
"field_value": "### Hello world!\n\n- Item 1\n- Item 2\n- Item 3",
|
|
102
|
+
"empty_value": "",
|
|
103
|
+
},
|
|
99
104
|
{
|
|
100
105
|
"field_type": CustomFieldTypeChoices.TYPE_JSON,
|
|
101
106
|
"field_value": {"dict_key": "key value"},
|
|
@@ -394,35 +399,57 @@ class CustomFieldManagerTest(TestCase):
|
|
|
394
399
|
self.assertEqual(CustomField.objects.get_for_model(Location).count(), 2)
|
|
395
400
|
self.assertEqual(CustomField.objects.get_for_model(VirtualMachine).count(), 0)
|
|
396
401
|
|
|
397
|
-
def
|
|
398
|
-
"""Test that the
|
|
399
|
-
|
|
400
|
-
qs1 = CustomField.objects.get_for_model(Location)
|
|
401
|
-
|
|
402
|
+
def test_get_for_model_caching_and_cache_invalidation(self):
|
|
403
|
+
"""Test that the cache is used and is properly invalidated when CustomFields are created or deleted."""
|
|
402
404
|
# Assert that the cache is used when calling get_for_model a second time
|
|
403
|
-
|
|
404
|
-
self.
|
|
405
|
+
CustomField.objects.get_for_model(Location)
|
|
406
|
+
with self.assertNumQueries(0):
|
|
407
|
+
CustomField.objects.get_for_model(Location)
|
|
408
|
+
|
|
409
|
+
# Assert that different values of exclude_filter_disabled are cached separately
|
|
410
|
+
with self.assertNumQueries(1):
|
|
411
|
+
CustomField.objects.get_for_model(Location, exclude_filter_disabled=True)
|
|
412
|
+
with self.assertNumQueries(0):
|
|
413
|
+
CustomField.objects.get_for_model(Location, exclude_filter_disabled=True)
|
|
414
|
+
with self.assertNumQueries(0):
|
|
415
|
+
CustomField.objects.get_for_model(Location)
|
|
416
|
+
|
|
417
|
+
# Assert that different models are cached separately
|
|
418
|
+
with self.assertNumQueries(1):
|
|
419
|
+
CustomField.objects.get_for_model(VirtualMachine)
|
|
420
|
+
with self.assertNumQueries(0):
|
|
421
|
+
CustomField.objects.get_for_model(VirtualMachine)
|
|
422
|
+
with self.assertNumQueries(0):
|
|
423
|
+
CustomField.objects.get_for_model(Location)
|
|
405
424
|
|
|
406
425
|
# Assert that the cache is invalidated on object save
|
|
407
426
|
custom_field = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, label="Test CF1", default="foo")
|
|
408
427
|
custom_field.save()
|
|
409
|
-
|
|
410
|
-
|
|
428
|
+
with self.assertNumQueries(1):
|
|
429
|
+
CustomField.objects.get_for_model(Location)
|
|
430
|
+
with self.assertNumQueries(0):
|
|
431
|
+
CustomField.objects.get_for_model(Location)
|
|
411
432
|
|
|
412
433
|
# Assert that the cache is invalidated when adding a CustomField.content_types m2m relationship
|
|
413
434
|
custom_field.content_types.set([self.content_type])
|
|
414
|
-
|
|
415
|
-
|
|
435
|
+
with self.assertNumQueries(1):
|
|
436
|
+
CustomField.objects.get_for_model(Location)
|
|
437
|
+
with self.assertNumQueries(0):
|
|
438
|
+
CustomField.objects.get_for_model(Location)
|
|
416
439
|
|
|
417
440
|
# Assert that the cache is invalidated when removing a CustomField.content_types m2m relationship
|
|
418
441
|
custom_field.content_types.set([])
|
|
419
|
-
|
|
420
|
-
|
|
442
|
+
with self.assertNumQueries(1):
|
|
443
|
+
CustomField.objects.get_for_model(Location)
|
|
444
|
+
with self.assertNumQueries(0):
|
|
445
|
+
CustomField.objects.get_for_model(Location)
|
|
421
446
|
|
|
422
447
|
# Assert that the cache is invalidated on object delete
|
|
423
448
|
custom_field.delete()
|
|
424
|
-
|
|
425
|
-
|
|
449
|
+
with self.assertNumQueries(1):
|
|
450
|
+
CustomField.objects.get_for_model(Location)
|
|
451
|
+
with self.assertNumQueries(0):
|
|
452
|
+
CustomField.objects.get_for_model(Location)
|
|
426
453
|
|
|
427
454
|
|
|
428
455
|
class CustomFieldDataAPITest(APITestCase):
|
|
@@ -432,130 +459,162 @@ class CustomFieldDataAPITest(APITestCase):
|
|
|
432
459
|
For tests of the api/extras/custom-fields/ REST API endpoint itself, see test_api.py.
|
|
433
460
|
"""
|
|
434
461
|
|
|
435
|
-
|
|
436
|
-
|
|
462
|
+
user_permissions = (
|
|
463
|
+
"dcim.add_location",
|
|
464
|
+
"dcim.change_location",
|
|
465
|
+
"dcim.view_location",
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
def setUp(self):
|
|
469
|
+
super().setUp()
|
|
437
470
|
content_type = ContentType.objects.get_for_model(Location)
|
|
438
471
|
|
|
439
472
|
# Text custom field
|
|
440
|
-
|
|
473
|
+
self.cf_text = CustomField(
|
|
441
474
|
type=CustomFieldTypeChoices.TYPE_TEXT, label="Text Field", key="text_cf", default="FOO"
|
|
442
475
|
)
|
|
443
|
-
|
|
444
|
-
|
|
476
|
+
self.cf_text.validated_save()
|
|
477
|
+
self.cf_text.content_types.set([content_type])
|
|
445
478
|
|
|
446
479
|
# Integer custom field
|
|
447
|
-
|
|
480
|
+
self.cf_integer = CustomField(
|
|
448
481
|
type=CustomFieldTypeChoices.TYPE_INTEGER, label="Number Field", key="number_cf", default=12
|
|
449
482
|
)
|
|
450
|
-
|
|
451
|
-
|
|
483
|
+
self.cf_integer.validated_save()
|
|
484
|
+
self.cf_integer.content_types.set([content_type])
|
|
452
485
|
|
|
453
486
|
# Boolean custom field
|
|
454
|
-
|
|
487
|
+
self.cf_boolean = CustomField(
|
|
455
488
|
type=CustomFieldTypeChoices.TYPE_BOOLEAN,
|
|
456
489
|
label="Boolean Field",
|
|
457
490
|
key="boolean_cf",
|
|
458
491
|
default=False,
|
|
459
492
|
)
|
|
460
|
-
|
|
461
|
-
|
|
493
|
+
self.cf_boolean.validated_save()
|
|
494
|
+
self.cf_boolean.content_types.set([content_type])
|
|
462
495
|
|
|
463
496
|
# Date custom field
|
|
464
|
-
|
|
497
|
+
self.cf_date = CustomField(
|
|
465
498
|
type=CustomFieldTypeChoices.TYPE_DATE,
|
|
466
499
|
label="Date Field",
|
|
467
500
|
key="date_cf",
|
|
468
501
|
default="2020-01-01",
|
|
469
502
|
)
|
|
470
|
-
|
|
471
|
-
|
|
503
|
+
self.cf_date.validated_save()
|
|
504
|
+
self.cf_date.content_types.set([content_type])
|
|
472
505
|
|
|
473
506
|
# URL custom field
|
|
474
|
-
|
|
507
|
+
self.cf_url = CustomField(
|
|
475
508
|
type=CustomFieldTypeChoices.TYPE_URL,
|
|
476
509
|
label="URL Field",
|
|
477
510
|
key="url_cf",
|
|
478
511
|
default="http://example.com/1",
|
|
479
512
|
)
|
|
480
|
-
|
|
481
|
-
|
|
513
|
+
self.cf_url.validated_save()
|
|
514
|
+
self.cf_url.content_types.set([content_type])
|
|
482
515
|
|
|
483
516
|
# Select custom field
|
|
484
|
-
|
|
517
|
+
self.cf_select = CustomField(
|
|
485
518
|
type=CustomFieldTypeChoices.TYPE_SELECT,
|
|
486
519
|
label="Choice Field",
|
|
487
520
|
key="choice_cf",
|
|
488
521
|
)
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
CustomFieldChoice.objects.create(custom_field=
|
|
492
|
-
CustomFieldChoice.objects.create(custom_field=
|
|
493
|
-
CustomFieldChoice.objects.create(custom_field=
|
|
494
|
-
|
|
495
|
-
|
|
522
|
+
self.cf_select.validated_save()
|
|
523
|
+
self.cf_select.content_types.set([content_type])
|
|
524
|
+
CustomFieldChoice.objects.create(custom_field=self.cf_select, value="Foo")
|
|
525
|
+
CustomFieldChoice.objects.create(custom_field=self.cf_select, value="Bar")
|
|
526
|
+
CustomFieldChoice.objects.create(custom_field=self.cf_select, value="Baz")
|
|
527
|
+
self.cf_select.default = "Foo"
|
|
528
|
+
self.cf_select.validated_save()
|
|
496
529
|
|
|
497
530
|
# Multi-select custom field
|
|
498
|
-
|
|
531
|
+
self.cf_multi_select = CustomField(
|
|
499
532
|
type=CustomFieldTypeChoices.TYPE_MULTISELECT,
|
|
500
533
|
label="Multiple Choice Field",
|
|
501
534
|
key="multi_choice_cf",
|
|
502
535
|
)
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
CustomFieldChoice.objects.create(custom_field=
|
|
506
|
-
CustomFieldChoice.objects.create(custom_field=
|
|
507
|
-
CustomFieldChoice.objects.create(custom_field=
|
|
508
|
-
|
|
509
|
-
|
|
536
|
+
self.cf_multi_select.validated_save()
|
|
537
|
+
self.cf_multi_select.content_types.set([content_type])
|
|
538
|
+
CustomFieldChoice.objects.create(custom_field=self.cf_multi_select, value="Foo")
|
|
539
|
+
CustomFieldChoice.objects.create(custom_field=self.cf_multi_select, value="Bar")
|
|
540
|
+
CustomFieldChoice.objects.create(custom_field=self.cf_multi_select, value="Baz")
|
|
541
|
+
self.cf_multi_select.default = ["Foo", "Bar"]
|
|
542
|
+
self.cf_multi_select.validated_save()
|
|
543
|
+
|
|
544
|
+
# Markdown custom field
|
|
545
|
+
self.cf_markdown = CustomField(
|
|
546
|
+
type=CustomFieldTypeChoices.TYPE_MARKDOWN,
|
|
547
|
+
label="Markdown Field",
|
|
548
|
+
key="markdown_cf",
|
|
549
|
+
default="# One\n\n## Two\n\n### Three",
|
|
550
|
+
)
|
|
551
|
+
self.cf_markdown.validated_save()
|
|
552
|
+
self.cf_markdown.content_types.set([content_type])
|
|
553
|
+
|
|
554
|
+
# JSON custom field
|
|
555
|
+
self.cf_json = CustomField(
|
|
556
|
+
type=CustomFieldTypeChoices.TYPE_JSON,
|
|
557
|
+
label="JSON Field",
|
|
558
|
+
key="json_cf",
|
|
559
|
+
default={"dict": ["key1", "key2"]},
|
|
560
|
+
)
|
|
561
|
+
self.cf_json.validated_save()
|
|
562
|
+
self.cf_json.content_types.set([content_type])
|
|
563
|
+
|
|
564
|
+
self.all_cfs = [
|
|
565
|
+
self.cf_text,
|
|
566
|
+
self.cf_integer,
|
|
567
|
+
self.cf_boolean,
|
|
568
|
+
self.cf_date,
|
|
569
|
+
self.cf_url,
|
|
570
|
+
self.cf_select,
|
|
571
|
+
self.cf_multi_select,
|
|
572
|
+
self.cf_markdown,
|
|
573
|
+
self.cf_json,
|
|
574
|
+
]
|
|
510
575
|
|
|
511
576
|
if "example_plugin" in settings.PLUGINS:
|
|
512
|
-
|
|
577
|
+
self.cf_plugin_field = CustomField.objects.get(key="example_plugin_auto_custom_field")
|
|
578
|
+
self.all_cfs.append(self.cf_plugin_field)
|
|
513
579
|
|
|
514
|
-
|
|
580
|
+
self.statuses = Status.objects.get_for_model(Location)
|
|
515
581
|
|
|
516
582
|
# Create some locations
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
Location.objects.create(name="Location 1", status=
|
|
520
|
-
Location.objects.create(name="Location 2", status=
|
|
583
|
+
self.lt = LocationType.objects.get(name="Campus")
|
|
584
|
+
self.locations = (
|
|
585
|
+
Location.objects.create(name="Location 1", status=self.statuses[0], location_type=self.lt),
|
|
586
|
+
Location.objects.create(name="Location 2", status=self.statuses[0], location_type=self.lt),
|
|
521
587
|
)
|
|
522
588
|
|
|
523
589
|
# Assign custom field values for location 2
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
590
|
+
self.locations[1]._custom_field_data = {
|
|
591
|
+
self.cf_text.key: "bar",
|
|
592
|
+
self.cf_integer.key: 456,
|
|
593
|
+
self.cf_boolean.key: True,
|
|
594
|
+
self.cf_date.key: "2020-01-02",
|
|
595
|
+
self.cf_url.key: "http://example.com/2",
|
|
596
|
+
self.cf_select.key: "Bar",
|
|
597
|
+
self.cf_multi_select.key: ["Bar", "Baz"],
|
|
598
|
+
self.cf_markdown.key: "### Hello world!\n\n- Item 1\n- Item 2\n- Item 3",
|
|
599
|
+
self.cf_json.key: {"hello": "world"},
|
|
532
600
|
}
|
|
533
601
|
if "example_plugin" in settings.PLUGINS:
|
|
534
|
-
|
|
535
|
-
|
|
602
|
+
self.locations[1]._custom_field_data[self.cf_plugin_field.key] = "Custom value"
|
|
603
|
+
self.locations[1].validated_save()
|
|
604
|
+
self.list_url = reverse("dcim-api:location-list")
|
|
605
|
+
self.detail_url = reverse("dcim-api:location-detail", kwargs={"pk": self.locations[1].pk})
|
|
536
606
|
|
|
537
607
|
def test_get_single_object_without_custom_field_data(self):
|
|
538
608
|
"""
|
|
539
609
|
Validate that custom fields are present on an object even if it has no values defined.
|
|
540
610
|
"""
|
|
541
611
|
url = reverse("dcim-api:location-detail", kwargs={"pk": self.locations[0].pk})
|
|
542
|
-
self.add_permissions("dcim.view_location")
|
|
543
612
|
|
|
544
613
|
response = self.client.get(url, **self.header)
|
|
545
614
|
self.assertEqual(response.data["name"], self.locations[0].name)
|
|
546
615
|
# A model directly instantiated via the ORM does NOT automatically receive custom field default values.
|
|
547
616
|
# This is arguably a bug. See https://github.com/nautobot/nautobot/issues/3312 for details.
|
|
548
|
-
expected_data = {
|
|
549
|
-
"text_cf": None,
|
|
550
|
-
"number_cf": None,
|
|
551
|
-
"boolean_cf": None,
|
|
552
|
-
"date_cf": None,
|
|
553
|
-
"url_cf": None,
|
|
554
|
-
"choice_cf": None,
|
|
555
|
-
"multi_choice_cf": None,
|
|
556
|
-
}
|
|
557
|
-
if "example_plugin" in settings.PLUGINS:
|
|
558
|
-
expected_data["example_plugin_auto_custom_field"] = None
|
|
617
|
+
expected_data = {cf.key: None for cf in self.all_cfs}
|
|
559
618
|
self.assertEqual(response.data["custom_fields"], expected_data)
|
|
560
619
|
|
|
561
620
|
def test_get_single_object_with_custom_field_data(self):
|
|
@@ -563,18 +622,12 @@ class CustomFieldDataAPITest(APITestCase):
|
|
|
563
622
|
Validate that custom fields are present and correctly set for an object with values defined.
|
|
564
623
|
"""
|
|
565
624
|
location2_cfvs = self.locations[1].cf
|
|
566
|
-
|
|
567
|
-
self.add_permissions("dcim.view_location")
|
|
568
|
-
|
|
569
|
-
response = self.client.get(url, **self.header)
|
|
625
|
+
response = self.client.get(self.detail_url, **self.header)
|
|
570
626
|
self.assertEqual(response.data["name"], self.locations[1].name)
|
|
571
|
-
self.
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
self.assertEqual(response.data["custom_fields"]["url_cf"], location2_cfvs["url_cf"])
|
|
576
|
-
self.assertEqual(response.data["custom_fields"]["choice_cf"], location2_cfvs["choice_cf"])
|
|
577
|
-
self.assertEqual(response.data["custom_fields"]["multi_choice_cf"], location2_cfvs["multi_choice_cf"])
|
|
627
|
+
for cf in self.all_cfs:
|
|
628
|
+
self.assertIn(cf.key, response.data["custom_fields"])
|
|
629
|
+
self.assertIn(cf.key, location2_cfvs)
|
|
630
|
+
self.assertEqual(response.data["custom_fields"][cf.key], location2_cfvs[cf.key])
|
|
578
631
|
|
|
579
632
|
def test_create_single_object_with_defaults(self):
|
|
580
633
|
"""
|
|
@@ -585,35 +638,20 @@ class CustomFieldDataAPITest(APITestCase):
|
|
|
585
638
|
"location_type": self.lt.pk,
|
|
586
639
|
"status": self.statuses[0].pk,
|
|
587
640
|
}
|
|
588
|
-
|
|
589
|
-
self.add_permissions("dcim.add_location")
|
|
590
|
-
|
|
591
|
-
response = self.client.post(url, data, format="json", **self.header)
|
|
641
|
+
response = self.client.post(self.list_url, data, format="json", **self.header)
|
|
592
642
|
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
|
593
643
|
|
|
594
644
|
# Validate response data
|
|
595
645
|
response_cf = response.data["custom_fields"]
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
self.assertEqual(response_cf["date_cf"], self.cf_date.default)
|
|
600
|
-
self.assertEqual(response_cf["url_cf"], self.cf_url.default)
|
|
601
|
-
self.assertEqual(response_cf["choice_cf"], self.cf_select.default)
|
|
602
|
-
self.assertEqual(response_cf["multi_choice_cf"], self.cf_multi_select.default)
|
|
603
|
-
if "example_plugin" in settings.PLUGINS:
|
|
604
|
-
self.assertEqual(response_cf["example_plugin_auto_custom_field"], self.cf_plugin_field.default)
|
|
646
|
+
for cf in self.all_cfs:
|
|
647
|
+
self.assertIn(cf.key, response_cf)
|
|
648
|
+
self.assertEqual(response_cf[cf.key], cf.default)
|
|
605
649
|
|
|
606
650
|
# Validate database data
|
|
607
651
|
location = Location.objects.get(pk=response.data["id"])
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
self.assertEqual(str(location.cf["date_cf"]), self.cf_date.default)
|
|
612
|
-
self.assertEqual(location.cf["url_cf"], self.cf_url.default)
|
|
613
|
-
self.assertEqual(location.cf["choice_cf"], self.cf_select.default)
|
|
614
|
-
self.assertEqual(location.cf["multi_choice_cf"], self.cf_multi_select.default)
|
|
615
|
-
if "example_plugin" in settings.PLUGINS:
|
|
616
|
-
self.assertEqual(location.cf["example_plugin_auto_custom_field"], self.cf_plugin_field.default)
|
|
652
|
+
for cf in self.all_cfs:
|
|
653
|
+
self.assertIn(cf.key, location.cf)
|
|
654
|
+
self.assertEqual(location.cf[cf.key], cf.default)
|
|
617
655
|
|
|
618
656
|
def test_create_single_object_with_values(self):
|
|
619
657
|
"""
|
|
@@ -624,51 +662,35 @@ class CustomFieldDataAPITest(APITestCase):
|
|
|
624
662
|
"status": self.statuses[0].pk,
|
|
625
663
|
"location_type": self.lt.pk,
|
|
626
664
|
"custom_fields": {
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
665
|
+
self.cf_text.key: "bar",
|
|
666
|
+
self.cf_integer.key: 456,
|
|
667
|
+
self.cf_boolean.key: True,
|
|
668
|
+
self.cf_date.key: "2020-01-02",
|
|
669
|
+
self.cf_url.key: "http://example.com/2",
|
|
670
|
+
self.cf_select.key: "Bar",
|
|
671
|
+
self.cf_multi_select.key: ["Baz"],
|
|
672
|
+
self.cf_markdown.key: "[hello](http://example.com)",
|
|
673
|
+
self.cf_json.key: {"foo": "bar"},
|
|
634
674
|
},
|
|
635
675
|
}
|
|
636
676
|
if "example_plugin" in settings.PLUGINS:
|
|
637
677
|
data["custom_fields"]["example_plugin_auto_custom_field"] = "Custom value"
|
|
638
|
-
|
|
639
|
-
self.add_permissions("dcim.add_location")
|
|
640
|
-
|
|
641
|
-
response = self.client.post(url, data, format="json", **self.header)
|
|
678
|
+
response = self.client.post(self.list_url, data, format="json", **self.header)
|
|
642
679
|
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
|
643
680
|
|
|
644
681
|
# Validate response data
|
|
645
682
|
response_cf = response.data["custom_fields"]
|
|
646
683
|
data_cf = data["custom_fields"]
|
|
647
|
-
self.
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
self.assertEqual(response_cf["url_cf"], data_cf["url_cf"])
|
|
652
|
-
self.assertEqual(response_cf["choice_cf"], data_cf["choice_cf"])
|
|
653
|
-
self.assertEqual(response_cf["multi_choice_cf"], data_cf["multi_choice_cf"])
|
|
654
|
-
if "example_plugin" in settings.PLUGINS:
|
|
655
|
-
self.assertEqual(
|
|
656
|
-
response_cf["example_plugin_auto_custom_field"], data_cf["example_plugin_auto_custom_field"]
|
|
657
|
-
)
|
|
684
|
+
for cf in self.all_cfs:
|
|
685
|
+
self.assertIn(cf.key, response_cf)
|
|
686
|
+
self.assertIn(cf.key, data_cf)
|
|
687
|
+
self.assertEqual(response_cf[cf.key], data_cf[cf.key])
|
|
658
688
|
|
|
659
689
|
# Validate database data
|
|
660
690
|
location = Location.objects.get(pk=response.data["id"])
|
|
661
|
-
self.
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
self.assertEqual(str(location.cf["date_cf"]), data_cf["date_cf"])
|
|
665
|
-
self.assertEqual(location.cf["url_cf"], data_cf["url_cf"])
|
|
666
|
-
self.assertEqual(location.cf["choice_cf"], data_cf["choice_cf"])
|
|
667
|
-
self.assertEqual(location.cf["multi_choice_cf"], data_cf["multi_choice_cf"])
|
|
668
|
-
if "example_plugin" in settings.PLUGINS:
|
|
669
|
-
self.assertEqual(
|
|
670
|
-
location.cf["example_plugin_auto_custom_field"], data_cf["example_plugin_auto_custom_field"]
|
|
671
|
-
)
|
|
691
|
+
for cf in self.all_cfs:
|
|
692
|
+
self.assertIn(cf.key, location.cf)
|
|
693
|
+
self.assertEqual(location.cf[cf.key], data_cf[cf.key])
|
|
672
694
|
|
|
673
695
|
def test_create_multiple_objects_with_defaults(self):
|
|
674
696
|
"""
|
|
@@ -692,50 +714,37 @@ class CustomFieldDataAPITest(APITestCase):
|
|
|
692
714
|
"status": self.statuses[0].pk,
|
|
693
715
|
},
|
|
694
716
|
)
|
|
695
|
-
|
|
696
|
-
self.add_permissions("dcim.add_location")
|
|
697
|
-
|
|
698
|
-
response = self.client.post(url, data, format="json", **self.header)
|
|
717
|
+
response = self.client.post(self.list_url, data, format="json", **self.header)
|
|
699
718
|
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
|
700
719
|
self.assertEqual(len(response.data), len(data))
|
|
701
720
|
|
|
702
721
|
for i, _obj in enumerate(data):
|
|
703
722
|
# Validate response data
|
|
704
723
|
response_cf = response.data[i]["custom_fields"]
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
self.assertEqual(response_cf["date_cf"], self.cf_date.default)
|
|
709
|
-
self.assertEqual(response_cf["url_cf"], self.cf_url.default)
|
|
710
|
-
self.assertEqual(response_cf["choice_cf"], self.cf_select.default)
|
|
711
|
-
self.assertEqual(response_cf["multi_choice_cf"], self.cf_multi_select.default)
|
|
712
|
-
if "example_plugin" in settings.PLUGINS:
|
|
713
|
-
self.assertEqual(response_cf["example_plugin_auto_custom_field"], self.cf_plugin_field.default)
|
|
724
|
+
for cf in self.all_cfs:
|
|
725
|
+
self.assertIn(cf.key, response_cf)
|
|
726
|
+
self.assertEqual(response_cf[cf.key], cf.default)
|
|
714
727
|
|
|
715
728
|
# Validate database data
|
|
716
729
|
location = Location.objects.get(pk=response.data[i]["id"])
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
self.assertEqual(str(location.cf["date_cf"]), self.cf_date.default)
|
|
721
|
-
self.assertEqual(location.cf["url_cf"], self.cf_url.default)
|
|
722
|
-
self.assertEqual(location.cf["choice_cf"], self.cf_select.default)
|
|
723
|
-
self.assertEqual(location.cf["multi_choice_cf"], self.cf_multi_select.default)
|
|
724
|
-
if "example_plugin" in settings.PLUGINS:
|
|
725
|
-
self.assertEqual(location.cf["example_plugin_auto_custom_field"], self.cf_plugin_field.default)
|
|
730
|
+
for cf in self.all_cfs:
|
|
731
|
+
self.assertIn(cf.key, location.cf)
|
|
732
|
+
self.assertEqual(location.cf[cf.key], cf.default)
|
|
726
733
|
|
|
727
734
|
def test_create_multiple_objects_with_values(self):
|
|
728
735
|
"""
|
|
729
736
|
Create a three new locations, each with custom fields defined.
|
|
730
737
|
"""
|
|
731
738
|
custom_field_data = {
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
+
self.cf_text.key: "bar",
|
|
740
|
+
self.cf_integer.key: 456,
|
|
741
|
+
self.cf_boolean.key: True,
|
|
742
|
+
self.cf_date.key: "2020-01-02",
|
|
743
|
+
self.cf_url.key: "http://example.com/2",
|
|
744
|
+
self.cf_select.key: "Bar",
|
|
745
|
+
self.cf_multi_select.key: ["Foo", "Bar"],
|
|
746
|
+
self.cf_markdown.key: "### Heading",
|
|
747
|
+
self.cf_json.key: {"dict1": {"dict2": {}}},
|
|
739
748
|
}
|
|
740
749
|
if "example_plugin" in settings.PLUGINS:
|
|
741
750
|
custom_field_data["example_plugin_auto_custom_field"] = "Custom value"
|
|
@@ -759,43 +768,23 @@ class CustomFieldDataAPITest(APITestCase):
|
|
|
759
768
|
"custom_fields": custom_field_data,
|
|
760
769
|
},
|
|
761
770
|
)
|
|
762
|
-
|
|
763
|
-
self.add_permissions("dcim.add_location")
|
|
764
|
-
|
|
765
|
-
response = self.client.post(url, data, format="json", **self.header)
|
|
771
|
+
response = self.client.post(self.list_url, data, format="json", **self.header)
|
|
766
772
|
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
|
767
773
|
self.assertEqual(len(response.data), len(data))
|
|
768
774
|
|
|
769
775
|
for i, _obj in enumerate(data):
|
|
770
776
|
# Validate response data
|
|
771
777
|
response_cf = response.data[i]["custom_fields"]
|
|
772
|
-
self.
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
self.assertEqual(response_cf["url_cf"], custom_field_data["url_cf"])
|
|
777
|
-
self.assertEqual(response_cf["choice_cf"], custom_field_data["choice_cf"])
|
|
778
|
-
self.assertEqual(response_cf["multi_choice_cf"], custom_field_data["multi_choice_cf"])
|
|
779
|
-
if "example_plugin" in settings.PLUGINS:
|
|
780
|
-
self.assertEqual(
|
|
781
|
-
response_cf["example_plugin_auto_custom_field"],
|
|
782
|
-
custom_field_data["example_plugin_auto_custom_field"],
|
|
783
|
-
)
|
|
778
|
+
for cf in self.all_cfs:
|
|
779
|
+
self.assertIn(cf.key, response_cf)
|
|
780
|
+
self.assertIn(cf.key, custom_field_data)
|
|
781
|
+
self.assertEqual(response_cf[cf.key], custom_field_data[cf.key])
|
|
784
782
|
|
|
785
783
|
# Validate database data
|
|
786
784
|
location = Location.objects.get(pk=response.data[i]["id"])
|
|
787
|
-
self.
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
self.assertEqual(str(location.cf["date_cf"]), custom_field_data["date_cf"])
|
|
791
|
-
self.assertEqual(location.cf["url_cf"], custom_field_data["url_cf"])
|
|
792
|
-
self.assertEqual(location.cf["choice_cf"], custom_field_data["choice_cf"])
|
|
793
|
-
self.assertEqual(location.cf["multi_choice_cf"], custom_field_data["multi_choice_cf"])
|
|
794
|
-
if "example_plugin" in settings.PLUGINS:
|
|
795
|
-
self.assertEqual(
|
|
796
|
-
location.cf["example_plugin_auto_custom_field"],
|
|
797
|
-
custom_field_data["example_plugin_auto_custom_field"],
|
|
798
|
-
)
|
|
785
|
+
for cf in self.all_cfs:
|
|
786
|
+
self.assertIn(cf.key, location.cf)
|
|
787
|
+
self.assertEqual(location.cf[cf.key], custom_field_data[cf.key])
|
|
799
788
|
|
|
800
789
|
def test_update_single_object_with_values(self):
|
|
801
790
|
"""
|
|
@@ -806,114 +795,150 @@ class CustomFieldDataAPITest(APITestCase):
|
|
|
806
795
|
original_cfvs = {**location.cf}
|
|
807
796
|
data = {
|
|
808
797
|
"custom_fields": {
|
|
809
|
-
|
|
810
|
-
|
|
798
|
+
self.cf_text.key: "ABCD",
|
|
799
|
+
self.cf_integer.key: 1234,
|
|
811
800
|
},
|
|
812
801
|
}
|
|
813
|
-
|
|
814
|
-
self.add_permissions("dcim.change_location")
|
|
815
|
-
|
|
816
|
-
response = self.client.patch(url, data, format="json", **self.header)
|
|
802
|
+
response = self.client.patch(self.detail_url, data, format="json", **self.header)
|
|
817
803
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
818
804
|
|
|
819
805
|
# Validate response data
|
|
820
806
|
response_cf = response.data["custom_fields"]
|
|
821
|
-
self.
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
self.assertEqual(response_cf["choice_cf"], original_cfvs["choice_cf"])
|
|
827
|
-
self.assertEqual(response_cf["multi_choice_cf"], original_cfvs["multi_choice_cf"])
|
|
828
|
-
if "example_plugin" in settings.PLUGINS:
|
|
829
|
-
self.assertEqual(
|
|
830
|
-
response_cf["example_plugin_auto_custom_field"], original_cfvs["example_plugin_auto_custom_field"]
|
|
831
|
-
)
|
|
807
|
+
for cf in self.all_cfs:
|
|
808
|
+
if cf.key in data["custom_fields"]:
|
|
809
|
+
self.assertEqual(response_cf[cf.key], data["custom_fields"][cf.key])
|
|
810
|
+
else:
|
|
811
|
+
self.assertEqual(response_cf[cf.key], original_cfvs[cf.key])
|
|
832
812
|
|
|
833
813
|
# Validate database data
|
|
834
814
|
location.refresh_from_db()
|
|
835
|
-
self.
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
self.assertEqual(location.cf["boolean_cf"], original_cfvs["boolean_cf"])
|
|
841
|
-
self.assertEqual(location.cf["date_cf"], original_cfvs["date_cf"])
|
|
842
|
-
self.assertEqual(location.cf["url_cf"], original_cfvs["url_cf"])
|
|
843
|
-
self.assertEqual(location.cf["choice_cf"], original_cfvs["choice_cf"])
|
|
844
|
-
self.assertEqual(location.cf["multi_choice_cf"], original_cfvs["multi_choice_cf"])
|
|
845
|
-
if "example_plugin" in settings.PLUGINS:
|
|
846
|
-
self.assertEqual(
|
|
847
|
-
location.cf["example_plugin_auto_custom_field"], original_cfvs["example_plugin_auto_custom_field"]
|
|
848
|
-
)
|
|
849
|
-
|
|
850
|
-
def test_minimum_maximum_values_validation(self):
|
|
851
|
-
url = reverse("dcim-api:location-detail", kwargs={"pk": self.locations[1].pk})
|
|
852
|
-
self.add_permissions("dcim.change_location")
|
|
815
|
+
for cf in self.all_cfs:
|
|
816
|
+
if cf.key in data["custom_fields"]:
|
|
817
|
+
self.assertEqual(location.cf[cf.key], data["custom_fields"][cf.key])
|
|
818
|
+
else:
|
|
819
|
+
self.assertEqual(location.cf[cf.key], original_cfvs[cf.key])
|
|
853
820
|
|
|
821
|
+
def test_integer_minimum_maximum_values_validation(self):
|
|
854
822
|
self.cf_integer.validation_minimum = 10
|
|
855
823
|
self.cf_integer.validation_maximum = 20
|
|
856
824
|
self.cf_integer.save()
|
|
857
825
|
|
|
858
|
-
data = {"custom_fields": {
|
|
859
|
-
response = self.client.patch(
|
|
826
|
+
data = {"custom_fields": {self.cf_integer.key: 9}}
|
|
827
|
+
response = self.client.patch(self.detail_url, data, format="json", **self.header)
|
|
860
828
|
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
861
829
|
|
|
862
|
-
data = {"custom_fields": {
|
|
863
|
-
response = self.client.patch(
|
|
830
|
+
data = {"custom_fields": {self.cf_integer.key: 21}}
|
|
831
|
+
response = self.client.patch(self.detail_url, data, format="json", **self.header)
|
|
864
832
|
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
865
833
|
|
|
866
|
-
data = {"custom_fields": {
|
|
867
|
-
response = self.client.patch(
|
|
834
|
+
data = {"custom_fields": {self.cf_integer.key: 15}}
|
|
835
|
+
response = self.client.patch(self.detail_url, data, format="json", **self.header)
|
|
868
836
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
869
837
|
|
|
870
|
-
def
|
|
871
|
-
url = reverse("dcim-api:location-detail", kwargs={"pk": self.locations[1].pk})
|
|
872
|
-
self.add_permissions("dcim.change_location")
|
|
873
|
-
|
|
838
|
+
def test_integer_bigint_values_of_custom_field_maximum_attribute(self):
|
|
874
839
|
self.cf_integer.validation_maximum = 5000000000
|
|
875
840
|
self.cf_integer.save()
|
|
876
841
|
|
|
877
|
-
data = {"custom_fields": {
|
|
878
|
-
response = self.client.patch(
|
|
842
|
+
data = {"custom_fields": {self.cf_integer.key: 4294967294}}
|
|
843
|
+
response = self.client.patch(self.detail_url, data, format="json", **self.header)
|
|
879
844
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
880
845
|
|
|
881
|
-
data = {"custom_fields": {
|
|
882
|
-
response = self.client.patch(
|
|
846
|
+
data = {"custom_fields": {self.cf_integer.key: 5000000001}}
|
|
847
|
+
response = self.client.patch(self.detail_url, data, format="json", **self.header)
|
|
883
848
|
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
884
849
|
|
|
885
|
-
def
|
|
886
|
-
url = reverse("dcim-api:location-detail", kwargs={"pk": self.locations[1].pk})
|
|
887
|
-
self.add_permissions("dcim.change_location")
|
|
888
|
-
|
|
850
|
+
def test_integer_bigint_values_of_custom_field_minimum_attribute(self):
|
|
889
851
|
self.cf_integer.validation_minimum = -5000000000
|
|
890
852
|
self.cf_integer.save()
|
|
891
853
|
|
|
892
|
-
data = {"custom_fields": {
|
|
893
|
-
response = self.client.patch(
|
|
854
|
+
data = {"custom_fields": {self.cf_integer.key: -4294967294}}
|
|
855
|
+
response = self.client.patch(self.detail_url, data, format="json", **self.header)
|
|
894
856
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
895
857
|
|
|
896
|
-
data = {"custom_fields": {
|
|
897
|
-
response = self.client.patch(
|
|
858
|
+
data = {"custom_fields": {self.cf_integer.key: -5000000001}}
|
|
859
|
+
response = self.client.patch(self.detail_url, data, format="json", **self.header)
|
|
898
860
|
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
899
861
|
|
|
900
|
-
def
|
|
901
|
-
|
|
902
|
-
|
|
862
|
+
def test_text_minimum_maximum_length_validation(self):
|
|
863
|
+
# No minimum or maximum length by default
|
|
864
|
+
data = {
|
|
865
|
+
"custom_fields": {
|
|
866
|
+
self.cf_text.key: "",
|
|
867
|
+
self.cf_url.key: "",
|
|
868
|
+
self.cf_json.key: "",
|
|
869
|
+
self.cf_markdown.key: "",
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
response = self.client.patch(self.detail_url, data, format="json", **self.header)
|
|
873
|
+
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
874
|
+
|
|
875
|
+
data = {
|
|
876
|
+
"custom_fields": {
|
|
877
|
+
self.cf_text.key: "a" * 500,
|
|
878
|
+
self.cf_url.key: "b" * 500,
|
|
879
|
+
self.cf_json.key: "c" * 500,
|
|
880
|
+
self.cf_markdown.key: "d" * 500,
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
response = self.client.patch(self.detail_url, data, format="json", **self.header)
|
|
884
|
+
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
885
|
+
|
|
886
|
+
for cf in [self.cf_text, self.cf_url, self.cf_json, self.cf_markdown]:
|
|
887
|
+
if cf != self.cf_json:
|
|
888
|
+
cf.validation_minimum = len(cf.default)
|
|
889
|
+
invalid_value = cf.default[:-1]
|
|
890
|
+
else:
|
|
891
|
+
cf.validation_minimum = len(json.dumps(cf.default))
|
|
892
|
+
invalid_value = {}
|
|
893
|
+
cf.validated_save()
|
|
903
894
|
|
|
895
|
+
try:
|
|
896
|
+
invalid_data = {"custom_fields": {cf.key: invalid_value}}
|
|
897
|
+
response = self.client.patch(self.detail_url, invalid_data, format="json", **self.header)
|
|
898
|
+
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
899
|
+
|
|
900
|
+
valid_data = {"custom_fields": {cf.key: cf.default}}
|
|
901
|
+
response = self.client.patch(self.detail_url, valid_data, format="json", **self.header)
|
|
902
|
+
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
903
|
+
finally:
|
|
904
|
+
cf.validation_minimum = None
|
|
905
|
+
cf.validated_save()
|
|
906
|
+
|
|
907
|
+
for cf in [self.cf_text, self.cf_url, self.cf_json, self.cf_markdown]:
|
|
908
|
+
if cf != self.cf_json:
|
|
909
|
+
cf.validation_maximum = len(cf.default)
|
|
910
|
+
invalid_value = cf.default + "1"
|
|
911
|
+
else:
|
|
912
|
+
cf.validation_maximum = len(json.dumps(cf.default))
|
|
913
|
+
invalid_value = json.dumps(cf.default) + "1"
|
|
914
|
+
cf.validated_save()
|
|
915
|
+
|
|
916
|
+
try:
|
|
917
|
+
invalid_data = {"custom_fields": {cf.key: invalid_value}}
|
|
918
|
+
response = self.client.patch(self.detail_url, invalid_data, format="json", **self.header)
|
|
919
|
+
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
920
|
+
|
|
921
|
+
valid_data = {"custom_fields": {cf.key: cf.default}}
|
|
922
|
+
response = self.client.patch(self.detail_url, valid_data, format="json", **self.header)
|
|
923
|
+
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
924
|
+
finally:
|
|
925
|
+
cf.validation_maximum = None
|
|
926
|
+
cf.validated_save()
|
|
927
|
+
|
|
928
|
+
def test_regex_validation(self):
|
|
904
929
|
self.cf_text.validation_regex = r"^[A-Z]{3}$" # Three uppercase letters
|
|
905
930
|
self.cf_text.save()
|
|
906
931
|
|
|
907
|
-
data = {"custom_fields": {
|
|
908
|
-
response = self.client.patch(
|
|
932
|
+
data = {"custom_fields": {self.cf_text.key: "ABC123"}}
|
|
933
|
+
response = self.client.patch(self.detail_url, data, format="json", **self.header)
|
|
909
934
|
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
910
935
|
|
|
911
|
-
data = {"custom_fields": {
|
|
912
|
-
response = self.client.patch(
|
|
936
|
+
data = {"custom_fields": {self.cf_text.key: "abc"}}
|
|
937
|
+
response = self.client.patch(self.detail_url, data, format="json", **self.header)
|
|
913
938
|
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
914
939
|
|
|
915
|
-
data = {"custom_fields": {
|
|
916
|
-
response = self.client.patch(
|
|
940
|
+
data = {"custom_fields": {self.cf_text.key: "ABC"}}
|
|
941
|
+
response = self.client.patch(self.detail_url, data, format="json", **self.header)
|
|
917
942
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
918
943
|
|
|
919
944
|
def test_select_regex_validation(self):
|
|
@@ -935,6 +960,26 @@ class CustomFieldDataAPITest(APITestCase):
|
|
|
935
960
|
response = self.client.post(url, data, format="json", **self.header)
|
|
936
961
|
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
|
937
962
|
|
|
963
|
+
def test_select_minimum_maximum_validation(self):
|
|
964
|
+
url = reverse("extras-api:customfieldchoice-list")
|
|
965
|
+
self.add_permissions("extras.add_customfieldchoice")
|
|
966
|
+
|
|
967
|
+
self.cf_select.validation_minimum = len(self.cf_select.default)
|
|
968
|
+
self.cf_select.validation_maximum = len(self.cf_select.default)
|
|
969
|
+
self.cf_select.save()
|
|
970
|
+
|
|
971
|
+
data = {"custom_field": self.cf_select.id, "value": self.cf_select.default[:-1], "weight": 100}
|
|
972
|
+
response = self.client.post(url, data, format="json", **self.header)
|
|
973
|
+
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
974
|
+
|
|
975
|
+
data = {"custom_field": self.cf_select.id, "value": self.cf_select.default + "A", "weight": 100}
|
|
976
|
+
response = self.client.post(url, data, format="json", **self.header)
|
|
977
|
+
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
978
|
+
|
|
979
|
+
data = {"custom_field": self.cf_select.id, "value": "q" * len(self.cf_select.default), "weight": 100}
|
|
980
|
+
response = self.client.post(url, data, format="json", **self.header)
|
|
981
|
+
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
|
982
|
+
|
|
938
983
|
def test_text_type_with_invalid_values(self):
|
|
939
984
|
"""
|
|
940
985
|
Try and create a new location with an invalid value for a text type.
|
|
@@ -944,23 +989,20 @@ class CustomFieldDataAPITest(APITestCase):
|
|
|
944
989
|
"status": self.statuses[0].pk,
|
|
945
990
|
"location_type": self.lt.pk,
|
|
946
991
|
"custom_fields": {
|
|
947
|
-
|
|
992
|
+
self.cf_text.key: ["I", "am", "a", "disallowed", "type"],
|
|
948
993
|
},
|
|
949
994
|
}
|
|
950
|
-
|
|
951
|
-
self.add_permissions("dcim.add_location")
|
|
952
|
-
|
|
953
|
-
response = self.client.post(url, data, format="json", **self.header)
|
|
995
|
+
response = self.client.post(self.list_url, data, format="json", **self.header)
|
|
954
996
|
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
955
997
|
self.assertIn("Value must be a string", str(response.content))
|
|
956
998
|
|
|
957
|
-
data["custom_fields"].update({
|
|
958
|
-
response = self.client.post(
|
|
999
|
+
data["custom_fields"].update({self.cf_text.key: 2})
|
|
1000
|
+
response = self.client.post(self.list_url, data, format="json", **self.header)
|
|
959
1001
|
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
960
1002
|
self.assertIn("Value must be a string", str(response.content))
|
|
961
1003
|
|
|
962
|
-
data["custom_fields"].update({
|
|
963
|
-
response = self.client.post(
|
|
1004
|
+
data["custom_fields"].update({self.cf_text.key: True})
|
|
1005
|
+
response = self.client.post(self.list_url, data, format="json", **self.header)
|
|
964
1006
|
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
965
1007
|
self.assertIn("Value must be a string", str(response.content))
|
|
966
1008
|
|
|
@@ -974,9 +1016,7 @@ class CustomFieldDataAPITest(APITestCase):
|
|
|
974
1016
|
"location_type": self.lt.pk,
|
|
975
1017
|
"status": self.statuses[0].pk,
|
|
976
1018
|
}
|
|
977
|
-
|
|
978
|
-
self.add_permissions("dcim.add_location")
|
|
979
|
-
response = self.client.post(url, data, format="json", **self.header)
|
|
1019
|
+
response = self.client.post(self.list_url, data, format="json", **self.header)
|
|
980
1020
|
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
981
1021
|
self.assertIn("Required field cannot be empty", str(response.content))
|
|
982
1022
|
|
|
@@ -987,7 +1027,7 @@ class CustomFieldDataAPITest(APITestCase):
|
|
|
987
1027
|
f"Location N,{self.lt.composite_key},{self.statuses[0].name}",
|
|
988
1028
|
]
|
|
989
1029
|
)
|
|
990
|
-
response = self.client.post(
|
|
1030
|
+
response = self.client.post(self.list_url, csvdata, content_type="text/csv", **self.header)
|
|
991
1031
|
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
992
1032
|
self.assertIn("Required field cannot be empty", str(response.content))
|
|
993
1033
|
|
|
@@ -997,12 +1037,10 @@ class CustomFieldDataAPITest(APITestCase):
|
|
|
997
1037
|
"location_type": self.lt.pk,
|
|
998
1038
|
"status": self.statuses[0].pk,
|
|
999
1039
|
"custom_fields": {
|
|
1000
|
-
|
|
1040
|
+
self.cf_select.key: "Frobozz",
|
|
1001
1041
|
},
|
|
1002
1042
|
}
|
|
1003
|
-
|
|
1004
|
-
self.add_permissions("dcim.add_location")
|
|
1005
|
-
response = self.client.post(url, data, format="json", **self.header)
|
|
1043
|
+
response = self.client.post(self.list_url, data, format="json", **self.header)
|
|
1006
1044
|
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
1007
1045
|
self.assertIn("Invalid choice", str(response.content))
|
|
1008
1046
|
|
|
@@ -1013,7 +1051,7 @@ class CustomFieldDataAPITest(APITestCase):
|
|
|
1013
1051
|
f"Location N,{self.lt.composite_key},{self.statuses[0].name},Frobozz",
|
|
1014
1052
|
]
|
|
1015
1053
|
)
|
|
1016
|
-
response = self.client.post(
|
|
1054
|
+
response = self.client.post(self.list_url, csvdata, content_type="text/csv", **self.header)
|
|
1017
1055
|
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
1018
1056
|
self.assertIn("Invalid choice", str(response.content))
|
|
1019
1057
|
|
|
@@ -1262,7 +1300,6 @@ class CustomFieldModelTest(TestCase):
|
|
|
1262
1300
|
"""Test that omitting required custom fields raises a ValidationError."""
|
|
1263
1301
|
label = "Custom Field"
|
|
1264
1302
|
custom_field = CustomField.objects.create(
|
|
1265
|
-
# 2.0 TODO: #824 remove name field
|
|
1266
1303
|
label=label,
|
|
1267
1304
|
key="custom_field",
|
|
1268
1305
|
type=CustomFieldTypeChoices.TYPE_TEXT,
|
|
@@ -1279,7 +1316,6 @@ class CustomFieldModelTest(TestCase):
|
|
|
1279
1316
|
"""Test that removing required custom fields and then updating an object raises a ValidationError."""
|
|
1280
1317
|
label = "Custom Field"
|
|
1281
1318
|
custom_field = CustomField.objects.create(
|
|
1282
|
-
# 2.0 TODO: #824 remove name field
|
|
1283
1319
|
label=label,
|
|
1284
1320
|
key="custom_field",
|
|
1285
1321
|
type=CustomFieldTypeChoices.TYPE_TEXT,
|
|
@@ -2158,7 +2194,7 @@ class CustomFieldTableTest(TestCase):
|
|
|
2158
2194
|
for col_name, col_expected_value in custom_column_expected.items():
|
|
2159
2195
|
internal_col_name = "cf_" + col_name
|
|
2160
2196
|
custom_column = location_table.base_columns.get(internal_col_name)
|
|
2161
|
-
self.assertIsNotNone(custom_column)
|
|
2197
|
+
self.assertIsNotNone(custom_column, internal_col_name)
|
|
2162
2198
|
self.assertIsInstance(custom_column, CustomFieldColumn)
|
|
2163
2199
|
|
|
2164
2200
|
rendered_value = bound_row.get_cell(internal_col_name)
|