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
@@ -250,6 +250,11 @@ def get_model_for_view_name(view_name):
250
250
  Return the model class associated with the given view_name e.g. "circuits:circuit_detail", "dcim:device_list" and etc.
251
251
  If the app_label or model_name contained by the given view_name is invalid, this will return `None`.
252
252
  """
253
+ if view_name == "users-api:group-detail":
254
+ return Group
255
+ if view_name == "extras-api:contenttype-detail":
256
+ return ContentType
257
+
253
258
  split_view_name = view_name.split(":")
254
259
  if len(split_view_name) == 2:
255
260
  app_label, model_name = split_view_name # dcim, device_list
@@ -257,7 +262,13 @@ def get_model_for_view_name(view_name):
257
262
  _, app_label, model_name = split_view_name # plugins, app_name, model_list
258
263
  else:
259
264
  raise ValueError(f"Unexpected View Name: {view_name}")
260
- model_name = model_name.split("_")[0] # device
265
+
266
+ delimiter = "_"
267
+ if app_label.endswith("-api"):
268
+ app_label = app_label.replace("-api", "")
269
+ delimiter = "-"
270
+
271
+ model_name = model_name.split(delimiter)[0] # device
261
272
 
262
273
  try:
263
274
  model = apps.get_model(app_label=app_label, model_name=model_name)
@@ -214,8 +214,16 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
214
214
  resolved_path = resolve(request.path)
215
215
  list_url = f"{resolved_path.app_name}:{resolved_path.url_name}"
216
216
 
217
+ skip_user_and_global_default_saved_view = False
218
+ if self.filterset is not None:
219
+ skip_user_and_global_default_saved_view = get_filterable_params_from_filter_params(
220
+ request.GET.copy(),
221
+ self.non_filter_params,
222
+ self.filterset(),
223
+ )
224
+
217
225
  # If the user clicks on the clear view button, we do not check for global or user defaults
218
- if not clear_view and not request.GET.get("saved_view"):
226
+ if not skip_user_and_global_default_saved_view and not clear_view and not request.GET.get("saved_view"):
219
227
  # Check if there is a default for this view for this specific user
220
228
  if not isinstance(user, AnonymousUser):
221
229
  try:
@@ -723,8 +723,16 @@ class ObjectListViewMixin(NautobotViewSetMixin, mixins.ListModelMixin):
723
723
  if response is not None:
724
724
  return response
725
725
 
726
+ skip_user_and_global_default_saved_view = False
727
+ if self.filterset_class is not None:
728
+ skip_user_and_global_default_saved_view = get_filterable_params_from_filter_params(
729
+ request.GET.copy(),
730
+ self.non_filter_params,
731
+ self.filterset_class(),
732
+ )
733
+
726
734
  # If the user clicks on the clear view button, we do not check for global or user defaults
727
- if not clear_view and not request.GET.get("saved_view"):
735
+ if not skip_user_and_global_default_saved_view and not clear_view and not request.GET.get("saved_view"):
728
736
  # Check if there is a default for this view for this specific user
729
737
  app_label, model_name = queryset.model._meta.label.split(".")
730
738
  view_name = f"{app_label}:{model_name.lower()}_list"
@@ -1,6 +1,7 @@
1
1
  import contextlib
2
2
 
3
3
  from django.contrib.contenttypes.models import ContentType
4
+ from django.core.exceptions import ValidationError
4
5
  from drf_spectacular.utils import extend_schema_field
5
6
  from rest_framework import serializers
6
7
  from rest_framework.validators import UniqueTogetherValidator, UniqueValidator
@@ -560,11 +561,46 @@ class DeviceSerializer(TaggedModelSerializerMixin, NautobotModelSerializer):
560
561
  )
561
562
  validator(attrs, self)
562
563
 
564
+ # Validate parent bay
565
+ if parent_bay := attrs.get("parent_bay", None):
566
+ if parent_bay.installed_device and parent_bay.installed_device != self.instance:
567
+ raise ValidationError(
568
+ {
569
+ "installed_device": f"Cannot install device; parent bay is already taken ({parent_bay.installed_device})"
570
+ }
571
+ )
572
+
573
+ if self.instance:
574
+ parent_bay.installed_device = self.instance
575
+ parent_bay.full_clean()
576
+
563
577
  # Enforce model validation
564
578
  super().validate(attrs)
565
579
 
566
580
  return attrs
567
581
 
582
+ def create(self, validated_data):
583
+ instance = super().create(validated_data)
584
+ self.update_parent_bay(validated_data, instance)
585
+ return instance
586
+
587
+ def update(self, instance, validated_data):
588
+ instance = super().update(instance, validated_data)
589
+ self.update_parent_bay(validated_data, instance)
590
+ return instance
591
+
592
+ def update_parent_bay(self, validated_data, instance):
593
+ update_parent_bay = "parent_bay" in validated_data.keys()
594
+ parent_bay = validated_data.get("parent_bay")
595
+ if update_parent_bay:
596
+ if parent_bay:
597
+ parent_bay.installed_device = instance
598
+ parent_bay.save()
599
+ elif hasattr(instance, "parent_bay"):
600
+ parent_bay = instance.parent_bay
601
+ parent_bay.installed_device = None
602
+ parent_bay.validated_save()
603
+
568
604
 
569
605
  class DeviceNAPALMSerializer(serializers.Serializer):
570
606
  method = serializers.DictField()
@@ -74,6 +74,7 @@ from nautobot.dcim.models import (
74
74
  )
75
75
  from nautobot.extras.api.views import (
76
76
  ConfigContextQuerySetMixin,
77
+ CustomFieldModelViewSet,
77
78
  NautobotModelViewSet,
78
79
  )
79
80
  from nautobot.extras.choices import SecretsGroupAccessTypeChoices, SecretsGroupSecretTypeChoices
@@ -182,7 +183,7 @@ class RackGroupViewSet(NautobotModelViewSet):
182
183
 
183
184
 
184
185
  class RackViewSet(NautobotModelViewSet):
185
- queryset = Rack.objects.select_related("rack_group__location").annotate(
186
+ queryset = Rack.objects.select_related("role", "status", "rack_group__location").annotate(
186
187
  device_count=count_related(Device, "rack"),
187
188
  power_feed_count=count_related(PowerFeed, "rack"),
188
189
  )
@@ -300,13 +301,13 @@ class DeviceTypeViewSet(NautobotModelViewSet):
300
301
  #
301
302
 
302
303
 
303
- class ConsolePortTemplateViewSet(NautobotModelViewSet):
304
+ class ConsolePortTemplateViewSet(CustomFieldModelViewSet):
304
305
  queryset = ConsolePortTemplate.objects.select_related("device_type__manufacturer", "module_type__manufacturer")
305
306
  serializer_class = serializers.ConsolePortTemplateSerializer
306
307
  filterset_class = filters.ConsolePortTemplateFilterSet
307
308
 
308
309
 
309
- class ConsoleServerPortTemplateViewSet(NautobotModelViewSet):
310
+ class ConsoleServerPortTemplateViewSet(CustomFieldModelViewSet):
310
311
  queryset = ConsoleServerPortTemplate.objects.select_related(
311
312
  "device_type__manufacturer", "module_type__manufacturer"
312
313
  )
@@ -314,43 +315,43 @@ class ConsoleServerPortTemplateViewSet(NautobotModelViewSet):
314
315
  filterset_class = filters.ConsoleServerPortTemplateFilterSet
315
316
 
316
317
 
317
- class PowerPortTemplateViewSet(NautobotModelViewSet):
318
+ class PowerPortTemplateViewSet(CustomFieldModelViewSet):
318
319
  queryset = PowerPortTemplate.objects.select_related("device_type__manufacturer", "module_type__manufacturer")
319
320
  serializer_class = serializers.PowerPortTemplateSerializer
320
321
  filterset_class = filters.PowerPortTemplateFilterSet
321
322
 
322
323
 
323
- class PowerOutletTemplateViewSet(NautobotModelViewSet):
324
+ class PowerOutletTemplateViewSet(CustomFieldModelViewSet):
324
325
  queryset = PowerOutletTemplate.objects.select_related("device_type__manufacturer", "module_type__manufacturer")
325
326
  serializer_class = serializers.PowerOutletTemplateSerializer
326
327
  filterset_class = filters.PowerOutletTemplateFilterSet
327
328
 
328
329
 
329
- class InterfaceTemplateViewSet(NautobotModelViewSet):
330
+ class InterfaceTemplateViewSet(CustomFieldModelViewSet):
330
331
  queryset = InterfaceTemplate.objects.select_related("device_type__manufacturer", "module_type__manufacturer")
331
332
  serializer_class = serializers.InterfaceTemplateSerializer
332
333
  filterset_class = filters.InterfaceTemplateFilterSet
333
334
 
334
335
 
335
- class FrontPortTemplateViewSet(NautobotModelViewSet):
336
+ class FrontPortTemplateViewSet(CustomFieldModelViewSet):
336
337
  queryset = FrontPortTemplate.objects.select_related("device_type__manufacturer", "module_type__manufacturer")
337
338
  serializer_class = serializers.FrontPortTemplateSerializer
338
339
  filterset_class = filters.FrontPortTemplateFilterSet
339
340
 
340
341
 
341
- class RearPortTemplateViewSet(NautobotModelViewSet):
342
+ class RearPortTemplateViewSet(CustomFieldModelViewSet):
342
343
  queryset = RearPortTemplate.objects.select_related("device_type__manufacturer", "module_type__manufacturer")
343
344
  serializer_class = serializers.RearPortTemplateSerializer
344
345
  filterset_class = filters.RearPortTemplateFilterSet
345
346
 
346
347
 
347
- class DeviceBayTemplateViewSet(NautobotModelViewSet):
348
+ class DeviceBayTemplateViewSet(CustomFieldModelViewSet):
348
349
  queryset = DeviceBayTemplate.objects.select_related("device_type__manufacturer")
349
350
  serializer_class = serializers.DeviceBayTemplateSerializer
350
351
  filterset_class = filters.DeviceBayTemplateFilterSet
351
352
 
352
353
 
353
- class ModuleBayTemplateViewSet(NautobotModelViewSet):
354
+ class ModuleBayTemplateViewSet(CustomFieldModelViewSet):
354
355
  queryset = ModuleBayTemplate.objects.select_related("device_type__manufacturer", "module_type__manufacturer")
355
356
  serializer_class = serializers.ModuleBayTemplateSerializer
356
357
  filterset_class = filters.ModuleBayTemplateFilterSet
@@ -831,7 +832,7 @@ class VirtualDeviceContextViewSet(NautobotModelViewSet):
831
832
  filterset_class = filters.VirtualDeviceContextFilterSet
832
833
 
833
834
 
834
- class InterfaceVDCAssignmentViewSet(NautobotModelViewSet):
835
+ class InterfaceVDCAssignmentViewSet(ModelViewSet):
835
836
  queryset = InterfaceVDCAssignment.objects.all()
836
837
  serializer_class = serializers.InterfaceVDCAssignmentSerializer
837
838
  filterset_class = filters.InterfaceVDCAssignmentFilterSet
@@ -96,17 +96,30 @@ class RackElevationSVG:
96
96
  device_fullname = str(device) + device_bay_details
97
97
  device_shortname = settings.UI_RACK_VIEW_TRUNCATE_FUNCTION(str(device)) + device_bay_details
98
98
 
99
- color = device.role.color
100
- reverse_url = reverse("dcim:device", kwargs={"pk": device.pk})
99
+ role_color = device.role.color
100
+ status_color = device.status.color
101
+ device_reverse_url = reverse("dcim:device", kwargs={"pk": device.pk})
102
+ status_reverse_url = reverse("extras:status", kwargs={"pk": device.status.pk})
101
103
  link = drawing.add(
102
104
  drawing.a(
103
- href=f"{self.base_url}{reverse_url}",
105
+ href=f"{self.base_url}{device_reverse_url}",
104
106
  target="_top",
105
107
  fill="black",
106
108
  )
107
109
  )
108
110
  link.set_desc(self._get_device_description(device))
109
- link.add(drawing.rect(start, end, style=f"fill: #{color}", class_="slot"))
111
+ link.add(drawing.rect(start, end, style=f"fill: #{role_color}", class_="slot"))
112
+
113
+ status_rect = drawing.add(
114
+ drawing.a(
115
+ href=f"{self.base_url}{status_reverse_url}",
116
+ target="_top",
117
+ fill="black",
118
+ )
119
+ )
120
+ status_rect.set_desc(device.status.name)
121
+ status_end = (end[0] / 20, end[1]) # width, y
122
+ status_rect.add(drawing.rect(start, status_end, style=f"fill: #{status_color}"))
110
123
 
111
124
  # Embed front device type image if one exists
112
125
  if self.include_images and device.device_type.front_image:
nautobot/dcim/factory.py CHANGED
@@ -64,7 +64,7 @@ from nautobot.dcim.models import (
64
64
  )
65
65
  from nautobot.extras.models import ExternalIntegration, Role, Status
66
66
  from nautobot.extras.utils import FeatureQuery
67
- from nautobot.ipam.models import Prefix, VLAN, VLANGroup
67
+ from nautobot.ipam.models import Prefix, VLAN, VLANGroup, VRF
68
68
  from nautobot.tenancy.models import Tenant
69
69
  from nautobot.virtualization.models import Cluster
70
70
 
@@ -1008,3 +1008,11 @@ class VirtualDeviceContextFactory(PrimaryModelFactory):
1008
1008
  self.interfaces.set(extracted)
1009
1009
  else:
1010
1010
  self.interfaces.set(get_random_instances(Interface.objects.filter(device=self.device)))
1011
+
1012
+ @factory.post_generation
1013
+ def vrfs(self, create, extracted, **kwargs):
1014
+ if create:
1015
+ if extracted:
1016
+ self.vrfs.set(extracted)
1017
+ else:
1018
+ self.vrfs.set(get_random_instances(VRF.objects.all()))
@@ -101,7 +101,7 @@ from nautobot.extras.filters import (
101
101
  from nautobot.extras.models import ExternalIntegration, SecretsGroup
102
102
  from nautobot.extras.utils import FeatureQuery
103
103
  from nautobot.ipam.models import IPAddress, VLAN, VLANGroup
104
- from nautobot.tenancy.filters import TenancyModelFilterSetMixin
104
+ from nautobot.tenancy.filters.mixins import TenancyModelFilterSetMixin
105
105
  from nautobot.tenancy.models import Tenant
106
106
  from nautobot.virtualization.models import Cluster, VirtualMachine
107
107
  from nautobot.wireless.models import RadioProfile, WirelessNetwork
@@ -362,6 +362,12 @@ class RackGroupFilterSet(LocatableModelFilterSetMixin, NautobotFilterSet, NameSe
362
362
  to_field_name="name",
363
363
  label="Parent (name or ID)",
364
364
  )
365
+ ancestors = NaturalKeyOrPKMultipleChoiceFilter(
366
+ queryset=Location.objects.all(),
367
+ to_field_name="name",
368
+ label="Location(s) and ancestors thereof (name or ID)",
369
+ method="_ancestors",
370
+ )
365
371
  children = NaturalKeyOrPKMultipleChoiceFilter(
366
372
  queryset=RackGroup.objects.all(),
367
373
  to_field_name="name",
@@ -392,6 +398,26 @@ class RackGroupFilterSet(LocatableModelFilterSetMixin, NautobotFilterSet, NameSe
392
398
  model = RackGroup
393
399
  fields = ["id", "name", "description", "racks"]
394
400
 
401
+ def generate_query__ancestors(self, value):
402
+ """Helper method used by _ancestors() method."""
403
+ if value:
404
+ locations = Location.objects.filter(pk__in=[v.pk for v in value])
405
+ pk_list = []
406
+ for location in locations:
407
+ parent_locations = location.ancestors(include_self=True)
408
+ pk_list.extend([v.pk for v in parent_locations])
409
+ params = Q(location__pk__in=pk_list)
410
+ return params
411
+ return Q()
412
+
413
+ @extend_schema_field({"type": "string"})
414
+ def _ancestors(self, queryset, name, value):
415
+ """FilterSet method for, given a location, getting RackGroups that exist with in the parent Location(s) and the location itself."""
416
+ if value:
417
+ params = self.generate_query__ancestors(value)
418
+ return queryset.filter(params)
419
+ return queryset
420
+
395
421
 
396
422
  class RackFilterSet(
397
423
  NautobotFilterSet,
nautobot/dcim/forms.py CHANGED
@@ -17,6 +17,7 @@ from nautobot.core.forms import (
17
17
  AutoPositionPatternField,
18
18
  BootstrapMixin,
19
19
  BulkEditNullBooleanSelect,
20
+ ClearableFileInput,
20
21
  ColorSelect,
21
22
  CommentField,
22
23
  DatePicker,
@@ -509,7 +510,7 @@ class RackForm(LocatableModelFormMixin, NautobotModelForm, TenancyForm):
509
510
  rack_group = DynamicModelChoiceField(
510
511
  queryset=RackGroup.objects.all(),
511
512
  required=False,
512
- query_params={"location": "$location"},
513
+ query_params={"ancestors": "$location"},
513
514
  )
514
515
  comments = CommentField()
515
516
 
@@ -850,12 +851,8 @@ class DeviceTypeForm(NautobotModelForm):
850
851
  widgets = {
851
852
  "subdevice_role": StaticSelect2(),
852
853
  # Exclude SVG images (unsupported by PIL)
853
- "front_image": forms.ClearableFileInput(
854
- attrs={"accept": "image/bmp,image/gif,image/jpeg,image/png,image/tiff"}
855
- ),
856
- "rear_image": forms.ClearableFileInput(
857
- attrs={"accept": "image/bmp,image/gif,image/jpeg,image/png,image/tiff"}
858
- ),
854
+ "front_image": ClearableFileInput(attrs={"accept": "image/bmp,image/gif,image/jpeg,image/png,image/tiff"}),
855
+ "rear_image": ClearableFileInput(attrs={"accept": "image/bmp,image/gif,image/jpeg,image/png,image/tiff"}),
859
856
  }
860
857
 
861
858
 
@@ -5301,6 +5298,11 @@ class VirtualDeviceContextForm(NautobotModelForm):
5301
5298
  required=True,
5302
5299
  query_params={"content_types": VirtualDeviceContext._meta.label_lower},
5303
5300
  )
5301
+ vrfs = DynamicModelMultipleChoiceField(
5302
+ queryset=VRF.objects.all(),
5303
+ required=False,
5304
+ label="VRFs",
5305
+ )
5304
5306
 
5305
5307
  class Meta:
5306
5308
  model = VirtualDeviceContext
@@ -5311,6 +5313,7 @@ class VirtualDeviceContextForm(NautobotModelForm):
5311
5313
  "status",
5312
5314
  "identifier",
5313
5315
  "interfaces",
5316
+ "vrfs",
5314
5317
  "primary_ip4",
5315
5318
  "primary_ip6",
5316
5319
  "tenant",
@@ -5326,11 +5329,15 @@ class VirtualDeviceContextForm(NautobotModelForm):
5326
5329
  self.fields["device"].disabled = True
5327
5330
  self.fields["device"].required = False
5328
5331
 
5332
+ self.initial["vrfs"] = self.instance.vrfs.values_list("id", flat=True)
5333
+
5329
5334
  def save(self, commit=True):
5330
5335
  instance = super().save(commit)
5331
5336
  if commit:
5332
5337
  interfaces = self.cleaned_data["interfaces"]
5333
5338
  instance.interfaces.set(interfaces)
5339
+ vrfs = self.cleaned_data["vrfs"]
5340
+ instance.vrfs.set(vrfs)
5334
5341
  return instance
5335
5342
 
5336
5343
 
@@ -5348,6 +5355,8 @@ class VirtualDeviceContextBulkEditForm(
5348
5355
  remove_interfaces = DynamicModelMultipleChoiceField(
5349
5356
  queryset=Interface.objects.all(), required=False, query_params={"device": "$device"}
5350
5357
  )
5358
+ add_vrfs = DynamicModelMultipleChoiceField(queryset=VRF.objects.all(), required=False)
5359
+ remove_vrfs = DynamicModelMultipleChoiceField(queryset=VRF.objects.all(), required=False)
5351
5360
 
5352
5361
  class Meta:
5353
5362
  model = VirtualDeviceContext
@@ -670,11 +670,17 @@ class Device(PrimaryModel, ConfigContextModel):
670
670
 
671
671
  # Validate location
672
672
  if self.location is not None:
673
- # TODO: after Location model replaced Site, which was not a hierarchical model, should we allow users to assign a Rack belongs to
674
- # the parent Location or the child location of `self.location`?
675
-
676
- if self.rack is not None and self.rack.location != self.location:
677
- raise ValidationError({"rack": f'Rack "{self.rack}" does not belong to location "{self.location}".'})
673
+ if self.rack is not None:
674
+ device_location = self.location
675
+ # Rack's location must be a child location or the same location as that of the parent device.
676
+ # Location is a required field on rack.
677
+ rack_location = self.rack.location
678
+ if device_location not in rack_location.ancestors(include_self=True):
679
+ raise ValidationError(
680
+ {
681
+ "rack": f'Rack "{self.rack}" does not belong to location "{self.location}" and its descendants.'
682
+ }
683
+ )
678
684
 
679
685
  # self.cluster is validated somewhat later, see below
680
686
 
@@ -1419,11 +1425,10 @@ class Controller(PrimaryModel):
1419
1425
  "controller_device": ("Cannot assign both a device and a device redundancy group to a controller."),
1420
1426
  },
1421
1427
  )
1422
-
1423
1428
  if self.location:
1424
1429
  if ContentType.objects.get_for_model(self) not in self.location.location_type.content_types.all():
1425
1430
  raise ValidationError(
1426
- {"location": f'Devices may not associate to locations of type "{self.location.location_type}".'}
1431
+ {"location": f'Controllers may not associate to locations of type "{self.location.location_type}".'}
1427
1432
  )
1428
1433
 
1429
1434
  def get_capabilities_display(self):
nautobot/dcim/signals.py CHANGED
@@ -16,6 +16,7 @@ from .models import (
16
16
  DeviceRedundancyGroup,
17
17
  Interface,
18
18
  InterfaceVDCAssignment,
19
+ LocationType,
19
20
  PathEndpoint,
20
21
  PowerPanel,
21
22
  Rack,
@@ -355,3 +356,28 @@ def handle_controller_managed_device_group_controller_change(instance, raw=False
355
356
  group.controller = instance.controller
356
357
  group.save()
357
358
  logger.debug("Updated controller from parent %s for child %s", instance, group)
359
+
360
+
361
+ @receiver(m2m_changed, sender=LocationType.content_types.through)
362
+ def content_type_changed(instance, action, **kwargs):
363
+ """
364
+ Prevents removal of a ContentType from LocationType if it's in use by any models
365
+ associated with the locations.
366
+ """
367
+
368
+ if action != "pre_remove":
369
+ return
370
+
371
+ removed_content_types = ContentType.objects.filter(pk__in=kwargs.get("pk_set", []))
372
+
373
+ for content_type in removed_content_types:
374
+ model_class = content_type.model_class()
375
+
376
+ if model_class.objects.filter(location__location_type=instance).exists():
377
+ raise ValidationError(
378
+ {
379
+ "content_types": (
380
+ f"Cannot remove the content type {content_type} as currently at least one {model_class._meta.verbose_name} is associated to a location of this location type. "
381
+ )
382
+ }
383
+ )
@@ -13,8 +13,8 @@
13
13
  {% for near_end, cable, far_end in traced_path %}
14
14
 
15
15
  {# Near end #}
16
- {% if near_end.device %}
17
- {% include 'dcim/trace/device.html' with device=near_end.device %}
16
+ {% if near_end.device or near_end.module %}
17
+ {% include 'dcim/trace/device.html' with device=near_end.parent %}
18
18
  {% include 'dcim/trace/termination.html' with termination=near_end %}
19
19
  {% elif near_end.power_panel %}
20
20
  {% include 'dcim/trace/powerpanel.html' with powerpanel=near_end.power_panel %}
@@ -30,10 +30,10 @@
30
30
  {% endif %}
31
31
 
32
32
  {# Far end #}
33
- {% if far_end.device %}
33
+ {% if far_end.device or far_end.module %}
34
34
  {% include 'dcim/trace/termination.html' with termination=far_end %}
35
35
  {% if forloop.last %}
36
- {% include 'dcim/trace/device.html' with device=far_end.device %}
36
+ {% include 'dcim/trace/device.html' with device=far_end.parent %}
37
37
  {% endif %}
38
38
  {% elif far_end.power_panel %}
39
39
  {% include 'dcim/trace/termination.html' with termination=far_end %}
@@ -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>
@@ -49,8 +54,13 @@
49
54
  </tr>
50
55
  {% if object.connected_endpoint %}
51
56
  <tr>
52
- <td>Device</td>
53
- <td>{{ object.connected_endpoint.device|hyperlinked_object }}</td>
57
+ {% if object.connected_endpoint.device %}
58
+ <td>Device</td>
59
+ <td>{{ object.connected_endpoint.device|hyperlinked_object }}</td>
60
+ {% else %}
61
+ <td>Module</td>
62
+ <td>{{ object.connected_endpoint.module|hyperlinked_object }}</td>
63
+ {% endif %}
54
64
  </tr>
55
65
  <tr>
56
66
  <td>Console Server 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>
@@ -49,8 +54,13 @@
49
54
  </tr>
50
55
  {% if object.connected_endpoint %}
51
56
  <tr>
52
- <td>Device</td>
53
- <td>{{ object.connected_endpoint.device|hyperlinked_object }}</td>
57
+ {% if object.connected_endpoint.device %}
58
+ <td>Device</td>
59
+ <td>{{ object.connected_endpoint.device|hyperlinked_object }}</td>
60
+ {% else %}
61
+ <td>Module</td>
62
+ <td>{{ object.connected_endpoint.module|hyperlinked_object }}</td>
63
+ {% endif %}
54
64
  </tr>
55
65
  <tr>
56
66
  <td>Console Port</td>
@@ -24,9 +24,9 @@
24
24
  {% for iface in interfaces %}
25
25
  <tr data-interface-name="{{ iface.name }}">
26
26
  <td>{{ iface }}</td>
27
- {% if iface.connected_endpoint.device %}
28
- <td class="configured_device" data="{{ iface.connected_endpoint.device }}" data-chassis="{{ iface.connected_endpoint.device.virtual_chassis.name }}">
29
- {{ iface.connected_endpoint.device|hyperlinked_object }}
27
+ {% if iface.connected_endpoint.device or iface.connected_endpoint.module %}
28
+ <td class="configured_device" data="{{ iface.connected_endpoint.parent }}" data-chassis="{{ iface.connected_endpoint.parent.virtual_chassis.name }}">
29
+ {{ iface.connected_endpoint.parent|hyperlinked_object }}
30
30
  </td>
31
31
  <td class="configured_interface" data-interface-name="{{ iface.connected_endpoint }}">
32
32
  <span title="{{ iface.connected_endpoint.get_type_display }}">{{ iface.connected_endpoint }}</span>
@@ -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>
@@ -101,11 +101,16 @@
101
101
  </a>
102
102
  </td>
103
103
  </tr>
104
- {% if object.connected_endpoint.device %}
104
+ {% if object.connected_endpoint.device or object.connected_endpoint.module %}
105
105
  {% with iface=object.connected_endpoint %}
106
106
  <tr>
107
- <td>Device</td>
108
- <td>{{ iface.device|hyperlinked_object }}</td>
107
+ {% if iface.device %}
108
+ <td>Device</td>
109
+ <td>{{ iface.device|hyperlinked_object }}</td>
110
+ {% else %}
111
+ <td>Module</td>
112
+ <td>{{ iface.module|hyperlinked_object }}</td>
113
+ {% endif %}
109
114
  </tr>
110
115
  <tr>
111
116
  <td>Interface</td>
@@ -201,7 +206,7 @@
201
206
  <tbody>
202
207
  {% for member in object.member_interfaces.all %}
203
208
  <tr>
204
- <td>{{ member.device|hyperlinked_object }}</td>
209
+ <td>{{ member.parent|hyperlinked_object }}</td>
205
210
  <td>{{ member|hyperlinked_object }}</td>
206
211
  <td>
207
212
  {{ member.get_type_display }}
@@ -39,7 +39,7 @@
39
39
  <td>Connected Device</td>
40
40
  <td>
41
41
  {% if object.connected_endpoint %}
42
- {{ object.connected_endpoint.device|hyperlinked_object }}
42
+ {{ object.connected_endpoint.parent|hyperlinked_object }}
43
43
  ({{ object.connected_endpoint }})
44
44
  {% else %}
45
45
  <span class="text-muted">None</span>
@@ -110,8 +110,13 @@
110
110
  </tr>
111
111
  {% if object.connected_endpoint %}
112
112
  <tr>
113
- <td>Device</td>
114
- <td>{{ object.connected_endpoint.device|hyperlinked_object }}</td>
113
+ {% if object.connected_endpoint.device %}
114
+ <td>Device</td>
115
+ <td>{{ object.connected_endpoint.device|hyperlinked_object }}</td>
116
+ {% else %}
117
+ <td>Module</td>
118
+ <td>{{ object.connected_endpoint.module|hyperlinked_object }}</td>
119
+ {% endif %}
115
120
  </tr>
116
121
  <tr>
117
122
  <td>Power Port</td>