nautobot 2.4.10__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/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/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_jobs.py +20 -4
- nautobot/core/tests/test_utils.py +193 -0
- nautobot/core/tests/test_views_utils.py +53 -2
- nautobot/core/ui/object_detail.py +4 -0
- nautobot/core/utils/lookup.py +4 -2
- nautobot/core/utils/module_loading.py +86 -58
- 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 +14 -0
- nautobot/dcim/models/device_components.py +13 -1
- nautobot/dcim/models/devices.py +62 -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/datasources/git.py +11 -3
- nautobot/extras/forms/forms.py +9 -5
- nautobot/extras/jobs.py +4 -2
- nautobot/extras/models/datasources.py +5 -8
- nautobot/extras/models/jobs.py +5 -0
- nautobot/extras/plugins/__init__.py +3 -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_filters.py +4 -4
- nautobot/extras/tests/test_jobs.py +23 -10
- nautobot/extras/tests/test_models.py +19 -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 +19 -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 +32 -9
- 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 +31 -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 +252 -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 -8
- nautobot/project-static/docs/user-guide/administration/security/notices.html +31 -8
- 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/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.10.dist-info → nautobot-2.4.11.dist-info}/METADATA +4 -4
- {nautobot-2.4.10.dist-info → nautobot-2.4.11.dist-info}/RECORD +416 -401
- /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.10.dist-info → nautobot-2.4.11.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.4.10.dist-info → nautobot-2.4.11.dist-info}/NOTICE +0 -0
- {nautobot-2.4.10.dist-info → nautobot-2.4.11.dist-info}/WHEEL +0 -0
- {nautobot-2.4.10.dist-info → nautobot-2.4.11.dist-info}/entry_points.txt +0 -0
|
@@ -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,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/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
|
|
|
@@ -1,29 +1,13 @@
|
|
|
1
|
-
from
|
|
2
|
-
import
|
|
3
|
-
from
|
|
1
|
+
from importlib.machinery import FileFinder, SOURCE_SUFFIXES, SourceFileLoader
|
|
2
|
+
from importlib.util import module_from_spec
|
|
3
|
+
from keyword import iskeyword
|
|
4
4
|
import logging
|
|
5
|
-
import os
|
|
6
5
|
import pkgutil
|
|
7
6
|
import sys
|
|
8
7
|
|
|
9
8
|
logger = logging.getLogger(__name__)
|
|
10
9
|
|
|
11
10
|
|
|
12
|
-
@contextmanager
|
|
13
|
-
def _temporarily_add_to_sys_path(path):
|
|
14
|
-
"""
|
|
15
|
-
Allow loading of modules and packages from within the provided directory by temporarily modifying `sys.path`.
|
|
16
|
-
|
|
17
|
-
On exit, it restores the original `sys.path` value.
|
|
18
|
-
"""
|
|
19
|
-
old_sys_path = sys.path.copy()
|
|
20
|
-
sys.path.insert(0, path)
|
|
21
|
-
try:
|
|
22
|
-
yield
|
|
23
|
-
finally:
|
|
24
|
-
sys.path = old_sys_path
|
|
25
|
-
|
|
26
|
-
|
|
27
11
|
def clear_module_from_sys_modules(module_name):
|
|
28
12
|
"""
|
|
29
13
|
Remove the module and all its submodules from sys.modules.
|
|
@@ -33,7 +17,29 @@ def clear_module_from_sys_modules(module_name):
|
|
|
33
17
|
del sys.modules[name]
|
|
34
18
|
|
|
35
19
|
|
|
36
|
-
def
|
|
20
|
+
def check_name_safe_to_import_privately(name: str) -> tuple[bool, str]:
|
|
21
|
+
"""
|
|
22
|
+
Make sure the given package/module name is "safe" to import from the filesystem.
|
|
23
|
+
|
|
24
|
+
In other words, make sure it's:
|
|
25
|
+
- a valid Python identifier and not a reserved keyword
|
|
26
|
+
- not the name of an existing "real" Python package or builtin
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
(bool, str): Whether safe to load, and an explanatory string fragment for logging/exception messages.
|
|
30
|
+
"""
|
|
31
|
+
if not name.isidentifier():
|
|
32
|
+
return False, "not a valid identifier"
|
|
33
|
+
if iskeyword(name):
|
|
34
|
+
return False, "a reserved keyword"
|
|
35
|
+
if name in sys.builtin_module_names:
|
|
36
|
+
return False, "a Python builtin"
|
|
37
|
+
if any(module_info.name == name for module_info in pkgutil.iter_modules()):
|
|
38
|
+
return False, "the name of an installed Python package"
|
|
39
|
+
return True, "a valid and non-conflicting module name"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def import_modules_privately(path, module_path=None, module_prefix="", ignore_import_errors=True):
|
|
37
43
|
"""
|
|
38
44
|
Import modules from the filesystem without adding the path permanently to `sys.path`.
|
|
39
45
|
|
|
@@ -49,49 +55,71 @@ def import_modules_privately(path, module_path=None, ignore_import_errors=True):
|
|
|
49
55
|
ignore_import_errors (bool): Exceptions raised while importing modules will be caught and logged.
|
|
50
56
|
If this is set as False, they will then be re-raised to be handled by the caller of this function.
|
|
51
57
|
"""
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
58
|
+
loaded_modules = []
|
|
59
|
+
# We formerly used pkgutil.walk_packages() here to handle submodule loading with a multi-entry module_path,
|
|
60
|
+
# but that has the downside (and risk!) of automatically importing all packages that it finds in the given path,
|
|
61
|
+
# whether or not we actually want to do so. So instead, for the case where a module_path is provided, we need to
|
|
62
|
+
# iteratively import each submodule ourselves.
|
|
63
|
+
if module_path:
|
|
64
|
+
# Git repository case - e.g. import_modules_privately(settings.GIT_ROOT, module_path=[repository_slug, "jobs"])
|
|
65
|
+
# Here we want to ONLY auto-load the module sequence identified by module_path.
|
|
66
|
+
permitted, reason = check_name_safe_to_import_privately(module_path[0])
|
|
67
|
+
if not permitted:
|
|
68
|
+
logger.error("Unable to load module %r from %s as it is %s", module_path[0], path, reason)
|
|
69
|
+
else:
|
|
70
|
+
module = None
|
|
71
|
+
module_name = module_path.pop(0)
|
|
72
|
+
submodule_name = module_name
|
|
73
|
+
try:
|
|
74
|
+
while True:
|
|
75
|
+
finder = FileFinder(path, (SourceFileLoader, SOURCE_SUFFIXES))
|
|
76
|
+
finder.invalidate_caches()
|
|
77
|
+
spec = finder.find_spec(module_name)
|
|
78
|
+
if spec is None or spec.loader is None:
|
|
79
|
+
logger.error("Unable to find module spec and/or loader for %r", submodule_name)
|
|
80
|
+
break
|
|
81
|
+
spec.name = submodule_name
|
|
82
|
+
spec.loader.name = submodule_name
|
|
83
|
+
submodule = module_from_spec(spec)
|
|
84
|
+
sys.modules[submodule_name] = submodule
|
|
85
|
+
spec.loader.exec_module(submodule)
|
|
86
|
+
if module is not None:
|
|
87
|
+
setattr(module, module_name, submodule)
|
|
88
|
+
module = submodule
|
|
89
|
+
loaded_modules.append(module)
|
|
90
|
+
if module_path:
|
|
91
|
+
submodule_name = f"{module_name}.{module_path[0]}"
|
|
92
|
+
module_name = module_path.pop(0)
|
|
93
|
+
path = module.__path__[0]
|
|
94
|
+
else:
|
|
95
|
+
break
|
|
96
|
+
except Exception as exc:
|
|
97
|
+
logger.error("Unable to load module %s from %s: %s", module_name, path, exc)
|
|
98
|
+
if not ignore_import_errors:
|
|
99
|
+
raise
|
|
55
100
|
else:
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
for finder, discovered_module_name,
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
or discovered_module_name.startswith(f"{module_prefix}.") # my_repo/jobs/foobar.py
|
|
63
|
-
):
|
|
101
|
+
# JOBS_ROOT case - import ALL top-level modules/packages that we can find in the given path;
|
|
102
|
+
# they can implement and import submodules as desired by themselves, but we only autoimport top-level ones.
|
|
103
|
+
for finder, discovered_module_name, _ in pkgutil.iter_modules([path]):
|
|
104
|
+
permitted, reason = check_name_safe_to_import_privately(discovered_module_name)
|
|
105
|
+
if not permitted:
|
|
106
|
+
logger.error("Unable to load module %r from %s as it is %s", discovered_module_name, path, reason)
|
|
64
107
|
continue
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
existing_module = None
|
|
69
|
-
if existing_module is not None:
|
|
70
|
-
existing_module_path = os.path.realpath(existing_module.origin)
|
|
71
|
-
if not existing_module_path.startswith(path):
|
|
72
|
-
logger.error(
|
|
73
|
-
"Unable to load module %s from %s as it conflicts with existing module %s",
|
|
74
|
-
discovered_module_name,
|
|
75
|
-
path,
|
|
76
|
-
existing_module_path,
|
|
77
|
-
)
|
|
78
|
-
continue
|
|
79
|
-
|
|
80
|
-
if discovered_module_name in sys.modules:
|
|
81
|
-
clear_module_from_sys_modules(discovered_module_name)
|
|
108
|
+
module_name = discovered_module_name
|
|
109
|
+
if module_name in sys.modules:
|
|
110
|
+
clear_module_from_sys_modules(module_name)
|
|
82
111
|
|
|
83
112
|
try:
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
module = importlib.import_module(discovered_module_name)
|
|
93
|
-
importlib.reload(module)
|
|
113
|
+
spec = finder.find_spec(discovered_module_name)
|
|
114
|
+
if spec is None or spec.loader is None:
|
|
115
|
+
logger.error("Unable to find module spec and/or loader for %r", discovered_module_name)
|
|
116
|
+
continue
|
|
117
|
+
module = module_from_spec(spec)
|
|
118
|
+
sys.modules[module_name] = module
|
|
119
|
+
spec.loader.exec_module(module)
|
|
120
|
+
loaded_modules.append(module)
|
|
94
121
|
except Exception as exc:
|
|
95
122
|
logger.error("Unable to load module %s from %s: %s", discovered_module_name, path, exc)
|
|
96
123
|
if not ignore_import_errors:
|
|
97
124
|
raise
|
|
125
|
+
return loaded_modules
|
nautobot/core/views/generic.py
CHANGED
|
@@ -54,6 +54,7 @@ from nautobot.core.views.utils import (
|
|
|
54
54
|
check_filter_for_display,
|
|
55
55
|
common_detail_view_context,
|
|
56
56
|
get_csv_form_fields_from_serializer_class,
|
|
57
|
+
get_saved_views_for_user,
|
|
57
58
|
handle_protectederror,
|
|
58
59
|
import_csv_helper,
|
|
59
60
|
prepare_cloned_fields,
|
|
@@ -315,18 +316,7 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
|
|
|
315
316
|
table_config_form = None
|
|
316
317
|
current_saved_view = None
|
|
317
318
|
current_saved_view_pk = self.request.GET.get("saved_view", None)
|
|
318
|
-
|
|
319
|
-
# User should be able to see any saved view that he has the list view access to.
|
|
320
|
-
if user.has_perms(["extras.view_savedview"]):
|
|
321
|
-
saved_views = SavedView.objects.filter(view=list_url).order_by("name").only("pk", "name")
|
|
322
|
-
else:
|
|
323
|
-
shared_saved_views = (
|
|
324
|
-
SavedView.objects.filter(view=list_url, is_shared=True).order_by("name").only("pk", "name")
|
|
325
|
-
)
|
|
326
|
-
user_owned_saved_views = (
|
|
327
|
-
SavedView.objects.filter(view=list_url, owner=user).order_by("name").only("pk", "name")
|
|
328
|
-
)
|
|
329
|
-
saved_views = shared_saved_views | user_owned_saved_views
|
|
319
|
+
saved_views = get_saved_views_for_user(user, list_url)
|
|
330
320
|
|
|
331
321
|
if current_saved_view_pk:
|
|
332
322
|
try:
|
nautobot/core/views/mixins.py
CHANGED
|
@@ -239,6 +239,9 @@ class NautobotViewSetMixin(GenericViewSet, AccessMixin, GetReturnURLMixin, FormV
|
|
|
239
239
|
table_class = None
|
|
240
240
|
notes_form_class = NoteForm
|
|
241
241
|
permission_classes = []
|
|
242
|
+
# custom view attributes used for permission checks and handling
|
|
243
|
+
custom_view_base_action = None
|
|
244
|
+
custom_view_additional_permissions = None
|
|
242
245
|
|
|
243
246
|
def get_permissions_for_model(self, model, actions):
|
|
244
247
|
"""
|
|
@@ -249,6 +252,10 @@ class NautobotViewSetMixin(GenericViewSet, AccessMixin, GetReturnURLMixin, FormV
|
|
|
249
252
|
"""
|
|
250
253
|
model_permissions = []
|
|
251
254
|
for action in actions:
|
|
255
|
+
# Append additional object permissions if specified.
|
|
256
|
+
if self.custom_view_additional_permissions:
|
|
257
|
+
model_permissions.append(*self.custom_view_additional_permissions)
|
|
258
|
+
# Append the model-level permissions for the action.
|
|
252
259
|
model_permissions.append(f"{model._meta.app_label}.{action}_{model._meta.model_name}")
|
|
253
260
|
return model_permissions
|
|
254
261
|
|
|
@@ -501,7 +508,13 @@ class NautobotViewSetMixin(GenericViewSet, AccessMixin, GetReturnURLMixin, FormV
|
|
|
501
508
|
|
|
502
509
|
def get_action(self):
|
|
503
510
|
"""Helper method for retrieving action and if action not set defaulting to action name."""
|
|
504
|
-
|
|
511
|
+
if self.custom_view_base_action:
|
|
512
|
+
return self.custom_view_base_action
|
|
513
|
+
if self.action in PERMISSIONS_ACTION_MAP:
|
|
514
|
+
# If the action is in the action_map, return the mapped permission
|
|
515
|
+
return PERMISSIONS_ACTION_MAP[self.action]
|
|
516
|
+
|
|
517
|
+
return self.action
|
|
505
518
|
|
|
506
519
|
def get_extra_context(self, request, instance=None):
|
|
507
520
|
"""
|
|
@@ -1032,6 +1045,11 @@ class BulkEditAndBulkDeleteModelMixin:
|
|
|
1032
1045
|
|
|
1033
1046
|
filter_query_params = new_filter_query_params
|
|
1034
1047
|
|
|
1048
|
+
if nullified_fields := request.POST.getlist("_nullify"):
|
|
1049
|
+
form_data["_nullify"] = nullified_fields
|
|
1050
|
+
else:
|
|
1051
|
+
form_data["_nullify"] = []
|
|
1052
|
+
|
|
1035
1053
|
job_form = BulkEditObjects.as_form(
|
|
1036
1054
|
data={
|
|
1037
1055
|
"form_data": form_data,
|
nautobot/core/views/renderers.py
CHANGED
|
@@ -28,6 +28,7 @@ from nautobot.core.views.utils import (
|
|
|
28
28
|
check_filter_for_display,
|
|
29
29
|
common_detail_view_context,
|
|
30
30
|
get_csv_form_fields_from_serializer_class,
|
|
31
|
+
get_saved_views_for_user,
|
|
31
32
|
view_changes_not_saved,
|
|
32
33
|
)
|
|
33
34
|
from nautobot.extras.models import SavedView
|
|
@@ -235,7 +236,8 @@ class NautobotHTMLRenderer(renderers.BrowsableAPIRenderer):
|
|
|
235
236
|
if view.filterset_form_class is not None:
|
|
236
237
|
filter_form = view.filterset_form_class(view.filter_params, label_suffix="")
|
|
237
238
|
table = self.construct_table(view, request=request, permissions=permissions)
|
|
238
|
-
|
|
239
|
+
q_placeholder = "Search " + bettertitle(model._meta.verbose_name_plural)
|
|
240
|
+
search_form = SearchForm(data=view.filter_params, q_placeholder=q_placeholder)
|
|
239
241
|
elif view.action == "destroy":
|
|
240
242
|
form = form_class(initial=request.GET)
|
|
241
243
|
elif view.action in ["create", "update"]:
|
|
@@ -309,18 +311,7 @@ class NautobotHTMLRenderer(renderers.BrowsableAPIRenderer):
|
|
|
309
311
|
list_url = f"{resolved_path.app_name}:{resolved_path.url_name}"
|
|
310
312
|
saved_views = None
|
|
311
313
|
if model.is_saved_view_model:
|
|
312
|
-
|
|
313
|
-
# User should be able to see any saved view that he has the list view access to.
|
|
314
|
-
if request.user.has_perms(["extras.view_savedview"]):
|
|
315
|
-
saved_views = SavedView.objects.filter(view=list_url).order_by("name").only("pk", "name")
|
|
316
|
-
else:
|
|
317
|
-
shared_saved_views = (
|
|
318
|
-
SavedView.objects.filter(view=list_url, is_shared=True).order_by("name").only("pk", "name")
|
|
319
|
-
)
|
|
320
|
-
user_owned_saved_views = (
|
|
321
|
-
SavedView.objects.filter(view=list_url, owner=request.user).order_by("name").only("pk", "name")
|
|
322
|
-
)
|
|
323
|
-
saved_views = shared_saved_views | user_owned_saved_views
|
|
314
|
+
saved_views = get_saved_views_for_user(request.user, list_url)
|
|
324
315
|
|
|
325
316
|
new_changes_not_applied = view_changes_not_saved(request, view, self.saved_view)
|
|
326
317
|
context.update(
|
nautobot/core/views/utils.py
CHANGED
|
@@ -17,6 +17,7 @@ from nautobot.core.utils.data import is_uuid
|
|
|
17
17
|
from nautobot.core.utils.filtering import get_filter_field_label
|
|
18
18
|
from nautobot.core.utils.lookup import get_created_and_last_updated_usernames_for_model, get_form_for_model
|
|
19
19
|
from nautobot.core.views.paginator import EnhancedPaginator, get_paginate_count
|
|
20
|
+
from nautobot.extras.models import SavedView
|
|
20
21
|
from nautobot.extras.tables import AssociatedContactsTable, DynamicGroupTable, ObjectMetadataTable
|
|
21
22
|
|
|
22
23
|
|
|
@@ -382,3 +383,18 @@ def common_detail_view_context(request, instance):
|
|
|
382
383
|
context["associated_object_metadata_table"] = None
|
|
383
384
|
|
|
384
385
|
return context
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def get_saved_views_for_user(user, list_url):
|
|
389
|
+
# We are not using .restrict(request.user, "view") here
|
|
390
|
+
# User should be able to see any saved view that he has the list view access to.
|
|
391
|
+
saved_views = SavedView.objects.filter(view=list_url).order_by("name").only("pk", "name")
|
|
392
|
+
if user.has_perms(["extras.view_savedview"]):
|
|
393
|
+
return saved_views
|
|
394
|
+
|
|
395
|
+
shared_saved_views = saved_views.filter(is_shared=True)
|
|
396
|
+
if user.is_authenticated:
|
|
397
|
+
user_owned_saved_views = SavedView.objects.filter(view=list_url, owner=user).order_by("name").only("pk", "name")
|
|
398
|
+
return shared_saved_views | user_owned_saved_views
|
|
399
|
+
|
|
400
|
+
return shared_saved_views
|
nautobot/dcim/api/serializers.py
CHANGED
|
@@ -79,6 +79,7 @@ from nautobot.dcim.models import (
|
|
|
79
79
|
Module,
|
|
80
80
|
ModuleBay,
|
|
81
81
|
ModuleBayTemplate,
|
|
82
|
+
ModuleFamily,
|
|
82
83
|
ModuleType,
|
|
83
84
|
PathEndpoint,
|
|
84
85
|
Platform,
|
|
@@ -1124,3 +1125,15 @@ class InterfaceVDCAssignmentSerializer(ValidatedModelSerializer):
|
|
|
1124
1125
|
class Meta:
|
|
1125
1126
|
model = InterfaceVDCAssignment
|
|
1126
1127
|
fields = "__all__"
|
|
1128
|
+
|
|
1129
|
+
|
|
1130
|
+
class ModuleFamilySerializer(NautobotModelSerializer):
|
|
1131
|
+
"""API serializer for ModuleFamily objects."""
|
|
1132
|
+
|
|
1133
|
+
url = serializers.HyperlinkedIdentityField(view_name="dcim-api:modulefamily-detail")
|
|
1134
|
+
module_type_count = serializers.IntegerField(read_only=True)
|
|
1135
|
+
module_bay_count = serializers.IntegerField(read_only=True)
|
|
1136
|
+
|
|
1137
|
+
class Meta:
|
|
1138
|
+
model = ModuleFamily
|
|
1139
|
+
fields = "__all__"
|