nautobot 2.4.2__py3-none-any.whl → 2.4.4__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.
Files changed (267) hide show
  1. nautobot/apps/filters.py +2 -0
  2. nautobot/circuits/filters.py +1 -1
  3. nautobot/circuits/templates/circuits/inc/circuit_termination.html +1 -1
  4. nautobot/circuits/tests/integration/test_circuit.py +135 -0
  5. nautobot/circuits/tests/test_models.py +5 -3
  6. nautobot/circuits/views.py +4 -1
  7. nautobot/cloud/api/views.py +3 -3
  8. nautobot/cloud/filters.py +3 -6
  9. nautobot/cloud/tests/test_filters.py +21 -0
  10. nautobot/core/admin.py +2 -0
  11. nautobot/core/constants.py +0 -1
  12. nautobot/core/forms/__init__.py +2 -0
  13. nautobot/core/forms/forms.py +2 -1
  14. nautobot/core/forms/widgets.py +8 -0
  15. nautobot/core/jobs/__init__.py +2 -1
  16. nautobot/core/management/commands/generate_performance_test_endpoints.py +271 -0
  17. nautobot/core/models/utils.py +6 -1
  18. nautobot/core/templates/generic/object_bulk_delete.html +1 -1
  19. nautobot/core/templates/generic/object_bulk_edit.html +1 -1
  20. nautobot/core/templates/generic/object_bulk_import.html +1 -1
  21. nautobot/core/templates/generic/object_create.html +5 -0
  22. nautobot/core/templates/generic/object_delete.html +1 -1
  23. nautobot/core/templates/generic/object_detail.html +1 -1
  24. nautobot/core/templates/generic/object_edit.html +1 -1
  25. nautobot/core/templates/inc/javascript.html +3 -0
  26. nautobot/core/templates/widgets/clearable_file.html +5 -0
  27. nautobot/core/templatetags/helpers.py +3 -3
  28. nautobot/core/templatetags/ui_framework.py +20 -4
  29. nautobot/core/testing/forms.py +1 -1
  30. nautobot/core/testing/integration.py +37 -7
  31. nautobot/core/tests/test_api.py +1 -1
  32. nautobot/core/tests/test_commands.py +31 -0
  33. nautobot/core/tests/test_graphql.py +3 -3
  34. nautobot/core/tests/test_jobs.py +4 -1
  35. nautobot/core/tests/test_utils.py +17 -2
  36. nautobot/core/ui/object_detail.py +1 -1
  37. nautobot/core/utils/lookup.py +12 -1
  38. nautobot/core/views/generic.py +9 -1
  39. nautobot/core/views/mixins.py +9 -1
  40. nautobot/dcim/api/serializers.py +36 -0
  41. nautobot/dcim/api/views.py +12 -11
  42. nautobot/dcim/elevations.py +17 -4
  43. nautobot/dcim/factory.py +9 -1
  44. nautobot/dcim/filters/__init__.py +27 -1
  45. nautobot/dcim/forms.py +16 -7
  46. nautobot/dcim/models/devices.py +12 -7
  47. nautobot/dcim/signals.py +26 -0
  48. nautobot/dcim/templates/dcim/cable_trace.html +4 -4
  49. nautobot/dcim/templates/dcim/consoleport.html +14 -4
  50. nautobot/dcim/templates/dcim/consoleserverport.html +14 -4
  51. nautobot/dcim/templates/dcim/device/lldp_neighbors.html +3 -3
  52. nautobot/dcim/templates/dcim/frontport.html +7 -2
  53. nautobot/dcim/templates/dcim/interface.html +9 -4
  54. nautobot/dcim/templates/dcim/powerfeed.html +8 -3
  55. nautobot/dcim/templates/dcim/poweroutlet.html +14 -4
  56. nautobot/dcim/templates/dcim/powerport.html +14 -4
  57. nautobot/dcim/templates/dcim/rearport.html +7 -2
  58. nautobot/dcim/templates/dcim/virtualdevicecontext_retrieve.html +0 -62
  59. nautobot/dcim/templates/dcim/virtualdevicecontext_update.html +6 -0
  60. nautobot/dcim/tests/integration/test_fileinputpicker.py +87 -0
  61. nautobot/dcim/tests/test_api.py +176 -0
  62. nautobot/dcim/tests/test_filters.py +56 -3
  63. nautobot/dcim/tests/test_models.py +41 -1
  64. nautobot/dcim/views.py +24 -14
  65. nautobot/extras/api/mixins.py +1 -1
  66. nautobot/extras/api/views.py +4 -4
  67. nautobot/extras/filters/__init__.py +4 -0
  68. nautobot/extras/forms/forms.py +4 -0
  69. nautobot/extras/jobs.py +8 -1
  70. nautobot/extras/models/datasources.py +7 -3
  71. nautobot/extras/plugins/__init__.py +26 -1
  72. nautobot/extras/templates/extras/inc/jobresult.html +12 -13
  73. nautobot/extras/templates/extras/job.html +1 -0
  74. nautobot/extras/templates/extras/objectchange.html +28 -12
  75. nautobot/extras/tests/test_api.py +16 -15
  76. nautobot/extras/tests/test_dynamicgroups.py +14 -0
  77. nautobot/extras/tests/test_filters.py +2 -0
  78. nautobot/extras/tests/test_plugins.py +32 -1
  79. nautobot/extras/tests/test_views.py +209 -11
  80. nautobot/extras/utils.py +30 -0
  81. nautobot/extras/views.py +32 -14
  82. nautobot/ipam/api/serializers.py +7 -8
  83. nautobot/ipam/api/views.py +5 -5
  84. nautobot/ipam/factory.py +27 -8
  85. nautobot/ipam/filters.py +67 -29
  86. nautobot/ipam/formfields.py +51 -0
  87. nautobot/ipam/forms.py +15 -7
  88. nautobot/ipam/migrations/0051_added_optional_vrf_relationship_to_vdc.py +41 -0
  89. nautobot/ipam/models.py +63 -5
  90. nautobot/ipam/tables.py +21 -7
  91. nautobot/ipam/tests/test_api.py +107 -66
  92. nautobot/ipam/tests/test_filters.py +145 -5
  93. nautobot/ipam/tests/test_views.py +15 -2
  94. nautobot/project-static/bootstrap-filestyle-1.2.3/bootstrap-filestyle.min.js +11 -0
  95. nautobot/project-static/css/base.css +11 -0
  96. nautobot/project-static/css/dark.css +2 -1
  97. nautobot/project-static/docs/apps/index.html +1 -1
  98. nautobot/project-static/docs/apps/nautobot-apps.html +1 -1
  99. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +62 -0
  100. nautobot/project-static/docs/development/apps/api/configuration-view.html +0 -3
  101. nautobot/project-static/docs/development/apps/api/models/graphql.html +9 -13
  102. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +94 -1
  103. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +2 -5
  104. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +0 -3
  105. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +0 -3
  106. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +0 -3
  107. nautobot/project-static/docs/development/apps/api/prometheus.html +0 -3
  108. nautobot/project-static/docs/development/apps/api/setup.html +1 -1
  109. nautobot/project-static/docs/development/apps/api/testing.html +0 -6
  110. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +0 -3
  111. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +0 -3
  112. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +0 -3
  113. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +0 -3
  114. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +1 -7
  115. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +0 -7
  116. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +0 -4
  117. nautobot/project-static/docs/development/apps/api/views/notes.html +0 -3
  118. nautobot/project-static/docs/development/apps/index.html +2 -35
  119. nautobot/project-static/docs/development/apps/migration/code-updates.html +7 -6
  120. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +2 -2
  121. nautobot/project-static/docs/development/apps/migration/from-v1.html +3 -3
  122. nautobot/project-static/docs/development/core/application-registry.html +0 -6
  123. nautobot/project-static/docs/development/core/best-practices.html +1 -28
  124. nautobot/project-static/docs/development/core/bootstrap-ui.html +1 -1
  125. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +65 -11
  126. nautobot/project-static/docs/development/core/getting-started.html +14 -18
  127. nautobot/project-static/docs/development/core/homepage.html +0 -3
  128. nautobot/project-static/docs/development/core/index.html +1 -1
  129. nautobot/project-static/docs/development/core/minikube-dev-environment-for-k8s-jobs.html +3 -3
  130. nautobot/project-static/docs/development/core/model-checklist.html +1 -1
  131. nautobot/project-static/docs/development/core/navigation-menu.html +1 -1
  132. nautobot/project-static/docs/development/core/release-checklist.html +1 -1
  133. nautobot/project-static/docs/development/core/settings.html +1 -1
  134. nautobot/project-static/docs/development/core/style-guide.html +4 -9
  135. nautobot/project-static/docs/development/core/templates.html +0 -3
  136. nautobot/project-static/docs/development/core/testing.html +0 -9
  137. nautobot/project-static/docs/development/jobs/index.html +11 -30
  138. nautobot/project-static/docs/development/jobs/migration/from-v1.html +3 -2
  139. nautobot/project-static/docs/index.html +3 -2
  140. nautobot/project-static/docs/objects.inv +0 -0
  141. nautobot/project-static/docs/overview/application_stack.html +2 -20
  142. nautobot/project-static/docs/release-notes/version-1.0.html +2 -2
  143. nautobot/project-static/docs/release-notes/version-1.1.html +2 -2
  144. nautobot/project-static/docs/release-notes/version-1.2.html +3 -3
  145. nautobot/project-static/docs/release-notes/version-1.3.html +1 -1
  146. nautobot/project-static/docs/release-notes/version-1.4.html +17 -17
  147. nautobot/project-static/docs/release-notes/version-1.5.html +8 -8
  148. nautobot/project-static/docs/release-notes/version-1.6.html +4 -4
  149. nautobot/project-static/docs/release-notes/version-2.0.html +10 -10
  150. nautobot/project-static/docs/release-notes/version-2.1.html +7 -7
  151. nautobot/project-static/docs/release-notes/version-2.2.html +1 -1
  152. nautobot/project-static/docs/release-notes/version-2.3.html +4 -4
  153. nautobot/project-static/docs/release-notes/version-2.4.html +379 -0
  154. nautobot/project-static/docs/requirements.txt +1 -1
  155. nautobot/project-static/docs/search/search_index.json +1 -1
  156. nautobot/project-static/docs/sitemap.xml +290 -290
  157. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  158. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +3 -3
  159. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +4 -4
  160. nautobot/project-static/docs/user-guide/administration/configuration/redis.html +1 -1
  161. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +3 -13
  162. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +5 -5
  163. nautobot/project-static/docs/user-guide/administration/guides/docker.html +3 -18
  164. nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +1 -1
  165. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +4 -4
  166. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +15 -15
  167. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +2 -2
  168. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +1 -1
  169. nautobot/project-static/docs/user-guide/administration/installation/index.html +0 -16
  170. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +1 -1
  171. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +7 -10
  172. nautobot/project-static/docs/user-guide/administration/installation/services.html +1 -12
  173. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +3 -3
  174. nautobot/project-static/docs/user-guide/administration/security/index.html +1 -1
  175. nautobot/project-static/docs/user-guide/administration/security/notices.html +1 -0
  176. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +5 -35
  177. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +1 -1
  178. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/tables/v2-code-location-changes.yaml +1 -1
  179. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +12 -9
  180. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +0 -4
  181. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +0 -3
  182. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +0 -4
  183. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +0 -4
  184. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +0 -4
  185. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +0 -4
  186. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +0 -4
  187. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +0 -4
  188. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +0 -3
  189. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +0 -4
  190. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +0 -4
  191. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +0 -4
  192. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +1 -17
  193. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +0 -3
  194. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +0 -4
  195. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +0 -4
  196. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +0 -3
  197. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +1 -7
  198. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +0 -4
  199. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +0 -4
  200. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +0 -4
  201. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +0 -4
  202. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +0 -4
  203. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +0 -4
  204. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +0 -4
  205. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +0 -6
  206. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +0 -3
  207. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +0 -4
  208. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +0 -4
  209. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +0 -8
  210. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +15 -15
  211. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +1 -1
  212. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +0 -6
  213. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +0 -3
  214. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +3 -15
  215. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +1 -27
  216. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +0 -8
  217. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +3 -6
  218. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +0 -8
  219. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +0 -7
  220. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +0 -3
  221. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +0 -3
  222. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobqueue.html +2 -2
  223. nautobot/project-static/docs/user-guide/platform-functionality/jobs/kubernetes-job-support.html +6 -6
  224. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +0 -14
  225. nautobot/project-static/docs/user-guide/platform-functionality/note.html +0 -3
  226. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +1 -10
  227. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +0 -3
  228. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +0 -14
  229. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +0 -19
  230. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +3 -9
  231. nautobot/project-static/docs/user-guide/platform-functionality/status.html +0 -8
  232. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +0 -4
  233. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +1 -13
  234. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +0 -5
  235. nautobot/project-static/js/dropdown.js +28 -0
  236. nautobot/project-static/js/editor.js +292 -0
  237. nautobot/project-static/monaco-editor-0.52.2/README.md +81 -0
  238. nautobot/project-static/monaco-editor-0.52.2/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
  239. nautobot/project-static/monaco-editor-0.52.2/vs/base/worker/workerMain.js +31 -0
  240. nautobot/project-static/monaco-editor-0.52.2/vs/basic-languages/xml/xml.js +10 -0
  241. nautobot/project-static/monaco-editor-0.52.2/vs/basic-languages/yaml/yaml.js +10 -0
  242. nautobot/project-static/monaco-editor-0.52.2/vs/editor/editor.main.css +8 -0
  243. nautobot/project-static/monaco-editor-0.52.2/vs/editor/editor.main.js +798 -0
  244. nautobot/project-static/monaco-editor-0.52.2/vs/language/json/jsonMode.js +19 -0
  245. nautobot/project-static/monaco-editor-0.52.2/vs/language/json/jsonWorker.js +42 -0
  246. nautobot/project-static/monaco-editor-0.52.2/vs/loader.js +11 -0
  247. nautobot/tenancy/filters/__init__.py +3 -5
  248. nautobot/tenancy/forms.py +9 -0
  249. nautobot/tenancy/templates/tenancy/tenant_create.html +21 -0
  250. nautobot/tenancy/templates/tenancy/tenant_edit.html +2 -21
  251. nautobot/tenancy/templates/tenancy/tenantgroup.html +2 -44
  252. nautobot/tenancy/templates/tenancy/tenantgroup_retrieve.html +1 -0
  253. nautobot/tenancy/tests/test_filters.py +10 -0
  254. nautobot/tenancy/tests/test_views.py +5 -1
  255. nautobot/tenancy/urls.py +7 -79
  256. nautobot/tenancy/views.py +51 -80
  257. nautobot/virtualization/views.py +0 -1
  258. nautobot/wireless/api/serializers.py +6 -1
  259. nautobot/wireless/api/views.py +3 -3
  260. nautobot/wireless/tables.py +9 -4
  261. nautobot/wireless/tests/test_api.py +5 -9
  262. {nautobot-2.4.2.dist-info → nautobot-2.4.4.dist-info}/METADATA +9 -9
  263. {nautobot-2.4.2.dist-info → nautobot-2.4.4.dist-info}/RECORD +267 -246
  264. {nautobot-2.4.2.dist-info → nautobot-2.4.4.dist-info}/LICENSE.txt +0 -0
  265. {nautobot-2.4.2.dist-info → nautobot-2.4.4.dist-info}/NOTICE +0 -0
  266. {nautobot-2.4.2.dist-info → nautobot-2.4.4.dist-info}/WHEEL +0 -0
  267. {nautobot-2.4.2.dist-info → nautobot-2.4.4.dist-info}/entry_points.txt +0 -0
@@ -8,8 +8,13 @@
8
8
  </div>
9
9
  <table class="table table-hover panel-body attr-table">
10
10
  <tr>
11
- <td>Device</td>
12
- <td>{{ object.device|hyperlinked_object }}</td>
11
+ {% if object.device %}
12
+ <td>Device</td>
13
+ <td>{{ object.device|hyperlinked_object }}</td>
14
+ {% else %}
15
+ <td>Module</td>
16
+ <td>{{ object.module|hyperlinked_object }}</td>
17
+ {% endif %}
13
18
  </tr>
14
19
  <tr>
15
20
  <td>Name</td>
@@ -57,8 +62,13 @@
57
62
  </tr>
58
63
  {% if object.connected_endpoint %}
59
64
  <tr>
60
- <td>Device</td>
61
- <td>{{ object.connected_endpoint.device|hyperlinked_object }}</td>
65
+ {% if object.connected_endpoint.device %}
66
+ <td>Device</td>
67
+ <td>{{ object.connected_endpoint.device|hyperlinked_object }}</td>
68
+ {% else %}
69
+ <td>Module</td>
70
+ <td>{{ object.connected_endpoint.module|hyperlinked_object }}</td>
71
+ {% endif %}
62
72
  </tr>
63
73
  <tr>
64
74
  <td>Power Port</td>
@@ -8,8 +8,13 @@
8
8
  </div>
9
9
  <table class="table table-hover panel-body attr-table">
10
10
  <tr>
11
- <td>Device</td>
12
- <td>{{ object.device|hyperlinked_object }}</td>
11
+ {% if object.device %}
12
+ <td>Device</td>
13
+ <td>{{ object.device|hyperlinked_object }}</td>
14
+ {% else %}
15
+ <td>Module</td>
16
+ <td>{{ object.module|hyperlinked_object }}</td>
17
+ {% endif %}
13
18
  </tr>
14
19
  <tr>
15
20
  <td>Name</td>
@@ -57,8 +62,13 @@
57
62
  </tr>
58
63
  {% if object.connected_endpoint %}
59
64
  <tr>
60
- <td>Device</td>
61
- <td>{{ object.connected_endpoint.device|hyperlinked_object }}</td>
65
+ {% if object.connected_endpoint.device %}
66
+ <td>Device</td>
67
+ <td>{{ object.connected_endpoint.device|hyperlinked_object }}</td>
68
+ {% else %}
69
+ <td>Module</td>
70
+ <td>{{ object.connected_endpoint.module|hyperlinked_object }}</td>
71
+ {% endif %}
62
72
  </tr>
63
73
  <tr>
64
74
  <td>Power Outlet / Feed</td>
@@ -8,8 +8,13 @@
8
8
  </div>
9
9
  <table class="table table-hover panel-body attr-table">
10
10
  <tr>
11
- <td>Device</td>
12
- <td>{{ object.device|hyperlinked_object }}</td>
11
+ {% if object.device %}
12
+ <td>Device</td>
13
+ <td>{{ object.device|hyperlinked_object }}</td>
14
+ {% else %}
15
+ <td>Module</td>
16
+ <td>{{ object.module|hyperlinked_object }}</td>
17
+ {% endif %}
13
18
  </tr>
14
19
  <tr>
15
20
  <td>Name</td>
@@ -4,65 +4,3 @@
4
4
  {% block extra_breadcrumbs %}
5
5
  <li><a href="{% url 'dcim:device' pk=object.device.pk %}">{{ object.device }}</a></li>
6
6
  {% endblock extra_breadcrumbs %}
7
-
8
-
9
- {% block content_left_page %}
10
- <div class="panel panel-default">
11
- <div class="panel-heading">
12
- <strong>Virtual Device Context</strong>
13
- </div>
14
- <table class="table table-hover panel-body attr-table">
15
- <tr>
16
- <td>Name</td>
17
- <td>
18
- {{ object.name }}
19
- </td>
20
- </tr>
21
- <tr>
22
- <td>Identifier</td>
23
- <td>
24
- {{ object.identifier }}
25
- </td>
26
- </tr>
27
- <tr>
28
- <td>Role</td>
29
- <td>
30
- {{ object.role| hyperlinked_object_with_color }}
31
- </td>
32
- </tr>
33
- <tr>
34
- <td>Status</td>
35
- <td>
36
- {{ object.status| hyperlinked_object_with_color }}
37
- </td>
38
- </tr>
39
- <tr>
40
- <td>Device</td>
41
- <td>
42
- {{ object.device|hyperlinked_object }}
43
- </td>
44
- </tr>
45
- <tr>
46
- <td>Primary IPv4</td>
47
- <td>
48
- {{ object.primary_ip4|hyperlinked_object }}
49
- </td>
50
- </tr>
51
- <tr>
52
- <td>Primary IPv6</td>
53
- <td>
54
- {{ object.primary_ip6|hyperlinked_object }}
55
- </td>
56
- </tr>
57
- {% include 'inc/tenant_table_row.html' %}
58
- <tr>
59
- <td>Description</td>
60
- <td>{{ object.description|placeholder }}</td>
61
- </tr>
62
- </table>
63
- </div>
64
- {% endblock content_left_page %}
65
-
66
- {% block content_full_width_page %}
67
- {% include 'panel_table.html' with table=interfaces_table heading="Interfaces" %}
68
- {% endblock content_full_width_page %}
@@ -23,6 +23,12 @@
23
23
  {% endif %}
24
24
  </div>
25
25
  </div>
26
+ <div class="panel panel-default">
27
+ <div class="panel-heading"><strong>VRF Assignments</strong></div>
28
+ <div class="panel-body">
29
+ {% render_field form.vrfs %}
30
+ </div>
31
+ </div>
26
32
  {% include 'inc/tenancy_form_panel.html' %}
27
33
  {% include 'inc/extras_features_edit_form_fields.html' %}
28
34
  {% endblock %}
@@ -0,0 +1,87 @@
1
+ from django.urls import reverse
2
+
3
+ from nautobot.core.testing.integration import SeleniumTestCase, WebDriverWait
4
+ from nautobot.dcim.models import Location, LocationType
5
+ from nautobot.extras.models import Job, Status
6
+
7
+
8
+ class ClearableFileInputTestCase(SeleniumTestCase):
9
+ def setUp(self):
10
+ super().setUp()
11
+ self.user.is_superuser = True
12
+ self.user.save()
13
+ self.login(self.user.username, self.password)
14
+
15
+ def tearDown(self):
16
+ self.logout()
17
+ super().tearDown()
18
+
19
+ def _assert_file_picker(self, uri_to_visit: str, page_loaded_confirmation: str, file_input_selector_id: str):
20
+ """
21
+ Ensure clearable input file type has working clear and info display.
22
+ """
23
+ self.browser.visit(f"{self.live_server_url}{uri_to_visit}")
24
+ WebDriverWait(self.browser, 10).until(lambda driver: driver.is_text_present(page_loaded_confirmation))
25
+
26
+ # Find the first file input button and scroll to it
27
+ front_image_button = self.browser.find_by_css("span.group-span-filestyle.input-group-btn").first
28
+ front_image_button.scroll_to()
29
+
30
+ # cancel button is NOT visible initially
31
+ self.assertFalse(self.browser.find_by_css("button.clear-button").first.visible)
32
+
33
+ # Test file text changes after selecting a file
34
+ file_selection_indicator_css = "div.bootstrap-filestyle input[type='text'].form-control"
35
+ self.assertEqual(self.browser.find_by_css(file_selection_indicator_css).first.value, "")
36
+ front_image_file_input = self.browser.find_by_id(file_input_selector_id).first
37
+ front_image_file_input.value = "/dev/null"
38
+ self.assertEqual(self.browser.find_by_css(file_selection_indicator_css).first.value, "null")
39
+
40
+ # clear button is now visible
41
+ clear_button = self.browser.find_by_css("button.clear-button").first
42
+ self.assertTrue(clear_button.visible)
43
+
44
+ # clicking clearbutton should hide the button, and wipe the file input value
45
+ clear_button.click()
46
+ self.assertFalse(clear_button.visible)
47
+ self.assertEqual(front_image_file_input.value, "")
48
+
49
+ def test_add_device_page(self):
50
+ """
51
+ Confirm device type add page input is working correctly.
52
+ """
53
+ self._assert_file_picker(
54
+ uri_to_visit=reverse("dcim:devicetype_add"),
55
+ page_loaded_confirmation="Device Type",
56
+ file_input_selector_id="id_front_image",
57
+ )
58
+
59
+ def test_job_runner_page(self):
60
+ """
61
+ Confirm job run page file input is working correctly.
62
+ """
63
+ example_job = Job.objects.get(name="Example File Input/Output job").pk
64
+ job_example_file_uri = reverse("extras:job_run", kwargs={"pk": example_job})
65
+ self._assert_file_picker(
66
+ uri_to_visit=job_example_file_uri,
67
+ page_loaded_confirmation="Example File",
68
+ file_input_selector_id="id_input_file",
69
+ )
70
+
71
+ def test_location_image_attachment_view(self):
72
+ """
73
+ Confirm location image attachment page is working correctly.
74
+ """
75
+ location_type, _ = LocationType.objects.get_or_create(name="Campus")
76
+ location_status = Status.objects.get_for_model(Location).first()
77
+ location, _ = Location.objects.get_or_create(
78
+ name="Test Location 1", location_type=location_type, status=location_status
79
+ )
80
+ location_image_attach_uri = reverse(
81
+ "dcim:location_add_image", kwargs={"object_id": location.id, "model": Location}
82
+ )
83
+ self._assert_file_picker(
84
+ uri_to_visit=location_image_attach_uri,
85
+ page_loaded_confirmation="Image attachment",
86
+ file_input_selector_id="id_image",
87
+ )
@@ -1716,6 +1716,182 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
1716
1716
  )
1717
1717
  self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
1718
1718
 
1719
+ def _parent_device_test_data(self):
1720
+ location = Location.objects.filter(location_type=LocationType.objects.get(name="Campus")).first()
1721
+ device_status = Status.objects.get_for_model(Device).first()
1722
+ device_role = Role.objects.get_for_model(Device).first()
1723
+ device_type = DeviceType.objects.first()
1724
+
1725
+ device_type_parent = DeviceType.objects.create(
1726
+ manufacturer=device_type.manufacturer,
1727
+ model=f"{device_type.model} Parent",
1728
+ u_height=device_type.u_height,
1729
+ subdevice_role=SubdeviceRoleChoices.ROLE_PARENT,
1730
+ )
1731
+ device_type_child = DeviceType.objects.create(
1732
+ manufacturer=device_type.manufacturer,
1733
+ model=f"{device_type.model} Child",
1734
+ u_height=device_type.u_height,
1735
+ subdevice_role=SubdeviceRoleChoices.ROLE_CHILD,
1736
+ )
1737
+
1738
+ parent_device = Device.objects.create(
1739
+ device_type=device_type_parent,
1740
+ role=device_role,
1741
+ status=device_status,
1742
+ name="Device Parent",
1743
+ location=location,
1744
+ )
1745
+ device_bay_1 = DeviceBay.objects.create(name="db1", device_id=parent_device.pk)
1746
+ device_bay_2 = DeviceBay.objects.create(name="db2", device_id=parent_device.pk)
1747
+
1748
+ return parent_device, device_bay_1, device_bay_2, device_type_child
1749
+
1750
+ def test_creating_device_with_parent_bay(self):
1751
+ # Create test data
1752
+ parent_device, device_bay_1, device_bay_2, device_type_child = self._parent_device_test_data()
1753
+
1754
+ self.add_permissions("dcim.add_device")
1755
+ url = reverse("dcim-api:device-list")
1756
+
1757
+ # Test creating device with parent bay by device bay data
1758
+ data = {
1759
+ "device_type": device_type_child.pk,
1760
+ "role": parent_device.role.pk,
1761
+ "location": parent_device.location.pk,
1762
+ "name": "Device parent bay test #1",
1763
+ "status": parent_device.status.pk,
1764
+ "parent_bay": {"device": {"name": parent_device.name}, "name": device_bay_1.name},
1765
+ }
1766
+
1767
+ response = self.client.post(url, data, format="json", **self.header)
1768
+ self.assertHttpStatus(response, status.HTTP_201_CREATED)
1769
+
1770
+ created_device = Device.objects.get(name="Device parent bay test #1")
1771
+ self.assertEqual(created_device.parent_bay.pk, device_bay_1.pk)
1772
+
1773
+ # Test creating device with parent bay by device_bay.pk
1774
+ data = {
1775
+ "device_type": device_type_child.pk,
1776
+ "role": parent_device.role.pk,
1777
+ "location": parent_device.location.pk,
1778
+ "name": "Device parent bay test #2",
1779
+ "status": parent_device.status.pk,
1780
+ "parent_bay": device_bay_2.pk,
1781
+ }
1782
+
1783
+ response = self.client.post(url, data, format="json", **self.header)
1784
+ self.assertHttpStatus(response, status.HTTP_201_CREATED)
1785
+
1786
+ created_device = Device.objects.get(name="Device parent bay test #2")
1787
+ self.assertEqual(created_device.parent_bay.pk, device_bay_2.pk)
1788
+
1789
+ # Test creating device with parent bay already taken
1790
+ data = {
1791
+ "device_type": device_type_child.pk,
1792
+ "role": parent_device.role.pk,
1793
+ "location": parent_device.location.pk,
1794
+ "name": "Device parent bay test #3",
1795
+ "status": parent_device.status.pk,
1796
+ "parent_bay": device_bay_1.pk,
1797
+ }
1798
+
1799
+ response = self.client.post(url, data, format="json", **self.header)
1800
+ self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
1801
+ self.assertIn("Cannot install device; parent bay is already taken", response.content.decode(response.charset))
1802
+
1803
+ # Assert that on the #1 device, parent bay stayed the same
1804
+ old_device = Device.objects.get(name="Device parent bay test #1")
1805
+ self.assertEqual(old_device.parent_bay.pk, device_bay_1.pk)
1806
+
1807
+ def test_update_device_with_parent_bay(self):
1808
+ # Create test data
1809
+ parent_device, device_bay_1, device_bay_2, device_type_child = self._parent_device_test_data()
1810
+
1811
+ self.add_permissions("dcim.change_device")
1812
+
1813
+ child_device = Device.objects.create(
1814
+ device_type=device_type_child,
1815
+ role=parent_device.role,
1816
+ location=parent_device.location,
1817
+ name="Device parent bay test #4",
1818
+ status=parent_device.status,
1819
+ )
1820
+ # Test setting parent bay during the update
1821
+ patch_data = {"parent_bay": device_bay_1.pk}
1822
+ response = self.client.patch(self._get_detail_url(child_device), patch_data, format="json", **self.header)
1823
+ self.assertHttpStatus(response, status.HTTP_200_OK)
1824
+
1825
+ updated_device = Device.objects.get(name="Device parent bay test #4")
1826
+ self.assertEqual(updated_device.parent_bay.pk, device_bay_1.pk)
1827
+
1828
+ # Changing the parent bay is not allowed without removing it first
1829
+ patch_data = {"parent_bay": device_bay_2.pk}
1830
+ response = self.client.patch(self._get_detail_url(child_device), patch_data, format="json", **self.header)
1831
+ self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
1832
+ self.assertIn(
1833
+ f"Cannot install the specified device; device is already installed in {device_bay_1.name}",
1834
+ response.content.decode(response.charset),
1835
+ )
1836
+
1837
+ # Assert that parent bay stayed the same
1838
+ updated_device = Device.objects.get(name="Device parent bay test #4")
1839
+ self.assertEqual(updated_device.parent_bay.pk, device_bay_1.pk)
1840
+
1841
+ def test_reassign_device_to_parent_bay(self):
1842
+ # Create test data
1843
+ parent_device, device_bay_1, device_bay_2, device_type_child = self._parent_device_test_data()
1844
+ device_name = "Device parent bay test #5"
1845
+ child_device = Device.objects.create(
1846
+ device_type=device_type_child,
1847
+ role=parent_device.role,
1848
+ location=parent_device.location,
1849
+ name=device_name,
1850
+ status=parent_device.status,
1851
+ )
1852
+ device_bay_1.installed_device = child_device
1853
+ device_bay_1.save()
1854
+
1855
+ self.add_permissions("dcim.change_device", "dcim.view_device", "dcim.change_devicebay")
1856
+ child_device_detail_url = self._get_detail_url(child_device)
1857
+
1858
+ response = self.client.get(child_device_detail_url, **self.header)
1859
+ self.assertHttpStatus(response, status.HTTP_200_OK)
1860
+ self.assertEqual(response.json()["parent_bay"]["id"], str(device_bay_1.pk))
1861
+
1862
+ # Test unassigning parent bay
1863
+ patch_data = {"parent_bay": None}
1864
+ response = self.client.patch(child_device_detail_url, patch_data, format="json", **self.header)
1865
+ self.assertHttpStatus(response, status.HTTP_200_OK)
1866
+
1867
+ child_device.refresh_from_db()
1868
+ with self.assertRaises(DeviceBay.DoesNotExist):
1869
+ child_device.parent_bay
1870
+
1871
+ # And assign it again
1872
+ patch_data = {"parent_bay": device_bay_2.pk}
1873
+ response = self.client.patch(child_device_detail_url, patch_data, format="json", **self.header)
1874
+ self.assertHttpStatus(response, status.HTTP_200_OK)
1875
+
1876
+ child_device.refresh_from_db()
1877
+ self.assertEqual(child_device.parent_bay.pk, device_bay_2.pk)
1878
+
1879
+ # Unassign it through device bay
1880
+ patch_data = {"installed_device": None}
1881
+ response = self.client.patch(self._get_detail_url(device_bay_2), patch_data, format="json", **self.header)
1882
+ self.assertHttpStatus(response, status.HTTP_200_OK)
1883
+
1884
+ child_device.refresh_from_db()
1885
+ self.assertFalse(hasattr(child_device, "parent_bay"))
1886
+
1887
+ # And assign through device bay
1888
+ patch_data = {"installed_device": child_device.pk}
1889
+ response = self.client.patch(self._get_detail_url(device_bay_1), patch_data, format="json", **self.header)
1890
+ self.assertHttpStatus(response, status.HTTP_200_OK)
1891
+
1892
+ child_device.refresh_from_db()
1893
+ self.assertEqual(child_device.parent_bay.pk, device_bay_1.pk)
1894
+
1719
1895
 
1720
1896
  class ModuleTestCase(APIViewTestCases.APIViewTestCase):
1721
1897
  model = Module
@@ -157,6 +157,8 @@ def common_test_data(cls):
157
157
  cls.loc0 = loc0
158
158
  cls.loc1 = loc1
159
159
  cls.nested_loc = nested_loc
160
+ cls.loc2 = loc2
161
+ cls.loc3 = loc3
160
162
 
161
163
  provider = Provider.objects.first()
162
164
  circuit_type = CircuitType.objects.first()
@@ -1150,6 +1152,24 @@ class RackGroupTestCase(FilterTestCases.FilterTestCase):
1150
1152
  name="Rack Group 4",
1151
1153
  location=cls.loc1,
1152
1154
  )
1155
+ RackGroup.objects.create(
1156
+ name="Rack Group 5",
1157
+ location=cls.loc2,
1158
+ description="C",
1159
+ )
1160
+ RackGroup.objects.create(
1161
+ name="Rack Group 6",
1162
+ location=cls.loc2,
1163
+ )
1164
+ RackGroup.objects.create(
1165
+ name="Rack Group 7",
1166
+ location=cls.loc3,
1167
+ description="C",
1168
+ )
1169
+ RackGroup.objects.create(
1170
+ name="Rack Group 8",
1171
+ location=cls.loc3,
1172
+ )
1153
1173
 
1154
1174
  def test_children(self):
1155
1175
  child_groups = RackGroup.objects.filter(name__startswith="Child").filter(parent__isnull=False)[:2]
@@ -1161,6 +1181,24 @@ class RackGroupTestCase(FilterTestCases.FilterTestCase):
1161
1181
  params = {"children": [rack_group_4.pk, rack_group_4.pk]}
1162
1182
  self.assertFalse(self.filterset(params, self.queryset).qs.exists())
1163
1183
 
1184
+ def test_ancestors(self):
1185
+ with self.subTest():
1186
+ pk_list = []
1187
+ parent_locations = self.loc3.ancestors(include_self=True)
1188
+ pk_list.extend([v.pk for v in parent_locations])
1189
+ params = Q(location__pk__in=pk_list)
1190
+ expected_queryset = RackGroup.objects.filter(params)
1191
+ params = {"ancestors": [self.loc3.pk]}
1192
+ self.assertQuerysetEqualAndNotEmpty(self.filterset(params, self.queryset).qs, expected_queryset)
1193
+ with self.subTest():
1194
+ pk_list = []
1195
+ parent_locations = self.loc2.ancestors(include_self=True)
1196
+ pk_list.extend([v.pk for v in parent_locations])
1197
+ params = Q(location__pk__in=pk_list)
1198
+ expected_queryset = RackGroup.objects.filter(params)
1199
+ params = {"ancestors": [self.loc2.pk]}
1200
+ self.assertQuerysetEqualAndNotEmpty(self.filterset(params, self.queryset).qs, expected_queryset)
1201
+
1164
1202
 
1165
1203
  class RackTestCase(FilterTestCases.FilterTestCase, FilterTestCases.TenancyFilterTestCaseMixin):
1166
1204
  queryset = Rack.objects.all()
@@ -4188,12 +4226,13 @@ class InterfaceVDCAssignmentTestCase(FilterTestCases.FilterTestCase):
4188
4226
 
4189
4227
  @classmethod
4190
4228
  def setUpTestData(cls):
4191
- device = Device.objects.first()
4229
+ device_1 = Device.objects.first()
4230
+ device_2 = Device.objects.last()
4192
4231
  vdc_status = Status.objects.get_for_model(VirtualDeviceContext)[0]
4193
4232
  interface_status = Status.objects.get_for_model(Interface)[0]
4194
4233
  interfaces = [
4195
4234
  Interface.objects.create(
4196
- device=device,
4235
+ device=device_1,
4197
4236
  type=InterfaceTypeChoices.TYPE_1GE_FIXED,
4198
4237
  name=f"Interface 00{idx}",
4199
4238
  status=interface_status,
@@ -4202,7 +4241,7 @@ class InterfaceVDCAssignmentTestCase(FilterTestCases.FilterTestCase):
4202
4241
  ]
4203
4242
  vdcs = [
4204
4243
  VirtualDeviceContext.objects.create(
4205
- device=device,
4244
+ device=device_1,
4206
4245
  status=vdc_status,
4207
4246
  identifier=200 + idx,
4208
4247
  name=f"Test VDC {idx}",
@@ -4213,3 +4252,17 @@ class InterfaceVDCAssignmentTestCase(FilterTestCases.FilterTestCase):
4213
4252
  InterfaceVDCAssignment.objects.create(virtual_device_context=vdcs[1], interface=interfaces[0])
4214
4253
  InterfaceVDCAssignment.objects.create(virtual_device_context=vdcs[1], interface=interfaces[1])
4215
4254
  InterfaceVDCAssignment.objects.create(virtual_device_context=vdcs[2], interface=interfaces[2])
4255
+ InterfaceVDCAssignment.objects.create(
4256
+ virtual_device_context=VirtualDeviceContext.objects.create(
4257
+ device=device_2,
4258
+ status=vdc_status,
4259
+ identifier=200,
4260
+ name="Test VDC 0",
4261
+ ),
4262
+ interface=Interface.objects.create(
4263
+ device=device_2,
4264
+ type=InterfaceTypeChoices.TYPE_1GE_FIXED,
4265
+ name="Interface 000",
4266
+ status=interface_status,
4267
+ ),
4268
+ )
@@ -1166,6 +1166,19 @@ class LocationTypeTestCase(TestCase):
1166
1166
  child_loc.delete()
1167
1167
  child.validated_save()
1168
1168
 
1169
+ def test_removing_content_type(self):
1170
+ """Validation check to prevent removing an in-use content type from a LocationType."""
1171
+
1172
+ location_type = LocationType.objects.get(name="Campus")
1173
+ device_ct = ContentType.objects.get_for_model(Device)
1174
+
1175
+ with self.assertRaises(ValidationError) as cm:
1176
+ location_type.content_types.remove(device_ct)
1177
+ self.assertIn(
1178
+ f"Cannot remove the content type {device_ct} as currently at least one device is associated to a location",
1179
+ str(cm.exception),
1180
+ )
1181
+
1169
1182
 
1170
1183
  class LocationTestCase(ModelTestCases.BaseModelTestCase):
1171
1184
  model = Location
@@ -1895,6 +1908,33 @@ class DeviceTestCase(ModelTestCases.BaseModelTestCase):
1895
1908
  parent_device.rack = rack
1896
1909
  parent_device.save()
1897
1910
 
1911
+ # Test assigning a rack in the child location of the parent device location
1912
+ location_status = Status.objects.get_for_model(Location).first()
1913
+ child_location = Location.objects.create(
1914
+ name="Child Location 1",
1915
+ location_type=self.location_type_3,
1916
+ status=location_status,
1917
+ parent=parent_device.location,
1918
+ )
1919
+ child_rack = Rack.objects.create(name="Rack 2", location=child_location, status=self.device_status)
1920
+ parent_device.rack = child_rack
1921
+ parent_device.validated_save()
1922
+
1923
+ # Test assigning a rack outside the child locations of the parent device location
1924
+ new_location = Location.objects.create(
1925
+ name="New Location 1",
1926
+ status=location_status,
1927
+ location_type=self.location_type_3,
1928
+ )
1929
+ invalid_rack = Rack.objects.create(name="Rack 3", location=new_location, status=self.device_status)
1930
+ parent_device.rack = invalid_rack
1931
+ with self.assertRaises(ValidationError) as cm:
1932
+ parent_device.validated_save()
1933
+ self.assertIn(
1934
+ f'Rack "{invalid_rack}" does not belong to location "{parent_device.location}" and its descendants.',
1935
+ str(cm.exception),
1936
+ )
1937
+
1898
1938
  child_mtime_after_parent_rack_update_save = str(Device.objects.get(name="Child Device 1").last_updated)
1899
1939
 
1900
1940
  self.assertNotEqual(child_mtime_after_parent_noop_save, child_mtime_after_parent_rack_update_save)
@@ -2790,7 +2830,7 @@ class ControllerTestCase(ModelTestCases.BaseModelTestCase):
2790
2830
  controller.validated_save()
2791
2831
  self.assertEqual(
2792
2832
  error.exception.message_dict["location"][0],
2793
- f'Devices may not associate to locations of type "{location_type}".',
2833
+ f'Controllers may not associate to locations of type "{location_type}".',
2794
2834
  )
2795
2835
 
2796
2836
 
nautobot/dcim/views.py CHANGED
@@ -61,7 +61,7 @@ from nautobot.dcim.utils import get_all_network_driver_mappings, get_network_dri
61
61
  from nautobot.extras.models import Contact, ContactAssociation, Role, Status, Team
62
62
  from nautobot.extras.views import ObjectChangeLogView, ObjectConfigContextView, ObjectDynamicGroupsView
63
63
  from nautobot.ipam.models import IPAddress, Prefix, Service, VLAN
64
- from nautobot.ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable, VRFDeviceAssignmentTable
64
+ from nautobot.ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable, VRFDeviceAssignmentTable, VRFTable
65
65
  from nautobot.virtualization.models import VirtualMachine
66
66
  from nautobot.wireless.forms import ControllerManagedDeviceGroupWirelessNetworkFormSet
67
67
  from nautobot.wireless.models import (
@@ -1922,7 +1922,7 @@ class DeviceView(generic.ObjectView):
1922
1922
 
1923
1923
  # VRF assignments
1924
1924
  vrf_assignments = instance.vrf_assignments.restrict(request.user, "view")
1925
- vrf_table = VRFDeviceAssignmentTable(vrf_assignments, exclude=("virtual_machine", "device"))
1925
+ vrf_table = VRFDeviceAssignmentTable(vrf_assignments)
1926
1926
 
1927
1927
  # Software images
1928
1928
  if instance.software_version is not None:
@@ -4513,15 +4513,25 @@ class VirtualDeviceContextUIViewSet(NautobotUIViewSet):
4513
4513
  queryset = VirtualDeviceContext.objects.all()
4514
4514
  serializer_class = serializers.VirtualDeviceContextSerializer
4515
4515
  table_class = tables.VirtualDeviceContextTable
4516
-
4517
- def get_extra_context(self, request, instance):
4518
- if self.action == "retrieve":
4519
- interfaces_table = tables.InterfaceTable(
4520
- instance.interfaces.restrict(request.user, "view"), orderable=False, exclude=("device",)
4521
- )
4522
-
4523
- return {
4524
- "interfaces_table": interfaces_table,
4525
- **super().get_extra_context(request, instance),
4526
- }
4527
- return super().get_extra_context(request, instance)
4516
+ object_detail_content = object_detail.ObjectDetailContent(
4517
+ panels=(
4518
+ object_detail.ObjectFieldsPanel(
4519
+ section=SectionChoices.LEFT_HALF,
4520
+ weight=100,
4521
+ fields="__all__",
4522
+ ),
4523
+ object_detail.ObjectsTablePanel(
4524
+ weight=200,
4525
+ table_class=tables.InterfaceTable,
4526
+ table_attribute="interfaces",
4527
+ section=SectionChoices.FULL_WIDTH,
4528
+ exclude_columns=["device"],
4529
+ ),
4530
+ object_detail.ObjectsTablePanel(
4531
+ weight=300,
4532
+ table_class=VRFTable,
4533
+ table_attribute="vrfs",
4534
+ section=SectionChoices.FULL_WIDTH,
4535
+ ),
4536
+ ),
4537
+ )
@@ -37,7 +37,7 @@ class TaggedModelSerializerMixin(BaseModelSerializer):
37
37
 
38
38
  def _save_tags(self, instance, tags):
39
39
  if tags:
40
- instance.tags.set([t.name for t in tags])
40
+ instance.tags.set(tags)
41
41
  else:
42
42
  instance.tags.clear()
43
43