Skip to content

Commit 47dced9

Browse files
committed
Merge branch 'gispath' into 'master'
Version 5.0 – GIS Path enablede (Breaking Changes) See merge request 701/netbox/cesnet_service_path_plugin!25
2 parents ac011e8 + 6e318f6 commit 47dced9

37 files changed

+2992
-182
lines changed
Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
"""Top-level package for Cesnet ServicePath Plugin."""
22

3+
from importlib import metadata
34
from netbox.plugins import PluginConfig
45

5-
from .version import __description__, __name__, __version__
6+
# Get package metadata from pyproject.toml
7+
_metadata = metadata.metadata("cesnet_service_path_plugin")
8+
__version__ = _metadata["Version"]
9+
__description__ = _metadata["Summary"]
10+
__name__ = _metadata["Name"]
11+
__author__ = _metadata["Author"]
12+
__email__ = _metadata["Author-email"]
613

714

815
class CesnetServicePathPluginConfig(PluginConfig):
@@ -11,6 +18,7 @@ class CesnetServicePathPluginConfig(PluginConfig):
1118
description = __description__
1219
version = __version__
1320
base_url = "cesnet-service-path-plugin"
21+
author = __email__
1422

1523

1624
config = CesnetServicePathPluginConfig

cesnet_service_path_plugin/api/serializers/segment.py

Lines changed: 169 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
from circuits.api.serializers import CircuitSerializer, ProviderSerializer
23
from dcim.api.serializers import (
34
LocationSerializer,
@@ -7,19 +8,23 @@
78
from rest_framework import serializers
89

910
from cesnet_service_path_plugin.models.segment import Segment
11+
from cesnet_service_path_plugin.utils import export_segment_paths_as_geojson
1012

1113

1214
class SegmentSerializer(NetBoxModelSerializer):
13-
url = serializers.HyperlinkedIdentityField(
14-
view_name="plugins-api:cesnet_service_path_plugin-api:segment-detail"
15-
)
15+
"""Default serializer Segment - excludes heavy geometry fields"""
16+
17+
url = serializers.HyperlinkedIdentityField(view_name="plugins-api:cesnet_service_path_plugin-api:segment-detail")
1618
provider = ProviderSerializer(required=True, nested=True)
1719
site_a = SiteSerializer(required=True, nested=True)
1820
location_a = LocationSerializer(required=True, nested=True)
1921
site_b = SiteSerializer(required=True, nested=True)
2022
location_b = LocationSerializer(required=True, nested=True)
2123
circuits = CircuitSerializer(required=False, many=True, nested=True)
2224

25+
# Only include lightweight path info
26+
has_path_data = serializers.SerializerMethodField(read_only=True)
27+
2328
class Meta:
2429
model = Segment
2530
fields = (
@@ -40,6 +45,11 @@ class Meta:
4045
"site_b",
4146
"location_b",
4247
"circuits",
48+
# Only basic path info, no heavy geometry
49+
"path_length_km",
50+
"path_source_format",
51+
"path_notes",
52+
"has_path_data",
4353
"tags",
4454
)
4555
brief_fields = (
@@ -48,9 +58,165 @@ class Meta:
4858
"display",
4959
"name",
5060
"status",
61+
"has_path_data",
5162
"tags",
5263
)
5364

65+
def get_has_path_data(self, obj):
66+
return obj.has_path_data()
67+
68+
69+
class SegmentListSerializer(NetBoxModelSerializer):
70+
"""Lightweight serializer for list views - excludes heavy geometry fields"""
71+
72+
url = serializers.HyperlinkedIdentityField(view_name="plugins-api:cesnet_service_path_plugin-api:segment-detail")
73+
provider = ProviderSerializer(required=True, nested=True)
74+
site_a = SiteSerializer(required=True, nested=True)
75+
location_a = LocationSerializer(required=True, nested=True)
76+
site_b = SiteSerializer(required=True, nested=True)
77+
location_b = LocationSerializer(required=True, nested=True)
78+
circuits = CircuitSerializer(required=False, many=True, nested=True)
79+
80+
# Only include lightweight path info
81+
has_path_data = serializers.SerializerMethodField(read_only=True)
82+
83+
class Meta:
84+
model = Segment
85+
fields = (
86+
"id",
87+
"url",
88+
"display",
89+
"name",
90+
"status",
91+
"network_label",
92+
"install_date",
93+
"termination_date",
94+
"provider",
95+
"provider_segment_id",
96+
"provider_segment_name",
97+
"provider_segment_contract",
98+
"site_a",
99+
"location_a",
100+
"site_b",
101+
"location_b",
102+
"circuits",
103+
# Only basic path info, no heavy geometry
104+
"path_length_km",
105+
"path_source_format",
106+
"path_notes",
107+
"has_path_data",
108+
"tags",
109+
)
110+
brief_fields = (
111+
"id",
112+
"url",
113+
"display",
114+
"name",
115+
"status",
116+
"has_path_data",
117+
"tags",
118+
)
119+
120+
def get_has_path_data(self, obj):
121+
return obj.has_path_data()
122+
123+
124+
class SegmentDetailSerializer(NetBoxModelSerializer):
125+
"""Full serializer with all geometry data for detail views"""
126+
127+
# This is your existing SegmentSerializer - just rename it
128+
url = serializers.HyperlinkedIdentityField(view_name="plugins-api:cesnet_service_path_plugin-api:segment-detail")
129+
provider = ProviderSerializer(required=True, nested=True)
130+
site_a = SiteSerializer(required=True, nested=True)
131+
location_a = LocationSerializer(required=True, nested=True)
132+
site_b = SiteSerializer(required=True, nested=True)
133+
location_b = LocationSerializer(required=True, nested=True)
134+
circuits = CircuitSerializer(required=False, many=True, nested=True)
135+
136+
# All the heavy geometry fields
137+
path_geometry_geojson = serializers.SerializerMethodField(read_only=True)
138+
path_coordinates = serializers.SerializerMethodField(read_only=True)
139+
path_bounds = serializers.SerializerMethodField(read_only=True)
140+
has_path_data = serializers.SerializerMethodField(read_only=True)
141+
142+
class Meta:
143+
model = Segment
144+
fields = (
145+
"id",
146+
"url",
147+
"display",
148+
"name",
149+
"status",
150+
"network_label",
151+
"install_date",
152+
"termination_date",
153+
"provider",
154+
"provider_segment_id",
155+
"provider_segment_name",
156+
"provider_segment_contract",
157+
"site_a",
158+
"location_a",
159+
"site_b",
160+
"location_b",
161+
"circuits",
162+
# All path geometry fields
163+
"path_geometry_geojson",
164+
"path_coordinates",
165+
"path_bounds",
166+
"path_length_km",
167+
"path_source_format",
168+
"path_notes",
169+
"has_path_data",
170+
"tags",
171+
)
172+
brief_fields = (
173+
"id",
174+
"url",
175+
"display",
176+
"name",
177+
"status",
178+
"has_path_data",
179+
"tags",
180+
)
181+
182+
def get_path_geometry_geojson(self, obj):
183+
"""
184+
Return path geometry as GeoJSON Feature
185+
"""
186+
if not obj.has_path_data():
187+
return None
188+
189+
try:
190+
191+
geojson_str = export_segment_paths_as_geojson([obj])
192+
geojson_data = json.loads(geojson_str)
193+
194+
# Return just the first (and only) feature, not the entire FeatureCollection
195+
if geojson_data.get("features"):
196+
return geojson_data["features"][0]
197+
return None
198+
except Exception:
199+
# Fallback to basic GeoJSON if utility function fails
200+
return obj.get_path_geojson()
201+
202+
def get_path_coordinates(self, obj):
203+
"""
204+
Return path coordinates as list of LineString coordinate arrays
205+
"""
206+
return obj.get_path_coordinates()
207+
208+
def get_path_bounds(self, obj):
209+
"""
210+
Return bounding box of the path geometry [xmin, ymin, xmax, ymax]
211+
"""
212+
return obj.get_path_bounds()
213+
214+
def get_has_path_data(self, obj):
215+
"""
216+
Return boolean indicating if segment has path data
217+
"""
218+
return obj.has_path_data()
219+
54220
def validate(self, data):
55221
# Enforce model validation
56222
super().validate(data)

cesnet_service_path_plugin/api/serializers/segment_circuit_mapping.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
from rest_framework import serializers
22
from netbox.api.serializers import NetBoxModelSerializer
3-
from cesnet_service_path_plugin.api.serializers.segment import SegmentSerializer
3+
from cesnet_service_path_plugin.api.serializers.segment import SegmentListSerializer
44
from cesnet_service_path_plugin.api.serializers.service_path import ServicePathSerializer
55
from cesnet_service_path_plugin.models import SegmentCircuitMapping
66
from circuits.api.serializers import CircuitSerializer
77

8+
89
class SegmentCircuitMappingSerializer(NetBoxModelSerializer):
910
url = serializers.HyperlinkedIdentityField(
1011
view_name="plugins-api:cesnet_service_path_plugin-api:segmentcircuitmapping-detail"
1112
)
1213
circuit = CircuitSerializer(nested=True)
13-
segment = SegmentSerializer(nested=True)
14+
segment = SegmentListSerializer(nested=True)
1415

1516
class Meta:
1617
model = SegmentCircuitMapping

cesnet_service_path_plugin/api/serializers/service_path.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22
from netbox.api.serializers import NetBoxModelSerializer
33
from rest_framework import serializers
44

5-
from cesnet_service_path_plugin.api.serializers.segment import SegmentSerializer
5+
from cesnet_service_path_plugin.api.serializers.segment import SegmentListSerializer
66
from cesnet_service_path_plugin.models import ServicePath
77

88

99
class ServicePathSerializer(NetBoxModelSerializer):
1010
url = serializers.HyperlinkedIdentityField(
1111
view_name="plugins-api:cesnet_service_path_plugin-api:servicepath-detail"
1212
)
13-
segments = SegmentSerializer(many=True, read_only=True, nested=True)
13+
segments = SegmentListSerializer(many=True, read_only=True, nested=True)
1414
circuits = CircuitSerializer(required=False, many=True, nested=True)
1515

1616
class Meta:

cesnet_service_path_plugin/api/serializers/service_path_segment_mapping.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from rest_framework import serializers
22
from netbox.api.serializers import NetBoxModelSerializer
3-
from cesnet_service_path_plugin.api.serializers.segment import SegmentSerializer
3+
from cesnet_service_path_plugin.api.serializers.segment import SegmentListSerializer
44
from cesnet_service_path_plugin.api.serializers.service_path import (
55
ServicePathSerializer,
66
)
@@ -16,7 +16,7 @@ class ServicePathSegmentMappingSerializer(NetBoxModelSerializer):
1616
# required=True
1717
# )
1818
service_path = ServicePathSerializer(nested=True)
19-
segment = SegmentSerializer(nested=True)
19+
segment = SegmentListSerializer(nested=True)
2020

2121
class Meta:
2222
model = ServicePathSegmentMapping

cesnet_service_path_plugin/api/views/segment.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,21 @@
33

44
from cesnet_service_path_plugin.models import Segment
55
from cesnet_service_path_plugin.filtersets import SegmentFilterSet
6-
from cesnet_service_path_plugin.api.serializers import SegmentSerializer
6+
from cesnet_service_path_plugin.api.serializers import SegmentSerializer, SegmentDetailSerializer
77

88

99
class SegmentViewSet(NetBoxModelViewSet):
1010
metadata_class = ContentTypeMetadata
1111
queryset = Segment.objects.all()
12-
serializer_class = SegmentSerializer
1312
filterset_class = SegmentFilterSet
13+
14+
def get_serializer_class(self):
15+
"""
16+
Return appropriate serializer based on action
17+
"""
18+
if self.action == "retrieve":
19+
pathdata = self.request.query_params.get("pathdata", "false").lower() == "true"
20+
if pathdata:
21+
return SegmentDetailSerializer
22+
23+
return SegmentSerializer

cesnet_service_path_plugin/filtersets/segment.py

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,16 @@ class SegmentFilterSet(NetBoxModelFilterSet):
7676
label="Circuit (ID)",
7777
)
7878

79+
# New filter for path data
80+
has_path_data = django_filters.MultipleChoiceFilter(
81+
choices=[
82+
(True, "Yes"),
83+
(False, "No"),
84+
],
85+
method="_has_path_data",
86+
label="Has Path Data",
87+
)
88+
7989
class Meta:
8090
model = Segment
8191
fields = [
@@ -92,6 +102,7 @@ class Meta:
92102
"location_a",
93103
"site_b",
94104
"location_b",
105+
"has_path_data", # Added to Meta fields
95106
]
96107

97108
def _at_any_site(self, queryset, name, value):
@@ -110,6 +121,42 @@ def _at_any_location(self, queryset, name, value):
110121
location_b = Q(location_b__in=value)
111122
return queryset.filter(location_a | location_b)
112123

124+
def _has_path_data(self, queryset, name, value):
125+
"""
126+
Filter segments based on whether they have path data or not
127+
128+
Args:
129+
value: List of selected values from choices
130+
[True] - show only segments with path data
131+
[False] - show only segments without path data
132+
[True, False] - show all segments (both with and without)
133+
[] - show all segments (nothing selected)
134+
"""
135+
if not value:
136+
# Nothing selected, show all segments
137+
return queryset
138+
139+
# Convert string values to boolean (django-filter sometimes passes strings)
140+
bool_values = []
141+
for v in value:
142+
if v is True or v == "True" or v == True:
143+
bool_values.append(True)
144+
elif v is False or v == "False" or v == False:
145+
bool_values.append(False)
146+
147+
if True in bool_values and False in bool_values:
148+
# Both selected, show all segments
149+
return queryset
150+
elif True in bool_values:
151+
# Only "Yes" selected, show segments with path data
152+
return queryset.filter(path_geometry__isnull=False)
153+
elif False in bool_values:
154+
# Only "No" selected, show segments without path data
155+
return queryset.filter(path_geometry__isnull=True)
156+
else:
157+
# Fallback: show all segments
158+
return queryset
159+
113160
def search(self, queryset, name, value):
114161
site_a = Q(site_a__name__icontains=value)
115162
site_b = Q(site_b__name__icontains=value)
@@ -121,12 +168,5 @@ def search(self, queryset, name, value):
121168
status = Q(status__iexact=value)
122169

123170
return queryset.filter(
124-
site_a
125-
| site_b
126-
| location_a
127-
| location_b
128-
| segment_name
129-
| network_label
130-
| provider_segment_id
131-
| status
171+
site_a | site_b | location_a | location_b | segment_name | network_label | provider_segment_id | status
132172
)

0 commit comments

Comments
 (0)