nautobot 2.0.0a3__py3-none-any.whl → 2.0.0b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nautobot/apps/api.py +6 -8
- nautobot/apps/forms.py +0 -2
- nautobot/apps/ui.py +0 -8
- nautobot/circuits/api/serializers.py +9 -117
- nautobot/circuits/api/urls.py +1 -1
- nautobot/circuits/api/views.py +0 -1
- nautobot/circuits/forms.py +0 -65
- nautobot/circuits/migrations/0014_related_name_changes.py +1 -1
- nautobot/circuits/migrations/0016_tagsfield.py +34 -0
- nautobot/circuits/migrations/0017_fixup_null_statuses.py +22 -0
- nautobot/circuits/migrations/0018_status_nonnullable.py +22 -0
- nautobot/circuits/models.py +3 -87
- nautobot/circuits/navigation.py +14 -69
- nautobot/circuits/signals.py +0 -2
- nautobot/circuits/tables.py +39 -1
- nautobot/circuits/tests/integration/test_relationships.py +9 -9
- nautobot/circuits/tests/test_api.py +4 -8
- nautobot/circuits/tests/test_filters.py +10 -4
- nautobot/circuits/tests/test_models.py +5 -1
- nautobot/circuits/tests/test_views.py +27 -5
- nautobot/circuits/views.py +18 -10
- nautobot/core/api/__init__.py +8 -2
- nautobot/core/api/fields.py +15 -6
- nautobot/core/api/filter_backends.py +3 -2
- nautobot/core/api/metadata.py +237 -30
- nautobot/core/api/mixins.py +94 -0
- nautobot/core/api/pagination.py +4 -0
- nautobot/core/api/parsers.py +154 -0
- nautobot/core/api/renderers.py +153 -2
- nautobot/core/api/schema.py +46 -2
- nautobot/core/api/serializers.py +377 -35
- nautobot/core/api/urls.py +11 -3
- nautobot/core/api/utils.py +174 -2
- nautobot/core/api/versioning.py +32 -10
- nautobot/core/api/views.py +266 -72
- nautobot/core/apps/__init__.py +138 -220
- nautobot/core/celery/__init__.py +112 -41
- nautobot/core/celery/backends.py +19 -12
- nautobot/core/celery/control.py +46 -0
- nautobot/core/celery/encoders.py +53 -0
- nautobot/core/celery/log.py +38 -0
- nautobot/core/celery/schedulers.py +23 -4
- nautobot/core/celery/task.py +1 -16
- nautobot/core/checks.py +0 -27
- nautobot/core/choices.py +0 -113
- nautobot/core/{cli.py → cli/__init__.py} +1 -1
- nautobot/core/cli/__main__.py +3 -0
- nautobot/core/constants.py +0 -24
- nautobot/core/context_processors.py +12 -0
- nautobot/core/filters.py +2 -2
- nautobot/core/forms/__init__.py +0 -4
- nautobot/core/forms/fields.py +38 -65
- nautobot/core/forms/forms.py +4 -1
- nautobot/core/forms/utils.py +0 -52
- nautobot/core/graphql/schema.py +4 -27
- nautobot/core/jobs/__init__.py +75 -0
- nautobot/core/management/commands/build_ui.py +255 -0
- nautobot/core/management/commands/generate_test_data.py +3 -2
- nautobot/core/management/commands/post_upgrade.py +24 -24
- nautobot/core/models/__init__.py +26 -1
- nautobot/core/models/fields.py +24 -5
- nautobot/core/models/generics.py +2 -42
- nautobot/core/models/managers.py +5 -0
- nautobot/core/models/name_color_content_types.py +0 -14
- nautobot/core/models/tree_queries.py +14 -4
- nautobot/core/models/utils.py +5 -6
- nautobot/core/models/validators.py +17 -8
- nautobot/core/releases.py +8 -10
- nautobot/core/settings.py +80 -42
- nautobot/core/tables.py +5 -5
- nautobot/core/tasks.py +4 -7
- nautobot/core/templates/base.html +1 -49
- nautobot/core/templates/base_django.html +49 -0
- nautobot/core/templates/base_react.html +55 -0
- nautobot/core/templates/buttons/export.html +6 -4
- nautobot/core/templates/generic/object_bulk_create.html +10 -21
- nautobot/core/templates/generic/object_list.html +3 -1
- nautobot/core/templates/generic/object_retrieve_plugin_full_width.html +3 -0
- nautobot/core/templates/inc/footer.html +1 -0
- nautobot/core/templates/inc/javascript.html +0 -14
- nautobot/core/templates/inc/nav_menu.html +28 -33
- nautobot/core/templates/inc/object_details_advanced_panel.html +13 -0
- nautobot/core/templates/inc/relationships_table_rows.html +2 -2
- nautobot/core/templates/nautobot_config.py.j2 +8 -20
- nautobot/core/templates/plugin_template/__init__.py-tpl +1 -2
- nautobot/core/templates/rest_framework/api.html +8 -0
- nautobot/core/templatetags/buttons.py +32 -28
- nautobot/core/testing/__init__.py +47 -44
- nautobot/core/testing/api.py +362 -47
- nautobot/core/testing/filters.py +1 -1
- nautobot/core/testing/migrations.py +2 -0
- nautobot/core/testing/mixins.py +22 -9
- nautobot/core/testing/schema.py +2 -1
- nautobot/core/testing/views.py +21 -46
- nautobot/core/tests/integration/test_filters.py +17 -8
- nautobot/core/tests/integration/test_navbar.py +11 -34
- nautobot/core/tests/integration/test_plugin_navbar.py +9 -103
- nautobot/core/tests/nautobot_config.py +2 -3
- nautobot/core/tests/test_api.py +290 -21
- nautobot/core/tests/test_checks.py +0 -7
- nautobot/core/tests/test_filters.py +107 -59
- nautobot/core/tests/test_forms.py +26 -92
- nautobot/core/tests/test_graphql.py +110 -77
- nautobot/core/tests/test_logging.py +4 -0
- nautobot/core/tests/test_managers.py +3 -1
- nautobot/core/tests/test_models.py +2 -0
- nautobot/core/tests/test_paginator.py +3 -1
- nautobot/core/tests/test_releases.py +12 -12
- nautobot/core/tests/test_templatetags_helpers.py +4 -4
- nautobot/core/tests/test_utils.py +32 -68
- nautobot/core/tests/test_views.py +12 -15
- nautobot/core/utils/data.py +17 -0
- nautobot/core/utils/deprecation.py +9 -6
- nautobot/core/utils/filtering.py +8 -3
- nautobot/core/utils/git.py +12 -4
- nautobot/core/utils/lookup.py +3 -1
- nautobot/core/utils/requests.py +1 -104
- nautobot/core/views/__init__.py +1 -0
- nautobot/core/views/generic.py +75 -110
- nautobot/core/views/mixins.py +52 -61
- nautobot/core/views/renderers.py +6 -7
- nautobot/core/views/utils.py +80 -0
- nautobot/dcim/api/serializers.py +160 -667
- nautobot/dcim/api/urls.py +1 -1
- nautobot/dcim/api/views.py +7 -44
- nautobot/dcim/choices.py +2 -0
- nautobot/dcim/filters/__init__.py +21 -0
- nautobot/dcim/form_mixins.py +1 -27
- nautobot/dcim/forms.py +19 -765
- nautobot/dcim/migrations/0024_alter_device_and_rack_role_add_new_role.py +2 -1
- nautobot/dcim/migrations/0025_device_and_rack_roles_data_migrations.py +19 -13
- nautobot/dcim/migrations/0027_remove_device_role_and_rack_role.py +1 -1
- nautobot/dcim/migrations/0028_rename_foreignkey_fields.py +1 -1
- nautobot/dcim/migrations/0030_migrate_region_and_site_data_to_locations.py +2 -2
- nautobot/dcim/migrations/0035_related_name_changes.py +1 -1
- nautobot/dcim/migrations/0036_remove_region_and_site.py +1 -1
- nautobot/dcim/migrations/0040_tagsfield.py +109 -0
- nautobot/dcim/migrations/{0040_ipam__namespaces.py → 0041_ipam__namespaces.py} +1 -1
- nautobot/dcim/migrations/0042_fixup_null_statuses.py +51 -0
- nautobot/dcim/migrations/0043_status_nonnullable.py +72 -0
- nautobot/dcim/models/cables.py +3 -33
- nautobot/dcim/models/device_component_templates.py +6 -0
- nautobot/dcim/models/device_components.py +12 -198
- nautobot/dcim/models/devices.py +30 -143
- nautobot/dcim/models/locations.py +3 -64
- nautobot/dcim/models/power.py +3 -50
- nautobot/dcim/models/racks.py +7 -84
- nautobot/dcim/navigation.py +141 -467
- nautobot/dcim/signals.py +0 -2
- nautobot/dcim/tables/locations.py +2 -2
- nautobot/dcim/tables/power.py +1 -2
- nautobot/dcim/templates/dcim/console_port_connection_list.html +7 -0
- nautobot/dcim/templates/dcim/devicetype.html +2 -2
- nautobot/dcim/templates/dcim/interface_connection_list.html +7 -0
- nautobot/dcim/templates/dcim/location.html +16 -1
- nautobot/dcim/templates/dcim/locationtype.html +15 -0
- nautobot/dcim/templates/dcim/power_port_connection_list.html +7 -0
- nautobot/dcim/templates/dcim/rackgroup.html +0 -12
- nautobot/dcim/tests/test_api.py +166 -81
- nautobot/dcim/tests/test_cablepaths.py +41 -35
- nautobot/dcim/tests/test_filters.py +67 -23
- nautobot/dcim/tests/test_forms.py +5 -205
- nautobot/dcim/tests/test_graphql.py +7 -2
- nautobot/dcim/tests/test_migrations.py +6 -11
- nautobot/dcim/tests/test_models.py +182 -110
- nautobot/dcim/tests/test_natural_ordering.py +11 -8
- nautobot/dcim/tests/test_signals.py +6 -3
- nautobot/dcim/tests/test_views.py +197 -175
- nautobot/dcim/urls.py +11 -16
- nautobot/dcim/views.py +7 -134
- nautobot/docs/additional-features/caching.md +6 -87
- nautobot/docs/additional-features/job-scheduling-and-approvals.md +3 -0
- nautobot/docs/additional-features/jobs.md +177 -195
- nautobot/docs/administration/nautobot-server.md +6 -21
- nautobot/docs/administration/replicating-nautobot.md +0 -10
- nautobot/docs/configuration/optional-settings.md +32 -41
- nautobot/docs/configuration/required-settings.md +11 -52
- nautobot/docs/development/application-registry.md +2 -13
- nautobot/docs/development/extending-models.md +15 -17
- nautobot/docs/development/generic-views.md +0 -2
- nautobot/docs/development/getting-started.md +55 -5
- nautobot/docs/development/navigation-menu.md +22 -93
- nautobot/docs/development/react-ui.md +105 -0
- nautobot/docs/development/role-internals.md +1 -3
- nautobot/docs/development/style-guide.md +6 -4
- nautobot/docs/index.md +3 -2
- nautobot/docs/installation/migrating-from-netbox.md +11 -42
- nautobot/docs/installation/nautobot.md +1 -1
- nautobot/docs/installation/tables/v2-api-behavior-changes.yaml +70 -0
- nautobot/docs/installation/tables/v2-api-removed-fields.yaml +142 -0
- nautobot/docs/installation/tables/v2-api-renamed-fields.yaml +124 -0
- nautobot/docs/installation/tables/v2-code-location-changes.yaml +241 -0
- nautobot/docs/installation/tables/v2-code-removals.yaml +67 -0
- nautobot/docs/installation/tables/v2-database-behavior-changes.yaml +37 -0
- nautobot/docs/installation/tables/v2-database-removed-fields.yaml +166 -0
- nautobot/docs/installation/tables/v2-database-renamed-fields.yaml +340 -0
- nautobot/docs/installation/tables/v2-filters-corrected-fields.yaml +28 -0
- nautobot/docs/installation/tables/v2-filters-enhanced-fields.yaml +241 -0
- nautobot/docs/installation/tables/v2-filters-removed-fields.yaml +553 -0
- nautobot/docs/installation/tables/v2-filters-renamed-fields.yaml +223 -0
- nautobot/docs/installation/tables/v2-logging-renamed-loggers.yaml +23 -0
- nautobot/docs/installation/upgrading-from-nautobot-v1.md +170 -747
- nautobot/docs/models/dcim/device.md +3 -0
- nautobot/docs/models/dcim/deviceredundancygroup.md +3 -3
- nautobot/docs/models/extras/computedfield.md +4 -4
- nautobot/docs/models/extras/gitrepository.md +3 -0
- nautobot/docs/models/extras/job.md +1 -0
- nautobot/docs/models/extras/jobbutton.md +18 -13
- nautobot/docs/models/extras/jobhook.md +7 -4
- nautobot/docs/models/extras/jobresult.md +6 -2
- nautobot/docs/models/extras/relationship.md +2 -2
- nautobot/docs/models/extras/status.md +6 -19
- nautobot/docs/models/ipam/ipaddress.md +3 -0
- nautobot/docs/models/virtualization/virtualmachine.md +3 -0
- nautobot/docs/plugins/development.md +83 -21
- nautobot/docs/release-notes/version-1.5.md +53 -0
- nautobot/docs/release-notes/version-2.0.md +180 -0
- nautobot/docs/requirements.txt +1 -0
- nautobot/docs/rest-api/overview.md +384 -215
- nautobot/docs/rest-api/ui-related-endpoints.md +9 -0
- nautobot/extras/admin.py +3 -5
- nautobot/extras/api/customfields.py +15 -39
- nautobot/extras/api/fields.py +0 -11
- nautobot/extras/api/mixins.py +45 -0
- nautobot/extras/api/relationships.py +63 -158
- nautobot/extras/api/serializers.py +165 -700
- nautobot/extras/api/urls.py +1 -1
- nautobot/extras/api/views.py +294 -280
- nautobot/extras/apps.py +4 -7
- nautobot/extras/choices.py +11 -9
- nautobot/extras/constants.py +9 -3
- nautobot/extras/datasources/__init__.py +2 -0
- nautobot/extras/datasources/git.py +135 -186
- nautobot/extras/datasources/registry.py +25 -35
- nautobot/extras/filters/__init__.py +20 -19
- nautobot/extras/filters/mixins.py +4 -4
- nautobot/extras/forms/forms.py +63 -127
- nautobot/extras/forms/mixins.py +23 -51
- nautobot/extras/health_checks.py +0 -33
- nautobot/extras/jobs.py +387 -565
- nautobot/extras/management/commands/runjob.py +24 -62
- nautobot/extras/managers.py +30 -7
- nautobot/extras/migrations/0058_jobresult_add_time_status_idxs.py +38 -0
- nautobot/extras/migrations/{0058_joblogentry_scheduledjob_webhook_data_migration.py → 0059_joblogentry_scheduledjob_webhook_data_migration.py} +1 -1
- nautobot/extras/migrations/{0059_alter_joblogentry_scheduledjob_webhook_fields.py → 0060_alter_joblogentry_scheduledjob_webhook_fields.py} +1 -1
- nautobot/extras/migrations/{0060_role_and_alter_status.py → 0061_role_and_alter_status.py} +1 -7
- nautobot/extras/migrations/{0061_collect_roles_from_related_apps_roles.py → 0062_collect_roles_from_related_apps_roles.py} +33 -32
- nautobot/extras/migrations/{0062_alter_role_options.py → 0063_alter_role_options.py} +1 -1
- nautobot/extras/migrations/{0063_alter_configcontext_and_add_new_role.py → 0064_alter_configcontext_and_add_new_role.py} +1 -1
- nautobot/extras/migrations/0065_configcontext_data_migrations.py +44 -0
- nautobot/extras/migrations/{0065_rename_configcontext_role.py → 0066_rename_configcontext_role.py} +1 -1
- nautobot/extras/migrations/{0066_jobresult__add_celery_fields.py → 0067_jobresult__add_celery_fields.py} +36 -2
- nautobot/extras/migrations/{0067_created_datetime.py → 0068_created_datetime.py} +1 -1
- nautobot/extras/migrations/{0068_remove_site_and_region_attributes_from_config_context.py → 0069_remove_site_and_region_attributes_from_config_context.py} +1 -1
- nautobot/extras/migrations/{0069_replace_related_names.py → 0070_replace_related_names.py} +1 -1
- nautobot/extras/migrations/{0070_rename_model_fields.py → 0071_rename_model_fields.py} +1 -1
- nautobot/extras/migrations/0072_job__unique_name_data_migration.py +86 -0
- nautobot/extras/migrations/{0072_job__unique_name.py → 0073_job__unique_name.py} +13 -9
- nautobot/extras/migrations/{0073_remove_gitrepository_fields.py → 0074_remove_gitrepository_fields.py} +1 -1
- nautobot/extras/migrations/{0074_rename_slug_to_key_for_custom_field.py → 0075_rename_slug_to_key_for_custom_field.py} +1 -1
- nautobot/extras/migrations/{0075_migrate_custom_field_data.py → 0076_migrate_custom_field_data.py} +1 -1
- nautobot/extras/migrations/{0076_remove_name_field_and_make_label_field_non_nullable.py → 0077_remove_name_field_and_make_label_field_non_nullable.py} +1 -1
- nautobot/extras/migrations/{0077_remove_slug.py → 0078_remove_slug.py} +1 -5
- nautobot/extras/migrations/0079_tagsfield.py +28 -0
- nautobot/extras/migrations/0080_rename_relationship_slug_to_key.py +17 -0
- nautobot/extras/migrations/0081_rename_relationship_name_to_label.py +29 -0
- nautobot/extras/migrations/0082_ensure_relationship_keys_are_unique.py +43 -0
- nautobot/extras/migrations/0083_rename_computed_field_slug_to_key.py +21 -0
- nautobot/extras/migrations/0084_taggeditem_cleanup.py +43 -0
- nautobot/extras/migrations/0085_taggeditem_uniqueness.py +22 -0
- nautobot/extras/migrations/0086_job__celery_task_fields__dryrun_support.py +81 -0
- nautobot/extras/migrations/0087_job__commit_default_data_migration.py +26 -0
- nautobot/extras/migrations/0088_joblogentry__log_level_default.py +17 -0
- nautobot/extras/migrations/0089_joblogentry__log_level_data_migration.py +34 -0
- nautobot/extras/migrations/0090_scheduledjob__data_migration.py +57 -0
- nautobot/extras/models/__init__.py +2 -3
- nautobot/extras/models/change_logging.py +0 -36
- nautobot/extras/models/customfields.py +39 -33
- nautobot/extras/models/datasources.py +48 -50
- nautobot/extras/models/groups.py +5 -6
- nautobot/extras/models/jobs.py +189 -321
- nautobot/extras/models/mixins.py +0 -71
- nautobot/extras/models/models.py +0 -19
- nautobot/extras/models/relationships.py +19 -13
- nautobot/extras/models/roles.py +0 -34
- nautobot/extras/models/secrets.py +2 -26
- nautobot/extras/models/statuses.py +6 -5
- nautobot/extras/models/tags.py +2 -17
- nautobot/extras/navigation.py +89 -307
- nautobot/extras/plugins/__init__.py +3 -120
- nautobot/extras/plugins/utils.py +0 -3
- nautobot/extras/plugins/validators.py +5 -4
- nautobot/extras/plugins/views.py +16 -3
- nautobot/extras/querysets.py +1 -7
- nautobot/extras/registry.py +3 -0
- nautobot/extras/signals.py +26 -60
- nautobot/extras/tables.py +34 -40
- nautobot/extras/tasks.py +0 -12
- nautobot/extras/templates/extras/configcontext.html +1 -1
- nautobot/extras/templates/extras/configcontextschema.html +16 -1
- nautobot/extras/templates/extras/customfield.html +0 -13
- nautobot/extras/templates/extras/gitrepository.html +3 -3
- nautobot/extras/templates/extras/inc/jobresult.html +10 -0
- nautobot/extras/templates/extras/inc/panel_jobhistory.html +1 -1
- nautobot/extras/templates/extras/job.html +35 -25
- nautobot/extras/templates/extras/job_approval_request.html +15 -30
- nautobot/extras/templates/extras/job_detail.html +13 -31
- nautobot/extras/templates/extras/job_edit.html +15 -17
- nautobot/extras/templates/extras/jobresult.html +24 -6
- nautobot/extras/templates/extras/scheduledjob.html +2 -2
- nautobot/extras/templates/extras/secret.html +28 -0
- nautobot/extras/templatetags/job_buttons.py +1 -0
- nautobot/extras/{tests/example_jobs → test_jobs}/api_test_job.py +13 -6
- nautobot/extras/test_jobs/atomic_transaction.py +53 -0
- nautobot/extras/test_jobs/dry_run.py +29 -0
- nautobot/extras/{tests/example_jobs/test_duplicate_name.py → test_jobs/duplicate_name.py} +4 -0
- nautobot/extras/test_jobs/duplicate_name2.py +9 -0
- nautobot/extras/test_jobs/fail.py +23 -0
- nautobot/extras/{tests/example_jobs/test_field_default.py → test_jobs/field_default.py} +4 -0
- nautobot/extras/{tests/example_jobs/test_field_order.py → test_jobs/field_order.py} +4 -0
- nautobot/extras/{tests/example_jobs/test_file_upload_fail.py → test_jobs/file_upload_fail.py} +11 -6
- nautobot/extras/test_jobs/file_upload_pass.py +25 -0
- nautobot/extras/test_jobs/has_sensitive_variables.py +25 -0
- nautobot/extras/test_jobs/ipaddress_vars.py +66 -0
- nautobot/extras/test_jobs/job_button_receiver.py +28 -0
- nautobot/extras/test_jobs/job_hook_receiver.py +29 -0
- nautobot/extras/test_jobs/job_variables.py +88 -0
- nautobot/extras/test_jobs/location_with_custom_field.py +45 -0
- nautobot/extras/test_jobs/log_redaction.py +20 -0
- nautobot/extras/test_jobs/log_skip_db_logging.py +17 -0
- nautobot/extras/test_jobs/modify_db.py +25 -0
- nautobot/extras/{tests/example_jobs/test_no_field_order.py → test_jobs/no_field_order.py} +4 -0
- nautobot/extras/test_jobs/object_var_optional.py +21 -0
- nautobot/extras/test_jobs/object_var_required.py +21 -0
- nautobot/extras/test_jobs/object_vars.py +26 -0
- nautobot/extras/test_jobs/pass.py +25 -0
- nautobot/extras/test_jobs/profiling.py +32 -0
- nautobot/extras/test_jobs/read_only_job.py +15 -0
- nautobot/extras/{tests/example_jobs/test_required_args.py → test_jobs/required_args.py} +4 -0
- nautobot/extras/{tests/example_jobs/test_soft_time_limit_greater_than_time_limit.py → test_jobs/soft_time_limit_greater_than_time_limit.py} +5 -1
- nautobot/extras/{tests/example_jobs/test_task_queues.py → test_jobs/task_queues.py} +5 -1
- nautobot/extras/tests/integration/test_computedfields.py +1 -1
- nautobot/extras/tests/integration/test_configcontextschema.py +5 -3
- nautobot/extras/tests/integration/test_customfields.py +4 -2
- nautobot/extras/tests/integration/test_dynamicgroups.py +1 -1
- nautobot/extras/tests/integration/test_jobs.py +25 -27
- nautobot/extras/tests/integration/test_notes.py +8 -4
- nautobot/extras/tests/integration/test_relationships.py +2 -2
- nautobot/extras/tests/test_api.py +649 -642
- nautobot/extras/tests/test_changelog.py +3 -3
- nautobot/extras/tests/test_context_managers.py +5 -3
- nautobot/extras/tests/test_customfields.py +92 -50
- nautobot/extras/tests/test_datasources.py +189 -112
- nautobot/extras/tests/test_dynamicgroups.py +7 -8
- nautobot/extras/tests/test_filters.py +137 -89
- nautobot/extras/tests/test_forms.py +73 -75
- nautobot/extras/tests/{test_scripts.py → test_job_variables.py} +43 -49
- nautobot/extras/tests/test_jobs.py +262 -263
- nautobot/extras/tests/test_migrations.py +4 -3
- nautobot/extras/tests/test_models.py +116 -161
- nautobot/extras/tests/test_plugins.py +38 -60
- nautobot/extras/tests/test_relationships.py +167 -120
- nautobot/extras/tests/test_tags.py +6 -11
- nautobot/extras/tests/test_utils.py +31 -1
- nautobot/extras/tests/test_views.py +201 -145
- nautobot/extras/tests/test_webhooks.py +6 -2
- nautobot/extras/urls.py +42 -42
- nautobot/extras/utils.py +137 -163
- nautobot/extras/views.py +78 -152
- nautobot/ipam/api/fields.py +17 -0
- nautobot/ipam/api/serializers.py +58 -164
- nautobot/ipam/api/urls.py +1 -1
- nautobot/ipam/api/views.py +3 -2
- nautobot/ipam/apps.py +1 -2
- nautobot/ipam/filters.py +1 -10
- nautobot/ipam/forms.py +4 -177
- nautobot/ipam/lookups.py +1 -0
- nautobot/ipam/management/commands/__init__.py +0 -0
- nautobot/ipam/management/commands/fix_prefix_broadcast.py +17 -0
- nautobot/ipam/migrations/0010_alter_ipam_role_add_new_role.py +1 -1
- nautobot/ipam/migrations/0011_migrate_ipam_role_data.py +32 -38
- nautobot/ipam/migrations/0020_related_name_changes.py +1 -1
- nautobot/ipam/migrations/0022_aggregate_to_prefix_data_migration.py +2 -2
- nautobot/ipam/migrations/0028_tagsfield.py +44 -0
- nautobot/ipam/migrations/0029_ip_address_to_interface_uniqueness_constraints.py +18 -0
- nautobot/ipam/migrations/{0028_ipam__namespaces.py → 0030_ipam__namespaces.py} +77 -28
- nautobot/ipam/migrations/0031_ipam__prefix__add_parent.py +58 -0
- nautobot/ipam/migrations/0032_ipam__namespaces_finish.py +63 -0
- nautobot/ipam/migrations/0033_fixup_null_statuses.py +26 -0
- nautobot/ipam/migrations/0034_status_nonnullable.py +36 -0
- nautobot/ipam/models.py +100 -236
- nautobot/ipam/navigation.py +36 -181
- nautobot/ipam/querysets.py +20 -25
- nautobot/ipam/signals.py +49 -6
- nautobot/ipam/tables.py +10 -3
- nautobot/ipam/templates/ipam/namespace_ipaddresses.html +11 -0
- nautobot/ipam/templates/ipam/namespace_prefixes.html +11 -0
- nautobot/ipam/templates/ipam/namespace_retrieve.html +17 -4
- nautobot/ipam/templates/ipam/namespace_vrfs.html +11 -0
- nautobot/ipam/templates/ipam/prefix.html +1 -1
- nautobot/ipam/templates/ipam/vlangroup.html +0 -13
- nautobot/ipam/templates/ipam/vrf_edit.html +6 -0
- nautobot/ipam/tests/integration/test_prefixes.py +3 -26
- nautobot/ipam/tests/test_api.py +22 -19
- nautobot/ipam/tests/test_filters.py +59 -23
- nautobot/ipam/tests/test_migrations.py +6 -10
- nautobot/ipam/tests/test_models.py +323 -198
- nautobot/ipam/tests/test_ordering.py +2 -2
- nautobot/ipam/tests/test_querysets.py +44 -24
- nautobot/ipam/tests/test_views.py +73 -26
- nautobot/ipam/urls.py +16 -0
- nautobot/ipam/{utils.py → utils/__init__.py} +2 -2
- nautobot/ipam/utils/migrations.py +713 -0
- nautobot/ipam/views.py +137 -20
- nautobot/project-static/docs/404.html +1178 -10
- nautobot/project-static/docs/additional-features/caching.html +1224 -159
- nautobot/project-static/docs/additional-features/change-logging.html +1180 -12
- nautobot/project-static/docs/additional-features/config-contexts.html +1180 -12
- nautobot/project-static/docs/additional-features/graphql.html +1179 -11
- nautobot/project-static/docs/additional-features/healthcheck.html +1180 -12
- nautobot/project-static/docs/additional-features/job-scheduling-and-approvals.html +1184 -12
- nautobot/project-static/docs/additional-features/jobs.html +1514 -328
- nautobot/project-static/docs/additional-features/napalm.html +1180 -12
- nautobot/project-static/docs/additional-features/prometheus-metrics.html +1180 -12
- nautobot/project-static/docs/additional-features/template-filters.html +1180 -12
- nautobot/project-static/docs/administration/celery-queues.html +1178 -10
- nautobot/project-static/docs/administration/nautobot-server.html +1451 -304
- nautobot/project-static/docs/administration/nautobot-shell.html +1178 -10
- nautobot/project-static/docs/administration/permissions.html +1178 -10
- nautobot/project-static/docs/administration/replicating-nautobot.html +1262 -113
- nautobot/project-static/docs/apps/index.html +1178 -10
- nautobot/project-static/docs/apps/nautobot-apps.html +1178 -10
- nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +1580 -426
- nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +1178 -10
- nautobot/project-static/docs/code-reference/nautobot/apps/api.html +3481 -1838
- nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +1178 -10
- nautobot/project-static/docs/code-reference/nautobot/apps/config.html +1178 -10
- nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +1185 -11
- nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +1719 -551
- nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +2062 -930
- nautobot/project-static/docs/code-reference/nautobot/apps/models.html +1946 -659
- nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +1180 -12
- nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +1189 -21
- nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +9283 -6218
- nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +2734 -2122
- nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +1178 -10
- nautobot/project-static/docs/code-reference/nautobot/apps/views.html +2337 -1300
- nautobot/project-static/docs/configuration/authentication/ldap.html +1178 -10
- nautobot/project-static/docs/configuration/authentication/remote.html +1178 -10
- nautobot/project-static/docs/configuration/authentication/sso.html +1178 -10
- nautobot/project-static/docs/configuration/index.html +1178 -10
- nautobot/project-static/docs/configuration/optional-settings.html +1311 -160
- nautobot/project-static/docs/configuration/required-settings.html +1312 -211
- nautobot/project-static/docs/core-functionality/circuits.html +1178 -10
- nautobot/project-static/docs/core-functionality/device-types.html +1178 -10
- nautobot/project-static/docs/core-functionality/devices.html +1182 -10
- nautobot/project-static/docs/core-functionality/ipam.html +1182 -10
- nautobot/project-static/docs/core-functionality/power.html +1178 -10
- nautobot/project-static/docs/core-functionality/secrets.html +1178 -10
- nautobot/project-static/docs/core-functionality/services.html +1178 -10
- nautobot/project-static/docs/core-functionality/sites-and-racks.html +1178 -10
- nautobot/project-static/docs/core-functionality/tenancy.html +1178 -10
- nautobot/project-static/docs/core-functionality/virtualization.html +1182 -10
- nautobot/project-static/docs/core-functionality/vlans.html +1179 -11
- nautobot/project-static/docs/development/application-registry.html +1190 -42
- nautobot/project-static/docs/development/best-practices.html +1178 -10
- nautobot/project-static/docs/development/docker-compose-advanced-use-cases.html +1178 -10
- nautobot/project-static/docs/development/extending-models.html +1238 -83
- nautobot/project-static/docs/development/generic-views.html +1180 -14
- nautobot/project-static/docs/development/getting-started.html +1365 -90
- nautobot/project-static/docs/development/homepage.html +1178 -10
- nautobot/project-static/docs/development/index.html +1178 -10
- nautobot/project-static/docs/development/model-features.html +1178 -10
- nautobot/project-static/docs/development/natural-keys.html +1178 -10
- nautobot/project-static/docs/development/navigation-menu.html +1215 -125
- nautobot/project-static/docs/development/react-ui.html +4199 -0
- nautobot/project-static/docs/development/release-checklist.html +1178 -10
- nautobot/project-static/docs/development/role-internals.html +1179 -12
- nautobot/project-static/docs/development/style-guide.html +1188 -19
- nautobot/project-static/docs/development/templates.html +1178 -10
- nautobot/project-static/docs/development/testing.html +1178 -10
- nautobot/project-static/docs/development/user-preferences.html +1178 -10
- nautobot/project-static/docs/docker/index.html +1178 -10
- nautobot/project-static/docs/index.html +1183 -12
- nautobot/project-static/docs/installation/centos.html +1178 -10
- nautobot/project-static/docs/installation/external-authentication.html +1178 -10
- nautobot/project-static/docs/installation/http-server.html +1178 -10
- nautobot/project-static/docs/installation/index.html +1178 -10
- nautobot/project-static/docs/installation/migrating-from-netbox.html +1305 -189
- nautobot/project-static/docs/installation/migrating-from-postgresql.html +1178 -10
- nautobot/project-static/docs/installation/nautobot.html +1179 -11
- nautobot/project-static/docs/installation/region-and-site-data-migration-guide.html +1178 -10
- nautobot/project-static/docs/installation/selinux-troubleshooting.html +1178 -10
- nautobot/project-static/docs/installation/services.html +1178 -10
- nautobot/project-static/docs/installation/tables/v2-api-behavior-changes.yaml +70 -0
- nautobot/project-static/docs/installation/tables/v2-api-removed-fields.yaml +142 -0
- nautobot/project-static/docs/installation/tables/v2-api-renamed-fields.yaml +124 -0
- nautobot/project-static/docs/installation/tables/v2-code-location-changes.yaml +241 -0
- nautobot/project-static/docs/installation/tables/v2-code-removals.yaml +67 -0
- nautobot/project-static/docs/installation/tables/v2-database-behavior-changes.yaml +37 -0
- nautobot/project-static/docs/installation/tables/v2-database-removed-fields.yaml +166 -0
- nautobot/project-static/docs/installation/tables/v2-database-renamed-fields.yaml +340 -0
- nautobot/project-static/docs/installation/tables/v2-filters-corrected-fields.yaml +28 -0
- nautobot/project-static/docs/installation/tables/v2-filters-enhanced-fields.yaml +241 -0
- nautobot/project-static/docs/installation/tables/v2-filters-removed-fields.yaml +553 -0
- nautobot/project-static/docs/installation/tables/v2-filters-renamed-fields.yaml +223 -0
- nautobot/project-static/docs/installation/tables/v2-logging-renamed-loggers.yaml +23 -0
- nautobot/project-static/docs/installation/ubuntu.html +1178 -10
- nautobot/project-static/docs/installation/upgrading-from-nautobot-v1.html +3823 -2152
- nautobot/project-static/docs/installation/upgrading.html +1178 -10
- nautobot/project-static/docs/models/circuits/circuit.html +1293 -103
- nautobot/project-static/docs/models/circuits/circuittermination.html +1293 -103
- nautobot/project-static/docs/models/circuits/circuittype.html +1293 -103
- nautobot/project-static/docs/models/circuits/provider.html +1293 -103
- nautobot/project-static/docs/models/circuits/providernetwork.html +1293 -103
- nautobot/project-static/docs/models/dcim/cable.html +1324 -103
- nautobot/project-static/docs/models/dcim/consoleport.html +1293 -103
- nautobot/project-static/docs/models/dcim/consoleporttemplate.html +1293 -103
- nautobot/project-static/docs/models/dcim/consoleserverport.html +1293 -103
- nautobot/project-static/docs/models/dcim/consoleserverporttemplate.html +1293 -103
- nautobot/project-static/docs/models/dcim/device.html +1326 -132
- nautobot/project-static/docs/models/dcim/devicebay.html +1293 -103
- nautobot/project-static/docs/models/dcim/devicebaytemplate.html +1293 -103
- nautobot/project-static/docs/models/dcim/deviceredundancygroup.html +1379 -97
- nautobot/project-static/docs/models/dcim/devicetype.html +1293 -103
- nautobot/project-static/docs/models/dcim/frontport.html +1293 -103
- nautobot/project-static/docs/models/dcim/frontporttemplate.html +1293 -103
- nautobot/project-static/docs/models/dcim/interface.html +1293 -103
- nautobot/project-static/docs/models/dcim/interfacetemplate.html +1293 -103
- nautobot/project-static/docs/models/dcim/inventoryitem.html +1293 -103
- nautobot/project-static/docs/models/dcim/location.html +1293 -103
- nautobot/project-static/docs/models/dcim/locationtype.html +1293 -103
- nautobot/project-static/docs/models/dcim/manufacturer.html +1292 -102
- nautobot/project-static/docs/models/dcim/platform.html +1272 -82
- nautobot/project-static/docs/models/dcim/powerfeed.html +1270 -80
- nautobot/project-static/docs/models/dcim/poweroutlet.html +1272 -82
- nautobot/project-static/docs/models/dcim/poweroutlettemplate.html +1272 -82
- nautobot/project-static/docs/models/dcim/powerpanel.html +1270 -80
- nautobot/project-static/docs/models/dcim/powerport.html +1272 -82
- nautobot/project-static/docs/models/dcim/powerporttemplate.html +1272 -82
- nautobot/project-static/docs/models/dcim/rack.html +1272 -82
- nautobot/project-static/docs/models/dcim/rackgroup.html +1272 -82
- nautobot/project-static/docs/models/dcim/rackreservation.html +1272 -82
- nautobot/project-static/docs/models/dcim/rearport.html +1286 -96
- nautobot/project-static/docs/models/dcim/rearporttemplate.html +1286 -96
- nautobot/project-static/docs/models/dcim/region.html +1178 -10
- nautobot/project-static/docs/models/dcim/site.html +1178 -10
- nautobot/project-static/docs/models/dcim/virtualchassis.html +1284 -94
- nautobot/project-static/docs/models/extras/computedfield.html +1184 -16
- nautobot/project-static/docs/models/extras/configcontext.html +1314 -86
- nautobot/project-static/docs/models/extras/configcontextschema.html +1276 -86
- nautobot/project-static/docs/models/extras/customfield.html +1180 -12
- nautobot/project-static/docs/models/extras/customlink.html +1180 -12
- nautobot/project-static/docs/models/extras/dynamicgroup.html +1180 -12
- nautobot/project-static/docs/models/extras/exporttemplate.html +1180 -12
- nautobot/project-static/docs/models/extras/gitrepository.html +1184 -12
- nautobot/project-static/docs/models/extras/graphqlquery.html +1321 -86
- nautobot/project-static/docs/models/extras/imageattachment.html +1276 -86
- nautobot/project-static/docs/models/extras/job.html +1277 -86
- nautobot/project-static/docs/models/extras/jobbutton.html +1201 -29
- nautobot/project-static/docs/models/extras/jobhook.html +1188 -16
- nautobot/project-static/docs/models/extras/joblogentry.html +1274 -84
- nautobot/project-static/docs/models/extras/jobresult.html +1364 -169
- nautobot/project-static/docs/models/extras/note.html +1180 -12
- nautobot/project-static/docs/models/extras/relationship.html +1182 -14
- nautobot/project-static/docs/models/extras/role.html +1320 -86
- nautobot/project-static/docs/models/extras/secret.html +1314 -86
- nautobot/project-static/docs/models/extras/secretsgroup.html +1276 -86
- nautobot/project-static/docs/models/extras/status.html +1188 -59
- nautobot/project-static/docs/models/extras/tag.html +1180 -12
- nautobot/project-static/docs/models/extras/webhook.html +1180 -12
- nautobot/project-static/docs/models/ipam/ipaddress.html +1327 -102
- nautobot/project-static/docs/models/ipam/prefix.html +1276 -86
- nautobot/project-static/docs/models/ipam/rir.html +1276 -86
- nautobot/project-static/docs/models/ipam/routetarget.html +1276 -86
- nautobot/project-static/docs/models/ipam/service.html +1276 -86
- nautobot/project-static/docs/models/ipam/vlan.html +1276 -86
- nautobot/project-static/docs/models/ipam/vlangroup.html +1276 -86
- nautobot/project-static/docs/models/ipam/vrf.html +1276 -86
- nautobot/project-static/docs/models/tenancy/tenant.html +1276 -86
- nautobot/project-static/docs/models/tenancy/tenantgroup.html +1276 -86
- nautobot/project-static/docs/models/users/objectpermission.html +1314 -86
- nautobot/project-static/docs/models/users/token.html +1276 -86
- nautobot/project-static/docs/models/virtualization/cluster.html +1276 -86
- nautobot/project-static/docs/models/virtualization/clustergroup.html +1276 -86
- nautobot/project-static/docs/models/virtualization/clustertype.html +1276 -86
- nautobot/project-static/docs/models/virtualization/virtualmachine.html +1321 -127
- nautobot/project-static/docs/models/virtualization/vminterface.html +1276 -86
- nautobot/project-static/docs/objects.inv +0 -0
- nautobot/project-static/docs/plugins/development.html +1726 -495
- nautobot/project-static/docs/plugins/index.html +1178 -10
- nautobot/project-static/docs/plugins/porting-from-netbox.html +1178 -10
- nautobot/project-static/docs/release-notes/index.html +1178 -10
- nautobot/project-static/docs/release-notes/version-1.0.html +1178 -10
- nautobot/project-static/docs/release-notes/version-1.1.html +1178 -10
- nautobot/project-static/docs/release-notes/version-1.2.html +1178 -10
- nautobot/project-static/docs/release-notes/version-1.3.html +1178 -10
- nautobot/project-static/docs/release-notes/version-1.4.html +1178 -10
- nautobot/project-static/docs/release-notes/version-1.5.html +1608 -225
- nautobot/project-static/docs/release-notes/version-2.0.html +1547 -47
- nautobot/project-static/docs/requirements.txt +1 -0
- nautobot/project-static/docs/rest-api/authentication.html +1179 -11
- nautobot/project-static/docs/rest-api/filtering.html +1178 -10
- nautobot/project-static/docs/rest-api/overview.html +1841 -446
- nautobot/project-static/docs/rest-api/ui-related-endpoints.html +4057 -0
- nautobot/project-static/docs/search/search_index.json +1 -1
- nautobot/project-static/docs/sitemap.xml +197 -187
- nautobot/project-static/docs/sitemap.xml.gz +0 -0
- nautobot/project-static/docs/user-guides/custom-fields.html +1178 -10
- nautobot/project-static/docs/user-guides/getting-started/creating-devices.html +1178 -10
- nautobot/project-static/docs/user-guides/getting-started/index.html +1178 -10
- nautobot/project-static/docs/user-guides/getting-started/interfaces.html +1178 -10
- nautobot/project-static/docs/user-guides/getting-started/ipam.html +1178 -10
- nautobot/project-static/docs/user-guides/getting-started/platforms.html +1178 -10
- nautobot/project-static/docs/user-guides/getting-started/regions.html +1178 -10
- nautobot/project-static/docs/user-guides/getting-started/search-bar.html +1178 -10
- nautobot/project-static/docs/user-guides/getting-started/tenants.html +1178 -10
- nautobot/project-static/docs/user-guides/getting-started/vlans-and-vlan-groups.html +1178 -10
- nautobot/project-static/docs/user-guides/git-data-source.html +1178 -10
- nautobot/project-static/docs/user-guides/graphql.html +1178 -10
- nautobot/project-static/docs/user-guides/relationships.html +1178 -10
- nautobot/project-static/docs/user-guides/s3-django-storage.html +1178 -10
- nautobot/project-static/js/forms.js +16 -9
- nautobot/project-static/js/theme.js +5 -0
- nautobot/tenancy/api/serializers.py +4 -32
- nautobot/tenancy/api/urls.py +1 -1
- nautobot/tenancy/forms.py +0 -28
- nautobot/tenancy/migrations/0008_tagsfield.py +19 -0
- nautobot/tenancy/models.py +0 -25
- nautobot/tenancy/navigation.py +6 -39
- nautobot/tenancy/templates/tenancy/tenant.html +12 -12
- nautobot/tenancy/templates/tenancy/tenantgroup.html +1 -1
- nautobot/tenancy/tests/test_api.py +1 -3
- nautobot/tenancy/tests/test_filters.py +10 -5
- nautobot/tenancy/views.py +0 -2
- nautobot/ui/.eslintignore +6 -0
- nautobot/ui/.gitignore +10 -0
- nautobot/ui/.prettierignore +9 -0
- nautobot/ui/.prettierrc +4 -0
- nautobot/ui/README.md +33 -0
- nautobot/ui/app_imports.js.j2 +7 -0
- nautobot/ui/craco.config.js +46 -0
- nautobot/ui/jsconfig-base.json +11 -0
- nautobot/ui/jsconfig.json +5 -0
- nautobot/ui/lib/nautobot-craco-alias-plugin.js +40 -0
- nautobot/ui/package-lock.json +21451 -0
- nautobot/ui/package.json +70 -0
- nautobot/ui/public/index.html +47 -0
- nautobot/ui/public/logo192.png +0 -0
- nautobot/ui/public/logo512.png +0 -0
- nautobot/ui/public/manifest.json +25 -0
- nautobot/ui/public/nautobot_logo.svg +131 -0
- nautobot/ui/public/robots.txt +3 -0
- nautobot/ui/src/App.js +71 -0
- nautobot/ui/src/components/AppFullWidthComponents.js +8 -0
- nautobot/ui/src/components/AppTab.js +40 -0
- nautobot/ui/src/components/Apps.js +60 -0
- nautobot/ui/src/components/HomeChangelogPanel.js +98 -0
- nautobot/ui/src/components/HomePanel.js +58 -0
- nautobot/ui/src/components/JobHistoryTable.js +78 -0
- nautobot/ui/src/components/Layout.js +53 -0
- nautobot/ui/src/components/LoadingWidget.js +25 -0
- nautobot/ui/src/components/Navbar.js +116 -0
- nautobot/ui/src/components/NotificationPopover.js +27 -0
- nautobot/ui/src/components/ObjectListTable.js +209 -0
- nautobot/ui/src/components/ReferenceDataTag.js +35 -0
- nautobot/ui/src/components/RouterButton.js +10 -0
- nautobot/ui/src/components/RouterLink.js +10 -0
- nautobot/ui/src/components/SidebarNav.js +147 -0
- nautobot/ui/src/components/Table.js +48 -0
- nautobot/ui/src/components/TableItem.js +71 -0
- nautobot/ui/src/components/__tests__/AppFullWidthComponents.test.js +16 -0
- nautobot/ui/src/components/__tests__/AppTab.test.js +21 -0
- nautobot/ui/src/components/__tests__/Apps.test.js +14 -0
- nautobot/ui/src/components/__tests__/Layout.test.js +33 -0
- nautobot/ui/src/components/__tests__/Table.test.js +36 -0
- nautobot/ui/src/components/__tests__/TableItem.test.js +37 -0
- nautobot/ui/src/components/__tests__/paginator.test.js +43 -0
- nautobot/ui/src/components/__tests__/paginator_form.test.js +13 -0
- nautobot/ui/src/components/pagination.js +93 -0
- nautobot/ui/src/components/paginator.js +79 -0
- nautobot/ui/src/components/paginator_form.js +43 -0
- nautobot/ui/src/components/usePagination.js +57 -0
- nautobot/ui/src/constants/apiPath.js +10 -0
- nautobot/ui/src/constants/icons.js +15 -0
- nautobot/ui/src/constants/size.js +15 -0
- nautobot/ui/src/index.js +65 -0
- nautobot/ui/src/reportWebVitals.js +15 -0
- nautobot/ui/src/router.js +77 -0
- nautobot/ui/src/utils/api.js +131 -0
- nautobot/ui/src/utils/app-import.js +15 -0
- nautobot/ui/src/utils/color.js +15 -0
- nautobot/ui/src/utils/date.js +14 -0
- nautobot/ui/src/utils/index.js +15 -0
- nautobot/ui/src/utils/navigation.js +32 -0
- nautobot/ui/src/utils/session.js +64 -0
- nautobot/ui/src/utils/store.js +242 -0
- nautobot/ui/src/utils/string.js +6 -0
- nautobot/ui/src/utils/url.js +4 -0
- nautobot/ui/src/views/Home.js +138 -0
- nautobot/ui/src/views/InstalledApps.js +80 -0
- nautobot/ui/src/views/Login.js +48 -0
- nautobot/ui/src/views/Logout.js +20 -0
- nautobot/ui/src/views/__tests__/BSCreateViewTemplate.test.js +11 -0
- nautobot/ui/src/views/__tests__/BSListViewTemplate.test.js +107 -0
- nautobot/ui/src/views/__tests__/Login.test.js +15 -0
- nautobot/ui/src/views/generic/GenericView.js +142 -0
- nautobot/ui/src/views/generic/ObjectCreate.js +96 -0
- nautobot/ui/src/views/generic/ObjectList.js +127 -0
- nautobot/ui/src/views/generic/ObjectRetrieve.js +551 -0
- nautobot/users/admin.py +1 -1
- nautobot/users/api/serializers.py +51 -61
- nautobot/users/api/urls.py +1 -1
- nautobot/users/api/views.py +53 -2
- nautobot/users/tests/test_api.py +110 -25
- nautobot/virtualization/api/serializers.py +18 -130
- nautobot/virtualization/api/urls.py +1 -1
- nautobot/virtualization/api/views.py +1 -22
- nautobot/virtualization/forms.py +13 -99
- nautobot/virtualization/migrations/0012_alter_virtualmachine_role_add_new_role.py +1 -1
- nautobot/virtualization/migrations/0013_migrate_virtualmachine_role_data.py +18 -11
- nautobot/virtualization/migrations/0015_rename_foreignkey_fields.py +1 -1
- nautobot/virtualization/migrations/0018_related_name_changes.py +1 -1
- nautobot/virtualization/migrations/0021_tagsfield_and_vminterface_to_primarymodel.py +39 -0
- nautobot/virtualization/migrations/0022_vminterface_timestamps_data_migration.py +17 -0
- nautobot/virtualization/migrations/{0021_ipam__namespaces.py → 0023_ipam__namespaces.py} +2 -2
- nautobot/virtualization/migrations/0024_fixup_null_statuses.py +25 -0
- nautobot/virtualization/migrations/0025_status_nonnullable.py +29 -0
- nautobot/virtualization/models.py +31 -123
- nautobot/virtualization/navigation.py +18 -99
- nautobot/virtualization/templates/virtualization/virtualmachine.html +2 -1
- nautobot/virtualization/templates/virtualization/virtualmachine_edit.html +6 -0
- nautobot/virtualization/tests/test_api.py +25 -26
- nautobot/virtualization/tests/test_filters.py +41 -15
- nautobot/virtualization/tests/test_models.py +31 -7
- nautobot/virtualization/tests/test_views.py +42 -25
- nautobot/virtualization/views.py +7 -6
- {nautobot-2.0.0a3.dist-info → nautobot-2.0.0b1.dist-info}/METADATA +3 -7
- {nautobot-2.0.0a3.dist-info → nautobot-2.0.0b1.dist-info}/RECORD +744 -602
- {nautobot-2.0.0a3.dist-info → nautobot-2.0.0b1.dist-info}/WHEEL +1 -1
- nautobot/circuits/api/nested_serializers.py +0 -69
- nautobot/core/templates/plugin_template/navigation.py-tpl +0 -22
- nautobot/dcim/api/nested_serializers.py +0 -356
- nautobot/dcim/templates/dcim/device_import.html +0 -5
- nautobot/dcim/templates/dcim/device_import_child.html +0 -5
- nautobot/dcim/templates/dcim/inc/device_import_header.html +0 -4
- nautobot/extras/api/nested_serializers.py +0 -353
- nautobot/extras/migrations/0064_configcontext_data_migrations.py +0 -41
- nautobot/extras/migrations/0071_job__unique_name_data_migration.py +0 -46
- nautobot/extras/reports.py +0 -60
- nautobot/extras/scripts.py +0 -72
- nautobot/extras/tests/example_jobs/script_variables.py +0 -67
- nautobot/extras/tests/example_jobs/test_duplicate_name2.py +0 -5
- nautobot/extras/tests/example_jobs/test_fail.py +0 -16
- nautobot/extras/tests/example_jobs/test_file_upload_pass.py +0 -20
- nautobot/extras/tests/example_jobs/test_ipaddress_vars.py +0 -52
- nautobot/extras/tests/example_jobs/test_job_button_receiver.py +0 -21
- nautobot/extras/tests/example_jobs/test_job_hook_receiver.py +0 -20
- nautobot/extras/tests/example_jobs/test_location_with_custom_field.py +0 -35
- nautobot/extras/tests/example_jobs/test_log_redaction.py +0 -14
- nautobot/extras/tests/example_jobs/test_modify_db.py +0 -18
- nautobot/extras/tests/example_jobs/test_object_var_optional.py +0 -14
- nautobot/extras/tests/example_jobs/test_object_var_required.py +0 -14
- nautobot/extras/tests/example_jobs/test_object_vars.py +0 -29
- nautobot/extras/tests/example_jobs/test_pass.py +0 -19
- nautobot/extras/tests/example_jobs/test_read_only_fail.py +0 -24
- nautobot/extras/tests/example_jobs/test_read_only_no_commit_field.py +0 -10
- nautobot/extras/tests/example_jobs/test_read_only_pass.py +0 -22
- nautobot/ipam/api/nested_serializers.py +0 -159
- nautobot/ipam/migrations/0029_ipam__prefix__add_parent.py +0 -31
- nautobot/ipam/migrations/0030_ipam__prefix__data_migration.py +0 -13
- nautobot/ipam/migrations/0031_ipam__ipaddress__add_parent.py +0 -41
- nautobot/ipam/migrations/0032_ipam__ipaddress__data_migration.py +0 -11
- nautobot/tenancy/api/nested_serializers.py +0 -31
- nautobot/users/api/nested_serializers.py +0 -67
- nautobot/virtualization/api/nested_serializers.py +0 -65
- /nautobot/extras/{tests/example_jobs → test_jobs}/__init__.py +0 -0
- /nautobot/{dcim/models/sites.py → ipam/management/__init__.py} +0 -0
- {nautobot-2.0.0a3.dist-info → nautobot-2.0.0b1.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.0.0a3.dist-info → nautobot-2.0.0b1.dist-info}/entry_points.txt +0 -0
nautobot/core/testing/api.py
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
from io import StringIO
|
|
1
3
|
from typing import Optional, Sequence, Union
|
|
2
4
|
|
|
3
5
|
from django.conf import settings
|
|
6
|
+
from django.contrib.contenttypes.fields import GenericForeignKey
|
|
4
7
|
from django.contrib.contenttypes.models import ContentType
|
|
5
|
-
from django.db.models import ForeignKey
|
|
8
|
+
from django.db.models import ForeignKey, ManyToManyField
|
|
6
9
|
from django.test import override_settings, tag
|
|
7
10
|
from django.urls import reverse
|
|
8
11
|
from django.utils.text import slugify
|
|
@@ -10,8 +13,12 @@ from rest_framework import status
|
|
|
10
13
|
from rest_framework.test import APITransactionTestCase as _APITransactionTestCase
|
|
11
14
|
|
|
12
15
|
from nautobot.core import testing
|
|
16
|
+
from nautobot.core.api.utils import get_serializer_for_model
|
|
17
|
+
from nautobot.core.models import fields as core_fields
|
|
18
|
+
from nautobot.core.models.tree_queries import TreeModel
|
|
13
19
|
from nautobot.core.testing import mixins, views
|
|
14
20
|
from nautobot.core.utils import lookup
|
|
21
|
+
from nautobot.core.utils.data import is_uuid
|
|
15
22
|
from nautobot.extras import choices as extras_choices
|
|
16
23
|
from nautobot.extras import models as extras_models
|
|
17
24
|
from nautobot.extras import registry
|
|
@@ -185,13 +192,26 @@ class APIViewTestCases:
|
|
|
185
192
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
186
193
|
|
|
187
194
|
class ListObjectsViewTestCase(APITestCase):
|
|
188
|
-
brief_fields = []
|
|
189
195
|
choices_fields = None
|
|
190
196
|
filterset = None
|
|
191
197
|
|
|
192
198
|
def get_filterset(self):
|
|
193
199
|
return self.filterset or lookup.get_filterset_for_model(self.model)
|
|
194
200
|
|
|
201
|
+
def get_depth_fields(self):
|
|
202
|
+
"""Get a list of model fields that could be tested with the ?depth query parameter"""
|
|
203
|
+
depth_fields = []
|
|
204
|
+
for field in self.model._meta.fields:
|
|
205
|
+
if not field.name.startswith("_"):
|
|
206
|
+
if isinstance(field, (ForeignKey, GenericForeignKey, ManyToManyField, core_fields.TagsField)) and (
|
|
207
|
+
# we represent content-types as "app_label.modelname" rather than as FKs
|
|
208
|
+
field.related_model != ContentType
|
|
209
|
+
# user is a model field on Token but not a field on TokenSerializer
|
|
210
|
+
and not (field.name == "user" and self.model == users_models.Token)
|
|
211
|
+
):
|
|
212
|
+
depth_fields.append(field.name)
|
|
213
|
+
return depth_fields
|
|
214
|
+
|
|
195
215
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
196
216
|
def test_list_objects_anonymous(self):
|
|
197
217
|
"""
|
|
@@ -214,24 +234,59 @@ class APIViewTestCases:
|
|
|
214
234
|
self.assertEqual(len(response.data["results"]), self._get_queryset().count())
|
|
215
235
|
|
|
216
236
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
|
217
|
-
def
|
|
237
|
+
def test_list_objects_depth_0(self):
|
|
218
238
|
"""
|
|
219
|
-
GET a list of objects using the "
|
|
239
|
+
GET a list of objects using the "?depth=0" parameter.
|
|
220
240
|
"""
|
|
241
|
+
depth_fields = self.get_depth_fields()
|
|
221
242
|
self.add_permissions(f"{self.model._meta.app_label}.view_{self.model._meta.model_name}")
|
|
222
|
-
url = f"{self._get_list_url()}?
|
|
243
|
+
url = f"{self._get_list_url()}?depth=0"
|
|
223
244
|
response = self.client.get(url, **self.header)
|
|
224
245
|
|
|
225
246
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
226
247
|
self.assertIsInstance(response.data, dict)
|
|
227
248
|
self.assertIn("results", response.data)
|
|
228
249
|
self.assertEqual(len(response.data["results"]), self._get_queryset().count())
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
250
|
+
|
|
251
|
+
for response_data in response.data["results"]:
|
|
252
|
+
for field in depth_fields:
|
|
253
|
+
self.assertIn(field, response_data)
|
|
254
|
+
if isinstance(response_data[field], list):
|
|
255
|
+
for entry in response_data[field]:
|
|
256
|
+
self.assertTrue(is_uuid(entry))
|
|
257
|
+
else:
|
|
258
|
+
if response_data[field] is not None:
|
|
259
|
+
# The response should be a detail API URL, ending in the UUID of the relevant object
|
|
260
|
+
# http://nautobot.example.com/api/circuits/providers/<uuid>/
|
|
261
|
+
# ^^^^^^
|
|
262
|
+
self.assertTrue(is_uuid(response_data[field].split("/")[-2]))
|
|
263
|
+
|
|
264
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
|
265
|
+
def test_list_objects_depth_1(self):
|
|
266
|
+
"""
|
|
267
|
+
GET a list of objects using the "?depth=1" parameter.
|
|
268
|
+
"""
|
|
269
|
+
depth_fields = self.get_depth_fields()
|
|
270
|
+
self.add_permissions(f"{self.model._meta.app_label}.view_{self.model._meta.model_name}")
|
|
271
|
+
url = f"{self._get_list_url()}?depth=1"
|
|
272
|
+
response = self.client.get(url, **self.header)
|
|
273
|
+
|
|
274
|
+
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
275
|
+
self.assertIsInstance(response.data, dict)
|
|
276
|
+
self.assertIn("results", response.data)
|
|
277
|
+
self.assertEqual(len(response.data["results"]), self._get_queryset().count())
|
|
278
|
+
|
|
279
|
+
for response_data in response.data["results"]:
|
|
280
|
+
for field in depth_fields:
|
|
281
|
+
self.assertIn(field, response_data)
|
|
282
|
+
if isinstance(response_data[field], list):
|
|
283
|
+
for entry in response_data[field]:
|
|
284
|
+
self.assertIsInstance(entry, dict)
|
|
285
|
+
self.assertTrue(is_uuid(entry["id"]))
|
|
286
|
+
else:
|
|
287
|
+
if response_data[field] is not None:
|
|
288
|
+
self.assertIsInstance(response_data[field], dict)
|
|
289
|
+
self.assertTrue(is_uuid(response_data[field]["id"]))
|
|
235
290
|
|
|
236
291
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
|
237
292
|
def test_list_objects_without_permission(self):
|
|
@@ -293,6 +348,48 @@ class APIViewTestCases:
|
|
|
293
348
|
for entry in response.data["results"]:
|
|
294
349
|
self.assertIn(str(entry["id"]), [str(instance1.pk), str(instance2.pk)])
|
|
295
350
|
|
|
351
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
|
352
|
+
def test_list_objects_ascending_ordered(self):
|
|
353
|
+
# Simple sorting check for models with a "name" field
|
|
354
|
+
# TreeModels don't support sorting at this time (order_by is not supported by TreeQuerySet)
|
|
355
|
+
# They will pass api == queryset tests below but will fail the user expected sort test
|
|
356
|
+
if hasattr(self.model, "name") and not issubclass(self.model, TreeModel):
|
|
357
|
+
self.add_permissions(f"{self.model._meta.app_label}.view_{self.model._meta.model_name}")
|
|
358
|
+
response = self.client.get(f"{self._get_list_url()}?sort=name&limit=3", **self.header)
|
|
359
|
+
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
360
|
+
result_list = list(map(lambda p: p["name"], response.data["results"]))
|
|
361
|
+
self.assertEqual(
|
|
362
|
+
result_list,
|
|
363
|
+
list(self._get_queryset().order_by("name").values_list("name", flat=True)[:3]),
|
|
364
|
+
"API sort not identical to QuerySet.order_by",
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
|
368
|
+
def test_list_objects_descending_ordered(self):
|
|
369
|
+
# Simple sorting check for models with a "name" field
|
|
370
|
+
# TreeModels don't support sorting at this time (order_by is not supported by TreeQuerySet)
|
|
371
|
+
# They will pass api == queryset tests below but will fail the user expected sort test
|
|
372
|
+
if hasattr(self.model, "name") and not issubclass(self.model, TreeModel):
|
|
373
|
+
self.add_permissions(f"{self.model._meta.app_label}.view_{self.model._meta.model_name}")
|
|
374
|
+
response = self.client.get(f"{self._get_list_url()}?sort=-name&limit=3", **self.header)
|
|
375
|
+
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
376
|
+
result_list = list(map(lambda p: p["name"], response.data["results"]))
|
|
377
|
+
self.assertEqual(
|
|
378
|
+
result_list,
|
|
379
|
+
list(self._get_queryset().order_by("-name").values_list("name", flat=True)[:3]),
|
|
380
|
+
"API sort not identical to QuerySet.order_by",
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
response_ascending = self.client.get(f"{self._get_list_url()}?sort=name&limit=3", **self.header)
|
|
384
|
+
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
385
|
+
result_list_ascending = list(map(lambda p: p["name"], response_ascending.data["results"]))
|
|
386
|
+
|
|
387
|
+
self.assertNotEqual(
|
|
388
|
+
result_list,
|
|
389
|
+
result_list_ascending,
|
|
390
|
+
"Same results obtained when sorting by name and by -name (QuerySet not ordering)",
|
|
391
|
+
)
|
|
392
|
+
|
|
296
393
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=[], STRICT_FILTERING=True)
|
|
297
394
|
def test_list_objects_unknown_filter_strict_filtering(self):
|
|
298
395
|
"""
|
|
@@ -335,6 +432,64 @@ class APIViewTestCases:
|
|
|
335
432
|
response = self.client.options(self._get_list_url(), **self.header)
|
|
336
433
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
337
434
|
|
|
435
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
|
436
|
+
def test_list_objects_csv(self):
|
|
437
|
+
"""
|
|
438
|
+
GET a list of objects in CSV format as an authenticated user with permission to view some objects.
|
|
439
|
+
"""
|
|
440
|
+
self.assertGreaterEqual(
|
|
441
|
+
self._get_queryset().count(),
|
|
442
|
+
3,
|
|
443
|
+
f"Test requires the creation of at least three {self.model} instances",
|
|
444
|
+
)
|
|
445
|
+
instance1, instance2, instance3 = self._get_queryset()[:3]
|
|
446
|
+
|
|
447
|
+
# Add object-level permission
|
|
448
|
+
obj_perm = users_models.ObjectPermission(
|
|
449
|
+
name="Test permission",
|
|
450
|
+
constraints={"pk__in": [instance1.pk, instance2.pk]},
|
|
451
|
+
actions=["view"],
|
|
452
|
+
)
|
|
453
|
+
obj_perm.save()
|
|
454
|
+
obj_perm.users.add(self.user)
|
|
455
|
+
obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
|
|
456
|
+
|
|
457
|
+
# Try filtered GET to objects specifying CSV format as a query parameter
|
|
458
|
+
response_1 = self.client.get(
|
|
459
|
+
f"{self._get_list_url()}?format=csv&id={instance1.pk}&id={instance3.pk}", **self.header
|
|
460
|
+
)
|
|
461
|
+
self.assertHttpStatus(response_1, status.HTTP_200_OK)
|
|
462
|
+
self.assertEqual(response_1.get("Content-Type"), "text/csv; charset=UTF-8")
|
|
463
|
+
|
|
464
|
+
# Try same request specifying CSV format via the ACCEPT header
|
|
465
|
+
response_2 = self.client.get(
|
|
466
|
+
f"{self._get_list_url()}?id={instance1.pk}&id={instance3.pk}", **self.header, HTTP_ACCEPT="text/csv"
|
|
467
|
+
)
|
|
468
|
+
self.assertHttpStatus(response_2, status.HTTP_200_OK)
|
|
469
|
+
self.assertEqual(response_2.get("Content-Type"), "text/csv; charset=UTF-8")
|
|
470
|
+
|
|
471
|
+
self.maxDiff = None
|
|
472
|
+
# This check is more useful than it might seem. Any related object that wasn't CSV-converted correctly
|
|
473
|
+
# will likely be rendered incorrectly as an API URL, and that API URL *will* differ between the
|
|
474
|
+
# two responses based on the inclusion or omission of the "?format=csv" parameter.
|
|
475
|
+
self.assertEqual(
|
|
476
|
+
response_1.content.decode(response_1.charset), response_2.content.decode(response_2.charset)
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
# Load the csv data back into a list of object dicts
|
|
480
|
+
reader = csv.DictReader(StringIO(response_1.content.decode(response_1.charset)))
|
|
481
|
+
rows = list(reader)
|
|
482
|
+
# Should only have one entry (instance1) since we filtered out instance2 and permissions block instance3
|
|
483
|
+
self.assertEqual(1, len(rows))
|
|
484
|
+
self.assertEqual(rows[0]["id"], str(instance1.pk))
|
|
485
|
+
self.assertEqual(rows[0]["display"], getattr(instance1, "display", str(instance1)))
|
|
486
|
+
if hasattr(self.model, "_custom_field_data"):
|
|
487
|
+
custom_fields = extras_models.CustomField.objects.get_for_model(self.model)
|
|
488
|
+
for cf in custom_fields:
|
|
489
|
+
self.assertIn(f"cf_{cf.key}", rows[0])
|
|
490
|
+
self.assertEqual(rows[0][f"cf_{cf.key}"], instance1._custom_field_data.get(cf.key) or "")
|
|
491
|
+
# TODO what other generic tests should we run on the data?
|
|
492
|
+
|
|
338
493
|
class CreateObjectViewTestCase(APITestCase):
|
|
339
494
|
create_data = []
|
|
340
495
|
validation_excluded_fields = []
|
|
@@ -390,7 +545,13 @@ class APIViewTestCases:
|
|
|
390
545
|
|
|
391
546
|
initial_count = self._get_queryset().count()
|
|
392
547
|
for i, create_data in enumerate(self.create_data):
|
|
393
|
-
|
|
548
|
+
if i == len(self.create_data) - 1:
|
|
549
|
+
# Test to see if depth parameter is ignored in POST request.
|
|
550
|
+
response = self.client.post(
|
|
551
|
+
self._get_list_url() + "?depth=3", create_data, format="json", **self.header
|
|
552
|
+
)
|
|
553
|
+
else:
|
|
554
|
+
response = self.client.post(self._get_list_url(), create_data, format="json", **self.header)
|
|
394
555
|
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
|
395
556
|
self.assertEqual(self._get_queryset().count(), initial_count + i + 1)
|
|
396
557
|
instance = self._get_queryset().get(pk=response.data["id"])
|
|
@@ -411,6 +572,56 @@ class APIViewTestCases:
|
|
|
411
572
|
self.assertEqual(len(objectchanges), 1)
|
|
412
573
|
self.assertEqual(objectchanges[0].action, extras_choices.ObjectChangeActionChoices.ACTION_CREATE)
|
|
413
574
|
|
|
575
|
+
def test_recreate_object_csv(self):
|
|
576
|
+
"""CSV export an object, delete it, and recreate it via CSV import."""
|
|
577
|
+
if hasattr(self, "get_deletable_object"):
|
|
578
|
+
# provided by DeleteObjectViewTestCase mixin
|
|
579
|
+
instance = self.get_deletable_object()
|
|
580
|
+
else:
|
|
581
|
+
# try to do it ourselves
|
|
582
|
+
instance = testing.get_deletable_objects(self.model, self._get_queryset()).first()
|
|
583
|
+
if instance is None:
|
|
584
|
+
self.fail("Couldn't find a single deletable object!")
|
|
585
|
+
|
|
586
|
+
# Add object-level permission
|
|
587
|
+
obj_perm = users_models.ObjectPermission(name="Test permission", actions=["add", "view"])
|
|
588
|
+
obj_perm.save()
|
|
589
|
+
obj_perm.users.add(self.user)
|
|
590
|
+
obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
|
|
591
|
+
|
|
592
|
+
response = self.client.get(self._get_detail_url(instance) + "?format=csv", **self.header)
|
|
593
|
+
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
594
|
+
csv_data = response.content.decode(response.charset)
|
|
595
|
+
|
|
596
|
+
serializer_class = get_serializer_for_model(self.model)
|
|
597
|
+
old_serializer = serializer_class(instance, context={"request": None})
|
|
598
|
+
old_data = old_serializer.data
|
|
599
|
+
instance.delete()
|
|
600
|
+
|
|
601
|
+
response = self.client.post(self._get_list_url(), csv_data, content_type="text/csv", **self.header)
|
|
602
|
+
self.assertHttpStatus(response, status.HTTP_201_CREATED, csv_data)
|
|
603
|
+
# Note that create via CSV is always treated as a bulk-create, and so the response is always a list of dicts
|
|
604
|
+
new_instance = self._get_queryset().get(pk=response.data[0]["id"])
|
|
605
|
+
self.assertNotEqual(new_instance.pk, instance.pk)
|
|
606
|
+
|
|
607
|
+
new_serializer = serializer_class(new_instance, context={"request": None})
|
|
608
|
+
new_data = new_serializer.data
|
|
609
|
+
for field_name, field in new_serializer.fields.items():
|
|
610
|
+
if field.read_only or field.write_only:
|
|
611
|
+
continue
|
|
612
|
+
if field_name in ["created", "last_updated"]:
|
|
613
|
+
self.assertNotEqual(
|
|
614
|
+
old_data[field_name],
|
|
615
|
+
new_data[field_name],
|
|
616
|
+
f"{field_name} should have been updated on delete/recreate but it didn't change!",
|
|
617
|
+
)
|
|
618
|
+
else:
|
|
619
|
+
self.assertEqual(
|
|
620
|
+
old_data[field_name],
|
|
621
|
+
new_data[field_name],
|
|
622
|
+
f"{field_name} should have been unchanged on delete/recreate but it differs!",
|
|
623
|
+
)
|
|
624
|
+
|
|
414
625
|
def test_bulk_create_objects(self):
|
|
415
626
|
"""
|
|
416
627
|
POST a set of objects in a single request.
|
|
@@ -466,6 +677,27 @@ class APIViewTestCases:
|
|
|
466
677
|
"""
|
|
467
678
|
PATCH a single object identified by its ID.
|
|
468
679
|
"""
|
|
680
|
+
|
|
681
|
+
def strip_serialized_object(this_object):
|
|
682
|
+
"""
|
|
683
|
+
Only here to work around acceptable differences in PATCH response vs GET response which are known bugs.
|
|
684
|
+
"""
|
|
685
|
+
# Work around for https://github.com/nautobot/nautobot/issues/3321
|
|
686
|
+
this_object.pop("last_updated", None)
|
|
687
|
+
# PATCH response always includes "opt-in" fields, but GET response does not.
|
|
688
|
+
this_object.pop("computed_fields", None)
|
|
689
|
+
this_object.pop("config_context", None)
|
|
690
|
+
this_object.pop("relationships", None)
|
|
691
|
+
|
|
692
|
+
for value in this_object.values():
|
|
693
|
+
if isinstance(value, dict):
|
|
694
|
+
strip_serialized_object(value)
|
|
695
|
+
elif isinstance(value, list):
|
|
696
|
+
for list_dict in value:
|
|
697
|
+
if isinstance(list_dict, dict):
|
|
698
|
+
strip_serialized_object(list_dict)
|
|
699
|
+
|
|
700
|
+
self.maxDiff = None
|
|
469
701
|
instance = self._get_queryset().first()
|
|
470
702
|
url = self._get_detail_url(instance)
|
|
471
703
|
update_data = self.update_data or getattr(self, "create_data")[0]
|
|
@@ -476,8 +708,44 @@ class APIViewTestCases:
|
|
|
476
708
|
obj_perm.users.add(self.user)
|
|
477
709
|
obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
|
|
478
710
|
|
|
711
|
+
# Verify that an empty PATCH results in no change to the object.
|
|
712
|
+
# This is to catch issues like https://github.com/nautobot/nautobot/issues/3533
|
|
713
|
+
|
|
714
|
+
# Add object-level permission for GET
|
|
715
|
+
obj_perm.actions = ["view"]
|
|
716
|
+
obj_perm.save()
|
|
717
|
+
# Get initial serialized object representation
|
|
718
|
+
get_response = self.client.get(url, **self.header)
|
|
719
|
+
self.assertHttpStatus(get_response, status.HTTP_200_OK)
|
|
720
|
+
initial_serialized_object = get_response.json()
|
|
721
|
+
strip_serialized_object(initial_serialized_object)
|
|
722
|
+
|
|
723
|
+
# Redefine object-level permission for PATCH
|
|
724
|
+
obj_perm.actions = ["change"]
|
|
725
|
+
obj_perm.save()
|
|
726
|
+
|
|
727
|
+
# Send empty PATCH request
|
|
728
|
+
response = self.client.patch(url, {}, format="json", **self.header)
|
|
729
|
+
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
730
|
+
serialized_object = response.json()
|
|
731
|
+
strip_serialized_object(serialized_object)
|
|
732
|
+
self.assertEqual(initial_serialized_object, serialized_object)
|
|
733
|
+
|
|
734
|
+
# Verify ObjectChange creation -- yes, even though nothing actually changed
|
|
735
|
+
# This may change (hah) at some point -- see https://github.com/nautobot/nautobot/issues/3321
|
|
736
|
+
if hasattr(self.model, "to_objectchange"):
|
|
737
|
+
objectchanges = lookup.get_changes_for_model(instance)
|
|
738
|
+
self.assertEqual(len(objectchanges), 1)
|
|
739
|
+
self.assertEqual(objectchanges[0].action, extras_choices.ObjectChangeActionChoices.ACTION_UPDATE)
|
|
740
|
+
objectchanges.delete()
|
|
741
|
+
|
|
742
|
+
# Verify that a PATCH with some data updates that data correctly.
|
|
479
743
|
response = self.client.patch(url, update_data, format="json", **self.header)
|
|
480
744
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
745
|
+
# Check for unexpected side effects on fields we DIDN'T intend to update
|
|
746
|
+
for field in initial_serialized_object:
|
|
747
|
+
if field not in update_data:
|
|
748
|
+
self.assertEqual(initial_serialized_object[field], serialized_object[field])
|
|
481
749
|
instance.refresh_from_db()
|
|
482
750
|
self.assertInstanceEqual(instance, update_data, exclude=self.validation_excluded_fields, api=True)
|
|
483
751
|
|
|
@@ -487,6 +755,36 @@ class APIViewTestCases:
|
|
|
487
755
|
self.assertEqual(len(objectchanges), 1)
|
|
488
756
|
self.assertEqual(objectchanges[0].action, extras_choices.ObjectChangeActionChoices.ACTION_UPDATE)
|
|
489
757
|
|
|
758
|
+
def test_get_put_round_trip(self):
|
|
759
|
+
"""GET and then PUT an object and verify that it's accepted and unchanged."""
|
|
760
|
+
self.maxDiff = None
|
|
761
|
+
# Add object-level permission
|
|
762
|
+
obj_perm = users_models.ObjectPermission(name="Test permission", actions=["view", "change"])
|
|
763
|
+
obj_perm.save()
|
|
764
|
+
obj_perm.users.add(self.user)
|
|
765
|
+
obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
|
|
766
|
+
|
|
767
|
+
instance = self._get_queryset().first()
|
|
768
|
+
url = self._get_detail_url(instance)
|
|
769
|
+
|
|
770
|
+
# GET object representation
|
|
771
|
+
opt_in_fields = getattr(get_serializer_for_model(self.model).Meta, "opt_in_fields", None)
|
|
772
|
+
if opt_in_fields:
|
|
773
|
+
url += "?" + "&".join([f"include={field}" for field in opt_in_fields])
|
|
774
|
+
get_response = self.client.get(url, **self.header)
|
|
775
|
+
self.assertHttpStatus(get_response, status.HTTP_200_OK)
|
|
776
|
+
initial_serialized_object = get_response.json()
|
|
777
|
+
|
|
778
|
+
# PUT same object representation
|
|
779
|
+
put_response = self.client.put(url, initial_serialized_object, format="json", **self.header)
|
|
780
|
+
self.assertHttpStatus(put_response, status.HTTP_200_OK, initial_serialized_object)
|
|
781
|
+
updated_serialized_object = put_response.json()
|
|
782
|
+
|
|
783
|
+
# Work around for https://github.com/nautobot/nautobot/issues/3321
|
|
784
|
+
initial_serialized_object.pop("last_updated", None)
|
|
785
|
+
updated_serialized_object.pop("last_updated", None)
|
|
786
|
+
self.assertEqual(initial_serialized_object, updated_serialized_object)
|
|
787
|
+
|
|
490
788
|
def test_bulk_update_objects(self):
|
|
491
789
|
"""
|
|
492
790
|
PATCH a set of objects in a single request.
|
|
@@ -522,36 +820,6 @@ class APIViewTestCases:
|
|
|
522
820
|
api=True,
|
|
523
821
|
)
|
|
524
822
|
|
|
525
|
-
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
|
526
|
-
def test_options_objects_returns_display_and_value(self):
|
|
527
|
-
"""
|
|
528
|
-
Make an OPTIONS request for a list endpoint and validate choices use the display and value keys.
|
|
529
|
-
"""
|
|
530
|
-
# Save self.user as superuser to be able to view available choices on list views.
|
|
531
|
-
self.user.is_superuser = True
|
|
532
|
-
self.user.save()
|
|
533
|
-
|
|
534
|
-
response = self.client.options(self._get_list_url(), **self.header)
|
|
535
|
-
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
536
|
-
data = response.json()
|
|
537
|
-
|
|
538
|
-
self.assertIn("actions", data)
|
|
539
|
-
|
|
540
|
-
# Grab any field that has choices defined (fields with enums)
|
|
541
|
-
if "POST" in data["actions"]:
|
|
542
|
-
field_choices = {k: v["choices"] for k, v in data["actions"]["POST"].items() if "choices" in v}
|
|
543
|
-
elif "PUT" in data["actions"]: # JobModelViewSet supports editing but not creation
|
|
544
|
-
field_choices = {k: v["choices"] for k, v in data["actions"]["PUT"].items() if "choices" in v}
|
|
545
|
-
else:
|
|
546
|
-
self.fail(f"Neither PUT nor POST are available actions in: {data['actions']}")
|
|
547
|
-
|
|
548
|
-
# Will successfully assert if field_choices has entries and will not fail if model as no enum choices
|
|
549
|
-
# Broken down to provide better failure messages
|
|
550
|
-
for field, choices in field_choices.items():
|
|
551
|
-
for choice in choices:
|
|
552
|
-
self.assertIn("display", choice, f"A choice in {field} is missing the display key")
|
|
553
|
-
self.assertIn("value", choice, f"A choice in {field} is missing the value key")
|
|
554
|
-
|
|
555
823
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
|
556
824
|
def test_options_returns_expected_choices(self):
|
|
557
825
|
"""
|
|
@@ -571,11 +839,25 @@ class APIViewTestCases:
|
|
|
571
839
|
|
|
572
840
|
self.assertIn("actions", data)
|
|
573
841
|
|
|
574
|
-
# Grab any field
|
|
575
|
-
if
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
842
|
+
# Grab any field that has choices defined (fields with enums)
|
|
843
|
+
if any(
|
|
844
|
+
[
|
|
845
|
+
"POST" in data["actions"],
|
|
846
|
+
"PUT" in data["actions"],
|
|
847
|
+
]
|
|
848
|
+
):
|
|
849
|
+
schema = data["schema"]
|
|
850
|
+
props = schema["properties"]
|
|
851
|
+
fields = props.keys()
|
|
852
|
+
field_choices = set()
|
|
853
|
+
for field_name in fields:
|
|
854
|
+
obj = props[field_name]
|
|
855
|
+
if "enum" in obj and "enumNames" in obj:
|
|
856
|
+
enum = obj["enum"]
|
|
857
|
+
# Zipping to assert that the enum and the mapping have the same number of items.
|
|
858
|
+
model_field_choices = dict(zip(obj["enumNames"], enum))
|
|
859
|
+
self.assertEqual(len(enum), len(model_field_choices))
|
|
860
|
+
field_choices.add(field_name)
|
|
579
861
|
else:
|
|
580
862
|
self.fail(f"Neither PUT nor POST are available actions in: {data['actions']}")
|
|
581
863
|
|
|
@@ -686,6 +968,39 @@ class APIViewTestCases:
|
|
|
686
968
|
self.assertIn("notes_url", response.data)
|
|
687
969
|
self.assertIn(f"{url}notes/", str(response.data["notes_url"]))
|
|
688
970
|
|
|
971
|
+
class TreeModelAPIViewTestCaseMixin:
|
|
972
|
+
"""Test `?depth=2` query parameter for TreeModel"""
|
|
973
|
+
|
|
974
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
|
975
|
+
def test_list_objects_depth_2(self):
|
|
976
|
+
"""
|
|
977
|
+
GET a list of objects using the "?depth=2" parameter.
|
|
978
|
+
TreeModel Only
|
|
979
|
+
"""
|
|
980
|
+
field = "parent"
|
|
981
|
+
|
|
982
|
+
self.add_permissions(f"{self.model._meta.app_label}.view_{self.model._meta.model_name}")
|
|
983
|
+
url = f"{self._get_list_url()}?depth=2"
|
|
984
|
+
response = self.client.get(url, **self.header)
|
|
985
|
+
|
|
986
|
+
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
987
|
+
self.assertIsInstance(response.data, dict)
|
|
988
|
+
self.assertIn("results", response.data)
|
|
989
|
+
self.assertEqual(len(response.data["results"]), self._get_queryset().count())
|
|
990
|
+
|
|
991
|
+
response_data = response.data["results"]
|
|
992
|
+
for data in response_data:
|
|
993
|
+
# First Level Parent
|
|
994
|
+
self.assertEqual(field in data, True)
|
|
995
|
+
if data[field] is not None:
|
|
996
|
+
self.assertIsInstance(data[field], dict)
|
|
997
|
+
self.assertTrue(is_uuid(data[field]["id"]))
|
|
998
|
+
# Second Level Parent
|
|
999
|
+
self.assertIn(field, data[field])
|
|
1000
|
+
if data[field][field] is not None:
|
|
1001
|
+
self.assertIsInstance(data[field][field], dict)
|
|
1002
|
+
self.assertTrue(is_uuid(data[field][field]["id"]))
|
|
1003
|
+
|
|
689
1004
|
class APIViewTestCase(
|
|
690
1005
|
GetObjectViewTestCase,
|
|
691
1006
|
ListObjectsViewTestCase,
|
nautobot/core/testing/filters.py
CHANGED
|
@@ -119,7 +119,7 @@ class FilterTestCases:
|
|
|
119
119
|
"""Test all `RelatedMembershipBooleanFilter` filters found in `self.filterset.get_filters()`.
|
|
120
120
|
|
|
121
121
|
This test asserts that `filter=True` matches `self.queryset.filter(field__isnull=False)` and
|
|
122
|
-
that `filter=False` matches `self.queryset.filter(field__isnull=
|
|
122
|
+
that `filter=False` matches `self.queryset.filter(field__isnull=True)`.
|
|
123
123
|
"""
|
|
124
124
|
for filter_name, filter_object in self.filterset.get_filters().items():
|
|
125
125
|
if not isinstance(filter_object, RelatedMembershipBooleanFilter):
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from unittest import skip
|
|
1
2
|
from django.apps import apps
|
|
2
3
|
from django.core.management import call_command
|
|
3
4
|
from django.db import connection
|
|
@@ -5,6 +6,7 @@ from django.db.migrations.executor import MigrationExecutor
|
|
|
5
6
|
from django.test import TestCase
|
|
6
7
|
|
|
7
8
|
|
|
9
|
+
@skip("TODO: Havoc has been wreaked on migrations in 2.0, so this test is currently broken.")
|
|
8
10
|
class NautobotDataMigrationTest(TestCase):
|
|
9
11
|
@property
|
|
10
12
|
def app(self):
|
nautobot/core/testing/mixins.py
CHANGED
|
@@ -9,8 +9,7 @@ from django.db.models import JSONField, ManyToManyField
|
|
|
9
9
|
from django.forms.models import model_to_dict
|
|
10
10
|
from django.utils.text import slugify
|
|
11
11
|
from netaddr import IPNetwork
|
|
12
|
-
from rest_framework.test import APIClient
|
|
13
|
-
from taggit.managers import TaggableManager
|
|
12
|
+
from rest_framework.test import APIClient, APIRequestFactory
|
|
14
13
|
|
|
15
14
|
from nautobot.core import testing
|
|
16
15
|
from nautobot.core.models import fields as core_fields
|
|
@@ -93,7 +92,7 @@ class NautobotTestCaseMixin:
|
|
|
93
92
|
field = None
|
|
94
93
|
|
|
95
94
|
# Handle ManyToManyFields
|
|
96
|
-
if value and isinstance(field, (ManyToManyField,
|
|
95
|
+
if value and isinstance(field, (ManyToManyField, core_fields.TagsField)):
|
|
97
96
|
# Only convert ContentType to <app_label>.<model> for API serializers/views
|
|
98
97
|
if api and field.related_model is ContentType:
|
|
99
98
|
model_dict[key] = sorted([f"{ct.app_label}.{ct.model}" for ct in value])
|
|
@@ -150,14 +149,13 @@ class NautobotTestCaseMixin:
|
|
|
150
149
|
if isinstance(expected_status, int):
|
|
151
150
|
expected_status = [expected_status]
|
|
152
151
|
if response.status_code not in expected_status:
|
|
152
|
+
err_message = f"Expected HTTP status(es) {expected_status}; received {response.status_code}:"
|
|
153
153
|
if hasattr(response, "data"):
|
|
154
154
|
# REST API response; pass the response data through directly
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
err = form_errors or response.content.decode(response.charset) or "No data"
|
|
160
|
-
err_message = f"Expected HTTP status(es) {expected_status}; received {response.status_code}: {err}"
|
|
155
|
+
err_message += f"\n{response.data}"
|
|
156
|
+
# Attempt to extract form validation errors from the response HTML
|
|
157
|
+
form_errors = testing.extract_form_failures(response.content.decode(response.charset))
|
|
158
|
+
err_message += "\n" + str(form_errors or response.content.decode(response.charset) or "No data")
|
|
161
159
|
if msg:
|
|
162
160
|
err_message = f"{msg}\n{err_message}"
|
|
163
161
|
self.assertIn(response.status_code, expected_status, err_message)
|
|
@@ -183,6 +181,9 @@ class NautobotTestCaseMixin:
|
|
|
183
181
|
if isinstance(v, list):
|
|
184
182
|
# Sort lists of values. This includes items like tags, or other M2M fields
|
|
185
183
|
new_model_dict[k] = sorted(v)
|
|
184
|
+
elif k == "data_schema" and isinstance(v, str):
|
|
185
|
+
# Standardize the data_schema JSON, since the column is JSON and MySQL/dolt do not guarantee order
|
|
186
|
+
new_model_dict[k] = self.standardize_json(v)
|
|
186
187
|
else:
|
|
187
188
|
new_model_dict[k] = v
|
|
188
189
|
|
|
@@ -193,6 +194,9 @@ class NautobotTestCaseMixin:
|
|
|
193
194
|
if isinstance(v, list):
|
|
194
195
|
# Sort lists of values. This includes items like tags, or other M2M fields
|
|
195
196
|
relevant_data[k] = sorted(v)
|
|
197
|
+
elif k == "data_schema" and isinstance(v, str):
|
|
198
|
+
# Standardize the data_schema JSON, since the column is JSON and MySQL/dolt do not guarantee order
|
|
199
|
+
relevant_data[k] = self.standardize_json(v)
|
|
196
200
|
else:
|
|
197
201
|
relevant_data[k] = v
|
|
198
202
|
|
|
@@ -210,6 +214,15 @@ class NautobotTestCaseMixin:
|
|
|
210
214
|
# Convenience methods
|
|
211
215
|
#
|
|
212
216
|
|
|
217
|
+
def absolute_api_url(self, obj):
|
|
218
|
+
"""Get the absolute API URL ("http://nautobot.example.com/api/...") for a given object."""
|
|
219
|
+
request = APIRequestFactory(SERVER_NAME="nautobot.example.com").get("")
|
|
220
|
+
return request.build_absolute_uri(obj.get_absolute_url(api=True))
|
|
221
|
+
|
|
222
|
+
def standardize_json(self, data):
|
|
223
|
+
obj = json.loads(data)
|
|
224
|
+
return json.dumps(obj, sort_keys=True)
|
|
225
|
+
|
|
213
226
|
@classmethod
|
|
214
227
|
def create_tags(cls, *names):
|
|
215
228
|
"""
|
nautobot/core/testing/schema.py
CHANGED
|
@@ -4,6 +4,7 @@ import yaml
|
|
|
4
4
|
from django.conf import settings
|
|
5
5
|
from django.core.management import call_command
|
|
6
6
|
from django.test import tag
|
|
7
|
+
from rest_framework.settings import api_settings
|
|
7
8
|
|
|
8
9
|
from nautobot.core.testing import views
|
|
9
10
|
|
|
@@ -18,7 +19,7 @@ class OpenAPISchemaTestCases:
|
|
|
18
19
|
# We could load the schema from the /api/swagger.yaml endpoint in setUp(self) via self.client,
|
|
19
20
|
# but it's fairly expensive to do so. Better to do so only once per class.
|
|
20
21
|
cls.schemas = {}
|
|
21
|
-
for api_version in
|
|
22
|
+
for api_version in api_settings.ALLOWED_VERSIONS:
|
|
22
23
|
out = StringIO()
|
|
23
24
|
err = StringIO()
|
|
24
25
|
call_command("spectacular", "--api-version", api_version, stdout=out, stderr=err)
|