netbox-plugin-plan-drawing 1.0.0__tar.gz → 1.1.0__tar.gz

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 (55) hide show
  1. {netbox_plugin_plan_drawing-1.0.0/netbox_plugin_plan_drawing.egg-info → netbox_plugin_plan_drawing-1.1.0}/PKG-INFO +1 -1
  2. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/netbox_plugin_plan_drawing/__init__.py +1 -1
  3. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/netbox_plugin_plan_drawing/static/netbox_plugin_plan_drawing/plan-drawing.css +31 -0
  4. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/netbox_plugin_plan_drawing/static/netbox_plugin_plan_drawing/viewer.js +61 -15
  5. netbox_plugin_plan_drawing-1.1.0/netbox_plugin_plan_drawing/templates/netbox_plugin_plan_drawing/inc/element_contents.html +21 -0
  6. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/netbox_plugin_plan_drawing/templates/netbox_plugin_plan_drawing/plandrawing.html +9 -0
  7. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/netbox_plugin_plan_drawing/tests/test_views.py +24 -0
  8. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/netbox_plugin_plan_drawing/views.py +77 -1
  9. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0/netbox_plugin_plan_drawing.egg-info}/PKG-INFO +1 -1
  10. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/netbox_plugin_plan_drawing.egg-info/SOURCES.txt +1 -0
  11. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/pyproject.toml +1 -1
  12. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/LICENSE +0 -0
  13. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/MANIFEST.in +0 -0
  14. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/README.md +0 -0
  15. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/drawio-overrides/LICENSE +0 -0
  16. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/drawio-overrides/js/PostConfig.js +0 -0
  17. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/drawio-overrides/js/PreConfig.js +0 -0
  18. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/netbox_plugin_plan_drawing/api/__init__.py +0 -0
  19. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/netbox_plugin_plan_drawing/api/serializers.py +0 -0
  20. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/netbox_plugin_plan_drawing/api/urls.py +0 -0
  21. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/netbox_plugin_plan_drawing/api/views.py +0 -0
  22. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/netbox_plugin_plan_drawing/assets.py +0 -0
  23. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/netbox_plugin_plan_drawing/drawio.py +0 -0
  24. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/netbox_plugin_plan_drawing/filtersets.py +0 -0
  25. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/netbox_plugin_plan_drawing/forms.py +0 -0
  26. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/netbox_plugin_plan_drawing/management/__init__.py +0 -0
  27. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/netbox_plugin_plan_drawing/management/commands/__init__.py +0 -0
  28. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/netbox_plugin_plan_drawing/management/commands/plan_drawing_changelog_stats.py +0 -0
  29. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/netbox_plugin_plan_drawing/migrations/0001_initial.py +0 -0
  30. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/netbox_plugin_plan_drawing/migrations/__init__.py +0 -0
  31. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/netbox_plugin_plan_drawing/models.py +0 -0
  32. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/netbox_plugin_plan_drawing/navigation.py +0 -0
  33. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/netbox_plugin_plan_drawing/object_types.py +0 -0
  34. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/netbox_plugin_plan_drawing/search.py +0 -0
  35. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/netbox_plugin_plan_drawing/static/netbox_plugin_plan_drawing/drawio-netbox.js +0 -0
  36. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/netbox_plugin_plan_drawing/static/netbox_plugin_plan_drawing/editor.js +0 -0
  37. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/netbox_plugin_plan_drawing/tables.py +0 -0
  38. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/netbox_plugin_plan_drawing/template_content.py +0 -0
  39. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/netbox_plugin_plan_drawing/templates/netbox_plugin_plan_drawing/inc/object_drawings_panel.html +0 -0
  40. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/netbox_plugin_plan_drawing/templates/netbox_plugin_plan_drawing/plandrawing_draw.html +0 -0
  41. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/netbox_plugin_plan_drawing/templatetags/__init__.py +0 -0
  42. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/netbox_plugin_plan_drawing/templatetags/plan_drawing.py +0 -0
  43. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/netbox_plugin_plan_drawing/tests/__init__.py +0 -0
  44. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/netbox_plugin_plan_drawing/tests/test_api.py +0 -0
  45. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/netbox_plugin_plan_drawing/tests/test_drawio.py +0 -0
  46. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/netbox_plugin_plan_drawing/tests/test_models.py +0 -0
  47. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/netbox_plugin_plan_drawing/tests/test_object_types.py +0 -0
  48. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/netbox_plugin_plan_drawing/tests/utils.py +0 -0
  49. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/netbox_plugin_plan_drawing/urls.py +0 -0
  50. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/netbox_plugin_plan_drawing.egg-info/dependency_links.txt +0 -0
  51. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/netbox_plugin_plan_drawing.egg-info/requires.txt +0 -0
  52. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/netbox_plugin_plan_drawing.egg-info/top_level.txt +0 -0
  53. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/scripts/fetch_drawio.py +0 -0
  54. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/setup.cfg +0 -0
  55. {netbox_plugin_plan_drawing-1.0.0 → netbox_plugin_plan_drawing-1.1.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: netbox-plugin-plan-drawing
3
- Version: 1.0.0
3
+ Version: 1.1.0
4
4
  Summary: NetBox plugin for plan drawings (floor plans etc.) with draw.io, linking drawing elements to NetBox objects
5
5
  Author-email: Stefan Bethke <stb@lassitu.de>
6
6
  License-Expression: MIT
@@ -5,7 +5,7 @@ class PlanDrawingConfig(PluginConfig):
5
5
  name = 'netbox_plugin_plan_drawing'
6
6
  verbose_name = 'Plan Drawings'
7
7
  description = 'Plan drawings (floor plans etc.) edited with draw.io, with elements linked to NetBox objects'
8
- version = '1.0.0'
8
+ version = '1.1.0'
9
9
  author = 'Stefan Bethke'
10
10
  author_email = 'stb@lassitu.de'
11
11
  base_url = 'plan-drawing'
@@ -36,6 +36,37 @@
36
36
  background-color: color-mix(in srgb, var(--plan-drawing-select-color) 12%, transparent);
37
37
  }
38
38
 
39
+ /* The objects in a location or rack are only listed while it is selected, or while one of the
40
+ objects in it is selected from the list. */
41
+ .plan-drawing-element:not(.active, .plan-drawing-expanded) .plan-drawing-element-contents {
42
+ display: none;
43
+ }
44
+
45
+ .plan-drawing-contents {
46
+ padding-left: 0.75rem;
47
+ border-left: 1px solid var(--bs-border-color);
48
+ }
49
+
50
+ .plan-drawing-node {
51
+ padding: 0 0.25rem;
52
+ cursor: default;
53
+ border-radius: var(--bs-border-radius-sm);
54
+ }
55
+
56
+ /* Objects in a location or rack that are linked in the drawing as well: clicking them selects
57
+ their elements. */
58
+ .plan-drawing-node-linked {
59
+ cursor: pointer;
60
+ }
61
+
62
+ .plan-drawing-node-linked:hover {
63
+ background-color: var(--bs-tertiary-bg);
64
+ }
65
+
66
+ .plan-drawing-node.active {
67
+ background-color: color-mix(in srgb, var(--plan-drawing-select-color) 20%, transparent);
68
+ }
69
+
39
70
  /* Listing only the current page's elements: their page names are redundant. */
40
71
  .plan-drawing-this-page .plan-drawing-element-page {
41
72
  display: none;
@@ -3,7 +3,8 @@
3
3
  * the list of elements linked to NetBox objects: clicking either one highlights the other,
4
4
  * searching the list highlights all matching elements, and ?highlight=<type>:<id> in the page
5
5
  * URL highlights the elements of that object when the page opens. The list shows the elements
6
- * of the current page, or of all pages.
6
+ * of the current page, or of all pages. A selected location or rack lists the objects in it;
7
+ * clicking those that are linked in the drawing as well selects their elements.
7
8
  */
8
9
  (function () {
9
10
  'use strict';
@@ -52,8 +53,9 @@
52
53
  this.container = container;
53
54
  this.elements = elements;
54
55
  this.rows = Array.from(document.querySelectorAll('.plan-drawing-element'));
56
+ this.nodes = Array.from(document.querySelectorAll('.plan-drawing-node-linked'));
55
57
  this.highlights = [];
56
- this.hoverHighlight = null;
58
+ this.hoverHighlights = [];
57
59
  this.selected = [];
58
60
  this.matches = [];
59
61
  this.searchTerms = [];
@@ -245,13 +247,13 @@
245
247
  this.rows.forEach(function (row) {
246
248
  const index = parseInt(row.dataset.index, 10);
247
249
  row.addEventListener('click', function (evt) {
248
- if (evt.target.closest('a')) {
250
+ if (evt.target.closest('a, .plan-drawing-element-contents')) {
249
251
  return;
250
252
  }
251
253
  self.select([index], { switchPage: true, scrollDrawing: true });
252
254
  });
253
255
  row.addEventListener('keydown', function (evt) {
254
- if (evt.key === 'Enter' || evt.key === ' ') {
256
+ if (evt.target === row && (evt.key === 'Enter' || evt.key === ' ')) {
255
257
  evt.preventDefault();
256
258
  self.select([index], { switchPage: true, scrollDrawing: true });
257
259
  }
@@ -264,6 +266,40 @@
264
266
  });
265
267
  });
266
268
 
269
+ // The objects in a location or rack that are linked in the drawing as well select their
270
+ // elements, and keep the location or rack expanded.
271
+ this.nodes.forEach(function (node) {
272
+ const type = node.dataset.objectType;
273
+ const id = parseInt(node.dataset.objectId, 10);
274
+ const row = parseInt(node.closest('.plan-drawing-element').dataset.index, 10);
275
+ node.indices = [];
276
+ self.elements.forEach(function (element, index) {
277
+ if (element.object_type === type && element.object_id === id) {
278
+ node.indices.push(index);
279
+ }
280
+ });
281
+ const select = function () {
282
+ self.select(node.indices, { switchPage: true, scrollDrawing: true, expand: row });
283
+ };
284
+ node.addEventListener('click', function (evt) {
285
+ if (!evt.target.closest('a')) {
286
+ select();
287
+ }
288
+ });
289
+ node.addEventListener('keydown', function (evt) {
290
+ if (evt.target === node && (evt.key === 'Enter' || evt.key === ' ')) {
291
+ evt.preventDefault();
292
+ select();
293
+ }
294
+ });
295
+ node.addEventListener('mouseenter', function () {
296
+ self.hover(node.indices);
297
+ });
298
+ node.addEventListener('mouseleave', function () {
299
+ self.hover(row);
300
+ });
301
+ });
302
+
267
303
  const search = document.getElementById('plan-drawing-search');
268
304
  if (search) {
269
305
  search.addEventListener('input', function () {
@@ -332,7 +368,14 @@
332
368
  this.refreshHighlights();
333
369
 
334
370
  this.rows.forEach(function (row) {
335
- row.classList.toggle('active', indices.indexOf(parseInt(row.dataset.index, 10)) >= 0);
371
+ const index = parseInt(row.dataset.index, 10);
372
+ row.classList.toggle('active', indices.indexOf(index) >= 0);
373
+ row.classList.toggle('plan-drawing-expanded', index === options.expand);
374
+ });
375
+ this.nodes.forEach(function (node) {
376
+ node.classList.toggle('active', node.indices.some(function (index) {
377
+ return indices.indexOf(index) >= 0;
378
+ }));
336
379
  });
337
380
 
338
381
  if (options.scrollList && indices.length > 0) {
@@ -350,17 +393,20 @@
350
393
  }
351
394
  };
352
395
 
353
- PlanDrawingViewer.prototype.hover = function (index) {
354
- if (this.hoverHighlight) {
355
- this.hoverHighlight.destroy();
356
- this.hoverHighlight = null;
357
- }
358
- if (index != null) {
359
- const cell = this.cellForElement(index);
360
- if (cell) {
361
- this.hoverHighlight = this.addHighlight(cell, HOVER_COLOR, 3);
396
+ /** Highlights the elements with the given index or indices on the current page. */
397
+ PlanDrawingViewer.prototype.hover = function (indices) {
398
+ const self = this;
399
+ this.hoverHighlights.forEach(function (highlight) {
400
+ highlight.destroy();
401
+ });
402
+ this.hoverHighlights = [];
403
+ [].concat(indices == null ? [] : indices).forEach(function (index) {
404
+ const cell = self.cellForElement(index);
405
+ const highlight = cell && self.addHighlight(cell, HOVER_COLOR, 3);
406
+ if (highlight) {
407
+ self.hoverHighlights.push(highlight);
362
408
  }
363
- }
409
+ });
364
410
  };
365
411
 
366
412
  PlanDrawingViewer.prototype.matchesSearch = function (element) {
@@ -0,0 +1,21 @@
1
+ {% load i18n %}
2
+ <ul class="list-unstyled mb-0 plan-drawing-contents">
3
+ {% for node in nodes %}
4
+ <li>
5
+ {% if node.linked %}
6
+ <div class="d-flex justify-content-between gap-2 plan-drawing-node plan-drawing-node-linked" data-object-type="{{ node.object_type }}" data-object-id="{{ node.object_id }}" tabindex="0" title="{% trans 'Show in the drawing' %}">
7
+ {% else %}
8
+ <div class="d-flex justify-content-between gap-2 plan-drawing-node">
9
+ {% endif %}
10
+ <span>
11
+ <a href="{{ node.url }}">{{ node.display }}</a>
12
+ {% if node.linked %}<i class="mdi mdi-link-variant text-muted"></i>{% endif %}
13
+ </span>
14
+ <span class="text-muted text-nowrap">{{ node.object_type_label }}{% if node.position is not None %} &middot; U{{ node.position|floatformat:"-1" }}{% endif %}</span>
15
+ </div>
16
+ {% if node.children %}
17
+ {% include 'netbox_plugin_plan_drawing/inc/element_contents.html' with nodes=node.children %}
18
+ {% endif %}
19
+ </li>
20
+ {% endfor %}
21
+ </ul>
@@ -77,6 +77,15 @@
77
77
  <small class="text-muted">
78
78
  {{ element.object_type_label }}{% if element.label and element.label != element.display %} &middot; {{ element.label }}{% endif %}
79
79
  </small>
80
+ {% if element.contents is not None %}
81
+ <div class="small mt-1 plan-drawing-element-contents">
82
+ {% if element.contents %}
83
+ {% include 'netbox_plugin_plan_drawing/inc/element_contents.html' with nodes=element.contents %}
84
+ {% else %}
85
+ <span class="text-muted">{% trans "No objects in it." %}</span>
86
+ {% endif %}
87
+ </div>
88
+ {% endif %}
80
89
  </div>
81
90
  {% empty %}
82
91
  <div class="list-group-item text-muted">{% trans "No drawing elements are linked to NetBox objects." %}</div>
@@ -43,6 +43,30 @@ class PlanDrawingViewsTestCase(TestCase):
43
43
  self.assertEqual(response.context['elements'][0]['display'], str(self.rack))
44
44
  self.assertEqual(response.context['elements'][0]['url'], self.rack.get_absolute_url())
45
45
 
46
+ def test_detail_lists_location_and_rack_contents(self):
47
+ self.drawing.content = self.content(
48
+ linked_cell('b', 'Room', 'dcim.location', self.location.pk),
49
+ linked_cell('c', 'Device', 'dcim.device', self.device.pk),
50
+ )
51
+ self.drawing.save()
52
+
53
+ response = self.client.get(self.drawing.get_absolute_url())
54
+
55
+ contents = {e['object_type']: e['contents'] for e in response.context['elements']}
56
+ self.assertIsNone(contents['dcim.device'])
57
+ [rack] = contents['dcim.location']
58
+ self.assertEqual((rack['display'], rack['url'], rack['linked']), (str(self.rack), self.rack.get_absolute_url(), False))
59
+ [device] = rack['children']
60
+ self.assertEqual((device['object_type'], device['object_id'], device['linked']), ('dcim.device', self.device.pk, True))
61
+ self.assertContains(response, f'data-object-type="dcim.device" data-object-id="{self.device.pk}"')
62
+
63
+ def test_detail_lists_rack_contents(self):
64
+ response = self.client.get(self.drawing.get_absolute_url())
65
+
66
+ [device] = response.context['elements'][0]['contents']
67
+ self.assertEqual((device['display'], device['linked']), (str(self.device), False))
68
+ self.assertContains(response, f'href="{self.device.get_absolute_url()}"')
69
+
46
70
  def test_detail_lists_elements_in_page_order(self):
47
71
  self.drawing.content = drawing_file(
48
72
  ('z', 'Zeta', graph_model(linked_cell('a', 'Rack', 'dcim.rack', self.rack.pk))),
@@ -3,13 +3,16 @@ from collections import defaultdict
3
3
 
4
4
  from django.core.exceptions import ValidationError
5
5
  from django.db import transaction
6
+ from django.db.models import F, Q
6
7
  from django.http import JsonResponse
7
8
  from django.middleware.csrf import get_token
8
9
  from django.templatetags.static import static
10
+ from django.utils.text import capfirst
9
11
  from django.shortcuts import get_object_or_404
10
12
  from django.urls import NoReverseMatch, reverse
11
13
  from django.views import View
12
14
 
15
+ from dcim.models import Device, Location, PowerPanel, Rack
13
16
  from netbox.views import generic
14
17
  from utilities.permissions import get_permission_for_model
15
18
  from utilities.query import count_related
@@ -26,11 +29,82 @@ from .tables import PlanDrawingTable
26
29
  DRAWIO_INDEX = 'netbox_plugin_plan_drawing/drawio/index.html'
27
30
 
28
31
 
32
+ def location_and_rack_contents(objects, user):
33
+ """
34
+ Return the trees of objects in the given locations and racks, keyed by (model, pk) of the
35
+ location or rack. A location contains its child locations, racks, devices not mounted in a
36
+ rack, and power panels; a rack contains its devices, top to bottom; and a device contains the
37
+ devices installed in its device bays. Objects in the given list are marked as linked. Objects
38
+ the user may not view are left out.
39
+ """
40
+ linked = {(type(obj), obj.pk) for obj in objects}
41
+ location_ids = set()
42
+ for obj in objects:
43
+ if isinstance(obj, Location):
44
+ location_ids.update(obj.get_descendants(include_self=True).values_list('pk', flat=True))
45
+ linked_rack_ids = [obj.pk for obj in objects if isinstance(obj, Rack)]
46
+ if not location_ids and not linked_rack_ids:
47
+ return {}
48
+
49
+ locations = Location.objects.restrict(user, 'view').filter(pk__in=location_ids)
50
+ racks = list(Rack.objects.restrict(user, 'view').filter(Q(location__in=location_ids) | Q(pk__in=linked_rack_ids)))
51
+ devices = Device.objects.restrict(user, 'view').filter(
52
+ Q(rack__in=[rack.pk for rack in racks]) | Q(location__in=location_ids, rack__isnull=True),
53
+ ).select_related('parent_bay').order_by(F('position').desc(nulls_last=True), 'name')
54
+ panels = PowerPanel.objects.restrict(user, 'view').filter(location__in=location_ids)
55
+
56
+ nodes = {}
57
+
58
+ def add(obj, *parents, position=None):
59
+ """
60
+ Add the object below the first of the parents, given as (model, pk), that is in the tree.
61
+ """
62
+ node = nodes[(type(obj), obj.pk)] = {
63
+ 'display': str(obj),
64
+ 'url': obj.get_absolute_url(),
65
+ 'object_type': f'{obj._meta.app_label}.{obj._meta.model_name}',
66
+ 'object_id': obj.pk,
67
+ 'object_type_label': capfirst(obj._meta.verbose_name),
68
+ 'position': position,
69
+ 'linked': (type(obj), obj.pk) in linked,
70
+ 'children': [],
71
+ }
72
+ for parent in parents:
73
+ if parent in nodes:
74
+ nodes[parent]['children'].append(node)
75
+ break
76
+
77
+ # Locations are ordered by their path, so that parents are added before their children.
78
+ for location in locations:
79
+ add(location, (Location, location.parent_id))
80
+ for rack in racks:
81
+ add(rack, (Location, rack.location_id))
82
+ # Devices in device bays go below their parent device, once it has been added; devices whose
83
+ # parent is not in the tree go below their rack or location.
84
+ pending = list(devices)
85
+ while pending:
86
+ remaining = []
87
+ pending_ids = {device.pk for device in pending}
88
+ for device in pending:
89
+ parent_bay = getattr(device, 'parent_bay', None)
90
+ parent = (Device, parent_bay.device_id) if parent_bay else None
91
+ if parent and parent not in nodes and parent_bay.device_id in pending_ids:
92
+ remaining.append(device)
93
+ continue
94
+ add(device, parent, (Rack, device.rack_id), (Location, device.location_id), position=device.position)
95
+ if len(remaining) == len(pending):
96
+ break
97
+ pending = remaining
98
+ for panel in panels:
99
+ add(panel, (Location, panel.location_id))
100
+ return nodes
101
+
102
+
29
103
  def linked_elements(drawing, user):
30
104
  """
31
105
  Return the drawing's linked elements, with the NetBox objects they are linked to, for the
32
106
  drawing viewer, in the order of the drawing's pages. Objects the user may not view are shown
33
- without their name.
107
+ without their name. Locations and racks come with the objects in them.
34
108
  """
35
109
  elements = list(drawing.elements.select_related('assigned_object_type'))
36
110
 
@@ -44,6 +118,7 @@ def linked_elements(drawing, user):
44
118
  continue
45
119
  for obj in model.objects.restrict(user, 'view').filter(pk__in=ids):
46
120
  objects[(content_type.pk, obj.pk)] = obj
121
+ contents = location_and_rack_contents(list(objects.values()), user)
47
122
 
48
123
  result = []
49
124
  for element in elements:
@@ -60,6 +135,7 @@ def linked_elements(drawing, user):
60
135
  'object_id': element.assigned_object_id,
61
136
  'display': str(obj) if obj else None,
62
137
  'url': obj.get_absolute_url() if obj else None,
138
+ 'contents': contents[(model, obj.pk)]['children'] if isinstance(obj, (Location, Rack)) else None,
63
139
  })
64
140
 
65
141
  try:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: netbox-plugin-plan-drawing
3
- Version: 1.0.0
3
+ Version: 1.1.0
4
4
  Summary: NetBox plugin for plan drawings (floor plans etc.) with draw.io, linking drawing elements to NetBox objects
5
5
  Author-email: Stefan Bethke <stb@lassitu.de>
6
6
  License-Expression: MIT
@@ -39,6 +39,7 @@ netbox_plugin_plan_drawing/static/netbox_plugin_plan_drawing/plan-drawing.css
39
39
  netbox_plugin_plan_drawing/static/netbox_plugin_plan_drawing/viewer.js
40
40
  netbox_plugin_plan_drawing/templates/netbox_plugin_plan_drawing/plandrawing.html
41
41
  netbox_plugin_plan_drawing/templates/netbox_plugin_plan_drawing/plandrawing_draw.html
42
+ netbox_plugin_plan_drawing/templates/netbox_plugin_plan_drawing/inc/element_contents.html
42
43
  netbox_plugin_plan_drawing/templates/netbox_plugin_plan_drawing/inc/object_drawings_panel.html
43
44
  netbox_plugin_plan_drawing/templatetags/__init__.py
44
45
  netbox_plugin_plan_drawing/templatetags/plan_drawing.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "netbox-plugin-plan-drawing"
7
- version = "1.0.0"
7
+ version = "1.1.0"
8
8
  authors = [
9
9
  { name = "Stefan Bethke", email = "stb@lassitu.de" },
10
10
  ]