nautobot 2.4.8__py3-none-any.whl → 2.4.10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of nautobot might be problematic. Click here for more details.
- nautobot/circuits/templates/circuits/circuittype.html +1 -1
- nautobot/circuits/templates/circuits/circuittype_retrieve.html +1 -39
- nautobot/circuits/views.py +18 -23
- nautobot/cloud/templates/cloud/cloudresourcetype_retrieve.html +4 -111
- nautobot/cloud/views.py +56 -25
- nautobot/core/api/parsers.py +56 -2
- nautobot/core/celery/schedulers.py +1 -0
- nautobot/core/filters.py +1 -1
- nautobot/core/graphql/schema.py +1 -0
- nautobot/core/jobs/__init__.py +14 -3
- nautobot/core/models/__init__.py +2 -0
- nautobot/core/tables.py +13 -6
- nautobot/core/testing/views.py +27 -2
- nautobot/core/tests/test_csv.py +92 -1
- nautobot/core/tests/test_jinja_filters.py +59 -0
- nautobot/core/tests/test_jobs.py +113 -0
- nautobot/core/tests/test_ui.py +53 -1
- nautobot/core/tests/test_utils.py +11 -0
- nautobot/core/tests/test_views.py +73 -0
- nautobot/core/ui/object_detail.py +19 -12
- nautobot/core/urls.py +2 -2
- nautobot/core/utils/filtering.py +3 -0
- nautobot/core/views/__init__.py +21 -0
- nautobot/core/views/renderers.py +1 -1
- nautobot/dcim/forms.py +10 -0
- nautobot/dcim/models/device_component_templates.py +4 -0
- nautobot/dcim/models/device_components.py +12 -0
- nautobot/dcim/models/devices.py +6 -0
- nautobot/dcim/templates/dcim/devicefamily_retrieve.html +1 -43
- nautobot/dcim/templates/dcim/deviceredundancygroup_retrieve.html +1 -59
- nautobot/dcim/templates/dcim/devicetype.html +2 -217
- nautobot/dcim/templates/dcim/devicetype_edit.html +2 -32
- nautobot/dcim/templates/dcim/devicetype_retrieve.html +217 -0
- nautobot/dcim/templates/dcim/devicetype_update.html +32 -0
- nautobot/dcim/templates/dcim/inc/rack_elevation.html +1 -1
- nautobot/dcim/templates/dcim/modulebay_retrieve.html +1 -84
- nautobot/dcim/templates/dcim/rack_elevation.html +14 -0
- nautobot/dcim/templates/dcim/rackreservation_retrieve.html +0 -68
- nautobot/dcim/tests/integration/test_fileinputpicker.py +1 -1
- nautobot/dcim/urls.py +1 -36
- nautobot/dcim/views.py +133 -81
- nautobot/extras/api/views.py +4 -6
- nautobot/extras/context_managers.py +2 -2
- nautobot/extras/migrations/0024_job_data_migration.py +1 -1
- nautobot/extras/models/customfields.py +2 -0
- nautobot/extras/models/datasources.py +8 -0
- nautobot/extras/models/groups.py +18 -0
- nautobot/extras/models/jobs.py +92 -62
- nautobot/extras/models/metadata.py +2 -0
- nautobot/extras/models/models.py +4 -0
- nautobot/extras/models/secrets.py +7 -0
- nautobot/extras/secrets/__init__.py +14 -0
- nautobot/extras/tables.py +11 -1
- nautobot/extras/templates/extras/computedfield_retrieve.html +1 -55
- nautobot/extras/templates/extras/inc/job_tiles.html +1 -1
- nautobot/extras/templates/extras/jobresult.html +1 -1
- nautobot/extras/templates/extras/metadatatype_retrieve.html +1 -66
- nautobot/extras/templates/extras/scheduledjob.html +25 -9
- nautobot/extras/tests/test_api.py +1 -1
- nautobot/extras/tests/test_context_managers.py +20 -0
- nautobot/extras/tests/test_models.py +26 -0
- nautobot/extras/tests/test_views.py +15 -2
- nautobot/extras/utils.py +18 -16
- nautobot/extras/views.py +65 -26
- nautobot/ipam/models.py +32 -0
- nautobot/ipam/tables.py +3 -4
- nautobot/project-static/docs/404.html +4 -4
- nautobot/project-static/docs/apps/index.html +4 -4
- nautobot/project-static/docs/apps/nautobot-apps.html +4 -4
- nautobot/project-static/docs/assets/javascripts/{bundle.c8b220af.min.js → bundle.13a4f30d.min.js} +4 -4
- nautobot/project-static/docs/assets/javascripts/{bundle.c8b220af.min.js.map → bundle.13a4f30d.min.js.map} +2 -2
- nautobot/project-static/docs/assets/javascripts/workers/{search.f8cc74c7.min.js → search.d50fe291.min.js} +2 -2
- nautobot/project-static/docs/assets/javascripts/workers/{search.f8cc74c7.min.js.map → search.d50fe291.min.js.map} +1 -1
- nautobot/project-static/docs/assets/stylesheets/{main.2afb09e1.min.css → main.342714a4.min.css} +1 -1
- nautobot/project-static/docs/assets/stylesheets/{main.2afb09e1.min.css.map → main.342714a4.min.css.map} +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +4 -4
- nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +4 -4
- nautobot/project-static/docs/code-reference/nautobot/apps/api.html +4 -4
- nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +4 -4
- nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +4 -4
- nautobot/project-static/docs/code-reference/nautobot/apps/config.html +4 -4
- nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +4 -4
- nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +4 -4
- nautobot/project-static/docs/code-reference/nautobot/apps/events.html +4 -4
- nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +4 -4
- nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +4 -4
- nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +4 -4
- nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +4 -4
- nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +4 -4
- nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +4 -4
- nautobot/project-static/docs/code-reference/nautobot/apps/models.html +4 -4
- nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +4 -4
- nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +4 -4
- nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +4 -4
- nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +4 -4
- nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +6 -8
- nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +4 -4
- nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +36 -6
- nautobot/project-static/docs/code-reference/nautobot/apps/views.html +4 -4
- nautobot/project-static/docs/development/apps/api/configuration-view.html +4 -4
- nautobot/project-static/docs/development/apps/api/database-backend-config.html +4 -4
- nautobot/project-static/docs/development/apps/api/models/django-admin.html +4 -4
- nautobot/project-static/docs/development/apps/api/models/global-search.html +4 -4
- nautobot/project-static/docs/development/apps/api/models/graphql.html +4 -4
- nautobot/project-static/docs/development/apps/api/models/index.html +4 -4
- nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +4 -4
- nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +4 -4
- nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +4 -4
- nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +4 -4
- nautobot/project-static/docs/development/apps/api/platform-features/index.html +4 -4
- nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +4 -4
- nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +4 -4
- nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +4 -4
- nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +43 -42
- nautobot/project-static/docs/development/apps/api/platform-features/table-extensions.html +4 -4
- nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +4 -4
- nautobot/project-static/docs/development/apps/api/prometheus.html +4 -4
- nautobot/project-static/docs/development/apps/api/setup.html +4 -4
- nautobot/project-static/docs/development/apps/api/testing.html +4 -4
- nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +4 -4
- nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +4 -4
- nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +4 -4
- nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +4 -4
- nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +4 -4
- nautobot/project-static/docs/development/apps/api/views/base-template.html +4 -4
- nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +4 -4
- nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +4 -4
- nautobot/project-static/docs/development/apps/api/views/help-documentation.html +4 -4
- nautobot/project-static/docs/development/apps/api/views/index.html +4 -4
- nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +4 -4
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +4 -4
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +4 -4
- nautobot/project-static/docs/development/apps/api/views/notes.html +4 -4
- nautobot/project-static/docs/development/apps/api/views/rest-api.html +4 -4
- nautobot/project-static/docs/development/apps/api/views/urls.html +4 -4
- nautobot/project-static/docs/development/apps/index.html +4 -4
- nautobot/project-static/docs/development/apps/migration/code-updates.html +4 -4
- nautobot/project-static/docs/development/apps/migration/dependency-updates.html +4 -4
- nautobot/project-static/docs/development/apps/migration/from-v1.html +4 -4
- nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +4 -4
- nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +4 -4
- nautobot/project-static/docs/development/apps/migration/model-updates/global.html +4 -4
- nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +4 -4
- nautobot/project-static/docs/development/apps/migration/ui-component-framework/best-practices.html +4 -4
- nautobot/project-static/docs/development/apps/migration/ui-component-framework/custom-content.html +4 -4
- nautobot/project-static/docs/development/apps/migration/ui-component-framework/index.html +4 -4
- nautobot/project-static/docs/development/apps/migration/ui-component-framework/migration-steps.html +4 -4
- nautobot/project-static/docs/development/apps/porting-from-netbox.html +4 -4
- nautobot/project-static/docs/development/core/application-registry.html +4 -4
- nautobot/project-static/docs/development/core/best-practices.html +4 -4
- nautobot/project-static/docs/development/core/bootstrap-ui.html +4 -4
- nautobot/project-static/docs/development/core/caching.html +4 -4
- nautobot/project-static/docs/development/core/controllers.html +4 -4
- nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +41 -55
- nautobot/project-static/docs/development/core/generic-views.html +4 -4
- nautobot/project-static/docs/development/core/getting-started.html +4 -4
- nautobot/project-static/docs/development/core/homepage.html +4 -4
- nautobot/project-static/docs/development/core/index.html +4 -4
- nautobot/project-static/docs/development/core/minikube-dev-environment-for-k8s-jobs.html +4 -4
- nautobot/project-static/docs/development/core/model-checklist.html +4 -4
- nautobot/project-static/docs/development/core/model-features.html +4 -4
- nautobot/project-static/docs/development/core/natural-keys.html +4 -4
- nautobot/project-static/docs/development/core/navigation-menu.html +4 -4
- nautobot/project-static/docs/development/core/release-checklist.html +4 -4
- nautobot/project-static/docs/development/core/role-internals.html +4 -4
- nautobot/project-static/docs/development/core/settings.html +4 -4
- nautobot/project-static/docs/development/core/style-guide.html +4 -4
- nautobot/project-static/docs/development/core/templates.html +4 -4
- nautobot/project-static/docs/development/core/testing.html +4 -4
- nautobot/project-static/docs/development/core/ui-component-framework.html +4 -4
- nautobot/project-static/docs/development/core/user-preferences.html +4 -4
- nautobot/project-static/docs/development/index.html +4 -4
- nautobot/project-static/docs/development/jobs/getting-started.html +4 -4
- nautobot/project-static/docs/development/jobs/index.html +4 -4
- nautobot/project-static/docs/development/jobs/installation.html +4 -4
- nautobot/project-static/docs/development/jobs/job-extensions.html +4 -4
- nautobot/project-static/docs/development/jobs/job-logging.html +4 -4
- nautobot/project-static/docs/development/jobs/job-patterns.html +4 -4
- nautobot/project-static/docs/development/jobs/job-structure.html +4 -4
- nautobot/project-static/docs/development/jobs/migration/from-v1.html +4 -4
- nautobot/project-static/docs/development/jobs/testing.html +4 -4
- nautobot/project-static/docs/index.html +4 -4
- nautobot/project-static/docs/overview/application_stack.html +4 -4
- nautobot/project-static/docs/overview/design_philosophy.html +4 -4
- nautobot/project-static/docs/release-notes/index.html +4 -4
- nautobot/project-static/docs/release-notes/version-1.0.html +4 -4
- nautobot/project-static/docs/release-notes/version-1.1.html +4 -4
- nautobot/project-static/docs/release-notes/version-1.2.html +4 -4
- nautobot/project-static/docs/release-notes/version-1.3.html +4 -4
- nautobot/project-static/docs/release-notes/version-1.4.html +4 -4
- nautobot/project-static/docs/release-notes/version-1.5.html +4 -4
- nautobot/project-static/docs/release-notes/version-1.6.html +301 -4
- nautobot/project-static/docs/release-notes/version-2.0.html +4 -4
- nautobot/project-static/docs/release-notes/version-2.1.html +4 -4
- nautobot/project-static/docs/release-notes/version-2.2.html +4 -4
- nautobot/project-static/docs/release-notes/version-2.3.html +4 -4
- nautobot/project-static/docs/release-notes/version-2.4.html +291 -4
- nautobot/project-static/docs/requirements.txt +1 -1
- nautobot/project-static/docs/search/search_index.json +1 -1
- nautobot/project-static/docs/sitemap.xml +298 -298
- nautobot/project-static/docs/sitemap.xml.gz +0 -0
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +4 -4
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +4 -4
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +4 -4
- nautobot/project-static/docs/user-guide/administration/configuration/index.html +4 -4
- nautobot/project-static/docs/user-guide/administration/configuration/redis.html +4 -4
- nautobot/project-static/docs/user-guide/administration/configuration/settings.html +4 -4
- nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +4 -4
- nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +4 -4
- nautobot/project-static/docs/user-guide/administration/guides/docker.html +4 -4
- nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +4 -4
- nautobot/project-static/docs/user-guide/administration/guides/permissions.html +4 -4
- nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +4 -4
- nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +4 -4
- nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +4 -4
- nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +4 -4
- nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +4 -4
- nautobot/project-static/docs/user-guide/administration/installation/app-install.html +4 -4
- nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +4 -4
- nautobot/project-static/docs/user-guide/administration/installation/http-server.html +4 -4
- nautobot/project-static/docs/user-guide/administration/installation/index.html +4 -4
- nautobot/project-static/docs/user-guide/administration/installation/install_system.html +4 -4
- nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +4 -4
- nautobot/project-static/docs/user-guide/administration/installation/services.html +4 -4
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +4 -4
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +4 -4
- nautobot/project-static/docs/user-guide/administration/security/index.html +4 -5
- nautobot/project-static/docs/user-guide/administration/security/notices.html +117 -5
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +4 -4
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +4 -4
- nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +4 -4
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +4 -4
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +4 -4
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +4 -4
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +4 -4
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +4 -4
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +4 -4
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +4 -4
- nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualdevicecontext.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/wireless/index.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/wireless/radioprofile.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/wireless/supporteddatarate.html +4 -4
- nautobot/project-static/docs/user-guide/core-data-model/wireless/wirelessnetwork.html +4 -4
- nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +4 -4
- nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +4 -4
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +4 -4
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +4 -4
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +4 -4
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +4 -4
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +4 -4
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +4 -4
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +4 -4
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +4 -4
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +4 -4
- nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +4 -4
- nautobot/project-static/docs/user-guide/feature-guides/graphql.html +4 -4
- nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +4 -4
- nautobot/project-static/docs/user-guide/feature-guides/relationships.html +4 -4
- nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +4 -4
- nautobot/project-static/docs/user-guide/feature-guides/wireless-networks-and-controllers.html +4 -4
- nautobot/project-static/docs/user-guide/index.html +4 -4
- nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +4 -4
- nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +4 -4
- nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +4 -4
- nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +4 -4
- nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +4 -4
- nautobot/project-static/docs/user-guide/platform-functionality/events.html +4 -4
- nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +4 -4
- nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +4 -4
- nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +4 -4
- nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +4 -4
- nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +4 -4
- nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +4 -4
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +4 -4
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +4 -4
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +4 -4
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +4 -4
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobqueue.html +4 -4
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/kubernetes-job-support.html +4 -4
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/managing-jobs.html +4 -4
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +4 -4
- nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +4 -4
- nautobot/project-static/docs/user-guide/platform-functionality/note.html +4 -4
- nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +4 -4
- nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +4 -4
- nautobot/project-static/docs/user-guide/platform-functionality/rendering-jinja-templates.html +4 -4
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +4 -4
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +4 -4
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +4 -4
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +4 -4
- nautobot/project-static/docs/user-guide/platform-functionality/role.html +4 -4
- nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +4 -4
- nautobot/project-static/docs/user-guide/platform-functionality/secret.html +4 -4
- nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +4 -4
- nautobot/project-static/docs/user-guide/platform-functionality/status.html +4 -4
- nautobot/project-static/docs/user-guide/platform-functionality/tag.html +4 -4
- nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +4 -4
- nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +4 -4
- nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +4 -4
- nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +4 -4
- nautobot/project-static/js/forms.js +0 -14
- nautobot/users/models.py +4 -0
- nautobot/virtualization/models.py +4 -0
- nautobot/wireless/tables.py +1 -0
- nautobot/wireless/templates/wireless/wirelessnetwork_retrieve.html +1 -55
- nautobot/wireless/views.py +33 -28
- {nautobot-2.4.8.dist-info → nautobot-2.4.10.dist-info}/METADATA +4 -4
- {nautobot-2.4.8.dist-info → nautobot-2.4.10.dist-info}/RECORD +387 -384
- {nautobot-2.4.8.dist-info → nautobot-2.4.10.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.4.8.dist-info → nautobot-2.4.10.dist-info}/NOTICE +0 -0
- {nautobot-2.4.8.dist-info → nautobot-2.4.10.dist-info}/WHEEL +0 -0
- {nautobot-2.4.8.dist-info → nautobot-2.4.10.dist-info}/entry_points.txt +0 -0
nautobot/core/tests/test_csv.py
CHANGED
|
@@ -7,7 +7,7 @@ from django.urls import reverse
|
|
|
7
7
|
|
|
8
8
|
from nautobot.core.constants import CSV_NO_OBJECT, CSV_NULL_TYPE, VARBINARY_IP_FIELD_REPR_OF_CSV_NO_OBJECT
|
|
9
9
|
from nautobot.dcim.api.serializers import DeviceSerializer
|
|
10
|
-
from nautobot.dcim.models.devices import Controller, Device, DeviceType
|
|
10
|
+
from nautobot.dcim.models.devices import Controller, Device, DeviceType, Platform, SoftwareImageFile, SoftwareVersion
|
|
11
11
|
from nautobot.dcim.models.locations import Location
|
|
12
12
|
from nautobot.extras.models.roles import Role
|
|
13
13
|
from nautobot.extras.models.statuses import Status
|
|
@@ -317,3 +317,94 @@ class CSVParsingRelatedTestCase(TestCase):
|
|
|
317
317
|
tenant=self.device2.tenant,
|
|
318
318
|
)
|
|
319
319
|
self.assertEqual(device4.tags.count(), 0)
|
|
320
|
+
|
|
321
|
+
@override_settings(ALLOWED_HOSTS=["*"])
|
|
322
|
+
def test_m2m_field_import(self):
|
|
323
|
+
"""Test CSV import of M2M field."""
|
|
324
|
+
|
|
325
|
+
platform = Platform.objects.first()
|
|
326
|
+
software_version_status = Status.objects.get_for_model(SoftwareVersion).first()
|
|
327
|
+
software_image_file_status = Status.objects.get_for_model(SoftwareImageFile).first()
|
|
328
|
+
|
|
329
|
+
software_version = SoftwareVersion.objects.create(
|
|
330
|
+
platform=platform, version="Test version 1.0.0", status=software_version_status
|
|
331
|
+
)
|
|
332
|
+
software_image_files = (
|
|
333
|
+
SoftwareImageFile.objects.create(
|
|
334
|
+
software_version=software_version,
|
|
335
|
+
image_file_name="software_image_file_qs_test_1.bin",
|
|
336
|
+
status=software_image_file_status,
|
|
337
|
+
),
|
|
338
|
+
SoftwareImageFile.objects.create(
|
|
339
|
+
software_version=software_version,
|
|
340
|
+
image_file_name="software_image_file_qs_test_2.bin",
|
|
341
|
+
status=software_image_file_status,
|
|
342
|
+
default_image=True,
|
|
343
|
+
),
|
|
344
|
+
SoftwareImageFile.objects.create(
|
|
345
|
+
software_version=software_version,
|
|
346
|
+
image_file_name="software_image_file_qs_test_3.bin",
|
|
347
|
+
status=software_image_file_status,
|
|
348
|
+
),
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
user = UserFactory.create()
|
|
352
|
+
user.is_superuser = True
|
|
353
|
+
user.is_active = True
|
|
354
|
+
user.save()
|
|
355
|
+
self.client.force_login(user)
|
|
356
|
+
|
|
357
|
+
with self.subTest("Import M2M field using list of UUIDs"):
|
|
358
|
+
import_data = f"""name,device_type,location,role,status,software_image_files
|
|
359
|
+
TestDevice5,{self.device.device_type.pk},{self.device.location.pk},{self.device.role.pk},{self.device.status.pk},"{software_image_files[0].pk},{software_image_files[1].pk}"
|
|
360
|
+
"""
|
|
361
|
+
data = {"csv_data": import_data}
|
|
362
|
+
url = reverse("dcim:device_import")
|
|
363
|
+
response = self.client.post(url, data)
|
|
364
|
+
|
|
365
|
+
self.assertEqual(response.status_code, 200)
|
|
366
|
+
self.assertEqual(Device.objects.count(), 3)
|
|
367
|
+
|
|
368
|
+
# Assert TestDevice5 got created with the right fields
|
|
369
|
+
device5 = Device.objects.get(
|
|
370
|
+
name="TestDevice5",
|
|
371
|
+
location=self.device.location,
|
|
372
|
+
device_type=self.device.device_type,
|
|
373
|
+
role=self.device.role,
|
|
374
|
+
status=self.device.status,
|
|
375
|
+
tenant=None,
|
|
376
|
+
)
|
|
377
|
+
self.assertEqual(device5.software_image_files.count(), 2)
|
|
378
|
+
|
|
379
|
+
with self.subTest("Import M2M field using multiple identifying fields"):
|
|
380
|
+
import_data = f"""name,device_type,location,role,status,software_image_files__software_version,software_image_files__image_file_name
|
|
381
|
+
TestDevice6,{self.device.device_type.pk},{self.device.location.pk},{self.device.role.pk},{self.device.status.pk},"{software_version.pk},{software_version.pk}","{software_image_files[0].image_file_name},{software_image_files[1].image_file_name}"
|
|
382
|
+
"""
|
|
383
|
+
data = {"csv_data": import_data}
|
|
384
|
+
url = reverse("dcim:device_import")
|
|
385
|
+
response = self.client.post(url, data)
|
|
386
|
+
|
|
387
|
+
self.assertEqual(response.status_code, 200)
|
|
388
|
+
self.assertEqual(Device.objects.count(), 4)
|
|
389
|
+
|
|
390
|
+
# Assert TestDevice5 got created with the right fields
|
|
391
|
+
device6 = Device.objects.get(
|
|
392
|
+
name="TestDevice6",
|
|
393
|
+
location=self.device.location,
|
|
394
|
+
device_type=self.device.device_type,
|
|
395
|
+
role=self.device.role,
|
|
396
|
+
status=self.device.status,
|
|
397
|
+
tenant=None,
|
|
398
|
+
)
|
|
399
|
+
self.assertEqual(device6.software_image_files.count(), 2)
|
|
400
|
+
|
|
401
|
+
with self.subTest("Import M2M field using incorrect number of values"):
|
|
402
|
+
import_data = f"""name,device_type,location,role,status,software_image_files__software_version,software_image_files__image_file_name
|
|
403
|
+
TestDevice7,{self.device.device_type.pk},{self.device.location.pk},{self.device.role.pk},{self.device.status.pk},"{software_version.pk},{software_version.pk}","{software_image_files[0].image_file_name},{software_image_files[1].image_file_name},{software_image_files[2].image_file_name}"
|
|
404
|
+
"""
|
|
405
|
+
data = {"csv_data": import_data}
|
|
406
|
+
url = reverse("dcim:device_import")
|
|
407
|
+
response = self.client.post(url, data)
|
|
408
|
+
self.assertEqual(response.status_code, 200)
|
|
409
|
+
self.assertContains(response, "Incorrect number of values provided for the software_image_files field")
|
|
410
|
+
self.assertEqual(Device.objects.count(), 4)
|
|
@@ -4,6 +4,8 @@ from netutils.utils import jinja2_convenience_function
|
|
|
4
4
|
|
|
5
5
|
from nautobot.core.utils import data
|
|
6
6
|
from nautobot.dcim import models as dcim_models
|
|
7
|
+
from nautobot.extras import models as extras_models
|
|
8
|
+
from nautobot.ipam import models as ipam_models
|
|
7
9
|
|
|
8
10
|
|
|
9
11
|
class NautobotJinjaFilterTest(TestCase):
|
|
@@ -85,3 +87,60 @@ class NautobotJinjaFilterTest(TestCase):
|
|
|
85
87
|
self.fail("SecurityError raised on safe Jinja template render")
|
|
86
88
|
else:
|
|
87
89
|
self.assertEqual(value, location.parent.name)
|
|
90
|
+
|
|
91
|
+
def test_render_blocks_various_unsafe_methods(self):
|
|
92
|
+
"""Assert that Jinja template rendering correctly blocks various unsafe Nautobot APIs."""
|
|
93
|
+
device = dcim_models.Device.objects.first()
|
|
94
|
+
dynamic_group = extras_models.DynamicGroup.objects.first()
|
|
95
|
+
git_repository = extras_models.GitRepository.objects.create(
|
|
96
|
+
name="repo", slug="repo", remote_url="file:///", branch="main"
|
|
97
|
+
)
|
|
98
|
+
interface = dcim_models.Interface.objects.first()
|
|
99
|
+
interface_template = dcim_models.InterfaceTemplate.objects.first()
|
|
100
|
+
location = dcim_models.Location.objects.first()
|
|
101
|
+
module = dcim_models.Module.objects.first()
|
|
102
|
+
prefix = ipam_models.Prefix.objects.first()
|
|
103
|
+
secret = extras_models.Secret.objects.create(name="secret", provider="environment-variable")
|
|
104
|
+
vrf = ipam_models.VRF.objects.first()
|
|
105
|
+
|
|
106
|
+
context = {
|
|
107
|
+
"device": device,
|
|
108
|
+
"dynamic_group": dynamic_group,
|
|
109
|
+
"git_repository": git_repository,
|
|
110
|
+
"interface": interface,
|
|
111
|
+
"interface_template": interface_template,
|
|
112
|
+
"location": location,
|
|
113
|
+
"module": module,
|
|
114
|
+
"prefix": prefix,
|
|
115
|
+
"secret": secret,
|
|
116
|
+
"vrf": vrf,
|
|
117
|
+
"JobResult": extras_models.JobResult,
|
|
118
|
+
"ScheduledJob": extras_models.ScheduledJob,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for call in [
|
|
122
|
+
"device.create_components()",
|
|
123
|
+
"dynamic_group.add_members([])",
|
|
124
|
+
"dynamic_group.remove_members([])",
|
|
125
|
+
"git_repository.sync(None)",
|
|
126
|
+
"git_repository.clone_to_directory()",
|
|
127
|
+
"git_repository.cleanup_cloned_directory('/tmp/')",
|
|
128
|
+
"interface.render_name_template()",
|
|
129
|
+
"interface.add_ip_addresses([])",
|
|
130
|
+
"interface_template.instantiate(device)",
|
|
131
|
+
"interface_template.instantiate_model(interface_template, device)",
|
|
132
|
+
"location.validated_save()",
|
|
133
|
+
"module.create_components()",
|
|
134
|
+
"module.render_component_names()",
|
|
135
|
+
"prefix.reparent_ips()",
|
|
136
|
+
"prefix.reparent_subnets()",
|
|
137
|
+
"secret.get_value()",
|
|
138
|
+
"vrf.add_device(device)",
|
|
139
|
+
"vrf.add_prefix(prefix)",
|
|
140
|
+
"JobResult.enqueue_job(None, None)",
|
|
141
|
+
"JobResult.log('hello world')",
|
|
142
|
+
"ScheduledJob.create_schedule(None, None)",
|
|
143
|
+
]:
|
|
144
|
+
with self.subTest(call=call):
|
|
145
|
+
with self.assertRaises(SecurityError):
|
|
146
|
+
data.render_jinja2(template_code="{{ " + call + " }}", context=context)
|
nautobot/core/tests/test_jobs.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import codecs
|
|
2
|
+
import csv
|
|
2
3
|
from datetime import timedelta
|
|
4
|
+
from io import StringIO
|
|
3
5
|
import json
|
|
4
6
|
from pathlib import Path
|
|
5
7
|
|
|
@@ -10,6 +12,7 @@ from django.utils import timezone
|
|
|
10
12
|
import yaml
|
|
11
13
|
|
|
12
14
|
from nautobot.circuits.models import Circuit, CircuitType, Provider
|
|
15
|
+
from nautobot.core.jobs import ExportObjectList
|
|
13
16
|
from nautobot.core.jobs.cleanup import CleanupTypes
|
|
14
17
|
from nautobot.core.testing import create_job_result_and_run_job, TransactionTestCase
|
|
15
18
|
from nautobot.core.testing.context import load_event_broker_override_settings
|
|
@@ -25,6 +28,7 @@ from nautobot.extras.models import (
|
|
|
25
28
|
JobResult,
|
|
26
29
|
ObjectChange,
|
|
27
30
|
Role,
|
|
31
|
+
SavedView,
|
|
28
32
|
Status,
|
|
29
33
|
Tag,
|
|
30
34
|
)
|
|
@@ -40,6 +44,32 @@ class ExportObjectListTest(TransactionTestCase):
|
|
|
40
44
|
|
|
41
45
|
databases = ("default", "job_logs")
|
|
42
46
|
|
|
47
|
+
def _create_saved_view(self, model_class=Status, config=None):
|
|
48
|
+
"""Helper to create a SavedView with optional filter config."""
|
|
49
|
+
return SavedView.objects.create(
|
|
50
|
+
name="Global default View",
|
|
51
|
+
owner=self.user,
|
|
52
|
+
view=f"{model_class._meta.app_label}:{model_class._meta.model_name}_list",
|
|
53
|
+
is_global_default=True,
|
|
54
|
+
config=config or {},
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def _run_export_job(self, query_string, model_class=Status):
|
|
58
|
+
"""Helper to run export job and return parsed CSV rows."""
|
|
59
|
+
job_result = create_job_result_and_run_job(
|
|
60
|
+
"nautobot.core.jobs",
|
|
61
|
+
"ExportObjectList",
|
|
62
|
+
content_type=ContentType.objects.get_for_model(model_class).pk,
|
|
63
|
+
query_string=query_string,
|
|
64
|
+
)
|
|
65
|
+
self.assertJobResultStatus(job_result)
|
|
66
|
+
self.assertTrue(job_result.files.exists())
|
|
67
|
+
self.assertEqual(
|
|
68
|
+
Path(job_result.files.first().file.name).name, f"nautobot_{model_class._meta.verbose_name_plural}.csv"
|
|
69
|
+
)
|
|
70
|
+
csv_data = job_result.files.first().file.read().decode("utf-8").lstrip("\ufeff")
|
|
71
|
+
return list(csv.DictReader(StringIO(csv_data)))
|
|
72
|
+
|
|
43
73
|
def test_export_without_permission(self):
|
|
44
74
|
"""Job should enforce user permissions on the content-type being asked for export."""
|
|
45
75
|
job_result = create_job_result_and_run_job(
|
|
@@ -136,6 +166,89 @@ class ExportObjectListTest(TransactionTestCase):
|
|
|
136
166
|
data = yaml.safe_load(yaml_data)
|
|
137
167
|
self.assertEqual(data["manufacturer"], "Cisco")
|
|
138
168
|
|
|
169
|
+
def test_get_saved_view_filter_params(self):
|
|
170
|
+
"""Test various cases for the saved view filter parameters."""
|
|
171
|
+
saved_view = self._create_saved_view(config={"filter_params": {"name": ["Active"]}})
|
|
172
|
+
test_cases = [
|
|
173
|
+
# (query_params, expected_output)
|
|
174
|
+
({"saved_view": saved_view.pk}, {"name": ["Active"]}),
|
|
175
|
+
(
|
|
176
|
+
{
|
|
177
|
+
"saved_view": saved_view.pk,
|
|
178
|
+
"name": ["Active"],
|
|
179
|
+
"content_types": ["dcim.devices"],
|
|
180
|
+
}, # new filter content_types
|
|
181
|
+
{"name": ["Active"]},
|
|
182
|
+
),
|
|
183
|
+
(
|
|
184
|
+
{"saved_view": saved_view.pk, "content_types": ["dcim.devices"]}, # name filter was deleted
|
|
185
|
+
{},
|
|
186
|
+
),
|
|
187
|
+
({"saved_view": saved_view.pk, "all_filters_removed": "true"}, {}),
|
|
188
|
+
(
|
|
189
|
+
{"name": ["Active"]}, # No saved view provided
|
|
190
|
+
{},
|
|
191
|
+
),
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
for query_params, expected_output in test_cases:
|
|
195
|
+
with self.subTest(query_params=query_params, expected_output=expected_output):
|
|
196
|
+
job = ExportObjectList()
|
|
197
|
+
filter_params = job._get_saved_view_filter_params(query_params)
|
|
198
|
+
self.assertEqual(filter_params, expected_output)
|
|
199
|
+
|
|
200
|
+
def test_export_saved_view_to_csv_without_filters(self):
|
|
201
|
+
"""Export a SavedView to CSV without any filters applied."""
|
|
202
|
+
# URL: /?saved_view=<id>
|
|
203
|
+
sv = self._create_saved_view()
|
|
204
|
+
rows = self._run_export_job(query_string=f"saved_view={sv.pk}")
|
|
205
|
+
self.assertEqual(len(rows), Status.objects.count())
|
|
206
|
+
|
|
207
|
+
def test_export_saved_view_to_csv_with_filters_from_saved_view(self):
|
|
208
|
+
"""Export a SavedView to CSV using filters defined in the SavedView config."""
|
|
209
|
+
# URL: /?saved_view=<id>
|
|
210
|
+
filter_name = Status.objects.first().name
|
|
211
|
+
sv = self._create_saved_view(config={"filter_params": {"name": [filter_name]}})
|
|
212
|
+
rows = self._run_export_job(query_string=f"saved_view={sv.pk}")
|
|
213
|
+
self.assertGreaterEqual(Status.objects.count(), 1) # Ensure multiple Statuses exist and filter works
|
|
214
|
+
self.assertEqual(len(rows), 1)
|
|
215
|
+
self.assertEqual(rows[0]["name"], filter_name)
|
|
216
|
+
|
|
217
|
+
def test_export_saved_view_to_csv_with_combined_filters(self):
|
|
218
|
+
"""Export a SavedView to CSV using combined filters from SavedView config and query params."""
|
|
219
|
+
# URL: /?saved_view=<id>&name=<filter_name>&name=<filter_name2>
|
|
220
|
+
filter_name = Status.objects.first().name
|
|
221
|
+
filter_name2 = Status.objects.last().name
|
|
222
|
+
sv = self._create_saved_view(config={"filter_params": {"name": [filter_name]}})
|
|
223
|
+
rows = self._run_export_job(query_string=f"saved_view={sv.pk}&name={filter_name}&name={filter_name2}")
|
|
224
|
+
self.assertEqual(len(rows), 2)
|
|
225
|
+
self.assertEqual(rows[0]["name"], filter_name)
|
|
226
|
+
self.assertEqual(rows[1]["name"], filter_name2)
|
|
227
|
+
|
|
228
|
+
def test_export_saved_view_manufacturer_to_csv_with_replaced_filters(self):
|
|
229
|
+
"""Export a SavedView manufacturer to CSV after replacing filters."""
|
|
230
|
+
# URL: /?saved_view=<id>&description=<manufacturer2>
|
|
231
|
+
manufacturer = Manufacturer.objects.create(name="Test Manufacturer")
|
|
232
|
+
manufacturer2 = Manufacturer.objects.create(name="Test2 Manufacturer", description="test filter")
|
|
233
|
+
filter_name = manufacturer.name
|
|
234
|
+
filter_description = manufacturer2.description
|
|
235
|
+
sv = self._create_saved_view(model_class=Manufacturer, config={"filter_params": {"name": [filter_name]}})
|
|
236
|
+
rows = self._run_export_job(
|
|
237
|
+
query_string=f"saved_view={sv.pk}&description={filter_description}", model_class=Manufacturer
|
|
238
|
+
)
|
|
239
|
+
self.assertEqual(len(rows), 1)
|
|
240
|
+
self.assertEqual(rows[0]["name"], manufacturer2.name)
|
|
241
|
+
self.assertEqual(rows[0]["description"], filter_description)
|
|
242
|
+
self.assertTrue(all(row["name"] != filter_name for row in rows))
|
|
243
|
+
|
|
244
|
+
def test_export_saved_view_to_csv_after_removing_all_filters(self):
|
|
245
|
+
"""Export a SavedView to CSV after removing all filters."""
|
|
246
|
+
# URL: /?saved_view=<id>&all_filters_removed=true
|
|
247
|
+
filter_name = Status.objects.first().name
|
|
248
|
+
sv = self._create_saved_view(config={"filter_params": {"name": [filter_name]}})
|
|
249
|
+
rows = self._run_export_job(query_string=f"saved_view={sv.pk}&all_filters_removed=true")
|
|
250
|
+
self.assertEqual(len(rows), Status.objects.count())
|
|
251
|
+
|
|
139
252
|
|
|
140
253
|
class ImportObjectsTestCase(TransactionTestCase):
|
|
141
254
|
databases = ("default", "job_logs")
|
nautobot/core/tests/test_ui.py
CHANGED
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
from unittest.mock import patch
|
|
4
4
|
|
|
5
5
|
from django.template import Context
|
|
6
|
+
from django.test import RequestFactory
|
|
6
7
|
|
|
7
8
|
from nautobot.core.templatetags.helpers import HTML_NONE
|
|
8
9
|
from nautobot.core.testing import TestCase
|
|
9
|
-
from nautobot.core.ui.object_detail import BaseTextPanel, DataTablePanel, Panel
|
|
10
|
+
from nautobot.core.ui.object_detail import BaseTextPanel, DataTablePanel, ObjectsTablePanel, Panel
|
|
11
|
+
from nautobot.dcim.models import DeviceRedundancyGroup
|
|
12
|
+
from nautobot.dcim.tables.devices import DeviceTable
|
|
10
13
|
|
|
11
14
|
|
|
12
15
|
class DataTablePanelTest(TestCase):
|
|
@@ -148,3 +151,52 @@ class BaseTextPanelTest(TestCase):
|
|
|
148
151
|
panel = BaseTextPanel(weight=100)
|
|
149
152
|
with self.assertRaises(NotImplementedError):
|
|
150
153
|
panel.get_value({})
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class ObjectsTablePanelTest(TestCase):
|
|
157
|
+
def setUp(self):
|
|
158
|
+
super().setUp()
|
|
159
|
+
self.factory = RequestFactory()
|
|
160
|
+
self.request = self.factory.get("/")
|
|
161
|
+
self.request.user = self.user
|
|
162
|
+
|
|
163
|
+
def test_include_exclude_columns(self):
|
|
164
|
+
panel = ObjectsTablePanel(
|
|
165
|
+
weight=100,
|
|
166
|
+
table_class=DeviceTable,
|
|
167
|
+
table_attribute="devices_sorted",
|
|
168
|
+
related_field_name="device_redundancy_group",
|
|
169
|
+
include_columns=[
|
|
170
|
+
"device_redundancy_group_priority",
|
|
171
|
+
],
|
|
172
|
+
exclude_columns=[
|
|
173
|
+
"rack",
|
|
174
|
+
],
|
|
175
|
+
)
|
|
176
|
+
redundancy_group = DeviceRedundancyGroup.objects.first()
|
|
177
|
+
context = {
|
|
178
|
+
"request": self.request,
|
|
179
|
+
"object": redundancy_group,
|
|
180
|
+
}
|
|
181
|
+
result = panel.get_extra_context(context)
|
|
182
|
+
columns = result["body_content_table"].columns
|
|
183
|
+
self.assertIn("device_redundancy_group_priority", [col.name for col in columns])
|
|
184
|
+
self.assertNotIn("rack", [col.name for col in columns])
|
|
185
|
+
|
|
186
|
+
def test_invalid_include_columns(self):
|
|
187
|
+
with self.assertRaises(ValueError) as context:
|
|
188
|
+
panel = ObjectsTablePanel(
|
|
189
|
+
weight=100,
|
|
190
|
+
table_class=DeviceTable,
|
|
191
|
+
table_attribute="devices_sorted",
|
|
192
|
+
related_field_name="device_redundancy_group",
|
|
193
|
+
include_columns=["non_existent_column"],
|
|
194
|
+
)
|
|
195
|
+
redundancy_group = DeviceRedundancyGroup.objects.first()
|
|
196
|
+
context_data = {
|
|
197
|
+
"request": self.request,
|
|
198
|
+
"object": redundancy_group,
|
|
199
|
+
}
|
|
200
|
+
panel.get_extra_context(context_data)
|
|
201
|
+
|
|
202
|
+
self.assertIn("non-existent column `non_existent_column`", str(context.exception))
|
|
@@ -714,6 +714,17 @@ class LookupRelatedFunctionTest(TestCase):
|
|
|
714
714
|
form_field.queryset, extras_utils.ChangeLoggedModelsQuery().as_queryset()
|
|
715
715
|
)
|
|
716
716
|
|
|
717
|
+
form_field = filtering.get_filterset_parameter_form_field(
|
|
718
|
+
extras_models.ObjectMetadata, "assigned_object_type"
|
|
719
|
+
)
|
|
720
|
+
self.assertIsInstance(form_field, forms.MultipleContentTypeField)
|
|
721
|
+
self.assertQuerysetEqualAndNotEmpty(
|
|
722
|
+
form_field.queryset,
|
|
723
|
+
ContentType.objects.filter(extras_utils.FeatureQuery("metadata").get_query()).order_by(
|
|
724
|
+
"app_label", "model"
|
|
725
|
+
),
|
|
726
|
+
)
|
|
727
|
+
|
|
717
728
|
with self.subTest("Test prefers_id"):
|
|
718
729
|
form_field = filtering.get_filterset_parameter_form_field(dcim_models.Device, "location")
|
|
719
730
|
self.assertEqual("id", form_field.to_field_name)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import os
|
|
2
3
|
import re
|
|
4
|
+
import tempfile
|
|
3
5
|
from unittest import mock, skipIf
|
|
4
6
|
import urllib.parse
|
|
5
7
|
|
|
@@ -185,6 +187,77 @@ class HomeViewTestCase(TestCase):
|
|
|
185
187
|
self.assertNotIn("Welcome to Nautobot!", response.content.decode(response.charset))
|
|
186
188
|
|
|
187
189
|
|
|
190
|
+
class MediaViewTestCase(TestCase):
|
|
191
|
+
def test_media_unauthenticated(self):
|
|
192
|
+
"""
|
|
193
|
+
Test that unauthenticated users are redirected to login when accessing media files whether they exist or not.
|
|
194
|
+
"""
|
|
195
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
196
|
+
with override_settings(
|
|
197
|
+
MEDIA_ROOT=temp_dir,
|
|
198
|
+
BRANDING_FILEPATHS={"logo": os.path.join("branding", "logo.txt")},
|
|
199
|
+
):
|
|
200
|
+
file_path = os.path.join(temp_dir, "foo.txt")
|
|
201
|
+
url = reverse("media", kwargs={"path": "foo.txt"})
|
|
202
|
+
self.client.logout()
|
|
203
|
+
|
|
204
|
+
# Unauthenticated request to nonexistent media file should redirect to login page
|
|
205
|
+
response = self.client.get(url)
|
|
206
|
+
self.assertRedirects(
|
|
207
|
+
response, expected_url=f"{reverse('login')}?next={url}", status_code=302, target_status_code=200
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Unauthenticated request to existent media file should redirect to login page as well
|
|
211
|
+
with open(file_path, "w") as f:
|
|
212
|
+
f.write("Hello, world!")
|
|
213
|
+
response = self.client.get(url)
|
|
214
|
+
self.assertRedirects(
|
|
215
|
+
response, expected_url=f"{reverse('login')}?next={url}", status_code=302, target_status_code=200
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
def test_branding_media(self):
|
|
219
|
+
"""
|
|
220
|
+
Test that users can access branding files listed in `settings.BRANDING_FILEPATHS` regardless of authentication.
|
|
221
|
+
"""
|
|
222
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
223
|
+
with override_settings(
|
|
224
|
+
MEDIA_ROOT=temp_dir,
|
|
225
|
+
BRANDING_FILEPATHS={"logo": os.path.join("branding", "logo.txt")},
|
|
226
|
+
):
|
|
227
|
+
os.makedirs(os.path.join(temp_dir, "branding"))
|
|
228
|
+
file_path = os.path.join(temp_dir, "branding", "logo.txt")
|
|
229
|
+
with open(file_path, "w") as f:
|
|
230
|
+
f.write("Hello, world!")
|
|
231
|
+
|
|
232
|
+
url = reverse("media", kwargs={"path": "branding/logo.txt"})
|
|
233
|
+
|
|
234
|
+
# Authenticated request succeeds
|
|
235
|
+
response = self.client.get(url)
|
|
236
|
+
self.assertHttpStatus(response, 200)
|
|
237
|
+
self.assertIn("Hello, world!", b"".join(response).decode(response.charset))
|
|
238
|
+
|
|
239
|
+
# Unauthenticated request also succeeds
|
|
240
|
+
self.client.logout()
|
|
241
|
+
response = self.client.get(url)
|
|
242
|
+
self.assertHttpStatus(response, 200)
|
|
243
|
+
self.assertIn("Hello, world!", b"".join(response).decode(response.charset))
|
|
244
|
+
|
|
245
|
+
def test_media_authenticated(self):
|
|
246
|
+
"""
|
|
247
|
+
Test that authenticated users can access regular media files stored in the `MEDIA_ROOT`.
|
|
248
|
+
"""
|
|
249
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
250
|
+
with override_settings(MEDIA_ROOT=temp_dir):
|
|
251
|
+
file_path = os.path.join(temp_dir, "foo.txt")
|
|
252
|
+
with open(file_path, "w") as f:
|
|
253
|
+
f.write("Hello, world!")
|
|
254
|
+
|
|
255
|
+
url = reverse("media", kwargs={"path": "foo.txt"})
|
|
256
|
+
response = self.client.get(url)
|
|
257
|
+
self.assertHttpStatus(response, 200)
|
|
258
|
+
self.assertIn("Hello, world!", b"".join(response).decode(response.charset))
|
|
259
|
+
|
|
260
|
+
|
|
188
261
|
@override_settings(BRANDING_TITLE="Nautobot")
|
|
189
262
|
class SearchFieldsTestCase(TestCase):
|
|
190
263
|
def test_search_bar_redirect_to_login(self):
|
|
@@ -728,9 +728,7 @@ class ObjectsTablePanel(Panel):
|
|
|
728
728
|
table_title (str, optional): The title to display in the panel heading for the table.
|
|
729
729
|
If None, defaults to the plural verbose name of the table model.
|
|
730
730
|
include_columns (list, optional): A list of field names to include in the table display.
|
|
731
|
-
If provided, only these fields will be displayed in the table.
|
|
732
731
|
exclude_columns (list, optional): A list of field names to exclude from the table display.
|
|
733
|
-
Mutually exclusive with `include_columns`.
|
|
734
732
|
add_button_route (str, optional): The route used to generate the "add" button URL. Defaults to "default",
|
|
735
733
|
which uses the default table's model `add` route.
|
|
736
734
|
add_permissions (list, optional): A list of permissions required for the "add" button to be displayed.
|
|
@@ -777,8 +775,6 @@ class ObjectsTablePanel(Panel):
|
|
|
777
775
|
self.order_by_fields = order_by_fields
|
|
778
776
|
self.table_title = table_title
|
|
779
777
|
self.max_display_count = max_display_count
|
|
780
|
-
if exclude_columns and include_columns:
|
|
781
|
-
raise ValueError("You can only specify either `exclude_columns` or `include_columns`")
|
|
782
778
|
self.include_columns = include_columns
|
|
783
779
|
self.exclude_columns = exclude_columns
|
|
784
780
|
self.add_button_route = add_button_route
|
|
@@ -878,14 +874,17 @@ class ObjectsTablePanel(Panel):
|
|
|
878
874
|
# to redirect the user back to the correct tab after editing/deleteing an object
|
|
879
875
|
body_content_table.columns["actions"].column.extra_context["return_url_extra"] = f"?tab={self.tab_id}"
|
|
880
876
|
|
|
881
|
-
if self.exclude_columns
|
|
877
|
+
if self.exclude_columns:
|
|
882
878
|
for column in body_content_table.columns:
|
|
883
|
-
if
|
|
884
|
-
self.include_columns and column.name not in self.include_columns
|
|
885
|
-
):
|
|
879
|
+
if column.name in self.exclude_columns:
|
|
886
880
|
body_content_table.columns.hide(column.name)
|
|
887
|
-
|
|
888
|
-
|
|
881
|
+
|
|
882
|
+
if self.include_columns:
|
|
883
|
+
for column in self.include_columns:
|
|
884
|
+
if column not in body_content_table.base_columns:
|
|
885
|
+
raise ValueError(f"You are specifying a non-existent column `{column}`")
|
|
886
|
+
body_content_table.columns.show(column)
|
|
887
|
+
|
|
889
888
|
# Enable bulk action toggle if the user has appropriate permissions
|
|
890
889
|
user = request.user
|
|
891
890
|
if self.enable_bulk_actions and (
|
|
@@ -906,10 +905,18 @@ class ObjectsTablePanel(Panel):
|
|
|
906
905
|
body_content_table_model = body_content_table.Meta.model
|
|
907
906
|
related_field_name = self.related_field_name or self.table_filter or obj._meta.model_name
|
|
908
907
|
|
|
908
|
+
list_url = getattr(self.table_class, "list_url", None)
|
|
909
|
+
if not list_url:
|
|
910
|
+
list_url = get_route_for_model(body_content_table_model, "list")
|
|
911
|
+
|
|
909
912
|
try:
|
|
910
|
-
list_route = reverse(
|
|
911
|
-
body_content_table_list_url = f"{list_route}?{related_field_name}={obj.pk}"
|
|
913
|
+
list_route = reverse(list_url)
|
|
912
914
|
except NoReverseMatch:
|
|
915
|
+
list_route = None
|
|
916
|
+
|
|
917
|
+
if list_route:
|
|
918
|
+
body_content_table_list_url = f"{list_route}?{related_field_name}={obj.pk}"
|
|
919
|
+
else:
|
|
913
920
|
body_content_table_list_url = None
|
|
914
921
|
|
|
915
922
|
body_content_table_add_url = self._get_table_add_url(context)
|
nautobot/core/urls.py
CHANGED
|
@@ -2,13 +2,13 @@ from django.conf import settings
|
|
|
2
2
|
from django.http import HttpResponse, HttpResponseNotFound
|
|
3
3
|
from django.urls import include, path
|
|
4
4
|
from django.views.generic import TemplateView
|
|
5
|
-
from django.views.static import serve
|
|
6
5
|
|
|
7
6
|
from nautobot.core.views import (
|
|
8
7
|
AboutView,
|
|
9
8
|
CustomGraphQLView,
|
|
10
9
|
get_file_with_authorization,
|
|
11
10
|
HomeView,
|
|
11
|
+
MediaView,
|
|
12
12
|
NautobotMetricsView,
|
|
13
13
|
NautobotMetricsViewAuth,
|
|
14
14
|
RenderJinjaView,
|
|
@@ -51,7 +51,7 @@ urlpatterns = [
|
|
|
51
51
|
# GraphQL
|
|
52
52
|
path("graphql/", CustomGraphQLView.as_view(graphiql=True), name="graphql"),
|
|
53
53
|
# Serving static media in Django (TODO: should be DEBUG mode only - "This view is NOT hardened for production use")
|
|
54
|
-
path("media/<path:path>",
|
|
54
|
+
path("media/<path:path>", MediaView.as_view(), name="media"),
|
|
55
55
|
# Admin
|
|
56
56
|
path("admin/", admin_site.urls),
|
|
57
57
|
# Errors
|
nautobot/core/utils/filtering.py
CHANGED
|
@@ -135,11 +135,14 @@ def get_filterset_parameter_form_field(model, parameter, filterset=None):
|
|
|
135
135
|
from nautobot.core.models.fields import slugify_dashes_to_underscores # Avoid circular import
|
|
136
136
|
|
|
137
137
|
plural_name = slugify_dashes_to_underscores(model._meta.verbose_name_plural)
|
|
138
|
+
|
|
138
139
|
# Cable-connectable models use "cable_terminations", not "cables", as the feature name
|
|
139
140
|
if plural_name == "cables":
|
|
140
141
|
plural_name = "cable_terminations"
|
|
141
142
|
elif plural_name == "metadata_types":
|
|
142
143
|
plural_name = "metadata"
|
|
144
|
+
elif plural_name == "object_metadata":
|
|
145
|
+
plural_name = "metadata"
|
|
143
146
|
try:
|
|
144
147
|
form_field = MultipleContentTypeField(choices_as_strings=True, feature=plural_name)
|
|
145
148
|
except KeyError:
|
nautobot/core/views/__init__.py
CHANGED
|
@@ -3,6 +3,7 @@ import datetime
|
|
|
3
3
|
import logging
|
|
4
4
|
import os
|
|
5
5
|
import platform
|
|
6
|
+
import posixpath
|
|
6
7
|
import re
|
|
7
8
|
import sys
|
|
8
9
|
import time
|
|
@@ -24,6 +25,7 @@ from django.views.csrf import csrf_failure as _csrf_failure
|
|
|
24
25
|
from django.views.decorators.csrf import requires_csrf_token
|
|
25
26
|
from django.views.defaults import ERROR_500_TEMPLATE_NAME, page_not_found
|
|
26
27
|
from django.views.generic import TemplateView, View
|
|
28
|
+
from django.views.static import serve
|
|
27
29
|
from graphene_django.views import GraphQLView
|
|
28
30
|
from packaging import version
|
|
29
31
|
from prometheus_client import (
|
|
@@ -133,6 +135,25 @@ class HomeView(AccessMixin, TemplateView):
|
|
|
133
135
|
return self.render_to_response(context)
|
|
134
136
|
|
|
135
137
|
|
|
138
|
+
class MediaView(AccessMixin, View):
|
|
139
|
+
"""
|
|
140
|
+
Serves media files while enforcing login restrictions.
|
|
141
|
+
|
|
142
|
+
This view wraps Django's `serve()` function to ensure that access to media files (with the exception of
|
|
143
|
+
branding files defined in `settings.BRANDING_FILEPATHS`) is restricted to authenticated users.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
def get(self, request, path):
|
|
147
|
+
if request.user.is_authenticated:
|
|
148
|
+
return serve(request, path, document_root=settings.MEDIA_ROOT)
|
|
149
|
+
|
|
150
|
+
# Unauthenticated users can access BRANDING_FILEPATHS only
|
|
151
|
+
if posixpath.normpath(path).lstrip("/") in settings.BRANDING_FILEPATHS.values():
|
|
152
|
+
return serve(request, path, document_root=settings.MEDIA_ROOT)
|
|
153
|
+
|
|
154
|
+
return self.handle_no_permission()
|
|
155
|
+
|
|
156
|
+
|
|
136
157
|
class WorkerStatusView(UserPassesTestMixin, TemplateView):
|
|
137
158
|
template_name = "utilities/worker_status.html"
|
|
138
159
|
|
nautobot/core/views/renderers.py
CHANGED
|
@@ -301,7 +301,7 @@ class NautobotHTMLRenderer(renderers.BrowsableAPIRenderer):
|
|
|
301
301
|
# If the view does not have a object_detail_content attribute, set it to None.
|
|
302
302
|
context["object_detail_content"] = None
|
|
303
303
|
context.update(common_detail_view_context(request, instance))
|
|
304
|
-
|
|
304
|
+
if view.action == "list":
|
|
305
305
|
# Construct valid actions for list view.
|
|
306
306
|
valid_actions = self.validate_action_buttons(view, request)
|
|
307
307
|
# Query SavedViews for dropdown button
|
nautobot/dcim/forms.py
CHANGED
|
@@ -727,6 +727,16 @@ class RackReservationForm(NautobotModelForm, TenancyForm):
|
|
|
727
727
|
"tags",
|
|
728
728
|
]
|
|
729
729
|
|
|
730
|
+
def __init__(self, *args, **kwargs):
|
|
731
|
+
super().__init__(*args, **kwargs)
|
|
732
|
+
|
|
733
|
+
rack = getattr(self.instance, "rack", None)
|
|
734
|
+
if rack:
|
|
735
|
+
if not self.initial.get("location"):
|
|
736
|
+
self.initial["location"] = rack.location
|
|
737
|
+
if not self.initial.get("rack_group"):
|
|
738
|
+
self.initial["rack_group"] = rack.rack_group
|
|
739
|
+
|
|
730
740
|
|
|
731
741
|
class RackReservationBulkEditForm(TagsBulkEditFormMixin, NautobotBulkEditForm):
|
|
732
742
|
pk = forms.ModelMultipleChoiceField(queryset=RackReservation.objects.all(), widget=forms.MultipleHiddenInput())
|