nautobot 2.4.10__py3-none-any.whl → 2.4.12__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of nautobot might be problematic. Click here for more details.
- nautobot/circuits/templates/circuits/circuittermination_retrieve.html +1 -114
- nautobot/circuits/templates/circuits/inc/circuit_termination_header_extra_content.html +1 -1
- nautobot/circuits/views.py +76 -6
- nautobot/cloud/navigation.py +1 -1
- 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/components/panel/header_extra_content_table.html +12 -1
- nautobot/core/templates/components/panel/panel.html +4 -0
- nautobot/core/templates/generic/object_retrieve.html +2 -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 +17 -0
- nautobot/core/tests/test_views.py +2 -2
- nautobot/core/tests/test_views_utils.py +53 -2
- nautobot/core/ui/object_detail.py +5 -1
- nautobot/core/utils/lookup.py +4 -2
- nautobot/core/utils/module_loading.py +23 -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 +116 -0
- nautobot/dcim/filters/mixins.py +2 -1
- 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 +72 -0
- nautobot/dcim/navigation.py +16 -0
- nautobot/dcim/tables/__init__.py +2 -0
- nautobot/dcim/tables/devices.py +50 -0
- nautobot/dcim/tables/devicetypes.py +35 -1
- nautobot/dcim/tables/template_code.py +2 -0
- nautobot/dcim/templates/dcim/controller/base.html +1 -9
- nautobot/dcim/templates/dcim/controller_retrieve.html +2 -83
- nautobot/dcim/templates/dcim/controller_wirelessnetworks.html +2 -25
- 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/templates/dcim/virtualchassis.html +2 -51
- nautobot/dcim/templates/dcim/virtualchassis_add.html +2 -22
- nautobot/dcim/templates/dcim/virtualchassis_create.html +22 -0
- nautobot/dcim/templates/dcim/virtualchassis_edit.html +2 -93
- nautobot/dcim/templates/dcim/virtualchassis_retrieve.html +51 -0
- nautobot/dcim/templates/dcim/virtualchassis_update.html +93 -0
- 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 +5 -80
- nautobot/dcim/views.py +584 -342
- nautobot/extras/api/views.py +9 -2
- nautobot/extras/datasources/git.py +9 -1
- nautobot/extras/forms/forms.py +9 -5
- nautobot/extras/jobs.py +4 -2
- nautobot/extras/jobs_ui.py +208 -0
- nautobot/extras/models/datasources.py +5 -8
- nautobot/extras/models/jobs.py +5 -0
- nautobot/extras/models/tags.py +4 -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/configcontext_format.html +6 -0
- nautobot/extras/templates/extras/job_detail.html +1 -326
- 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 +2 -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 +186 -178
- 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 +33 -10
- nautobot/project-static/docs/apps/index.html +33 -10
- nautobot/project-static/docs/apps/nautobot-apps.html +33 -10
- nautobot/project-static/docs/assets/javascripts/{bundle.13a4f30d.min.js → bundle.56ea9cef.min.js} +2 -2
- nautobot/project-static/docs/assets/javascripts/{bundle.13a4f30d.min.js.map → bundle.56ea9cef.min.js.map} +2 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +33 -10
- nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +33 -10
- nautobot/project-static/docs/code-reference/nautobot/apps/api.html +33 -10
- nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +33 -10
- nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +33 -10
- nautobot/project-static/docs/code-reference/nautobot/apps/config.html +33 -10
- nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +33 -10
- nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +33 -10
- nautobot/project-static/docs/code-reference/nautobot/apps/events.html +33 -10
- nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +33 -10
- nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +33 -10
- nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +33 -10
- nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +33 -10
- nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +33 -10
- nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +33 -10
- nautobot/project-static/docs/code-reference/nautobot/apps/models.html +33 -10
- nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +33 -10
- nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +33 -10
- nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +33 -10
- nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +122 -10
- nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +33 -10
- nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +33 -10
- nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +33 -10
- nautobot/project-static/docs/code-reference/nautobot/apps/views.html +33 -10
- nautobot/project-static/docs/development/apps/api/configuration-view.html +33 -10
- nautobot/project-static/docs/development/apps/api/database-backend-config.html +33 -10
- nautobot/project-static/docs/development/apps/api/models/django-admin.html +33 -10
- nautobot/project-static/docs/development/apps/api/models/global-search.html +33 -10
- nautobot/project-static/docs/development/apps/api/models/graphql.html +33 -10
- nautobot/project-static/docs/development/apps/api/models/index.html +33 -10
- nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +42 -10
- nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +33 -10
- nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +33 -10
- nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +33 -10
- nautobot/project-static/docs/development/apps/api/platform-features/index.html +33 -10
- nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +33 -10
- nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +33 -10
- nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +33 -10
- nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +34 -11
- nautobot/project-static/docs/development/apps/api/platform-features/table-extensions.html +33 -10
- nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +33 -10
- nautobot/project-static/docs/development/apps/api/prometheus.html +33 -10
- nautobot/project-static/docs/development/apps/api/setup.html +33 -10
- nautobot/project-static/docs/development/apps/api/testing.html +33 -10
- nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +33 -10
- nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +33 -10
- nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +33 -10
- nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +33 -10
- nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +33 -10
- nautobot/project-static/docs/development/apps/api/views/base-template.html +33 -10
- nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +33 -10
- nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +33 -10
- nautobot/project-static/docs/development/apps/api/views/help-documentation.html +33 -10
- nautobot/project-static/docs/development/apps/api/views/index.html +33 -10
- nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +33 -10
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +33 -10
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +33 -10
- nautobot/project-static/docs/development/apps/api/views/notes.html +33 -10
- nautobot/project-static/docs/development/apps/api/views/rest-api.html +33 -10
- nautobot/project-static/docs/development/apps/api/views/urls.html +33 -10
- nautobot/project-static/docs/development/apps/index.html +33 -10
- nautobot/project-static/docs/development/apps/migration/code-updates.html +33 -10
- nautobot/project-static/docs/development/apps/migration/dependency-updates.html +33 -10
- nautobot/project-static/docs/development/apps/migration/from-v1.html +33 -10
- nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +33 -10
- nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +33 -10
- nautobot/project-static/docs/development/apps/migration/model-updates/global.html +33 -10
- nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +33 -10
- nautobot/project-static/docs/development/apps/migration/ui-component-framework/best-practices.html +33 -10
- nautobot/project-static/docs/development/apps/migration/ui-component-framework/custom-content.html +33 -10
- nautobot/project-static/docs/development/apps/migration/ui-component-framework/index.html +33 -10
- nautobot/project-static/docs/development/apps/migration/ui-component-framework/migration-steps.html +33 -10
- nautobot/project-static/docs/development/apps/porting-from-netbox.html +33 -10
- nautobot/project-static/docs/development/core/application-registry.html +33 -10
- nautobot/project-static/docs/development/core/best-practices.html +33 -10
- nautobot/project-static/docs/development/core/bootstrap-ui.html +33 -10
- nautobot/project-static/docs/development/core/caching.html +33 -10
- nautobot/project-static/docs/development/core/controllers.html +33 -10
- nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +33 -10
- nautobot/project-static/docs/development/core/generic-views.html +33 -10
- nautobot/project-static/docs/development/core/getting-started.html +33 -10
- nautobot/project-static/docs/development/core/homepage.html +33 -10
- nautobot/project-static/docs/development/core/index.html +33 -10
- nautobot/project-static/docs/development/core/minikube-dev-environment-for-k8s-jobs.html +33 -10
- nautobot/project-static/docs/development/core/model-checklist.html +33 -10
- nautobot/project-static/docs/development/core/model-features.html +33 -10
- nautobot/project-static/docs/development/core/natural-keys.html +33 -10
- nautobot/project-static/docs/development/core/navigation-menu.html +33 -10
- nautobot/project-static/docs/development/core/release-checklist.html +33 -10
- nautobot/project-static/docs/development/core/role-internals.html +33 -10
- nautobot/project-static/docs/development/core/settings.html +33 -10
- nautobot/project-static/docs/development/core/style-guide.html +33 -10
- nautobot/project-static/docs/development/core/templates.html +33 -10
- nautobot/project-static/docs/development/core/testing.html +33 -10
- nautobot/project-static/docs/development/core/ui-component-framework.html +33 -10
- nautobot/project-static/docs/development/core/user-preferences.html +33 -10
- nautobot/project-static/docs/development/index.html +33 -10
- nautobot/project-static/docs/development/jobs/getting-started.html +33 -10
- nautobot/project-static/docs/development/jobs/index.html +33 -10
- nautobot/project-static/docs/development/jobs/installation.html +33 -10
- nautobot/project-static/docs/development/jobs/job-extensions.html +33 -10
- nautobot/project-static/docs/development/jobs/job-logging.html +33 -10
- nautobot/project-static/docs/development/jobs/job-patterns.html +33 -10
- nautobot/project-static/docs/development/jobs/job-structure.html +33 -10
- nautobot/project-static/docs/development/jobs/migration/from-v1.html +33 -10
- nautobot/project-static/docs/development/jobs/testing.html +33 -10
- nautobot/project-static/docs/index.html +33 -10
- 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 +33 -10
- nautobot/project-static/docs/overview/design_philosophy.html +33 -10
- nautobot/project-static/docs/release-notes/index.html +33 -10
- nautobot/project-static/docs/release-notes/version-1.0.html +33 -10
- nautobot/project-static/docs/release-notes/version-1.1.html +33 -10
- nautobot/project-static/docs/release-notes/version-1.2.html +33 -10
- nautobot/project-static/docs/release-notes/version-1.3.html +33 -10
- nautobot/project-static/docs/release-notes/version-1.4.html +33 -10
- nautobot/project-static/docs/release-notes/version-1.5.html +33 -10
- nautobot/project-static/docs/release-notes/version-1.6.html +33 -10
- nautobot/project-static/docs/release-notes/version-2.0.html +33 -10
- nautobot/project-static/docs/release-notes/version-2.1.html +33 -10
- nautobot/project-static/docs/release-notes/version-2.2.html +33 -10
- nautobot/project-static/docs/release-notes/version-2.3.html +33 -10
- nautobot/project-static/docs/release-notes/version-2.4.html +363 -10
- nautobot/project-static/docs/requirements.txt +1 -1
- 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 +33 -10
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +33 -10
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +33 -10
- nautobot/project-static/docs/user-guide/administration/configuration/index.html +33 -10
- nautobot/project-static/docs/user-guide/administration/configuration/redis.html +33 -10
- nautobot/project-static/docs/user-guide/administration/configuration/settings.html +33 -10
- nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +33 -10
- nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +33 -10
- nautobot/project-static/docs/user-guide/administration/guides/docker.html +33 -10
- nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +33 -10
- nautobot/project-static/docs/user-guide/administration/guides/permissions.html +33 -10
- nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +33 -10
- nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +33 -10
- nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +33 -10
- nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +33 -10
- nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +33 -10
- nautobot/project-static/docs/user-guide/administration/installation/app-install.html +33 -10
- nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +33 -10
- nautobot/project-static/docs/user-guide/administration/installation/http-server.html +33 -10
- nautobot/project-static/docs/user-guide/administration/installation/index.html +33 -10
- nautobot/project-static/docs/user-guide/administration/installation/install_system.html +33 -10
- nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +33 -10
- nautobot/project-static/docs/user-guide/administration/installation/services.html +33 -10
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +33 -10
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +33 -10
- nautobot/project-static/docs/user-guide/administration/security/index.html +33 -10
- nautobot/project-static/docs/user-guide/administration/security/notices.html +33 -10
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +33 -10
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +33 -10
- nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +33 -10
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +33 -10
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +33 -10
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +33 -10
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +33 -10
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +33 -10
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +33 -10
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +33 -10
- nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +45 -22
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +37 -10
- nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +37 -10
- nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +37 -10
- 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 +36 -13
- nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualdevicecontext.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/wireless/index.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/wireless/radioprofile.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/wireless/supporteddatarate.html +33 -10
- nautobot/project-static/docs/user-guide/core-data-model/wireless/wirelessnetwork.html +33 -10
- nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +33 -10
- nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +33 -10
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +33 -10
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +33 -10
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +33 -10
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +33 -10
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +33 -10
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +33 -10
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +33 -10
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +33 -10
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +33 -10
- nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +44 -18
- nautobot/project-static/docs/user-guide/feature-guides/graphql.html +33 -10
- nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +33 -10
- nautobot/project-static/docs/user-guide/feature-guides/relationships.html +33 -10
- nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +33 -10
- nautobot/project-static/docs/user-guide/feature-guides/wireless-networks-and-controllers.html +33 -10
- nautobot/project-static/docs/user-guide/index.html +33 -10
- nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +33 -10
- nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +33 -10
- nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +33 -10
- nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +33 -10
- nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +33 -10
- nautobot/project-static/docs/user-guide/platform-functionality/events.html +33 -10
- nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +33 -10
- nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +33 -10
- nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +39 -11
- nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +33 -10
- nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +33 -10
- nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +33 -10
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +33 -10
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +33 -10
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +33 -10
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +33 -10
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobqueue.html +33 -10
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/kubernetes-job-support.html +33 -10
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/managing-jobs.html +33 -10
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +33 -10
- nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +33 -10
- nautobot/project-static/docs/user-guide/platform-functionality/note.html +33 -10
- nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +33 -10
- nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +33 -10
- nautobot/project-static/docs/user-guide/platform-functionality/rendering-jinja-templates.html +33 -10
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +33 -10
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +33 -10
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +33 -10
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +33 -10
- nautobot/project-static/docs/user-guide/platform-functionality/role.html +33 -10
- nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +33 -10
- nautobot/project-static/docs/user-guide/platform-functionality/secret.html +33 -10
- nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +33 -10
- nautobot/project-static/docs/user-guide/platform-functionality/status.html +33 -10
- nautobot/project-static/docs/user-guide/platform-functionality/tag.html +33 -10
- nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +33 -10
- nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +33 -10
- nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +33 -10
- nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +33 -10
- 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/navigation.py +1 -1
- nautobot/wireless/tables.py +18 -3
- {nautobot-2.4.10.dist-info → nautobot-2.4.12.dist-info}/METADATA +4 -4
- {nautobot-2.4.10.dist-info → nautobot-2.4.12.dist-info}/RECORD +439 -419
- /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.12.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.4.10.dist-info → nautobot-2.4.12.dist-info}/NOTICE +0 -0
- {nautobot-2.4.10.dist-info → nautobot-2.4.12.dist-info}/WHEEL +0 -0
- {nautobot-2.4.10.dist-info → nautobot-2.4.12.dist-info}/entry_points.txt +0 -0
nautobot/core/testing/views.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import contextlib
|
|
2
2
|
import re
|
|
3
3
|
from typing import Optional, Sequence
|
|
4
|
-
from unittest import skipIf
|
|
4
|
+
from unittest import mock, skipIf
|
|
5
5
|
import uuid
|
|
6
6
|
|
|
7
7
|
from django.apps import apps
|
|
@@ -9,6 +9,7 @@ from django.conf import settings
|
|
|
9
9
|
from django.contrib.contenttypes.models import ContentType
|
|
10
10
|
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
|
11
11
|
from django.core.validators import URLValidator
|
|
12
|
+
from django.db.models import ManyToManyField, Model, QuerySet
|
|
12
13
|
from django.test import override_settings, tag, TestCase as _TestCase
|
|
13
14
|
from django.urls import NoReverseMatch, reverse
|
|
14
15
|
from django.utils.html import escape
|
|
@@ -16,6 +17,7 @@ from django.utils.http import urlencode
|
|
|
16
17
|
from django.utils.text import slugify
|
|
17
18
|
from tree_queries.models import TreeNode
|
|
18
19
|
|
|
20
|
+
from nautobot.core.jobs.bulk_actions import BulkEditObjects
|
|
19
21
|
from nautobot.core.models.generics import PrimaryModel
|
|
20
22
|
from nautobot.core.models.tree_queries import TreeModel
|
|
21
23
|
from nautobot.core.templatetags import helpers
|
|
@@ -141,6 +143,8 @@ class ViewTestCases:
|
|
|
141
143
|
Retrieve a single instance.
|
|
142
144
|
"""
|
|
143
145
|
|
|
146
|
+
custom_action_required_permissions = {}
|
|
147
|
+
|
|
144
148
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
145
149
|
def test_get_object_anonymous(self):
|
|
146
150
|
# Make the request as an unauthenticated user
|
|
@@ -246,6 +250,21 @@ class ViewTestCases:
|
|
|
246
250
|
self.assertBodyContains(response, f"{instance.get_absolute_url()}#advanced")
|
|
247
251
|
self.assertBodyContains(response, "Advanced")
|
|
248
252
|
|
|
253
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
|
254
|
+
def test_custom_actions(self):
|
|
255
|
+
instance = self._get_queryset().first()
|
|
256
|
+
for url_name, required_permissions in self.custom_action_required_permissions.items():
|
|
257
|
+
url = reverse(url_name, kwargs={"pk": instance.pk})
|
|
258
|
+
self.assertHttpStatus(self.client.get(url), 403)
|
|
259
|
+
for permission in required_permissions[:-1]:
|
|
260
|
+
self.add_permissions(permission)
|
|
261
|
+
self.assertHttpStatus(self.client.get(url), 403)
|
|
262
|
+
|
|
263
|
+
self.add_permissions(required_permissions[-1])
|
|
264
|
+
self.assertHttpStatus(self.client.get(url), 200)
|
|
265
|
+
# delete the permissions here so that repetitive calls to add_permissions do not create duplicate permissions.
|
|
266
|
+
self.remove_permissions(*required_permissions)
|
|
267
|
+
|
|
249
268
|
class GetObjectChangelogViewTestCase(ModelViewTestCase):
|
|
250
269
|
"""
|
|
251
270
|
View the changelog for an instance.
|
|
@@ -769,6 +788,17 @@ class ViewTestCases:
|
|
|
769
788
|
response_body = response.content.decode(response.charset)
|
|
770
789
|
self.assertIn("/login/?next=" + self._get_url("list"), response_body, msg=response_body)
|
|
771
790
|
|
|
791
|
+
def test_list_objects_anonymous_with_exempt_permission_for_one_view_only(self):
|
|
792
|
+
# Make the request as an unauthenticated user
|
|
793
|
+
self.client.logout()
|
|
794
|
+
# Test if AnonymousUser can properly render the whole list view
|
|
795
|
+
with override_settings(EXEMPT_VIEW_PERMISSIONS=[self.model._meta.label_lower]):
|
|
796
|
+
response = self.client.get(self._get_url("list"))
|
|
797
|
+
self.assertHttpStatus(response, 200)
|
|
798
|
+
# There should be some rows
|
|
799
|
+
self.assertBodyContains(response, '<tr class="even')
|
|
800
|
+
self.assertBodyContains(response, '<tr class="odd')
|
|
801
|
+
|
|
772
802
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
773
803
|
def test_list_objects_filtered(self):
|
|
774
804
|
instance1, instance2 = self._get_queryset().all()[:2]
|
|
@@ -1049,6 +1079,7 @@ class ViewTestCases:
|
|
|
1049
1079
|
def validate_redirect_to_job_result(self, response):
|
|
1050
1080
|
# Get the last Bulk Edit Objects JobResult created
|
|
1051
1081
|
job_result = JobResult.objects.filter(name="Bulk Edit Objects").first()
|
|
1082
|
+
self.assertIsNotNone(job_result, "No JobResult was created - likely the bulk_edit_data is invalid!")
|
|
1052
1083
|
# Assert redirect to Job Results
|
|
1053
1084
|
self.assertRedirects(
|
|
1054
1085
|
response,
|
|
@@ -1082,8 +1113,71 @@ class ViewTestCases:
|
|
|
1082
1113
|
# Assign model-level permission
|
|
1083
1114
|
self.add_permissions(f"{self.model._meta.app_label}.change_{self.model._meta.model_name}")
|
|
1084
1115
|
|
|
1085
|
-
|
|
1086
|
-
|
|
1116
|
+
with mock.patch.object(JobResult, "enqueue_job", wraps=JobResult.enqueue_job) as mock_enqueue_job:
|
|
1117
|
+
response = self.client.post(self._get_url("bulk_edit"), data)
|
|
1118
|
+
self.validate_redirect_to_job_result(response)
|
|
1119
|
+
mock_enqueue_job.assert_called()
|
|
1120
|
+
|
|
1121
|
+
# Verify that the provided self.bulk_edit_data was passed through correctly to the job.
|
|
1122
|
+
# The below is a bit gross because of multiple layers of data encoding and decoding involved. Sorry!
|
|
1123
|
+
job_form = BulkEditObjects.as_form(BulkEditObjects.deserialize_data(mock_enqueue_job.call_args.kwargs))
|
|
1124
|
+
job_form.is_valid()
|
|
1125
|
+
job_kwargs = job_form.cleaned_data
|
|
1126
|
+
|
|
1127
|
+
bulk_edit_form_class = lookup.get_form_for_model(self.model, form_prefix="BulkEdit")
|
|
1128
|
+
bulk_edit_form = bulk_edit_form_class(self.model, job_kwargs["form_data"])
|
|
1129
|
+
bulk_edit_form.is_valid()
|
|
1130
|
+
passed_bulk_edit_data = bulk_edit_form.cleaned_data
|
|
1131
|
+
|
|
1132
|
+
for key, value in self.bulk_edit_data.items():
|
|
1133
|
+
with self.subTest(key=key):
|
|
1134
|
+
if isinstance(passed_bulk_edit_data.get(key), Model):
|
|
1135
|
+
self.assertEqual(passed_bulk_edit_data.get(key).pk, value)
|
|
1136
|
+
elif isinstance(passed_bulk_edit_data.get(key), QuerySet):
|
|
1137
|
+
self.assertEqual(
|
|
1138
|
+
sorted(passed_bulk_edit_data.get(key).values_list("pk", flat=True)), sorted(value)
|
|
1139
|
+
)
|
|
1140
|
+
else:
|
|
1141
|
+
self.assertEqual(passed_bulk_edit_data.get(key), bulk_edit_form.fields[key].clean(value))
|
|
1142
|
+
|
|
1143
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1144
|
+
def test_bulk_edit_objects_nullable_fields(self):
|
|
1145
|
+
"""Assert that "set null" fields on the bulk-edit form are correctly passed through to the job."""
|
|
1146
|
+
bulk_edit_form_class = lookup.get_form_for_model(self.model, form_prefix="BulkEdit")
|
|
1147
|
+
bulk_edit_form = bulk_edit_form_class(self.model)
|
|
1148
|
+
if not getattr(bulk_edit_form, "nullable_fields", ()):
|
|
1149
|
+
self.skipTest(f"no nullable fields on {bulk_edit_form_class}")
|
|
1150
|
+
|
|
1151
|
+
for field_name in bulk_edit_form.nullable_fields:
|
|
1152
|
+
with self.subTest(field_name=field_name):
|
|
1153
|
+
if field_name.startswith("cf_"):
|
|
1154
|
+
# TODO check whether customfield is nullable
|
|
1155
|
+
continue
|
|
1156
|
+
if field_name.startswith("cr_"):
|
|
1157
|
+
# TODO check whether relationship is required
|
|
1158
|
+
continue
|
|
1159
|
+
model_field = self.model._meta.get_field(field_name)
|
|
1160
|
+
if isinstance(model_field, ManyToManyField):
|
|
1161
|
+
# always nullable
|
|
1162
|
+
continue
|
|
1163
|
+
self.assertTrue(model_field.null or model_field.blank)
|
|
1164
|
+
|
|
1165
|
+
pk_list = list(self._get_queryset().values_list("pk", flat=True)[:3])
|
|
1166
|
+
data = {
|
|
1167
|
+
"pk": pk_list,
|
|
1168
|
+
"_apply": True, # Form button
|
|
1169
|
+
"_nullify": list(bulk_edit_form.nullable_fields),
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
# Assign model-level permission
|
|
1173
|
+
self.add_permissions(f"{self.model._meta.app_label}.change_{self.model._meta.model_name}")
|
|
1174
|
+
|
|
1175
|
+
with mock.patch.object(JobResult, "enqueue_job", wraps=JobResult.enqueue_job) as mock_enqueue_job:
|
|
1176
|
+
response = self.client.post(self._get_url("bulk_edit"), data)
|
|
1177
|
+
self.validate_redirect_to_job_result(response)
|
|
1178
|
+
mock_enqueue_job.assert_called()
|
|
1179
|
+
|
|
1180
|
+
self.assertEqual(mock_enqueue_job.call_args.kwargs["form_data"].get("_nullify"), data["_nullify"])
|
|
1087
1181
|
|
|
1088
1182
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1089
1183
|
def test_bulk_edit_form_contains_all_pks(self):
|
|
@@ -1182,11 +1276,13 @@ class ViewTestCases:
|
|
|
1182
1276
|
}
|
|
1183
1277
|
data.update(utils.post_data(self.bulk_edit_data))
|
|
1184
1278
|
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1279
|
+
with mock.patch.object(JobResult, "enqueue_job", wraps=JobResult.enqueue_job) as mock_enqueue_job:
|
|
1280
|
+
# Attempt to bulk edit permitted objects into a non-permitted state
|
|
1281
|
+
response = self.client.post(self._get_url("bulk_edit"), data)
|
|
1282
|
+
# NOTE: There is no way of testing constrained failure as bulk edit is a system Job;
|
|
1283
|
+
# and can only be tested by checking JobLogs.
|
|
1284
|
+
self.validate_redirect_to_job_result(response)
|
|
1285
|
+
mock_enqueue_job.assert_called()
|
|
1190
1286
|
|
|
1191
1287
|
class BulkDeleteObjectsViewTestCase(ModelViewTestCase):
|
|
1192
1288
|
"""
|
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.
|
|
@@ -17,6 +17,7 @@ from nautobot.core.models import fields as core_fields, utils as models_utils, v
|
|
|
17
17
|
from nautobot.core.testing import TestCase
|
|
18
18
|
from nautobot.core.utils import data as data_utils, filtering, lookup, querysets, requests
|
|
19
19
|
from nautobot.core.utils.migrations import update_object_change_ct_for_replaced_models
|
|
20
|
+
from nautobot.core.utils.module_loading import check_name_safe_to_import_privately
|
|
20
21
|
from nautobot.dcim import filters as dcim_filters, forms as dcim_forms, models as dcim_models, tables
|
|
21
22
|
from nautobot.extras import models as extras_models, utils as extras_utils
|
|
22
23
|
from nautobot.extras.choices import ObjectChangeActionChoices, RelationshipTypeChoices
|
|
@@ -959,6 +960,22 @@ class TestMigrationUtils(TestCase):
|
|
|
959
960
|
self.assertEqual(ObjectChange.objects.get(request_id=request_id).related_object_type, location_ct)
|
|
960
961
|
|
|
961
962
|
|
|
963
|
+
class TestModuleLoadingUtils(TestCase):
|
|
964
|
+
def test_check_name_safe_to_import_privately(self):
|
|
965
|
+
for invalid in (
|
|
966
|
+
"foo.bar", # not a valid identifier
|
|
967
|
+
"😂", # not a valid identifier
|
|
968
|
+
"from", # reserved keyword
|
|
969
|
+
"sys", # Python builtin
|
|
970
|
+
"nautobot", # installed package
|
|
971
|
+
"tkinter", # system library
|
|
972
|
+
):
|
|
973
|
+
with self.subTest(f"Invalid name: {invalid}"):
|
|
974
|
+
permitted, reason = check_name_safe_to_import_privately(invalid)
|
|
975
|
+
self.assertFalse(permitted)
|
|
976
|
+
self.assertIsInstance(reason, str)
|
|
977
|
+
|
|
978
|
+
|
|
962
979
|
class TestQuerySetUtils(TestCase):
|
|
963
980
|
def test_maybe_select_related(self):
|
|
964
981
|
# If possible, select_related should be called
|
|
@@ -764,11 +764,11 @@ class TestObjectDetailView(TestCase):
|
|
|
764
764
|
url = reverse("circuits:provider", args=(provider.pk,))
|
|
765
765
|
response = self.client.get(f"{url}?tab=main")
|
|
766
766
|
self.assertHttpStatus(response, 200)
|
|
767
|
-
response_data = response.content.decode(response.charset)
|
|
767
|
+
response_data = extract_page_body(response.content.decode(response.charset))
|
|
768
768
|
view_move_url = reverse("circuits:circuit_list") + f"?provider={provider.id}"
|
|
769
769
|
|
|
770
770
|
# Assert Badge Count in table panel header
|
|
771
|
-
panel_header = f"""<
|
|
771
|
+
panel_header = f"""<strong>Circuits</strong> <a href="{view_move_url}" class="badge badge-primary">10</a>"""
|
|
772
772
|
self.assertInHTML(panel_header, response_data)
|
|
773
773
|
|
|
774
774
|
# Assert view X more btn
|
|
@@ -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)
|
|
@@ -867,7 +867,7 @@ class ObjectsTablePanel(Panel):
|
|
|
867
867
|
body_content_table_queryset = body_content_table_queryset.order_by(*self.order_by_fields)
|
|
868
868
|
body_content_table_queryset = body_content_table_queryset.distinct()
|
|
869
869
|
body_content_table = body_content_table_class(
|
|
870
|
-
body_content_table_queryset, hide_hierarchy_ui=self.hide_hierarchy_ui
|
|
870
|
+
body_content_table_queryset, hide_hierarchy_ui=self.hide_hierarchy_ui, user=request.user
|
|
871
871
|
)
|
|
872
872
|
if self.tab_id and "actions" in body_content_table.columns:
|
|
873
873
|
# Use the `self.tab_id`, if it exists, to determine the correct return URL for the table
|
|
@@ -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,6 +1,7 @@
|
|
|
1
1
|
from contextlib import contextmanager
|
|
2
2
|
import importlib
|
|
3
3
|
from importlib.util import find_spec, module_from_spec
|
|
4
|
+
from keyword import iskeyword
|
|
4
5
|
import logging
|
|
5
6
|
import os
|
|
6
7
|
import pkgutil
|
|
@@ -33,6 +34,28 @@ def clear_module_from_sys_modules(module_name):
|
|
|
33
34
|
del sys.modules[name]
|
|
34
35
|
|
|
35
36
|
|
|
37
|
+
def check_name_safe_to_import_privately(name: str) -> tuple[bool, str]:
|
|
38
|
+
"""
|
|
39
|
+
Make sure the given package/module name is "safe" to import from the filesystem.
|
|
40
|
+
|
|
41
|
+
In other words, make sure it's:
|
|
42
|
+
- a valid Python identifier and not a reserved keyword
|
|
43
|
+
- not the name of an existing "real" Python package or builtin
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
(bool, str): Whether safe to load, and an explanatory string fragment for logging/exception messages.
|
|
47
|
+
"""
|
|
48
|
+
if not name.isidentifier():
|
|
49
|
+
return False, "not a valid identifier"
|
|
50
|
+
if iskeyword(name):
|
|
51
|
+
return False, "a reserved keyword"
|
|
52
|
+
if name in sys.builtin_module_names:
|
|
53
|
+
return False, "a Python builtin"
|
|
54
|
+
if any(module_info.name == name for module_info in pkgutil.iter_modules()):
|
|
55
|
+
return False, "the name of an installed Python package"
|
|
56
|
+
return True, "a valid and non-conflicting module name"
|
|
57
|
+
|
|
58
|
+
|
|
36
59
|
def import_modules_privately(path, module_path=None, ignore_import_errors=True):
|
|
37
60
|
"""
|
|
38
61
|
Import modules from the filesystem without adding the path permanently to `sys.path`.
|
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__"
|
nautobot/dcim/api/urls.py
CHANGED
|
@@ -17,6 +17,7 @@ router.register("rack-reservations", views.RackReservationViewSet)
|
|
|
17
17
|
router.register("manufacturers", views.ManufacturerViewSet)
|
|
18
18
|
router.register("device-families", views.DeviceFamilyViewSet)
|
|
19
19
|
router.register("device-types", views.DeviceTypeViewSet)
|
|
20
|
+
router.register("module-families", views.ModuleFamilyViewSet)
|
|
20
21
|
router.register("module-types", views.ModuleTypeViewSet)
|
|
21
22
|
|
|
22
23
|
# Device type and Module type components
|
nautobot/dcim/api/views.py
CHANGED
|
@@ -54,6 +54,7 @@ from nautobot.dcim.models import (
|
|
|
54
54
|
Module,
|
|
55
55
|
ModuleBay,
|
|
56
56
|
ModuleBayTemplate,
|
|
57
|
+
ModuleFamily,
|
|
57
58
|
ModuleType,
|
|
58
59
|
Platform,
|
|
59
60
|
PowerFeed,
|
|
@@ -644,6 +645,14 @@ class CableViewSet(NautobotModelViewSet):
|
|
|
644
645
|
serializer_class = serializers.CableSerializer
|
|
645
646
|
filterset_class = filters.CableFilterSet
|
|
646
647
|
|
|
648
|
+
def get_queryset(self):
|
|
649
|
+
# 6933 fix: with prefetch related in queryset
|
|
650
|
+
# DeviceInterface is not properly cleared of _path_id
|
|
651
|
+
queryset = super().get_queryset()
|
|
652
|
+
if self.action == "destroy":
|
|
653
|
+
queryset = queryset.prefetch_related(None)
|
|
654
|
+
return queryset
|
|
655
|
+
|
|
647
656
|
|
|
648
657
|
#
|
|
649
658
|
# Virtual chassis
|
|
@@ -836,3 +845,14 @@ class InterfaceVDCAssignmentViewSet(ModelViewSet):
|
|
|
836
845
|
queryset = InterfaceVDCAssignment.objects.all()
|
|
837
846
|
serializer_class = serializers.InterfaceVDCAssignmentSerializer
|
|
838
847
|
filterset_class = filters.InterfaceVDCAssignmentFilterSet
|
|
848
|
+
|
|
849
|
+
|
|
850
|
+
class ModuleFamilyViewSet(NautobotModelViewSet):
|
|
851
|
+
"""API viewset for interacting with ModuleFamily objects."""
|
|
852
|
+
|
|
853
|
+
queryset = ModuleFamily.objects.annotate(
|
|
854
|
+
module_type_count=count_related(ModuleType, "module_family"),
|
|
855
|
+
module_bay_count=count_related(ModuleBay, "module_family"),
|
|
856
|
+
)
|
|
857
|
+
serializer_class = serializers.ModuleFamilySerializer
|
|
858
|
+
filterset_class = filters.ModuleFamilyFilterSet
|
nautobot/dcim/apps.py
CHANGED
nautobot/dcim/factory.py
CHANGED
|
@@ -49,6 +49,7 @@ from nautobot.dcim.models import (
|
|
|
49
49
|
Module,
|
|
50
50
|
ModuleBay,
|
|
51
51
|
ModuleBayTemplate,
|
|
52
|
+
ModuleFamily,
|
|
52
53
|
ModuleType,
|
|
53
54
|
Platform,
|
|
54
55
|
PowerOutletTemplate,
|
|
@@ -787,6 +788,7 @@ class ModuleTypeFactory(PrimaryModelFactory):
|
|
|
787
788
|
exclude = ("has_part_number", "has_comments")
|
|
788
789
|
|
|
789
790
|
manufacturer = random_instance(Manufacturer, allow_null=False)
|
|
791
|
+
module_family = random_instance(ModuleFamily, allow_null=True)
|
|
790
792
|
|
|
791
793
|
has_part_number = NautobotBoolIterator()
|
|
792
794
|
part_number = factory.Maybe("has_part_number", factory.Faker("ean", length=8), "")
|
|
@@ -970,6 +972,15 @@ class ModuleBayTemplateFactory(ModularDeviceComponentTemplateFactory):
|
|
|
970
972
|
factory.LazyAttribute(lambda o: o.module_type.module_bay_templates.count() + 1),
|
|
971
973
|
)
|
|
972
974
|
|
|
975
|
+
class Params:
|
|
976
|
+
has_module_family = NautobotBoolIterator()
|
|
977
|
+
|
|
978
|
+
module_family = factory.Maybe(
|
|
979
|
+
"has_module_family",
|
|
980
|
+
random_instance(ModuleFamily),
|
|
981
|
+
None,
|
|
982
|
+
)
|
|
983
|
+
|
|
973
984
|
|
|
974
985
|
class VirtualDeviceContextFactory(PrimaryModelFactory):
|
|
975
986
|
class Meta:
|