Skip to content

Commit ff29107

Browse files
committed
fix(api): Fix schema and field definitions for OpenAPI
Add `get_internal_type()` to custom field classes for Django compatibility, annotate path parameters and operation IDs for background endpoints, and provide serializer context on the RQ base viewset to clear schema warnings. Fixes #20365
1 parent 69a7c97 commit ff29107

File tree

6 files changed

+81
-34
lines changed

6 files changed

+81
-34
lines changed

netbox/core/api/serializers_/tasks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class BackgroundTaskSerializer(serializers.Serializer):
1313
url = serializers.HyperlinkedIdentityField(
1414
view_name='core-api:rqtask-detail',
1515
lookup_field='id',
16-
lookup_url_kwarg='pk'
16+
lookup_url_kwarg='id'
1717
)
1818
description = serializers.CharField()
1919
origin = serializers.CharField()

netbox/core/api/views.py

Lines changed: 58 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from django_rq.settings import QUEUES_LIST
66
from django_rq.utils import get_statistics
77
from drf_spectacular.types import OpenApiTypes
8-
from drf_spectacular.utils import extend_schema
8+
from drf_spectacular.utils import OpenApiParameter, extend_schema
99
from rest_framework import viewsets
1010
from rest_framework.decorators import action
1111
from rest_framework.exceptions import PermissionDenied
@@ -24,6 +24,7 @@
2424
from netbox.api.metadata import ContentTypeMetadata
2525
from netbox.api.pagination import LimitOffsetListPagination
2626
from netbox.api.viewsets import NetBoxModelViewSet, NetBoxReadOnlyModelViewSet
27+
2728
from . import serializers
2829

2930

@@ -117,29 +118,49 @@ def list(self, request):
117118
def get_serializer(self, *args, **kwargs):
118119
"""
119120
Return the serializer instance that should be used for validating and
120-
deserializing input, and for serializing output.
121+
deserializing input and for serializing output.
121122
"""
122123
serializer_class = self.get_serializer_class()
123124
kwargs['context'] = self.get_serializer_context()
124125
return serializer_class(*args, **kwargs)
125126

127+
def get_serializer_class(self):
128+
"""
129+
Return the class to use for the serializer.
130+
"""
131+
return self.serializer_class
132+
133+
def get_serializer_context(self):
134+
"""
135+
Extra context provided to the serializer class.
136+
"""
137+
return {
138+
'request': self.request,
139+
'format': self.format_kwarg,
140+
'view': self,
141+
}
142+
126143

127144
class BackgroundQueueViewSet(BaseRQViewSet):
128145
"""
129146
Retrieve a list of RQ Queues.
130-
Note: Queue names are not URL safe so not returning a detail view.
147+
Note: Queue names are not URL safe, so not returning a detail view.
131148
"""
132149
serializer_class = serializers.BackgroundQueueSerializer
133150
lookup_field = 'name'
134151
lookup_value_regex = r'[\w.@+-]+'
135152

136153
def get_view_name(self):
137-
return "Background Queues"
154+
return 'Background Queues'
138155

139156
def get_data(self):
140-
return get_statistics(run_maintenance_tasks=True)["queues"]
157+
return get_statistics(run_maintenance_tasks=True)['queues']
141158

142-
@extend_schema(responses={200: OpenApiTypes.OBJECT})
159+
@extend_schema(
160+
operation_id='core_background_queues_retrieve_by_name',
161+
parameters=[OpenApiParameter(name='name', type=OpenApiTypes.STR, location=OpenApiParameter.PATH)],
162+
responses={200: OpenApiTypes.OBJECT},
163+
)
143164
def retrieve(self, request, name):
144165
data = self.get_data()
145166
if not data:
@@ -161,12 +182,17 @@ class BackgroundWorkerViewSet(BaseRQViewSet):
161182
lookup_field = 'name'
162183

163184
def get_view_name(self):
164-
return "Background Workers"
185+
return 'Background Workers'
165186

166187
def get_data(self):
167188
config = QUEUES_LIST[0]
168189
return Worker.all(get_redis_connection(config['connection_config']))
169190

191+
@extend_schema(
192+
operation_id='core_background_workers_retrieve_by_name',
193+
parameters=[OpenApiParameter(name='name', type=OpenApiTypes.STR, location=OpenApiParameter.PATH)],
194+
responses={200: OpenApiTypes.OBJECT},
195+
)
170196
def retrieve(self, request, name):
171197
# all the RQ queues should use the same connection
172198
config = QUEUES_LIST[0]
@@ -184,9 +210,10 @@ class BackgroundTaskViewSet(BaseRQViewSet):
184210
Retrieve a list of RQ Tasks.
185211
"""
186212
serializer_class = serializers.BackgroundTaskSerializer
213+
lookup_field = 'id'
187214

188215
def get_view_name(self):
189-
return "Background Tasks"
216+
return 'Background Tasks'
190217

191218
def get_data(self):
192219
return get_rq_jobs()
@@ -199,45 +226,53 @@ def get_task_from_id(self, task_id):
199226

200227
return task
201228

202-
@extend_schema(responses={200: OpenApiTypes.OBJECT})
203-
def retrieve(self, request, pk):
229+
@extend_schema(
230+
operation_id='core_background_tasks_retrieve_by_id',
231+
parameters=[OpenApiParameter(name='id', type=OpenApiTypes.STR, location=OpenApiParameter.PATH)],
232+
responses={200: OpenApiTypes.OBJECT},
233+
)
234+
def retrieve(self, request, id):
204235
"""
205236
Retrieve the details of the specified RQ Task.
206237
"""
207-
task = self.get_task_from_id(pk)
238+
task = self.get_task_from_id(id)
208239
serializer = self.serializer_class(task, context={'request': request})
209240
return Response(serializer.data)
210241

211-
@action(methods=["POST"], detail=True)
212-
def delete(self, request, pk):
242+
@extend_schema(parameters=[OpenApiParameter(name='id', type=OpenApiTypes.STR, location=OpenApiParameter.PATH)])
243+
@action(methods=['POST'], detail=True)
244+
def delete(self, request, id):
213245
"""
214246
Delete the specified RQ Task.
215247
"""
216-
delete_rq_job(pk)
248+
delete_rq_job(id)
217249
return HttpResponse(status=200)
218250

219-
@action(methods=["POST"], detail=True)
220-
def requeue(self, request, pk):
251+
@extend_schema(parameters=[OpenApiParameter(name='id', type=OpenApiTypes.STR, location=OpenApiParameter.PATH)])
252+
@action(methods=['POST'], detail=True)
253+
def requeue(self, request, id):
221254
"""
222255
Requeues the specified RQ Task.
223256
"""
224-
requeue_rq_job(pk)
257+
requeue_rq_job(id)
225258
return HttpResponse(status=200)
226259

227-
@action(methods=["POST"], detail=True)
228-
def enqueue(self, request, pk):
260+
@extend_schema(parameters=[OpenApiParameter(name='id', type=OpenApiTypes.STR, location=OpenApiParameter.PATH)])
261+
@action(methods=['POST'], detail=True)
262+
def enqueue(self, request, id):
229263
"""
230264
Enqueues the specified RQ Task.
231265
"""
232-
enqueue_rq_job(pk)
266+
enqueue_rq_job(id)
233267
return HttpResponse(status=200)
234268

235-
@action(methods=["POST"], detail=True)
236-
def stop(self, request, pk):
269+
@extend_schema(parameters=[OpenApiParameter(name='id', type=OpenApiTypes.STR, location=OpenApiParameter.PATH)])
270+
@action(methods=['POST'], detail=True)
271+
def stop(self, request, id):
237272
"""
238273
Stops the specified RQ Task.
239274
"""
240-
stopped_jobs = stop_rq_job(pk)
275+
stopped_jobs = stop_rq_job(id)
241276
if len(stopped_jobs) == 1:
242277
return HttpResponse(status=200)
243278
else:

netbox/dcim/fields.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,17 @@ class eui64_unix_expanded_uppercase(eui64_unix_expanded):
2626
#
2727

2828
class MACAddressField(models.Field):
29-
description = "PostgreSQL MAC Address field"
29+
description = 'PostgreSQL MAC Address field'
3030

3131
def python_type(self):
3232
return EUI
3333

3434
def from_db_value(self, value, expression, connection):
3535
return self.to_python(value)
3636

37+
def get_internal_type(self):
38+
return 'CharField'
39+
3740
def to_python(self, value):
3841
if value is None:
3942
return value
@@ -54,14 +57,17 @@ def get_prep_value(self, value):
5457

5558

5659
class WWNField(models.Field):
57-
description = "World Wide Name field"
60+
description = 'World Wide Name field'
5861

5962
def python_type(self):
6063
return EUI
6164

6265
def from_db_value(self, value, expression, connection):
6366
return self.to_python(value)
6467

68+
def get_internal_type(self):
69+
return 'CharField'
70+
6571
def to_python(self, value):
6672
if value is None:
6773
return value

netbox/extras/api/serializers_/customfields.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ class CustomFieldChoiceSetSerializer(ChangeLogMessageSerializer, ValidatedModelS
2626
max_length=2
2727
)
2828
)
29+
choices_count = serializers.IntegerField(read_only=True)
2930

3031
class Meta:
3132
model = CustomFieldChoiceSet

netbox/ipam/fields.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ def python_type(self):
2626
def from_db_value(self, value, expression, connection):
2727
return self.to_python(value)
2828

29+
def get_internal_type(self):
30+
return 'CharField'
31+
2932
def to_python(self, value):
3033
if not value:
3134
return value
@@ -57,7 +60,7 @@ class IPNetworkField(BaseIPField):
5760
"""
5861
IP prefix (network and mask)
5962
"""
60-
description = "PostgreSQL CIDR field"
63+
description = 'PostgreSQL CIDR field'
6164
default_validators = [validators.prefix_validator]
6265

6366
def db_type(self, connection):
@@ -83,7 +86,7 @@ class IPAddressField(BaseIPField):
8386
"""
8487
IP address (host address and mask)
8588
"""
86-
description = "PostgreSQL INET field"
89+
description = 'PostgreSQL INET field'
8790

8891
def db_type(self, connection):
8992
return 'inet'
@@ -110,7 +113,7 @@ def db_type(self, connection):
110113

111114

112115
class ASNField(models.BigIntegerField):
113-
description = "32-bit ASN field"
116+
description = '32-bit ASN field'
114117
default_validators = [
115118
MinValueValidator(BGP_ASN_MIN),
116119
MaxValueValidator(BGP_ASN_MAX),

netbox/ipam/filtersets.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -354,13 +354,13 @@ class PrefixFilterSet(NetBoxModelFilterSet, ScopedFilterSet, TenancyFilterSet, C
354354
vlan_group_id = django_filters.ModelMultipleChoiceFilter(
355355
field_name='vlan__group',
356356
queryset=VLANGroup.objects.all(),
357-
to_field_name="id",
357+
to_field_name='id',
358358
label=_('VLAN Group (ID)'),
359359
)
360360
vlan_group = django_filters.ModelMultipleChoiceFilter(
361361
field_name='vlan__group__slug',
362362
queryset=VLANGroup.objects.all(),
363-
to_field_name="slug",
363+
to_field_name='slug',
364364
label=_('VLAN Group (slug)'),
365365
)
366366
vlan_id = django_filters.ModelMultipleChoiceFilter(
@@ -695,12 +695,12 @@ def search_by_parent(self, queryset, name, value):
695695
return queryset.filter(q)
696696

697697
def parse_inet_addresses(self, value):
698-
'''
698+
"""
699699
Parse networks or IP addresses and cast to a format
700700
acceptable by the Postgres inet type.
701701
702702
Skips invalid values.
703-
'''
703+
"""
704704
parsed = []
705705
for addr in value:
706706
if netaddr.valid_ipv4(addr) or netaddr.valid_ipv6(addr):
@@ -718,7 +718,7 @@ def filter_address(self, queryset, name, value):
718718
# as argument. If they are all invalid,
719719
# we return an empty queryset
720720
value = self.parse_inet_addresses(value)
721-
if (len(value) == 0):
721+
if len(value) == 0:
722722
return queryset.none()
723723

724724
try:
@@ -1079,6 +1079,7 @@ def get_for_device(self, queryset, name, value):
10791079
def get_for_virtualmachine(self, queryset, name, value):
10801080
return queryset.get_for_virtualmachine(value)
10811081

1082+
@extend_schema_field(OpenApiTypes.INT)
10821083
def filter_interface_id(self, queryset, name, value):
10831084
if value is None:
10841085
return queryset.none()
@@ -1087,6 +1088,7 @@ def filter_interface_id(self, queryset, name, value):
10871088
Q(interfaces_as_untagged=value)
10881089
).distinct()
10891090

1091+
@extend_schema_field(OpenApiTypes.INT)
10901092
def filter_vminterface_id(self, queryset, name, value):
10911093
if value is None:
10921094
return queryset.none()

0 commit comments

Comments
 (0)