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
|
@@ -1268,39 +1268,11 @@ class PlatformTestCase(FilterTestCases.NameOnlyFilterTestCase):
|
|
|
1268
1268
|
params = {"devices": [devices[0].pk, devices[1].pk]}
|
|
1269
1269
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), len(devices))
|
|
1270
1270
|
|
|
1271
|
-
def test_has_devices(self):
|
|
1272
|
-
with self.subTest():
|
|
1273
|
-
params = {"has_devices": True}
|
|
1274
|
-
self.assertQuerysetEqual(
|
|
1275
|
-
self.filterset(params, self.queryset).qs,
|
|
1276
|
-
self.queryset.exclude(devices__isnull=True),
|
|
1277
|
-
)
|
|
1278
|
-
with self.subTest():
|
|
1279
|
-
params = {"has_devices": False}
|
|
1280
|
-
self.assertQuerysetEqual(
|
|
1281
|
-
self.filterset(params, self.queryset).qs,
|
|
1282
|
-
self.queryset.exclude(devices__isnull=False),
|
|
1283
|
-
)
|
|
1284
|
-
|
|
1285
1271
|
def test_virtual_machines(self):
|
|
1286
1272
|
virtual_machines = [VirtualMachine.objects.first(), VirtualMachine.objects.last()]
|
|
1287
1273
|
params = {"virtual_machines": [virtual_machines[0].pk, virtual_machines[1].pk]}
|
|
1288
1274
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), len(virtual_machines))
|
|
1289
1275
|
|
|
1290
|
-
def test_has_virtual_machines(self):
|
|
1291
|
-
with self.subTest():
|
|
1292
|
-
params = {"has_virtual_machines": True}
|
|
1293
|
-
self.assertQuerysetEqual(
|
|
1294
|
-
self.filterset(params, self.queryset).qs,
|
|
1295
|
-
self.queryset.exclude(virtual_machines__isnull=True),
|
|
1296
|
-
)
|
|
1297
|
-
with self.subTest():
|
|
1298
|
-
params = {"has_virtual_machines": False}
|
|
1299
|
-
self.assertQuerysetEqual(
|
|
1300
|
-
self.filterset(params, self.queryset).qs,
|
|
1301
|
-
self.queryset.exclude(virtual_machines__isnull=False),
|
|
1302
|
-
)
|
|
1303
|
-
|
|
1304
1276
|
|
|
1305
1277
|
class DeviceTestCase(FilterTestCases.FilterTestCase, FilterTestCases.TenancyFilterTestCaseMixin):
|
|
1306
1278
|
queryset = Device.objects.all()
|
nautobot/dcim/views.py
CHANGED
|
@@ -11,7 +11,7 @@ from django.forms import (
|
|
|
11
11
|
ModelMultipleChoiceField,
|
|
12
12
|
MultipleHiddenInput,
|
|
13
13
|
)
|
|
14
|
-
from django.shortcuts import get_object_or_404, redirect, render
|
|
14
|
+
from django.shortcuts import get_object_or_404, HttpResponse, redirect, render
|
|
15
15
|
from django.utils.functional import cached_property
|
|
16
16
|
from django.utils.html import format_html
|
|
17
17
|
from django.views.generic import View
|
|
@@ -2316,7 +2316,7 @@ class CableCreateView(generic.ObjectEditView):
|
|
|
2316
2316
|
"rear-port": forms.ConnectCableToRearPortForm,
|
|
2317
2317
|
"power-feed": forms.ConnectCableToPowerFeedForm,
|
|
2318
2318
|
"circuit-termination": forms.ConnectCableToCircuitTerminationForm,
|
|
2319
|
-
}
|
|
2319
|
+
}.get(kwargs.get("termination_b_type"), None)
|
|
2320
2320
|
|
|
2321
2321
|
return super().dispatch(request, *args, **kwargs)
|
|
2322
2322
|
|
|
@@ -2333,6 +2333,9 @@ class CableCreateView(generic.ObjectEditView):
|
|
|
2333
2333
|
return obj
|
|
2334
2334
|
|
|
2335
2335
|
def get(self, request, *args, **kwargs):
|
|
2336
|
+
if self.model_form is None:
|
|
2337
|
+
return HttpResponse(status_code=400)
|
|
2338
|
+
|
|
2336
2339
|
obj = self.alter_obj(self.get_object(kwargs), request, args, kwargs)
|
|
2337
2340
|
|
|
2338
2341
|
# Parse initial data manually to avoid setting field values as lists
|
nautobot/extras/api/urls.py
CHANGED
|
@@ -2,8 +2,7 @@ from nautobot.core.api.routers import OrderedDefaultRouter
|
|
|
2
2
|
|
|
3
3
|
from . import views
|
|
4
4
|
|
|
5
|
-
router = OrderedDefaultRouter()
|
|
6
|
-
router.APIRootView = views.ExtrasRootView
|
|
5
|
+
router = OrderedDefaultRouter(view_name="Extras")
|
|
7
6
|
|
|
8
7
|
# Computed Fields
|
|
9
8
|
router.register("computed-fields", views.ComputedFieldViewSet)
|
nautobot/extras/api/views.py
CHANGED
|
@@ -16,7 +16,6 @@ from rest_framework.exceptions import MethodNotAllowed, PermissionDenied, Valida
|
|
|
16
16
|
from rest_framework.parsers import JSONParser, MultiPartParser
|
|
17
17
|
from rest_framework.permissions import IsAuthenticated
|
|
18
18
|
from rest_framework.response import Response
|
|
19
|
-
from rest_framework.routers import APIRootView
|
|
20
19
|
|
|
21
20
|
from nautobot.core.api.authentication import TokenPermissions
|
|
22
21
|
from nautobot.core.api.utils import get_serializer_for_model
|
|
@@ -74,15 +73,6 @@ from nautobot.extras.utils import get_worker_count
|
|
|
74
73
|
from . import serializers
|
|
75
74
|
|
|
76
75
|
|
|
77
|
-
class ExtrasRootView(APIRootView):
|
|
78
|
-
"""
|
|
79
|
-
Extras API root view
|
|
80
|
-
"""
|
|
81
|
-
|
|
82
|
-
def get_view_name(self):
|
|
83
|
-
return "Extras"
|
|
84
|
-
|
|
85
|
-
|
|
86
76
|
class NotesViewSetMixin:
|
|
87
77
|
def restrict_queryset(self, request, *args, **kwargs):
|
|
88
78
|
"""
|
nautobot/extras/choices.py
CHANGED
|
@@ -64,11 +64,25 @@ class CustomFieldTypeChoices(ChoiceSet):
|
|
|
64
64
|
(TYPE_MARKDOWN, "Markdown"),
|
|
65
65
|
)
|
|
66
66
|
|
|
67
|
+
# Types that support validation_minimum/validation_maximum
|
|
68
|
+
MIN_MAX_TYPES = (
|
|
69
|
+
TYPE_TEXT,
|
|
70
|
+
TYPE_INTEGER,
|
|
71
|
+
TYPE_URL,
|
|
72
|
+
TYPE_SELECT,
|
|
73
|
+
TYPE_MULTISELECT,
|
|
74
|
+
TYPE_JSON,
|
|
75
|
+
TYPE_MARKDOWN,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Types that support validation_regex
|
|
67
79
|
REGEX_TYPES = (
|
|
68
80
|
TYPE_TEXT,
|
|
69
81
|
TYPE_URL,
|
|
70
82
|
TYPE_SELECT,
|
|
71
83
|
TYPE_MULTISELECT,
|
|
84
|
+
TYPE_JSON,
|
|
85
|
+
TYPE_MARKDOWN,
|
|
72
86
|
)
|
|
73
87
|
|
|
74
88
|
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
from collections import OrderedDict
|
|
2
2
|
from datetime import date, datetime
|
|
3
|
-
|
|
3
|
+
import json
|
|
4
4
|
import logging
|
|
5
5
|
import re
|
|
6
6
|
|
|
7
7
|
from django import forms
|
|
8
8
|
from django.contrib.contenttypes.models import ContentType
|
|
9
|
+
from django.core.cache import cache
|
|
9
10
|
from django.core.exceptions import ObjectDoesNotExist
|
|
10
11
|
from django.core.serializers.json import DjangoJSONEncoder
|
|
11
12
|
from django.core.validators import RegexValidator, ValidationError
|
|
@@ -46,13 +47,20 @@ logger = logging.getLogger(__name__)
|
|
|
46
47
|
class ComputedFieldManager(BaseManager.from_queryset(RestrictedQuerySet)):
|
|
47
48
|
use_in_migrations = True
|
|
48
49
|
|
|
49
|
-
@lru_cache(maxsize=128)
|
|
50
50
|
def get_for_model(self, model):
|
|
51
51
|
"""
|
|
52
52
|
Return all ComputedFields assigned to the given model.
|
|
53
53
|
"""
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
concrete_model = model._meta.concrete_model
|
|
55
|
+
cache_key = f"{self.get_for_model.cache_key_prefix}.{concrete_model._meta.label_lower}"
|
|
56
|
+
queryset = cache.get(cache_key)
|
|
57
|
+
if queryset is None:
|
|
58
|
+
content_type = ContentType.objects.get_for_model(concrete_model)
|
|
59
|
+
queryset = self.get_queryset().filter(content_type=content_type)
|
|
60
|
+
cache.set(cache_key, queryset)
|
|
61
|
+
return queryset
|
|
62
|
+
|
|
63
|
+
get_for_model.cache_key_prefix = "nautobot.extras.computedfield.get_for_model"
|
|
56
64
|
|
|
57
65
|
|
|
58
66
|
@extras_features("graphql")
|
|
@@ -298,7 +306,6 @@ class CustomFieldModel(models.Model):
|
|
|
298
306
|
class CustomFieldManager(BaseManager.from_queryset(RestrictedQuerySet)):
|
|
299
307
|
use_in_migrations = True
|
|
300
308
|
|
|
301
|
-
@lru_cache(maxsize=128)
|
|
302
309
|
def get_for_model(self, model, exclude_filter_disabled=False):
|
|
303
310
|
"""
|
|
304
311
|
Return all CustomFields assigned to the given model.
|
|
@@ -307,11 +314,20 @@ class CustomFieldManager(BaseManager.from_queryset(RestrictedQuerySet)):
|
|
|
307
314
|
model: The django model to which custom fields are registered
|
|
308
315
|
exclude_filter_disabled: Exclude any custom fields which have filter logic disabled
|
|
309
316
|
"""
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
317
|
+
concrete_model = model._meta.concrete_model
|
|
318
|
+
cache_key = (
|
|
319
|
+
f"{self.get_for_model.cache_key_prefix}.{concrete_model._meta.label_lower}.{exclude_filter_disabled}"
|
|
320
|
+
)
|
|
321
|
+
queryset = cache.get(cache_key)
|
|
322
|
+
if queryset is None:
|
|
323
|
+
content_type = ContentType.objects.get_for_model(concrete_model)
|
|
324
|
+
queryset = self.get_queryset().filter(content_types=content_type)
|
|
325
|
+
if exclude_filter_disabled:
|
|
326
|
+
queryset = queryset.exclude(filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED)
|
|
327
|
+
cache.set(cache_key, queryset)
|
|
328
|
+
return queryset
|
|
329
|
+
|
|
330
|
+
get_for_model.cache_key_prefix = "nautobot.extras.customfield.get_for_model"
|
|
315
331
|
|
|
316
332
|
|
|
317
333
|
@extras_features("webhooks")
|
|
@@ -375,13 +391,13 @@ class CustomField(BaseModel, ChangeLoggedModel, NotesMixin):
|
|
|
375
391
|
blank=True,
|
|
376
392
|
null=True,
|
|
377
393
|
verbose_name="Minimum value",
|
|
378
|
-
help_text="Minimum allowed value (for numeric fields).",
|
|
394
|
+
help_text="Minimum allowed value (for numeric fields) or length (for text fields).",
|
|
379
395
|
)
|
|
380
396
|
validation_maximum = models.BigIntegerField(
|
|
381
397
|
blank=True,
|
|
382
398
|
null=True,
|
|
383
399
|
verbose_name="Maximum value",
|
|
384
|
-
help_text="Maximum allowed value (for numeric fields).",
|
|
400
|
+
help_text="Maximum allowed value (for numeric fields) or length (for text fields).",
|
|
385
401
|
)
|
|
386
402
|
validation_regex = models.CharField(
|
|
387
403
|
blank=True,
|
|
@@ -449,16 +465,16 @@ class CustomField(BaseModel, ChangeLoggedModel, NotesMixin):
|
|
|
449
465
|
except ValidationError as err:
|
|
450
466
|
raise ValidationError({"default": f'Invalid default value "{self.default}": {err.message}'})
|
|
451
467
|
|
|
452
|
-
# Minimum/maximum values can be set only for
|
|
453
|
-
if self.validation_minimum is not None and self.type
|
|
454
|
-
raise ValidationError({"validation_minimum": "A minimum value may be set
|
|
455
|
-
if self.validation_maximum is not None and self.type
|
|
456
|
-
raise ValidationError({"validation_maximum": "A maximum value may be set
|
|
468
|
+
# Minimum/maximum values can be set only for fields that support them
|
|
469
|
+
if self.validation_minimum is not None and self.type not in CustomFieldTypeChoices.MIN_MAX_TYPES:
|
|
470
|
+
raise ValidationError({"validation_minimum": "A minimum value may not be set for fields of this type"})
|
|
471
|
+
if self.validation_maximum is not None and self.type not in CustomFieldTypeChoices.MIN_MAX_TYPES:
|
|
472
|
+
raise ValidationError({"validation_maximum": "A maximum value may not be set for fields of this type"})
|
|
457
473
|
|
|
458
474
|
# Regex validation can be set only for text, url, select and multi-select fields
|
|
459
475
|
if self.validation_regex and self.type not in CustomFieldTypeChoices.REGEX_TYPES:
|
|
460
476
|
raise ValidationError(
|
|
461
|
-
{"validation_regex": "Regular expression validation is supported
|
|
477
|
+
{"validation_regex": "Regular expression validation is not supported for fields of this type"}
|
|
462
478
|
)
|
|
463
479
|
|
|
464
480
|
# Choices can be set only on selection fields
|
|
@@ -525,13 +541,35 @@ class CustomField(BaseModel, ChangeLoggedModel, NotesMixin):
|
|
|
525
541
|
widget=DatePicker(),
|
|
526
542
|
)
|
|
527
543
|
|
|
528
|
-
# Text
|
|
529
|
-
elif self.type in (
|
|
544
|
+
# Text-like fields
|
|
545
|
+
elif self.type in (
|
|
546
|
+
CustomFieldTypeChoices.TYPE_URL,
|
|
547
|
+
CustomFieldTypeChoices.TYPE_TEXT,
|
|
548
|
+
CustomFieldTypeChoices.TYPE_MARKDOWN,
|
|
549
|
+
):
|
|
530
550
|
if self.type == CustomFieldTypeChoices.TYPE_URL:
|
|
531
|
-
field = LaxURLField(
|
|
551
|
+
field = LaxURLField(
|
|
552
|
+
required=required,
|
|
553
|
+
initial=initial,
|
|
554
|
+
min_length=self.validation_minimum,
|
|
555
|
+
max_length=self.validation_maximum,
|
|
556
|
+
)
|
|
532
557
|
elif self.type == CustomFieldTypeChoices.TYPE_TEXT:
|
|
533
|
-
field = forms.CharField(
|
|
534
|
-
|
|
558
|
+
field = forms.CharField(
|
|
559
|
+
required=required,
|
|
560
|
+
initial=initial,
|
|
561
|
+
min_length=self.validation_minimum,
|
|
562
|
+
max_length=self.validation_maximum,
|
|
563
|
+
)
|
|
564
|
+
elif self.type == CustomFieldTypeChoices.TYPE_MARKDOWN:
|
|
565
|
+
field = CommentField(
|
|
566
|
+
required=required,
|
|
567
|
+
initial=initial,
|
|
568
|
+
widget=SmallTextarea,
|
|
569
|
+
label=None,
|
|
570
|
+
min_length=self.validation_minimum,
|
|
571
|
+
max_length=self.validation_maximum,
|
|
572
|
+
)
|
|
535
573
|
if self.validation_regex:
|
|
536
574
|
field.validators = [
|
|
537
575
|
RegexValidator(
|
|
@@ -540,12 +578,10 @@ class CustomField(BaseModel, ChangeLoggedModel, NotesMixin):
|
|
|
540
578
|
)
|
|
541
579
|
]
|
|
542
580
|
|
|
543
|
-
# Markdown
|
|
544
|
-
elif self.type == CustomFieldTypeChoices.TYPE_MARKDOWN:
|
|
545
|
-
field = CommentField(widget=SmallTextarea, label=None)
|
|
546
|
-
|
|
547
581
|
# JSON
|
|
548
582
|
elif self.type == CustomFieldTypeChoices.TYPE_JSON:
|
|
583
|
+
# Unlike the above cases, we don't apply min_length/max_length to the field,
|
|
584
|
+
# nor do we add a RegexValidator to the field, as these all apply after parsing and validating the JSON
|
|
549
585
|
if simple_json_filter:
|
|
550
586
|
field = JSONField(encoder=DjangoJSONEncoder, required=required, initial=None, widget=TextInput)
|
|
551
587
|
else:
|
|
@@ -606,15 +642,33 @@ class CustomField(BaseModel, ChangeLoggedModel, NotesMixin):
|
|
|
606
642
|
"""
|
|
607
643
|
if value not in [None, "", []]:
|
|
608
644
|
# Validate text field
|
|
609
|
-
if self.type in (
|
|
645
|
+
if self.type in (
|
|
646
|
+
CustomFieldTypeChoices.TYPE_TEXT,
|
|
647
|
+
CustomFieldTypeChoices.TYPE_URL,
|
|
648
|
+
CustomFieldTypeChoices.TYPE_MARKDOWN,
|
|
649
|
+
):
|
|
610
650
|
if not isinstance(value, str):
|
|
611
651
|
raise ValidationError("Value must be a string")
|
|
612
|
-
|
|
652
|
+
if self.validation_minimum is not None and len(value) < self.validation_minimum:
|
|
653
|
+
raise ValidationError(f"Value must be at least {self.validation_minimum} characters in length")
|
|
654
|
+
if self.validation_maximum is not None and len(value) > self.validation_maximum:
|
|
655
|
+
raise ValidationError(f"Value must not exceed {self.validation_maximum} characters in length")
|
|
613
656
|
if self.validation_regex and not re.search(self.validation_regex, value):
|
|
614
657
|
raise ValidationError(f"Value must match regex '{self.validation_regex}'")
|
|
615
658
|
|
|
659
|
+
# Validate JSON
|
|
660
|
+
elif self.type == CustomFieldTypeChoices.TYPE_JSON:
|
|
661
|
+
if self.validation_regex or self.validation_minimum is not None or self.validation_maximum is not None:
|
|
662
|
+
json_value = json.dumps(value)
|
|
663
|
+
if self.validation_minimum is not None and len(json_value) < self.validation_minimum:
|
|
664
|
+
raise ValidationError(f"Value must be at least {self.validation_minimum} characters in length")
|
|
665
|
+
if self.validation_maximum is not None and len(json_value) > self.validation_maximum:
|
|
666
|
+
raise ValidationError(f"Value must not exceed {self.validation_maximum} characters in length")
|
|
667
|
+
if self.validation_regex and not re.search(self.validation_regex, json_value):
|
|
668
|
+
raise ValidationError(f"Value must match regex '{self.validation_regex}'")
|
|
669
|
+
|
|
616
670
|
# Validate integer
|
|
617
|
-
|
|
671
|
+
elif self.type == CustomFieldTypeChoices.TYPE_INTEGER:
|
|
618
672
|
try:
|
|
619
673
|
value = int(value)
|
|
620
674
|
except ValueError:
|
|
@@ -625,14 +679,14 @@ class CustomField(BaseModel, ChangeLoggedModel, NotesMixin):
|
|
|
625
679
|
raise ValidationError(f"Value must not exceed {self.validation_maximum}")
|
|
626
680
|
|
|
627
681
|
# Validate boolean
|
|
628
|
-
|
|
682
|
+
elif self.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
|
|
629
683
|
try:
|
|
630
684
|
value = is_truthy(value)
|
|
631
685
|
except ValueError as exc:
|
|
632
686
|
raise ValidationError("Value must be true or false.") from exc
|
|
633
687
|
|
|
634
688
|
# Validate date
|
|
635
|
-
|
|
689
|
+
elif self.type == CustomFieldTypeChoices.TYPE_DATE:
|
|
636
690
|
if not isinstance(value, date):
|
|
637
691
|
try:
|
|
638
692
|
datetime.strptime(value, "%Y-%m-%d")
|
|
@@ -640,13 +694,13 @@ class CustomField(BaseModel, ChangeLoggedModel, NotesMixin):
|
|
|
640
694
|
raise ValidationError("Date values must be in the format YYYY-MM-DD.")
|
|
641
695
|
|
|
642
696
|
# Validate selected choice
|
|
643
|
-
|
|
697
|
+
elif self.type == CustomFieldTypeChoices.TYPE_SELECT:
|
|
644
698
|
if value not in self.custom_field_choices.values_list("value", flat=True):
|
|
645
699
|
raise ValidationError(
|
|
646
700
|
f"Invalid choice ({value}). Available choices are: {', '.join(self.custom_field_choices.values_list('value', flat=True))}"
|
|
647
701
|
)
|
|
648
702
|
|
|
649
|
-
|
|
703
|
+
elif self.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
|
|
650
704
|
if isinstance(value, str):
|
|
651
705
|
value = value.split(",")
|
|
652
706
|
if not set(value).issubset(self.custom_field_choices.values_list("value", flat=True)):
|
|
@@ -706,6 +760,11 @@ class CustomFieldChoice(BaseModel, ChangeLoggedModel):
|
|
|
706
760
|
if self.custom_field.type not in (CustomFieldTypeChoices.TYPE_SELECT, CustomFieldTypeChoices.TYPE_MULTISELECT):
|
|
707
761
|
raise ValidationError("Custom field choices can only be assigned to selection fields.")
|
|
708
762
|
|
|
763
|
+
if self.custom_field.validation_minimum is not None and len(self.value) < self.custom_field.validation_minimum:
|
|
764
|
+
raise ValidationError(f"Value must be at least {self.custom_field.validation_minimum} characters long.")
|
|
765
|
+
if self.custom_field.validation_maximum is not None and len(self.value) > self.custom_field.validation_maximum:
|
|
766
|
+
raise ValidationError(f"Value must not exceed {self.custom_field.validation_maximum} characters long.")
|
|
767
|
+
|
|
709
768
|
if not re.search(self.custom_field.validation_regex, self.value):
|
|
710
769
|
raise ValidationError(f"Value must match regex {self.custom_field.validation_regex} got {self.value}.")
|
|
711
770
|
|
nautobot/extras/models/groups.py
CHANGED
|
@@ -325,7 +325,7 @@ class DynamicGroup(OrganizationalModel):
|
|
|
325
325
|
@property
|
|
326
326
|
def members_cache_key(self):
|
|
327
327
|
"""Return the cache key for this group's members."""
|
|
328
|
-
return f"
|
|
328
|
+
return f"nautobot.extras.dynamicgroup.{self.id}.members_cached"
|
|
329
329
|
|
|
330
330
|
@property
|
|
331
331
|
def members_cached(self):
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
from functools import lru_cache
|
|
2
1
|
import logging
|
|
3
2
|
|
|
4
3
|
from django import forms
|
|
5
4
|
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
|
|
6
5
|
from django.contrib.contenttypes.models import ContentType
|
|
6
|
+
from django.core.cache import cache
|
|
7
7
|
from django.core.exceptions import ValidationError
|
|
8
8
|
from django.core.serializers.json import DjangoJSONEncoder
|
|
9
9
|
from django.db import models
|
|
@@ -329,7 +329,6 @@ class RelationshipModel(models.Model):
|
|
|
329
329
|
class RelationshipManager(BaseManager.from_queryset(RestrictedQuerySet)):
|
|
330
330
|
use_in_migrations = True
|
|
331
331
|
|
|
332
|
-
@lru_cache(maxsize=128)
|
|
333
332
|
def get_for_model(self, model, hidden=None):
|
|
334
333
|
"""
|
|
335
334
|
Return all Relationships assigned to the given model.
|
|
@@ -345,7 +344,6 @@ class RelationshipManager(BaseManager.from_queryset(RestrictedQuerySet)):
|
|
|
345
344
|
self.get_for_model_destination(model, hidden=hidden),
|
|
346
345
|
)
|
|
347
346
|
|
|
348
|
-
@lru_cache(maxsize=128)
|
|
349
347
|
def get_for_model_source(self, model, hidden=None):
|
|
350
348
|
"""
|
|
351
349
|
Return all Relationships assigned to the given model for the source side only.
|
|
@@ -354,15 +352,21 @@ class RelationshipManager(BaseManager.from_queryset(RestrictedQuerySet)):
|
|
|
354
352
|
model (Model): The django model to which relationships are registered
|
|
355
353
|
hidden (bool): Filter based on the value of the hidden flag, or None to not apply this filter
|
|
356
354
|
"""
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
355
|
+
concrete_model = model._meta.concrete_model
|
|
356
|
+
cache_key = f"{self.get_for_model_source.cache_key_prefix}.{concrete_model._meta.label_lower}.{hidden}"
|
|
357
|
+
queryset = cache.get(cache_key)
|
|
358
|
+
if queryset is None:
|
|
359
|
+
content_type = ContentType.objects.get_for_model(concrete_model)
|
|
360
|
+
queryset = (
|
|
361
|
+
self.get_queryset().filter(source_type=content_type).select_related("source_type", "destination_type")
|
|
362
|
+
) # You almost always will want access to the source_type/destination_type
|
|
363
|
+
if hidden is not None:
|
|
364
|
+
queryset = queryset.filter(source_hidden=hidden)
|
|
365
|
+
cache.set(cache_key, queryset)
|
|
366
|
+
return queryset
|
|
367
|
+
|
|
368
|
+
get_for_model_source.cache_key_prefix = "nautobot.extras.relationship.get_for_model_source"
|
|
369
|
+
|
|
366
370
|
def get_for_model_destination(self, model, hidden=None):
|
|
367
371
|
"""
|
|
368
372
|
Return all Relationships assigned to the given model for the destination side only.
|
|
@@ -371,13 +375,22 @@ class RelationshipManager(BaseManager.from_queryset(RestrictedQuerySet)):
|
|
|
371
375
|
model (Model): The django model to which relationships are registered
|
|
372
376
|
hidden (bool): Filter based on the value of the hidden flag, or None to not apply this filter
|
|
373
377
|
"""
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
378
|
+
concrete_model = model._meta.concrete_model
|
|
379
|
+
cache_key = f"{self.get_for_model_destination.cache_key_prefix}.{concrete_model._meta.label_lower}.{hidden}"
|
|
380
|
+
queryset = cache.get(cache_key)
|
|
381
|
+
if queryset is None:
|
|
382
|
+
content_type = ContentType.objects.get_for_model(concrete_model)
|
|
383
|
+
queryset = (
|
|
384
|
+
self.get_queryset()
|
|
385
|
+
.filter(destination_type=content_type)
|
|
386
|
+
.select_related("source_type", "destination_type")
|
|
387
|
+
) # You almost always will want access to the source_type/destination_type
|
|
388
|
+
if hidden is not None:
|
|
389
|
+
queryset = queryset.filter(destination_hidden=hidden)
|
|
390
|
+
cache.set(cache_key, queryset)
|
|
391
|
+
return queryset
|
|
392
|
+
|
|
393
|
+
get_for_model_destination.cache_key_prefix = "nautobot.extras.relationship.get_for_model_destination"
|
|
381
394
|
|
|
382
395
|
def get_required_for_model(self, model):
|
|
383
396
|
"""
|
nautobot/extras/navigation.py
CHANGED
|
@@ -66,7 +66,7 @@ menu_items = (
|
|
|
66
66
|
name="Roles",
|
|
67
67
|
weight=100,
|
|
68
68
|
permissions=[
|
|
69
|
-
"extras.
|
|
69
|
+
"extras.view_role",
|
|
70
70
|
],
|
|
71
71
|
buttons=(
|
|
72
72
|
NavMenuAddButton(
|
|
@@ -120,7 +120,7 @@ menu_items = (
|
|
|
120
120
|
),
|
|
121
121
|
NavMenuItem(
|
|
122
122
|
link="extras:secretsgroup_list",
|
|
123
|
-
name="
|
|
123
|
+
name="Secrets Groups",
|
|
124
124
|
weight=200,
|
|
125
125
|
permissions=["extras.view_secretsgroup"],
|
|
126
126
|
buttons=(
|
|
@@ -154,6 +154,7 @@ menu_items = (
|
|
|
154
154
|
weight=200,
|
|
155
155
|
permissions=[
|
|
156
156
|
"extras.view_job",
|
|
157
|
+
"extras.view_scheduledjob",
|
|
157
158
|
],
|
|
158
159
|
buttons=(),
|
|
159
160
|
),
|
|
@@ -331,6 +331,14 @@ class TemplateExtension:
|
|
|
331
331
|
"""
|
|
332
332
|
raise NotImplementedError
|
|
333
333
|
|
|
334
|
+
def list_buttons(self):
|
|
335
|
+
"""
|
|
336
|
+
Buttons that will be rendered and added to the existing list of buttons on the list page view. Content
|
|
337
|
+
should be returned as an HTML string. Note that content does not need to be marked as safe because this is
|
|
338
|
+
automatically handled.
|
|
339
|
+
"""
|
|
340
|
+
raise NotImplementedError
|
|
341
|
+
|
|
334
342
|
def detail_tabs(self):
|
|
335
343
|
"""
|
|
336
344
|
Tabs that will be rendered and added to the existing list of tabs on the detail page view.
|
nautobot/extras/plugins/views.py
CHANGED
|
@@ -2,7 +2,6 @@ from collections import OrderedDict
|
|
|
2
2
|
|
|
3
3
|
from django.apps import apps
|
|
4
4
|
from django.conf import settings
|
|
5
|
-
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
6
5
|
from django.http import Http404
|
|
7
6
|
from django.shortcuts import render
|
|
8
7
|
from django.urls.exceptions import NoReverseMatch
|
|
@@ -14,8 +13,9 @@ from rest_framework.response import Response
|
|
|
14
13
|
from rest_framework.reverse import reverse
|
|
15
14
|
from rest_framework.views import APIView
|
|
16
15
|
|
|
17
|
-
from nautobot.core.api.views import NautobotAPIVersionMixin
|
|
16
|
+
from nautobot.core.api.views import AuthenticatedAPIRootView, NautobotAPIVersionMixin
|
|
18
17
|
from nautobot.core.forms import TableConfigForm
|
|
18
|
+
from nautobot.core.views.generic import GenericView
|
|
19
19
|
from nautobot.core.views.mixins import AdminRequiredMixin
|
|
20
20
|
from nautobot.core.views.paginator import EnhancedPaginator, get_paginate_count
|
|
21
21
|
from nautobot.extras.plugins.tables import InstalledPluginsTable
|
|
@@ -67,7 +67,7 @@ class InstalledPluginsView(AdminRequiredMixin, View):
|
|
|
67
67
|
)
|
|
68
68
|
|
|
69
69
|
|
|
70
|
-
class InstalledPluginDetailView(
|
|
70
|
+
class InstalledPluginDetailView(GenericView):
|
|
71
71
|
"""
|
|
72
72
|
View for showing details of an installed plugin.
|
|
73
73
|
"""
|
|
@@ -92,7 +92,6 @@ class InstalledPluginsAPIView(NautobotAPIVersionMixin, APIView):
|
|
|
92
92
|
"""
|
|
93
93
|
|
|
94
94
|
permission_classes = [permissions.IsAdminUser]
|
|
95
|
-
_ignore_model_permissions = True
|
|
96
95
|
|
|
97
96
|
def get_view_name(self):
|
|
98
97
|
return "Installed Plugins"
|
|
@@ -128,11 +127,9 @@ class InstalledPluginsAPIView(NautobotAPIVersionMixin, APIView):
|
|
|
128
127
|
return Response([self._get_plugin_data(apps.get_app_config(plugin)) for plugin in settings.PLUGINS])
|
|
129
128
|
|
|
130
129
|
|
|
131
|
-
class PluginsAPIRootView(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
def get_view_name(self):
|
|
135
|
-
return "Plugins"
|
|
130
|
+
class PluginsAPIRootView(AuthenticatedAPIRootView):
|
|
131
|
+
name = "Apps"
|
|
132
|
+
description = "API extension point for installed Nautobot Apps"
|
|
136
133
|
|
|
137
134
|
@staticmethod
|
|
138
135
|
def _get_plugin_entry(plugin, app_config, request, format_):
|
nautobot/extras/querysets.py
CHANGED
|
@@ -210,7 +210,7 @@ class DynamicGroupQuerySet(RestrictedQuerySet):
|
|
|
210
210
|
Return the cache key for the queryset of `DynamicGroup` objects that are eligible to potentially contain the
|
|
211
211
|
given object.
|
|
212
212
|
"""
|
|
213
|
-
return f"{obj._meta.label_lower}._get_eligible_dynamic_groups"
|
|
213
|
+
return f"nautobot.{obj._meta.label_lower}._get_eligible_dynamic_groups"
|
|
214
214
|
|
|
215
215
|
def _get_eligible_dynamic_groups(self, obj, use_cache=False):
|
|
216
216
|
"""
|
nautobot/extras/signals.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import contextlib
|
|
1
2
|
import contextvars
|
|
2
3
|
from datetime import timedelta
|
|
3
4
|
import logging
|
|
@@ -18,6 +19,7 @@ from django.db.models.signals import m2m_changed, post_delete, post_save, pre_de
|
|
|
18
19
|
from django.dispatch import receiver
|
|
19
20
|
from django.utils import timezone
|
|
20
21
|
from django_prometheus.models import model_deletes, model_inserts, model_updates
|
|
22
|
+
import redis.exceptions
|
|
21
23
|
|
|
22
24
|
from nautobot.core.celery import app, import_jobs_as_celery_tasks
|
|
23
25
|
from nautobot.core.utils.config import get_settings_or_config
|
|
@@ -65,8 +67,8 @@ def _get_user_if_authenticated(user, instance):
|
|
|
65
67
|
@receiver(post_save)
|
|
66
68
|
@receiver(m2m_changed)
|
|
67
69
|
@receiver(post_delete)
|
|
68
|
-
def
|
|
69
|
-
"""Invalidate the
|
|
70
|
+
def invalidate_models_cache(sender, **kwargs):
|
|
71
|
+
"""Invalidate the related-models cache for ComputedFields, CustomFields and Relationships."""
|
|
70
72
|
if sender is CustomField.content_types.through:
|
|
71
73
|
manager = CustomField.objects
|
|
72
74
|
elif sender in (ComputedField, CustomField, Relationship):
|
|
@@ -80,9 +82,13 @@ def invalidate_lru_cache(sender, **kwargs):
|
|
|
80
82
|
"get_for_model_destination",
|
|
81
83
|
)
|
|
82
84
|
|
|
83
|
-
for
|
|
84
|
-
if hasattr(manager,
|
|
85
|
-
getattr(manager,
|
|
85
|
+
for method_name in cached_methods:
|
|
86
|
+
if hasattr(manager, method_name):
|
|
87
|
+
method = getattr(manager, method_name)
|
|
88
|
+
if hasattr(method, "cache_key_prefix"):
|
|
89
|
+
with contextlib.suppress(redis.exceptions.ConnectionError):
|
|
90
|
+
# TODO: *maybe* target more narrowly, e.g. only clear the cache for specific related content-types?
|
|
91
|
+
cache.delete_pattern(f"{method.cache_key_prefix}.*")
|
|
86
92
|
|
|
87
93
|
|
|
88
94
|
@receiver(post_save)
|
|
@@ -343,7 +349,7 @@ def dynamic_group_eligible_groups_changed(sender, instance, **kwargs):
|
|
|
343
349
|
return
|
|
344
350
|
|
|
345
351
|
content_type = instance.content_type
|
|
346
|
-
cache_key = f"{content_type.app_label}.{content_type.model}._get_eligible_dynamic_groups"
|
|
352
|
+
cache_key = f"nautobot.{content_type.app_label}.{content_type.model}._get_eligible_dynamic_groups"
|
|
347
353
|
cache.set(
|
|
348
354
|
cache_key,
|
|
349
355
|
DynamicGroup.objects.filter(content_type_id=instance.content_type_id),
|