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,4 +1,5 @@
1
1
  from copy import deepcopy
2
+ from io import BytesIO
2
3
  import logging
3
4
  import re
4
5
 
@@ -12,16 +13,18 @@ from django.core.exceptions import (
12
13
  )
13
14
  from django.db import transaction, IntegrityError
14
15
  from django.db.models import ManyToManyField, ProtectedError
15
- from django.forms import Form, ModelMultipleChoiceField, MultipleHiddenInput, Textarea
16
- from django.http import HttpResponse
16
+ from django.forms import Form, ModelMultipleChoiceField, MultipleHiddenInput
17
+ from django.http import HttpResponse, JsonResponse
17
18
  from django.shortcuts import get_object_or_404, redirect, render
18
- from django.urls import NoReverseMatch, reverse
19
19
  from django.utils.html import escape
20
20
  from django.utils.http import is_safe_url
21
21
  from django.utils.safestring import mark_safe
22
22
  from django.views.generic import View
23
23
  from django_tables2 import RequestConfig
24
+ from rest_framework.exceptions import ParseError
24
25
 
26
+ from nautobot.core.api.parsers import NautobotCSVParser
27
+ from nautobot.core.api.utils import get_serializer_for_model
25
28
  from nautobot.core.forms import SearchForm
26
29
  from nautobot.core.exceptions import AbortTransaction
27
30
  from nautobot.core.forms import (
@@ -36,7 +39,6 @@ from nautobot.core.forms import (
36
39
  )
37
40
  from nautobot.core.forms.forms import DynamicFilterFormSet
38
41
  from nautobot.core.templatetags.helpers import bettertitle, validated_viewname
39
- from nautobot.core.utils.lookup import get_route_for_model
40
42
  from nautobot.core.utils.permissions import get_permission_for_model
41
43
  from nautobot.core.utils.requests import (
42
44
  convert_querydict_to_factory_formset_acceptable_querydict,
@@ -45,9 +47,13 @@ from nautobot.core.utils.requests import (
45
47
  )
46
48
  from nautobot.core.views.paginator import EnhancedPaginator, get_paginate_count
47
49
  from nautobot.core.views.mixins import GetReturnURLMixin, ObjectPermissionRequiredMixin
48
- from nautobot.core.views.utils import check_filter_for_display, csv_format, handle_protectederror, prepare_cloned_fields
49
- from nautobot.extras.models import CustomField, ExportTemplate
50
- from nautobot.extras.models.change_logging import ChangeLoggedModel
50
+ from nautobot.core.views.utils import (
51
+ check_filter_for_display,
52
+ get_csv_form_fields_from_serializer_class,
53
+ handle_protectederror,
54
+ prepare_cloned_fields,
55
+ )
56
+ from nautobot.extras.models import ExportTemplate
51
57
  from nautobot.extras.utils import remove_prefix_from_cf_key
52
58
 
53
59
 
@@ -61,6 +67,7 @@ class ObjectView(ObjectPermissionRequiredMixin, View):
61
67
 
62
68
  queryset = None
63
69
  template_name = None
70
+ use_new_ui = True
64
71
 
65
72
  def get_required_permission(self):
66
73
  return get_permission_for_model(self.queryset.model, "view")
@@ -89,55 +96,41 @@ class ObjectView(ObjectPermissionRequiredMixin, View):
89
96
  "active_tab": request.GET.get("tab", "main"),
90
97
  }
91
98
 
92
- # 2.0 TODO: Remove this method in 2.0. Can be retrieved from instance itself now
93
- # instance.get_changelog_url()
94
- # Only available on models that support changelogs
95
- def get_changelog_url(self, instance):
96
- """Return the changelog URL for a given instance."""
97
- meta = self.queryset.model._meta
98
-
99
- # Don't try to generate a changelog_url for an ObjectChange.
100
- if meta.model_name == "objectchange":
101
- return None
102
-
103
- route = get_route_for_model(instance, "changelog")
104
-
105
- # Iterate the pk-like fields and try to get a URL, or return None.
106
- fields = ["pk", "slug"]
107
- for field in fields:
108
- if not hasattr(instance, field):
109
- continue
110
-
111
- try:
112
- return reverse(route, kwargs={field: getattr(instance, field)})
113
- except NoReverseMatch:
114
- continue
115
-
116
- # This object likely doesn't have a changelog route defined.
117
- return None
118
-
119
99
  def get(self, request, *args, **kwargs):
120
100
  """
121
101
  Generic GET handler for accessing an object by PK or slug
122
102
  """
123
103
  instance = get_object_or_404(self.queryset, **kwargs)
124
104
 
125
- changelog_url = None
126
-
127
- if isinstance(instance, ChangeLoggedModel):
128
- changelog_url = instance.get_changelog_url()
105
+ # TODO: this feels inelegant - should the tabs lookup be a dedicated endpoint rather than piggybacking
106
+ # on the object-retrieve endpoint?
107
+ # TODO: similar functionality probably needed in NautobotUIViewSet as well, not currently present
108
+ if request.GET.get("viewconfig", None) == "true":
109
+ # TODO: we shouldn't be importing a private-named function from another module. Should it be renamed?
110
+ from nautobot.extras.templatetags.plugins import _get_registered_content
129
111
 
130
- return render(
131
- request,
132
- self.get_template_name(),
133
- {
112
+ temp_fake_context = {
134
113
  "object": instance,
135
- "verbose_name": self.queryset.model._meta.verbose_name,
136
- "verbose_name_plural": self.queryset.model._meta.verbose_name_plural,
137
- "changelog_url": changelog_url, # 2.0 TODO: Remove in 2.0. This information can be retrieved from the object itself now.
138
- **self.get_extra_context(request, instance),
139
- },
140
- )
114
+ "request": request,
115
+ "settings": {},
116
+ "csrf_token": "",
117
+ "perms": {},
118
+ }
119
+
120
+ plugin_tabs = _get_registered_content(instance, "detail_tabs", temp_fake_context, return_html=False)
121
+ resp = {"tabs": plugin_tabs}
122
+ return JsonResponse(resp)
123
+ else:
124
+ return render(
125
+ request,
126
+ self.get_template_name(),
127
+ {
128
+ "object": instance,
129
+ "verbose_name": self.queryset.model._meta.verbose_name,
130
+ "verbose_name_plural": self.queryset.model._meta.verbose_name_plural,
131
+ **self.get_extra_context(request, instance),
132
+ },
133
+ )
141
134
 
142
135
 
143
136
  class ObjectListView(ObjectPermissionRequiredMixin, View):
@@ -165,6 +158,7 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
165
158
  "per_page", # used by get_paginate_count
166
159
  "sort", # table sorting
167
160
  )
161
+ use_new_ui = True
168
162
 
169
163
  def get_filter_params(self, request):
170
164
  """Helper function - take request.GET and discard any parameters that are not used for queryset filtering."""
@@ -174,6 +168,7 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
174
168
  def get_required_permission(self):
175
169
  return get_permission_for_model(self.queryset.model, "view")
176
170
 
171
+ # TODO: remove this as well?
177
172
  def queryset_to_yaml(self):
178
173
  """
179
174
  Export the queryset of objects as concatenated YAML documents.
@@ -182,35 +177,6 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
182
177
 
183
178
  return "---\n".join(yaml_data)
184
179
 
185
- def queryset_to_csv(self):
186
- """
187
- Export the queryset of objects as comma-separated value (CSV), using the model's to_csv() method.
188
- """
189
- csv_data = []
190
- custom_field_keys = []
191
-
192
- # Start with the column headers
193
- headers = self.queryset.model.csv_headers.copy()
194
-
195
- # Add custom field headers, if any
196
- if hasattr(self.queryset.model, "_custom_field_data"):
197
- for custom_field in CustomField.objects.get_for_model(self.queryset.model):
198
- headers.append(custom_field.add_prefix_to_cf_key())
199
- custom_field_keys.append(custom_field.key)
200
-
201
- csv_data.append(",".join(headers))
202
-
203
- # Iterate through the queryset appending each object
204
- for obj in self.queryset:
205
- data = obj.to_csv()
206
-
207
- for custom_field_key in custom_field_keys:
208
- data += (obj.cf.get(custom_field_key, ""),)
209
-
210
- csv_data.append(csv_format(data))
211
-
212
- return "\n".join(csv_data)
213
-
214
180
  def validate_action_buttons(self, request):
215
181
  """Verify actions in self.action_buttons are valid view actions."""
216
182
 
@@ -286,13 +252,6 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
286
252
  response["Content-Disposition"] = f'attachment; filename="{filename}"'
287
253
  return response
288
254
 
289
- # Fall back to built-in CSV formatting if export requested but no template specified
290
- elif "export" in request.GET and hasattr(model, "to_csv"):
291
- response = HttpResponse(self.queryset_to_csv(), content_type="text/csv")
292
- filename = f"{settings.BRANDING_PREPENDED_FILENAME}{self.queryset.model._meta.verbose_name_plural}.csv"
293
- response["Content-Disposition"] = f'attachment; filename="{filename}"'
294
- return response
295
-
296
255
  # Provide a hook to tweak the queryset based on the request immediately prior to rendering the object list
297
256
  self.queryset = self.alter_queryset(request)
298
257
 
@@ -871,31 +830,31 @@ class BulkImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
871
830
  Import objects in bulk (CSV format).
872
831
 
873
832
  queryset: Base queryset for the model
874
- model_form: The form used to create each imported object
875
833
  table: The django-tables2 Table used to render the list of imported objects
876
834
  template_name: The name of the template
877
- widget_attrs: A dict of attributes to apply to the import widget (e.g. to require a session key)
878
835
  """
879
836
 
880
837
  queryset = None
881
- model_form = None
882
838
  table = None
883
839
  template_name = "generic/object_bulk_import.html"
884
- widget_attrs = {}
840
+
841
+ def __init__(self, *args, **kwargs):
842
+ super().__init__(*args, **kwargs)
843
+ self.serializer_class = get_serializer_for_model(self.queryset.model)
844
+ self.fields = get_csv_form_fields_from_serializer_class(self.serializer_class)
845
+ self.required_field_names = [
846
+ field["name"]
847
+ for field in get_csv_form_fields_from_serializer_class(self.serializer_class)
848
+ if field["required"]
849
+ ]
885
850
 
886
851
  def _import_form(self, *args, **kwargs):
887
852
  class CSVImportForm(BootstrapMixin, Form):
888
- csv_data = CSVDataField(from_form=self.model_form, widget=Textarea(attrs=self.widget_attrs))
889
- csv_file = CSVFileField(from_form=self.model_form)
853
+ csv_data = CSVDataField(required_field_names=self.required_field_names)
854
+ csv_file = CSVFileField()
890
855
 
891
856
  return CSVImportForm(*args, **kwargs)
892
857
 
893
- def _save_obj(self, obj_form, request):
894
- """
895
- Provide a hook to modify the object immediately before saving it (e.g. to encrypt secret data).
896
- """
897
- return obj_form.save()
898
-
899
858
  def get_required_permission(self):
900
859
  return get_permission_for_model(self.queryset.model, "add")
901
860
 
@@ -905,8 +864,8 @@ class BulkImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
905
864
  self.template_name,
906
865
  {
907
866
  "form": self._import_form(),
908
- "fields": self.model_form().fields,
909
- "obj_type": self.model_form._meta.model._meta.verbose_name,
867
+ "fields": self.fields,
868
+ "obj_type": self.queryset.model._meta.verbose_name,
910
869
  "return_url": self.get_return_url(request),
911
870
  "active_tab": "csv-data",
912
871
  },
@@ -927,18 +886,24 @@ class BulkImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
927
886
  field_name = "csv_file"
928
887
  else:
929
888
  field_name = "csv_data"
930
- headers, records = form.cleaned_data[field_name]
931
- for row, data in enumerate(records, start=1):
932
- obj_form = self.model_form(data, headers=headers)
933
- restrict_form_fields(obj_form, request.user)
934
-
935
- if obj_form.is_valid():
936
- obj = self._save_obj(obj_form, request)
937
- new_objs.append(obj)
889
+ csvtext = form.cleaned_data[field_name]
890
+
891
+ try:
892
+ data = NautobotCSVParser().parse(
893
+ stream=BytesIO(csvtext.encode("utf-8")),
894
+ parser_context={"request": request, "serializer_class": self.serializer_class},
895
+ )
896
+ serializer = self.serializer_class(data=data, context={"request": request}, many=True)
897
+ if serializer.is_valid():
898
+ new_objs = serializer.save()
938
899
  else:
939
- for field, err in obj_form.errors.items():
940
- form.add_error(field_name, f"Row {row} {field}: {err[0]}")
900
+ for row, errors in enumerate(serializer.errors, start=1):
901
+ for field, err in errors.items():
902
+ form.add_error(field_name, f"Row {row}: {field}: {err[0]}")
941
903
  raise ValidationError("")
904
+ except ParseError as exc:
905
+ form.add_error(None, str(exc))
906
+ raise ValidationError("")
942
907
 
943
908
  # Enforce object-level permissions
944
909
  if self.queryset.filter(pk__in=[obj.pk for obj in new_objs]).count() != len(new_objs):
@@ -977,8 +942,8 @@ class BulkImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
977
942
  self.template_name,
978
943
  {
979
944
  "form": form,
980
- "fields": self.model_form().fields,
981
- "obj_type": self.model_form._meta.model._meta.verbose_name,
945
+ "fields": self.fields,
946
+ "obj_type": self.queryset.model._meta.verbose_name,
982
947
  "return_url": self.get_return_url(request),
983
948
  "active_tab": "csv-file" if form.has_error("csv_file") else "csv-data",
984
949
  },
@@ -1,3 +1,4 @@
1
+ from io import BytesIO
1
2
  import logging
2
3
 
3
4
  from django.contrib import messages
@@ -11,7 +12,7 @@ from django.core.exceptions import (
11
12
  )
12
13
  from django.db import transaction
13
14
  from django.db.models import ManyToManyField, ProtectedError
14
- from django.forms import Form, ModelMultipleChoiceField, MultipleHiddenInput, Textarea
15
+ from django.forms import Form, ModelMultipleChoiceField, MultipleHiddenInput
15
16
  from django.http import HttpResponse
16
17
  from django.shortcuts import get_object_or_404, redirect
17
18
  from django.template.loader import select_template, TemplateDoesNotExist
@@ -29,7 +30,8 @@ from rest_framework.viewsets import GenericViewSet
29
30
 
30
31
  from drf_spectacular.utils import extend_schema
31
32
 
32
- from nautobot.core.api.views import BulkCreateModelMixin, BulkDestroyModelMixin, BulkUpdateModelMixin
33
+ from nautobot.core.api.parsers import NautobotCSVParser
34
+ from nautobot.core.api.views import BulkDestroyModelMixin, BulkUpdateModelMixin
33
35
  from nautobot.core.forms import (
34
36
  BootstrapMixin,
35
37
  ConfirmationForm,
@@ -40,8 +42,12 @@ from nautobot.core.forms import (
40
42
  from nautobot.core.utils import lookup, permissions
41
43
  from nautobot.core.views.renderers import NautobotHTMLRenderer
42
44
  from nautobot.core.utils.requests import get_filterable_params_from_filter_params
43
- from nautobot.core.views.utils import csv_format, handle_protectederror, prepare_cloned_fields
44
- from nautobot.extras.models import CustomField, ExportTemplate
45
+ from nautobot.core.views.utils import (
46
+ get_csv_form_fields_from_serializer_class,
47
+ handle_protectederror,
48
+ prepare_cloned_fields,
49
+ )
50
+ from nautobot.extras.models import ExportTemplate
45
51
  from nautobot.extras.forms import NoteForm
46
52
  from nautobot.extras.tables import ObjectChangeTable, NoteTable
47
53
  from nautobot.extras.utils import remove_prefix_from_cf_key
@@ -179,8 +185,12 @@ class GetReturnURLMixin:
179
185
  # Note that the use of both `obj.present_in_database` and `obj.pk` is correct here because this conditional
180
186
  # handles all three of the create, update, and delete operations. When Django deletes an instance
181
187
  # from the DB, it sets the instance's PK field to None, regardless of the use of a UUID.
182
- if obj is not None and obj.present_in_database and obj.pk and hasattr(obj, "get_absolute_url"):
183
- return obj.get_absolute_url()
188
+ try:
189
+ if obj is not None and obj.present_in_database and obj.pk:
190
+ return obj.get_absolute_url()
191
+ except AttributeError:
192
+ # Model has no get_absolute_url() method or no reverse match
193
+ pass
184
194
 
185
195
  # Fall back to the default URL (if specified) for the view.
186
196
  if self.default_return_url is not None:
@@ -205,7 +215,6 @@ class NautobotViewSetMixin(GenericViewSet, AccessMixin, GetReturnURLMixin, FormV
205
215
 
206
216
  renderer_classes = [NautobotHTMLRenderer]
207
217
  logger = logging.getLogger(__name__)
208
- lookup_field = "slug"
209
218
  # Attributes that need to be specified: form_class, queryset, serializer_class, table_class for most mixins.
210
219
  # filterset and filter_params will be initialized in filter_queryset() in ObjectListViewMixin
211
220
  filter_params = None
@@ -522,12 +531,15 @@ class NautobotViewSetMixin(GenericViewSet, AccessMixin, GetReturnURLMixin, FormV
522
531
  if self.action in ["create", "update"]:
523
532
  form_class = getattr(self, "form_class", None)
524
533
  elif self.action == "bulk_create":
534
+ required_field_names = [
535
+ field["name"]
536
+ for field in get_csv_form_fields_from_serializer_class(self.serializer_class)
537
+ if field["required"]
538
+ ]
525
539
 
526
540
  class BulkCreateForm(BootstrapMixin, Form):
527
- csv_data = CSVDataField(
528
- from_form=self.bulk_create_form_class, widget=Textarea(attrs=self.bulk_create_widget_attrs)
529
- )
530
- csv_file = CSVFileField(from_form=self.bulk_create_form_class)
541
+ csv_data = CSVDataField(required_field_names=required_field_names)
542
+ csv_file = CSVFileField()
531
543
 
532
544
  form_class = BulkCreateForm
533
545
  else:
@@ -566,6 +578,17 @@ class ObjectDetailViewMixin(NautobotViewSetMixin, mixins.RetrieveModelMixin):
566
578
  UI mixin to retrieve a model instance.
567
579
  """
568
580
 
581
+ def retrieve(self, request, *args, **kwargs):
582
+ """
583
+ Retrieve a model instance.
584
+ """
585
+ instance = self.get_object()
586
+ serializer = self.get_serializer(instance)
587
+
588
+ context = serializer.data
589
+ context["use_new_ui"] = True
590
+ return Response(context)
591
+
569
592
 
570
593
  class ObjectListViewMixin(NautobotViewSetMixin, mixins.ListModelMixin):
571
594
  """
@@ -622,13 +645,6 @@ class ObjectListViewMixin(NautobotViewSetMixin, mixins.ListModelMixin):
622
645
  response["Content-Disposition"] = f'attachment; filename="{filename}"'
623
646
  return response
624
647
 
625
- # Fall back to built-in CSV formatting if export requested but no template specified
626
- elif "export" in request.GET and hasattr(model, "to_csv"):
627
- response = HttpResponse(self.queryset_to_csv(), content_type="text/csv")
628
- filename = f"nautobot_{queryset.model._meta.verbose_name_plural}.csv"
629
- response["Content-Disposition"] = f'attachment; filename="{filename}"'
630
- return response
631
-
632
648
  return None
633
649
 
634
650
  def queryset_to_yaml(self):
@@ -640,40 +656,11 @@ class ObjectListViewMixin(NautobotViewSetMixin, mixins.ListModelMixin):
640
656
 
641
657
  return "---\n".join(yaml_data)
642
658
 
643
- def queryset_to_csv(self):
644
- """
645
- Export the queryset of objects as comma-separated value (CSV), using the model's to_csv() method.
646
- """
647
- queryset = self.filter_queryset(self.get_queryset())
648
- csv_data = []
649
- custom_fields = []
650
- # Start with the column headers
651
- headers = queryset.model.csv_headers.copy()
652
-
653
- # Add custom field headers, if any
654
- if hasattr(queryset.model, "_custom_field_data"):
655
- for custom_field in CustomField.objects.get_for_model(queryset.model):
656
- headers.append(custom_field.add_prefix_to_cf_key())
657
- custom_fields.append(custom_field.key)
658
-
659
- csv_data.append(",".join(headers))
660
-
661
- # Iterate through the queryset appending each object
662
- for obj in queryset:
663
- data = obj.to_csv()
664
-
665
- for custom_field in custom_fields:
666
- data += (obj.cf.get(custom_field, ""),)
667
-
668
- csv_data.append(csv_format(data))
669
-
670
- return "\n".join(csv_data)
671
-
672
659
  def list(self, request, *args, **kwargs):
673
660
  """
674
661
  List the model instances.
675
662
  """
676
- context = {}
663
+ context = {"use_new_ui": True}
677
664
  if "export" in request.GET:
678
665
  queryset = self.get_queryset()
679
666
  model = queryset.model
@@ -895,14 +882,12 @@ class ObjectBulkDestroyViewMixin(NautobotViewSetMixin, BulkDestroyModelMixin):
895
882
  return Response(data)
896
883
 
897
884
 
898
- class ObjectBulkCreateViewMixin(NautobotViewSetMixin, BulkCreateModelMixin):
885
+ class ObjectBulkCreateViewMixin(NautobotViewSetMixin):
899
886
  """
900
887
  UI mixin to bulk create model instances.
901
888
  """
902
889
 
903
890
  bulk_create_active_tab = "csv-data"
904
- bulk_create_form_class = None
905
- bulk_create_widget_attrs = {}
906
891
 
907
892
  def _process_bulk_create_form(self, form):
908
893
  # Iterate through CSV data and bind each row to a new model form instance.
@@ -918,18 +903,24 @@ class ObjectBulkCreateViewMixin(NautobotViewSetMixin, BulkCreateModelMixin):
918
903
  self.bulk_create_active_tab = "csv-file"
919
904
  else:
920
905
  field_name = "csv_data"
921
- headers, records = form.cleaned_data[field_name]
922
- for row, data in enumerate(records, start=1):
923
- obj_form = self.bulk_create_form_class(data, headers=headers)
924
- restrict_form_fields(obj_form, request.user)
925
-
926
- if obj_form.is_valid():
927
- obj = self.form_save(obj_form)
928
- new_objs.append(obj)
906
+
907
+ csvtext = form.cleaned_data[field_name]
908
+ try:
909
+ data = NautobotCSVParser().parse(
910
+ stream=BytesIO(csvtext.encode("utf-8")),
911
+ parser_context={"request": request, "serializer_class": self.serializer_class},
912
+ )
913
+ serializer = self.serializer_class(data=data, context={"request": request}, many=True)
914
+ if serializer.is_valid():
915
+ new_objs = serializer.save()
929
916
  else:
930
- for field, err in obj_form.errors.items():
931
- form.add_error(field_name, f"Row {row} {field}: {err[0]}")
917
+ for row, errors in enumerate(serializer.errors, start=1):
918
+ for field, err in errors.items():
919
+ form.add_error(field_name, f"Row {row}: {field}: {err[0]}")
932
920
  raise ValidationError("")
921
+ except exceptions.ParseError as exc:
922
+ form.add_error(None, str(exc))
923
+ raise ValidationError("")
933
924
 
934
925
  # Enforce object-level permissions
935
926
  if queryset.filter(pk__in=[obj.pk for obj in new_objs]).count() != len(new_objs):
@@ -19,8 +19,8 @@ from nautobot.core.utils.requests import (
19
19
  normalize_querydict,
20
20
  )
21
21
  from nautobot.core.views.paginator import EnhancedPaginator, get_paginate_count
22
- from nautobot.core.views.utils import check_filter_for_display
23
- from nautobot.extras.models.change_logging import ChangeLoggedModel, ObjectChange
22
+ from nautobot.core.views.utils import check_filter_for_display, get_csv_form_fields_from_serializer_class
23
+ from nautobot.extras.models.change_logging import ObjectChange
24
24
  from nautobot.extras.utils import get_base_template
25
25
 
26
26
 
@@ -136,7 +136,6 @@ class NautobotHTMLRenderer(renderers.BrowsableAPIRenderer):
136
136
  form = None
137
137
  table = None
138
138
  search_form = None
139
- changelog_url = None
140
139
  instance = None
141
140
  filter_form = None
142
141
  queryset = view.alter_queryset(request)
@@ -146,8 +145,6 @@ class NautobotHTMLRenderer(renderers.BrowsableAPIRenderer):
146
145
  if view.action in ["create", "retrieve", "update", "destroy", "changelog", "notes"]:
147
146
  instance = view.get_object()
148
147
  return_url = view.get_return_url(request, instance)
149
- if isinstance(instance, ChangeLoggedModel):
150
- changelog_url = instance.get_changelog_url()
151
148
  else:
152
149
  return_url = view.get_return_url(request)
153
150
  # Get form for context rendering according to view.action unless it is previously set.
@@ -208,7 +205,6 @@ class NautobotHTMLRenderer(renderers.BrowsableAPIRenderer):
208
205
  table = self.construct_table(view, object=instance, content_type=content_type)
209
206
 
210
207
  context = {
211
- "changelog_url": changelog_url, # NOTE: This context key is deprecated in favor of `object.get_changelog_url`.
212
208
  "content_type": content_type,
213
209
  "form": form,
214
210
  "filter_form": filter_form,
@@ -249,7 +245,7 @@ class NautobotHTMLRenderer(renderers.BrowsableAPIRenderer):
249
245
  context.update(
250
246
  {
251
247
  "active_tab": view.bulk_create_active_tab if view.bulk_create_active_tab else "csv-data",
252
- "fields": view.bulk_create_form_class(model).fields if view.bulk_create_form_class else None,
248
+ "fields": get_csv_form_fields_from_serializer_class(view.serializer_class),
253
249
  }
254
250
  )
255
251
  elif view.action in ["changelog", "notes"]:
@@ -270,4 +266,7 @@ class NautobotHTMLRenderer(renderers.BrowsableAPIRenderer):
270
266
  # Get the corresponding template based on self.action in view.get_template_name() unless it is already specified in the Response() data.
271
267
  # See form_valid() for self.action == "bulk_create".
272
268
  self.template = data.get("template", view.get_template_name())
269
+
270
+ # NautobotUIViewSets pass "use_new_ui" in context as they share the same class and are just different methods
271
+ self.use_new_ui = data.get("use_new_ui", False)
273
272
  return super().render(data, accepted_media_type=accepted_media_type, renderer_context=renderer_context)
@@ -6,6 +6,9 @@ from django.db.models import ForeignKey
6
6
  from django.utils.html import escape
7
7
  from django.utils.safestring import mark_safe
8
8
 
9
+ from rest_framework import serializers
10
+
11
+ from nautobot.core.api.fields import ChoiceField, ContentTypeField, TimeZoneSerializerField
9
12
  from nautobot.core.models.utils import is_taggable
10
13
  from nautobot.core.utils.data import is_uuid
11
14
  from nautobot.core.utils.filtering import get_filter_field_label
@@ -56,9 +59,14 @@ def check_filter_for_display(filters, field_name, values):
56
59
  return resolved_filter
57
60
 
58
61
 
62
+ # 2.2 TODO: remove this method as it's no longer used in core.
59
63
  def csv_format(data):
60
64
  """
65
+ Convert the given list of data to a CSV row string.
66
+
61
67
  Encapsulate any data which contains a comma within double quotes.
68
+
69
+ Obsolete, as CSV rendering in Nautobot core is now handled by nautobot.core.api.renderers.NautobotCSVRenderer.
62
70
  """
63
71
  csv = []
64
72
  for value in data:
@@ -85,6 +93,78 @@ def csv_format(data):
85
93
  return ",".join(csv)
86
94
 
87
95
 
96
+ def get_csv_form_fields_from_serializer_class(serializer_class):
97
+ """From the given serializer class, build a list of field dicts suitable for rendering in the CSV import form."""
98
+ serializer = serializer_class(context={"request": None, "depth": 0})
99
+ fields = []
100
+ for field_name, field in serializer.fields.items():
101
+ if field.read_only:
102
+ continue
103
+ if field_name == "custom_fields":
104
+ from nautobot.extras.choices import CustomFieldTypeChoices
105
+ from nautobot.extras.models import CustomField
106
+
107
+ cfs = CustomField.objects.get_for_model(serializer_class.Meta.model)
108
+ for cf in cfs:
109
+ cf_form_field = cf.to_form_field(set_initial=False)
110
+ field_info = {
111
+ "name": cf.add_prefix_to_cf_key(),
112
+ "required": cf_form_field.required,
113
+ "label": cf_form_field.label,
114
+ "help_text": cf_form_field.help_text,
115
+ }
116
+ if cf.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
117
+ field_info["format"] = mark_safe("<code>true</code> or <code>false</code>")
118
+ elif cf.type == CustomFieldTypeChoices.TYPE_DATE:
119
+ field_info["format"] = mark_safe("<code>YYYY-MM-DD</code>")
120
+ elif cf.type == CustomFieldTypeChoices.TYPE_SELECT:
121
+ field_info["choices"] = {cfc.value: cfc.value for cfc in cf.custom_field_choices.all()}
122
+ elif cf.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
123
+ field_info["format"] = mark_safe('<code>"value,value"</code>')
124
+ field_info["choices"] = {cfc.value: cfc.value for cfc in cf.custom_field_choices.all()}
125
+ fields.append(field_info)
126
+ continue
127
+
128
+ field_info = {
129
+ "name": field_name,
130
+ "required": field.required,
131
+ "label": field.label,
132
+ "help_text": field.help_text,
133
+ }
134
+ if isinstance(field, serializers.BooleanField):
135
+ field_info["format"] = mark_safe("<code>true</code> or <code>false</code>")
136
+ elif isinstance(field, serializers.DateField):
137
+ field_info["format"] = mark_safe("<code>YYYY-MM-DD</code>")
138
+ elif isinstance(field, TimeZoneSerializerField):
139
+ field_info["format"] = mark_safe(
140
+ '<a href="https://en.wikipedia.org/wiki/List_of_tz_database_time_zones">available options</a>'
141
+ )
142
+ elif isinstance(field, serializers.ManyRelatedField):
143
+ if isinstance(field.child_relation, ContentTypeField):
144
+ field_info["format"] = mark_safe('<code>"app_label.model,app_label.model"</code>')
145
+ else:
146
+ field_info["format"] = mark_safe('<code>"natural_key_slug,natural_key_slug"</code>')
147
+ elif isinstance(field, serializers.RelatedField):
148
+ if isinstance(field, ContentTypeField):
149
+ field_info["format"] = mark_safe("<code>app_label.model</code>")
150
+ else:
151
+ field_info["format"] = mark_safe("<code>natural_key_slug</code>")
152
+ elif isinstance(field, (serializers.ListField, serializers.MultipleChoiceField)):
153
+ field_info["format"] = mark_safe('<code>"value,value"</code>')
154
+ elif isinstance(field, (serializers.DictField, serializers.JSONField)):
155
+ pass # Not trivial to specify a format as it could be a JSON dict or a comma-separated string
156
+
157
+ if isinstance(field, ChoiceField):
158
+ field_info["choices"] = field.choices
159
+
160
+ fields.append(field_info)
161
+
162
+ # Move all required fields to the start of the list
163
+ # TODO this ordering should be defined by the serializer instead...
164
+ fields = sorted(fields, key=lambda info: 1 if info["required"] else 2)
165
+ return fields
166
+
167
+
88
168
  def handle_protectederror(obj_list, request, e):
89
169
  """
90
170
  Generate a user-friendly error message in response to a ProtectedError exception.