Compare commits

..
9 Commits
Author SHA1 Message Date
mattieserver becfaaa054 bump version (#397) 2023-10-13 13:08:18 +02:00
Mario bec301eb9d fix provider network error (#394) 2023-09-28 15:50:04 +02:00
Mario f28623b378 fix image save for old Python versions (#390) 2023-09-28 15:49:05 +02:00
Mario 16837047be fix device filter (#393) 2023-09-28 15:06:04 +02:00
Mario 987c7ef34c Fixes #377: Non-devices do not save coordinates (#387)
* corrected nodeId handling

* add models

* import models, migrate db

* correct migration deps

* load coordinates from db

* add serializers, correct imports

* add views and templates

* fix X-Y swap in help text

* add non-devices to coordinate group view
2023-09-27 09:13:17 +02:00
Mario dbe331afb8 add filters / housekeeping (#384) 2023-09-25 17:28:32 +02:00
Mario 86c0c55896 Closes #381: Filter by site group (#382)
* add Site Group filter

* revise query_params
2023-09-21 16:15:45 +02:00
mattieserver 78b8a5ae97 v3.7.0 (#371) 2023-08-31 13:48:01 +02:00
mattieserver 71e4807cb1 prepare for netbox v3.6.0 release (#366)
* prepare for netbox v3.6.0 release

* updated readme to reflect the version
2023-08-31 12:33:09 +02:00
30 changed files with 1800 additions and 196 deletions
+1
View File
@@ -50,6 +50,7 @@ python3 manage.py collectstatic --no-input
| netbox version | netbox-topology-views version | | netbox version | netbox-topology-views version |
| -------------- | ----------------------------- | | -------------- | ----------------------------- |
| >= 3.6.0 | >= v3.7.X |
| >= 3.5.0 | >= v3.4.X | | >= 3.5.0 | >= v3.4.X |
| >= 3.4.0 | >= v3.X.X | | >= 3.4.0 | >= v3.X.X |
| >= 3.3.0 | >= v3.0.0 | | >= 3.3.0 | >= v3.0.0 |
+1 -1
View File
@@ -5,7 +5,7 @@ class TopologyViewsConfig(PluginConfig):
name = "netbox_topology_views" name = "netbox_topology_views"
verbose_name = "Topology views" verbose_name = "Topology views"
description = "An plugin to render topology maps" description = "An plugin to render topology maps"
version = "3.6.2" version = "3.8.0"
author = "Mattijs Vanhaverbeke" author = "Mattijs Vanhaverbeke"
author_email = "author@example.com" author_email = "author@example.com"
base_url = "netbox_topology_views" base_url = "netbox_topology_views"
+16 -1
View File
@@ -2,7 +2,7 @@ from dcim.models import Device, DeviceRole
from rest_framework.serializers import ModelSerializer from rest_framework.serializers import ModelSerializer
from netbox.api.serializers import NetBoxModelSerializer from netbox.api.serializers import NetBoxModelSerializer
from netbox_topology_views.models import RoleImage, IndividualOptions, CoordinateGroup, Coordinate from netbox_topology_views.models import RoleImage, IndividualOptions, CoordinateGroup, Coordinate, CircuitCoordinate, PowerPanelCoordinate, PowerFeedCoordinate
class TopologyDummySerializer(ModelSerializer): class TopologyDummySerializer(ModelSerializer):
@@ -32,6 +32,21 @@ class CoordinateSerializer(NetBoxModelSerializer):
model = Coordinate model = Coordinate
fields = ("x", "y") fields = ("x", "y")
class CircuitCoordinateSerializer(NetBoxModelSerializer):
class Meta:
model = CircuitCoordinate
fields = ("x", "y")
class PowerPanelCoordinateSerializer(NetBoxModelSerializer):
class Meta:
model = PowerPanelCoordinate
fields = ("x", "y")
class PowerFeedCoordinateSerializer(NetBoxModelSerializer):
class Meta:
model = PowerFeedCoordinate
fields = ("x", "y")
class IndividualOptionsSerializer(NetBoxModelSerializer): class IndividualOptionsSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = IndividualOptions model = IndividualOptions
+34 -15
View File
@@ -1,4 +1,5 @@
from typing import Dict from typing import Dict
import sys
from circuits.models import Circuit from circuits.models import Circuit
from dcim.models import Device, DeviceRole, PowerFeed, PowerPanel from dcim.models import Device, DeviceRole, PowerFeed, PowerPanel
@@ -14,7 +15,8 @@ from netbox_topology_views.api.serializers import (
RoleImageSerializer, RoleImageSerializer,
TopologyDummySerializer, TopologyDummySerializer,
) )
from netbox_topology_views.models import RoleImage, IndividualOptions, CoordinateGroup, Coordinate import netbox_topology_views.models
from netbox_topology_views.models import RoleImage, IndividualOptions, CoordinateGroup, Coordinate, CircuitCoordinate, PowerPanelCoordinate, PowerFeedCoordinate
from netbox_topology_views.views import get_topology_data from netbox_topology_views.views import get_topology_data
from netbox_topology_views.utils import get_image_from_url, export_data_to_xml, get_query_settings from netbox_topology_views.utils import get_image_from_url, export_data_to_xml, get_query_settings
from netbox_topology_views.filters import DeviceFilterSet from netbox_topology_views.filters import DeviceFilterSet
@@ -41,20 +43,26 @@ class SaveCoordsViewSet(PermissionRequiredMixin, ReadOnlyModelViewSet):
if device_id.startswith("c"): if device_id.startswith("c"):
device_id = device_id.lstrip("c") device_id = device_id.lstrip("c")
actual_device = Circuit.objects.get(id=device_id) actual_device = Circuit.objects.get(id=device_id)
model_name = 'CircuitCoordinate'
elif device_id.startswith("p"): elif device_id.startswith("p"):
device_id = device_id.lstrip("p") device_id = device_id.lstrip("p")
actual_device = PowerPanel.objects.get(id=device_id) actual_device = PowerPanel.objects.get(id=device_id)
model_name = 'PowerPanelCoordinate'
elif device_id.startswith("f"): elif device_id.startswith("f"):
device_id = device_id.lstrip("f") device_id = device_id.lstrip("f")
actual_device = PowerFeed.objects.get(id=device_id) actual_device = PowerFeed.objects.get(id=device_id)
model_name = 'PowerFeedCoordinate'
elif device_id.isnumeric(): elif device_id.isnumeric():
actual_device = Device.objects.get(id=device_id) actual_device = Device.objects.get(id=device_id)
model_name = 'Coordinate'
if not actual_device: if not actual_device:
return Response({"status": "invalid node_id in body"}, status=400) return Response({"status": "invalid node_id in body"}, status=400)
model_class = getattr(netbox_topology_views.models, model_name)
if group_id is None or group_id == "default": if group_id is None or group_id == "default":
group_id = Coordinate.get_or_create_default_group(group_id) group_id = model_class.get_or_create_default_group(group_id)
if not group_id: if not group_id:
return Response( return Response(
{"status": "Error while creating default group."}, status=500 {"status": "Error while creating default group."}, status=500
@@ -66,12 +74,12 @@ class SaveCoordsViewSet(PermissionRequiredMixin, ReadOnlyModelViewSet):
# Hen-and-egg-problem. Thanks, Django! By default, Django updates records that # Hen-and-egg-problem. Thanks, Django! By default, Django updates records that
# already exist and inserts otherwise. This does not work with our # already exist and inserts otherwise. This does not work with our
# unique_together key if no pk is given. But: No record, no pk. # unique_together key if no pk is given. But: No record, no pk.
if not Coordinate.objects.filter(group=group, device=actual_device): if not model_class.objects.filter(group=group, device=actual_device):
# Unique group/device pair does not exist. Prepare new data set # Unique group/device pair does not exist. Prepare new data set
coords = Coordinate(group=group, device=actual_device, x=x_coord, y=y_coord) coords = model_class(group=group, device=actual_device, x=x_coord, y=y_coord)
else: else:
# Unique group/device pair already exists. Update data # Unique group/device pair already exists. Update data
coords = Coordinate(pk=Coordinate.objects.get(group=group, device=actual_device).pk, group=group, device=actual_device, x=x_coord, y=y_coord) coords = model_class(pk=model_class.objects.get(group=group, device=actual_device).pk, group=group, device=actual_device, x=x_coord, y=y_coord)
coords.save() coords.save()
except: except:
return Response( return Response(
@@ -90,7 +98,7 @@ class ExportTopoToXML(PermissionRequiredMixin, ViewSet):
self.filterset = DeviceFilterSet self.filterset = DeviceFilterSet
self.queryset = Device.objects.all().select_related( self.queryset = Device.objects.all().select_related(
"device_type", "device_role" "device_type", "role"
) )
self.queryset = self.filterset(request.GET, self.queryset).qs self.queryset = self.filterset(request.GET, self.queryset).qs
@@ -142,15 +150,26 @@ class SaveRoleImageViewSet(PermissionRequiredMixin, ReadOnlyModelViewSet):
{"status": "Missing or malformed request body"}, status=400 {"status": "Missing or malformed request body"}, status=400
) )
device_roles = { if sys.version_info >= (3,9,0):
k: v.removeprefix(settings.STATIC_URL) device_roles = {
for k, v in request.data.items() k: v.removeprefix(settings.STATIC_URL)
if k.isnumeric()} for k, v in request.data.items()
content_type_ids = { if k.isnumeric()}
k[2:]: v.removeprefix(settings.STATIC_URL) content_type_ids = {
for k, v in request.data.items() k[2:]: v.removeprefix(settings.STATIC_URL)
if k.startswith("ct") and k[2:].isnumeric() for k, v in request.data.items()
} if k.startswith("ct") and k[2:].isnumeric()
}
else:
device_roles = {}
for k, v in request.data.items():
if k.isdigit() and v.startswith(settings.STATIC_URL):
device_roles[k] = v[len(settings.STATIC_URL):]
content_type_ids = {}
for k, v in request.data.items():
if k.startswith("ct") and k[2:].isdigit() and v.startswith(settings.STATIC_URL):
content_type_ids[k[2:]] = v[len(settings.STATIC_URL):]
roles: Dict[int, DeviceRole] = DeviceRole.objects.in_bulk(device_roles.keys()) roles: Dict[int, DeviceRole] = DeviceRole.objects.in_bulk(device_roles.keys())
content_types: Dict[int, ContentType] = ContentType.objects.in_bulk( content_types: Dict[int, ContentType] = ContentType.objects.in_bulk(
+185 -8
View File
@@ -1,28 +1,56 @@
import django_filters import django_filters
from circuits.models import Circuit
from dcim.choices import DeviceStatusChoices from dcim.choices import DeviceStatusChoices
from dcim.models import Device, DeviceRole, Location, Rack, Region, Site from dcim.models import Device, DeviceRole, Location, Rack, Region, Site, SiteGroup, Manufacturer, DeviceType, Platform, PowerPanel, PowerFeed
from django.db.models import Q from django.db.models import Q
from extras.filtersets import LocalConfigContextFilterSet
from extras.models import ConfigTemplate
from netbox.filtersets import NetBoxModelFilterSet from netbox.filtersets import NetBoxModelFilterSet
from tenancy.filtersets import TenancyFilterSet from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet
from utilities.filters import TreeNodeMultipleChoiceFilter from utilities.filters import TreeNodeMultipleChoiceFilter, MultiValueCharFilter, MultiValueMACAddressFilter
from netbox_topology_views.models import CoordinateGroup, Coordinate from netbox_topology_views.models import CoordinateGroup, Coordinate, CircuitCoordinate, PowerPanelCoordinate, PowerFeedCoordinate
class DeviceFilterSet(TenancyFilterSet, NetBoxModelFilterSet): class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet, LocalConfigContextFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method="search", method="search",
label="Search", label="Search",
) )
device_role_id = django_filters.ModelMultipleChoiceFilter( manufacturer_id = django_filters.ModelMultipleChoiceFilter(
field_name="device_role_id", field_name='device_type__manufacturer',
queryset=Manufacturer.objects.all(),
label="Manufacturer (ID)",
)
manufacturer = django_filters.ModelMultipleChoiceFilter(
field_name='device_type__manufacturer__slug',
queryset=Manufacturer.objects.all(),
to_field_name='slug',
label="Manufacturer (slug)",
)
device_type_id = django_filters.ModelMultipleChoiceFilter(
queryset=DeviceType.objects.all(),
label="Device type (ID)",
)
role_id = django_filters.ModelMultipleChoiceFilter(
field_name="role_id",
queryset=DeviceRole.objects.all(), queryset=DeviceRole.objects.all(),
label="Role (ID)", label="Role (ID)",
) )
platform_id = django_filters.ModelMultipleChoiceFilter(
queryset=Platform.objects.all(),
label="Platform (ID)",
)
region_id = TreeNodeMultipleChoiceFilter( region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(), queryset=Region.objects.all(),
field_name="site__region", field_name="site__region",
lookup_expr="in", lookup_expr="in",
label="Region (ID)", label="Region (ID)",
) )
site_group_id = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name="site__group",
lookup_expr="in",
label="Site Group (ID)",
)
site_id = django_filters.ModelMultipleChoiceFilter( site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(), queryset=Site.objects.all(),
label="Site (ID)", label="Site (ID)",
@@ -42,10 +70,57 @@ class DeviceFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
choices=DeviceStatusChoices, choices=DeviceStatusChoices,
null_value=None, null_value=None,
) )
mac_address = MultiValueMACAddressFilter(
field_name='interfaces__mac_address',
label="MAC address",
)
serial = MultiValueCharFilter(
lookup_expr='iexact'
)
console_ports = django_filters.BooleanFilter(
method='_console_ports',
label="Has console ports",
)
console_server_ports = django_filters.BooleanFilter(
method='_console_server_ports',
label="Has console server ports",
)
power_ports = django_filters.BooleanFilter(
method='_power_ports',
label="Has power ports",
)
power_outlets = django_filters.BooleanFilter(
method='_power_outlets',
label="Has power outlets",
)
interfaces = django_filters.BooleanFilter(
method='_interfaces',
label="Has interfaces",
)
pass_through_ports = django_filters.BooleanFilter(
method='_pass_through_ports',
label="Has pass-through ports",
)
config_template_id = django_filters.ModelMultipleChoiceFilter(
queryset=ConfigTemplate.objects.all(),
label="Config template (ID)",
)
has_primary_ip = django_filters.BooleanFilter(
method='_has_primary_ip',
label="Has a primary IP",
)
has_oob_ip = django_filters.BooleanFilter(
method='_has_oob_ip',
label="Has an out-of-band IP",
)
virtual_chassis_member = django_filters.BooleanFilter(
method='_virtual_chassis_member',
label="Is a virtual chassis member",
)
class Meta: class Meta:
model = Device model = Device
fields = ["id", "name"] fields = ["id", "name", "asset_tag", "airflow"]
def search(self, queryset, name, value): def search(self, queryset, name, value):
"""Perform the filtered search.""" """Perform the filtered search."""
@@ -54,6 +129,108 @@ class DeviceFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
qs_filter = Q(name__icontains=value) qs_filter = Q(name__icontains=value)
return queryset.filter(qs_filter) return queryset.filter(qs_filter)
def _console_ports(self, queryset, name, value):
return queryset.exclude(consoleports__isnull=value)
def _console_server_ports(self, queryset, name, value):
return queryset.exclude(consoleserverports__isnull=value)
def _power_ports(self, queryset, name, value):
return queryset.exclude(powerports__isnull=value)
def _power_outlets(self, queryset, name, value):
return queryset.exclude(poweroutlets__isnull=value)
def _interfaces(self, queryset, name, value):
return queryset.exclude(interfaces__isnull=value)
def _pass_through_ports(self, queryset, name, value):
return queryset.exclude(
frontports__isnull=value,
rearports__isnull=value
)
def _has_primary_ip(self, queryset, name, value):
params = Q(primary_ip4__isnull=False) | Q(primary_ip6__isnull=False)
if value:
return queryset.filter(params)
return queryset.exclude(params)
def _has_oob_ip(self, queryset, name, value):
params = Q(oob_ip__isnull=False)
if value:
return queryset.filter(params)
return queryset.exclude(params)
def _virtual_chassis_member(self, queryset, name, value):
return queryset.exclude(virtual_chassis__isnull=value)
class CircuitCoordinatesFilterSet(NetBoxModelFilterSet):
group = django_filters.ModelMultipleChoiceFilter(
queryset = CoordinateGroup.objects.all(),
)
device = django_filters.ModelMultipleChoiceFilter(
queryset = Circuit.objects.all(),
)
class Meta:
model = CircuitCoordinate
fields = ['id', 'group', 'device', 'x', 'y']
def search(self, queryset, name, value):
"""Perform the filtered search."""
if not value.strip():
return queryset
return queryset.filter(
Q(group__name__icontains=value) |
Q(device__name__icontains=value)
)
class PowerPanelCoordinatesFilterSet(NetBoxModelFilterSet):
group = django_filters.ModelMultipleChoiceFilter(
queryset = CoordinateGroup.objects.all(),
)
device = django_filters.ModelMultipleChoiceFilter(
queryset = PowerPanel.objects.all(),
)
class Meta:
model = PowerPanelCoordinate
fields = ['id', 'group', 'device', 'x', 'y']
def search(self, queryset, name, value):
"""Perform the filtered search."""
if not value.strip():
return queryset
return queryset.filter(
Q(group__name__icontains=value) |
Q(device__name__icontains=value)
)
class PowerFeedCoordinatesFilterSet(NetBoxModelFilterSet):
group = django_filters.ModelMultipleChoiceFilter(
queryset = CoordinateGroup.objects.all(),
)
device = django_filters.ModelMultipleChoiceFilter(
queryset = PowerFeed.objects.all(),
)
class Meta:
model = PowerFeedCoordinate
fields = ['id', 'group', 'device', 'x', 'y']
def search(self, queryset, name, value):
"""Perform the filtered search."""
if not value.strip():
return queryset
return queryset.filter(
Q(group__name__icontains=value) |
Q(device__name__icontains=value)
)
class CoordinatesFilterSet(NetBoxModelFilterSet): class CoordinatesFilterSet(NetBoxModelFilterSet):
group = django_filters.ModelMultipleChoiceFilter( group = django_filters.ModelMultipleChoiceFilter(
queryset = CoordinateGroup.objects.all(), queryset = CoordinateGroup.objects.all(),
+367 -140
View File
@@ -5,148 +5,258 @@ from django.conf import settings
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from dcim.models import Device, Site, Region, DeviceRole, Location, Rack from circuits.models import Circuit
from dcim.models import Device, Site, SiteGroup, Region, DeviceRole, Location, Rack, Manufacturer, DeviceType, Platform, PowerPanel, PowerFeed
from django import forms from django import forms
from dcim.choices import DeviceStatusChoices from dcim.choices import DeviceStatusChoices, DeviceAirflowChoices
from tenancy.models import TenantGroup, Tenant from tenancy.models import TenantGroup, Tenant
from tenancy.forms import TenancyFilterForm from extras.models import ConfigTemplate
from extras.forms import LocalConfigContextFilterForm
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
from django.conf import settings from django.conf import settings
from netbox.forms import NetBoxModelFilterSetForm, NetBoxModelForm, NetBoxModelImportForm from netbox.forms import NetBoxModelFilterSetForm, NetBoxModelForm, NetBoxModelImportForm
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, add_blank_choice
from utilities.forms.fields import ( from utilities.forms.fields import (
TagFilterField, TagFilterField,
DynamicModelMultipleChoiceField DynamicModelMultipleChoiceField
) )
from netbox_topology_views.models import IndividualOptions, CoordinateGroup, Coordinate from netbox_topology_views.models import IndividualOptions, CoordinateGroup, Coordinate, CircuitCoordinate, PowerPanelCoordinate, PowerFeedCoordinate
class DeviceFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class DeviceFilterForm(
LocalConfigContextFilterForm,
TenancyFilterForm,
ContactModelFilterForm,
NetBoxModelFilterSetForm
):
model = Device model = Device
fieldsets = ( fieldsets = (
( (None, ('q', 'filter_id', 'tag')),
None, (_('Options'), (
( 'group', 'save_coords', 'show_unconnected', 'show_cables', 'show_logical_connections',
"q", 'show_single_cable_logical_conns', 'show_neighbors', 'show_circuit', 'show_power', 'show_wireless',
"filter_id", )),
), (_('Device'), ('id',)),
), (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
( (_('Operation'), ('status', 'role_id', 'airflow', 'serial', 'asset_tag', 'mac_address')),
None, (_('Hardware'), ('manufacturer_id', 'device_type_id', 'platform_id')),
( (_('Tenant'), ('tenant_group_id', 'tenant_id')),
"group", (_('Contacts'), ('contact', 'contact_role', 'contact_group')),
"save_coords", (_('Components'), (
"show_unconnected", 'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports',
"show_cables", )),
"show_logical_connections", (_('Miscellaneous'), (
"show_single_cable_logical_conns", 'has_primary_ip', 'has_oob_ip', 'virtual_chassis_member', 'config_template_id', 'local_context_data',
"show_neighbors", )),
"show_circuit",
"show_power",
"show_wireless",
),
),
(
None,
(
"tenant_group_id",
"tenant_id",
),
),
(None, ("region_id", "site_id", "location_id", "rack_id")),
(
None,
(
"device_role_id",
"id",
"status",
),
),
(None, ("tag",)),
) )
group = forms.ModelChoiceField( group = forms.ModelChoiceField(
queryset=CoordinateGroup.objects.all(), queryset=CoordinateGroup.objects.all(),
required=False, required=False,
label=_("Coordinate group"), label=_('Coordinate group'),
)
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), required=False, label=_("Region")
)
device_role_id = DynamicModelMultipleChoiceField(
queryset=DeviceRole.objects.all(), required=False, label=_("Device Role")
) )
id = DynamicModelMultipleChoiceField( id = DynamicModelMultipleChoiceField(
queryset=Device.objects.all(), queryset=Device.objects.all(),
required=False, required=False,
label=_("Device"), label=_('Device'),
query_params={ query_params={
"location_id": "$location_id", 'location_id': '$location_id',
"region_id": "$region_id", 'region_id': '$region_id',
"site_id": "$site_id", 'site_group_id': '$site_group_id',
"role_id": "$device_role_id", 'site_id': '$site_id',
'role_id': '$role_id',
'contact': '$contact',
'contact_role': '$contact_role',
'contact_group': '$contact_group',
}, },
) )
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region')
)
site_group_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site Group'),
)
site_id = DynamicModelMultipleChoiceField( site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
required=False, required=False,
query_params={ query_params={
"region_id": "$region_id", 'region_id': '$region_id',
'group_id': '$site_group_id',
}, },
label=_("Site"), label=_('Site'),
) )
location_id = DynamicModelMultipleChoiceField( location_id = DynamicModelMultipleChoiceField(
queryset=Location.objects.all(), queryset=Location.objects.all(),
required=False, required=False,
query_params={ query_params={
"region_id": "$region_id", 'region_id': '$region_id',
"site_id": "$site_id", 'site_group_id': '$site_group_id',
'site_id': '$site_id',
}, },
label=_("Location"), label=_('Location'),
) )
rack_id = DynamicModelMultipleChoiceField( rack_id = DynamicModelMultipleChoiceField(
queryset=Rack.objects.all(), queryset=Rack.objects.all(),
required=False, required=False,
query_params={ query_params={
"region_id": "$region_id", 'region_id': '$region_id',
"site_id": "$site_id", 'site_group_id': '$site_group_id',
"location_id": "$location_id", 'site_id': '$site_id',
'location_id': '$location_id',
}, },
label=_("Rack"), label=_('Rack'),
) )
status = forms.MultipleChoiceField( status = forms.MultipleChoiceField(
choices=DeviceStatusChoices, required=False, label=_("Device Status") choices=DeviceStatusChoices,
required=False,
label=_('Device Status')
) )
role_id = DynamicModelMultipleChoiceField(
queryset=DeviceRole.objects.all(),
required=False,
label=_('Role')
)
airflow = forms.MultipleChoiceField(
label=_('Airflow'),
choices=add_blank_choice(DeviceAirflowChoices),
required=False
)
serial = forms.CharField(
label=_('Serial'),
required=False
)
asset_tag = forms.CharField(
label=_('Asset tag'),
required=False
)
mac_address = forms.CharField(
required=False,
label=_('MAC address')
)
manufacturer_id = DynamicModelMultipleChoiceField(
queryset=Manufacturer.objects.all(),
required=False,
label=_('Manufacturer')
)
device_type_id = DynamicModelMultipleChoiceField(
queryset=DeviceType.objects.all(),
required=False,
query_params={
'manufacturer_id': '$manufacturer_id'
},
label=_('Model')
)
platform_id = DynamicModelMultipleChoiceField(
queryset=Platform.objects.all(),
required=False,
null_option='None',
label=_('Platform')
)
console_ports = forms.NullBooleanField(
required=False,
label=_('Has console ports'),
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
console_server_ports = forms.NullBooleanField(
required=False,
label=_('Has console server ports'),
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
power_ports = forms.NullBooleanField(
required=False,
label=_('Has power ports'),
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
power_outlets = forms.NullBooleanField(
required=False,
label=_('Has power outlets'),
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
interfaces = forms.NullBooleanField(
required=False,
label=_('Has interfaces'),
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
pass_through_ports = forms.NullBooleanField(
required=False,
label=_('Has pass-through ports'),
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
config_template_id = DynamicModelMultipleChoiceField(
queryset=ConfigTemplate.objects.all(),
required=False,
label=_('Config template')
)
has_primary_ip = forms.NullBooleanField(
required=False,
label=_('Has a primary IP'),
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
has_oob_ip = forms.NullBooleanField(
required=False,
label='Has an OOB IP',
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
virtual_chassis_member = forms.NullBooleanField(
required=False,
label=_('Virtual chassis member'),
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
tag = TagFilterField(model) tag = TagFilterField(model)
# options # options
save_coords = forms.BooleanField( save_coords = forms.BooleanField(
label=_("Save Coordinates"), label=_('Save Coordinates'),
required=False, required=False,
disabled=(not settings.PLUGINS_CONFIG["netbox_topology_views"]["allow_coordinates_saving"] or settings.PLUGINS_CONFIG["netbox_topology_views"]["always_save_coordinates"]), disabled=(not settings.PLUGINS_CONFIG['netbox_topology_views']['allow_coordinates_saving'] or settings.PLUGINS_CONFIG['netbox_topology_views']['always_save_coordinates']),
initial=(settings.PLUGINS_CONFIG["netbox_topology_views"]["always_save_coordinates"]) initial=(settings.PLUGINS_CONFIG['netbox_topology_views']['always_save_coordinates'])
) )
show_unconnected = forms.BooleanField( show_unconnected = forms.BooleanField(
label=_("Show Unconnected"), required=False, initial=False label=_('Show Unconnected'), required=False, initial=False
) )
show_cables = forms.BooleanField( show_cables = forms.BooleanField(
label =_("Show Cables"), required=False, initial=False label =_('Show Cables'), required=False, initial=False
) )
show_logical_connections = forms.BooleanField( show_logical_connections = forms.BooleanField(
label =_("Show Logical Connections"), required=False, initial=False label =_('Show Logical Connections'), required=False, initial=False
) )
show_single_cable_logical_conns = forms.BooleanField( show_single_cable_logical_conns = forms.BooleanField(
label =_("Show redundant Cable and Logical Connection"), required=False, initial=False label =_('Show redundant Cable and Logical Connection'), required=False, initial=False
) )
show_neighbors = forms.BooleanField( show_neighbors = forms.BooleanField(
label =_("Show Neighbors"), required=False, initial=False label =_('Show Neighbors'), required=False, initial=False
) )
show_circuit = forms.BooleanField( show_circuit = forms.BooleanField(
label=_("Show Circuit Terminations"), required=False, initial=False label=_('Show Circuit Terminations'), required=False, initial=False
) )
show_power = forms.BooleanField( show_power = forms.BooleanField(
label=_("Show Power Feeds"), required=False, initial=False label=_('Show Power Feeds'), required=False, initial=False
) )
show_wireless = forms.BooleanField( show_wireless = forms.BooleanField(
label =_("Show Wireless Links"), required=False, initial=False label =_('Show Wireless Links'), required=False, initial=False
) )
class CoordinateGroupsForm(NetBoxModelForm): class CoordinateGroupsForm(NetBoxModelForm):
@@ -163,6 +273,33 @@ class CoordinateGroupsImportForm(NetBoxModelImportForm):
model = CoordinateGroup model = CoordinateGroup
fields = ('name', 'description') fields = ('name', 'description')
class CircuitCoordinatesForm(NetBoxModelForm):
fieldsets = (
('CircuitCoordinate', ('group', 'device', 'x', 'y')),
)
class Meta:
model = CircuitCoordinate
fields = ('group', 'device', 'x', 'y')
class PowerPanelCoordinatesForm(NetBoxModelForm):
fieldsets = (
('PowerPanel', ('group', 'device', 'x', 'y')),
)
class Meta:
model = PowerPanelCoordinate
fields = ('group', 'device', 'x', 'y')
class PowerFeedCoordinatesForm(NetBoxModelForm):
fieldsets = (
('PowerFeedCoordinate', ('group', 'device', 'x', 'y')),
)
class Meta:
model = PowerFeedCoordinate
fields = ('group', 'device', 'x', 'y')
class CoordinatesForm(NetBoxModelForm): class CoordinatesForm(NetBoxModelForm):
fieldsets = ( fieldsets = (
('Coordinate', ('group', 'device', 'x', 'y')), ('Coordinate', ('group', 'device', 'x', 'y')),
@@ -172,11 +309,101 @@ class CoordinatesForm(NetBoxModelForm):
model = Coordinate model = Coordinate
fields = ('group', 'device', 'x', 'y') fields = ('group', 'device', 'x', 'y')
class CircuitCoordinatesImportForm(NetBoxModelImportForm):
class Meta:
model = CircuitCoordinate
fields = ('group', 'device', 'x', 'y')
class PowerPanelCoordinatesImportForm(NetBoxModelImportForm):
class Meta:
model = PowerPanelCoordinate
fields = ('group', 'device', 'x', 'y')
class PowerFeedCoordinatesImportForm(NetBoxModelImportForm):
class Meta:
model = PowerFeedCoordinate
fields = ('group', 'device', 'x', 'y')
class CoordinatesImportForm(NetBoxModelImportForm): class CoordinatesImportForm(NetBoxModelImportForm):
class Meta: class Meta:
model = Coordinate model = Coordinate
fields = ('group', 'device', 'x', 'y') fields = ('group', 'device', 'x', 'y')
class CircuitCoordinatesFilterForm(NetBoxModelFilterSetForm):
model = CircuitCoordinate
fieldsets = (
(None, ('q', 'filter_id')),
('CircuitCoordinates', ('group', 'device', 'x', 'y'))
)
group = forms.ModelMultipleChoiceField(
queryset=CoordinateGroup.objects.all(),
required=False
)
device = DynamicModelMultipleChoiceField(
queryset=Circuit.objects.all(),
required=False
)
x = forms.IntegerField(
required=False
)
y = forms.IntegerField(
required=False
)
class PowerPanelCoordinatesFilterForm(NetBoxModelFilterSetForm):
model = PowerPanelCoordinate
fieldsets = (
(None, ('q', 'filter_id')),
('PowerPanelCoordinates', ('group', 'device', 'x', 'y'))
)
group = forms.ModelMultipleChoiceField(
queryset=CoordinateGroup.objects.all(),
required=False
)
device = DynamicModelMultipleChoiceField(
queryset=PowerPanel.objects.all(),
required=False
)
x = forms.IntegerField(
required=False
)
y = forms.IntegerField(
required=False
)
class PowerFeedCoordinatesFilterForm(NetBoxModelFilterSetForm):
model = Coordinate
fieldsets = (
(None, ('q', 'filter_id')),
('PowerFeedCoordinates', ('group', 'device', 'x', 'y'))
)
group = forms.ModelMultipleChoiceField(
queryset=CoordinateGroup.objects.all(),
required=False
)
device = DynamicModelMultipleChoiceField(
queryset=PowerFeed.objects.all(),
required=False
)
x = forms.IntegerField(
required=False
)
y = forms.IntegerField(
required=False
)
class CoordinatesFilterForm(NetBoxModelFilterSetForm): class CoordinatesFilterForm(NetBoxModelFilterSetForm):
model = Coordinate model = Coordinate
fieldsets = ( fieldsets = (
@@ -207,20 +434,20 @@ class IndividualOptionsForm(NetBoxModelForm):
( (
None, None,
( (
"user_id", 'user_id',
"ignore_cable_type", 'ignore_cable_type',
"preselected_device_roles", 'preselected_device_roles',
"preselected_tags", 'preselected_tags',
"save_coords", 'save_coords',
"show_unconnected", 'show_unconnected',
"show_cables", 'show_cables',
"show_logical_connections", 'show_logical_connections',
"show_single_cable_logical_conns", 'show_single_cable_logical_conns',
"show_neighbors", 'show_neighbors',
"show_circuit", 'show_circuit',
"show_power", 'show_power',
"show_wireless", 'show_wireless',
"draw_default_layout", 'draw_default_layout',
), ),
), ),
) )
@@ -228,105 +455,105 @@ class IndividualOptionsForm(NetBoxModelForm):
user_id = forms.CharField(widget=forms.HiddenInput()) user_id = forms.CharField(widget=forms.HiddenInput())
ignore_cable_type = forms.MultipleChoiceField( ignore_cable_type = forms.MultipleChoiceField(
label=_("Ignore Termination Types"), label=_('Ignore Termination Types'),
required=False, required=False,
choices=IndividualOptions.CHOICES, choices=IndividualOptions.CHOICES,
help_text=_("Choose Termination Types that you want to be ignored. " help_text=_('Choose Termination Types that you want to be ignored. '
"If any ignored Termination Type is part of a connection, the " 'If any ignored Termination Type is part of a connection, the '
"cable is not displayed.") 'cable is not displayed.')
) )
preselected_device_roles = DynamicModelMultipleChoiceField( preselected_device_roles = DynamicModelMultipleChoiceField(
label=_("Preselected Device Role"), label=_('Preselected Device Role'),
queryset=DeviceRole.objects.all(), queryset=DeviceRole.objects.all(),
required=False, required=False,
help_text=_("Select Device Roles that you want to have " help_text=_('Select Device Roles that you want to have '
"preselected in the filter tab.") 'preselected in the filter tab.')
) )
preselected_tags = forms.ModelMultipleChoiceField( preselected_tags = forms.ModelMultipleChoiceField(
label=_("Preselected Tags"), label=_('Preselected Tags'),
queryset=Device.tags.all(), queryset=Device.tags.all(),
required=False, required=False,
help_text=_("Select Tags that you want to have " help_text=_('Select Tags that you want to have '
"preselected in the filter tab.") 'preselected in the filter tab.')
) )
save_coords = forms.BooleanField( save_coords = forms.BooleanField(
label=_("Save Coordinates"), label=_('Save Coordinates'),
required=False, required=False,
initial=False, initial=False,
help_text=_("Coordinates of nodes will be saved if dragged to a different " help_text=_('Coordinates of nodes will be saved if dragged to a different '
"position. This option depends on parameters set in the config file. " 'position. This option depends on parameters set in the config file. '
"It has no effect if 'allow_coordinates_saving' has not been set or " 'It has no effect if \'allow_coordinates_saving\' has not been set or '
" 'always_save_coordinates' has been set.") ' \'always_save_coordinates\' has been set.')
) )
show_unconnected = forms.BooleanField( show_unconnected = forms.BooleanField(
label=_("Show Unconnected"), label=_('Show Unconnected'),
required=False, required=False,
initial=False, initial=False,
help_text=_("Draws devices that have no connections or for which no " help_text=_('Draws devices that have no connections or for which no '
"connection is displayed. This option depends on other parameters " 'connection is displayed. This option depends on other parameters '
"like 'Show Cables' and 'Show Logical Connections'.") 'like \'Show Cables\' and \'Show Logical Connections\'.')
) )
show_cables = forms.BooleanField( show_cables = forms.BooleanField(
label =_("Show Cables"), label =_('Show Cables'),
required=False, required=False,
initial=False, initial=False,
help_text=_("Displays connections between interfaces that are connected " help_text=_('Displays connections between interfaces that are connected '
"with one or more cables. These connections are displayed as solid " 'with one or more cables. These connections are displayed as solid '
"lines in the color of the cable.") 'lines in the color of the cable.')
) )
show_logical_connections = forms.BooleanField( show_logical_connections = forms.BooleanField(
label =_("Show Logical Connections"), label =_('Show Logical Connections'),
required=False, required=False,
initial=False, initial=False,
help_text=_("Displays connections between devices that are not " help_text=_('Displays connections between devices that are not '
"directly connected (e.g. via patch panels). These connections " 'directly connected (e.g. via patch panels). These connections '
"are displayed as yellow dotted lines.") 'are displayed as yellow dotted lines.')
) )
show_single_cable_logical_conns = forms.BooleanField( show_single_cable_logical_conns = forms.BooleanField(
label = ("Show redundant Cable and Logical Connection"), label = ('Show redundant Cable and Logical Connection'),
required = False, required = False,
initial=False, initial=False,
help_text=_("Shows a logical connection (in addition to a cable), " help_text=_('Shows a logical connection (in addition to a cable), '
"even if a cable is directly connected. Leaving this option " 'even if a cable is directly connected. Leaving this option '
"disabled prevents that redundant display. This option only " 'disabled prevents that redundant display. This option only '
"has an effect if 'Show Logical Connections' is activated.") 'has an effect if \'Show Logical Connections\' is activated.')
) )
show_neighbors = forms.BooleanField( show_neighbors = forms.BooleanField(
label =_("Show Neighbors"), label =_('Show Neighbors'),
required=False, required=False,
initial=False, initial=False,
help_text=_("Adds neighbors to the filter result set automatically. " help_text=_('Adds neighbors to the filter result set automatically. '
"Link peers will be added if 'Show Cables' is ticked, far-end " 'Link peers will be added if \'Show Cables\' is ticked, far-end '
"terminations will be added if 'Show Logical Connections' is ticked.") 'terminations will be added if \'Show Logical Connections\' is ticked.')
) )
show_circuit = forms.BooleanField( show_circuit = forms.BooleanField(
label=_("Show Circuit Terminations"), label=_('Show Circuit Terminations'),
required=False, required=False,
initial=False, initial=False,
help_text=_("Displays connections between circuit terminations. " help_text=_('Displays connections between circuit terminations. '
"These connections are displayed as blue dashed lines.") 'These connections are displayed as blue dashed lines.')
) )
show_power = forms.BooleanField( show_power = forms.BooleanField(
label=_("Show Power Feeds"), label=_('Show Power Feeds'),
required=False, required=False,
initial=False, initial=False,
help_text=_("Displays connections between power outlets and power " help_text=_('Displays connections between power outlets and power '
"ports. These connections are displayed as solid lines in the " 'ports. These connections are displayed as solid lines in the '
"color of the cable. This option depends on 'Show Cables'.") 'color of the cable. This option depends on \'Show Cables\'.')
) )
show_wireless = forms.BooleanField( show_wireless = forms.BooleanField(
label =_("Show Wireless Links"), label =_('Show Wireless Links'),
required=False, required=False,
initial=False, initial=False,
help_text=_("Displays wireless connections. These connections are " help_text=_('Displays wireless connections. These connections are '
"displayed as blue dotted lines.") 'displayed as blue dotted lines.')
) )
draw_default_layout = forms.BooleanField( draw_default_layout = forms.BooleanField(
label = ("Draw Default Layout"), label = ('Draw Default Layout'),
required=False, required=False,
initial=False, initial=False,
help_text=_("Enable this option if you want to draw the topology on " help_text=_('Enable this option if you want to draw the topology on '
"the initial load (when you go to the topology plugin page).") 'the initial load (when you go to the topology plugin page).')
) )
class Meta: class Meta:
@@ -0,0 +1,70 @@
# Generated by Django 4.1.8 on 2023-09-25 21:18
from django.db import migrations, models
import django.db.models.deletion
import taggit.managers
import utilities.json
class Migration(migrations.Migration):
dependencies = [
('netbox_topology_views', '0005_individualoptions_save_coords'),
]
operations = [
migrations.CreateModel(
name='PowerPanelCoordinate',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
('x', models.IntegerField()),
('y', models.IntegerField()),
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dcim.powerpanel')),
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='netbox_topology_views.coordinategroup')),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
],
options={
'ordering': ['group', 'device'],
'unique_together': {('device', 'group')},
},
),
migrations.CreateModel(
name='PowerFeedCoordinate',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
('x', models.IntegerField()),
('y', models.IntegerField()),
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dcim.powerfeed')),
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='netbox_topology_views.coordinategroup')),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
],
options={
'ordering': ['group', 'device'],
'unique_together': {('device', 'group')},
},
),
migrations.CreateModel(
name='CircuitCoordinate',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
('x', models.IntegerField()),
('y', models.IntegerField()),
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='circuits.circuit')),
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='netbox_topology_views.coordinategroup')),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
],
options={
'ordering': ['group', 'device'],
'unique_together': {('device', 'group')},
},
),
]
+151 -3
View File
@@ -1,7 +1,8 @@
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
from dcim.models import Device, DeviceRole from circuits.models import Circuit
from dcim.models import Device, DeviceRole, PowerPanel, PowerFeed
from extras.models import Tag from extras.models import Tag
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
@@ -130,11 +131,11 @@ class Coordinate(NetBoxModel):
x = models.IntegerField( x = models.IntegerField(
help_text='X-coordinate of the device (horizontal) on the canvas. ' help_text='X-coordinate of the device (horizontal) on the canvas. '
'Smaller values correspond to a position further up on the monitor.', 'Smaller values correspond to a position further to the left on the monitor.',
) )
y = models.IntegerField( y = models.IntegerField(
help_text='Y-coordinate of the device (vertical) on the canvas. ' help_text='Y-coordinate of the device (vertical) on the canvas. '
'Smaller values correspond to a position further to the left on the monitor.', 'Smaller values correspond to a position further up on the monitor.',
) )
def get_or_create_default_group(group_id): def get_or_create_default_group(group_id):
@@ -168,6 +169,153 @@ class Coordinate(NetBoxModel):
def get_absolute_url(self): def get_absolute_url(self):
return reverse('plugins:netbox_topology_views:coordinate', args=[self.pk]) return reverse('plugins:netbox_topology_views:coordinate', args=[self.pk])
class CircuitCoordinate(NetBoxModel):
"""
Coordinates are being used to place devices in a topology view onto a certain
position. Devices belong to one or more coordinate groups. They have to
be unique together.
"""
device = models.ForeignKey(Circuit, on_delete=models.CASCADE)
group = models.ForeignKey(CoordinateGroup, on_delete=models.CASCADE)
x = models.IntegerField(
help_text='X-coordinate of the device (horizontal) on the canvas. '
'Smaller values correspond to a position further to the left on the monitor.',
)
y = models.IntegerField(
help_text='Y-coordinate of the device (vertical) on the canvas. '
'Smaller values correspond to a position further up on the monitor.',
)
def get_or_create_default_group(group_id):
# Default group named "default" must always exist in order to make sure
# that coordinate values can be stored even if no coordinate group has been
# selected. The default group will be added automatically if it does not exist.
try:
if CoordinateGroup.objects.filter(name="default"):
group = CoordinateGroup.objects.get(name="default")
group_id = group.pk
else:
group = CoordinateGroup(
name="default",
description="Automatically generated default group. If you delete "
"this group, all default coordinates are gone for good but "
"the group itself will be re-created."
)
group.save()
group_id = group.pk
except:
return False
return group_id
class Meta:
ordering = ['group', 'device']
unique_together = ('device', 'group')
def __str__(self):
return f'{self.x};{self.y}'
def get_absolute_url(self):
return reverse('plugins:netbox_topology_views:circuitcoordinate', args=[self.pk])
class PowerPanelCoordinate(NetBoxModel):
"""
Coordinates are being used to place devices in a topology view onto a certain
position. Devices belong to one or more coordinate groups. They have to
be unique together.
"""
device = models.ForeignKey(PowerPanel, on_delete=models.CASCADE)
group = models.ForeignKey(CoordinateGroup, on_delete=models.CASCADE)
x = models.IntegerField(
help_text='X-coordinate of the device (horizontal) on the canvas. '
'Smaller values correspond to a position further to the left on the monitor.',
)
y = models.IntegerField(
help_text='Y-coordinate of the device (vertical) on the canvas. '
'Smaller values correspond to a position further up on the monitor.',
)
def get_or_create_default_group(group_id):
# Default group named "default" must always exist in order to make sure
# that coordinate values can be stored even if no coordinate group has been
# selected. The default group will be added automatically if it does not exist.
try:
if CoordinateGroup.objects.filter(name="default"):
group = CoordinateGroup.objects.get(name="default")
group_id = group.pk
else:
group = CoordinateGroup(
name="default",
description="Automatically generated default group. If you delete "
"this group, all default coordinates are gone for good but "
"the group itself will be re-created."
)
group.save()
group_id = group.pk
except:
return False
return group_id
class Meta:
ordering = ['group', 'device']
unique_together = ('device', 'group')
def __str__(self):
return f'{self.x};{self.y}'
def get_absolute_url(self):
return reverse('plugins:netbox_topology_views:powerpanelcoordinate', args=[self.pk])
class PowerFeedCoordinate(NetBoxModel):
"""
Coordinates are being used to place devices in a topology view onto a certain
position. Devices belong to one or more coordinate groups. They have to
be unique together.
"""
device = models.ForeignKey(PowerFeed, on_delete=models.CASCADE)
group = models.ForeignKey(CoordinateGroup, on_delete=models.CASCADE)
x = models.IntegerField(
help_text='X-coordinate of the device (horizontal) on the canvas. '
'Smaller values correspond to a position further to the left on the monitor.',
)
y = models.IntegerField(
help_text='Y-coordinate of the device (vertical) on the canvas. '
'Smaller values correspond to a position further up on the monitor.',
)
def get_or_create_default_group(group_id):
# Default group named "default" must always exist in order to make sure
# that coordinate values can be stored even if no coordinate group has been
# selected. The default group will be added automatically if it does not exist.
try:
if CoordinateGroup.objects.filter(name="default"):
group = CoordinateGroup.objects.get(name="default")
group_id = group.pk
else:
group = CoordinateGroup(
name="default",
description="Automatically generated default group. If you delete "
"this group, all default coordinates are gone for good but "
"the group itself will be re-created."
)
group.save()
group_id = group.pk
except:
return False
return group_id
class Meta:
ordering = ['group', 'device']
unique_together = ('device', 'group')
def __str__(self):
return f'{self.x};{self.y}'
def get_absolute_url(self):
return reverse('plugins:netbox_topology_views:powerfeedcoordinate', args=[self.pk])
class IndividualOptions(NetBoxModel): class IndividualOptions(NetBoxModel):
CHOICES = ( CHOICES = (
('interface', 'interface'), ('interface', 'interface'),
+59 -1
View File
@@ -18,6 +18,57 @@ coordinategroup_buttons = (
) )
) )
circuitcoordinate_buttons = (
PluginMenuButton(
link='plugins:netbox_topology_views:circuitcoordinate_add',
title='Add',
icon_class='mdi mdi-plus-thick',
color=ButtonColorChoices.GREEN,
permissions=['netbox_topology_views.add_coordinate']
),
PluginMenuButton(
link='plugins:netbox_topology_views:circuitcoordinate_import',
title='Import',
icon_class='mdi mdi-upload',
color=ButtonColorChoices.CYAN,
permissions=['netbox_topology_views.add_coordinate']
)
)
powerpanelcoordinate_buttons = (
PluginMenuButton(
link='plugins:netbox_topology_views:powerpanelcoordinate_add',
title='Add',
icon_class='mdi mdi-plus-thick',
color=ButtonColorChoices.GREEN,
permissions=['netbox_topology_views.add_coordinate']
),
PluginMenuButton(
link='plugins:netbox_topology_views:powerpanelcoordinate_import',
title='Import',
icon_class='mdi mdi-upload',
color=ButtonColorChoices.CYAN,
permissions=['netbox_topology_views.add_coordinate']
)
)
powerfeedcoordinate_buttons = (
PluginMenuButton(
link='plugins:netbox_topology_views:powerfeedcoordinate_add',
title='Add',
icon_class='mdi mdi-plus-thick',
color=ButtonColorChoices.GREEN,
permissions=['netbox_topology_views.add_coordinate']
),
PluginMenuButton(
link='plugins:netbox_topology_views:powerfeedcoordinate_import',
title='Import',
icon_class='mdi mdi-upload',
color=ButtonColorChoices.CYAN,
permissions=['netbox_topology_views.add_coordinate']
)
)
coordinate_buttons = ( coordinate_buttons = (
PluginMenuButton( PluginMenuButton(
link='plugins:netbox_topology_views:coordinate_add', link='plugins:netbox_topology_views:coordinate_add',
@@ -42,8 +93,15 @@ menu = PluginMenu(
('TOPOLOGY', ('TOPOLOGY',
( (
PluginMenuItem(link="plugins:netbox_topology_views:home", link_text="Topology", permissions=["dcim.view_site", "dcim.view_device"]), PluginMenuItem(link="plugins:netbox_topology_views:home", link_text="Topology", permissions=["dcim.view_site", "dcim.view_device"]),
),
),
('COORDINATES',
(
PluginMenuItem(link="plugins:netbox_topology_views:coordinategroup_list", link_text="Coordinate Groups", buttons=coordinategroup_buttons, permissions=['netbox_topology_views.view_coordinategroup']), PluginMenuItem(link="plugins:netbox_topology_views:coordinategroup_list", link_text="Coordinate Groups", buttons=coordinategroup_buttons, permissions=['netbox_topology_views.view_coordinategroup']),
PluginMenuItem(link="plugins:netbox_topology_views:coordinate_list", link_text="Coordinates", buttons=coordinate_buttons, permissions=['netbox_topology_views.view_coordinate']), PluginMenuItem(link="plugins:netbox_topology_views:coordinate_list", link_text="Device Coordinates", buttons=coordinate_buttons, permissions=['netbox_topology_views.view_coordinate']),
PluginMenuItem(link="plugins:netbox_topology_views:powerfeedcoordinate_list", link_text="Power Feed Coords", buttons=powerfeedcoordinate_buttons, permissions=['netbox_topology_views.view_coordinate']),
PluginMenuItem(link="plugins:netbox_topology_views:powerpanelcoordinate_list", link_text="Power Panel Coords", buttons=powerpanelcoordinate_buttons, permissions=['netbox_topology_views.view_coordinate']),
PluginMenuItem(link="plugins:netbox_topology_views:circuitcoordinate_list", link_text="Circuit Coordinates", buttons=circuitcoordinate_buttons, permissions=['netbox_topology_views.view_coordinate']),
), ),
), ),
('PREFERENCES', ('PREFERENCES',
File diff suppressed because one or more lines are too long
+20 -3
View File
@@ -79,7 +79,26 @@ const coordSaveCheckbox = document.querySelector('#id_save_coords')
Promise.allSettled( Promise.allSettled(
Object.entries(graph.getPositions(params.nodes)).map( Object.entries(graph.getPositions(params.nodes)).map(
async ([nodeId, nodePosition]) => { async ([nodeId, nodePosition]) => {
window.nodes.update({id: parseInt(nodeId), physics: false, x: nodePosition.x, y: nodePosition.y}); if(!isNaN(parseInt(nodeId))) {
nodeKey = parseInt(nodeId);
}
else {
nodeKey = nodeId;
}
try {
window.nodes.update({id: nodeKey, physics: false, x: nodePosition.x, y: nodePosition.y});
}
catch (e) {
console.log([
'Error while executing window.nodes.update()',
'nodeId: ' + nodeId,
'nodeKey: ' + nodeKey,
'x: ' + nodePosition.x,
'y: ' + nodePosition.y
]);
console.log(e);
}
const res = await fetch( const res = await fetch(
'/' + basePath + 'api/plugins/netbox_topology_views/save-coords/save_coords/', '/' + basePath + 'api/plugins/netbox_topology_views/save-coords/save_coords/',
{ {
@@ -97,8 +116,6 @@ const coordSaveCheckbox = document.querySelector('#id_save_coords')
}) })
} }
) )
console.log(nodeId, res.status, res.statusText)
} }
) )
) )
+472 -3
View File
@@ -1,12 +1,12 @@
{ {
"name": "netbox_topology_views", "name": "netbox_topology_views",
"version": "3.6.0", "version": "3.7.0",
"lockfileVersion": 3, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "netbox_topology_views", "name": "netbox_topology_views",
"version": "3.6.0", "version": "3.7.0",
"dependencies": { "dependencies": {
"vis-data": "^7.1.6", "vis-data": "^7.1.6",
"vis-network": "^9.1.6", "vis-network": "^9.1.6",
@@ -102,6 +102,7 @@
"dependencies": { "dependencies": {
"anymatch": "~3.1.2", "anymatch": "~3.1.2",
"braces": "~3.0.2", "braces": "~3.0.2",
"fsevents": "~2.3.2",
"glob-parent": "~5.1.2", "glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0", "is-binary-path": "~2.1.0",
"is-glob": "~4.0.1", "is-glob": "~4.0.1",
@@ -404,6 +405,29 @@
"integrity": "sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==", "integrity": "sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": {
"@esbuild/linux-loong64": "0.14.54",
"esbuild-android-64": "0.14.54",
"esbuild-android-arm64": "0.14.54",
"esbuild-darwin-64": "0.14.54",
"esbuild-darwin-arm64": "0.14.54",
"esbuild-freebsd-64": "0.14.54",
"esbuild-freebsd-arm64": "0.14.54",
"esbuild-linux-32": "0.14.54",
"esbuild-linux-64": "0.14.54",
"esbuild-linux-arm": "0.14.54",
"esbuild-linux-arm64": "0.14.54",
"esbuild-linux-mips64le": "0.14.54",
"esbuild-linux-ppc64le": "0.14.54",
"esbuild-linux-riscv64": "0.14.54",
"esbuild-linux-s390x": "0.14.54",
"esbuild-netbsd-64": "0.14.54",
"esbuild-openbsd-64": "0.14.54",
"esbuild-sunos-64": "0.14.54",
"esbuild-windows-32": "0.14.54",
"esbuild-windows-64": "0.14.54",
"esbuild-windows-arm64": "0.14.54"
},
"bin": { "bin": {
"esbuild": "bin/esbuild" "esbuild": "bin/esbuild"
}, },
@@ -787,5 +811,450 @@
"component-emitter": "^1.3.0" "component-emitter": "^1.3.0"
} }
} }
},
"dependencies": {
"@egjs/hammerjs": {
"version": "2.0.17",
"resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz",
"integrity": "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==",
"requires": {
"@types/hammerjs": "^2.0.36"
}
},
"@esbuild/linux-loong64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz",
"integrity": "sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==",
"dev": true,
"optional": true
},
"@types/hammerjs": {
"version": "2.0.41",
"resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.41.tgz",
"integrity": "sha512-ewXv/ceBaJprikMcxCmWU1FKyMAQ2X7a9Gtmzw8fcg2kIePI1crERDM818W+XYrxqdBBOdlf2rm137bU+BltCA=="
},
"anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dev": true,
"requires": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
}
},
"binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
"dev": true
},
"braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"dev": true,
"requires": {
"fill-range": "^7.0.1"
}
},
"chokidar": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
"dev": true,
"requires": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"fsevents": "~2.3.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
}
},
"component-emitter": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
},
"esbuild": {
"version": "0.12.29",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.29.tgz",
"integrity": "sha512-w/XuoBCSwepyiZtIRsKsetiLDUVGPVw1E/R3VTFSecIy8UR7Cq3SOtwKHJMFoVqqVG36aGkzh4e8BvpO1Fdc7g==",
"dev": true
},
"esbuild-android-64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz",
"integrity": "sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==",
"dev": true,
"optional": true
},
"esbuild-android-arm64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz",
"integrity": "sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==",
"dev": true,
"optional": true
},
"esbuild-darwin-64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz",
"integrity": "sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==",
"dev": true,
"optional": true
},
"esbuild-darwin-arm64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz",
"integrity": "sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==",
"dev": true,
"optional": true
},
"esbuild-freebsd-64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz",
"integrity": "sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==",
"dev": true,
"optional": true
},
"esbuild-freebsd-arm64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz",
"integrity": "sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==",
"dev": true,
"optional": true
},
"esbuild-linux-32": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz",
"integrity": "sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==",
"dev": true,
"optional": true
},
"esbuild-linux-64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz",
"integrity": "sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==",
"dev": true,
"optional": true
},
"esbuild-linux-arm": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz",
"integrity": "sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==",
"dev": true,
"optional": true
},
"esbuild-linux-arm64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz",
"integrity": "sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==",
"dev": true,
"optional": true
},
"esbuild-linux-mips64le": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz",
"integrity": "sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==",
"dev": true,
"optional": true
},
"esbuild-linux-ppc64le": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz",
"integrity": "sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==",
"dev": true,
"optional": true
},
"esbuild-linux-riscv64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz",
"integrity": "sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==",
"dev": true,
"optional": true
},
"esbuild-linux-s390x": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz",
"integrity": "sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==",
"dev": true,
"optional": true
},
"esbuild-netbsd-64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz",
"integrity": "sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==",
"dev": true,
"optional": true
},
"esbuild-openbsd-64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz",
"integrity": "sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==",
"dev": true,
"optional": true
},
"esbuild-sass-plugin": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/esbuild-sass-plugin/-/esbuild-sass-plugin-1.8.2.tgz",
"integrity": "sha512-ZBjONsRSpmzMKvxSNohnNXCNaBBVlYLqYhyZtN8leGGkJktCvVMKC6g9V5TWIemk1SEaW2XK+YFxz3Whw3+YYw==",
"dev": true,
"requires": {
"esbuild": "^0.14.5",
"picomatch": "^2.3.0",
"resolve": "^1.20.0",
"sass": "^1.45.0"
},
"dependencies": {
"esbuild": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.54.tgz",
"integrity": "sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==",
"dev": true,
"requires": {
"@esbuild/linux-loong64": "0.14.54",
"esbuild-android-64": "0.14.54",
"esbuild-android-arm64": "0.14.54",
"esbuild-darwin-64": "0.14.54",
"esbuild-darwin-arm64": "0.14.54",
"esbuild-freebsd-64": "0.14.54",
"esbuild-freebsd-arm64": "0.14.54",
"esbuild-linux-32": "0.14.54",
"esbuild-linux-64": "0.14.54",
"esbuild-linux-arm": "0.14.54",
"esbuild-linux-arm64": "0.14.54",
"esbuild-linux-mips64le": "0.14.54",
"esbuild-linux-ppc64le": "0.14.54",
"esbuild-linux-riscv64": "0.14.54",
"esbuild-linux-s390x": "0.14.54",
"esbuild-netbsd-64": "0.14.54",
"esbuild-openbsd-64": "0.14.54",
"esbuild-sunos-64": "0.14.54",
"esbuild-windows-32": "0.14.54",
"esbuild-windows-64": "0.14.54",
"esbuild-windows-arm64": "0.14.54"
}
}
}
},
"esbuild-sunos-64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz",
"integrity": "sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==",
"dev": true,
"optional": true
},
"esbuild-windows-32": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz",
"integrity": "sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==",
"dev": true,
"optional": true
},
"esbuild-windows-64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz",
"integrity": "sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==",
"dev": true,
"optional": true
},
"esbuild-windows-arm64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz",
"integrity": "sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==",
"dev": true,
"optional": true
},
"fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"dev": true,
"requires": {
"to-regex-range": "^5.0.1"
}
},
"fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"optional": true
},
"function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"dev": true
},
"glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"requires": {
"is-glob": "^4.0.1"
}
},
"has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"dev": true,
"requires": {
"function-bind": "^1.1.1"
}
},
"immutable": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz",
"integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==",
"dev": true
},
"is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"requires": {
"binary-extensions": "^2.0.0"
}
},
"is-core-module": {
"version": "2.12.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz",
"integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==",
"dev": true,
"requires": {
"has": "^1.0.3"
}
},
"is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true
},
"is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"requires": {
"is-extglob": "^2.1.1"
}
},
"is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true
},
"keycharm": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/keycharm/-/keycharm-0.4.0.tgz",
"integrity": "sha512-TyQTtsabOVv3MeOpR92sIKk/br9wxS+zGj4BG7CR8YbK4jM3tyIBaF0zhzeBUMx36/Q/iQLOKKOT+3jOQtemRQ=="
},
"normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true
},
"path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
},
"picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true
},
"readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"requires": {
"picomatch": "^2.2.1"
}
},
"resolve": {
"version": "1.22.2",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz",
"integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==",
"dev": true,
"requires": {
"is-core-module": "^2.11.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
}
},
"sass": {
"version": "1.63.6",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.63.6.tgz",
"integrity": "sha512-MJuxGMHzaOW7ipp+1KdELtqKbfAWbH7OLIdoSMnVe3EXPMTmxTmlaZDCTsgIpPCs3w99lLo9/zDKkOrJuT5byw==",
"dev": true,
"requires": {
"chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0",
"source-map-js": ">=0.6.2 <2.0.0"
}
},
"source-map-js": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
"dev": true
},
"supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true
},
"timsort": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz",
"integrity": "sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A=="
},
"to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"requires": {
"is-number": "^7.0.0"
}
},
"uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
},
"vis-data": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/vis-data/-/vis-data-7.1.6.tgz",
"integrity": "sha512-lG7LJdkawlKSXsdcEkxe/zRDyW29a4r7N7PMwxCPxK12/QIdqxJwcMxwjVj9ozdisRhP5TyWDHZwsgjmj0g6Dg==",
"requires": {}
},
"vis-network": {
"version": "9.1.6",
"resolved": "https://registry.npmjs.org/vis-network/-/vis-network-9.1.6.tgz",
"integrity": "sha512-Eiwx1JleAsUqfy4pzcsFngCVlCEdjAtRPB/OwCV7PHBm+o2jtE4IZPcPITAEGUlxvL4Fdw7/lZsfD32dL+IL6g==",
"requires": {}
},
"vis-util": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/vis-util/-/vis-util-5.0.3.tgz",
"integrity": "sha512-Wf9STUcFrDzK4/Zr7B6epW2Kvm3ORNWF+WiwEz2dpf5RdWkLUXFSbLcuB88n1W6tCdFwVN+v3V4/Xmn9PeL39g==",
"requires": {}
}
} }
} }
@@ -1,7 +1,7 @@
{ {
"private": true, "private": true,
"name": "netbox_topology_views", "name": "netbox_topology_views",
"version": "3.6.2", "version": "3.8.0",
"scripts": { "scripts": {
"bundle": "node bundle.js", "bundle": "node bundle.js",
"bundle:styles": "node bundle.js --styles", "bundle:styles": "node bundle.js --styles",
+43 -2
View File
@@ -1,7 +1,7 @@
import django_tables2 as tables import django_tables2 as tables
from netbox.tables import NetBoxTable, ChoiceFieldColumn from netbox.tables import NetBoxTable, ChoiceFieldColumn
from netbox_topology_views.models import CoordinateGroup, Coordinate from netbox_topology_views.models import CoordinateGroup, Coordinate, CircuitCoordinate, PowerPanelCoordinate, PowerFeedCoordinate
class CoordinateGroupListTable(NetBoxTable): class CoordinateGroupListTable(NetBoxTable):
name = tables.Column( name = tables.Column(
@@ -14,6 +14,48 @@ class CoordinateGroupListTable(NetBoxTable):
fields = ('pk', 'id', 'name', 'description', 'devices') fields = ('pk', 'id', 'name', 'description', 'devices')
default_columns = ('name', 'description', 'devices') default_columns = ('name', 'description', 'devices')
class CircuitCoordinateListTable(NetBoxTable):
group = tables.Column(
linkify=True
)
device = tables.Column(
linkify=True
)
class Meta(NetBoxTable.Meta):
model = CircuitCoordinate
fields = ('pk', 'id', 'group', 'device', 'x', 'y')
default_columns = ('id', 'group', 'device', 'x', 'y')
class PowerPanelCoordinateListTable(NetBoxTable):
group = tables.Column(
linkify=True
)
device = tables.Column(
linkify=True
)
class Meta(NetBoxTable.Meta):
model = PowerPanelCoordinate
fields = ('pk', 'id', 'group', 'device', 'x', 'y')
default_columns = ('id', 'group', 'device', 'x', 'y')
class PowerFeedCoordinateListTable(NetBoxTable):
group = tables.Column(
linkify=True
)
device = tables.Column(
linkify=True
)
class Meta(NetBoxTable.Meta):
model = PowerFeedCoordinate
fields = ('pk', 'id', 'group', 'device', 'x', 'y')
default_columns = ('id', 'group', 'device', 'x', 'y')
class CoordinateListTable(NetBoxTable): class CoordinateListTable(NetBoxTable):
group = tables.Column( group = tables.Column(
linkify=True linkify=True
@@ -27,4 +69,3 @@ class CoordinateListTable(NetBoxTable):
model = Coordinate model = Coordinate
fields = ('pk', 'id', 'group', 'device', 'x', 'y') fields = ('pk', 'id', 'group', 'device', 'x', 'y')
default_columns = ('id', 'group', 'device', 'x', 'y') default_columns = ('id', 'group', 'device', 'x', 'y')
@@ -0,0 +1,42 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% block title %}Topology Views Coordinates{% endblock title %}
{% block content %}
<div class="row mb-3">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
Coordinates
</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">Group</th>
<td>{{ object.group|linkify }}</td>
</tr>
<tr>
<th scope="row">Circuit</th>
<td>{{ object.device|linkify }}</td>
</tr>
<tr>
<th scope="row">X-Coordinate</th>
<td>{{ object.x }}</td>
</tr>
<tr>
<th scope="row">Y-Coordinate</th>
<td>{{ object.y }}</td>
</tr>
</table>
</div>
</div>
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
{% include 'inc/panels/custom_fields.html' %}
{% plugin_right_page object %}
</div>
</div>
{% endblock content %}
@@ -0,0 +1,3 @@
{% extends 'generic/object_edit.html' %}
{% block title %}Add new Circuit Coordinates{% endblock title %}
@@ -0,0 +1,3 @@
{% extends 'generic/object_edit.html' %}
{% block title %}Edit Circuit Coordinates{% endblock title %}
@@ -0,0 +1,3 @@
{% extends 'generic/object_list.html' %}
{% block title %}Circuit Coordinates{% endblock title %}
@@ -32,10 +32,34 @@
{% plugin_right_page object %} {% plugin_right_page object %}
</div> </div>
</div> </div>
<div class="row"> <div class="row mb-3">
<div class="col col-md-12"> <div class="col col-md-12">
<div class="card"> <div class="card">
<h5 class="card-header">Coordinates</h5> <h5 class="card-header">Circuit Coordinates</h5>
<div class="card-body table-responsive">
{% render_table circuitcoordinates_table %}
</div>
</div>
</div>
<div class="col col-md-12">
<div class="card">
<h5 class="card-header">Power Panel Coordinates</h5>
<div class="card-body table-responsive">
{% render_table powerpanelcoordinates_table %}
</div>
</div>
</div>
<div class="col col-md-12">
<div class="card">
<h5 class="card-header">Power Feed Coordinates</h5>
<div class="card-body table-responsive">
{% render_table powerfeedcoordinates_table %}
</div>
</div>
</div>
<div class="col col-md-12">
<div class="card">
<h5 class="card-header">Device Coordinates</h5>
<div class="card-body table-responsive"> <div class="card-body table-responsive">
{% render_table coordinates_table %} {% render_table coordinates_table %}
</div> </div>
@@ -0,0 +1,42 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% block title %}Topology Views Coordinates{% endblock title %}
{% block content %}
<div class="row mb-3">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
Coordinates
</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">Group</th>
<td>{{ object.group|linkify }}</td>
</tr>
<tr>
<th scope="row">Power Feed</th>
<td>{{ object.device|linkify }}</td>
</tr>
<tr>
<th scope="row">X-Coordinate</th>
<td>{{ object.x }}</td>
</tr>
<tr>
<th scope="row">Y-Coordinate</th>
<td>{{ object.y }}</td>
</tr>
</table>
</div>
</div>
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
{% include 'inc/panels/custom_fields.html' %}
{% plugin_right_page object %}
</div>
</div>
{% endblock content %}
@@ -0,0 +1,3 @@
{% extends 'generic/object_edit.html' %}
{% block title %}Add new Power Feed Coordinates{% endblock title %}
@@ -0,0 +1,3 @@
{% extends 'generic/object_edit.html' %}
{% block title %}Edit Power Feed Coordinates{% endblock title %}
@@ -0,0 +1,3 @@
{% extends 'generic/object_list.html' %}
{% block title %}Power Feed Coordinates{% endblock title %}
@@ -0,0 +1,42 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% block title %}Topology Views Coordinates{% endblock title %}
{% block content %}
<div class="row mb-3">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
Coordinates
</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">Group</th>
<td>{{ object.group|linkify }}</td>
</tr>
<tr>
<th scope="row">Power Panel</th>
<td>{{ object.device|linkify }}</td>
</tr>
<tr>
<th scope="row">X-Coordinate</th>
<td>{{ object.x }}</td>
</tr>
<tr>
<th scope="row">Y-Coordinate</th>
<td>{{ object.y }}</td>
</tr>
</table>
</div>
</div>
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
{% include 'inc/panels/custom_fields.html' %}
{% plugin_right_page object %}
</div>
</div>
{% endblock content %}
@@ -0,0 +1,3 @@
{% extends 'generic/object_edit.html' %}
{% block title %}Add new Power Panel Coordinates{% endblock title %}
@@ -0,0 +1,3 @@
{% extends 'generic/object_edit.html' %}
{% block title %}Edit Power Panel Coordinates{% endblock title %}
@@ -0,0 +1,3 @@
{% extends 'generic/object_list.html' %}
{% block title %}Power Panel Coordinates{% endblock title %}
+27
View File
@@ -20,6 +20,33 @@ urlpatterns = (
path("coordinate-groups/<int:pk>/delete/", views.CoordinateGroupDeleteView.as_view(), name="coordinategroup_delete"), path("coordinate-groups/<int:pk>/delete/", views.CoordinateGroupDeleteView.as_view(), name="coordinategroup_delete"),
path("coordinate-groups/<int:pk>/changelog/", ObjectChangeLogView.as_view(), name="coordinategroup_changelog", kwargs={'model': models.CoordinateGroup}), path("coordinate-groups/<int:pk>/changelog/", ObjectChangeLogView.as_view(), name="coordinategroup_changelog", kwargs={'model': models.CoordinateGroup}),
# Circuit Coordinate
path("circuitcoordinate/", views.CircuitCoordinateListView.as_view(), name="circuitcoordinate_list"),
path("circuitcoordinate/add/", views.CircuitCoordinateAddView.as_view(), name="circuitcoordinate_add"),
path('circuitcoordinate/import/', views.CircuitCoordinateBulkImportView.as_view(), name='circuitcoordinate_import'),
path("circuitcoordinate/<int:pk>/", views.CircuitCoordinateView.as_view(), name="circuitcoordinate"),
path("circuitcoordinate/<int:pk>/edit/", views.CircuitCoordinateEditView.as_view(), name="circuitcoordinate_edit"),
path("circuitcoordinate/<int:pk>/delete/", views.CircuitCoordinateDeleteView.as_view(), name="circuitcoordinate_delete"),
path("circuitcoordinate/<int:pk>/changelog/", ObjectChangeLogView.as_view(), name="circuitcoordinate_changelog", kwargs={'model': models.CircuitCoordinate}),
# Power Panel Coordinate
path("powerpanelcoordinate/", views.PowerPanelCoordinateListView.as_view(), name="powerpanelcoordinate_list"),
path("powerpanelcoordinate/add/", views.PowerPanelCoordinateAddView.as_view(), name="powerpanelcoordinate_add"),
path('powerpanelcoordinate/import/', views.PowerPanelCoordinateBulkImportView.as_view(), name='powerpanelcoordinate_import'),
path("powerpanelcoordinate/<int:pk>/", views.PowerPanelCoordinateView.as_view(), name="powerpanelcoordinate"),
path("powerpanelcoordinate/<int:pk>/edit/", views.PowerPanelCoordinateEditView.as_view(), name="powerpanelcoordinate_edit"),
path("powerpanelcoordinate/<int:pk>/delete/", views.PowerPanelCoordinateDeleteView.as_view(), name="powerpanelcoordinate_delete"),
path("powerpanelcoordinate/<int:pk>/changelog/", ObjectChangeLogView.as_view(), name="powerpanelcoordinate_changelog", kwargs={'model': models.PowerPanelCoordinate}),
# Power Feed Coordinate
path("powerfeedcoordinate/", views.PowerFeedCoordinateListView.as_view(), name="powerfeedcoordinate_list"),
path("powerfeedcoordinate/add/", views.PowerFeedCoordinateAddView.as_view(), name="powerfeedcoordinate_add"),
path('powerfeedcoordinate/import/', views.PowerFeedCoordinateBulkImportView.as_view(), name='powerfeedcoordinate_import'),
path("powerfeedcoordinate/<int:pk>/", views.PowerFeedCoordinateView.as_view(), name="powerfeedcoordinate"),
path("powerfeedcoordinate/<int:pk>/edit/", views.PowerFeedCoordinateEditView.as_view(), name="powerfeedcoordinate_edit"),
path("powerfeedcoordinate/<int:pk>/delete/", views.PowerFeedCoordinateDeleteView.as_view(), name="powerfeedcoordinate_delete"),
path("powerfeedcoordinate/<int:pk>/changelog/", ObjectChangeLogView.as_view(), name="powerfeedcoordinate_changelog", kwargs={'model': models.PowerFeedCoordinate}),
# Coordinate # Coordinate
path("coordinate/", views.CoordinateListView.as_view(), name="coordinate_list"), path("coordinate/", views.CoordinateListView.as_view(), name="coordinate_list"),
path("coordinate/add/", views.CoordinateAddView.as_view(), name="coordinate_add"), path("coordinate/add/", views.CoordinateAddView.as_view(), name="coordinate_add"),
+172 -14
View File
@@ -5,7 +5,7 @@ import time
from itertools import chain from itertools import chain
from utilities.htmx import is_htmx from utilities.htmx import is_htmx
from circuits.models import Circuit, CircuitTermination from circuits.models import Circuit, CircuitTermination, ProviderNetwork
from dcim.models import ( from dcim.models import (
Cable, Cable,
CableTermination, CableTermination,
@@ -37,20 +37,36 @@ from netbox.views.generic import (
ObjectChangeLogView, ObjectChangeLogView,
BulkImportView BulkImportView
) )
from netbox_topology_views.filters import DeviceFilterSet, CoordinatesFilterSet, CircuitCoordinatesFilterSet, PowerPanelCoordinatesFilterSet, PowerFeedCoordinatesFilterSet
from netbox_topology_views.filters import DeviceFilterSet, CoordinatesFilterSet
from netbox_topology_views.forms import ( from netbox_topology_views.forms import (
DeviceFilterForm, DeviceFilterForm,
IndividualOptionsForm, IndividualOptionsForm,
CoordinateGroupsForm, CoordinateGroupsForm,
CircuitCoordinatesForm,
CircuitCoordinatesFilterForm,
CircuitCoordinatesImportForm,
PowerPanelCoordinatesForm,
PowerPanelCoordinatesFilterForm,
PowerPanelCoordinatesImportForm,
PowerFeedCoordinatesForm,
PowerFeedCoordinatesFilterForm,
PowerFeedCoordinatesImportForm,
CoordinatesForm, CoordinatesForm,
CoordinatesFilterForm, CoordinatesFilterForm,
CoordinateGroupsImportForm, CoordinateGroupsImportForm,
CoordinatesImportForm CoordinatesImportForm
) )
from netbox_topology_views.models import RoleImage, CoordinateGroup, Coordinate, IndividualOptions import netbox_topology_views.models
from netbox_topology_views.tables import CoordinateGroupListTable, CoordinateListTable from netbox_topology_views.models import (
RoleImage,
IndividualOptions,
CoordinateGroup,
Coordinate,
CircuitCoordinate,
PowerPanelCoordinate,
PowerFeedCoordinate,
)
from netbox_topology_views.tables import CoordinateGroupListTable, CoordinateListTable, CircuitCoordinateListTable, PowerPanelCoordinateListTable, PowerFeedCoordinateListTable
from netbox_topology_views.utils import ( from netbox_topology_views.utils import (
CONF_IMAGE_DIR, CONF_IMAGE_DIR,
find_image_url, find_image_url,
@@ -67,7 +83,7 @@ from netbox_topology_views.utils import (
def get_image_for_entity(entity: Union[Device, Circuit, PowerPanel, PowerFeed]): def get_image_for_entity(entity: Union[Device, Circuit, PowerPanel, PowerFeed]):
is_device = isinstance(entity, Device) is_device = isinstance(entity, Device)
query = ( query = (
{"object_id": entity.device_role_id} {"object_id": entity.role_id}
if is_device if is_device
else {"content_type_id": ContentType.objects.get_for_model(entity).pk} else {"content_type_id": ContentType.objects.get_for_model(entity).pk}
) )
@@ -88,6 +104,7 @@ def create_node(
if isinstance(device, Circuit): if isinstance(device, Circuit):
dev_name = device.cid dev_name = device.cid
node["id"] = f"c{device.pk}" node["id"] = f"c{device.pk}"
model_name = 'CircuitCoordinate'
if device.provider is not None: if device.provider is not None:
node_content += ( node_content += (
@@ -98,6 +115,7 @@ def create_node(
elif isinstance(device, PowerPanel): elif isinstance(device, PowerPanel):
dev_name = device.name dev_name = device.name
node["id"] = f"p{device.pk}" node["id"] = f"p{device.pk}"
model_name = 'PowerPanelCoordinate'
if device.site is not None: if device.site is not None:
node_content += f"<tr><th>Site: </th><td>{device.site.name}</td></tr>" node_content += f"<tr><th>Site: </th><td>{device.site.name}</td></tr>"
@@ -108,6 +126,7 @@ def create_node(
elif isinstance(device, PowerFeed): elif isinstance(device, PowerFeed):
dev_name = device.name dev_name = device.name
node["id"] = f"f{device.pk}" node["id"] = f"f{device.pk}"
model_name = 'PowerFeedCoordinate'
if device.power_panel is not None: if device.power_panel is not None:
node_content += ( node_content += (
@@ -124,6 +143,7 @@ def create_node(
if device.voltage is not None: if device.voltage is not None:
node_content += f"<tr><th>Voltage: </th><td>{device.voltage}</td></tr>" node_content += f"<tr><th>Voltage: </th><td>{device.voltage}</td></tr>"
else: else:
model_name = 'Coordinate'
dev_name = device.name dev_name = device.name
if dev_name is None: if dev_name is None:
dev_name = device.device_type.get_full_name dev_name = device.device_type.get_full_name
@@ -163,8 +183,10 @@ def create_node(
if device.device_role.color != "": if device.device_role.color != "":
node["color.border"] = "#" + device.device_role.color node["color.border"] = "#" + device.device_role.color
model_class = getattr(netbox_topology_views.models, model_name)
if group_id is None or group_id == "default": if group_id is None or group_id == "default":
group_id = Coordinate.get_or_create_default_group(group_id) group_id = model_class.get_or_create_default_group(group_id)
if not group_id: if not group_id:
print('Exception occured while handling default group.') print('Exception occured while handling default group.')
return node return node
@@ -176,10 +198,10 @@ def create_node(
# will not be placed correctly by vis-network. # will not be placed correctly by vis-network.
node["x"] = 0 node["x"] = 0
node["y"] = 0 node["y"] = 0
if Coordinate.objects.filter(group=group, device=device.pk).values('x') and Coordinate.objects.filter(group=group, device=device.pk).values('y'): if model_class.objects.filter(group=group, device=device.pk).values('x') and model_class.objects.filter(group=group, device=device.pk).values('y'):
# Coordinates data for the device exists in Coordinates Group. Let's assign them # Coordinates data for the device exists in Coordinates Group. Let's assign them
node["x"] = Coordinate.objects.get(group=group, device=device.pk).x node["x"] = model_class.objects.get(group=group, device=device.pk).x
node["y"] = Coordinate.objects.get(group=group, device=device.pk).y node["y"] = model_class.objects.get(group=group, device=device.pk).y
node["physics"] = False node["physics"] = False
elif "coordinates" in device.custom_field_data: elif "coordinates" in device.custom_field_data:
# We prefer the new Coordinate model but leave the deprecated method # We prefer the new Coordinate model but leave the deprecated method
@@ -352,7 +374,8 @@ def get_topology_data(
) )
for path_complete_interface in path_complete_interfaces: for path_complete_interface in path_complete_interfaces:
for connected_endpoint in path_complete_interface.connected_endpoints: for connected_endpoint in path_complete_interface.connected_endpoints:
device_ids.append(connected_endpoint.device.id) if type(connected_endpoint) != ProviderNetwork:
device_ids.append(connected_endpoint.device.id)
if show_circuit: if show_circuit:
circuit_terminations = CircuitTermination.objects.filter( circuit_terminations = CircuitTermination.objects.filter(
@@ -653,7 +676,7 @@ class TopologyHomeView(PermissionRequiredMixin, View):
def get(self, request): def get(self, request):
self.filterset = DeviceFilterSet self.filterset = DeviceFilterSet
self.queryset = Device.objects.all().select_related( self.queryset = Device.objects.all().select_related(
"device_type", "device_role" "device_type", "role"
) )
self.queryset = self.filterset(request.GET, self.queryset).qs self.queryset = self.filterset(request.GET, self.queryset).qs
self.model = self.queryset.model self.model = self.queryset.model
@@ -694,7 +717,7 @@ class TopologyHomeView(PermissionRequiredMixin, View):
preselected_tags = IndividualOptions.objects.get(id=individualOptions.id).preselected_tags.all().values_list(Lower('name'), flat=True) preselected_tags = IndividualOptions.objects.get(id=individualOptions.id).preselected_tags.all().values_list(Lower('name'), flat=True)
q = QueryDict(mutable=True) q = QueryDict(mutable=True)
q.setlist("device_role_id", list(preselected_device_roles)) q.setlist("role_id", list(preselected_device_roles))
q.setlist("tag", list(preselected_tags)) q.setlist("tag", list(preselected_tags))
if individualOptions.save_coords: q['save_coords'] = "on" if individualOptions.save_coords: q['save_coords'] = "on"
@@ -799,6 +822,132 @@ class TopologyImagesView(PermissionRequiredMixin, View):
}, },
) )
class CircuitCoordinateView(PermissionRequiredMixin, ObjectView):
permission_required = 'netbox_topology_views.view_coordinate'
queryset = CircuitCoordinate.objects.all()
class CircuitCoordinateAddView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'netbox_topology_views.add_coordinate'
queryset = CircuitCoordinate.objects.all()
form = CircuitCoordinatesForm
template_name = 'netbox_topology_views/circuitcoordinate_add.html'
class CircuitCoordinateBulkImportView(BulkImportView):
queryset = CircuitCoordinate.objects.all()
model_form = CircuitCoordinatesImportForm
class CircuitCoordinateListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'netbox_topology_views.view_coordinate'
queryset = CircuitCoordinate.objects.all()
table = CircuitCoordinateListTable
template_name = 'netbox_topology_views/circuitcoordinate_list.html'
filterset = CircuitCoordinatesFilterSet
filterset_form = CircuitCoordinatesFilterForm
class CircuitCoordinateEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'netbox_topology_views.change_coordinate'
queryset = CircuitCoordinate.objects.all()
form = CircuitCoordinatesForm
template_name = 'netbox_topology_views/circuitcoordinate_edit.html'
class CircuitCoordinateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'netbox_topology_views.delete_coordinate'
queryset = CircuitCoordinate.objects.all()
class CircuitCoordinateChangeLogView(PermissionRequiredMixin, ObjectChangeLogView):
permission_required = 'netbox_topology_views.view_coordinate'
queryset = CircuitCoordinate.objects.all()
class PowerPanelCoordinateView(PermissionRequiredMixin, ObjectView):
permission_required = 'netbox_topology_views.view_coordinate'
queryset = PowerPanelCoordinate.objects.all()
class PowerPanelCoordinateAddView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'netbox_topology_views.add_coordinate'
queryset = PowerPanelCoordinate.objects.all()
form = PowerPanelCoordinatesForm
template_name = 'netbox_topology_views/powerpanelcoordinate_add.html'
class PowerPanelCoordinateBulkImportView(BulkImportView):
queryset = PowerPanelCoordinate.objects.all()
model_form = PowerPanelCoordinatesImportForm
class PowerPanelCoordinateListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'netbox_topology_views.view_coordinate'
queryset = PowerPanelCoordinate.objects.all()
table = PowerPanelCoordinateListTable
template_name = 'netbox_topology_views/powerpanelcoordinate_list.html'
filterset = PowerPanelCoordinatesFilterSet
filterset_form = PowerPanelCoordinatesFilterForm
class PowerPanelCoordinateEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'netbox_topology_views.change_coordinate'
queryset = PowerPanelCoordinate.objects.all()
form = PowerPanelCoordinatesForm
template_name = 'netbox_topology_views/powerpanelcoordinate_edit.html'
class PowerPanelCoordinateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'netbox_topology_views.delete_coordinate'
queryset = PowerPanelCoordinate.objects.all()
class PowerPanelCoordinateChangeLogView(PermissionRequiredMixin, ObjectChangeLogView):
permission_required = 'netbox_topology_views.view_coordinate'
queryset = PowerPanelCoordinate.objects.all()
class PowerFeedCoordinateView(PermissionRequiredMixin, ObjectView):
permission_required = 'netbox_topology_views.view_coordinate'
queryset = PowerFeedCoordinate.objects.all()
class PowerFeedCoordinateAddView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'netbox_topology_views.add_coordinate'
queryset = PowerFeedCoordinate.objects.all()
form = PowerFeedCoordinatesForm
template_name = 'netbox_topology_views/powerfeedcoordinate_add.html'
class PowerFeedCoordinateBulkImportView(BulkImportView):
queryset = PowerFeedCoordinate.objects.all()
model_form = PowerFeedCoordinatesImportForm
class PowerFeedCoordinateListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'netbox_topology_views.view_coordinate'
queryset = PowerFeedCoordinate.objects.all()
table = PowerFeedCoordinateListTable
template_name = 'netbox_topology_views/powerfeedcoordinate_list.html'
filterset = PowerFeedCoordinatesFilterSet
filterset_form = PowerFeedCoordinatesFilterForm
class PowerFeedCoordinateEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'netbox_topology_views.change_coordinate'
queryset = PowerFeedCoordinate.objects.all()
form = PowerFeedCoordinatesForm
template_name = 'netbox_topology_views/powerfeedcoordinate_edit.html'
class PowerFeedCoordinateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'netbox_topology_views.delete_coordinate'
queryset = PowerFeedCoordinate.objects.all()
class PowerFeedCoordinateChangeLogView(PermissionRequiredMixin, ObjectChangeLogView):
permission_required = 'netbox_topology_views.view_coordinate'
queryset = PowerFeedCoordinate.objects.all()
class CoordinateView(PermissionRequiredMixin, ObjectView): class CoordinateView(PermissionRequiredMixin, ObjectView):
permission_required = 'netbox_topology_views.view_coordinate' permission_required = 'netbox_topology_views.view_coordinate'
@@ -847,10 +996,19 @@ class CoordinateGroupView(PermissionRequiredMixin, ObjectView):
queryset = CoordinateGroup.objects.all() queryset = CoordinateGroup.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
circuittable = CircuitCoordinateListTable(instance.circuitcoordinate_set.all())
circuittable.configure(request)
powerpaneltable = PowerPanelCoordinateListTable(instance.powerpanelcoordinate_set.all())
powerpaneltable.configure(request)
powerfeedtable = PowerFeedCoordinateListTable(instance.powerfeedcoordinate_set.all())
powerfeedtable.configure(request)
table = CoordinateListTable(instance.coordinate_set.all()) table = CoordinateListTable(instance.coordinate_set.all())
table.configure(request) table.configure(request)
return { return {
'circuitcoordinates_table': circuittable,
'powerpanelcoordinates_table': powerpaneltable,
'powerfeedcoordinates_table': powerfeedtable,
'coordinates_table': table, 'coordinates_table': table,
} }
+1 -1
View File
@@ -7,7 +7,7 @@ long_description = readme.read_text()
setup( setup(
name="netbox-topology-views", name="netbox-topology-views",
version="3.6.2", version="3.8.0",
description="An NetBox plugin to create Topology maps", description="An NetBox plugin to create Topology maps",
long_description=long_description, long_description=long_description,
long_description_content_type="text/markdown", long_description_content_type="text/markdown",