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
@@ -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,
@@ -2132,7 +2594,7 @@ class JobTestCase(
2132
2594
 
2133
2595
  self.assertInHTML('<option value="uniquequeue" selected>', content)
2134
2596
  self.assertInHTML(
2135
- '<input type="text" name="var" value="456" class="form-control form-control" required placeholder="None" id="id_var">',
2597
+ '<input type="text" name="var" value="456" class="form-control" required placeholder="None" id="id_var">',
2136
2598
  content,
2137
2599
  )
2138
2600
  self.assertInHTML('<input type="hidden" name="_profile" value="True" id="id__profile">', content)
@@ -2532,6 +2994,27 @@ class JobButtonRenderingTestCase(TestCase):
2532
2994
  )
2533
2995
 
2534
2996
 
2997
+ class JobCustomTemplateTestCase(TestCase):
2998
+ @classmethod
2999
+ def setUpTestData(cls):
3000
+ # Job model objects are automatically created during database migrations
3001
+
3002
+ # But we do need to make sure the ones we're testing are flagged appropriately
3003
+ cls.example_job = Job.objects.get(job_class_name="ExampleCustomFormJob")
3004
+ cls.example_job.enabled = True
3005
+ cls.example_job.save()
3006
+
3007
+ cls.run_url = reverse("extras:job_run", kwargs={"pk": cls.example_job.pk})
3008
+
3009
+ def test_rendering_custom_template(self):
3010
+ obj_perm = ObjectPermission(name="Test permission", actions=["view", "run"])
3011
+ obj_perm.save()
3012
+ obj_perm.users.add(self.user)
3013
+ obj_perm.object_types.add(ContentType.objects.get_for_model(Job))
3014
+ with self.assertTemplateUsed("example_app/custom_job_form.html"):
3015
+ self.client.get(self.run_url)
3016
+
3017
+
2535
3018
  # TODO: Convert to StandardTestCases.Views
2536
3019
  class ObjectChangeTestCase(TestCase):
2537
3020
  user_permissions = ("extras.view_objectchange",)
@@ -2566,6 +3049,56 @@ class ObjectChangeTestCase(TestCase):
2566
3049
  self.assertHttpStatus(response, 200)
2567
3050
 
2568
3051
 
3052
+ class ObjectMetadataTestCase(
3053
+ ViewTestCases.GetObjectViewTestCase,
3054
+ ViewTestCases.GetObjectChangelogViewTestCase,
3055
+ ViewTestCases.ListObjectsViewTestCase,
3056
+ ):
3057
+ model = ObjectMetadata
3058
+
3059
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
3060
+ def test_value_column_in_list_view_rendered_correctly(self):
3061
+ """
3062
+ GET a list of objects as an authenticated user with permission to view the objects.
3063
+ """
3064
+ instance1 = self._get_queryset().filter(contact__isnull=False).first()
3065
+ instance2 = self._get_queryset().filter(team__isnull=False).first()
3066
+
3067
+ # Try GET to permitted objects
3068
+ response = self.client.get(self._get_url("list"))
3069
+ self.assertHttpStatus(response, 200)
3070
+ content = extract_page_body(response.content.decode(response.charset))
3071
+ # Check if the contact or team absolute url is rendered in the ObjectListView table
3072
+ self.assertIn(instance1.contact.get_absolute_url(), content, msg=content)
3073
+ self.assertIn(instance2.team.get_absolute_url(), content, msg=content)
3074
+ # TODO check if other types of values are rendered correctly
3075
+
3076
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
3077
+ def test_list_objects_with_constrained_permission(self):
3078
+ instance1 = self._get_queryset().first()
3079
+ instance2 = self._get_queryset().filter(~Q(assigned_object_id=instance1.assigned_object_id)).first()
3080
+ self._get_queryset().filter(~Q(pk=instance1.pk) & ~Q(pk=instance2.pk)).delete()
3081
+
3082
+ # Add object-level permission
3083
+ obj_perm = ObjectPermission(
3084
+ name="Test permission",
3085
+ constraints={"pk": instance1.pk},
3086
+ actions=["view", "add"],
3087
+ )
3088
+ obj_perm.save()
3089
+ obj_perm.users.add(self.user)
3090
+ obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
3091
+
3092
+ # Try GET with object-level permission
3093
+ response = self.client.get(self._get_url("list"))
3094
+ self.assertHttpStatus(response, 200)
3095
+ content = extract_page_body(response.content.decode(response.charset))
3096
+ # Since we do not render the absolute url in ObjectListView of ObjectMetadata, we need to check assigned_object
3097
+ # fields and if they are rendered.
3098
+ self.assertIn(instance1.assigned_object.get_absolute_url(), content, msg=content)
3099
+ self.assertNotIn(instance2.assigned_object.get_absolute_url(), content, msg=content)
3100
+
3101
+
2569
3102
  class RelationshipTestCase(
2570
3103
  ViewTestCases.CreateObjectViewTestCase,
2571
3104
  ViewTestCases.DeleteObjectViewTestCase,
@@ -2823,7 +3356,51 @@ class RelationshipAssociationTestCase(
2823
3356
  self.assertNotIn(instance2.destination.name, content, msg=content)
2824
3357
 
2825
3358
 
3359
+ class StaticGroupAssociationTestCase(
3360
+ ViewTestCases.BulkDeleteObjectsViewTestCase,
3361
+ ViewTestCases.DeleteObjectViewTestCase,
3362
+ ViewTestCases.GetObjectViewTestCase,
3363
+ ViewTestCases.GetObjectChangelogViewTestCase,
3364
+ ViewTestCases.ListObjectsViewTestCase,
3365
+ ):
3366
+ model = StaticGroupAssociation
3367
+
3368
+ def test_list_objects_omits_hidden_by_default(self):
3369
+ """The list view should not by default include associations for hidden groups."""
3370
+ sga1 = StaticGroupAssociation.all_objects.filter(
3371
+ dynamic_group__group_type=DynamicGroupTypeChoices.TYPE_STATIC
3372
+ ).first()
3373
+ self.assertIsNotNone(sga1)
3374
+ sga2 = StaticGroupAssociation.all_objects.exclude(
3375
+ dynamic_group__group_type=DynamicGroupTypeChoices.TYPE_STATIC
3376
+ ).first()
3377
+ self.assertIsNotNone(sga2)
3378
+
3379
+ self.add_permissions("extras.view_staticgroupassociation")
3380
+ response = self.client.get(self._get_url("list"))
3381
+ self.assertHttpStatus(response, 200)
3382
+ content = extract_page_body(response.content.decode(response.charset))
3383
+
3384
+ self.assertIn(sga1.get_absolute_url(), content, msg=content)
3385
+ self.assertNotIn(sga2.get_absolute_url(), content, msg=content)
3386
+
3387
+ def test_list_objects_can_explicitly_include_hidden(self):
3388
+ """The list view can include hidden groups' associations with the correct query parameter."""
3389
+ sga1 = StaticGroupAssociation.all_objects.exclude(
3390
+ dynamic_group__group_type=DynamicGroupTypeChoices.TYPE_STATIC
3391
+ ).first()
3392
+ self.assertIsNotNone(sga1)
3393
+
3394
+ self.add_permissions("extras.view_staticgroupassociation")
3395
+ response = self.client.get(f"{self._get_url('list')}?dynamic_group={sga1.dynamic_group.pk}")
3396
+ self.assertHttpStatus(response, 200)
3397
+ content = extract_page_body(response.content.decode(response.charset))
3398
+
3399
+ self.assertIn(sga1.get_absolute_url(), content, msg=content)
3400
+
3401
+
2826
3402
  class StatusTestCase(
3403
+ # TODO? ViewTestCases.BulkDeleteObjectsViewTestCase,
2827
3404
  ViewTestCases.CreateObjectViewTestCase,
2828
3405
  ViewTestCases.DeleteObjectViewTestCase,
2829
3406
  ViewTestCases.EditObjectViewTestCase,
@@ -2855,6 +3432,11 @@ class TeamTestCase(ViewTestCases.PrimaryObjectViewTestCase):
2855
3432
 
2856
3433
  @classmethod
2857
3434
  def setUpTestData(cls):
3435
+ # Teams associated with ObjectMetadata objects are protected, create some deletable teams
3436
+ Team.objects.create(name="Deletable team 1")
3437
+ Team.objects.create(name="Deletable team 2")
3438
+ Team.objects.create(name="Deletable team 3")
3439
+
2858
3440
  cls.form_data = {
2859
3441
  "name": "new team",
2860
3442
  "phone": "555-0122",
@@ -3063,11 +3645,7 @@ class RoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
3063
3645
  for model_class in eligible_ct_model_classes:
3064
3646
  verbose_name_plural = model_class._meta.verbose_name_plural
3065
3647
  content_type = ContentType.objects.get_for_model(model_class)
3066
- result = " ".join(elem.capitalize() for elem in verbose_name_plural.split())
3067
- if result == "Ip Addresses":
3068
- result = "IP Addresses"
3069
- elif result == "Vlans":
3070
- result = "VLANs"
3648
+ result = " ".join(bettertitle(elem) for elem in verbose_name_plural.split())
3071
3649
  # Assert tables are correctly rendered
3072
3650
  if content_type not in role_content_types:
3073
3651
  if result == "Contact Associations":