nautobot 2.4.9__py3-none-any.whl → 2.4.11__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of nautobot might be problematic. Click here for more details.
- nautobot/cloud/tests/test_views.py +13 -1
- nautobot/cloud/views.py +39 -9
- nautobot/core/api/parsers.py +56 -2
- nautobot/core/celery/__init__.py +21 -0
- nautobot/core/celery/encoders.py +3 -0
- nautobot/core/forms/forms.py +4 -1
- nautobot/core/jobs/bulk_actions.py +8 -8
- nautobot/core/jobs/cleanup.py +11 -0
- nautobot/core/management/commands/generate_test_data.py +2 -1
- nautobot/core/models/__init__.py +2 -0
- nautobot/core/templates/generic/object_retrieve.html +1 -1
- nautobot/core/testing/mixins.py +19 -1
- nautobot/core/testing/views.py +104 -8
- nautobot/core/tests/test_csv.py +92 -1
- nautobot/core/tests/test_jinja_filters.py +59 -0
- nautobot/core/tests/test_jobs.py +20 -4
- nautobot/core/tests/test_utils.py +193 -0
- nautobot/core/tests/test_views.py +73 -0
- nautobot/core/tests/test_views_utils.py +53 -2
- nautobot/core/ui/object_detail.py +4 -0
- nautobot/core/urls.py +2 -2
- nautobot/core/utils/lookup.py +4 -2
- nautobot/core/utils/module_loading.py +86 -58
- nautobot/core/views/__init__.py +21 -0
- nautobot/core/views/generic.py +2 -12
- nautobot/core/views/mixins.py +19 -1
- nautobot/core/views/renderers.py +4 -13
- nautobot/core/views/utils.py +16 -0
- nautobot/dcim/api/serializers.py +13 -0
- nautobot/dcim/api/urls.py +1 -0
- nautobot/dcim/api/views.py +20 -0
- nautobot/dcim/apps.py +1 -0
- nautobot/dcim/factory.py +11 -0
- nautobot/dcim/filters/__init__.py +110 -0
- nautobot/dcim/forms.py +205 -19
- nautobot/dcim/migrations/0070_modulefamily_models.py +92 -0
- nautobot/dcim/models/__init__.py +2 -0
- nautobot/dcim/models/device_component_templates.py +18 -0
- nautobot/dcim/models/device_components.py +25 -1
- nautobot/dcim/models/devices.py +68 -0
- nautobot/dcim/navigation.py +16 -0
- nautobot/dcim/tables/__init__.py +2 -0
- nautobot/dcim/tables/devices.py +48 -0
- nautobot/dcim/tables/devicetypes.py +35 -1
- nautobot/dcim/tables/template_code.py +2 -0
- nautobot/dcim/templates/dcim/controllermanageddevicegroup_retrieve.html +1 -90
- nautobot/dcim/templates/dcim/inc/cable_toggle_buttons.html +1 -1
- nautobot/dcim/templates/dcim/interfaceredundancygroup_retrieve.html +1 -63
- nautobot/dcim/templates/dcim/location.html +2 -249
- nautobot/dcim/templates/dcim/location_edit.html +2 -38
- nautobot/dcim/templates/dcim/location_retrieve.html +249 -0
- nautobot/dcim/templates/dcim/location_update.html +38 -0
- nautobot/dcim/templates/dcim/module_update.html +1 -0
- nautobot/dcim/templates/dcim/modulebay_retrieve.html +93 -1
- nautobot/dcim/templates/dcim/modulefamily_retrieve.html +31 -0
- nautobot/dcim/templates/dcim/moduletype_retrieve.html +6 -0
- nautobot/dcim/templates/dcim/powerfeed_retrieve.html +1 -160
- nautobot/dcim/tests/test_api.py +35 -0
- nautobot/dcim/tests/test_filters.py +102 -3
- nautobot/dcim/tests/test_models.py +146 -0
- nautobot/dcim/tests/test_views.py +70 -97
- nautobot/dcim/urls.py +4 -22
- nautobot/dcim/views.py +439 -153
- nautobot/extras/api/views.py +9 -2
- nautobot/extras/context_managers.py +2 -2
- nautobot/extras/datasources/git.py +11 -3
- nautobot/extras/forms/forms.py +9 -5
- nautobot/extras/jobs.py +4 -2
- nautobot/extras/models/customfields.py +2 -0
- nautobot/extras/models/datasources.py +13 -8
- nautobot/extras/models/groups.py +18 -0
- nautobot/extras/models/jobs.py +19 -0
- nautobot/extras/models/metadata.py +2 -0
- nautobot/extras/models/models.py +4 -0
- nautobot/extras/models/secrets.py +7 -0
- nautobot/extras/plugins/__init__.py +3 -0
- nautobot/extras/secrets/__init__.py +14 -0
- nautobot/extras/tables.py +40 -3
- nautobot/extras/templates/extras/configcontext.html +2 -220
- nautobot/extras/templates/extras/configcontext_edit.html +2 -50
- nautobot/extras/templates/extras/configcontext_retrieve.html +2 -0
- nautobot/extras/templates/extras/configcontext_update.html +50 -0
- nautobot/extras/templates/extras/configcontextschema.html +2 -48
- nautobot/extras/templates/extras/configcontextschema_edit.html +2 -19
- nautobot/extras/templates/extras/configcontextschema_retrieve.html +48 -0
- nautobot/extras/templates/extras/configcontextschema_update.html +19 -0
- nautobot/extras/templates/extras/inc/configcontext_data.html +1 -0
- nautobot/extras/templates/extras/inc/json_data.html +1 -1
- nautobot/extras/templates/extras/inc/json_format.html +2 -2
- nautobot/extras/templates/extras/job_edit.html +12 -6
- nautobot/extras/templates/extras/tag.html +2 -52
- nautobot/extras/templates/extras/tag_edit.html +2 -15
- nautobot/extras/templates/extras/tag_retrieve.html +52 -0
- nautobot/extras/templates/extras/tag_update.html +15 -0
- nautobot/extras/templates/extras/team_retrieve.html +2 -2
- nautobot/extras/tests/test_api.py +15 -15
- nautobot/extras/tests/test_context_managers.py +20 -0
- nautobot/extras/tests/test_filters.py +4 -4
- nautobot/extras/tests/test_jobs.py +23 -10
- nautobot/extras/tests/test_models.py +45 -8
- nautobot/extras/tests/test_plugins.py +6 -3
- nautobot/extras/tests/test_views.py +66 -11
- nautobot/extras/urls.py +4 -134
- nautobot/extras/views.py +113 -158
- nautobot/ipam/models.py +51 -4
- nautobot/ipam/tables.py +19 -0
- nautobot/ipam/templates/ipam/vlan.html +2 -84
- nautobot/ipam/templates/ipam/vlan_edit.html +2 -24
- nautobot/ipam/templates/ipam/vlan_retrieve.html +84 -0
- nautobot/ipam/templates/ipam/vlan_update.html +24 -0
- nautobot/ipam/tests/test_views.py +5 -0
- nautobot/ipam/urls.py +1 -21
- nautobot/ipam/views.py +45 -70
- nautobot/project-static/docs/404.html +31 -8
- nautobot/project-static/docs/apps/index.html +31 -8
- nautobot/project-static/docs/apps/nautobot-apps.html +31 -8
- nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +31 -8
- nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +31 -8
- nautobot/project-static/docs/code-reference/nautobot/apps/api.html +31 -8
- nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +31 -8
- nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +31 -8
- nautobot/project-static/docs/code-reference/nautobot/apps/config.html +31 -8
- nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +31 -8
- nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +31 -8
- nautobot/project-static/docs/code-reference/nautobot/apps/events.html +31 -8
- nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +31 -8
- nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +31 -8
- nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +31 -8
- nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +31 -8
- nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +31 -8
- nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +31 -8
- nautobot/project-static/docs/code-reference/nautobot/apps/models.html +31 -8
- nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +31 -8
- nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +31 -8
- nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +31 -8
- nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +120 -8
- nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +31 -8
- nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +31 -8
- nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +31 -8
- nautobot/project-static/docs/code-reference/nautobot/apps/views.html +31 -8
- nautobot/project-static/docs/development/apps/api/configuration-view.html +31 -8
- nautobot/project-static/docs/development/apps/api/database-backend-config.html +31 -8
- nautobot/project-static/docs/development/apps/api/models/django-admin.html +31 -8
- nautobot/project-static/docs/development/apps/api/models/global-search.html +31 -8
- nautobot/project-static/docs/development/apps/api/models/graphql.html +31 -8
- nautobot/project-static/docs/development/apps/api/models/index.html +31 -8
- nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +40 -8
- nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +31 -8
- nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +31 -8
- nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +31 -8
- nautobot/project-static/docs/development/apps/api/platform-features/index.html +31 -8
- nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +31 -8
- nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +31 -8
- nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +31 -8
- nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +70 -46
- nautobot/project-static/docs/development/apps/api/platform-features/table-extensions.html +31 -8
- nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +31 -8
- nautobot/project-static/docs/development/apps/api/prometheus.html +31 -8
- nautobot/project-static/docs/development/apps/api/setup.html +31 -8
- nautobot/project-static/docs/development/apps/api/testing.html +31 -8
- nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +31 -8
- nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +31 -8
- nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +31 -8
- nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +31 -8
- nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +31 -8
- nautobot/project-static/docs/development/apps/api/views/base-template.html +31 -8
- nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +31 -8
- nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +31 -8
- nautobot/project-static/docs/development/apps/api/views/help-documentation.html +31 -8
- nautobot/project-static/docs/development/apps/api/views/index.html +31 -8
- nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +31 -8
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +31 -8
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +31 -8
- nautobot/project-static/docs/development/apps/api/views/notes.html +31 -8
- nautobot/project-static/docs/development/apps/api/views/rest-api.html +31 -8
- nautobot/project-static/docs/development/apps/api/views/urls.html +31 -8
- nautobot/project-static/docs/development/apps/index.html +31 -8
- nautobot/project-static/docs/development/apps/migration/code-updates.html +31 -8
- nautobot/project-static/docs/development/apps/migration/dependency-updates.html +31 -8
- nautobot/project-static/docs/development/apps/migration/from-v1.html +31 -8
- nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +31 -8
- nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +31 -8
- nautobot/project-static/docs/development/apps/migration/model-updates/global.html +31 -8
- nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +31 -8
- nautobot/project-static/docs/development/apps/migration/ui-component-framework/best-practices.html +31 -8
- nautobot/project-static/docs/development/apps/migration/ui-component-framework/custom-content.html +31 -8
- nautobot/project-static/docs/development/apps/migration/ui-component-framework/index.html +31 -8
- nautobot/project-static/docs/development/apps/migration/ui-component-framework/migration-steps.html +31 -8
- nautobot/project-static/docs/development/apps/porting-from-netbox.html +31 -8
- nautobot/project-static/docs/development/core/application-registry.html +31 -8
- nautobot/project-static/docs/development/core/best-practices.html +31 -8
- nautobot/project-static/docs/development/core/bootstrap-ui.html +31 -8
- nautobot/project-static/docs/development/core/caching.html +31 -8
- nautobot/project-static/docs/development/core/controllers.html +31 -8
- nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +31 -8
- nautobot/project-static/docs/development/core/generic-views.html +31 -8
- nautobot/project-static/docs/development/core/getting-started.html +31 -8
- nautobot/project-static/docs/development/core/homepage.html +31 -8
- nautobot/project-static/docs/development/core/index.html +31 -8
- nautobot/project-static/docs/development/core/minikube-dev-environment-for-k8s-jobs.html +31 -8
- nautobot/project-static/docs/development/core/model-checklist.html +31 -8
- nautobot/project-static/docs/development/core/model-features.html +31 -8
- nautobot/project-static/docs/development/core/natural-keys.html +31 -8
- nautobot/project-static/docs/development/core/navigation-menu.html +31 -8
- nautobot/project-static/docs/development/core/release-checklist.html +31 -8
- nautobot/project-static/docs/development/core/role-internals.html +31 -8
- nautobot/project-static/docs/development/core/settings.html +31 -8
- nautobot/project-static/docs/development/core/style-guide.html +31 -8
- nautobot/project-static/docs/development/core/templates.html +31 -8
- nautobot/project-static/docs/development/core/testing.html +31 -8
- nautobot/project-static/docs/development/core/ui-component-framework.html +31 -8
- nautobot/project-static/docs/development/core/user-preferences.html +31 -8
- nautobot/project-static/docs/development/index.html +31 -8
- nautobot/project-static/docs/development/jobs/getting-started.html +35 -8
- nautobot/project-static/docs/development/jobs/index.html +31 -8
- nautobot/project-static/docs/development/jobs/installation.html +31 -8
- nautobot/project-static/docs/development/jobs/job-extensions.html +31 -8
- nautobot/project-static/docs/development/jobs/job-logging.html +31 -8
- nautobot/project-static/docs/development/jobs/job-patterns.html +31 -8
- nautobot/project-static/docs/development/jobs/job-structure.html +31 -8
- nautobot/project-static/docs/development/jobs/migration/from-v1.html +31 -8
- nautobot/project-static/docs/development/jobs/testing.html +31 -8
- nautobot/project-static/docs/index.html +31 -8
- nautobot/project-static/docs/insert-analytics.sh +36 -0
- nautobot/project-static/docs/objects.inv +0 -0
- nautobot/project-static/docs/overview/application_stack.html +31 -8
- nautobot/project-static/docs/overview/design_philosophy.html +31 -8
- nautobot/project-static/docs/release-notes/index.html +31 -8
- nautobot/project-static/docs/release-notes/version-1.0.html +31 -8
- nautobot/project-static/docs/release-notes/version-1.1.html +31 -8
- nautobot/project-static/docs/release-notes/version-1.2.html +31 -8
- nautobot/project-static/docs/release-notes/version-1.3.html +31 -8
- nautobot/project-static/docs/release-notes/version-1.4.html +31 -8
- nautobot/project-static/docs/release-notes/version-1.5.html +31 -8
- nautobot/project-static/docs/release-notes/version-1.6.html +328 -8
- nautobot/project-static/docs/release-notes/version-2.0.html +31 -8
- nautobot/project-static/docs/release-notes/version-2.1.html +31 -8
- nautobot/project-static/docs/release-notes/version-2.2.html +31 -8
- nautobot/project-static/docs/release-notes/version-2.3.html +31 -8
- nautobot/project-static/docs/release-notes/version-2.4.html +353 -8
- nautobot/project-static/docs/search/search_index.json +1 -1
- nautobot/project-static/docs/sitemap.xml +302 -298
- nautobot/project-static/docs/sitemap.xml.gz +0 -0
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +31 -8
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +31 -8
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +31 -8
- nautobot/project-static/docs/user-guide/administration/configuration/index.html +31 -8
- nautobot/project-static/docs/user-guide/administration/configuration/redis.html +31 -8
- nautobot/project-static/docs/user-guide/administration/configuration/settings.html +31 -8
- nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +31 -8
- nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +31 -8
- nautobot/project-static/docs/user-guide/administration/guides/docker.html +31 -8
- nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +31 -8
- nautobot/project-static/docs/user-guide/administration/guides/permissions.html +31 -8
- nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +31 -8
- nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +31 -8
- nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +31 -8
- nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +31 -8
- nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +31 -8
- nautobot/project-static/docs/user-guide/administration/installation/app-install.html +31 -8
- nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +31 -8
- nautobot/project-static/docs/user-guide/administration/installation/http-server.html +31 -8
- nautobot/project-static/docs/user-guide/administration/installation/index.html +31 -8
- nautobot/project-static/docs/user-guide/administration/installation/install_system.html +31 -8
- nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +31 -8
- nautobot/project-static/docs/user-guide/administration/installation/services.html +31 -8
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +31 -8
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +31 -8
- nautobot/project-static/docs/user-guide/administration/security/index.html +31 -9
- nautobot/project-static/docs/user-guide/administration/security/notices.html +144 -9
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +31 -8
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +31 -8
- nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +31 -8
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +31 -8
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +31 -8
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +31 -8
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +31 -8
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +31 -8
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +31 -8
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +31 -8
- nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +43 -20
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +35 -8
- nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +35 -8
- nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +35 -8
- nautobot/project-static/docs/user-guide/core-data-model/dcim/modulefamily.html +10261 -0
- nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +34 -11
- nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualdevicecontext.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/wireless/index.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/wireless/radioprofile.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/wireless/supporteddatarate.html +31 -8
- nautobot/project-static/docs/user-guide/core-data-model/wireless/wirelessnetwork.html +31 -8
- nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +31 -8
- nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +31 -8
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +31 -8
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +31 -8
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +31 -8
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +31 -8
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +31 -8
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +31 -8
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +31 -8
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +31 -8
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +31 -8
- nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +41 -15
- nautobot/project-static/docs/user-guide/feature-guides/graphql.html +31 -8
- nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +31 -8
- nautobot/project-static/docs/user-guide/feature-guides/relationships.html +31 -8
- nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +31 -8
- nautobot/project-static/docs/user-guide/feature-guides/wireless-networks-and-controllers.html +31 -8
- nautobot/project-static/docs/user-guide/index.html +31 -8
- nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +31 -8
- nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +31 -8
- nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +31 -8
- nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +31 -8
- nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +31 -8
- nautobot/project-static/docs/user-guide/platform-functionality/events.html +31 -8
- nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +31 -8
- nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +31 -8
- nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +37 -9
- nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +31 -8
- nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +31 -8
- nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +31 -8
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +31 -8
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +31 -8
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +31 -8
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +31 -8
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobqueue.html +31 -8
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/kubernetes-job-support.html +31 -8
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/managing-jobs.html +31 -8
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +31 -8
- nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +31 -8
- nautobot/project-static/docs/user-guide/platform-functionality/note.html +31 -8
- nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +31 -8
- nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +31 -8
- nautobot/project-static/docs/user-guide/platform-functionality/rendering-jinja-templates.html +31 -8
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +31 -8
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +31 -8
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +31 -8
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +31 -8
- nautobot/project-static/docs/user-guide/platform-functionality/role.html +31 -8
- nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +31 -8
- nautobot/project-static/docs/user-guide/platform-functionality/secret.html +31 -8
- nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +31 -8
- nautobot/project-static/docs/user-guide/platform-functionality/status.html +31 -8
- nautobot/project-static/docs/user-guide/platform-functionality/tag.html +31 -8
- nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +31 -8
- nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +31 -8
- nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +31 -8
- nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +31 -8
- nautobot/tenancy/tables.py +2 -0
- nautobot/users/models.py +4 -0
- nautobot/virtualization/models.py +4 -0
- nautobot/virtualization/tests/test_views.py +1 -1
- nautobot/wireless/forms.py +0 -1
- nautobot/wireless/models.py +1 -1
- nautobot/wireless/tables.py +7 -0
- {nautobot-2.4.9.dist-info → nautobot-2.4.11.dist-info}/METADATA +4 -4
- {nautobot-2.4.9.dist-info → nautobot-2.4.11.dist-info}/RECORD +433 -418
- /nautobot/dcim/templates/dcim/{platform_edit.html → platform_create.html} +0 -0
- /nautobot/extras/test_jobs/{pass.py → pass_job.py} +0 -0
- {nautobot-2.4.9.dist-info → nautobot-2.4.11.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.4.9.dist-info → nautobot-2.4.11.dist-info}/NOTICE +0 -0
- {nautobot-2.4.9.dist-info → nautobot-2.4.11.dist-info}/WHEEL +0 -0
- {nautobot-2.4.9.dist-info → nautobot-2.4.11.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
|
@@ -689,10 +689,10 @@ class LogsCleanupTestCase(TransactionTestCase):
|
|
|
689
689
|
cleanup_types=[CleanupTypes.JOB_RESULT],
|
|
690
690
|
max_age=60,
|
|
691
691
|
)
|
|
692
|
-
self.assertFalse(JobResult.objects.filter(date_done__lt=cutoff).exists())
|
|
693
|
-
self.assertTrue(JobResult.objects.filter(date_done__gte=cutoff).exists())
|
|
694
|
-
self.assertTrue(ObjectChange.objects.filter(time__lt=cutoff).exists())
|
|
695
|
-
self.assertTrue(ObjectChange.objects.filter(time__gte=cutoff).exists())
|
|
692
|
+
self.assertFalse(JobResult.objects.filter(date_done__lt=cutoff).exists(), cm.output)
|
|
693
|
+
self.assertTrue(JobResult.objects.filter(date_done__gte=cutoff).exists(), cm.output)
|
|
694
|
+
self.assertTrue(ObjectChange.objects.filter(time__lt=cutoff).exists(), cm.output)
|
|
695
|
+
self.assertTrue(ObjectChange.objects.filter(time__gte=cutoff).exists(), cm.output)
|
|
696
696
|
|
|
697
697
|
started_logs = {
|
|
698
698
|
"job_result_id": str(job_result.id),
|
|
@@ -840,6 +840,22 @@ class BulkEditTestCase(TransactionTestCase):
|
|
|
840
840
|
)
|
|
841
841
|
self._common_no_error_test_assertion(Role, job_result, Role.objects.all().count(), color="aa1409")
|
|
842
842
|
|
|
843
|
+
def test_bulk_edit_objects_nullify(self):
|
|
844
|
+
"""
|
|
845
|
+
Bulk edit Role instances to nullify their weight.
|
|
846
|
+
"""
|
|
847
|
+
self.add_permissions("extras.change_role", "extras.view_role")
|
|
848
|
+
job_result = create_job_result_and_run_job(
|
|
849
|
+
"nautobot.core.jobs.bulk_actions",
|
|
850
|
+
"BulkEditObjects",
|
|
851
|
+
content_type=self.role_ct.id,
|
|
852
|
+
edit_all=True,
|
|
853
|
+
filter_query_params={},
|
|
854
|
+
form_data={"_nullify": ["weight"]},
|
|
855
|
+
username=self.user.username,
|
|
856
|
+
)
|
|
857
|
+
self._common_no_error_test_assertion(Role, job_result, Role.objects.all().count(), weight__isnull=True)
|
|
858
|
+
|
|
843
859
|
def test_bulk_edit_select_some(self):
|
|
844
860
|
"""
|
|
845
861
|
Bulk edit selected Namespace instances.
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import os.path
|
|
3
|
+
import sys
|
|
4
|
+
import tempfile
|
|
1
5
|
from unittest import mock
|
|
2
6
|
import uuid
|
|
3
7
|
|
|
@@ -17,6 +21,11 @@ from nautobot.core.models import fields as core_fields, utils as models_utils, v
|
|
|
17
21
|
from nautobot.core.testing import TestCase
|
|
18
22
|
from nautobot.core.utils import data as data_utils, filtering, lookup, querysets, requests
|
|
19
23
|
from nautobot.core.utils.migrations import update_object_change_ct_for_replaced_models
|
|
24
|
+
from nautobot.core.utils.module_loading import (
|
|
25
|
+
check_name_safe_to_import_privately,
|
|
26
|
+
clear_module_from_sys_modules,
|
|
27
|
+
import_modules_privately,
|
|
28
|
+
)
|
|
20
29
|
from nautobot.dcim import filters as dcim_filters, forms as dcim_forms, models as dcim_models, tables
|
|
21
30
|
from nautobot.extras import models as extras_models, utils as extras_utils
|
|
22
31
|
from nautobot.extras.choices import ObjectChangeActionChoices, RelationshipTypeChoices
|
|
@@ -959,6 +968,190 @@ class TestMigrationUtils(TestCase):
|
|
|
959
968
|
self.assertEqual(ObjectChange.objects.get(request_id=request_id).related_object_type, location_ct)
|
|
960
969
|
|
|
961
970
|
|
|
971
|
+
class TestModuleLoadingUtils(TestCase):
|
|
972
|
+
def test_check_name_safe_to_import_privately(self):
|
|
973
|
+
for invalid in (
|
|
974
|
+
"foo.bar", # not a valid identifier
|
|
975
|
+
"😂", # not a valid identifier
|
|
976
|
+
"from", # reserved keyword
|
|
977
|
+
"sys", # Python builtin
|
|
978
|
+
"nautobot", # installed package
|
|
979
|
+
"tkinter", # system library
|
|
980
|
+
):
|
|
981
|
+
with self.subTest(f"Invalid name: {invalid}"):
|
|
982
|
+
permitted, reason = check_name_safe_to_import_privately(invalid)
|
|
983
|
+
self.assertFalse(permitted)
|
|
984
|
+
self.assertIsInstance(reason, str)
|
|
985
|
+
|
|
986
|
+
def _create_test_files(self, root_directory: str, contents: dict):
|
|
987
|
+
"""Helper function to create arbitrary text files in a given directory."""
|
|
988
|
+
for relative_path, file_contents in contents.items():
|
|
989
|
+
os.makedirs(os.path.dirname(os.path.join(root_directory, relative_path)), exist_ok=True)
|
|
990
|
+
with open(os.path.join(root_directory, relative_path), "wt") as fd:
|
|
991
|
+
fd.write(file_contents)
|
|
992
|
+
|
|
993
|
+
def test_import_modules_privately_jobs_root_case(self):
|
|
994
|
+
with tempfile.TemporaryDirectory() as tempdir:
|
|
995
|
+
try:
|
|
996
|
+
contents = {
|
|
997
|
+
# Job file treated as a standalone module
|
|
998
|
+
"some_jobs.py": 'name = "some_jobs"',
|
|
999
|
+
# Job subdirectory treated as a package
|
|
1000
|
+
"my_jobs/__init__.py": '''\
|
|
1001
|
+
import my_jobs.some_submodule
|
|
1002
|
+
from . import relative_submodule
|
|
1003
|
+
name = "my_jobs"''',
|
|
1004
|
+
"my_jobs/some_submodule/__init__.py": 'name = "my_jobs.some_submodule"',
|
|
1005
|
+
"my_jobs/relative_submodule/__init__.py": 'name = "my_jobs.relative_submodule"',
|
|
1006
|
+
# Job file that shouldn't be loaded as it conflicts
|
|
1007
|
+
"tkinter.py": 'name = "tkinter"',
|
|
1008
|
+
# Job submodule that shouldn't be loaded as it conflicts
|
|
1009
|
+
"turtle/__init__.py": 'name = "turtle"',
|
|
1010
|
+
}
|
|
1011
|
+
self._create_test_files(tempdir, contents)
|
|
1012
|
+
|
|
1013
|
+
modules = import_modules_privately(tempdir, ignore_import_errors=False)
|
|
1014
|
+
self.assertEqual(["my_jobs", "some_jobs"], sorted([module.__name__ for module in modules]))
|
|
1015
|
+
# assertIn/assertNotIn are super noisy when dealing with the huge sys.modules dict, so instead:
|
|
1016
|
+
if "some_jobs" not in sys.modules:
|
|
1017
|
+
self.fail("Valid module wasn't loaded from JOBS_ROOT")
|
|
1018
|
+
if "my_jobs" not in sys.modules:
|
|
1019
|
+
self.fail("Valid package wasn't loaded from JOBS_ROOT")
|
|
1020
|
+
with self.assertRaises(KeyError, msg="conflicting module name was loaded unsafely from JOBS_ROOT"):
|
|
1021
|
+
sys.modules["tkinter"]
|
|
1022
|
+
with self.assertRaises(KeyError, msg="conflicting package name was loaded unsafely from JOBS_ROOT"):
|
|
1023
|
+
sys.modules["turtle"]
|
|
1024
|
+
|
|
1025
|
+
self.assertEqual(sys.modules["some_jobs"].name, "some_jobs")
|
|
1026
|
+
self.assertEqual(sys.modules["my_jobs"].name, "my_jobs")
|
|
1027
|
+
self.assertEqual(sys.modules["my_jobs"].some_submodule.name, "my_jobs.some_submodule")
|
|
1028
|
+
# self.assertEqual(sys.modules["my_jobs"].relative_submodule.name, "my_jobs.relative_submodule")
|
|
1029
|
+
|
|
1030
|
+
finally:
|
|
1031
|
+
clear_module_from_sys_modules("some_jobs")
|
|
1032
|
+
clear_module_from_sys_modules("my_jobs")
|
|
1033
|
+
|
|
1034
|
+
self.assertNotIn("some_jobs", sys.modules.keys())
|
|
1035
|
+
self.assertNotIn("my_jobs", sys.modules.keys())
|
|
1036
|
+
self.assertNotIn("my_jobs.some_submodule", sys.modules.keys())
|
|
1037
|
+
|
|
1038
|
+
# Test reloading of modules after code changes
|
|
1039
|
+
try:
|
|
1040
|
+
contents["some_jobs.py"] = 'name = "some_jobs_new"'
|
|
1041
|
+
contents["my_jobs/__init__.py"] = '''\
|
|
1042
|
+
import my_jobs.some_submodule
|
|
1043
|
+
from . import relative_submodule
|
|
1044
|
+
name = "my_jobs_new"'''
|
|
1045
|
+
contents["my_jobs/some_submodule/__init__.py"] = 'name = "my_jobs.some_submodule_new"'
|
|
1046
|
+
self._create_test_files(tempdir, contents)
|
|
1047
|
+
|
|
1048
|
+
modules = import_modules_privately(tempdir, ignore_import_errors=False)
|
|
1049
|
+
self.assertEqual(["my_jobs", "some_jobs"], sorted([module.__name__ for module in modules]))
|
|
1050
|
+
# assertIn/assertNotIn are super noisy when dealing with the huge sys.modules dict, so instead:
|
|
1051
|
+
if "some_jobs" not in sys.modules:
|
|
1052
|
+
self.fail("Valid module wasn't loaded from JOBS_ROOT")
|
|
1053
|
+
if "my_jobs" not in sys.modules:
|
|
1054
|
+
self.fail("Valid package wasn't loaded from JOBS_ROOT")
|
|
1055
|
+
with self.assertRaises(KeyError, msg="conflicting module name was loaded unsafely from JOBS_ROOT"):
|
|
1056
|
+
sys.modules["tkinter"]
|
|
1057
|
+
with self.assertRaises(KeyError, msg="conflicting package name was loaded unsafely from JOBS_ROOT"):
|
|
1058
|
+
sys.modules["turtle"]
|
|
1059
|
+
|
|
1060
|
+
self.assertEqual(sys.modules["some_jobs"].name, "some_jobs_new")
|
|
1061
|
+
self.assertEqual(sys.modules["my_jobs"].name, "my_jobs_new")
|
|
1062
|
+
self.assertEqual(sys.modules["my_jobs"].some_submodule.name, "my_jobs.some_submodule_new")
|
|
1063
|
+
|
|
1064
|
+
finally:
|
|
1065
|
+
clear_module_from_sys_modules("some_jobs")
|
|
1066
|
+
clear_module_from_sys_modules("my_jobs")
|
|
1067
|
+
|
|
1068
|
+
self.assertNotIn("some_jobs", sys.modules.keys())
|
|
1069
|
+
self.assertNotIn("my_jobs", sys.modules.keys())
|
|
1070
|
+
self.assertNotIn("my_jobs.some_submodule", sys.modules.keys())
|
|
1071
|
+
|
|
1072
|
+
def test_import_modules_privately_git_repo_jobs_case(self):
|
|
1073
|
+
with tempfile.TemporaryDirectory() as tempdir:
|
|
1074
|
+
try:
|
|
1075
|
+
contents = {
|
|
1076
|
+
# Repo that we intend to load
|
|
1077
|
+
"my_repo/__init__.py": 'name = "my_repo"',
|
|
1078
|
+
"my_repo/jobs/__init__.py": '''\
|
|
1079
|
+
import my_repo.jobs.some_jobs
|
|
1080
|
+
from . import some_other_jobs
|
|
1081
|
+
name = "my_repo.jobs"''',
|
|
1082
|
+
"my_repo/jobs/some_jobs.py": 'name = "my_repo.jobs.some_jobs"',
|
|
1083
|
+
"my_repo/jobs/some_other_jobs.py": 'name = "my_repo.jobs.some_other_jobs"',
|
|
1084
|
+
# A separate repo, not intended to be loaded
|
|
1085
|
+
"other_repo/__init__.py": "",
|
|
1086
|
+
# File that shouldn't be loaded as it conflicts
|
|
1087
|
+
"tkinter.py": "",
|
|
1088
|
+
# Package that shouldn't be loaded as it conflicts
|
|
1089
|
+
"turtle/__init__.py": "",
|
|
1090
|
+
}
|
|
1091
|
+
self._create_test_files(tempdir, contents)
|
|
1092
|
+
|
|
1093
|
+
modules = import_modules_privately(tempdir, module_path=["my_repo", "jobs"], ignore_import_errors=False)
|
|
1094
|
+
self.assertEqual(["my_repo", "my_repo.jobs"], sorted([module.__name__ for module in modules]))
|
|
1095
|
+
# assertIn/assertNotIn are super noisy when dealing with the huge sys.modules dict, so instead:
|
|
1096
|
+
if "my_repo" not in sys.modules:
|
|
1097
|
+
self.fail("Valid repo wasn't loaded from GIT_ROOT")
|
|
1098
|
+
if "my_repo.jobs" not in sys.modules:
|
|
1099
|
+
self.fail("Valid repo subdirectory wasn't loaded from GIT_ROOT")
|
|
1100
|
+
with self.assertRaises(KeyError, msg="unexpected repo was loaded from GIT_ROOT"):
|
|
1101
|
+
sys.modules["other_repo"]
|
|
1102
|
+
with self.assertRaises(KeyError, msg="conflicting module name was loaded unsafely from GIT_ROOT"):
|
|
1103
|
+
sys.modules["tkinter"]
|
|
1104
|
+
with self.assertRaises(KeyError, msg="conflicting package name was loaded unsafely from GIT_ROOT"):
|
|
1105
|
+
sys.modules["turtle"]
|
|
1106
|
+
|
|
1107
|
+
self.assertEqual(sys.modules["my_repo"].name, "my_repo")
|
|
1108
|
+
self.assertEqual(sys.modules["my_repo.jobs"].name, "my_repo.jobs")
|
|
1109
|
+
self.assertEqual(sys.modules["my_repo.jobs"].some_jobs.name, "my_repo.jobs.some_jobs")
|
|
1110
|
+
self.assertEqual(sys.modules["my_repo.jobs"].some_other_jobs.name, "my_repo.jobs.some_other_jobs")
|
|
1111
|
+
|
|
1112
|
+
finally:
|
|
1113
|
+
clear_module_from_sys_modules("my_repo")
|
|
1114
|
+
|
|
1115
|
+
self.assertNotIn("my_repo", sys.modules.keys())
|
|
1116
|
+
self.assertNotIn("my_repo.jobs", sys.modules.keys())
|
|
1117
|
+
|
|
1118
|
+
# Test reloading of modules after code changes
|
|
1119
|
+
try:
|
|
1120
|
+
contents["my_repo/__init__.py"] = 'name = "my_repo_new"'
|
|
1121
|
+
contents["my_repo/jobs/__init__.py"] = '''\
|
|
1122
|
+
import my_repo.jobs.some_jobs
|
|
1123
|
+
from . import some_other_jobs
|
|
1124
|
+
name = "my_repo.jobs_new"'''
|
|
1125
|
+
contents["my_repo/jobs/some_jobs.py"] = 'name = "my_repo.jobs.some_jobs_new"'
|
|
1126
|
+
contents["my_repo/jobs/some_other_jobs.py"] = 'name = "my_repo.jobs.some_other_jobs_new"'
|
|
1127
|
+
self._create_test_files(tempdir, contents)
|
|
1128
|
+
|
|
1129
|
+
modules = import_modules_privately(tempdir, module_path=["my_repo", "jobs"], ignore_import_errors=False)
|
|
1130
|
+
self.assertEqual(["my_repo", "my_repo.jobs"], sorted([module.__name__ for module in modules]))
|
|
1131
|
+
# assertIn/assertNotIn are super noisy when dealing with the huge sys.modules dict, so instead:
|
|
1132
|
+
if "my_repo" not in sys.modules:
|
|
1133
|
+
self.fail("Valid repo wasn't loaded from GIT_ROOT")
|
|
1134
|
+
if "my_repo.jobs" not in sys.modules:
|
|
1135
|
+
self.fail("Valid repo subdirectory wasn't loaded from GIT_ROOT")
|
|
1136
|
+
with self.assertRaises(KeyError, msg="unexpected repo was loaded from GIT_ROOT"):
|
|
1137
|
+
sys.modules["other_repo"]
|
|
1138
|
+
with self.assertRaises(KeyError, msg="conflicting module name was loaded unsafely from GIT_ROOT"):
|
|
1139
|
+
sys.modules["tkinter"]
|
|
1140
|
+
with self.assertRaises(KeyError, msg="conflicting package name was loaded unsafely from GIT_ROOT"):
|
|
1141
|
+
sys.modules["turtle"]
|
|
1142
|
+
|
|
1143
|
+
self.assertEqual(sys.modules["my_repo"].name, "my_repo_new")
|
|
1144
|
+
self.assertEqual(sys.modules["my_repo.jobs"].name, "my_repo.jobs_new")
|
|
1145
|
+
self.assertEqual(sys.modules["my_repo.jobs"].some_jobs.name, "my_repo.jobs.some_jobs_new")
|
|
1146
|
+
self.assertEqual(sys.modules["my_repo.jobs"].some_other_jobs.name, "my_repo.jobs.some_other_jobs_new")
|
|
1147
|
+
|
|
1148
|
+
finally:
|
|
1149
|
+
clear_module_from_sys_modules("my_repo")
|
|
1150
|
+
|
|
1151
|
+
self.assertNotIn("my_repo", sys.modules.keys())
|
|
1152
|
+
self.assertNotIn("my_repo.jobs", sys.modules.keys())
|
|
1153
|
+
|
|
1154
|
+
|
|
962
1155
|
class TestQuerySetUtils(TestCase):
|
|
963
1156
|
def test_maybe_select_related(self):
|
|
964
1157
|
# If possible, select_related should be called
|
|
@@ -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):
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import urllib.parse
|
|
2
2
|
|
|
3
|
+
from django.contrib.auth.models import AnonymousUser
|
|
3
4
|
from django.db import ProgrammingError
|
|
4
5
|
from django.test import TestCase
|
|
5
6
|
|
|
6
7
|
from nautobot.core.models.querysets import count_related
|
|
7
|
-
from nautobot.core.
|
|
8
|
+
from nautobot.core.testing import TransactionTestCase
|
|
9
|
+
from nautobot.core.views.utils import check_filter_for_display, get_saved_views_for_user, prepare_cloned_fields
|
|
8
10
|
from nautobot.dcim.filters import DeviceFilterSet
|
|
9
11
|
from nautobot.dcim.models import Device, DeviceRedundancyGroup, DeviceType, InventoryItem, Location, Manufacturer
|
|
10
|
-
from nautobot.extras.models import Role, Status
|
|
12
|
+
from nautobot.extras.models import Role, SavedView, Status
|
|
13
|
+
from nautobot.users.models import User
|
|
11
14
|
|
|
12
15
|
|
|
13
16
|
class CheckFilterForDisplayTest(TestCase):
|
|
@@ -168,3 +171,51 @@ class CheckPrepareClonedFields(TestCase):
|
|
|
168
171
|
self.assertTrue(isinstance(query_params["description"], list))
|
|
169
172
|
self.assertTrue(len(query_params["description"]) == 1)
|
|
170
173
|
self.assertTrue(query_params["description"][0] == description)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class GetSavedViewsForUserTestCase(TransactionTestCase):
|
|
177
|
+
"""
|
|
178
|
+
Class to test `get_saved_views_for_user`.
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
def create_saved_view(self, name, owner=None, is_shared=False):
|
|
182
|
+
"""Helper to create a SavedView."""
|
|
183
|
+
return SavedView.objects.create(
|
|
184
|
+
name=name, owner=owner or self.user, view="dcim:device_list", is_shared=is_shared
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
def setUp(self):
|
|
188
|
+
super().setUp()
|
|
189
|
+
self.user2 = User.objects.create_user(username="second_user")
|
|
190
|
+
self.create_saved_view(name="saved_view")
|
|
191
|
+
self.create_saved_view(name="saved_view_shared", is_shared=True)
|
|
192
|
+
self.create_saved_view(name="saved_view_different_owner", owner=self.user2)
|
|
193
|
+
self.create_saved_view(name="saved_view_shared_different_owner", is_shared=True, owner=self.user2)
|
|
194
|
+
|
|
195
|
+
def test_user_with_permissions_get_all_saved_views(self):
|
|
196
|
+
"""Test if for user with permissions method will return all saved views."""
|
|
197
|
+
self.add_permissions("extras.view_savedview")
|
|
198
|
+
saved_views = get_saved_views_for_user(self.user, "dcim:device_list")
|
|
199
|
+
self.assertEqual(saved_views.count(), 4)
|
|
200
|
+
expected_names = [
|
|
201
|
+
"saved_view",
|
|
202
|
+
"saved_view_different_owner",
|
|
203
|
+
"saved_view_shared",
|
|
204
|
+
"saved_view_shared_different_owner",
|
|
205
|
+
]
|
|
206
|
+
self.assertEqual(list(saved_views.values_list("name", flat=True)), expected_names)
|
|
207
|
+
|
|
208
|
+
def test_user_without_permissions_get_shared_views_and_own_views_only(self):
|
|
209
|
+
"""Test if user without permissions can see shared views and own views."""
|
|
210
|
+
saved_views = get_saved_views_for_user(self.user, "dcim:device_list")
|
|
211
|
+
self.assertEqual(saved_views.count(), 3)
|
|
212
|
+
expected_names = ["saved_view", "saved_view_shared", "saved_view_shared_different_owner"]
|
|
213
|
+
self.assertEqual(list(saved_views.values_list("name", flat=True)), expected_names)
|
|
214
|
+
|
|
215
|
+
def test_anonymous_user_get_shared_views_only(self):
|
|
216
|
+
"""Test if method is working with anonymous users and return only shared views."""
|
|
217
|
+
user = AnonymousUser()
|
|
218
|
+
saved_views = get_saved_views_for_user(user, "dcim:device_list")
|
|
219
|
+
self.assertEqual(saved_views.count(), 2)
|
|
220
|
+
expected_names = ["saved_view_shared", "saved_view_shared_different_owner"]
|
|
221
|
+
self.assertEqual(list(saved_views.values_list("name", flat=True)), expected_names)
|
|
@@ -1266,6 +1266,10 @@ class ObjectFieldsPanel(KeyValueTablePanel):
|
|
|
1266
1266
|
|
|
1267
1267
|
data[field_name] = field_value
|
|
1268
1268
|
|
|
1269
|
+
# Ensuring the `name` field is displayed first, if present.
|
|
1270
|
+
if "name" in data:
|
|
1271
|
+
data = {"name": data["name"], **{k: v for k, v in data.items() if k != "name"}}
|
|
1272
|
+
|
|
1269
1273
|
return data
|
|
1270
1274
|
|
|
1271
1275
|
def render_key(self, key, value, context: 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/lookup.py
CHANGED
|
@@ -314,13 +314,15 @@ def get_created_and_last_updated_usernames_for_model(instance):
|
|
|
314
314
|
created_by = None
|
|
315
315
|
last_updated_by = None
|
|
316
316
|
try:
|
|
317
|
-
created_by_record =
|
|
317
|
+
created_by_record = (
|
|
318
|
+
object_change_records.filter(action=ObjectChangeActionChoices.ACTION_CREATE).only("user_name").first()
|
|
319
|
+
)
|
|
318
320
|
if created_by_record is not None:
|
|
319
321
|
created_by = created_by_record.user_name
|
|
320
322
|
except ObjectChange.DoesNotExist:
|
|
321
323
|
pass
|
|
322
324
|
|
|
323
|
-
last_updated_by_record = object_change_records.first()
|
|
325
|
+
last_updated_by_record = object_change_records.only("user_name").first()
|
|
324
326
|
if last_updated_by_record:
|
|
325
327
|
last_updated_by = last_updated_by_record.user_name
|
|
326
328
|
|