nautobot 2.4.0b1__py3-none-any.whl → 2.4.1__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/apps/__init__.py +1 -1
- nautobot/apps/api.py +8 -8
- nautobot/apps/change_logging.py +2 -2
- nautobot/apps/choices.py +4 -4
- nautobot/apps/events.py +3 -3
- nautobot/apps/factory.py +2 -2
- nautobot/apps/filters.py +1 -1
- nautobot/apps/forms.py +20 -20
- nautobot/apps/graphql.py +2 -2
- nautobot/apps/jobs.py +8 -8
- nautobot/apps/models.py +19 -19
- nautobot/apps/tables.py +1 -1
- nautobot/apps/testing.py +10 -10
- nautobot/apps/ui.py +2 -2
- nautobot/apps/utils.py +7 -7
- nautobot/apps/views.py +7 -7
- nautobot/circuits/api/serializers.py +1 -0
- nautobot/circuits/api/views.py +4 -8
- nautobot/circuits/tables.py +2 -1
- nautobot/circuits/templates/circuits/circuit_create.html +1 -7
- nautobot/circuits/views.py +3 -3
- nautobot/cloud/api/views.py +6 -10
- nautobot/cloud/models.py +1 -1
- nautobot/cloud/views.py +0 -16
- nautobot/core/api/constants.py +11 -0
- nautobot/core/api/fields.py +5 -5
- nautobot/core/api/filter_backends.py +3 -9
- nautobot/core/api/schema.py +13 -2
- nautobot/core/api/serializers.py +40 -34
- nautobot/core/api/views.py +56 -4
- nautobot/core/celery/log.py +4 -4
- nautobot/core/celery/schedulers.py +2 -2
- nautobot/core/choices.py +2 -2
- nautobot/core/events/__init__.py +3 -3
- nautobot/core/filters.py +67 -35
- nautobot/core/forms/__init__.py +19 -19
- nautobot/core/forms/fields.py +14 -11
- nautobot/core/forms/forms.py +33 -2
- nautobot/core/graphql/types.py +1 -1
- nautobot/core/jobs/__init__.py +28 -7
- nautobot/core/jobs/bulk_actions.py +285 -0
- nautobot/core/jobs/cleanup.py +48 -12
- nautobot/core/jobs/groups.py +1 -1
- nautobot/core/management/commands/validate_models.py +1 -1
- nautobot/core/models/__init__.py +3 -1
- nautobot/core/models/query_functions.py +2 -2
- nautobot/core/models/tree_queries.py +6 -3
- nautobot/core/settings.py +29 -2
- nautobot/core/settings.yaml +21 -0
- nautobot/core/tables.py +79 -61
- nautobot/core/templates/about.html +67 -0
- nautobot/core/templates/inc/media.html +3 -0
- nautobot/core/templates/inc/nav_menu.html +1 -0
- nautobot/core/templates/inc/tenancy_form_panel.html +9 -0
- nautobot/core/templates/inc/tenant_table_row.html +11 -0
- nautobot/core/templates/nautobot_config.py.j2 +13 -0
- nautobot/core/templates/search.html +7 -0
- nautobot/core/templates/utilities/render_jinja2.html +1 -1
- nautobot/core/templates/utilities/templatetags/tag.html +1 -1
- nautobot/core/templates/utilities/theme_preview.html +7 -0
- nautobot/core/templatetags/helpers.py +11 -2
- nautobot/core/testing/__init__.py +8 -8
- nautobot/core/testing/api.py +170 -15
- nautobot/core/testing/filters.py +45 -10
- nautobot/core/testing/forms.py +2 -0
- nautobot/core/testing/integration.py +86 -4
- nautobot/core/testing/mixins.py +7 -2
- nautobot/core/testing/views.py +44 -29
- nautobot/core/tests/integration/test_app_home.py +0 -1
- nautobot/core/tests/integration/test_app_navbar.py +0 -1
- nautobot/core/tests/integration/test_filters.py +0 -2
- nautobot/core/tests/integration/test_home.py +0 -1
- nautobot/core/tests/integration/test_navbar.py +0 -1
- nautobot/core/tests/integration/test_view_authentication.py +1 -0
- nautobot/core/tests/runner.py +1 -1
- nautobot/core/tests/test_api.py +98 -1
- nautobot/core/tests/test_csv.py +25 -3
- nautobot/core/tests/test_filters.py +209 -246
- nautobot/core/tests/test_forms.py +1 -0
- nautobot/core/tests/test_jobs.py +460 -1
- nautobot/core/tests/test_models.py +9 -0
- nautobot/core/tests/test_settings_schema.py +7 -0
- nautobot/core/tests/test_tables.py +100 -0
- nautobot/core/tests/test_utils.py +63 -1
- nautobot/core/tests/test_views.py +30 -3
- nautobot/core/ui/nav.py +1 -0
- nautobot/core/ui/object_detail.py +15 -1
- nautobot/core/urls.py +11 -0
- nautobot/core/utils/lookup.py +11 -8
- nautobot/core/utils/querysets.py +64 -0
- nautobot/core/utils/requests.py +24 -9
- nautobot/core/views/__init__.py +42 -0
- nautobot/core/views/generic.py +131 -197
- nautobot/core/views/mixins.py +126 -38
- nautobot/core/views/renderers.py +6 -6
- nautobot/dcim/api/serializers.py +56 -64
- nautobot/dcim/api/views.py +47 -113
- nautobot/dcim/constants.py +6 -13
- nautobot/dcim/factory.py +6 -1
- nautobot/dcim/filters/__init__.py +31 -2
- nautobot/dcim/forms.py +36 -17
- nautobot/dcim/graphql/types.py +2 -2
- nautobot/dcim/migrations/0067_controllermanageddevicegroup_tenant.py +25 -0
- nautobot/dcim/models/__init__.py +1 -1
- nautobot/dcim/models/device_component_templates.py +2 -2
- nautobot/dcim/models/device_components.py +22 -20
- nautobot/dcim/models/devices.py +10 -1
- nautobot/dcim/models/locations.py +3 -3
- nautobot/dcim/models/power.py +6 -5
- nautobot/dcim/models/racks.py +4 -4
- nautobot/dcim/tables/__init__.py +3 -3
- nautobot/dcim/tables/devices.py +7 -5
- nautobot/dcim/tables/devicetypes.py +2 -2
- nautobot/dcim/tables/racks.py +1 -1
- nautobot/dcim/templates/dcim/controller_create.html +1 -7
- nautobot/dcim/templates/dcim/controller_retrieve.html +1 -9
- nautobot/dcim/templates/dcim/controllermanageddevicegroup_create.html +2 -0
- nautobot/dcim/templates/dcim/controllermanageddevicegroup_retrieve.html +5 -0
- nautobot/dcim/templates/dcim/device.html +1 -9
- nautobot/dcim/templates/dcim/device_edit.html +36 -37
- nautobot/dcim/templates/dcim/location.html +1 -9
- nautobot/dcim/templates/dcim/location_edit.html +1 -7
- nautobot/dcim/templates/dcim/rack.html +1 -9
- nautobot/dcim/templates/dcim/rack_edit.html +1 -7
- nautobot/dcim/templates/dcim/rackreservation.html +1 -9
- nautobot/dcim/templates/dcim/virtualdevicecontext_retrieve.html +1 -9
- nautobot/dcim/templates/dcim/virtualdevicecontext_update.html +1 -7
- nautobot/dcim/tests/integration/test_controller.py +62 -0
- nautobot/dcim/tests/integration/test_controller_managed_device_group.py +71 -0
- nautobot/dcim/tests/integration/test_device_bulk_delete.py +189 -0
- nautobot/dcim/tests/integration/test_device_bulk_edit.py +181 -0
- nautobot/dcim/tests/test_api.py +16 -5
- nautobot/dcim/tests/test_filters.py +33 -0
- nautobot/dcim/tests/test_forms.py +51 -2
- nautobot/dcim/tests/test_graphql.py +52 -0
- nautobot/dcim/tests/test_jobs.py +118 -0
- nautobot/dcim/tests/test_models.py +52 -9
- nautobot/dcim/tests/test_views.py +21 -83
- nautobot/dcim/views.py +1 -13
- nautobot/extras/api/customfields.py +2 -2
- nautobot/extras/api/serializers.py +90 -85
- nautobot/extras/api/views.py +22 -27
- nautobot/extras/constants.py +2 -0
- nautobot/extras/filters/__init__.py +8 -6
- nautobot/extras/forms/base.py +2 -2
- nautobot/extras/forms/forms.py +139 -31
- nautobot/extras/forms/mixins.py +14 -6
- nautobot/extras/group_sync.py +3 -3
- nautobot/extras/health_checks.py +1 -2
- nautobot/extras/jobs.py +85 -18
- nautobot/extras/managers.py +3 -1
- nautobot/extras/migrations/0018_joblog_data_migration.py +7 -9
- nautobot/extras/migrations/0120_job_is_singleton_job_is_singleton_override.py +22 -0
- nautobot/extras/migrations/0121_alter_team_contacts.py +17 -0
- nautobot/extras/models/__init__.py +1 -1
- nautobot/extras/models/contacts.py +1 -1
- nautobot/extras/models/customfields.py +12 -11
- nautobot/extras/models/groups.py +11 -9
- nautobot/extras/models/jobs.py +23 -4
- nautobot/extras/models/models.py +2 -2
- nautobot/extras/plugins/__init__.py +13 -2
- nautobot/extras/plugins/marketplace_manifest.yml +84 -79
- nautobot/extras/plugins/tables.py +16 -14
- nautobot/extras/plugins/views.py +65 -69
- nautobot/extras/registry.py +1 -1
- nautobot/extras/secrets/__init__.py +2 -2
- nautobot/extras/tables.py +7 -5
- nautobot/extras/templates/extras/dynamicgroup.html +1 -9
- nautobot/extras/templates/extras/job_detail.html +16 -0
- nautobot/extras/templates/extras/job_edit.html +1 -0
- nautobot/extras/templates/extras/jobqueue_retrieve.html +1 -9
- nautobot/extras/templates/extras/marketplace.html +29 -11
- nautobot/extras/templates/extras/plugin_detail.html +32 -15
- nautobot/extras/templates/extras/plugins_tiles.html +21 -10
- nautobot/extras/templatetags/job_buttons.py +4 -4
- nautobot/extras/test_jobs/api_test_job.py +1 -1
- nautobot/extras/test_jobs/atomic_transaction.py +2 -2
- nautobot/extras/test_jobs/dry_run.py +1 -1
- nautobot/extras/test_jobs/fail.py +5 -5
- nautobot/extras/test_jobs/file_output.py +1 -1
- nautobot/extras/test_jobs/file_upload_fail.py +1 -1
- nautobot/extras/test_jobs/file_upload_pass.py +1 -1
- nautobot/extras/test_jobs/ipaddress_vars.py +3 -1
- nautobot/extras/test_jobs/jobs_module/jobs_submodule/jobs.py +1 -1
- nautobot/extras/test_jobs/location_with_custom_field.py +1 -1
- nautobot/extras/test_jobs/log_redaction.py +1 -1
- nautobot/extras/test_jobs/log_skip_db_logging.py +1 -1
- nautobot/extras/test_jobs/modify_db.py +1 -1
- nautobot/extras/test_jobs/object_var_optional.py +1 -1
- nautobot/extras/test_jobs/object_var_required.py +1 -1
- nautobot/extras/test_jobs/object_vars.py +1 -1
- nautobot/extras/test_jobs/pass.py +3 -3
- nautobot/extras/test_jobs/profiling.py +1 -1
- nautobot/extras/test_jobs/relative_import.py +3 -3
- nautobot/extras/test_jobs/singleton.py +16 -0
- nautobot/extras/test_jobs/soft_time_limit_greater_than_time_limit.py +1 -1
- nautobot/extras/test_jobs/task_queues.py +1 -1
- nautobot/extras/tests/integration/test_plugin_banner.py +0 -2
- nautobot/extras/tests/test_api.py +13 -13
- nautobot/extras/tests/test_customfields.py +1 -1
- nautobot/extras/tests/test_datasources.py +2 -1
- nautobot/extras/tests/test_dynamicgroups.py +1 -1
- nautobot/extras/tests/test_filters.py +6 -6
- nautobot/extras/tests/test_forms.py +33 -1
- nautobot/extras/tests/test_jobs.py +178 -32
- nautobot/extras/tests/test_models.py +16 -10
- nautobot/extras/tests/test_plugins.py +62 -9
- nautobot/extras/tests/test_relationships.py +120 -9
- nautobot/extras/tests/test_views.py +56 -194
- nautobot/extras/utils.py +3 -2
- nautobot/extras/views.py +30 -98
- nautobot/ipam/api/fields.py +3 -3
- nautobot/ipam/api/serializers.py +41 -33
- nautobot/ipam/api/views.py +68 -117
- nautobot/ipam/factory.py +1 -1
- nautobot/ipam/filters.py +3 -2
- nautobot/ipam/lookups.py +101 -62
- nautobot/ipam/models.py +66 -16
- nautobot/ipam/querysets.py +2 -2
- nautobot/ipam/tables.py +23 -7
- nautobot/ipam/templates/ipam/ipaddress.html +1 -9
- nautobot/ipam/templates/ipam/ipaddress_bulk_add.html +1 -7
- nautobot/ipam/templates/ipam/ipaddress_edit.html +1 -7
- nautobot/ipam/templates/ipam/prefix.html +1 -9
- nautobot/ipam/templates/ipam/prefix_edit.html +1 -7
- nautobot/ipam/templates/ipam/vlan.html +1 -9
- nautobot/ipam/templates/ipam/vlan_edit.html +1 -7
- nautobot/ipam/templates/ipam/vrf_edit.html +1 -7
- nautobot/ipam/tests/test_api.py +436 -3
- nautobot/ipam/tests/test_forms.py +49 -47
- nautobot/ipam/tests/test_migrations.py +30 -30
- nautobot/ipam/tests/test_models.py +95 -34
- nautobot/ipam/tests/test_querysets.py +63 -1
- nautobot/ipam/tests/test_views.py +3 -0
- nautobot/ipam/utils/__init__.py +36 -6
- nautobot/ipam/views.py +61 -87
- nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap-theme.css.map +1 -1
- nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap-theme.min.css.map +1 -1
- nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap.css +40 -2
- nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap.css.map +1 -1
- nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap.min.css +1 -1
- nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap.min.css.map +1 -1
- nautobot/project-static/docs/404.html +46 -4
- nautobot/project-static/docs/apps/index.html +46 -4
- nautobot/project-static/docs/apps/nautobot-apps.html +47 -6
- nautobot/project-static/docs/assets/_mkdocstrings.css +25 -1
- nautobot/project-static/docs/assets/javascripts/{bundle.83f73b43.min.js → bundle.88dd0f4e.min.js} +2 -2
- nautobot/project-static/docs/assets/javascripts/{bundle.83f73b43.min.js.map → bundle.88dd0f4e.min.js.map} +2 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +62 -10
- nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +59 -7
- nautobot/project-static/docs/code-reference/nautobot/apps/api.html +374 -122
- nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +90 -18
- nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +95 -21
- nautobot/project-static/docs/code-reference/nautobot/apps/config.html +53 -6
- nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +52 -5
- nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +79 -17
- nautobot/project-static/docs/code-reference/nautobot/apps/events.html +102 -28
- nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +108 -21
- nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +131 -38
- nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +239 -65
- nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +581 -165
- nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +109 -36
- nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +453 -167
- nautobot/project-static/docs/code-reference/nautobot/apps/models.html +493 -211
- nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +60 -8
- nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +71 -15
- nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +407 -55
- nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +620 -205
- nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +858 -412
- nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +59 -7
- nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +448 -186
- nautobot/project-static/docs/code-reference/nautobot/apps/views.html +365 -147
- nautobot/project-static/docs/development/apps/api/configuration-view.html +46 -4
- nautobot/project-static/docs/development/apps/api/database-backend-config.html +46 -4
- nautobot/project-static/docs/development/apps/api/models/django-admin.html +46 -4
- nautobot/project-static/docs/development/apps/api/models/global-search.html +46 -4
- nautobot/project-static/docs/development/apps/api/models/graphql.html +46 -4
- nautobot/project-static/docs/development/apps/api/models/index.html +46 -4
- nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +46 -4
- nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +46 -4
- nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +46 -4
- nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +46 -4
- nautobot/project-static/docs/development/apps/api/platform-features/index.html +46 -4
- nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +46 -4
- nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +46 -4
- nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +46 -4
- nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +46 -4
- nautobot/project-static/docs/development/apps/api/platform-features/table-extensions.html +68 -7
- nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +46 -4
- nautobot/project-static/docs/development/apps/api/prometheus.html +46 -4
- nautobot/project-static/docs/development/apps/api/setup.html +46 -4
- nautobot/project-static/docs/development/apps/api/testing.html +46 -4
- nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +46 -4
- nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +46 -4
- nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +46 -4
- nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +46 -4
- nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +46 -4
- nautobot/project-static/docs/development/apps/api/views/base-template.html +46 -4
- nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +46 -4
- nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +46 -4
- nautobot/project-static/docs/development/apps/api/views/help-documentation.html +46 -4
- nautobot/project-static/docs/development/apps/api/views/index.html +46 -4
- nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +46 -4
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +46 -4
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +46 -4
- nautobot/project-static/docs/development/apps/api/views/notes.html +46 -4
- nautobot/project-static/docs/development/apps/api/views/rest-api.html +52 -6
- nautobot/project-static/docs/development/apps/api/views/urls.html +46 -4
- nautobot/project-static/docs/development/apps/index.html +46 -4
- nautobot/project-static/docs/development/apps/migration/code-updates.html +46 -4
- nautobot/project-static/docs/development/apps/migration/dependency-updates.html +46 -4
- nautobot/project-static/docs/development/apps/migration/from-v1.html +46 -4
- nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +46 -4
- nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +46 -4
- nautobot/project-static/docs/development/apps/migration/model-updates/global.html +46 -4
- nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +46 -4
- nautobot/project-static/docs/development/apps/migration/ui-component-framework/best-practices.html +50 -8
- nautobot/project-static/docs/development/apps/migration/ui-component-framework/custom-content.html +46 -4
- nautobot/project-static/docs/development/apps/migration/ui-component-framework/index.html +211 -14
- nautobot/project-static/docs/development/apps/migration/ui-component-framework/migration-steps.html +46 -4
- nautobot/project-static/docs/development/apps/porting-from-netbox.html +46 -4
- nautobot/project-static/docs/development/core/application-registry.html +46 -4
- nautobot/project-static/docs/development/core/best-practices.html +46 -4
- nautobot/project-static/docs/development/core/bootstrap-ui.html +46 -4
- nautobot/project-static/docs/development/core/caching.html +46 -4
- nautobot/project-static/docs/development/core/controllers.html +46 -4
- nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +73 -74
- nautobot/project-static/docs/development/core/generic-views.html +46 -4
- nautobot/project-static/docs/development/core/getting-started.html +249 -224
- nautobot/project-static/docs/development/core/homepage.html +49 -7
- nautobot/project-static/docs/development/core/index.html +46 -4
- nautobot/project-static/docs/development/core/{local-k8s.html → minikube-dev-environment-for-k8s-jobs.html} +469 -168
- nautobot/project-static/docs/development/core/model-checklist.html +56 -12
- nautobot/project-static/docs/development/core/model-features.html +46 -4
- nautobot/project-static/docs/development/core/natural-keys.html +46 -4
- nautobot/project-static/docs/development/core/navigation-menu.html +46 -4
- nautobot/project-static/docs/development/core/release-checklist.html +49 -7
- nautobot/project-static/docs/development/core/role-internals.html +46 -4
- nautobot/project-static/docs/development/core/settings.html +46 -4
- nautobot/project-static/docs/development/core/style-guide.html +49 -7
- nautobot/project-static/docs/development/core/templates.html +46 -4
- nautobot/project-static/docs/development/core/testing.html +46 -4
- nautobot/project-static/docs/development/core/ui-component-framework.html +369 -273
- nautobot/project-static/docs/development/core/user-preferences.html +46 -4
- nautobot/project-static/docs/development/index.html +46 -4
- nautobot/project-static/docs/development/jobs/index.html +216 -122
- nautobot/project-static/docs/development/jobs/migration/from-v1.html +46 -4
- nautobot/project-static/docs/index.html +54 -23
- nautobot/project-static/docs/media/development/core/kubernetes/k8s_job_edit.png +0 -0
- nautobot/project-static/docs/media/development/core/kubernetes/k8s_job_edit_button.png +0 -0
- nautobot/project-static/docs/media/development/core/kubernetes/k8s_job_list_nav.png +0 -0
- nautobot/project-static/docs/media/development/core/kubernetes/k8s_job_list_view.png +0 -0
- nautobot/project-static/docs/media/development/core/kubernetes/k8s_job_queue.png +0 -0
- nautobot/project-static/docs/media/development/core/kubernetes/k8s_job_queue_add.png +0 -0
- nautobot/project-static/docs/media/development/core/kubernetes/k8s_job_queue_config.png +0 -0
- nautobot/project-static/docs/media/development/core/kubernetes/k8s_job_result_completed.png +0 -0
- nautobot/project-static/docs/media/development/core/kubernetes/k8s_job_result_nav.png +0 -0
- nautobot/project-static/docs/media/development/core/kubernetes/k8s_job_result_pending.png +0 -0
- nautobot/project-static/docs/media/development/core/kubernetes/k8s_job_run_form.png +0 -0
- nautobot/project-static/docs/media/development/core/kubernetes/k8s_nautobot_login.png +0 -0
- nautobot/project-static/docs/media/development/core/kubernetes/k8s_run_job.png +0 -0
- nautobot/project-static/docs/media/development/core/kubernetes/k8s_run_scheduled_job_form.png +0 -0
- nautobot/project-static/docs/media/development/core/kubernetes/k8s_scheduled_job_result.png +0 -0
- nautobot/project-static/docs/media/development/core/ui-component-framework/buttons-example.png +0 -0
- nautobot/project-static/docs/media/development/core/ui-component-framework/cluster-type-before-after-example.png +0 -0
- nautobot/project-static/docs/media/development/core/ui-component-framework/object-fields-panel-example_2.png +0 -0
- nautobot/project-static/docs/media/development/core/ui-component-framework/stats-panel-example-code.png +0 -0
- nautobot/project-static/docs/objects.inv +0 -0
- nautobot/project-static/docs/overview/application_stack.html +47 -7
- nautobot/project-static/docs/overview/design_philosophy.html +46 -4
- nautobot/project-static/docs/release-notes/index.html +52 -12
- nautobot/project-static/docs/release-notes/version-1.0.html +234 -193
- nautobot/project-static/docs/release-notes/version-1.1.html +231 -190
- nautobot/project-static/docs/release-notes/version-1.2.html +306 -265
- nautobot/project-static/docs/release-notes/version-1.3.html +332 -291
- nautobot/project-static/docs/release-notes/version-1.4.html +417 -377
- nautobot/project-static/docs/release-notes/version-1.5.html +605 -566
- nautobot/project-static/docs/release-notes/version-1.6.html +904 -447
- nautobot/project-static/docs/release-notes/version-2.0.html +528 -489
- nautobot/project-static/docs/release-notes/version-2.1.html +363 -324
- nautobot/project-static/docs/release-notes/version-2.2.html +356 -317
- nautobot/project-static/docs/release-notes/version-2.3.html +997 -352
- nautobot/project-static/docs/release-notes/version-2.4.html +525 -101
- nautobot/project-static/docs/requirements.txt +2 -2
- nautobot/project-static/docs/search/search_index.json +1 -1
- nautobot/project-static/docs/sitemap.xml +295 -287
- nautobot/project-static/docs/sitemap.xml.gz +0 -0
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +46 -4
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +46 -4
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +48 -6
- nautobot/project-static/docs/user-guide/administration/configuration/index.html +46 -4
- nautobot/project-static/docs/user-guide/administration/configuration/redis.html +46 -4
- nautobot/project-static/docs/user-guide/administration/configuration/settings.html +110 -8
- nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +46 -4
- nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +46 -4
- nautobot/project-static/docs/user-guide/administration/guides/docker.html +46 -4
- nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +46 -4
- nautobot/project-static/docs/user-guide/administration/guides/permissions.html +46 -4
- nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +46 -4
- nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +46 -4
- nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +46 -4
- nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +48 -6
- nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +46 -4
- nautobot/project-static/docs/user-guide/administration/installation/app-install.html +46 -4
- nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +46 -4
- nautobot/project-static/docs/user-guide/administration/installation/http-server.html +66 -8
- nautobot/project-static/docs/user-guide/administration/installation/index.html +46 -4
- nautobot/project-static/docs/user-guide/administration/installation/install_system.html +47 -5
- nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +46 -4
- nautobot/project-static/docs/user-guide/administration/installation/services.html +46 -4
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +46 -4
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +46 -4
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +46 -4
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +46 -4
- nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +46 -4
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +46 -4
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +46 -4
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +46 -4
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +46 -4
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +49 -8
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +46 -4
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +46 -4
- nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualdevicecontext.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +50 -12
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +49 -7
- nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/wireless/index.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/wireless/radioprofile.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/wireless/supporteddatarate.html +46 -4
- nautobot/project-static/docs/user-guide/core-data-model/wireless/wirelessnetwork.html +46 -4
- nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +46 -4
- nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +46 -4
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +46 -4
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +46 -4
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +46 -4
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +46 -4
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +46 -4
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +46 -4
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +46 -4
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +46 -4
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +46 -4
- nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +51 -7
- nautobot/project-static/docs/user-guide/feature-guides/graphql.html +46 -4
- nautobot/project-static/docs/user-guide/feature-guides/images/wireless/central-mode.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/wireless/device-group-add.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/wireless/device-group-create-1.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/wireless/device-group-create-2.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/wireless/radio-profile-add.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/wireless/radio-profile-create.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/wireless/supported-data-rate-add.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/wireless/supported-data-rate-create.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/wireless/wireless-controller-add.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/wireless/wireless-controller-create-1.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/wireless/wireless-controller-create-2.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/wireless/wireless-network-add.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/wireless/wireless-network-create.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +46 -4
- nautobot/project-static/docs/user-guide/feature-guides/relationships.html +46 -4
- nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +49 -7
- nautobot/project-static/docs/user-guide/feature-guides/wireless-networks-and-controllers.html +9444 -0
- nautobot/project-static/docs/user-guide/index.html +46 -4
- nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +46 -4
- nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +50 -8
- nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +46 -4
- nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +46 -4
- nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +46 -4
- nautobot/project-static/docs/user-guide/platform-functionality/events.html +46 -4
- nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +46 -4
- nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +46 -4
- nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +46 -4
- nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +46 -4
- nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +46 -4
- nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +46 -4
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +50 -7
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +46 -4
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +49 -7
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +46 -4
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobqueue.html +46 -4
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/kubernetes-job-support.html +9722 -0
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +46 -4
- nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +46 -4
- nautobot/project-static/docs/user-guide/platform-functionality/note.html +46 -4
- nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +46 -4
- nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +47 -5
- nautobot/project-static/docs/user-guide/platform-functionality/rendering-jinja-templates.html +46 -4
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +46 -4
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +94 -25
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +74 -5
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +46 -4
- nautobot/project-static/docs/user-guide/platform-functionality/role.html +46 -4
- nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +46 -4
- nautobot/project-static/docs/user-guide/platform-functionality/secret.html +46 -4
- nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +46 -4
- nautobot/project-static/docs/user-guide/platform-functionality/status.html +46 -4
- nautobot/project-static/docs/user-guide/platform-functionality/tag.html +46 -4
- nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +46 -4
- nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +46 -4
- nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +46 -4
- nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +46 -4
- nautobot/project-static/js/forms.js +1 -1
- nautobot/tenancy/api/views.py +9 -13
- nautobot/tenancy/views.py +4 -2
- nautobot/users/admin.py +1 -1
- nautobot/users/api/serializers.py +5 -4
- nautobot/users/api/views.py +3 -3
- nautobot/virtualization/api/serializers.py +4 -4
- nautobot/virtualization/api/views.py +5 -24
- nautobot/virtualization/filters.py +20 -3
- nautobot/virtualization/models.py +1 -1
- nautobot/virtualization/tables.py +2 -2
- nautobot/virtualization/templates/virtualization/cluster_edit.html +1 -7
- nautobot/virtualization/templates/virtualization/virtualmachine.html +1 -9
- nautobot/virtualization/templates/virtualization/virtualmachine_edit.html +2 -8
- nautobot/virtualization/tests/test_filters.py +17 -0
- nautobot/wireless/filters.py +2 -2
- nautobot/wireless/forms.py +1 -1
- nautobot/wireless/templates/wireless/wirelessnetwork_retrieve.html +1 -9
- nautobot/wireless/tests/integration/__init__.py +0 -0
- nautobot/wireless/tests/integration/test_radio_profile.py +42 -0
- nautobot/wireless/tests/test_filters.py +29 -1
- nautobot/wireless/tests/test_views.py +22 -1
- nautobot/wireless/views.py +0 -10
- {nautobot-2.4.0b1.dist-info → nautobot-2.4.1.dist-info}/METADATA +6 -6
- {nautobot-2.4.0b1.dist-info → nautobot-2.4.1.dist-info}/RECORD +600 -550
- {nautobot-2.4.0b1.dist-info → nautobot-2.4.1.dist-info}/WHEEL +1 -1
- nautobot/core/fixtures/user-data.json +0 -59
- {nautobot-2.4.0b1.dist-info → nautobot-2.4.1.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.4.0b1.dist-info → nautobot-2.4.1.dist-info}/NOTICE +0 -0
- {nautobot-2.4.0b1.dist-info → nautobot-2.4.1.dist-info}/entry_points.txt +0 -0
nautobot/core/testing/api.py
CHANGED
|
@@ -5,11 +5,13 @@ from typing import Optional, Sequence, Union
|
|
|
5
5
|
from django.conf import settings
|
|
6
6
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
|
7
7
|
from django.contrib.contenttypes.models import ContentType
|
|
8
|
+
from django.db import connections, DEFAULT_DB_ALIAS
|
|
8
9
|
from django.db.models import ForeignKey, ManyToManyField, QuerySet
|
|
9
10
|
from django.test import override_settings, tag
|
|
11
|
+
from django.test.utils import CaptureQueriesContext
|
|
10
12
|
from django.urls import reverse
|
|
11
13
|
from django.utils.text import slugify
|
|
12
|
-
from rest_framework import status
|
|
14
|
+
from rest_framework import serializers, status
|
|
13
15
|
from rest_framework.relations import ManyRelatedField
|
|
14
16
|
from rest_framework.test import APITransactionTestCase as _APITransactionTestCase
|
|
15
17
|
|
|
@@ -94,6 +96,15 @@ class APITestCase(views.ModelTestCase):
|
|
|
94
96
|
for verboten in self.VERBOTEN_STRINGS:
|
|
95
97
|
self.assertNotIn(verboten, response_raw_content)
|
|
96
98
|
|
|
99
|
+
def get_m2m_fields(self):
|
|
100
|
+
"""Get serializer field names that are many-to-many or one-to-many and thus affected by ?exclude_m2m=true."""
|
|
101
|
+
serializer_class = get_serializer_for_model(self.model)
|
|
102
|
+
m2m_fields = []
|
|
103
|
+
for field_name, field_instance in serializer_class().fields.items():
|
|
104
|
+
if isinstance(field_instance, (serializers.ManyRelatedField, serializers.ListSerializer)):
|
|
105
|
+
m2m_fields.append(field_name)
|
|
106
|
+
return m2m_fields
|
|
107
|
+
|
|
97
108
|
|
|
98
109
|
@tag("unit")
|
|
99
110
|
class APIViewTestCases:
|
|
@@ -231,7 +242,7 @@ class APIViewTestCases:
|
|
|
231
242
|
def get_depth_fields(self):
|
|
232
243
|
"""Get a list of model fields that could be tested with the ?depth query parameter"""
|
|
233
244
|
depth_fields = []
|
|
234
|
-
for field in self.model._meta.
|
|
245
|
+
for field in self.model._meta.get_fields():
|
|
235
246
|
if not field.name.startswith("_"):
|
|
236
247
|
if isinstance(field, (ForeignKey, GenericForeignKey, ManyToManyField, core_fields.TagsField)) and (
|
|
237
248
|
# we represent content-types as "app_label.modelname" rather than as FKs
|
|
@@ -240,6 +251,9 @@ class APIViewTestCases:
|
|
|
240
251
|
and not (field.name == "user" and self.model == users_models.Token)
|
|
241
252
|
):
|
|
242
253
|
depth_fields.append(field.name)
|
|
254
|
+
serializer_class = get_serializer_for_model(self.model)
|
|
255
|
+
serializer = serializer_class()
|
|
256
|
+
depth_fields = [field_name for field_name in depth_fields if field_name in serializer.fields]
|
|
243
257
|
return depth_fields
|
|
244
258
|
|
|
245
259
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
@@ -269,9 +283,12 @@ class APIViewTestCases:
|
|
|
269
283
|
GET a list of objects using the "?depth=0" parameter.
|
|
270
284
|
"""
|
|
271
285
|
depth_fields = self.get_depth_fields()
|
|
286
|
+
m2m_fields = self.get_m2m_fields()
|
|
272
287
|
self.add_permissions(f"{self.model._meta.app_label}.view_{self.model._meta.model_name}")
|
|
273
|
-
|
|
274
|
-
|
|
288
|
+
list_url = f"{self._get_list_url()}?depth=0"
|
|
289
|
+
with CaptureQueriesContext(connections[DEFAULT_DB_ALIAS]) as cqc:
|
|
290
|
+
response = self.client.get(list_url, **self.header)
|
|
291
|
+
base_num_queries = len(cqc)
|
|
275
292
|
|
|
276
293
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
277
294
|
self.assertIsInstance(response.data, dict)
|
|
@@ -280,15 +297,23 @@ class APIViewTestCases:
|
|
|
280
297
|
self.assert_no_verboten_content(response)
|
|
281
298
|
|
|
282
299
|
for response_data in response.data["results"]:
|
|
300
|
+
for field in m2m_fields:
|
|
301
|
+
self.assertIn(field, response_data)
|
|
302
|
+
self.assertIsInstance(response_data[field], list)
|
|
283
303
|
for field in depth_fields:
|
|
284
304
|
self.assertIn(field, response_data)
|
|
285
305
|
if isinstance(response_data[field], list):
|
|
286
306
|
for entry in response_data[field]:
|
|
287
307
|
self.assertIsInstance(entry, dict)
|
|
288
|
-
|
|
308
|
+
if entry["object_type"] in ["auth.group"]:
|
|
309
|
+
self.assertIsInstance(entry["id"], int)
|
|
310
|
+
else:
|
|
311
|
+
self.assertTrue(is_uuid(entry["id"]))
|
|
312
|
+
self.assertEqual(len(entry.keys()), 3) # just id/object_type/url
|
|
289
313
|
else:
|
|
290
314
|
if response_data[field] is not None:
|
|
291
315
|
self.assertIsInstance(response_data[field], dict)
|
|
316
|
+
self.assertEqual(len(response_data[field].keys()), 3) # just id/object_type/url
|
|
292
317
|
url = response_data[field]["url"]
|
|
293
318
|
pk = response_data[field]["id"]
|
|
294
319
|
object_type = response_data[field]["object_type"]
|
|
@@ -296,22 +321,67 @@ class APIViewTestCases:
|
|
|
296
321
|
# URL ending in the UUID of the relevant object:
|
|
297
322
|
# http://nautobot.example.com/api/circuits/providers/<uuid>/
|
|
298
323
|
# ^^^^^^
|
|
299
|
-
|
|
300
|
-
|
|
324
|
+
if object_type in ["auth.group"]:
|
|
325
|
+
self.assertIsInstance(url.split("/")[-2], int)
|
|
326
|
+
self.assertIsInstance(pk, int)
|
|
327
|
+
else:
|
|
328
|
+
self.assertTrue(is_uuid(url.split("/")[-2]))
|
|
329
|
+
self.assertTrue(is_uuid(pk))
|
|
301
330
|
|
|
302
331
|
with self.subTest(f"Assert object_type {object_type} is valid"):
|
|
303
332
|
app_label, model_name = object_type.split(".")
|
|
304
333
|
ContentType.objects.get(app_label=app_label, model=model_name)
|
|
305
334
|
|
|
335
|
+
list_url += "&exclude_m2m=true"
|
|
336
|
+
with CaptureQueriesContext(connections[DEFAULT_DB_ALIAS]) as cqc:
|
|
337
|
+
response = self.client.get(list_url, **self.header)
|
|
338
|
+
|
|
339
|
+
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
340
|
+
self.assertIsInstance(response.data, dict)
|
|
341
|
+
self.assertIn("results", response.data)
|
|
342
|
+
self.assert_no_verboten_content(response)
|
|
343
|
+
|
|
344
|
+
if m2m_fields:
|
|
345
|
+
if self.model._meta.app_label in [
|
|
346
|
+
"circuits",
|
|
347
|
+
"cloud",
|
|
348
|
+
"dcim",
|
|
349
|
+
"extras",
|
|
350
|
+
"ipam",
|
|
351
|
+
"tenancy",
|
|
352
|
+
"users",
|
|
353
|
+
"virtualization",
|
|
354
|
+
"wireless",
|
|
355
|
+
]:
|
|
356
|
+
self.assertLess(
|
|
357
|
+
len(cqc), base_num_queries, "Number of queries did not decrease with ?exclude_m2m=true"
|
|
358
|
+
)
|
|
359
|
+
else:
|
|
360
|
+
# Less strict check for non-core APIs
|
|
361
|
+
self.assertLessEqual(
|
|
362
|
+
len(cqc), base_num_queries, "Number of queries increased with ?exclude_m2m=true"
|
|
363
|
+
)
|
|
364
|
+
else:
|
|
365
|
+
# No M2M fields to exclude
|
|
366
|
+
self.assertLessEqual(len(cqc), base_num_queries, "Number of queries increased with ?exclude_m2m=true")
|
|
367
|
+
|
|
368
|
+
for response_data in response.data["results"]:
|
|
369
|
+
for field in m2m_fields:
|
|
370
|
+
self.assertNotIn(field, response_data)
|
|
371
|
+
# TODO: we should assert that all other fields are still present, but there's a few corner cases...
|
|
372
|
+
|
|
306
373
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
|
307
374
|
def test_list_objects_depth_1(self):
|
|
308
375
|
"""
|
|
309
376
|
GET a list of objects using the "?depth=1" parameter.
|
|
310
377
|
"""
|
|
311
378
|
depth_fields = self.get_depth_fields()
|
|
379
|
+
m2m_fields = self.get_m2m_fields()
|
|
312
380
|
self.add_permissions(f"{self.model._meta.app_label}.view_{self.model._meta.model_name}")
|
|
313
|
-
|
|
314
|
-
|
|
381
|
+
list_url = f"{self._get_list_url()}?depth=1"
|
|
382
|
+
with CaptureQueriesContext(connections[DEFAULT_DB_ALIAS]) as cqc:
|
|
383
|
+
response = self.client.get(list_url, **self.header)
|
|
384
|
+
base_num_queries = len(cqc)
|
|
315
385
|
|
|
316
386
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
317
387
|
self.assertIsInstance(response.data, dict)
|
|
@@ -320,16 +390,65 @@ class APIViewTestCases:
|
|
|
320
390
|
self.assert_no_verboten_content(response)
|
|
321
391
|
|
|
322
392
|
for response_data in response.data["results"]:
|
|
393
|
+
for field in m2m_fields:
|
|
394
|
+
self.assertIn(field, response_data)
|
|
395
|
+
self.assertIsInstance(response_data[field], list)
|
|
323
396
|
for field in depth_fields:
|
|
324
397
|
self.assertIn(field, response_data)
|
|
325
398
|
if isinstance(response_data[field], list):
|
|
326
399
|
for entry in response_data[field]:
|
|
327
400
|
self.assertIsInstance(entry, dict)
|
|
328
|
-
|
|
401
|
+
if entry["object_type"] in ["auth.group"]:
|
|
402
|
+
self.assertIsInstance(entry["id"], int)
|
|
403
|
+
else:
|
|
404
|
+
self.assertTrue(is_uuid(entry["id"]))
|
|
405
|
+
self.assertGreater(len(entry.keys()), 3, entry) # not just id/object_type/url!
|
|
329
406
|
else:
|
|
330
407
|
if response_data[field] is not None:
|
|
331
408
|
self.assertIsInstance(response_data[field], dict)
|
|
332
|
-
|
|
409
|
+
if response_data[field]["object_type"] in ["auth.group"]:
|
|
410
|
+
self.assertIsInstance(response_data[field]["id"], int)
|
|
411
|
+
else:
|
|
412
|
+
self.assertTrue(is_uuid(response_data[field]["id"]))
|
|
413
|
+
self.assertGreater(len(response_data[field].keys()), 3, response_data[field])
|
|
414
|
+
|
|
415
|
+
list_url += "&exclude_m2m=true"
|
|
416
|
+
with CaptureQueriesContext(connections[DEFAULT_DB_ALIAS]) as cqc:
|
|
417
|
+
response = self.client.get(list_url, **self.header)
|
|
418
|
+
|
|
419
|
+
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
420
|
+
self.assertIsInstance(response.data, dict)
|
|
421
|
+
self.assertIn("results", response.data)
|
|
422
|
+
self.assert_no_verboten_content(response)
|
|
423
|
+
|
|
424
|
+
if m2m_fields:
|
|
425
|
+
if self.model._meta.app_label in [
|
|
426
|
+
"circuits",
|
|
427
|
+
"cloud",
|
|
428
|
+
"dcim",
|
|
429
|
+
"extras",
|
|
430
|
+
"ipam",
|
|
431
|
+
"tenancy",
|
|
432
|
+
"users",
|
|
433
|
+
"virtualization",
|
|
434
|
+
"wireless",
|
|
435
|
+
]:
|
|
436
|
+
self.assertLess(
|
|
437
|
+
len(cqc), base_num_queries, "Number of queries did not decrease with ?exclude_m2m=true"
|
|
438
|
+
)
|
|
439
|
+
else:
|
|
440
|
+
# Less strict check for non-core APIs
|
|
441
|
+
self.assertLessEqual(
|
|
442
|
+
len(cqc), base_num_queries, "Number of queries increased with ?exclude_m2m=true"
|
|
443
|
+
)
|
|
444
|
+
else:
|
|
445
|
+
# No M2M fields to exclude
|
|
446
|
+
self.assertLessEqual(len(cqc), base_num_queries, "Number of queries increased with ?exclude_m2m=true")
|
|
447
|
+
|
|
448
|
+
for response_data in response.data["results"]:
|
|
449
|
+
for field in m2m_fields:
|
|
450
|
+
self.assertNotIn(field, response_data)
|
|
451
|
+
# TODO: we should assert that all other fields are still present, but there's a few corner cases...
|
|
333
452
|
|
|
334
453
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
|
335
454
|
def test_list_objects_without_permission(self):
|
|
@@ -655,7 +774,11 @@ class APIViewTestCases:
|
|
|
655
774
|
self.assertHttpStatus(response, status.HTTP_201_CREATED, csv_data)
|
|
656
775
|
# Note that create via CSV is always treated as a bulk-create, and so the response is always a list of dicts
|
|
657
776
|
new_instance = self._get_queryset().get(pk=response.data[0]["id"])
|
|
658
|
-
|
|
777
|
+
if isinstance(orig_pk, int):
|
|
778
|
+
self.assertNotEqual(new_instance.pk, orig_pk)
|
|
779
|
+
else:
|
|
780
|
+
# for our non-integer PKs, we're expecting the creation to respect the requested PK
|
|
781
|
+
self.assertEqual(new_instance.pk, orig_pk)
|
|
659
782
|
|
|
660
783
|
new_serializer = serializer_class(new_instance, context={"request": None})
|
|
661
784
|
new_data = new_serializer.data
|
|
@@ -737,7 +860,7 @@ class APIViewTestCases:
|
|
|
737
860
|
|
|
738
861
|
def strip_serialized_object(this_object):
|
|
739
862
|
"""
|
|
740
|
-
|
|
863
|
+
Work around acceptable differences in PATCH response vs GET response which are known behaviors.
|
|
741
864
|
"""
|
|
742
865
|
# Work around for https://github.com/nautobot/nautobot/issues/3321
|
|
743
866
|
this_object.pop("last_updated", None)
|
|
@@ -746,6 +869,12 @@ class APIViewTestCases:
|
|
|
746
869
|
this_object.pop("config_context", None)
|
|
747
870
|
this_object.pop("relationships", None)
|
|
748
871
|
|
|
872
|
+
serializer = get_serializer_for_model(self.model)()
|
|
873
|
+
for field_name, field_instance in serializer.fields.items():
|
|
874
|
+
if field_instance.read_only:
|
|
875
|
+
# Likely a derived field, might change as a consequence of other data updates
|
|
876
|
+
this_object.pop(field_name, None)
|
|
877
|
+
|
|
749
878
|
for value in this_object.values():
|
|
750
879
|
if isinstance(value, dict):
|
|
751
880
|
strip_serialized_object(value)
|
|
@@ -789,7 +918,7 @@ class APIViewTestCases:
|
|
|
789
918
|
self.assertEqual(initial_serialized_object, serialized_object)
|
|
790
919
|
|
|
791
920
|
# Verify ObjectChange creation -- yes, even though nothing actually changed
|
|
792
|
-
# This may change (hah) at some point -- see https://github.com/nautobot/nautobot/issues/3321
|
|
921
|
+
# TODO: This may change (hah) at some point -- see https://github.com/nautobot/nautobot/issues/3321
|
|
793
922
|
if hasattr(self.model, "to_objectchange"):
|
|
794
923
|
objectchanges = lookup.get_changes_for_model(instance)
|
|
795
924
|
self.assertEqual(objectchanges[0].action, extras_choices.ObjectChangeActionChoices.ACTION_UPDATE)
|
|
@@ -798,10 +927,16 @@ class APIViewTestCases:
|
|
|
798
927
|
# Verify that a PATCH with some data updates that data correctly.
|
|
799
928
|
response = self.client.patch(url, update_data, format="json", **self.header)
|
|
800
929
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
930
|
+
serialized_object = response.json()
|
|
931
|
+
strip_serialized_object(serialized_object)
|
|
801
932
|
# Check for unexpected side effects on fields we DIDN'T intend to update
|
|
802
933
|
for field in initial_serialized_object:
|
|
803
934
|
if field not in update_data:
|
|
804
|
-
self.assertEqual(
|
|
935
|
+
self.assertEqual(
|
|
936
|
+
initial_serialized_object[field],
|
|
937
|
+
serialized_object[field],
|
|
938
|
+
f"data changed unexpectedly for field '{field}'",
|
|
939
|
+
)
|
|
805
940
|
instance.refresh_from_db()
|
|
806
941
|
self.assertInstanceEqual(instance, update_data, exclude=self.validation_excluded_fields, api=True)
|
|
807
942
|
|
|
@@ -810,6 +945,26 @@ class APIViewTestCases:
|
|
|
810
945
|
objectchanges = lookup.get_changes_for_model(instance)
|
|
811
946
|
self.assertEqual(objectchanges[0].action, extras_choices.ObjectChangeActionChoices.ACTION_UPDATE)
|
|
812
947
|
|
|
948
|
+
# Verify that a PATCH with ?exclude_m2m=true correctly excludes many-to-many fields from the response
|
|
949
|
+
# This also doubles as a test for idempotence of the PATCH request.
|
|
950
|
+
response = self.client.patch(url + "?exclude_m2m=true", update_data, format="json", **self.header)
|
|
951
|
+
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
952
|
+
m2m_fields = self.get_m2m_fields()
|
|
953
|
+
serialized_object = response.json()
|
|
954
|
+
strip_serialized_object(serialized_object)
|
|
955
|
+
for field in m2m_fields:
|
|
956
|
+
self.assertNotIn(field, serialized_object)
|
|
957
|
+
# Check for unexpected side effects on fields we DIDN'T intend to update
|
|
958
|
+
for field in initial_serialized_object:
|
|
959
|
+
if field not in update_data and field not in m2m_fields:
|
|
960
|
+
self.assertEqual(
|
|
961
|
+
initial_serialized_object[field],
|
|
962
|
+
serialized_object[field],
|
|
963
|
+
f"data changed unexpectedly for field '{field}'",
|
|
964
|
+
)
|
|
965
|
+
instance.refresh_from_db()
|
|
966
|
+
self.assertInstanceEqual(instance, update_data, exclude=self.validation_excluded_fields, api=True)
|
|
967
|
+
|
|
813
968
|
def test_get_put_round_trip(self):
|
|
814
969
|
"""GET and then PUT an object and verify that it's accepted and unchanged."""
|
|
815
970
|
self.maxDiff = None
|
nautobot/core/testing/filters.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import random
|
|
2
2
|
import string
|
|
3
|
-
from typing import ClassVar, Iterable
|
|
3
|
+
from typing import ClassVar, Iterable, Optional
|
|
4
4
|
|
|
5
5
|
from django.contrib.contenttypes.models import ContentType
|
|
6
6
|
from django.db.models import Count, Q, QuerySet
|
|
@@ -30,6 +30,8 @@ class FilterTestCases:
|
|
|
30
30
|
class BaseFilterTestCase(views.TestCase):
|
|
31
31
|
"""Base class for testing of FilterSets."""
|
|
32
32
|
|
|
33
|
+
queryset: ClassVar[Optional[QuerySet]] = None # TODO: declared as Optional only to avoid a breaking change
|
|
34
|
+
|
|
33
35
|
def get_filterset_test_values(self, field_name, queryset=None):
|
|
34
36
|
"""Returns a list of distinct values from the requested queryset field to use in filterset tests.
|
|
35
37
|
|
|
@@ -71,8 +73,7 @@ class FilterTestCases:
|
|
|
71
73
|
class FilterTestCase(BaseFilterTestCase):
|
|
72
74
|
"""Add common tests for all FilterSets."""
|
|
73
75
|
|
|
74
|
-
|
|
75
|
-
filterset: ClassVar[FilterSet]
|
|
76
|
+
filterset: ClassVar[Optional[type[FilterSet]]] = None # TODO: declared Optional only to avoid breaking change
|
|
76
77
|
|
|
77
78
|
# filter predicate fields that should be excluded from q test case
|
|
78
79
|
exclude_q_filter_predicates = []
|
|
@@ -84,7 +85,7 @@ class FilterTestCases:
|
|
|
84
85
|
# ["filter1"],
|
|
85
86
|
# ["filter2", "field2__name"],
|
|
86
87
|
# ]
|
|
87
|
-
generic_filter_tests: ClassVar[Iterable]
|
|
88
|
+
generic_filter_tests: ClassVar[Iterable] = ()
|
|
88
89
|
|
|
89
90
|
def setUp(self):
|
|
90
91
|
for attr in ["queryset", "filterset", "generic_filter_tests"]:
|
|
@@ -94,12 +95,38 @@ class FilterTestCases:
|
|
|
94
95
|
|
|
95
96
|
def get_q_filter(self):
|
|
96
97
|
"""Helper method to return q filter."""
|
|
98
|
+
self.assertIsNotNone(self.filterset)
|
|
97
99
|
return self.filterset.declared_filters["q"].filter_predicates
|
|
98
100
|
|
|
101
|
+
def test_id(self):
|
|
102
|
+
"""Verify that the filterset supports filtering by id with only lookup `__n`."""
|
|
103
|
+
self.assertIsNotNone(self.filterset)
|
|
104
|
+
|
|
105
|
+
with self.subTest("Assert `id`"):
|
|
106
|
+
params = {"id": list(self.queryset.values_list("pk", flat=True)[:2])}
|
|
107
|
+
expected_queryset = self.queryset.filter(id__in=params["id"])
|
|
108
|
+
filterset = self.filterset(params, self.queryset) # pylint: disable=not-callable # see assertion above
|
|
109
|
+
self.assertTrue(filterset.is_valid())
|
|
110
|
+
self.assertQuerysetEqualAndNotEmpty(filterset.qs.order_by("id"), expected_queryset.order_by("id"))
|
|
111
|
+
|
|
112
|
+
with self.subTest("Assert negate lookup"):
|
|
113
|
+
params = {"id__n": list(self.queryset.values_list("pk", flat=True)[:2])}
|
|
114
|
+
expected_queryset = self.queryset.exclude(id__in=params["id__n"])
|
|
115
|
+
filterset = self.filterset(params, self.queryset) # pylint: disable=not-callable # see assertion above
|
|
116
|
+
self.assertTrue(filterset.is_valid())
|
|
117
|
+
self.assertQuerysetEqualAndNotEmpty(filterset.qs.order_by("id"), expected_queryset.order_by("id"))
|
|
118
|
+
|
|
119
|
+
with self.subTest("Assert invalid lookup"):
|
|
120
|
+
params = {"id__in": list(self.queryset.values_list("pk", flat=True)[:2])}
|
|
121
|
+
filterset = self.filterset(params, self.queryset) # pylint: disable=not-callable # see assertion above
|
|
122
|
+
self.assertFalse(filterset.is_valid())
|
|
123
|
+
self.assertIn("Unknown filter field", filterset.errors.as_text())
|
|
124
|
+
|
|
99
125
|
def test_invalid_filter(self):
|
|
100
126
|
"""Verify that the filterset reports as invalid when initialized with an unsupported filter parameter."""
|
|
101
127
|
params = {"ice_cream_flavor": ["chocolate"]}
|
|
102
|
-
self.
|
|
128
|
+
self.assertIsNotNone(self.filterset)
|
|
129
|
+
self.assertFalse(self.filterset(params, self.queryset).is_valid()) # pylint: disable=not-callable
|
|
103
130
|
|
|
104
131
|
def test_filters_generic(self):
|
|
105
132
|
"""Test all multiple choice filters declared in `self.generic_filter_tests`.
|
|
@@ -167,13 +194,16 @@ class FilterTestCases:
|
|
|
167
194
|
status=Status.objects.get_for_model(ContactAssociation).last(),
|
|
168
195
|
)
|
|
169
196
|
|
|
197
|
+
if self.generic_filter_tests:
|
|
198
|
+
self.assertIsNotNone(self.filterset)
|
|
199
|
+
|
|
170
200
|
for test in self.generic_filter_tests:
|
|
171
201
|
filter_name = test[0]
|
|
172
202
|
field_name = test[-1] # default to filter_name if a second list item was not supplied
|
|
173
203
|
with self.subTest(f"{self.filterset.__name__} filter {filter_name} ({field_name})"):
|
|
174
204
|
test_data = self.get_filterset_test_values(field_name)
|
|
175
205
|
params = {filter_name: test_data}
|
|
176
|
-
filterset_result = self.filterset(params, self.queryset).qs
|
|
206
|
+
filterset_result = self.filterset(params, self.queryset).qs # pylint: disable=not-callable
|
|
177
207
|
qs_result = self.queryset.filter(**{f"{field_name}__in": test_data}).distinct()
|
|
178
208
|
self.assertQuerysetEqualAndNotEmpty(filterset_result, qs_result, ordered=False)
|
|
179
209
|
|
|
@@ -184,6 +214,7 @@ class FilterTestCases:
|
|
|
184
214
|
This test asserts that `filter=True` matches `self.queryset.filter(field__isnull=False)` and
|
|
185
215
|
that `filter=False` matches `self.queryset.filter(field__isnull=True)`.
|
|
186
216
|
"""
|
|
217
|
+
self.assertIsNotNone(self.filterset)
|
|
187
218
|
for filter_name, filter_object in self.filterset.get_filters().items():
|
|
188
219
|
if not isinstance(filter_object, RelatedMembershipBooleanFilter):
|
|
189
220
|
continue
|
|
@@ -191,11 +222,11 @@ class FilterTestCases:
|
|
|
191
222
|
continue
|
|
192
223
|
field_name = filter_object.field_name
|
|
193
224
|
with self.subTest(f"{self.filterset.__name__} RelatedMembershipBooleanFilter {filter_name} (True)"):
|
|
194
|
-
filterset_result = self.filterset({filter_name: True}, self.queryset).qs
|
|
225
|
+
filterset_result = self.filterset({filter_name: True}, self.queryset).qs # pylint: disable=not-callable
|
|
195
226
|
qs_result = self.queryset.filter(**{f"{field_name}__isnull": filter_object.exclude}).distinct()
|
|
196
227
|
self.assertQuerysetEqualAndNotEmpty(filterset_result, qs_result)
|
|
197
228
|
with self.subTest(f"{self.filterset.__name__} RelatedMembershipBooleanFilter {filter_name} (False)"):
|
|
198
|
-
filterset_result = self.filterset({filter_name: False}, self.queryset).qs
|
|
229
|
+
filterset_result = self.filterset({filter_name: False}, self.queryset).qs # pylint: disable=not-callable
|
|
199
230
|
qs_result = self.queryset.exclude(**{f"{field_name}__isnull": filter_object.exclude}).distinct()
|
|
200
231
|
self.assertQuerysetEqualAndNotEmpty(filterset_result, qs_result)
|
|
201
232
|
|
|
@@ -204,6 +235,8 @@ class FilterTestCases:
|
|
|
204
235
|
if not issubclass(self.queryset.model, PrimaryModel):
|
|
205
236
|
self.skipTest("Not a PrimaryModel")
|
|
206
237
|
|
|
238
|
+
self.assertIsNotNone(self.filterset)
|
|
239
|
+
|
|
207
240
|
# Find an instance with at least two tags (should be common given our factory design)
|
|
208
241
|
for instance in list(self.queryset):
|
|
209
242
|
if len(instance.tags.all()) >= 2:
|
|
@@ -220,7 +253,7 @@ class FilterTestCases:
|
|
|
220
253
|
self.queryset.first().tags.add(test_tags_filter_a, test_tags_filter_b)
|
|
221
254
|
tags = [test_tags_filter_a, test_tags_filter_b]
|
|
222
255
|
params = {"tags": [tags[0].name, tags[1].pk]}
|
|
223
|
-
filterset_result = self.filterset(params, self.queryset).qs
|
|
256
|
+
filterset_result = self.filterset(params, self.queryset).qs # pylint: disable=not-callable
|
|
224
257
|
# Tags is an AND filter not an OR filter
|
|
225
258
|
qs_result = self.queryset.filter(tags=tags[0]).filter(tags=tags[1]).distinct()
|
|
226
259
|
self.assertQuerysetEqualAndNotEmpty(filterset_result, qs_result)
|
|
@@ -271,6 +304,8 @@ class FilterTestCases:
|
|
|
271
304
|
"""
|
|
272
305
|
self._assert_valid_filter_predicates(obj, obj_field_name)
|
|
273
306
|
|
|
307
|
+
self.assertIsNotNone(self.filterset)
|
|
308
|
+
|
|
274
309
|
# Generic test only supports CharField or TextFields, skip all other types
|
|
275
310
|
obj_field = obj._meta.get_field(obj_field_name)
|
|
276
311
|
if not isinstance(obj_field, (CharField, TextField)):
|
|
@@ -290,7 +325,7 @@ class FilterTestCases:
|
|
|
290
325
|
lookup = randomized_attr_value[1:].upper()
|
|
291
326
|
model_queryset = self.queryset.filter(**{f"{filter_field_name}__icontains": lookup})
|
|
292
327
|
params = {"q": lookup}
|
|
293
|
-
filterset_result = self.filterset(params, self.queryset)
|
|
328
|
+
filterset_result = self.filterset(params, self.queryset) # pylint: disable=not-callable
|
|
294
329
|
|
|
295
330
|
self.assertTrue(filterset_result.is_valid())
|
|
296
331
|
self.assertQuerysetEqualAndNotEmpty(
|
nautobot/core/testing/forms.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from django.test import tag, TestCase
|
|
2
2
|
|
|
3
|
+
from nautobot.core.api.constants import NON_FILTER_QUERY_PARAMS
|
|
3
4
|
from nautobot.core.forms.fields import DynamicModelChoiceMixin
|
|
4
5
|
from nautobot.core.utils.lookup import get_filterset_for_model
|
|
5
6
|
|
|
@@ -18,6 +19,7 @@ class FormTestCases:
|
|
|
18
19
|
continue
|
|
19
20
|
with self.subTest(f"Assert {self.form_class.__name__}.{field_name} query_params are valid."):
|
|
20
21
|
query_params_fields = set(field_class.query_params.keys())
|
|
22
|
+
query_params_fields = query_params_fields - set(NON_FILTER_QUERY_PARAMS)
|
|
21
23
|
if not query_params_fields:
|
|
22
24
|
self.skipTest(f"{self.form_class.__name__}.{field_name} has no query_params")
|
|
23
25
|
field_model = field_class.queryset.model
|
|
@@ -6,7 +6,9 @@ from django.test import override_settings, tag
|
|
|
6
6
|
from django.urls import reverse
|
|
7
7
|
from django.utils.functional import classproperty
|
|
8
8
|
from selenium.webdriver.common.keys import Keys
|
|
9
|
+
from selenium.webdriver.support.wait import WebDriverWait
|
|
9
10
|
from splinter.browser import Browser
|
|
11
|
+
from splinter.exceptions import ElementDoesNotExist
|
|
10
12
|
|
|
11
13
|
from nautobot.core import testing
|
|
12
14
|
|
|
@@ -20,6 +22,71 @@ SELENIUM_HOST = os.getenv("NAUTOBOT_SELENIUM_HOST", "host.docker.internal")
|
|
|
20
22
|
LOGIN_URL = reverse(settings.LOGIN_URL)
|
|
21
23
|
|
|
22
24
|
|
|
25
|
+
class ObjectsListMixin:
|
|
26
|
+
"""
|
|
27
|
+
Helper class for easier testing and navigating on standard Nautobot objects list page.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def select_all_items(self):
|
|
31
|
+
self.browser.find_by_xpath('//*[@id="object_list_form"]//input[@class="toggle"]').click()
|
|
32
|
+
|
|
33
|
+
def select_one_item(self):
|
|
34
|
+
self.browser.find_by_xpath('//*[@id="object_list_form"]//input[@name="pk"]').click()
|
|
35
|
+
|
|
36
|
+
def click_bulk_delete(self):
|
|
37
|
+
self.browser.find_by_xpath(
|
|
38
|
+
'//*[@id="object_list_form"]//button[@type="submit"]/following-sibling::button[1]'
|
|
39
|
+
).click()
|
|
40
|
+
self.browser.find_by_xpath('//*[@id="object_list_form"]//button[@name="_delete"]').click()
|
|
41
|
+
|
|
42
|
+
def click_bulk_edit(self):
|
|
43
|
+
self.browser.find_by_xpath('//*[@id="object_list_form"]//button[@type="submit"]').click()
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def objects_list_visible_items(self):
|
|
47
|
+
objects_table_container = self.browser.find_by_xpath('//*[@id="object_list_form"]/div[1]/div')
|
|
48
|
+
try:
|
|
49
|
+
objects_table = objects_table_container.find_by_tag("tbody")
|
|
50
|
+
return len(objects_table.find_by_tag("tr"))
|
|
51
|
+
except ElementDoesNotExist:
|
|
52
|
+
return 0
|
|
53
|
+
|
|
54
|
+
def apply_filter(self, field, value):
|
|
55
|
+
self.browser.find_by_xpath('//*[@id="id__filterbtn"]').click()
|
|
56
|
+
self.fill_filters_select2_field(field, value)
|
|
57
|
+
self.browser.find_by_xpath('//*[@id="default-filter"]//button[@type="submit"]').click()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class BulkOperationsMixin:
|
|
61
|
+
def confirm_bulk_delete_operation(self):
|
|
62
|
+
self.browser.find_by_xpath('//button[@name="_confirm" and @type="submit"]').click()
|
|
63
|
+
|
|
64
|
+
def submit_bulk_edit_operation(self):
|
|
65
|
+
self.browser.find_by_xpath("//button[@name='_apply']", wait_time=5).click()
|
|
66
|
+
|
|
67
|
+
def wait_for_job_result(self):
|
|
68
|
+
end_statuses = ["Completed", "Failed"]
|
|
69
|
+
WebDriverWait(self.browser, 30).until(
|
|
70
|
+
lambda driver: driver.find_by_id("pending-result-label").text in end_statuses
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
return self.browser.find_by_id("pending-result-label").text
|
|
74
|
+
|
|
75
|
+
def verify_job_description(self, expected_job_description):
|
|
76
|
+
job_description = self.browser.find_by_xpath('//td[text()="Job Description"]/following-sibling::td[1]').text
|
|
77
|
+
self.assertEqual(job_description, expected_job_description)
|
|
78
|
+
|
|
79
|
+
def assertIsBulkDeleteJob(self):
|
|
80
|
+
self.verify_job_description("Bulk delete objects.")
|
|
81
|
+
|
|
82
|
+
def assertIsBulkEditJob(self):
|
|
83
|
+
self.verify_job_description("Bulk edit objects.")
|
|
84
|
+
|
|
85
|
+
def assertJobStatusIsCompleted(self):
|
|
86
|
+
job_status = self.wait_for_job_result()
|
|
87
|
+
self.assertEqual(job_status, "Completed")
|
|
88
|
+
|
|
89
|
+
|
|
23
90
|
# In CI, sometimes the FQDN of SELENIUM_HOST gets used, other times it seems to be just the hostname?
|
|
24
91
|
@override_settings(ALLOWED_HOSTS=["nautobot.example.com", SELENIUM_HOST, SELENIUM_HOST.split(".")[0]])
|
|
25
92
|
@tag("integration")
|
|
@@ -112,21 +179,36 @@ class SeleniumTestCase(StaticLiveServerTestCase, testing.NautobotTestCaseMixin):
|
|
|
112
179
|
# Wait for body element to appear
|
|
113
180
|
self.assertTrue(self.browser.is_element_present_by_tag("body", wait_time=5), "Page failed to load")
|
|
114
181
|
|
|
115
|
-
def
|
|
182
|
+
def _fill_select2_field(self, field_name, value, search_box_class=None):
|
|
116
183
|
"""
|
|
117
184
|
Helper function to fill a Select2 single selection field.
|
|
118
185
|
"""
|
|
186
|
+
if search_box_class is None:
|
|
187
|
+
search_box_class = "select2-search select2-search--dropdown"
|
|
188
|
+
|
|
119
189
|
self.browser.find_by_xpath(f"//select[@id='id_{field_name}']//following-sibling::span").click()
|
|
120
|
-
search_box = self.browser.find_by_xpath(
|
|
121
|
-
"//*[@class='select2-search select2-search--dropdown']//input", wait_time=5
|
|
122
|
-
)
|
|
190
|
+
search_box = self.browser.find_by_xpath(f"//*[@class='{search_box_class}']//input", wait_time=5)
|
|
123
191
|
for _ in search_box.first.type(value, slowly=True):
|
|
124
192
|
pass
|
|
125
193
|
|
|
126
194
|
# wait for "searching" to disappear
|
|
127
195
|
self.browser.is_element_not_present_by_css(".loading-results", wait_time=5)
|
|
196
|
+
return search_box
|
|
197
|
+
|
|
198
|
+
def fill_select2_field(self, field_name, value):
|
|
199
|
+
"""
|
|
200
|
+
Helper function to fill a Select2 single selection field on add/edit forms.
|
|
201
|
+
"""
|
|
202
|
+
search_box = self._fill_select2_field(field_name, value)
|
|
128
203
|
search_box.first.type(Keys.ENTER)
|
|
129
204
|
|
|
205
|
+
def fill_filters_select2_field(self, field_name, value):
|
|
206
|
+
"""
|
|
207
|
+
Helper function to fill a Select2 single selection field on filters modals.
|
|
208
|
+
"""
|
|
209
|
+
self._fill_select2_field(field_name, value, search_box_class="select2-search select2-search--inline")
|
|
210
|
+
self.browser.find_by_xpath(f"//li[@class='select2-results__option' and text()='{value}']").click()
|
|
211
|
+
|
|
130
212
|
def fill_select2_multiselect_field(self, field_name, value):
|
|
131
213
|
"""
|
|
132
214
|
Helper function to fill a Select2 multi-selection field.
|
nautobot/core/testing/mixins.py
CHANGED
|
@@ -144,13 +144,18 @@ class NautobotTestCaseMixin:
|
|
|
144
144
|
# Permissions management
|
|
145
145
|
#
|
|
146
146
|
|
|
147
|
-
def add_permissions(self, *names):
|
|
147
|
+
def add_permissions(self, *names, **kwargs):
|
|
148
148
|
"""
|
|
149
149
|
Assign a set of permissions to the test user. Accepts permission names in the form <app>.<action>_<model>.
|
|
150
|
+
Additional keyword arguments will be passed to the ObjectPermission constructor to allow creating more detailed permissions.
|
|
151
|
+
|
|
152
|
+
Examples:
|
|
153
|
+
>>> add_permissions("ipam.add_vlangroup", "ipam.view_vlangroup")
|
|
154
|
+
>>> add_permissions("ipam.add_vlangroup", "ipam.view_vlangroup", constraints={"pk": "uuid-1234"})
|
|
150
155
|
"""
|
|
151
156
|
for name in names:
|
|
152
157
|
ct, action = permissions.resolve_permission_ct(name)
|
|
153
|
-
obj_perm = users_models.ObjectPermission(name=name, actions=[action])
|
|
158
|
+
obj_perm = users_models.ObjectPermission(name=name, actions=[action], **kwargs)
|
|
154
159
|
obj_perm.save()
|
|
155
160
|
obj_perm.users.add(self.user)
|
|
156
161
|
obj_perm.object_types.add(ct)
|