From 9cc16078113d43d70477a00a12aff9fac59be1b1 Mon Sep 17 00:00:00 2001 From: dreng Date: Thu, 4 May 2023 22:17:18 +0200 Subject: [PATCH] add load/save values, fix filterset --- netbox_topology_views/api/views.py | 38 +++++++++++++++++++++++++++++- netbox_topology_views/filters.py | 20 ++++++++++++---- netbox_topology_views/forms.py | 18 +++++--------- netbox_topology_views/models.py | 7 +----- netbox_topology_views/views.py | 32 ++++++++++++++++++++++--- 5 files changed, 88 insertions(+), 27 deletions(-) diff --git a/netbox_topology_views/api/views.py b/netbox_topology_views/api/views.py index 026b8be..720502a 100644 --- a/netbox_topology_views/api/views.py +++ b/netbox_topology_views/api/views.py @@ -14,7 +14,7 @@ from netbox_topology_views.api.serializers import ( RoleImageSerializer, TopologyDummySerializer, ) -from netbox_topology_views.models import RoleImage, IndividualOptions +from netbox_topology_views.models import RoleImage, IndividualOptions, CoordinateGroup, Coordinate 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.filters import DeviceFilterSet @@ -50,6 +50,8 @@ class SaveCoordsViewSet(ReadOnlyModelViewSet): if not actual_device: return Response({"status": "invalid node_id in body"}, status=400) + # Storing coordinates in custom field is deprecated now. + # We preserve this for backwards compatibility. try: actual_device.custom_field_data["coordinates"] = "%s;%s" % ( x_coord, @@ -61,6 +63,40 @@ class SaveCoordsViewSet(ReadOnlyModelViewSet): {"status": "coords custom field could not be saved"}, status=500 ) + # 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: + group = CoordinateGroup.objects.get(name="default") + except CoordinateGroup.DoesNotExist: + try: + 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() + except: + return Response( + {"status": "Error while creating default group."}, status=500 + ) + + try: + # Hen-and-egg-problem. Thanks, Django! By default, Django updates records that + # already exist and inserts otherwise. This does not work with our + # unique_together key if no pk is given. But: No record, no pk. + if not Coordinate.objects.filter(group=group, device=actual_device): + # Unique group/device pair does not exist. Prepare new data set + coords = Coordinate(group=group, device=actual_device, x=x_coord, y=y_coord) + else: + # 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.save() + except: + return Response( + {"status": "Coordinates could not be saved."}, status=500 + ) return Response({"status": "saved coords"}) class ExportTopoToXML(PermissionRequiredMixin, ViewSet): diff --git a/netbox_topology_views/filters.py b/netbox_topology_views/filters.py index 0c081e7..e2467a5 100644 --- a/netbox_topology_views/filters.py +++ b/netbox_topology_views/filters.py @@ -1,7 +1,7 @@ import django_filters from dcim.choices import DeviceStatusChoices from dcim.models import Device, DeviceRole, Location, Rack, Region, Site -from .models import Coordinate +from .models import Coordinate, CoordinateGroup from django.db.models import Q from netbox.filtersets import NetBoxModelFilterSet from tenancy.filtersets import TenancyFilterSet @@ -55,14 +55,24 @@ class DeviceFilterSet(TenancyFilterSet, NetBoxModelFilterSet): qs_filter = Q(name__icontains=value) return queryset.filter(qs_filter) -class CoordinateFilterSet(NetBoxModelFilterSet): +class CoordinatesFilterSet(NetBoxModelFilterSet): + group = django_filters.ModelMultipleChoiceFilter( + queryset = CoordinateGroup.objects.all(), + ) + + device = django_filters.ModelMultipleChoiceFilter( + queryset = Device.objects.all(), + ) + class Meta: model = Coordinate - fields = ('id', 'group', 'device', 'x', 'y') + fields = ['id', 'group', 'device', 'x', 'y'] def search(self, queryset, name, value): """Perform the filtered search.""" if not value.strip(): return queryset - qs_filter = Q(group__icontains=value | Q(device__icontains=value)) - return queryset.filter(qs_filter) \ No newline at end of file + return queryset.filter( + Q(group__name__icontains=value) | + Q(device__name__icontains=value) + ) \ No newline at end of file diff --git a/netbox_topology_views/forms.py b/netbox_topology_views/forms.py index b3dab90..a1f2d71 100644 --- a/netbox_topology_views/forms.py +++ b/netbox_topology_views/forms.py @@ -162,27 +162,21 @@ class CoordinatesForm(NetBoxModelForm): class CoordinatesFilterForm(NetBoxModelFilterSetForm): model = Coordinate + fieldsets = ( + (None, ('q', 'filter_id')), + ('Coordinates', ('group', 'device', 'x', 'y')) + ) group = forms.ModelMultipleChoiceField( queryset=CoordinateGroup.objects.all(), required=False ) - device = DynamicModelChoiceField( + device = DynamicModelMultipleChoiceField( queryset=Device.objects.all(), required=False ) - id = DynamicModelMultipleChoiceField( - queryset=Device.objects.all(), - required=False, - label=_("Device"), - query_params={ - "location_id": "$location_id", - "region_id": "$region_id", - "site_id": "$site_id", - "role_id": "$device_role_id", - }, - ) + x = forms.IntegerField( required=False ) diff --git a/netbox_topology_views/models.py b/netbox_topology_views/models.py index 98f902d..0f13a50 100644 --- a/netbox_topology_views/models.py +++ b/netbox_topology_views/models.py @@ -121,7 +121,7 @@ class CoordinateGroup(NetBoxModel): class Coordinate(NetBoxModel): """ - Coordinates are being used to place devices in a topology view onto a certian + 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. """ @@ -147,11 +147,6 @@ class Coordinate(NetBoxModel): def get_absolute_url(self): return reverse('plugins:netbox_topology_views:coordinate', args=[self.pk]) - def set_xy_from_text_coords(self, coords: str): - xy = coords.split(';') - self.x = xy[0] - self.y = xy[1] - class IndividualOptions(NetBoxModel): CHOICES = ( ('interface', 'interface'), diff --git a/netbox_topology_views/views.py b/netbox_topology_views/views.py index 4ce3a12..77def4a 100644 --- a/netbox_topology_views/views.py +++ b/netbox_topology_views/views.py @@ -32,7 +32,7 @@ from wireless.models import WirelessLink from netbox.views.generic import ObjectView, ObjectListView, ObjectEditView, ObjectDeleteView, ObjectChangeLogView -from netbox_topology_views.filters import DeviceFilterSet, CoordinateFilterSet +from netbox_topology_views.filters import DeviceFilterSet, CoordinatesFilterSet from netbox_topology_views.forms import DeviceFilterForm, IndividualOptionsForm, CoordinateGroupsForm, CoordinatesForm, CoordinatesFilterForm from netbox_topology_views.models import RoleImage, CoordinateGroup, Coordinate, IndividualOptions from netbox_topology_views.tables import CoordinateGroupListTable, CoordinateListTable @@ -156,8 +156,31 @@ def create_node( node["href"] = device.get_absolute_url() node["image"] = get_image_for_entity(device) + # 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: + group = CoordinateGroup.objects.get(name="default") + except CoordinateGroup.DoesNotExist: + try: + 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() + except: + pass + node["physics"] = True - if "coordinates" in device.custom_field_data: + if Coordinate.objects.filter(group=group, device=device.pk).values('x') and Coordinate.objects.filter(group=group, device=device.pk).values('y'): + node["x"] = Coordinate.objects.get(group=group, device=device.pk).x + node["y"] = Coordinate.objects.get(group=group, device=device.pk).y + node["physics"] = False + elif "coordinates" in device.custom_field_data: + # We prefer the new Coordinate model but leave the deprecated method + # for now as fallback for compatibility reasons if device.custom_field_data["coordinates"] is not None: if ";" in device.custom_field_data["coordinates"]: cords = device.custom_field_data["coordinates"].split(";") @@ -166,6 +189,9 @@ def create_node( node["physics"] = False elif save_coords: node["physics"] = False + elif save_coords: + node["physics"] = False + return node @@ -772,7 +798,7 @@ class CoordinateListView(PermissionRequiredMixin, ObjectListView): queryset = Coordinate.objects.all() table = CoordinateListTable template_name = 'netbox_topology_views/coordinate_list.html' - filterset = CoordinateFilterSet + filterset = CoordinatesFilterSet filterset_form = CoordinatesFilterForm class CoordinateEditView(PermissionRequiredMixin, ObjectEditView):