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

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

Potentially problematic release.


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

Files changed (697) hide show
  1. nautobot/apps/forms.py +4 -0
  2. nautobot/apps/models.py +10 -1
  3. nautobot/circuits/__init__.py +0 -1
  4. nautobot/circuits/apps.py +1 -0
  5. nautobot/circuits/factory.py +15 -3
  6. nautobot/circuits/filters.py +13 -0
  7. nautobot/circuits/forms.py +13 -0
  8. nautobot/circuits/migrations/0021_alter_circuit_status_alter_circuittermination__path.py +32 -0
  9. nautobot/circuits/migrations/0022_circuittermination_cloud_network.py +25 -0
  10. nautobot/circuits/models.py +16 -3
  11. nautobot/circuits/tables.py +16 -2
  12. nautobot/circuits/templates/circuits/circuittermination_create.html +10 -2
  13. nautobot/circuits/templates/circuits/circuittermination_retrieve.html +6 -0
  14. nautobot/circuits/templates/circuits/inc/circuit_termination.html +6 -1
  15. nautobot/circuits/tests/test_api.py +7 -5
  16. nautobot/circuits/tests/test_filters.py +12 -5
  17. nautobot/circuits/tests/test_models.py +33 -2
  18. nautobot/circuits/views.py +2 -3
  19. nautobot/cloud/__init__.py +0 -0
  20. nautobot/cloud/api/__init__.py +0 -0
  21. nautobot/cloud/api/serializers.py +54 -0
  22. nautobot/cloud/api/urls.py +16 -0
  23. nautobot/cloud/api/views.py +48 -0
  24. nautobot/cloud/apps.py +13 -0
  25. nautobot/cloud/factory.py +111 -0
  26. nautobot/cloud/filters.py +184 -0
  27. nautobot/cloud/forms.py +333 -0
  28. nautobot/cloud/homepage.py +43 -0
  29. nautobot/cloud/migrations/0001_initial.py +304 -0
  30. nautobot/cloud/migrations/__init__.py +0 -0
  31. nautobot/cloud/models.py +247 -0
  32. nautobot/cloud/navigation.py +85 -0
  33. nautobot/cloud/tables.py +173 -0
  34. nautobot/cloud/templates/cloud/cloudaccount_retrieve.html +43 -0
  35. nautobot/cloud/templates/cloud/cloudnetwork_retrieve.html +128 -0
  36. nautobot/cloud/templates/cloud/cloudnetwork_update.html +33 -0
  37. nautobot/cloud/templates/cloud/cloudresourcetype_retrieve.html +100 -0
  38. nautobot/cloud/templates/cloud/cloudservice_retrieve.html +65 -0
  39. nautobot/cloud/templates/cloud/cloudservice_update.html +25 -0
  40. nautobot/cloud/tests/__init__.py +0 -0
  41. nautobot/cloud/tests/test_api.py +248 -0
  42. nautobot/cloud/tests/test_filters.py +113 -0
  43. nautobot/cloud/tests/test_models.py +43 -0
  44. nautobot/cloud/tests/test_views.py +153 -0
  45. nautobot/cloud/urls.py +14 -0
  46. nautobot/cloud/views.py +181 -0
  47. nautobot/core/__init__.py +0 -3
  48. nautobot/core/api/metadata.py +1 -0
  49. nautobot/core/api/parsers.py +7 -1
  50. nautobot/core/api/urls.py +1 -0
  51. nautobot/core/api/utils.py +1 -0
  52. nautobot/core/api/views.py +4 -0
  53. nautobot/core/apps/__init__.py +6 -3
  54. nautobot/core/constants.py +8 -0
  55. nautobot/core/factory.py +32 -1
  56. nautobot/core/filters.py +96 -28
  57. nautobot/core/forms/fields.py +10 -4
  58. nautobot/core/forms/forms.py +1 -1
  59. nautobot/core/forms/widgets.py +18 -1
  60. nautobot/core/graphql/generators.py +2 -2
  61. nautobot/core/graphql/schema.py +34 -4
  62. nautobot/core/jobs/__init__.py +17 -6
  63. nautobot/core/jobs/cleanup.py +100 -0
  64. nautobot/core/jobs/groups.py +38 -0
  65. nautobot/core/management/commands/generate_test_data.py +116 -3
  66. nautobot/core/models/__init__.py +34 -9
  67. nautobot/core/models/generics.py +19 -3
  68. nautobot/core/models/name_color_content_types.py +7 -28
  69. nautobot/core/models/querysets.py +4 -3
  70. nautobot/core/models/tree_queries.py +1 -1
  71. nautobot/core/models/utils.py +21 -5
  72. nautobot/core/settings.py +4 -30
  73. nautobot/core/settings.yaml +34 -27
  74. nautobot/core/settings_funcs.py +103 -0
  75. nautobot/core/tables.py +127 -56
  76. nautobot/core/templates/admin/search_form.html +1 -1
  77. nautobot/core/templates/buttons/add.html +11 -3
  78. nautobot/core/templates/buttons/consolidated_bulk_action_buttons.html +13 -0
  79. nautobot/core/templates/buttons/consolidated_detail_view_action_buttons.html +13 -0
  80. nautobot/core/templates/buttons/export.html +101 -53
  81. nautobot/core/templates/buttons/job_import.html +11 -3
  82. nautobot/core/templates/generic/object_bulk_destroy.html +3 -1
  83. nautobot/core/templates/generic/object_bulk_update.html +3 -1
  84. nautobot/core/templates/generic/object_changelog.html +0 -9
  85. nautobot/core/templates/generic/object_list.html +156 -17
  86. nautobot/core/templates/generic/object_retrieve.html +80 -16
  87. nautobot/core/templates/inc/extras_features_edit_form_fields.html +8 -0
  88. nautobot/core/templates/inc/javascript.html +2 -0
  89. nautobot/core/templates/inc/media.html +2 -2
  90. nautobot/core/templates/inc/nav_menu.html +1 -0
  91. nautobot/core/templates/inc/paginator.html +7 -7
  92. nautobot/core/templates/inc/search_panel.html +2 -2
  93. nautobot/core/templates/inc/table.html +2 -2
  94. nautobot/core/templates/nautobot_config.py.j2 +13 -23
  95. nautobot/core/templates/utilities/templatetags/dynamic_group_assignment_modal.html +37 -0
  96. nautobot/core/templates/utilities/templatetags/filter_form_modal.html +2 -2
  97. nautobot/core/templates/utilities/templatetags/saved_view_modal.html +38 -0
  98. nautobot/core/templates/utilities/theme_preview.html +25 -8
  99. nautobot/core/templates/utilities/worker_status.html +152 -0
  100. nautobot/core/templatetags/buttons.py +335 -38
  101. nautobot/core/templatetags/form_helpers.py +1 -1
  102. nautobot/core/templatetags/helpers.py +181 -11
  103. nautobot/core/testing/api.py +5 -4
  104. nautobot/core/testing/filters.py +51 -13
  105. nautobot/core/testing/mixins.py +46 -0
  106. nautobot/core/testing/models.py +22 -0
  107. nautobot/core/testing/schema.py +4 -8
  108. nautobot/core/testing/views.py +31 -14
  109. nautobot/core/tests/integration/test_general_functionality.py +1 -1
  110. nautobot/core/tests/integration/test_import_objects_ui.py +1 -0
  111. nautobot/core/tests/integration/test_swagger.py +1 -1
  112. nautobot/core/tests/nautobot_config.py +0 -1
  113. nautobot/core/tests/runner.py +2 -2
  114. nautobot/core/tests/test_api.py +1 -0
  115. nautobot/core/tests/test_authentication.py +7 -2
  116. nautobot/core/tests/test_filters.py +11 -9
  117. nautobot/core/tests/test_forms.py +9 -0
  118. nautobot/core/tests/test_graphql.py +27 -16
  119. nautobot/core/tests/test_jobs.py +123 -74
  120. nautobot/core/tests/test_tables.py +3 -1
  121. nautobot/core/tests/test_templatetags_helpers.py +12 -5
  122. nautobot/core/tests/test_utils.py +31 -20
  123. nautobot/core/tests/test_views.py +6 -6
  124. nautobot/core/urls.py +8 -3
  125. nautobot/core/utils/deprecation.py +29 -0
  126. nautobot/core/utils/filtering.py +12 -9
  127. nautobot/core/utils/lookup.py +37 -2
  128. nautobot/core/utils/requests.py +4 -1
  129. nautobot/core/views/__init__.py +137 -24
  130. nautobot/core/views/generic.py +118 -66
  131. nautobot/core/views/mixins.py +104 -35
  132. nautobot/core/views/paginator.py +9 -3
  133. nautobot/core/views/renderers.py +121 -56
  134. nautobot/core/views/utils.py +79 -1
  135. nautobot/dcim/__init__.py +0 -1
  136. nautobot/dcim/api/serializers.py +180 -44
  137. nautobot/dcim/api/urls.py +7 -3
  138. nautobot/dcim/api/views.py +53 -7
  139. nautobot/dcim/apps.py +3 -0
  140. nautobot/dcim/choices.py +25 -0
  141. nautobot/dcim/constants.py +7 -0
  142. nautobot/dcim/factory.py +249 -18
  143. nautobot/dcim/filters/__init__.py +369 -193
  144. nautobot/dcim/filters/mixins.py +274 -1
  145. nautobot/dcim/forms.py +817 -109
  146. nautobot/dcim/graphql/types.py +2 -2
  147. nautobot/dcim/homepage.py +1 -1
  148. nautobot/dcim/migrations/0059_add_role_field_to_interface_models.py +27 -0
  149. nautobot/dcim/migrations/0060_alter_cable_status_alter_consoleport__path_and_more.py +303 -0
  150. nautobot/dcim/migrations/0061_module_models.py +861 -0
  151. nautobot/dcim/migrations/0062_module_data_migration.py +25 -0
  152. nautobot/dcim/models/__init__.py +8 -0
  153. nautobot/dcim/models/cables.py +15 -0
  154. nautobot/dcim/models/device_component_templates.py +207 -53
  155. nautobot/dcim/models/device_components.py +275 -106
  156. nautobot/dcim/models/devices.py +466 -13
  157. nautobot/dcim/navigation.py +47 -0
  158. nautobot/dcim/signals.py +3 -3
  159. nautobot/dcim/tables/__init__.py +35 -23
  160. nautobot/dcim/tables/devices.py +231 -59
  161. nautobot/dcim/tables/devicetypes.py +65 -9
  162. nautobot/dcim/tables/racks.py +5 -1
  163. nautobot/dcim/tables/template_code.py +46 -26
  164. nautobot/dcim/templates/dcim/cable_connect.html +76 -3
  165. nautobot/dcim/templates/dcim/console_port_connection_list.html +7 -5
  166. nautobot/dcim/templates/dcim/device/base.html +15 -7
  167. nautobot/dcim/templates/dcim/device/consoleports.html +2 -3
  168. nautobot/dcim/templates/dcim/device/consoleserverports.html +2 -3
  169. nautobot/dcim/templates/dcim/device/devicebays.html +6 -7
  170. nautobot/dcim/templates/dcim/device/frontports.html +2 -3
  171. nautobot/dcim/templates/dcim/device/interfaces.html +2 -3
  172. nautobot/dcim/templates/dcim/device/inventory.html +2 -3
  173. nautobot/dcim/templates/dcim/device/modulebays.html +49 -0
  174. nautobot/dcim/templates/dcim/device/poweroutlets.html +2 -3
  175. nautobot/dcim/templates/dcim/device/powerports.html +2 -3
  176. nautobot/dcim/templates/dcim/device/rearports.html +2 -3
  177. nautobot/dcim/templates/dcim/device.html +45 -1
  178. nautobot/dcim/templates/dcim/device_component.html +13 -5
  179. nautobot/dcim/templates/dcim/device_list.html +2 -1
  180. nautobot/dcim/templates/dcim/deviceredundancygroup_retrieve.html +0 -6
  181. nautobot/dcim/templates/dcim/devicetype.html +99 -98
  182. nautobot/dcim/templates/dcim/devicetype_list.html +8 -16
  183. nautobot/dcim/templates/dcim/inc/devicetype_component_table.html +1 -1
  184. nautobot/dcim/templates/dcim/inc/moduletype_component_table.html +39 -0
  185. nautobot/dcim/templates/dcim/interface.html +17 -2
  186. nautobot/dcim/templates/dcim/interface_connection_list.html +7 -5
  187. nautobot/dcim/templates/dcim/interface_edit.html +1 -0
  188. nautobot/dcim/templates/dcim/manufacturer.html +24 -0
  189. nautobot/dcim/templates/dcim/module/base.html +97 -0
  190. nautobot/dcim/templates/dcim/module_bulk_destroy.html +5 -0
  191. nautobot/dcim/templates/dcim/module_consoleports.html +53 -0
  192. nautobot/dcim/templates/dcim/module_consoleserverports.html +53 -0
  193. nautobot/dcim/templates/dcim/module_destroy.html +5 -0
  194. nautobot/dcim/templates/dcim/module_frontports.html +53 -0
  195. nautobot/dcim/templates/dcim/module_interfaces.html +57 -0
  196. nautobot/dcim/templates/dcim/module_list.html +20 -0
  197. nautobot/dcim/templates/dcim/module_modulebays.html +49 -0
  198. nautobot/dcim/templates/dcim/module_poweroutlets.html +53 -0
  199. nautobot/dcim/templates/dcim/module_powerports.html +53 -0
  200. nautobot/dcim/templates/dcim/module_rearports.html +53 -0
  201. nautobot/dcim/templates/dcim/module_retrieve.html +63 -0
  202. nautobot/dcim/templates/dcim/module_update.html +71 -0
  203. nautobot/dcim/templates/dcim/modulebay_bulk_destroy.html +5 -0
  204. nautobot/dcim/templates/dcim/modulebay_destroy.html +8 -0
  205. nautobot/dcim/templates/dcim/modulebay_retrieve.html +101 -0
  206. nautobot/dcim/templates/dcim/moduletype_list.html +11 -0
  207. nautobot/dcim/templates/dcim/moduletype_retrieve.html +142 -0
  208. nautobot/dcim/templates/dcim/power_port_connection_list.html +7 -5
  209. nautobot/dcim/templates/dcim/softwareimagefile_retrieve.html +65 -19
  210. nautobot/dcim/tests/integration/test_cable_connect_form.py +4 -4
  211. nautobot/dcim/tests/test_api.py +691 -208
  212. nautobot/dcim/tests/test_filters.py +836 -217
  213. nautobot/dcim/tests/test_models.py +1072 -39
  214. nautobot/dcim/tests/test_views.py +1488 -358
  215. nautobot/dcim/urls.py +17 -2
  216. nautobot/dcim/utils.py +2 -3
  217. nautobot/dcim/views.py +1107 -120
  218. nautobot/extras/__init__.py +0 -1
  219. nautobot/extras/api/serializers.py +115 -3
  220. nautobot/extras/api/urls.py +12 -0
  221. nautobot/extras/api/views.py +125 -7
  222. nautobot/extras/apps.py +2 -2
  223. nautobot/extras/choices.py +43 -0
  224. nautobot/extras/context_managers.py +13 -8
  225. nautobot/extras/datasources/git.py +2 -0
  226. nautobot/extras/factory.py +422 -9
  227. nautobot/extras/filters/__init__.py +174 -3
  228. nautobot/extras/filters/mixins.py +46 -43
  229. nautobot/extras/forms/base.py +17 -4
  230. nautobot/extras/forms/forms.py +227 -8
  231. nautobot/extras/forms/mixins.py +93 -0
  232. nautobot/extras/graphql/types.py +23 -10
  233. nautobot/extras/homepage.py +16 -13
  234. nautobot/extras/jobs.py +2 -2
  235. nautobot/extras/management/__init__.py +1 -0
  236. nautobot/extras/management/commands/refresh_dynamic_group_member_caches.py +1 -16
  237. nautobot/extras/migrations/0021_customfield_changelog_data.py +1 -0
  238. nautobot/extras/migrations/0109_dynamicgroup_group_type_dynamicgroup_tags_and_more.py +108 -0
  239. nautobot/extras/migrations/0110_alter_configcontext_cluster_groups_and_more.py +111 -0
  240. nautobot/extras/migrations/0111_metadata.py +162 -0
  241. nautobot/extras/migrations/0112_dynamic_group_group_type_data_migration.py +28 -0
  242. nautobot/extras/migrations/0113_saved_views.py +77 -0
  243. nautobot/extras/models/__init__.py +15 -1
  244. nautobot/extras/models/change_logging.py +3 -3
  245. nautobot/extras/models/contacts.py +4 -0
  246. nautobot/extras/models/customfields.py +18 -3
  247. nautobot/extras/models/groups.py +389 -225
  248. nautobot/extras/models/jobs.py +4 -84
  249. nautobot/extras/models/metadata.py +441 -0
  250. nautobot/extras/models/mixins.py +72 -62
  251. nautobot/extras/models/models.py +116 -9
  252. nautobot/extras/models/relationships.py +9 -2
  253. nautobot/extras/models/tags.py +13 -2
  254. nautobot/extras/navigation.py +57 -0
  255. nautobot/extras/plugins/__init__.py +3 -1
  256. nautobot/extras/querysets.py +30 -66
  257. nautobot/extras/signals.py +96 -114
  258. nautobot/extras/tables.py +171 -47
  259. nautobot/extras/templates/extras/dynamicgroup.html +44 -15
  260. nautobot/extras/templates/extras/dynamicgroup_edit.html +2 -0
  261. nautobot/extras/templates/extras/job.html +1 -1
  262. nautobot/extras/templates/extras/job_detail.html +0 -11
  263. nautobot/extras/templates/extras/jobresult.html +61 -74
  264. nautobot/extras/templates/extras/metadatatype_create.html +89 -0
  265. nautobot/extras/templates/extras/metadatatype_retrieve.html +67 -0
  266. nautobot/extras/templates/extras/object_dynamicgroups.html +7 -0
  267. nautobot/extras/templates/extras/objectchange_list.html +0 -12
  268. nautobot/extras/templates/extras/plugins_list.html +1 -3
  269. nautobot/extras/templates/extras/role_retrieve.html +48 -0
  270. nautobot/extras/templates/extras/staticgroupassociation_retrieve.html +20 -0
  271. nautobot/extras/tests/integration/test_customfields.py +1 -0
  272. nautobot/extras/tests/test_api.py +501 -22
  273. nautobot/extras/tests/test_changelog.py +20 -9
  274. nautobot/extras/tests/test_context_managers.py +22 -15
  275. nautobot/extras/tests/test_datasources.py +13 -1
  276. nautobot/extras/tests/test_dynamicgroups.py +201 -171
  277. nautobot/extras/tests/test_filters.py +211 -12
  278. nautobot/extras/tests/test_jobs.py +4 -4
  279. nautobot/extras/tests/test_models.py +499 -4
  280. nautobot/extras/tests/test_relationships.py +1 -0
  281. nautobot/extras/tests/test_views.py +565 -28
  282. nautobot/extras/tests/test_webhooks.py +1 -1
  283. nautobot/extras/urls.py +5 -0
  284. nautobot/extras/utils.py +56 -45
  285. nautobot/extras/views.py +585 -96
  286. nautobot/ipam/__init__.py +0 -1
  287. nautobot/ipam/apps.py +1 -0
  288. nautobot/ipam/factory.py +17 -19
  289. nautobot/ipam/filters.py +14 -1
  290. nautobot/ipam/forms.py +9 -5
  291. nautobot/ipam/graphql/types.py +2 -2
  292. nautobot/ipam/migrations/0047_alter_ipaddress_role_alter_ipaddress_status_and_more.py +59 -0
  293. nautobot/ipam/models.py +23 -9
  294. nautobot/ipam/querysets.py +1 -1
  295. nautobot/ipam/signals.py +4 -2
  296. nautobot/ipam/tables.py +1 -0
  297. nautobot/ipam/templates/ipam/ipaddress_interfaces.html +1 -1
  298. nautobot/ipam/templates/ipam/ipaddress_vm_interfaces.html +1 -1
  299. nautobot/ipam/templates/ipam/prefix.html +1 -0
  300. nautobot/ipam/tests/test_api.py +37 -18
  301. nautobot/ipam/tests/test_filters.py +26 -2
  302. nautobot/ipam/tests/test_models.py +8 -3
  303. nautobot/ipam/tests/test_querysets.py +1 -1
  304. nautobot/ipam/tests/test_views.py +3 -2
  305. nautobot/ipam/urls.py +2 -2
  306. nautobot/ipam/views.py +25 -28
  307. nautobot/project-static/css/base.css +20 -1
  308. nautobot/project-static/css/dark.css +11 -0
  309. nautobot/project-static/docs/404.html +884 -80
  310. nautobot/project-static/docs/apps/index.html +884 -80
  311. nautobot/project-static/docs/apps/nautobot-apps.html +884 -80
  312. nautobot/project-static/docs/assets/_mkdocstrings.css +5 -0
  313. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +911 -112
  314. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +896 -93
  315. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +1457 -790
  316. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +927 -136
  317. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +969 -180
  318. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +893 -91
  319. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +889 -85
  320. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +983 -185
  321. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +938 -143
  322. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +1064 -274
  323. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +1190 -346
  324. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +1663 -865
  325. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +1156 -373
  326. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +2200 -1502
  327. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +2229 -1421
  328. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +904 -103
  329. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +955 -155
  330. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +1002 -215
  331. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +1911 -1275
  332. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +1835 -1091
  333. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +896 -93
  334. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +2323 -1693
  335. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +1785 -1023
  336. nautobot/project-static/docs/development/apps/api/configuration-view.html +884 -80
  337. nautobot/project-static/docs/development/apps/api/database-backend-config.html +884 -80
  338. nautobot/project-static/docs/development/apps/api/models/django-admin.html +884 -80
  339. nautobot/project-static/docs/development/apps/api/models/global-search.html +884 -80
  340. nautobot/project-static/docs/development/apps/api/models/graphql.html +884 -80
  341. nautobot/project-static/docs/development/apps/api/models/index.html +922 -81
  342. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +884 -80
  343. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +884 -80
  344. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +884 -80
  345. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +884 -80
  346. nautobot/project-static/docs/development/apps/api/platform-features/index.html +884 -80
  347. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +884 -80
  348. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +884 -80
  349. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +884 -80
  350. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +884 -80
  351. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +884 -80
  352. nautobot/project-static/docs/development/apps/api/prometheus.html +884 -80
  353. nautobot/project-static/docs/development/apps/api/setup.html +884 -80
  354. nautobot/project-static/docs/development/apps/api/testing.html +884 -80
  355. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +884 -80
  356. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +884 -80
  357. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +884 -80
  358. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +884 -80
  359. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +884 -80
  360. nautobot/project-static/docs/development/apps/api/views/base-template.html +884 -80
  361. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +884 -80
  362. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +884 -80
  363. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +884 -80
  364. nautobot/project-static/docs/development/apps/api/views/index.html +884 -80
  365. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +884 -80
  366. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +884 -80
  367. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +884 -80
  368. nautobot/project-static/docs/development/apps/api/views/notes.html +884 -80
  369. nautobot/project-static/docs/development/apps/api/views/rest-api.html +884 -80
  370. nautobot/project-static/docs/development/apps/api/views/urls.html +884 -80
  371. nautobot/project-static/docs/development/apps/index.html +884 -80
  372. nautobot/project-static/docs/development/apps/migration/code-updates.html +884 -80
  373. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +884 -80
  374. nautobot/project-static/docs/development/apps/migration/from-v1.html +884 -80
  375. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +884 -80
  376. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +884 -80
  377. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +884 -80
  378. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +884 -80
  379. nautobot/project-static/docs/development/apps/porting-from-netbox.html +884 -80
  380. nautobot/project-static/docs/development/core/application-registry.html +884 -80
  381. nautobot/project-static/docs/development/core/best-practices.html +885 -80
  382. nautobot/project-static/docs/development/core/bootstrap-ui.html +884 -80
  383. nautobot/project-static/docs/development/core/caching.html +884 -80
  384. nautobot/project-static/docs/development/core/controllers.html +884 -80
  385. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +884 -80
  386. nautobot/project-static/docs/development/core/generic-views.html +884 -80
  387. nautobot/project-static/docs/development/core/getting-started.html +884 -80
  388. nautobot/project-static/docs/development/core/homepage.html +884 -80
  389. nautobot/project-static/docs/development/core/index.html +884 -91
  390. nautobot/project-static/docs/development/core/model-checklist.html +887 -81
  391. nautobot/project-static/docs/development/core/model-features.html +884 -80
  392. nautobot/project-static/docs/development/core/natural-keys.html +884 -80
  393. nautobot/project-static/docs/development/core/navigation-menu.html +884 -80
  394. nautobot/project-static/docs/development/core/release-checklist.html +887 -83
  395. nautobot/project-static/docs/development/core/role-internals.html +884 -80
  396. nautobot/project-static/docs/development/core/settings.html +884 -80
  397. nautobot/project-static/docs/development/core/style-guide.html +885 -81
  398. nautobot/project-static/docs/development/core/templates.html +896 -81
  399. nautobot/project-static/docs/development/core/testing.html +884 -80
  400. nautobot/project-static/docs/development/core/user-preferences.html +884 -80
  401. nautobot/project-static/docs/development/index.html +884 -80
  402. nautobot/project-static/docs/development/jobs/index.html +1247 -457
  403. nautobot/project-static/docs/development/jobs/migration/from-v1.html +884 -80
  404. nautobot/project-static/docs/index.html +13 -8228
  405. nautobot/project-static/docs/media/models/cloud_aws_direct_connect_dark.png +0 -0
  406. nautobot/project-static/docs/media/models/cloud_aws_direct_connect_light.png +0 -0
  407. nautobot/project-static/docs/models/cloud/cloudaccount.html +15 -0
  408. nautobot/project-static/docs/models/cloud/cloudnetwork.html +15 -0
  409. nautobot/project-static/docs/models/cloud/cloudnetworkprefixassignment.html +15 -0
  410. nautobot/project-static/docs/models/cloud/cloudresourcetype.html +15 -0
  411. nautobot/project-static/docs/models/cloud/cloudservice.html +15 -0
  412. nautobot/project-static/docs/models/cloud/cloudservicenetworkassignment.html +15 -0
  413. nautobot/project-static/docs/models/dcim/module.html +15 -0
  414. nautobot/project-static/docs/models/dcim/modulebay.html +15 -0
  415. nautobot/project-static/docs/models/dcim/modulebaytemplate.html +15 -0
  416. nautobot/project-static/docs/models/dcim/moduletype.html +15 -0
  417. nautobot/project-static/docs/models/extras/metadatachoice.html +15 -0
  418. nautobot/project-static/docs/models/extras/metadatatype.html +15 -0
  419. nautobot/project-static/docs/models/extras/objectmetadata.html +15 -0
  420. nautobot/project-static/docs/models/extras/role.html +15 -0
  421. nautobot/project-static/docs/models/extras/savedview.html +15 -0
  422. nautobot/project-static/docs/models/extras/staticgroupassociation.html +15 -0
  423. nautobot/project-static/docs/models/extras/status.html +15 -0
  424. nautobot/project-static/docs/objects.inv +0 -0
  425. nautobot/project-static/docs/overview/application_stack.html +892 -81
  426. nautobot/project-static/docs/overview/design_philosophy.html +886 -82
  427. nautobot/project-static/docs/overview/index.html +9032 -13
  428. nautobot/project-static/docs/release-notes/index.html +887 -83
  429. nautobot/project-static/docs/release-notes/version-1.0.html +884 -80
  430. nautobot/project-static/docs/release-notes/version-1.1.html +884 -80
  431. nautobot/project-static/docs/release-notes/version-1.2.html +884 -80
  432. nautobot/project-static/docs/release-notes/version-1.3.html +884 -80
  433. nautobot/project-static/docs/release-notes/version-1.4.html +884 -80
  434. nautobot/project-static/docs/release-notes/version-1.5.html +885 -81
  435. nautobot/project-static/docs/release-notes/version-1.6.html +885 -81
  436. nautobot/project-static/docs/release-notes/version-2.0.html +884 -80
  437. nautobot/project-static/docs/release-notes/version-2.1.html +884 -80
  438. nautobot/project-static/docs/release-notes/version-2.2.html +990 -323
  439. nautobot/project-static/docs/release-notes/version-2.3.html +9524 -0
  440. nautobot/project-static/docs/requirements.txt +4 -4
  441. nautobot/project-static/docs/search/search_index.json +1 -1
  442. nautobot/project-static/docs/sitemap.xml +335 -260
  443. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  444. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +884 -80
  445. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +884 -80
  446. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +884 -80
  447. nautobot/project-static/docs/user-guide/administration/configuration/index.html +884 -80
  448. nautobot/project-static/docs/user-guide/administration/configuration/optional-settings.html +983 -197
  449. nautobot/project-static/docs/user-guide/administration/configuration/required-settings.html +884 -80
  450. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +884 -80
  451. nautobot/project-static/docs/user-guide/administration/guides/caching.html +884 -80
  452. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +888 -84
  453. nautobot/project-static/docs/user-guide/administration/guides/healthcheck.html +884 -80
  454. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +884 -80
  455. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +884 -80
  456. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +884 -80
  457. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +884 -80
  458. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +884 -80
  459. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +884 -80
  460. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +884 -80
  461. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +884 -80
  462. nautobot/project-static/docs/user-guide/administration/installation/index.html +888 -80
  463. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +884 -80
  464. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +884 -80
  465. nautobot/project-static/docs/user-guide/administration/installation/services.html +888 -80
  466. nautobot/project-static/docs/user-guide/administration/installation-extras/docker.html +900 -91
  467. nautobot/project-static/docs/user-guide/administration/installation-extras/health-checks.html +884 -80
  468. nautobot/project-static/docs/user-guide/administration/installation-extras/selinux-troubleshooting.html +884 -80
  469. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +884 -80
  470. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +884 -80
  471. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +915 -163
  472. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +884 -80
  473. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +884 -80
  474. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +884 -80
  475. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +884 -80
  476. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +884 -80
  477. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +884 -80
  478. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +884 -80
  479. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +884 -80
  480. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +884 -80
  481. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +884 -80
  482. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +884 -80
  483. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +885 -81
  484. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +884 -80
  485. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +888 -80
  486. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +887 -83
  487. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +8984 -0
  488. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +8828 -0
  489. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +8829 -0
  490. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +8828 -0
  491. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +8829 -0
  492. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +8833 -0
  493. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +8828 -0
  494. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +898 -94
  495. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +915 -97
  496. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +915 -97
  497. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +910 -92
  498. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +915 -97
  499. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +898 -94
  500. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +898 -94
  501. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +905 -97
  502. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +912 -108
  503. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +913 -109
  504. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +910 -106
  505. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +898 -94
  506. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +906 -97
  507. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +918 -100
  508. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +928 -110
  509. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +920 -98
  510. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +898 -94
  511. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +929 -111
  512. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +920 -102
  513. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +910 -106
  514. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +913 -109
  515. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +914 -106
  516. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +8828 -0
  517. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +8846 -0
  518. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +8843 -0
  519. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +8823 -0
  520. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +908 -104
  521. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +898 -94
  522. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +932 -75
  523. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +916 -98
  524. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +898 -94
  525. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +935 -78
  526. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +913 -95
  527. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +921 -117
  528. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +910 -106
  529. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +898 -94
  530. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +914 -96
  531. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +916 -98
  532. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +898 -94
  533. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +898 -94
  534. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +898 -94
  535. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +884 -80
  536. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +884 -80
  537. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +889 -81
  538. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +889 -81
  539. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +884 -80
  540. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +884 -80
  541. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +884 -80
  542. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +884 -80
  543. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +884 -80
  544. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +884 -80
  545. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +884 -80
  546. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +884 -80
  547. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +884 -80
  548. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +884 -80
  549. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +893 -88
  550. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +884 -80
  551. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +884 -80
  552. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +884 -80
  553. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +884 -80
  554. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +884 -80
  555. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +889 -81
  556. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +884 -80
  557. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +884 -80
  558. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +884 -80
  559. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +884 -80
  560. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +884 -80
  561. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +884 -80
  562. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +884 -80
  563. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +884 -80
  564. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +884 -80
  565. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +884 -80
  566. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +884 -80
  567. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +884 -80
  568. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +884 -80
  569. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/clear-view-button.png +0 -0
  570. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/cleared-view.png +0 -0
  571. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/config-table-columns-to-locations.png +0 -0
  572. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/configure-button.png +0 -0
  573. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/create-saved-view-success.png +0 -0
  574. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/current-saved-view-drop-down-menu.png +0 -0
  575. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/default-location-list-view.png +0 -0
  576. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/dropdown-button-after-new-saved-view.png +0 -0
  577. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/filter-application-to-locations.png +0 -0
  578. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/filter-button.png +0 -0
  579. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/global-default-location-list-view.png +0 -0
  580. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/location-list-view-with-saved-views.png +0 -0
  581. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/navigation-menu.png +0 -0
  582. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/save-as-new-view-drop-down.png +0 -0
  583. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/save-view-modal.png +0 -0
  584. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-buttons.png +0 -0
  585. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-success.png +0 -0
  586. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-view-unchecked.png +0 -0
  587. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-view.png +0 -0
  588. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-different-user.png +0 -0
  589. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-modal-unchecked.png +0 -0
  590. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/set-as-my-default-button.png +0 -0
  591. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/set-as-my-default-success.png +0 -0
  592. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/unsaved-saved-view.png +0 -0
  593. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/updated-saved-view.png +0 -0
  594. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +884 -80
  595. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +884 -80
  596. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +884 -80
  597. nautobot/project-static/docs/user-guide/index.html +884 -80
  598. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +884 -80
  599. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +884 -80
  600. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +884 -80
  601. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +884 -80
  602. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +1250 -777
  603. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +887 -83
  604. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +884 -80
  605. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +884 -80
  606. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +884 -80
  607. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +884 -80
  608. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +884 -80
  609. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +884 -80
  610. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +887 -83
  611. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +884 -80
  612. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +884 -80
  613. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +888 -80
  614. nautobot/project-static/docs/user-guide/platform-functionality/metadata.html +8948 -0
  615. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +887 -83
  616. nautobot/project-static/docs/user-guide/platform-functionality/note.html +884 -80
  617. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +884 -80
  618. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +884 -80
  619. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +884 -80
  620. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +884 -80
  621. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +884 -80
  622. nautobot/project-static/docs/user-guide/platform-functionality/role.html +887 -83
  623. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +9137 -0
  624. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +887 -83
  625. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +8933 -0
  626. nautobot/project-static/docs/user-guide/platform-functionality/status.html +884 -80
  627. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +884 -80
  628. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +942 -113
  629. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +884 -80
  630. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +884 -80
  631. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +884 -80
  632. nautobot/project-static/js/forms.js +71 -0
  633. nautobot/project-static/js/table_sorting_indicator.js +46 -0
  634. nautobot/project-static/js/tableconfig.js +6 -1
  635. nautobot/project-static/materialdesignicons-7.4.47/css/materialdesignicons.min.css +3 -0
  636. nautobot/project-static/{materialdesignicons-6.5.95 → materialdesignicons-7.4.47}/fonts/materialdesignicons-webfont.eot +0 -0
  637. nautobot/project-static/{materialdesignicons-6.5.95 → materialdesignicons-7.4.47}/fonts/materialdesignicons-webfont.ttf +0 -0
  638. nautobot/project-static/materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.woff +0 -0
  639. nautobot/project-static/materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.woff2 +0 -0
  640. nautobot/tenancy/__init__.py +0 -1
  641. nautobot/tenancy/apps.py +1 -0
  642. nautobot/tenancy/factory.py +3 -2
  643. nautobot/tenancy/filters/__init__.py +1 -0
  644. nautobot/tenancy/forms.py +1 -1
  645. nautobot/tenancy/templates/tenancy/tenant.html +22 -18
  646. nautobot/tenancy/views.py +11 -10
  647. nautobot/users/__init__.py +0 -1
  648. nautobot/users/api/serializers.py +1 -1
  649. nautobot/users/api/views.py +4 -2
  650. nautobot/users/apps.py +3 -2
  651. nautobot/users/factory.py +3 -3
  652. nautobot/users/migrations/0010_user_default_saved_views.py +20 -0
  653. nautobot/users/models.py +12 -0
  654. nautobot/users/tests/test_filters.py +6 -3
  655. nautobot/users/urls.py +8 -0
  656. nautobot/virtualization/__init__.py +0 -1
  657. nautobot/virtualization/apps.py +1 -0
  658. nautobot/virtualization/filters.py +6 -1
  659. nautobot/virtualization/forms.py +11 -3
  660. nautobot/virtualization/graphql/types.py +2 -2
  661. nautobot/virtualization/migrations/0029_add_role_field_to_interface_models.py +27 -0
  662. nautobot/virtualization/migrations/0030_alter_virtualmachine_local_config_context_data_owner_content_type_and_more.py +67 -0
  663. nautobot/virtualization/tables.py +15 -5
  664. nautobot/virtualization/templates/virtualization/virtualmachine.html +1 -1
  665. nautobot/virtualization/templates/virtualization/vminterface.html +7 -1
  666. nautobot/virtualization/templates/virtualization/vminterface_edit.html +1 -0
  667. nautobot/virtualization/tests/test_api.py +9 -4
  668. nautobot/virtualization/tests/test_filters.py +22 -0
  669. nautobot/virtualization/tests/test_models.py +7 -3
  670. nautobot/virtualization/tests/test_views.py +19 -3
  671. nautobot/virtualization/urls.py +2 -2
  672. nautobot/virtualization/views.py +10 -32
  673. {nautobot-2.2.9.dist-info → nautobot-2.3.0b1.dist-info}/METADATA +21 -19
  674. {nautobot-2.2.9.dist-info → nautobot-2.3.0b1.dist-info}/RECORD +679 -559
  675. nautobot/project-static/materialdesignicons-6.5.95/.github/ISSUE_TEMPLATE.md +0 -3
  676. nautobot/project-static/materialdesignicons-6.5.95/README.md +0 -25
  677. nautobot/project-static/materialdesignicons-6.5.95/css/materialdesignicons.css +0 -26654
  678. nautobot/project-static/materialdesignicons-6.5.95/css/materialdesignicons.css.map +0 -16
  679. nautobot/project-static/materialdesignicons-6.5.95/css/materialdesignicons.min.css +0 -3
  680. nautobot/project-static/materialdesignicons-6.5.95/css/materialdesignicons.min.css.map +0 -16
  681. nautobot/project-static/materialdesignicons-6.5.95/fonts/materialdesignicons-webfont.woff +0 -0
  682. nautobot/project-static/materialdesignicons-6.5.95/fonts/materialdesignicons-webfont.woff2 +0 -0
  683. nautobot/project-static/materialdesignicons-6.5.95/package.json +0 -28
  684. nautobot/project-static/materialdesignicons-6.5.95/preview.html +0 -717
  685. nautobot/project-static/materialdesignicons-6.5.95/scss/_animated.scss +0 -27
  686. nautobot/project-static/materialdesignicons-6.5.95/scss/_core.scss +0 -10
  687. nautobot/project-static/materialdesignicons-6.5.95/scss/_extras.scss +0 -65
  688. nautobot/project-static/materialdesignicons-6.5.95/scss/_functions.scss +0 -20
  689. nautobot/project-static/materialdesignicons-6.5.95/scss/_icons.scss +0 -10
  690. nautobot/project-static/materialdesignicons-6.5.95/scss/_path.scss +0 -10
  691. nautobot/project-static/materialdesignicons-6.5.95/scss/_variables.scss +0 -6606
  692. nautobot/project-static/materialdesignicons-6.5.95/scss/materialdesignicons.scss +0 -8
  693. /nautobot/project-static/{materialdesignicons-6.5.95 → materialdesignicons-7.4.47}/LICENSE +0 -0
  694. {nautobot-2.2.9.dist-info → nautobot-2.3.0b1.dist-info}/LICENSE.txt +0 -0
  695. {nautobot-2.2.9.dist-info → nautobot-2.3.0b1.dist-info}/NOTICE +0 -0
  696. {nautobot-2.2.9.dist-info → nautobot-2.3.0b1.dist-info}/WHEEL +0 -0
  697. {nautobot-2.2.9.dist-info → nautobot-2.3.0b1.dist-info}/entry_points.txt +0 -0
@@ -7,15 +7,17 @@ from django.conf import settings
7
7
  from django.contrib.auth import get_user_model
8
8
  from django.contrib.contenttypes.models import ContentType
9
9
  from django.core.exceptions import ValidationError
10
+ from django.db.models import Q
10
11
  from django.test import override_settings
11
12
  from django.urls import reverse
12
13
  from django.utils import timezone
13
- from django.utils.html import format_html
14
+ from django.utils.html import escape, format_html
14
15
 
15
16
  from nautobot.circuits.models import Circuit
16
17
  from nautobot.core.choices import ColorChoices
17
18
  from nautobot.core.models.fields import slugify_dashes_to_underscores
18
- from nautobot.core.testing import extract_form_failures, extract_page_body, TestCase, ViewTestCases
19
+ from nautobot.core.templatetags.helpers import bettertitle
20
+ from nautobot.core.testing import extract_form_failures, extract_page_body, ModelViewTestCase, TestCase, ViewTestCases
19
21
  from nautobot.core.testing.utils import disable_warnings, post_data
20
22
  from nautobot.core.utils.permissions import get_permission_for_model
21
23
  from nautobot.dcim.models import (
@@ -31,8 +33,10 @@ from nautobot.dcim.models import (
31
33
  from nautobot.dcim.tests import test_views
32
34
  from nautobot.extras.choices import (
33
35
  CustomFieldTypeChoices,
36
+ DynamicGroupTypeChoices,
34
37
  JobExecutionType,
35
38
  LogLevelChoices,
39
+ MetadataTypeDataTypeChoices,
36
40
  ObjectChangeActionChoices,
37
41
  SecretsGroupAccessTypeChoices,
38
42
  SecretsGroupSecretTypeChoices,
@@ -56,18 +60,23 @@ from nautobot.extras.models import (
56
60
  JobButton,
57
61
  JobLogEntry,
58
62
  JobResult,
63
+ MetadataType,
59
64
  Note,
60
65
  ObjectChange,
66
+ ObjectMetadata,
61
67
  Relationship,
62
68
  RelationshipAssociation,
63
69
  Role,
70
+ SavedView,
64
71
  ScheduledJob,
65
72
  Secret,
66
73
  SecretsGroup,
67
74
  SecretsGroupAssociation,
75
+ StaticGroupAssociation,
68
76
  Status,
69
77
  Tag,
70
78
  Team,
79
+ UserSavedViewAssociation,
71
80
  Webhook,
72
81
  )
73
82
  from nautobot.extras.templatetags.job_buttons import NO_CONFIRM_BUTTON
@@ -75,6 +84,7 @@ from nautobot.extras.tests.constants import BIG_GRAPHQL_DEVICE_QUERY
75
84
  from nautobot.extras.tests.test_relationships import RequiredRelationshipTestMixin
76
85
  from nautobot.extras.utils import RoleModelsQuery, TaggableClassesQuery
77
86
  from nautobot.ipam.models import IPAddress, Prefix, VLAN, VLANGroup
87
+ from nautobot.tenancy.models import Tenant
78
88
  from nautobot.users.models import ObjectPermission
79
89
 
80
90
  # Use the proper swappable User model
@@ -364,6 +374,11 @@ class ContactTestCase(ViewTestCases.PrimaryObjectViewTestCase):
364
374
 
365
375
  @classmethod
366
376
  def setUpTestData(cls):
377
+ # Contacts associated with ObjectMetadata objects are protected, create some deletable contacts
378
+ Contact.objects.create(name="Deletable contact 1")
379
+ Contact.objects.create(name="Deletable contact 2")
380
+ Contact.objects.create(name="Deletable contact 3")
381
+
367
382
  cls.form_data = {
368
383
  "name": "new contact",
369
384
  "phone": "555-0121",
@@ -788,6 +803,9 @@ class DynamicGroupTestCase(
788
803
  "name": "new_dynamic_group",
789
804
  "description": "I am a new dynamic group object.",
790
805
  "content_type": content_type.pk,
806
+ "group_type": DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER,
807
+ "tenant": Tenant.objects.first().pk,
808
+ "tags": [t.pk for t in Tag.objects.get_for_model(DynamicGroup)],
791
809
  # Management form fields required for the dynamic formset
792
810
  "dynamic_group_memberships-TOTAL_FORMS": "0",
793
811
  "dynamic_group_memberships-INITIAL_FORMS": "1",
@@ -795,6 +813,9 @@ class DynamicGroupTestCase(
795
813
  "dynamic_group_memberships-MAX_NUM_FORMS": "1000",
796
814
  }
797
815
 
816
+ def _get_queryset(self):
817
+ return super()._get_queryset().filter(group_type=DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER) # TODO
818
+
798
819
  def test_get_object_with_permission(self):
799
820
  instance = self._get_queryset().first()
800
821
  # Add view permissions for the group's members:
@@ -879,6 +900,16 @@ class DynamicGroupTestCase(
879
900
  response = self.client.get(url)
880
901
  self.assertHttpStatus(response, 404)
881
902
 
903
+ def test_edit_object_with_permission(self):
904
+ instance = self._get_queryset().first()
905
+ self.form_data["content_type"] = instance.content_type.pk # Content-type is not editable after creation
906
+ super().test_edit_object_with_permission()
907
+
908
+ def test_edit_object_with_constrained_permission(self):
909
+ instance = self._get_queryset().first()
910
+ self.form_data["content_type"] = instance.content_type.pk # Content-type is not editable after creation
911
+ super().test_edit_object_with_constrained_permission()
912
+
882
913
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
883
914
  def test_edit_saved_filter(self):
884
915
  """Test that editing a filter works using the edit view."""
@@ -915,6 +946,80 @@ class DynamicGroupTestCase(
915
946
  response = self.client.get(path + "?content_type=dcim.device")
916
947
  self.assertHttpStatus(response, 200)
917
948
 
949
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
950
+ def test_bulk_assign_successful(self):
951
+ location_ct = ContentType.objects.get_for_model(Location)
952
+ group_1 = DynamicGroup.objects.create(
953
+ content_type=location_ct, name="Group 1", group_type=DynamicGroupTypeChoices.TYPE_STATIC
954
+ )
955
+ group_2 = DynamicGroup.objects.create(
956
+ content_type=location_ct, name="Group 2", group_type=DynamicGroupTypeChoices.TYPE_STATIC
957
+ )
958
+ group_2.add_members(Location.objects.filter(name__startswith="Root"))
959
+
960
+ self.add_permissions(
961
+ "extras.add_staticgroupassociation", "extras.delete_staticgroupassociation", "extras.add_dynamicgroup"
962
+ )
963
+
964
+ url = reverse("extras:dynamicgroup_bulk_assign")
965
+ request = {
966
+ "path": url,
967
+ "data": post_data(
968
+ {
969
+ "content_type": location_ct.pk,
970
+ "pk": list(Location.objects.filter(parent__isnull=True).values_list("pk", flat=True)),
971
+ "create_and_assign_to_new_group_name": "Root Locations",
972
+ "add_to_groups": [group_1.pk],
973
+ "remove_from_groups": [group_2.pk],
974
+ }
975
+ ),
976
+ }
977
+ response = self.client.post(**request, follow=True)
978
+ self.assertHttpStatus(response, 200)
979
+ new_group = DynamicGroup.objects.get(name="Root Locations")
980
+ self.assertEqual(new_group.content_type, location_ct)
981
+ self.assertEqual(new_group.group_type, DynamicGroupTypeChoices.TYPE_STATIC)
982
+ self.assertQuerysetEqualAndNotEmpty(Location.objects.filter(parent__isnull=True), new_group.members)
983
+ self.assertQuerysetEqualAndNotEmpty(Location.objects.filter(parent__isnull=True), group_1.members)
984
+ self.assertQuerysetEqualAndNotEmpty(
985
+ Location.objects.filter(name__startswith="Root").exclude(parent__isnull=True), group_2.members
986
+ )
987
+
988
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
989
+ def test_bulk_assign_non_static_groups_forbidden(self):
990
+ location_ct = ContentType.objects.get_for_model(Location)
991
+ group_1 = DynamicGroup.objects.create(content_type=location_ct, name="Group 1")
992
+ group_2 = DynamicGroup.objects.create(
993
+ content_type=location_ct, name="Group 2", group_type=DynamicGroupTypeChoices.TYPE_DYNAMIC_SET
994
+ )
995
+
996
+ self.add_permissions(
997
+ "extras.add_staticgroupassociation", "extras.delete_staticgroupassociation", "extras.add_dynamicgroup"
998
+ )
999
+
1000
+ url = reverse("extras:dynamicgroup_bulk_assign")
1001
+ request = {
1002
+ "path": url,
1003
+ "data": post_data(
1004
+ {
1005
+ "content_type": location_ct.pk,
1006
+ "pk": list(Location.objects.filter(parent__isnull=True).distinct().values_list("pk", flat=True)),
1007
+ "add_to_groups": [group_1.pk],
1008
+ },
1009
+ ),
1010
+ }
1011
+ response = self.client.post(**request, follow=True)
1012
+ self.assertHttpStatus(response, 200)
1013
+ # TODO check for specific form validation error?
1014
+
1015
+ del request["data"]["add_to_groups"]
1016
+ request["data"]["remove_from_groups"] = [group_2.pk]
1017
+ response = self.client.post(**request, follow=True)
1018
+ self.assertHttpStatus(response, 200)
1019
+ # TODO check for specific form validation error?
1020
+
1021
+ # TODO: negative tests for bulk assign - global and object-level permission violations, invalid data, etc.
1022
+
918
1023
 
919
1024
  class ExportTemplateTestCase(
920
1025
  ViewTestCases.CreateObjectViewTestCase,
@@ -1063,6 +1168,50 @@ class GitRepositoryTestCase(
1063
1168
  # TODO: mock/stub out `enqueue_git_repository_diff_origin_and_local` and test successful POST with permissions
1064
1169
 
1065
1170
 
1171
+ class MetadataTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
1172
+ model = MetadataType
1173
+ bulk_edit_data = {"description": "A new description"}
1174
+
1175
+ def setUp(self):
1176
+ super().setUp()
1177
+ self.form_data = {
1178
+ "name": "New Metadata Type",
1179
+ "description": "A new type of metadata",
1180
+ "data_type": MetadataTypeDataTypeChoices.TYPE_DATETIME,
1181
+ "content_types": [
1182
+ ContentType.objects.get_for_model(Device).pk,
1183
+ ContentType.objects.get_for_model(ContactAssociation).pk,
1184
+ ],
1185
+ "choices-TOTAL_FORMS": "0",
1186
+ "choices-INITIAL_FORMS": "5",
1187
+ "choices-MIN_NUM_FORMS": "0",
1188
+ "choices-MAX_NUM_FORMS": "1000",
1189
+ }
1190
+
1191
+ def get_deletable_object(self):
1192
+ return MetadataType.objects.create(name="Delete Me", data_type=MetadataTypeDataTypeChoices.TYPE_SELECT)
1193
+
1194
+ def get_deletable_object_pks(self):
1195
+ mdts = [
1196
+ MetadataType.objects.create(name="SoR", data_type=MetadataTypeDataTypeChoices.TYPE_SELECT),
1197
+ MetadataType.objects.create(name="Colors", data_type=MetadataTypeDataTypeChoices.TYPE_MULTISELECT),
1198
+ MetadataType.objects.create(
1199
+ name="Location Metadata Type", data_type=MetadataTypeDataTypeChoices.TYPE_SELECT
1200
+ ),
1201
+ ]
1202
+ return [mdt.pk for mdt in mdts]
1203
+
1204
+ def test_edit_object_with_constrained_permission(self):
1205
+ # Can't change data_type once set
1206
+ self.form_data["data_type"] = self.model.objects.first().data_type
1207
+ return super().test_edit_object_with_constrained_permission()
1208
+
1209
+ def test_edit_object_with_permission(self):
1210
+ # Can't change data_type once set
1211
+ self.form_data["data_type"] = self.model.objects.first().data_type
1212
+ return super().test_edit_object_with_permission()
1213
+
1214
+
1066
1215
  class NoteTestCase(
1067
1216
  ViewTestCases.CreateObjectViewTestCase,
1068
1217
  ViewTestCases.DeleteObjectViewTestCase,
@@ -1125,6 +1274,319 @@ class NoteTestCase(
1125
1274
  self.assertNotContains(response, self.expected_object_note, html=True)
1126
1275
 
1127
1276
 
1277
+ class SavedViewTest(ModelViewTestCase):
1278
+ """
1279
+ Tests for Saved Views
1280
+ """
1281
+
1282
+ model = SavedView
1283
+
1284
+ def get_view_url_for_saved_view(self, saved_view, action="detail"):
1285
+ """
1286
+ Since saved view detail url redirects, we need to manually construct its detail url
1287
+ to test the content of its response.
1288
+ """
1289
+ view = saved_view.view
1290
+ pk = saved_view.pk
1291
+
1292
+ if action == "detail":
1293
+ url = reverse(view) + f"?saved_view={pk}"
1294
+ elif action == "edit":
1295
+ url = saved_view.get_absolute_url() + "update-config/"
1296
+ else:
1297
+ url = reverse("extras:savedview_add")
1298
+
1299
+ return url
1300
+
1301
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1302
+ def test_get_object_anonymous(self):
1303
+ # Make the request as an unauthenticated user
1304
+ self.client.logout()
1305
+ instance = self._get_queryset().first()
1306
+ response = self.client.get(instance.get_absolute_url(), follow=True)
1307
+ self.assertHttpStatus(response, 200)
1308
+ # This view should redirect to /login/?next={saved_view's absolute url}
1309
+ self.assertRedirects(response, f"/login/?next={instance.get_absolute_url()}")
1310
+
1311
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
1312
+ def test_get_object_without_permission(self):
1313
+ instance = self._get_queryset().first()
1314
+ view = instance.view
1315
+ app_label = view.split(":")[0]
1316
+ model_name = view.split(":")[1].split("_")[0]
1317
+ # SavedView detail view should only require the model's view permission
1318
+ self.add_permissions(f"{app_label}.view_{model_name}")
1319
+
1320
+ # Try GET with model-level permission
1321
+ response = self.client.get(instance.get_absolute_url(), follow=True)
1322
+ self.assertHttpStatus(response, 200)
1323
+
1324
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
1325
+ def test_get_object_with_permission(self):
1326
+ instance = self._get_queryset().first()
1327
+ view = instance.view
1328
+ app_label = view.split(":")[0]
1329
+ model_name = view.split(":")[1].split("_")[0]
1330
+ # Add model-level permission
1331
+ self.add_permissions("extras.view_savedview")
1332
+ self.add_permissions(f"{app_label}.view_{model_name}")
1333
+
1334
+ # Try GET with model-level permission
1335
+ # SavedView detail view should redirect to the View from which it is derived
1336
+ response = self.client.get(instance.get_absolute_url(), follow=True)
1337
+ self.assertHttpStatus(response, 200)
1338
+ response_body = extract_page_body(response.content.decode(response.charset))
1339
+ self.assertIn(escape(instance.name), response_body, msg=response_body)
1340
+
1341
+ query_strings = ["&table_changes_pending=true", "&per_page=1234", "&status=active", "&sort=name"]
1342
+ for string in query_strings:
1343
+ view_url = self.get_view_url_for_saved_view(instance) + string
1344
+ response = self.client.get(view_url)
1345
+ self.assertHttpStatus(response, 200)
1346
+ response_body = extract_page_body(response.content.decode(response.charset))
1347
+ # Assert that the star sign is rendered on the page since there are unsaved changes
1348
+ self.assertIn('<i title="Pending changes not saved">', response_body, msg=response_body)
1349
+
1350
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
1351
+ def test_get_object_with_constrained_permission(self):
1352
+ instance1, instance2 = self._get_queryset().all()[:2]
1353
+
1354
+ # Add object-level permission
1355
+ obj_perm = ObjectPermission(
1356
+ name="Test permission",
1357
+ constraints={"pk": instance1.pk},
1358
+ actions=["view", "add", "change", "delete"],
1359
+ )
1360
+ obj_perm.save()
1361
+ obj_perm.users.add(self.user)
1362
+ obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
1363
+ app_label = instance1.view.split(":")[0]
1364
+ model_name = instance1.view.split(":")[1].split("_")[0]
1365
+ self.add_permissions(f"{app_label}.view_{model_name}")
1366
+
1367
+ # Try GET to permitted object
1368
+ self.assertHttpStatus(self.client.get(instance1.get_absolute_url()), 302)
1369
+
1370
+ # Try GET to non-permitted object
1371
+ # Should be able to get to any SavedView instance as long as the user has "{app_label}.view_{model_name}" permission
1372
+ app_label = instance2.view.split(":")[0]
1373
+ model_name = instance2.view.split(":")[1].split("_")[0]
1374
+ self.add_permissions(f"{app_label}.view_{model_name}")
1375
+ self.assertHttpStatus(self.client.get(instance2.get_absolute_url()), 302)
1376
+
1377
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1378
+ def test_update_saved_view_as_different_user(self):
1379
+ instance = self._get_queryset().first()
1380
+ update_query_strings = ["per_page=12", "&status=active", "&name=new_name_filter", "&sort=name"]
1381
+ update_url = self.get_view_url_for_saved_view(instance, "edit") + "?" + "".join(update_query_strings)
1382
+ different_user = User.objects.create(username="User 1", is_active=True)
1383
+ # Try update the saved view with a different user from the owner of the saved view
1384
+ self.client.force_login(different_user)
1385
+ response = self.client.get(update_url, follow=True)
1386
+ self.assertHttpStatus(response, 200)
1387
+ response_body = extract_page_body(response.content.decode(response.charset))
1388
+ self.assertIn(
1389
+ f"You do not have the required permission to modify this Saved View owned by {instance.owner}",
1390
+ response_body,
1391
+ msg=response_body,
1392
+ )
1393
+
1394
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1395
+ def test_update_saved_view_as_owner(self):
1396
+ instance = self._get_queryset().first()
1397
+ update_query_strings = ["per_page=12", "&status=active", "&name=new_name_filter", "&sort=name"]
1398
+ update_url = self.get_view_url_for_saved_view(instance, "edit") + "?" + "".join(update_query_strings)
1399
+ # Try update the saved view with the same user as the owner of the saved view
1400
+ instance.owner.is_active = True
1401
+ instance.owner.save()
1402
+ self.client.force_login(instance.owner)
1403
+ response = self.client.get(update_url)
1404
+ self.assertHttpStatus(response, 302)
1405
+ instance.refresh_from_db()
1406
+ self.assertEqual(instance.config["pagination_count"], 12)
1407
+ self.assertEqual(instance.config["filter_params"]["status"], ["active"])
1408
+ self.assertEqual(instance.config["filter_params"]["name"], ["new_name_filter"])
1409
+ self.assertEqual(instance.config["sort_order"], ["name"])
1410
+
1411
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1412
+ def test_delete_saved_view_as_different_user(self):
1413
+ instance = self._get_queryset().first()
1414
+ instance.config = {
1415
+ "filter_params": {
1416
+ "location_type": ["Campus", "Building", "Floor", "Elevator"],
1417
+ "tenant": ["Krause, Welch and Fuentes"],
1418
+ },
1419
+ "table_config": {"LocationTable": {"columns": ["name", "status", "location_type", "tags"]}},
1420
+ }
1421
+ instance.validated_save()
1422
+ delete_url = reverse("extras:savedview_delete", kwargs={"pk": instance.pk})
1423
+ different_user = User.objects.create(username="User 2", is_active=True)
1424
+ # Try delete the saved view with a different user from the owner of the saved view
1425
+ self.client.force_login(different_user)
1426
+ response = self.client.post(delete_url, follow=True)
1427
+ self.assertHttpStatus(response, 200)
1428
+ response_body = extract_page_body(response.content.decode(response.charset))
1429
+ self.assertIn(
1430
+ f"You do not have the required permission to delete this Saved View owned by {instance.owner}",
1431
+ response_body,
1432
+ msg=response_body,
1433
+ )
1434
+
1435
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1436
+ def test_delete_saved_view_as_owner(self):
1437
+ instance = self._get_queryset().first()
1438
+ instance.config = {
1439
+ "filter_params": {
1440
+ "location_type": ["Campus", "Building", "Floor", "Elevator"],
1441
+ "tenant": ["Krause, Welch and Fuentes"],
1442
+ },
1443
+ "table_config": {"LocationTable": {"columns": ["name", "status", "location_type", "tags"]}},
1444
+ }
1445
+ instance.validated_save()
1446
+ delete_url = reverse("extras:savedview_delete", kwargs={"pk": instance.pk})
1447
+ # Delete functionality should work even without "extras.delete_savedview" permissions
1448
+ # if the saved view belongs to the user.
1449
+ instance.owner.is_active = True
1450
+ instance.owner.save()
1451
+ self.client.force_login(instance.owner)
1452
+ response = self.client.post(delete_url, follow=True)
1453
+ self.assertHttpStatus(response, 200)
1454
+ response_body = extract_page_body(response.content.decode(response.charset))
1455
+ self.assertIn(
1456
+ "Are you sure you want to delete saved view",
1457
+ response_body,
1458
+ msg=response_body,
1459
+ )
1460
+
1461
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1462
+ def test_create_saved_view(self):
1463
+ instance = self._get_queryset().first()
1464
+ # User should be able to create saved view with only "{app_label}.view_{model_name}" permission
1465
+ # self.add_permissions("extras.add_savedview")
1466
+ view = instance.view
1467
+ app_label = view.split(":")[0]
1468
+ model_name = view.split(":")[1].split("_")[0]
1469
+ self.add_permissions(f"{app_label}.view_{model_name}")
1470
+ create_query_strings = [
1471
+ f"saved_view={instance.pk}",
1472
+ "&per_page=12",
1473
+ "&status=active",
1474
+ "&name=new_name_filter",
1475
+ "&sort=name",
1476
+ ]
1477
+ create_url = self.get_view_url_for_saved_view(instance, "create")
1478
+ request = {
1479
+ "path": create_url,
1480
+ "data": post_data(
1481
+ {"name": "New Test View", "view": f"{instance.view}", "params": "".join(create_query_strings)}
1482
+ ),
1483
+ }
1484
+ self.assertHttpStatus(self.client.post(**request), 302)
1485
+ instance = SavedView.objects.get(name="New Test View")
1486
+ self.assertEqual(instance.config["pagination_count"], 12)
1487
+ self.assertEqual(instance.config["filter_params"]["status"], ["active"])
1488
+ self.assertEqual(instance.config["filter_params"]["name"], ["new_name_filter"])
1489
+ self.assertEqual(instance.config["sort_order"], ["name"])
1490
+
1491
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1492
+ def test_is_global_default(self):
1493
+ view_name = "dcim:location_list"
1494
+ SavedView.objects.create(
1495
+ name="Global Location Default View",
1496
+ owner=self.user,
1497
+ view=view_name,
1498
+ is_global_default=True,
1499
+ )
1500
+ response = self.client.get(reverse(view_name), follow=True)
1501
+ # Assert that Location List View got redirected to Saved View set as global default
1502
+ self.assertHttpStatus(response, 200)
1503
+ response_body = extract_page_body(response.content.decode(response.charset))
1504
+ self.assertInHTML(
1505
+ """
1506
+ <strong>
1507
+ Global Location Default View
1508
+ </strong>
1509
+ """,
1510
+ response_body,
1511
+ )
1512
+
1513
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1514
+ def test_user_default(self):
1515
+ view_name = "dcim:location_list"
1516
+ sv = SavedView.objects.create(
1517
+ name="User Location Default View",
1518
+ owner=self.user,
1519
+ view=view_name,
1520
+ is_global_default=True,
1521
+ )
1522
+ UserSavedViewAssociation.objects.create(user=self.user, saved_view=sv, view_name=sv.view)
1523
+ response = self.client.get(reverse(view_name), follow=True)
1524
+ # Assert that Location List View got redirected to Saved View set as user default
1525
+ self.assertHttpStatus(response, 200)
1526
+ response_body = extract_page_body(response.content.decode(response.charset))
1527
+ self.assertInHTML(
1528
+ """
1529
+ <strong>
1530
+ User Location Default View
1531
+ </strong>
1532
+ """,
1533
+ response_body,
1534
+ )
1535
+
1536
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1537
+ def test_user_default_precedes_global_default(self):
1538
+ view_name = "dcim:location_list"
1539
+ SavedView.objects.create(
1540
+ name="Global Location Default View",
1541
+ owner=self.user,
1542
+ view=view_name,
1543
+ is_global_default=True,
1544
+ )
1545
+ sv = SavedView.objects.create(
1546
+ name="User Location Default View",
1547
+ owner=self.user,
1548
+ view=view_name,
1549
+ )
1550
+ UserSavedViewAssociation.objects.create(user=self.user, saved_view=sv, view_name=sv.view)
1551
+ response = self.client.get(reverse(view_name), follow=True)
1552
+ # Assert that Location List View got redirected to Saved View set as user default
1553
+ self.assertHttpStatus(response, 200)
1554
+ response_body = extract_page_body(response.content.decode(response.charset))
1555
+ self.assertInHTML(
1556
+ """
1557
+ <strong>
1558
+ User Location Default View
1559
+ </strong>
1560
+ """,
1561
+ response_body,
1562
+ )
1563
+
1564
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
1565
+ def test_is_shared(self):
1566
+ view_name = "dcim:location_list"
1567
+ new_user = User.objects.create(username="Different User", is_active=True)
1568
+ sv_shared = SavedView.objects.create(
1569
+ name="Shared Location Saved View",
1570
+ owner=new_user,
1571
+ view=view_name,
1572
+ )
1573
+ sv_not_shared = SavedView.objects.create(
1574
+ name="Private Location Saved View",
1575
+ owner=new_user,
1576
+ view=view_name,
1577
+ is_shared=False,
1578
+ )
1579
+ app_label = view_name.split(":")[0]
1580
+ model_name = view_name.split(":")[1].split("_")[0]
1581
+ self.add_permissions(f"{app_label}.view_{model_name}")
1582
+ response = self.client.get(reverse(view_name), follow=True)
1583
+ # Assert that Location List View got redirected to Saved View set as user default
1584
+ self.assertHttpStatus(response, 200)
1585
+ response_body = extract_page_body(response.content.decode(response.charset))
1586
+ self.assertIn(str(sv_shared.pk), response_body, msg=response_body)
1587
+ self.assertNotIn(str(sv_not_shared.pk), response_body, msg=response_body)
1588
+
1589
+
1128
1590
  # Not a full-fledged PrimaryObjectViewTestCase as there's no BulkEditView for Secrets
1129
1591
  class SecretTestCase(
1130
1592
  ViewTestCases.GetObjectViewTestCase,
@@ -2532,27 +2994,6 @@ class JobButtonRenderingTestCase(TestCase):
2532
2994
  )
2533
2995
 
2534
2996
 
2535
- class JobCustomTemplateTestCase(TestCase):
2536
- @classmethod
2537
- def setUpTestData(cls):
2538
- # Job model objects are automatically created during database migrations
2539
-
2540
- # But we do need to make sure the ones we're testing are flagged appropriately
2541
- cls.example_job = Job.objects.get(job_class_name="ExampleCustomFormJob")
2542
- cls.example_job.enabled = True
2543
- cls.example_job.save()
2544
-
2545
- cls.run_url = reverse("extras:job_run", kwargs={"pk": cls.example_job.pk})
2546
-
2547
- def test_rendering_custom_template(self):
2548
- obj_perm = ObjectPermission(name="Test permission", actions=["view", "run"])
2549
- obj_perm.save()
2550
- obj_perm.users.add(self.user)
2551
- obj_perm.object_types.add(ContentType.objects.get_for_model(Job))
2552
- with self.assertTemplateUsed("example_app/custom_job_form.html"):
2553
- self.client.get(self.run_url)
2554
-
2555
-
2556
2997
  # TODO: Convert to StandardTestCases.Views
2557
2998
  class ObjectChangeTestCase(TestCase):
2558
2999
  user_permissions = ("extras.view_objectchange",)
@@ -2587,6 +3028,57 @@ class ObjectChangeTestCase(TestCase):
2587
3028
  self.assertHttpStatus(response, 200)
2588
3029
 
2589
3030
 
3031
+ class ObjectMetadataTestCase(
3032
+ ViewTestCases.DeleteObjectViewTestCase,
3033
+ ViewTestCases.BulkDeleteObjectsViewTestCase,
3034
+ ViewTestCases.GetObjectChangelogViewTestCase,
3035
+ ViewTestCases.ListObjectsViewTestCase,
3036
+ ):
3037
+ model = ObjectMetadata
3038
+
3039
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
3040
+ def test_value_column_in_list_view_rendered_correctly(self):
3041
+ """
3042
+ GET a list of objects as an authenticated user with permission to view the objects.
3043
+ """
3044
+ instance1 = self._get_queryset().filter(contact__isnull=False).first()
3045
+ instance2 = self._get_queryset().filter(team__isnull=False).first()
3046
+
3047
+ # Try GET to permitted objects
3048
+ response = self.client.get(self._get_url("list"))
3049
+ self.assertHttpStatus(response, 200)
3050
+ content = extract_page_body(response.content.decode(response.charset))
3051
+ # Check if the contact or team absolute url is rendered in the ObjectListView table
3052
+ self.assertIn(instance1.contact.get_absolute_url(), content, msg=content)
3053
+ self.assertIn(instance2.team.get_absolute_url(), content, msg=content)
3054
+ # TODO check if other types of values are rendered correctly
3055
+
3056
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
3057
+ def test_list_objects_with_constrained_permission(self):
3058
+ instance1 = self._get_queryset().first()
3059
+ instance2 = self._get_queryset().filter(~Q(assigned_object_id=instance1.assigned_object_id)).first()
3060
+ self._get_queryset().filter(~Q(pk=instance1.pk) & ~Q(pk=instance2.pk)).delete()
3061
+
3062
+ # Add object-level permission
3063
+ obj_perm = ObjectPermission(
3064
+ name="Test permission",
3065
+ constraints={"pk": instance1.pk},
3066
+ actions=["view", "add"],
3067
+ )
3068
+ obj_perm.save()
3069
+ obj_perm.users.add(self.user)
3070
+ obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
3071
+
3072
+ # Try GET with object-level permission
3073
+ response = self.client.get(self._get_url("list"))
3074
+ self.assertHttpStatus(response, 200)
3075
+ content = extract_page_body(response.content.decode(response.charset))
3076
+ # Since we do not render the absolute url in ObjectListView of ObjectMetadata, we need to check assigned_object
3077
+ # fields and if they are rendered.
3078
+ self.assertIn(instance1.assigned_object.get_absolute_url(), content, msg=content)
3079
+ self.assertNotIn(instance2.assigned_object.get_absolute_url(), content, msg=content)
3080
+
3081
+
2590
3082
  class RelationshipTestCase(
2591
3083
  ViewTestCases.CreateObjectViewTestCase,
2592
3084
  ViewTestCases.DeleteObjectViewTestCase,
@@ -2844,7 +3336,51 @@ class RelationshipAssociationTestCase(
2844
3336
  self.assertNotIn(instance2.destination.name, content, msg=content)
2845
3337
 
2846
3338
 
3339
+ class StaticGroupAssociationTestCase(
3340
+ ViewTestCases.BulkDeleteObjectsViewTestCase,
3341
+ ViewTestCases.DeleteObjectViewTestCase,
3342
+ ViewTestCases.GetObjectViewTestCase,
3343
+ ViewTestCases.GetObjectChangelogViewTestCase,
3344
+ ViewTestCases.ListObjectsViewTestCase,
3345
+ ):
3346
+ model = StaticGroupAssociation
3347
+
3348
+ def test_list_objects_omits_hidden_by_default(self):
3349
+ """The list view should not by default include associations for hidden groups."""
3350
+ sga1 = StaticGroupAssociation.all_objects.filter(
3351
+ dynamic_group__group_type=DynamicGroupTypeChoices.TYPE_STATIC
3352
+ ).first()
3353
+ self.assertIsNotNone(sga1)
3354
+ sga2 = StaticGroupAssociation.all_objects.exclude(
3355
+ dynamic_group__group_type=DynamicGroupTypeChoices.TYPE_STATIC
3356
+ ).first()
3357
+ self.assertIsNotNone(sga2)
3358
+
3359
+ self.add_permissions("extras.view_staticgroupassociation")
3360
+ response = self.client.get(self._get_url("list"))
3361
+ self.assertHttpStatus(response, 200)
3362
+ content = extract_page_body(response.content.decode(response.charset))
3363
+
3364
+ self.assertIn(sga1.get_absolute_url(), content, msg=content)
3365
+ self.assertNotIn(sga2.get_absolute_url(), content, msg=content)
3366
+
3367
+ def test_list_objects_can_explicitly_include_hidden(self):
3368
+ """The list view can include hidden groups' associations with the correct query parameter."""
3369
+ sga1 = StaticGroupAssociation.all_objects.exclude(
3370
+ dynamic_group__group_type=DynamicGroupTypeChoices.TYPE_STATIC
3371
+ ).first()
3372
+ self.assertIsNotNone(sga1)
3373
+
3374
+ self.add_permissions("extras.view_staticgroupassociation")
3375
+ response = self.client.get(f"{self._get_url('list')}?dynamic_group={sga1.dynamic_group.pk}")
3376
+ self.assertHttpStatus(response, 200)
3377
+ content = extract_page_body(response.content.decode(response.charset))
3378
+
3379
+ self.assertIn(sga1.get_absolute_url(), content, msg=content)
3380
+
3381
+
2847
3382
  class StatusTestCase(
3383
+ # TODO? ViewTestCases.BulkDeleteObjectsViewTestCase,
2848
3384
  ViewTestCases.CreateObjectViewTestCase,
2849
3385
  ViewTestCases.DeleteObjectViewTestCase,
2850
3386
  ViewTestCases.EditObjectViewTestCase,
@@ -2876,6 +3412,11 @@ class TeamTestCase(ViewTestCases.PrimaryObjectViewTestCase):
2876
3412
 
2877
3413
  @classmethod
2878
3414
  def setUpTestData(cls):
3415
+ # Teams associated with ObjectMetadata objects are protected, create some deletable teams
3416
+ Team.objects.create(name="Deletable team 1")
3417
+ Team.objects.create(name="Deletable team 2")
3418
+ Team.objects.create(name="Deletable team 3")
3419
+
2879
3420
  cls.form_data = {
2880
3421
  "name": "new team",
2881
3422
  "phone": "555-0122",
@@ -3084,11 +3625,7 @@ class RoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
3084
3625
  for model_class in eligible_ct_model_classes:
3085
3626
  verbose_name_plural = model_class._meta.verbose_name_plural
3086
3627
  content_type = ContentType.objects.get_for_model(model_class)
3087
- result = " ".join(elem.capitalize() for elem in verbose_name_plural.split())
3088
- if result == "Ip Addresses":
3089
- result = "IP Addresses"
3090
- elif result == "Vlans":
3091
- result = "VLANs"
3628
+ result = " ".join(bettertitle(elem) for elem in verbose_name_plural.split())
3092
3629
  # Assert tables are correctly rendered
3093
3630
  if content_type not in role_content_types:
3094
3631
  if result == "Contact Associations":