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
@@ -1,11 +1,10 @@
1
1
  """Dynamic Groups Models."""
2
2
 
3
3
  import logging
4
- import pickle
5
4
 
6
5
  from django import forms
6
+ from django.contrib.contenttypes.fields import GenericForeignKey
7
7
  from django.contrib.contenttypes.models import ContentType
8
- from django.core.cache import cache
9
8
  from django.core.exceptions import ValidationError
10
9
  from django.core.serializers.json import DjangoJSONEncoder
11
10
  from django.db import models
@@ -17,12 +16,14 @@ from nautobot.core.forms.constants import BOOLEAN_WITH_BLANK_CHOICES
17
16
  from nautobot.core.forms.fields import DynamicModelChoiceField
18
17
  from nautobot.core.forms.widgets import StaticSelect2
19
18
  from nautobot.core.models import BaseManager, BaseModel
20
- from nautobot.core.models.generics import OrganizationalModel
21
- from nautobot.core.utils.config import get_settings_or_config
19
+ from nautobot.core.models.generics import OrganizationalModel, PrimaryModel
20
+ from nautobot.core.models.querysets import RestrictedQuerySet
21
+ from nautobot.core.utils.data import is_uuid
22
+ from nautobot.core.utils.deprecation import method_deprecated, method_deprecated_in_favor_of
22
23
  from nautobot.core.utils.lookup import get_filterset_for_model, get_form_for_model
23
- from nautobot.extras.choices import DynamicGroupOperatorChoices
24
+ from nautobot.extras.choices import DynamicGroupOperatorChoices, DynamicGroupTypeChoices
24
25
  from nautobot.extras.querysets import DynamicGroupMembershipQuerySet, DynamicGroupQuerySet
25
- from nautobot.extras.utils import extras_features
26
+ from nautobot.extras.utils import extras_features, FeatureQuery
26
27
 
27
28
  logger = logging.getLogger(__name__)
28
29
 
@@ -34,35 +35,47 @@ logger = logging.getLogger(__name__)
34
35
  "graphql",
35
36
  "webhooks",
36
37
  )
37
- class DynamicGroup(OrganizationalModel):
38
- """Dynamic Group Model."""
38
+ class DynamicGroup(PrimaryModel):
39
+ """A group of related objects sharing a common content-type."""
39
40
 
40
- name = models.CharField(max_length=CHARFIELD_MAX_LENGTH, unique=True, help_text="Dynamic Group name")
41
+ name = models.CharField(max_length=CHARFIELD_MAX_LENGTH, unique=True)
41
42
  description = models.CharField(max_length=CHARFIELD_MAX_LENGTH, blank=True)
43
+ group_type = models.CharField(
44
+ choices=DynamicGroupTypeChoices.CHOICES, max_length=16, default=DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER
45
+ )
42
46
  content_type = models.ForeignKey(
43
47
  to=ContentType,
44
48
  on_delete=models.CASCADE,
45
49
  verbose_name="Object Type",
46
- help_text="The type of object for this Dynamic Group.",
50
+ help_text="The type of object contained in this group.",
47
51
  related_name="dynamic_groups",
52
+ limit_choices_to=FeatureQuery("dynamic_groups"),
53
+ )
54
+ tenant = models.ForeignKey(
55
+ to="tenancy.Tenant",
56
+ on_delete=models.PROTECT,
57
+ related_name="managed_dynamic_groups", # "dynamic_groups" clash with Tenant.dynamic_groups property
58
+ blank=True,
59
+ null=True,
48
60
  )
49
61
  filter = models.JSONField(
50
62
  encoder=DjangoJSONEncoder,
51
63
  editable=False,
52
64
  default=dict,
53
- help_text="A JSON-encoded dictionary of filter parameters for group membership",
65
+ help_text="A JSON-encoded dictionary of filter parameters defining membership of this group",
54
66
  )
55
67
  children = models.ManyToManyField(
56
68
  "extras.DynamicGroup",
57
- help_text="Child DynamicGroups of filter parameters for group membership",
69
+ help_text='"Child" groups that are combined together to define membership of this group',
58
70
  through="extras.DynamicGroupMembership",
59
71
  through_fields=("parent_group", "group"),
60
72
  related_name="parents",
61
73
  )
62
74
 
63
75
  objects = BaseManager.from_queryset(DynamicGroupQuerySet)()
76
+ is_dynamic_group_associable_model = False
64
77
 
65
- clone_fields = ["content_type", "filter"]
78
+ clone_fields = ["content_type", "group_type", "filter", "tenant"]
66
79
 
67
80
  # This is used as a `startswith` check on field names, so these can be explicit fields or just
68
81
  # substrings.
@@ -80,12 +93,6 @@ class DynamicGroup(OrganizationalModel):
80
93
  class Meta:
81
94
  ordering = ["content_type", "name"]
82
95
 
83
- def __init__(self, *args, **kwargs):
84
- super().__init__(*args, **kwargs)
85
-
86
- # Accessing this sets the dynamic attributes. Is there a better way? Maybe?
87
- getattr(self, "model")
88
-
89
96
  def __str__(self):
90
97
  return self.name
91
98
 
@@ -103,50 +110,43 @@ class DynamicGroup(OrganizationalModel):
103
110
  except models.ObjectDoesNotExist:
104
111
  model = None
105
112
 
106
- if model is not None:
107
- self._set_object_classes(model)
108
-
109
113
  self._model = model
110
114
 
111
115
  return self._model
112
116
 
113
- def _set_object_classes(self, model):
114
- """
115
- Given the `content_type` for this group, dynamically map object classes to this instance.
116
- Protocol for return values:
117
-
118
- - True: Model and object classes mapped.
119
- - False: Model not yet mapped (likely because of no `content_type`)
120
- """
121
-
122
- # If object classes have already been mapped, return True.
123
- if getattr(self, "_object_classes_mapped", False):
124
- return True
117
+ @property
118
+ def filterset_class(self):
119
+ if getattr(self, "_filterset_class", None) is None:
120
+ try:
121
+ self._filterset_class = get_filterset_for_model(self.model)
122
+ except TypeError:
123
+ self._filterset_class = None
124
+ return self._filterset_class
125
125
 
126
- # Try to set the object classes for this model.
127
- try:
128
- self.filterset_class = get_filterset_for_model(model)
129
- self.filterform_class = get_form_for_model(model, form_prefix="Filter")
130
- self.form_class = get_form_for_model(model)
131
- # We expect this to happen on new instances or in any case where `model` was not properly
132
- # available to the caller, so always fail closed.
133
- except TypeError:
134
- logger.debug("Failed to map object classes for model %s", model)
135
- self.filterset_class = None
136
- self.filterform_class = None
137
- self.form_class = None
138
- self._object_classes_mapped = False
139
- else:
140
- self._object_classes_mapped = True
126
+ @property
127
+ def filterform_class(self):
128
+ if getattr(self, "_filterform_class", None) is None:
129
+ try:
130
+ self._filterform_class = get_form_for_model(self.model, form_prefix="Filter")
131
+ except TypeError:
132
+ self._filterform_class = None
133
+ return self._filterform_class
141
134
 
142
- return self._object_classes_mapped
135
+ @property
136
+ def form_class(self):
137
+ if getattr(self, "_form_class", None) is None:
138
+ try:
139
+ self._form_class = get_form_for_model(self.model)
140
+ except TypeError:
141
+ self._form_class = None
142
+ return self._form_class
143
143
 
144
144
  @cached_property
145
145
  def _map_filter_fields(self):
146
146
  """Return all FilterForm fields in a dictionary."""
147
147
 
148
148
  # Fail gracefully with an empty dict if nothing is working yet.
149
- if not self._set_object_classes(self.model):
149
+ if not self.form_class:
150
150
  return {}
151
151
 
152
152
  # Get model form and fields
@@ -289,74 +289,148 @@ class DynamicGroup(OrganizationalModel):
289
289
 
290
290
  return self._map_filter_fields
291
291
 
292
- def get_queryset(self):
292
+ @property
293
+ def members(self):
293
294
  """
294
- Return a queryset for the `content_type` model of this group.
295
+ Return the (cached) member objects for this group.
295
296
 
296
- The queryset is generated based on the `filterset_class` for the Model.
297
+ If up-to-the-minute accuracy is needed, call `update_cached_members()` instead.
297
298
  """
299
+ # Since associated_object is a GenericForeignKey, we can't just do:
300
+ # return self.static_group_associations.values_list("associated_object", flat=True)
301
+ return self.model.objects.filter(
302
+ pk__in=self.static_group_associations(manager="all_objects").values_list("associated_object_id", flat=True)
303
+ )
304
+
305
+ @members.setter
306
+ def members(self, value):
307
+ """Set the member objects (QuerySet or list of records) for this staticly defined group."""
308
+ if self.group_type != DynamicGroupTypeChoices.TYPE_STATIC:
309
+ raise ValidationError(
310
+ f"Group {self} is not staticly defined, setting its members directly is not permitted."
311
+ )
312
+ return self._set_members(value)
313
+
314
+ def _set_members(self, value):
315
+ """Internal API for updating the static/cached members of this group."""
316
+ if isinstance(value, models.QuerySet):
317
+ if value.model != self.model:
318
+ raise TypeError(f"QuerySet does not contain {self.model._meta.label_lower} objects")
319
+ to_remove = self.members.exclude(pk__in=value.values_list("pk", flat=True))
320
+ self._remove_members(to_remove)
321
+ to_add = value.exclude(pk__in=self.members.values_list("pk", flat=True))
322
+ self._add_members(to_add)
323
+ else:
324
+ for obj in value:
325
+ if not isinstance(obj, self.model):
326
+ raise TypeError(f"{obj} is not a {self.model._meta.label_lower}")
327
+ to_remove = []
328
+ for member in self.members:
329
+ if member not in value:
330
+ to_remove.append(member)
331
+ self._remove_members(to_remove)
332
+ to_add = []
333
+ members = self.members
334
+ for candidate in value:
335
+ if candidate not in members:
336
+ to_add.append(candidate)
337
+ self._add_members(to_add)
338
+
339
+ return self.members
340
+
341
+ def add_members(self, objects_to_add):
342
+ """Add the given list or QuerySet of objects to this staticly defined group."""
343
+ if self.group_type != DynamicGroupTypeChoices.TYPE_STATIC:
344
+ raise ValidationError(f"Group {self} is not staticly defined, adding members directly is not permitted.")
345
+ return self._add_members(objects_to_add)
346
+
347
+ def _add_members(self, objects_to_add):
348
+ """Internal API for adding the given list or QuerySet of objects to the cached/static members of this group."""
349
+ if isinstance(objects_to_add, models.QuerySet):
350
+ if objects_to_add.model != self.model:
351
+ raise TypeError(f"QuerySet does not contain {self.model._meta.label_lower} objects")
352
+ else:
353
+ for obj in objects_to_add:
354
+ if not isinstance(obj, self.model):
355
+ raise TypeError(f"{obj} is not a {self.model._meta.label_lower}")
356
+
357
+ if self.group_type == DynamicGroupTypeChoices.TYPE_STATIC:
358
+ for obj in objects_to_add:
359
+ # We don't use `.bulk_create()` currently because we want change logging for these creates.
360
+ # Might be a good future performance improvement though.
361
+ StaticGroupAssociation.all_objects.get_or_create(
362
+ dynamic_group=self, associated_object_type=self.content_type, associated_object_id=obj.pk
363
+ )
364
+ else:
365
+ # Cached/hidden static group associations, so we can use bulk-create to bypass change logging.
366
+ existing_members = self.members
367
+ sgas = [
368
+ StaticGroupAssociation(
369
+ dynamic_group=self, associated_object_type=self.content_type, associated_object_id=obj.pk
370
+ )
371
+ for obj in objects_to_add
372
+ if obj not in existing_members
373
+ ]
374
+ StaticGroupAssociation.all_objects.bulk_create(sgas)
375
+
376
+ def remove_members(self, objects_to_remove):
377
+ """Remove the given list or QuerySet of objects from this staticly defined group."""
378
+ if self.group_type != DynamicGroupTypeChoices.TYPE_STATIC:
379
+ raise ValidationError(f"Group {self} is not staticly defined, removing members directly is not permitted.")
380
+ return self._remove_members(objects_to_remove)
381
+
382
+ def _remove_members(self, objects_to_remove):
383
+ """Internal API for removing the given list or QuerySet from the cached/static members of this Group."""
384
+ if isinstance(objects_to_remove, models.QuerySet):
385
+ if objects_to_remove.model != self.model:
386
+ raise TypeError(f"QuerySet does not contain {self.model._meta.label_lower} objects")
387
+ StaticGroupAssociation.all_objects.filter(
388
+ dynamic_group=self,
389
+ associated_object_type=self.content_type,
390
+ associated_object_id__in=objects_to_remove.values_list("pk", flat=True),
391
+ ).delete()
392
+ else:
393
+ pks_to_remove = set()
394
+ for obj in objects_to_remove:
395
+ if not isinstance(obj, self.model):
396
+ raise TypeError(f"{obj} is not a {self.model._meta.label_lower}")
397
+ pks_to_remove.add(obj.pk)
298
398
 
299
- model = self.model
300
-
301
- if model is None:
302
- raise RuntimeError(f"Could not determine queryset for model '{model}'")
303
-
304
- filterset = self.filterset_class(self.filter, model.objects.all())
305
- if not filterset.is_valid():
306
- logger.warning('Filter for DynamicGroup "%s" is not valid', self)
307
- return model.objects.none()
308
-
309
- qs = filterset.qs
310
-
311
- # Make sure that this instance can't be a member of its own group.
312
- if self.present_in_database and model == self.__class__:
313
- qs = qs.exclude(pk=self.pk)
314
-
315
- return qs
316
-
317
- @property
318
- def members(self):
319
- """Return the member objects for this group, never cached."""
320
- # If there are child groups, return the generated group queryset, otherwise use this group's
321
- # `filter` directly.
322
- if self.children.exists():
323
- return self.get_group_queryset()
324
- return self.get_queryset()
399
+ StaticGroupAssociation.all_objects.filter(
400
+ dynamic_group=self, associated_object_type=self.content_type, associated_object_id__in=pks_to_remove
401
+ ).delete()
325
402
 
326
403
  @property
404
+ @method_deprecated("Members are now cached in the database via StaticGroupAssociations rather than in Redis.")
327
405
  def members_cache_key(self):
328
- """Return the cache key for this group's members."""
406
+ """Obsolete cache key for this group's members."""
329
407
  return f"nautobot.extras.dynamicgroup.{self.id}.members_cached"
330
408
 
331
409
  @property
410
+ @method_deprecated_in_favor_of(members.fget)
332
411
  def members_cached(self):
333
- """Return the member objects for this group, cached if available."""
412
+ """Deprecated - use `members()` instead."""
413
+ return self.members
334
414
 
335
- unpickled_query = None
336
- try:
337
- cached_query = cache.get(self.members_cache_key)
338
- if cached_query is not None:
339
- unpickled_query = pickle.loads(cached_query) # noqa: S301 # suspicious-pickle-usage -- we know, but we control what's in the DB
340
- except pickle.UnpicklingError:
341
- logger.warning("Failed to unpickle cached members for %s", self)
342
- finally:
343
- if unpickled_query is None:
344
- unpickled_query = self.members.all()
345
- cached_query = pickle.dumps(unpickled_query) # Explicitly pickle the query to evaluate it.
346
- cache.set(
347
- self.members_cache_key, cached_query, get_settings_or_config("DYNAMIC_GROUPS_MEMBER_CACHE_TIMEOUT")
348
- )
349
-
350
- return unpickled_query
351
-
352
- def update_cached_members(self):
415
+ def update_cached_members(self, members=None):
353
416
  """
354
- Update the cached members of the groups. Also returns the updated cached members.
417
+ Update the cached members of this group and return the resulting members.
355
418
  """
419
+ if members is None:
420
+ if self.group_type in (
421
+ DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER,
422
+ DynamicGroupTypeChoices.TYPE_DYNAMIC_SET,
423
+ ):
424
+ members = self._get_group_queryset()
425
+ elif self.group_type == DynamicGroupTypeChoices.TYPE_STATIC:
426
+ return self.members # nothing to do
427
+ else:
428
+ raise RuntimeError(f"Unknown/invalid group_type {self.group_type}")
356
429
 
357
- cache.delete(self.members_cache_key)
430
+ self._set_members(members)
431
+ logger.debug("Refreshed cache for %s, now with %d members", self, self.count)
358
432
 
359
- return self.members_cached
433
+ return members
360
434
 
361
435
  def has_member(self, obj, use_cache=False):
362
436
  """
@@ -366,7 +440,7 @@ class DynamicGroup(OrganizationalModel):
366
440
 
367
441
  Args:
368
442
  obj (django.db.models.Model): The object to check for membership.
369
- use_cache (bool, optional): Whether to use the cache and run the query directly. Defaults to False.
443
+ use_cache (bool, optional): Obsolete; cache is now always used.
370
444
 
371
445
  Returns:
372
446
  bool: True if the object is a member of this group, otherwise False.
@@ -374,23 +448,18 @@ class DynamicGroup(OrganizationalModel):
374
448
 
375
449
  # Object's class may have content type cached, so check that first.
376
450
  try:
377
- if use_cache and type(obj)._content_type.id != self.content_type_id:
451
+ if type(obj)._content_type.id != self.content_type_id:
378
452
  return False
379
453
  except AttributeError:
380
454
  # Object did not have `_content_type` even though we wanted to use it.
381
- pass
382
-
383
- if not use_cache and ContentType.objects.get_for_model(obj).id != self.content_type_id:
384
- return False
455
+ if ContentType.objects.get_for_model(obj).id != self.content_type_id:
456
+ return False
385
457
 
386
- if not use_cache:
387
- return self.members.filter(pk=obj.pk).exists()
388
- else:
389
- return obj in list(self.members_cached)
458
+ return self.members.filter(pk=obj.pk).exists()
390
459
 
391
460
  @property
392
461
  def count(self):
393
- """Return the number of member objects in this group."""
462
+ """Return the (cached) number of member objects in this group."""
394
463
  return self.members.count()
395
464
 
396
465
  def get_group_members_url(self):
@@ -404,9 +473,12 @@ class DynamicGroup(OrganizationalModel):
404
473
  """
405
474
  Set all desired fields from `form_data` into `filter` dict.
406
475
 
407
- :param form_data:
408
- Dict of filter parameters, generally from a filter form's `cleaned_data`
476
+ Args:
477
+ form_data (dict): Dict of filter parameters, generally from a filter form's `cleaned_data`
409
478
  """
479
+ if self.group_type != DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER:
480
+ raise ValidationError(f"Group {self} is not a filter-defined group (instead, group_type {self.group_type})")
481
+
410
482
  # Get the authoritative source of filter fields we want to keep.
411
483
  filter_fields = self.get_filter_fields()
412
484
 
@@ -471,7 +543,7 @@ class DynamicGroup(OrganizationalModel):
471
543
 
472
544
  def get_initial(self):
473
545
  """
474
- Return an form-friendly version of `self.filter` for initial form data.
546
+ Return a form-friendly version of `self.filter` for initial form data.
475
547
 
476
548
  This is intended for use to populate the dynamically-generated filter form created by
477
549
  `generate_filter_form()`.
@@ -514,11 +586,17 @@ class DynamicGroup(OrganizationalModel):
514
586
  # Accessing `self.model` will determine if the `content_type` is not correctly set, blocking validation.
515
587
  if self.model is None:
516
588
  raise ValidationError({"filter": "Filter requires a `content_type` to be set"})
589
+ if self.filterset_class is None:
590
+ raise ValidationError({"filter": "Unable to locate the FilterSet class for this model."})
517
591
 
518
- # Validate against the filterset's internal form validation.
519
- filterset = self.filterset_class(self.filter)
520
- if not filterset.is_valid():
521
- raise ValidationError(filterset.errors)
592
+ if self.group_type != DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER:
593
+ if self.filter:
594
+ raise ValidationError({"filter": "Filter can only be set for groups of type `dynamic-filter`."})
595
+ else:
596
+ # Validate against the filterset's internal form validation.
597
+ filterset = self.filterset_class(self.filter)
598
+ if not filterset.is_valid():
599
+ raise ValidationError(filterset.errors)
522
600
 
523
601
  def delete(self, *args, **kwargs):
524
602
  """Check if we're a child and attempt to block delete if we are."""
@@ -548,14 +626,17 @@ class DynamicGroup(OrganizationalModel):
548
626
  if self.content_type != database_object.content_type:
549
627
  raise ValidationError({"content_type": "ContentType cannot be changed once created"})
550
628
 
551
- def generate_query_for_filter(self, filter_field, value):
629
+ # TODO limit most changes to self.group_type as well.
630
+
631
+ def _generate_query_for_filter(self, filter_field, value):
552
632
  """
553
633
  Return a `Q` object generated from a `filter_field` and `value`.
554
634
 
555
- :param filter_field:
556
- Filter instance
557
- :param value:
558
- Value passed to the filter
635
+ Helper to `_generate_filter_based_query()`.
636
+
637
+ Args:
638
+ filter_field (Filter): filterset filter field instance
639
+ value (Any): value passed to the filter
559
640
  """
560
641
  query = models.Q()
561
642
  if filter_field is None:
@@ -587,7 +668,7 @@ class DynamicGroup(OrganizationalModel):
587
668
  # pass it to `generate_query` to get a correct Q object back out. When values are being
588
669
  # reconstructed from saved filters, lists of names are common e.g. (`{"location": ["ams01",
589
670
  # "ams02"]}`, the value being a list of location names (`["ams01", "ams02"]`).
590
- if value and isinstance(value, list) and isinstance(value[0], str):
671
+ if value and isinstance(value, list) and isinstance(value[0], str) and not is_uuid(value[0]):
591
672
  model_field = django_filters.utils.get_model_field(self._model, filter_field.field_name)
592
673
  related_model = model_field.related_model
593
674
  lookup_kwargs = {f"{to_field_name}__in": value}
@@ -618,35 +699,39 @@ class DynamicGroup(OrganizationalModel):
618
699
 
619
700
  return query
620
701
 
621
- def generate_query_for_group(self, group):
702
+ def _generate_filter_based_query(self):
622
703
  """
623
- Return a `Q` object generated from all filters for a `group`.
704
+ Return a `Q` object generated from this group's filters.
624
705
 
625
- :param group:
626
- DynamicGroup instance
706
+ Helper to `generate_query()`.
627
707
  """
628
- fs = group.filterset_class(group.filter, group.get_queryset())
708
+ if self.group_type != DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER:
709
+ raise RuntimeError(f"{self} is not a dynamic-filter group")
710
+
711
+ filterset = self.filterset_class(self.filter, self.model.objects.all())
629
712
  query = models.Q()
630
713
 
631
714
  # In this case we want all filters for a group's filter dict in a set intersection (boolean
632
715
  # AND) because ALL filter conditions must match for the filter parameters to be valid.
633
- for field_name, value in fs.data.items():
634
- filter_field = fs.filters.get(field_name)
635
- query &= self.generate_query_for_filter(filter_field, value)
716
+ for field_name, value in filterset.data.items():
717
+ filter_field = filterset.filters.get(field_name)
718
+ query &= self._generate_query_for_filter(filter_field, value)
636
719
 
637
720
  return query
638
721
 
639
- def perform_membership_set_operation(self, operator, query, next_set):
722
+ def _perform_membership_set_operation(self, operator, query, next_set):
640
723
  """
641
- Perform set operation for a group membership. The `operator` and `next_set` are used to
642
- decide the appropriate action to take on the `query`. The updated `Q` object is returned.
643
-
644
- :param operator:
645
- DynamicGroupOperatorChoices choice (str)
646
- :param query:
647
- Q instance
648
- :param next_set:
649
- Q instance
724
+ Perform set operation for a group membership.
725
+
726
+ The `operator` and `next_set` are used to decide the appropriate action to take on the `query`.
727
+
728
+ Args:
729
+ operator (str): DynamicGroupOperatorChoices choice
730
+ query (Q): Query so far
731
+ next_set (Q): Additional query to apply based on the operator.
732
+
733
+ Returns:
734
+ Q: updated query object
650
735
  """
651
736
  if operator == "union":
652
737
  query |= next_set
@@ -657,74 +742,71 @@ class DynamicGroup(OrganizationalModel):
657
742
 
658
743
  return query
659
744
 
660
- def generate_members_query(self):
661
- """
662
- Return a `Q` object generated from all direct members or from self if `filter` is set.
663
- """
664
- if self.filter:
665
- return self.generate_query_for_group(self)
666
-
667
- query = models.Q()
668
- for membership in self.dynamic_group_memberships.all():
669
- group = membership.group
670
- operator = membership.operator
671
- next_set = self.generate_query_for_group(group)
672
- query = self.perform_membership_set_operation(operator, query, next_set)
673
-
674
- return query
675
-
676
745
  def generate_query(self):
677
746
  """
678
- Return a `Q` object generated recursively from all nested filters for this dynamic group.
747
+ Return a `Q` object generated recursively from this dynamic group.
679
748
  """
680
- query = models.Q()
681
- memberships = self.dynamic_group_memberships.all()
749
+ if self.group_type == DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER:
750
+ return self._generate_filter_based_query()
682
751
 
683
- # If this group has no children, just return a single query.
684
- if not memberships.exists():
685
- return self.generate_members_query()
752
+ if self.group_type == DynamicGroupTypeChoices.TYPE_DYNAMIC_SET:
753
+ query = models.Q()
754
+ memberships = self.dynamic_group_memberships.all()
755
+ # Enumerate the filters for each child group, trusting that they handle their own children.
756
+ for membership in memberships:
757
+ group = membership.group
758
+ operator = membership.operator
759
+ logger.debug("Processing group %s...", group)
686
760
 
687
- # Enumerate the filters for each child group, trusting that they handle their own children.
688
- for membership in memberships:
689
- group = membership.group
690
- operator = membership.operator
691
- logger.debug("Processing group %s...", group)
761
+ if group.group_type == DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER:
762
+ logger.debug("Query: %s -> %s -> %s", group, group.filter, operator)
692
763
 
693
- if group.filter:
694
- logger.debug("Query: %s -> %s -> %s", group, group.filter, operator)
764
+ next_set = group.generate_query()
765
+ query = self._perform_membership_set_operation(operator, query, next_set)
695
766
 
696
- next_set = group.generate_members_query()
697
- query = self.perform_membership_set_operation(operator, query, next_set)
767
+ return query
698
768
 
699
- return query
769
+ # TODO? if self.group_type == DynamicGroupTypeChoices.TYPE_STATIC:
770
+
771
+ raise RuntimeError(f"generate_query not implemented for group_type {self.group_type}")
700
772
 
701
- def get_group_queryset(self):
702
- """Return a filtered queryset of all descendant groups."""
773
+ def _get_group_queryset(self):
774
+ """Construct the queryset representing dynamic membership of this group."""
703
775
  query = self.generate_query()
704
- qs = self.get_queryset()
705
- return qs.filter(query)
776
+ return self.model.objects.filter(query)
706
777
 
778
+ # TODO: unused in core
707
779
  def add_child(self, child, operator, weight):
708
780
  """
709
781
  Add a child group including `operator` and `weight`.
710
782
 
711
- :param child:
712
- DynamicGroup instance
713
- :param operator:
714
- DynamicGroupOperatorChoices choice value used to dictate filtering behavior
715
- :param weight:
716
- Integer weight used to order filtering
783
+ Args:
784
+ child (DynamicGroup): child group to add
785
+ operator (str): DynamicGroupOperatorChoices choice value used to dictate filtering behavior
786
+ weight (int): Integer weight used to order filtering
717
787
  """
788
+ if self.group_type != DynamicGroupTypeChoices.TYPE_DYNAMIC_SET:
789
+ if self.filter or self.group_type != DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER:
790
+ raise ValidationError(f"{self} is not a dynamic-set group.")
791
+ else:
792
+ # For backwards compatibility
793
+ self.group_type = DynamicGroupTypeChoices.TYPE_DYNAMIC_SET
794
+ self.validated_save()
795
+
718
796
  instance = self.children.through(parent_group=self, group=child, operator=operator, weight=weight)
719
797
  return instance.validated_save()
720
798
 
799
+ # TODO: unused in core
721
800
  def remove_child(self, child):
722
801
  """
723
802
  Remove a child group.
724
803
 
725
- :param child:
726
- DynamicGroup instance
804
+ Args:
805
+ child (DynamicGroup): child group to remove
727
806
  """
807
+ if self.group_type != DynamicGroupTypeChoices.TYPE_DYNAMIC_SET:
808
+ raise ValidationError(f"{self} is not a dynamic-set group.")
809
+
728
810
  instance = self.children.through.objects.get(parent_group=self, group=child)
729
811
  return instance.delete()
730
812
 
@@ -732,8 +814,8 @@ class DynamicGroup(OrganizationalModel):
732
814
  """
733
815
  Recursively return a list of the children of all child groups.
734
816
 
735
- :param group:
736
- DynamicGroup from which to traverse. If not set, this group is used.
817
+ Args:
818
+ group (DynamicGroup): parent group to traverse from. If not set, this group (self) is used.
737
819
  """
738
820
  if group is None:
739
821
  group = self
@@ -743,7 +825,7 @@ class DynamicGroup(OrganizationalModel):
743
825
  logger.debug("Processing group %s...", child_group)
744
826
  descendants.append(child_group)
745
827
  if child_group.children.exists():
746
- descendants.extend(self.get_descendants(child_group))
828
+ descendants.extend(child_group.get_descendants())
747
829
 
748
830
  return descendants
749
831
 
@@ -751,8 +833,8 @@ class DynamicGroup(OrganizationalModel):
751
833
  """
752
834
  Recursively return a list of the parents of all parent groups.
753
835
 
754
- :param group:
755
- DynamicGroup from which to traverse. If not set, this group is used.
836
+ Args:
837
+ group (DynamicGroup): child group to traverse from. If not set, this group (self) is used.
756
838
  """
757
839
  if group is None:
758
840
  group = self
@@ -762,10 +844,11 @@ class DynamicGroup(OrganizationalModel):
762
844
  logger.debug("Processing group %s...", parent_group)
763
845
  ancestors.append(parent_group)
764
846
  if parent_group.parents.exists():
765
- ancestors.extend(self.get_ancestors(parent_group))
847
+ ancestors.extend(parent_group.get_ancestors())
766
848
 
767
849
  return ancestors
768
850
 
851
+ # TODO: unused in core
769
852
  def get_siblings(self, include_self=False):
770
853
  """Return groups that share the same parents."""
771
854
  siblings = DynamicGroup.objects.filter(parents__in=self.parents.all())
@@ -774,19 +857,25 @@ class DynamicGroup(OrganizationalModel):
774
857
 
775
858
  return siblings.exclude(pk=self.pk)
776
859
 
860
+ # TODO: this is an interesting definition of "root node", as a node with no children has is_root() = False??
861
+ # TODO: unused in core
777
862
  def is_root(self):
778
863
  """Return whether this is a root node (has children, but no parents)."""
779
864
  return self.children.exists() and not self.parents.exists()
780
865
 
866
+ # TODO: this is an interesting definition of "leaf node", as a node with no parents has is_leaf() = False??
867
+ # TODO: unused in core
781
868
  def is_leaf(self):
782
869
  """Return whether this is a leaf node (has parents, but no children)."""
783
870
  return self.parents.exists() and not self.children.exists()
784
871
 
872
+ # TODO: unused in core
785
873
  def get_ancestors_queryset(self):
786
874
  """Return a queryset of all ancestors."""
787
875
  pks = [obj.pk for obj in self.get_ancestors()]
788
876
  return self.ordered_queryset_from_pks(pks)
789
877
 
878
+ # TODO: unused in core
790
879
  def get_descendants_queryset(self):
791
880
  """Return a queryset of all descendants."""
792
881
  pks = [obj.pk for obj in self.get_descendants()]
@@ -819,14 +908,15 @@ class DynamicGroup(OrganizationalModel):
819
908
 
820
909
  def flatten_ancestors_tree(self, tree):
821
910
  """
822
- Recursively flatten a tree mapping of ancestors to a list, adding a `depth attribute to each
911
+ Recursively flatten a tree mapping of ancestors to a list, adding a `depth` attribute to each
823
912
  instance in the list that can be used for visualizing tree depth.
824
913
 
825
- :param tree:
826
- Output from `ancestors_tree()`
914
+ Args:
915
+ tree (dict): Output from `ancestors_tree()`
827
916
  """
828
- return self._flatten_tree(tree, descending=False)
917
+ return self._flatten_tree(tree)
829
918
 
919
+ # TODO: unused in core
830
920
  def descendants_tree(self):
831
921
  """
832
922
  Return a nested mapping of descendants with the following structure:
@@ -851,44 +941,35 @@ class DynamicGroup(OrganizationalModel):
851
941
 
852
942
  return tree
853
943
 
944
+ # TODO: unused in core
854
945
  def flatten_descendants_tree(self, tree):
855
946
  """
856
947
  Recursively flatten a tree mapping of descendants to a list, adding a `depth` attribute to each
857
948
  instance in the list that can be used for visualizing tree depth.
858
949
 
859
- :param tree:
860
- Output from `descendats_tree()`
950
+ Args:
951
+ tree (dict): Output from `descendants_tree()`
861
952
  """
862
- return self._flatten_tree(tree, descending=True)
953
+ return self._flatten_tree(tree)
863
954
 
864
- def _flatten_tree(self, tree, descending=True, nodes=None, depth=1):
955
+ def _flatten_tree(self, tree, nodes=None, depth=1):
865
956
  """
866
957
  Recursively flatten a tree mapping to a list, adding a `depth` attribute to each instance in
867
958
  the list that can be used for visualizing tree depth.
868
959
 
869
- :param tree:
870
- A nested dictionary tree
871
- :param descending:
872
- Whether to traverse descendants or ancestors. If not set, defaults to descending.
873
- :param nodes:
874
- An ordered list used to hold the flattened nodes
875
- :param depth:
876
- The tree traversal depth
960
+ Args:
961
+ tree (dict): Output from `ancestors_tree()` or `descendants_tree()`
962
+ nodes (list): Used in recursion, will contain the flattened nodes.
963
+ depth (int): Used in recursion, the tree traversal depth.
877
964
  """
878
965
 
879
966
  if nodes is None:
880
967
  nodes = []
881
968
 
882
- if descending:
883
- method = "get_descendants"
884
- else:
885
- method = "get_ancestors"
886
-
887
969
  for item in tree:
888
970
  item.depth = depth
889
971
  nodes.append(item)
890
- branches = getattr(item, method)()
891
- self._flatten_tree(branches, nodes=nodes, descending=descending, depth=depth + 1)
972
+ self._flatten_tree(tree[item], nodes=nodes, depth=depth + 1)
892
973
 
893
974
  return nodes
894
975
 
@@ -907,6 +988,7 @@ class DynamicGroup(OrganizationalModel):
907
988
 
908
989
  return tree
909
990
 
991
+ # TODO: unused in core
910
992
  def _ordered_filter(self, queryset, field_names, values):
911
993
  """
912
994
  Filters the provided `queryset` using `{field_name}__in` expressions for each field_name in the
@@ -943,6 +1025,7 @@ class DynamicGroup(OrganizationalModel):
943
1025
 
944
1026
  return queryset.filter(**filter_condition).order_by(order_by)
945
1027
 
1028
+ # TODO: unused in core
946
1029
  def ordered_queryset_from_pks(self, pk_list):
947
1030
  """
948
1031
  Generates a queryset ordered by the provided list of primary keys.
@@ -966,6 +1049,7 @@ class DynamicGroupMembership(BaseModel):
966
1049
  objects = BaseManager.from_queryset(DynamicGroupMembershipQuerySet)()
967
1050
 
968
1051
  documentation_static_path = "docs/user-guide/platform-functionality/dynamicgroup.html"
1052
+ is_metadata_associable_model = False
969
1053
 
970
1054
  class Meta:
971
1055
  unique_together = ["group", "parent_group", "operator", "weight"]
@@ -984,11 +1068,13 @@ class DynamicGroupMembership(BaseModel):
984
1068
  """Return the group filter."""
985
1069
  return self.group.filter
986
1070
 
1071
+ # TODO: unused in core
987
1072
  @property
988
1073
  def members(self):
989
1074
  """Return the group members."""
990
1075
  return self.group.members
991
1076
 
1077
+ # TODO: unused in core
992
1078
  @property
993
1079
  def count(self):
994
1080
  """Return the group count."""
@@ -1001,10 +1087,12 @@ class DynamicGroupMembership(BaseModel):
1001
1087
  return self.group.get_absolute_url(api=api)
1002
1088
  return super().get_absolute_url(api=api)
1003
1089
 
1090
+ # TODO: unused in core
1004
1091
  def get_group_members_url(self):
1005
1092
  """Return the group members URL."""
1006
1093
  return self.group.get_group_members_url()
1007
1094
 
1095
+ # TODO: unused in core
1008
1096
  def get_siblings(self, include_self=False):
1009
1097
  """Return group memberships that share the same parent group."""
1010
1098
  siblings = DynamicGroupMembership.objects.filter(parent_group=self.parent_group)
@@ -1013,19 +1101,19 @@ class DynamicGroupMembership(BaseModel):
1013
1101
 
1014
1102
  return siblings.exclude(pk=self.pk)
1015
1103
 
1104
+ # TODO: unused in core
1016
1105
  def generate_query(self):
1017
1106
  return self.group.generate_query()
1018
1107
 
1019
1108
  def clean(self):
1020
1109
  super().clean()
1021
1110
 
1022
- # Enforce mutual exclusivity between filter & children.
1023
- if self.parent_group.filter:
1024
- raise ValidationError(
1025
- {
1026
- "parent_group": "A parent group may have either a filter or child groups, but not both. Clear the parent filter and try again."
1027
- }
1028
- )
1111
+ # Enforce group types
1112
+ if self.parent_group.group_type != DynamicGroupTypeChoices.TYPE_DYNAMIC_SET and self.parent_group.filter:
1113
+ raise ValidationError({"parent_group": 'A parent group must be of `group_type` `"dynamic-set"`.'})
1114
+
1115
+ if self.group.group_type == DynamicGroupTypeChoices.TYPE_STATIC:
1116
+ raise ValidationError({"group": 'Groups of `group_type` `"static"` may not be child groups at this time.'})
1029
1117
 
1030
1118
  # Enforce matching content_type
1031
1119
  if self.parent_group.content_type != self.group.content_type:
@@ -1037,3 +1125,79 @@ class DynamicGroupMembership(BaseModel):
1037
1125
 
1038
1126
  if self.group in self.parent_group.get_ancestors():
1039
1127
  raise ValidationError({"group": "Cannot add ancestor as a child"})
1128
+
1129
+ def save(self, *args, **kwargs):
1130
+ # For backwards compatibility
1131
+ if self.parent_group.group_type == DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER and not self.parent_group.filter:
1132
+ self.parent_group.group_type = DynamicGroupTypeChoices.TYPE_DYNAMIC_SET
1133
+ self.parent_group.save()
1134
+ return super().save(*args, **kwargs)
1135
+
1136
+
1137
+ class StaticGroupAssociationManager(BaseManager.from_queryset(RestrictedQuerySet)):
1138
+ use_in_migrations = True
1139
+
1140
+
1141
+ class StaticGroupAssociationDefaultManager(StaticGroupAssociationManager):
1142
+ """Subclass of StaticGroupAssociationManager that automatically filters out cached/hidden associations."""
1143
+
1144
+ def get_queryset(self):
1145
+ return super().get_queryset().filter(dynamic_group__group_type=DynamicGroupTypeChoices.TYPE_STATIC)
1146
+
1147
+
1148
+ @extras_features(
1149
+ "custom_validators",
1150
+ "export_templates",
1151
+ "graphql",
1152
+ "webhooks",
1153
+ )
1154
+ class StaticGroupAssociation(OrganizationalModel):
1155
+ """Intermediary model for associating an object statically to a DynamicGroup of group_type `static`."""
1156
+
1157
+ dynamic_group = models.ForeignKey(
1158
+ to=DynamicGroup, on_delete=models.CASCADE, related_name="static_group_associations"
1159
+ )
1160
+ associated_object_type = models.ForeignKey(
1161
+ to=ContentType,
1162
+ on_delete=models.CASCADE,
1163
+ related_name="static_group_associations",
1164
+ limit_choices_to=FeatureQuery("dynamic_groups"),
1165
+ )
1166
+ associated_object_id = models.UUIDField(db_index=True)
1167
+ associated_object = GenericForeignKey(ct_field="associated_object_type", fk_field="associated_object_id")
1168
+
1169
+ objects = StaticGroupAssociationDefaultManager()
1170
+ all_objects = StaticGroupAssociationManager()
1171
+
1172
+ is_contact_associable_model = False
1173
+ is_dynamic_group_associable_model = False
1174
+ is_saved_view_model = False
1175
+
1176
+ class Meta:
1177
+ unique_together = [["dynamic_group", "associated_object_type", "associated_object_id"]]
1178
+ ordering = ["dynamic_group", "associated_object_type", "associated_object_id"]
1179
+ indexes = [
1180
+ models.Index(
1181
+ name="extras_sga_double",
1182
+ fields=["dynamic_group", "associated_object_id"],
1183
+ ),
1184
+ models.Index(
1185
+ name="extras_sga_associated_object",
1186
+ fields=["associated_object_type_id", "associated_object_id"],
1187
+ ),
1188
+ ]
1189
+
1190
+ def __str__(self):
1191
+ return f"{self.associated_object} as a member of {self.dynamic_group}"
1192
+
1193
+ def clean(self):
1194
+ super().clean()
1195
+
1196
+ if self.associated_object_type != self.dynamic_group.content_type:
1197
+ raise ValidationError({"associated_object_type": "Must match the dynamic_group.content_type"})
1198
+
1199
+ def to_objectchange(self, *args, **kwargs):
1200
+ """Change log StaticGroupAssociations belonging to a "static" group; all others are an implementation detail."""
1201
+ if self.dynamic_group.group_type != DynamicGroupTypeChoices.TYPE_STATIC:
1202
+ return None
1203
+ return super().to_objectchange(*args, **kwargs)