nautobot 2.2.9__py3-none-any.whl → 2.3.0b1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (697) hide show
  1. nautobot/apps/forms.py +4 -0
  2. nautobot/apps/models.py +10 -1
  3. nautobot/circuits/__init__.py +0 -1
  4. nautobot/circuits/apps.py +1 -0
  5. nautobot/circuits/factory.py +15 -3
  6. nautobot/circuits/filters.py +13 -0
  7. nautobot/circuits/forms.py +13 -0
  8. nautobot/circuits/migrations/0021_alter_circuit_status_alter_circuittermination__path.py +32 -0
  9. nautobot/circuits/migrations/0022_circuittermination_cloud_network.py +25 -0
  10. nautobot/circuits/models.py +16 -3
  11. nautobot/circuits/tables.py +16 -2
  12. nautobot/circuits/templates/circuits/circuittermination_create.html +10 -2
  13. nautobot/circuits/templates/circuits/circuittermination_retrieve.html +6 -0
  14. nautobot/circuits/templates/circuits/inc/circuit_termination.html +6 -1
  15. nautobot/circuits/tests/test_api.py +7 -5
  16. nautobot/circuits/tests/test_filters.py +12 -5
  17. nautobot/circuits/tests/test_models.py +33 -2
  18. nautobot/circuits/views.py +2 -3
  19. nautobot/cloud/__init__.py +0 -0
  20. nautobot/cloud/api/__init__.py +0 -0
  21. nautobot/cloud/api/serializers.py +54 -0
  22. nautobot/cloud/api/urls.py +16 -0
  23. nautobot/cloud/api/views.py +48 -0
  24. nautobot/cloud/apps.py +13 -0
  25. nautobot/cloud/factory.py +111 -0
  26. nautobot/cloud/filters.py +184 -0
  27. nautobot/cloud/forms.py +333 -0
  28. nautobot/cloud/homepage.py +43 -0
  29. nautobot/cloud/migrations/0001_initial.py +304 -0
  30. nautobot/cloud/migrations/__init__.py +0 -0
  31. nautobot/cloud/models.py +247 -0
  32. nautobot/cloud/navigation.py +85 -0
  33. nautobot/cloud/tables.py +173 -0
  34. nautobot/cloud/templates/cloud/cloudaccount_retrieve.html +43 -0
  35. nautobot/cloud/templates/cloud/cloudnetwork_retrieve.html +128 -0
  36. nautobot/cloud/templates/cloud/cloudnetwork_update.html +33 -0
  37. nautobot/cloud/templates/cloud/cloudresourcetype_retrieve.html +100 -0
  38. nautobot/cloud/templates/cloud/cloudservice_retrieve.html +65 -0
  39. nautobot/cloud/templates/cloud/cloudservice_update.html +25 -0
  40. nautobot/cloud/tests/__init__.py +0 -0
  41. nautobot/cloud/tests/test_api.py +248 -0
  42. nautobot/cloud/tests/test_filters.py +113 -0
  43. nautobot/cloud/tests/test_models.py +43 -0
  44. nautobot/cloud/tests/test_views.py +153 -0
  45. nautobot/cloud/urls.py +14 -0
  46. nautobot/cloud/views.py +181 -0
  47. nautobot/core/__init__.py +0 -3
  48. nautobot/core/api/metadata.py +1 -0
  49. nautobot/core/api/parsers.py +7 -1
  50. nautobot/core/api/urls.py +1 -0
  51. nautobot/core/api/utils.py +1 -0
  52. nautobot/core/api/views.py +4 -0
  53. nautobot/core/apps/__init__.py +6 -3
  54. nautobot/core/constants.py +8 -0
  55. nautobot/core/factory.py +32 -1
  56. nautobot/core/filters.py +96 -28
  57. nautobot/core/forms/fields.py +10 -4
  58. nautobot/core/forms/forms.py +1 -1
  59. nautobot/core/forms/widgets.py +18 -1
  60. nautobot/core/graphql/generators.py +2 -2
  61. nautobot/core/graphql/schema.py +34 -4
  62. nautobot/core/jobs/__init__.py +17 -6
  63. nautobot/core/jobs/cleanup.py +100 -0
  64. nautobot/core/jobs/groups.py +38 -0
  65. nautobot/core/management/commands/generate_test_data.py +116 -3
  66. nautobot/core/models/__init__.py +34 -9
  67. nautobot/core/models/generics.py +19 -3
  68. nautobot/core/models/name_color_content_types.py +7 -28
  69. nautobot/core/models/querysets.py +4 -3
  70. nautobot/core/models/tree_queries.py +1 -1
  71. nautobot/core/models/utils.py +21 -5
  72. nautobot/core/settings.py +4 -30
  73. nautobot/core/settings.yaml +34 -27
  74. nautobot/core/settings_funcs.py +103 -0
  75. nautobot/core/tables.py +127 -56
  76. nautobot/core/templates/admin/search_form.html +1 -1
  77. nautobot/core/templates/buttons/add.html +11 -3
  78. nautobot/core/templates/buttons/consolidated_bulk_action_buttons.html +13 -0
  79. nautobot/core/templates/buttons/consolidated_detail_view_action_buttons.html +13 -0
  80. nautobot/core/templates/buttons/export.html +101 -53
  81. nautobot/core/templates/buttons/job_import.html +11 -3
  82. nautobot/core/templates/generic/object_bulk_destroy.html +3 -1
  83. nautobot/core/templates/generic/object_bulk_update.html +3 -1
  84. nautobot/core/templates/generic/object_changelog.html +0 -9
  85. nautobot/core/templates/generic/object_list.html +156 -17
  86. nautobot/core/templates/generic/object_retrieve.html +80 -16
  87. nautobot/core/templates/inc/extras_features_edit_form_fields.html +8 -0
  88. nautobot/core/templates/inc/javascript.html +2 -0
  89. nautobot/core/templates/inc/media.html +2 -2
  90. nautobot/core/templates/inc/nav_menu.html +1 -0
  91. nautobot/core/templates/inc/paginator.html +7 -7
  92. nautobot/core/templates/inc/search_panel.html +2 -2
  93. nautobot/core/templates/inc/table.html +2 -2
  94. nautobot/core/templates/nautobot_config.py.j2 +13 -23
  95. nautobot/core/templates/utilities/templatetags/dynamic_group_assignment_modal.html +37 -0
  96. nautobot/core/templates/utilities/templatetags/filter_form_modal.html +2 -2
  97. nautobot/core/templates/utilities/templatetags/saved_view_modal.html +38 -0
  98. nautobot/core/templates/utilities/theme_preview.html +25 -8
  99. nautobot/core/templates/utilities/worker_status.html +152 -0
  100. nautobot/core/templatetags/buttons.py +335 -38
  101. nautobot/core/templatetags/form_helpers.py +1 -1
  102. nautobot/core/templatetags/helpers.py +181 -11
  103. nautobot/core/testing/api.py +5 -4
  104. nautobot/core/testing/filters.py +51 -13
  105. nautobot/core/testing/mixins.py +46 -0
  106. nautobot/core/testing/models.py +22 -0
  107. nautobot/core/testing/schema.py +4 -8
  108. nautobot/core/testing/views.py +31 -14
  109. nautobot/core/tests/integration/test_general_functionality.py +1 -1
  110. nautobot/core/tests/integration/test_import_objects_ui.py +1 -0
  111. nautobot/core/tests/integration/test_swagger.py +1 -1
  112. nautobot/core/tests/nautobot_config.py +0 -1
  113. nautobot/core/tests/runner.py +2 -2
  114. nautobot/core/tests/test_api.py +1 -0
  115. nautobot/core/tests/test_authentication.py +7 -2
  116. nautobot/core/tests/test_filters.py +11 -9
  117. nautobot/core/tests/test_forms.py +9 -0
  118. nautobot/core/tests/test_graphql.py +27 -16
  119. nautobot/core/tests/test_jobs.py +123 -74
  120. nautobot/core/tests/test_tables.py +3 -1
  121. nautobot/core/tests/test_templatetags_helpers.py +12 -5
  122. nautobot/core/tests/test_utils.py +31 -20
  123. nautobot/core/tests/test_views.py +6 -6
  124. nautobot/core/urls.py +8 -3
  125. nautobot/core/utils/deprecation.py +29 -0
  126. nautobot/core/utils/filtering.py +12 -9
  127. nautobot/core/utils/lookup.py +37 -2
  128. nautobot/core/utils/requests.py +4 -1
  129. nautobot/core/views/__init__.py +137 -24
  130. nautobot/core/views/generic.py +118 -66
  131. nautobot/core/views/mixins.py +104 -35
  132. nautobot/core/views/paginator.py +9 -3
  133. nautobot/core/views/renderers.py +121 -56
  134. nautobot/core/views/utils.py +79 -1
  135. nautobot/dcim/__init__.py +0 -1
  136. nautobot/dcim/api/serializers.py +180 -44
  137. nautobot/dcim/api/urls.py +7 -3
  138. nautobot/dcim/api/views.py +53 -7
  139. nautobot/dcim/apps.py +3 -0
  140. nautobot/dcim/choices.py +25 -0
  141. nautobot/dcim/constants.py +7 -0
  142. nautobot/dcim/factory.py +249 -18
  143. nautobot/dcim/filters/__init__.py +369 -193
  144. nautobot/dcim/filters/mixins.py +274 -1
  145. nautobot/dcim/forms.py +817 -109
  146. nautobot/dcim/graphql/types.py +2 -2
  147. nautobot/dcim/homepage.py +1 -1
  148. nautobot/dcim/migrations/0059_add_role_field_to_interface_models.py +27 -0
  149. nautobot/dcim/migrations/0060_alter_cable_status_alter_consoleport__path_and_more.py +303 -0
  150. nautobot/dcim/migrations/0061_module_models.py +861 -0
  151. nautobot/dcim/migrations/0062_module_data_migration.py +25 -0
  152. nautobot/dcim/models/__init__.py +8 -0
  153. nautobot/dcim/models/cables.py +15 -0
  154. nautobot/dcim/models/device_component_templates.py +207 -53
  155. nautobot/dcim/models/device_components.py +275 -106
  156. nautobot/dcim/models/devices.py +466 -13
  157. nautobot/dcim/navigation.py +47 -0
  158. nautobot/dcim/signals.py +3 -3
  159. nautobot/dcim/tables/__init__.py +35 -23
  160. nautobot/dcim/tables/devices.py +231 -59
  161. nautobot/dcim/tables/devicetypes.py +65 -9
  162. nautobot/dcim/tables/racks.py +5 -1
  163. nautobot/dcim/tables/template_code.py +46 -26
  164. nautobot/dcim/templates/dcim/cable_connect.html +76 -3
  165. nautobot/dcim/templates/dcim/console_port_connection_list.html +7 -5
  166. nautobot/dcim/templates/dcim/device/base.html +15 -7
  167. nautobot/dcim/templates/dcim/device/consoleports.html +2 -3
  168. nautobot/dcim/templates/dcim/device/consoleserverports.html +2 -3
  169. nautobot/dcim/templates/dcim/device/devicebays.html +6 -7
  170. nautobot/dcim/templates/dcim/device/frontports.html +2 -3
  171. nautobot/dcim/templates/dcim/device/interfaces.html +2 -3
  172. nautobot/dcim/templates/dcim/device/inventory.html +2 -3
  173. nautobot/dcim/templates/dcim/device/modulebays.html +49 -0
  174. nautobot/dcim/templates/dcim/device/poweroutlets.html +2 -3
  175. nautobot/dcim/templates/dcim/device/powerports.html +2 -3
  176. nautobot/dcim/templates/dcim/device/rearports.html +2 -3
  177. nautobot/dcim/templates/dcim/device.html +45 -1
  178. nautobot/dcim/templates/dcim/device_component.html +13 -5
  179. nautobot/dcim/templates/dcim/device_list.html +2 -1
  180. nautobot/dcim/templates/dcim/deviceredundancygroup_retrieve.html +0 -6
  181. nautobot/dcim/templates/dcim/devicetype.html +99 -98
  182. nautobot/dcim/templates/dcim/devicetype_list.html +8 -16
  183. nautobot/dcim/templates/dcim/inc/devicetype_component_table.html +1 -1
  184. nautobot/dcim/templates/dcim/inc/moduletype_component_table.html +39 -0
  185. nautobot/dcim/templates/dcim/interface.html +17 -2
  186. nautobot/dcim/templates/dcim/interface_connection_list.html +7 -5
  187. nautobot/dcim/templates/dcim/interface_edit.html +1 -0
  188. nautobot/dcim/templates/dcim/manufacturer.html +24 -0
  189. nautobot/dcim/templates/dcim/module/base.html +97 -0
  190. nautobot/dcim/templates/dcim/module_bulk_destroy.html +5 -0
  191. nautobot/dcim/templates/dcim/module_consoleports.html +53 -0
  192. nautobot/dcim/templates/dcim/module_consoleserverports.html +53 -0
  193. nautobot/dcim/templates/dcim/module_destroy.html +5 -0
  194. nautobot/dcim/templates/dcim/module_frontports.html +53 -0
  195. nautobot/dcim/templates/dcim/module_interfaces.html +57 -0
  196. nautobot/dcim/templates/dcim/module_list.html +20 -0
  197. nautobot/dcim/templates/dcim/module_modulebays.html +49 -0
  198. nautobot/dcim/templates/dcim/module_poweroutlets.html +53 -0
  199. nautobot/dcim/templates/dcim/module_powerports.html +53 -0
  200. nautobot/dcim/templates/dcim/module_rearports.html +53 -0
  201. nautobot/dcim/templates/dcim/module_retrieve.html +63 -0
  202. nautobot/dcim/templates/dcim/module_update.html +71 -0
  203. nautobot/dcim/templates/dcim/modulebay_bulk_destroy.html +5 -0
  204. nautobot/dcim/templates/dcim/modulebay_destroy.html +8 -0
  205. nautobot/dcim/templates/dcim/modulebay_retrieve.html +101 -0
  206. nautobot/dcim/templates/dcim/moduletype_list.html +11 -0
  207. nautobot/dcim/templates/dcim/moduletype_retrieve.html +142 -0
  208. nautobot/dcim/templates/dcim/power_port_connection_list.html +7 -5
  209. nautobot/dcim/templates/dcim/softwareimagefile_retrieve.html +65 -19
  210. nautobot/dcim/tests/integration/test_cable_connect_form.py +4 -4
  211. nautobot/dcim/tests/test_api.py +691 -208
  212. nautobot/dcim/tests/test_filters.py +836 -217
  213. nautobot/dcim/tests/test_models.py +1072 -39
  214. nautobot/dcim/tests/test_views.py +1488 -358
  215. nautobot/dcim/urls.py +17 -2
  216. nautobot/dcim/utils.py +2 -3
  217. nautobot/dcim/views.py +1107 -120
  218. nautobot/extras/__init__.py +0 -1
  219. nautobot/extras/api/serializers.py +115 -3
  220. nautobot/extras/api/urls.py +12 -0
  221. nautobot/extras/api/views.py +125 -7
  222. nautobot/extras/apps.py +2 -2
  223. nautobot/extras/choices.py +43 -0
  224. nautobot/extras/context_managers.py +13 -8
  225. nautobot/extras/datasources/git.py +2 -0
  226. nautobot/extras/factory.py +422 -9
  227. nautobot/extras/filters/__init__.py +174 -3
  228. nautobot/extras/filters/mixins.py +46 -43
  229. nautobot/extras/forms/base.py +17 -4
  230. nautobot/extras/forms/forms.py +227 -8
  231. nautobot/extras/forms/mixins.py +93 -0
  232. nautobot/extras/graphql/types.py +23 -10
  233. nautobot/extras/homepage.py +16 -13
  234. nautobot/extras/jobs.py +2 -2
  235. nautobot/extras/management/__init__.py +1 -0
  236. nautobot/extras/management/commands/refresh_dynamic_group_member_caches.py +1 -16
  237. nautobot/extras/migrations/0021_customfield_changelog_data.py +1 -0
  238. nautobot/extras/migrations/0109_dynamicgroup_group_type_dynamicgroup_tags_and_more.py +108 -0
  239. nautobot/extras/migrations/0110_alter_configcontext_cluster_groups_and_more.py +111 -0
  240. nautobot/extras/migrations/0111_metadata.py +162 -0
  241. nautobot/extras/migrations/0112_dynamic_group_group_type_data_migration.py +28 -0
  242. nautobot/extras/migrations/0113_saved_views.py +77 -0
  243. nautobot/extras/models/__init__.py +15 -1
  244. nautobot/extras/models/change_logging.py +3 -3
  245. nautobot/extras/models/contacts.py +4 -0
  246. nautobot/extras/models/customfields.py +18 -3
  247. nautobot/extras/models/groups.py +389 -225
  248. nautobot/extras/models/jobs.py +4 -84
  249. nautobot/extras/models/metadata.py +441 -0
  250. nautobot/extras/models/mixins.py +72 -62
  251. nautobot/extras/models/models.py +116 -9
  252. nautobot/extras/models/relationships.py +9 -2
  253. nautobot/extras/models/tags.py +13 -2
  254. nautobot/extras/navigation.py +57 -0
  255. nautobot/extras/plugins/__init__.py +3 -1
  256. nautobot/extras/querysets.py +30 -66
  257. nautobot/extras/signals.py +96 -114
  258. nautobot/extras/tables.py +171 -47
  259. nautobot/extras/templates/extras/dynamicgroup.html +44 -15
  260. nautobot/extras/templates/extras/dynamicgroup_edit.html +2 -0
  261. nautobot/extras/templates/extras/job.html +1 -1
  262. nautobot/extras/templates/extras/job_detail.html +0 -11
  263. nautobot/extras/templates/extras/jobresult.html +61 -74
  264. nautobot/extras/templates/extras/metadatatype_create.html +89 -0
  265. nautobot/extras/templates/extras/metadatatype_retrieve.html +67 -0
  266. nautobot/extras/templates/extras/object_dynamicgroups.html +7 -0
  267. nautobot/extras/templates/extras/objectchange_list.html +0 -12
  268. nautobot/extras/templates/extras/plugins_list.html +1 -3
  269. nautobot/extras/templates/extras/role_retrieve.html +48 -0
  270. nautobot/extras/templates/extras/staticgroupassociation_retrieve.html +20 -0
  271. nautobot/extras/tests/integration/test_customfields.py +1 -0
  272. nautobot/extras/tests/test_api.py +501 -22
  273. nautobot/extras/tests/test_changelog.py +20 -9
  274. nautobot/extras/tests/test_context_managers.py +22 -15
  275. nautobot/extras/tests/test_datasources.py +13 -1
  276. nautobot/extras/tests/test_dynamicgroups.py +201 -171
  277. nautobot/extras/tests/test_filters.py +211 -12
  278. nautobot/extras/tests/test_jobs.py +4 -4
  279. nautobot/extras/tests/test_models.py +499 -4
  280. nautobot/extras/tests/test_relationships.py +1 -0
  281. nautobot/extras/tests/test_views.py +565 -28
  282. nautobot/extras/tests/test_webhooks.py +1 -1
  283. nautobot/extras/urls.py +5 -0
  284. nautobot/extras/utils.py +56 -45
  285. nautobot/extras/views.py +585 -96
  286. nautobot/ipam/__init__.py +0 -1
  287. nautobot/ipam/apps.py +1 -0
  288. nautobot/ipam/factory.py +17 -19
  289. nautobot/ipam/filters.py +14 -1
  290. nautobot/ipam/forms.py +9 -5
  291. nautobot/ipam/graphql/types.py +2 -2
  292. nautobot/ipam/migrations/0047_alter_ipaddress_role_alter_ipaddress_status_and_more.py +59 -0
  293. nautobot/ipam/models.py +23 -9
  294. nautobot/ipam/querysets.py +1 -1
  295. nautobot/ipam/signals.py +4 -2
  296. nautobot/ipam/tables.py +1 -0
  297. nautobot/ipam/templates/ipam/ipaddress_interfaces.html +1 -1
  298. nautobot/ipam/templates/ipam/ipaddress_vm_interfaces.html +1 -1
  299. nautobot/ipam/templates/ipam/prefix.html +1 -0
  300. nautobot/ipam/tests/test_api.py +37 -18
  301. nautobot/ipam/tests/test_filters.py +26 -2
  302. nautobot/ipam/tests/test_models.py +8 -3
  303. nautobot/ipam/tests/test_querysets.py +1 -1
  304. nautobot/ipam/tests/test_views.py +3 -2
  305. nautobot/ipam/urls.py +2 -2
  306. nautobot/ipam/views.py +25 -28
  307. nautobot/project-static/css/base.css +20 -1
  308. nautobot/project-static/css/dark.css +11 -0
  309. nautobot/project-static/docs/404.html +884 -80
  310. nautobot/project-static/docs/apps/index.html +884 -80
  311. nautobot/project-static/docs/apps/nautobot-apps.html +884 -80
  312. nautobot/project-static/docs/assets/_mkdocstrings.css +5 -0
  313. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +911 -112
  314. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +896 -93
  315. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +1457 -790
  316. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +927 -136
  317. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +969 -180
  318. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +893 -91
  319. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +889 -85
  320. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +983 -185
  321. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +938 -143
  322. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +1064 -274
  323. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +1190 -346
  324. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +1663 -865
  325. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +1156 -373
  326. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +2200 -1502
  327. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +2229 -1421
  328. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +904 -103
  329. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +955 -155
  330. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +1002 -215
  331. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +1911 -1275
  332. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +1835 -1091
  333. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +896 -93
  334. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +2323 -1693
  335. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +1785 -1023
  336. nautobot/project-static/docs/development/apps/api/configuration-view.html +884 -80
  337. nautobot/project-static/docs/development/apps/api/database-backend-config.html +884 -80
  338. nautobot/project-static/docs/development/apps/api/models/django-admin.html +884 -80
  339. nautobot/project-static/docs/development/apps/api/models/global-search.html +884 -80
  340. nautobot/project-static/docs/development/apps/api/models/graphql.html +884 -80
  341. nautobot/project-static/docs/development/apps/api/models/index.html +922 -81
  342. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +884 -80
  343. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +884 -80
  344. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +884 -80
  345. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +884 -80
  346. nautobot/project-static/docs/development/apps/api/platform-features/index.html +884 -80
  347. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +884 -80
  348. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +884 -80
  349. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +884 -80
  350. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +884 -80
  351. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +884 -80
  352. nautobot/project-static/docs/development/apps/api/prometheus.html +884 -80
  353. nautobot/project-static/docs/development/apps/api/setup.html +884 -80
  354. nautobot/project-static/docs/development/apps/api/testing.html +884 -80
  355. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +884 -80
  356. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +884 -80
  357. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +884 -80
  358. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +884 -80
  359. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +884 -80
  360. nautobot/project-static/docs/development/apps/api/views/base-template.html +884 -80
  361. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +884 -80
  362. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +884 -80
  363. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +884 -80
  364. nautobot/project-static/docs/development/apps/api/views/index.html +884 -80
  365. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +884 -80
  366. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +884 -80
  367. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +884 -80
  368. nautobot/project-static/docs/development/apps/api/views/notes.html +884 -80
  369. nautobot/project-static/docs/development/apps/api/views/rest-api.html +884 -80
  370. nautobot/project-static/docs/development/apps/api/views/urls.html +884 -80
  371. nautobot/project-static/docs/development/apps/index.html +884 -80
  372. nautobot/project-static/docs/development/apps/migration/code-updates.html +884 -80
  373. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +884 -80
  374. nautobot/project-static/docs/development/apps/migration/from-v1.html +884 -80
  375. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +884 -80
  376. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +884 -80
  377. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +884 -80
  378. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +884 -80
  379. nautobot/project-static/docs/development/apps/porting-from-netbox.html +884 -80
  380. nautobot/project-static/docs/development/core/application-registry.html +884 -80
  381. nautobot/project-static/docs/development/core/best-practices.html +885 -80
  382. nautobot/project-static/docs/development/core/bootstrap-ui.html +884 -80
  383. nautobot/project-static/docs/development/core/caching.html +884 -80
  384. nautobot/project-static/docs/development/core/controllers.html +884 -80
  385. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +884 -80
  386. nautobot/project-static/docs/development/core/generic-views.html +884 -80
  387. nautobot/project-static/docs/development/core/getting-started.html +884 -80
  388. nautobot/project-static/docs/development/core/homepage.html +884 -80
  389. nautobot/project-static/docs/development/core/index.html +884 -91
  390. nautobot/project-static/docs/development/core/model-checklist.html +887 -81
  391. nautobot/project-static/docs/development/core/model-features.html +884 -80
  392. nautobot/project-static/docs/development/core/natural-keys.html +884 -80
  393. nautobot/project-static/docs/development/core/navigation-menu.html +884 -80
  394. nautobot/project-static/docs/development/core/release-checklist.html +887 -83
  395. nautobot/project-static/docs/development/core/role-internals.html +884 -80
  396. nautobot/project-static/docs/development/core/settings.html +884 -80
  397. nautobot/project-static/docs/development/core/style-guide.html +885 -81
  398. nautobot/project-static/docs/development/core/templates.html +896 -81
  399. nautobot/project-static/docs/development/core/testing.html +884 -80
  400. nautobot/project-static/docs/development/core/user-preferences.html +884 -80
  401. nautobot/project-static/docs/development/index.html +884 -80
  402. nautobot/project-static/docs/development/jobs/index.html +1247 -457
  403. nautobot/project-static/docs/development/jobs/migration/from-v1.html +884 -80
  404. nautobot/project-static/docs/index.html +13 -8228
  405. nautobot/project-static/docs/media/models/cloud_aws_direct_connect_dark.png +0 -0
  406. nautobot/project-static/docs/media/models/cloud_aws_direct_connect_light.png +0 -0
  407. nautobot/project-static/docs/models/cloud/cloudaccount.html +15 -0
  408. nautobot/project-static/docs/models/cloud/cloudnetwork.html +15 -0
  409. nautobot/project-static/docs/models/cloud/cloudnetworkprefixassignment.html +15 -0
  410. nautobot/project-static/docs/models/cloud/cloudresourcetype.html +15 -0
  411. nautobot/project-static/docs/models/cloud/cloudservice.html +15 -0
  412. nautobot/project-static/docs/models/cloud/cloudservicenetworkassignment.html +15 -0
  413. nautobot/project-static/docs/models/dcim/module.html +15 -0
  414. nautobot/project-static/docs/models/dcim/modulebay.html +15 -0
  415. nautobot/project-static/docs/models/dcim/modulebaytemplate.html +15 -0
  416. nautobot/project-static/docs/models/dcim/moduletype.html +15 -0
  417. nautobot/project-static/docs/models/extras/metadatachoice.html +15 -0
  418. nautobot/project-static/docs/models/extras/metadatatype.html +15 -0
  419. nautobot/project-static/docs/models/extras/objectmetadata.html +15 -0
  420. nautobot/project-static/docs/models/extras/role.html +15 -0
  421. nautobot/project-static/docs/models/extras/savedview.html +15 -0
  422. nautobot/project-static/docs/models/extras/staticgroupassociation.html +15 -0
  423. nautobot/project-static/docs/models/extras/status.html +15 -0
  424. nautobot/project-static/docs/objects.inv +0 -0
  425. nautobot/project-static/docs/overview/application_stack.html +892 -81
  426. nautobot/project-static/docs/overview/design_philosophy.html +886 -82
  427. nautobot/project-static/docs/overview/index.html +9032 -13
  428. nautobot/project-static/docs/release-notes/index.html +887 -83
  429. nautobot/project-static/docs/release-notes/version-1.0.html +884 -80
  430. nautobot/project-static/docs/release-notes/version-1.1.html +884 -80
  431. nautobot/project-static/docs/release-notes/version-1.2.html +884 -80
  432. nautobot/project-static/docs/release-notes/version-1.3.html +884 -80
  433. nautobot/project-static/docs/release-notes/version-1.4.html +884 -80
  434. nautobot/project-static/docs/release-notes/version-1.5.html +885 -81
  435. nautobot/project-static/docs/release-notes/version-1.6.html +885 -81
  436. nautobot/project-static/docs/release-notes/version-2.0.html +884 -80
  437. nautobot/project-static/docs/release-notes/version-2.1.html +884 -80
  438. nautobot/project-static/docs/release-notes/version-2.2.html +990 -323
  439. nautobot/project-static/docs/release-notes/version-2.3.html +9524 -0
  440. nautobot/project-static/docs/requirements.txt +4 -4
  441. nautobot/project-static/docs/search/search_index.json +1 -1
  442. nautobot/project-static/docs/sitemap.xml +335 -260
  443. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  444. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +884 -80
  445. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +884 -80
  446. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +884 -80
  447. nautobot/project-static/docs/user-guide/administration/configuration/index.html +884 -80
  448. nautobot/project-static/docs/user-guide/administration/configuration/optional-settings.html +983 -197
  449. nautobot/project-static/docs/user-guide/administration/configuration/required-settings.html +884 -80
  450. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +884 -80
  451. nautobot/project-static/docs/user-guide/administration/guides/caching.html +884 -80
  452. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +888 -84
  453. nautobot/project-static/docs/user-guide/administration/guides/healthcheck.html +884 -80
  454. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +884 -80
  455. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +884 -80
  456. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +884 -80
  457. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +884 -80
  458. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +884 -80
  459. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +884 -80
  460. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +884 -80
  461. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +884 -80
  462. nautobot/project-static/docs/user-guide/administration/installation/index.html +888 -80
  463. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +884 -80
  464. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +884 -80
  465. nautobot/project-static/docs/user-guide/administration/installation/services.html +888 -80
  466. nautobot/project-static/docs/user-guide/administration/installation-extras/docker.html +900 -91
  467. nautobot/project-static/docs/user-guide/administration/installation-extras/health-checks.html +884 -80
  468. nautobot/project-static/docs/user-guide/administration/installation-extras/selinux-troubleshooting.html +884 -80
  469. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +884 -80
  470. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +884 -80
  471. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +915 -163
  472. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +884 -80
  473. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +884 -80
  474. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +884 -80
  475. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +884 -80
  476. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +884 -80
  477. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +884 -80
  478. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +884 -80
  479. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +884 -80
  480. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +884 -80
  481. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +884 -80
  482. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +884 -80
  483. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +885 -81
  484. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +884 -80
  485. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +888 -80
  486. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +887 -83
  487. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +8984 -0
  488. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +8828 -0
  489. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +8829 -0
  490. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +8828 -0
  491. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +8829 -0
  492. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +8833 -0
  493. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +8828 -0
  494. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +898 -94
  495. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +915 -97
  496. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +915 -97
  497. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +910 -92
  498. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +915 -97
  499. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +898 -94
  500. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +898 -94
  501. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +905 -97
  502. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +912 -108
  503. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +913 -109
  504. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +910 -106
  505. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +898 -94
  506. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +906 -97
  507. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +918 -100
  508. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +928 -110
  509. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +920 -98
  510. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +898 -94
  511. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +929 -111
  512. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +920 -102
  513. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +910 -106
  514. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +913 -109
  515. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +914 -106
  516. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +8828 -0
  517. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +8846 -0
  518. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +8843 -0
  519. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +8823 -0
  520. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +908 -104
  521. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +898 -94
  522. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +932 -75
  523. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +916 -98
  524. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +898 -94
  525. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +935 -78
  526. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +913 -95
  527. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +921 -117
  528. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +910 -106
  529. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +898 -94
  530. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +914 -96
  531. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +916 -98
  532. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +898 -94
  533. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +898 -94
  534. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +898 -94
  535. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +884 -80
  536. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +884 -80
  537. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +889 -81
  538. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +889 -81
  539. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +884 -80
  540. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +884 -80
  541. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +884 -80
  542. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +884 -80
  543. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +884 -80
  544. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +884 -80
  545. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +884 -80
  546. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +884 -80
  547. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +884 -80
  548. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +884 -80
  549. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +893 -88
  550. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +884 -80
  551. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +884 -80
  552. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +884 -80
  553. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +884 -80
  554. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +884 -80
  555. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +889 -81
  556. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +884 -80
  557. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +884 -80
  558. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +884 -80
  559. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +884 -80
  560. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +884 -80
  561. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +884 -80
  562. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +884 -80
  563. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +884 -80
  564. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +884 -80
  565. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +884 -80
  566. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +884 -80
  567. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +884 -80
  568. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +884 -80
  569. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/clear-view-button.png +0 -0
  570. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/cleared-view.png +0 -0
  571. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/config-table-columns-to-locations.png +0 -0
  572. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/configure-button.png +0 -0
  573. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/create-saved-view-success.png +0 -0
  574. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/current-saved-view-drop-down-menu.png +0 -0
  575. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/default-location-list-view.png +0 -0
  576. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/dropdown-button-after-new-saved-view.png +0 -0
  577. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/filter-application-to-locations.png +0 -0
  578. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/filter-button.png +0 -0
  579. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/global-default-location-list-view.png +0 -0
  580. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/location-list-view-with-saved-views.png +0 -0
  581. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/navigation-menu.png +0 -0
  582. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/save-as-new-view-drop-down.png +0 -0
  583. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/save-view-modal.png +0 -0
  584. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-buttons.png +0 -0
  585. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-success.png +0 -0
  586. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-view-unchecked.png +0 -0
  587. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-view.png +0 -0
  588. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-different-user.png +0 -0
  589. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-modal-unchecked.png +0 -0
  590. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/set-as-my-default-button.png +0 -0
  591. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/set-as-my-default-success.png +0 -0
  592. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/unsaved-saved-view.png +0 -0
  593. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/updated-saved-view.png +0 -0
  594. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +884 -80
  595. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +884 -80
  596. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +884 -80
  597. nautobot/project-static/docs/user-guide/index.html +884 -80
  598. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +884 -80
  599. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +884 -80
  600. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +884 -80
  601. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +884 -80
  602. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +1250 -777
  603. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +887 -83
  604. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +884 -80
  605. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +884 -80
  606. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +884 -80
  607. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +884 -80
  608. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +884 -80
  609. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +884 -80
  610. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +887 -83
  611. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +884 -80
  612. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +884 -80
  613. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +888 -80
  614. nautobot/project-static/docs/user-guide/platform-functionality/metadata.html +8948 -0
  615. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +887 -83
  616. nautobot/project-static/docs/user-guide/platform-functionality/note.html +884 -80
  617. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +884 -80
  618. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +884 -80
  619. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +884 -80
  620. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +884 -80
  621. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +884 -80
  622. nautobot/project-static/docs/user-guide/platform-functionality/role.html +887 -83
  623. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +9137 -0
  624. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +887 -83
  625. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +8933 -0
  626. nautobot/project-static/docs/user-guide/platform-functionality/status.html +884 -80
  627. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +884 -80
  628. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +942 -113
  629. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +884 -80
  630. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +884 -80
  631. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +884 -80
  632. nautobot/project-static/js/forms.js +71 -0
  633. nautobot/project-static/js/table_sorting_indicator.js +46 -0
  634. nautobot/project-static/js/tableconfig.js +6 -1
  635. nautobot/project-static/materialdesignicons-7.4.47/css/materialdesignicons.min.css +3 -0
  636. nautobot/project-static/{materialdesignicons-6.5.95 → materialdesignicons-7.4.47}/fonts/materialdesignicons-webfont.eot +0 -0
  637. nautobot/project-static/{materialdesignicons-6.5.95 → materialdesignicons-7.4.47}/fonts/materialdesignicons-webfont.ttf +0 -0
  638. nautobot/project-static/materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.woff +0 -0
  639. nautobot/project-static/materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.woff2 +0 -0
  640. nautobot/tenancy/__init__.py +0 -1
  641. nautobot/tenancy/apps.py +1 -0
  642. nautobot/tenancy/factory.py +3 -2
  643. nautobot/tenancy/filters/__init__.py +1 -0
  644. nautobot/tenancy/forms.py +1 -1
  645. nautobot/tenancy/templates/tenancy/tenant.html +22 -18
  646. nautobot/tenancy/views.py +11 -10
  647. nautobot/users/__init__.py +0 -1
  648. nautobot/users/api/serializers.py +1 -1
  649. nautobot/users/api/views.py +4 -2
  650. nautobot/users/apps.py +3 -2
  651. nautobot/users/factory.py +3 -3
  652. nautobot/users/migrations/0010_user_default_saved_views.py +20 -0
  653. nautobot/users/models.py +12 -0
  654. nautobot/users/tests/test_filters.py +6 -3
  655. nautobot/users/urls.py +8 -0
  656. nautobot/virtualization/__init__.py +0 -1
  657. nautobot/virtualization/apps.py +1 -0
  658. nautobot/virtualization/filters.py +6 -1
  659. nautobot/virtualization/forms.py +11 -3
  660. nautobot/virtualization/graphql/types.py +2 -2
  661. nautobot/virtualization/migrations/0029_add_role_field_to_interface_models.py +27 -0
  662. nautobot/virtualization/migrations/0030_alter_virtualmachine_local_config_context_data_owner_content_type_and_more.py +67 -0
  663. nautobot/virtualization/tables.py +15 -5
  664. nautobot/virtualization/templates/virtualization/virtualmachine.html +1 -1
  665. nautobot/virtualization/templates/virtualization/vminterface.html +7 -1
  666. nautobot/virtualization/templates/virtualization/vminterface_edit.html +1 -0
  667. nautobot/virtualization/tests/test_api.py +9 -4
  668. nautobot/virtualization/tests/test_filters.py +22 -0
  669. nautobot/virtualization/tests/test_models.py +7 -3
  670. nautobot/virtualization/tests/test_views.py +19 -3
  671. nautobot/virtualization/urls.py +2 -2
  672. nautobot/virtualization/views.py +10 -32
  673. {nautobot-2.2.9.dist-info → nautobot-2.3.0b1.dist-info}/METADATA +21 -19
  674. {nautobot-2.2.9.dist-info → nautobot-2.3.0b1.dist-info}/RECORD +679 -559
  675. nautobot/project-static/materialdesignicons-6.5.95/.github/ISSUE_TEMPLATE.md +0 -3
  676. nautobot/project-static/materialdesignicons-6.5.95/README.md +0 -25
  677. nautobot/project-static/materialdesignicons-6.5.95/css/materialdesignicons.css +0 -26654
  678. nautobot/project-static/materialdesignicons-6.5.95/css/materialdesignicons.css.map +0 -16
  679. nautobot/project-static/materialdesignicons-6.5.95/css/materialdesignicons.min.css +0 -3
  680. nautobot/project-static/materialdesignicons-6.5.95/css/materialdesignicons.min.css.map +0 -16
  681. nautobot/project-static/materialdesignicons-6.5.95/fonts/materialdesignicons-webfont.woff +0 -0
  682. nautobot/project-static/materialdesignicons-6.5.95/fonts/materialdesignicons-webfont.woff2 +0 -0
  683. nautobot/project-static/materialdesignicons-6.5.95/package.json +0 -28
  684. nautobot/project-static/materialdesignicons-6.5.95/preview.html +0 -717
  685. nautobot/project-static/materialdesignicons-6.5.95/scss/_animated.scss +0 -27
  686. nautobot/project-static/materialdesignicons-6.5.95/scss/_core.scss +0 -10
  687. nautobot/project-static/materialdesignicons-6.5.95/scss/_extras.scss +0 -65
  688. nautobot/project-static/materialdesignicons-6.5.95/scss/_functions.scss +0 -20
  689. nautobot/project-static/materialdesignicons-6.5.95/scss/_icons.scss +0 -10
  690. nautobot/project-static/materialdesignicons-6.5.95/scss/_path.scss +0 -10
  691. nautobot/project-static/materialdesignicons-6.5.95/scss/_variables.scss +0 -6606
  692. nautobot/project-static/materialdesignicons-6.5.95/scss/materialdesignicons.scss +0 -8
  693. /nautobot/project-static/{materialdesignicons-6.5.95 → materialdesignicons-7.4.47}/LICENSE +0 -0
  694. {nautobot-2.2.9.dist-info → nautobot-2.3.0b1.dist-info}/LICENSE.txt +0 -0
  695. {nautobot-2.2.9.dist-info → nautobot-2.3.0b1.dist-info}/NOTICE +0 -0
  696. {nautobot-2.2.9.dist-info → nautobot-2.3.0b1.dist-info}/WHEEL +0 -0
  697. {nautobot-2.2.9.dist-info → nautobot-2.3.0b1.dist-info}/entry_points.txt +0 -0
nautobot/extras/views.py CHANGED
@@ -1,16 +1,20 @@
1
+ from datetime import timedelta
1
2
  import logging
3
+ from urllib.parse import parse_qs
2
4
 
3
5
  from celery import chain
4
6
  from django.contrib import messages
7
+ from django.contrib.auth.models import AnonymousUser
5
8
  from django.contrib.contenttypes.models import ContentType
6
9
  from django.core.exceptions import ObjectDoesNotExist, ValidationError
7
- from django.db import transaction
10
+ from django.db import IntegrityError, transaction
8
11
  from django.db.models import ProtectedError, Q
9
12
  from django.forms.utils import pretty_name
10
13
  from django.http import Http404, HttpResponse, HttpResponseForbidden
11
14
  from django.shortcuts import get_object_or_404, redirect, render
12
15
  from django.template.loader import get_template, TemplateDoesNotExist
13
16
  from django.urls import reverse
17
+ from django.urls.exceptions import NoReverseMatch
14
18
  from django.utils import timezone
15
19
  from django.utils.encoding import iri_to_uri
16
20
  from django.utils.html import format_html
@@ -18,38 +22,51 @@ from django.utils.http import url_has_allowed_host_and_scheme
18
22
  from django.views.generic import View
19
23
  from django_tables2 import RequestConfig
20
24
  from jsonschema.validators import Draft7Validator
25
+ from rest_framework.decorators import action
21
26
 
22
27
  from nautobot.core.forms import restrict_form_fields
23
28
  from nautobot.core.models.querysets import count_related
24
29
  from nautobot.core.models.utils import pretty_print_query
25
30
  from nautobot.core.tables import ButtonsColumn
26
- from nautobot.core.utils.lookup import get_table_for_model
31
+ from nautobot.core.utils.config import get_settings_or_config
32
+ from nautobot.core.utils.lookup import (
33
+ get_filterset_for_model,
34
+ get_route_for_model,
35
+ get_table_class_string_from_view_name,
36
+ get_table_for_model,
37
+ )
38
+ from nautobot.core.utils.permissions import get_permission_for_model
27
39
  from nautobot.core.utils.requests import normalize_querydict
28
40
  from nautobot.core.views import generic, viewsets
29
41
  from nautobot.core.views.mixins import (
42
+ GetReturnURLMixin,
30
43
  ObjectBulkDestroyViewMixin,
31
44
  ObjectBulkUpdateViewMixin,
45
+ ObjectChangeLogViewMixin,
32
46
  ObjectDestroyViewMixin,
47
+ ObjectDetailViewMixin,
33
48
  ObjectEditViewMixin,
49
+ ObjectListViewMixin,
34
50
  ObjectPermissionRequiredMixin,
35
51
  )
36
52
  from nautobot.core.views.paginator import EnhancedPaginator, get_paginate_count
37
53
  from nautobot.core.views.utils import prepare_cloned_fields
38
54
  from nautobot.core.views.viewsets import NautobotUIViewSet
39
- from nautobot.dcim.models import Controller, Device, Interface, Location, Rack
40
- from nautobot.dcim.tables import ControllerTable, DeviceTable, RackTable
55
+ from nautobot.dcim.models import Controller, Device, Interface, Module, Rack
56
+ from nautobot.dcim.tables import ControllerTable, DeviceTable, InterfaceTable, ModuleTable, RackTable
41
57
  from nautobot.extras.constants import JOB_OVERRIDABLE_FIELDS
58
+ from nautobot.extras.context_managers import deferred_change_logging_for_bulk_operation
42
59
  from nautobot.extras.signals import change_context_state
43
60
  from nautobot.extras.tasks import delete_custom_field_data
44
61
  from nautobot.extras.utils import get_base_template, get_worker_count
45
62
  from nautobot.ipam.models import IPAddress, Prefix, VLAN
46
63
  from nautobot.ipam.tables import IPAddressTable, PrefixTable, VLANTable
47
64
  from nautobot.virtualization.models import VirtualMachine, VMInterface
48
- from nautobot.virtualization.tables import VirtualMachineTable
65
+ from nautobot.virtualization.tables import VirtualMachineTable, VMInterfaceTable
49
66
 
50
67
  from . import filters, forms, tables
51
68
  from .api import serializers
52
- from .choices import JobExecutionType, JobResultStatusChoices, LogLevelChoices
69
+ from .choices import DynamicGroupTypeChoices, JobExecutionType, JobResultStatusChoices, LogLevelChoices
53
70
  from .datasources import (
54
71
  enqueue_git_repository_diff_origin_and_local,
55
72
  enqueue_pull_git_repository_and_refresh_data,
@@ -75,19 +92,24 @@ from .models import (
75
92
  JobHook,
76
93
  JobLogEntry,
77
94
  JobResult,
95
+ MetadataType,
78
96
  Note,
79
97
  ObjectChange,
98
+ ObjectMetadata,
80
99
  Relationship,
81
100
  RelationshipAssociation,
82
101
  Role,
102
+ SavedView,
83
103
  ScheduledJob,
84
104
  Secret,
85
105
  SecretsGroup,
86
106
  SecretsGroupAssociation,
107
+ StaticGroupAssociation,
87
108
  Status,
88
109
  Tag,
89
110
  TaggedItem,
90
111
  Team,
112
+ UserSavedViewAssociation,
91
113
  Webhook,
92
114
  )
93
115
  from .registry import registry
@@ -698,12 +720,19 @@ class DynamicGroupView(generic.ObjectView):
698
720
 
699
721
  def get_extra_context(self, request, instance):
700
722
  context = super().get_extra_context(request, instance)
701
- model = instance.content_type.model_class()
723
+ model = instance.model
702
724
  table_class = get_table_for_model(model)
703
725
 
726
+ if instance.group_type != DynamicGroupTypeChoices.TYPE_STATIC:
727
+ # Ensure that members cache is up-to-date for this specific group
728
+ members = instance.update_cached_members()
729
+ messages.success(request, f"Refreshed cached members list for {instance}")
730
+ else:
731
+ members = instance.members
732
+
704
733
  if table_class is not None:
705
734
  # Members table (for display on Members nav tab)
706
- members_table = table_class(instance.members.restrict(request.user, "view"), orderable=False)
735
+ members_table = table_class(members.restrict(request.user, "view"), orderable=False)
707
736
  paginate = {
708
737
  "paginator_class": EnhancedPaginator,
709
738
  "per_page": get_paginate_count(request),
@@ -723,7 +752,16 @@ class DynamicGroupView(generic.ObjectView):
723
752
  ancestors_table = tables.NestedDynamicGroupAncestorsTable(ancestors, orderable=False)
724
753
  ancestors_tree = instance.flatten_ancestors_tree(instance.ancestors_tree())
725
754
 
726
- context["raw_query"] = pretty_print_query(instance.generate_query())
755
+ if instance.group_type != DynamicGroupTypeChoices.TYPE_STATIC:
756
+ context["raw_query"] = pretty_print_query(instance.generate_query())
757
+ context["members_list_url"] = None
758
+ else:
759
+ context["raw_query"] = None
760
+ try:
761
+ context["members_list_url"] = reverse(get_route_for_model(instance.model, "list"))
762
+ except NoReverseMatch:
763
+ context["members_list_url"] = None
764
+ context["members_verbose_name_plural"] = instance.model._meta.verbose_name_plural
727
765
  context["members_table"] = members_table
728
766
  context["ancestors_table"] = ancestors_table
729
767
  context["ancestors_tree"] = ancestors_tree
@@ -775,16 +813,19 @@ class DynamicGroupEditView(generic.ObjectEditView):
775
813
  # Obtain the instance, but do not yet `save()` it to the database.
776
814
  obj = form.save(commit=False)
777
815
 
778
- # Process the filter form and save the query filters to `obj.filter`.
779
816
  ctx = self.get_extra_context(request, obj)
780
- filter_form = ctx["filter_form"]
781
- if filter_form.is_valid():
782
- obj.set_filter(filter_form.cleaned_data)
783
- else:
784
- raise RuntimeError(filter_form.errors)
817
+ if obj.group_type == DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER:
818
+ # Process the filter form and save the query filters to `obj.filter`.
819
+ filter_form = ctx["filter_form"]
820
+ if filter_form.is_valid():
821
+ obj.set_filter(filter_form.cleaned_data)
822
+ else:
823
+ raise RuntimeError(filter_form.errors)
785
824
 
786
825
  # After filters have been set, now we save the object to the database.
787
826
  obj.save()
827
+ # Save m2m fields, such as Tags https://docs.djangoproject.com/en/3.2/topics/forms/modelforms/#the-save-method
828
+ form.save_m2m()
788
829
  # Check that the new object conforms with any assigned object-level permissions
789
830
  self.queryset.get(pk=obj.pk)
790
831
 
@@ -867,6 +908,7 @@ class DynamicGroupBulkDeleteView(generic.BulkDeleteView):
867
908
  filterset = filters.DynamicGroupFilterSet
868
909
 
869
910
 
911
+ # 3.0 TODO: remove, deprecated since 2.3 (#5845)
870
912
  class ObjectDynamicGroupsView(generic.GenericView):
871
913
  """
872
914
  Present a list of dynamic groups associated to a particular object.
@@ -883,16 +925,18 @@ class ObjectDynamicGroupsView(generic.GenericView):
883
925
  obj = get_object_or_404(model, **kwargs)
884
926
 
885
927
  # Gather all dynamic groups for this object (and its related objects)
886
- dynamicsgroups_table = tables.DynamicGroupTable(
887
- data=obj.dynamic_groups_cached.restrict(request.user, "view"), orderable=False
928
+ dynamicgroups_table = tables.DynamicGroupTable(
929
+ data=obj.dynamic_groups.restrict(request.user, "view"), orderable=False
888
930
  )
931
+ dynamicgroups_table.columns.hide("content_type")
932
+ dynamicgroups_table.columns.hide("members")
889
933
 
890
934
  # Apply the request context
891
935
  paginate = {
892
936
  "paginator_class": EnhancedPaginator,
893
937
  "per_page": get_paginate_count(request),
894
938
  }
895
- RequestConfig(request, paginate).configure(dynamicsgroups_table)
939
+ RequestConfig(request, paginate).configure(dynamicgroups_table)
896
940
 
897
941
  self.base_template = get_base_template(self.base_template, model)
898
942
 
@@ -903,7 +947,7 @@ class ObjectDynamicGroupsView(generic.GenericView):
903
947
  "object": obj,
904
948
  "verbose_name": obj._meta.verbose_name,
905
949
  "verbose_name_plural": obj._meta.verbose_name_plural,
906
- "table": dynamicsgroups_table,
950
+ "table": dynamicgroups_table,
907
951
  "base_template": self.base_template,
908
952
  "active_tab": "dynamic-groups",
909
953
  },
@@ -1344,25 +1388,55 @@ class JobRunView(ObjectPermissionRequiredMixin, View):
1344
1388
  schedule_type = schedule_form.cleaned_data["_schedule_type"]
1345
1389
 
1346
1390
  if (not dryrun and job_model.approval_required) or schedule_type in JobExecutionType.SCHEDULE_CHOICES:
1347
- scheduled_job = ScheduledJob.create_schedule(
1348
- job_model,
1349
- request.user,
1350
- name=schedule_form.cleaned_data.get("_schedule_name"),
1351
- start_time=schedule_form.cleaned_data.get("_schedule_start_time"),
1391
+ crontab = ""
1392
+
1393
+ if schedule_type == JobExecutionType.TYPE_IMMEDIATELY:
1394
+ # The job must be approved.
1395
+ # If the schedule_type is immediate, we still create the task, but mark it for approval
1396
+ # as a once in the future task with the due date set to the current time. This means
1397
+ # when approval is granted, the task is immediately due for execution.
1398
+ schedule_type = JobExecutionType.TYPE_FUTURE
1399
+ schedule_datetime = timezone.now()
1400
+ schedule_name = f"{job_model} - {schedule_datetime}"
1401
+
1402
+ else:
1403
+ schedule_name = schedule_form.cleaned_data["_schedule_name"]
1404
+
1405
+ if schedule_type == JobExecutionType.TYPE_CUSTOM:
1406
+ crontab = schedule_form.cleaned_data["_recurrence_custom_time"]
1407
+ # doing .get("key", "default") returns None instead of "default" here for some reason
1408
+ schedule_datetime = schedule_form.cleaned_data.get("_schedule_start_time")
1409
+ if schedule_datetime is None:
1410
+ # "_schedule_start_time" is checked against ScheduledJob.earliest_possible_time()
1411
+ # which returns timezone.now() + timedelta(seconds=15)
1412
+ schedule_datetime = timezone.now() + timedelta(seconds=20)
1413
+ else:
1414
+ schedule_datetime = schedule_form.cleaned_data["_schedule_start_time"]
1415
+
1416
+ celery_kwargs = {"nautobot_job_profile": profile, "queue": task_queue}
1417
+ scheduled_job = ScheduledJob(
1418
+ name=schedule_name,
1419
+ task=job_model.class_path,
1420
+ job_model=job_model,
1421
+ start_time=schedule_datetime,
1422
+ description=f"Nautobot job {schedule_name} scheduled by {request.user} for {schedule_datetime}",
1423
+ kwargs=job_class.serialize_data(job_form.cleaned_data),
1424
+ celery_kwargs=celery_kwargs,
1352
1425
  interval=schedule_type,
1353
- crontab=schedule_form.cleaned_data.get("_recurrence_custom_time"),
1426
+ one_off=schedule_type == JobExecutionType.TYPE_FUTURE,
1427
+ queue=task_queue,
1428
+ user=request.user,
1354
1429
  approval_required=job_model.approval_required,
1355
- task_queue=task_queue,
1356
- profile=profile,
1357
- **job_class.serialize_data(job_form.cleaned_data),
1430
+ crontab=crontab,
1358
1431
  )
1432
+ scheduled_job.validated_save()
1359
1433
 
1360
1434
  if job_model.approval_required:
1361
- messages.success(request, f"Job {scheduled_job.name} successfully submitted for approval")
1362
- return redirect(return_url or "extras:scheduledjob_approval_queue_list")
1435
+ messages.success(request, f"Job {schedule_name} successfully submitted for approval")
1436
+ return redirect(return_url if return_url else "extras:scheduledjob_approval_queue_list")
1363
1437
  else:
1364
- messages.success(request, f"Job {scheduled_job.name} successfully scheduled")
1365
- return redirect(return_url or "extras:scheduledjob_list")
1438
+ messages.success(request, f"Job {schedule_name} successfully scheduled")
1439
+ return redirect(return_url if return_url else "extras:scheduledjob_list")
1366
1440
 
1367
1441
  else:
1368
1442
  # Enqueue job for immediate execution
@@ -1583,6 +1657,252 @@ class JobApprovalRequestView(generic.ObjectView):
1583
1657
  )
1584
1658
 
1585
1659
 
1660
+ #
1661
+ # Saved Views
1662
+ #
1663
+
1664
+
1665
+ class SavedViewUIViewSet(
1666
+ ObjectDetailViewMixin,
1667
+ ObjectChangeLogViewMixin,
1668
+ ObjectDestroyViewMixin,
1669
+ ObjectEditViewMixin,
1670
+ ObjectListViewMixin,
1671
+ ):
1672
+ queryset = SavedView.objects.all()
1673
+ form_class = forms.SavedViewForm
1674
+ filterset_class = filters.SavedViewFilterSet
1675
+ serializer_class = serializers.SavedViewSerializer
1676
+ table_class = tables.SavedViewTable
1677
+ action_buttons = ("export",)
1678
+
1679
+ def alter_queryset(self, request):
1680
+ """
1681
+ Two scenarios we need to handle here:
1682
+ 1. User can view all saved views with extras.view_savedview permission.
1683
+ 2. User without the permission can only view shared savedviews and his/her own saved views.
1684
+ """
1685
+ queryset = super().alter_queryset(request)
1686
+ user = request.user
1687
+ if user.has_perms(["extras.view_savedview"]):
1688
+ saved_views = queryset.restrict(user, "view")
1689
+ else:
1690
+ shared_saved_views = queryset.filter(is_shared=True)
1691
+ user_owned_saved_views = queryset.filter(owner=user)
1692
+ saved_views = shared_saved_views | user_owned_saved_views
1693
+ return saved_views
1694
+
1695
+ def get_queryset(self):
1696
+ """
1697
+ Get the list of items for this view.
1698
+ All users should be able to see saved views so we do not apply extra permissions.
1699
+ """
1700
+ return self.queryset.all()
1701
+
1702
+ def check_permissions(self, request):
1703
+ """
1704
+ Override this method to not check any permissions.
1705
+ Since users with <app_label>.view_<model_name> permissions should be able to view saved views related to this model.
1706
+ And those permissions will be enforced in the related view.
1707
+ """
1708
+
1709
+ def dispatch(self, request, *args, **kwargs):
1710
+ if isinstance(request.user, AnonymousUser):
1711
+ return self.handle_no_permission()
1712
+ return super().dispatch(request, *args, **kwargs)
1713
+
1714
+ def extra_message_context(self, obj):
1715
+ """
1716
+ Context variables for this extra message.
1717
+ """
1718
+ return {"new_global_default_view": obj}
1719
+
1720
+ def extra_message(self, **kwargs):
1721
+ new_global_default_view = kwargs.get("new_global_default_view")
1722
+ view_name = new_global_default_view.view
1723
+ message = ""
1724
+ if new_global_default_view.is_global_default:
1725
+ message += f"<br>The global default saved View for '{view_name}' is set to <a href='{new_global_default_view.get_absolute_url()}'>{new_global_default_view.name}</a>."
1726
+ return message
1727
+
1728
+ def list(self, request, *args, **kwargs):
1729
+ if not request.user.has_perms(["extras.view_savedview"]):
1730
+ return self.handle_no_permission()
1731
+ return super().list(request, *args, **kwargs)
1732
+
1733
+ def retrieve(self, request, *args, **kwargs):
1734
+ """
1735
+ The detail view for a saved view should the related ObjectListView with saved configurations applied
1736
+ """
1737
+ instance = self.get_object()
1738
+ list_view_url = reverse(instance.view) + f"?saved_view={instance.pk}"
1739
+ return redirect(list_view_url)
1740
+
1741
+ @action(detail=True, name="Set Default", methods=["get"], url_path="set-default", url_name="set_default")
1742
+ def set_default(self, request, *args, **kwargs):
1743
+ """
1744
+ Set current saved view as the the request.user default view. Overriding the global default view if there is one.
1745
+ """
1746
+ user = request.user
1747
+ sv = SavedView.objects.get(pk=kwargs.get("pk", None))
1748
+ UserSavedViewAssociation.objects.filter(user=user, view_name=sv.view).delete()
1749
+ UserSavedViewAssociation.objects.create(user=user, saved_view=sv, view_name=sv.view)
1750
+ list_view_url = sv.get_absolute_url()
1751
+ messages.success(
1752
+ request, f"Successfully set current view '{sv.name}' as the default '{sv.view}' view for user {user}"
1753
+ )
1754
+ return redirect(list_view_url)
1755
+
1756
+ @action(detail=True, name="Update Config", methods=["get"], url_path="update-config", url_name="update_config")
1757
+ def update_saved_view_config(self, request, *args, **kwargs):
1758
+ """
1759
+ Extract filter_params, pagination and sort_order from request.GET and apply it to the SavedView specified
1760
+ """
1761
+ sv = SavedView.objects.get(pk=kwargs.get("pk", None))
1762
+ if sv.owner == request.user or request.user.has_perms(["extras.change_savedview"]):
1763
+ pass
1764
+ else:
1765
+ messages.error(
1766
+ request, f"You do not have the required permission to modify this Saved View owned by {sv.owner}"
1767
+ )
1768
+ return redirect(self.get_return_url(request, obj=sv))
1769
+ table_changes_pending = request.GET.get("table_changes_pending", False)
1770
+ all_filters_removed = request.GET.get("all_filters_removed", False)
1771
+ pagination_count = request.GET.get("per_page", None)
1772
+ if pagination_count is not None:
1773
+ sv.config["pagination_count"] = int(pagination_count)
1774
+ sort_order = request.GET.getlist("sort", [])
1775
+ if sort_order:
1776
+ sv.config["sort_order"] = sort_order
1777
+
1778
+ filter_params = {}
1779
+ for key in request.GET:
1780
+ if key in self.non_filter_params:
1781
+ continue
1782
+ # TODO: this is fragile, other single-value filters will also be unhappy if given a list
1783
+ if key == "q":
1784
+ filter_params[key] = request.GET.get(key)
1785
+ else:
1786
+ filter_params[key] = request.GET.getlist(key)
1787
+
1788
+ if filter_params:
1789
+ sv.config["filter_params"] = filter_params
1790
+ elif all_filters_removed:
1791
+ sv.config["filter_params"] = {}
1792
+
1793
+ if table_changes_pending:
1794
+ table_class = get_table_class_string_from_view_name(sv.view)
1795
+ if table_class:
1796
+ if sv.config.get("table_config", None) is None:
1797
+ sv.config["table_config"] = {}
1798
+ sv.config["table_config"][f"{table_class}"] = request.user.get_config(f"tables.{table_class}")
1799
+
1800
+ sv.validated_save()
1801
+ list_view_url = sv.get_absolute_url()
1802
+ messages.success(request, f"Successfully updated current view {sv.name}")
1803
+ return redirect(list_view_url)
1804
+
1805
+ def create(self, request, *args, **kwargs):
1806
+ """
1807
+ This method will extract filter_params, pagination and sort_order from request.GET
1808
+ and the name of the new SavedView from request.POST to create a new SavedView.
1809
+ """
1810
+ name = request.POST.get("name")
1811
+ is_shared = request.POST.get("is_shared", False)
1812
+ if is_shared:
1813
+ is_shared = True
1814
+ params = request.POST.get("params", "")
1815
+
1816
+ param_dict = parse_qs(params)
1817
+
1818
+ single_value_params = ["saved_view", "table_changes_pending", "all_filters_removed", "q", "per_page"]
1819
+ for key in param_dict.keys():
1820
+ if key in single_value_params:
1821
+ param_dict[key] = param_dict[key][0]
1822
+
1823
+ derived_view_pk = param_dict.get("saved_view", None)
1824
+ derived_instance = None
1825
+ if derived_view_pk:
1826
+ derived_instance = self.get_queryset().get(pk=derived_view_pk)
1827
+ view_name = request.POST.get("view")
1828
+ try:
1829
+ reverse(view_name)
1830
+ except NoReverseMatch:
1831
+ messages.error(request, f"Invalid view name {view_name} specified.")
1832
+ if derived_view_pk:
1833
+ return redirect(self.get_return_url(request, obj=derived_instance))
1834
+ else:
1835
+ return redirect(self.get_return_url(request))
1836
+ table_changes_pending = param_dict.get("table_changes_pending", False)
1837
+ all_filters_removed = param_dict.get("all_filters_removed", False)
1838
+ try:
1839
+ sv = SavedView.objects.create(name=name, owner=request.user, view=view_name, is_shared=is_shared)
1840
+ except IntegrityError:
1841
+ messages.error(request, f"You already have a Saved View named '{name}' for this view '{view_name}'")
1842
+ if derived_view_pk:
1843
+ return redirect(self.get_return_url(request, obj=derived_instance))
1844
+ else:
1845
+ return redirect(reverse(view_name))
1846
+ pagination_count = param_dict.get("per_page", None)
1847
+ if not pagination_count:
1848
+ if derived_instance and derived_instance.config.get("pagination_count", None):
1849
+ pagination_count = derived_instance.config["pagination_count"]
1850
+ else:
1851
+ pagination_count = get_settings_or_config("PAGINATE_COUNT")
1852
+ sv.config["pagination_count"] = int(pagination_count)
1853
+ sort_order = param_dict.get("sort", [])
1854
+ if not sort_order:
1855
+ if derived_instance:
1856
+ sort_order = derived_instance.config.get("sort_order", [])
1857
+ sv.config["sort_order"] = sort_order
1858
+
1859
+ sv.config["filter_params"] = {}
1860
+ for key in param_dict:
1861
+ if key in [*self.non_filter_params, "view"]:
1862
+ continue
1863
+ sv.config["filter_params"][key] = param_dict.get(key)
1864
+ if not sv.config["filter_params"]:
1865
+ if derived_instance and all_filters_removed:
1866
+ sv.config["filter_params"] = {}
1867
+ elif derived_instance:
1868
+ sv.config["filter_params"] = derived_instance.config["filter_params"]
1869
+
1870
+ table_class = get_table_class_string_from_view_name(view_name)
1871
+ sv.config["table_config"] = {}
1872
+ if table_class:
1873
+ if table_changes_pending or derived_instance is None:
1874
+ sv.config["table_config"][f"{table_class}"] = request.user.get_config(f"tables.{table_class}")
1875
+ elif derived_instance.config.get("table_config") and derived_instance.config["table_config"].get(
1876
+ f"{table_class}"
1877
+ ):
1878
+ sv.config["table_config"][f"{table_class}"] = derived_instance.config["table_config"][f"{table_class}"]
1879
+ try:
1880
+ sv.validated_save()
1881
+ list_view_url = sv.get_absolute_url()
1882
+ message = f"Successfully created new Saved View '{sv.name}'."
1883
+ messages.success(request, message)
1884
+ return redirect(list_view_url)
1885
+ except ValidationError as e:
1886
+ messages.error(request, e)
1887
+ return redirect(self.get_return_url(request))
1888
+
1889
+ def destroy(self, request, *args, **kwargs):
1890
+ """
1891
+ request.GET: render the ObjectDeleteConfirmationForm which is passed to NautobotHTMLRenderer as Response.
1892
+ request.POST: call perform_destroy() which validates the form and perform the action of delete.
1893
+ Override to add more variables to Response
1894
+ """
1895
+ sv = SavedView.objects.get(pk=kwargs.get("pk", None))
1896
+ if sv.owner == request.user or request.user.has_perms(["extras.delete_savedview"]):
1897
+ pass
1898
+ else:
1899
+ messages.error(
1900
+ request, f"You do not have the required permission to delete this Saved View owned by {sv.owner}"
1901
+ )
1902
+ return redirect(self.get_return_url(request, obj=sv))
1903
+ return super().destroy(request, *args, **kwargs)
1904
+
1905
+
1586
1906
  class ScheduledJobListView(generic.ObjectListView):
1587
1907
  queryset = ScheduledJob.objects.enabled()
1588
1908
  table = tables.ScheduledJobTable
@@ -1756,13 +2076,8 @@ class JobLogEntryTableView(generic.GenericView):
1756
2076
  else:
1757
2077
  queryset = instance.job_log_entries.all()
1758
2078
  log_table = tables.JobLogEntryTable(data=queryset, user=request.user)
1759
- paginate = {
1760
- "paginator_class": EnhancedPaginator,
1761
- "per_page": get_paginate_count(request),
1762
- }
1763
- RequestConfig(request, paginate).configure(log_table)
1764
- table = log_table.as_html(request)
1765
- return HttpResponse(table)
2079
+ RequestConfig(request).configure(log_table)
2080
+ return HttpResponse(log_table.as_html(request))
1766
2081
 
1767
2082
 
1768
2083
  #
@@ -1882,6 +2197,60 @@ class ObjectChangeLogView(generic.GenericView):
1882
2197
  )
1883
2198
 
1884
2199
 
2200
+ #
2201
+ # Metadata
2202
+ #
2203
+
2204
+
2205
+ class MetadataTypeUIViewSet(NautobotUIViewSet):
2206
+ bulk_update_form_class = forms.MetadataTypeBulkEditForm
2207
+ filterset_class = filters.MetadataTypeFilterSet
2208
+ filterset_form_class = forms.MetadataTypeFilterForm
2209
+ form_class = forms.MetadataTypeForm
2210
+ queryset = MetadataType.objects.all()
2211
+ serializer_class = serializers.MetadataTypeSerializer
2212
+ table_class = tables.MetadataTypeTable
2213
+
2214
+ def get_extra_context(self, request, instance):
2215
+ context = super().get_extra_context(request, instance)
2216
+
2217
+ if self.action in ("create", "update"):
2218
+ if request.POST:
2219
+ context["choices"] = forms.MetadataChoiceFormSet(data=request.POST, instance=instance)
2220
+ else:
2221
+ context["choices"] = forms.MetadataChoiceFormSet(instance=instance)
2222
+
2223
+ return context
2224
+
2225
+ def form_save(self, form, **kwargs):
2226
+ obj = super().form_save(form, **kwargs)
2227
+
2228
+ # Process the formset for choices
2229
+ ctx = self.get_extra_context(self.request, obj)
2230
+ choices = ctx["choices"]
2231
+ if choices.is_valid():
2232
+ choices.save()
2233
+ else:
2234
+ raise ValidationError(choices.errors)
2235
+
2236
+ return obj
2237
+
2238
+
2239
+ class ObjectMetadataUIViewSet(
2240
+ ObjectBulkDestroyViewMixin,
2241
+ ObjectChangeLogViewMixin,
2242
+ ObjectDestroyViewMixin,
2243
+ ObjectDetailViewMixin,
2244
+ ObjectListViewMixin,
2245
+ ):
2246
+ filterset_class = filters.ObjectMetadataFilterSet
2247
+ filterset_form_class = forms.ObjectMetadataFilterForm
2248
+ queryset = ObjectMetadata.objects.all().order_by("assigned_object_type", "scoped_fields")
2249
+ serializer_class = serializers.ObjectMetadataSerializer
2250
+ table_class = tables.ObjectMetadataTable
2251
+ action_buttons = ("export",)
2252
+
2253
+
1885
2254
  #
1886
2255
  # Notes
1887
2256
  #
@@ -2041,43 +2410,32 @@ class RoleUIViewSet(viewsets.NautobotUIViewSet):
2041
2410
  }
2042
2411
 
2043
2412
  if ContentType.objects.get_for_model(Device) in context["content_types"]:
2044
- devices = instance.devices.select_related(
2045
- "status",
2046
- "location",
2047
- "tenant",
2048
- "role",
2049
- "rack",
2050
- "device_type",
2051
- ).restrict(request.user, "view")
2413
+ devices = instance.devices.restrict(request.user, "view")
2052
2414
  device_table = DeviceTable(devices)
2053
2415
  device_table.columns.hide("role")
2054
2416
  RequestConfig(request, paginate).configure(device_table)
2055
2417
  context["device_table"] = device_table
2056
2418
 
2419
+ if ContentType.objects.get_for_model(Interface) in context["content_types"]:
2420
+ interfaces = instance.interfaces.restrict(request.user, "view")
2421
+ interface_table = InterfaceTable(interfaces)
2422
+ interface_table.columns.hide("role")
2423
+ RequestConfig(request, paginate).configure(interface_table)
2424
+ context["interface_table"] = interface_table
2425
+
2057
2426
  if ContentType.objects.get_for_model(Controller) in context["content_types"]:
2058
- controllers = instance.controllers.select_related(
2059
- "status",
2060
- "location",
2061
- "tenant",
2062
- "role",
2063
- ).restrict(request.user, "view")
2427
+ controllers = instance.controllers.restrict(request.user, "view")
2064
2428
  controller_table = ControllerTable(controllers)
2065
2429
  controller_table.columns.hide("role")
2066
2430
  RequestConfig(request, paginate).configure(controller_table)
2067
2431
  context["controller_table"] = controller_table
2068
2432
 
2069
2433
  if ContentType.objects.get_for_model(IPAddress) in context["content_types"]:
2070
- ipaddress = (
2071
- instance.ip_addresses.select_related("status", "tenant")
2072
- .restrict(request.user, "view")
2073
- .annotate(
2074
- interface_count=count_related(Interface, "ip_addresses"),
2075
- interface_parent_count=count_related(Device, "interfaces__ip_addresses", distinct=True),
2076
- vm_interface_count=count_related(VMInterface, "ip_addresses"),
2077
- vm_interface_parent_count=count_related(
2078
- VirtualMachine, "interfaces__ip_addresses", distinct=True
2079
- ),
2080
- )
2434
+ ipaddress = instance.ip_addresses.restrict(request.user, "view").annotate(
2435
+ interface_count=count_related(Interface, "ip_addresses"),
2436
+ interface_parent_count=count_related(Device, "interfaces__ip_addresses", distinct=True),
2437
+ vm_interface_count=count_related(VMInterface, "ip_addresses"),
2438
+ vm_interface_parent_count=count_related(VirtualMachine, "interfaces__ip_addresses", distinct=True),
2081
2439
  )
2082
2440
  ipaddress_table = IPAddressTable(ipaddress)
2083
2441
  ipaddress_table.columns.hide("role")
@@ -2085,57 +2443,41 @@ class RoleUIViewSet(viewsets.NautobotUIViewSet):
2085
2443
  context["ipaddress_table"] = ipaddress_table
2086
2444
 
2087
2445
  if ContentType.objects.get_for_model(Prefix) in context["content_types"]:
2088
- prefixes = (
2089
- instance.prefixes.select_related(
2090
- "status",
2091
- "tenant",
2092
- "vlan",
2093
- "namespace",
2094
- )
2095
- .restrict(request.user, "view")
2096
- .annotate(location_count=count_related(Location, "prefixes"))
2097
- )
2446
+ prefixes = instance.prefixes.restrict(request.user, "view")
2098
2447
  prefix_table = PrefixTable(prefixes)
2099
2448
  prefix_table.columns.hide("role")
2100
2449
  RequestConfig(request, paginate).configure(prefix_table)
2101
2450
  context["prefix_table"] = prefix_table
2102
2451
  if ContentType.objects.get_for_model(Rack) in context["content_types"]:
2103
- racks = instance.racks.select_related(
2104
- "location",
2105
- "status",
2106
- "tenant",
2107
- "rack_group",
2108
- ).restrict(request.user, "view")
2452
+ racks = instance.racks.restrict(request.user, "view")
2109
2453
  rack_table = RackTable(racks)
2110
2454
  rack_table.columns.hide("role")
2111
2455
  RequestConfig(request, paginate).configure(rack_table)
2112
2456
  context["rack_table"] = rack_table
2113
2457
  if ContentType.objects.get_for_model(VirtualMachine) in context["content_types"]:
2114
- virtual_machines = instance.virtual_machines.select_related(
2115
- "cluster",
2116
- "role",
2117
- "status",
2118
- "tenant",
2119
- ).restrict(request.user, "view")
2458
+ virtual_machines = instance.virtual_machines.restrict(request.user, "view")
2120
2459
  virtual_machine_table = VirtualMachineTable(virtual_machines)
2121
2460
  virtual_machine_table.columns.hide("role")
2122
2461
  RequestConfig(request, paginate).configure(virtual_machine_table)
2123
2462
  context["virtual_machine_table"] = virtual_machine_table
2124
-
2463
+ if ContentType.objects.get_for_model(VMInterface) in context["content_types"]:
2464
+ vm_interfaces = instance.vm_interfaces.restrict(request.user, "view")
2465
+ vminterface_table = VMInterfaceTable(vm_interfaces)
2466
+ vminterface_table.columns.hide("role")
2467
+ RequestConfig(request, paginate).configure(vminterface_table)
2468
+ context["vminterface_table"] = vminterface_table
2125
2469
  if ContentType.objects.get_for_model(VLAN) in context["content_types"]:
2126
- vlans = (
2127
- instance.vlans.annotate(location_count=count_related(Location, "vlans"))
2128
- .select_related(
2129
- "vlan_group",
2130
- "status",
2131
- "tenant",
2132
- )
2133
- .restrict(request.user, "view")
2134
- )
2470
+ vlans = instance.vlans.restrict(request.user, "view")
2135
2471
  vlan_table = VLANTable(vlans)
2136
2472
  vlan_table.columns.hide("role")
2137
2473
  RequestConfig(request, paginate).configure(vlan_table)
2138
2474
  context["vlan_table"] = vlan_table
2475
+ if ContentType.objects.get_for_model(Module) in context["content_types"]:
2476
+ modules = instance.modules.restrict(request.user, "view")
2477
+ module_table = ModuleTable(modules)
2478
+ module_table.columns.hide("role")
2479
+ RequestConfig(request, paginate).configure(module_table)
2480
+ context["module_table"] = module_table
2139
2481
  return context
2140
2482
 
2141
2483
 
@@ -2334,6 +2676,153 @@ class SecretsGroupBulkDeleteView(generic.BulkDeleteView):
2334
2676
  table = tables.SecretsGroupTable
2335
2677
 
2336
2678
 
2679
+ #
2680
+ # Static Groups
2681
+ #
2682
+
2683
+
2684
+ class StaticGroupAssociationUIViewSet(
2685
+ ObjectBulkDestroyViewMixin,
2686
+ ObjectChangeLogViewMixin,
2687
+ ObjectDestroyViewMixin,
2688
+ ObjectDetailViewMixin,
2689
+ ObjectListViewMixin,
2690
+ # TODO anything else?
2691
+ ):
2692
+ filterset_class = filters.StaticGroupAssociationFilterSet
2693
+ filterset_form_class = forms.StaticGroupAssociationFilterForm
2694
+ queryset = StaticGroupAssociation.all_objects.all()
2695
+ serializer_class = serializers.StaticGroupAssociationSerializer
2696
+ table_class = tables.StaticGroupAssociationTable
2697
+ action_buttons = ("export",)
2698
+
2699
+ def alter_queryset(self, request):
2700
+ queryset = super().alter_queryset(request)
2701
+ # Default to only showing associations for static-type groups:
2702
+ if request is None or "dynamic_group" not in request.GET:
2703
+ queryset = queryset.filter(dynamic_group__group_type=DynamicGroupTypeChoices.TYPE_STATIC)
2704
+ return queryset
2705
+
2706
+
2707
+ class DynamicGroupBulkAssignView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
2708
+ queryset = StaticGroupAssociation.objects.all()
2709
+ form_class = forms.DynamicGroupBulkAssignForm
2710
+
2711
+ def get_required_permission(self):
2712
+ return get_permission_for_model(self.queryset.model, "add")
2713
+
2714
+ def get(self, request):
2715
+ return redirect(self.get_return_url(request))
2716
+
2717
+ def post(self, request, **kwargs):
2718
+ """
2719
+ Update the static group assignments of the provided `pk_list` (or `_all`) of the given `content_type`.
2720
+
2721
+ Unlike BulkEditView, this takes a single POST rather than two to perform its operation as
2722
+ there's no separate confirmation step involved.
2723
+ """
2724
+ # TODO more error handling - content-type doesn't exist, model_class not found, filterset missing, etc.
2725
+ content_type = ContentType.objects.get(pk=request.POST.get("content_type"))
2726
+ model = content_type.model_class()
2727
+ self.default_return_url = get_route_for_model(model, "list")
2728
+ filterset_class = get_filterset_for_model(model)
2729
+
2730
+ if request.POST.get("_all"):
2731
+ if filterset_class is not None:
2732
+ pk_list = list(filterset_class(request.GET, model.objects.only("pk")).qs.values_list("pk", flat=True))
2733
+ else:
2734
+ pk_list = list(model.objects.all().values_list("pk", flat=True))
2735
+ else:
2736
+ pk_list = request.POST.getlist("pk")
2737
+
2738
+ form = self.form_class(model, request.POST)
2739
+ restrict_form_fields(form, request.user)
2740
+
2741
+ if form.is_valid():
2742
+ logger.debug("Form validation was successful")
2743
+ try:
2744
+ with transaction.atomic():
2745
+ add_to_groups = list(form.cleaned_data["add_to_groups"])
2746
+ new_group_name = form.cleaned_data["create_and_assign_to_new_group_name"]
2747
+ if new_group_name:
2748
+ if not request.user.has_perm("extras.add_dynamicgroup"):
2749
+ raise DynamicGroup.DoesNotExist
2750
+ else:
2751
+ new_group = DynamicGroup(
2752
+ name=new_group_name,
2753
+ content_type=content_type,
2754
+ group_type=DynamicGroupTypeChoices.TYPE_STATIC,
2755
+ )
2756
+ new_group.validated_save()
2757
+ # Check permissions
2758
+ DynamicGroup.objects.restrict(request.user, "add").get(pk=new_group.pk)
2759
+
2760
+ add_to_groups.append(new_group)
2761
+ msg = "Created dynamic group"
2762
+ logger.info(f"{msg} {new_group} (PK: {new_group.pk})")
2763
+ msg = format_html('{} <a href="{}">{}</a>', msg, new_group.get_absolute_url(), new_group)
2764
+ messages.success(self.request, msg)
2765
+
2766
+ with deferred_change_logging_for_bulk_operation():
2767
+ associations = []
2768
+ for pk in pk_list:
2769
+ for dynamic_group in add_to_groups:
2770
+ association, created = StaticGroupAssociation.objects.get_or_create(
2771
+ dynamic_group=dynamic_group,
2772
+ associated_object_type_id=content_type.id,
2773
+ associated_object_id=pk,
2774
+ )
2775
+ association.validated_save()
2776
+ associations.append(association)
2777
+ if created:
2778
+ logger.debug("Created %s", association)
2779
+
2780
+ # Enforce object-level permissions
2781
+ if self.queryset.filter(pk__in=[assoc.pk for assoc in associations]).count() != len(
2782
+ associations
2783
+ ):
2784
+ raise StaticGroupAssociation.DoesNotExist
2785
+
2786
+ if associations:
2787
+ msg = (
2788
+ f"Added {len(pk_list)} {model._meta.verbose_name_plural} "
2789
+ f"to {len(add_to_groups)} dynamic group(s)."
2790
+ )
2791
+ logger.info(msg)
2792
+ messages.success(self.request, msg)
2793
+
2794
+ if form.cleaned_data["remove_from_groups"]:
2795
+ for dynamic_group in form.cleaned_data["remove_from_groups"]:
2796
+ (
2797
+ StaticGroupAssociation.objects.restrict(request.user, "delete")
2798
+ .filter(
2799
+ dynamic_group=dynamic_group,
2800
+ associated_object_type=content_type,
2801
+ associated_object_id__in=pk_list,
2802
+ )
2803
+ .delete()
2804
+ )
2805
+
2806
+ msg = (
2807
+ f"Removed {len(pk_list)} {model._meta.verbose_name_plural} from "
2808
+ f"{len(form.cleaned_data['remove_from_groups'])} dynamic group(s)."
2809
+ )
2810
+ logger.info(msg)
2811
+ messages.success(self.request, msg)
2812
+ except ValidationError as e:
2813
+ messages.error(self.request, e)
2814
+ except ObjectDoesNotExist:
2815
+ msg = "Static group association failed due to object-level permissions violation"
2816
+ logger.warning(msg)
2817
+ messages.error(self.request, msg)
2818
+
2819
+ else:
2820
+ logger.debug("Form validation failed")
2821
+ messages.error(self.request, form.errors)
2822
+
2823
+ return redirect(self.get_return_url(request))
2824
+
2825
+
2337
2826
  #
2338
2827
  # Custom statuses
2339
2828
  #