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.
Files changed (780) hide show
  1. nautobot/apps/api.py +6 -8
  2. nautobot/apps/forms.py +0 -2
  3. nautobot/apps/ui.py +0 -8
  4. nautobot/circuits/api/serializers.py +9 -117
  5. nautobot/circuits/api/urls.py +1 -1
  6. nautobot/circuits/api/views.py +0 -1
  7. nautobot/circuits/forms.py +0 -65
  8. nautobot/circuits/migrations/0014_related_name_changes.py +1 -1
  9. nautobot/circuits/migrations/0016_tagsfield.py +34 -0
  10. nautobot/circuits/migrations/0017_fixup_null_statuses.py +22 -0
  11. nautobot/circuits/migrations/0018_status_nonnullable.py +22 -0
  12. nautobot/circuits/models.py +3 -87
  13. nautobot/circuits/navigation.py +14 -69
  14. nautobot/circuits/signals.py +0 -2
  15. nautobot/circuits/tables.py +39 -1
  16. nautobot/circuits/tests/integration/test_relationships.py +9 -9
  17. nautobot/circuits/tests/test_api.py +4 -8
  18. nautobot/circuits/tests/test_filters.py +10 -4
  19. nautobot/circuits/tests/test_models.py +5 -1
  20. nautobot/circuits/tests/test_views.py +27 -5
  21. nautobot/circuits/views.py +18 -10
  22. nautobot/core/api/__init__.py +8 -2
  23. nautobot/core/api/fields.py +15 -6
  24. nautobot/core/api/filter_backends.py +3 -2
  25. nautobot/core/api/metadata.py +237 -30
  26. nautobot/core/api/mixins.py +94 -0
  27. nautobot/core/api/pagination.py +4 -0
  28. nautobot/core/api/parsers.py +154 -0
  29. nautobot/core/api/renderers.py +153 -2
  30. nautobot/core/api/schema.py +46 -2
  31. nautobot/core/api/serializers.py +377 -35
  32. nautobot/core/api/urls.py +11 -3
  33. nautobot/core/api/utils.py +174 -2
  34. nautobot/core/api/versioning.py +32 -10
  35. nautobot/core/api/views.py +266 -72
  36. nautobot/core/apps/__init__.py +138 -220
  37. nautobot/core/celery/__init__.py +112 -41
  38. nautobot/core/celery/backends.py +19 -12
  39. nautobot/core/celery/control.py +46 -0
  40. nautobot/core/celery/encoders.py +53 -0
  41. nautobot/core/celery/log.py +38 -0
  42. nautobot/core/celery/schedulers.py +23 -4
  43. nautobot/core/celery/task.py +1 -16
  44. nautobot/core/checks.py +0 -27
  45. nautobot/core/choices.py +0 -113
  46. nautobot/core/{cli.py → cli/__init__.py} +1 -1
  47. nautobot/core/cli/__main__.py +3 -0
  48. nautobot/core/constants.py +0 -24
  49. nautobot/core/context_processors.py +12 -0
  50. nautobot/core/filters.py +2 -2
  51. nautobot/core/forms/__init__.py +0 -4
  52. nautobot/core/forms/fields.py +38 -65
  53. nautobot/core/forms/forms.py +4 -1
  54. nautobot/core/forms/utils.py +0 -52
  55. nautobot/core/graphql/schema.py +4 -27
  56. nautobot/core/jobs/__init__.py +75 -0
  57. nautobot/core/management/commands/build_ui.py +255 -0
  58. nautobot/core/management/commands/generate_test_data.py +3 -2
  59. nautobot/core/management/commands/post_upgrade.py +24 -24
  60. nautobot/core/models/__init__.py +26 -1
  61. nautobot/core/models/fields.py +24 -5
  62. nautobot/core/models/generics.py +2 -42
  63. nautobot/core/models/managers.py +5 -0
  64. nautobot/core/models/name_color_content_types.py +0 -14
  65. nautobot/core/models/tree_queries.py +14 -4
  66. nautobot/core/models/utils.py +5 -6
  67. nautobot/core/models/validators.py +17 -8
  68. nautobot/core/releases.py +8 -10
  69. nautobot/core/settings.py +80 -42
  70. nautobot/core/tables.py +5 -5
  71. nautobot/core/tasks.py +4 -7
  72. nautobot/core/templates/base.html +1 -49
  73. nautobot/core/templates/base_django.html +49 -0
  74. nautobot/core/templates/base_react.html +55 -0
  75. nautobot/core/templates/buttons/export.html +6 -4
  76. nautobot/core/templates/generic/object_bulk_create.html +10 -21
  77. nautobot/core/templates/generic/object_list.html +3 -1
  78. nautobot/core/templates/generic/object_retrieve_plugin_full_width.html +3 -0
  79. nautobot/core/templates/inc/footer.html +1 -0
  80. nautobot/core/templates/inc/javascript.html +0 -14
  81. nautobot/core/templates/inc/nav_menu.html +28 -33
  82. nautobot/core/templates/inc/object_details_advanced_panel.html +13 -0
  83. nautobot/core/templates/inc/relationships_table_rows.html +2 -2
  84. nautobot/core/templates/nautobot_config.py.j2 +8 -20
  85. nautobot/core/templates/plugin_template/__init__.py-tpl +1 -2
  86. nautobot/core/templates/rest_framework/api.html +8 -0
  87. nautobot/core/templatetags/buttons.py +32 -28
  88. nautobot/core/testing/__init__.py +47 -44
  89. nautobot/core/testing/api.py +362 -47
  90. nautobot/core/testing/filters.py +1 -1
  91. nautobot/core/testing/migrations.py +2 -0
  92. nautobot/core/testing/mixins.py +22 -9
  93. nautobot/core/testing/schema.py +2 -1
  94. nautobot/core/testing/views.py +21 -46
  95. nautobot/core/tests/integration/test_filters.py +17 -8
  96. nautobot/core/tests/integration/test_navbar.py +11 -34
  97. nautobot/core/tests/integration/test_plugin_navbar.py +9 -103
  98. nautobot/core/tests/nautobot_config.py +2 -3
  99. nautobot/core/tests/test_api.py +290 -21
  100. nautobot/core/tests/test_checks.py +0 -7
  101. nautobot/core/tests/test_filters.py +107 -59
  102. nautobot/core/tests/test_forms.py +26 -92
  103. nautobot/core/tests/test_graphql.py +110 -77
  104. nautobot/core/tests/test_logging.py +4 -0
  105. nautobot/core/tests/test_managers.py +3 -1
  106. nautobot/core/tests/test_models.py +2 -0
  107. nautobot/core/tests/test_paginator.py +3 -1
  108. nautobot/core/tests/test_releases.py +12 -12
  109. nautobot/core/tests/test_templatetags_helpers.py +4 -4
  110. nautobot/core/tests/test_utils.py +32 -68
  111. nautobot/core/tests/test_views.py +12 -15
  112. nautobot/core/utils/data.py +17 -0
  113. nautobot/core/utils/deprecation.py +9 -6
  114. nautobot/core/utils/filtering.py +8 -3
  115. nautobot/core/utils/git.py +12 -4
  116. nautobot/core/utils/lookup.py +3 -1
  117. nautobot/core/utils/requests.py +1 -104
  118. nautobot/core/views/__init__.py +1 -0
  119. nautobot/core/views/generic.py +75 -110
  120. nautobot/core/views/mixins.py +52 -61
  121. nautobot/core/views/renderers.py +6 -7
  122. nautobot/core/views/utils.py +80 -0
  123. nautobot/dcim/api/serializers.py +160 -667
  124. nautobot/dcim/api/urls.py +1 -1
  125. nautobot/dcim/api/views.py +7 -44
  126. nautobot/dcim/choices.py +2 -0
  127. nautobot/dcim/filters/__init__.py +21 -0
  128. nautobot/dcim/form_mixins.py +1 -27
  129. nautobot/dcim/forms.py +19 -765
  130. nautobot/dcim/migrations/0024_alter_device_and_rack_role_add_new_role.py +2 -1
  131. nautobot/dcim/migrations/0025_device_and_rack_roles_data_migrations.py +19 -13
  132. nautobot/dcim/migrations/0027_remove_device_role_and_rack_role.py +1 -1
  133. nautobot/dcim/migrations/0028_rename_foreignkey_fields.py +1 -1
  134. nautobot/dcim/migrations/0030_migrate_region_and_site_data_to_locations.py +2 -2
  135. nautobot/dcim/migrations/0035_related_name_changes.py +1 -1
  136. nautobot/dcim/migrations/0036_remove_region_and_site.py +1 -1
  137. nautobot/dcim/migrations/0040_tagsfield.py +109 -0
  138. nautobot/dcim/migrations/{0040_ipam__namespaces.py → 0041_ipam__namespaces.py} +1 -1
  139. nautobot/dcim/migrations/0042_fixup_null_statuses.py +51 -0
  140. nautobot/dcim/migrations/0043_status_nonnullable.py +72 -0
  141. nautobot/dcim/models/cables.py +3 -33
  142. nautobot/dcim/models/device_component_templates.py +6 -0
  143. nautobot/dcim/models/device_components.py +12 -198
  144. nautobot/dcim/models/devices.py +30 -143
  145. nautobot/dcim/models/locations.py +3 -64
  146. nautobot/dcim/models/power.py +3 -50
  147. nautobot/dcim/models/racks.py +7 -84
  148. nautobot/dcim/navigation.py +141 -467
  149. nautobot/dcim/signals.py +0 -2
  150. nautobot/dcim/tables/locations.py +2 -2
  151. nautobot/dcim/tables/power.py +1 -2
  152. nautobot/dcim/templates/dcim/console_port_connection_list.html +7 -0
  153. nautobot/dcim/templates/dcim/devicetype.html +2 -2
  154. nautobot/dcim/templates/dcim/interface_connection_list.html +7 -0
  155. nautobot/dcim/templates/dcim/location.html +16 -1
  156. nautobot/dcim/templates/dcim/locationtype.html +15 -0
  157. nautobot/dcim/templates/dcim/power_port_connection_list.html +7 -0
  158. nautobot/dcim/templates/dcim/rackgroup.html +0 -12
  159. nautobot/dcim/tests/test_api.py +166 -81
  160. nautobot/dcim/tests/test_cablepaths.py +41 -35
  161. nautobot/dcim/tests/test_filters.py +67 -23
  162. nautobot/dcim/tests/test_forms.py +5 -205
  163. nautobot/dcim/tests/test_graphql.py +7 -2
  164. nautobot/dcim/tests/test_migrations.py +6 -11
  165. nautobot/dcim/tests/test_models.py +182 -110
  166. nautobot/dcim/tests/test_natural_ordering.py +11 -8
  167. nautobot/dcim/tests/test_signals.py +6 -3
  168. nautobot/dcim/tests/test_views.py +197 -175
  169. nautobot/dcim/urls.py +11 -16
  170. nautobot/dcim/views.py +7 -134
  171. nautobot/docs/additional-features/caching.md +6 -87
  172. nautobot/docs/additional-features/job-scheduling-and-approvals.md +3 -0
  173. nautobot/docs/additional-features/jobs.md +177 -195
  174. nautobot/docs/administration/nautobot-server.md +6 -21
  175. nautobot/docs/administration/replicating-nautobot.md +0 -10
  176. nautobot/docs/configuration/optional-settings.md +32 -41
  177. nautobot/docs/configuration/required-settings.md +11 -52
  178. nautobot/docs/development/application-registry.md +2 -13
  179. nautobot/docs/development/extending-models.md +15 -17
  180. nautobot/docs/development/generic-views.md +0 -2
  181. nautobot/docs/development/getting-started.md +55 -5
  182. nautobot/docs/development/navigation-menu.md +22 -93
  183. nautobot/docs/development/react-ui.md +105 -0
  184. nautobot/docs/development/role-internals.md +1 -3
  185. nautobot/docs/development/style-guide.md +6 -4
  186. nautobot/docs/index.md +3 -2
  187. nautobot/docs/installation/migrating-from-netbox.md +11 -42
  188. nautobot/docs/installation/nautobot.md +1 -1
  189. nautobot/docs/installation/tables/v2-api-behavior-changes.yaml +70 -0
  190. nautobot/docs/installation/tables/v2-api-removed-fields.yaml +142 -0
  191. nautobot/docs/installation/tables/v2-api-renamed-fields.yaml +124 -0
  192. nautobot/docs/installation/tables/v2-code-location-changes.yaml +241 -0
  193. nautobot/docs/installation/tables/v2-code-removals.yaml +67 -0
  194. nautobot/docs/installation/tables/v2-database-behavior-changes.yaml +37 -0
  195. nautobot/docs/installation/tables/v2-database-removed-fields.yaml +166 -0
  196. nautobot/docs/installation/tables/v2-database-renamed-fields.yaml +340 -0
  197. nautobot/docs/installation/tables/v2-filters-corrected-fields.yaml +28 -0
  198. nautobot/docs/installation/tables/v2-filters-enhanced-fields.yaml +241 -0
  199. nautobot/docs/installation/tables/v2-filters-removed-fields.yaml +553 -0
  200. nautobot/docs/installation/tables/v2-filters-renamed-fields.yaml +223 -0
  201. nautobot/docs/installation/tables/v2-logging-renamed-loggers.yaml +23 -0
  202. nautobot/docs/installation/upgrading-from-nautobot-v1.md +170 -747
  203. nautobot/docs/models/dcim/device.md +3 -0
  204. nautobot/docs/models/dcim/deviceredundancygroup.md +3 -3
  205. nautobot/docs/models/extras/computedfield.md +4 -4
  206. nautobot/docs/models/extras/gitrepository.md +3 -0
  207. nautobot/docs/models/extras/job.md +1 -0
  208. nautobot/docs/models/extras/jobbutton.md +18 -13
  209. nautobot/docs/models/extras/jobhook.md +7 -4
  210. nautobot/docs/models/extras/jobresult.md +6 -2
  211. nautobot/docs/models/extras/relationship.md +2 -2
  212. nautobot/docs/models/extras/status.md +6 -19
  213. nautobot/docs/models/ipam/ipaddress.md +3 -0
  214. nautobot/docs/models/virtualization/virtualmachine.md +3 -0
  215. nautobot/docs/plugins/development.md +83 -21
  216. nautobot/docs/release-notes/version-1.5.md +53 -0
  217. nautobot/docs/release-notes/version-2.0.md +180 -0
  218. nautobot/docs/requirements.txt +1 -0
  219. nautobot/docs/rest-api/overview.md +384 -215
  220. nautobot/docs/rest-api/ui-related-endpoints.md +9 -0
  221. nautobot/extras/admin.py +3 -5
  222. nautobot/extras/api/customfields.py +15 -39
  223. nautobot/extras/api/fields.py +0 -11
  224. nautobot/extras/api/mixins.py +45 -0
  225. nautobot/extras/api/relationships.py +63 -158
  226. nautobot/extras/api/serializers.py +165 -700
  227. nautobot/extras/api/urls.py +1 -1
  228. nautobot/extras/api/views.py +294 -280
  229. nautobot/extras/apps.py +4 -7
  230. nautobot/extras/choices.py +11 -9
  231. nautobot/extras/constants.py +9 -3
  232. nautobot/extras/datasources/__init__.py +2 -0
  233. nautobot/extras/datasources/git.py +135 -186
  234. nautobot/extras/datasources/registry.py +25 -35
  235. nautobot/extras/filters/__init__.py +20 -19
  236. nautobot/extras/filters/mixins.py +4 -4
  237. nautobot/extras/forms/forms.py +63 -127
  238. nautobot/extras/forms/mixins.py +23 -51
  239. nautobot/extras/health_checks.py +0 -33
  240. nautobot/extras/jobs.py +387 -565
  241. nautobot/extras/management/commands/runjob.py +24 -62
  242. nautobot/extras/managers.py +30 -7
  243. nautobot/extras/migrations/0058_jobresult_add_time_status_idxs.py +38 -0
  244. nautobot/extras/migrations/{0058_joblogentry_scheduledjob_webhook_data_migration.py → 0059_joblogentry_scheduledjob_webhook_data_migration.py} +1 -1
  245. nautobot/extras/migrations/{0059_alter_joblogentry_scheduledjob_webhook_fields.py → 0060_alter_joblogentry_scheduledjob_webhook_fields.py} +1 -1
  246. nautobot/extras/migrations/{0060_role_and_alter_status.py → 0061_role_and_alter_status.py} +1 -7
  247. nautobot/extras/migrations/{0061_collect_roles_from_related_apps_roles.py → 0062_collect_roles_from_related_apps_roles.py} +33 -32
  248. nautobot/extras/migrations/{0062_alter_role_options.py → 0063_alter_role_options.py} +1 -1
  249. nautobot/extras/migrations/{0063_alter_configcontext_and_add_new_role.py → 0064_alter_configcontext_and_add_new_role.py} +1 -1
  250. nautobot/extras/migrations/0065_configcontext_data_migrations.py +44 -0
  251. nautobot/extras/migrations/{0065_rename_configcontext_role.py → 0066_rename_configcontext_role.py} +1 -1
  252. nautobot/extras/migrations/{0066_jobresult__add_celery_fields.py → 0067_jobresult__add_celery_fields.py} +36 -2
  253. nautobot/extras/migrations/{0067_created_datetime.py → 0068_created_datetime.py} +1 -1
  254. 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
  255. nautobot/extras/migrations/{0069_replace_related_names.py → 0070_replace_related_names.py} +1 -1
  256. nautobot/extras/migrations/{0070_rename_model_fields.py → 0071_rename_model_fields.py} +1 -1
  257. nautobot/extras/migrations/0072_job__unique_name_data_migration.py +86 -0
  258. nautobot/extras/migrations/{0072_job__unique_name.py → 0073_job__unique_name.py} +13 -9
  259. nautobot/extras/migrations/{0073_remove_gitrepository_fields.py → 0074_remove_gitrepository_fields.py} +1 -1
  260. nautobot/extras/migrations/{0074_rename_slug_to_key_for_custom_field.py → 0075_rename_slug_to_key_for_custom_field.py} +1 -1
  261. nautobot/extras/migrations/{0075_migrate_custom_field_data.py → 0076_migrate_custom_field_data.py} +1 -1
  262. 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
  263. nautobot/extras/migrations/{0077_remove_slug.py → 0078_remove_slug.py} +1 -5
  264. nautobot/extras/migrations/0079_tagsfield.py +28 -0
  265. nautobot/extras/migrations/0080_rename_relationship_slug_to_key.py +17 -0
  266. nautobot/extras/migrations/0081_rename_relationship_name_to_label.py +29 -0
  267. nautobot/extras/migrations/0082_ensure_relationship_keys_are_unique.py +43 -0
  268. nautobot/extras/migrations/0083_rename_computed_field_slug_to_key.py +21 -0
  269. nautobot/extras/migrations/0084_taggeditem_cleanup.py +43 -0
  270. nautobot/extras/migrations/0085_taggeditem_uniqueness.py +22 -0
  271. nautobot/extras/migrations/0086_job__celery_task_fields__dryrun_support.py +81 -0
  272. nautobot/extras/migrations/0087_job__commit_default_data_migration.py +26 -0
  273. nautobot/extras/migrations/0088_joblogentry__log_level_default.py +17 -0
  274. nautobot/extras/migrations/0089_joblogentry__log_level_data_migration.py +34 -0
  275. nautobot/extras/migrations/0090_scheduledjob__data_migration.py +57 -0
  276. nautobot/extras/models/__init__.py +2 -3
  277. nautobot/extras/models/change_logging.py +0 -36
  278. nautobot/extras/models/customfields.py +39 -33
  279. nautobot/extras/models/datasources.py +48 -50
  280. nautobot/extras/models/groups.py +5 -6
  281. nautobot/extras/models/jobs.py +189 -321
  282. nautobot/extras/models/mixins.py +0 -71
  283. nautobot/extras/models/models.py +0 -19
  284. nautobot/extras/models/relationships.py +19 -13
  285. nautobot/extras/models/roles.py +0 -34
  286. nautobot/extras/models/secrets.py +2 -26
  287. nautobot/extras/models/statuses.py +6 -5
  288. nautobot/extras/models/tags.py +2 -17
  289. nautobot/extras/navigation.py +89 -307
  290. nautobot/extras/plugins/__init__.py +3 -120
  291. nautobot/extras/plugins/utils.py +0 -3
  292. nautobot/extras/plugins/validators.py +5 -4
  293. nautobot/extras/plugins/views.py +16 -3
  294. nautobot/extras/querysets.py +1 -7
  295. nautobot/extras/registry.py +3 -0
  296. nautobot/extras/signals.py +26 -60
  297. nautobot/extras/tables.py +34 -40
  298. nautobot/extras/tasks.py +0 -12
  299. nautobot/extras/templates/extras/configcontext.html +1 -1
  300. nautobot/extras/templates/extras/configcontextschema.html +16 -1
  301. nautobot/extras/templates/extras/customfield.html +0 -13
  302. nautobot/extras/templates/extras/gitrepository.html +3 -3
  303. nautobot/extras/templates/extras/inc/jobresult.html +10 -0
  304. nautobot/extras/templates/extras/inc/panel_jobhistory.html +1 -1
  305. nautobot/extras/templates/extras/job.html +35 -25
  306. nautobot/extras/templates/extras/job_approval_request.html +15 -30
  307. nautobot/extras/templates/extras/job_detail.html +13 -31
  308. nautobot/extras/templates/extras/job_edit.html +15 -17
  309. nautobot/extras/templates/extras/jobresult.html +24 -6
  310. nautobot/extras/templates/extras/scheduledjob.html +2 -2
  311. nautobot/extras/templates/extras/secret.html +28 -0
  312. nautobot/extras/templatetags/job_buttons.py +1 -0
  313. nautobot/extras/{tests/example_jobs → test_jobs}/api_test_job.py +13 -6
  314. nautobot/extras/test_jobs/atomic_transaction.py +53 -0
  315. nautobot/extras/test_jobs/dry_run.py +29 -0
  316. nautobot/extras/{tests/example_jobs/test_duplicate_name.py → test_jobs/duplicate_name.py} +4 -0
  317. nautobot/extras/test_jobs/duplicate_name2.py +9 -0
  318. nautobot/extras/test_jobs/fail.py +23 -0
  319. nautobot/extras/{tests/example_jobs/test_field_default.py → test_jobs/field_default.py} +4 -0
  320. nautobot/extras/{tests/example_jobs/test_field_order.py → test_jobs/field_order.py} +4 -0
  321. nautobot/extras/{tests/example_jobs/test_file_upload_fail.py → test_jobs/file_upload_fail.py} +11 -6
  322. nautobot/extras/test_jobs/file_upload_pass.py +25 -0
  323. nautobot/extras/test_jobs/has_sensitive_variables.py +25 -0
  324. nautobot/extras/test_jobs/ipaddress_vars.py +66 -0
  325. nautobot/extras/test_jobs/job_button_receiver.py +28 -0
  326. nautobot/extras/test_jobs/job_hook_receiver.py +29 -0
  327. nautobot/extras/test_jobs/job_variables.py +88 -0
  328. nautobot/extras/test_jobs/location_with_custom_field.py +45 -0
  329. nautobot/extras/test_jobs/log_redaction.py +20 -0
  330. nautobot/extras/test_jobs/log_skip_db_logging.py +17 -0
  331. nautobot/extras/test_jobs/modify_db.py +25 -0
  332. nautobot/extras/{tests/example_jobs/test_no_field_order.py → test_jobs/no_field_order.py} +4 -0
  333. nautobot/extras/test_jobs/object_var_optional.py +21 -0
  334. nautobot/extras/test_jobs/object_var_required.py +21 -0
  335. nautobot/extras/test_jobs/object_vars.py +26 -0
  336. nautobot/extras/test_jobs/pass.py +25 -0
  337. nautobot/extras/test_jobs/profiling.py +32 -0
  338. nautobot/extras/test_jobs/read_only_job.py +15 -0
  339. nautobot/extras/{tests/example_jobs/test_required_args.py → test_jobs/required_args.py} +4 -0
  340. 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
  341. nautobot/extras/{tests/example_jobs/test_task_queues.py → test_jobs/task_queues.py} +5 -1
  342. nautobot/extras/tests/integration/test_computedfields.py +1 -1
  343. nautobot/extras/tests/integration/test_configcontextschema.py +5 -3
  344. nautobot/extras/tests/integration/test_customfields.py +4 -2
  345. nautobot/extras/tests/integration/test_dynamicgroups.py +1 -1
  346. nautobot/extras/tests/integration/test_jobs.py +25 -27
  347. nautobot/extras/tests/integration/test_notes.py +8 -4
  348. nautobot/extras/tests/integration/test_relationships.py +2 -2
  349. nautobot/extras/tests/test_api.py +649 -642
  350. nautobot/extras/tests/test_changelog.py +3 -3
  351. nautobot/extras/tests/test_context_managers.py +5 -3
  352. nautobot/extras/tests/test_customfields.py +92 -50
  353. nautobot/extras/tests/test_datasources.py +189 -112
  354. nautobot/extras/tests/test_dynamicgroups.py +7 -8
  355. nautobot/extras/tests/test_filters.py +137 -89
  356. nautobot/extras/tests/test_forms.py +73 -75
  357. nautobot/extras/tests/{test_scripts.py → test_job_variables.py} +43 -49
  358. nautobot/extras/tests/test_jobs.py +262 -263
  359. nautobot/extras/tests/test_migrations.py +4 -3
  360. nautobot/extras/tests/test_models.py +116 -161
  361. nautobot/extras/tests/test_plugins.py +38 -60
  362. nautobot/extras/tests/test_relationships.py +167 -120
  363. nautobot/extras/tests/test_tags.py +6 -11
  364. nautobot/extras/tests/test_utils.py +31 -1
  365. nautobot/extras/tests/test_views.py +201 -145
  366. nautobot/extras/tests/test_webhooks.py +6 -2
  367. nautobot/extras/urls.py +42 -42
  368. nautobot/extras/utils.py +137 -163
  369. nautobot/extras/views.py +78 -152
  370. nautobot/ipam/api/fields.py +17 -0
  371. nautobot/ipam/api/serializers.py +58 -164
  372. nautobot/ipam/api/urls.py +1 -1
  373. nautobot/ipam/api/views.py +3 -2
  374. nautobot/ipam/apps.py +1 -2
  375. nautobot/ipam/filters.py +1 -10
  376. nautobot/ipam/forms.py +4 -177
  377. nautobot/ipam/lookups.py +1 -0
  378. nautobot/ipam/management/commands/__init__.py +0 -0
  379. nautobot/ipam/management/commands/fix_prefix_broadcast.py +17 -0
  380. nautobot/ipam/migrations/0010_alter_ipam_role_add_new_role.py +1 -1
  381. nautobot/ipam/migrations/0011_migrate_ipam_role_data.py +32 -38
  382. nautobot/ipam/migrations/0020_related_name_changes.py +1 -1
  383. nautobot/ipam/migrations/0022_aggregate_to_prefix_data_migration.py +2 -2
  384. nautobot/ipam/migrations/0028_tagsfield.py +44 -0
  385. nautobot/ipam/migrations/0029_ip_address_to_interface_uniqueness_constraints.py +18 -0
  386. nautobot/ipam/migrations/{0028_ipam__namespaces.py → 0030_ipam__namespaces.py} +77 -28
  387. nautobot/ipam/migrations/0031_ipam__prefix__add_parent.py +58 -0
  388. nautobot/ipam/migrations/0032_ipam__namespaces_finish.py +63 -0
  389. nautobot/ipam/migrations/0033_fixup_null_statuses.py +26 -0
  390. nautobot/ipam/migrations/0034_status_nonnullable.py +36 -0
  391. nautobot/ipam/models.py +100 -236
  392. nautobot/ipam/navigation.py +36 -181
  393. nautobot/ipam/querysets.py +20 -25
  394. nautobot/ipam/signals.py +49 -6
  395. nautobot/ipam/tables.py +10 -3
  396. nautobot/ipam/templates/ipam/namespace_ipaddresses.html +11 -0
  397. nautobot/ipam/templates/ipam/namespace_prefixes.html +11 -0
  398. nautobot/ipam/templates/ipam/namespace_retrieve.html +17 -4
  399. nautobot/ipam/templates/ipam/namespace_vrfs.html +11 -0
  400. nautobot/ipam/templates/ipam/prefix.html +1 -1
  401. nautobot/ipam/templates/ipam/vlangroup.html +0 -13
  402. nautobot/ipam/templates/ipam/vrf_edit.html +6 -0
  403. nautobot/ipam/tests/integration/test_prefixes.py +3 -26
  404. nautobot/ipam/tests/test_api.py +22 -19
  405. nautobot/ipam/tests/test_filters.py +59 -23
  406. nautobot/ipam/tests/test_migrations.py +6 -10
  407. nautobot/ipam/tests/test_models.py +323 -198
  408. nautobot/ipam/tests/test_ordering.py +2 -2
  409. nautobot/ipam/tests/test_querysets.py +44 -24
  410. nautobot/ipam/tests/test_views.py +73 -26
  411. nautobot/ipam/urls.py +16 -0
  412. nautobot/ipam/{utils.py → utils/__init__.py} +2 -2
  413. nautobot/ipam/utils/migrations.py +713 -0
  414. nautobot/ipam/views.py +137 -20
  415. nautobot/project-static/docs/404.html +1178 -10
  416. nautobot/project-static/docs/additional-features/caching.html +1224 -159
  417. nautobot/project-static/docs/additional-features/change-logging.html +1180 -12
  418. nautobot/project-static/docs/additional-features/config-contexts.html +1180 -12
  419. nautobot/project-static/docs/additional-features/graphql.html +1179 -11
  420. nautobot/project-static/docs/additional-features/healthcheck.html +1180 -12
  421. nautobot/project-static/docs/additional-features/job-scheduling-and-approvals.html +1184 -12
  422. nautobot/project-static/docs/additional-features/jobs.html +1514 -328
  423. nautobot/project-static/docs/additional-features/napalm.html +1180 -12
  424. nautobot/project-static/docs/additional-features/prometheus-metrics.html +1180 -12
  425. nautobot/project-static/docs/additional-features/template-filters.html +1180 -12
  426. nautobot/project-static/docs/administration/celery-queues.html +1178 -10
  427. nautobot/project-static/docs/administration/nautobot-server.html +1451 -304
  428. nautobot/project-static/docs/administration/nautobot-shell.html +1178 -10
  429. nautobot/project-static/docs/administration/permissions.html +1178 -10
  430. nautobot/project-static/docs/administration/replicating-nautobot.html +1262 -113
  431. nautobot/project-static/docs/apps/index.html +1178 -10
  432. nautobot/project-static/docs/apps/nautobot-apps.html +1178 -10
  433. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +1580 -426
  434. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +1178 -10
  435. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +3481 -1838
  436. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +1178 -10
  437. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +1178 -10
  438. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +1185 -11
  439. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +1719 -551
  440. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +2062 -930
  441. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +1946 -659
  442. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +1180 -12
  443. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +1189 -21
  444. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +9283 -6218
  445. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +2734 -2122
  446. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +1178 -10
  447. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +2337 -1300
  448. nautobot/project-static/docs/configuration/authentication/ldap.html +1178 -10
  449. nautobot/project-static/docs/configuration/authentication/remote.html +1178 -10
  450. nautobot/project-static/docs/configuration/authentication/sso.html +1178 -10
  451. nautobot/project-static/docs/configuration/index.html +1178 -10
  452. nautobot/project-static/docs/configuration/optional-settings.html +1311 -160
  453. nautobot/project-static/docs/configuration/required-settings.html +1312 -211
  454. nautobot/project-static/docs/core-functionality/circuits.html +1178 -10
  455. nautobot/project-static/docs/core-functionality/device-types.html +1178 -10
  456. nautobot/project-static/docs/core-functionality/devices.html +1182 -10
  457. nautobot/project-static/docs/core-functionality/ipam.html +1182 -10
  458. nautobot/project-static/docs/core-functionality/power.html +1178 -10
  459. nautobot/project-static/docs/core-functionality/secrets.html +1178 -10
  460. nautobot/project-static/docs/core-functionality/services.html +1178 -10
  461. nautobot/project-static/docs/core-functionality/sites-and-racks.html +1178 -10
  462. nautobot/project-static/docs/core-functionality/tenancy.html +1178 -10
  463. nautobot/project-static/docs/core-functionality/virtualization.html +1182 -10
  464. nautobot/project-static/docs/core-functionality/vlans.html +1179 -11
  465. nautobot/project-static/docs/development/application-registry.html +1190 -42
  466. nautobot/project-static/docs/development/best-practices.html +1178 -10
  467. nautobot/project-static/docs/development/docker-compose-advanced-use-cases.html +1178 -10
  468. nautobot/project-static/docs/development/extending-models.html +1238 -83
  469. nautobot/project-static/docs/development/generic-views.html +1180 -14
  470. nautobot/project-static/docs/development/getting-started.html +1365 -90
  471. nautobot/project-static/docs/development/homepage.html +1178 -10
  472. nautobot/project-static/docs/development/index.html +1178 -10
  473. nautobot/project-static/docs/development/model-features.html +1178 -10
  474. nautobot/project-static/docs/development/natural-keys.html +1178 -10
  475. nautobot/project-static/docs/development/navigation-menu.html +1215 -125
  476. nautobot/project-static/docs/development/react-ui.html +4199 -0
  477. nautobot/project-static/docs/development/release-checklist.html +1178 -10
  478. nautobot/project-static/docs/development/role-internals.html +1179 -12
  479. nautobot/project-static/docs/development/style-guide.html +1188 -19
  480. nautobot/project-static/docs/development/templates.html +1178 -10
  481. nautobot/project-static/docs/development/testing.html +1178 -10
  482. nautobot/project-static/docs/development/user-preferences.html +1178 -10
  483. nautobot/project-static/docs/docker/index.html +1178 -10
  484. nautobot/project-static/docs/index.html +1183 -12
  485. nautobot/project-static/docs/installation/centos.html +1178 -10
  486. nautobot/project-static/docs/installation/external-authentication.html +1178 -10
  487. nautobot/project-static/docs/installation/http-server.html +1178 -10
  488. nautobot/project-static/docs/installation/index.html +1178 -10
  489. nautobot/project-static/docs/installation/migrating-from-netbox.html +1305 -189
  490. nautobot/project-static/docs/installation/migrating-from-postgresql.html +1178 -10
  491. nautobot/project-static/docs/installation/nautobot.html +1179 -11
  492. nautobot/project-static/docs/installation/region-and-site-data-migration-guide.html +1178 -10
  493. nautobot/project-static/docs/installation/selinux-troubleshooting.html +1178 -10
  494. nautobot/project-static/docs/installation/services.html +1178 -10
  495. nautobot/project-static/docs/installation/tables/v2-api-behavior-changes.yaml +70 -0
  496. nautobot/project-static/docs/installation/tables/v2-api-removed-fields.yaml +142 -0
  497. nautobot/project-static/docs/installation/tables/v2-api-renamed-fields.yaml +124 -0
  498. nautobot/project-static/docs/installation/tables/v2-code-location-changes.yaml +241 -0
  499. nautobot/project-static/docs/installation/tables/v2-code-removals.yaml +67 -0
  500. nautobot/project-static/docs/installation/tables/v2-database-behavior-changes.yaml +37 -0
  501. nautobot/project-static/docs/installation/tables/v2-database-removed-fields.yaml +166 -0
  502. nautobot/project-static/docs/installation/tables/v2-database-renamed-fields.yaml +340 -0
  503. nautobot/project-static/docs/installation/tables/v2-filters-corrected-fields.yaml +28 -0
  504. nautobot/project-static/docs/installation/tables/v2-filters-enhanced-fields.yaml +241 -0
  505. nautobot/project-static/docs/installation/tables/v2-filters-removed-fields.yaml +553 -0
  506. nautobot/project-static/docs/installation/tables/v2-filters-renamed-fields.yaml +223 -0
  507. nautobot/project-static/docs/installation/tables/v2-logging-renamed-loggers.yaml +23 -0
  508. nautobot/project-static/docs/installation/ubuntu.html +1178 -10
  509. nautobot/project-static/docs/installation/upgrading-from-nautobot-v1.html +3823 -2152
  510. nautobot/project-static/docs/installation/upgrading.html +1178 -10
  511. nautobot/project-static/docs/models/circuits/circuit.html +1293 -103
  512. nautobot/project-static/docs/models/circuits/circuittermination.html +1293 -103
  513. nautobot/project-static/docs/models/circuits/circuittype.html +1293 -103
  514. nautobot/project-static/docs/models/circuits/provider.html +1293 -103
  515. nautobot/project-static/docs/models/circuits/providernetwork.html +1293 -103
  516. nautobot/project-static/docs/models/dcim/cable.html +1324 -103
  517. nautobot/project-static/docs/models/dcim/consoleport.html +1293 -103
  518. nautobot/project-static/docs/models/dcim/consoleporttemplate.html +1293 -103
  519. nautobot/project-static/docs/models/dcim/consoleserverport.html +1293 -103
  520. nautobot/project-static/docs/models/dcim/consoleserverporttemplate.html +1293 -103
  521. nautobot/project-static/docs/models/dcim/device.html +1326 -132
  522. nautobot/project-static/docs/models/dcim/devicebay.html +1293 -103
  523. nautobot/project-static/docs/models/dcim/devicebaytemplate.html +1293 -103
  524. nautobot/project-static/docs/models/dcim/deviceredundancygroup.html +1379 -97
  525. nautobot/project-static/docs/models/dcim/devicetype.html +1293 -103
  526. nautobot/project-static/docs/models/dcim/frontport.html +1293 -103
  527. nautobot/project-static/docs/models/dcim/frontporttemplate.html +1293 -103
  528. nautobot/project-static/docs/models/dcim/interface.html +1293 -103
  529. nautobot/project-static/docs/models/dcim/interfacetemplate.html +1293 -103
  530. nautobot/project-static/docs/models/dcim/inventoryitem.html +1293 -103
  531. nautobot/project-static/docs/models/dcim/location.html +1293 -103
  532. nautobot/project-static/docs/models/dcim/locationtype.html +1293 -103
  533. nautobot/project-static/docs/models/dcim/manufacturer.html +1292 -102
  534. nautobot/project-static/docs/models/dcim/platform.html +1272 -82
  535. nautobot/project-static/docs/models/dcim/powerfeed.html +1270 -80
  536. nautobot/project-static/docs/models/dcim/poweroutlet.html +1272 -82
  537. nautobot/project-static/docs/models/dcim/poweroutlettemplate.html +1272 -82
  538. nautobot/project-static/docs/models/dcim/powerpanel.html +1270 -80
  539. nautobot/project-static/docs/models/dcim/powerport.html +1272 -82
  540. nautobot/project-static/docs/models/dcim/powerporttemplate.html +1272 -82
  541. nautobot/project-static/docs/models/dcim/rack.html +1272 -82
  542. nautobot/project-static/docs/models/dcim/rackgroup.html +1272 -82
  543. nautobot/project-static/docs/models/dcim/rackreservation.html +1272 -82
  544. nautobot/project-static/docs/models/dcim/rearport.html +1286 -96
  545. nautobot/project-static/docs/models/dcim/rearporttemplate.html +1286 -96
  546. nautobot/project-static/docs/models/dcim/region.html +1178 -10
  547. nautobot/project-static/docs/models/dcim/site.html +1178 -10
  548. nautobot/project-static/docs/models/dcim/virtualchassis.html +1284 -94
  549. nautobot/project-static/docs/models/extras/computedfield.html +1184 -16
  550. nautobot/project-static/docs/models/extras/configcontext.html +1314 -86
  551. nautobot/project-static/docs/models/extras/configcontextschema.html +1276 -86
  552. nautobot/project-static/docs/models/extras/customfield.html +1180 -12
  553. nautobot/project-static/docs/models/extras/customlink.html +1180 -12
  554. nautobot/project-static/docs/models/extras/dynamicgroup.html +1180 -12
  555. nautobot/project-static/docs/models/extras/exporttemplate.html +1180 -12
  556. nautobot/project-static/docs/models/extras/gitrepository.html +1184 -12
  557. nautobot/project-static/docs/models/extras/graphqlquery.html +1321 -86
  558. nautobot/project-static/docs/models/extras/imageattachment.html +1276 -86
  559. nautobot/project-static/docs/models/extras/job.html +1277 -86
  560. nautobot/project-static/docs/models/extras/jobbutton.html +1201 -29
  561. nautobot/project-static/docs/models/extras/jobhook.html +1188 -16
  562. nautobot/project-static/docs/models/extras/joblogentry.html +1274 -84
  563. nautobot/project-static/docs/models/extras/jobresult.html +1364 -169
  564. nautobot/project-static/docs/models/extras/note.html +1180 -12
  565. nautobot/project-static/docs/models/extras/relationship.html +1182 -14
  566. nautobot/project-static/docs/models/extras/role.html +1320 -86
  567. nautobot/project-static/docs/models/extras/secret.html +1314 -86
  568. nautobot/project-static/docs/models/extras/secretsgroup.html +1276 -86
  569. nautobot/project-static/docs/models/extras/status.html +1188 -59
  570. nautobot/project-static/docs/models/extras/tag.html +1180 -12
  571. nautobot/project-static/docs/models/extras/webhook.html +1180 -12
  572. nautobot/project-static/docs/models/ipam/ipaddress.html +1327 -102
  573. nautobot/project-static/docs/models/ipam/prefix.html +1276 -86
  574. nautobot/project-static/docs/models/ipam/rir.html +1276 -86
  575. nautobot/project-static/docs/models/ipam/routetarget.html +1276 -86
  576. nautobot/project-static/docs/models/ipam/service.html +1276 -86
  577. nautobot/project-static/docs/models/ipam/vlan.html +1276 -86
  578. nautobot/project-static/docs/models/ipam/vlangroup.html +1276 -86
  579. nautobot/project-static/docs/models/ipam/vrf.html +1276 -86
  580. nautobot/project-static/docs/models/tenancy/tenant.html +1276 -86
  581. nautobot/project-static/docs/models/tenancy/tenantgroup.html +1276 -86
  582. nautobot/project-static/docs/models/users/objectpermission.html +1314 -86
  583. nautobot/project-static/docs/models/users/token.html +1276 -86
  584. nautobot/project-static/docs/models/virtualization/cluster.html +1276 -86
  585. nautobot/project-static/docs/models/virtualization/clustergroup.html +1276 -86
  586. nautobot/project-static/docs/models/virtualization/clustertype.html +1276 -86
  587. nautobot/project-static/docs/models/virtualization/virtualmachine.html +1321 -127
  588. nautobot/project-static/docs/models/virtualization/vminterface.html +1276 -86
  589. nautobot/project-static/docs/objects.inv +0 -0
  590. nautobot/project-static/docs/plugins/development.html +1726 -495
  591. nautobot/project-static/docs/plugins/index.html +1178 -10
  592. nautobot/project-static/docs/plugins/porting-from-netbox.html +1178 -10
  593. nautobot/project-static/docs/release-notes/index.html +1178 -10
  594. nautobot/project-static/docs/release-notes/version-1.0.html +1178 -10
  595. nautobot/project-static/docs/release-notes/version-1.1.html +1178 -10
  596. nautobot/project-static/docs/release-notes/version-1.2.html +1178 -10
  597. nautobot/project-static/docs/release-notes/version-1.3.html +1178 -10
  598. nautobot/project-static/docs/release-notes/version-1.4.html +1178 -10
  599. nautobot/project-static/docs/release-notes/version-1.5.html +1608 -225
  600. nautobot/project-static/docs/release-notes/version-2.0.html +1547 -47
  601. nautobot/project-static/docs/requirements.txt +1 -0
  602. nautobot/project-static/docs/rest-api/authentication.html +1179 -11
  603. nautobot/project-static/docs/rest-api/filtering.html +1178 -10
  604. nautobot/project-static/docs/rest-api/overview.html +1841 -446
  605. nautobot/project-static/docs/rest-api/ui-related-endpoints.html +4057 -0
  606. nautobot/project-static/docs/search/search_index.json +1 -1
  607. nautobot/project-static/docs/sitemap.xml +197 -187
  608. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  609. nautobot/project-static/docs/user-guides/custom-fields.html +1178 -10
  610. nautobot/project-static/docs/user-guides/getting-started/creating-devices.html +1178 -10
  611. nautobot/project-static/docs/user-guides/getting-started/index.html +1178 -10
  612. nautobot/project-static/docs/user-guides/getting-started/interfaces.html +1178 -10
  613. nautobot/project-static/docs/user-guides/getting-started/ipam.html +1178 -10
  614. nautobot/project-static/docs/user-guides/getting-started/platforms.html +1178 -10
  615. nautobot/project-static/docs/user-guides/getting-started/regions.html +1178 -10
  616. nautobot/project-static/docs/user-guides/getting-started/search-bar.html +1178 -10
  617. nautobot/project-static/docs/user-guides/getting-started/tenants.html +1178 -10
  618. nautobot/project-static/docs/user-guides/getting-started/vlans-and-vlan-groups.html +1178 -10
  619. nautobot/project-static/docs/user-guides/git-data-source.html +1178 -10
  620. nautobot/project-static/docs/user-guides/graphql.html +1178 -10
  621. nautobot/project-static/docs/user-guides/relationships.html +1178 -10
  622. nautobot/project-static/docs/user-guides/s3-django-storage.html +1178 -10
  623. nautobot/project-static/js/forms.js +16 -9
  624. nautobot/project-static/js/theme.js +5 -0
  625. nautobot/tenancy/api/serializers.py +4 -32
  626. nautobot/tenancy/api/urls.py +1 -1
  627. nautobot/tenancy/forms.py +0 -28
  628. nautobot/tenancy/migrations/0008_tagsfield.py +19 -0
  629. nautobot/tenancy/models.py +0 -25
  630. nautobot/tenancy/navigation.py +6 -39
  631. nautobot/tenancy/templates/tenancy/tenant.html +12 -12
  632. nautobot/tenancy/templates/tenancy/tenantgroup.html +1 -1
  633. nautobot/tenancy/tests/test_api.py +1 -3
  634. nautobot/tenancy/tests/test_filters.py +10 -5
  635. nautobot/tenancy/views.py +0 -2
  636. nautobot/ui/.eslintignore +6 -0
  637. nautobot/ui/.gitignore +10 -0
  638. nautobot/ui/.prettierignore +9 -0
  639. nautobot/ui/.prettierrc +4 -0
  640. nautobot/ui/README.md +33 -0
  641. nautobot/ui/app_imports.js.j2 +7 -0
  642. nautobot/ui/craco.config.js +46 -0
  643. nautobot/ui/jsconfig-base.json +11 -0
  644. nautobot/ui/jsconfig.json +5 -0
  645. nautobot/ui/lib/nautobot-craco-alias-plugin.js +40 -0
  646. nautobot/ui/package-lock.json +21451 -0
  647. nautobot/ui/package.json +70 -0
  648. nautobot/ui/public/index.html +47 -0
  649. nautobot/ui/public/logo192.png +0 -0
  650. nautobot/ui/public/logo512.png +0 -0
  651. nautobot/ui/public/manifest.json +25 -0
  652. nautobot/ui/public/nautobot_logo.svg +131 -0
  653. nautobot/ui/public/robots.txt +3 -0
  654. nautobot/ui/src/App.js +71 -0
  655. nautobot/ui/src/components/AppFullWidthComponents.js +8 -0
  656. nautobot/ui/src/components/AppTab.js +40 -0
  657. nautobot/ui/src/components/Apps.js +60 -0
  658. nautobot/ui/src/components/HomeChangelogPanel.js +98 -0
  659. nautobot/ui/src/components/HomePanel.js +58 -0
  660. nautobot/ui/src/components/JobHistoryTable.js +78 -0
  661. nautobot/ui/src/components/Layout.js +53 -0
  662. nautobot/ui/src/components/LoadingWidget.js +25 -0
  663. nautobot/ui/src/components/Navbar.js +116 -0
  664. nautobot/ui/src/components/NotificationPopover.js +27 -0
  665. nautobot/ui/src/components/ObjectListTable.js +209 -0
  666. nautobot/ui/src/components/ReferenceDataTag.js +35 -0
  667. nautobot/ui/src/components/RouterButton.js +10 -0
  668. nautobot/ui/src/components/RouterLink.js +10 -0
  669. nautobot/ui/src/components/SidebarNav.js +147 -0
  670. nautobot/ui/src/components/Table.js +48 -0
  671. nautobot/ui/src/components/TableItem.js +71 -0
  672. nautobot/ui/src/components/__tests__/AppFullWidthComponents.test.js +16 -0
  673. nautobot/ui/src/components/__tests__/AppTab.test.js +21 -0
  674. nautobot/ui/src/components/__tests__/Apps.test.js +14 -0
  675. nautobot/ui/src/components/__tests__/Layout.test.js +33 -0
  676. nautobot/ui/src/components/__tests__/Table.test.js +36 -0
  677. nautobot/ui/src/components/__tests__/TableItem.test.js +37 -0
  678. nautobot/ui/src/components/__tests__/paginator.test.js +43 -0
  679. nautobot/ui/src/components/__tests__/paginator_form.test.js +13 -0
  680. nautobot/ui/src/components/pagination.js +93 -0
  681. nautobot/ui/src/components/paginator.js +79 -0
  682. nautobot/ui/src/components/paginator_form.js +43 -0
  683. nautobot/ui/src/components/usePagination.js +57 -0
  684. nautobot/ui/src/constants/apiPath.js +10 -0
  685. nautobot/ui/src/constants/icons.js +15 -0
  686. nautobot/ui/src/constants/size.js +15 -0
  687. nautobot/ui/src/index.js +65 -0
  688. nautobot/ui/src/reportWebVitals.js +15 -0
  689. nautobot/ui/src/router.js +77 -0
  690. nautobot/ui/src/utils/api.js +131 -0
  691. nautobot/ui/src/utils/app-import.js +15 -0
  692. nautobot/ui/src/utils/color.js +15 -0
  693. nautobot/ui/src/utils/date.js +14 -0
  694. nautobot/ui/src/utils/index.js +15 -0
  695. nautobot/ui/src/utils/navigation.js +32 -0
  696. nautobot/ui/src/utils/session.js +64 -0
  697. nautobot/ui/src/utils/store.js +242 -0
  698. nautobot/ui/src/utils/string.js +6 -0
  699. nautobot/ui/src/utils/url.js +4 -0
  700. nautobot/ui/src/views/Home.js +138 -0
  701. nautobot/ui/src/views/InstalledApps.js +80 -0
  702. nautobot/ui/src/views/Login.js +48 -0
  703. nautobot/ui/src/views/Logout.js +20 -0
  704. nautobot/ui/src/views/__tests__/BSCreateViewTemplate.test.js +11 -0
  705. nautobot/ui/src/views/__tests__/BSListViewTemplate.test.js +107 -0
  706. nautobot/ui/src/views/__tests__/Login.test.js +15 -0
  707. nautobot/ui/src/views/generic/GenericView.js +142 -0
  708. nautobot/ui/src/views/generic/ObjectCreate.js +96 -0
  709. nautobot/ui/src/views/generic/ObjectList.js +127 -0
  710. nautobot/ui/src/views/generic/ObjectRetrieve.js +551 -0
  711. nautobot/users/admin.py +1 -1
  712. nautobot/users/api/serializers.py +51 -61
  713. nautobot/users/api/urls.py +1 -1
  714. nautobot/users/api/views.py +53 -2
  715. nautobot/users/tests/test_api.py +110 -25
  716. nautobot/virtualization/api/serializers.py +18 -130
  717. nautobot/virtualization/api/urls.py +1 -1
  718. nautobot/virtualization/api/views.py +1 -22
  719. nautobot/virtualization/forms.py +13 -99
  720. nautobot/virtualization/migrations/0012_alter_virtualmachine_role_add_new_role.py +1 -1
  721. nautobot/virtualization/migrations/0013_migrate_virtualmachine_role_data.py +18 -11
  722. nautobot/virtualization/migrations/0015_rename_foreignkey_fields.py +1 -1
  723. nautobot/virtualization/migrations/0018_related_name_changes.py +1 -1
  724. nautobot/virtualization/migrations/0021_tagsfield_and_vminterface_to_primarymodel.py +39 -0
  725. nautobot/virtualization/migrations/0022_vminterface_timestamps_data_migration.py +17 -0
  726. nautobot/virtualization/migrations/{0021_ipam__namespaces.py → 0023_ipam__namespaces.py} +2 -2
  727. nautobot/virtualization/migrations/0024_fixup_null_statuses.py +25 -0
  728. nautobot/virtualization/migrations/0025_status_nonnullable.py +29 -0
  729. nautobot/virtualization/models.py +31 -123
  730. nautobot/virtualization/navigation.py +18 -99
  731. nautobot/virtualization/templates/virtualization/virtualmachine.html +2 -1
  732. nautobot/virtualization/templates/virtualization/virtualmachine_edit.html +6 -0
  733. nautobot/virtualization/tests/test_api.py +25 -26
  734. nautobot/virtualization/tests/test_filters.py +41 -15
  735. nautobot/virtualization/tests/test_models.py +31 -7
  736. nautobot/virtualization/tests/test_views.py +42 -25
  737. nautobot/virtualization/views.py +7 -6
  738. {nautobot-2.0.0a3.dist-info → nautobot-2.0.0b1.dist-info}/METADATA +3 -7
  739. {nautobot-2.0.0a3.dist-info → nautobot-2.0.0b1.dist-info}/RECORD +744 -602
  740. {nautobot-2.0.0a3.dist-info → nautobot-2.0.0b1.dist-info}/WHEEL +1 -1
  741. nautobot/circuits/api/nested_serializers.py +0 -69
  742. nautobot/core/templates/plugin_template/navigation.py-tpl +0 -22
  743. nautobot/dcim/api/nested_serializers.py +0 -356
  744. nautobot/dcim/templates/dcim/device_import.html +0 -5
  745. nautobot/dcim/templates/dcim/device_import_child.html +0 -5
  746. nautobot/dcim/templates/dcim/inc/device_import_header.html +0 -4
  747. nautobot/extras/api/nested_serializers.py +0 -353
  748. nautobot/extras/migrations/0064_configcontext_data_migrations.py +0 -41
  749. nautobot/extras/migrations/0071_job__unique_name_data_migration.py +0 -46
  750. nautobot/extras/reports.py +0 -60
  751. nautobot/extras/scripts.py +0 -72
  752. nautobot/extras/tests/example_jobs/script_variables.py +0 -67
  753. nautobot/extras/tests/example_jobs/test_duplicate_name2.py +0 -5
  754. nautobot/extras/tests/example_jobs/test_fail.py +0 -16
  755. nautobot/extras/tests/example_jobs/test_file_upload_pass.py +0 -20
  756. nautobot/extras/tests/example_jobs/test_ipaddress_vars.py +0 -52
  757. nautobot/extras/tests/example_jobs/test_job_button_receiver.py +0 -21
  758. nautobot/extras/tests/example_jobs/test_job_hook_receiver.py +0 -20
  759. nautobot/extras/tests/example_jobs/test_location_with_custom_field.py +0 -35
  760. nautobot/extras/tests/example_jobs/test_log_redaction.py +0 -14
  761. nautobot/extras/tests/example_jobs/test_modify_db.py +0 -18
  762. nautobot/extras/tests/example_jobs/test_object_var_optional.py +0 -14
  763. nautobot/extras/tests/example_jobs/test_object_var_required.py +0 -14
  764. nautobot/extras/tests/example_jobs/test_object_vars.py +0 -29
  765. nautobot/extras/tests/example_jobs/test_pass.py +0 -19
  766. nautobot/extras/tests/example_jobs/test_read_only_fail.py +0 -24
  767. nautobot/extras/tests/example_jobs/test_read_only_no_commit_field.py +0 -10
  768. nautobot/extras/tests/example_jobs/test_read_only_pass.py +0 -22
  769. nautobot/ipam/api/nested_serializers.py +0 -159
  770. nautobot/ipam/migrations/0029_ipam__prefix__add_parent.py +0 -31
  771. nautobot/ipam/migrations/0030_ipam__prefix__data_migration.py +0 -13
  772. nautobot/ipam/migrations/0031_ipam__ipaddress__add_parent.py +0 -41
  773. nautobot/ipam/migrations/0032_ipam__ipaddress__data_migration.py +0 -11
  774. nautobot/tenancy/api/nested_serializers.py +0 -31
  775. nautobot/users/api/nested_serializers.py +0 -67
  776. nautobot/virtualization/api/nested_serializers.py +0 -65
  777. /nautobot/extras/{tests/example_jobs → test_jobs}/__init__.py +0 -0
  778. /nautobot/{dcim/models/sites.py → ipam/management/__init__.py} +0 -0
  779. {nautobot-2.0.0a3.dist-info → nautobot-2.0.0b1.dist-info}/LICENSE.txt +0 -0
  780. {nautobot-2.0.0a3.dist-info → nautobot-2.0.0b1.dist-info}/entry_points.txt +0 -0
@@ -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 test_list_objects_brief(self):
237
+ def test_list_objects_depth_0(self):
218
238
  """
219
- GET a list of objects using the "brief" parameter.
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()}?brief=1"
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
- self.assertEqual(
230
- sorted(response.data["results"][0]),
231
- self.brief_fields,
232
- "In order to test the brief API parameter the brief fields need to be manually added to "
233
- "self.brief_fields. If this is already the case, perhaps the serializer is implemented incorrectly?",
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
- response = self.client.post(self._get_list_url(), create_data, format="json", **self.header)
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 name that has choices defined (fields with enums)
575
- if "POST" in data["actions"]:
576
- field_choices = {k for k, v in data["actions"]["POST"].items() if "choices" in v}
577
- elif "PUT" in data["actions"]: # JobModelViewSet supports editing but not creation
578
- field_choices = {k for k, v in data["actions"]["PUT"].items() if "choices" in v}
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,
@@ -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=False)`.
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):
@@ -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, TaggableManager)):
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
- err = response.data
156
- else:
157
- # Attempt to extract form validation errors from the response HTML
158
- form_errors = testing.extract_form_failures(response.content.decode(response.charset))
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
  """
@@ -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 settings.REST_FRAMEWORK_ALLOWED_VERSIONS:
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)