nautobot 2.4.17__py3-none-any.whl → 2.4.19__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.

Potentially problematic release.


This version of nautobot might be problematic. Click here for more details.

Files changed (627) hide show
  1. nautobot/apps/ui.py +6 -0
  2. nautobot/apps/views.py +2 -0
  3. nautobot/circuits/tables.py +1 -1
  4. nautobot/circuits/templates/circuits/circuit_create.html +7 -7
  5. nautobot/circuits/templates/circuits/circuit_retrieve.html +1 -5
  6. nautobot/circuits/templates/circuits/circuittermination_create.html +26 -26
  7. nautobot/circuits/templates/circuits/circuittermination_retrieve.html +1 -8
  8. nautobot/circuits/templates/circuits/inc/circuit_termination.html +20 -20
  9. nautobot/circuits/templates/circuits/inc/circuit_termination_header_extra_content.html +3 -3
  10. nautobot/circuits/templates/circuits/inc/circuit_termination_speed_fragment.html +9 -0
  11. nautobot/circuits/templates/circuits/providernetwork_retrieve.html +1 -3
  12. nautobot/circuits/tests/integration/test_circuit.py +2 -2
  13. nautobot/circuits/views.py +49 -15
  14. nautobot/cloud/templates/cloud/cloudaccount_retrieve.html +1 -4
  15. nautobot/cloud/templates/cloud/cloudnetwork_retrieve.html +1 -7
  16. nautobot/cloud/templates/cloud/cloudresourcetype_retrieve.html +1 -3
  17. nautobot/cloud/templates/cloud/cloudservice_retrieve.html +1 -5
  18. nautobot/cloud/views.py +45 -0
  19. nautobot/core/filters.py +2 -2
  20. nautobot/core/graphql/generators.py +5 -2
  21. nautobot/core/jobs/bulk_actions.py +48 -85
  22. nautobot/core/models/querysets.py +2 -1
  23. nautobot/core/settings.py +1 -0
  24. nautobot/core/settings.yaml +9 -0
  25. nautobot/core/tables.py +21 -23
  26. nautobot/core/templates/40x.html +15 -15
  27. nautobot/core/templates/500.html +21 -21
  28. nautobot/core/templates/admin/app_index.html +8 -8
  29. nautobot/core/templates/admin/base.html +104 -104
  30. nautobot/core/templates/admin/change_form.html +65 -65
  31. nautobot/core/templates/admin/change_list.html +60 -60
  32. nautobot/core/templates/admin/change_list_results.html +39 -39
  33. nautobot/core/templates/admin/config/config.html +47 -47
  34. nautobot/core/templates/admin/delete_confirmation.html +47 -47
  35. nautobot/core/templates/admin/edit_inline/stacked.html +124 -124
  36. nautobot/core/templates/admin/edit_inline/tabular.html +60 -60
  37. nautobot/core/templates/admin/includes/fieldset.html +4 -4
  38. nautobot/core/templates/admin/index.html +60 -60
  39. nautobot/core/templates/admin/prepopulated_fields_js.html +18 -18
  40. nautobot/core/templates/admin/submit_line.html +4 -4
  41. nautobot/core/templates/base_django.html +46 -46
  42. nautobot/core/templates/buttons/consolidated_bulk_action_buttons.html +8 -8
  43. nautobot/core/templates/buttons/consolidated_detail_view_action_buttons.html +8 -8
  44. nautobot/core/templates/buttons/export.html +3 -3
  45. nautobot/core/templates/components/breadcrumbs.html +19 -0
  46. nautobot/core/templates/components/button/default.html +3 -3
  47. nautobot/core/templates/components/button/dropdown.html +7 -7
  48. nautobot/core/templates/components/button/formbutton.html +4 -4
  49. nautobot/core/templates/components/panel/body_content_data_table.html +1 -1
  50. nautobot/core/templates/components/panel/body_wrapper_generic_table.html +3 -0
  51. nautobot/core/templates/components/panel/footer_content_table.html +3 -1
  52. nautobot/core/templates/components/panel/header_extra_content_table.html +10 -1
  53. nautobot/core/templates/components/tab/content_wrapper.html +1 -1
  54. nautobot/core/templates/components/tab/label_wrapper.html +1 -1
  55. nautobot/core/templates/components/tab/label_wrapper_distinct_view.html +10 -3
  56. nautobot/core/templates/generic/object_bulk_add_component.html +40 -40
  57. nautobot/core/templates/generic/object_bulk_create.html +3 -3
  58. nautobot/core/templates/generic/object_bulk_destroy.html +6 -6
  59. nautobot/core/templates/generic/object_bulk_update.html +52 -52
  60. nautobot/core/templates/generic/object_changelog.html +0 -2
  61. nautobot/core/templates/generic/object_import.html +33 -33
  62. nautobot/core/templates/generic/object_list.html +271 -268
  63. nautobot/core/templates/generic/object_notes.html +0 -2
  64. nautobot/core/templates/generic/object_retrieve.html +264 -257
  65. nautobot/core/templates/graphene/graphiql.html +127 -127
  66. nautobot/core/templates/home.html +62 -62
  67. nautobot/core/templates/inc/computed_fields/panel_data.html +13 -13
  68. nautobot/core/templates/inc/created_updated.html +8 -8
  69. nautobot/core/templates/inc/custom_fields/panel_data.html +13 -13
  70. nautobot/core/templates/inc/dynamic_groups_panel.html +11 -11
  71. nautobot/core/templates/inc/footer.html +19 -19
  72. nautobot/core/templates/inc/javascript.html +1 -1
  73. nautobot/core/templates/inc/media.html +46 -46
  74. nautobot/core/templates/inc/nav_menu.html +1 -1
  75. nautobot/core/templates/inc/relationships_table_rows.html +22 -22
  76. nautobot/core/templates/inc/tenant_table_row.html +1 -1
  77. nautobot/core/templates/login.html +77 -77
  78. nautobot/core/templates/media_failure.html +38 -38
  79. nautobot/core/templates/panel_table.html +1 -1
  80. nautobot/core/templates/rest_framework/api.html +3 -3
  81. nautobot/core/templates/search.html +1 -1
  82. nautobot/core/templates/swagger_ui.html +9 -9
  83. nautobot/core/templates/utilities/confirmation_form.html +18 -18
  84. nautobot/core/templates/utilities/render_field.html +1 -1
  85. nautobot/core/templates/utilities/render_jinja2.html +43 -43
  86. nautobot/core/templates/utilities/templatetags/filter_form_modal.html +56 -56
  87. nautobot/core/templates/utilities/templatetags/utilization_graph.html +1 -1
  88. nautobot/core/templates/utilities/theme_preview.html +799 -799
  89. nautobot/core/templates/utilities/worker_status.html +122 -122
  90. nautobot/core/templates/widgets/clearable_file.html +3 -3
  91. nautobot/core/templates/widgets/sluginput.html +1 -1
  92. nautobot/core/templatetags/buttons.py +8 -2
  93. nautobot/core/templatetags/helpers.py +24 -0
  94. nautobot/core/templatetags/ui_framework.py +40 -5
  95. nautobot/core/testing/filters.py +37 -21
  96. nautobot/core/testing/integration.py +7 -4
  97. nautobot/core/testing/views.py +49 -5
  98. nautobot/core/tests/test_breadcrumbs.py +78 -4
  99. nautobot/core/tests/test_commands.py +7 -4
  100. nautobot/core/tests/test_graphql.py +20 -5
  101. nautobot/core/tests/test_jobs.py +34 -21
  102. nautobot/core/tests/test_tables.py +43 -6
  103. nautobot/core/tests/test_templatetags_ui_framework.py +146 -0
  104. nautobot/core/tests/test_titles.py +2 -2
  105. nautobot/core/tests/test_ui.py +188 -1
  106. nautobot/core/tests/test_utils.py +35 -0
  107. nautobot/core/tests/test_views.py +45 -0
  108. nautobot/core/tests/test_views_generic.py +43 -0
  109. nautobot/core/tests/test_views_utils.py +239 -5
  110. nautobot/core/ui/breadcrumbs.py +220 -28
  111. nautobot/core/ui/bulk_buttons.py +8 -0
  112. nautobot/core/ui/object_detail.py +181 -60
  113. nautobot/core/ui/titles.py +10 -5
  114. nautobot/core/utils/requests.py +27 -2
  115. nautobot/core/views/__init__.py +24 -3
  116. nautobot/core/views/generic.py +70 -35
  117. nautobot/core/views/mixins.py +226 -122
  118. nautobot/core/views/utils.py +270 -1
  119. nautobot/dcim/api/serializers.py +8 -2
  120. nautobot/dcim/constants.py +1 -0
  121. nautobot/dcim/factory.py +4 -3
  122. nautobot/dcim/filters/mixins.py +1 -2
  123. nautobot/dcim/forms.py +5 -1
  124. nautobot/dcim/migrations/0074_alter_rack_u_height.py +21 -0
  125. nautobot/dcim/models/devices.py +30 -1
  126. nautobot/dcim/models/racks.py +2 -2
  127. nautobot/dcim/tables/__init__.py +2 -0
  128. nautobot/dcim/tables/devices.py +24 -0
  129. nautobot/dcim/tables/power.py +2 -2
  130. nautobot/dcim/templates/dcim/cable.html +53 -53
  131. nautobot/dcim/templates/dcim/cable_connect.html +182 -182
  132. nautobot/dcim/templates/dcim/cable_trace.html +1 -1
  133. nautobot/dcim/templates/dcim/console_port_connection_list.html +5 -5
  134. nautobot/dcim/templates/dcim/consoleport.html +86 -86
  135. nautobot/dcim/templates/dcim/consoleserverport.html +86 -86
  136. nautobot/dcim/templates/dcim/controller_create.html +34 -34
  137. nautobot/dcim/templates/dcim/controllermanageddevicegroup_create.html +68 -68
  138. nautobot/dcim/templates/dcim/device/base.html +1 -114
  139. nautobot/dcim/templates/dcim/device/config.html +17 -17
  140. nautobot/dcim/templates/dcim/device/consoleports.html +1 -52
  141. nautobot/dcim/templates/dcim/device/consoleserverports.html +1 -52
  142. nautobot/dcim/templates/dcim/device/devicebays.html +1 -48
  143. nautobot/dcim/templates/dcim/device/frontports.html +1 -52
  144. nautobot/dcim/templates/dcim/device/interfaces.html +1 -56
  145. nautobot/dcim/templates/dcim/device/inventory.html +1 -48
  146. nautobot/dcim/templates/dcim/device/lldp_neighbors.html +64 -64
  147. nautobot/dcim/templates/dcim/device/modulebays.html +1 -48
  148. nautobot/dcim/templates/dcim/device/poweroutlets.html +1 -52
  149. nautobot/dcim/templates/dcim/device/powerports.html +1 -52
  150. nautobot/dcim/templates/dcim/device/rearports.html +1 -52
  151. nautobot/dcim/templates/dcim/device/status.html +66 -66
  152. nautobot/dcim/templates/dcim/device/wireless.html +1 -72
  153. nautobot/dcim/templates/dcim/device.html +4 -422
  154. nautobot/dcim/templates/dcim/device_component.html +0 -19
  155. nautobot/dcim/templates/dcim/device_component_add.html +25 -25
  156. nautobot/dcim/templates/dcim/device_create.html +229 -0
  157. nautobot/dcim/templates/dcim/device_edit.html +2 -227
  158. nautobot/dcim/templates/dcim/devicebay.html +41 -41
  159. nautobot/dcim/templates/dcim/devicebay_populate.html +32 -32
  160. nautobot/dcim/templates/dcim/devicetype_component_add.html +28 -28
  161. nautobot/dcim/templates/dcim/devicetype_retrieve.html +1 -3
  162. nautobot/dcim/templates/dcim/frontport.html +84 -84
  163. nautobot/dcim/templates/dcim/inc/cable_toggle_buttons.html +1 -1
  164. nautobot/dcim/templates/dcim/inc/device_interface_filter.html +8 -0
  165. nautobot/dcim/templates/dcim/inc/device_napalm_tabs.html +1 -15
  166. nautobot/dcim/templates/dcim/inc/location_hierarchy.html +22 -22
  167. nautobot/dcim/templates/dcim/interface.html +206 -206
  168. nautobot/dcim/templates/dcim/interface_connection_list.html +5 -5
  169. nautobot/dcim/templates/dcim/interfaceredundancygroupassociation_create.html +6 -6
  170. nautobot/dcim/templates/dcim/inventoryitem.html +44 -44
  171. nautobot/dcim/templates/dcim/inventoryitem_add.html +32 -32
  172. nautobot/dcim/templates/dcim/inventoryitem_edit.html +22 -22
  173. nautobot/dcim/templates/dcim/location_migrate_data_to_contact.html +46 -46
  174. nautobot/dcim/templates/dcim/location_retrieve.html +1 -7
  175. nautobot/dcim/templates/dcim/locationtype.html +1 -6
  176. nautobot/dcim/templates/dcim/locationtype_retrieve.html +1 -7
  177. nautobot/dcim/templates/dcim/module/base.html +85 -85
  178. nautobot/dcim/templates/dcim/module_interfaces.html +1 -1
  179. nautobot/dcim/templates/dcim/module_modulebays.html +1 -1
  180. nautobot/dcim/templates/dcim/module_retrieve.html +52 -52
  181. nautobot/dcim/templates/dcim/module_update.html +61 -61
  182. nautobot/dcim/templates/dcim/modulebay_destroy.html +1 -1
  183. nautobot/dcim/templates/dcim/modulebay_retrieve.html +83 -99
  184. nautobot/dcim/templates/dcim/modulebay_update.html +33 -33
  185. nautobot/dcim/templates/dcim/modulefamily_retrieve.html +1 -1
  186. nautobot/dcim/templates/dcim/moduletype_retrieve.html +140 -144
  187. nautobot/dcim/templates/dcim/platform_create.html +38 -38
  188. nautobot/dcim/templates/dcim/power_port_connection_list.html +5 -5
  189. nautobot/dcim/templates/dcim/powerfeed_retrieve.html +1 -8
  190. nautobot/dcim/templates/dcim/poweroutlet.html +85 -85
  191. nautobot/dcim/templates/dcim/powerpanel_retrieve.html +1 -8
  192. nautobot/dcim/templates/dcim/powerport.html +91 -91
  193. nautobot/dcim/templates/dcim/rack_elevation_list.html +18 -18
  194. nautobot/dcim/templates/dcim/rack_retrieve.html +264 -274
  195. nautobot/dcim/templates/dcim/rackreservation_retrieve.html +0 -3
  196. nautobot/dcim/templates/dcim/rearport.html +78 -78
  197. nautobot/dcim/templates/dcim/virtualchassis_retrieve.html +1 -50
  198. nautobot/dcim/templates/dcim/virtualdevicecontext_retrieve.html +1 -5
  199. nautobot/dcim/tests/integration/test_device_bulk_operations.py +3 -2
  200. nautobot/dcim/tests/integration/test_location_bulk_operations.py +6 -2
  201. nautobot/dcim/tests/test_api.py +33 -1
  202. nautobot/dcim/tests/test_views.py +189 -4
  203. nautobot/dcim/ui.py +29 -0
  204. nautobot/dcim/urls.py +1 -109
  205. nautobot/dcim/utils.py +30 -0
  206. nautobot/dcim/views.py +1149 -550
  207. nautobot/extras/filters/mixins.py +1 -1
  208. nautobot/extras/forms/forms.py +15 -0
  209. nautobot/extras/models/groups.py +10 -1
  210. nautobot/extras/models/jobs.py +2 -2
  211. nautobot/extras/plugins/views.py +18 -5
  212. nautobot/extras/tables.py +24 -2
  213. nautobot/extras/templates/extras/computedfield_edit.html +4 -4
  214. nautobot/extras/templates/extras/configcontext_update.html +1 -1
  215. nautobot/extras/templates/extras/configcontextschema_retrieve.html +32 -32
  216. nautobot/extras/templates/extras/customfield_retrieve.html +1 -128
  217. nautobot/extras/templates/extras/customfield_update.html +23 -23
  218. nautobot/extras/templates/extras/dynamicgroup.html +2 -99
  219. nautobot/extras/templates/extras/dynamicgroup_edit.html +2 -199
  220. nautobot/extras/templates/extras/dynamicgroup_retrieve.html +99 -0
  221. nautobot/extras/templates/extras/dynamicgroup_update.html +199 -0
  222. nautobot/extras/templates/extras/gitrepository.html +2 -82
  223. nautobot/extras/templates/extras/gitrepository_list.html +10 -10
  224. nautobot/extras/templates/extras/gitrepository_object_edit.html +2 -13
  225. nautobot/extras/templates/extras/gitrepository_retrieve.html +82 -0
  226. nautobot/extras/templates/extras/gitrepository_update.html +13 -0
  227. nautobot/extras/templates/extras/graphqlquery_retrieve.html +73 -73
  228. nautobot/extras/templates/extras/inc/configcontext_format.html +2 -2
  229. nautobot/extras/templates/extras/inc/job_table.html +10 -10
  230. nautobot/extras/templates/extras/inc/jobresult.html +21 -21
  231. nautobot/extras/templates/extras/inc/jobresult_js.html +6 -6
  232. nautobot/extras/templates/extras/inc/tags_panel.html +10 -10
  233. nautobot/extras/templates/extras/job.html +64 -64
  234. nautobot/extras/templates/extras/job_approval_request.html +9 -9
  235. nautobot/extras/templates/extras/job_bulk_edit.html +13 -13
  236. nautobot/extras/templates/extras/job_edit.html +45 -45
  237. nautobot/extras/templates/extras/job_list.html +4 -4
  238. nautobot/extras/templates/extras/jobresult_retrieve.html +0 -25
  239. nautobot/extras/templates/extras/marketplace.html +101 -101
  240. nautobot/extras/templates/extras/metadatatype_create.html +20 -20
  241. nautobot/extras/templates/extras/note_retrieve.html +0 -52
  242. nautobot/extras/templates/extras/object_assign_contact_or_team.html +18 -18
  243. nautobot/extras/templates/extras/object_configcontext.html +1 -3
  244. nautobot/extras/templates/extras/objectchange.html +2 -165
  245. nautobot/extras/templates/extras/objectchange_retrieve.html +165 -0
  246. nautobot/extras/templates/extras/plugin_detail.html +44 -48
  247. nautobot/extras/templates/extras/plugins_list.html +9 -11
  248. nautobot/extras/templates/extras/plugins_tiles.html +26 -26
  249. nautobot/extras/templates/extras/relationship_edit.html +4 -4
  250. nautobot/extras/templates/extras/role_retrieve.html +13 -13
  251. nautobot/extras/templates/extras/scheduled_jobs_approval_queue_list.html +21 -21
  252. nautobot/extras/templates/extras/scheduledjob.html +128 -128
  253. nautobot/extras/templates/extras/secret_create.html +53 -53
  254. nautobot/extras/templates/extras/secretsgroup_update.html +13 -13
  255. nautobot/extras/templates/extras/templatetags/plugin_object_detail_tabs.html +3 -3
  256. nautobot/extras/templates/extras/webhook.html +79 -79
  257. nautobot/extras/tests/integration/test_relationships.py +6 -6
  258. nautobot/extras/tests/test_dynamicgroups.py +73 -18
  259. nautobot/extras/tests/test_filters.py +1 -1
  260. nautobot/extras/tests/test_jobs.py +2 -0
  261. nautobot/extras/tests/test_views.py +8 -3
  262. nautobot/extras/urls.py +3 -97
  263. nautobot/extras/views.py +524 -456
  264. nautobot/ipam/filters.py +2 -2
  265. nautobot/ipam/migrations/0053_alter_vrfdeviceassignment_options_and_more.py +20 -0
  266. nautobot/ipam/models.py +34 -0
  267. nautobot/ipam/querysets.py +3 -3
  268. nautobot/ipam/signals.py +6 -1
  269. nautobot/ipam/tables.py +3 -1
  270. nautobot/ipam/templates/ipam/inc/prefix_header_extra_content_table.html +4 -0
  271. nautobot/ipam/templates/ipam/inc/toggle_available.html +8 -8
  272. nautobot/ipam/templates/ipam/inc/vlangroup_header.html +4 -4
  273. nautobot/ipam/templates/ipam/ipaddress.html +119 -123
  274. nautobot/ipam/templates/ipam/ipaddress_assign.html +10 -10
  275. nautobot/ipam/templates/ipam/ipaddress_edit.html +1 -1
  276. nautobot/ipam/templates/ipam/ipaddress_merge.html +180 -180
  277. nautobot/ipam/templates/ipam/ipaddresstointerface_retrieve.html +48 -48
  278. nautobot/ipam/templates/ipam/prefix.html +2 -115
  279. nautobot/ipam/templates/ipam/prefix_create.html +34 -0
  280. nautobot/ipam/templates/ipam/prefix_edit.html +1 -34
  281. nautobot/ipam/templates/ipam/prefix_retrieve.html +3 -0
  282. nautobot/ipam/templates/ipam/service_retrieve.html +1 -6
  283. nautobot/ipam/templates/ipam/vlan_retrieve.html +1 -7
  284. nautobot/ipam/templates/ipam/vrf_edit.html +1 -1
  285. nautobot/ipam/tests/test_api.py +5 -0
  286. nautobot/ipam/tests/test_models.py +387 -0
  287. nautobot/ipam/tests/test_querysets.py +46 -0
  288. nautobot/ipam/tests/test_views.py +34 -0
  289. nautobot/ipam/ui.py +145 -0
  290. nautobot/ipam/urls.py +1 -46
  291. nautobot/ipam/utils/__init__.py +26 -0
  292. nautobot/ipam/utils/migrations.py +1 -1
  293. nautobot/ipam/views.py +234 -112
  294. nautobot/project-static/docs/404.html +11 -11
  295. nautobot/project-static/docs/apps/index.html +11 -11
  296. nautobot/project-static/docs/apps/nautobot-apps.html +11 -11
  297. nautobot/project-static/docs/assets/javascripts/{bundle.92b07e13.min.js → bundle.f55a23d4.min.js} +2 -2
  298. nautobot/project-static/docs/assets/javascripts/{bundle.92b07e13.min.js.map → bundle.f55a23d4.min.js.map} +2 -2
  299. nautobot/project-static/docs/assets/stylesheets/{main.7e37652d.min.css → main.e53b48f4.min.css} +1 -1
  300. nautobot/project-static/docs/assets/stylesheets/{main.7e37652d.min.css.map → main.e53b48f4.min.css.map} +1 -1
  301. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +11 -11
  302. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +11 -11
  303. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +11 -11
  304. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +11 -11
  305. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +11 -11
  306. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +11 -11
  307. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +11 -11
  308. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +11 -11
  309. nautobot/project-static/docs/code-reference/nautobot/apps/events.html +11 -11
  310. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +11 -11
  311. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +11 -11
  312. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +11 -11
  313. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +11 -11
  314. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +11 -11
  315. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +11 -11
  316. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +11 -11
  317. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +11 -11
  318. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +11 -11
  319. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +11 -11
  320. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +83 -11
  321. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +1265 -281
  322. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +11 -11
  323. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +12 -12
  324. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +452 -29
  325. nautobot/project-static/docs/development/apps/api/configuration-view.html +11 -11
  326. nautobot/project-static/docs/development/apps/api/database-backend-config.html +11 -11
  327. nautobot/project-static/docs/development/apps/api/models/django-admin.html +11 -11
  328. nautobot/project-static/docs/development/apps/api/models/global-search.html +11 -11
  329. nautobot/project-static/docs/development/apps/api/models/graphql.html +11 -11
  330. nautobot/project-static/docs/development/apps/api/models/index.html +11 -11
  331. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +12 -12
  332. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +11 -11
  333. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +11 -11
  334. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +11 -11
  335. nautobot/project-static/docs/development/apps/api/platform-features/index.html +11 -11
  336. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +11 -11
  337. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +11 -11
  338. nautobot/project-static/docs/development/apps/api/platform-features/prepopulating-data.html +11 -11
  339. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +11 -11
  340. nautobot/project-static/docs/development/apps/api/platform-features/table-extensions.html +11 -11
  341. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +11 -11
  342. nautobot/project-static/docs/development/apps/api/prometheus.html +11 -11
  343. nautobot/project-static/docs/development/apps/api/setup.html +11 -11
  344. nautobot/project-static/docs/development/apps/api/testing.html +11 -11
  345. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +11 -11
  346. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +11 -11
  347. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +11 -11
  348. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +11 -11
  349. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +11 -11
  350. nautobot/project-static/docs/development/apps/api/views/base-template.html +11 -11
  351. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +11 -11
  352. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +11 -11
  353. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +11 -11
  354. nautobot/project-static/docs/development/apps/api/views/index.html +11 -11
  355. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +11 -11
  356. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +11 -11
  357. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +11 -11
  358. nautobot/project-static/docs/development/apps/api/views/notes.html +11 -11
  359. nautobot/project-static/docs/development/apps/api/views/rest-api.html +11 -11
  360. nautobot/project-static/docs/development/apps/api/views/urls.html +11 -11
  361. nautobot/project-static/docs/development/apps/index.html +11 -11
  362. nautobot/project-static/docs/development/apps/migration/code-updates.html +11 -11
  363. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +11 -11
  364. nautobot/project-static/docs/development/apps/migration/from-v1.html +11 -11
  365. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +11 -11
  366. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +11 -11
  367. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +11 -11
  368. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +11 -11
  369. nautobot/project-static/docs/development/apps/migration/ui-component-framework/best-practices.html +11 -11
  370. nautobot/project-static/docs/development/apps/migration/ui-component-framework/breadcrumbs-titles.html +11 -11
  371. nautobot/project-static/docs/development/apps/migration/ui-component-framework/custom-content.html +11 -11
  372. nautobot/project-static/docs/development/apps/migration/ui-component-framework/index.html +11 -11
  373. nautobot/project-static/docs/development/apps/migration/ui-component-framework/migration-steps.html +11 -11
  374. nautobot/project-static/docs/development/apps/porting-from-netbox.html +11 -11
  375. nautobot/project-static/docs/development/core/application-registry.html +11 -11
  376. nautobot/project-static/docs/development/core/best-practices.html +11 -11
  377. nautobot/project-static/docs/development/core/bootstrap-ui.html +11 -11
  378. nautobot/project-static/docs/development/core/caching.html +11 -11
  379. nautobot/project-static/docs/development/core/controllers.html +11 -11
  380. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +11 -11
  381. nautobot/project-static/docs/development/core/generic-views.html +11 -11
  382. nautobot/project-static/docs/development/core/getting-started.html +50 -63
  383. nautobot/project-static/docs/development/core/homepage.html +11 -11
  384. nautobot/project-static/docs/development/core/index.html +11 -11
  385. nautobot/project-static/docs/development/core/minikube-dev-environment-for-k8s-jobs.html +11 -11
  386. nautobot/project-static/docs/development/core/model-checklist.html +11 -11
  387. nautobot/project-static/docs/development/core/model-features.html +11 -11
  388. nautobot/project-static/docs/development/core/natural-keys.html +11 -11
  389. nautobot/project-static/docs/development/core/navigation-menu.html +11 -11
  390. nautobot/project-static/docs/development/core/release-checklist.html +11 -11
  391. nautobot/project-static/docs/development/core/role-internals.html +11 -11
  392. nautobot/project-static/docs/development/core/settings.html +11 -11
  393. nautobot/project-static/docs/development/core/style-guide.html +15 -11
  394. nautobot/project-static/docs/development/core/templates.html +11 -11
  395. nautobot/project-static/docs/development/core/testing.html +11 -11
  396. nautobot/project-static/docs/development/core/ui-component-framework.html +17 -22
  397. nautobot/project-static/docs/development/core/user-preferences.html +11 -11
  398. nautobot/project-static/docs/development/index.html +11 -11
  399. nautobot/project-static/docs/development/jobs/getting-started.html +11 -11
  400. nautobot/project-static/docs/development/jobs/index.html +11 -11
  401. nautobot/project-static/docs/development/jobs/installation.html +11 -11
  402. nautobot/project-static/docs/development/jobs/job-extensions.html +11 -11
  403. nautobot/project-static/docs/development/jobs/job-logging.html +11 -11
  404. nautobot/project-static/docs/development/jobs/job-patterns.html +11 -11
  405. nautobot/project-static/docs/development/jobs/job-structure.html +11 -11
  406. nautobot/project-static/docs/development/jobs/migration/from-v1.html +11 -11
  407. nautobot/project-static/docs/development/jobs/testing.html +11 -11
  408. nautobot/project-static/docs/index.html +11 -11
  409. nautobot/project-static/docs/objects.inv +0 -0
  410. nautobot/project-static/docs/overview/application_stack.html +11 -11
  411. nautobot/project-static/docs/overview/design_philosophy.html +11 -11
  412. nautobot/project-static/docs/release-notes/index.html +11 -11
  413. nautobot/project-static/docs/release-notes/version-1.0.html +11 -11
  414. nautobot/project-static/docs/release-notes/version-1.1.html +11 -11
  415. nautobot/project-static/docs/release-notes/version-1.2.html +11 -11
  416. nautobot/project-static/docs/release-notes/version-1.3.html +11 -11
  417. nautobot/project-static/docs/release-notes/version-1.4.html +11 -11
  418. nautobot/project-static/docs/release-notes/version-1.5.html +11 -11
  419. nautobot/project-static/docs/release-notes/version-1.6.html +11 -11
  420. nautobot/project-static/docs/release-notes/version-2.0.html +11 -11
  421. nautobot/project-static/docs/release-notes/version-2.1.html +11 -11
  422. nautobot/project-static/docs/release-notes/version-2.2.html +11 -11
  423. nautobot/project-static/docs/release-notes/version-2.3.html +11 -11
  424. nautobot/project-static/docs/release-notes/version-2.4.html +418 -11
  425. nautobot/project-static/docs/search/search_index.json +1 -1
  426. nautobot/project-static/docs/sitemap.xml +300 -300
  427. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  428. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +11 -11
  429. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +11 -11
  430. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +11 -11
  431. nautobot/project-static/docs/user-guide/administration/configuration/index.html +11 -11
  432. nautobot/project-static/docs/user-guide/administration/configuration/redis.html +11 -11
  433. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +38 -11
  434. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +11 -11
  435. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +89 -14
  436. nautobot/project-static/docs/user-guide/administration/guides/docker.html +11 -11
  437. nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +11 -11
  438. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +11 -11
  439. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +11 -11
  440. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +11 -11
  441. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +11 -11
  442. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +11 -11
  443. nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +11 -11
  444. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +11 -11
  445. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +11 -11
  446. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +11 -11
  447. nautobot/project-static/docs/user-guide/administration/installation/index.html +11 -11
  448. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +11 -11
  449. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +11 -11
  450. nautobot/project-static/docs/user-guide/administration/installation/services.html +11 -11
  451. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +11 -11
  452. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +11 -11
  453. nautobot/project-static/docs/user-guide/administration/security/index.html +11 -11
  454. nautobot/project-static/docs/user-guide/administration/security/notices.html +11 -11
  455. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +11 -11
  456. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +11 -11
  457. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +11 -11
  458. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +11 -11
  459. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +11 -11
  460. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +11 -11
  461. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +11 -11
  462. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +11 -11
  463. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +11 -11
  464. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +11 -11
  465. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +11 -11
  466. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +11 -11
  467. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +11 -11
  468. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +11 -11
  469. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +11 -11
  470. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +11 -11
  471. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +11 -11
  472. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +11 -11
  473. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +11 -11
  474. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +11 -11
  475. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +11 -11
  476. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +11 -11
  477. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +11 -11
  478. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +11 -11
  479. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +11 -11
  480. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +11 -11
  481. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +11 -11
  482. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +11 -11
  483. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +11 -11
  484. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +11 -11
  485. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +11 -11
  486. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +11 -11
  487. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +11 -11
  488. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +11 -11
  489. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +11 -11
  490. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +11 -11
  491. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +11 -11
  492. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +11 -11
  493. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +11 -11
  494. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +11 -11
  495. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +11 -11
  496. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +11 -11
  497. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +11 -11
  498. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +11 -11
  499. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +11 -11
  500. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +11 -11
  501. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +11 -11
  502. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +11 -11
  503. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulefamily.html +11 -11
  504. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +11 -11
  505. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +11 -11
  506. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +11 -11
  507. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +11 -11
  508. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +11 -11
  509. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +11 -11
  510. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +11 -11
  511. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +11 -11
  512. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +11 -11
  513. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +11 -11
  514. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +11 -11
  515. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +11 -11
  516. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +11 -11
  517. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +11 -11
  518. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +11 -11
  519. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +11 -11
  520. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualdevicecontext.html +11 -11
  521. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +11 -11
  522. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +11 -11
  523. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +11 -11
  524. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +11 -11
  525. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +11 -11
  526. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +11 -11
  527. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +11 -11
  528. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +11 -11
  529. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +11 -11
  530. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +11 -11
  531. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +11 -11
  532. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +11 -11
  533. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +11 -11
  534. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +11 -11
  535. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +11 -11
  536. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +11 -11
  537. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +11 -11
  538. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +11 -11
  539. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +11 -11
  540. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +11 -11
  541. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +11 -11
  542. nautobot/project-static/docs/user-guide/core-data-model/wireless/index.html +11 -11
  543. nautobot/project-static/docs/user-guide/core-data-model/wireless/radioprofile.html +11 -11
  544. nautobot/project-static/docs/user-guide/core-data-model/wireless/supporteddatarate.html +11 -11
  545. nautobot/project-static/docs/user-guide/core-data-model/wireless/wirelessnetwork.html +11 -11
  546. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +11 -11
  547. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +11 -11
  548. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +11 -11
  549. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +11 -11
  550. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +11 -11
  551. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +11 -11
  552. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +11 -11
  553. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +11 -11
  554. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +11 -11
  555. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +11 -11
  556. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +11 -11
  557. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +11 -11
  558. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +11 -11
  559. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +11 -11
  560. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +11 -11
  561. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +11 -11
  562. nautobot/project-static/docs/user-guide/feature-guides/wireless-networks-and-controllers.html +11 -11
  563. nautobot/project-static/docs/user-guide/index.html +11 -11
  564. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +11 -11
  565. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +11 -11
  566. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +11 -11
  567. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +11 -11
  568. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +11 -11
  569. nautobot/project-static/docs/user-guide/platform-functionality/events.html +11 -11
  570. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +11 -11
  571. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +11 -11
  572. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +11 -11
  573. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +11 -11
  574. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +11 -11
  575. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +11 -11
  576. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +11 -11
  577. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +11 -11
  578. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +11 -11
  579. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +11 -11
  580. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobqueue.html +11 -11
  581. nautobot/project-static/docs/user-guide/platform-functionality/jobs/kubernetes-job-support.html +11 -11
  582. nautobot/project-static/docs/user-guide/platform-functionality/jobs/managing-jobs.html +11 -11
  583. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +11 -11
  584. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +11 -11
  585. nautobot/project-static/docs/user-guide/platform-functionality/note.html +11 -11
  586. nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +11 -11
  587. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +11 -11
  588. nautobot/project-static/docs/user-guide/platform-functionality/rendering-jinja-templates.html +11 -11
  589. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +11 -11
  590. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +11 -11
  591. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +11 -11
  592. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +11 -11
  593. nautobot/project-static/docs/user-guide/platform-functionality/role.html +11 -11
  594. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +11 -11
  595. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +11 -11
  596. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +11 -11
  597. nautobot/project-static/docs/user-guide/platform-functionality/status.html +11 -11
  598. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +11 -11
  599. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +11 -11
  600. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +11 -11
  601. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +11 -11
  602. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +11 -11
  603. nautobot/project-static/img/nautobot_icon.svg +32 -34
  604. nautobot/project-static/js/table_sorting_indicator.js +0 -2
  605. nautobot/tenancy/templates/tenancy/tenant.html +1 -7
  606. nautobot/tenancy/views.py +13 -0
  607. nautobot/users/templates/users/api_tokens.html +4 -4
  608. nautobot/users/templates/users/base.html +28 -28
  609. nautobot/virtualization/templates/virtualization/cluster.html +64 -64
  610. nautobot/virtualization/templates/virtualization/inc/virtualmachine_vminterface_filter.html +8 -0
  611. nautobot/virtualization/templates/virtualization/virtualmachine_component_add.html +25 -25
  612. nautobot/virtualization/templates/virtualization/virtualmachine_retrieve.html +1 -251
  613. nautobot/virtualization/templates/virtualization/vminterface.html +70 -70
  614. nautobot/virtualization/urls.py +0 -12
  615. nautobot/virtualization/views.py +158 -54
  616. nautobot/wireless/templates/wireless/wirelessnetwork_create.html +13 -13
  617. nautobot/wireless/tests/integration/test_radio_profile.py +1 -1
  618. {nautobot-2.4.17.dist-info → nautobot-2.4.19.dist-info}/METADATA +4 -4
  619. {nautobot-2.4.17.dist-info → nautobot-2.4.19.dist-info}/RECORD +623 -607
  620. nautobot/core/templates/inc/breadcrumbs.html +0 -14
  621. nautobot/ipam/templates/ipam/prefix_ipaddresses.html +0 -11
  622. nautobot/ipam/templates/ipam/prefix_prefixes.html +0 -11
  623. nautobot/project-static/docs/requirements.txt +0 -14
  624. {nautobot-2.4.17.dist-info → nautobot-2.4.19.dist-info}/LICENSE.txt +0 -0
  625. {nautobot-2.4.17.dist-info → nautobot-2.4.19.dist-info}/NOTICE +0 -0
  626. {nautobot-2.4.17.dist-info → nautobot-2.4.19.dist-info}/WHEEL +0 -0
  627. {nautobot-2.4.17.dist-info → nautobot-2.4.19.dist-info}/entry_points.txt +0 -0
nautobot/extras/views.py CHANGED
@@ -7,10 +7,11 @@ from django.contrib import messages
7
7
  from django.contrib.contenttypes.models import ContentType
8
8
  from django.core.exceptions import ObjectDoesNotExist, ValidationError
9
9
  from django.db import IntegrityError, transaction
10
- from django.db.models import ProtectedError, Q
10
+ from django.db.models import Q
11
11
  from django.forms.utils import pretty_name
12
12
  from django.http import Http404, HttpResponse, HttpResponseForbidden
13
13
  from django.shortcuts import get_object_or_404, redirect, render
14
+ from django.template.defaultfilters import urlencode
14
15
  from django.template.loader import get_template, TemplateDoesNotExist
15
16
  from django.urls import reverse
16
17
  from django.urls.exceptions import NoReverseMatch
@@ -25,7 +26,6 @@ from jsonschema.validators import Draft7Validator
25
26
  from rest_framework.decorators import action
26
27
  from rest_framework.permissions import IsAuthenticated
27
28
 
28
- from nautobot.apps.ui import BaseTextPanel
29
29
  from nautobot.core.choices import ButtonActionColorChoices
30
30
  from nautobot.core.constants import PAGINATE_COUNT_DEFAULT
31
31
  from nautobot.core.events import publish_event
@@ -36,8 +36,16 @@ from nautobot.core.models.utils import pretty_print_query, serialize_object_v2
36
36
  from nautobot.core.tables import ButtonsColumn
37
37
  from nautobot.core.templatetags import helpers
38
38
  from nautobot.core.ui import object_detail
39
+ from nautobot.core.ui.breadcrumbs import (
40
+ BaseBreadcrumbItem,
41
+ Breadcrumbs,
42
+ context_object_attr,
43
+ InstanceParentBreadcrumbItem,
44
+ ModelBreadcrumbItem,
45
+ ViewNameBreadcrumbItem,
46
+ )
39
47
  from nautobot.core.ui.choices import SectionChoices
40
- from nautobot.core.ui.object_detail import ObjectDetailContent, ObjectFieldsPanel, ObjectTextPanel
48
+ from nautobot.core.ui.titles import Titles
41
49
  from nautobot.core.utils.config import get_settings_or_config
42
50
  from nautobot.core.utils.lookup import (
43
51
  get_filterset_for_model,
@@ -46,11 +54,9 @@ from nautobot.core.utils.lookup import (
46
54
  get_table_class_string_from_view_name,
47
55
  get_table_for_model,
48
56
  )
49
- from nautobot.core.utils.permissions import get_permission_for_model
50
57
  from nautobot.core.utils.requests import is_single_choice_field, normalize_querydict
51
58
  from nautobot.core.views import generic, viewsets
52
59
  from nautobot.core.views.mixins import (
53
- GetReturnURLMixin,
54
60
  ObjectBulkCreateViewMixin,
55
61
  ObjectBulkDestroyViewMixin,
56
62
  ObjectBulkUpdateViewMixin,
@@ -63,7 +69,7 @@ from nautobot.core.views.mixins import (
63
69
  ObjectPermissionRequiredMixin,
64
70
  )
65
71
  from nautobot.core.views.paginator import EnhancedPaginator, get_paginate_count
66
- from nautobot.core.views.utils import prepare_cloned_fields
72
+ from nautobot.core.views.utils import get_obj_from_context, prepare_cloned_fields
67
73
  from nautobot.core.views.viewsets import NautobotUIViewSet
68
74
  from nautobot.dcim.models import Controller, Device, Interface, Module, Rack, VirtualDeviceContext
69
75
  from nautobot.dcim.tables import (
@@ -160,12 +166,12 @@ class ComputedFieldUIViewSet(NautobotUIViewSet):
160
166
  fields="__all__",
161
167
  exclude_fields=["template"],
162
168
  ),
163
- ObjectTextPanel(
169
+ object_detail.ObjectTextPanel(
164
170
  label="Template",
165
171
  section=SectionChoices.FULL_WIDTH,
166
172
  weight=100,
167
173
  object_field="template",
168
- render_as=ObjectTextPanel.RenderOptions.CODE,
174
+ render_as=object_detail.ObjectTextPanel.RenderOptions.CODE,
169
175
  ),
170
176
  ),
171
177
  )
@@ -576,6 +582,59 @@ class CustomFieldUIViewSet(NautobotUIViewSet):
576
582
  template_name = "extras/customfield_update.html"
577
583
  action_buttons = ("add",)
578
584
 
585
+ class CustomFieldObjectFieldsPanel(object_detail.ObjectFieldsPanel):
586
+ def render_value(self, key, value, context):
587
+ obj = get_obj_from_context(context, self.context_object_key)
588
+ _type = getattr(obj, "type", None)
589
+
590
+ if key == "default":
591
+ if not value:
592
+ return helpers.HTML_NONE
593
+ if _type == "markdown":
594
+ return helpers.render_markdown(value)
595
+ elif _type == "json":
596
+ return helpers.render_json(value)
597
+ else:
598
+ return helpers.placeholder(value)
599
+ return super().render_value(key, value, context)
600
+
601
+ object_detail_content = object_detail.ObjectDetailContent(
602
+ panels=[
603
+ CustomFieldObjectFieldsPanel(
604
+ weight=100,
605
+ section=SectionChoices.LEFT_HALF,
606
+ fields="__all__",
607
+ exclude_fields=["content_types", "validation_minimum", "validation_maximum", "validation_regex"],
608
+ ),
609
+ object_detail.DataTablePanel(
610
+ weight=200,
611
+ section=SectionChoices.LEFT_HALF,
612
+ label="Custom Field Choices",
613
+ context_data_key="choices_data",
614
+ context_columns_key="columns",
615
+ context_column_headers_key="header",
616
+ ),
617
+ object_detail.ObjectFieldsPanel(
618
+ section=SectionChoices.RIGHT_HALF,
619
+ weight=100,
620
+ label="Assignment",
621
+ fields=[
622
+ "content_types",
623
+ ],
624
+ key_transforms={"content_types": "Content Types"},
625
+ ),
626
+ object_detail.ObjectFieldsPanel(
627
+ section=SectionChoices.RIGHT_HALF,
628
+ weight=200,
629
+ label="Validation Rules",
630
+ fields=["validation_minimum", "validation_maximum", "validation_regex"],
631
+ value_transforms={
632
+ "validation_regex": [lambda val: None if val == "" else val, helpers.pre_tag],
633
+ },
634
+ ),
635
+ ]
636
+ )
637
+
579
638
  def get_extra_context(self, request, instance):
580
639
  context = super().get_extra_context(request, instance)
581
640
 
@@ -585,6 +644,16 @@ class CustomFieldUIViewSet(NautobotUIViewSet):
585
644
  else:
586
645
  context["choices"] = forms.CustomFieldChoiceFormSet(instance=instance)
587
646
 
647
+ if self.action == "retrieve":
648
+ choices_data = []
649
+
650
+ for choice in instance.custom_field_choices.all():
651
+ choices_data.append({"value": choice.value, "weight": choice.weight})
652
+
653
+ context["columns"] = ["value", "weight"]
654
+ context["header"] = ["Value", "Weight"]
655
+ context["choices_data"] = choices_data
656
+
588
657
  return context
589
658
 
590
659
  def form_save(self, form, **kwargs):
@@ -613,9 +682,9 @@ class CustomLinkUIViewSet(NautobotUIViewSet):
613
682
  serializer_class = serializers.CustomLinkSerializer
614
683
  table_class = tables.CustomLinkTable
615
684
 
616
- object_detail_content = ObjectDetailContent(
685
+ object_detail_content = object_detail.ObjectDetailContent(
617
686
  panels=[
618
- ObjectFieldsPanel(
687
+ object_detail.ObjectFieldsPanel(
619
688
  label="Custom Link",
620
689
  section=SectionChoices.LEFT_HALF,
621
690
  weight=100,
@@ -649,212 +718,263 @@ class CustomLinkUIViewSet(NautobotUIViewSet):
649
718
  #
650
719
 
651
720
 
652
- class DynamicGroupListView(generic.ObjectListView):
721
+ class DynamicGroupUIViewSet(NautobotUIViewSet):
722
+ bulk_update_form_class = forms.DynamicGroupBulkEditForm
723
+ filterset_class = filters.DynamicGroupFilterSet
724
+ filterset_form_class = forms.DynamicGroupFilterForm
725
+ form_class = forms.DynamicGroupForm
653
726
  queryset = DynamicGroup.objects.all()
654
- table = tables.DynamicGroupTable
655
- filterset = filters.DynamicGroupFilterSet
656
- filterset_form = forms.DynamicGroupFilterForm
727
+ serializer_class = serializers.DynamicGroupSerializer
728
+ table_class = tables.DynamicGroupTable
657
729
  action_buttons = ("add",)
658
730
 
659
-
660
- class DynamicGroupView(generic.ObjectView):
661
- queryset = DynamicGroup.objects.all()
662
-
663
731
  def get_extra_context(self, request, instance):
664
732
  context = super().get_extra_context(request, instance)
665
- model = instance.model
666
- table_class = get_table_for_model(model)
667
-
668
- if instance.group_type != DynamicGroupTypeChoices.TYPE_STATIC:
669
- # Ensure that members cache is up-to-date for this specific group
670
- members = instance.update_cached_members()
671
- messages.success(request, f"Refreshed cached members list for {instance}")
672
- else:
673
- members = instance.members
674
-
675
- if table_class is not None:
676
- # Members table (for display on Members nav tab)
677
- if hasattr(members, "without_tree_fields"):
678
- members = members.without_tree_fields()
679
- members_table = table_class(
680
- members.restrict(request.user, "view"),
681
- orderable=False,
682
- exclude=["dynamic_group_count"],
683
- hide_hierarchy_ui=True,
684
- )
685
- paginate = {
686
- "paginator_class": EnhancedPaginator,
687
- "per_page": get_paginate_count(request),
688
- }
689
- RequestConfig(request, paginate).configure(members_table)
690
-
691
- # Descendants table
692
- descendants_memberships = instance.membership_tree()
693
- descendants_table = tables.NestedDynamicGroupDescendantsTable(
694
- descendants_memberships,
695
- orderable=False,
696
- )
697
- descendants_tree = {m.pk: m.depth for m in descendants_memberships}
733
+ if self.action in ("create", "update"):
734
+ filterform_class = instance.generate_filter_form()
735
+ if filterform_class is None:
736
+ context["filter_form"] = None
737
+ elif request.POST:
738
+ context["filter_form"] = filterform_class(data=request.POST)
739
+ else:
740
+ initial = instance.get_initial()
741
+ context["filter_form"] = filterform_class(initial=initial)
698
742
 
699
- # Ancestors table
700
- ancestors = instance.get_ancestors()
701
- ancestors_table = tables.NestedDynamicGroupAncestorsTable(ancestors, orderable=False)
702
- ancestors_tree = instance.flatten_ancestors_tree(instance.ancestors_tree())
743
+ formset_kwargs = {"instance": instance}
744
+ if request.POST:
745
+ formset_kwargs["data"] = request.POST
746
+ context["children"] = forms.DynamicGroupMembershipFormSet(**formset_kwargs)
703
747
 
748
+ elif self.action == "retrieve":
749
+ model = instance.model
750
+ table_class = get_table_for_model(model)
704
751
  if instance.group_type != DynamicGroupTypeChoices.TYPE_STATIC:
705
- context["raw_query"] = pretty_print_query(instance.generate_query())
706
- context["members_list_url"] = None
752
+ # Ensure that members cache is up-to-date for this specific group
753
+ members = instance.update_cached_members()
754
+ messages.success(request, f"Refreshed cached members list for {instance}")
707
755
  else:
708
- context["raw_query"] = None
709
- try:
710
- context["members_list_url"] = reverse(get_route_for_model(instance.model, "list"))
711
- except NoReverseMatch:
712
- context["members_list_url"] = None
713
- context["members_verbose_name_plural"] = instance.model._meta.verbose_name_plural
714
- context["members_table"] = members_table
715
- context["ancestors_table"] = ancestors_table
716
- context["ancestors_tree"] = ancestors_tree
717
- context["descendants_table"] = descendants_table
718
- context["descendants_tree"] = descendants_tree
756
+ members = instance.members
757
+ if table_class is not None:
758
+ if hasattr(members, "without_tree_fields"):
759
+ members = members.without_tree_fields()
760
+
761
+ members_table = table_class(
762
+ members.restrict(request.user, "view"),
763
+ orderable=False,
764
+ exclude=["dynamic_group_count"],
765
+ hide_hierarchy_ui=True,
766
+ )
767
+ paginate = {
768
+ "paginator_class": EnhancedPaginator,
769
+ "per_page": get_paginate_count(request),
770
+ }
771
+ RequestConfig(request, paginate).configure(members_table)
719
772
 
720
- return context
773
+ # Descendants table
774
+ descendants_memberships = instance.membership_tree()
775
+ descendants_table = tables.NestedDynamicGroupDescendantsTable(
776
+ descendants_memberships,
777
+ orderable=False,
778
+ )
779
+ descendants_tree = {m.pk: m.depth for m in descendants_memberships}
721
780
 
781
+ # Ancestors table
782
+ ancestors = instance.get_ancestors()
783
+ ancestors_table = tables.NestedDynamicGroupAncestorsTable(
784
+ ancestors,
785
+ orderable=False,
786
+ )
787
+ ancestors_tree = instance.flatten_ancestors_tree(instance.ancestors_tree())
788
+ if instance.group_type != DynamicGroupTypeChoices.TYPE_STATIC:
789
+ context["raw_query"] = pretty_print_query(instance.generate_query())
790
+ context["members_list_url"] = None
791
+ else:
792
+ context["raw_query"] = None
793
+ try:
794
+ context["members_list_url"] = reverse(get_route_for_model(instance.model, "list"))
795
+ except NoReverseMatch:
796
+ context["members_list_url"] = None
797
+
798
+ context.update(
799
+ {
800
+ "members_verbose_name_plural": instance.model._meta.verbose_name_plural,
801
+ "members_table": members_table,
802
+ "ancestors_table": ancestors_table,
803
+ "ancestors_tree": ancestors_tree,
804
+ "descendants_table": descendants_table,
805
+ "descendants_tree": descendants_tree,
806
+ }
807
+ )
722
808
 
723
- class DynamicGroupEditView(generic.ObjectEditView):
724
- queryset = DynamicGroup.objects.all()
725
- model_form = forms.DynamicGroupForm
726
- template_name = "extras/dynamicgroup_edit.html"
809
+ return context
727
810
 
728
- def get_extra_context(self, request, instance):
729
- ctx = super().get_extra_context(request, instance)
811
+ def form_save(self, form, **kwargs):
812
+ obj = form.save(commit=False)
813
+ context = self.get_extra_context(self.request, obj)
814
+
815
+ # Save filters
816
+ if obj.group_type == DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER:
817
+ filter_form = context.get("filter_form")
818
+ if not filter_form or not filter_form.is_valid():
819
+ form.add_error(None, "Errors encountered when saving Dynamic Group associations. See below.")
820
+ raise ValidationError("invalid dynamic group filter_form")
821
+ try:
822
+ obj.set_filter(filter_form.cleaned_data)
823
+ except ValidationError as err:
824
+ form.add_error(None, "Invalid filter detected in existing DynamicGroup filter data.")
825
+ for msg in getattr(err, "messages", [str(err)]):
826
+ if msg:
827
+ form.add_error(None, msg)
828
+ raise
829
+
830
+ # After filters have been set, now we save the object to the database.
831
+ obj.save()
832
+ # Save m2m fields, such as Tags https://docs.djangoproject.com/en/3.2/topics/forms/modelforms/#the-save-method
833
+ form.save_m2m()
834
+
835
+ # Process the formsets for children
836
+ children = context.get("children")
837
+ if children and not children.is_valid():
838
+ form.add_error(None, "Errors encountered when saving Dynamic Group associations. See below.")
839
+ # dedupe only non-field errors to avoid duplicates in the banner
840
+ added_errors = set()
841
+ for f in children.forms:
842
+ for msg in f.non_field_errors():
843
+ if msg not in added_errors:
844
+ form.add_error(None, msg)
845
+ added_errors.add(msg)
846
+ raise ValidationError("invalid DynamicGroupMembershipFormSet")
847
+
848
+ if children:
849
+ children.save()
730
850
 
731
- filterform_class = instance.generate_filter_form()
851
+ return obj
732
852
 
733
- if filterform_class is None:
734
- filter_form = None
735
- elif request.POST:
736
- filter_form = filterform_class(data=request.POST)
737
- else:
738
- initial = instance.get_initial()
739
- filter_form = filterform_class(initial=initial)
853
+ # Suppress the global top banner when ValidationError happens
854
+ def _handle_validation_error(self, e):
855
+ self.has_error = True
740
856
 
741
- ctx["filter_form"] = filter_form
857
+ @action(
858
+ detail=False,
859
+ methods=["GET", "POST"],
860
+ url_path="assign-members",
861
+ url_name="bulk_assign",
862
+ custom_view_base_action="add",
863
+ custom_view_additional_permissions=[
864
+ "extras.add_staticgroupassociation",
865
+ ],
866
+ )
867
+ def bulk_assign(self, request):
868
+ """
869
+ Update the static group assignments of the provided `pk_list` (or `_all`) of the given `content_type`.
742
870
 
743
- formset_kwargs = {"instance": instance}
744
- if request.POST:
745
- formset_kwargs["data"] = request.POST
871
+ Unlike BulkEditView, this takes a single POST rather than two to perform its operation as
872
+ there's no separate confirmation step involved.
873
+ """
874
+ if request.method == "GET":
875
+ return redirect(reverse("extras:staticgroupassociation_list"))
746
876
 
747
- ctx["children"] = forms.DynamicGroupMembershipFormSet(**formset_kwargs)
877
+ # TODO more error handling - content-type doesn't exist, model_class not found, filterset missing, etc.
878
+ content_type = ContentType.objects.get(pk=request.POST.get("content_type"))
879
+ model = content_type.model_class()
880
+ self.default_return_url = get_route_for_model(model, "list")
881
+ filterset_class = get_filterset_for_model(model)
748
882
 
749
- return ctx
883
+ if request.POST.get("_all"):
884
+ if filterset_class:
885
+ pk_list = list(filterset_class(request.GET, model.objects.only("pk")).qs.values_list("pk", flat=True))
886
+ else:
887
+ pk_list = list(model.objects.values_list("pk", flat=True))
888
+ else:
889
+ pk_list = request.POST.getlist("pk")
750
890
 
751
- def post(self, request, *args, **kwargs):
752
- obj = self.alter_obj(self.get_object(kwargs), request, args, kwargs)
753
- form = self.model_form(data=request.POST, files=request.FILES, instance=obj)
891
+ form = forms.DynamicGroupBulkAssignForm(model, request.POST)
754
892
  restrict_form_fields(form, request.user)
755
893
 
756
894
  if form.is_valid():
757
895
  logger.debug("Form validation was successful")
758
-
759
896
  try:
760
897
  with transaction.atomic():
761
- object_created = not form.instance.present_in_database
762
- # Obtain the instance, but do not yet `save()` it to the database.
763
- obj = form.save(commit=False)
764
-
765
- ctx = self.get_extra_context(request, obj)
766
- if obj.group_type == DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER:
767
- # Process the filter form and save the query filters to `obj.filter`.
768
- filter_form = ctx["filter_form"]
769
- if filter_form.is_valid():
770
- obj.set_filter(filter_form.cleaned_data)
898
+ add_to_groups = list(form.cleaned_data["add_to_groups"])
899
+ new_group_name = form.cleaned_data["create_and_assign_to_new_group_name"]
900
+ if new_group_name:
901
+ if not request.user.has_perm("extras.add_dynamicgroup"):
902
+ raise DynamicGroup.DoesNotExist
771
903
  else:
772
- raise RuntimeError(filter_form.errors)
904
+ new_group = DynamicGroup(
905
+ name=new_group_name,
906
+ content_type=content_type,
907
+ group_type=DynamicGroupTypeChoices.TYPE_STATIC,
908
+ )
909
+ new_group.validated_save()
910
+ # Check permissions
911
+ DynamicGroup.objects.restrict(request.user, "add").get(pk=new_group.pk)
773
912
 
774
- # After filters have been set, now we save the object to the database.
775
- obj.save()
776
- # Save m2m fields, such as Tags https://docs.djangoproject.com/en/3.2/topics/forms/modelforms/#the-save-method
777
- form.save_m2m()
778
- # Check that the new object conforms with any assigned object-level permissions
779
- self.queryset.get(pk=obj.pk)
913
+ add_to_groups.append(new_group)
914
+ msg = "Created dynamic group"
915
+ logger.info(f"{msg} {new_group} (PK: {new_group.pk})")
916
+ msg = format_html('{} <a href="{}">{}</a>', msg, new_group.get_absolute_url(), new_group)
917
+ messages.success(request, msg)
780
918
 
781
- # Process the formsets for children
782
- children = ctx["children"]
783
- if children.is_valid():
784
- children.save()
785
- else:
786
- raise RuntimeError(children.errors)
787
- verb = "Created" if object_created else "Modified"
788
- msg = f"{verb} {self.queryset.model._meta.verbose_name}"
789
- logger.info(f"{msg} {obj} (PK: {obj.pk})")
790
- try:
791
- msg = format_html('{} <a href="{}">{}</a>', msg, obj.get_absolute_url(), obj)
792
- except AttributeError:
793
- msg = format_html("{} {}", msg, obj)
794
- messages.success(request, msg)
919
+ with deferred_change_logging_for_bulk_operation():
920
+ associations = []
921
+ for pk in pk_list:
922
+ for dynamic_group in add_to_groups:
923
+ association, created = StaticGroupAssociation.objects.get_or_create(
924
+ dynamic_group=dynamic_group,
925
+ associated_object_type_id=content_type.id,
926
+ associated_object_id=pk,
927
+ )
928
+ association.validated_save()
929
+ associations.append(association)
930
+ if created:
931
+ logger.debug("Created %s", association)
795
932
 
796
- if "_addanother" in request.POST:
797
- # If the object has clone_fields, pre-populate a new instance of the form
798
- if hasattr(obj, "clone_fields"):
799
- url = f"{request.path}?{prepare_cloned_fields(obj)}"
800
- return redirect(url)
933
+ # Enforce object-level permissions
934
+ permitted_associations = StaticGroupAssociation.objects.restrict(request.user, "add")
935
+ if permitted_associations.filter(pk__in=[assoc.pk for assoc in associations]).count() != len(
936
+ associations
937
+ ):
938
+ raise StaticGroupAssociation.DoesNotExist
801
939
 
802
- return redirect(request.get_full_path())
940
+ if associations:
941
+ msg = (
942
+ f"Added {len(pk_list)} {model._meta.verbose_name_plural} "
943
+ f"to {len(add_to_groups)} dynamic group(s)."
944
+ )
945
+ logger.info(msg)
946
+ messages.success(request, msg)
803
947
 
804
- return_url = form.cleaned_data.get("return_url")
805
- if url_has_allowed_host_and_scheme(url=return_url, allowed_hosts=request.get_host()):
806
- return redirect(iri_to_uri(return_url))
807
- else:
808
- return redirect(self.get_return_url(request, obj))
948
+ if form.cleaned_data["remove_from_groups"]:
949
+ for dynamic_group in form.cleaned_data["remove_from_groups"]:
950
+ (
951
+ StaticGroupAssociation.objects.restrict(request.user, "delete")
952
+ .filter(
953
+ dynamic_group=dynamic_group,
954
+ associated_object_type=content_type,
955
+ associated_object_id__in=pk_list,
956
+ )
957
+ .delete()
958
+ )
809
959
 
960
+ msg = (
961
+ f"Removed {len(pk_list)} {model._meta.verbose_name_plural} from "
962
+ f"{len(form.cleaned_data['remove_from_groups'])} dynamic group(s)."
963
+ )
964
+ logger.info(msg)
965
+ messages.success(request, msg)
966
+ except ValidationError as e:
967
+ messages.error(request, e)
810
968
  except ObjectDoesNotExist:
811
- msg = "Object save failed due to object-level permissions violation."
812
- logger.debug(msg)
813
- form.add_error(None, msg)
814
- except RuntimeError:
815
- msg = "Errors encountered when saving Dynamic Group associations. See below."
816
- logger.debug(msg)
817
- form.add_error(None, msg)
818
- except ProtectedError as err:
819
- # e.g. Trying to delete a something that is in use.
820
- err_msg = err.args[0]
821
- protected_obj = err.protected_objects[0]
822
- msg = f"{protected_obj.value}: {err_msg} Please cancel this edit and start again."
823
- logger.debug(msg)
824
- form.add_error(None, msg)
825
- except ValidationError as err:
826
- msg = "Invalid filter detected in existing DynamicGroup filter data."
827
- logger.debug(msg)
828
- err_messages = err.args[0].split("\n")
829
- for message in err_messages:
830
- if message:
831
- form.add_error(None, message)
969
+ msg = "Static group association failed due to object-level permissions violation"
970
+ logger.warning(msg)
971
+ messages.error(request, msg)
832
972
 
833
973
  else:
834
974
  logger.debug("Form validation failed")
975
+ messages.error(request, form.errors)
835
976
 
836
- return render(
837
- request,
838
- self.template_name,
839
- {
840
- "obj": obj,
841
- "obj_type": self.queryset.model._meta.verbose_name,
842
- "form": form,
843
- "return_url": self.get_return_url(request, obj),
844
- "editing": obj.present_in_database,
845
- **self.get_extra_context(request, obj),
846
- },
847
- )
848
-
849
-
850
- class DynamicGroupDeleteView(generic.ObjectDeleteView):
851
- queryset = DynamicGroup.objects.all()
852
-
853
-
854
- class DynamicGroupBulkDeleteView(generic.BulkDeleteView):
855
- queryset = DynamicGroup.objects.all()
856
- table = tables.DynamicGroupTable
857
- filterset = filters.DynamicGroupFilterSet
977
+ return redirect(self.get_return_url(request))
858
978
 
859
979
 
860
980
  class ObjectDynamicGroupsView(generic.GenericView):
@@ -870,6 +990,8 @@ class ObjectDynamicGroupsView(generic.GenericView):
870
990
  """
871
991
 
872
992
  base_template: Optional[str] = None
993
+ breadcrumbs = Breadcrumbs()
994
+ view_titles = Titles()
873
995
 
874
996
  def get(self, request, model, **kwargs):
875
997
  # Handle QuerySet restriction of parent object if needed
@@ -903,6 +1025,9 @@ class ObjectDynamicGroupsView(generic.GenericView):
903
1025
  "table": dynamicgroups_table,
904
1026
  "base_template": base_template,
905
1027
  "active_tab": "dynamic-groups",
1028
+ "breadcrumbs": self.breadcrumbs,
1029
+ "view_titles": self.view_titles,
1030
+ "detail": True,
906
1031
  },
907
1032
  )
908
1033
 
@@ -921,26 +1046,26 @@ class ExportTemplateUIViewSet(NautobotUIViewSet):
921
1046
  serializer_class = serializers.ExportTemplateSerializer
922
1047
  table_class = tables.ExportTemplateTable
923
1048
 
924
- object_detail_content = ObjectDetailContent(
1049
+ object_detail_content = object_detail.ObjectDetailContent(
925
1050
  panels=[
926
- ObjectFieldsPanel(
1051
+ object_detail.ObjectFieldsPanel(
927
1052
  label="Details",
928
1053
  section=SectionChoices.LEFT_HALF,
929
1054
  weight=100,
930
1055
  fields=["name", "owner", "description"],
931
1056
  ),
932
- ObjectFieldsPanel(
1057
+ object_detail.ObjectFieldsPanel(
933
1058
  label="Template",
934
1059
  section=SectionChoices.LEFT_HALF,
935
1060
  weight=200,
936
1061
  fields=["content_type", "mime_type", "file_extension"],
937
1062
  ),
938
- ObjectTextPanel(
1063
+ object_detail.ObjectTextPanel(
939
1064
  label="Code Template",
940
1065
  section=SectionChoices.RIGHT_HALF,
941
1066
  weight=100,
942
1067
  object_field="template_code",
943
- render_as=ObjectTextPanel.RenderOptions.CODE,
1068
+ render_as=object_detail.ObjectTextPanel.RenderOptions.CODE,
944
1069
  ),
945
1070
  ]
946
1071
  )
@@ -987,97 +1112,6 @@ class ExternalIntegrationUIViewSet(NautobotUIViewSet):
987
1112
  #
988
1113
 
989
1114
 
990
- class GitRepositoryListView(generic.ObjectListView):
991
- queryset = GitRepository.objects.all()
992
- filterset = filters.GitRepositoryFilterSet
993
- filterset_form = forms.GitRepositoryFilterForm
994
- table = tables.GitRepositoryTable
995
- template_name = "extras/gitrepository_list.html"
996
-
997
- def extra_context(self):
998
- # Get the newest results for each repository name
999
- results = {
1000
- r.task_kwargs["repository"]: r
1001
- for r in JobResult.objects.filter(
1002
- task_name__startswith="nautobot.core.jobs.GitRepository",
1003
- task_kwargs__repository__isnull=False,
1004
- status__in=JobResultStatusChoices.READY_STATES,
1005
- )
1006
- .order_by("date_done")
1007
- .defer("result")
1008
- }
1009
- return {
1010
- "job_results": results,
1011
- "datasource_contents": get_datasource_contents("extras.gitrepository"),
1012
- }
1013
-
1014
-
1015
- class GitRepositoryView(generic.ObjectView):
1016
- queryset = GitRepository.objects.all()
1017
-
1018
- def get_extra_context(self, request, instance):
1019
- return {
1020
- "datasource_contents": get_datasource_contents("extras.gitrepository"),
1021
- **super().get_extra_context(request, instance),
1022
- }
1023
-
1024
-
1025
- class GitRepositoryEditView(generic.ObjectEditView):
1026
- queryset = GitRepository.objects.all()
1027
- model_form = forms.GitRepositoryForm
1028
- template_name = "extras/gitrepository_object_edit.html"
1029
-
1030
- # TODO(jathan): Align with changes for v2 where we're not stashing the user on the instance for
1031
- # magical calls and instead discretely calling `repo.sync(user=user, dry_run=dry_run)`, but
1032
- # again, this will be moved to the API calls, so just something to keep in mind.
1033
- def alter_obj(self, obj, request, url_args, url_kwargs):
1034
- # A GitRepository needs to know the originating request when it's saved so that it can enqueue using it
1035
- obj.user = request.user
1036
- return super().alter_obj(obj, request, url_args, url_kwargs)
1037
-
1038
- def get_return_url(self, request, obj=None, default_return_url=None):
1039
- if request.method == "POST":
1040
- return reverse("extras:gitrepository_result", kwargs={"pk": obj.pk})
1041
- return super().get_return_url(request, obj=obj, default_return_url=default_return_url)
1042
-
1043
-
1044
- class GitRepositoryDeleteView(generic.ObjectDeleteView):
1045
- queryset = GitRepository.objects.all()
1046
-
1047
-
1048
- class GitRepositoryBulkImportView(generic.BulkImportView): # 3.0 TODO: remove, unused
1049
- queryset = GitRepository.objects.all()
1050
- table = tables.GitRepositoryBulkTable
1051
-
1052
-
1053
- class GitRepositoryBulkEditView(generic.BulkEditView):
1054
- queryset = GitRepository.objects.select_related("secrets_group")
1055
- filterset = filters.GitRepositoryFilterSet
1056
- table = tables.GitRepositoryBulkTable
1057
- form = forms.GitRepositoryBulkEditForm
1058
-
1059
- def alter_obj(self, obj, request, url_args, url_kwargs):
1060
- # A GitRepository needs to know the originating request when it's saved so that it can enqueue using it
1061
- obj.request = request
1062
- return super().alter_obj(obj, request, url_args, url_kwargs)
1063
-
1064
- def extra_context(self):
1065
- return {
1066
- "datasource_contents": get_datasource_contents("extras.gitrepository"),
1067
- }
1068
-
1069
-
1070
- class GitRepositoryBulkDeleteView(generic.BulkDeleteView):
1071
- queryset = GitRepository.objects.all()
1072
- table = tables.GitRepositoryBulkTable
1073
- filterset = filters.GitRepositoryFilterSet
1074
-
1075
- def extra_context(self):
1076
- return {
1077
- "datasource_contents": get_datasource_contents("extras.gitrepository"),
1078
- }
1079
-
1080
-
1081
1115
  def check_and_call_git_repository_function(request, pk, func):
1082
1116
  """Helper for checking Git permissions and worker availability, then calling provided function if all is well
1083
1117
  Args:
@@ -1102,40 +1136,88 @@ def check_and_call_git_repository_function(request, pk, func):
1102
1136
  return redirect(job_result.get_absolute_url())
1103
1137
 
1104
1138
 
1105
- class GitRepositorySyncView(generic.GenericView):
1106
- def post(self, request, pk):
1107
- return check_and_call_git_repository_function(request, pk, enqueue_pull_git_repository_and_refresh_data)
1108
-
1109
-
1110
- class GitRepositoryDryRunView(generic.GenericView):
1111
- def post(self, request, pk):
1112
- return check_and_call_git_repository_function(request, pk, enqueue_git_repository_diff_origin_and_local)
1139
+ class GitRepositoryUIViewSet(NautobotUIViewSet):
1140
+ bulk_update_form_class = forms.GitRepositoryBulkEditForm
1141
+ filterset_form_class = forms.GitRepositoryFilterForm
1142
+ queryset = GitRepository.objects.all()
1143
+ form_class = forms.GitRepositoryForm
1144
+ filterset_class = filters.GitRepositoryFilterSet
1145
+ serializer_class = serializers.GitRepositorySerializer
1146
+ table_class = tables.GitRepositoryTable
1113
1147
 
1148
+ def get_extra_context(self, request, instance=None):
1149
+ context = super().get_extra_context(request, instance)
1150
+ context["datasource_contents"] = get_datasource_contents("extras.gitrepository")
1151
+
1152
+ if self.action in ("list", "bulk_update", "bulk_destroy"):
1153
+ results = {
1154
+ r.task_kwargs["repository"]: r
1155
+ for r in JobResult.objects.filter(
1156
+ task_name__startswith="nautobot.core.jobs.GitRepository",
1157
+ task_kwargs__repository__isnull=False,
1158
+ status__in=JobResultStatusChoices.READY_STATES,
1159
+ )
1160
+ .order_by("date_done")
1161
+ .defer("result")
1162
+ }
1163
+ context["job_results"] = results
1114
1164
 
1115
- class GitRepositoryResultView(generic.ObjectView):
1116
- """
1117
- Display a JobResult and its Job data.
1118
- """
1165
+ return context
1119
1166
 
1120
- queryset = GitRepository.objects.all()
1121
- template_name = "extras/gitrepository_result.html"
1167
+ def form_valid(self, form):
1168
+ if hasattr(form, "instance") and form.instance is not None:
1169
+ form.instance.user = self.request.user
1170
+ form.instance.request = self.request
1171
+ return super().form_valid(form)
1122
1172
 
1123
- def get_required_permission(self):
1124
- return "extras.view_gitrepository"
1173
+ def get_return_url(self, request, obj=None, default_return_url=None):
1174
+ # Only redirect to result if object exists and action is not deletion
1175
+ if request.method == "POST" and obj is not None and self.action != "destroy":
1176
+ return reverse("extras:gitrepository_result", kwargs={"pk": obj.pk})
1177
+ return super().get_return_url(request, obj=obj, default_return_url=default_return_url)
1125
1178
 
1126
- def get_extra_context(self, request, instance):
1179
+ @action(
1180
+ detail=True,
1181
+ url_path="result",
1182
+ url_name="result",
1183
+ custom_view_base_action="view",
1184
+ )
1185
+ def result(self, request, pk=None):
1186
+ instance = self.get_object()
1127
1187
  job_result = instance.get_latest_sync()
1128
1188
 
1129
- if job_result is None:
1130
- job_result = {}
1131
-
1132
- return {
1133
- "result": job_result,
1189
+ context = {
1190
+ "result": job_result or {},
1134
1191
  "base_template": "extras/gitrepository.html",
1135
1192
  "object": instance,
1136
1193
  "active_tab": "result",
1194
+ "verbose_name": instance._meta.verbose_name,
1137
1195
  }
1138
1196
 
1197
+ return render(request, "extras/gitrepository_result.html", context)
1198
+
1199
+ @action(
1200
+ detail=True,
1201
+ methods=["post"],
1202
+ url_path="sync",
1203
+ url_name="sync",
1204
+ custom_view_base_action="change",
1205
+ custom_view_additional_permissions=["extras.change_gitrepository"],
1206
+ )
1207
+ def sync(self, request, pk=None):
1208
+ return check_and_call_git_repository_function(request, pk, enqueue_pull_git_repository_and_refresh_data)
1209
+
1210
+ @action(
1211
+ detail=True,
1212
+ methods=["post"],
1213
+ url_path="dry-run",
1214
+ url_name="dryrun",
1215
+ custom_view_base_action="change",
1216
+ custom_view_additional_permissions=["extras.change_gitrepository"],
1217
+ )
1218
+ def dry_run(self, request, pk=None):
1219
+ return check_and_call_git_repository_function(request, pk, enqueue_git_repository_diff_origin_and_local)
1220
+
1139
1221
 
1140
1222
  #
1141
1223
  # Saved GraphQL queries
@@ -2036,9 +2118,9 @@ class JobHookUIViewSet(NautobotUIViewSet):
2036
2118
  table_class = tables.JobHookTable
2037
2119
  queryset = JobHook.objects.all()
2038
2120
 
2039
- object_detail_content = ObjectDetailContent(
2121
+ object_detail_content = object_detail.ObjectDetailContent(
2040
2122
  panels=(
2041
- ObjectFieldsPanel(
2123
+ object_detail.ObjectFieldsPanel(
2042
2124
  weight=100,
2043
2125
  section=SectionChoices.LEFT_HALF,
2044
2126
  fields="__all__",
@@ -2064,6 +2146,37 @@ class JobResultUIViewSet(
2064
2146
  table_class = tables.JobResultTable
2065
2147
  queryset = JobResult.objects.all()
2066
2148
  action_buttons = ()
2149
+ breadcrumbs = Breadcrumbs(
2150
+ items={
2151
+ "detail": [
2152
+ ModelBreadcrumbItem(),
2153
+ # if result.job_model is not None
2154
+ BaseBreadcrumbItem(
2155
+ label=context_object_attr("job_model.grouping", context_key="result"),
2156
+ should_render=lambda c: c["result"].job_model is not None,
2157
+ ),
2158
+ InstanceParentBreadcrumbItem(
2159
+ instance_key="result",
2160
+ parent_key="job_model",
2161
+ parent_lookup_key="name",
2162
+ should_render=lambda c: c["result"].job_model is not None,
2163
+ ),
2164
+ # elif job in context
2165
+ ViewNameBreadcrumbItem(
2166
+ view_name="extras:jobresult_list",
2167
+ label=context_object_attr("class_path", context_key="job"),
2168
+ reverse_query_params=lambda c: {"name": urlencode(c["job"].class_path)},
2169
+ should_render=lambda c: c["result"].job_model is None and c["job"] is not None,
2170
+ ),
2171
+ # else
2172
+ BaseBreadcrumbItem(
2173
+ label=context_object_attr("name", context_key="result"),
2174
+ should_render=lambda c: c["result"].job_model is None and c["job"] is None,
2175
+ ),
2176
+ ]
2177
+ },
2178
+ detail_item_label=context_object_attr("date_created"),
2179
+ )
2067
2180
 
2068
2181
  def get_extra_context(self, request, instance):
2069
2182
  context = super().get_extra_context(request, instance)
@@ -2134,9 +2247,9 @@ class JobButtonUIViewSet(NautobotUIViewSet):
2134
2247
  queryset = JobButton.objects.all()
2135
2248
  serializer_class = serializers.JobButtonSerializer
2136
2249
  table_class = tables.JobButtonTable
2137
- object_detail_content = ObjectDetailContent(
2250
+ object_detail_content = object_detail.ObjectDetailContent(
2138
2251
  panels=(
2139
- ObjectFieldsPanel(
2252
+ object_detail.ObjectFieldsPanel(
2140
2253
  weight=100,
2141
2254
  section=SectionChoices.LEFT_HALF,
2142
2255
  fields="__all__",
@@ -2153,18 +2266,16 @@ class JobButtonUIViewSet(NautobotUIViewSet):
2153
2266
  #
2154
2267
  # Change logging
2155
2268
  #
2156
-
2157
-
2158
- class ObjectChangeListView(generic.ObjectListView):
2269
+ class ObjectChangeUIViewSet(ObjectDetailViewMixin, ObjectListViewMixin):
2270
+ filterset_class = filters.ObjectChangeFilterSet
2271
+ filterset_form_class = forms.ObjectChangeFilterForm
2159
2272
  queryset = ObjectChange.objects.all()
2160
- filterset = filters.ObjectChangeFilterSet
2161
- filterset_form = forms.ObjectChangeFilterForm
2162
- table = tables.ObjectChangeTable
2163
- template_name = "extras/objectchange_list.html"
2273
+ serializer_class = serializers.ObjectChangeSerializer
2274
+ table_class = tables.ObjectChangeTable
2164
2275
  action_buttons = ("export",)
2165
2276
 
2166
2277
  # 2.0 TODO: Remove this remapping and solve it at the `BaseFilterSet` as it is addressing a breaking change.
2167
- def get(self, request, **kwargs):
2278
+ def get(self, request, *args, **kwargs):
2168
2279
  # Remappings below allow previous queries of time_before and time_after to use
2169
2280
  # newer methods specifying the lookup method.
2170
2281
 
@@ -2180,26 +2291,34 @@ class ObjectChangeListView(generic.ObjectListView):
2180
2291
  request.GET.update({"time__lte": request.GET.get("time_before")})
2181
2292
  request.GET._mutable = False
2182
2293
 
2183
- return super().get(request=request, **kwargs)
2294
+ return super().get(request=request, *args, **kwargs)
2184
2295
 
2296
+ def get_extra_context(self, request, instance):
2297
+ """
2298
+ Adds snapshot diff and related changes table for the object change detail view.
2299
+ """
2300
+ context = super().get_extra_context(request, instance)
2185
2301
 
2186
- class ObjectChangeView(generic.ObjectView):
2187
- queryset = ObjectChange.objects.all()
2302
+ if self.action == "retrieve":
2303
+ related_changes = instance.get_related_changes(user=request.user).filter(request_id=instance.request_id)
2304
+ related_changes_table = tables.ObjectChangeTable(
2305
+ data=related_changes[:50], # Limit for performance
2306
+ orderable=False,
2307
+ )
2308
+ snapshots = instance.get_snapshots()
2188
2309
 
2189
- def get_extra_context(self, request, instance):
2190
- related_changes = instance.get_related_changes(user=request.user).filter(request_id=instance.request_id)
2191
- related_changes_table = tables.ObjectChangeTable(data=related_changes[:50], orderable=False)
2310
+ context.update(
2311
+ {
2312
+ "diff_added": snapshots["differences"]["added"],
2313
+ "diff_removed": snapshots["differences"]["removed"],
2314
+ "next_change": instance.get_next_change(request.user),
2315
+ "prev_change": instance.get_prev_change(request.user),
2316
+ "related_changes_table": related_changes_table,
2317
+ "related_changes_count": related_changes.count(),
2318
+ }
2319
+ )
2192
2320
 
2193
- snapshots = instance.get_snapshots()
2194
- return {
2195
- "diff_added": snapshots["differences"]["added"],
2196
- "diff_removed": snapshots["differences"]["removed"],
2197
- "next_change": instance.get_next_change(request.user),
2198
- "prev_change": instance.get_prev_change(request.user),
2199
- "related_changes_table": related_changes_table,
2200
- "related_changes_count": related_changes.count(),
2201
- **super().get_extra_context(request, instance),
2202
- }
2321
+ return context
2203
2322
 
2204
2323
 
2205
2324
  class ObjectChangeLogView(generic.GenericView):
@@ -2251,6 +2370,10 @@ class ObjectChangeLogView(generic.GenericView):
2251
2370
  "table": objectchanges_table,
2252
2371
  "base_template": base_template,
2253
2372
  "active_tab": "changelog",
2373
+ "breadcrumbs": self.get_breadcrumbs(obj, view_type=""),
2374
+ "view_titles": self.get_view_titles(obj, view_type=""),
2375
+ "detail": True,
2376
+ "view_action": "changelog",
2254
2377
  },
2255
2378
  )
2256
2379
 
@@ -2344,9 +2467,68 @@ class NoteUIViewSet(
2344
2467
  serializer_class = serializers.NoteSerializer
2345
2468
  table_class = tables.NoteTable
2346
2469
  action_buttons = ()
2470
+ breadcrumbs = Breadcrumbs(
2471
+ items={
2472
+ "detail": [
2473
+ ModelBreadcrumbItem(model=Note),
2474
+ ModelBreadcrumbItem(
2475
+ model=lambda c: c["object"].assigned_object,
2476
+ action="notes",
2477
+ reverse_kwargs=lambda c: {"pk": c["object"].assigned_object.pk},
2478
+ label=lambda c: c["object"].assigned_object,
2479
+ should_render=lambda c: c["object"].assigned_object,
2480
+ ),
2481
+ ]
2482
+ }
2483
+ )
2484
+
2485
+ object_detail_content = object_detail.ObjectDetailContent(
2486
+ panels=(
2487
+ object_detail.ObjectFieldsPanel(
2488
+ weight=100,
2489
+ section=SectionChoices.LEFT_HALF,
2490
+ fields=["user", "assigned_object_type", "assigned_object"],
2491
+ ),
2492
+ object_detail.ObjectTextPanel(
2493
+ label="Text",
2494
+ section=SectionChoices.LEFT_HALF,
2495
+ weight=200,
2496
+ object_field="note",
2497
+ render_as=object_detail.ObjectTextPanel.RenderOptions.MARKDOWN,
2498
+ ),
2499
+ ),
2500
+ )
2501
+
2502
+ def form_save(self, form, commit=True, *args, **kwargs):
2503
+ """
2504
+ Save the form instance while ensuring the Note's `user` and `user_name` fields
2505
+ are correctly populated.
2506
+
2507
+ Args:
2508
+ form (Form): The validated form instance to be saved.
2509
+ commit (bool): If True, save the instance to the database immediately.
2510
+ *args, **kwargs: Additional arguments to maintain compatibility with
2511
+ the parent method signature.
2512
+
2513
+ Returns:
2514
+ Note: The saved or unsaved Note instance with `user` and `user_name` set.
2515
+
2516
+ Behavior:
2517
+ - Sets `user` to the currently authenticated user.
2518
+ - Sets `user_name` to the username of the authenticated user.
2519
+ - Saves the instance if `commit=True`.
2520
+ """
2521
+ # Get instance without committing to DB
2522
+ obj = super().form_save(form, commit=False, *args, **kwargs)
2523
+
2524
+ # Assign user info (only authenticated users can create notes)
2525
+ obj.user = self.request.user
2526
+ obj.user_name = self.request.user.get_username()
2527
+
2528
+ # Save to DB if commit is True
2529
+ if commit:
2530
+ obj.save()
2347
2531
 
2348
- def alter_obj(self, obj, request, url_args, url_kwargs):
2349
- obj.user = request.user
2350
2532
  return obj
2351
2533
 
2352
2534
 
@@ -2396,6 +2578,10 @@ class ObjectNotesView(generic.GenericView):
2396
2578
  "base_template": base_template,
2397
2579
  "active_tab": "notes",
2398
2580
  "form": notes_form,
2581
+ "breadcrumbs": self.get_breadcrumbs(obj, view_type=""),
2582
+ "view_titles": self.get_view_titles(obj, view_type=""),
2583
+ "detail": True,
2584
+ "view_action": "notes",
2399
2585
  },
2400
2586
  )
2401
2587
 
@@ -2414,9 +2600,9 @@ class RelationshipUIViewSet(NautobotUIViewSet):
2414
2600
  table_class = tables.RelationshipTable
2415
2601
  queryset = Relationship.objects.all()
2416
2602
 
2417
- object_detail_content = ObjectDetailContent(
2603
+ object_detail_content = object_detail.ObjectDetailContent(
2418
2604
  panels=(
2419
- ObjectFieldsPanel(
2605
+ object_detail.ObjectFieldsPanel(
2420
2606
  label="Relationship",
2421
2607
  section=SectionChoices.LEFT_HALF,
2422
2608
  weight=100,
@@ -2432,13 +2618,13 @@ class RelationshipUIViewSet(NautobotUIViewSet):
2432
2618
  "destination_filter",
2433
2619
  ],
2434
2620
  ),
2435
- ObjectFieldsPanel(
2621
+ object_detail.ObjectFieldsPanel(
2436
2622
  label="Source Attributes",
2437
2623
  section=SectionChoices.RIGHT_HALF,
2438
2624
  weight=100,
2439
2625
  fields=["source_type", "source_label", "source_hidden", "source_filter"],
2440
2626
  ),
2441
- ObjectFieldsPanel(
2627
+ object_detail.ObjectFieldsPanel(
2442
2628
  label="Destination Attributes",
2443
2629
  section=SectionChoices.RIGHT_HALF,
2444
2630
  weight=200,
@@ -2597,6 +2783,7 @@ class SecretUIViewSet(
2597
2783
  table_title="Groups containing this secret",
2598
2784
  table_class=tables.SecretsGroupTable,
2599
2785
  table_attribute="secrets_groups",
2786
+ distinct=True,
2600
2787
  related_field_name="secrets",
2601
2788
  footer_content_template_path=None,
2602
2789
  ),
@@ -2644,7 +2831,7 @@ class SecretsGroupUIViewSet(NautobotUIViewSet):
2644
2831
  table_class = tables.SecretsGroupTable
2645
2832
  queryset = SecretsGroup.objects.all()
2646
2833
 
2647
- object_detail_content = ObjectDetailContent(
2834
+ object_detail_content = object_detail.ObjectDetailContent(
2648
2835
  panels=(
2649
2836
  object_detail.ObjectFieldsPanel(
2650
2837
  label="Secrets Group Details",
@@ -2712,125 +2899,6 @@ class StaticGroupAssociationUIViewSet(
2712
2899
  return queryset
2713
2900
 
2714
2901
 
2715
- class DynamicGroupBulkAssignView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
2716
- queryset = StaticGroupAssociation.objects.all()
2717
- form_class = forms.DynamicGroupBulkAssignForm
2718
-
2719
- def get_required_permission(self):
2720
- return get_permission_for_model(self.queryset.model, "add")
2721
-
2722
- def get(self, request):
2723
- return redirect(self.get_return_url(request))
2724
-
2725
- def post(self, request, **kwargs):
2726
- """
2727
- Update the static group assignments of the provided `pk_list` (or `_all`) of the given `content_type`.
2728
-
2729
- Unlike BulkEditView, this takes a single POST rather than two to perform its operation as
2730
- there's no separate confirmation step involved.
2731
- """
2732
- # TODO more error handling - content-type doesn't exist, model_class not found, filterset missing, etc.
2733
- content_type = ContentType.objects.get(pk=request.POST.get("content_type"))
2734
- model = content_type.model_class()
2735
- self.default_return_url = get_route_for_model(model, "list")
2736
- filterset_class = get_filterset_for_model(model)
2737
-
2738
- if request.POST.get("_all"):
2739
- if filterset_class is not None:
2740
- pk_list = list(filterset_class(request.GET, model.objects.only("pk")).qs.values_list("pk", flat=True))
2741
- else:
2742
- pk_list = list(model.objects.all().values_list("pk", flat=True))
2743
- else:
2744
- pk_list = request.POST.getlist("pk")
2745
-
2746
- form = self.form_class(model, request.POST)
2747
- restrict_form_fields(form, request.user)
2748
-
2749
- if form.is_valid():
2750
- logger.debug("Form validation was successful")
2751
- try:
2752
- with transaction.atomic():
2753
- add_to_groups = list(form.cleaned_data["add_to_groups"])
2754
- new_group_name = form.cleaned_data["create_and_assign_to_new_group_name"]
2755
- if new_group_name:
2756
- if not request.user.has_perm("extras.add_dynamicgroup"):
2757
- raise DynamicGroup.DoesNotExist
2758
- else:
2759
- new_group = DynamicGroup(
2760
- name=new_group_name,
2761
- content_type=content_type,
2762
- group_type=DynamicGroupTypeChoices.TYPE_STATIC,
2763
- )
2764
- new_group.validated_save()
2765
- # Check permissions
2766
- DynamicGroup.objects.restrict(request.user, "add").get(pk=new_group.pk)
2767
-
2768
- add_to_groups.append(new_group)
2769
- msg = "Created dynamic group"
2770
- logger.info(f"{msg} {new_group} (PK: {new_group.pk})")
2771
- msg = format_html('{} <a href="{}">{}</a>', msg, new_group.get_absolute_url(), new_group)
2772
- messages.success(self.request, msg)
2773
-
2774
- with deferred_change_logging_for_bulk_operation():
2775
- associations = []
2776
- for pk in pk_list:
2777
- for dynamic_group in add_to_groups:
2778
- association, created = StaticGroupAssociation.objects.get_or_create(
2779
- dynamic_group=dynamic_group,
2780
- associated_object_type_id=content_type.id,
2781
- associated_object_id=pk,
2782
- )
2783
- association.validated_save()
2784
- associations.append(association)
2785
- if created:
2786
- logger.debug("Created %s", association)
2787
-
2788
- # Enforce object-level permissions
2789
- if self.queryset.filter(pk__in=[assoc.pk for assoc in associations]).count() != len(
2790
- associations
2791
- ):
2792
- raise StaticGroupAssociation.DoesNotExist
2793
-
2794
- if associations:
2795
- msg = (
2796
- f"Added {len(pk_list)} {model._meta.verbose_name_plural} "
2797
- f"to {len(add_to_groups)} dynamic group(s)."
2798
- )
2799
- logger.info(msg)
2800
- messages.success(self.request, msg)
2801
-
2802
- if form.cleaned_data["remove_from_groups"]:
2803
- for dynamic_group in form.cleaned_data["remove_from_groups"]:
2804
- (
2805
- StaticGroupAssociation.objects.restrict(request.user, "delete")
2806
- .filter(
2807
- dynamic_group=dynamic_group,
2808
- associated_object_type=content_type,
2809
- associated_object_id__in=pk_list,
2810
- )
2811
- .delete()
2812
- )
2813
-
2814
- msg = (
2815
- f"Removed {len(pk_list)} {model._meta.verbose_name_plural} from "
2816
- f"{len(form.cleaned_data['remove_from_groups'])} dynamic group(s)."
2817
- )
2818
- logger.info(msg)
2819
- messages.success(self.request, msg)
2820
- except ValidationError as e:
2821
- messages.error(self.request, e)
2822
- except ObjectDoesNotExist:
2823
- msg = "Static group association failed due to object-level permissions violation"
2824
- logger.warning(msg)
2825
- messages.error(self.request, msg)
2826
-
2827
- else:
2828
- logger.debug("Form validation failed")
2829
- messages.error(self.request, form.errors)
2830
-
2831
- return redirect(self.get_return_url(request))
2832
-
2833
-
2834
2902
  #
2835
2903
  # Custom statuses
2836
2904
  #
@@ -2955,33 +3023,33 @@ class WebhookUIViewSet(NautobotUIViewSet):
2955
3023
  serializer_class = serializers.WebhookSerializer
2956
3024
  table_class = tables.WebhookTable
2957
3025
 
2958
- object_detail_content = ObjectDetailContent(
3026
+ object_detail_content = object_detail.ObjectDetailContent(
2959
3027
  panels=[
2960
- ObjectFieldsPanel(
3028
+ object_detail.ObjectFieldsPanel(
2961
3029
  label="Webhook",
2962
3030
  section=SectionChoices.LEFT_HALF,
2963
3031
  weight=100,
2964
3032
  fields=("name", "content_types", "type_create", "type_update", "type_delete", "enabled"),
2965
3033
  ),
2966
- ObjectFieldsPanel(
3034
+ object_detail.ObjectFieldsPanel(
2967
3035
  label="HTTP",
2968
3036
  section=SectionChoices.LEFT_HALF,
2969
3037
  weight=100,
2970
3038
  fields=("http_method", "http_content_type", "payload_url", "additional_headers"),
2971
3039
  value_transforms={"additional_headers": [helpers.pre_tag]},
2972
3040
  ),
2973
- ObjectFieldsPanel(
3041
+ object_detail.ObjectFieldsPanel(
2974
3042
  label="Security",
2975
3043
  section=SectionChoices.LEFT_HALF,
2976
3044
  weight=100,
2977
3045
  fields=("secret", "ssl_verification", "ca_file_path"),
2978
3046
  ),
2979
- ObjectTextPanel(
3047
+ object_detail.ObjectTextPanel(
2980
3048
  label="Body Template",
2981
3049
  section=SectionChoices.RIGHT_HALF,
2982
3050
  weight=100,
2983
3051
  object_field="body_template",
2984
- render_as=BaseTextPanel.RenderOptions.CODE,
3052
+ render_as=object_detail.BaseTextPanel.RenderOptions.CODE,
2985
3053
  ),
2986
3054
  ]
2987
3055
  )