nautobot 2.4.14__py3-none-any.whl → 2.4.16__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.
- nautobot/apps/choices.py +8 -0
- nautobot/apps/ui.py +14 -0
- nautobot/core/api/views.py +2 -0
- nautobot/core/choices.py +4 -0
- nautobot/core/filters.py +21 -41
- nautobot/core/management/commands/check_job_approval_status.py +47 -0
- nautobot/core/management/commands/generate_test_data.py +1 -1
- nautobot/core/management/commands/migrate.py +1 -1
- nautobot/core/models/tree_queries.py +17 -0
- nautobot/core/settings.py +2 -2
- nautobot/core/tables.py +25 -2
- nautobot/core/templates/base_django.html +1 -1
- nautobot/core/templates/components/panel/header_extra_content_table.html +1 -1
- nautobot/core/templates/generic/object_list.html +17 -20
- nautobot/core/templates/inc/breadcrumbs.html +14 -0
- nautobot/core/templatetags/buttons.py +2 -4
- nautobot/core/templatetags/helpers.py +29 -6
- nautobot/core/templatetags/ui_framework.py +21 -0
- nautobot/core/testing/filters.py +20 -3
- nautobot/core/testing/forms.py +1 -1
- nautobot/core/tests/integration/test_filters.py +2 -2
- nautobot/core/tests/test_breadcrumbs.py +366 -0
- nautobot/core/tests/test_commands.py +40 -0
- nautobot/core/tests/test_filters.py +51 -1
- nautobot/core/tests/test_forms.py +1 -1
- nautobot/core/tests/test_graphql.py +4 -4
- nautobot/core/tests/test_titles.py +183 -0
- nautobot/core/tests/test_tree_queries.py +30 -0
- nautobot/core/tests/test_views.py +2 -2
- nautobot/core/tests/test_views_utils.py +1 -1
- nautobot/core/ui/breadcrumbs.py +538 -0
- nautobot/core/ui/bulk_buttons.py +53 -0
- nautobot/core/ui/object_detail.py +31 -8
- nautobot/core/ui/titles.py +127 -0
- nautobot/core/ui/utils.py +25 -0
- nautobot/core/utils/migrations.py +1 -1
- nautobot/core/views/__init__.py +1 -1
- nautobot/core/views/mixins.py +26 -1
- nautobot/core/views/renderers.py +20 -2
- nautobot/core/views/utils.py +13 -12
- nautobot/dcim/api/serializers.py +9 -0
- nautobot/dcim/choices.py +53 -0
- nautobot/dcim/filters/__init__.py +15 -3
- nautobot/dcim/forms.py +120 -7
- nautobot/dcim/management/commands/trace_paths.py +1 -1
- nautobot/dcim/migrations/0072_alter_powerfeed_options_and_more.py +97 -0
- nautobot/dcim/models/device_component_templates.py +8 -0
- nautobot/dcim/models/device_components.py +31 -12
- nautobot/dcim/models/devices.py +1 -1
- nautobot/dcim/models/power.py +171 -10
- nautobot/dcim/models/racks.py +7 -4
- nautobot/dcim/tables/devices.py +2 -0
- nautobot/dcim/tables/devicetypes.py +1 -0
- nautobot/dcim/tables/power.py +30 -2
- nautobot/dcim/templates/dcim/device.html +2 -2
- nautobot/dcim/templates/dcim/devicetype_retrieve.html +1 -214
- nautobot/dcim/templates/dcim/location_retrieve.html +2 -2
- nautobot/dcim/templates/dcim/powerfeed_edit.html +8 -0
- nautobot/dcim/templates/dcim/powerfeed_retrieve.html +1 -1
- nautobot/dcim/tests/integration/test_device_bulk_operations.py +61 -0
- nautobot/dcim/tests/test_api.py +24 -4
- nautobot/dcim/tests/test_filters.py +91 -13
- nautobot/dcim/tests/test_models.py +262 -0
- nautobot/dcim/tests/test_views.py +20 -12
- nautobot/dcim/utils.py +9 -0
- nautobot/dcim/views.py +390 -77
- nautobot/extras/factory.py +19 -20
- nautobot/extras/filters/__init__.py +3 -2
- nautobot/extras/filters/mixins.py +15 -1
- nautobot/extras/forms/__init__.py +2 -1
- nautobot/extras/forms/forms.py +62 -0
- nautobot/extras/managers.py +4 -1
- nautobot/extras/migrations/0125_jobresult_date_started.py +18 -0
- nautobot/extras/models/customfields.py +1 -2
- nautobot/extras/models/datasources.py +1 -2
- nautobot/extras/models/jobs.py +7 -3
- nautobot/extras/plugins/views.py +24 -1
- nautobot/extras/secrets/__init__.py +1 -1
- nautobot/extras/tables.py +21 -0
- nautobot/extras/templates/extras/customfield.html +2 -129
- nautobot/extras/templates/extras/customfield_edit.html +2 -108
- nautobot/extras/templates/extras/customfield_retrieve.html +129 -0
- nautobot/extras/templates/extras/customfield_update.html +108 -0
- nautobot/extras/templates/extras/inc/jobresult.html +7 -3
- nautobot/extras/templates/extras/jobresult.html +2 -155
- nautobot/extras/templates/extras/jobresult_retrieve.html +155 -0
- nautobot/extras/templates/extras/marketplace.html +5 -6
- nautobot/extras/templates/extras/note.html +2 -53
- nautobot/extras/templates/extras/note_retrieve.html +53 -0
- nautobot/extras/templates/extras/plugins_list.html +5 -6
- nautobot/extras/templates/extras/secretsgroup_retrieve.html +2 -29
- nautobot/extras/templatetags/custom_links.py +2 -2
- nautobot/extras/templatetags/job_buttons.py +1 -1
- nautobot/extras/templatetags/plugins.py +1 -1
- nautobot/extras/tests/integration/test_computedfields.py +2 -2
- nautobot/extras/tests/integration/test_customfields.py +14 -11
- nautobot/extras/tests/integration/test_dynamicgroups.py +1 -1
- nautobot/extras/tests/integration/test_notes.py +1 -1
- nautobot/extras/tests/integration/test_plugins.py +6 -6
- nautobot/extras/tests/integration/test_relationships.py +2 -2
- nautobot/extras/tests/test_filters.py +9 -0
- nautobot/extras/tests/test_forms.py +2 -2
- nautobot/extras/tests/test_plugins.py +14 -3
- nautobot/extras/tests/test_relationships.py +7 -7
- nautobot/extras/tests/test_views.py +172 -1
- nautobot/extras/urls.py +3 -59
- nautobot/extras/utils.py +1 -1
- nautobot/extras/views.py +96 -182
- nautobot/ipam/tables.py +8 -15
- nautobot/ipam/tests/migration/test_migrations.py +8 -8
- nautobot/ipam/tests/test_api.py +2 -2
- nautobot/ipam/tests/test_models.py +1 -1
- nautobot/project-static/docs/404.html +23 -0
- nautobot/project-static/docs/apps/index.html +23 -0
- nautobot/project-static/docs/apps/nautobot-apps.html +23 -0
- nautobot/project-static/docs/assets/_mkdocstrings.css +44 -6
- nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +28 -0
- nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +25 -0
- nautobot/project-static/docs/code-reference/nautobot/apps/api.html +128 -20
- nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +37 -4
- nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +39 -6
- nautobot/project-static/docs/code-reference/nautobot/apps/config.html +25 -0
- nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +24 -0
- nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +32 -5
- nautobot/project-static/docs/code-reference/nautobot/apps/events.html +41 -8
- nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +39 -7
- nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +43 -10
- nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +74 -59
- nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +143 -28
- nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +43 -12
- nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +135 -53
- nautobot/project-static/docs/code-reference/nautobot/apps/models.html +229 -36
- nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +27 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +30 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +162 -18
- nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +258 -51
- nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +5987 -2620
- nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +25 -0
- nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +154 -55
- nautobot/project-static/docs/code-reference/nautobot/apps/views.html +150 -35
- nautobot/project-static/docs/development/apps/api/configuration-view.html +23 -0
- nautobot/project-static/docs/development/apps/api/database-backend-config.html +23 -0
- nautobot/project-static/docs/development/apps/api/models/django-admin.html +23 -0
- nautobot/project-static/docs/development/apps/api/models/global-search.html +23 -0
- nautobot/project-static/docs/development/apps/api/models/graphql.html +23 -0
- nautobot/project-static/docs/development/apps/api/models/index.html +23 -0
- nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +23 -0
- nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +23 -0
- nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +23 -0
- nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +23 -0
- nautobot/project-static/docs/development/apps/api/platform-features/index.html +23 -0
- nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +23 -0
- nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +23 -0
- nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +23 -0
- nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +23 -0
- nautobot/project-static/docs/development/apps/api/platform-features/table-extensions.html +23 -0
- nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +23 -0
- nautobot/project-static/docs/development/apps/api/prometheus.html +23 -0
- nautobot/project-static/docs/development/apps/api/setup.html +23 -0
- nautobot/project-static/docs/development/apps/api/testing.html +23 -0
- nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +23 -0
- nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +23 -0
- nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +23 -0
- nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +23 -0
- nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +23 -0
- nautobot/project-static/docs/development/apps/api/views/base-template.html +23 -0
- nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +23 -0
- nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +23 -0
- nautobot/project-static/docs/development/apps/api/views/help-documentation.html +23 -0
- nautobot/project-static/docs/development/apps/api/views/index.html +23 -0
- nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +23 -0
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +31 -2
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +23 -0
- nautobot/project-static/docs/development/apps/api/views/notes.html +23 -0
- nautobot/project-static/docs/development/apps/api/views/rest-api.html +23 -0
- nautobot/project-static/docs/development/apps/api/views/urls.html +23 -0
- nautobot/project-static/docs/development/apps/index.html +23 -0
- nautobot/project-static/docs/development/apps/migration/code-updates.html +23 -0
- nautobot/project-static/docs/development/apps/migration/dependency-updates.html +23 -0
- nautobot/project-static/docs/development/apps/migration/from-v1.html +23 -0
- nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +23 -0
- nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +23 -0
- nautobot/project-static/docs/development/apps/migration/model-updates/global.html +23 -0
- nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +23 -0
- nautobot/project-static/docs/development/apps/migration/ui-component-framework/best-practices.html +26 -3
- nautobot/project-static/docs/development/apps/migration/ui-component-framework/breadcrumbs-titles.html +10544 -0
- nautobot/project-static/docs/development/apps/migration/ui-component-framework/custom-content.html +23 -0
- nautobot/project-static/docs/development/apps/migration/ui-component-framework/index.html +23 -0
- nautobot/project-static/docs/development/apps/migration/ui-component-framework/migration-steps.html +23 -0
- nautobot/project-static/docs/development/apps/porting-from-netbox.html +26 -3
- nautobot/project-static/docs/development/core/application-registry.html +23 -0
- nautobot/project-static/docs/development/core/best-practices.html +23 -0
- nautobot/project-static/docs/development/core/bootstrap-ui.html +23 -0
- nautobot/project-static/docs/development/core/caching.html +23 -0
- nautobot/project-static/docs/development/core/controllers.html +23 -0
- nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +23 -0
- nautobot/project-static/docs/development/core/generic-views.html +23 -0
- nautobot/project-static/docs/development/core/getting-started.html +23 -0
- nautobot/project-static/docs/development/core/homepage.html +23 -0
- nautobot/project-static/docs/development/core/index.html +23 -0
- nautobot/project-static/docs/development/core/minikube-dev-environment-for-k8s-jobs.html +23 -0
- nautobot/project-static/docs/development/core/model-checklist.html +23 -0
- nautobot/project-static/docs/development/core/model-features.html +23 -0
- nautobot/project-static/docs/development/core/natural-keys.html +23 -0
- nautobot/project-static/docs/development/core/navigation-menu.html +23 -0
- nautobot/project-static/docs/development/core/release-checklist.html +23 -0
- nautobot/project-static/docs/development/core/role-internals.html +23 -0
- nautobot/project-static/docs/development/core/settings.html +23 -0
- nautobot/project-static/docs/development/core/style-guide.html +23 -0
- nautobot/project-static/docs/development/core/templates.html +23 -0
- nautobot/project-static/docs/development/core/testing.html +23 -0
- nautobot/project-static/docs/development/core/ui-component-framework.html +713 -255
- nautobot/project-static/docs/development/core/user-preferences.html +23 -0
- nautobot/project-static/docs/development/index.html +23 -0
- nautobot/project-static/docs/development/jobs/getting-started.html +23 -0
- nautobot/project-static/docs/development/jobs/index.html +23 -0
- nautobot/project-static/docs/development/jobs/installation.html +23 -0
- nautobot/project-static/docs/development/jobs/job-extensions.html +23 -0
- nautobot/project-static/docs/development/jobs/job-logging.html +23 -0
- nautobot/project-static/docs/development/jobs/job-patterns.html +23 -0
- nautobot/project-static/docs/development/jobs/job-structure.html +23 -0
- nautobot/project-static/docs/development/jobs/migration/from-v1.html +23 -0
- nautobot/project-static/docs/development/jobs/testing.html +23 -0
- nautobot/project-static/docs/index.html +23 -0
- nautobot/project-static/docs/media/development/core/ui-component-framework/breadcrumbs-titles-data-flow.png +0 -0
- nautobot/project-static/docs/media/power_distribution.png +0 -0
- nautobot/project-static/docs/objects.inv +0 -0
- nautobot/project-static/docs/overview/application_stack.html +23 -0
- nautobot/project-static/docs/overview/design_philosophy.html +23 -0
- nautobot/project-static/docs/release-notes/index.html +23 -0
- nautobot/project-static/docs/release-notes/version-1.0.html +23 -0
- nautobot/project-static/docs/release-notes/version-1.1.html +23 -0
- nautobot/project-static/docs/release-notes/version-1.2.html +23 -0
- nautobot/project-static/docs/release-notes/version-1.3.html +23 -0
- nautobot/project-static/docs/release-notes/version-1.4.html +23 -0
- nautobot/project-static/docs/release-notes/version-1.5.html +23 -0
- nautobot/project-static/docs/release-notes/version-1.6.html +23 -0
- nautobot/project-static/docs/release-notes/version-2.0.html +23 -0
- nautobot/project-static/docs/release-notes/version-2.1.html +23 -0
- nautobot/project-static/docs/release-notes/version-2.2.html +23 -0
- nautobot/project-static/docs/release-notes/version-2.3.html +23 -0
- nautobot/project-static/docs/release-notes/version-2.4.html +306 -0
- nautobot/project-static/docs/requirements.txt +2 -2
- nautobot/project-static/docs/search/search_index.json +1 -1
- nautobot/project-static/docs/sitemap.xml +303 -299
- nautobot/project-static/docs/sitemap.xml.gz +0 -0
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +23 -0
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +23 -0
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +23 -0
- nautobot/project-static/docs/user-guide/administration/configuration/index.html +23 -0
- nautobot/project-static/docs/user-guide/administration/configuration/redis.html +23 -0
- nautobot/project-static/docs/user-guide/administration/configuration/settings.html +23 -0
- nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +23 -0
- nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +23 -0
- nautobot/project-static/docs/user-guide/administration/guides/docker.html +23 -0
- nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +23 -0
- nautobot/project-static/docs/user-guide/administration/guides/permissions.html +23 -0
- nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +23 -0
- nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +23 -0
- nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +23 -0
- nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +23 -0
- nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +23 -0
- nautobot/project-static/docs/user-guide/administration/installation/app-install.html +23 -0
- nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +23 -0
- nautobot/project-static/docs/user-guide/administration/installation/http-server.html +23 -0
- nautobot/project-static/docs/user-guide/administration/installation/index.html +23 -0
- nautobot/project-static/docs/user-guide/administration/installation/install_system.html +23 -0
- nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +23 -0
- nautobot/project-static/docs/user-guide/administration/installation/services.html +23 -0
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +23 -0
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +23 -0
- nautobot/project-static/docs/user-guide/administration/security/index.html +23 -0
- nautobot/project-static/docs/user-guide/administration/security/notices.html +23 -0
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +284 -219
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +23 -0
- nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +23 -0
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +23 -0
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +23 -0
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +23 -0
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +23 -0
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +23 -0
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +23 -0
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +23 -0
- nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/dcim/modulefamily.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +305 -5
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +24 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +136 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +41 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +40 -0
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualdevicecontext.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/wireless/index.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/wireless/radioprofile.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/wireless/supporteddatarate.html +23 -0
- nautobot/project-static/docs/user-guide/core-data-model/wireless/wirelessnetwork.html +23 -0
- nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +23 -0
- nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +23 -0
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +23 -0
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +23 -0
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +23 -0
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +23 -0
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +23 -0
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +23 -0
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +23 -0
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +23 -0
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +23 -0
- nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +23 -0
- nautobot/project-static/docs/user-guide/feature-guides/graphql.html +23 -0
- nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +23 -0
- nautobot/project-static/docs/user-guide/feature-guides/relationships.html +23 -0
- nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +23 -0
- nautobot/project-static/docs/user-guide/feature-guides/wireless-networks-and-controllers.html +23 -0
- nautobot/project-static/docs/user-guide/index.html +23 -0
- nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +23 -0
- nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +23 -0
- nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +23 -0
- nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +23 -0
- nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +23 -0
- nautobot/project-static/docs/user-guide/platform-functionality/events.html +23 -0
- nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +23 -0
- nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +23 -0
- nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +23 -0
- nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +23 -0
- nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +23 -0
- nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +23 -0
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +23 -0
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +24 -1
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +23 -0
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +23 -0
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobqueue.html +23 -0
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/kubernetes-job-support.html +23 -0
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/managing-jobs.html +23 -0
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +23 -0
- nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +23 -0
- nautobot/project-static/docs/user-guide/platform-functionality/note.html +23 -0
- nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +23 -0
- nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +23 -0
- nautobot/project-static/docs/user-guide/platform-functionality/rendering-jinja-templates.html +23 -0
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +23 -0
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +23 -0
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +23 -0
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +23 -0
- nautobot/project-static/docs/user-guide/platform-functionality/role.html +23 -0
- nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +23 -0
- nautobot/project-static/docs/user-guide/platform-functionality/secret.html +23 -0
- nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +23 -0
- nautobot/project-static/docs/user-guide/platform-functionality/status.html +23 -0
- nautobot/project-static/docs/user-guide/platform-functionality/tag.html +23 -0
- nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +23 -0
- nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +23 -0
- nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +23 -0
- nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +23 -0
- nautobot/users/tests/test_api.py +2 -2
- nautobot/virtualization/templates/virtualization/virtualmachine.html +2 -252
- nautobot/virtualization/templates/virtualization/virtualmachine_edit.html +2 -75
- nautobot/virtualization/templates/virtualization/virtualmachine_retrieve.html +252 -0
- nautobot/virtualization/templates/virtualization/virtualmachine_update.html +75 -0
- nautobot/virtualization/urls.py +3 -61
- nautobot/virtualization/views.py +48 -72
- {nautobot-2.4.14.dist-info → nautobot-2.4.16.dist-info}/METADATA +24 -24
- {nautobot-2.4.14.dist-info → nautobot-2.4.16.dist-info}/RECORD +434 -417
- {nautobot-2.4.14.dist-info → nautobot-2.4.16.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.4.14.dist-info → nautobot-2.4.16.dist-info}/NOTICE +0 -0
- {nautobot-2.4.14.dist-info → nautobot-2.4.16.dist-info}/WHEEL +0 -0
- {nautobot-2.4.14.dist-info → nautobot-2.4.16.dist-info}/entry_points.txt +0 -0
nautobot/core/testing/filters.py
CHANGED
|
@@ -15,6 +15,7 @@ from nautobot.core.filters import (
|
|
|
15
15
|
ContentTypeChoiceFilter,
|
|
16
16
|
ContentTypeFilter,
|
|
17
17
|
ContentTypeMultipleChoiceFilter,
|
|
18
|
+
NaturalKeyOrPKMultipleChoiceFilter,
|
|
18
19
|
RelatedMembershipBooleanFilter,
|
|
19
20
|
SearchFilter,
|
|
20
21
|
)
|
|
@@ -207,15 +208,29 @@ class FilterTestCases:
|
|
|
207
208
|
qs_result = self.queryset.filter(**{f"{field_name}__in": test_data}).distinct()
|
|
208
209
|
self.assertQuerysetEqualAndNotEmpty(filterset_result, qs_result, ordered=False)
|
|
209
210
|
|
|
211
|
+
def test_automagic_filters(self):
|
|
212
|
+
"""https://github.com/nautobot/nautobot/issues/6656"""
|
|
213
|
+
self.assertIsNotNone(self.filterset)
|
|
214
|
+
fs = self.filterset() # pylint: disable=not-callable
|
|
215
|
+
if getattr(self.queryset.model, "is_contact_associable_model", False):
|
|
216
|
+
self.assertIsInstance(fs.filters["contacts"], NaturalKeyOrPKMultipleChoiceFilter)
|
|
217
|
+
self.assertIsInstance(fs.filters["contacts__n"], NaturalKeyOrPKMultipleChoiceFilter)
|
|
218
|
+
self.assertIsInstance(fs.filters["teams"], NaturalKeyOrPKMultipleChoiceFilter)
|
|
219
|
+
self.assertIsInstance(fs.filters["teams__n"], NaturalKeyOrPKMultipleChoiceFilter)
|
|
220
|
+
|
|
221
|
+
if getattr(self.queryset.model, "is_dynamic_group_associable_model", False):
|
|
222
|
+
self.assertIsInstance(fs.filters["dynamic_groups"], NaturalKeyOrPKMultipleChoiceFilter)
|
|
223
|
+
self.assertIsInstance(fs.filters["dynamic_groups__n"], NaturalKeyOrPKMultipleChoiceFilter)
|
|
224
|
+
|
|
210
225
|
def test_boolean_filters_generic(self):
|
|
211
|
-
"""Test all `RelatedMembershipBooleanFilter` filters found in `self.filterset.
|
|
226
|
+
"""Test all `RelatedMembershipBooleanFilter` filters found in `self.filterset.filters`
|
|
212
227
|
except for the ones with custom filter logic defined in its `method` attribute.
|
|
213
228
|
|
|
214
229
|
This test asserts that `filter=True` matches `self.queryset.filter(field__isnull=False)` and
|
|
215
230
|
that `filter=False` matches `self.queryset.filter(field__isnull=True)`.
|
|
216
231
|
"""
|
|
217
232
|
self.assertIsNotNone(self.filterset)
|
|
218
|
-
for filter_name, filter_object in self.filterset
|
|
233
|
+
for filter_name, filter_object in self.filterset().filters.items(): # pylint: disable=not-callable
|
|
219
234
|
if not isinstance(filter_object, RelatedMembershipBooleanFilter):
|
|
220
235
|
continue
|
|
221
236
|
if filter_object.method is not None:
|
|
@@ -380,6 +395,8 @@ class FilterTestCases:
|
|
|
380
395
|
self._assert_q_filter_predicate_validity(obj, obj_field_name, filter_field_name, lookup_method)
|
|
381
396
|
|
|
382
397
|
def test_content_type_related_fields_uses_content_type_filter(self):
|
|
398
|
+
self.assertIsNotNone(self.filterset)
|
|
399
|
+
fs = self.filterset() # pylint: disable=not-callable
|
|
383
400
|
for field in self.queryset.model._meta.fields:
|
|
384
401
|
related_model = getattr(field, "related_model", None)
|
|
385
402
|
if not related_model or related_model != ContentType:
|
|
@@ -387,7 +404,7 @@ class FilterTestCases:
|
|
|
387
404
|
with self.subTest(
|
|
388
405
|
f"Assert {self.filterset.__class__.__name__}.{field.name} implements ContentTypeFilter"
|
|
389
406
|
):
|
|
390
|
-
filter_field =
|
|
407
|
+
filter_field = fs.filters.get(field.name)
|
|
391
408
|
if not filter_field:
|
|
392
409
|
# This field is not part of the Filterset.
|
|
393
410
|
continue
|
nautobot/core/testing/forms.py
CHANGED
|
@@ -24,7 +24,7 @@ class FormTestCases:
|
|
|
24
24
|
self.skipTest(f"{self.form_class.__name__}.{field_name} has no query_params")
|
|
25
25
|
field_model = field_class.queryset.model
|
|
26
26
|
filterset_class = get_filterset_for_model(field_model)
|
|
27
|
-
filterset_fields = set(filterset_class
|
|
27
|
+
filterset_fields = set(filterset_class().filters.keys())
|
|
28
28
|
invalid_query_params = query_params_fields - filterset_fields
|
|
29
29
|
self.assertFalse(
|
|
30
30
|
invalid_query_params,
|
|
@@ -148,7 +148,7 @@ class ListViewFilterTestCase(SeleniumTestCase):
|
|
|
148
148
|
|
|
149
149
|
def test_input_field_gets_updated(self):
|
|
150
150
|
"""Assert that a filter input/select field on Dynamic Filter Form updates if same field is updated."""
|
|
151
|
-
self.browser.visit(f
|
|
151
|
+
self.browser.visit(f"{self.live_server_url}{reverse('dcim:location_list')}")
|
|
152
152
|
|
|
153
153
|
text_field_name = self.custom_fields[0].add_prefix_to_cf_key()
|
|
154
154
|
integer_field_name = self.custom_fields[1].add_prefix_to_cf_key()
|
|
@@ -225,7 +225,7 @@ class ListViewFilterTestCase(SeleniumTestCase):
|
|
|
225
225
|
def test_advanced_filter_application(self):
|
|
226
226
|
"""Assert that filters are applied successfully when using the advanced filter."""
|
|
227
227
|
# Go to the location list view
|
|
228
|
-
self.browser.visit(f
|
|
228
|
+
self.browser.visit(f"{self.live_server_url}{reverse('dcim:location_list')}")
|
|
229
229
|
# create a new tag
|
|
230
230
|
tag = Tag.objects.create(name="Tag1")
|
|
231
231
|
tag.content_types.set([ContentType.objects.get_for_model(Location)])
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for the updated breadcrumbs.py following Nautobot testing conventions.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from operator import itemgetter
|
|
6
|
+
from unittest.mock import patch
|
|
7
|
+
|
|
8
|
+
from django.template import Context
|
|
9
|
+
from django.utils.http import urlencode
|
|
10
|
+
|
|
11
|
+
from nautobot.core.testing import TestCase
|
|
12
|
+
from nautobot.core.ui.breadcrumbs import (
|
|
13
|
+
BaseBreadcrumbItem,
|
|
14
|
+
Breadcrumbs,
|
|
15
|
+
InstanceBreadcrumbItem,
|
|
16
|
+
ModelBreadcrumbItem,
|
|
17
|
+
ViewNameBreadcrumbItem,
|
|
18
|
+
)
|
|
19
|
+
from nautobot.dcim.models import Device, LocationType
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BreadcrumbItemsTestCase(TestCase):
|
|
23
|
+
"""Test cases for BreadcrumbItem class."""
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def setUpTestData(cls):
|
|
27
|
+
"""Create test data."""
|
|
28
|
+
cls.location_type = LocationType.objects.create(name="Test Location Type Breadcrumbs")
|
|
29
|
+
|
|
30
|
+
def test_view_name_item(self):
|
|
31
|
+
"""Test breadcrumb view name item."""
|
|
32
|
+
item = ViewNameBreadcrumbItem(view_name="home", label="Home")
|
|
33
|
+
context = Context({})
|
|
34
|
+
|
|
35
|
+
url, label = item.as_pair(context)
|
|
36
|
+
|
|
37
|
+
self.assertEqual(url, "/")
|
|
38
|
+
self.assertEqual(label, "Home")
|
|
39
|
+
|
|
40
|
+
def test_view_name_item_with_kwargs_and_query_params(self):
|
|
41
|
+
"""Test breadcrumb view name item and kwargs."""
|
|
42
|
+
item = ViewNameBreadcrumbItem(
|
|
43
|
+
view_name="dcim:locationtype",
|
|
44
|
+
reverse_kwargs={"pk": self.location_type.pk},
|
|
45
|
+
reverse_query_params={"name": "test"},
|
|
46
|
+
label="Filtered Locations Types",
|
|
47
|
+
)
|
|
48
|
+
context = Context({})
|
|
49
|
+
|
|
50
|
+
url, label = item.as_pair(context)
|
|
51
|
+
|
|
52
|
+
self.assertEqual(url, f"/dcim/location-types/{self.location_type.pk}/?name=test")
|
|
53
|
+
self.assertEqual(label, "Filtered Locations Types")
|
|
54
|
+
|
|
55
|
+
def test_view_name_item_with_kwargs_and_query_params_callable(self):
|
|
56
|
+
"""Test breadcrumb view name item and kwargs."""
|
|
57
|
+
item = ViewNameBreadcrumbItem(
|
|
58
|
+
view_name="dcim:locationtype",
|
|
59
|
+
reverse_kwargs=lambda c: {"pk": c["object"].pk},
|
|
60
|
+
reverse_query_params=lambda c: {"name": c["object"].name},
|
|
61
|
+
label="Filtered Locations Types",
|
|
62
|
+
)
|
|
63
|
+
context = Context({"object": self.location_type})
|
|
64
|
+
|
|
65
|
+
url, label = item.as_pair(context)
|
|
66
|
+
|
|
67
|
+
self.assertEqual(
|
|
68
|
+
url, f"/dcim/location-types/{self.location_type.pk}/?{urlencode({'name': self.location_type.name})}"
|
|
69
|
+
)
|
|
70
|
+
self.assertEqual(label, "Filtered Locations Types")
|
|
71
|
+
|
|
72
|
+
def test_callable_label_and_view_name(self):
|
|
73
|
+
"""Test label and view_name as callables."""
|
|
74
|
+
item = ViewNameBreadcrumbItem(
|
|
75
|
+
view_name=lambda _: "home",
|
|
76
|
+
label=lambda context: f"Hi, {context['user']}!",
|
|
77
|
+
)
|
|
78
|
+
context = Context({"user": "Frodo"})
|
|
79
|
+
url, label = item.as_pair(context)
|
|
80
|
+
self.assertEqual(url, "/")
|
|
81
|
+
self.assertEqual(label, "Hi, Frodo!")
|
|
82
|
+
|
|
83
|
+
def test_model_items(self):
|
|
84
|
+
"""Test breadcrumb model items."""
|
|
85
|
+
test_cases = [
|
|
86
|
+
{
|
|
87
|
+
"name": "model_class",
|
|
88
|
+
"kwargs": {"model": Device},
|
|
89
|
+
"expected_url": "/dcim/devices/",
|
|
90
|
+
"expected_label": "Devices",
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"name": "model_instance",
|
|
94
|
+
"kwargs": {"model": self.location_type},
|
|
95
|
+
"expected_url": "/dcim/location-types/",
|
|
96
|
+
"expected_label": "Location Types",
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"name": "model_instance_callable",
|
|
100
|
+
"kwargs": {"model": itemgetter("object")},
|
|
101
|
+
"expected_url": "/dcim/location-types/",
|
|
102
|
+
"expected_label": "Location Types",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"name": "model_str",
|
|
106
|
+
"kwargs": {"model": "dcim.device"},
|
|
107
|
+
"expected_url": "/dcim/devices/",
|
|
108
|
+
"expected_label": "Devices",
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"name": "model_class_custom_url_action",
|
|
112
|
+
"kwargs": {"model": Device, "action": "add", "label_type": "singular"},
|
|
113
|
+
"expected_url": "/dcim/devices/add/",
|
|
114
|
+
"expected_label": "Device",
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"name": "model_class_with_kwargs",
|
|
118
|
+
"kwargs": {
|
|
119
|
+
"model": Device,
|
|
120
|
+
"action": "",
|
|
121
|
+
"label_type": "singular",
|
|
122
|
+
"reverse_kwargs": {"pk": "947a8a80-9e62-5605-ab18-7a47c588f0ad"},
|
|
123
|
+
},
|
|
124
|
+
"expected_url": "/dcim/devices/947a8a80-9e62-5605-ab18-7a47c588f0ad/",
|
|
125
|
+
"expected_label": "Device",
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"name": "model_class_with_query_params",
|
|
129
|
+
"kwargs": {"model": Device, "reverse_query_params": {"filter": "abc"}},
|
|
130
|
+
"expected_url": "/dcim/devices/?filter=abc",
|
|
131
|
+
"expected_label": "Devices",
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"name": "model_class_with_query_params_callable",
|
|
135
|
+
"kwargs": {
|
|
136
|
+
"model": itemgetter("model_type"),
|
|
137
|
+
"reverse_query_params": lambda c: {"name": c["device_name"]},
|
|
138
|
+
},
|
|
139
|
+
"expected_url": "/dcim/devices/?name=abc",
|
|
140
|
+
"expected_label": "Devices",
|
|
141
|
+
},
|
|
142
|
+
]
|
|
143
|
+
for test_case in test_cases:
|
|
144
|
+
with self.subTest(action=test_case["name"]):
|
|
145
|
+
item = ModelBreadcrumbItem(**test_case["kwargs"])
|
|
146
|
+
context = Context({"object": self.location_type, "model_type": Device, "device_name": "abc"})
|
|
147
|
+
|
|
148
|
+
url, label = item.as_pair(context)
|
|
149
|
+
|
|
150
|
+
self.assertEqual(url, test_case["expected_url"])
|
|
151
|
+
self.assertEqual(label, test_case["expected_label"])
|
|
152
|
+
|
|
153
|
+
def test_model_item_from_context(self):
|
|
154
|
+
"""Test breadcrumb item with model from context."""
|
|
155
|
+
item = ModelBreadcrumbItem(model_key="object")
|
|
156
|
+
context = Context({"object": self.location_type})
|
|
157
|
+
|
|
158
|
+
url, label = item.as_pair(context)
|
|
159
|
+
|
|
160
|
+
self.assertEqual(url, "/dcim/location-types/")
|
|
161
|
+
self.assertEqual(label, "Location Types")
|
|
162
|
+
|
|
163
|
+
def test_instance_item(self):
|
|
164
|
+
"""Test breadcrumb item with instance from context."""
|
|
165
|
+
item = InstanceBreadcrumbItem(instance_key="object")
|
|
166
|
+
context = Context({"object": self.location_type})
|
|
167
|
+
|
|
168
|
+
url, label = item.as_pair(context)
|
|
169
|
+
|
|
170
|
+
self.assertEqual(url, f"/dcim/location-types/{self.location_type.pk}/")
|
|
171
|
+
self.assertEqual(label, str(self.location_type))
|
|
172
|
+
|
|
173
|
+
def test_label_override(self):
|
|
174
|
+
"""Test that explicit label overrides automatic label generation."""
|
|
175
|
+
items = [
|
|
176
|
+
ViewNameBreadcrumbItem(view_name="dcim:locationtype", label="Custom Label"),
|
|
177
|
+
ModelBreadcrumbItem(model=LocationType, label="Custom Label"),
|
|
178
|
+
InstanceBreadcrumbItem(label="Custom Label"),
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
context = Context({"object": self.location_type})
|
|
182
|
+
|
|
183
|
+
for item in items:
|
|
184
|
+
with self.subTest():
|
|
185
|
+
_, label = item.as_pair(context)
|
|
186
|
+
self.assertEqual(label, "Custom Label")
|
|
187
|
+
|
|
188
|
+
def test_no_reverse_match(self):
|
|
189
|
+
"""Test handling of NoReverseMatch exception."""
|
|
190
|
+
item = ViewNameBreadcrumbItem(view_name="nonexistent")
|
|
191
|
+
context = Context({})
|
|
192
|
+
|
|
193
|
+
url, label = item.as_pair(context)
|
|
194
|
+
|
|
195
|
+
self.assertEqual(url, "")
|
|
196
|
+
self.assertEqual(label, "")
|
|
197
|
+
|
|
198
|
+
def test_empty_context_keys(self):
|
|
199
|
+
"""Test breadcrumb item when context keys are missing."""
|
|
200
|
+
context = Context({})
|
|
201
|
+
item = InstanceBreadcrumbItem(instance_key="missing_key")
|
|
202
|
+
|
|
203
|
+
url, label = item.as_pair(context)
|
|
204
|
+
self.assertEqual(url, "")
|
|
205
|
+
self.assertEqual(label, "")
|
|
206
|
+
|
|
207
|
+
item = ModelBreadcrumbItem(model_key="missing_key")
|
|
208
|
+
url, label = item.as_pair(context)
|
|
209
|
+
self.assertEqual(url, "")
|
|
210
|
+
self.assertEqual(label, "")
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class BreadcrumbsTestCase(TestCase):
|
|
214
|
+
"""Test cases for Breadcrumbs class."""
|
|
215
|
+
|
|
216
|
+
@classmethod
|
|
217
|
+
def setUpTestData(cls):
|
|
218
|
+
"""Create test data."""
|
|
219
|
+
cls.location_type = LocationType.objects.create(name="Test Location Type Breadcrumbs")
|
|
220
|
+
|
|
221
|
+
def test_default_initialization(self):
|
|
222
|
+
"""Test that Breadcrumbs initializes with default values."""
|
|
223
|
+
breadcrumbs = Breadcrumbs()
|
|
224
|
+
|
|
225
|
+
# Should have defaults for list and details
|
|
226
|
+
self.assertEqual(len(breadcrumbs.items["list"]), 2)
|
|
227
|
+
self.assertEqual(len(breadcrumbs.items["detail"]), 3)
|
|
228
|
+
|
|
229
|
+
# Verify adding items
|
|
230
|
+
new_item = BaseBreadcrumbItem()
|
|
231
|
+
breadcrumbs = Breadcrumbs(items={"detail": [new_item], "list": [new_item], "custom_action": [new_item]})
|
|
232
|
+
|
|
233
|
+
self.assertEqual(len(breadcrumbs.items["list"]), 1)
|
|
234
|
+
self.assertEqual(breadcrumbs.items["list"][0], new_item)
|
|
235
|
+
|
|
236
|
+
self.assertEqual(len(breadcrumbs.items["detail"]), 2)
|
|
237
|
+
self.assertEqual(breadcrumbs.items["detail"][0], new_item)
|
|
238
|
+
|
|
239
|
+
self.assertEqual(len(breadcrumbs.items["custom_action"]), 1)
|
|
240
|
+
self.assertEqual(breadcrumbs.items["custom_action"][0], new_item)
|
|
241
|
+
|
|
242
|
+
def test_custom_items(self):
|
|
243
|
+
"""Test Breadcrumbs with custom items."""
|
|
244
|
+
custom_list_item = ViewNameBreadcrumbItem(view_name="home", label="Home")
|
|
245
|
+
custom_items = {
|
|
246
|
+
"list": [ViewNameBreadcrumbItem(view_name="home", label="Home")],
|
|
247
|
+
}
|
|
248
|
+
breadcrumbs = Breadcrumbs(items=custom_items)
|
|
249
|
+
|
|
250
|
+
self.assertEqual(len(breadcrumbs.items["list"]), 1)
|
|
251
|
+
self.assertEqual(breadcrumbs.items["list"][0], custom_list_item)
|
|
252
|
+
|
|
253
|
+
# Other defaults should still exist
|
|
254
|
+
self.assertIn("detail", breadcrumbs.items)
|
|
255
|
+
self.assertEqual(len(breadcrumbs.items["detail"]), 3)
|
|
256
|
+
|
|
257
|
+
def test_get_items_from_action_static_method(self):
|
|
258
|
+
"""Test the _get_items_from_action static method."""
|
|
259
|
+
test_items = {
|
|
260
|
+
"list": [ViewNameBreadcrumbItem(view_name="", label="List Item")],
|
|
261
|
+
"detail": [ViewNameBreadcrumbItem(view_name="", label="Detail Item")],
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
# Test specific action found
|
|
265
|
+
result = Breadcrumbs.get_items_for_action(test_items, "list", False)
|
|
266
|
+
self.assertEqual(len(result), 1)
|
|
267
|
+
self.assertEqual(result[0].label, "List Item")
|
|
268
|
+
|
|
269
|
+
# Test fallback to detail when action not found and detail=True
|
|
270
|
+
result = Breadcrumbs.get_items_for_action(test_items, "nonexistent", True)
|
|
271
|
+
self.assertEqual(len(result), 1)
|
|
272
|
+
self.assertEqual(result[0].label, "Detail Item")
|
|
273
|
+
|
|
274
|
+
# Test no fallback when detail=False
|
|
275
|
+
result = Breadcrumbs.get_items_for_action(test_items, "nonexistent", False)
|
|
276
|
+
self.assertEqual(len(result), 0)
|
|
277
|
+
self.assertEqual(result, [])
|
|
278
|
+
|
|
279
|
+
def test_detail_fallback_behavior(self):
|
|
280
|
+
"""Test that detail fallback works correctly in get_breadcrumbs_items."""
|
|
281
|
+
breadcrumbs = Breadcrumbs()
|
|
282
|
+
expected_items = [
|
|
283
|
+
("/dcim/location-types/", "Location Types"),
|
|
284
|
+
(f"/dcim/location-types/{self.location_type.pk}/", str(self.location_type)),
|
|
285
|
+
]
|
|
286
|
+
|
|
287
|
+
# Test with an action that doesn't exist but detail=True
|
|
288
|
+
context = Context(
|
|
289
|
+
{"view_action": "custom_action", "detail": True, "model": LocationType, "object": self.location_type}
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
items = breadcrumbs.get_breadcrumbs_items(context)
|
|
293
|
+
|
|
294
|
+
# Should get 2 items from detail fallback
|
|
295
|
+
self.assertEqual(len(items), 2)
|
|
296
|
+
self.assertEqual(items, expected_items)
|
|
297
|
+
|
|
298
|
+
def test_render_method(self):
|
|
299
|
+
"""Test the render method."""
|
|
300
|
+
breadcrumbs = Breadcrumbs()
|
|
301
|
+
context = Context({"view_action": "list", "model": Device})
|
|
302
|
+
|
|
303
|
+
html = breadcrumbs.render(context)
|
|
304
|
+
|
|
305
|
+
expected_html = """<ol class="breadcrumb"><li><a href="/dcim/devices/">Devices</a></li></ol>"""
|
|
306
|
+
self.assertHTMLEqual(html, expected_html)
|
|
307
|
+
|
|
308
|
+
def test_get_extra_context(self):
|
|
309
|
+
"""Test that get_extra_context can be extended."""
|
|
310
|
+
|
|
311
|
+
class CustomBreadcrumbs(Breadcrumbs):
|
|
312
|
+
def get_extra_context(self, context: Context):
|
|
313
|
+
return {"custom_key": "custom_value"}
|
|
314
|
+
|
|
315
|
+
render_context = {}
|
|
316
|
+
|
|
317
|
+
def capture_context(template, context, **kwargs):
|
|
318
|
+
nonlocal render_context
|
|
319
|
+
render_context = context.flatten()
|
|
320
|
+
return ""
|
|
321
|
+
|
|
322
|
+
breadcrumbs = CustomBreadcrumbs(items={"list": []})
|
|
323
|
+
context = Context({})
|
|
324
|
+
|
|
325
|
+
with patch("nautobot.core.ui.breadcrumbs.render_component_template", side_effect=capture_context):
|
|
326
|
+
breadcrumbs.render(context)
|
|
327
|
+
|
|
328
|
+
# Check that custom context was passed
|
|
329
|
+
self.assertEqual(render_context.get("custom_key"), "custom_value")
|
|
330
|
+
|
|
331
|
+
def test_should_render_skips_items(self):
|
|
332
|
+
"""Breadcrumbs should skip items where should_render(context) is False."""
|
|
333
|
+
|
|
334
|
+
item_visible = ViewNameBreadcrumbItem(view_name="home", label="Visible", should_render=lambda _: True)
|
|
335
|
+
item_hidden = ViewNameBreadcrumbItem(view_name="home", label="Hidden", should_render=lambda _: False)
|
|
336
|
+
breadcrumbs = Breadcrumbs(items={"custom_action": [item_visible, item_hidden]})
|
|
337
|
+
context = Context({"view_action": "custom_action"})
|
|
338
|
+
|
|
339
|
+
items = breadcrumbs.get_breadcrumbs_items(context)
|
|
340
|
+
self.assertEqual(len(items), 1)
|
|
341
|
+
self.assertIn(("/", "Visible"), items)
|
|
342
|
+
self.assertNotIn(("/", "Hidden"), items)
|
|
343
|
+
|
|
344
|
+
def test_filter_breadcrumbs_items_removes_empty_pairs(self):
|
|
345
|
+
"""filter_breadcrumbs_items should remove items where label is empty or None."""
|
|
346
|
+
|
|
347
|
+
breadcrumbs = Breadcrumbs()
|
|
348
|
+
# (url, label) pairs: only the last should remain
|
|
349
|
+
pairs = [
|
|
350
|
+
("", ""), # empty
|
|
351
|
+
("", " "), # whitespace
|
|
352
|
+
("", "\t"), # whitespace
|
|
353
|
+
("", "Non-empty"), # label not empty
|
|
354
|
+
("/foo", ""), # url not empty
|
|
355
|
+
("/foo", "Label"), # url, label not empty
|
|
356
|
+
(None, None), # both None
|
|
357
|
+
(None, "Label"), # label not None
|
|
358
|
+
("/bar", None), # url not None
|
|
359
|
+
]
|
|
360
|
+
expected = [
|
|
361
|
+
("", "Non-empty"),
|
|
362
|
+
("/foo", "Label"),
|
|
363
|
+
(None, "Label"),
|
|
364
|
+
]
|
|
365
|
+
filtered = breadcrumbs.filter_breadcrumbs_items(pairs, Context({}))
|
|
366
|
+
self.assertEqual(filtered, expected)
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
from io import StringIO
|
|
2
2
|
|
|
3
3
|
from django.core.management import call_command
|
|
4
|
+
from django.utils.timezone import now
|
|
4
5
|
import yaml
|
|
5
6
|
|
|
7
|
+
from nautobot.core.management.commands.check_job_approval_status import ApprovalRequiredScheduledJobsError
|
|
6
8
|
from nautobot.core.testing import TestCase
|
|
9
|
+
from nautobot.extras.choices import JobExecutionType
|
|
10
|
+
from nautobot.extras.models import Job, ScheduledJob
|
|
7
11
|
|
|
8
12
|
|
|
9
13
|
class ManagementCommandTestCase(TestCase):
|
|
@@ -29,3 +33,39 @@ class ManagementCommandTestCase(TestCase):
|
|
|
29
33
|
self.assertHttpStatus(
|
|
30
34
|
response, 200, f"{view_name}: {endpoint} returns status Code {response.status_code} instead of 200"
|
|
31
35
|
)
|
|
36
|
+
|
|
37
|
+
def test_check_job_approval_status_no_jobs(self):
|
|
38
|
+
out = StringIO()
|
|
39
|
+
# update all jobs to not have approval_required=True
|
|
40
|
+
Job.objects.update(approval_required=False)
|
|
41
|
+
call_command("check_job_approval_status", stdout=out)
|
|
42
|
+
output = out.getvalue()
|
|
43
|
+
self.assertIn("No approval_required jobs or scheduled jobs found.", output)
|
|
44
|
+
|
|
45
|
+
def test_check_job_approval_status_with__with_approval_required_jobs(self):
|
|
46
|
+
out = StringIO()
|
|
47
|
+
self.assertTrue(Job.objects.filter(approval_required=True).exists())
|
|
48
|
+
self.assertFalse(ScheduledJob.objects.filter(approval_required=True).exists())
|
|
49
|
+
call_command("check_job_approval_status", stdout=out)
|
|
50
|
+
output = out.getvalue()
|
|
51
|
+
self.assertIn("Following jobs still have `approval_required=True`.", output)
|
|
52
|
+
|
|
53
|
+
def test_check_job_approval_status_with_approval_required_scheduled_jobs(self):
|
|
54
|
+
job = Job.objects.first()
|
|
55
|
+
scheduled_job = ScheduledJob.objects.create(
|
|
56
|
+
name="Scheduled Job",
|
|
57
|
+
task="test_managment_command.TestManagmentCommand",
|
|
58
|
+
job_model=job,
|
|
59
|
+
interval=JobExecutionType.TYPE_IMMEDIATELY,
|
|
60
|
+
user=self.user,
|
|
61
|
+
approval_required=True,
|
|
62
|
+
start_time=now(),
|
|
63
|
+
)
|
|
64
|
+
self.assertTrue(ScheduledJob.objects.filter(approval_required=True).exists())
|
|
65
|
+
with self.assertRaises(ApprovalRequiredScheduledJobsError) as cm:
|
|
66
|
+
call_command("check_job_approval_status")
|
|
67
|
+
|
|
68
|
+
self.assertIn(
|
|
69
|
+
"These need to be approved (and run) or denied before upgrading to Nautobot v3", str(cm.exception)
|
|
70
|
+
)
|
|
71
|
+
self.assertIn(scheduled_job.name, str(cm.exception))
|
|
@@ -19,10 +19,60 @@ from nautobot.core.utils import lookup
|
|
|
19
19
|
from nautobot.dcim import choices as dcim_choices, filters as dcim_filters, models as dcim_models
|
|
20
20
|
from nautobot.dcim.models import Controller, Device
|
|
21
21
|
from nautobot.extras import models as extras_models
|
|
22
|
-
from nautobot.extras.utils import FeatureQuery
|
|
22
|
+
from nautobot.extras.utils import FeatureQuery, RoleModelsQuery
|
|
23
23
|
from nautobot.ipam import models as ipam_models
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
class ContentTypeMultipleChoiceFilterTest(testing.TestCase):
|
|
27
|
+
class RoleFilterSet(filters.BaseFilterSet):
|
|
28
|
+
content_types = filters.ContentTypeMultipleChoiceFilter(
|
|
29
|
+
choices=RoleModelsQuery().get_choices,
|
|
30
|
+
conjoined=False,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
class Meta:
|
|
34
|
+
model = extras_models.Role
|
|
35
|
+
fields = ["content_types"]
|
|
36
|
+
|
|
37
|
+
def test_filter_variations(self):
|
|
38
|
+
with self.subTest("single label"):
|
|
39
|
+
filterset = self.RoleFilterSet({"content_types": ["ipam.ipaddress"]}, extras_models.Role.objects.all())
|
|
40
|
+
qs = extras_models.Role.objects.filter(
|
|
41
|
+
content_types__in=[ContentType.objects.get_for_model(ipam_models.IPAddress)]
|
|
42
|
+
)
|
|
43
|
+
self.assertQuerysetEqualAndNotEmpty(filterset.qs, qs)
|
|
44
|
+
|
|
45
|
+
with self.subTest("multiple labels"):
|
|
46
|
+
filterset = self.RoleFilterSet(
|
|
47
|
+
{"content_types": ["ipam.ipaddress", "dcim.rack"]}, extras_models.Role.objects.all()
|
|
48
|
+
)
|
|
49
|
+
# remember, conjoined=False
|
|
50
|
+
qs = extras_models.Role.objects.filter(
|
|
51
|
+
django_models.Q(content_types__in=[ContentType.objects.get_for_model(ipam_models.IPAddress)])
|
|
52
|
+
| django_models.Q(content_types__in=[ContentType.objects.get_for_model(dcim_models.Rack)])
|
|
53
|
+
).distinct()
|
|
54
|
+
self.assertQuerysetEqualAndNotEmpty(filterset.qs, qs)
|
|
55
|
+
|
|
56
|
+
with self.subTest("exclude single label"):
|
|
57
|
+
filterset = self.RoleFilterSet({"content_types__n": ["ipam.ipaddress"]}, extras_models.Role.objects.all())
|
|
58
|
+
qs = extras_models.Role.objects.exclude(
|
|
59
|
+
content_types__in=[ContentType.objects.get_for_model(ipam_models.IPAddress)]
|
|
60
|
+
)
|
|
61
|
+
self.assertQuerysetEqualAndNotEmpty(filterset.qs, qs)
|
|
62
|
+
|
|
63
|
+
with self.subTest("exclude multiple labels"):
|
|
64
|
+
filterset = self.RoleFilterSet(
|
|
65
|
+
{"content_types__n": ["ipam.ipaddress", "dcim.rack"]}, extras_models.Role.objects.all()
|
|
66
|
+
)
|
|
67
|
+
self.assertTrue(filterset.is_valid(), filterset.errors)
|
|
68
|
+
# remember, conjoined=False
|
|
69
|
+
qs = extras_models.Role.objects.exclude(
|
|
70
|
+
django_models.Q(content_types__in=[ContentType.objects.get_for_model(ipam_models.IPAddress)])
|
|
71
|
+
| django_models.Q(content_types__in=[ContentType.objects.get_for_model(dcim_models.Rack)])
|
|
72
|
+
).distinct()
|
|
73
|
+
self.assertQuerysetEqualAndNotEmpty(filterset.qs, qs)
|
|
74
|
+
|
|
75
|
+
|
|
26
76
|
class TreeNodeMultipleChoiceFilterTest(TestCase):
|
|
27
77
|
class LocationFilterSet(filters.BaseFilterSet):
|
|
28
78
|
parent = filters.TreeNodeMultipleChoiceFilter(queryset=dcim_models.Location.objects.all())
|
|
@@ -581,7 +581,7 @@ class JSONFieldTest(testing.TestCase):
|
|
|
581
581
|
device_content_type = ContentType.objects.get_for_model(dcim_models.Device)
|
|
582
582
|
custom_field.content_types.set([device_content_type])
|
|
583
583
|
# Fetch URL with filter parameter
|
|
584
|
-
response = self.client.get(f
|
|
584
|
+
response = self.client.get(f"{reverse('dcim:device_list')}?name=Foo%20Device")
|
|
585
585
|
self.assertIn("Foo Device", str(response.content))
|
|
586
586
|
|
|
587
587
|
def test_prepare_value_with_utf8(self):
|
|
@@ -527,18 +527,18 @@ class GraphQLAPIPermissionTest(GraphQLTestCaseBase):
|
|
|
527
527
|
for i in range(2):
|
|
528
528
|
# Rack permission
|
|
529
529
|
rack_obj_permission = ObjectPermission.objects.create(
|
|
530
|
-
name=f"Permission Rack {i+1}",
|
|
530
|
+
name=f"Permission Rack {i + 1}",
|
|
531
531
|
actions=["view", "add", "change", "delete"],
|
|
532
|
-
constraints={"location__name": f"Location {i+1}"},
|
|
532
|
+
constraints={"location__name": f"Location {i + 1}"},
|
|
533
533
|
)
|
|
534
534
|
rack_obj_permission.object_types.add(rack_object_type)
|
|
535
535
|
rack_obj_permission.groups.add(cls.groups[i])
|
|
536
536
|
rack_obj_permission.users.add(cls.users[i])
|
|
537
537
|
|
|
538
538
|
location_obj_permission = ObjectPermission.objects.create(
|
|
539
|
-
name=f"Permission Location {i+1}",
|
|
539
|
+
name=f"Permission Location {i + 1}",
|
|
540
540
|
actions=["view", "add", "change", "delete"],
|
|
541
|
-
constraints={"name": f"Location {i+1}"},
|
|
541
|
+
constraints={"name": f"Location {i + 1}"},
|
|
542
542
|
)
|
|
543
543
|
location_obj_permission.object_types.add(location_object_type)
|
|
544
544
|
location_obj_permission.groups.add(cls.groups[i])
|