nautobot 2.3.16__py3-none-any.whl → 2.4.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 (721) hide show
  1. nautobot/__init__.py +15 -0
  2. nautobot/apps/__init__.py +1 -1
  3. nautobot/apps/api.py +8 -10
  4. nautobot/apps/change_logging.py +2 -2
  5. nautobot/apps/choices.py +4 -4
  6. nautobot/apps/config.py +32 -3
  7. nautobot/apps/events.py +19 -0
  8. nautobot/apps/exceptions.py +0 -2
  9. nautobot/apps/factory.py +2 -2
  10. nautobot/apps/filters.py +1 -1
  11. nautobot/apps/forms.py +20 -20
  12. nautobot/apps/graphql.py +2 -2
  13. nautobot/apps/jobs.py +8 -8
  14. nautobot/apps/models.py +19 -19
  15. nautobot/apps/tables.py +1 -1
  16. nautobot/apps/testing.py +10 -10
  17. nautobot/apps/ui.py +44 -9
  18. nautobot/apps/utils.py +7 -15
  19. nautobot/apps/views.py +8 -6
  20. nautobot/circuits/api/serializers.py +1 -0
  21. nautobot/circuits/api/views.py +4 -8
  22. nautobot/circuits/navigation.py +0 -57
  23. nautobot/circuits/templates/circuits/circuit_create.html +1 -7
  24. nautobot/circuits/templates/circuits/circuit_retrieve.html +0 -71
  25. nautobot/circuits/templates/circuits/inc/circuit_termination.html +6 -64
  26. nautobot/circuits/templates/circuits/inc/circuit_termination_cable_fragment.html +40 -0
  27. nautobot/circuits/templates/circuits/inc/circuit_termination_header_extra_content.html +26 -0
  28. nautobot/circuits/templates/circuits/provider_retrieve.html +0 -76
  29. nautobot/circuits/tests/integration/test_relationships.py +33 -24
  30. nautobot/circuits/tests/test_filters.py +4 -8
  31. nautobot/circuits/views.py +140 -23
  32. nautobot/cloud/api/views.py +6 -10
  33. nautobot/cloud/factory.py +4 -1
  34. nautobot/cloud/tests/test_filters.py +5 -4
  35. nautobot/cloud/views.py +0 -16
  36. nautobot/core/api/constants.py +11 -0
  37. nautobot/core/api/filter_backends.py +3 -9
  38. nautobot/core/api/metadata.py +28 -256
  39. nautobot/core/api/pagination.py +3 -2
  40. nautobot/core/api/renderers.py +3 -0
  41. nautobot/core/api/schema.py +13 -2
  42. nautobot/core/api/serializers.py +45 -259
  43. nautobot/core/api/urls.py +3 -4
  44. nautobot/core/api/utils.py +0 -62
  45. nautobot/core/api/views.py +99 -157
  46. nautobot/core/apps/__init__.py +22 -578
  47. nautobot/core/celery/__init__.py +13 -0
  48. nautobot/core/celery/schedulers.py +47 -2
  49. nautobot/core/choices.py +2 -2
  50. nautobot/core/cli/__init__.py +8 -0
  51. nautobot/core/constants.py +7 -0
  52. nautobot/core/events/__init__.py +116 -0
  53. nautobot/core/events/base.py +27 -0
  54. nautobot/core/events/exceptions.py +10 -0
  55. nautobot/core/events/redis_broker.py +48 -0
  56. nautobot/core/events/syslog_broker.py +19 -0
  57. nautobot/core/exceptions.py +0 -6
  58. nautobot/core/forms/__init__.py +19 -19
  59. nautobot/core/forms/fields.py +57 -9
  60. nautobot/core/forms/forms.py +33 -2
  61. nautobot/core/forms/utils.py +2 -1
  62. nautobot/core/graphql/schema.py +3 -1
  63. nautobot/core/jobs/__init__.py +24 -3
  64. nautobot/core/jobs/bulk_actions.py +248 -0
  65. nautobot/core/jobs/cleanup.py +1 -1
  66. nautobot/core/management/commands/generate_test_data.py +21 -0
  67. nautobot/core/middleware.py +16 -0
  68. nautobot/core/models/fields.py +11 -7
  69. nautobot/core/settings.py +68 -4
  70. nautobot/core/settings.yaml +99 -0
  71. nautobot/core/tables.py +10 -46
  72. nautobot/core/tasks.py +1 -1
  73. nautobot/core/templates/about.html +67 -0
  74. nautobot/core/templates/components/button/default.html +7 -0
  75. nautobot/core/templates/components/button/dropdown.html +20 -0
  76. nautobot/core/templates/components/layout/one_over_two.html +19 -0
  77. nautobot/core/templates/components/layout/two_over_one.html +19 -0
  78. nautobot/core/templates/components/panel/body_content_data_table.html +27 -0
  79. nautobot/core/templates/components/panel/body_content_objects_table.html +4 -0
  80. nautobot/core/templates/components/panel/body_content_tags.html +6 -0
  81. nautobot/core/templates/components/panel/body_content_text.html +12 -0
  82. nautobot/core/templates/components/panel/body_wrapper_generic.html +3 -0
  83. nautobot/core/templates/components/panel/body_wrapper_key_value_table.html +3 -0
  84. nautobot/core/templates/components/panel/body_wrapper_table.html +3 -0
  85. nautobot/core/templates/components/panel/footer_contacts_table.html +20 -0
  86. nautobot/core/templates/components/panel/footer_content_table.html +14 -0
  87. nautobot/core/templates/components/panel/grouping_toggle.html +14 -0
  88. nautobot/core/templates/components/panel/header_extra_content_table.html +3 -0
  89. nautobot/core/templates/components/panel/panel.html +16 -0
  90. nautobot/core/templates/components/panel/stats_panel_body.html +8 -0
  91. nautobot/core/templates/components/tab/content_wrapper.html +3 -0
  92. nautobot/core/templates/components/tab/label_wrapper.html +5 -0
  93. nautobot/core/templates/components/tab/label_wrapper_distinct_view.html +3 -0
  94. nautobot/core/templates/generic/object_retrieve.html +28 -17
  95. nautobot/core/templates/inc/computed_fields/panel_data.html +4 -7
  96. nautobot/core/templates/inc/custom_fields/panel.html +2 -2
  97. nautobot/core/templates/inc/custom_fields/panel_data.html +4 -7
  98. nautobot/core/templates/inc/footer.html +1 -0
  99. nautobot/core/templates/inc/nav_menu.html +2 -1
  100. nautobot/core/templates/inc/relationships_panel.html +1 -1
  101. nautobot/core/templates/inc/tenancy_form_panel.html +9 -0
  102. nautobot/core/templates/inc/tenant_table_row.html +11 -0
  103. nautobot/core/templates/nautobot_config.py.j2 +13 -0
  104. nautobot/core/templates/panel_table.html +12 -0
  105. nautobot/core/templates/utilities/render_jinja2.html +117 -0
  106. nautobot/core/templates/utilities/templatetags/tag.html +1 -1
  107. nautobot/core/templates/utilities/theme_preview.html +7 -0
  108. nautobot/core/templatetags/helpers.py +104 -6
  109. nautobot/core/templatetags/ui_framework.py +40 -0
  110. nautobot/core/testing/__init__.py +8 -8
  111. nautobot/core/testing/api.py +187 -137
  112. nautobot/core/testing/context.py +18 -0
  113. nautobot/core/testing/filters.py +41 -35
  114. nautobot/core/testing/forms.py +2 -0
  115. nautobot/core/testing/views.py +65 -148
  116. nautobot/core/tests/integration/test_view_authentication.py +1 -1
  117. nautobot/core/tests/nautobot_config.py +198 -0
  118. nautobot/core/tests/runner.py +2 -2
  119. nautobot/core/tests/test_api.py +154 -176
  120. nautobot/core/tests/test_events.py +214 -0
  121. nautobot/core/tests/test_forms.py +1 -0
  122. nautobot/core/tests/test_jinja_filters.py +1 -0
  123. nautobot/core/tests/test_jobs.py +387 -14
  124. nautobot/core/tests/test_navigations.py +7 -241
  125. nautobot/core/tests/test_settings_schema.py +7 -0
  126. nautobot/core/tests/test_tables.py +100 -0
  127. nautobot/core/tests/test_templatetags_helpers.py +16 -0
  128. nautobot/core/tests/test_ui.py +150 -0
  129. nautobot/core/tests/test_utils.py +55 -18
  130. nautobot/core/tests/test_views.py +124 -5
  131. nautobot/core/ui/__init__.py +0 -0
  132. nautobot/core/ui/base.py +11 -0
  133. nautobot/core/ui/choices.py +44 -0
  134. nautobot/core/ui/homepage.py +167 -0
  135. nautobot/core/ui/nav.py +280 -0
  136. nautobot/core/ui/object_detail.py +1855 -0
  137. nautobot/core/ui/utils.py +36 -0
  138. nautobot/core/urls.py +6 -0
  139. nautobot/core/utils/config.py +30 -3
  140. nautobot/core/utils/lookup.py +12 -2
  141. nautobot/core/utils/querysets.py +64 -0
  142. nautobot/core/utils/requests.py +24 -9
  143. nautobot/core/views/__init__.py +48 -1
  144. nautobot/core/views/generic.py +37 -140
  145. nautobot/core/views/mixins.py +82 -32
  146. nautobot/core/views/paginator.py +8 -5
  147. nautobot/core/views/renderers.py +9 -9
  148. nautobot/core/views/utils.py +11 -0
  149. nautobot/core/wsgi.py +3 -3
  150. nautobot/dcim/api/serializers.py +34 -141
  151. nautobot/dcim/api/urls.py +5 -0
  152. nautobot/dcim/api/views.py +57 -110
  153. nautobot/dcim/apps.py +1 -0
  154. nautobot/dcim/choices.py +28 -0
  155. nautobot/dcim/factory.py +58 -0
  156. nautobot/dcim/filters/__init__.py +204 -2
  157. nautobot/dcim/forms.py +219 -9
  158. nautobot/dcim/migrations/0063_interfacevdcassignment_virtualdevicecontext_and_more.py +165 -0
  159. nautobot/dcim/migrations/0064_virtualdevicecontext_status_data_migration.py +28 -0
  160. nautobot/dcim/migrations/0065_controller_capabilities_and_more.py +29 -0
  161. nautobot/dcim/migrations/0066_controllermanageddevicegroup_radio_profiles_and_more.py +33 -0
  162. nautobot/dcim/migrations/0067_controllermanageddevicegroup_tenant.py +25 -0
  163. nautobot/dcim/models/__init__.py +5 -1
  164. nautobot/dcim/models/devices.py +180 -2
  165. nautobot/dcim/models/racks.py +2 -2
  166. nautobot/dcim/navigation.py +25 -224
  167. nautobot/dcim/signals.py +44 -0
  168. nautobot/dcim/tables/__init__.py +2 -0
  169. nautobot/dcim/tables/devices.py +103 -7
  170. nautobot/dcim/tables/racks.py +1 -1
  171. nautobot/dcim/templates/dcim/controller/base.html +10 -0
  172. nautobot/dcim/templates/dcim/controller_create.html +2 -7
  173. nautobot/dcim/templates/dcim/controller_retrieve.html +6 -10
  174. nautobot/dcim/templates/dcim/controller_wirelessnetworks.html +25 -0
  175. nautobot/dcim/templates/dcim/controllermanageddevicegroup_create.html +68 -0
  176. nautobot/dcim/templates/dcim/controllermanageddevicegroup_retrieve.html +51 -0
  177. nautobot/dcim/templates/dcim/device/base.html +6 -42
  178. nautobot/dcim/templates/dcim/device/wireless.html +73 -0
  179. nautobot/dcim/templates/dcim/device.html +4 -10
  180. nautobot/dcim/templates/dcim/device_edit.html +36 -37
  181. nautobot/dcim/templates/dcim/interface.html +1 -0
  182. nautobot/dcim/templates/dcim/interface_edit.html +1 -0
  183. nautobot/dcim/templates/dcim/location.html +1 -9
  184. nautobot/dcim/templates/dcim/location_edit.html +1 -7
  185. nautobot/dcim/templates/dcim/locationtype.html +0 -107
  186. nautobot/dcim/templates/dcim/locationtype_retrieve.html +8 -0
  187. nautobot/dcim/templates/dcim/rack.html +1 -9
  188. nautobot/dcim/templates/dcim/rack_edit.html +1 -7
  189. nautobot/dcim/templates/dcim/rackreservation.html +1 -9
  190. nautobot/dcim/templates/dcim/virtualdevicecontext_retrieve.html +68 -0
  191. nautobot/dcim/templates/dcim/virtualdevicecontext_update.html +28 -0
  192. nautobot/dcim/tests/integration/test_controller.py +62 -0
  193. nautobot/dcim/tests/integration/test_controller_managed_device_group.py +71 -0
  194. nautobot/dcim/tests/test_api.py +188 -64
  195. nautobot/dcim/tests/test_filters.py +171 -76
  196. nautobot/dcim/tests/test_jobs.py +118 -0
  197. nautobot/dcim/tests/test_models.py +157 -6
  198. nautobot/dcim/tests/test_signals.py +1 -0
  199. nautobot/dcim/tests/test_views.py +118 -88
  200. nautobot/dcim/urls.py +72 -27
  201. nautobot/dcim/utils.py +2 -2
  202. nautobot/dcim/views.py +356 -61
  203. nautobot/extras/api/serializers.py +39 -18
  204. nautobot/extras/api/urls.py +4 -0
  205. nautobot/extras/api/views.py +89 -31
  206. nautobot/extras/choices.py +13 -0
  207. nautobot/extras/constants.py +2 -1
  208. nautobot/extras/context_managers.py +23 -6
  209. nautobot/extras/datasources/git.py +4 -1
  210. nautobot/extras/factory.py +27 -0
  211. nautobot/extras/filters/__init__.py +66 -5
  212. nautobot/extras/forms/base.py +2 -2
  213. nautobot/extras/forms/forms.py +262 -59
  214. nautobot/extras/forms/mixins.py +2 -2
  215. nautobot/extras/graphql/types.py +25 -1
  216. nautobot/extras/jobs.py +109 -15
  217. nautobot/extras/management/__init__.py +1 -0
  218. nautobot/extras/management/commands/runjob.py +7 -79
  219. nautobot/extras/management/commands/runjob_with_job_result.py +46 -0
  220. nautobot/extras/management/utils.py +87 -0
  221. nautobot/extras/migrations/0117_create_job_queue_model.py +129 -0
  222. nautobot/extras/migrations/0118_task_queue_to_job_queue_migration.py +78 -0
  223. nautobot/extras/migrations/0119_remove_task_queues_from_job_and_queue_from_scheduled_job.py +28 -0
  224. nautobot/extras/migrations/0120_job_is_singleton_job_is_singleton_override.py +22 -0
  225. nautobot/extras/migrations/0121_alter_team_contacts.py +17 -0
  226. nautobot/extras/models/__init__.py +5 -1
  227. nautobot/extras/models/change_logging.py +7 -3
  228. nautobot/extras/models/contacts.py +1 -1
  229. nautobot/extras/models/groups.py +0 -2
  230. nautobot/extras/models/jobs.py +233 -33
  231. nautobot/extras/models/relationships.py +69 -1
  232. nautobot/extras/models/secrets.py +5 -0
  233. nautobot/extras/navigation.py +20 -262
  234. nautobot/extras/plugins/__init__.py +54 -19
  235. nautobot/extras/plugins/marketplace_manifest.yml +455 -0
  236. nautobot/extras/plugins/tables.py +16 -14
  237. nautobot/extras/plugins/urls.py +1 -0
  238. nautobot/extras/plugins/views.py +103 -60
  239. nautobot/extras/registry.py +1 -1
  240. nautobot/extras/secrets/__init__.py +2 -2
  241. nautobot/extras/signals.py +39 -1
  242. nautobot/extras/tables.py +37 -1
  243. nautobot/extras/templates/extras/dynamicgroup.html +1 -9
  244. nautobot/extras/templates/extras/externalintegration_retrieve.html +0 -47
  245. nautobot/extras/templates/extras/inc/tags_panel.html +1 -5
  246. nautobot/extras/templates/extras/job_bulk_edit.html +2 -1
  247. nautobot/extras/templates/extras/job_detail.html +52 -6
  248. nautobot/extras/templates/extras/job_edit.html +6 -2
  249. nautobot/extras/templates/extras/job_list.html +2 -7
  250. nautobot/extras/templates/extras/jobqueue_retrieve.html +36 -0
  251. nautobot/extras/templates/extras/marketplace.html +296 -0
  252. nautobot/extras/templates/extras/plugin_detail.html +32 -15
  253. nautobot/extras/templates/extras/plugins_list.html +35 -1
  254. nautobot/extras/templates/extras/plugins_tiles.html +90 -0
  255. nautobot/extras/templates/extras/role_retrieve.html +16 -0
  256. nautobot/extras/templates/extras/secret.html +0 -65
  257. nautobot/extras/templates/extras/secret_check.js +16 -0
  258. nautobot/extras/templates/extras/secret_create.html +114 -0
  259. nautobot/extras/templates/extras/secret_edit.html +1 -114
  260. nautobot/extras/templates/extras/secretsgroup_edit.html +1 -1
  261. nautobot/extras/templates/extras/templatetags/plugin_object_detail_tabs.html +2 -0
  262. nautobot/extras/templatetags/job_buttons.py +5 -4
  263. nautobot/extras/templatetags/plugins.py +69 -6
  264. nautobot/extras/test_jobs/singleton.py +16 -0
  265. nautobot/extras/tests/test_api.py +145 -43
  266. nautobot/extras/tests/test_context_managers.py +4 -1
  267. nautobot/extras/tests/test_filters.py +213 -529
  268. nautobot/extras/tests/test_job_variables.py +73 -152
  269. nautobot/extras/tests/test_jobs.py +181 -51
  270. nautobot/extras/tests/test_models.py +61 -6
  271. nautobot/extras/tests/test_plugins.py +62 -9
  272. nautobot/extras/tests/test_relationships.py +123 -9
  273. nautobot/extras/tests/test_utils.py +23 -2
  274. nautobot/extras/tests/test_views.py +146 -145
  275. nautobot/extras/tests/test_webhooks.py +2 -1
  276. nautobot/extras/urls.py +2 -20
  277. nautobot/extras/utils.py +119 -4
  278. nautobot/extras/views.py +168 -125
  279. nautobot/extras/webhooks.py +5 -2
  280. nautobot/ipam/api/serializers.py +10 -103
  281. nautobot/ipam/api/views.py +31 -49
  282. nautobot/ipam/factory.py +1 -1
  283. nautobot/ipam/filters.py +3 -2
  284. nautobot/ipam/models.py +10 -12
  285. nautobot/ipam/navigation.py +0 -90
  286. nautobot/ipam/tables.py +3 -1
  287. nautobot/ipam/templates/ipam/ipaddress.html +1 -9
  288. nautobot/ipam/templates/ipam/ipaddress_bulk_add.html +1 -7
  289. nautobot/ipam/templates/ipam/ipaddress_edit.html +1 -7
  290. nautobot/ipam/templates/ipam/prefix.html +1 -9
  291. nautobot/ipam/templates/ipam/prefix_edit.html +1 -7
  292. nautobot/ipam/templates/ipam/routetarget.html +0 -28
  293. nautobot/ipam/templates/ipam/vlan.html +1 -9
  294. nautobot/ipam/templates/ipam/vlan_edit.html +1 -7
  295. nautobot/ipam/templates/ipam/vrf.html +0 -47
  296. nautobot/ipam/templates/ipam/vrf_edit.html +1 -7
  297. nautobot/ipam/tests/test_api.py +7 -5
  298. nautobot/ipam/tests/test_filters.py +39 -119
  299. nautobot/ipam/tests/test_forms.py +0 -2
  300. nautobot/ipam/tests/test_models.py +56 -36
  301. nautobot/ipam/tests/test_views.py +3 -0
  302. nautobot/ipam/urls.py +3 -69
  303. nautobot/ipam/utils/__init__.py +16 -10
  304. nautobot/ipam/views.py +91 -162
  305. nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap-theme.css.map +1 -1
  306. nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap-theme.min.css.map +1 -1
  307. nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap.css +40 -2
  308. nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap.css.map +1 -1
  309. nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap.min.css +1 -1
  310. nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap.min.css.map +1 -1
  311. nautobot/project-static/css/base.css +38 -3
  312. nautobot/project-static/docs/404.html +461 -17
  313. nautobot/project-static/docs/apps/index.html +461 -17
  314. nautobot/project-static/docs/apps/nautobot-apps.html +462 -19
  315. nautobot/project-static/docs/assets/_mkdocstrings.css +25 -1
  316. nautobot/project-static/docs/assets/extra.css +5 -1
  317. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +477 -23
  318. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +474 -20
  319. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +790 -289
  320. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +505 -31
  321. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +510 -34
  322. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +471 -20
  323. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +467 -18
  324. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +497 -33
  325. nautobot/project-static/docs/code-reference/nautobot/apps/events.html +9883 -0
  326. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +523 -75
  327. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +546 -51
  328. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +670 -94
  329. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +1030 -177
  330. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +524 -49
  331. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +874 -188
  332. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +955 -235
  333. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +475 -21
  334. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +486 -28
  335. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +661 -99
  336. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +947 -479
  337. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +6425 -1234
  338. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +474 -20
  339. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +877 -344
  340. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +828 -171
  341. nautobot/project-static/docs/development/apps/api/configuration-view.html +461 -17
  342. nautobot/project-static/docs/development/apps/api/database-backend-config.html +461 -17
  343. nautobot/project-static/docs/development/apps/api/models/django-admin.html +461 -17
  344. nautobot/project-static/docs/development/apps/api/models/global-search.html +461 -17
  345. nautobot/project-static/docs/development/apps/api/models/graphql.html +461 -17
  346. nautobot/project-static/docs/development/apps/api/models/index.html +461 -17
  347. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +461 -17
  348. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +461 -17
  349. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +461 -17
  350. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +461 -17
  351. nautobot/project-static/docs/development/apps/api/platform-features/index.html +461 -17
  352. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +461 -17
  353. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +461 -17
  354. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +461 -17
  355. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +461 -17
  356. nautobot/project-static/docs/development/apps/api/platform-features/table-extensions.html +461 -17
  357. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +461 -17
  358. nautobot/project-static/docs/development/apps/api/prometheus.html +461 -17
  359. nautobot/project-static/docs/development/apps/api/setup.html +465 -153
  360. nautobot/project-static/docs/development/apps/api/testing.html +461 -17
  361. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +461 -17
  362. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +461 -17
  363. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +461 -17
  364. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +461 -17
  365. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +741 -128
  366. nautobot/project-static/docs/development/apps/api/views/base-template.html +461 -17
  367. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +461 -17
  368. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +461 -17
  369. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +461 -17
  370. nautobot/project-static/docs/development/apps/api/views/index.html +463 -18
  371. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +465 -17
  372. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +491 -17
  373. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +461 -17
  374. nautobot/project-static/docs/development/apps/api/views/notes.html +461 -17
  375. nautobot/project-static/docs/development/apps/api/views/rest-api.html +467 -19
  376. nautobot/project-static/docs/development/apps/api/views/urls.html +461 -17
  377. nautobot/project-static/docs/development/apps/index.html +461 -17
  378. nautobot/project-static/docs/development/apps/migration/code-updates.html +462 -50
  379. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +462 -18
  380. nautobot/project-static/docs/development/apps/migration/from-v1.html +461 -17
  381. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +461 -17
  382. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +461 -17
  383. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +461 -17
  384. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +464 -20
  385. nautobot/project-static/docs/development/apps/migration/ui-component-framework/best-practices.html +9261 -0
  386. nautobot/project-static/docs/development/apps/migration/ui-component-framework/custom-content.html +9375 -0
  387. nautobot/project-static/docs/development/apps/migration/ui-component-framework/index.html +9671 -0
  388. nautobot/project-static/docs/development/apps/migration/ui-component-framework/migration-steps.html +9559 -0
  389. nautobot/project-static/docs/development/apps/porting-from-netbox.html +464 -20
  390. nautobot/project-static/docs/development/core/application-registry.html +461 -17
  391. nautobot/project-static/docs/development/core/best-practices.html +461 -17
  392. nautobot/project-static/docs/development/core/bootstrap-ui.html +461 -17
  393. nautobot/project-static/docs/development/core/caching.html +461 -17
  394. nautobot/project-static/docs/development/core/controllers.html +463 -17
  395. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +464 -20
  396. nautobot/project-static/docs/development/core/generic-views.html +461 -17
  397. nautobot/project-static/docs/development/core/getting-started.html +539 -127
  398. nautobot/project-static/docs/development/core/homepage.html +472 -28
  399. nautobot/project-static/docs/development/core/index.html +461 -17
  400. nautobot/project-static/docs/development/core/minikube-dev-environment-for-k8s-jobs.html +9754 -0
  401. nautobot/project-static/docs/development/core/model-checklist.html +471 -25
  402. nautobot/project-static/docs/development/core/model-features.html +461 -17
  403. nautobot/project-static/docs/development/core/natural-keys.html +461 -17
  404. nautobot/project-static/docs/development/core/navigation-menu.html +478 -24
  405. nautobot/project-static/docs/development/core/release-checklist.html +478 -46
  406. nautobot/project-static/docs/development/core/role-internals.html +461 -17
  407. nautobot/project-static/docs/development/core/settings.html +461 -17
  408. nautobot/project-static/docs/development/core/style-guide.html +464 -20
  409. nautobot/project-static/docs/development/core/templates.html +471 -20
  410. nautobot/project-static/docs/development/core/testing.html +461 -17
  411. nautobot/project-static/docs/development/core/ui-component-framework.html +11116 -0
  412. nautobot/project-static/docs/development/core/user-preferences.html +464 -20
  413. nautobot/project-static/docs/development/index.html +461 -17
  414. nautobot/project-static/docs/development/jobs/index.html +499 -19
  415. nautobot/project-static/docs/development/jobs/migration/from-v1.html +461 -17
  416. nautobot/project-static/docs/index.html +469 -36
  417. nautobot/project-static/docs/media/development/core/kubernetes/k8s_job_edit.png +0 -0
  418. nautobot/project-static/docs/media/development/core/kubernetes/k8s_job_edit_button.png +0 -0
  419. nautobot/project-static/docs/media/development/core/kubernetes/k8s_job_list_nav.png +0 -0
  420. nautobot/project-static/docs/media/development/core/kubernetes/k8s_job_list_view.png +0 -0
  421. nautobot/project-static/docs/media/development/core/kubernetes/k8s_job_queue.png +0 -0
  422. nautobot/project-static/docs/media/development/core/kubernetes/k8s_job_queue_add.png +0 -0
  423. nautobot/project-static/docs/media/development/core/kubernetes/k8s_job_queue_config.png +0 -0
  424. nautobot/project-static/docs/media/development/core/kubernetes/k8s_job_result_completed.png +0 -0
  425. nautobot/project-static/docs/media/development/core/kubernetes/k8s_job_result_nav.png +0 -0
  426. nautobot/project-static/docs/media/development/core/kubernetes/k8s_job_result_pending.png +0 -0
  427. nautobot/project-static/docs/media/development/core/kubernetes/k8s_job_run_form.png +0 -0
  428. nautobot/project-static/docs/media/development/core/kubernetes/k8s_nautobot_login.png +0 -0
  429. nautobot/project-static/docs/media/development/core/kubernetes/k8s_run_job.png +0 -0
  430. nautobot/project-static/docs/media/development/core/kubernetes/k8s_run_scheduled_job_form.png +0 -0
  431. nautobot/project-static/docs/media/development/core/kubernetes/k8s_scheduled_job_result.png +0 -0
  432. nautobot/project-static/docs/media/development/core/ui-component-framework/basic-panel-layout.png +0 -0
  433. nautobot/project-static/docs/media/development/core/ui-component-framework/button-example.png +0 -0
  434. nautobot/project-static/docs/media/development/core/ui-component-framework/buttons-example.png +0 -0
  435. nautobot/project-static/docs/media/development/core/ui-component-framework/cluster-type-before-after-example.png +0 -0
  436. nautobot/project-static/docs/media/development/core/ui-component-framework/dropdown-button-example.png +0 -0
  437. nautobot/project-static/docs/media/development/core/ui-component-framework/grouped-key-value-table-panel-example-1.png +0 -0
  438. nautobot/project-static/docs/media/development/core/ui-component-framework/grouped-key-value-table-panel-example-2.png +0 -0
  439. nautobot/project-static/docs/media/development/core/ui-component-framework/object-fields-panel-example.png +0 -0
  440. nautobot/project-static/docs/media/development/core/ui-component-framework/object-fields-panel-example_2.png +0 -0
  441. nautobot/project-static/docs/media/development/core/ui-component-framework/stats-panel-example-code.png +0 -0
  442. nautobot/project-static/docs/media/development/core/ui-component-framework/stats-panel-example.png +0 -0
  443. nautobot/project-static/docs/media/development/core/ui-component-framework/table-panels-family.png +0 -0
  444. nautobot/project-static/docs/media/development/core/ui-component-framework/text-panels-family.png +0 -0
  445. nautobot/project-static/docs/media/development/core/ui-component-framework/ui-framework-example.png +0 -0
  446. nautobot/project-static/docs/media/models/virtual_device_context_overview.drawio +73 -0
  447. nautobot/project-static/docs/media/models/virtual_device_context_overview.png +0 -0
  448. nautobot/project-static/docs/models/dcim/virtualdevicecontext.html +14 -0
  449. nautobot/project-static/docs/models/extras/jobqueue.html +14 -0
  450. nautobot/project-static/docs/models/wireless/radioprofile.html +14 -0
  451. nautobot/project-static/docs/models/wireless/supporteddatarate.html +14 -0
  452. nautobot/project-static/docs/models/wireless/wirelessnetwork.html +14 -0
  453. nautobot/project-static/docs/objects.inv +0 -0
  454. nautobot/project-static/docs/overview/application_stack.html +467 -21
  455. nautobot/project-static/docs/overview/design_philosophy.html +461 -17
  456. nautobot/project-static/docs/release-notes/index.html +483 -20
  457. nautobot/project-static/docs/release-notes/version-1.0.html +649 -206
  458. nautobot/project-static/docs/release-notes/version-1.1.html +646 -203
  459. nautobot/project-static/docs/release-notes/version-1.2.html +721 -278
  460. nautobot/project-static/docs/release-notes/version-1.3.html +747 -304
  461. nautobot/project-static/docs/release-notes/version-1.4.html +832 -390
  462. nautobot/project-static/docs/release-notes/version-1.5.html +1020 -579
  463. nautobot/project-static/docs/release-notes/version-1.6.html +940 -516
  464. nautobot/project-static/docs/release-notes/version-2.0.html +943 -502
  465. nautobot/project-static/docs/release-notes/version-2.1.html +778 -337
  466. nautobot/project-static/docs/release-notes/version-2.2.html +771 -330
  467. nautobot/project-static/docs/release-notes/version-2.3.html +914 -471
  468. nautobot/project-static/docs/release-notes/version-2.4.html +10323 -0
  469. nautobot/project-static/docs/search/search_index.json +1 -1
  470. nautobot/project-static/docs/sitemap.xml +342 -270
  471. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  472. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +461 -17
  473. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +461 -17
  474. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +461 -17
  475. nautobot/project-static/docs/user-guide/administration/configuration/index.html +473 -30
  476. nautobot/project-static/docs/user-guide/administration/configuration/redis.html +461 -17
  477. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +842 -155
  478. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +461 -17
  479. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +461 -17
  480. nautobot/project-static/docs/user-guide/administration/guides/docker.html +474 -27
  481. nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +461 -17
  482. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +461 -17
  483. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +461 -17
  484. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +461 -17
  485. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +461 -17
  486. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +463 -19
  487. nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +461 -17
  488. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +461 -17
  489. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +461 -17
  490. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +481 -21
  491. nautobot/project-static/docs/user-guide/administration/installation/index.html +466 -18
  492. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +462 -18
  493. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +461 -17
  494. nautobot/project-static/docs/user-guide/administration/installation/services.html +461 -17
  495. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +461 -17
  496. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +482 -39
  497. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +475 -64
  498. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +475 -64
  499. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +461 -17
  500. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +461 -17
  501. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +461 -17
  502. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +461 -17
  503. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +461 -17
  504. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +464 -21
  505. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +461 -17
  506. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/tables/v2-code-nautobot-app-location.yaml +0 -16
  507. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +461 -17
  508. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +467 -19
  509. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +461 -17
  510. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +461 -17
  511. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +461 -17
  512. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +461 -17
  513. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +461 -17
  514. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +461 -17
  515. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +461 -17
  516. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +461 -17
  517. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +461 -17
  518. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +461 -17
  519. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +461 -17
  520. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +461 -17
  521. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +461 -17
  522. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +461 -17
  523. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +461 -17
  524. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +461 -17
  525. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +461 -17
  526. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +497 -18
  527. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +487 -20
  528. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +461 -17
  529. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +461 -17
  530. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +461 -17
  531. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +461 -17
  532. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +461 -17
  533. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +461 -17
  534. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +461 -17
  535. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +461 -17
  536. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +461 -17
  537. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +461 -17
  538. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +461 -17
  539. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +461 -17
  540. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +461 -17
  541. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +461 -17
  542. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +461 -17
  543. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +461 -17
  544. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +461 -17
  545. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +461 -17
  546. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +461 -17
  547. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +461 -17
  548. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +461 -17
  549. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +461 -17
  550. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +461 -17
  551. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +461 -17
  552. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +461 -17
  553. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +461 -17
  554. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +461 -17
  555. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +461 -17
  556. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +461 -17
  557. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +461 -17
  558. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +461 -17
  559. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +461 -17
  560. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +461 -17
  561. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +461 -17
  562. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualdevicecontext.html +9375 -0
  563. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +468 -28
  564. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +461 -17
  565. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +461 -17
  566. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +461 -17
  567. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +461 -17
  568. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +461 -17
  569. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +461 -17
  570. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +461 -17
  571. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +461 -17
  572. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +461 -17
  573. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +461 -17
  574. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +461 -17
  575. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +461 -17
  576. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +464 -20
  577. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +461 -17
  578. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +461 -17
  579. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +461 -17
  580. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +461 -17
  581. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +461 -17
  582. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +461 -17
  583. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +464 -20
  584. nautobot/project-static/docs/user-guide/core-data-model/wireless/index.html +9313 -0
  585. nautobot/project-static/docs/user-guide/core-data-model/wireless/radioprofile.html +9217 -0
  586. nautobot/project-static/docs/user-guide/core-data-model/wireless/supporteddatarate.html +9211 -0
  587. nautobot/project-static/docs/user-guide/core-data-model/wireless/wirelessnetwork.html +9277 -0
  588. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +461 -17
  589. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +461 -17
  590. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +461 -17
  591. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +461 -17
  592. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +461 -17
  593. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +461 -17
  594. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +461 -17
  595. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +461 -17
  596. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +461 -17
  597. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +461 -17
  598. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +461 -17
  599. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +466 -20
  600. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +461 -17
  601. nautobot/project-static/docs/user-guide/feature-guides/images/wireless/central-mode.png +0 -0
  602. nautobot/project-static/docs/user-guide/feature-guides/images/wireless/device-group-add.png +0 -0
  603. nautobot/project-static/docs/user-guide/feature-guides/images/wireless/device-group-create-1.png +0 -0
  604. nautobot/project-static/docs/user-guide/feature-guides/images/wireless/device-group-create-2.png +0 -0
  605. nautobot/project-static/docs/user-guide/feature-guides/images/wireless/radio-profile-add.png +0 -0
  606. nautobot/project-static/docs/user-guide/feature-guides/images/wireless/radio-profile-create.png +0 -0
  607. nautobot/project-static/docs/user-guide/feature-guides/images/wireless/supported-data-rate-add.png +0 -0
  608. nautobot/project-static/docs/user-guide/feature-guides/images/wireless/supported-data-rate-create.png +0 -0
  609. nautobot/project-static/docs/user-guide/feature-guides/images/wireless/wireless-controller-add.png +0 -0
  610. nautobot/project-static/docs/user-guide/feature-guides/images/wireless/wireless-controller-create-1.png +0 -0
  611. nautobot/project-static/docs/user-guide/feature-guides/images/wireless/wireless-controller-create-2.png +0 -0
  612. nautobot/project-static/docs/user-guide/feature-guides/images/wireless/wireless-network-add.png +0 -0
  613. nautobot/project-static/docs/user-guide/feature-guides/images/wireless/wireless-network-create.png +0 -0
  614. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +461 -17
  615. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +461 -17
  616. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +464 -20
  617. nautobot/project-static/docs/user-guide/feature-guides/wireless-networks-and-controllers.html +9444 -0
  618. nautobot/project-static/docs/user-guide/index.html +461 -17
  619. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +464 -20
  620. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +465 -21
  621. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +461 -17
  622. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +461 -17
  623. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +461 -17
  624. nautobot/project-static/docs/user-guide/platform-functionality/events.html +9617 -0
  625. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +464 -20
  626. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +461 -17
  627. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +461 -17
  628. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +461 -17
  629. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +461 -17
  630. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +461 -17
  631. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +470 -21
  632. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +464 -20
  633. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +464 -20
  634. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +461 -17
  635. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobqueue.html +9224 -0
  636. nautobot/project-static/docs/user-guide/platform-functionality/jobs/kubernetes-job-support.html +9722 -0
  637. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +464 -20
  638. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +461 -17
  639. nautobot/project-static/docs/user-guide/platform-functionality/note.html +461 -17
  640. nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +461 -17
  641. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +465 -21
  642. nautobot/project-static/docs/user-guide/platform-functionality/rendering-jinja-templates.html +9292 -0
  643. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +461 -17
  644. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +509 -38
  645. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +492 -21
  646. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +461 -17
  647. nautobot/project-static/docs/user-guide/platform-functionality/role.html +461 -17
  648. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +461 -17
  649. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +461 -17
  650. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +464 -20
  651. nautobot/project-static/docs/user-guide/platform-functionality/status.html +461 -17
  652. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +461 -17
  653. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +529 -54
  654. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +461 -17
  655. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +461 -17
  656. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +461 -17
  657. nautobot/project-static/img/jinja_logo.svg +97 -0
  658. nautobot/project-static/js/forms.js +6 -1
  659. nautobot/project-static/js/nav_menu.js +2 -1
  660. nautobot/tenancy/api/serializers.py +0 -2
  661. nautobot/tenancy/api/views.py +9 -13
  662. nautobot/tenancy/factory.py +1 -1
  663. nautobot/tenancy/navigation.py +0 -29
  664. nautobot/tenancy/templates/tenancy/tenant.html +4 -91
  665. nautobot/tenancy/tests/test_filters.py +29 -134
  666. nautobot/tenancy/views.py +35 -24
  667. nautobot/users/admin.py +2 -0
  668. nautobot/users/api/views.py +2 -2
  669. nautobot/users/forms.py +19 -0
  670. nautobot/users/templates/users/preferences.html +22 -0
  671. nautobot/users/tests/test_filters.py +1 -19
  672. nautobot/users/tests/test_views.py +57 -0
  673. nautobot/users/utils.py +8 -0
  674. nautobot/users/views.py +48 -11
  675. nautobot/virtualization/api/views.py +5 -24
  676. nautobot/virtualization/filters.py +1 -2
  677. nautobot/virtualization/models.py +1 -1
  678. nautobot/virtualization/navigation.py +0 -48
  679. nautobot/virtualization/tables.py +2 -2
  680. nautobot/virtualization/templates/virtualization/cluster_edit.html +1 -7
  681. nautobot/virtualization/templates/virtualization/clustertype.html +0 -39
  682. nautobot/virtualization/templates/virtualization/virtualmachine.html +1 -9
  683. nautobot/virtualization/templates/virtualization/virtualmachine_edit.html +2 -8
  684. nautobot/virtualization/tests/test_filters.py +57 -166
  685. nautobot/virtualization/views.py +18 -15
  686. nautobot/wireless/__init__.py +0 -0
  687. nautobot/wireless/api/__init__.py +0 -0
  688. nautobot/wireless/api/serializers.py +44 -0
  689. nautobot/wireless/api/urls.py +20 -0
  690. nautobot/wireless/api/views.py +34 -0
  691. nautobot/wireless/apps.py +8 -0
  692. nautobot/wireless/choices.py +345 -0
  693. nautobot/wireless/factory.py +138 -0
  694. nautobot/wireless/filters.py +167 -0
  695. nautobot/wireless/forms.py +283 -0
  696. nautobot/wireless/homepage.py +19 -0
  697. nautobot/wireless/migrations/0001_initial.py +223 -0
  698. nautobot/wireless/migrations/__init__.py +0 -0
  699. nautobot/wireless/models.py +207 -0
  700. nautobot/wireless/navigation.py +105 -0
  701. nautobot/wireless/tables.py +244 -0
  702. nautobot/wireless/templates/wireless/radioprofile_retrieve.html +81 -0
  703. nautobot/wireless/templates/wireless/supporteddatarate_retrieve.html +26 -0
  704. nautobot/wireless/templates/wireless/wirelessnetwork_create.html +88 -0
  705. nautobot/wireless/templates/wireless/wirelessnetwork_retrieve.html +56 -0
  706. nautobot/wireless/tests/__init__.py +0 -0
  707. nautobot/wireless/tests/integration/__init__.py +0 -0
  708. nautobot/wireless/tests/integration/test_radio_profile.py +42 -0
  709. nautobot/wireless/tests/test_api.py +247 -0
  710. nautobot/wireless/tests/test_filters.py +82 -0
  711. nautobot/wireless/tests/test_models.py +22 -0
  712. nautobot/wireless/tests/test_views.py +378 -0
  713. nautobot/wireless/urls.py +13 -0
  714. nautobot/wireless/views.py +119 -0
  715. {nautobot-2.3.16.dist-info → nautobot-2.4.0.dist-info}/METADATA +9 -12
  716. {nautobot-2.3.16.dist-info → nautobot-2.4.0.dist-info}/RECORD +720 -549
  717. nautobot/core/utils/navigation.py +0 -54
  718. {nautobot-2.3.16.dist-info → nautobot-2.4.0.dist-info}/LICENSE.txt +0 -0
  719. {nautobot-2.3.16.dist-info → nautobot-2.4.0.dist-info}/NOTICE +0 -0
  720. {nautobot-2.3.16.dist-info → nautobot-2.4.0.dist-info}/WHEEL +0 -0
  721. {nautobot-2.3.16.dist-info → nautobot-2.4.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,40 @@
1
+ from django import template
2
+ from django.utils.html import format_html_join
3
+
4
+ from nautobot.core.utils.lookup import get_view_for_model
5
+ from nautobot.core.views.utils import get_obj_from_context
6
+
7
+ register = template.Library()
8
+
9
+
10
+ @register.simple_tag(takes_context=True)
11
+ def render_tabs_labels(context, tabs):
12
+ """Render the tab labels for each Tab in the given `tabs` with the given `context`."""
13
+ if tabs is not None:
14
+ return format_html_join("\n", "{}", ([tab.render_label_wrapper(context)] for tab in tabs))
15
+ return ""
16
+
17
+
18
+ @register.simple_tag(takes_context=True)
19
+ def render_components(context, components):
20
+ """Render each component in the given `components` with the given `context`."""
21
+ if components is not None:
22
+ return format_html_join("\n", "{}", ([component.render(context)] for component in components))
23
+ return ""
24
+
25
+
26
+ @register.simple_tag(takes_context=True)
27
+ def render_detail_view_extra_buttons(context):
28
+ """
29
+ Render the "extra_buttons" if any from the base detail view associated with the context object.
30
+
31
+ This makes it possible for "extra" tabs (such as Changelog and Notes, and any added by App TemplateExtensions)
32
+ to automatically still render any `extra_buttons` defined by the base detail view, without the tab-specific views
33
+ needing to explicitly inherit from the base view.
34
+ """
35
+ obj = get_obj_from_context(context)
36
+ base_detail_view = get_view_for_model(obj)
37
+ object_detail_content = getattr(base_detail_view, "object_detail_content", None)
38
+ if object_detail_content is not None and object_detail_content.extra_buttons:
39
+ return render_components(context, object_detail_content.extra_buttons)
40
+ return ""
@@ -21,23 +21,23 @@ from nautobot.extras.models import Job, JobResult
21
21
  __all__ = (
22
22
  "APITestCase",
23
23
  "APIViewTestCases",
24
+ "FilterTestCases",
25
+ "JobClassInfo",
26
+ "ModelTestCase",
27
+ "ModelViewTestCase",
28
+ "NautobotTestCaseMixin",
29
+ "NautobotTestClient",
30
+ "TestCase",
31
+ "ViewTestCases",
24
32
  "create_job_result_and_run_job",
25
33
  "create_test_user",
26
34
  "disable_warnings",
27
35
  "extract_form_failures",
28
36
  "extract_page_body",
29
- "FilterTestCases",
30
37
  "get_deletable_objects",
31
38
  "get_job_class_and_model",
32
- "JobClassInfo",
33
- "ModelTestCase",
34
- "ModelViewTestCase",
35
- "NautobotTestCaseMixin",
36
- "NautobotTestClient",
37
39
  "post_data",
38
40
  "run_job_for_testing",
39
- "TestCase",
40
- "ViewTestCases",
41
41
  )
42
42
 
43
43
  # Use the proper swappable User model
@@ -5,18 +5,19 @@ from typing import Optional, Sequence, Union
5
5
  from django.conf import settings
6
6
  from django.contrib.contenttypes.fields import GenericForeignKey
7
7
  from django.contrib.contenttypes.models import ContentType
8
+ from django.db import connections, DEFAULT_DB_ALIAS
8
9
  from django.db.models import ForeignKey, ManyToManyField, QuerySet
9
10
  from django.test import override_settings, tag
11
+ from django.test.utils import CaptureQueriesContext
10
12
  from django.urls import reverse
11
13
  from django.utils.text import slugify
12
- from rest_framework import status
14
+ from rest_framework import serializers, status
13
15
  from rest_framework.relations import ManyRelatedField
14
16
  from rest_framework.test import APITransactionTestCase as _APITransactionTestCase
15
17
 
16
18
  from nautobot.core.api.utils import get_serializer_for_model
17
19
  from nautobot.core.models import fields as core_fields
18
20
  from nautobot.core.models.tree_queries import TreeModel
19
- from nautobot.core.templatetags.helpers import bettertitle
20
21
  from nautobot.core.testing import mixins, utils, views
21
22
  from nautobot.core.utils import lookup
22
23
  from nautobot.core.utils.data import is_uuid
@@ -95,6 +96,15 @@ class APITestCase(views.ModelTestCase):
95
96
  for verboten in self.VERBOTEN_STRINGS:
96
97
  self.assertNotIn(verboten, response_raw_content)
97
98
 
99
+ def get_m2m_fields(self):
100
+ """Get serializer field names that are many-to-many or one-to-many and thus affected by ?exclude_m2m=true."""
101
+ serializer_class = get_serializer_for_model(self.model)
102
+ m2m_fields = []
103
+ for field_name, field_instance in serializer_class().fields.items():
104
+ if isinstance(field_instance, (serializers.ManyRelatedField, serializers.ListSerializer)):
105
+ m2m_fields.append(field_name)
106
+ return m2m_fields
107
+
98
108
 
99
109
  @tag("unit")
100
110
  class APIViewTestCases:
@@ -169,6 +179,10 @@ class APIViewTestCases:
169
179
  if issubclass(self.model, extras_models.ChangeLoggedModel):
170
180
  self.assertIn("created", response.data)
171
181
  self.assertIn("last_updated", response.data)
182
+ if hasattr(self.model, "notes") and isinstance(instance1.notes, QuerySet):
183
+ self.assertIn("notes_url", response.data)
184
+ self.assertIn(f"{url}notes/", str(response.data["notes_url"]))
185
+ self.assertIn(instance1.get_notes_url(api=True), str(response.data["notes_url"]))
172
186
  # Fields that should be absent by default (opt-in fields):
173
187
  self.assertNotIn("computed_fields", response.data)
174
188
  self.assertNotIn("relationships", response.data)
@@ -218,58 +232,6 @@ class APIViewTestCases:
218
232
  response = self.client.options(url, **self.header)
219
233
  self.assertHttpStatus(response, status.HTTP_200_OK)
220
234
 
221
- with self.subTest("Assert Detail View Config is generated well"):
222
- # Namings Help
223
- # 1. detail_view_config: This is the detail view config set in the serializer.Meta.detail_view_config
224
- # 2. detail_view_schema: This is the retrieve schema generated from an OPTIONS request.
225
- # 3. advanced_view_schema: This is the advanced tab schema generated from an OPTIONS request.
226
- serializer = get_serializer_for_model(self._get_queryset().model)
227
- advanced_view_schema = response.data["view_options"]["retrieve"]["tabs"]["Advanced"]
228
-
229
- # Get default advanced tab fields
230
- self.assertEqual(len(advanced_view_schema), 1)
231
- self.assertIn("Object Details", advanced_view_schema[0])
232
- advanced_tab_fields = advanced_view_schema[0].get("Object Details")["fields"]
233
-
234
- if detail_view_config := getattr(serializer.Meta, "detail_view_config", None):
235
- detail_view_schema = response.data["view_options"]["retrieve"]["tabs"][
236
- bettertitle(self._get_queryset().model._meta.verbose_name)
237
- ]
238
- self.assertHttpStatus(response, status.HTTP_200_OK)
239
-
240
- # According to convention, fields in the advanced tab fields should not exist in
241
- # the `detail_view_schema`. Assert this is True.
242
- with self.subTest("Assert advanced tab fields should not exist in the detail_view_schema."):
243
- if detail_view_config.get("include_others"):
244
- # Handle "Other Fields" section specially as "Other Field" is dynamically added
245
- # by Nautobot and is not part of the serializer-defined detail_view_config
246
- other_fields = detail_view_schema[0]["Other Fields"]["fields"]
247
- for field in advanced_tab_fields:
248
- self.assertNotIn(field, other_fields)
249
-
250
- for col_idx, col in enumerate(detail_view_schema):
251
- for group_title, group in col.items():
252
- if group_title == "Other Fields":
253
- continue
254
- group_fields = group["fields"]
255
- # Config on the serializer
256
- if (
257
- col_idx < len(detail_view_config["layout"])
258
- and group_title in detail_view_config["layout"][col_idx]
259
- ):
260
- fields = detail_view_config["layout"][col_idx][group_title]["fields"]
261
- else:
262
- fields = []
263
-
264
- # Fields that are in the detail_view_schema must not be in the advanced tab as well
265
- for field in group_fields:
266
- self.assertNotIn(field, advanced_tab_fields)
267
-
268
- # Fields that are explicit in the detail_view_config must remain as such in the schema
269
- for field in fields:
270
- if field not in advanced_tab_fields:
271
- self.assertIn(field, group_fields)
272
-
273
235
  class ListObjectsViewTestCase(APITestCase):
274
236
  choices_fields = None
275
237
  filterset = None
@@ -280,7 +242,7 @@ class APIViewTestCases:
280
242
  def get_depth_fields(self):
281
243
  """Get a list of model fields that could be tested with the ?depth query parameter"""
282
244
  depth_fields = []
283
- for field in self.model._meta.fields:
245
+ for field in self.model._meta.get_fields():
284
246
  if not field.name.startswith("_"):
285
247
  if isinstance(field, (ForeignKey, GenericForeignKey, ManyToManyField, core_fields.TagsField)) and (
286
248
  # we represent content-types as "app_label.modelname" rather than as FKs
@@ -289,6 +251,9 @@ class APIViewTestCases:
289
251
  and not (field.name == "user" and self.model == users_models.Token)
290
252
  ):
291
253
  depth_fields.append(field.name)
254
+ serializer_class = get_serializer_for_model(self.model)
255
+ serializer = serializer_class()
256
+ depth_fields = [field_name for field_name in depth_fields if field_name in serializer.fields]
292
257
  return depth_fields
293
258
 
294
259
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
@@ -318,9 +283,12 @@ class APIViewTestCases:
318
283
  GET a list of objects using the "?depth=0" parameter.
319
284
  """
320
285
  depth_fields = self.get_depth_fields()
286
+ m2m_fields = self.get_m2m_fields()
321
287
  self.add_permissions(f"{self.model._meta.app_label}.view_{self.model._meta.model_name}")
322
- url = f"{self._get_list_url()}?depth=0"
323
- response = self.client.get(url, **self.header)
288
+ list_url = f"{self._get_list_url()}?depth=0"
289
+ with CaptureQueriesContext(connections[DEFAULT_DB_ALIAS]) as cqc:
290
+ response = self.client.get(list_url, **self.header)
291
+ base_num_queries = len(cqc)
324
292
 
325
293
  self.assertHttpStatus(response, status.HTTP_200_OK)
326
294
  self.assertIsInstance(response.data, dict)
@@ -329,15 +297,23 @@ class APIViewTestCases:
329
297
  self.assert_no_verboten_content(response)
330
298
 
331
299
  for response_data in response.data["results"]:
300
+ for field in m2m_fields:
301
+ self.assertIn(field, response_data)
302
+ self.assertIsInstance(response_data[field], list)
332
303
  for field in depth_fields:
333
304
  self.assertIn(field, response_data)
334
305
  if isinstance(response_data[field], list):
335
306
  for entry in response_data[field]:
336
307
  self.assertIsInstance(entry, dict)
337
- self.assertTrue(is_uuid(entry["id"]))
308
+ if entry["object_type"] in ["auth.group"]:
309
+ self.assertIsInstance(entry["id"], int)
310
+ else:
311
+ self.assertTrue(is_uuid(entry["id"]))
312
+ self.assertEqual(len(entry.keys()), 3) # just id/object_type/url
338
313
  else:
339
314
  if response_data[field] is not None:
340
315
  self.assertIsInstance(response_data[field], dict)
316
+ self.assertEqual(len(response_data[field].keys()), 3) # just id/object_type/url
341
317
  url = response_data[field]["url"]
342
318
  pk = response_data[field]["id"]
343
319
  object_type = response_data[field]["object_type"]
@@ -345,22 +321,67 @@ class APIViewTestCases:
345
321
  # URL ending in the UUID of the relevant object:
346
322
  # http://nautobot.example.com/api/circuits/providers/<uuid>/
347
323
  # ^^^^^^
348
- self.assertTrue(is_uuid(url.split("/")[-2]))
349
- self.assertTrue(is_uuid(pk))
324
+ if object_type in ["auth.group"]:
325
+ self.assertIsInstance(url.split("/")[-2], int)
326
+ self.assertIsInstance(pk, int)
327
+ else:
328
+ self.assertTrue(is_uuid(url.split("/")[-2]))
329
+ self.assertTrue(is_uuid(pk))
350
330
 
351
331
  with self.subTest(f"Assert object_type {object_type} is valid"):
352
332
  app_label, model_name = object_type.split(".")
353
333
  ContentType.objects.get(app_label=app_label, model=model_name)
354
334
 
335
+ list_url += "&exclude_m2m=true"
336
+ with CaptureQueriesContext(connections[DEFAULT_DB_ALIAS]) as cqc:
337
+ response = self.client.get(list_url, **self.header)
338
+
339
+ self.assertHttpStatus(response, status.HTTP_200_OK)
340
+ self.assertIsInstance(response.data, dict)
341
+ self.assertIn("results", response.data)
342
+ self.assert_no_verboten_content(response)
343
+
344
+ if m2m_fields:
345
+ if self.model._meta.app_label in [
346
+ "circuits",
347
+ "cloud",
348
+ "dcim",
349
+ "extras",
350
+ "ipam",
351
+ "tenancy",
352
+ "users",
353
+ "virtualization",
354
+ "wireless",
355
+ ]:
356
+ self.assertLess(
357
+ len(cqc), base_num_queries, "Number of queries did not decrease with ?exclude_m2m=true"
358
+ )
359
+ else:
360
+ # Less strict check for non-core APIs
361
+ self.assertLessEqual(
362
+ len(cqc), base_num_queries, "Number of queries increased with ?exclude_m2m=true"
363
+ )
364
+ else:
365
+ # No M2M fields to exclude
366
+ self.assertLessEqual(len(cqc), base_num_queries, "Number of queries increased with ?exclude_m2m=true")
367
+
368
+ for response_data in response.data["results"]:
369
+ for field in m2m_fields:
370
+ self.assertNotIn(field, response_data)
371
+ # TODO: we should assert that all other fields are still present, but there's a few corner cases...
372
+
355
373
  @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
356
374
  def test_list_objects_depth_1(self):
357
375
  """
358
376
  GET a list of objects using the "?depth=1" parameter.
359
377
  """
360
378
  depth_fields = self.get_depth_fields()
379
+ m2m_fields = self.get_m2m_fields()
361
380
  self.add_permissions(f"{self.model._meta.app_label}.view_{self.model._meta.model_name}")
362
- url = f"{self._get_list_url()}?depth=1"
363
- response = self.client.get(url, **self.header)
381
+ list_url = f"{self._get_list_url()}?depth=1"
382
+ with CaptureQueriesContext(connections[DEFAULT_DB_ALIAS]) as cqc:
383
+ response = self.client.get(list_url, **self.header)
384
+ base_num_queries = len(cqc)
364
385
 
365
386
  self.assertHttpStatus(response, status.HTTP_200_OK)
366
387
  self.assertIsInstance(response.data, dict)
@@ -369,16 +390,65 @@ class APIViewTestCases:
369
390
  self.assert_no_verboten_content(response)
370
391
 
371
392
  for response_data in response.data["results"]:
393
+ for field in m2m_fields:
394
+ self.assertIn(field, response_data)
395
+ self.assertIsInstance(response_data[field], list)
372
396
  for field in depth_fields:
373
397
  self.assertIn(field, response_data)
374
398
  if isinstance(response_data[field], list):
375
399
  for entry in response_data[field]:
376
400
  self.assertIsInstance(entry, dict)
377
- self.assertTrue(is_uuid(entry["id"]))
401
+ if entry["object_type"] in ["auth.group"]:
402
+ self.assertIsInstance(entry["id"], int)
403
+ else:
404
+ self.assertTrue(is_uuid(entry["id"]))
405
+ self.assertGreater(len(entry.keys()), 3, entry) # not just id/object_type/url!
378
406
  else:
379
407
  if response_data[field] is not None:
380
408
  self.assertIsInstance(response_data[field], dict)
381
- self.assertTrue(is_uuid(response_data[field]["id"]))
409
+ if response_data[field]["object_type"] in ["auth.group"]:
410
+ self.assertIsInstance(response_data[field]["id"], int)
411
+ else:
412
+ self.assertTrue(is_uuid(response_data[field]["id"]))
413
+ self.assertGreater(len(response_data[field].keys()), 3, response_data[field])
414
+
415
+ list_url += "&exclude_m2m=true"
416
+ with CaptureQueriesContext(connections[DEFAULT_DB_ALIAS]) as cqc:
417
+ response = self.client.get(list_url, **self.header)
418
+
419
+ self.assertHttpStatus(response, status.HTTP_200_OK)
420
+ self.assertIsInstance(response.data, dict)
421
+ self.assertIn("results", response.data)
422
+ self.assert_no_verboten_content(response)
423
+
424
+ if m2m_fields:
425
+ if self.model._meta.app_label in [
426
+ "circuits",
427
+ "cloud",
428
+ "dcim",
429
+ "extras",
430
+ "ipam",
431
+ "tenancy",
432
+ "users",
433
+ "virtualization",
434
+ "wireless",
435
+ ]:
436
+ self.assertLess(
437
+ len(cqc), base_num_queries, "Number of queries did not decrease with ?exclude_m2m=true"
438
+ )
439
+ else:
440
+ # Less strict check for non-core APIs
441
+ self.assertLessEqual(
442
+ len(cqc), base_num_queries, "Number of queries increased with ?exclude_m2m=true"
443
+ )
444
+ else:
445
+ # No M2M fields to exclude
446
+ self.assertLessEqual(len(cqc), base_num_queries, "Number of queries increased with ?exclude_m2m=true")
447
+
448
+ for response_data in response.data["results"]:
449
+ for field in m2m_fields:
450
+ self.assertNotIn(field, response_data)
451
+ # TODO: we should assert that all other fields are still present, but there's a few corner cases...
382
452
 
383
453
  @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
384
454
  def test_list_objects_without_permission(self):
@@ -637,10 +707,7 @@ class APIViewTestCases:
637
707
  POST a single object with permission.
638
708
  """
639
709
  # Add object-level permission
640
- obj_perm = users_models.ObjectPermission(name="Test permission", actions=["add"])
641
- obj_perm.save()
642
- obj_perm.users.add(self.user)
643
- obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
710
+ self.add_permissions(f"{self.model._meta.app_label}.add_{self.model._meta.model_name}")
644
711
 
645
712
  initial_count = self._get_queryset().count()
646
713
  for i, create_data in enumerate(self.create_data):
@@ -683,10 +750,10 @@ class APIViewTestCases:
683
750
  self.fail("Couldn't find a single deletable object!")
684
751
 
685
752
  # Add object-level permission
686
- obj_perm = users_models.ObjectPermission(name="Test permission", actions=["add", "view"])
687
- obj_perm.save()
688
- obj_perm.users.add(self.user)
689
- obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
753
+ self.add_permissions(
754
+ f"{self.model._meta.app_label}.add_{self.model._meta.model_name}",
755
+ f"{self.model._meta.app_label}.view_{self.model._meta.model_name}",
756
+ )
690
757
 
691
758
  response = self.client.get(self._get_detail_url(instance) + "?format=csv", **self.header)
692
759
  self.assertHttpStatus(response, status.HTTP_200_OK)
@@ -739,10 +806,7 @@ class APIViewTestCases:
739
806
  POST a set of objects in a single request.
740
807
  """
741
808
  # Add object-level permission
742
- obj_perm = users_models.ObjectPermission(name="Test permission", actions=["add"])
743
- obj_perm.save()
744
- obj_perm.users.add(self.user)
745
- obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
809
+ self.add_permissions(f"{self.model._meta.app_label}.add_{self.model._meta.model_name}")
746
810
 
747
811
  initial_count = self._get_queryset().count()
748
812
  response = self.client.post(self._get_list_url(), self.create_data, format="json", **self.header)
@@ -796,7 +860,7 @@ class APIViewTestCases:
796
860
 
797
861
  def strip_serialized_object(this_object):
798
862
  """
799
- Only here to work around acceptable differences in PATCH response vs GET response which are known bugs.
863
+ Work around acceptable differences in PATCH response vs GET response which are known behaviors.
800
864
  """
801
865
  # Work around for https://github.com/nautobot/nautobot/issues/3321
802
866
  this_object.pop("last_updated", None)
@@ -805,6 +869,12 @@ class APIViewTestCases:
805
869
  this_object.pop("config_context", None)
806
870
  this_object.pop("relationships", None)
807
871
 
872
+ serializer = get_serializer_for_model(self.model)()
873
+ for field_name, field_instance in serializer.fields.items():
874
+ if field_instance.read_only:
875
+ # Likely a derived field, might change as a consequence of other data updates
876
+ this_object.pop(field_name, None)
877
+
808
878
  for value in this_object.values():
809
879
  if isinstance(value, dict):
810
880
  strip_serialized_object(value)
@@ -848,7 +918,7 @@ class APIViewTestCases:
848
918
  self.assertEqual(initial_serialized_object, serialized_object)
849
919
 
850
920
  # Verify ObjectChange creation -- yes, even though nothing actually changed
851
- # This may change (hah) at some point -- see https://github.com/nautobot/nautobot/issues/3321
921
+ # TODO: This may change (hah) at some point -- see https://github.com/nautobot/nautobot/issues/3321
852
922
  if hasattr(self.model, "to_objectchange"):
853
923
  objectchanges = lookup.get_changes_for_model(instance)
854
924
  self.assertEqual(objectchanges[0].action, extras_choices.ObjectChangeActionChoices.ACTION_UPDATE)
@@ -857,10 +927,16 @@ class APIViewTestCases:
857
927
  # Verify that a PATCH with some data updates that data correctly.
858
928
  response = self.client.patch(url, update_data, format="json", **self.header)
859
929
  self.assertHttpStatus(response, status.HTTP_200_OK)
930
+ serialized_object = response.json()
931
+ strip_serialized_object(serialized_object)
860
932
  # Check for unexpected side effects on fields we DIDN'T intend to update
861
933
  for field in initial_serialized_object:
862
934
  if field not in update_data:
863
- self.assertEqual(initial_serialized_object[field], serialized_object[field])
935
+ self.assertEqual(
936
+ initial_serialized_object[field],
937
+ serialized_object[field],
938
+ f"data changed unexpectedly for field '{field}'",
939
+ )
864
940
  instance.refresh_from_db()
865
941
  self.assertInstanceEqual(instance, update_data, exclude=self.validation_excluded_fields, api=True)
866
942
 
@@ -869,14 +945,34 @@ class APIViewTestCases:
869
945
  objectchanges = lookup.get_changes_for_model(instance)
870
946
  self.assertEqual(objectchanges[0].action, extras_choices.ObjectChangeActionChoices.ACTION_UPDATE)
871
947
 
948
+ # Verify that a PATCH with ?exclude_m2m=true correctly excludes many-to-many fields from the response
949
+ # This also doubles as a test for idempotence of the PATCH request.
950
+ response = self.client.patch(url + "?exclude_m2m=true", update_data, format="json", **self.header)
951
+ self.assertHttpStatus(response, status.HTTP_200_OK)
952
+ m2m_fields = self.get_m2m_fields()
953
+ serialized_object = response.json()
954
+ strip_serialized_object(serialized_object)
955
+ for field in m2m_fields:
956
+ self.assertNotIn(field, serialized_object)
957
+ # Check for unexpected side effects on fields we DIDN'T intend to update
958
+ for field in initial_serialized_object:
959
+ if field not in update_data and field not in m2m_fields:
960
+ self.assertEqual(
961
+ initial_serialized_object[field],
962
+ serialized_object[field],
963
+ f"data changed unexpectedly for field '{field}'",
964
+ )
965
+ instance.refresh_from_db()
966
+ self.assertInstanceEqual(instance, update_data, exclude=self.validation_excluded_fields, api=True)
967
+
872
968
  def test_get_put_round_trip(self):
873
969
  """GET and then PUT an object and verify that it's accepted and unchanged."""
874
970
  self.maxDiff = None
875
971
  # Add object-level permission
876
- obj_perm = users_models.ObjectPermission(name="Test permission", actions=["view", "change"])
877
- obj_perm.save()
878
- obj_perm.users.add(self.user)
879
- obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
972
+ self.add_permissions(
973
+ f"{self.model._meta.app_label}.view_{self.model._meta.model_name}",
974
+ f"{self.model._meta.app_label}.change_{self.model._meta.model_name}",
975
+ )
880
976
 
881
977
  instance = self._get_queryset().first()
882
978
  url = self._get_detail_url(instance)
@@ -907,10 +1003,7 @@ class APIViewTestCases:
907
1003
  self.skipTest("Bulk update data not set")
908
1004
 
909
1005
  # Add object-level permission
910
- obj_perm = users_models.ObjectPermission(name="Test permission", actions=["change"])
911
- obj_perm.save()
912
- obj_perm.users.add(self.user)
913
- obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
1006
+ self.add_permissions(f"{self.model._meta.app_label}.change_{self.model._meta.model_name}")
914
1007
 
915
1008
  id_list = list(self._get_queryset().values_list("id", flat=True)[:3])
916
1009
  self.assertEqual(len(id_list), 3, "Insufficient number of objects to test bulk update")
@@ -954,24 +1047,11 @@ class APIViewTestCases:
954
1047
  self.assertIn("actions", data)
955
1048
 
956
1049
  # Grab any field that has choices defined (fields with enums)
957
- if any(
958
- [
959
- "POST" in data["actions"],
960
- "PUT" in data["actions"],
961
- ]
962
- ):
963
- schema = data["schema"]
964
- props = schema["properties"]
965
- fields = props.keys()
966
- field_choices = set()
967
- for field_name in fields:
968
- obj = props[field_name]
969
- if "enum" in obj and "enumNames" in obj:
970
- enum = obj["enum"]
971
- # Zipping to assert that the enum and the mapping have the same number of items.
972
- model_field_choices = dict(zip(obj["enumNames"], enum))
973
- self.assertEqual(len(enum), len(model_field_choices))
974
- field_choices.add(field_name)
1050
+ field_choices = {}
1051
+ if "POST" in data["actions"]:
1052
+ field_choices = {k for k, v in data["actions"]["POST"].items() if "choices" in v}
1053
+ elif "PUT" in data["actions"]:
1054
+ field_choices = {k for k, v in data["actions"]["PUT"].items() if "choices" in v}
975
1055
  else:
976
1056
  self.fail(f"Neither PUT nor POST are available actions in: {data['actions']}")
977
1057
 
@@ -1026,10 +1106,7 @@ class APIViewTestCases:
1026
1106
  url = self._get_detail_url(instance)
1027
1107
 
1028
1108
  # Add object-level permission
1029
- obj_perm = users_models.ObjectPermission(name="Test permission", actions=["delete"])
1030
- obj_perm.save()
1031
- obj_perm.users.add(self.user)
1032
- obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
1109
+ self.add_permissions(f"{self.model._meta.app_label}.delete_{self.model._meta.model_name}")
1033
1110
 
1034
1111
  response = self.client.delete(url, **self.header)
1035
1112
  self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
@@ -1046,10 +1123,7 @@ class APIViewTestCases:
1046
1123
  """
1047
1124
  id_list = self.get_deletable_object_pks()
1048
1125
  # Add object-level permission
1049
- obj_perm = users_models.ObjectPermission(name="Test permission", actions=["delete"])
1050
- obj_perm.save()
1051
- obj_perm.users.add(self.user)
1052
- obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
1126
+ self.add_permissions(f"{self.model._meta.app_label}.delete_{self.model._meta.model_name}")
1053
1127
 
1054
1128
  data = [{"id": id} for id in id_list]
1055
1129
 
@@ -1061,30 +1135,6 @@ class APIViewTestCases:
1061
1135
  class NotesURLViewTestCase(APITestCase):
1062
1136
  """Validate Notes URL on objects that have the Note model Mixin."""
1063
1137
 
1064
- @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
1065
- def test_notes_url_on_object(self):
1066
- if not hasattr(self.model, "notes"):
1067
- self.skipTest("Model doesn't appear to support Notes")
1068
- instance = self._get_queryset().first()
1069
- if not isinstance(instance.notes, QuerySet):
1070
- self.skipTest("Model has a notes field but it doesn't appear to be Notes")
1071
-
1072
- # Add object-level permission
1073
- obj_perm = users_models.ObjectPermission(
1074
- name="Test permission",
1075
- constraints={"pk": instance.pk},
1076
- actions=["view"],
1077
- )
1078
- obj_perm.save()
1079
- obj_perm.users.add(self.user)
1080
- obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
1081
- url = self._get_detail_url(instance)
1082
- response = self.client.get(url, **self.header)
1083
- self.assertHttpStatus(response, status.HTTP_200_OK)
1084
- self.assertIn("notes_url", response.data)
1085
- self.assertIn(f"{url}notes/", str(response.data["notes_url"]))
1086
- self.assertIn(instance.get_notes_url(api=True), str(response.data["notes_url"]))
1087
-
1088
1138
  @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
1089
1139
  def test_notes_url_functionality(self):
1090
1140
  if not hasattr(self.model, "notes"):
@@ -0,0 +1,18 @@
1
+ from django.test.utils import TestContextDecorator
2
+
3
+ from nautobot.core.events import _EVENT_BROKERS, deregister_event_broker, load_event_brokers
4
+
5
+
6
+ class load_event_broker_override_settings(TestContextDecorator):
7
+ def __init__(self, **kwargs) -> None:
8
+ self.options = kwargs
9
+ super().__init__()
10
+
11
+ def enable(self):
12
+ """Registered event brokers"""
13
+ load_event_brokers(self.options["EVENT_BROKERS"])
14
+
15
+ def disable(self):
16
+ """Clear all registered event brokers"""
17
+ for event_broker in _EVENT_BROKERS:
18
+ deregister_event_broker(event_broker)