nautobot 2.2.9__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 (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 +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 +95 -13
  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/schema.py +26 -4
  61. nautobot/core/jobs/__init__.py +16 -2
  62. nautobot/core/jobs/cleanup.py +100 -0
  63. nautobot/core/jobs/groups.py +38 -0
  64. nautobot/core/management/commands/generate_test_data.py +116 -3
  65. nautobot/core/models/__init__.py +34 -9
  66. nautobot/core/models/generics.py +19 -3
  67. nautobot/core/models/name_color_content_types.py +7 -28
  68. nautobot/core/models/querysets.py +4 -3
  69. nautobot/core/models/tree_queries.py +1 -1
  70. nautobot/core/models/utils.py +21 -5
  71. nautobot/core/settings.py +2 -17
  72. nautobot/core/settings.yaml +34 -13
  73. nautobot/core/settings_funcs.py +103 -0
  74. nautobot/core/tables.py +130 -56
  75. nautobot/core/templates/admin/search_form.html +1 -1
  76. nautobot/core/templates/buttons/add.html +11 -3
  77. nautobot/core/templates/buttons/consolidated_bulk_action_buttons.html +13 -0
  78. nautobot/core/templates/buttons/consolidated_detail_view_action_buttons.html +13 -0
  79. nautobot/core/templates/buttons/export.html +101 -53
  80. nautobot/core/templates/buttons/job_import.html +11 -3
  81. nautobot/core/templates/generic/object_bulk_destroy.html +3 -1
  82. nautobot/core/templates/generic/object_bulk_update.html +3 -1
  83. nautobot/core/templates/generic/object_changelog.html +0 -9
  84. nautobot/core/templates/generic/object_list.html +156 -17
  85. nautobot/core/templates/generic/object_retrieve.html +80 -16
  86. nautobot/core/templates/inc/extras_features_edit_form_fields.html +8 -0
  87. nautobot/core/templates/inc/javascript.html +2 -0
  88. nautobot/core/templates/inc/media.html +2 -2
  89. nautobot/core/templates/inc/nav_menu.html +1 -0
  90. nautobot/core/templates/inc/paginator.html +7 -7
  91. nautobot/core/templates/inc/search_panel.html +2 -2
  92. nautobot/core/templates/inc/table.html +2 -2
  93. nautobot/core/templates/nautobot_config.py.j2 +13 -8
  94. nautobot/core/templates/utilities/templatetags/dynamic_group_assignment_modal.html +37 -0
  95. nautobot/core/templates/utilities/templatetags/filter_form_modal.html +2 -2
  96. nautobot/core/templates/utilities/templatetags/saved_view_modal.html +38 -0
  97. nautobot/core/templates/utilities/theme_preview.html +25 -8
  98. nautobot/core/templates/utilities/worker_status.html +152 -0
  99. nautobot/core/templatetags/buttons.py +335 -38
  100. nautobot/core/templatetags/form_helpers.py +1 -1
  101. nautobot/core/templatetags/helpers.py +181 -11
  102. nautobot/core/testing/api.py +5 -4
  103. nautobot/core/testing/filters.py +63 -14
  104. nautobot/core/testing/mixins.py +46 -0
  105. nautobot/core/testing/models.py +22 -0
  106. nautobot/core/testing/schema.py +4 -8
  107. nautobot/core/testing/views.py +31 -14
  108. nautobot/core/tests/integration/test_import_objects_ui.py +1 -0
  109. nautobot/core/tests/integration/test_swagger.py +1 -1
  110. nautobot/core/tests/nautobot_config.py +0 -1
  111. nautobot/core/tests/runner.py +2 -2
  112. nautobot/core/tests/test_api.py +1 -0
  113. nautobot/core/tests/test_authentication.py +7 -2
  114. nautobot/core/tests/test_filters.py +11 -9
  115. nautobot/core/tests/test_forms.py +9 -0
  116. nautobot/core/tests/test_graphql.py +27 -16
  117. nautobot/core/tests/test_jobs.py +122 -0
  118. nautobot/core/tests/test_tables.py +3 -1
  119. nautobot/core/tests/test_templatetags_helpers.py +12 -5
  120. nautobot/core/tests/test_utils.py +31 -20
  121. nautobot/core/tests/test_views.py +6 -6
  122. nautobot/core/urls.py +8 -3
  123. nautobot/core/utils/deprecation.py +29 -0
  124. nautobot/core/utils/filtering.py +12 -9
  125. nautobot/core/utils/lookup.py +37 -2
  126. nautobot/core/utils/requests.py +4 -1
  127. nautobot/core/views/__init__.py +137 -24
  128. nautobot/core/views/generic.py +119 -67
  129. nautobot/core/views/mixins.py +105 -36
  130. nautobot/core/views/paginator.py +9 -3
  131. nautobot/core/views/renderers.py +121 -56
  132. nautobot/core/views/utils.py +81 -1
  133. nautobot/dcim/__init__.py +0 -1
  134. nautobot/dcim/api/serializers.py +180 -44
  135. nautobot/dcim/api/urls.py +7 -3
  136. nautobot/dcim/api/views.py +53 -7
  137. nautobot/dcim/apps.py +3 -0
  138. nautobot/dcim/choices.py +25 -0
  139. nautobot/dcim/constants.py +7 -0
  140. nautobot/dcim/factory.py +252 -18
  141. nautobot/dcim/filters/__init__.py +373 -193
  142. nautobot/dcim/filters/mixins.py +274 -1
  143. nautobot/dcim/forms.py +834 -121
  144. nautobot/dcim/graphql/types.py +2 -2
  145. nautobot/dcim/homepage.py +1 -1
  146. nautobot/dcim/migrations/0059_add_role_field_to_interface_models.py +27 -0
  147. nautobot/dcim/migrations/0060_alter_cable_status_alter_consoleport__path_and_more.py +303 -0
  148. nautobot/dcim/migrations/0061_module_models.py +862 -0
  149. nautobot/dcim/migrations/0062_module_data_migration.py +25 -0
  150. nautobot/dcim/models/__init__.py +8 -0
  151. nautobot/dcim/models/cables.py +15 -0
  152. nautobot/dcim/models/device_component_templates.py +207 -53
  153. nautobot/dcim/models/device_components.py +275 -99
  154. nautobot/dcim/models/devices.py +468 -13
  155. nautobot/dcim/models/racks.py +0 -1
  156. nautobot/dcim/navigation.py +47 -0
  157. nautobot/dcim/signals.py +3 -3
  158. nautobot/dcim/tables/__init__.py +35 -23
  159. nautobot/dcim/tables/devices.py +229 -43
  160. nautobot/dcim/tables/devicetypes.py +65 -9
  161. nautobot/dcim/tables/racks.py +5 -1
  162. nautobot/dcim/tables/template_code.py +46 -26
  163. nautobot/dcim/templates/dcim/cable_connect.html +76 -3
  164. nautobot/dcim/templates/dcim/console_port_connection_list.html +7 -5
  165. nautobot/dcim/templates/dcim/device/base.html +14 -6
  166. nautobot/dcim/templates/dcim/device/consoleports.html +2 -3
  167. nautobot/dcim/templates/dcim/device/consoleserverports.html +2 -3
  168. nautobot/dcim/templates/dcim/device/devicebays.html +6 -7
  169. nautobot/dcim/templates/dcim/device/frontports.html +2 -3
  170. nautobot/dcim/templates/dcim/device/interfaces.html +2 -3
  171. nautobot/dcim/templates/dcim/device/inventory.html +2 -3
  172. nautobot/dcim/templates/dcim/device/modulebays.html +49 -0
  173. nautobot/dcim/templates/dcim/device/poweroutlets.html +2 -3
  174. nautobot/dcim/templates/dcim/device/powerports.html +2 -3
  175. nautobot/dcim/templates/dcim/device/rearports.html +2 -3
  176. nautobot/dcim/templates/dcim/device.html +45 -1
  177. nautobot/dcim/templates/dcim/device_component.html +13 -5
  178. nautobot/dcim/templates/dcim/device_list.html +2 -1
  179. nautobot/dcim/templates/dcim/devicetype.html +99 -98
  180. nautobot/dcim/templates/dcim/devicetype_list.html +8 -16
  181. nautobot/dcim/templates/dcim/inc/devicetype_component_table.html +1 -1
  182. nautobot/dcim/templates/dcim/inc/moduletype_component_table.html +39 -0
  183. nautobot/dcim/templates/dcim/interface.html +17 -2
  184. nautobot/dcim/templates/dcim/interface_connection_list.html +7 -5
  185. nautobot/dcim/templates/dcim/interface_edit.html +1 -0
  186. nautobot/dcim/templates/dcim/manufacturer.html +24 -0
  187. nautobot/dcim/templates/dcim/module/base.html +97 -0
  188. nautobot/dcim/templates/dcim/module_bulk_destroy.html +5 -0
  189. nautobot/dcim/templates/dcim/module_consoleports.html +53 -0
  190. nautobot/dcim/templates/dcim/module_consoleserverports.html +53 -0
  191. nautobot/dcim/templates/dcim/module_destroy.html +5 -0
  192. nautobot/dcim/templates/dcim/module_frontports.html +53 -0
  193. nautobot/dcim/templates/dcim/module_interfaces.html +57 -0
  194. nautobot/dcim/templates/dcim/module_list.html +20 -0
  195. nautobot/dcim/templates/dcim/module_modulebays.html +49 -0
  196. nautobot/dcim/templates/dcim/module_poweroutlets.html +53 -0
  197. nautobot/dcim/templates/dcim/module_powerports.html +53 -0
  198. nautobot/dcim/templates/dcim/module_rearports.html +53 -0
  199. nautobot/dcim/templates/dcim/module_retrieve.html +63 -0
  200. nautobot/dcim/templates/dcim/module_update.html +71 -0
  201. nautobot/dcim/templates/dcim/modulebay_bulk_destroy.html +5 -0
  202. nautobot/dcim/templates/dcim/modulebay_destroy.html +8 -0
  203. nautobot/dcim/templates/dcim/modulebay_retrieve.html +101 -0
  204. nautobot/dcim/templates/dcim/moduletype_list.html +11 -0
  205. nautobot/dcim/templates/dcim/moduletype_retrieve.html +159 -0
  206. nautobot/dcim/templates/dcim/power_port_connection_list.html +7 -5
  207. nautobot/dcim/templates/dcim/softwareimagefile_retrieve.html +65 -19
  208. nautobot/dcim/tests/integration/test_cable_connect_form.py +4 -4
  209. nautobot/dcim/tests/test_api.py +693 -208
  210. nautobot/dcim/tests/test_filters.py +843 -217
  211. nautobot/dcim/tests/test_models.py +1072 -8
  212. nautobot/dcim/tests/test_views.py +1510 -341
  213. nautobot/dcim/urls.py +17 -2
  214. nautobot/dcim/utils.py +2 -3
  215. nautobot/dcim/views.py +1106 -116
  216. nautobot/extras/__init__.py +0 -1
  217. nautobot/extras/api/serializers.py +115 -3
  218. nautobot/extras/api/urls.py +12 -0
  219. nautobot/extras/api/views.py +66 -0
  220. nautobot/extras/apps.py +2 -2
  221. nautobot/extras/choices.py +43 -0
  222. nautobot/extras/context_managers.py +13 -8
  223. nautobot/extras/datasources/git.py +2 -0
  224. nautobot/extras/factory.py +460 -9
  225. nautobot/extras/filters/__init__.py +174 -3
  226. nautobot/extras/filters/mixins.py +46 -43
  227. nautobot/extras/forms/base.py +24 -5
  228. nautobot/extras/forms/forms.py +227 -8
  229. nautobot/extras/forms/mixins.py +93 -0
  230. nautobot/extras/graphql/types.py +23 -10
  231. nautobot/extras/homepage.py +14 -1
  232. nautobot/extras/management/__init__.py +1 -0
  233. nautobot/extras/management/commands/refresh_dynamic_group_member_caches.py +1 -16
  234. nautobot/extras/migrations/0021_customfield_changelog_data.py +1 -0
  235. nautobot/extras/migrations/0109_dynamicgroup_group_type_dynamicgroup_tags_and_more.py +108 -0
  236. nautobot/extras/migrations/0110_alter_configcontext_cluster_groups_and_more.py +111 -0
  237. nautobot/extras/migrations/0111_metadata.py +162 -0
  238. nautobot/extras/migrations/0112_dynamic_group_group_type_data_migration.py +28 -0
  239. nautobot/extras/migrations/0113_saved_views.py +77 -0
  240. nautobot/extras/models/__init__.py +15 -1
  241. nautobot/extras/models/change_logging.py +3 -3
  242. nautobot/extras/models/contacts.py +4 -0
  243. nautobot/extras/models/customfields.py +18 -3
  244. nautobot/extras/models/groups.py +389 -225
  245. nautobot/extras/models/jobs.py +6 -3
  246. nautobot/extras/models/metadata.py +441 -0
  247. nautobot/extras/models/mixins.py +72 -62
  248. nautobot/extras/models/models.py +118 -9
  249. nautobot/extras/models/relationships.py +9 -2
  250. nautobot/extras/models/tags.py +13 -2
  251. nautobot/extras/navigation.py +57 -0
  252. nautobot/extras/plugins/__init__.py +3 -1
  253. nautobot/extras/querysets.py +30 -66
  254. nautobot/extras/signals.py +95 -100
  255. nautobot/extras/tables.py +165 -12
  256. nautobot/extras/templates/extras/dynamicgroup.html +44 -15
  257. nautobot/extras/templates/extras/dynamicgroup_edit.html +2 -0
  258. nautobot/extras/templates/extras/job.html +1 -1
  259. nautobot/extras/templates/extras/jobresult.html +61 -74
  260. nautobot/extras/templates/extras/metadatatype_create.html +89 -0
  261. nautobot/extras/templates/extras/metadatatype_retrieve.html +67 -0
  262. nautobot/extras/templates/extras/object_dynamicgroups.html +7 -0
  263. nautobot/extras/templates/extras/objectchange_list.html +0 -12
  264. nautobot/extras/templates/extras/plugins_list.html +1 -3
  265. nautobot/extras/templates/extras/role_retrieve.html +48 -0
  266. nautobot/extras/templates/extras/staticgroupassociation_retrieve.html +20 -0
  267. nautobot/extras/tests/integration/test_customfields.py +1 -0
  268. nautobot/extras/tests/test_api.py +509 -23
  269. nautobot/extras/tests/test_changelog.py +20 -9
  270. nautobot/extras/tests/test_context_managers.py +22 -15
  271. nautobot/extras/tests/test_datasources.py +13 -1
  272. nautobot/extras/tests/test_dynamicgroups.py +201 -171
  273. nautobot/extras/tests/test_filters.py +211 -12
  274. nautobot/extras/tests/test_jobs.py +6 -6
  275. nautobot/extras/tests/test_models.py +501 -4
  276. nautobot/extras/tests/test_relationships.py +1 -0
  277. nautobot/extras/tests/test_views.py +565 -8
  278. nautobot/extras/tests/test_webhooks.py +1 -1
  279. nautobot/extras/urls.py +5 -0
  280. nautobot/extras/utils.py +51 -11
  281. nautobot/extras/views.py +542 -76
  282. nautobot/ipam/__init__.py +0 -1
  283. nautobot/ipam/apps.py +1 -0
  284. nautobot/ipam/factory.py +17 -19
  285. nautobot/ipam/filters.py +13 -0
  286. nautobot/ipam/forms.py +8 -4
  287. nautobot/ipam/graphql/types.py +2 -2
  288. nautobot/ipam/migrations/0047_alter_ipaddress_role_alter_ipaddress_status_and_more.py +59 -0
  289. nautobot/ipam/models.py +11 -8
  290. nautobot/ipam/querysets.py +1 -1
  291. nautobot/ipam/signals.py +4 -2
  292. nautobot/ipam/tables.py +5 -0
  293. nautobot/ipam/templates/ipam/ipaddress_interfaces.html +1 -1
  294. nautobot/ipam/templates/ipam/ipaddress_vm_interfaces.html +1 -1
  295. nautobot/ipam/templates/ipam/prefix.html +1 -0
  296. nautobot/ipam/tests/test_api.py +37 -18
  297. nautobot/ipam/tests/test_filters.py +26 -2
  298. nautobot/ipam/tests/test_models.py +6 -0
  299. nautobot/ipam/tests/test_querysets.py +1 -1
  300. nautobot/ipam/tests/test_views.py +3 -2
  301. nautobot/ipam/urls.py +2 -2
  302. nautobot/ipam/views.py +18 -26
  303. nautobot/project-static/css/base.css +20 -0
  304. nautobot/project-static/css/dark.css +11 -0
  305. nautobot/project-static/docs/404.html +892 -88
  306. nautobot/project-static/docs/apps/index.html +892 -88
  307. nautobot/project-static/docs/apps/nautobot-apps.html +892 -88
  308. nautobot/project-static/docs/assets/_mkdocstrings.css +5 -0
  309. nautobot/project-static/docs/assets/stylesheets/main.3cba04c6.min.css +1 -0
  310. nautobot/project-static/docs/assets/stylesheets/main.3cba04c6.min.css.map +1 -0
  311. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +919 -120
  312. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +904 -101
  313. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +1618 -903
  314. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +935 -144
  315. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +977 -188
  316. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +901 -99
  317. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +897 -93
  318. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +991 -193
  319. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +974 -131
  320. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +1078 -272
  321. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +1242 -334
  322. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +1727 -875
  323. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +1164 -381
  324. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +2088 -1374
  325. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +2246 -1422
  326. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +912 -111
  327. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +963 -163
  328. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +1010 -223
  329. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +1913 -1277
  330. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +1846 -1102
  331. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +904 -101
  332. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +2331 -1699
  333. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +1802 -1024
  334. nautobot/project-static/docs/development/apps/api/configuration-view.html +892 -88
  335. nautobot/project-static/docs/development/apps/api/database-backend-config.html +892 -88
  336. nautobot/project-static/docs/development/apps/api/models/django-admin.html +892 -88
  337. nautobot/project-static/docs/development/apps/api/models/global-search.html +892 -88
  338. nautobot/project-static/docs/development/apps/api/models/graphql.html +892 -88
  339. nautobot/project-static/docs/development/apps/api/models/index.html +942 -90
  340. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +892 -88
  341. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +892 -88
  342. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +892 -88
  343. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +892 -88
  344. nautobot/project-static/docs/development/apps/api/platform-features/index.html +892 -88
  345. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +892 -88
  346. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +892 -88
  347. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +892 -88
  348. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +892 -88
  349. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +892 -88
  350. nautobot/project-static/docs/development/apps/api/prometheus.html +892 -88
  351. nautobot/project-static/docs/development/apps/api/setup.html +892 -88
  352. nautobot/project-static/docs/development/apps/api/testing.html +892 -88
  353. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +892 -88
  354. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +892 -88
  355. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +892 -88
  356. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +892 -88
  357. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +892 -88
  358. nautobot/project-static/docs/development/apps/api/views/base-template.html +892 -88
  359. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +892 -88
  360. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +892 -88
  361. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +892 -88
  362. nautobot/project-static/docs/development/apps/api/views/index.html +892 -88
  363. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +892 -88
  364. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +892 -88
  365. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +892 -88
  366. nautobot/project-static/docs/development/apps/api/views/notes.html +892 -88
  367. nautobot/project-static/docs/development/apps/api/views/rest-api.html +892 -88
  368. nautobot/project-static/docs/development/apps/api/views/urls.html +892 -88
  369. nautobot/project-static/docs/development/apps/index.html +892 -88
  370. nautobot/project-static/docs/development/apps/migration/code-updates.html +892 -88
  371. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +892 -88
  372. nautobot/project-static/docs/development/apps/migration/from-v1.html +892 -88
  373. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +892 -88
  374. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +892 -88
  375. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +892 -88
  376. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +892 -88
  377. nautobot/project-static/docs/development/apps/porting-from-netbox.html +892 -88
  378. nautobot/project-static/docs/development/core/application-registry.html +892 -88
  379. nautobot/project-static/docs/development/core/best-practices.html +893 -88
  380. nautobot/project-static/docs/development/core/bootstrap-ui.html +892 -88
  381. nautobot/project-static/docs/development/core/caching.html +892 -88
  382. nautobot/project-static/docs/development/core/controllers.html +892 -88
  383. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +892 -88
  384. nautobot/project-static/docs/development/core/generic-views.html +892 -88
  385. nautobot/project-static/docs/development/core/getting-started.html +892 -88
  386. nautobot/project-static/docs/development/core/homepage.html +892 -88
  387. nautobot/project-static/docs/development/core/index.html +892 -88
  388. nautobot/project-static/docs/development/core/model-checklist.html +901 -89
  389. nautobot/project-static/docs/development/core/model-features.html +892 -88
  390. nautobot/project-static/docs/development/core/natural-keys.html +892 -88
  391. nautobot/project-static/docs/development/core/navigation-menu.html +892 -88
  392. nautobot/project-static/docs/development/core/release-checklist.html +895 -91
  393. nautobot/project-static/docs/development/core/role-internals.html +892 -88
  394. nautobot/project-static/docs/development/core/settings.html +892 -88
  395. nautobot/project-static/docs/development/core/style-guide.html +893 -89
  396. nautobot/project-static/docs/development/core/templates.html +904 -89
  397. nautobot/project-static/docs/development/core/testing.html +892 -88
  398. nautobot/project-static/docs/development/core/user-preferences.html +892 -88
  399. nautobot/project-static/docs/development/index.html +892 -88
  400. nautobot/project-static/docs/development/jobs/index.html +893 -89
  401. nautobot/project-static/docs/development/jobs/migration/from-v1.html +892 -88
  402. nautobot/project-static/docs/index.html +892 -88
  403. nautobot/project-static/docs/media/models/cloud_aws_direct_connect_dark.png +0 -0
  404. nautobot/project-static/docs/media/models/cloud_aws_direct_connect_light.png +0 -0
  405. nautobot/project-static/docs/models/cloud/cloudaccount.html +15 -0
  406. nautobot/project-static/docs/models/cloud/cloudnetwork.html +15 -0
  407. nautobot/project-static/docs/models/cloud/cloudnetworkprefixassignment.html +15 -0
  408. nautobot/project-static/docs/models/cloud/cloudresourcetype.html +15 -0
  409. nautobot/project-static/docs/models/cloud/cloudservice.html +15 -0
  410. nautobot/project-static/docs/models/cloud/cloudservicenetworkassignment.html +15 -0
  411. nautobot/project-static/docs/models/dcim/module.html +15 -0
  412. nautobot/project-static/docs/models/dcim/modulebay.html +15 -0
  413. nautobot/project-static/docs/models/dcim/modulebaytemplate.html +15 -0
  414. nautobot/project-static/docs/models/dcim/moduletype.html +15 -0
  415. nautobot/project-static/docs/models/extras/metadatachoice.html +15 -0
  416. nautobot/project-static/docs/models/extras/metadatatype.html +15 -0
  417. nautobot/project-static/docs/models/extras/objectmetadata.html +15 -0
  418. nautobot/project-static/docs/models/extras/role.html +15 -0
  419. nautobot/project-static/docs/models/extras/savedview.html +15 -0
  420. nautobot/project-static/docs/models/extras/staticgroupassociation.html +15 -0
  421. nautobot/project-static/docs/models/extras/status.html +15 -0
  422. nautobot/project-static/docs/objects.inv +0 -0
  423. nautobot/project-static/docs/overview/application_stack.html +900 -89
  424. nautobot/project-static/docs/overview/design_philosophy.html +892 -88
  425. nautobot/project-static/docs/release-notes/index.html +1129 -92
  426. nautobot/project-static/docs/release-notes/version-1.0.html +892 -88
  427. nautobot/project-static/docs/release-notes/version-1.1.html +892 -88
  428. nautobot/project-static/docs/release-notes/version-1.2.html +892 -88
  429. nautobot/project-static/docs/release-notes/version-1.3.html +892 -88
  430. nautobot/project-static/docs/release-notes/version-1.4.html +892 -88
  431. nautobot/project-static/docs/release-notes/version-1.5.html +893 -89
  432. nautobot/project-static/docs/release-notes/version-1.6.html +893 -89
  433. nautobot/project-static/docs/release-notes/version-2.0.html +892 -88
  434. nautobot/project-static/docs/release-notes/version-2.1.html +892 -88
  435. nautobot/project-static/docs/release-notes/version-2.2.html +895 -91
  436. nautobot/project-static/docs/release-notes/version-2.3.html +9954 -0
  437. nautobot/project-static/docs/requirements.txt +5 -5
  438. nautobot/project-static/docs/search/search_index.json +1 -1
  439. nautobot/project-static/docs/sitemap.xml +331 -256
  440. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  441. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +892 -88
  442. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +892 -88
  443. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +892 -88
  444. nautobot/project-static/docs/user-guide/administration/configuration/index.html +892 -88
  445. nautobot/project-static/docs/user-guide/administration/configuration/optional-settings.html +992 -174
  446. nautobot/project-static/docs/user-guide/administration/configuration/required-settings.html +892 -88
  447. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +892 -88
  448. nautobot/project-static/docs/user-guide/administration/guides/caching.html +892 -88
  449. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +896 -88
  450. nautobot/project-static/docs/user-guide/administration/guides/healthcheck.html +892 -88
  451. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +892 -88
  452. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +892 -88
  453. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +892 -88
  454. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +892 -88
  455. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +892 -88
  456. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +892 -88
  457. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +892 -88
  458. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +944 -153
  459. nautobot/project-static/docs/user-guide/administration/installation/index.html +901 -93
  460. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +934 -122
  461. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +954 -157
  462. nautobot/project-static/docs/user-guide/administration/installation/services.html +913 -112
  463. nautobot/project-static/docs/user-guide/administration/installation-extras/docker.html +908 -99
  464. nautobot/project-static/docs/user-guide/administration/installation-extras/health-checks.html +892 -88
  465. nautobot/project-static/docs/user-guide/administration/installation-extras/selinux-troubleshooting.html +892 -88
  466. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +892 -88
  467. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +892 -88
  468. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +893 -89
  469. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +892 -88
  470. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +892 -88
  471. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +892 -88
  472. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +892 -88
  473. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +892 -88
  474. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +892 -88
  475. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +892 -88
  476. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +892 -88
  477. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +892 -88
  478. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +892 -88
  479. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +892 -88
  480. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +893 -89
  481. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +892 -88
  482. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +896 -88
  483. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +895 -91
  484. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +8984 -0
  485. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +8828 -0
  486. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +8829 -0
  487. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +8828 -0
  488. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +8829 -0
  489. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +8833 -0
  490. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +8828 -0
  491. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +906 -102
  492. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +923 -105
  493. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +923 -105
  494. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +918 -100
  495. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +923 -105
  496. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +906 -102
  497. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +906 -102
  498. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +913 -105
  499. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +920 -116
  500. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +921 -117
  501. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +918 -114
  502. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +906 -102
  503. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +914 -105
  504. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +926 -108
  505. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +936 -118
  506. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +928 -106
  507. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +906 -102
  508. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +937 -119
  509. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +928 -110
  510. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +918 -114
  511. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +921 -117
  512. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +923 -115
  513. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +8828 -0
  514. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +8846 -0
  515. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +8843 -0
  516. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +8823 -0
  517. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +916 -112
  518. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +906 -102
  519. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +940 -83
  520. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +924 -106
  521. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +906 -102
  522. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +943 -86
  523. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +921 -103
  524. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +929 -125
  525. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +918 -114
  526. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +906 -102
  527. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +922 -104
  528. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +924 -106
  529. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +906 -102
  530. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +906 -102
  531. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +906 -102
  532. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +936 -88
  533. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +892 -88
  534. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +897 -89
  535. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +897 -89
  536. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +892 -88
  537. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +892 -88
  538. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +892 -88
  539. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +892 -88
  540. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +892 -88
  541. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +892 -88
  542. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +892 -88
  543. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +892 -88
  544. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +892 -88
  545. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +892 -88
  546. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +901 -96
  547. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +892 -88
  548. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +892 -88
  549. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +892 -88
  550. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +892 -88
  551. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +892 -88
  552. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +897 -89
  553. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +892 -88
  554. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +892 -88
  555. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +892 -88
  556. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +892 -88
  557. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +892 -88
  558. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +892 -88
  559. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +892 -88
  560. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +892 -88
  561. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +892 -88
  562. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +892 -88
  563. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +892 -88
  564. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +892 -88
  565. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +892 -88
  566. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/clear-view-button.png +0 -0
  567. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/cleared-view.png +0 -0
  568. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/config-table-columns-to-locations.png +0 -0
  569. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/configure-button.png +0 -0
  570. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/create-saved-view-success.png +0 -0
  571. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/current-saved-view-drop-down-menu.png +0 -0
  572. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/default-location-list-view.png +0 -0
  573. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/dropdown-button-after-new-saved-view.png +0 -0
  574. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/filter-application-to-locations.png +0 -0
  575. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/filter-button.png +0 -0
  576. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/global-default-location-list-view.png +0 -0
  577. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/location-list-view-with-saved-views.png +0 -0
  578. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/navigation-menu.png +0 -0
  579. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/save-as-new-view-drop-down.png +0 -0
  580. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/save-view-modal.png +0 -0
  581. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-buttons.png +0 -0
  582. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-success.png +0 -0
  583. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-view-unchecked.png +0 -0
  584. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-view.png +0 -0
  585. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-different-user.png +0 -0
  586. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-modal-unchecked.png +0 -0
  587. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/set-as-my-default-button.png +0 -0
  588. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/set-as-my-default-success.png +0 -0
  589. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/unsaved-saved-view.png +0 -0
  590. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/updated-saved-view.png +0 -0
  591. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +892 -88
  592. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +892 -88
  593. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +892 -88
  594. nautobot/project-static/docs/user-guide/index.html +892 -88
  595. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +892 -88
  596. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +892 -88
  597. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +892 -88
  598. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +892 -88
  599. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +1258 -785
  600. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +895 -91
  601. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +892 -88
  602. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +892 -88
  603. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +892 -88
  604. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +892 -88
  605. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +892 -88
  606. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +892 -88
  607. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +892 -88
  608. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +892 -88
  609. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +892 -88
  610. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +896 -88
  611. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +892 -88
  612. nautobot/project-static/docs/user-guide/platform-functionality/note.html +895 -91
  613. nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +9061 -0
  614. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +895 -91
  615. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +892 -88
  616. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +892 -88
  617. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +892 -88
  618. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +892 -88
  619. nautobot/project-static/docs/user-guide/platform-functionality/role.html +895 -91
  620. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +9137 -0
  621. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +895 -91
  622. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +8933 -0
  623. nautobot/project-static/docs/user-guide/platform-functionality/status.html +892 -88
  624. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +892 -88
  625. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +950 -121
  626. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +892 -88
  627. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +892 -88
  628. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +892 -88
  629. nautobot/project-static/js/forms.js +71 -0
  630. nautobot/project-static/js/table_sorting_indicator.js +46 -0
  631. nautobot/project-static/js/tableconfig.js +6 -1
  632. nautobot/project-static/materialdesignicons-7.4.47/css/materialdesignicons.min.css +3 -0
  633. nautobot/project-static/{materialdesignicons-6.5.95 → materialdesignicons-7.4.47}/fonts/materialdesignicons-webfont.eot +0 -0
  634. nautobot/project-static/{materialdesignicons-6.5.95 → materialdesignicons-7.4.47}/fonts/materialdesignicons-webfont.ttf +0 -0
  635. nautobot/project-static/materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.woff +0 -0
  636. nautobot/project-static/materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.woff2 +0 -0
  637. nautobot/tenancy/__init__.py +0 -1
  638. nautobot/tenancy/apps.py +1 -0
  639. nautobot/tenancy/factory.py +3 -2
  640. nautobot/tenancy/filters/__init__.py +1 -0
  641. nautobot/tenancy/forms.py +1 -1
  642. nautobot/tenancy/templates/tenancy/tenant.html +24 -20
  643. nautobot/tenancy/views.py +11 -10
  644. nautobot/users/__init__.py +0 -1
  645. nautobot/users/api/serializers.py +1 -1
  646. nautobot/users/api/views.py +4 -2
  647. nautobot/users/apps.py +3 -2
  648. nautobot/users/factory.py +3 -3
  649. nautobot/users/migrations/0010_user_default_saved_views.py +20 -0
  650. nautobot/users/models.py +12 -0
  651. nautobot/users/tests/test_filters.py +6 -3
  652. nautobot/users/urls.py +8 -0
  653. nautobot/virtualization/__init__.py +0 -1
  654. nautobot/virtualization/apps.py +1 -0
  655. nautobot/virtualization/filters.py +6 -1
  656. nautobot/virtualization/forms.py +11 -3
  657. nautobot/virtualization/graphql/types.py +2 -2
  658. nautobot/virtualization/migrations/0029_add_role_field_to_interface_models.py +27 -0
  659. nautobot/virtualization/migrations/0030_alter_virtualmachine_local_config_context_data_owner_content_type_and_more.py +67 -0
  660. nautobot/virtualization/models.py +0 -2
  661. nautobot/virtualization/tables.py +10 -3
  662. nautobot/virtualization/templates/virtualization/virtualmachine.html +1 -1
  663. nautobot/virtualization/templates/virtualization/vminterface.html +7 -1
  664. nautobot/virtualization/templates/virtualization/vminterface_edit.html +1 -0
  665. nautobot/virtualization/tests/test_api.py +9 -4
  666. nautobot/virtualization/tests/test_filters.py +22 -0
  667. nautobot/virtualization/tests/test_models.py +7 -3
  668. nautobot/virtualization/tests/test_views.py +19 -3
  669. nautobot/virtualization/urls.py +2 -2
  670. nautobot/virtualization/views.py +10 -32
  671. {nautobot-2.2.9.dist-info → nautobot-2.3.0.dist-info}/METADATA +20 -18
  672. {nautobot-2.2.9.dist-info → nautobot-2.3.0.dist-info}/RECORD +677 -557
  673. nautobot/project-static/docs/assets/stylesheets/main.76a95c52.min.css +0 -1
  674. nautobot/project-static/docs/assets/stylesheets/main.76a95c52.min.css.map +0 -1
  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.0.dist-info}/LICENSE.txt +0 -0
  695. {nautobot-2.2.9.dist-info → nautobot-2.3.0.dist-info}/NOTICE +0 -0
  696. {nautobot-2.2.9.dist-info → nautobot-2.3.0.dist-info}/WHEEL +0 -0
  697. {nautobot-2.2.9.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)
@@ -2587,6 +3049,56 @@ class ObjectChangeTestCase(TestCase):
2587
3049
  self.assertHttpStatus(response, 200)
2588
3050
 
2589
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
+
2590
3102
  class RelationshipTestCase(
2591
3103
  ViewTestCases.CreateObjectViewTestCase,
2592
3104
  ViewTestCases.DeleteObjectViewTestCase,
@@ -2844,7 +3356,51 @@ class RelationshipAssociationTestCase(
2844
3356
  self.assertNotIn(instance2.destination.name, content, msg=content)
2845
3357
 
2846
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
+
2847
3402
  class StatusTestCase(
3403
+ # TODO? ViewTestCases.BulkDeleteObjectsViewTestCase,
2848
3404
  ViewTestCases.CreateObjectViewTestCase,
2849
3405
  ViewTestCases.DeleteObjectViewTestCase,
2850
3406
  ViewTestCases.EditObjectViewTestCase,
@@ -2876,6 +3432,11 @@ class TeamTestCase(ViewTestCases.PrimaryObjectViewTestCase):
2876
3432
 
2877
3433
  @classmethod
2878
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
+
2879
3440
  cls.form_data = {
2880
3441
  "name": "new team",
2881
3442
  "phone": "555-0122",
@@ -3084,11 +3645,7 @@ class RoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
3084
3645
  for model_class in eligible_ct_model_classes:
3085
3646
  verbose_name_plural = model_class._meta.verbose_name_plural
3086
3647
  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"
3648
+ result = " ".join(bettertitle(elem) for elem in verbose_name_plural.split())
3092
3649
  # Assert tables are correctly rendered
3093
3650
  if content_type not in role_content_types:
3094
3651
  if result == "Contact Associations":