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/views/generic.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from copy import deepcopy
|
|
2
2
|
import logging
|
|
3
3
|
import re
|
|
4
|
+
from typing import ClassVar, Optional
|
|
4
5
|
|
|
5
6
|
from django.conf import settings
|
|
6
7
|
from django.contrib import messages
|
|
@@ -8,12 +9,11 @@ from django.contrib.auth.mixins import LoginRequiredMixin
|
|
|
8
9
|
from django.contrib.auth.models import AnonymousUser
|
|
9
10
|
from django.contrib.contenttypes.models import ContentType
|
|
10
11
|
from django.core.exceptions import (
|
|
11
|
-
FieldDoesNotExist,
|
|
12
12
|
ObjectDoesNotExist,
|
|
13
13
|
ValidationError,
|
|
14
14
|
)
|
|
15
15
|
from django.db import IntegrityError, transaction
|
|
16
|
-
from django.db.models import
|
|
16
|
+
from django.db.models import Model, ProtectedError, Q, QuerySet
|
|
17
17
|
from django.forms import Form, ModelMultipleChoiceField, MultipleHiddenInput
|
|
18
18
|
from django.http import HttpResponse
|
|
19
19
|
from django.shortcuts import get_object_or_404, redirect, render
|
|
@@ -22,7 +22,8 @@ from django.utils.encoding import iri_to_uri
|
|
|
22
22
|
from django.utils.html import format_html
|
|
23
23
|
from django.utils.http import url_has_allowed_host_and_scheme
|
|
24
24
|
from django.views.generic import View
|
|
25
|
-
from
|
|
25
|
+
from django_filters import FilterSet
|
|
26
|
+
from django_tables2 import RequestConfig, Table
|
|
26
27
|
|
|
27
28
|
from nautobot.core.api.utils import get_serializer_for_model
|
|
28
29
|
from nautobot.core.constants import MAX_PAGE_SIZE_DEFAULT
|
|
@@ -47,7 +48,7 @@ from nautobot.core.utils.requests import (
|
|
|
47
48
|
get_filterable_params_from_filter_params,
|
|
48
49
|
normalize_querydict,
|
|
49
50
|
)
|
|
50
|
-
from nautobot.core.views.mixins import
|
|
51
|
+
from nautobot.core.views.mixins import BulkEditAndBulkDeleteModelMixin, GetReturnURLMixin, ObjectPermissionRequiredMixin
|
|
51
52
|
from nautobot.core.views.paginator import EnhancedPaginator, get_paginate_count
|
|
52
53
|
from nautobot.core.views.utils import (
|
|
53
54
|
check_filter_for_display,
|
|
@@ -58,9 +59,7 @@ from nautobot.core.views.utils import (
|
|
|
58
59
|
prepare_cloned_fields,
|
|
59
60
|
view_changes_not_saved,
|
|
60
61
|
)
|
|
61
|
-
from nautobot.extras.context_managers import deferred_change_logging_for_bulk_operation
|
|
62
62
|
from nautobot.extras.models import ExportTemplate, SavedView, UserSavedViewAssociation
|
|
63
|
-
from nautobot.extras.utils import bulk_delete_with_bulk_change_logging, remove_prefix_from_cf_key
|
|
64
63
|
|
|
65
64
|
|
|
66
65
|
class GenericView(LoginRequiredMixin, View):
|
|
@@ -79,8 +78,8 @@ class ObjectView(ObjectPermissionRequiredMixin, View):
|
|
|
79
78
|
template_name: Name of the template to use
|
|
80
79
|
"""
|
|
81
80
|
|
|
82
|
-
queryset = None
|
|
83
|
-
template_name = None
|
|
81
|
+
queryset: ClassVar[Optional[QuerySet]] = None # TODO: required, declared Optional only to avoid breaking change
|
|
82
|
+
template_name: ClassVar[Optional[str]] = None
|
|
84
83
|
object_detail_content = None
|
|
85
84
|
|
|
86
85
|
def get_required_permission(self):
|
|
@@ -142,10 +141,10 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
|
|
|
142
141
|
non_filter_params: List of query parameters that are **not** used for queryset filtering
|
|
143
142
|
"""
|
|
144
143
|
|
|
145
|
-
queryset = None
|
|
146
|
-
filterset = None
|
|
147
|
-
filterset_form = None
|
|
148
|
-
table = None
|
|
144
|
+
queryset: Optional[QuerySet] = None # TODO: required, declared Optional only to avoid a breaking change
|
|
145
|
+
filterset: Optional[type[FilterSet]] = None
|
|
146
|
+
filterset_form: Optional[type[Form]] = None
|
|
147
|
+
table: Optional[type[Table]] = None
|
|
149
148
|
template_name = "generic/object_list.html"
|
|
150
149
|
action_buttons = ("add", "import", "export")
|
|
151
150
|
non_filter_params = (
|
|
@@ -162,7 +161,11 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
|
|
|
162
161
|
def get_filter_params(self, request):
|
|
163
162
|
"""Helper function - take request.GET and discard any parameters that are not used for queryset filtering."""
|
|
164
163
|
params = request.GET.copy()
|
|
165
|
-
filter_params = get_filterable_params_from_filter_params(
|
|
164
|
+
filter_params = get_filterable_params_from_filter_params(
|
|
165
|
+
params,
|
|
166
|
+
self.non_filter_params,
|
|
167
|
+
self.filterset(), # pylint: disable=not-callable # this fn is only called if filterset is not None
|
|
168
|
+
)
|
|
166
169
|
if params.get("saved_view") and not filter_params and not params.get("all_filters_removed"):
|
|
167
170
|
return SavedView.objects.get(pk=params.get("saved_view")).config.get("filter_params", {})
|
|
168
171
|
return filter_params
|
|
@@ -236,9 +239,9 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
|
|
|
236
239
|
except ObjectDoesNotExist:
|
|
237
240
|
pass
|
|
238
241
|
|
|
239
|
-
if self.filterset:
|
|
242
|
+
if self.filterset is not None:
|
|
240
243
|
filter_params = self.get_filter_params(request)
|
|
241
|
-
filterset = self.filterset(filter_params, self.queryset)
|
|
244
|
+
filterset = self.filterset(filter_params, self.queryset) # pylint:disable=not-callable # only if not None
|
|
242
245
|
self.queryset = filterset.qs
|
|
243
246
|
if not filterset.is_valid():
|
|
244
247
|
messages.error(
|
|
@@ -266,7 +269,7 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
|
|
|
266
269
|
else:
|
|
267
270
|
dynamic_filter_form = DynamicFilterFormSet(filterset=filterset)
|
|
268
271
|
|
|
269
|
-
if self.filterset_form:
|
|
272
|
+
if self.filterset_form is not None:
|
|
270
273
|
filter_form = self.filterset_form(filter_params, label_suffix="")
|
|
271
274
|
|
|
272
275
|
# Check for export template rendering
|
|
@@ -326,14 +329,14 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
|
|
|
326
329
|
messages.error(request, f"Saved view {current_saved_view_pk} not found")
|
|
327
330
|
|
|
328
331
|
# Construct the objects table
|
|
329
|
-
if self.table:
|
|
332
|
+
if self.table is not None:
|
|
330
333
|
if self.request.GET.getlist("sort") or (
|
|
331
334
|
current_saved_view is not None and current_saved_view.config.get("sort_order")
|
|
332
335
|
):
|
|
333
336
|
hide_hierarchy_ui = True # hide tree hierarchy if custom sort is used
|
|
334
337
|
table_changes_pending = self.request.GET.get("table_changes_pending", False)
|
|
335
338
|
|
|
336
|
-
table = self.table(
|
|
339
|
+
table = self.table( # pylint: disable=not-callable # we confirmed that self.table is not None
|
|
337
340
|
self.queryset,
|
|
338
341
|
table_changes_pending=table_changes_pending,
|
|
339
342
|
saved_view=current_saved_view,
|
|
@@ -410,8 +413,8 @@ class ObjectEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|
|
410
413
|
template_name: The name of the template
|
|
411
414
|
"""
|
|
412
415
|
|
|
413
|
-
queryset = None
|
|
414
|
-
model_form = None
|
|
416
|
+
queryset: Optional[QuerySet] = None # TODO: required, declared Optional only to avoid a breaking change
|
|
417
|
+
model_form: Optional[type[Form]] = None # TODO: required, declared Optional only to avoid a breaking change
|
|
415
418
|
template_name = "generic/object_create.html"
|
|
416
419
|
|
|
417
420
|
def get_required_permission(self):
|
|
@@ -455,7 +458,9 @@ class ObjectEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|
|
455
458
|
obj = self.alter_obj(self.get_object(kwargs), request, args, kwargs)
|
|
456
459
|
|
|
457
460
|
initial_data = normalize_querydict(request.GET, form_class=self.model_form)
|
|
458
|
-
|
|
461
|
+
if self.model_form is None:
|
|
462
|
+
raise RuntimeError("self.model_form must not be None")
|
|
463
|
+
form = self.model_form(instance=obj, initial=initial_data) # pylint: disable=not-callable
|
|
459
464
|
restrict_form_fields(form, request.user)
|
|
460
465
|
|
|
461
466
|
return render(
|
|
@@ -485,7 +490,9 @@ class ObjectEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|
|
485
490
|
def post(self, request, *args, **kwargs):
|
|
486
491
|
logger = logging.getLogger(__name__ + ".ObjectEditView")
|
|
487
492
|
obj = self.alter_obj(self.get_object(kwargs), request, args, kwargs)
|
|
488
|
-
|
|
493
|
+
if self.model_form is None:
|
|
494
|
+
raise RuntimeError("self.model_form must not be None")
|
|
495
|
+
form = self.model_form( # pylint: disable=not-callable
|
|
489
496
|
data=request.POST,
|
|
490
497
|
files=request.FILES,
|
|
491
498
|
initial=normalize_querydict(request.GET, form_class=self.model_form),
|
|
@@ -553,7 +560,7 @@ class ObjectDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|
|
553
560
|
template_name: The name of the template
|
|
554
561
|
"""
|
|
555
562
|
|
|
556
|
-
queryset = None
|
|
563
|
+
queryset: Optional[QuerySet] = None # TODO: required, declared Optional only to avoid a breaking change
|
|
557
564
|
template_name = "generic/object_delete.html"
|
|
558
565
|
|
|
559
566
|
def get_required_permission(self):
|
|
@@ -633,9 +640,9 @@ class BulkCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|
|
633
640
|
template_name: The name of the template
|
|
634
641
|
"""
|
|
635
642
|
|
|
636
|
-
queryset = None
|
|
637
|
-
form = None
|
|
638
|
-
model_form = None
|
|
643
|
+
queryset: Optional[QuerySet] = None # TODO: required, declared Optional only to avoid a breaking change
|
|
644
|
+
form: Optional[type[Form]] = None # TODO: required, declared Optional only to avoid a breaking change
|
|
645
|
+
model_form: Optional[type[Form]] = None # TODO: required, declared Optional only to avoid a breaking change
|
|
639
646
|
pattern_target = ""
|
|
640
647
|
template_name = None
|
|
641
648
|
|
|
@@ -645,12 +652,14 @@ class BulkCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|
|
645
652
|
def get(self, request):
|
|
646
653
|
# Set initial values for visible form fields from query args
|
|
647
654
|
initial = {}
|
|
655
|
+
if self.form is None or self.model_form is None:
|
|
656
|
+
raise RuntimeError("self.form and self.model_form must not be None")
|
|
648
657
|
for field in getattr(self.model_form._meta, "fields", []):
|
|
649
658
|
if request.GET.get(field):
|
|
650
659
|
initial[field] = request.GET[field]
|
|
651
660
|
|
|
652
|
-
form = self.form()
|
|
653
|
-
model_form = self.model_form(initial=initial)
|
|
661
|
+
form = self.form() # pylint: disable=not-callable
|
|
662
|
+
model_form = self.model_form(initial=initial) # pylint: disable=not-callable
|
|
654
663
|
|
|
655
664
|
return render(
|
|
656
665
|
request,
|
|
@@ -665,9 +674,11 @@ class BulkCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|
|
665
674
|
|
|
666
675
|
def post(self, request):
|
|
667
676
|
logger = logging.getLogger(__name__ + ".BulkCreateView")
|
|
677
|
+
if self.queryset is None or self.form is None or self.model_form is None:
|
|
678
|
+
raise RuntimeError("self.queryset, self.form, and self.model_form must not be None")
|
|
668
679
|
model = self.queryset.model
|
|
669
|
-
form = self.form(request.POST)
|
|
670
|
-
model_form = self.model_form(request.POST)
|
|
680
|
+
form = self.form(request.POST) # pylint: disable=not-callable
|
|
681
|
+
model_form = self.model_form(request.POST) # pylint: disable=not-callable
|
|
671
682
|
|
|
672
683
|
if form.is_valid():
|
|
673
684
|
logger.debug("Form validation was successful")
|
|
@@ -680,7 +691,7 @@ class BulkCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|
|
680
691
|
for value in pattern:
|
|
681
692
|
# Reinstantiate the model form each time to avoid overwriting the same instance. Use a mutable
|
|
682
693
|
# copy of the POST QueryDict so that we can update the target field value.
|
|
683
|
-
model_form = self.model_form(request.POST.copy())
|
|
694
|
+
model_form = self.model_form(request.POST.copy()) # pylint: disable=not-callable
|
|
684
695
|
model_form.data[self.pattern_target] = value
|
|
685
696
|
|
|
686
697
|
# Validate each new object independently.
|
|
@@ -742,8 +753,8 @@ class ObjectImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|
|
742
753
|
template_name: The name of the template
|
|
743
754
|
"""
|
|
744
755
|
|
|
745
|
-
queryset = None
|
|
746
|
-
model_form = None
|
|
756
|
+
queryset: Optional[QuerySet] = None # TODO: required, declared Optional only to avoid a breaking change
|
|
757
|
+
model_form: Optional[type[Form]] = None # TODO: required, declared Optional only to avoid a breaking change
|
|
747
758
|
related_object_forms = {}
|
|
748
759
|
template_name = "generic/object_import.html"
|
|
749
760
|
|
|
@@ -767,12 +778,15 @@ class ObjectImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|
|
767
778
|
logger = logging.getLogger(__name__ + ".ObjectImportView")
|
|
768
779
|
form = ImportForm(request.POST)
|
|
769
780
|
|
|
781
|
+
if self.model_form is None or self.queryset is None:
|
|
782
|
+
raise RuntimeError("self.model_form and self.queryset must not be None")
|
|
783
|
+
|
|
770
784
|
if form.is_valid():
|
|
771
785
|
logger.debug("Import form validation was successful")
|
|
772
786
|
|
|
773
787
|
# Initialize model form
|
|
774
788
|
data = form.cleaned_data["data"]
|
|
775
|
-
model_form = self.model_form(data)
|
|
789
|
+
model_form = self.model_form(data) # pylint: disable=not-callable
|
|
776
790
|
restrict_form_fields(model_form, request.user)
|
|
777
791
|
|
|
778
792
|
# Assign default values for any fields which were not specified. We have to do this manually because passing
|
|
@@ -887,8 +901,8 @@ class BulkImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View): #
|
|
|
887
901
|
template_name: The name of the template
|
|
888
902
|
"""
|
|
889
903
|
|
|
890
|
-
queryset = None
|
|
891
|
-
table = None
|
|
904
|
+
queryset: Optional[QuerySet] = None # TODO: required, declared Optional only to avoid a breaking change
|
|
905
|
+
table: Optional[type[Table]] = None # TODO: required, declared Optional only to avoid a breaking change
|
|
892
906
|
template_name = "generic/object_bulk_import.html"
|
|
893
907
|
|
|
894
908
|
def __init__(self, *args, **kwargs):
|
|
@@ -932,6 +946,9 @@ class BulkImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View): #
|
|
|
932
946
|
if form.is_valid():
|
|
933
947
|
logger.debug("Form validation was successful")
|
|
934
948
|
|
|
949
|
+
if self.queryset is None or self.table is None:
|
|
950
|
+
raise RuntimeError("self.queryset and self.table must not be None")
|
|
951
|
+
|
|
935
952
|
try:
|
|
936
953
|
# Iterate through CSV data and bind each row to a new model form instance.
|
|
937
954
|
with transaction.atomic():
|
|
@@ -942,7 +959,7 @@ class BulkImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View): #
|
|
|
942
959
|
raise ObjectDoesNotExist
|
|
943
960
|
|
|
944
961
|
# Compile a table containing the imported objects
|
|
945
|
-
obj_table = self.table(new_objs)
|
|
962
|
+
obj_table = self.table(new_objs) # pylint: disable=not-callable
|
|
946
963
|
|
|
947
964
|
if new_objs:
|
|
948
965
|
msg = f"Imported {len(new_objs)} {new_objs[0]._meta.verbose_name_plural}"
|
|
@@ -982,7 +999,7 @@ class BulkImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View): #
|
|
|
982
999
|
)
|
|
983
1000
|
|
|
984
1001
|
|
|
985
|
-
class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin,
|
|
1002
|
+
class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, BulkEditAndBulkDeleteModelMixin, View):
|
|
986
1003
|
"""
|
|
987
1004
|
Edit objects in bulk.
|
|
988
1005
|
|
|
@@ -993,10 +1010,10 @@ class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, EditAndDele
|
|
|
993
1010
|
template_name: The name of the template
|
|
994
1011
|
"""
|
|
995
1012
|
|
|
996
|
-
queryset = None
|
|
997
|
-
filterset = None
|
|
998
|
-
table = None
|
|
999
|
-
form = None
|
|
1013
|
+
queryset: Optional[QuerySet] = None # TODO: required, declared Optional only to avoid a breaking change
|
|
1014
|
+
filterset: Optional[type[FilterSet]] = None
|
|
1015
|
+
table: Optional[type[Table]] = None # TODO: required, declared Optional only to avoid a breaking change
|
|
1016
|
+
form: Optional[type[Form]] = None # TODO: required, declared Optional only to avoid a breaking change
|
|
1000
1017
|
template_name = "generic/object_bulk_edit.html"
|
|
1001
1018
|
|
|
1002
1019
|
def get_required_permission(self):
|
|
@@ -1015,6 +1032,8 @@ class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, EditAndDele
|
|
|
1015
1032
|
|
|
1016
1033
|
def post(self, request, **kwargs):
|
|
1017
1034
|
logger = logging.getLogger(__name__ + ".BulkEditView")
|
|
1035
|
+
if self.queryset is None or self.form is None or self.table is None:
|
|
1036
|
+
raise RuntimeError("self.queryset, self.form, and self.table must not be None")
|
|
1018
1037
|
model = self.queryset.model
|
|
1019
1038
|
edit_all = request.POST.get("_all")
|
|
1020
1039
|
|
|
@@ -1027,98 +1046,12 @@ class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, EditAndDele
|
|
|
1027
1046
|
queryset = self.queryset.filter(pk__in=pk_list)
|
|
1028
1047
|
|
|
1029
1048
|
if "_apply" in request.POST:
|
|
1030
|
-
form = self.form(model, request.POST, edit_all=edit_all)
|
|
1049
|
+
form = self.form(model, request.POST, edit_all=edit_all) # pylint: disable=not-callable
|
|
1031
1050
|
restrict_form_fields(form, request.user)
|
|
1032
1051
|
|
|
1033
1052
|
if form.is_valid():
|
|
1034
1053
|
logger.debug("Form validation was successful")
|
|
1035
|
-
|
|
1036
|
-
form_relationships = getattr(form, "relationships", [])
|
|
1037
|
-
standard_fields = [
|
|
1038
|
-
field
|
|
1039
|
-
for field in form.fields
|
|
1040
|
-
if field not in form_custom_fields + form_relationships + ["pk"] + ["object_note"]
|
|
1041
|
-
]
|
|
1042
|
-
nullified_fields = request.POST.getlist("_nullify")
|
|
1043
|
-
|
|
1044
|
-
try:
|
|
1045
|
-
with deferred_change_logging_for_bulk_operation():
|
|
1046
|
-
updated_objects = []
|
|
1047
|
-
queryset = queryset if edit_all else queryset.filter(pk__in=form.cleaned_data["pk"])
|
|
1048
|
-
for obj in queryset:
|
|
1049
|
-
obj = self.alter_obj(obj, request, [], kwargs)
|
|
1050
|
-
|
|
1051
|
-
# Update standard fields. If a field is listed in _nullify, delete its value.
|
|
1052
|
-
for name in standard_fields:
|
|
1053
|
-
try:
|
|
1054
|
-
model_field = model._meta.get_field(name)
|
|
1055
|
-
except FieldDoesNotExist:
|
|
1056
|
-
# This form field is used to modify a field rather than set its value directly
|
|
1057
|
-
model_field = None
|
|
1058
|
-
|
|
1059
|
-
# Handle nullification
|
|
1060
|
-
if name in form.nullable_fields and name in nullified_fields:
|
|
1061
|
-
if isinstance(model_field, ManyToManyField):
|
|
1062
|
-
getattr(obj, name).set([])
|
|
1063
|
-
else:
|
|
1064
|
-
setattr(obj, name, None if model_field is not None and model_field.null else "")
|
|
1065
|
-
|
|
1066
|
-
# ManyToManyFields
|
|
1067
|
-
elif isinstance(model_field, ManyToManyField):
|
|
1068
|
-
if form.cleaned_data[name]:
|
|
1069
|
-
getattr(obj, name).set(form.cleaned_data[name])
|
|
1070
|
-
# Normal fields
|
|
1071
|
-
elif form.cleaned_data[name] not in (None, ""):
|
|
1072
|
-
setattr(obj, name, form.cleaned_data[name])
|
|
1073
|
-
|
|
1074
|
-
# Update custom fields
|
|
1075
|
-
for field_name in form_custom_fields:
|
|
1076
|
-
if field_name in form.nullable_fields and field_name in nullified_fields:
|
|
1077
|
-
obj.cf[remove_prefix_from_cf_key(field_name)] = None
|
|
1078
|
-
elif form.cleaned_data.get(field_name) not in (None, "", []):
|
|
1079
|
-
obj.cf[remove_prefix_from_cf_key(field_name)] = form.cleaned_data[field_name]
|
|
1080
|
-
|
|
1081
|
-
obj.full_clean()
|
|
1082
|
-
obj.save()
|
|
1083
|
-
updated_objects.append(obj)
|
|
1084
|
-
logger.debug(f"Saved {obj} (PK: {obj.pk})")
|
|
1085
|
-
|
|
1086
|
-
# Add/remove tags
|
|
1087
|
-
if form.cleaned_data.get("add_tags", None):
|
|
1088
|
-
obj.tags.add(*form.cleaned_data["add_tags"])
|
|
1089
|
-
if form.cleaned_data.get("remove_tags", None):
|
|
1090
|
-
obj.tags.remove(*form.cleaned_data["remove_tags"])
|
|
1091
|
-
|
|
1092
|
-
if hasattr(form, "save_relationships") and callable(form.save_relationships):
|
|
1093
|
-
# Add/remove relationship associations
|
|
1094
|
-
form.save_relationships(instance=obj, nullified_fields=nullified_fields)
|
|
1095
|
-
|
|
1096
|
-
if hasattr(form, "save_note") and callable(form.save_note):
|
|
1097
|
-
form.save_note(instance=obj, user=request.user)
|
|
1098
|
-
|
|
1099
|
-
self.extra_post_save_action(obj, form)
|
|
1100
|
-
|
|
1101
|
-
# Enforce object-level permissions
|
|
1102
|
-
if self.queryset.filter(pk__in=[obj.pk for obj in updated_objects]).count() != len(
|
|
1103
|
-
updated_objects
|
|
1104
|
-
):
|
|
1105
|
-
raise ObjectDoesNotExist
|
|
1106
|
-
|
|
1107
|
-
if updated_objects:
|
|
1108
|
-
msg = f"Updated {len(updated_objects)} {model._meta.verbose_name_plural}"
|
|
1109
|
-
logger.info(msg)
|
|
1110
|
-
messages.success(self.request, msg)
|
|
1111
|
-
|
|
1112
|
-
return redirect(self.get_return_url(request))
|
|
1113
|
-
|
|
1114
|
-
except ValidationError as e:
|
|
1115
|
-
messages.error(self.request, f"{obj} failed validation: {e}")
|
|
1116
|
-
|
|
1117
|
-
except ObjectDoesNotExist:
|
|
1118
|
-
msg = "Object update failed due to object-level permissions violation"
|
|
1119
|
-
logger.debug(msg)
|
|
1120
|
-
form.add_error(None, msg)
|
|
1121
|
-
|
|
1054
|
+
return self.send_bulk_edit_objects_to_job(request, form, model)
|
|
1122
1055
|
else:
|
|
1123
1056
|
logger.debug("Form validation failed")
|
|
1124
1057
|
|
|
@@ -1134,13 +1067,13 @@ class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, EditAndDele
|
|
|
1134
1067
|
elif "device_type" in request.GET:
|
|
1135
1068
|
initial_data["device_type"] = request.GET.get("device_type")
|
|
1136
1069
|
|
|
1137
|
-
form = self.form(model, initial=initial_data, edit_all=edit_all)
|
|
1070
|
+
form = self.form(model, initial=initial_data, edit_all=edit_all) # pylint: disable=not-callable
|
|
1138
1071
|
restrict_form_fields(form, request.user)
|
|
1139
1072
|
|
|
1140
1073
|
# Retrieve objects being edited
|
|
1141
1074
|
table = None
|
|
1142
1075
|
if not edit_all:
|
|
1143
|
-
table = self.table(queryset, orderable=False)
|
|
1076
|
+
table = self.table(queryset, orderable=False) # pylint: disable=not-callable
|
|
1144
1077
|
if not table.rows:
|
|
1145
1078
|
messages.warning(request, f"No {model._meta.verbose_name_plural} were selected.")
|
|
1146
1079
|
return redirect(self.get_return_url(request))
|
|
@@ -1167,7 +1100,7 @@ class BulkRenameView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|
|
1167
1100
|
An extendable view for renaming objects in bulk.
|
|
1168
1101
|
"""
|
|
1169
1102
|
|
|
1170
|
-
queryset = None
|
|
1103
|
+
queryset: Optional[QuerySet] = None # TODO: required, declared Optional only to avoid a breaking change
|
|
1171
1104
|
template_name = "generic/object_bulk_rename.html"
|
|
1172
1105
|
|
|
1173
1106
|
def __init__(self, *args, **kwargs):
|
|
@@ -1262,7 +1195,7 @@ class BulkRenameView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|
|
1262
1195
|
return ""
|
|
1263
1196
|
|
|
1264
1197
|
|
|
1265
|
-
class BulkDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin,
|
|
1198
|
+
class BulkDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, BulkEditAndBulkDeleteModelMixin, View):
|
|
1266
1199
|
"""
|
|
1267
1200
|
Delete objects in bulk.
|
|
1268
1201
|
|
|
@@ -1273,10 +1206,10 @@ class BulkDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, EditAndDe
|
|
|
1273
1206
|
template_name: The name of the template
|
|
1274
1207
|
"""
|
|
1275
1208
|
|
|
1276
|
-
queryset = None
|
|
1277
|
-
filterset = None
|
|
1278
|
-
table = None
|
|
1279
|
-
form = None
|
|
1209
|
+
queryset: Optional[QuerySet] = None # TODO: required, declared Optional only to avoid a breaking change
|
|
1210
|
+
filterset: Optional[type[FilterSet]] = None
|
|
1211
|
+
table: Optional[type[Table]] = None # TODO: required, declared Optional only to avoid a breaking change
|
|
1212
|
+
form: Optional[type[Form]] = None
|
|
1280
1213
|
template_name = "generic/object_bulk_delete.html"
|
|
1281
1214
|
|
|
1282
1215
|
def get_required_permission(self):
|
|
@@ -1285,48 +1218,32 @@ class BulkDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, EditAndDe
|
|
|
1285
1218
|
def get(self, request):
|
|
1286
1219
|
return redirect(self.get_return_url(request))
|
|
1287
1220
|
|
|
1288
|
-
def _perform_delete_operation(self, request, queryset, model):
|
|
1289
|
-
logger = logging.getLogger(__name__ + ".BulkDeleteView")
|
|
1290
|
-
self.perform_pre_delete(request, queryset)
|
|
1291
|
-
try:
|
|
1292
|
-
_, deleted_info = bulk_delete_with_bulk_change_logging(queryset)
|
|
1293
|
-
deleted_count = deleted_info[model._meta.label]
|
|
1294
|
-
except ProtectedError as e:
|
|
1295
|
-
logger.info("Caught ProtectedError while attempting to delete objects")
|
|
1296
|
-
handle_protectederror(queryset, request, e)
|
|
1297
|
-
return redirect(self.get_return_url(request))
|
|
1298
|
-
msg = f"Deleted {deleted_count} {model._meta.verbose_name_plural}"
|
|
1299
|
-
logger.info(msg)
|
|
1300
|
-
messages.success(request, msg)
|
|
1301
|
-
return redirect(self.get_return_url(request))
|
|
1302
|
-
|
|
1303
1221
|
def post(self, request, **kwargs):
|
|
1304
1222
|
logger = logging.getLogger(f"{__name__}.BulkDeleteView")
|
|
1223
|
+
if self.queryset is None or self.table is None:
|
|
1224
|
+
raise RuntimeError("self.queryset and self.table must not be None")
|
|
1305
1225
|
model = self.queryset.model
|
|
1226
|
+
delete_all = request.POST.get("_all")
|
|
1306
1227
|
|
|
1307
1228
|
# Are we deleting *all* objects in the queryset or just a selected subset?
|
|
1308
|
-
if
|
|
1229
|
+
if delete_all:
|
|
1309
1230
|
queryset = self._get_bulk_edit_delete_all_queryset(request)
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
context = self._bulk_delete_all_context(request, queryset)
|
|
1315
|
-
context.update(self.extra_context())
|
|
1316
|
-
return render(request, self.template_name, context)
|
|
1317
|
-
|
|
1318
|
-
pk_list = request.POST.getlist("pk")
|
|
1231
|
+
pk_list = []
|
|
1232
|
+
else:
|
|
1233
|
+
pk_list = request.POST.getlist("pk")
|
|
1234
|
+
queryset = self.queryset.filter(pk__in=pk_list)
|
|
1319
1235
|
|
|
1320
1236
|
form_cls = self.get_form()
|
|
1321
1237
|
|
|
1322
1238
|
if "_confirm" in request.POST:
|
|
1323
1239
|
form = form_cls(request.POST)
|
|
1240
|
+
# Set pk field requirement here instead of BulkDeleteForm since get_form() may return a different form class
|
|
1241
|
+
if delete_all:
|
|
1242
|
+
form.fields["pk"].required = False
|
|
1243
|
+
|
|
1324
1244
|
if form.is_valid():
|
|
1325
1245
|
logger.debug("Form validation was successful")
|
|
1326
|
-
|
|
1327
|
-
# Delete objects
|
|
1328
|
-
queryset = self.queryset.filter(pk__in=pk_list)
|
|
1329
|
-
return self._perform_delete_operation(request, queryset, model)
|
|
1246
|
+
return self.send_bulk_delete_objects_to_job(request, pk_list, model, delete_all)
|
|
1330
1247
|
else:
|
|
1331
1248
|
logger.debug("Form validation failed")
|
|
1332
1249
|
|
|
@@ -1339,23 +1256,26 @@ class BulkDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, EditAndDe
|
|
|
1339
1256
|
)
|
|
1340
1257
|
|
|
1341
1258
|
# Retrieve objects being deleted
|
|
1342
|
-
table =
|
|
1343
|
-
if not
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1259
|
+
table = None
|
|
1260
|
+
if not delete_all:
|
|
1261
|
+
table = self.table(queryset, orderable=False) # pylint: disable=not-callable
|
|
1262
|
+
if not table.rows:
|
|
1263
|
+
messages.warning(
|
|
1264
|
+
request,
|
|
1265
|
+
f"No {model._meta.verbose_name_plural} were selected for deletion.",
|
|
1266
|
+
)
|
|
1267
|
+
return redirect(self.get_return_url(request))
|
|
1268
|
+
# Hide actions column if present
|
|
1269
|
+
if "actions" in table.columns:
|
|
1270
|
+
table.columns.hide("actions")
|
|
1352
1271
|
|
|
1353
1272
|
context = {
|
|
1354
1273
|
"form": form,
|
|
1355
1274
|
"obj_type_plural": model._meta.verbose_name_plural,
|
|
1356
1275
|
"table": table,
|
|
1357
1276
|
"return_url": self.get_return_url(request),
|
|
1358
|
-
"total_objs_to_delete":
|
|
1277
|
+
"total_objs_to_delete": queryset.count(),
|
|
1278
|
+
"delete_all": delete_all,
|
|
1359
1279
|
}
|
|
1360
1280
|
context.update(self.extra_context())
|
|
1361
1281
|
return render(request, self.template_name, context)
|
|
@@ -1391,17 +1311,19 @@ class ComponentCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View
|
|
|
1391
1311
|
Add one or more components (e.g. interfaces, console ports, etc.) to a Device or VirtualMachine.
|
|
1392
1312
|
"""
|
|
1393
1313
|
|
|
1394
|
-
queryset = None
|
|
1395
|
-
form = None
|
|
1396
|
-
model_form = None
|
|
1314
|
+
queryset: Optional[QuerySet] = None # TODO: required, declared Optional only to avoid a breaking change
|
|
1315
|
+
form: Optional[type[Form]] = None # TODO: required, declared Optional only to avoid a breaking change
|
|
1316
|
+
model_form: Optional[type[Form]] = None # TODO: required, declared Optional only to avoid a breaking change
|
|
1397
1317
|
template_name = "dcim/device_component_add.html"
|
|
1398
1318
|
|
|
1399
1319
|
def get_required_permission(self):
|
|
1400
1320
|
return get_permission_for_model(self.queryset.model, "add")
|
|
1401
1321
|
|
|
1402
1322
|
def get(self, request):
|
|
1403
|
-
|
|
1404
|
-
|
|
1323
|
+
if self.form is None or self.model_form is None:
|
|
1324
|
+
raise RuntimeError("self.form and self.model_form must not be None")
|
|
1325
|
+
form = self.form(initial=normalize_querydict(request.GET, form_class=self.form)) # pylint: disable=not-callable
|
|
1326
|
+
model_form = self.model_form(request.GET) # pylint: disable=not-callable
|
|
1405
1327
|
|
|
1406
1328
|
return render(
|
|
1407
1329
|
request,
|
|
@@ -1416,8 +1338,10 @@ class ComponentCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View
|
|
|
1416
1338
|
|
|
1417
1339
|
def post(self, request):
|
|
1418
1340
|
logger = logging.getLogger(__name__ + ".ComponentCreateView")
|
|
1419
|
-
|
|
1420
|
-
|
|
1341
|
+
if self.form is None or self.model_form is None or self.queryset is None:
|
|
1342
|
+
raise RuntimeError("self.form, self.model_form, and self.queryset must not be None")
|
|
1343
|
+
form = self.form(request.POST, initial=normalize_querydict(request.GET, form_class=self.form)) # pylint: disable=not-callable
|
|
1344
|
+
model_form = self.model_form(request.POST, initial=normalize_querydict(request.GET, form_class=self.model_form)) # pylint: disable=not-callable
|
|
1421
1345
|
|
|
1422
1346
|
if form.is_valid():
|
|
1423
1347
|
new_components = []
|
|
@@ -1432,7 +1356,7 @@ class ComponentCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View
|
|
|
1432
1356
|
data["label"] = label
|
|
1433
1357
|
if hasattr(form, "get_iterative_data"):
|
|
1434
1358
|
data.update(form.get_iterative_data(i))
|
|
1435
|
-
component_form = self.model_form(
|
|
1359
|
+
component_form = self.model_form( # pylint: disable=not-callable
|
|
1436
1360
|
data, initial=normalize_querydict(request.GET, form_class=self.model_form)
|
|
1437
1361
|
)
|
|
1438
1362
|
|
|
@@ -1493,13 +1417,13 @@ class BulkComponentCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin,
|
|
|
1493
1417
|
Add one or more components (e.g. interfaces, console ports, etc.) to a set of Devices or VirtualMachines.
|
|
1494
1418
|
"""
|
|
1495
1419
|
|
|
1496
|
-
parent_model = None
|
|
1420
|
+
parent_model: Optional[type[Model]] = None # TODO: required, declared Optional only to avoid a breaking change
|
|
1497
1421
|
parent_field = None
|
|
1498
|
-
form = None
|
|
1499
|
-
queryset = None
|
|
1500
|
-
model_form = None
|
|
1501
|
-
filterset = None
|
|
1502
|
-
table = None
|
|
1422
|
+
form: Optional[type[Form]] = None # TODO: required, declared Optional only to avoid a breaking change
|
|
1423
|
+
queryset: Optional[QuerySet] = None # TODO: required, declared Optional only to avoid a breaking change
|
|
1424
|
+
model_form: Optional[type[Form]] = None # TODO: required, declared Optional only to avoid a breaking change
|
|
1425
|
+
filterset: Optional[type[FilterSet]] = None
|
|
1426
|
+
table: Optional[type[Table]] = None # TODO: required, declared Optional only to avoid a breaking change
|
|
1503
1427
|
template_name = "generic/object_bulk_add_component.html"
|
|
1504
1428
|
|
|
1505
1429
|
def get_required_permission(self):
|
|
@@ -1507,13 +1431,23 @@ class BulkComponentCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin,
|
|
|
1507
1431
|
|
|
1508
1432
|
def post(self, request):
|
|
1509
1433
|
logger = logging.getLogger(__name__ + ".BulkComponentCreateView")
|
|
1434
|
+
if (
|
|
1435
|
+
self.form is None
|
|
1436
|
+
or self.model_form is None
|
|
1437
|
+
or self.parent_model is None
|
|
1438
|
+
or self.queryset is None
|
|
1439
|
+
or self.table is None
|
|
1440
|
+
):
|
|
1441
|
+
raise RuntimeError(
|
|
1442
|
+
"self.form, self.model_form, self.parent_model, self.queryset, and self.table must not be None"
|
|
1443
|
+
)
|
|
1510
1444
|
parent_model_name = self.parent_model._meta.verbose_name_plural
|
|
1511
1445
|
model_name = self.queryset.model._meta.verbose_name_plural
|
|
1512
1446
|
model = self.queryset.model
|
|
1513
1447
|
|
|
1514
1448
|
# Are we editing *all* objects in the queryset or just a selected subset?
|
|
1515
1449
|
if request.POST.get("_all") and self.filterset is not None:
|
|
1516
|
-
pk_list = [obj.pk for obj in self.filterset(request.GET, self.parent_model.objects.only("pk")).qs]
|
|
1450
|
+
pk_list = [obj.pk for obj in self.filterset(request.GET, self.parent_model.objects.only("pk")).qs] # pylint: disable=not-callable
|
|
1517
1451
|
else:
|
|
1518
1452
|
pk_list = request.POST.getlist("pk")
|
|
1519
1453
|
|
|
@@ -1524,10 +1458,10 @@ class BulkComponentCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin,
|
|
|
1524
1458
|
f"No {self.parent_model._meta.verbose_name_plural} were selected.",
|
|
1525
1459
|
)
|
|
1526
1460
|
return redirect(self.get_return_url(request))
|
|
1527
|
-
table = self.table(selected_objects)
|
|
1461
|
+
table = self.table(selected_objects) # pylint: disable=not-callable
|
|
1528
1462
|
|
|
1529
1463
|
if "_create" in request.POST:
|
|
1530
|
-
form = self.form(model, request.POST)
|
|
1464
|
+
form = self.form(model, request.POST) # pylint: disable=not-callable
|
|
1531
1465
|
|
|
1532
1466
|
if form.is_valid():
|
|
1533
1467
|
logger.debug("Form validation was successful")
|
|
@@ -1549,7 +1483,7 @@ class BulkComponentCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin,
|
|
|
1549
1483
|
"label": label,
|
|
1550
1484
|
}
|
|
1551
1485
|
component_data.update(data)
|
|
1552
|
-
component_form = self.model_form(component_data)
|
|
1486
|
+
component_form = self.model_form(component_data) # pylint: disable=not-callable
|
|
1553
1487
|
if component_form.is_valid():
|
|
1554
1488
|
instance = component_form.save()
|
|
1555
1489
|
logger.debug(f"Created {instance} on {instance.parent}")
|
|
@@ -1591,7 +1525,7 @@ class BulkComponentCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin,
|
|
|
1591
1525
|
logger.debug("Form validation failed")
|
|
1592
1526
|
|
|
1593
1527
|
else:
|
|
1594
|
-
form = self.form(model, initial={"pk": pk_list})
|
|
1528
|
+
form = self.form(model, initial={"pk": pk_list}) # pylint: disable=not-callable
|
|
1595
1529
|
|
|
1596
1530
|
return render(
|
|
1597
1531
|
request,
|