nautobot 2.0.0a2__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 (1029) hide show
  1. nautobot/__init__.py +1 -5
  2. nautobot/apps/api.py +6 -8
  3. nautobot/apps/forms.py +0 -2
  4. nautobot/apps/ui.py +0 -8
  5. nautobot/circuits/api/serializers.py +9 -119
  6. nautobot/circuits/api/urls.py +1 -1
  7. nautobot/circuits/api/views.py +0 -1
  8. nautobot/circuits/choices.py +0 -2
  9. nautobot/circuits/filters.py +7 -6
  10. nautobot/circuits/forms.py +3 -73
  11. nautobot/circuits/migrations/0001_initial_part_1.py +0 -1
  12. nautobot/circuits/migrations/0002_initial_part_2.py +0 -1
  13. nautobot/circuits/migrations/0003_auto_slug.py +0 -1
  14. nautobot/circuits/migrations/0004_increase_provider_account_length.py +0 -1
  15. nautobot/circuits/migrations/0005_providernetwork.py +0 -1
  16. nautobot/circuits/migrations/0006_cache_circuit_terminations.py +0 -1
  17. nautobot/circuits/migrations/0007_circuitterminations_primary_model.py +0 -1
  18. nautobot/circuits/migrations/0008_add_natural_indexing.py +0 -1
  19. nautobot/circuits/migrations/0009_circuittermination_location.py +0 -1
  20. nautobot/circuits/migrations/0010_rename_foreign_keys_and_related_names.py +0 -1
  21. nautobot/circuits/migrations/0011_remove_site_foreign_key_from_circuit_termination_class.py +0 -1
  22. nautobot/circuits/migrations/0012_created_datetime.py +0 -1
  23. nautobot/circuits/migrations/0013_alter_circuittermination__path.py +0 -1
  24. nautobot/circuits/migrations/0014_related_name_changes.py +1 -2
  25. nautobot/circuits/migrations/0015_remove_circuittype_provider_slug.py +20 -0
  26. nautobot/circuits/migrations/0016_tagsfield.py +34 -0
  27. nautobot/circuits/migrations/0017_fixup_null_statuses.py +22 -0
  28. nautobot/circuits/migrations/0018_status_nonnullable.py +22 -0
  29. nautobot/circuits/models.py +3 -93
  30. nautobot/circuits/navigation.py +14 -69
  31. nautobot/circuits/signals.py +0 -2
  32. nautobot/circuits/tables.py +42 -5
  33. nautobot/circuits/templates/circuits/circuit_retrieve.html +1 -1
  34. nautobot/circuits/templates/circuits/circuittermination_retrieve.html +1 -1
  35. nautobot/circuits/templates/circuits/circuittype_retrieve.html +1 -1
  36. nautobot/circuits/templates/circuits/provider_create.html +0 -1
  37. nautobot/circuits/templates/circuits/provider_retrieve.html +1 -1
  38. nautobot/circuits/tests/integration/test_relationships.py +13 -16
  39. nautobot/circuits/tests/test_api.py +13 -43
  40. nautobot/circuits/tests/test_filters.py +20 -15
  41. nautobot/circuits/tests/test_models.py +7 -3
  42. nautobot/circuits/tests/test_views.py +57 -67
  43. nautobot/circuits/views.py +18 -9
  44. nautobot/core/api/__init__.py +8 -2
  45. nautobot/core/api/authentication.py +0 -3
  46. nautobot/core/api/fields.py +15 -6
  47. nautobot/core/api/filter_backends.py +3 -2
  48. nautobot/core/api/metadata.py +237 -30
  49. nautobot/core/api/mixins.py +94 -0
  50. nautobot/core/api/pagination.py +3 -3
  51. nautobot/core/api/parsers.py +154 -0
  52. nautobot/core/api/renderers.py +153 -2
  53. nautobot/core/api/schema.py +47 -3
  54. nautobot/core/api/serializers.py +377 -37
  55. nautobot/core/api/urls.py +11 -3
  56. nautobot/core/api/utils.py +174 -2
  57. nautobot/core/api/versioning.py +32 -10
  58. nautobot/core/api/views.py +266 -75
  59. nautobot/core/apps/__init__.py +138 -221
  60. nautobot/core/celery/__init__.py +112 -41
  61. nautobot/core/celery/backends.py +19 -13
  62. nautobot/core/celery/control.py +46 -0
  63. nautobot/core/celery/encoders.py +53 -0
  64. nautobot/core/celery/log.py +38 -0
  65. nautobot/core/celery/schedulers.py +23 -4
  66. nautobot/core/celery/task.py +1 -16
  67. nautobot/core/checks.py +0 -27
  68. nautobot/core/choices.py +21 -113
  69. nautobot/core/{cli.py → cli/__init__.py} +1 -2
  70. nautobot/core/cli/__main__.py +3 -0
  71. nautobot/core/constants.py +25 -43
  72. nautobot/core/context_processors.py +12 -0
  73. nautobot/core/filters.py +2 -2
  74. nautobot/core/forms/__init__.py +0 -4
  75. nautobot/core/forms/fields.py +39 -68
  76. nautobot/core/forms/forms.py +27 -27
  77. nautobot/core/forms/utils.py +7 -59
  78. nautobot/core/forms/widgets.py +0 -1
  79. nautobot/core/graphql/__init__.py +2 -2
  80. nautobot/core/graphql/schema.py +4 -27
  81. nautobot/core/jobs/__init__.py +75 -0
  82. nautobot/core/management/commands/build_ui.py +255 -0
  83. nautobot/core/management/commands/celery.py +0 -1
  84. nautobot/core/management/commands/generate_test_data.py +18 -13
  85. nautobot/core/management/commands/post_upgrade.py +24 -24
  86. nautobot/core/management/commands/validate_models.py +0 -1
  87. nautobot/core/middleware.py +0 -1
  88. nautobot/core/models/__init__.py +26 -1
  89. nautobot/core/models/fields.py +24 -5
  90. nautobot/core/models/generics.py +2 -46
  91. nautobot/core/models/managers.py +5 -0
  92. nautobot/core/models/name_color_content_types.py +1 -19
  93. nautobot/core/models/tree_queries.py +14 -4
  94. nautobot/core/models/utils.py +9 -10
  95. nautobot/core/models/validators.py +17 -8
  96. nautobot/core/releases.py +8 -10
  97. nautobot/core/settings.py +81 -53
  98. nautobot/core/tables.py +5 -5
  99. nautobot/core/tasks.py +4 -7
  100. nautobot/core/templates/base.html +1 -49
  101. nautobot/core/templates/base_django.html +49 -0
  102. nautobot/core/templates/base_react.html +55 -0
  103. nautobot/core/templates/buttons/export.html +6 -4
  104. nautobot/core/templates/generic/object_bulk_create.html +10 -21
  105. nautobot/core/templates/generic/object_list.html +4 -1
  106. nautobot/core/templates/generic/object_retrieve_plugin_full_width.html +3 -0
  107. nautobot/core/templates/inc/footer.html +1 -0
  108. nautobot/core/templates/inc/javascript.html +0 -14
  109. nautobot/core/templates/inc/nav_menu.html +28 -33
  110. nautobot/core/templates/inc/object_details_advanced_panel.html +13 -0
  111. nautobot/core/templates/inc/relationships_table_rows.html +2 -2
  112. nautobot/core/templates/nautobot_config.py.j2 +8 -25
  113. nautobot/core/templates/plugin_template/__init__.py-tpl +1 -2
  114. nautobot/core/templates/rest_framework/api.html +8 -0
  115. nautobot/core/templatetags/buttons.py +32 -29
  116. nautobot/core/templatetags/helpers.py +1 -1
  117. nautobot/core/testing/__init__.py +47 -44
  118. nautobot/core/testing/api.py +365 -47
  119. nautobot/core/testing/filters.py +12 -7
  120. nautobot/core/testing/integration.py +1 -1
  121. nautobot/core/testing/migrations.py +2 -0
  122. nautobot/core/testing/mixins.py +22 -12
  123. nautobot/core/testing/schema.py +2 -1
  124. nautobot/core/testing/views.py +28 -51
  125. nautobot/core/tests/integration/test_filters.py +17 -8
  126. nautobot/core/tests/integration/test_navbar.py +11 -34
  127. nautobot/core/tests/integration/test_plugin_navbar.py +9 -103
  128. nautobot/core/tests/nautobot_config.py +2 -3
  129. nautobot/core/tests/runner.py +0 -1
  130. nautobot/core/tests/test_api.py +290 -24
  131. nautobot/core/tests/test_authentication.py +57 -14
  132. nautobot/core/tests/test_checks.py +0 -7
  133. nautobot/core/tests/test_choices.py +0 -1
  134. nautobot/core/tests/test_filters.py +117 -110
  135. nautobot/core/tests/test_forms.py +47 -110
  136. nautobot/core/tests/test_graphql.py +158 -135
  137. nautobot/core/tests/test_logging.py +4 -1
  138. nautobot/core/tests/test_managers.py +3 -5
  139. nautobot/core/tests/test_models.py +2 -0
  140. nautobot/core/tests/test_ordering.py +0 -2
  141. nautobot/core/tests/test_paginator.py +3 -1
  142. nautobot/core/tests/test_releases.py +12 -12
  143. nautobot/core/tests/test_templatetags_helpers.py +7 -4
  144. nautobot/core/tests/test_utils.py +112 -78
  145. nautobot/core/tests/test_views.py +12 -17
  146. nautobot/core/tests/test_views_utils.py +6 -9
  147. nautobot/core/utils/data.py +17 -0
  148. nautobot/core/utils/deprecation.py +13 -20
  149. nautobot/core/utils/filtering.py +53 -9
  150. nautobot/core/utils/git.py +12 -4
  151. nautobot/core/utils/lookup.py +3 -1
  152. nautobot/core/utils/requests.py +23 -116
  153. nautobot/core/views/__init__.py +1 -2
  154. nautobot/core/views/generic.py +131 -119
  155. nautobot/core/views/mixins.py +53 -62
  156. nautobot/core/views/paginator.py +0 -1
  157. nautobot/core/views/renderers.py +14 -12
  158. nautobot/core/views/utils.py +87 -4
  159. nautobot/dcim/api/serializers.py +160 -672
  160. nautobot/dcim/api/urls.py +1 -1
  161. nautobot/dcim/api/views.py +7 -46
  162. nautobot/dcim/choices.py +2 -25
  163. nautobot/dcim/elevations.py +0 -1
  164. nautobot/dcim/factory.py +15 -4
  165. nautobot/dcim/filters/__init__.py +42 -13
  166. nautobot/dcim/form_mixins.py +1 -27
  167. nautobot/dcim/forms.py +58 -797
  168. nautobot/dcim/management/commands/trace_paths.py +0 -1
  169. nautobot/dcim/migrations/0001_initial_part_1.py +0 -1
  170. nautobot/dcim/migrations/0002_initial_part_2.py +0 -1
  171. nautobot/dcim/migrations/0003_initial_part_3.py +0 -1
  172. nautobot/dcim/migrations/0004_initial_part_4.py +0 -1
  173. nautobot/dcim/migrations/0005_device_local_context_schema.py +0 -1
  174. nautobot/dcim/migrations/0006_auto_slug.py +0 -1
  175. nautobot/dcim/migrations/0007_device_secrets_group.py +0 -1
  176. nautobot/dcim/migrations/0008_increase_all_serial_lengths.py +0 -1
  177. nautobot/dcim/migrations/0009_add_natural_indexing.py +0 -1
  178. nautobot/dcim/migrations/0010_interface_status.py +0 -1
  179. nautobot/dcim/migrations/0011_interface_status_data_migration.py +0 -1
  180. nautobot/dcim/migrations/0012_interface_parent_bridge.py +0 -1
  181. nautobot/dcim/migrations/0013_location_location_type.py +0 -1
  182. nautobot/dcim/migrations/0014_location_status_data_migration.py +0 -1
  183. nautobot/dcim/migrations/0015_device_components__changeloggedmodel.py +0 -1
  184. nautobot/dcim/migrations/0016_device_components__timestamp_data_migration.py +0 -1
  185. nautobot/dcim/migrations/0017_locationtype_nestable.py +0 -1
  186. nautobot/dcim/migrations/0018_device_redundancy_group.py +0 -1
  187. nautobot/dcim/migrations/0019_device_redundancy_group_data_migration.py +0 -1
  188. nautobot/dcim/migrations/0020_move_site_fields_to_location_model.py +0 -1
  189. nautobot/dcim/migrations/0021_mptt_to_tree_queries.py +0 -1
  190. nautobot/dcim/migrations/0022_interface_mac_address_data_migration.py +0 -1
  191. nautobot/dcim/migrations/0023_alter_interface_mac_address.py +0 -1
  192. nautobot/dcim/migrations/0024_alter_device_and_rack_role_add_new_role.py +2 -2
  193. nautobot/dcim/migrations/0025_device_and_rack_roles_data_migrations.py +19 -14
  194. nautobot/dcim/migrations/0026_rename_device_and_rack_role.py +0 -1
  195. nautobot/dcim/migrations/0027_remove_device_role_and_rack_role.py +1 -2
  196. nautobot/dcim/migrations/0028_rename_foreignkey_fields.py +1 -2
  197. nautobot/dcim/migrations/0029_add_tree_managers_and_foreign_keys_pre_data_migration.py +0 -1
  198. nautobot/dcim/migrations/0030_migrate_region_and_site_data_to_locations.py +2 -2
  199. nautobot/dcim/migrations/0031_rename_path_end_point_related_name.py +0 -1
  200. nautobot/dcim/migrations/0032_remove_site_foreign_key_from_dcim_models.py +0 -1
  201. nautobot/dcim/migrations/0033_created_datetime.py +0 -1
  202. nautobot/dcim/migrations/0034_fixup_fks_and_related_names.py +0 -1
  203. nautobot/dcim/migrations/0035_related_name_changes.py +1 -2
  204. nautobot/dcim/migrations/0036_remove_region_and_site.py +1 -2
  205. nautobot/dcim/migrations/0037_interface_ip_addresses_m2m.py +0 -1
  206. nautobot/dcim/migrations/0038_alter_location_managers.py +0 -1
  207. nautobot/dcim/migrations/0039_remove_slug.py +24 -0
  208. nautobot/dcim/migrations/0040_tagsfield.py +109 -0
  209. nautobot/dcim/migrations/0041_ipam__namespaces.py +25 -0
  210. nautobot/dcim/migrations/0042_fixup_null_statuses.py +51 -0
  211. nautobot/dcim/migrations/0043_status_nonnullable.py +72 -0
  212. nautobot/dcim/models/cables.py +4 -35
  213. nautobot/dcim/models/device_component_templates.py +7 -2
  214. nautobot/dcim/models/device_components.py +26 -203
  215. nautobot/dcim/models/devices.py +30 -152
  216. nautobot/dcim/models/locations.py +3 -64
  217. nautobot/dcim/models/power.py +3 -51
  218. nautobot/dcim/models/racks.py +7 -86
  219. nautobot/dcim/navigation.py +141 -467
  220. nautobot/dcim/signals.py +0 -2
  221. nautobot/dcim/tables/devices.py +8 -5
  222. nautobot/dcim/tables/devicetypes.py +1 -1
  223. nautobot/dcim/tables/locations.py +2 -2
  224. nautobot/dcim/tables/power.py +2 -2
  225. nautobot/dcim/templates/dcim/console_port_connection_list.html +7 -0
  226. nautobot/dcim/templates/dcim/device.html +15 -4
  227. nautobot/dcim/templates/dcim/device_edit.html +6 -0
  228. nautobot/dcim/templates/dcim/deviceredundancygroup_create.html +0 -1
  229. nautobot/dcim/templates/dcim/devicetype.html +2 -2
  230. nautobot/dcim/templates/dcim/interface.html +4 -0
  231. nautobot/dcim/templates/dcim/interface_connection_list.html +7 -0
  232. nautobot/dcim/templates/dcim/interface_edit.html +1 -0
  233. nautobot/dcim/templates/dcim/location.html +16 -1
  234. nautobot/dcim/templates/dcim/locationtype.html +15 -0
  235. nautobot/dcim/templates/dcim/power_port_connection_list.html +7 -0
  236. nautobot/dcim/templates/dcim/rackgroup.html +0 -12
  237. nautobot/dcim/tests/integration/test_cable_connect_form.py +4 -4
  238. nautobot/dcim/tests/test_api.py +202 -130
  239. nautobot/dcim/tests/test_cablepaths.py +47 -42
  240. nautobot/dcim/tests/test_filters.py +156 -134
  241. nautobot/dcim/tests/test_forms.py +12 -213
  242. nautobot/dcim/tests/test_graphql.py +8 -3
  243. nautobot/dcim/tests/test_migrations.py +6 -11
  244. nautobot/dcim/tests/test_models.py +208 -158
  245. nautobot/dcim/tests/test_natural_ordering.py +12 -14
  246. nautobot/dcim/tests/test_signals.py +7 -4
  247. nautobot/dcim/tests/test_views.py +270 -264
  248. nautobot/dcim/urls.py +21 -26
  249. nautobot/dcim/views.py +14 -156
  250. nautobot/docs/additional-features/caching.md +6 -87
  251. nautobot/docs/additional-features/job-scheduling-and-approvals.md +3 -0
  252. nautobot/docs/additional-features/jobs.md +179 -197
  253. nautobot/docs/administration/nautobot-server.md +9 -24
  254. nautobot/docs/administration/nautobot-shell.md +6 -6
  255. nautobot/docs/administration/replicating-nautobot.md +0 -10
  256. nautobot/docs/configuration/index.md +9 -9
  257. nautobot/docs/configuration/optional-settings.md +32 -61
  258. nautobot/docs/configuration/required-settings.md +11 -52
  259. nautobot/docs/development/application-registry.md +2 -13
  260. nautobot/docs/development/best-practices.md +2 -1
  261. nautobot/docs/development/docker-compose-advanced-use-cases.md +1 -1
  262. nautobot/docs/development/extending-models.md +15 -17
  263. nautobot/docs/development/generic-views.md +0 -2
  264. nautobot/docs/development/getting-started.md +56 -6
  265. nautobot/docs/development/navigation-menu.md +22 -93
  266. nautobot/docs/development/react-ui.md +105 -0
  267. nautobot/docs/development/release-checklist.md +3 -3
  268. nautobot/docs/development/role-internals.md +1 -3
  269. nautobot/docs/development/style-guide.md +6 -4
  270. nautobot/docs/development/templates.md +2 -1
  271. nautobot/docs/docker/index.md +16 -14
  272. nautobot/docs/index.md +7 -3
  273. nautobot/docs/installation/index.md +4 -1
  274. nautobot/docs/installation/migrating-from-netbox.md +12 -43
  275. nautobot/docs/installation/migrating-from-postgresql.md +1 -1
  276. nautobot/docs/installation/nautobot.md +1 -1
  277. nautobot/docs/installation/tables/v2-api-behavior-changes.yaml +70 -0
  278. nautobot/docs/installation/tables/v2-api-removed-fields.yaml +142 -0
  279. nautobot/docs/installation/tables/v2-api-renamed-fields.yaml +124 -0
  280. nautobot/docs/installation/tables/v2-code-location-changes.yaml +241 -0
  281. nautobot/docs/installation/tables/v2-code-removals.yaml +67 -0
  282. nautobot/docs/installation/tables/v2-database-behavior-changes.yaml +37 -0
  283. nautobot/docs/installation/tables/v2-database-removed-fields.yaml +166 -0
  284. nautobot/docs/installation/tables/v2-database-renamed-fields.yaml +340 -0
  285. nautobot/docs/installation/tables/v2-filters-corrected-fields.yaml +28 -0
  286. nautobot/docs/installation/tables/v2-filters-enhanced-fields.yaml +241 -0
  287. nautobot/docs/installation/tables/v2-filters-removed-fields.yaml +553 -0
  288. nautobot/docs/installation/tables/v2-filters-renamed-fields.yaml +223 -0
  289. nautobot/docs/installation/tables/v2-logging-renamed-loggers.yaml +23 -0
  290. nautobot/docs/installation/upgrading-from-nautobot-v1.md +190 -636
  291. nautobot/docs/installation/upgrading.md +5 -2
  292. nautobot/docs/models/dcim/device.md +3 -0
  293. nautobot/docs/models/dcim/deviceredundancygroup.md +3 -3
  294. nautobot/docs/models/extras/computedfield.md +4 -4
  295. nautobot/docs/models/extras/dynamicgroup.md +9 -9
  296. nautobot/docs/models/extras/gitrepository.md +3 -0
  297. nautobot/docs/models/extras/job.md +1 -0
  298. nautobot/docs/models/extras/jobbutton.md +18 -13
  299. nautobot/docs/models/extras/jobhook.md +7 -4
  300. nautobot/docs/models/extras/jobresult.md +6 -2
  301. nautobot/docs/models/extras/relationship.md +2 -2
  302. nautobot/docs/models/extras/status.md +6 -19
  303. nautobot/docs/models/ipam/ipaddress.md +3 -0
  304. nautobot/docs/models/ipam/vrf.md +0 -3
  305. nautobot/docs/models/virtualization/virtualmachine.md +3 -0
  306. nautobot/docs/plugins/development.md +92 -24
  307. nautobot/docs/release-notes/version-1.5.md +96 -0
  308. nautobot/docs/release-notes/version-2.0.md +216 -0
  309. nautobot/docs/requirements.txt +5 -4
  310. nautobot/docs/rest-api/overview.md +384 -215
  311. nautobot/docs/rest-api/ui-related-endpoints.md +9 -0
  312. nautobot/extras/admin.py +3 -5
  313. nautobot/extras/api/customfields.py +15 -39
  314. nautobot/extras/api/fields.py +0 -11
  315. nautobot/extras/api/mixins.py +45 -0
  316. nautobot/extras/api/relationships.py +63 -159
  317. nautobot/extras/api/serializers.py +165 -706
  318. nautobot/extras/api/urls.py +1 -1
  319. nautobot/extras/api/views.py +295 -282
  320. nautobot/extras/apps.py +4 -7
  321. nautobot/extras/choices.py +11 -22
  322. nautobot/extras/constants.py +9 -3
  323. nautobot/extras/datasources/__init__.py +2 -0
  324. nautobot/extras/datasources/git.py +135 -186
  325. nautobot/extras/datasources/registry.py +25 -35
  326. nautobot/extras/factory.py +1 -3
  327. nautobot/extras/filters/__init__.py +49 -47
  328. nautobot/extras/filters/mixins.py +10 -8
  329. nautobot/extras/forms/forms.py +72 -148
  330. nautobot/extras/forms/mixins.py +34 -57
  331. nautobot/extras/health_checks.py +0 -33
  332. nautobot/extras/jobs.py +387 -566
  333. nautobot/extras/management/__init__.py +55 -48
  334. nautobot/extras/management/commands/renaturalize.py +0 -1
  335. nautobot/extras/management/commands/runjob.py +24 -62
  336. nautobot/extras/management/commands/webhook_receiver.py +0 -1
  337. nautobot/extras/managers.py +30 -7
  338. nautobot/extras/migrations/0001_initial_part_1.py +0 -1
  339. nautobot/extras/migrations/0002_initial_part_2.py +0 -1
  340. nautobot/extras/migrations/0003_initial_part_3.py +0 -1
  341. nautobot/extras/migrations/0004_populate_default_status_records.py +0 -1
  342. nautobot/extras/migrations/0005_configcontext_device_types.py +0 -1
  343. nautobot/extras/migrations/0006_graphqlquery.py +0 -1
  344. nautobot/extras/migrations/0007_configcontextschema.py +0 -1
  345. nautobot/extras/migrations/0008_jobresult__custom_field_data.py +0 -1
  346. nautobot/extras/migrations/0009_computedfield.py +0 -1
  347. nautobot/extras/migrations/0010_change_cf_validation_max_min_field_to_bigint.py +0 -1
  348. nautobot/extras/migrations/0011_fileattachment_fileproxy.py +0 -1
  349. nautobot/extras/migrations/0012_healthchecktestmodel.py +0 -1
  350. nautobot/extras/migrations/0013_default_fallback_value_computedfield.py +0 -1
  351. nautobot/extras/migrations/0014_auto_slug.py +0 -1
  352. nautobot/extras/migrations/0015_scheduled_job.py +0 -1
  353. nautobot/extras/migrations/0016_secret.py +0 -1
  354. nautobot/extras/migrations/0017_joblogentry.py +0 -1
  355. nautobot/extras/migrations/0018_joblog_data_migration.py +0 -2
  356. nautobot/extras/migrations/0019_joblogentry__meta_options__related_name.py +0 -1
  357. nautobot/extras/migrations/0020_customfield_changelog.py +0 -1
  358. nautobot/extras/migrations/0021_customfield_changelog_data.py +0 -1
  359. nautobot/extras/migrations/0022_objectchange_object_datav2.py +0 -1
  360. nautobot/extras/migrations/0023_job_model.py +0 -1
  361. nautobot/extras/migrations/0024_job_data_migration.py +0 -1
  362. nautobot/extras/migrations/0025_add_advanced_ui_boolean_to_customfield_conputedfield_and_relationship.py +0 -1
  363. nautobot/extras/migrations/0026_job_add_gitrepository_fk.py +0 -1
  364. nautobot/extras/migrations/0027_job_gitrepository_data_migration.py +0 -1
  365. nautobot/extras/migrations/0028_job_reduce_source.py +0 -1
  366. nautobot/extras/migrations/0029_dynamicgroup.py +0 -1
  367. nautobot/extras/migrations/0030_webhook_alter_unique_together.py +0 -1
  368. nautobot/extras/migrations/0031_tag_content_types.py +0 -1
  369. nautobot/extras/migrations/0032_tag_content_types_data_migration.py +0 -1
  370. nautobot/extras/migrations/0033_add__optimized_indexing.py +0 -1
  371. nautobot/extras/migrations/0034_alter_fileattachment_mimetype.py +0 -1
  372. nautobot/extras/migrations/0035_scheduledjob_crontab.py +0 -1
  373. nautobot/extras/migrations/0036_job_add_has_sensitive_variables.py +0 -1
  374. nautobot/extras/migrations/0037_configcontextschema__remove_name_unique__create_constraint_unique_name_owner.py +0 -1
  375. nautobot/extras/migrations/0038_configcontext_locations.py +0 -1
  376. nautobot/extras/migrations/0039_objectchange__add_change_context.py +0 -1
  377. nautobot/extras/migrations/0040_dynamicgroup__dynamicgroupmembership.py +0 -1
  378. nautobot/extras/migrations/0041_jobresult_job_kwargs.py +0 -1
  379. nautobot/extras/migrations/0042_job__add_is_job_hook_receiver.py +0 -1
  380. nautobot/extras/migrations/0043_note.py +0 -1
  381. nautobot/extras/migrations/0044_add_job_hook.py +0 -1
  382. nautobot/extras/migrations/0045_add_custom_field_slug.py +0 -1
  383. nautobot/extras/migrations/0046_populate_custom_field_slug_label.py +0 -1
  384. nautobot/extras/migrations/0047_enforce_custom_field_slug.py +0 -1
  385. nautobot/extras/migrations/0048_alter_objectchange_change_context_detail.py +0 -1
  386. nautobot/extras/migrations/0049_alter_tag_slug.py +0 -1
  387. nautobot/extras/migrations/0050_customfield_grouping.py +0 -1
  388. nautobot/extras/migrations/0051_add_job_task_queues.py +0 -1
  389. nautobot/extras/migrations/0052_configcontext_device_redundancy_groups.py +0 -1
  390. nautobot/extras/migrations/0053_relationship_required_on.py +0 -1
  391. nautobot/extras/migrations/0054_scheduledjob_kwargs_request_user_change.py +0 -1
  392. nautobot/extras/migrations/0055_configcontext_dynamic_groups.py +0 -1
  393. nautobot/extras/migrations/0056_objectchange_add_reverse_time_idx.py +0 -1
  394. nautobot/extras/migrations/0057_jobbutton.py +0 -1
  395. nautobot/extras/migrations/0058_jobresult_add_time_status_idxs.py +38 -0
  396. nautobot/extras/migrations/{0058_joblogentry_scheduledjob_webhook_data_migration.py → 0059_joblogentry_scheduledjob_webhook_data_migration.py} +1 -2
  397. nautobot/extras/migrations/{0059_alter_joblogentry_scheduledjob_webhook_fields.py → 0060_alter_joblogentry_scheduledjob_webhook_fields.py} +1 -2
  398. nautobot/extras/migrations/{0060_role_and_alter_status.py → 0061_role_and_alter_status.py} +1 -8
  399. nautobot/extras/migrations/{0061_collect_roles_from_related_apps_roles.py → 0062_collect_roles_from_related_apps_roles.py} +33 -33
  400. nautobot/extras/migrations/{0062_alter_role_options.py → 0063_alter_role_options.py} +1 -2
  401. nautobot/extras/migrations/{0063_alter_configcontext_and_add_new_role.py → 0064_alter_configcontext_and_add_new_role.py} +1 -2
  402. nautobot/extras/migrations/0065_configcontext_data_migrations.py +44 -0
  403. nautobot/extras/migrations/{0065_rename_configcontext_role.py → 0066_rename_configcontext_role.py} +1 -2
  404. nautobot/extras/migrations/{0066_jobresult__add_celery_fields.py → 0067_jobresult__add_celery_fields.py} +36 -3
  405. nautobot/extras/migrations/{0067_created_datetime.py → 0068_created_datetime.py} +1 -2
  406. nautobot/extras/migrations/{0068_remove_site_and_region_attributes_from_config_context.py → 0069_remove_site_and_region_attributes_from_config_context.py} +1 -2
  407. nautobot/extras/migrations/{0069_replace_related_names.py → 0070_replace_related_names.py} +1 -1
  408. nautobot/extras/migrations/{0070_rename_model_fields.py → 0071_rename_model_fields.py} +1 -2
  409. nautobot/extras/migrations/0072_job__unique_name_data_migration.py +86 -0
  410. nautobot/extras/migrations/{0072_job__unique_name.py → 0073_job__unique_name.py} +13 -10
  411. nautobot/extras/migrations/{0073_remove_gitrepository_fields.py → 0074_remove_gitrepository_fields.py} +1 -2
  412. nautobot/extras/migrations/{0074_rename_slug_to_key_for_custom_field.py → 0075_rename_slug_to_key_for_custom_field.py} +1 -1
  413. nautobot/extras/migrations/{0075_migrate_custom_field_data.py → 0076_migrate_custom_field_data.py} +1 -1
  414. 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
  415. nautobot/extras/migrations/0078_remove_slug.py +45 -0
  416. nautobot/extras/migrations/0079_tagsfield.py +28 -0
  417. nautobot/extras/migrations/0080_rename_relationship_slug_to_key.py +17 -0
  418. nautobot/extras/migrations/0081_rename_relationship_name_to_label.py +29 -0
  419. nautobot/extras/migrations/0082_ensure_relationship_keys_are_unique.py +43 -0
  420. nautobot/extras/migrations/0083_rename_computed_field_slug_to_key.py +21 -0
  421. nautobot/extras/migrations/0084_taggeditem_cleanup.py +43 -0
  422. nautobot/extras/migrations/0085_taggeditem_uniqueness.py +22 -0
  423. nautobot/extras/migrations/0086_job__celery_task_fields__dryrun_support.py +81 -0
  424. nautobot/extras/migrations/0087_job__commit_default_data_migration.py +26 -0
  425. nautobot/extras/migrations/0088_joblogentry__log_level_default.py +17 -0
  426. nautobot/extras/migrations/0089_joblogentry__log_level_data_migration.py +34 -0
  427. nautobot/extras/migrations/0090_scheduledjob__data_migration.py +57 -0
  428. nautobot/extras/models/__init__.py +2 -3
  429. nautobot/extras/models/change_logging.py +0 -36
  430. nautobot/extras/models/customfields.py +39 -33
  431. nautobot/extras/models/datasources.py +48 -50
  432. nautobot/extras/models/groups.py +5 -12
  433. nautobot/extras/models/jobs.py +190 -323
  434. nautobot/extras/models/mixins.py +0 -71
  435. nautobot/extras/models/models.py +1 -22
  436. nautobot/extras/models/relationships.py +20 -21
  437. nautobot/extras/models/roles.py +0 -23
  438. nautobot/extras/models/secrets.py +2 -31
  439. nautobot/extras/models/statuses.py +6 -5
  440. nautobot/extras/models/tags.py +2 -17
  441. nautobot/extras/navigation.py +89 -307
  442. nautobot/extras/plugins/__init__.py +3 -121
  443. nautobot/extras/plugins/utils.py +0 -3
  444. nautobot/extras/plugins/validators.py +5 -4
  445. nautobot/extras/plugins/views.py +16 -4
  446. nautobot/extras/querysets.py +1 -7
  447. nautobot/extras/registry.py +3 -0
  448. nautobot/extras/signals.py +26 -60
  449. nautobot/extras/tables.py +42 -49
  450. nautobot/extras/tasks.py +0 -12
  451. nautobot/extras/templates/extras/configcontext.html +1 -1
  452. nautobot/extras/templates/extras/configcontextschema.html +16 -1
  453. nautobot/extras/templates/extras/customfield.html +0 -13
  454. nautobot/extras/templates/extras/dynamicgroup_edit.html +0 -1
  455. nautobot/extras/templates/extras/gitrepository.html +3 -3
  456. nautobot/extras/templates/extras/inc/jobresult.html +10 -0
  457. nautobot/extras/templates/extras/inc/panel_jobhistory.html +1 -1
  458. nautobot/extras/templates/extras/job.html +35 -25
  459. nautobot/extras/templates/extras/job_approval_request.html +15 -30
  460. nautobot/extras/templates/extras/job_detail.html +13 -31
  461. nautobot/extras/templates/extras/job_edit.html +14 -17
  462. nautobot/extras/templates/extras/jobresult.html +24 -6
  463. nautobot/extras/templates/extras/objectchange_list.html +1 -1
  464. nautobot/extras/templates/extras/scheduledjob.html +2 -2
  465. nautobot/extras/templates/extras/secret.html +28 -0
  466. nautobot/extras/templates/extras/secret_edit.html +0 -1
  467. nautobot/extras/templates/extras/secretsgroup_edit.html +0 -1
  468. nautobot/extras/templatetags/custom_links.py +0 -2
  469. nautobot/extras/templatetags/job_buttons.py +1 -0
  470. nautobot/extras/templatetags/plugins.py +0 -1
  471. nautobot/extras/{tests/example_jobs → test_jobs}/api_test_job.py +13 -6
  472. nautobot/extras/test_jobs/atomic_transaction.py +53 -0
  473. nautobot/extras/test_jobs/dry_run.py +29 -0
  474. nautobot/extras/{tests/example_jobs/test_duplicate_name.py → test_jobs/duplicate_name.py} +4 -0
  475. nautobot/extras/test_jobs/duplicate_name2.py +9 -0
  476. nautobot/extras/test_jobs/fail.py +23 -0
  477. nautobot/extras/{tests/example_jobs/test_field_default.py → test_jobs/field_default.py} +4 -0
  478. nautobot/extras/{tests/example_jobs/test_field_order.py → test_jobs/field_order.py} +4 -0
  479. nautobot/extras/{tests/example_jobs/test_file_upload_fail.py → test_jobs/file_upload_fail.py} +11 -6
  480. nautobot/extras/test_jobs/file_upload_pass.py +25 -0
  481. nautobot/extras/test_jobs/has_sensitive_variables.py +25 -0
  482. nautobot/extras/test_jobs/ipaddress_vars.py +66 -0
  483. nautobot/extras/test_jobs/job_button_receiver.py +28 -0
  484. nautobot/extras/test_jobs/job_hook_receiver.py +29 -0
  485. nautobot/extras/test_jobs/job_variables.py +88 -0
  486. nautobot/extras/test_jobs/location_with_custom_field.py +45 -0
  487. nautobot/extras/test_jobs/log_redaction.py +20 -0
  488. nautobot/extras/test_jobs/log_skip_db_logging.py +17 -0
  489. nautobot/extras/test_jobs/modify_db.py +25 -0
  490. nautobot/extras/{tests/example_jobs/test_no_field_order.py → test_jobs/no_field_order.py} +4 -0
  491. nautobot/extras/test_jobs/object_var_optional.py +21 -0
  492. nautobot/extras/test_jobs/object_var_required.py +21 -0
  493. nautobot/extras/test_jobs/object_vars.py +26 -0
  494. nautobot/extras/test_jobs/pass.py +25 -0
  495. nautobot/extras/test_jobs/profiling.py +32 -0
  496. nautobot/extras/test_jobs/read_only_job.py +15 -0
  497. nautobot/extras/{tests/example_jobs/test_required_args.py → test_jobs/required_args.py} +4 -0
  498. 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
  499. nautobot/extras/{tests/example_jobs/test_task_queues.py → test_jobs/task_queues.py} +5 -1
  500. nautobot/extras/tests/integration/__init__.py +3 -3
  501. nautobot/extras/tests/integration/test_computedfields.py +1 -1
  502. nautobot/extras/tests/integration/test_configcontextschema.py +7 -5
  503. nautobot/extras/tests/integration/test_customfields.py +4 -2
  504. nautobot/extras/tests/integration/test_dynamicgroups.py +2 -2
  505. nautobot/extras/tests/integration/test_jobs.py +25 -27
  506. nautobot/extras/tests/integration/test_notes.py +8 -4
  507. nautobot/extras/tests/integration/test_plugins.py +4 -4
  508. nautobot/extras/tests/integration/test_relationships.py +2 -2
  509. nautobot/extras/tests/test_api.py +371 -381
  510. nautobot/extras/tests/test_changelog.py +17 -16
  511. nautobot/extras/tests/test_context_managers.py +5 -6
  512. nautobot/extras/tests/test_customfields.py +112 -73
  513. nautobot/extras/tests/test_datasources.py +191 -117
  514. nautobot/extras/tests/test_dynamicgroups.py +45 -68
  515. nautobot/extras/tests/test_filters.py +170 -130
  516. nautobot/extras/tests/test_forms.py +107 -109
  517. nautobot/extras/tests/{test_scripts.py → test_job_variables.py} +43 -49
  518. nautobot/extras/tests/test_jobs.py +271 -273
  519. nautobot/extras/tests/test_management.py +3 -6
  520. nautobot/extras/tests/test_migrations.py +5 -3
  521. nautobot/extras/tests/test_models.py +121 -173
  522. nautobot/extras/tests/test_notes.py +0 -1
  523. nautobot/extras/tests/test_plugins.py +55 -89
  524. nautobot/extras/tests/test_relationships.py +174 -130
  525. nautobot/extras/tests/test_tags.py +6 -12
  526. nautobot/extras/tests/test_utils.py +31 -1
  527. nautobot/extras/tests/test_views.py +223 -184
  528. nautobot/extras/tests/test_webhooks.py +16 -15
  529. nautobot/extras/urls.py +69 -69
  530. nautobot/extras/utils.py +137 -163
  531. nautobot/extras/views.py +81 -153
  532. nautobot/ipam/api/fields.py +17 -0
  533. nautobot/ipam/api/serializers.py +77 -164
  534. nautobot/ipam/api/urls.py +4 -1
  535. nautobot/ipam/api/views.py +28 -19
  536. nautobot/ipam/apps.py +1 -0
  537. nautobot/ipam/choices.py +5 -12
  538. nautobot/ipam/constants.py +1 -0
  539. nautobot/ipam/factory.py +41 -30
  540. nautobot/ipam/filters.py +58 -25
  541. nautobot/ipam/forms.py +82 -211
  542. nautobot/ipam/graphql/types.py +0 -9
  543. nautobot/ipam/lookups.py +13 -8
  544. nautobot/ipam/management/commands/__init__.py +0 -0
  545. nautobot/ipam/management/commands/fix_prefix_broadcast.py +17 -0
  546. nautobot/ipam/migrations/0001_initial_part_1.py +0 -1
  547. nautobot/ipam/migrations/0002_initial_part_2.py +0 -1
  548. nautobot/ipam/migrations/0003_remove_max_length.py +0 -1
  549. nautobot/ipam/migrations/0004_fixup_p2p_broadcast.py +0 -1
  550. nautobot/ipam/migrations/0005_auto_slug.py +0 -1
  551. nautobot/ipam/migrations/0006_ipaddress_nat_outside_list.py +0 -1
  552. nautobot/ipam/migrations/0007_add_natural_indexing.py +0 -1
  553. nautobot/ipam/migrations/0008_prefix_vlan_vlangroup_location.py +0 -1
  554. nautobot/ipam/migrations/0009_alter_vlan_name.py +0 -1
  555. nautobot/ipam/migrations/0010_alter_ipam_role_add_new_role.py +1 -2
  556. nautobot/ipam/migrations/0011_migrate_ipam_role_data.py +32 -39
  557. nautobot/ipam/migrations/0012_rename_ipam_roles.py +0 -1
  558. nautobot/ipam/migrations/0013_delete_role.py +0 -1
  559. nautobot/ipam/migrations/0014_rename_foreign_keys_and_related_names.py +0 -1
  560. nautobot/ipam/migrations/0015_prefix_add_type.py +0 -1
  561. nautobot/ipam/migrations/0016_prefix_type_data_migration.py +0 -3
  562. nautobot/ipam/migrations/0017_prefix_remove_is_pool.py +0 -1
  563. nautobot/ipam/migrations/0018_remove_site_foreign_key_from_ipam_models.py +0 -1
  564. nautobot/ipam/migrations/0019_created_datetime.py +0 -1
  565. nautobot/ipam/migrations/0020_related_name_changes.py +1 -2
  566. nautobot/ipam/migrations/0021_prefix_add_rir_and_date_allocated.py +0 -1
  567. nautobot/ipam/migrations/0022_aggregate_to_prefix_data_migration.py +3 -5
  568. nautobot/ipam/migrations/0023_delete_aggregate.py +0 -1
  569. nautobot/ipam/migrations/0024_interface_to_ipaddress_m2m.py +0 -1
  570. nautobot/ipam/migrations/0025_interface_ipaddress_m2m_data_migration.py +0 -1
  571. nautobot/ipam/migrations/0026_ipaddress_remove_assigned_object.py +0 -1
  572. nautobot/ipam/migrations/0027_remove_rir_slug.py +16 -0
  573. nautobot/ipam/migrations/0028_tagsfield.py +44 -0
  574. nautobot/ipam/migrations/0029_ip_address_to_interface_uniqueness_constraints.py +18 -0
  575. nautobot/ipam/migrations/0030_ipam__namespaces.py +231 -0
  576. nautobot/ipam/migrations/0031_ipam__prefix__add_parent.py +58 -0
  577. nautobot/ipam/migrations/0032_ipam__namespaces_finish.py +63 -0
  578. nautobot/ipam/migrations/0033_fixup_null_statuses.py +26 -0
  579. nautobot/ipam/migrations/0034_status_nonnullable.py +36 -0
  580. nautobot/ipam/models.py +579 -368
  581. nautobot/ipam/navigation.py +36 -159
  582. nautobot/ipam/querysets.py +117 -90
  583. nautobot/ipam/signals.py +89 -0
  584. nautobot/ipam/tables.py +86 -28
  585. nautobot/ipam/templates/ipam/ipaddress.html +14 -30
  586. nautobot/ipam/templates/ipam/ipaddress_edit.html +1 -0
  587. nautobot/ipam/templates/ipam/namespace_ipaddresses.html +11 -0
  588. nautobot/ipam/templates/ipam/namespace_prefixes.html +11 -0
  589. nautobot/ipam/templates/ipam/namespace_retrieve.html +42 -0
  590. nautobot/ipam/templates/ipam/namespace_vrfs.html +11 -0
  591. nautobot/ipam/templates/ipam/prefix.html +27 -33
  592. nautobot/ipam/templates/ipam/prefix_edit.html +7 -1
  593. nautobot/ipam/templates/ipam/vlangroup.html +0 -13
  594. nautobot/ipam/templates/ipam/vrf.html +6 -4
  595. nautobot/ipam/templates/ipam/vrf_edit.html +20 -2
  596. nautobot/ipam/tests/integration/test_prefixes.py +4 -27
  597. nautobot/ipam/tests/test_api.py +60 -61
  598. nautobot/ipam/tests/test_filters.py +187 -126
  599. nautobot/ipam/tests/test_forms.py +12 -6
  600. nautobot/ipam/tests/test_graphql.py +8 -6
  601. nautobot/ipam/tests/test_migrations.py +8 -13
  602. nautobot/ipam/tests/test_models.py +426 -274
  603. nautobot/ipam/tests/test_ordering.py +6 -3
  604. nautobot/ipam/tests/test_querysets.py +340 -96
  605. nautobot/ipam/tests/test_views.py +100 -55
  606. nautobot/ipam/urls.py +28 -5
  607. nautobot/ipam/{utils.py → utils/__init__.py} +2 -2
  608. nautobot/ipam/utils/migrations.py +713 -0
  609. nautobot/ipam/views.py +237 -122
  610. nautobot/project-static/docs/404.html +1399 -166
  611. nautobot/project-static/docs/additional-features/caching.html +1416 -320
  612. nautobot/project-static/docs/additional-features/change-logging.html +1389 -190
  613. nautobot/project-static/docs/additional-features/config-contexts.html +1389 -190
  614. nautobot/project-static/docs/additional-features/graphql.html +1389 -190
  615. nautobot/project-static/docs/additional-features/healthcheck.html +1389 -190
  616. nautobot/project-static/docs/additional-features/job-scheduling-and-approvals.html +1393 -190
  617. nautobot/project-static/docs/additional-features/jobs.html +1677 -460
  618. nautobot/project-static/docs/additional-features/napalm.html +1389 -190
  619. nautobot/project-static/docs/additional-features/prometheus-metrics.html +1389 -190
  620. nautobot/project-static/docs/additional-features/template-filters.html +1389 -190
  621. nautobot/project-static/docs/administration/celery-queues.html +1389 -190
  622. nautobot/project-static/docs/administration/nautobot-server.html +1553 -375
  623. nautobot/project-static/docs/administration/nautobot-shell.html +1395 -196
  624. nautobot/project-static/docs/administration/permissions.html +1389 -190
  625. nautobot/project-static/docs/administration/replicating-nautobot.html +1387 -207
  626. nautobot/project-static/docs/apps/index.html +1389 -190
  627. nautobot/project-static/docs/apps/nautobot-apps.html +1387 -175
  628. nautobot/project-static/docs/assets/javascripts/bundle.51198bba.min.js +29 -0
  629. nautobot/project-static/docs/assets/javascripts/bundle.51198bba.min.js.map +8 -0
  630. nautobot/project-static/docs/assets/javascripts/workers/{search.16e2a7d4.min.js → search.208ed371.min.js} +9 -15
  631. nautobot/project-static/docs/assets/javascripts/workers/{search.16e2a7d4.min.js.map → search.208ed371.min.js.map} +4 -4
  632. nautobot/project-static/docs/assets/stylesheets/main.ded33207.min.css +1 -0
  633. nautobot/project-static/docs/assets/stylesheets/main.ded33207.min.css.map +1 -0
  634. nautobot/project-static/docs/assets/stylesheets/palette.a0c5b2b5.min.css +1 -0
  635. nautobot/project-static/docs/assets/stylesheets/palette.a0c5b2b5.min.css.map +1 -0
  636. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +1775 -590
  637. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +1389 -190
  638. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +3588 -1922
  639. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +1461 -262
  640. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +1401 -170
  641. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +1396 -191
  642. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +2095 -894
  643. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +2357 -1194
  644. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +2258 -940
  645. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +1389 -190
  646. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +1400 -201
  647. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +11068 -7861
  648. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +2867 -2224
  649. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +1389 -190
  650. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +2641 -1573
  651. nautobot/project-static/docs/configuration/authentication/ldap.html +1389 -190
  652. nautobot/project-static/docs/configuration/authentication/remote.html +1389 -190
  653. nautobot/project-static/docs/configuration/authentication/sso.html +1389 -190
  654. nautobot/project-static/docs/configuration/index.html +1398 -199
  655. nautobot/project-static/docs/configuration/optional-settings.html +1418 -274
  656. nautobot/project-static/docs/configuration/required-settings.html +1419 -287
  657. nautobot/project-static/docs/core-functionality/circuits.html +1446 -247
  658. nautobot/project-static/docs/core-functionality/device-types.html +1448 -249
  659. nautobot/project-static/docs/core-functionality/devices.html +1452 -249
  660. nautobot/project-static/docs/core-functionality/ipam.html +1452 -253
  661. nautobot/project-static/docs/core-functionality/power.html +1448 -249
  662. nautobot/project-static/docs/core-functionality/secrets.html +1448 -249
  663. nautobot/project-static/docs/core-functionality/services.html +1448 -249
  664. nautobot/project-static/docs/core-functionality/sites-and-racks.html +1448 -249
  665. nautobot/project-static/docs/core-functionality/tenancy.html +1448 -249
  666. nautobot/project-static/docs/core-functionality/virtualization.html +1452 -249
  667. nautobot/project-static/docs/core-functionality/vlans.html +1448 -249
  668. nautobot/project-static/docs/development/application-registry.html +1393 -214
  669. nautobot/project-static/docs/development/best-practices.html +1392 -192
  670. nautobot/project-static/docs/development/docker-compose-advanced-use-cases.html +1390 -191
  671. nautobot/project-static/docs/development/extending-models.html +1443 -257
  672. nautobot/project-static/docs/development/generic-views.html +1403 -174
  673. nautobot/project-static/docs/development/getting-started.html +1568 -262
  674. nautobot/project-static/docs/development/homepage.html +1389 -190
  675. nautobot/project-static/docs/development/index.html +1389 -190
  676. nautobot/project-static/docs/development/model-features.html +1389 -190
  677. nautobot/project-static/docs/development/natural-keys.html +1389 -190
  678. nautobot/project-static/docs/development/navigation-menu.html +1451 -330
  679. nautobot/project-static/docs/development/react-ui.html +4199 -0
  680. nautobot/project-static/docs/development/release-checklist.html +1392 -193
  681. nautobot/project-static/docs/development/role-internals.html +1402 -172
  682. nautobot/project-static/docs/development/style-guide.html +1399 -199
  683. nautobot/project-static/docs/development/templates.html +1391 -191
  684. nautobot/project-static/docs/development/testing.html +1389 -190
  685. nautobot/project-static/docs/development/user-preferences.html +1389 -190
  686. nautobot/project-static/docs/docker/index.html +1408 -206
  687. nautobot/project-static/docs/index.html +1397 -180
  688. nautobot/project-static/docs/installation/centos.html +1401 -170
  689. nautobot/project-static/docs/installation/external-authentication.html +1389 -190
  690. nautobot/project-static/docs/installation/http-server.html +1389 -190
  691. nautobot/project-static/docs/installation/index.html +1394 -191
  692. nautobot/project-static/docs/installation/migrating-from-netbox.html +1452 -305
  693. nautobot/project-static/docs/installation/migrating-from-postgresql.html +1390 -191
  694. nautobot/project-static/docs/installation/nautobot.html +1390 -191
  695. nautobot/project-static/docs/installation/region-and-site-data-migration-guide.html +1389 -190
  696. nautobot/project-static/docs/installation/selinux-troubleshooting.html +1401 -170
  697. nautobot/project-static/docs/installation/services.html +1389 -190
  698. nautobot/project-static/docs/installation/tables/v2-api-behavior-changes.yaml +70 -0
  699. nautobot/project-static/docs/installation/tables/v2-api-removed-fields.yaml +142 -0
  700. nautobot/project-static/docs/installation/tables/v2-api-renamed-fields.yaml +124 -0
  701. nautobot/project-static/docs/installation/tables/v2-code-location-changes.yaml +241 -0
  702. nautobot/project-static/docs/installation/tables/v2-code-removals.yaml +67 -0
  703. nautobot/project-static/docs/installation/tables/v2-database-behavior-changes.yaml +37 -0
  704. nautobot/project-static/docs/installation/tables/v2-database-removed-fields.yaml +166 -0
  705. nautobot/project-static/docs/installation/tables/v2-database-renamed-fields.yaml +340 -0
  706. nautobot/project-static/docs/installation/tables/v2-filters-corrected-fields.yaml +28 -0
  707. nautobot/project-static/docs/installation/tables/v2-filters-enhanced-fields.yaml +241 -0
  708. nautobot/project-static/docs/installation/tables/v2-filters-removed-fields.yaml +553 -0
  709. nautobot/project-static/docs/installation/tables/v2-filters-renamed-fields.yaml +223 -0
  710. nautobot/project-static/docs/installation/tables/v2-logging-renamed-loggers.yaml +23 -0
  711. nautobot/project-static/docs/installation/ubuntu.html +1401 -170
  712. nautobot/project-static/docs/installation/upgrading-from-nautobot-v1.html +4254 -1923
  713. nautobot/project-static/docs/installation/upgrading.html +1395 -192
  714. nautobot/project-static/docs/models/circuits/circuit.html +1427 -174
  715. nautobot/project-static/docs/models/circuits/circuittermination.html +1427 -174
  716. nautobot/project-static/docs/models/circuits/circuittype.html +1427 -174
  717. nautobot/project-static/docs/models/circuits/provider.html +1427 -174
  718. nautobot/project-static/docs/models/circuits/providernetwork.html +1427 -174
  719. nautobot/project-static/docs/models/dcim/cable.html +1458 -174
  720. nautobot/project-static/docs/models/dcim/consoleport.html +1427 -174
  721. nautobot/project-static/docs/models/dcim/consoleporttemplate.html +1427 -174
  722. nautobot/project-static/docs/models/dcim/consoleserverport.html +1427 -174
  723. nautobot/project-static/docs/models/dcim/consoleserverporttemplate.html +1427 -174
  724. nautobot/project-static/docs/models/dcim/device.html +1431 -174
  725. nautobot/project-static/docs/models/dcim/devicebay.html +1427 -174
  726. nautobot/project-static/docs/models/dcim/devicebaytemplate.html +1427 -174
  727. nautobot/project-static/docs/models/dcim/deviceredundancygroup.html +1522 -177
  728. nautobot/project-static/docs/models/dcim/devicetype.html +1427 -174
  729. nautobot/project-static/docs/models/dcim/frontport.html +1427 -174
  730. nautobot/project-static/docs/models/dcim/frontporttemplate.html +1427 -174
  731. nautobot/project-static/docs/models/dcim/interface.html +1427 -174
  732. nautobot/project-static/docs/models/dcim/interfacetemplate.html +1427 -174
  733. nautobot/project-static/docs/models/dcim/inventoryitem.html +1427 -174
  734. nautobot/project-static/docs/models/dcim/location.html +1427 -174
  735. nautobot/project-static/docs/models/dcim/locationtype.html +1427 -174
  736. nautobot/project-static/docs/models/dcim/manufacturer.html +1427 -174
  737. nautobot/project-static/docs/models/dcim/platform.html +1427 -174
  738. nautobot/project-static/docs/models/dcim/powerfeed.html +1425 -172
  739. nautobot/project-static/docs/models/dcim/poweroutlet.html +1427 -174
  740. nautobot/project-static/docs/models/dcim/poweroutlettemplate.html +1427 -174
  741. nautobot/project-static/docs/models/dcim/powerpanel.html +1425 -172
  742. nautobot/project-static/docs/models/dcim/powerport.html +1427 -174
  743. nautobot/project-static/docs/models/dcim/powerporttemplate.html +1427 -174
  744. nautobot/project-static/docs/models/dcim/rack.html +1427 -174
  745. nautobot/project-static/docs/models/dcim/rackgroup.html +1427 -174
  746. nautobot/project-static/docs/models/dcim/rackreservation.html +1427 -174
  747. nautobot/project-static/docs/models/dcim/rearport.html +1427 -174
  748. nautobot/project-static/docs/models/dcim/rearporttemplate.html +1427 -174
  749. nautobot/project-static/docs/models/dcim/region.html +1401 -170
  750. nautobot/project-static/docs/models/dcim/site.html +1401 -170
  751. nautobot/project-static/docs/models/dcim/virtualchassis.html +1425 -172
  752. nautobot/project-static/docs/models/extras/computedfield.html +1393 -194
  753. nautobot/project-static/docs/models/extras/configcontext.html +1465 -174
  754. nautobot/project-static/docs/models/extras/configcontextschema.html +1421 -168
  755. nautobot/project-static/docs/models/extras/customfield.html +1389 -190
  756. nautobot/project-static/docs/models/extras/customlink.html +1389 -190
  757. nautobot/project-static/docs/models/extras/dynamicgroup.html +1398 -199
  758. nautobot/project-static/docs/models/extras/exporttemplate.html +1389 -190
  759. nautobot/project-static/docs/models/extras/gitrepository.html +1393 -190
  760. nautobot/project-static/docs/models/extras/graphqlquery.html +1469 -171
  761. nautobot/project-static/docs/models/extras/imageattachment.html +1434 -181
  762. nautobot/project-static/docs/models/extras/job.html +1411 -157
  763. nautobot/project-static/docs/models/extras/jobbutton.html +1410 -207
  764. nautobot/project-static/docs/models/extras/jobhook.html +1397 -194
  765. nautobot/project-static/docs/models/extras/joblogentry.html +1408 -155
  766. nautobot/project-static/docs/models/extras/jobresult.html +1417 -159
  767. nautobot/project-static/docs/models/extras/note.html +1389 -190
  768. nautobot/project-static/docs/models/extras/relationship.html +1391 -192
  769. nautobot/project-static/docs/models/extras/role.html +1495 -198
  770. nautobot/project-static/docs/models/extras/secret.html +1492 -201
  771. nautobot/project-static/docs/models/extras/secretsgroup.html +1410 -157
  772. nautobot/project-static/docs/models/extras/status.html +1381 -221
  773. nautobot/project-static/docs/models/extras/tag.html +1389 -190
  774. nautobot/project-static/docs/models/extras/webhook.html +1389 -190
  775. nautobot/project-static/docs/models/ipam/ipaddress.html +1488 -200
  776. nautobot/project-static/docs/models/ipam/prefix.html +1410 -157
  777. nautobot/project-static/docs/models/ipam/rir.html +1410 -157
  778. nautobot/project-static/docs/models/ipam/routetarget.html +1410 -157
  779. nautobot/project-static/docs/models/ipam/service.html +1410 -157
  780. nautobot/project-static/docs/models/ipam/vlan.html +1410 -157
  781. nautobot/project-static/docs/models/ipam/vlangroup.html +1410 -157
  782. nautobot/project-static/docs/models/ipam/vrf.html +1410 -161
  783. nautobot/project-static/docs/models/tenancy/tenant.html +1412 -159
  784. nautobot/project-static/docs/models/tenancy/tenantgroup.html +1412 -159
  785. nautobot/project-static/docs/models/users/objectpermission.html +1462 -171
  786. nautobot/project-static/docs/models/users/token.html +1410 -157
  787. nautobot/project-static/docs/models/virtualization/cluster.html +1410 -157
  788. nautobot/project-static/docs/models/virtualization/clustergroup.html +1410 -157
  789. nautobot/project-static/docs/models/virtualization/clustertype.html +1410 -157
  790. nautobot/project-static/docs/models/virtualization/virtualmachine.html +1414 -157
  791. nautobot/project-static/docs/models/virtualization/vminterface.html +1410 -157
  792. nautobot/project-static/docs/objects.inv +0 -0
  793. nautobot/project-static/docs/plugins/development.html +1916 -646
  794. nautobot/project-static/docs/plugins/index.html +1389 -190
  795. nautobot/project-static/docs/plugins/porting-from-netbox.html +1389 -190
  796. nautobot/project-static/docs/release-notes/index.html +1389 -190
  797. nautobot/project-static/docs/release-notes/version-1.0.html +1389 -190
  798. nautobot/project-static/docs/release-notes/version-1.1.html +1389 -190
  799. nautobot/project-static/docs/release-notes/version-1.2.html +1389 -190
  800. nautobot/project-static/docs/release-notes/version-1.3.html +1389 -190
  801. nautobot/project-static/docs/release-notes/version-1.4.html +1389 -190
  802. nautobot/project-static/docs/release-notes/version-1.5.html +2016 -397
  803. nautobot/project-static/docs/release-notes/version-2.0.html +1935 -287
  804. nautobot/project-static/docs/requirements.txt +5 -4
  805. nautobot/project-static/docs/rest-api/authentication.html +1389 -190
  806. nautobot/project-static/docs/rest-api/filtering.html +1389 -190
  807. nautobot/project-static/docs/rest-api/overview.html +2002 -576
  808. nautobot/project-static/docs/rest-api/ui-related-endpoints.html +4057 -0
  809. nautobot/project-static/docs/search/search_index.json +1 -1
  810. nautobot/project-static/docs/sitemap.xml +197 -187
  811. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  812. nautobot/project-static/docs/user-guides/custom-fields.html +1390 -191
  813. nautobot/project-static/docs/user-guides/getting-started/creating-devices.html +1392 -193
  814. nautobot/project-static/docs/user-guides/getting-started/index.html +1388 -189
  815. nautobot/project-static/docs/user-guides/getting-started/interfaces.html +1388 -189
  816. nautobot/project-static/docs/user-guides/getting-started/ipam.html +1386 -187
  817. nautobot/project-static/docs/user-guides/getting-started/platforms.html +1448 -249
  818. nautobot/project-static/docs/user-guides/getting-started/regions.html +1411 -212
  819. nautobot/project-static/docs/user-guides/getting-started/search-bar.html +1395 -196
  820. nautobot/project-static/docs/user-guides/getting-started/tenants.html +1448 -249
  821. nautobot/project-static/docs/user-guides/getting-started/vlans-and-vlan-groups.html +1448 -249
  822. nautobot/project-static/docs/user-guides/git-data-source.html +1405 -206
  823. nautobot/project-static/docs/user-guides/graphql.html +1402 -203
  824. nautobot/project-static/docs/user-guides/relationships.html +1448 -249
  825. nautobot/project-static/docs/user-guides/s3-django-storage.html +1448 -249
  826. nautobot/project-static/js/forms.js +16 -9
  827. nautobot/project-static/js/theme.js +5 -0
  828. nautobot/tenancy/api/serializers.py +4 -34
  829. nautobot/tenancy/api/urls.py +1 -1
  830. nautobot/tenancy/filters/__init__.py +9 -7
  831. nautobot/tenancy/filters/mixins.py +3 -2
  832. nautobot/tenancy/forms.py +3 -36
  833. nautobot/tenancy/migrations/0001_initial.py +0 -1
  834. nautobot/tenancy/migrations/0002_auto_slug.py +0 -1
  835. nautobot/tenancy/migrations/0003_mptt_to_tree_queries.py +0 -1
  836. nautobot/tenancy/migrations/0004_change_tree_manager_on_tree_models.py +0 -1
  837. nautobot/tenancy/migrations/0005_rename_foreign_keys_and_related_names.py +0 -1
  838. nautobot/tenancy/migrations/0006_created_datetime.py +0 -1
  839. nautobot/tenancy/migrations/0007_remove_tenant_tenantgroup_slug.py +20 -0
  840. nautobot/tenancy/migrations/0008_tagsfield.py +19 -0
  841. nautobot/tenancy/models.py +0 -30
  842. nautobot/tenancy/navigation.py +6 -39
  843. nautobot/tenancy/tables.py +4 -4
  844. nautobot/tenancy/templates/tenancy/tenant.html +12 -12
  845. nautobot/tenancy/templates/tenancy/tenant_edit.html +0 -1
  846. nautobot/tenancy/templates/tenancy/tenantgroup.html +1 -1
  847. nautobot/tenancy/tests/test_api.py +1 -12
  848. nautobot/tenancy/tests/test_filters.py +20 -12
  849. nautobot/tenancy/tests/test_views.py +11 -29
  850. nautobot/tenancy/urls.py +10 -10
  851. nautobot/tenancy/views.py +0 -3
  852. nautobot/ui/.eslintignore +6 -0
  853. nautobot/ui/.gitignore +10 -0
  854. nautobot/ui/.prettierignore +9 -0
  855. nautobot/ui/.prettierrc +4 -0
  856. nautobot/ui/README.md +33 -0
  857. nautobot/ui/app_imports.js.j2 +7 -0
  858. nautobot/ui/craco.config.js +46 -0
  859. nautobot/ui/jsconfig-base.json +11 -0
  860. nautobot/ui/jsconfig.json +5 -0
  861. nautobot/ui/lib/nautobot-craco-alias-plugin.js +40 -0
  862. nautobot/ui/package-lock.json +21451 -0
  863. nautobot/ui/package.json +70 -0
  864. nautobot/ui/public/index.html +47 -0
  865. nautobot/ui/public/logo192.png +0 -0
  866. nautobot/ui/public/logo512.png +0 -0
  867. nautobot/ui/public/manifest.json +25 -0
  868. nautobot/ui/public/nautobot_logo.svg +131 -0
  869. nautobot/ui/public/robots.txt +3 -0
  870. nautobot/ui/src/App.js +71 -0
  871. nautobot/ui/src/components/AppFullWidthComponents.js +8 -0
  872. nautobot/ui/src/components/AppTab.js +40 -0
  873. nautobot/ui/src/components/Apps.js +60 -0
  874. nautobot/ui/src/components/HomeChangelogPanel.js +98 -0
  875. nautobot/ui/src/components/HomePanel.js +58 -0
  876. nautobot/ui/src/components/JobHistoryTable.js +78 -0
  877. nautobot/ui/src/components/Layout.js +53 -0
  878. nautobot/ui/src/components/LoadingWidget.js +25 -0
  879. nautobot/ui/src/components/Navbar.js +116 -0
  880. nautobot/ui/src/components/NotificationPopover.js +27 -0
  881. nautobot/ui/src/components/ObjectListTable.js +209 -0
  882. nautobot/ui/src/components/ReferenceDataTag.js +35 -0
  883. nautobot/ui/src/components/RouterButton.js +10 -0
  884. nautobot/ui/src/components/RouterLink.js +10 -0
  885. nautobot/ui/src/components/SidebarNav.js +147 -0
  886. nautobot/ui/src/components/Table.js +48 -0
  887. nautobot/ui/src/components/TableItem.js +71 -0
  888. nautobot/ui/src/components/__tests__/AppFullWidthComponents.test.js +16 -0
  889. nautobot/ui/src/components/__tests__/AppTab.test.js +21 -0
  890. nautobot/ui/src/components/__tests__/Apps.test.js +14 -0
  891. nautobot/ui/src/components/__tests__/Layout.test.js +33 -0
  892. nautobot/ui/src/components/__tests__/Table.test.js +36 -0
  893. nautobot/ui/src/components/__tests__/TableItem.test.js +37 -0
  894. nautobot/ui/src/components/__tests__/paginator.test.js +43 -0
  895. nautobot/ui/src/components/__tests__/paginator_form.test.js +13 -0
  896. nautobot/ui/src/components/pagination.js +93 -0
  897. nautobot/ui/src/components/paginator.js +79 -0
  898. nautobot/ui/src/components/paginator_form.js +43 -0
  899. nautobot/ui/src/components/usePagination.js +57 -0
  900. nautobot/ui/src/constants/apiPath.js +10 -0
  901. nautobot/ui/src/constants/icons.js +15 -0
  902. nautobot/ui/src/constants/size.js +15 -0
  903. nautobot/ui/src/index.js +65 -0
  904. nautobot/ui/src/reportWebVitals.js +15 -0
  905. nautobot/ui/src/router.js +77 -0
  906. nautobot/ui/src/utils/api.js +131 -0
  907. nautobot/ui/src/utils/app-import.js +15 -0
  908. nautobot/ui/src/utils/color.js +15 -0
  909. nautobot/ui/src/utils/date.js +14 -0
  910. nautobot/ui/src/utils/index.js +15 -0
  911. nautobot/ui/src/utils/navigation.js +32 -0
  912. nautobot/ui/src/utils/session.js +64 -0
  913. nautobot/ui/src/utils/store.js +242 -0
  914. nautobot/ui/src/utils/string.js +6 -0
  915. nautobot/ui/src/utils/url.js +4 -0
  916. nautobot/ui/src/views/Home.js +138 -0
  917. nautobot/ui/src/views/InstalledApps.js +80 -0
  918. nautobot/ui/src/views/Login.js +48 -0
  919. nautobot/ui/src/views/Logout.js +20 -0
  920. nautobot/ui/src/views/__tests__/BSCreateViewTemplate.test.js +11 -0
  921. nautobot/ui/src/views/__tests__/BSListViewTemplate.test.js +107 -0
  922. nautobot/ui/src/views/__tests__/Login.test.js +15 -0
  923. nautobot/ui/src/views/generic/GenericView.js +142 -0
  924. nautobot/ui/src/views/generic/ObjectCreate.js +96 -0
  925. nautobot/ui/src/views/generic/ObjectList.js +127 -0
  926. nautobot/ui/src/views/generic/ObjectRetrieve.js +551 -0
  927. nautobot/users/admin.py +1 -1
  928. nautobot/users/api/serializers.py +51 -61
  929. nautobot/users/api/urls.py +1 -1
  930. nautobot/users/api/views.py +53 -2
  931. nautobot/users/migrations/0001_initial.py +0 -1
  932. nautobot/users/migrations/0002_token_ordering_by_created.py +0 -1
  933. nautobot/users/migrations/0003_alter_user_options.py +0 -1
  934. nautobot/users/migrations/0004_alter_user_managers.py +0 -1
  935. nautobot/users/tests/test_api.py +109 -28
  936. nautobot/users/tests/test_filters.py +0 -4
  937. nautobot/users/tests/test_models.py +0 -1
  938. nautobot/users/views.py +0 -7
  939. nautobot/virtualization/api/serializers.py +18 -132
  940. nautobot/virtualization/api/urls.py +1 -1
  941. nautobot/virtualization/api/views.py +1 -22
  942. nautobot/virtualization/choices.py +0 -2
  943. nautobot/virtualization/filters.py +12 -7
  944. nautobot/virtualization/forms.py +21 -117
  945. nautobot/virtualization/migrations/0001_initial.py +0 -1
  946. nautobot/virtualization/migrations/0002_virtualmachine_local_context_schema.py +0 -1
  947. nautobot/virtualization/migrations/0003_vminterface_verbose_name.py +0 -1
  948. nautobot/virtualization/migrations/0004_auto_slug.py +0 -1
  949. nautobot/virtualization/migrations/0005_add_natural_indexing.py +0 -1
  950. nautobot/virtualization/migrations/0006_vminterface_status.py +0 -1
  951. nautobot/virtualization/migrations/0007_vminterface_status_data_migration.py +0 -1
  952. nautobot/virtualization/migrations/0008_vminterface_parent.py +0 -1
  953. nautobot/virtualization/migrations/0009_cluster_location.py +0 -1
  954. nautobot/virtualization/migrations/0010_vminterface_mac_address_data_migration.py +0 -1
  955. nautobot/virtualization/migrations/0011_alter_vminterface_mac_address.py +0 -1
  956. nautobot/virtualization/migrations/0012_alter_virtualmachine_role_add_new_role.py +1 -2
  957. nautobot/virtualization/migrations/0013_migrate_virtualmachine_role_data.py +18 -12
  958. nautobot/virtualization/migrations/0014_rename_virtualmachine_roles.py +0 -1
  959. nautobot/virtualization/migrations/0015_rename_foreignkey_fields.py +1 -2
  960. nautobot/virtualization/migrations/0016_remove_site_foreign_key_from_cluster_class.py +0 -1
  961. nautobot/virtualization/migrations/0017_created_datetime.py +0 -1
  962. nautobot/virtualization/migrations/0018_related_name_changes.py +1 -2
  963. nautobot/virtualization/migrations/0019_vminterface_ip_addresses_m2m.py +0 -1
  964. nautobot/virtualization/migrations/0020_remove_clustergroup_clustertype_slug.py +20 -0
  965. nautobot/virtualization/migrations/0021_tagsfield_and_vminterface_to_primarymodel.py +39 -0
  966. nautobot/virtualization/migrations/0022_vminterface_timestamps_data_migration.py +17 -0
  967. nautobot/virtualization/migrations/0023_ipam__namespaces.py +25 -0
  968. nautobot/virtualization/migrations/0024_fixup_null_statuses.py +25 -0
  969. nautobot/virtualization/migrations/0025_status_nonnullable.py +29 -0
  970. nautobot/virtualization/models.py +39 -131
  971. nautobot/virtualization/navigation.py +18 -99
  972. nautobot/virtualization/tables.py +4 -4
  973. nautobot/virtualization/templates/virtualization/virtualmachine.html +13 -2
  974. nautobot/virtualization/templates/virtualization/virtualmachine_edit.html +6 -0
  975. nautobot/virtualization/tests/test_api.py +42 -52
  976. nautobot/virtualization/tests/test_filters.py +98 -75
  977. nautobot/virtualization/tests/test_models.py +36 -13
  978. nautobot/virtualization/tests/test_views.py +68 -73
  979. nautobot/virtualization/urls.py +10 -10
  980. nautobot/virtualization/views.py +8 -14
  981. {nautobot-2.0.0a2.dist-info → nautobot-2.0.0b1.dist-info}/METADATA +15 -22
  982. {nautobot-2.0.0a2.dist-info → nautobot-2.0.0b1.dist-info}/RECORD +987 -834
  983. {nautobot-2.0.0a2.dist-info → nautobot-2.0.0b1.dist-info}/WHEEL +1 -1
  984. nautobot/circuits/api/nested_serializers.py +0 -69
  985. nautobot/core/templates/plugin_template/navigation.py-tpl +0 -22
  986. nautobot/dcim/api/nested_serializers.py +0 -356
  987. nautobot/dcim/templates/dcim/device_import.html +0 -5
  988. nautobot/dcim/templates/dcim/device_import_child.html +0 -5
  989. nautobot/dcim/templates/dcim/inc/device_import_header.html +0 -4
  990. nautobot/extras/api/nested_serializers.py +0 -353
  991. nautobot/extras/migrations/0064_configcontext_data_migrations.py +0 -42
  992. nautobot/extras/migrations/0071_job__unique_name_data_migration.py +0 -47
  993. nautobot/extras/reports.py +0 -60
  994. nautobot/extras/scripts.py +0 -72
  995. nautobot/extras/tests/example_jobs/script_variables.py +0 -67
  996. nautobot/extras/tests/example_jobs/test_duplicate_name2.py +0 -5
  997. nautobot/extras/tests/example_jobs/test_fail.py +0 -16
  998. nautobot/extras/tests/example_jobs/test_file_upload_pass.py +0 -20
  999. nautobot/extras/tests/example_jobs/test_ipaddress_vars.py +0 -52
  1000. nautobot/extras/tests/example_jobs/test_job_button_receiver.py +0 -21
  1001. nautobot/extras/tests/example_jobs/test_job_hook_receiver.py +0 -20
  1002. nautobot/extras/tests/example_jobs/test_location_with_custom_field.py +0 -35
  1003. nautobot/extras/tests/example_jobs/test_log_redaction.py +0 -14
  1004. nautobot/extras/tests/example_jobs/test_modify_db.py +0 -19
  1005. nautobot/extras/tests/example_jobs/test_object_var_optional.py +0 -14
  1006. nautobot/extras/tests/example_jobs/test_object_var_required.py +0 -14
  1007. nautobot/extras/tests/example_jobs/test_object_vars.py +0 -29
  1008. nautobot/extras/tests/example_jobs/test_pass.py +0 -19
  1009. nautobot/extras/tests/example_jobs/test_read_only_fail.py +0 -24
  1010. nautobot/extras/tests/example_jobs/test_read_only_no_commit_field.py +0 -10
  1011. nautobot/extras/tests/example_jobs/test_read_only_pass.py +0 -22
  1012. nautobot/ipam/api/nested_serializers.py +0 -143
  1013. nautobot/project-static/docs/assets/javascripts/bundle.5a2dcb6a.min.js +0 -29
  1014. nautobot/project-static/docs/assets/javascripts/bundle.5a2dcb6a.min.js.map +0 -8
  1015. nautobot/project-static/docs/assets/javascripts/extra/bundle.5f09fbc3.min.js +0 -18
  1016. nautobot/project-static/docs/assets/javascripts/extra/bundle.5f09fbc3.min.js.map +0 -8
  1017. nautobot/project-static/docs/assets/stylesheets/extra.0d2c79a8.min.css +0 -1
  1018. nautobot/project-static/docs/assets/stylesheets/extra.0d2c79a8.min.css.map +0 -1
  1019. nautobot/project-static/docs/assets/stylesheets/main.975780f9.min.css +0 -1
  1020. nautobot/project-static/docs/assets/stylesheets/main.975780f9.min.css.map +0 -1
  1021. nautobot/project-static/docs/assets/stylesheets/palette.2505c338.min.css +0 -1
  1022. nautobot/project-static/docs/assets/stylesheets/palette.2505c338.min.css.map +0 -1
  1023. nautobot/tenancy/api/nested_serializers.py +0 -31
  1024. nautobot/users/api/nested_serializers.py +0 -67
  1025. nautobot/virtualization/api/nested_serializers.py +0 -65
  1026. /nautobot/extras/{tests/example_jobs → test_jobs}/__init__.py +0 -0
  1027. /nautobot/{dcim/models/sites.py → ipam/management/__init__.py} +0 -0
  1028. {nautobot-2.0.0a2.dist-info → nautobot-2.0.0b1.dist-info}/LICENSE.txt +0 -0
  1029. {nautobot-2.0.0a2.dist-info → nautobot-2.0.0b1.dist-info}/entry_points.txt +0 -0
@@ -1,6 +1,7 @@
1
1
  from datetime import datetime, timedelta
2
2
  import uuid
3
- from unittest import mock
3
+ import tempfile
4
+ from unittest import mock, skip
4
5
 
5
6
  from django.conf import settings
6
7
  from django.contrib.auth import get_user_model
@@ -26,8 +27,7 @@ from nautobot.dcim.models import (
26
27
  RackGroup,
27
28
  )
28
29
  from nautobot.dcim.tests import test_views
29
- from nautobot.extras.api.nested_serializers import NestedJobResultSerializer
30
- from nautobot.extras.api.serializers import ConfigContextSerializer
30
+ from nautobot.extras.api.serializers import ConfigContextSerializer, JobResultSerializer
31
31
  from nautobot.extras.choices import (
32
32
  DynamicGroupOperatorChoices,
33
33
  JobExecutionType,
@@ -65,10 +65,12 @@ from nautobot.extras.models import (
65
65
  Webhook,
66
66
  )
67
67
  from nautobot.extras.models.jobs import JobHook, JobButton
68
+
68
69
  from nautobot.extras.tests.test_relationships import RequiredRelationshipTestMixin
69
70
  from nautobot.extras.utils import TaggableClassesQuery
71
+
70
72
  from nautobot.ipam.factory import VLANFactory
71
- from nautobot.ipam.models import VLAN, VLANGroup
73
+ from nautobot.ipam.models import VLANGroup, VLAN
72
74
  from nautobot.users.models import ObjectPermission
73
75
 
74
76
 
@@ -90,32 +92,22 @@ class AppTest(APITestCase):
90
92
 
91
93
  class ComputedFieldTest(APIViewTestCases.APIViewTestCase):
92
94
  model = ComputedField
93
- brief_fields = [
94
- "content_type",
95
- "display",
96
- "id",
97
- "label",
98
- "url",
99
- ]
100
95
  choices_fields = ["content_type"]
101
96
  create_data = [
102
97
  {
103
98
  "content_type": "dcim.location",
104
- "slug": "cf4",
105
99
  "label": "Computed Field 4",
106
100
  "template": "{{ obj.name }}",
107
101
  "fallback_value": "error",
108
102
  },
109
103
  {
110
104
  "content_type": "dcim.location",
111
- "slug": "cf5",
112
105
  "label": "Computed Field 5",
113
106
  "template": "{{ obj.name }}",
114
107
  "fallback_value": "error",
115
108
  },
116
109
  {
117
110
  "content_type": "dcim.location",
118
- "slug": "cf6",
119
111
  "label": "Computed Field 6",
120
112
  "template": "{{ obj.name }}",
121
113
  },
@@ -128,7 +120,7 @@ class ComputedFieldTest(APIViewTestCases.APIViewTestCase):
128
120
  ]
129
121
  update_data = {
130
122
  "content_type": "dcim.location",
131
- "slug": "cf1",
123
+ "key": "cf1",
132
124
  "label": "My Computed Field",
133
125
  }
134
126
  bulk_update_data = {
@@ -142,21 +134,21 @@ class ComputedFieldTest(APIViewTestCases.APIViewTestCase):
142
134
  location_ct = ContentType.objects.get_for_model(Location)
143
135
 
144
136
  ComputedField.objects.create(
145
- slug="cf1",
137
+ key="cf1",
146
138
  label="Computed Field One",
147
139
  template="{{ obj.name }}",
148
140
  fallback_value="error",
149
141
  content_type=location_ct,
150
142
  )
151
143
  ComputedField.objects.create(
152
- slug="cf2",
144
+ key="cf2",
153
145
  label="Computed Field Two",
154
146
  template="{{ obj.name }}",
155
147
  fallback_value="error",
156
148
  content_type=location_ct,
157
149
  )
158
150
  ComputedField.objects.create(
159
- slug="cf3",
151
+ key="cf3",
160
152
  label="Computed Field Three",
161
153
  template="{{ obj.name }}",
162
154
  fallback_value="error",
@@ -182,7 +174,6 @@ class ComputedFieldTest(APIViewTestCases.APIViewTestCase):
182
174
 
183
175
  class ConfigContextTest(APIViewTestCases.APIViewTestCase):
184
176
  model = ConfigContext
185
- brief_fields = ["display", "id", "name", "url"]
186
177
  create_data = [
187
178
  {
188
179
  "name": "Config Context 4",
@@ -212,11 +203,14 @@ class ConfigContextTest(APIViewTestCases.APIViewTestCase):
212
203
  """
213
204
  Test rendering config context data for a device.
214
205
  """
215
- manufacturer = Manufacturer.objects.create(name="Manufacturer 1", slug="manufacturer-1")
206
+ manufacturer = Manufacturer.objects.first()
216
207
  devicetype = DeviceType.objects.create(manufacturer=manufacturer, model="Device Type 1", slug="device-type-1")
217
208
  devicerole = Role.objects.get_for_model(Device).first()
209
+ devicestatus = Status.objects.get_for_model(Device).first()
218
210
  location = Location.objects.filter(location_type=LocationType.objects.get(name="Campus")).first()
219
- device = Device.objects.create(name="Device 1", device_type=devicetype, role=devicerole, location=location)
211
+ device = Device.objects.create(
212
+ name="Device 1", device_type=devicetype, role=devicerole, status=devicestatus, location=location
213
+ )
220
214
 
221
215
  # Test default config contexts (created at test setup)
222
216
  rendered_context = device.get_config_context()
@@ -227,7 +221,7 @@ class ConfigContextTest(APIViewTestCases.APIViewTestCase):
227
221
  # Test API response as well
228
222
  self.add_permissions("dcim.view_device")
229
223
  device_url = reverse("dcim-api:device-detail", kwargs={"pk": device.pk})
230
- response = self.client.get(device_url, **self.header)
224
+ response = self.client.get(device_url + "?include=config_context", **self.header)
231
225
  self.assertHttpStatus(response, status.HTTP_200_OK)
232
226
  self.assertIn("config_context", response.data)
233
227
  self.assertEqual(response.data["config_context"], {"foo": 123, "bar": 456, "baz": 789}, response.data)
@@ -238,7 +232,7 @@ class ConfigContextTest(APIViewTestCases.APIViewTestCase):
238
232
  configcontext4.locations.add(location)
239
233
  rendered_context = device.get_config_context()
240
234
  self.assertEqual(rendered_context["location_data"], "ABC")
241
- response = self.client.get(device_url, **self.header)
235
+ response = self.client.get(device_url + "?include=config_context", **self.header)
242
236
  self.assertHttpStatus(response, status.HTTP_200_OK)
243
237
  self.assertIn("config_context", response.data)
244
238
  self.assertEqual(response.data["config_context"]["location_data"], "ABC", response.data["config_context"])
@@ -249,7 +243,7 @@ class ConfigContextTest(APIViewTestCases.APIViewTestCase):
249
243
  configcontext5.locations.add(location)
250
244
  rendered_context = device.get_config_context()
251
245
  self.assertEqual(rendered_context["foo"], 999)
252
- response = self.client.get(device_url, **self.header)
246
+ response = self.client.get(device_url + "?include=config_context", **self.header)
253
247
  self.assertHttpStatus(response, status.HTTP_200_OK)
254
248
  self.assertIn("config_context", response.data)
255
249
  self.assertEqual(response.data["config_context"]["foo"], 999, response.data["config_context"])
@@ -261,7 +255,7 @@ class ConfigContextTest(APIViewTestCases.APIViewTestCase):
261
255
  configcontext6.locations.add(location2)
262
256
  rendered_context = device.get_config_context()
263
257
  self.assertEqual(rendered_context["bar"], 456)
264
- response = self.client.get(device_url, **self.header)
258
+ response = self.client.get(device_url + "?include=config_context", **self.header)
265
259
  self.assertHttpStatus(response, status.HTTP_200_OK)
266
260
  self.assertIn("config_context", response.data)
267
261
  self.assertEqual(response.data["config_context"]["bar"], 456, response.data["config_context"])
@@ -285,7 +279,7 @@ class ConfigContextTest(APIViewTestCases.APIViewTestCase):
285
279
  }
286
280
  response = self.client.post(self._get_list_url(), data, format="json", **self.header)
287
281
  self.assertHttpStatus(response, status.HTTP_201_CREATED)
288
- self.assertEqual(response.data["config_context_schema"]["id"], str(schema.pk))
282
+ self.assertEqual(response.data["config_context_schema"], self.absolute_api_url(schema))
289
283
 
290
284
  def test_schema_validation_fails(self):
291
285
  """
@@ -322,7 +316,6 @@ class ConfigContextTest(APIViewTestCases.APIViewTestCase):
322
316
 
323
317
  class ConfigContextSchemaTest(APIViewTestCases.APIViewTestCase):
324
318
  model = ConfigContextSchema
325
- brief_fields = ["display", "id", "name", "slug", "url"]
326
319
  create_data = [
327
320
  {
328
321
  "name": "Schema 4",
@@ -385,31 +378,33 @@ class ContentTypeTest(APITestCase):
385
378
 
386
379
 
387
380
  class CreatedUpdatedFilterTest(APITestCase):
388
- def setUp(self):
389
- super().setUp()
390
-
391
- self.location1 = Location.objects.filter(location_type=LocationType.objects.get(name="Campus")).first()
392
- self.rackgroup1 = RackGroup.objects.create(
393
- location=self.location1, name="Test Rack Group 1", slug="test-rack-group-1"
394
- )
395
- self.rackrole1 = Role.objects.get_for_model(Rack).first()
396
- self.rack1 = Rack.objects.create(
397
- location=self.location1,
398
- rack_group=self.rackgroup1,
399
- role=self.rackrole1,
381
+ @classmethod
382
+ def setUpTestData(cls):
383
+ cls.location1 = Location.objects.filter(location_type=LocationType.objects.get(name="Campus")).first()
384
+ cls.rackgroup1 = RackGroup.objects.create(
385
+ location=cls.location1, name="Test Rack Group 1", slug="test-rack-group-1"
386
+ )
387
+ cls.rackrole1 = Role.objects.get_for_model(Rack).first()
388
+ cls.rackstatus1 = Status.objects.get_for_model(Rack).first()
389
+ cls.rack1 = Rack.objects.create(
390
+ location=cls.location1,
391
+ rack_group=cls.rackgroup1,
392
+ role=cls.rackrole1,
393
+ status=cls.rackstatus1,
400
394
  name="Test Rack 1",
401
395
  u_height=42,
402
396
  )
403
- self.rack2 = Rack.objects.create(
404
- location=self.location1,
405
- rack_group=self.rackgroup1,
406
- role=self.rackrole1,
397
+ cls.rack2 = Rack.objects.create(
398
+ location=cls.location1,
399
+ rack_group=cls.rackgroup1,
400
+ role=cls.rackrole1,
401
+ status=cls.rackstatus1,
407
402
  name="Test Rack 2",
408
403
  u_height=42,
409
404
  )
410
405
 
411
406
  # change the created and last_updated of one
412
- Rack.objects.filter(pk=self.rack2.pk).update(
407
+ Rack.objects.filter(pk=cls.rack2.pk).update(
413
408
  created=make_aware(datetime(2001, 2, 3, 0, 1, 2, 3)),
414
409
  last_updated=make_aware(datetime(2001, 2, 3, 1, 2, 3, 4)),
415
410
  )
@@ -483,7 +478,6 @@ class CustomFieldTest(APIViewTestCases.APIViewTestCase):
483
478
  """Tests for the CustomField REST API."""
484
479
 
485
480
  model = CustomField
486
- brief_fields = ["display", "id", "key", "url"]
487
481
  create_data = [
488
482
  {
489
483
  "content_types": ["dcim.location"],
@@ -556,7 +550,6 @@ class CustomFieldTest(APIViewTestCases.APIViewTestCase):
556
550
 
557
551
  class CustomLinkTest(APIViewTestCases.APIViewTestCase):
558
552
  model = CustomLink
559
- brief_fields = ["content_type", "display", "id", "name", "url"]
560
553
  create_data = [
561
554
  {
562
555
  "content_type": "dcim.location",
@@ -622,38 +615,44 @@ class DynamicGroupTestMixin:
622
615
  def setUpTestData(cls):
623
616
  # Create the objects required for devices.
624
617
  location_type = LocationType.objects.get(name="Campus")
618
+ location_status = Status.objects.get_for_model(Location).first()
625
619
  locations = (
626
- Location.objects.create(name="Location 1", slug="location-1", location_type=location_type),
627
- Location.objects.create(name="Location 2", slug="location-2", location_type=location_type),
628
- Location.objects.create(name="Location 3", slug="location-3", location_type=location_type),
620
+ Location.objects.create(
621
+ name="Location 1", slug="location-1", location_type=location_type, status=location_status
622
+ ),
623
+ Location.objects.create(
624
+ name="Location 2", slug="location-2", location_type=location_type, status=location_status
625
+ ),
626
+ Location.objects.create(
627
+ name="Location 3", slug="location-3", location_type=location_type, status=location_status
628
+ ),
629
629
  )
630
630
 
631
- manufacturer = Manufacturer.objects.create(name="Manufacturer 1", slug="manufacturer-1")
631
+ manufacturer = Manufacturer.objects.first()
632
632
  device_type = DeviceType.objects.create(
633
633
  manufacturer=manufacturer,
634
634
  model="device Type 1",
635
635
  slug="device-type-1",
636
636
  )
637
637
  device_role = Role.objects.get_for_model(Device).first()
638
- status_active = Status.objects.get(slug="active")
639
- status_planned = Status.objects.get(slug="planned")
638
+ statuses = Status.objects.get_for_model(Device)
640
639
  Device.objects.create(
641
640
  name="device-location-1",
642
- status=status_active,
641
+ status=statuses[0],
643
642
  role=device_role,
644
643
  device_type=device_type,
645
644
  location=locations[0],
646
645
  )
647
646
  Device.objects.create(
648
647
  name="device-location-2",
649
- status=status_active,
648
+ status=statuses[0],
650
649
  role=device_role,
651
650
  device_type=device_type,
652
651
  location=locations[1],
653
652
  )
654
653
  Device.objects.create(
655
654
  name="device-location-3",
656
- status=status_planned,
655
+ status=statuses[1],
657
656
  role=device_role,
658
657
  device_type=device_type,
659
658
  location=locations[2],
@@ -661,22 +660,19 @@ class DynamicGroupTestMixin:
661
660
 
662
661
  # Then the DynamicGroups.
663
662
  cls.content_type = ContentType.objects.get_for_model(Device)
664
- cls.groups = [
663
+ cls.groups = cls.groups = [
665
664
  DynamicGroup.objects.create(
666
665
  name="API DynamicGroup 1",
667
- slug="api-dynamicgroup-1",
668
666
  content_type=cls.content_type,
669
- filter={"status": ["active"]},
667
+ filter={"status": [statuses[0].name]},
670
668
  ),
671
669
  DynamicGroup.objects.create(
672
670
  name="API DynamicGroup 2",
673
- slug="api-dynamicgroup-2",
674
671
  content_type=cls.content_type,
675
- filter={"status": ["planned"]},
672
+ filter={"status": [statuses[0].name]},
676
673
  ),
677
674
  DynamicGroup.objects.create(
678
675
  name="API DynamicGroup 3",
679
- slug="api-dynamicgroup-3",
680
676
  content_type=cls.content_type,
681
677
  filter={"location": [f"{locations[2].slug}"]},
682
678
  ),
@@ -685,24 +681,20 @@ class DynamicGroupTestMixin:
685
681
 
686
682
  class DynamicGroupTest(DynamicGroupTestMixin, APIViewTestCases.APIViewTestCase):
687
683
  model = DynamicGroup
688
- brief_fields = ["content_type", "display", "id", "name", "slug", "url"]
689
684
  choices_fields = ["content_type"]
690
685
  create_data = [
691
686
  {
692
687
  "name": "API DynamicGroup 4",
693
- "slug": "api-dynamicgroup-4",
694
688
  "content_type": "dcim.device",
695
689
  "filter": {"location": ["location-1"]},
696
690
  },
697
691
  {
698
692
  "name": "API DynamicGroup 5",
699
- "slug": "api-dynamicgroup-5",
700
693
  "content_type": "dcim.device",
701
694
  "filter": {"has_interfaces": False},
702
695
  },
703
696
  {
704
697
  "name": "API DynamicGroup 6",
705
- "slug": "api-dynamicgroup-6",
706
698
  "content_type": "dcim.device",
707
699
  "filter": {"location": ["location-2"]},
708
700
  },
@@ -721,7 +713,6 @@ class DynamicGroupTest(DynamicGroupTestMixin, APIViewTestCases.APIViewTestCase):
721
713
 
722
714
  class DynamicGroupMembershipTest(DynamicGroupTestMixin, APIViewTestCases.APIViewTestCase):
723
715
  model = DynamicGroupMembership
724
- brief_fields = ["display", "group", "id", "operator", "parent_group", "url", "weight"]
725
716
  choices_fields = ["operator"]
726
717
 
727
718
  @classmethod
@@ -730,13 +721,11 @@ class DynamicGroupMembershipTest(DynamicGroupTestMixin, APIViewTestCases.APIView
730
721
 
731
722
  parent = DynamicGroup.objects.create(
732
723
  name="parent",
733
- slug="parent",
734
724
  content_type=cls.content_type,
735
725
  filter={},
736
726
  )
737
727
  parent2 = DynamicGroup.objects.create(
738
728
  name="parent2",
739
- slug="parent2",
740
729
  content_type=cls.content_type,
741
730
  filter={},
742
731
  )
@@ -782,10 +771,18 @@ class DynamicGroupMembershipTest(DynamicGroupTestMixin, APIViewTestCases.APIView
782
771
  },
783
772
  ]
784
773
 
774
+ # TODO: Either improve test base or or write a more specific test for this model.
775
+ @skip("DynamicGroupMembership has a `name` property but it's the Group name and not exposed on the API")
776
+ def test_list_objects_ascending_ordered(self):
777
+ pass
778
+
779
+ @skip("DynamicGroupMembership has a `name` property but it's the Group name and not exposed on the API")
780
+ def test_list_objects_descending_ordered(self):
781
+ pass
782
+
785
783
 
786
784
  class ExportTemplateTest(APIViewTestCases.APIViewTestCase):
787
785
  model = ExportTemplate
788
- brief_fields = ["display", "id", "name", "url"]
789
786
  create_data = [
790
787
  {
791
788
  "content_type": "dcim.device",
@@ -831,54 +828,55 @@ class ExportTemplateTest(APIViewTestCases.APIViewTestCase):
831
828
 
832
829
  class GitRepositoryTest(APIViewTestCases.APIViewTestCase):
833
830
  model = GitRepository
834
- brief_fields = ["display", "id", "name", "url"]
835
831
  bulk_update_data = {
836
832
  "branch": "develop",
837
833
  }
838
834
  choices_fields = ["provided_contents"]
839
835
  slug_source = "name"
836
+ slugify_function = staticmethod(slugify_dashes_to_underscores)
840
837
 
841
838
  @classmethod
842
839
  def setUpTestData(cls):
843
840
  secrets_groups = (
844
- SecretsGroup.objects.create(name="Secrets Group 1", slug="secrets-group-1"),
845
- SecretsGroup.objects.create(name="Secrets Group 2", slug="secrets-group-2"),
841
+ SecretsGroup.objects.create(name="Secrets Group 1"),
842
+ SecretsGroup.objects.create(name="Secrets Group 2"),
846
843
  )
847
844
 
848
845
  cls.repos = (
849
846
  GitRepository(
850
847
  name="Repo 1",
851
- slug="repo-1",
848
+ slug="repo_1",
852
849
  remote_url="https://example.com/repo1.git",
853
850
  secrets_group=secrets_groups[0],
854
851
  ),
855
852
  GitRepository(
856
853
  name="Repo 2",
857
- slug="repo-2",
854
+ slug="repo_2",
858
855
  remote_url="https://example.com/repo2.git",
859
856
  secrets_group=secrets_groups[0],
860
857
  ),
861
- GitRepository(name="Repo 3", slug="repo-3", remote_url="https://example.com/repo3.git"),
858
+ GitRepository(name="Repo 3", slug="repo_3", remote_url="https://example.com/repo3.git"),
862
859
  )
863
860
  for repo in cls.repos:
864
- repo.save(trigger_resync=False)
861
+ repo.save()
865
862
 
866
863
  cls.create_data = [
867
864
  {
868
865
  "name": "New Git Repository 1",
869
- "slug": "new-git-repository-1",
866
+ "slug": "new_git_repository_1",
870
867
  "remote_url": "https://example.com/newrepo1.git",
871
868
  "secrets_group": secrets_groups[1].pk,
869
+ "provided_contents": ["extras.configcontext", "extras.exporttemplate"],
872
870
  },
873
871
  {
874
872
  "name": "New Git Repository 2",
875
- "slug": "new-git-repository-2",
873
+ "slug": "new_git_repository_2",
876
874
  "remote_url": "https://example.com/newrepo2.git",
877
875
  "secrets_group": secrets_groups[1].pk,
878
876
  },
879
877
  {
880
878
  "name": "New Git Repository 3",
881
- "slug": "new-git-repository-3",
879
+ "slug": "new_git_repository_3",
882
880
  "remote_url": "https://example.com/newrepo3.git",
883
881
  "secrets_group": secrets_groups[1].pk,
884
882
  },
@@ -889,6 +887,13 @@ class GitRepositoryTest(APIViewTestCases.APIViewTestCase):
889
887
  },
890
888
  ]
891
889
 
890
+ # slug is enforced non-editable in clean because we want it to be providable by the user on creation
891
+ # but not modified afterward
892
+ cls.update_data = {
893
+ "name": "A Different Repo Name",
894
+ "remote_url": "https://example.com/fake.git",
895
+ }
896
+
892
897
  @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
893
898
  @mock.patch("nautobot.extras.api.views.get_worker_count")
894
899
  def test_run_git_sync_no_celery_worker(self, mock_get_worker_count):
@@ -924,10 +929,9 @@ class GitRepositoryTest(APIViewTestCases.APIViewTestCase):
924
929
  self.assertHttpStatus(response, status.HTTP_403_FORBIDDEN)
925
930
 
926
931
  @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
927
- @mock.patch("nautobot.extras.api.views.get_worker_count")
928
- def test_run_git_sync_with_permissions(self, mock_get_worker_count):
932
+ @mock.patch("nautobot.extras.api.views.get_worker_count", return_value=1)
933
+ def test_run_git_sync_with_permissions(self, _):
929
934
  """Git sync request can be submitted successfully."""
930
- mock_get_worker_count.return_value = 1
931
935
  self.add_permissions("extras.add_gitrepository")
932
936
  self.add_permissions("extras.change_gitrepository")
933
937
  url = reverse("extras-api:gitrepository-sync", kwargs={"pk": self.repos[0].id})
@@ -941,7 +945,7 @@ class GitRepositoryTest(APIViewTestCases.APIViewTestCase):
941
945
  url = self._get_list_url()
942
946
  data = {
943
947
  "name": "plugin_test",
944
- "slug": "plugin-test",
948
+ "slug": "plugin_test",
945
949
  "remote_url": "https://localhost/plugin-test",
946
950
  "provided_contents": ["example_plugin.textfile"],
947
951
  }
@@ -952,17 +956,13 @@ class GitRepositoryTest(APIViewTestCases.APIViewTestCase):
952
956
 
953
957
  class GraphQLQueryTest(APIViewTestCases.APIViewTestCase):
954
958
  model = GraphQLQuery
955
- brief_fields = ["display", "id", "name", "url"]
956
-
957
959
  create_data = [
958
960
  {
959
961
  "name": "graphql-query-4",
960
- "slug": "graphql-query-4",
961
962
  "query": "{ query: locations {name} }",
962
963
  },
963
964
  {
964
965
  "name": "graphql-query-5",
965
- "slug": "graphql-query-5",
966
966
  "query": '{ devices(role: "edge") { id, name, role { name slug } } }',
967
967
  },
968
968
  {
@@ -970,24 +970,20 @@ class GraphQLQueryTest(APIViewTestCases.APIViewTestCase):
970
970
  "query": '{ devices(role: "edge") { id, name, role { name slug } } }',
971
971
  },
972
972
  ]
973
- slug_source = "name"
974
973
 
975
974
  @classmethod
976
975
  def setUpTestData(cls):
977
976
  cls.graphqlqueries = (
978
977
  GraphQLQuery(
979
978
  name="graphql-query-1",
980
- slug="graphql-query-1",
981
979
  query="{ locations {name} }",
982
980
  ),
983
981
  GraphQLQuery(
984
982
  name="graphql-query-2",
985
- slug="graphql-query-2",
986
983
  query='{ devices(role: "edge") { id, name, role { name slug } } }',
987
984
  ),
988
985
  GraphQLQuery(
989
986
  name="graphql-query-3",
990
- slug="graphql-query-3",
991
987
  query="""
992
988
  query ($device: [String!]) {
993
989
  devices(name: $device) {
@@ -1014,7 +1010,6 @@ query ($device: [String!]) {
1014
1010
  }
1015
1011
  platform {
1016
1012
  name
1017
- slug
1018
1013
  manufacturer {
1019
1014
  name
1020
1015
  }
@@ -1108,7 +1103,6 @@ class ImageAttachmentTest(
1108
1103
  APIViewTestCases.DeleteObjectViewTestCase,
1109
1104
  ):
1110
1105
  model = ImageAttachment
1111
- brief_fields = ["display", "id", "image", "name", "url"]
1112
1106
  choices_fields = ["content_type"]
1113
1107
 
1114
1108
  @classmethod
@@ -1142,6 +1136,15 @@ class ImageAttachmentTest(
1142
1136
  image_width=100,
1143
1137
  )
1144
1138
 
1139
+ # TODO: Unskip after resolving #2908, #2909
1140
+ @skip("DRF's built-in OrderingFilter triggering natural key attribute error in our base")
1141
+ def test_list_objects_ascending_ordered(self):
1142
+ pass
1143
+
1144
+ @skip("DRF's built-in OrderingFilter triggering natural key attribute error in our base")
1145
+ def test_list_objects_descending_ordered(self):
1146
+ pass
1147
+
1145
1148
 
1146
1149
  class JobTest(
1147
1150
  # note no CreateObjectViewTestCase - we do not support user creation of Job records
@@ -1153,7 +1156,6 @@ class JobTest(
1153
1156
  """Test cases for the Jobs REST API."""
1154
1157
 
1155
1158
  model = Job
1156
- brief_fields = ["display", "grouping", "id", "job_class_name", "module_name", "name", "slug", "source", "url"]
1157
1159
  choices_fields = None
1158
1160
  update_data = {
1159
1161
  # source, module_name, job_class_name, installed are NOT editable
@@ -1161,18 +1163,15 @@ class JobTest(
1161
1163
  "grouping": "Overridden grouping",
1162
1164
  "name_override": True,
1163
1165
  "name": "Overridden name",
1164
- "slug": "overridden-slug",
1165
1166
  "description_override": True,
1166
1167
  "description": "This is an overridden description.",
1167
1168
  "enabled": True,
1168
1169
  "approval_required_override": True,
1169
1170
  "approval_required": True,
1170
- "commit_default_override": True,
1171
- "commit_default": False,
1171
+ "dryrun_default_override": True,
1172
+ "dryrun_default": True,
1172
1173
  "hidden_override": True,
1173
1174
  "hidden": True,
1174
- "read_only_override": True,
1175
- "read_only": True,
1176
1175
  "soft_time_limit_override": True,
1177
1176
  "soft_time_limit": 350.1,
1178
1177
  "time_limit_override": True,
@@ -1192,14 +1191,16 @@ class JobTest(
1192
1191
 
1193
1192
  def setUp(self):
1194
1193
  super().setUp()
1195
- self.default_job_name = "local/api_test_job/APITestJob"
1194
+ self.default_job_name = "api_test_job.APITestJob"
1195
+ self.job_class = get_job(self.default_job_name)
1196
+ self.assertIsNotNone(self.job_class)
1196
1197
  self.job_model = Job.objects.get_for_class_path(self.default_job_name)
1197
1198
  self.job_model.enabled = True
1198
1199
  self.job_model.validated_save()
1199
1200
 
1200
1201
  run_success_response_status = status.HTTP_201_CREATED
1201
1202
 
1202
- def get_run_url(self, class_path="local/api_test_job/APITestJob"):
1203
+ def get_run_url(self, class_path="api_test_job.APITestJob"):
1203
1204
  job_model = Job.objects.get_for_class_path(class_path)
1204
1205
  return reverse("extras-api:job-run", kwargs={"pk": job_model.pk})
1205
1206
 
@@ -1219,7 +1220,7 @@ class JobTest(
1219
1220
 
1220
1221
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1221
1222
  def test_update_job_with_sensitive_variables_set_approval_required_to_true(self):
1222
- job_model = Job.objects.get_for_class_path("local/api_test_job/APITestJob")
1223
+ job_model = Job.objects.get_for_class_path("api_test_job.APITestJob")
1223
1224
  job_model.has_sensitive_variables = True
1224
1225
  job_model.has_sensitive_variables_override = True
1225
1226
  job_model.validated_save()
@@ -1241,7 +1242,7 @@ class JobTest(
1241
1242
 
1242
1243
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1243
1244
  def test_update_approval_required_job_set_has_sensitive_variables_to_true(self):
1244
- job_model = Job.objects.get_for_class_path("local/api_test_job/APITestJob")
1245
+ job_model = Job.objects.get_for_class_path("api_test_job.APITestJob")
1245
1246
  job_model.approval_required = True
1246
1247
  job_model.approval_required_override = True
1247
1248
  job_model.validated_save()
@@ -1286,7 +1287,7 @@ class JobTest(
1286
1287
  mock_get_worker_count.return_value = 1
1287
1288
  obj_perm = ObjectPermission(
1288
1289
  name="Test permission",
1289
- constraints={"module_name__in": ["test_pass", "test_fail"]},
1290
+ constraints={"module_name__in": ["pass", "fail"]},
1290
1291
  actions=["run"],
1291
1292
  )
1292
1293
  obj_perm.save()
@@ -1300,10 +1301,10 @@ class JobTest(
1300
1301
  self.assertHttpStatus(response, status.HTTP_404_NOT_FOUND)
1301
1302
 
1302
1303
  # Try post to permitted job
1303
- job_model = Job.objects.get_for_class_path("local/test_pass/TestPass")
1304
+ job_model = Job.objects.get_for_class_path("pass.TestPass")
1304
1305
  job_model.enabled = True
1305
1306
  job_model.validated_save()
1306
- url = self.get_run_url("local/test_pass/TestPass")
1307
+ url = self.get_run_url("pass.TestPass")
1307
1308
  response = self.client.post(url, **self.header)
1308
1309
  self.assertHttpStatus(response, self.run_success_response_status)
1309
1310
 
@@ -1331,7 +1332,6 @@ class JobTest(
1331
1332
  self.add_permissions("extras.run_job")
1332
1333
 
1333
1334
  job_model = Job(
1334
- source="local",
1335
1335
  module_name="uninstalled_module",
1336
1336
  job_class_name="NoSuchJob",
1337
1337
  grouping="Uninstalled Module",
@@ -1341,7 +1341,7 @@ class JobTest(
1341
1341
  )
1342
1342
  job_model.validated_save()
1343
1343
 
1344
- url = self.get_run_url("local/uninstalled_module/NoSuchJob")
1344
+ url = self.get_run_url("uninstalled_module.NoSuchJob")
1345
1345
  with disable_warnings("django.request"):
1346
1346
  response = self.client.post(url, {}, format="json", **self.header)
1347
1347
  self.assertHttpStatus(response, status.HTTP_405_METHOD_NOT_ALLOWED)
@@ -1362,7 +1362,6 @@ class JobTest(
1362
1362
 
1363
1363
  data = {
1364
1364
  "data": job_data,
1365
- "commit": True,
1366
1365
  }
1367
1366
 
1368
1367
  url = self.get_run_url()
@@ -1388,7 +1387,6 @@ class JobTest(
1388
1387
 
1389
1388
  data = {
1390
1389
  "data": job_data,
1391
- "commit": True,
1392
1390
  "schedule": {
1393
1391
  "name": "test",
1394
1392
  "interval": "future",
@@ -1401,17 +1399,19 @@ class JobTest(
1401
1399
  self.assertHttpStatus(response, self.run_success_response_status)
1402
1400
 
1403
1401
  schedule = ScheduledJob.objects.last()
1404
- self.assertEqual(schedule.kwargs["data"]["var4"], str(device_role.pk))
1402
+ self.assertEqual(schedule.kwargs["var4"], str(device_role.pk))
1405
1403
 
1406
1404
  self.assertIn("scheduled_job", response.data)
1407
1405
  self.assertIn("job_result", response.data)
1408
1406
  self.assertEqual(response.data["scheduled_job"]["id"], str(schedule.pk))
1407
+ self.assertEqual(response.data["scheduled_job"]["url"], self.absolute_api_url(schedule))
1408
+ self.assertEqual(response.data["scheduled_job"]["name"], schedule.name)
1409
+ # Python < 3.11 doesn't understand the datetime string "2023-04-27T18:33:16.017865Z",
1410
+ # but it *does* understand the string "2023-04-27T18:33:17.330836+00:00"
1409
1411
  self.assertEqual(
1410
- response.data["scheduled_job"]["url"],
1411
- "http://nautobot.example.com" + reverse("extras-api:scheduledjob-detail", kwargs={"pk": schedule.pk}),
1412
+ datetime.fromisoformat(response.data["scheduled_job"]["start_time"].replace("Z", "+00:00")),
1413
+ schedule.start_time,
1412
1414
  )
1413
- self.assertEqual(response.data["scheduled_job"]["name"], schedule.name)
1414
- self.assertEqual(response.data["scheduled_job"]["start_time"], schedule.start_time)
1415
1415
  self.assertEqual(response.data["scheduled_job"]["interval"], schedule.interval)
1416
1416
  self.assertIsNone(response.data["job_result"])
1417
1417
 
@@ -1440,7 +1440,6 @@ class JobTest(
1440
1440
 
1441
1441
  data = {
1442
1442
  "data": job_data,
1443
- "commit": True,
1444
1443
  # schedule is omitted
1445
1444
  }
1446
1445
 
@@ -1456,13 +1455,15 @@ class JobTest(
1456
1455
  self.assertIsNotNone(schedule)
1457
1456
  self.assertEqual(schedule.interval, JobExecutionType.TYPE_IMMEDIATELY)
1458
1457
  self.assertEqual(schedule.approval_required, self.job_model.approval_required)
1459
- self.assertEqual(schedule.kwargs["data"]["var4"], str(device_role.pk))
1458
+ self.assertEqual(schedule.kwargs["var4"], str(device_role.pk))
1460
1459
 
1461
1460
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1462
1461
  @mock.patch("nautobot.extras.api.views.get_worker_count")
1463
- def test_run_job_object_var_lookup(self, mock_get_worker_count):
1462
+ @mock.patch("nautobot.extras.models.jobs.JobResult.enqueue_job")
1463
+ def test_run_job_object_var_lookup(self, mock_enqueue_job, mock_get_worker_count):
1464
1464
  """Job run requests can reference objects by their attributes."""
1465
1465
  mock_get_worker_count.return_value = 1
1466
+ mock_enqueue_job.return_value = None
1466
1467
  self.add_permissions("extras.run_job")
1467
1468
  device_role = Role.objects.get_for_model(Device).first()
1468
1469
  job_data = {
@@ -1474,7 +1475,7 @@ class JobTest(
1474
1475
 
1475
1476
  # This handles things like ObjectVar fields looked up by non-UUID
1476
1477
  # Jobs are executed with deserialized data
1477
- deserialized_data = get_job(self.default_job_name).deserialize_data(job_data)
1478
+ deserialized_data = self.job_class.deserialize_data(job_data)
1478
1479
 
1479
1480
  self.assertEqual(
1480
1481
  deserialized_data,
@@ -1485,24 +1486,39 @@ class JobTest(
1485
1486
  response = self.client.post(url, {"data": job_data}, format="json", **self.header)
1486
1487
  self.assertHttpStatus(response, self.run_success_response_status)
1487
1488
 
1488
- job_result = JobResult.objects.get(name=self.default_job_name)
1489
- self.assertIn("data", job_result.task_kwargs)
1489
+ # Ensure the enqueue_job args deserialize to the same as originally inputted
1490
+ expected_enqueue_job_args = (self.job_model, self.user)
1491
+ expected_enqueue_job_kwargs = {
1492
+ "task_queue": settings.CELERY_TASK_DEFAULT_QUEUE,
1493
+ **self.job_class.serialize_data(deserialized_data),
1494
+ }
1495
+ mock_enqueue_job.assert_called_with(*expected_enqueue_job_args, **expected_enqueue_job_kwargs)
1490
1496
 
1491
- # Ensure the stored task_kwargs deserialize to the same as originally inputted
1492
- self.assertEqual(
1493
- get_job("local/api_test_job/APITestJob").deserialize_data(job_result.task_kwargs["data"]), deserialized_data
1494
- )
1497
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1498
+ @mock.patch("nautobot.extras.api.views.get_worker_count")
1499
+ def test_run_job_response_job_result(self, mock_get_worker_count):
1500
+ """Test job run response contains nested job result."""
1501
+ mock_get_worker_count.return_value = 1
1502
+ self.add_permissions("extras.run_job")
1503
+ device_role = Role.objects.get_for_model(Device).first()
1504
+ job_data = {
1505
+ "var1": "FooBar",
1506
+ "var2": 123,
1507
+ "var3": False,
1508
+ "var4": {"name": device_role.name},
1509
+ }
1510
+
1511
+ url = self.get_run_url()
1512
+ response = self.client.post(url, {"data": job_data}, format="json", **self.header)
1513
+ self.assertHttpStatus(response, self.run_success_response_status)
1514
+
1515
+ job_result = JobResult.objects.get(name=self.job_model.name)
1495
1516
 
1496
1517
  self.assertIn("scheduled_job", response.data)
1497
1518
  self.assertIn("job_result", response.data)
1498
1519
  self.assertIsNone(response.data["scheduled_job"])
1499
- # The urls in a NestedJobResultSerializer depends on the request context, which we don't have
1500
1520
  data_job_result = response.data["job_result"]
1501
- del data_job_result["url"]
1502
- del data_job_result["user"]["url"]
1503
- expected_data_job_result = NestedJobResultSerializer(job_result, context={"request": None}).data
1504
- del expected_data_job_result["url"]
1505
- del expected_data_job_result["user"]["url"]
1521
+ expected_data_job_result = JobResultSerializer(job_result, context={"request": response.wsgi_request}).data
1506
1522
  self.assertEqual(data_job_result, expected_data_job_result)
1507
1523
 
1508
1524
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
@@ -1512,7 +1528,7 @@ class JobTest(
1512
1528
 
1513
1529
  test_file = SimpleUploadedFile(name="test_file.txt", content=b"I am content.\n")
1514
1530
 
1515
- job_model = Job.objects.get_for_class_path("local/test_field_order/TestFieldOrder")
1531
+ job_model = Job.objects.get_for_class_path("field_order.TestFieldOrder")
1516
1532
  job_model.enabled = True
1517
1533
  job_model.validated_save()
1518
1534
 
@@ -1523,10 +1539,9 @@ class JobTest(
1523
1539
  "var2": "Ground control to Major Tom",
1524
1540
  "var23": "Commencing countdown, engines on",
1525
1541
  "var1": test_file,
1526
- "_commit": True,
1527
1542
  }
1528
1543
 
1529
- url = self.get_run_url(class_path="local/test_field_order/TestFieldOrder")
1544
+ url = self.get_run_url(class_path="field_order.TestFieldOrder")
1530
1545
  response = self.client.post(url, data=job_data, **self.header)
1531
1546
  self.assertHttpStatus(response, self.run_success_response_status)
1532
1547
 
@@ -1537,7 +1552,7 @@ class JobTest(
1537
1552
 
1538
1553
  test_file = SimpleUploadedFile(name="test_file.txt", content=b"I am content.\n")
1539
1554
 
1540
- job_model = Job.objects.get_for_class_path("local/test_field_order/TestFieldOrder")
1555
+ job_model = Job.objects.get_for_class_path("field_order.TestFieldOrder")
1541
1556
  job_model.enabled = True
1542
1557
  job_model.validated_save()
1543
1558
 
@@ -1550,7 +1565,7 @@ class JobTest(
1550
1565
  "var1": test_file,
1551
1566
  }
1552
1567
 
1553
- url = self.get_run_url(class_path="local/test_field_order/TestFieldOrder")
1568
+ url = self.get_run_url(class_path="field_order.TestFieldOrder")
1554
1569
  response = self.client.post(url, data=job_data, **self.header)
1555
1570
  self.assertHttpStatus(response, self.run_success_response_status)
1556
1571
 
@@ -1561,7 +1576,7 @@ class JobTest(
1561
1576
 
1562
1577
  test_file = SimpleUploadedFile(name="test_file.txt", content=b"I am content.\n")
1563
1578
 
1564
- job_model = Job.objects.get_for_class_path("local/test_field_order/TestFieldOrder")
1579
+ job_model = Job.objects.get_for_class_path("field_order.TestFieldOrder")
1565
1580
  job_model.enabled = True
1566
1581
  job_model.validated_save()
1567
1582
 
@@ -1572,13 +1587,12 @@ class JobTest(
1572
1587
  "var2": "Ground control to Major Tom",
1573
1588
  "var23": "Commencing countdown, engines on",
1574
1589
  "var1": test_file,
1575
- "_commit": True,
1576
1590
  "_schedule_start_time": str(datetime.now() + timedelta(minutes=1)),
1577
1591
  "_schedule_interval": "future",
1578
1592
  "_schedule_name": "test",
1579
1593
  }
1580
1594
 
1581
- url = self.get_run_url(class_path="local/test_field_order/TestFieldOrder")
1595
+ url = self.get_run_url(class_path="field_order.TestFieldOrder")
1582
1596
  response = self.client.post(url, data=job_data, **self.header)
1583
1597
  self.assertHttpStatus(response, self.run_success_response_status)
1584
1598
 
@@ -1591,7 +1605,6 @@ class JobTest(
1591
1605
  d = Role.objects.get_for_model(Device).first()
1592
1606
  data = {
1593
1607
  "data": {"var1": "x", "var2": 1, "var3": False, "var4": d.pk},
1594
- "commit": True,
1595
1608
  "schedule": {
1596
1609
  "start_time": str(datetime.now() + timedelta(minutes=1)),
1597
1610
  "interval": "future",
@@ -1604,17 +1617,17 @@ class JobTest(
1604
1617
  self.assertHttpStatus(response, self.run_success_response_status)
1605
1618
 
1606
1619
  schedule = ScheduledJob.objects.last()
1607
- self.assertEqual(schedule.kwargs["scheduled_job_pk"], str(schedule.pk))
1608
-
1609
1620
  self.assertIn("scheduled_job", response.data)
1610
1621
  self.assertIn("job_result", response.data)
1611
1622
  self.assertEqual(response.data["scheduled_job"]["id"], str(schedule.pk))
1623
+ self.assertEqual(response.data["scheduled_job"]["url"], self.absolute_api_url(schedule))
1624
+ self.assertEqual(response.data["scheduled_job"]["name"], schedule.name)
1625
+ # Python < 3.11 doesn't understand the datetime string "2023-04-27T18:33:16.017865Z",
1626
+ # but it *does* understand the string "2023-04-27T18:33:17.330836+00:00"
1612
1627
  self.assertEqual(
1613
- response.data["scheduled_job"]["url"],
1614
- "http://nautobot.example.com" + reverse("extras-api:scheduledjob-detail", kwargs={"pk": schedule.pk}),
1628
+ datetime.fromisoformat(response.data["scheduled_job"]["start_time"].replace("Z", "+00:00")),
1629
+ schedule.start_time,
1615
1630
  )
1616
- self.assertEqual(response.data["scheduled_job"]["name"], schedule.name)
1617
- self.assertEqual(response.data["scheduled_job"]["start_time"], schedule.start_time)
1618
1631
  self.assertEqual(response.data["scheduled_job"]["interval"], schedule.interval)
1619
1632
  self.assertIsNone(response.data["job_result"])
1620
1633
 
@@ -1631,7 +1644,6 @@ class JobTest(
1631
1644
  url = reverse("extras-api:job-run", kwargs={"pk": job_model.pk})
1632
1645
  data = {
1633
1646
  "data": {},
1634
- "commit": True,
1635
1647
  "schedule": {
1636
1648
  "start_time": str(datetime.now() + timedelta(minutes=1)),
1637
1649
  "interval": "future",
@@ -1662,7 +1674,6 @@ class JobTest(
1662
1674
  url = reverse("extras-api:job-run", kwargs={"pk": job_model.pk})
1663
1675
  data = {
1664
1676
  "data": {},
1665
- "commit": True,
1666
1677
  "schedule": {
1667
1678
  "interval": "immediately",
1668
1679
  "name": "test",
@@ -1679,30 +1690,27 @@ class JobTest(
1679
1690
  )
1680
1691
 
1681
1692
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1682
- @mock.patch("nautobot.extras.api.views.get_worker_count")
1683
- def test_run_a_job_with_sensitive_variables_immediately(self, mock_get_worker_count):
1684
- mock_get_worker_count.return_value = 1
1693
+ @mock.patch("nautobot.extras.api.views.get_worker_count", return_value=1)
1694
+ def test_run_a_job_with_sensitive_variables_immediately(self, _):
1685
1695
  self.add_permissions("extras.run_job")
1686
1696
  d = Role.objects.get_for_model(Device).first()
1687
1697
  data = {
1688
1698
  "data": {"var1": "x", "var2": 1, "var3": False, "var4": d.pk},
1689
- "commit": True,
1690
1699
  "schedule": {
1691
1700
  "interval": "immediately",
1692
1701
  "name": "test",
1693
1702
  },
1694
1703
  }
1695
- job = Job.objects.get_for_class_path(self.default_job_name)
1696
- job.has_sensitive_variables = True
1697
- job.has_sensitive_variables_override = True
1698
- job.validated_save()
1704
+ self.job_model.has_sensitive_variables = True
1705
+ self.job_model.has_sensitive_variables_override = True
1706
+ self.job_model.validated_save()
1699
1707
 
1700
1708
  url = self.get_run_url()
1701
1709
  response = self.client.post(url, data, format="json", **self.header)
1702
1710
  self.assertHttpStatus(response, self.run_success_response_status)
1703
1711
 
1704
- job_result = JobResult.objects.get(name=self.default_job_name)
1705
- self.assertEqual(job_result.task_kwargs, None)
1712
+ job_result = JobResult.objects.get(name=self.job_model.name)
1713
+ self.assertEqual(job_result.task_kwargs, {})
1706
1714
 
1707
1715
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1708
1716
  @mock.patch("nautobot.extras.api.views.get_worker_count")
@@ -1712,7 +1720,6 @@ class JobTest(
1712
1720
  d = Role.objects.get_for_model(Device).first()
1713
1721
  data = {
1714
1722
  "data": {"var1": "x", "var2": 1, "var3": False, "var4": d.pk},
1715
- "commit": True,
1716
1723
  "schedule": {
1717
1724
  "start_time": str(datetime.now() - timedelta(minutes=1)),
1718
1725
  "interval": "future",
@@ -1732,7 +1739,6 @@ class JobTest(
1732
1739
  d = Role.objects.get_for_model(Device).first()
1733
1740
  data = {
1734
1741
  "data": {"var1": "x", "var2": 1, "var3": False, "var4": d.pk},
1735
- "commit": True,
1736
1742
  "schedule": {
1737
1743
  "start_time": str(datetime.now() + timedelta(minutes=1)),
1738
1744
  "interval": "hourly",
@@ -1749,12 +1755,14 @@ class JobTest(
1749
1755
  self.assertIn("scheduled_job", response.data)
1750
1756
  self.assertIn("job_result", response.data)
1751
1757
  self.assertEqual(response.data["scheduled_job"]["id"], str(schedule.pk))
1758
+ self.assertEqual(response.data["scheduled_job"]["url"], self.absolute_api_url(schedule))
1759
+ self.assertEqual(response.data["scheduled_job"]["name"], schedule.name)
1760
+ # Python < 3.11 doesn't understand the datetime string "2023-04-27T18:33:16.017865Z",
1761
+ # but it *does* understand the string "2023-04-27T18:33:17.330836+00:00"
1752
1762
  self.assertEqual(
1753
- response.data["scheduled_job"]["url"],
1754
- "http://nautobot.example.com" + reverse("extras-api:scheduledjob-detail", kwargs={"pk": schedule.pk}),
1763
+ datetime.fromisoformat(response.data["scheduled_job"]["start_time"].replace("Z", "+00:00")),
1764
+ schedule.start_time,
1755
1765
  )
1756
- self.assertEqual(response.data["scheduled_job"]["name"], schedule.name)
1757
- self.assertEqual(response.data["scheduled_job"]["start_time"], schedule.start_time)
1758
1766
  self.assertEqual(response.data["scheduled_job"]["interval"], schedule.interval)
1759
1767
  self.assertIsNone(response.data["job_result"])
1760
1768
 
@@ -1764,7 +1772,6 @@ class JobTest(
1764
1772
 
1765
1773
  data = {
1766
1774
  "data": "invalid",
1767
- "commit": True,
1768
1775
  }
1769
1776
 
1770
1777
  url = self.get_run_url()
@@ -1784,7 +1791,6 @@ class JobTest(
1784
1791
 
1785
1792
  data = {
1786
1793
  "data": job_data,
1787
- "commit": True,
1788
1794
  }
1789
1795
 
1790
1796
  url = self.get_run_url()
@@ -1803,7 +1809,6 @@ class JobTest(
1803
1809
 
1804
1810
  data = {
1805
1811
  "data": job_data,
1806
- "commit": True,
1807
1812
  }
1808
1813
 
1809
1814
  url = self.get_run_url()
@@ -1819,7 +1824,6 @@ class JobTest(
1819
1824
  d = Role.objects.get_for_model(Device).first()
1820
1825
  data = {
1821
1826
  "data": {"var1": "x", "var2": 1, "var3": False, "var4": d.pk},
1822
- "commit": True,
1823
1827
  "task_queue": "invalid",
1824
1828
  }
1825
1829
 
@@ -1838,7 +1842,6 @@ class JobTest(
1838
1842
  d = Role.objects.get_for_model(Device).first()
1839
1843
  data = {
1840
1844
  "data": {"var1": "x", "var2": 1, "var3": False, "var4": d.pk},
1841
- "commit": True,
1842
1845
  "task_queue": settings.CELERY_TASK_DEFAULT_QUEUE,
1843
1846
  }
1844
1847
 
@@ -1851,21 +1854,28 @@ class JobTest(
1851
1854
  def test_run_job_with_default_queue_with_empty_job_model_task_queues(self, _):
1852
1855
  self.add_permissions("extras.run_job")
1853
1856
  data = {
1854
- "commit": True,
1855
1857
  "task_queue": settings.CELERY_TASK_DEFAULT_QUEUE,
1856
1858
  }
1857
1859
 
1858
- job_model = Job.objects.get_for_class_path("local/test_pass/TestPass")
1860
+ job_model = Job.objects.get_for_class_path("pass.TestPass")
1859
1861
  job_model.enabled = True
1860
1862
  job_model.validated_save()
1861
- url = self.get_run_url("local/test_pass/TestPass")
1863
+ url = self.get_run_url("pass.TestPass")
1862
1864
  response = self.client.post(url, data, format="json", **self.header)
1863
1865
  self.assertHttpStatus(response, self.run_success_response_status)
1864
1866
 
1867
+ # TODO: Either improve test base or or write a more specific test for this model.
1868
+ @skip("Job has a `name` property but grouping is also used to sort Jobs")
1869
+ def test_list_objects_ascending_ordered(self):
1870
+ pass
1871
+
1872
+ @skip("Job has a `name` property but grouping is also used to sort Jobs")
1873
+ def test_list_objects_descending_ordered(self):
1874
+ pass
1875
+
1865
1876
 
1866
1877
  class JobHookTest(APIViewTestCases.APIViewTestCase):
1867
1878
  model = JobHook
1868
- brief_fields = ["display", "id", "name", "url"]
1869
1879
  choices_fields = []
1870
1880
  update_data = {
1871
1881
  "name": "Overridden name",
@@ -1972,7 +1982,6 @@ class JobHookTest(APIViewTestCases.APIViewTestCase):
1972
1982
 
1973
1983
  class JobButtonTest(APIViewTestCases.APIViewTestCase):
1974
1984
  model = JobButton
1975
- brief_fields = ["display", "id", "name", "url"]
1976
1985
  choices_fields = ["button_class"]
1977
1986
 
1978
1987
  @classmethod
@@ -2031,49 +2040,37 @@ class JobResultTest(
2031
2040
  APIViewTestCases.DeleteObjectViewTestCase,
2032
2041
  ):
2033
2042
  model = JobResult
2034
- brief_fields = ["date_created", "date_done", "display", "id", "name", "status", "url", "user"]
2035
2043
 
2036
2044
  @classmethod
2037
2045
  def setUpTestData(cls):
2038
2046
  jobs = Job.objects.all()[:2]
2039
- job_ct = ContentType.objects.get_for_model(Job)
2040
- git_ct = ContentType.objects.get_for_model(GitRepository)
2041
2047
 
2042
2048
  JobResult.objects.create(
2043
2049
  job_model=jobs[0],
2044
2050
  name=jobs[0].class_path,
2045
- obj_type=job_ct,
2046
- date_done=datetime.now(),
2051
+ date_done=now(),
2047
2052
  user=None,
2048
2053
  status=JobResultStatusChoices.STATUS_SUCCESS,
2049
- data={"output": "\nRan for 3 seconds"},
2050
- task_kwargs=None,
2054
+ task_kwargs={},
2051
2055
  scheduled_job=None,
2052
- task_id=uuid.uuid4(),
2053
2056
  )
2054
2057
  JobResult.objects.create(
2055
2058
  job_model=None,
2056
- name="Git Repository",
2057
- obj_type=git_ct,
2058
- date_done=datetime.now(),
2059
+ name="deleted_module.deleted_job",
2060
+ date_done=now(),
2059
2061
  user=None,
2060
2062
  status=JobResultStatusChoices.STATUS_SUCCESS,
2061
- data=None,
2062
2063
  task_kwargs={"repository_pk": uuid.uuid4()},
2063
2064
  scheduled_job=None,
2064
- task_id=uuid.uuid4(),
2065
2065
  )
2066
2066
  JobResult.objects.create(
2067
2067
  job_model=jobs[1],
2068
2068
  name=jobs[1].class_path,
2069
- obj_type=job_ct,
2070
2069
  date_done=None,
2071
2070
  user=None,
2072
2071
  status=JobResultStatusChoices.STATUS_PENDING,
2073
- data=None,
2074
2072
  task_kwargs={"data": {"device": uuid.uuid4(), "multichoices": ["red", "green"], "checkbox": False}},
2075
2073
  scheduled_job=None,
2076
- task_id=uuid.uuid4(),
2077
2074
  )
2078
2075
 
2079
2076
 
@@ -2082,27 +2079,11 @@ class JobLogEntryTest(
2082
2079
  APIViewTestCases.ListObjectsViewTestCase,
2083
2080
  ):
2084
2081
  model = JobLogEntry
2085
- brief_fields = [
2086
- "absolute_url",
2087
- "created",
2088
- "display",
2089
- "grouping",
2090
- "id",
2091
- "job_result",
2092
- "log_level",
2093
- "log_object",
2094
- "message",
2095
- "url",
2096
- ]
2097
2082
  choices_fields = []
2098
2083
 
2099
2084
  @classmethod
2100
2085
  def setUpTestData(cls):
2101
- cls.job_result = JobResult.objects.create(
2102
- name="test",
2103
- task_id=uuid.uuid4(),
2104
- obj_type=ContentType.objects.get_for_model(GitRepository),
2105
- )
2086
+ cls.job_result = JobResult.objects.create(name="test")
2106
2087
 
2107
2088
  for log_level in ("debug", "info", "success", "warning"):
2108
2089
  JobLogEntry.objects.create(
@@ -2125,16 +2106,15 @@ class ScheduledJobTest(
2125
2106
  APIViewTestCases.ListObjectsViewTestCase,
2126
2107
  ):
2127
2108
  model = ScheduledJob
2128
- brief_fields = ["crontab", "display", "id", "interval", "name", "start_time", "url"]
2129
2109
  choices_fields = []
2130
2110
 
2131
2111
  @classmethod
2132
2112
  def setUpTestData(cls):
2133
2113
  user = User.objects.create(username="user1", is_active=True)
2134
- job_model = Job.objects.get_for_class_path("local/test_pass/TestPass")
2114
+ job_model = Job.objects.get_for_class_path("pass.TestPass")
2135
2115
  ScheduledJob.objects.create(
2136
2116
  name="test1",
2137
- task="nautobot.extras.jobs.scheduled_job_handler",
2117
+ task="pass.TestPass",
2138
2118
  job_class=job_model.class_path,
2139
2119
  job_model=job_model,
2140
2120
  interval=JobExecutionType.TYPE_IMMEDIATELY,
@@ -2144,7 +2124,7 @@ class ScheduledJobTest(
2144
2124
  )
2145
2125
  ScheduledJob.objects.create(
2146
2126
  name="test2",
2147
- task="nautobot.extras.jobs.scheduled_job_handler",
2127
+ task="pass.TestPass",
2148
2128
  job_class=job_model.class_path,
2149
2129
  job_model=job_model,
2150
2130
  interval=JobExecutionType.TYPE_IMMEDIATELY,
@@ -2154,7 +2134,7 @@ class ScheduledJobTest(
2154
2134
  )
2155
2135
  ScheduledJob.objects.create(
2156
2136
  name="test3",
2157
- task="nautobot.extras.jobs.scheduled_job_handler",
2137
+ task="pass.TestPass",
2158
2138
  job_class=job_model.class_path,
2159
2139
  job_model=job_model,
2160
2140
  interval=JobExecutionType.TYPE_IMMEDIATELY,
@@ -2163,17 +2143,26 @@ class ScheduledJobTest(
2163
2143
  start_time=now(),
2164
2144
  )
2165
2145
 
2146
+ # TODO: Unskip after resolving #2908, #2909
2147
+ @skip("DRF's built-in OrderingFilter triggering natural key attribute error in our base")
2148
+ def test_list_objects_ascending_ordered(self):
2149
+ pass
2150
+
2151
+ @skip("DRF's built-in OrderingFilter triggering natural key attribute error in our base")
2152
+ def test_list_objects_descending_ordered(self):
2153
+ pass
2154
+
2166
2155
 
2167
2156
  class JobApprovalTest(APITestCase):
2168
2157
  @classmethod
2169
2158
  def setUpTestData(cls):
2170
2159
  cls.additional_user = User.objects.create(username="user1", is_active=True)
2171
- cls.job_model = Job.objects.get_for_class_path("local/test_pass/TestPass")
2160
+ cls.job_model = Job.objects.get_for_class_path("pass.TestPass")
2172
2161
  cls.job_model.enabled = True
2173
2162
  cls.job_model.save()
2174
2163
  cls.scheduled_job = ScheduledJob.objects.create(
2175
2164
  name="test",
2176
- task="nautobot.extras.jobs.scheduled_job_handler",
2165
+ task="pass.TestPass",
2177
2166
  job_class=cls.job_model.class_path,
2178
2167
  job_model=cls.job_model,
2179
2168
  interval=JobExecutionType.TYPE_IMMEDIATELY,
@@ -2181,6 +2170,19 @@ class JobApprovalTest(APITestCase):
2181
2170
  approval_required=True,
2182
2171
  start_time=now(),
2183
2172
  )
2173
+ cls.dryrun_job_model = Job.objects.get_for_class_path("dry_run.TestDryRun")
2174
+ cls.dryrun_job_model.enabled = True
2175
+ cls.dryrun_job_model.save()
2176
+ cls.dryrun_scheduled_job = ScheduledJob.objects.create(
2177
+ name="test",
2178
+ task="dry_run.TestDryRun",
2179
+ job_class=cls.dryrun_job_model.class_path,
2180
+ job_model=cls.dryrun_job_model,
2181
+ interval=JobExecutionType.TYPE_IMMEDIATELY,
2182
+ user=cls.additional_user,
2183
+ approval_required=True,
2184
+ start_time=now(),
2185
+ )
2184
2186
 
2185
2187
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2186
2188
  def test_approve_job_anonymous(self):
@@ -2214,7 +2216,7 @@ class JobApprovalTest(APITestCase):
2214
2216
  self.add_permissions("extras.approve_job", "extras.view_scheduledjob", "extras.change_scheduledjob")
2215
2217
  scheduled_job = ScheduledJob.objects.create(
2216
2218
  name="test",
2217
- task="nautobot.extras.jobs.scheduled_job_handler",
2219
+ task="pass.TestPass",
2218
2220
  job_class=self.job_model.class_path,
2219
2221
  job_model=self.job_model,
2220
2222
  interval=JobExecutionType.TYPE_IMMEDIATELY,
@@ -2238,7 +2240,7 @@ class JobApprovalTest(APITestCase):
2238
2240
  self.add_permissions("extras.approve_job", "extras.view_scheduledjob", "extras.change_scheduledjob")
2239
2241
  scheduled_job = ScheduledJob.objects.create(
2240
2242
  name="test",
2241
- task="nautobot.extras.jobs.scheduled_job_handler",
2243
+ task="pass.TestPass",
2242
2244
  job_class=self.job_model.class_path,
2243
2245
  job_model=self.job_model,
2244
2246
  interval=JobExecutionType.TYPE_FUTURE,
@@ -2256,7 +2258,7 @@ class JobApprovalTest(APITestCase):
2256
2258
  self.add_permissions("extras.approve_job", "extras.view_scheduledjob", "extras.change_scheduledjob")
2257
2259
  scheduled_job = ScheduledJob.objects.create(
2258
2260
  name="test",
2259
- task="nautobot.extras.jobs.scheduled_job_handler",
2261
+ task="pass.TestPass",
2260
2262
  job_class=self.job_model.class_path,
2261
2263
  job_model=self.job_model,
2262
2264
  interval=JobExecutionType.TYPE_FUTURE,
@@ -2300,7 +2302,7 @@ class JobApprovalTest(APITestCase):
2300
2302
 
2301
2303
  @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
2302
2304
  def test_dry_run_job_without_permission(self):
2303
- url = reverse("extras-api:scheduledjob-dry-run", kwargs={"pk": self.scheduled_job.pk})
2305
+ url = reverse("extras-api:scheduledjob-dry-run", kwargs={"pk": self.dryrun_scheduled_job.pk})
2304
2306
  with disable_warnings("django.request"):
2305
2307
  response = self.client.post(url, **self.header)
2306
2308
  self.assertHttpStatus(response, status.HTTP_403_FORBIDDEN)
@@ -2308,29 +2310,27 @@ class JobApprovalTest(APITestCase):
2308
2310
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2309
2311
  def test_dry_run_job_without_run_job_permission(self):
2310
2312
  self.add_permissions("extras.view_scheduledjob")
2311
- url = reverse("extras-api:scheduledjob-dry-run", kwargs={"pk": self.scheduled_job.pk})
2313
+ url = reverse("extras-api:scheduledjob-dry-run", kwargs={"pk": self.dryrun_scheduled_job.pk})
2312
2314
  response = self.client.post(url, **self.header)
2313
2315
  self.assertHttpStatus(response, status.HTTP_403_FORBIDDEN)
2314
2316
 
2315
2317
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2316
2318
  def test_dry_run_job(self):
2317
2319
  self.add_permissions("extras.run_job", "extras.view_scheduledjob")
2318
- url = reverse("extras-api:scheduledjob-dry-run", kwargs={"pk": self.scheduled_job.pk})
2320
+ url = reverse("extras-api:scheduledjob-dry-run", kwargs={"pk": self.dryrun_scheduled_job.pk})
2319
2321
  response = self.client.post(url, **self.header)
2320
2322
  self.assertHttpStatus(response, status.HTTP_200_OK)
2321
2323
 
2324
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2325
+ def test_dry_run_not_supported(self):
2326
+ self.add_permissions("extras.run_job", "extras.view_scheduledjob")
2327
+ url = reverse("extras-api:scheduledjob-dry-run", kwargs={"pk": self.scheduled_job.pk})
2328
+ response = self.client.post(url, **self.header)
2329
+ self.assertHttpStatus(response, status.HTTP_405_METHOD_NOT_ALLOWED)
2330
+
2322
2331
 
2323
2332
  class NoteTest(APIViewTestCases.APIViewTestCase):
2324
2333
  model = Note
2325
- brief_fields = [
2326
- "assigned_object",
2327
- "display",
2328
- "id",
2329
- "note",
2330
- "slug",
2331
- "url",
2332
- "user",
2333
- ]
2334
2334
  choices_fields = ["assigned_object_type"]
2335
2335
 
2336
2336
  @classmethod
@@ -2383,26 +2383,25 @@ class NoteTest(APIViewTestCases.APIViewTestCase):
2383
2383
 
2384
2384
  class RelationshipTest(APIViewTestCases.APIViewTestCase, RequiredRelationshipTestMixin):
2385
2385
  model = Relationship
2386
- brief_fields = ["display", "id", "name", "slug", "url"]
2387
2386
 
2388
2387
  create_data = [
2389
2388
  {
2390
- "name": "Device VLANs",
2391
- "slug": "device-vlans",
2389
+ "label": "Device VLANs",
2390
+ "key": "device_vlans",
2392
2391
  "type": "many-to-many",
2393
2392
  "source_type": "ipam.vlan",
2394
2393
  "destination_type": "dcim.device",
2395
2394
  },
2396
2395
  {
2397
- "name": "Primary VLAN",
2398
- "slug": "primary-vlan",
2396
+ "label": "Primary VLAN",
2397
+ "key": "primary_vlan",
2399
2398
  "type": "one-to-many",
2400
2399
  "source_type": "ipam.vlan",
2401
2400
  "destination_type": "dcim.device",
2402
2401
  },
2403
2402
  {
2404
- "name": "Primary Interface",
2405
- "slug": "primary-interface",
2403
+ "label": "Primary Interface",
2404
+ "key": "primary_interface",
2406
2405
  "type": "one-to-one",
2407
2406
  "source_type": "dcim.device",
2408
2407
  "source_label": "primary interface",
@@ -2410,7 +2409,7 @@ class RelationshipTest(APIViewTestCases.APIViewTestCase, RequiredRelationshipTes
2410
2409
  "destination_hidden": True,
2411
2410
  },
2412
2411
  {
2413
- "name": "Relationship 1",
2412
+ "label": "Relationship 1",
2414
2413
  "type": "one-to-one",
2415
2414
  "source_type": "dcim.device",
2416
2415
  "source_label": "primary interface",
@@ -2423,7 +2422,7 @@ class RelationshipTest(APIViewTestCases.APIViewTestCase, RequiredRelationshipTes
2423
2422
  "source_filter": {"slug": ["some-slug"]},
2424
2423
  }
2425
2424
  choices_fields = ["destination_type", "source_type", "type", "required_on"]
2426
- slug_source = "name"
2425
+ slug_source = "label"
2427
2426
  slugify_function = staticmethod(slugify_dashes_to_underscores)
2428
2427
 
2429
2428
  @classmethod
@@ -2433,15 +2432,15 @@ class RelationshipTest(APIViewTestCases.APIViewTestCase, RequiredRelationshipTes
2433
2432
 
2434
2433
  cls.relationships = (
2435
2434
  Relationship(
2436
- name="Related locations",
2437
- slug="related-locations",
2435
+ label="Related locations",
2436
+ key="related_locations",
2438
2437
  type="symmetric-many-to-many",
2439
2438
  source_type=location_type,
2440
2439
  destination_type=location_type,
2441
2440
  ),
2442
2441
  Relationship(
2443
- name="Unrelated locations",
2444
- slug="unrelated-locations",
2442
+ label="Unrelated locations",
2443
+ key="unrelated_locations",
2445
2444
  type="many-to-many",
2446
2445
  source_type=location_type,
2447
2446
  source_label="Other locations (from source side)",
@@ -2449,8 +2448,8 @@ class RelationshipTest(APIViewTestCases.APIViewTestCase, RequiredRelationshipTes
2449
2448
  destination_label="Other locations (from destination side)",
2450
2449
  ),
2451
2450
  Relationship(
2452
- name="Devices found elsewhere",
2453
- slug="devices-elsewhere",
2451
+ label="Devices found elsewhere",
2452
+ key="devices_elsewhere",
2454
2453
  type="many-to-many",
2455
2454
  source_type=location_type,
2456
2455
  destination_type=device_type,
@@ -2459,9 +2458,8 @@ class RelationshipTest(APIViewTestCases.APIViewTestCase, RequiredRelationshipTes
2459
2458
  for relationship in cls.relationships:
2460
2459
  relationship.validated_save()
2461
2460
  cls.lt = LocationType.objects.get(name="Campus")
2462
- cls.location = Location.objects.create(
2463
- name="Location 1", status=Status.objects.get(slug="active"), location_type=cls.lt
2464
- )
2461
+ location_status = Status.objects.get_for_model(Location).first()
2462
+ cls.location = Location.objects.create(name="Location 1", status=location_status, location_type=cls.lt)
2465
2463
 
2466
2464
  def test_get_all_relationships_on_location(self):
2467
2465
  """Verify that all relationships are accurately represented when requested."""
@@ -2476,13 +2474,10 @@ class RelationshipTest(APIViewTestCases.APIViewTestCase, RequiredRelationshipTes
2476
2474
  self.maxDiff = None
2477
2475
  self.assertEqual(
2478
2476
  {
2479
- self.relationships[0].slug: {
2477
+ self.relationships[0].key: {
2480
2478
  "id": str(self.relationships[0].pk),
2481
- "url": (
2482
- "http://nautobot.example.com"
2483
- + reverse("extras-api:relationship-detail", kwargs={"pk": self.relationships[0].pk})
2484
- ),
2485
- "name": self.relationships[0].name,
2479
+ "url": self.absolute_api_url(self.relationships[0]),
2480
+ "label": self.relationships[0].label,
2486
2481
  "type": self.relationships[0].type,
2487
2482
  "peer": {
2488
2483
  "label": "locations",
@@ -2490,13 +2485,10 @@ class RelationshipTest(APIViewTestCases.APIViewTestCase, RequiredRelationshipTes
2490
2485
  "objects": [],
2491
2486
  },
2492
2487
  },
2493
- self.relationships[1].slug: {
2488
+ self.relationships[1].key: {
2494
2489
  "id": str(self.relationships[1].pk),
2495
- "url": (
2496
- "http://nautobot.example.com"
2497
- + reverse("extras-api:relationship-detail", kwargs={"pk": self.relationships[1].pk})
2498
- ),
2499
- "name": self.relationships[1].name,
2490
+ "url": self.absolute_api_url(self.relationships[1]),
2491
+ "label": self.relationships[1].label,
2500
2492
  "type": self.relationships[1].type,
2501
2493
  "destination": {
2502
2494
  "label": self.relationships[1].source_label, # yes -- it's a bit confusing
@@ -2509,13 +2501,10 @@ class RelationshipTest(APIViewTestCases.APIViewTestCase, RequiredRelationshipTes
2509
2501
  "objects": [],
2510
2502
  },
2511
2503
  },
2512
- self.relationships[2].slug: {
2504
+ self.relationships[2].key: {
2513
2505
  "id": str(self.relationships[2].pk),
2514
- "url": (
2515
- "http://nautobot.example.com"
2516
- + reverse("extras-api:relationship-detail", kwargs={"pk": self.relationships[2].pk})
2517
- ),
2518
- "name": self.relationships[2].name,
2506
+ "url": self.absolute_api_url(self.relationships[2]),
2507
+ "label": self.relationships[2].label,
2519
2508
  "type": self.relationships[2].type,
2520
2509
  "destination": {
2521
2510
  "label": "devices",
@@ -2531,28 +2520,33 @@ class RelationshipTest(APIViewTestCases.APIViewTestCase, RequiredRelationshipTes
2531
2520
  """Verify that relationship associations can be populated at instance creation time."""
2532
2521
  location_type = LocationType.objects.get(name="Campus")
2533
2522
  existing_location_1 = Location.objects.create(
2534
- name="Existing Location 1", status=Status.objects.get(slug="active"), location_type=location_type
2523
+ name="Existing Location 1",
2524
+ status=Status.objects.get_for_model(Location).first(),
2525
+ location_type=location_type,
2535
2526
  )
2536
2527
  existing_location_2 = Location.objects.create(
2537
- name="Existing Location 2", status=Status.objects.get(slug="active"), location_type=location_type
2528
+ name="Existing Location 2",
2529
+ status=Status.objects.get_for_model(Location).first(),
2530
+ location_type=location_type,
2538
2531
  )
2539
- manufacturer = Manufacturer.objects.create(name="Manufacturer 1", slug="manufacturer-1")
2532
+ manufacturer = Manufacturer.objects.first()
2540
2533
  device_type = DeviceType.objects.create(
2541
2534
  manufacturer=manufacturer,
2542
2535
  model="device Type 1",
2543
2536
  slug="device-type-1",
2544
2537
  )
2545
2538
  device_role = Role.objects.get_for_model(Device).first()
2539
+ device_status = Status.objects.get_for_model(Device).first()
2546
2540
  existing_device_1 = Device.objects.create(
2547
2541
  name="existing-device-location-1",
2548
- status=Status.objects.get(slug="active"),
2542
+ status=device_status,
2549
2543
  role=device_role,
2550
2544
  device_type=device_type,
2551
2545
  location=existing_location_1,
2552
2546
  )
2553
2547
  existing_device_2 = Device.objects.create(
2554
2548
  name="existing-device-location-2",
2555
- status=Status.objects.get(slug="active"),
2549
+ status=device_status,
2556
2550
  role=device_role,
2557
2551
  device_type=device_type,
2558
2552
  location=existing_location_2,
@@ -2563,20 +2557,20 @@ class RelationshipTest(APIViewTestCases.APIViewTestCase, RequiredRelationshipTes
2563
2557
  reverse("dcim-api:location-list"),
2564
2558
  data={
2565
2559
  "name": "New location",
2566
- "status": Status.objects.get(slug="active").pk,
2560
+ "status": Status.objects.get_for_model(Location).first().pk,
2567
2561
  "location_type": location_type.pk,
2568
2562
  "relationships": {
2569
- self.relationships[0].slug: {
2563
+ self.relationships[0].key: {
2570
2564
  "peer": {
2571
2565
  "objects": [str(existing_location_1.pk)],
2572
2566
  },
2573
2567
  },
2574
- self.relationships[1].slug: {
2568
+ self.relationships[1].key: {
2575
2569
  "source": {
2576
2570
  "objects": [str(existing_location_2.pk)],
2577
2571
  },
2578
2572
  },
2579
- self.relationships[2].slug: {
2573
+ self.relationships[2].key: {
2580
2574
  "destination": {
2581
2575
  "objects": [
2582
2576
  {"name": "existing-device-location-1"},
@@ -2653,25 +2647,25 @@ class RelationshipTest(APIViewTestCases.APIViewTestCase, RequiredRelationshipTes
2653
2647
  **self.header,
2654
2648
  )
2655
2649
 
2656
- status_active = Status.objects.get(slug="active")
2650
+ device_status = Status.objects.get_for_model(Device).first()
2657
2651
 
2658
2652
  # Try deleting all devices and then creating 2 VLANs (fails):
2659
2653
  Device.objects.all().delete()
2660
2654
  response = send_bulk_data(
2661
2655
  "post",
2662
2656
  data=[
2663
- {"vid": "1", "name": "1", "status": status_active.pk},
2664
- {"vid": "2", "name": "2", "status": status_active.pk},
2657
+ {"vid": "1", "name": "1", "status": device_status.pk},
2658
+ {"vid": "2", "name": "2", "status": device_status.pk},
2665
2659
  ],
2666
2660
  )
2667
2661
  self.assertHttpStatus(response, 400)
2668
2662
  self.assertEqual(
2669
2663
  {
2670
2664
  "relationships": {
2671
- "vlans-devices-m2m": [
2665
+ "vlans_devices_m2m": [
2672
2666
  "VLANs require at least one device, but no devices exist yet. "
2673
2667
  "Create a device by posting to /api/dcim/devices/",
2674
- 'You need to specify ["relationships"]["vlans-devices-m2m"]["source"]["objects"].',
2668
+ 'You need to specify ["relationships"]["vlans_devices_m2m"]["source"]["objects"].',
2675
2669
  ]
2676
2670
  }
2677
2671
  },
@@ -2680,11 +2674,11 @@ class RelationshipTest(APIViewTestCases.APIViewTestCase, RequiredRelationshipTes
2680
2674
 
2681
2675
  # Create test device for association
2682
2676
  device_for_association = test_views.create_test_device("VLAN Required Device")
2683
- required_relationship_json = {"vlans-devices-m2m": {"source": {"objects": [str(device_for_association.id)]}}}
2677
+ required_relationship_json = {"vlans_devices_m2m": {"source": {"objects": [str(device_for_association.id)]}}}
2684
2678
  expected_error_json = {
2685
2679
  "relationships": {
2686
- "vlans-devices-m2m": [
2687
- 'You need to specify ["relationships"]["vlans-devices-m2m"]["source"]["objects"].'
2680
+ "vlans_devices_m2m": [
2681
+ 'You need to specify ["relationships"]["vlans_devices_m2m"]["source"]["objects"].'
2688
2682
  ]
2689
2683
  }
2690
2684
  }
@@ -2695,21 +2689,21 @@ class RelationshipTest(APIViewTestCases.APIViewTestCase, RequiredRelationshipTes
2695
2689
  vlan1_json_data = {
2696
2690
  "vid": "1",
2697
2691
  "name": "1",
2698
- "status": status_active.pk,
2692
+ "status": device_status.pk,
2699
2693
  }
2700
2694
  vlan2_json_data = {
2701
2695
  "vid": "2",
2702
2696
  "name": "2",
2703
- "status": status_active.pk,
2697
+ "status": device_status.pk,
2704
2698
  }
2705
2699
  else:
2706
2700
  vlan1, vlan2 = VLANFactory.create_batch(2)
2707
- vlan1_json_data = {"status": status_active.pk, "id": str(vlan1.id)}
2701
+ vlan1_json_data = {"status": device_status.pk, "id": str(vlan1.id)}
2708
2702
  # Add required fields for PUT method:
2709
2703
  if method == "put":
2710
2704
  vlan1_json_data.update({"vid": vlan1.vid, "name": vlan1.name})
2711
2705
 
2712
- vlan2_json_data = {"status": status_active.pk, "id": str(vlan2.id)}
2706
+ vlan2_json_data = {"status": device_status.pk, "id": str(vlan2.id)}
2713
2707
  # Add required fields for PUT method:
2714
2708
  if method == "put":
2715
2709
  vlan2_json_data.update({"vid": vlan2.vid, "name": vlan2.name})
@@ -2738,24 +2732,23 @@ class RelationshipTest(APIViewTestCases.APIViewTestCase, RequiredRelationshipTes
2738
2732
 
2739
2733
  # Check the relationship associations were actually created
2740
2734
  for vlan in response.json():
2741
- associated_device = vlan["relationships"]["vlans-devices-m2m"]["source"]["objects"][0]
2735
+ associated_device = vlan["relationships"]["vlans_devices_m2m"]["source"]["objects"][0]
2742
2736
  self.assertEqual(str(device_for_association.id), associated_device["id"])
2743
2737
 
2744
2738
 
2745
2739
  class RelationshipAssociationTest(APIViewTestCases.APIViewTestCase):
2746
2740
  model = RelationshipAssociation
2747
- brief_fields = ["destination_id", "display", "id", "relationship", "source_id", "url"]
2748
2741
  choices_fields = ["destination_type", "source_type"]
2749
2742
 
2750
2743
  @classmethod
2751
2744
  def setUpTestData(cls):
2752
2745
  cls.location_type = ContentType.objects.get_for_model(Location)
2753
2746
  cls.device_type = ContentType.objects.get_for_model(Device)
2754
- cls.status_active = Status.objects.get(slug="active")
2747
+ cls.location_status = Status.objects.get_for_model(Location).first()
2755
2748
 
2756
2749
  cls.relationship = Relationship(
2757
- name="Devices found elsewhere",
2758
- slug="elsewhere-devices",
2750
+ label="Devices found elsewhere",
2751
+ key="elsewhere_devices",
2759
2752
  type="many-to-many",
2760
2753
  source_type=cls.location_type,
2761
2754
  destination_type=cls.device_type,
@@ -2764,28 +2757,29 @@ class RelationshipAssociationTest(APIViewTestCases.APIViewTestCase):
2764
2757
  cls.lt = LocationType.objects.get(name="Campus")
2765
2758
  cls.locations = (
2766
2759
  Location.objects.create(
2767
- name="Empty Location", slug="empty", status=cls.status_active, location_type=cls.lt
2760
+ name="Empty Location", slug="empty", status=cls.location_status, location_type=cls.lt
2768
2761
  ),
2769
2762
  Location.objects.create(
2770
- name="Occupied Location", slug="occupied", status=cls.status_active, location_type=cls.lt
2763
+ name="Occupied Location", slug="occupied", status=cls.location_status, location_type=cls.lt
2771
2764
  ),
2772
2765
  Location.objects.create(
2773
2766
  name="Another Empty Location",
2774
2767
  slug="another-empty",
2775
- status=cls.status_active,
2768
+ status=cls.location_status,
2776
2769
  location_type=cls.lt,
2777
2770
  ),
2778
2771
  )
2779
- manufacturer = Manufacturer.objects.create(name="Manufacturer 1", slug="manufacturer-1")
2772
+ manufacturer = Manufacturer.objects.first()
2780
2773
  devicetype = DeviceType.objects.create(manufacturer=manufacturer, model="Device Type 1", slug="device-type-1")
2781
2774
  devicerole = Role.objects.get_for_model(Device).first()
2775
+ device_status = Status.objects.get_for_model(Device).first()
2782
2776
  cls.devices = [
2783
2777
  Device.objects.create(
2784
2778
  name=f"Device {num}",
2785
2779
  device_type=devicetype,
2786
2780
  role=devicerole,
2787
2781
  location=cls.locations[1],
2788
- status=cls.status_active,
2782
+ status=device_status,
2789
2783
  )
2790
2784
  for num in range(1, 5)
2791
2785
  ]
@@ -2844,8 +2838,8 @@ class RelationshipAssociationTest(APIViewTestCases.APIViewTestCase):
2844
2838
  """Test creation of invalid relationship association restricted by destination/source filter."""
2845
2839
 
2846
2840
  relationship = Relationship.objects.create(
2847
- name="Device to location Rel 1",
2848
- slug="device-to-location-rel-1",
2841
+ label="Device to location Rel 1",
2842
+ key="device_to_location_rel_1",
2849
2843
  source_type=self.device_type,
2850
2844
  source_filter={"name": [self.devices[0].name]},
2851
2845
  destination_type=self.location_type,
@@ -2886,7 +2880,7 @@ class RelationshipAssociationTest(APIViewTestCases.APIViewTestCase):
2886
2880
  self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
2887
2881
  self.assertEqual(
2888
2882
  response.data[side],
2889
- [f"{field_error_name} violates {relationship.name} {side}_filter restriction"],
2883
+ [f"{field_error_name} violates {relationship.label} {side}_filter restriction"],
2890
2884
  )
2891
2885
 
2892
2886
  def test_model_clean_method_is_called(self):
@@ -2914,62 +2908,37 @@ class RelationshipAssociationTest(APIViewTestCases.APIViewTestCase):
2914
2908
  """
2915
2909
  self.add_permissions("dcim.view_location")
2916
2910
  response = self.client.get(
2917
- reverse("dcim-api:location-detail", kwargs={"pk": self.locations[0].pk}) + "?include=relationships",
2911
+ reverse("dcim-api:location-detail", kwargs={"pk": self.locations[0].pk})
2912
+ + "?include=relationships"
2913
+ + "&depth=1",
2918
2914
  **self.header,
2919
2915
  )
2920
2916
  self.assertHttpStatus(response, status.HTTP_200_OK)
2921
2917
  self.assertIn("relationships", response.data)
2922
2918
  self.assertIsInstance(response.data["relationships"], dict)
2923
2919
  # Ensure consistent ordering
2924
- response.data["relationships"][self.relationship.slug]["destination"]["objects"].sort(key=lambda v: v["name"])
2920
+ response.data["relationships"][self.relationship.key]["destination"]["objects"].sort(key=lambda v: v["name"])
2925
2921
  self.maxDiff = None
2926
- self.assertEqual(
2927
- {
2928
- self.relationship.slug: {
2929
- "id": str(self.relationship.pk),
2930
- "url": (
2931
- "http://nautobot.example.com"
2932
- + reverse("extras-api:relationship-detail", kwargs={"pk": self.relationship.pk})
2933
- ),
2934
- "name": self.relationship.name,
2935
- "type": "many-to-many",
2936
- "destination": {
2937
- "label": "devices",
2938
- "object_type": "dcim.device",
2939
- "objects": [
2940
- {
2941
- "id": str(self.devices[0].pk),
2942
- "url": (
2943
- "http://nautobot.example.com"
2944
- + reverse("dcim-api:device-detail", kwargs={"pk": self.devices[0].pk})
2945
- ),
2946
- "display": self.devices[0].display,
2947
- "name": self.devices[0].name,
2948
- },
2949
- {
2950
- "id": str(self.devices[1].pk),
2951
- "url": (
2952
- "http://nautobot.example.com"
2953
- + reverse("dcim-api:device-detail", kwargs={"pk": self.devices[1].pk})
2954
- ),
2955
- "display": self.devices[1].display,
2956
- "name": self.devices[1].name,
2957
- },
2958
- {
2959
- "id": str(self.devices[2].pk),
2960
- "url": (
2961
- "http://nautobot.example.com"
2962
- + reverse("dcim-api:device-detail", kwargs={"pk": self.devices[2].pk})
2963
- ),
2964
- "display": self.devices[2].display,
2965
- "name": self.devices[2].name,
2966
- },
2967
- ],
2968
- },
2969
- },
2970
- },
2971
- response.data["relationships"],
2972
- )
2922
+ relationship_data = response.data["relationships"][self.relationship.key]
2923
+ self.assertEqual(relationship_data["id"], str(self.relationship.pk))
2924
+ self.assertEqual(relationship_data["url"], self.absolute_api_url(self.relationship))
2925
+ self.assertEqual(relationship_data["label"], self.relationship.label)
2926
+ self.assertEqual(relationship_data["type"], "many-to-many")
2927
+ self.assertEqual(relationship_data["destination"]["label"], "devices")
2928
+ self.assertEqual(relationship_data["destination"]["object_type"], "dcim.device")
2929
+
2930
+ objects = response.data["relationships"][self.relationship.key]["destination"]["objects"]
2931
+ for i, obj in enumerate(objects):
2932
+ self.assertEqual(obj["id"], str(self.devices[i].pk))
2933
+ self.assertEqual(obj["url"], self.absolute_api_url(self.devices[i]))
2934
+ self.assertEqual(
2935
+ obj["display"],
2936
+ self.devices[i].display,
2937
+ )
2938
+ self.assertEqual(
2939
+ obj["name"],
2940
+ self.devices[i].name,
2941
+ )
2973
2942
 
2974
2943
  def test_update_association_data_on_location(self):
2975
2944
  """
@@ -3031,21 +3000,21 @@ class RelationshipAssociationTest(APIViewTestCases.APIViewTestCase):
3031
3000
 
3032
3001
  with self.subTest("Error handling: wrong relationship"):
3033
3002
  Relationship.objects.create(
3034
- name="Device-to-Device",
3035
- slug="device-to-device",
3003
+ label="Device-to-Device",
3004
+ key="device_to_device",
3036
3005
  source_type=self.device_type,
3037
3006
  destination_type=self.device_type,
3038
3007
  type=RelationshipTypeChoices.TYPE_ONE_TO_ONE,
3039
3008
  )
3040
3009
  response = self.client.patch(
3041
3010
  url,
3042
- {"relationships": {"device-to-device": {"peer": {"objects": []}}}},
3011
+ {"relationships": {"device_to_device": {"peer": {"objects": []}}}},
3043
3012
  format="json",
3044
3013
  **self.header,
3045
3014
  )
3046
3015
  self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
3047
3016
  self.assertEqual(
3048
- str(response.data["relationships"][0]), '"device-to-device" is not a relationship on dcim.Location'
3017
+ str(response.data["relationships"][0]), '"device_to_device" is not a relationship on dcim.Location'
3049
3018
  )
3050
3019
  self.assertEqual(3, RelationshipAssociation.objects.filter(relationship=self.relationship).count())
3051
3020
  for association in self.associations:
@@ -3054,7 +3023,7 @@ class RelationshipAssociationTest(APIViewTestCases.APIViewTestCase):
3054
3023
  with self.subTest("Error handling: wrong relationship side"):
3055
3024
  response = self.client.patch(
3056
3025
  url,
3057
- {"relationships": {self.relationship.slug: {"source": {"objects": []}}}},
3026
+ {"relationships": {self.relationship.key: {"source": {"objects": []}}}},
3058
3027
  format="json",
3059
3028
  **self.header,
3060
3029
  )
@@ -3072,7 +3041,7 @@ class RelationshipAssociationTest(APIViewTestCases.APIViewTestCase):
3072
3041
  url,
3073
3042
  {
3074
3043
  "relationships": {
3075
- self.relationship.slug: {
3044
+ self.relationship.key: {
3076
3045
  "destination": {
3077
3046
  "objects": [
3078
3047
  # remove devices[0] by omission
@@ -3099,7 +3068,6 @@ class RelationshipAssociationTest(APIViewTestCases.APIViewTestCase):
3099
3068
 
3100
3069
  class SecretTest(APIViewTestCases.APIViewTestCase):
3101
3070
  model = Secret
3102
- brief_fields = ["display", "id", "name", "slug", "url"]
3103
3071
  bulk_update_data = {}
3104
3072
 
3105
3073
  create_data = [
@@ -3120,14 +3088,12 @@ class SecretTest(APIViewTestCases.APIViewTestCase):
3120
3088
  },
3121
3089
  {
3122
3090
  "name": "GitHub Token for My Repository",
3123
- "slug": "github-token-my-repository",
3124
3091
  "provider": "text-file",
3125
3092
  "parameters": {
3126
3093
  "path": "/github-tokens/user/myusername.txt",
3127
3094
  },
3128
3095
  },
3129
3096
  ]
3130
- slug_source = "name"
3131
3097
 
3132
3098
  @classmethod
3133
3099
  def setUpTestData(cls):
@@ -3152,17 +3118,55 @@ class SecretTest(APIViewTestCases.APIViewTestCase):
3152
3118
  for secret in secrets:
3153
3119
  secret.validated_save()
3154
3120
 
3121
+ def test_secret_check(self):
3122
+ """
3123
+ Ensure that we can check the validity of a secret.
3124
+ """
3125
+
3126
+ with self.subTest("Secret is not accessible"):
3127
+ test_secret = Secret.objects.create(
3128
+ name="secret-check-test-not-accessible",
3129
+ provider="text-file",
3130
+ parameters={"path": "/tmp/does-not-matter"},
3131
+ )
3132
+ response = self.client.get(reverse("extras-api:secret-check", kwargs={"pk": test_secret.pk}), **self.header)
3133
+ self.assertHttpStatus(response, status.HTTP_403_FORBIDDEN)
3134
+
3135
+ self.add_permissions("extras.view_secret")
3136
+
3137
+ with self.subTest("Secret check successful"):
3138
+ with tempfile.NamedTemporaryFile() as secret_file:
3139
+ secret_file.write(b"HELLO WORLD")
3140
+ test_secret = Secret.objects.create(
3141
+ name="secret-check-test-accessible",
3142
+ provider="text-file",
3143
+ parameters={"path": secret_file.name},
3144
+ )
3145
+ response = self.client.get(
3146
+ reverse("extras-api:secret-check", kwargs={"pk": test_secret.pk}), **self.header
3147
+ )
3148
+ self.assertHttpStatus(response, status.HTTP_200_OK)
3149
+ self.assertEqual(response.data["result"], True)
3150
+
3151
+ with self.subTest("Secret check failed"):
3152
+ test_secret = Secret.objects.create(
3153
+ name="secret-check-test-failed",
3154
+ provider="text-file",
3155
+ parameters={"path": "/tmp/does-not-exist"},
3156
+ )
3157
+ response = self.client.get(reverse("extras-api:secret-check", kwargs={"pk": test_secret.pk}), **self.header)
3158
+ self.assertHttpStatus(response, status.HTTP_200_OK)
3159
+ self.assertEqual(response.data["result"], False)
3160
+ self.assertIn("SecretValueNotFoundError", response.data["message"])
3161
+
3155
3162
 
3156
3163
  class SecretsGroupTest(APIViewTestCases.APIViewTestCase):
3157
3164
  model = SecretsGroup
3158
- brief_fields = ["display", "id", "name", "slug", "url"]
3159
3165
  bulk_update_data = {}
3160
3166
 
3161
- slug_source = "name"
3162
-
3163
3167
  @classmethod
3164
3168
  def setUpTestData(cls):
3165
- secrets = (
3169
+ secrets = secrets = (
3166
3170
  Secret.objects.create(
3167
3171
  name="secret-1", provider="environment-variable", parameters={"variable": "SOME_VAR"}
3168
3172
  ),
@@ -3172,9 +3176,9 @@ class SecretsGroupTest(APIViewTestCases.APIViewTestCase):
3172
3176
  )
3173
3177
 
3174
3178
  secrets_groups = (
3175
- SecretsGroup.objects.create(name="Group A", slug="group-a"),
3176
- SecretsGroup.objects.create(name="Group B", slug="group-b"),
3177
- SecretsGroup.objects.create(name="Group C", slug="group-c", description="Some group"),
3179
+ SecretsGroup.objects.create(name="Group A"),
3180
+ SecretsGroup.objects.create(name="Group B"),
3181
+ SecretsGroup.objects.create(name="Group C", description="Some group"),
3178
3182
  )
3179
3183
 
3180
3184
  SecretsGroupAssociation.objects.create(
@@ -3193,7 +3197,6 @@ class SecretsGroupTest(APIViewTestCases.APIViewTestCase):
3193
3197
  cls.create_data = [
3194
3198
  {
3195
3199
  "name": "Secrets Group 1",
3196
- "slug": "secrets-group-1",
3197
3200
  "description": "First Secrets Group",
3198
3201
  },
3199
3202
  {
@@ -3209,7 +3212,6 @@ class SecretsGroupTest(APIViewTestCases.APIViewTestCase):
3209
3212
 
3210
3213
  class SecretsGroupAssociationTest(APIViewTestCases.APIViewTestCase):
3211
3214
  model = SecretsGroupAssociation
3212
- brief_fields = ["access_type", "display", "id", "secret", "secret_type", "url"]
3213
3215
  bulk_update_data = {}
3214
3216
  choices_fields = ["access_type", "secret_type"]
3215
3217
 
@@ -3228,9 +3230,9 @@ class SecretsGroupAssociationTest(APIViewTestCases.APIViewTestCase):
3228
3230
  )
3229
3231
 
3230
3232
  secrets_groups = (
3231
- SecretsGroup.objects.create(name="Group A", slug="group-a"),
3232
- SecretsGroup.objects.create(name="Group B", slug="group-b"),
3233
- SecretsGroup.objects.create(name="Group C", slug="group-c", description="Some group"),
3233
+ SecretsGroup.objects.create(name="Group A"),
3234
+ SecretsGroup.objects.create(name="Group B"),
3235
+ SecretsGroup.objects.create(name="Group C", description="Some group"),
3234
3236
  )
3235
3237
 
3236
3238
  SecretsGroupAssociation.objects.create(
@@ -3276,7 +3278,6 @@ class SecretsGroupAssociationTest(APIViewTestCases.APIViewTestCase):
3276
3278
 
3277
3279
  class StatusTest(APIViewTestCases.APIViewTestCase):
3278
3280
  model = Status
3279
- brief_fields = ["display", "id", "name", "slug", "url"]
3280
3281
  bulk_update_data = {
3281
3282
  "color": "000000",
3282
3283
  }
@@ -3284,19 +3285,16 @@ class StatusTest(APIViewTestCases.APIViewTestCase):
3284
3285
  create_data = [
3285
3286
  {
3286
3287
  "name": "Pizza",
3287
- "slug": "pizza",
3288
3288
  "color": "0000ff",
3289
3289
  "content_types": ["dcim.device", "dcim.rack"],
3290
3290
  },
3291
3291
  {
3292
3292
  "name": "Oysters",
3293
- "slug": "oysters",
3294
3293
  "color": "00ff00",
3295
3294
  "content_types": ["ipam.ipaddress", "ipam.prefix"],
3296
3295
  },
3297
3296
  {
3298
3297
  "name": "Bad combinations",
3299
- "slug": "bad-combinations",
3300
3298
  "color": "ff0000",
3301
3299
  "content_types": ["dcim.device"],
3302
3300
  },
@@ -3306,12 +3304,10 @@ class StatusTest(APIViewTestCases.APIViewTestCase):
3306
3304
  "content_types": ["dcim.device"],
3307
3305
  },
3308
3306
  ]
3309
- slug_source = "name"
3310
3307
 
3311
3308
 
3312
3309
  class TagTest(APIViewTestCases.APIViewTestCase):
3313
3310
  model = Tag
3314
- brief_fields = ["color", "display", "id", "name", "slug", "url"]
3315
3311
  create_data = [
3316
3312
  {"name": "Tag 4", "slug": "tag-4", "content_types": [Location._meta.label_lower]},
3317
3313
  {"name": "Tag 5", "slug": "tag-5", "content_types": [Location._meta.label_lower]},
@@ -3395,7 +3391,6 @@ class TagTest(APIViewTestCases.APIViewTestCase):
3395
3391
 
3396
3392
  class WebhookTest(APIViewTestCases.APIViewTestCase):
3397
3393
  model = Webhook
3398
- brief_fields = ["display", "id", "name", "url"]
3399
3394
  create_data = [
3400
3395
  {
3401
3396
  "content_types": ["dcim.consoleport"],
@@ -3624,7 +3619,6 @@ class WebhookTest(APIViewTestCases.APIViewTestCase):
3624
3619
 
3625
3620
  class RoleTest(APIViewTestCases.APIViewTestCase):
3626
3621
  model = Role
3627
- brief_fields = ["display", "id", "name", "slug", "url"]
3628
3622
  bulk_update_data = {
3629
3623
  "color": "000000",
3630
3624
  }
@@ -3632,21 +3626,17 @@ class RoleTest(APIViewTestCases.APIViewTestCase):
3632
3626
  create_data = [
3633
3627
  {
3634
3628
  "name": "Role 1",
3635
- "slug": "role-1",
3636
3629
  "color": "0000ff",
3637
3630
  "content_types": ["dcim.device", "dcim.rack"],
3638
3631
  },
3639
3632
  {
3640
3633
  "name": "Role 2",
3641
- "slug": "role-2",
3642
3634
  "color": "0000ff",
3643
3635
  "content_types": ["dcim.rack"],
3644
3636
  },
3645
3637
  {
3646
3638
  "name": "Role 3",
3647
- "slug": "role-3",
3648
3639
  "color": "0000ff",
3649
3640
  "content_types": ["ipam.ipaddress", "ipam.vlan"],
3650
3641
  },
3651
3642
  ]
3652
- slug_source = "name"