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/api/serializers.py
CHANGED
|
@@ -5,16 +5,34 @@ from django.core.exceptions import (
|
|
|
5
5
|
FieldError,
|
|
6
6
|
MultipleObjectsReturned,
|
|
7
7
|
ObjectDoesNotExist,
|
|
8
|
+
ValidationError as DjangoValidationError,
|
|
8
9
|
)
|
|
9
10
|
from django.db.models import AutoField, ManyToManyField
|
|
11
|
+
from django.db.models.fields.related_descriptors import ManyToManyDescriptor
|
|
12
|
+
from django.urls import NoReverseMatch
|
|
13
|
+
from drf_spectacular.types import OpenApiTypes
|
|
10
14
|
from drf_spectacular.utils import extend_schema_field, PolymorphicProxySerializer as _PolymorphicProxySerializer
|
|
11
15
|
from rest_framework import serializers
|
|
12
|
-
from rest_framework.
|
|
16
|
+
from rest_framework.fields import CreateOnlyDefault
|
|
17
|
+
from rest_framework.exceptions import PermissionDenied, ValidationError
|
|
18
|
+
from rest_framework.reverse import reverse
|
|
19
|
+
from rest_framework.serializers import SerializerMethodField
|
|
20
|
+
from rest_framework.utils.model_meta import RelationInfo, _get_to_field
|
|
13
21
|
|
|
14
22
|
from nautobot.core.api.fields import ObjectTypeField
|
|
15
|
-
from nautobot.core.api.
|
|
23
|
+
from nautobot.core.api.mixins import WritableSerializerMixin
|
|
24
|
+
from nautobot.core.api.utils import (
|
|
25
|
+
dict_to_filter_params,
|
|
26
|
+
nested_serializer_factory,
|
|
27
|
+
)
|
|
28
|
+
from nautobot.core.models.managers import TagsManager
|
|
29
|
+
from nautobot.core.models.utils import construct_natural_key_slug
|
|
30
|
+
from nautobot.core.utils.lookup import get_route_for_model
|
|
16
31
|
from nautobot.core.utils.requests import normalize_querydict
|
|
17
|
-
|
|
32
|
+
from nautobot.extras.api.relationships import RelationshipsDataField
|
|
33
|
+
from nautobot.extras.api.customfields import CustomFieldsDataField, CustomFieldDefaultValues
|
|
34
|
+
from nautobot.extras.choices import RelationshipSideChoices
|
|
35
|
+
from nautobot.extras.models import RelationshipAssociation, Tag
|
|
18
36
|
|
|
19
37
|
logger = logging.getLogger(__name__)
|
|
20
38
|
|
|
@@ -82,18 +100,64 @@ class OptInFieldsMixin:
|
|
|
82
100
|
return self.__pruned_fields
|
|
83
101
|
|
|
84
102
|
|
|
85
|
-
class
|
|
103
|
+
class NautobotHyperlinkedRelatedField(WritableSerializerMixin, serializers.HyperlinkedRelatedField):
|
|
104
|
+
"""DRF's built-in HyperlinkedRelatedField combined with custom to_internal_value() function"""
|
|
105
|
+
|
|
106
|
+
def __init__(self, *args, **kwargs):
|
|
107
|
+
"""Override DRF's namespace-unaware default view_name logic for HyperlinkedRelatedField.
|
|
108
|
+
|
|
109
|
+
DRF defaults to '{model_name}-detail' instead of '{app_label}:{model_name}-detail'.
|
|
110
|
+
"""
|
|
111
|
+
if "view_name" not in kwargs or (kwargs["view_name"].endswith("-detail") and ":" not in kwargs["view_name"]):
|
|
112
|
+
if "queryset" not in kwargs:
|
|
113
|
+
logger.warning(
|
|
114
|
+
'"view_name=%r" is probably incorrect for this related API field; '
|
|
115
|
+
'unable to determine the correct "view_name" as "queryset" wasn\'t specified',
|
|
116
|
+
kwargs["view_name"],
|
|
117
|
+
)
|
|
118
|
+
else:
|
|
119
|
+
kwargs["view_name"] = get_route_for_model(kwargs["queryset"].model, "detail", api=True)
|
|
120
|
+
super().__init__(*args, **kwargs)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class BaseModelSerializer(OptInFieldsMixin, serializers.HyperlinkedModelSerializer):
|
|
86
124
|
"""
|
|
87
125
|
This base serializer implements common fields and logic for all ModelSerializers.
|
|
88
126
|
|
|
89
127
|
Namely, it:
|
|
90
128
|
|
|
91
129
|
- defines the `display` field which exposes a human friendly value for the given object.
|
|
92
|
-
- ensures that `id` field is always present on the serializer as well
|
|
130
|
+
- ensures that `id` field is always present on the serializer as well.
|
|
93
131
|
- ensures that `created` and `last_updated` fields are always present if applicable to this model and serializer.
|
|
132
|
+
- ensures that `object_type` field is always present on the serializer which represents the content-type of this
|
|
133
|
+
serializer's associated model (e.g. "dcim.device"). This is required as the OpenAPI schema, using the
|
|
134
|
+
PolymorphicProxySerializer class defined below, relies upon this field as a way to identify to the client
|
|
135
|
+
which of several possible serializers are in use for a given attribute.
|
|
136
|
+
- supports `?depth` query parameter. It is passed in as `nested_depth` to the `build_nested_field()` function
|
|
137
|
+
to enable the dynamic generation of nested serializers.
|
|
94
138
|
"""
|
|
95
139
|
|
|
140
|
+
serializer_related_field = NautobotHyperlinkedRelatedField
|
|
141
|
+
|
|
96
142
|
display = serializers.SerializerMethodField(read_only=True, help_text="Human friendly display value")
|
|
143
|
+
object_type = ObjectTypeField()
|
|
144
|
+
natural_key_slug = serializers.SerializerMethodField()
|
|
145
|
+
|
|
146
|
+
def __init__(self, *args, **kwargs):
|
|
147
|
+
super().__init__(*args, **kwargs)
|
|
148
|
+
# If it is not a Nested Serializer, we should set the depth argument to whatever is in the request's context
|
|
149
|
+
if not self.is_nested:
|
|
150
|
+
# We set our default depth value here to 1 because in OpenAPISchema
|
|
151
|
+
# get_serializer_context() (where we get the depth from self.request.query_params) is not called
|
|
152
|
+
# so in order to have enough information present in the OpenAPISchema, we set depth here to 1
|
|
153
|
+
# RestAPI serializer is not affected by this because get_serializer_context() is always called
|
|
154
|
+
# and depth is either passed into the request.query_params, or default to 0.
|
|
155
|
+
self.Meta.depth = self.context.get("depth", 1)
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def is_nested(self):
|
|
159
|
+
"""Return whether this is a nested serializer."""
|
|
160
|
+
return getattr(self.Meta, "is_nested", False)
|
|
97
161
|
|
|
98
162
|
@extend_schema_field(serializers.CharField)
|
|
99
163
|
def get_display(self, instance):
|
|
@@ -102,13 +166,21 @@ class BaseModelSerializer(OptInFieldsMixin, serializers.ModelSerializer):
|
|
|
102
166
|
"""
|
|
103
167
|
return getattr(instance, "display", str(instance))
|
|
104
168
|
|
|
169
|
+
@extend_schema_field(serializers.CharField)
|
|
170
|
+
def get_natural_key_slug(self, instance):
|
|
171
|
+
try:
|
|
172
|
+
return getattr(instance, "natural_key_slug", construct_natural_key_slug(instance.natural_key()))
|
|
173
|
+
except (AttributeError, NotImplementedError):
|
|
174
|
+
return "unknown"
|
|
175
|
+
|
|
105
176
|
def extend_field_names(self, fields, field_name, at_start=False, opt_in_only=False):
|
|
106
177
|
"""Prepend or append the given field_name to `fields` and optionally self.Meta.opt_in_fields as well."""
|
|
107
|
-
if field_name
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
178
|
+
if field_name in fields:
|
|
179
|
+
fields.remove(field_name)
|
|
180
|
+
if at_start:
|
|
181
|
+
fields.insert(0, field_name)
|
|
182
|
+
else:
|
|
183
|
+
fields.append(field_name)
|
|
112
184
|
if opt_in_only:
|
|
113
185
|
if not getattr(self.Meta, "opt_in_fields", None):
|
|
114
186
|
self.Meta.opt_in_fields = [field_name]
|
|
@@ -118,31 +190,104 @@ class BaseModelSerializer(OptInFieldsMixin, serializers.ModelSerializer):
|
|
|
118
190
|
|
|
119
191
|
def get_field_names(self, declared_fields, info):
|
|
120
192
|
"""
|
|
121
|
-
Override get_field_names() to
|
|
193
|
+
Override get_field_names() to add some custom logic.
|
|
122
194
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
enforce by convention that all consumers of BaseModelSerializer include each of these standard fields in their
|
|
127
|
-
`Meta.fields` which would surely lead to errors of omission; therefore we have chosen the former approach.
|
|
195
|
+
Assuming that we follow the pattern where `fields = "__all__" for the vast majority of serializers in Nautobot,
|
|
196
|
+
we do not strictly need to use this method to protect against inadvertently omitting standard fields
|
|
197
|
+
like `display`, `created`, and `last_updated`. However, we continue to do as a bit of redundant safety.
|
|
128
198
|
|
|
129
|
-
|
|
130
|
-
|
|
199
|
+
The other purpose of this method now is to manipulate the set of fields that "__all__" actually means as a
|
|
200
|
+
way of *excluding* fields that we *don't* want to include by default for performance or data exposure reasons.
|
|
131
201
|
"""
|
|
132
202
|
fields = list(super().get_field_names(declared_fields, info)) # Meta.fields could be defined as a tuple
|
|
203
|
+
|
|
204
|
+
# Add initial fields in "reverse" order since they're each inserted at the start of the list.
|
|
133
205
|
self.extend_field_names(fields, "display", at_start=True)
|
|
206
|
+
self.extend_field_names(fields, "object_type", at_start=True)
|
|
207
|
+
# Since we use HyperlinkedModelSerializer as our base class, "url" is auto-included by "__all__" but "id" isn't.
|
|
134
208
|
self.extend_field_names(fields, "id", at_start=True)
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
209
|
+
|
|
210
|
+
# Move these fields to the end
|
|
211
|
+
if hasattr(self.Meta.model, "created"):
|
|
212
|
+
self.extend_field_names(fields, "created")
|
|
213
|
+
if hasattr(self.Meta.model, "last_updated"):
|
|
214
|
+
self.extend_field_names(fields, "last_updated")
|
|
215
|
+
|
|
216
|
+
def filter_field(field):
|
|
217
|
+
# Eliminate all field names that start with "_" as those fields are not user-facing
|
|
218
|
+
if field.startswith("_"):
|
|
219
|
+
return False
|
|
220
|
+
# These are expensive to look up, so we have decided not to include them on nested serializers
|
|
221
|
+
if self.is_nested and isinstance(getattr(self.Meta.model, field, None), ManyToManyDescriptor):
|
|
222
|
+
return False
|
|
223
|
+
return True
|
|
224
|
+
|
|
225
|
+
fields = [field for field in fields if filter_field(field)]
|
|
141
226
|
return fields
|
|
142
227
|
|
|
228
|
+
def build_field(self, field_name, info, model_class, nested_depth):
|
|
229
|
+
"""
|
|
230
|
+
Return a two tuple of (cls, kwargs) to build a serializer field with.
|
|
231
|
+
"""
|
|
232
|
+
request = self.context.get("request")
|
|
233
|
+
# Make sure that PATCH/POST/PUT method response serializers are consistent
|
|
234
|
+
# with depth of 0
|
|
235
|
+
if request is not None and request.method != "GET":
|
|
236
|
+
nested_depth = 0
|
|
237
|
+
# For tags field, DRF does not recognize the relationship between tags and the model itself (?)
|
|
238
|
+
# so instead of calling build_nested_field() it will call build_property_field() which
|
|
239
|
+
# makes the field impervious to the `?depth` parameter.
|
|
240
|
+
# So we intercept it here to call build_nested_field()
|
|
241
|
+
# which will make the tags field be rendered with TagSerializer() and respect the `depth` parameter.
|
|
242
|
+
if isinstance(getattr(model_class, field_name, None), TagsManager) and nested_depth > 0:
|
|
243
|
+
tags_field = getattr(model_class, field_name)
|
|
244
|
+
relation_info = RelationInfo(
|
|
245
|
+
model_field=tags_field,
|
|
246
|
+
related_model=Tag,
|
|
247
|
+
to_many=True,
|
|
248
|
+
has_through_model=True,
|
|
249
|
+
to_field=_get_to_field(tags_field),
|
|
250
|
+
reverse=False,
|
|
251
|
+
)
|
|
252
|
+
return self.build_nested_field(field_name, relation_info, nested_depth)
|
|
253
|
+
|
|
254
|
+
return super().build_field(field_name, info, model_class, nested_depth)
|
|
255
|
+
|
|
256
|
+
def build_relational_field(self, field_name, relation_info):
|
|
257
|
+
"""Override DRF's default relational-field construction to be app-aware."""
|
|
258
|
+
field_class, field_kwargs = super().build_relational_field(field_name, relation_info)
|
|
259
|
+
if "view_name" in field_kwargs:
|
|
260
|
+
field_kwargs["view_name"] = get_route_for_model(relation_info.related_model, "detail", api=True)
|
|
261
|
+
return field_class, field_kwargs
|
|
262
|
+
|
|
263
|
+
def build_property_field(self, field_name, model_class):
|
|
264
|
+
"""
|
|
265
|
+
Create a property field for model methods and properties.
|
|
266
|
+
"""
|
|
267
|
+
if isinstance(getattr(model_class, field_name, None), TagsManager):
|
|
268
|
+
field_class = NautobotHyperlinkedRelatedField
|
|
269
|
+
field_kwargs = {
|
|
270
|
+
"queryset": Tag.objects.get_for_model(model_class),
|
|
271
|
+
"many": True,
|
|
272
|
+
"required": False,
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return field_class, field_kwargs
|
|
276
|
+
return super().build_property_field(field_name, model_class)
|
|
277
|
+
|
|
278
|
+
def build_nested_field(self, field_name, relation_info, nested_depth):
|
|
279
|
+
return nested_serializer_factory(relation_info, nested_depth)
|
|
280
|
+
|
|
281
|
+
def build_url_field(self, field_name, model_class):
|
|
282
|
+
"""Override DRF's default 'url' field construction to be app-aware."""
|
|
283
|
+
field_class, field_kwargs = super().build_url_field(field_name, model_class)
|
|
284
|
+
if "view_name" in field_kwargs:
|
|
285
|
+
field_kwargs["view_name"] = get_route_for_model(model_class, "detail", api=True)
|
|
286
|
+
return field_class, field_kwargs
|
|
287
|
+
|
|
143
288
|
|
|
144
289
|
class TreeModelSerializerMixin(BaseModelSerializer):
|
|
145
|
-
"""Add a `tree_depth` field to model serializers based on django-tree-queries."""
|
|
290
|
+
"""Add a `tree_depth` field to non-nested model serializers based on django-tree-queries."""
|
|
146
291
|
|
|
147
292
|
tree_depth = serializers.SerializerMethodField(read_only=True)
|
|
148
293
|
|
|
@@ -151,6 +296,13 @@ class TreeModelSerializerMixin(BaseModelSerializer):
|
|
|
151
296
|
"""The `tree_depth` is not a database field, but an annotation automatically added by django-tree-queries."""
|
|
152
297
|
return getattr(obj, "tree_depth", None)
|
|
153
298
|
|
|
299
|
+
def get_field_names(self, declared_fields, info):
|
|
300
|
+
"""Ensure that "tree_depth" is included on root serializers only, as nested objects are not annotated."""
|
|
301
|
+
fields = list(super().get_field_names(declared_fields, info))
|
|
302
|
+
if self.is_nested and "tree_depth" in fields:
|
|
303
|
+
fields.remove("tree_depth")
|
|
304
|
+
return fields
|
|
305
|
+
|
|
154
306
|
|
|
155
307
|
class ValidatedModelSerializer(BaseModelSerializer):
|
|
156
308
|
"""
|
|
@@ -178,7 +330,6 @@ class ValidatedModelSerializer(BaseModelSerializer):
|
|
|
178
330
|
for k, v in attrs.items():
|
|
179
331
|
setattr(instance, k, v)
|
|
180
332
|
instance.full_clean()
|
|
181
|
-
|
|
182
333
|
return data
|
|
183
334
|
|
|
184
335
|
|
|
@@ -193,15 +344,6 @@ class WritableNestedSerializer(BaseModelSerializer):
|
|
|
193
344
|
which of several possible nested serializers are in use for a given attribute.
|
|
194
345
|
"""
|
|
195
346
|
|
|
196
|
-
object_type = ObjectTypeField()
|
|
197
|
-
|
|
198
|
-
def get_field_names(self, declared_fields, info):
|
|
199
|
-
"""Ensure that the "object_type" field is always included in self.fields."""
|
|
200
|
-
fields = list(super().get_field_names(declared_fields, info))
|
|
201
|
-
if "object_type" not in fields:
|
|
202
|
-
fields.append("object_type")
|
|
203
|
-
return fields
|
|
204
|
-
|
|
205
347
|
def get_queryset(self):
|
|
206
348
|
return self.Meta.model.objects
|
|
207
349
|
|
|
@@ -315,3 +457,203 @@ class BulkOperationIntegerIDSerializer(serializers.Serializer):
|
|
|
315
457
|
class GraphQLAPISerializer(serializers.Serializer):
|
|
316
458
|
query = serializers.CharField(required=True, help_text="GraphQL query")
|
|
317
459
|
variables = serializers.JSONField(required=False, help_text="Variables in JSON Format")
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
class CustomFieldModelSerializerMixin(ValidatedModelSerializer):
|
|
463
|
+
"""
|
|
464
|
+
Extends ModelSerializer to render any CustomFields and their values associated with an object.
|
|
465
|
+
"""
|
|
466
|
+
|
|
467
|
+
computed_fields = SerializerMethodField(read_only=True)
|
|
468
|
+
custom_fields = CustomFieldsDataField(
|
|
469
|
+
source="_custom_field_data",
|
|
470
|
+
default=CreateOnlyDefault(CustomFieldDefaultValues()),
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
@extend_schema_field(OpenApiTypes.OBJECT)
|
|
474
|
+
def get_computed_fields(self, obj):
|
|
475
|
+
return obj.get_computed_fields()
|
|
476
|
+
|
|
477
|
+
def get_field_names(self, declared_fields, info):
|
|
478
|
+
"""Ensure that "custom_fields" and "computed_fields" are included appropriately."""
|
|
479
|
+
fields = list(super().get_field_names(declared_fields, info))
|
|
480
|
+
# Ensure that custom_fields field appears at the end, not the start, of the fields
|
|
481
|
+
self.extend_field_names(fields, "custom_fields")
|
|
482
|
+
if not self.is_nested:
|
|
483
|
+
# Only include computed_fields as opt-in.
|
|
484
|
+
self.extend_field_names(fields, "computed_fields", opt_in_only=True)
|
|
485
|
+
else:
|
|
486
|
+
# As computed fields are expensive, do not include them in nested serializers even if opted-in at the root
|
|
487
|
+
if "computed_fields" in fields:
|
|
488
|
+
fields.remove("computed_fields")
|
|
489
|
+
return fields
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
class RelationshipModelSerializerMixin(ValidatedModelSerializer):
|
|
493
|
+
"""Extend ValidatedModelSerializer with a `relationships` field."""
|
|
494
|
+
|
|
495
|
+
# TODO # 3024 need to change this as well to show just pks in depth=0
|
|
496
|
+
relationships = RelationshipsDataField(required=False, source="*")
|
|
497
|
+
|
|
498
|
+
def create(self, validated_data):
|
|
499
|
+
relationships_data = validated_data.pop("relationships", {})
|
|
500
|
+
required_relationships_errors = self.Meta().model.required_related_objects_errors(
|
|
501
|
+
output_for="api", initial_data=relationships_data
|
|
502
|
+
)
|
|
503
|
+
if required_relationships_errors:
|
|
504
|
+
raise ValidationError({"relationships": required_relationships_errors})
|
|
505
|
+
instance = super().create(validated_data)
|
|
506
|
+
if relationships_data:
|
|
507
|
+
try:
|
|
508
|
+
self._save_relationships(instance, relationships_data)
|
|
509
|
+
except DjangoValidationError as error:
|
|
510
|
+
raise ValidationError(str(error)) from error
|
|
511
|
+
return instance
|
|
512
|
+
|
|
513
|
+
def update(self, instance, validated_data):
|
|
514
|
+
relationships_key_specified = "relationships" in self.context["request"].data
|
|
515
|
+
relationships_data = validated_data.pop("relationships", {})
|
|
516
|
+
required_relationships_errors = self.Meta().model.required_related_objects_errors(
|
|
517
|
+
output_for="api",
|
|
518
|
+
initial_data=relationships_data,
|
|
519
|
+
relationships_key_specified=relationships_key_specified,
|
|
520
|
+
instance=instance,
|
|
521
|
+
)
|
|
522
|
+
if required_relationships_errors:
|
|
523
|
+
raise ValidationError({"relationships": required_relationships_errors})
|
|
524
|
+
|
|
525
|
+
instance = super().update(instance, validated_data)
|
|
526
|
+
if relationships_data:
|
|
527
|
+
try:
|
|
528
|
+
self._save_relationships(instance, relationships_data)
|
|
529
|
+
except DjangoValidationError as error:
|
|
530
|
+
raise ValidationError(str(error)) from error
|
|
531
|
+
return instance
|
|
532
|
+
|
|
533
|
+
def _save_relationships(self, instance, relationships):
|
|
534
|
+
"""Create/update RelationshipAssociations corresponding to a model instance."""
|
|
535
|
+
# relationships has already passed RelationshipsDataField.to_internal_value(), so we can skip some try/excepts
|
|
536
|
+
logger.debug("_save_relationships: %s : %s", instance, relationships)
|
|
537
|
+
for relationship, relationship_data in relationships.items():
|
|
538
|
+
for other_side in ["source", "destination", "peer"]:
|
|
539
|
+
if other_side not in relationship_data:
|
|
540
|
+
continue
|
|
541
|
+
|
|
542
|
+
other_type = getattr(relationship, f"{other_side}_type")
|
|
543
|
+
other_side_model = other_type.model_class()
|
|
544
|
+
|
|
545
|
+
expected_objects_data = relationship_data[other_side]
|
|
546
|
+
expected_objects = [
|
|
547
|
+
other_side_model.objects.get(**object_data) for object_data in expected_objects_data
|
|
548
|
+
]
|
|
549
|
+
|
|
550
|
+
this_side = RelationshipSideChoices.OPPOSITE[other_side]
|
|
551
|
+
|
|
552
|
+
if this_side != RelationshipSideChoices.SIDE_PEER:
|
|
553
|
+
existing_associations = relationship.relationship_associations.filter(
|
|
554
|
+
**{f"{this_side}_id": instance.pk}
|
|
555
|
+
)
|
|
556
|
+
existing_objects = [assoc.get_peer(instance) for assoc in existing_associations]
|
|
557
|
+
else:
|
|
558
|
+
existing_associations_1 = relationship.relationship_associations.filter(source_id=instance.pk)
|
|
559
|
+
existing_objects_1 = [assoc.get_peer(instance) for assoc in existing_associations_1]
|
|
560
|
+
existing_associations_2 = relationship.relationship_associations.filter(destination_id=instance.pk)
|
|
561
|
+
existing_objects_2 = [assoc.get_peer(instance) for assoc in existing_associations_2]
|
|
562
|
+
existing_associations = list(existing_associations_1) + list(existing_associations_2)
|
|
563
|
+
existing_objects = existing_objects_1 + existing_objects_2
|
|
564
|
+
|
|
565
|
+
add_objects = []
|
|
566
|
+
remove_assocs = []
|
|
567
|
+
|
|
568
|
+
for obj, assoc in zip(existing_objects, existing_associations):
|
|
569
|
+
if obj not in expected_objects:
|
|
570
|
+
remove_assocs.append(assoc)
|
|
571
|
+
for obj in expected_objects:
|
|
572
|
+
if obj not in existing_objects:
|
|
573
|
+
add_objects.append(obj)
|
|
574
|
+
|
|
575
|
+
for add_object in add_objects:
|
|
576
|
+
if "request" in self.context and not self.context["request"].user.has_perm(
|
|
577
|
+
"extras.add_relationshipassociation"
|
|
578
|
+
):
|
|
579
|
+
raise PermissionDenied("This user does not have permission to create RelationshipAssociations.")
|
|
580
|
+
if other_side != RelationshipSideChoices.SIDE_SOURCE:
|
|
581
|
+
assoc = RelationshipAssociation(
|
|
582
|
+
relationship=relationship,
|
|
583
|
+
source_type=relationship.source_type,
|
|
584
|
+
source_id=instance.id,
|
|
585
|
+
destination_type=relationship.destination_type,
|
|
586
|
+
destination_id=add_object.id,
|
|
587
|
+
)
|
|
588
|
+
else:
|
|
589
|
+
assoc = RelationshipAssociation(
|
|
590
|
+
relationship=relationship,
|
|
591
|
+
source_type=relationship.source_type,
|
|
592
|
+
source_id=add_object.id,
|
|
593
|
+
destination_type=relationship.destination_type,
|
|
594
|
+
destination_id=instance.id,
|
|
595
|
+
)
|
|
596
|
+
assoc.validated_save() # enforce relationship filter logic, etc.
|
|
597
|
+
logger.debug("Created %s", assoc)
|
|
598
|
+
|
|
599
|
+
for remove_assoc in remove_assocs:
|
|
600
|
+
if "request" in self.context and not self.context["request"].user.has_perm(
|
|
601
|
+
"extras.delete_relationshipassociation"
|
|
602
|
+
):
|
|
603
|
+
raise PermissionDenied("This user does not have permission to delete RelationshipAssociations.")
|
|
604
|
+
logger.debug("Deleting %s", remove_assoc)
|
|
605
|
+
remove_assoc.delete()
|
|
606
|
+
|
|
607
|
+
def get_field_names(self, declared_fields, info):
|
|
608
|
+
"""Ensure that "relationships" is included as an opt-in field on root serializers."""
|
|
609
|
+
fields = list(super().get_field_names(declared_fields, info))
|
|
610
|
+
if not self.is_nested:
|
|
611
|
+
# Only include relationships as opt-in.
|
|
612
|
+
self.extend_field_names(fields, "relationships", opt_in_only=True)
|
|
613
|
+
else:
|
|
614
|
+
# As relationships are expensive, do not include them on nested serializers even if opted in.
|
|
615
|
+
if "relationships" in fields:
|
|
616
|
+
fields.remove("relationships")
|
|
617
|
+
return fields
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
class NotesSerializerMixin(BaseModelSerializer):
|
|
621
|
+
"""Extend Serializer with a `notes` field."""
|
|
622
|
+
|
|
623
|
+
notes_url = serializers.SerializerMethodField()
|
|
624
|
+
|
|
625
|
+
def get_field_names(self, declared_fields, info):
|
|
626
|
+
"""Ensure that fields includes "notes_url" field if applicable."""
|
|
627
|
+
fields = list(super().get_field_names(declared_fields, info))
|
|
628
|
+
if hasattr(self.Meta.model, "notes"):
|
|
629
|
+
# Make sure the field is at the end of fields, instead of the beginning
|
|
630
|
+
self.extend_field_names(fields, "notes_url")
|
|
631
|
+
return fields
|
|
632
|
+
|
|
633
|
+
@extend_schema_field(serializers.URLField())
|
|
634
|
+
def get_notes_url(self, instance):
|
|
635
|
+
try:
|
|
636
|
+
notes_url = get_route_for_model(instance, "notes", api=True)
|
|
637
|
+
return reverse(notes_url, args=[instance.id], request=self.context["request"])
|
|
638
|
+
except NoReverseMatch:
|
|
639
|
+
model_name = type(instance).__name__
|
|
640
|
+
logger.warning(
|
|
641
|
+
(
|
|
642
|
+
f"Notes feature is not available for model {model_name}. "
|
|
643
|
+
"Please make sure to: "
|
|
644
|
+
f"1. Include NotesMixin from nautobot.extras.model.mixins in the {model_name} class definition "
|
|
645
|
+
f"2. Include NotesViewSetMixin from nautobot.extras.api.views in the {model_name}ViewSet "
|
|
646
|
+
"before including NotesSerializerMixin in the model serializer"
|
|
647
|
+
)
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
return None
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
class NautobotModelSerializer(
|
|
654
|
+
RelationshipModelSerializerMixin, CustomFieldModelSerializerMixin, NotesSerializerMixin, ValidatedModelSerializer
|
|
655
|
+
):
|
|
656
|
+
"""Base class to use for serializers based on OrganizationalModel or PrimaryModel.
|
|
657
|
+
|
|
658
|
+
Can also be used for models derived from BaseModel, so long as they support custom fields, notes, and relationships.
|
|
659
|
+
"""
|
nautobot/core/api/urls.py
CHANGED
|
@@ -10,6 +10,8 @@ from nautobot.core.api.views import (
|
|
|
10
10
|
APIRootView,
|
|
11
11
|
GetFilterSetFieldDOMElementAPIView,
|
|
12
12
|
GetFilterSetFieldLookupExpressionChoicesAPIView,
|
|
13
|
+
GetMenuAPIView,
|
|
14
|
+
GetObjectCountsView,
|
|
13
15
|
GraphQLDRFAPIView,
|
|
14
16
|
StatusView,
|
|
15
17
|
NautobotSpectacularSwaggerView,
|
|
@@ -17,9 +19,7 @@ from nautobot.core.api.views import (
|
|
|
17
19
|
)
|
|
18
20
|
from nautobot.extras.plugins.urls import plugin_api_patterns
|
|
19
21
|
|
|
20
|
-
|
|
21
22
|
core_api_patterns = [
|
|
22
|
-
# Lookup Expr
|
|
23
23
|
path(
|
|
24
24
|
"filterset-fields/lookup-choices/",
|
|
25
25
|
GetFilterSetFieldLookupExpressionChoicesAPIView.as_view(),
|
|
@@ -31,6 +31,12 @@ core_api_patterns = [
|
|
|
31
31
|
name="filtersetfield-retrieve-lookupvaluedomelement",
|
|
32
32
|
),
|
|
33
33
|
]
|
|
34
|
+
ui_api_patterns = [
|
|
35
|
+
# Lookup Expr
|
|
36
|
+
path("core/", include((core_api_patterns, "core-api"))),
|
|
37
|
+
path("get-menu/", GetMenuAPIView.as_view(), name="get-menu"),
|
|
38
|
+
path("get-object-counts/", GetObjectCountsView.as_view(), name="get-object-counts"),
|
|
39
|
+
]
|
|
34
40
|
|
|
35
41
|
urlpatterns = [
|
|
36
42
|
# Base views
|
|
@@ -52,6 +58,8 @@ urlpatterns = [
|
|
|
52
58
|
path("graphql/", GraphQLDRFAPIView.as_view(), name="graphql-api"),
|
|
53
59
|
# Plugins
|
|
54
60
|
path("plugins/", include((plugin_api_patterns, "plugins-api"))),
|
|
55
|
-
# Core
|
|
61
|
+
# Core, keeping for backwards compatibility of the legacy UI (Dynamic Filter Form)
|
|
56
62
|
path("core/", include((core_api_patterns, "core-api"))),
|
|
63
|
+
# UI
|
|
64
|
+
path("ui/", include((ui_api_patterns, "ui-api"))),
|
|
57
65
|
]
|