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