Coverage for dcim/api/serializers_/device_components.py: 89%
224 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
1from django.contrib.contenttypes.models import ContentType
2from django.utils.translation import gettext as _
3from netaddr import EUI, AddrFormatError
4from rest_framework import serializers
6from dcim.choices import *
7from dcim.constants import *
8from dcim.models import (
9 ConsolePort,
10 ConsoleServerPort,
11 CoolingIntake,
12 CoolingOutflow,
13 DeviceBay,
14 FrontPort,
15 Interface,
16 InventoryItem,
17 ModuleBay,
18 ModuleBayType,
19 PortMapping,
20 PowerOutlet,
21 PowerPort,
22 RearPort,
23 VirtualDeviceContext,
24)
25from ipam.api.serializers_.vlans import VLANSerializer, VLANTranslationPolicySerializer
26from ipam.api.serializers_.vrfs import VRFSerializer
27from ipam.models import VLAN
28from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
29from netbox.api.gfk_fields import GFKSerializerField
30from netbox.api.serializers import NetBoxModelSerializer
31from netbox.choices import DiameterUnitChoices, FlowRateUnitChoices
32from users.api.serializers_.mixins import OwnerMixin
33from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer
34from wireless.api.serializers_.nested import NestedWirelessLinkSerializer
35from wireless.api.serializers_.wirelesslans import WirelessLANSerializer
36from wireless.choices import *
37from wireless.models import WirelessLAN
39from .base import ConnectedEndpointsSerializer, PortSerializer
40from .cables import CabledObjectSerializer
41from .devices import DeviceSerializer, MACAddressSerializer, ModuleSerializer, VirtualDeviceContextSerializer
42from .devicetypes import ModuleBayTypeSerializer
43from .manufacturers import ManufacturerSerializer
44from .mixins import _UNSET, MACAddressShortcutMixin
45from .nested import NestedCoolingOutflowSerializer, NestedInterfaceSerializer
46from .roles import InventoryItemRoleSerializer
48__all__ = (
49 'ConsolePortSerializer',
50 'ConsoleServerPortSerializer',
51 'CoolingIntakeSerializer',
52 'CoolingOutflowSerializer',
53 'DeviceBaySerializer',
54 'FrontPortSerializer',
55 'InterfaceSerializer',
56 'InventoryItemSerializer',
57 'ModuleBaySerializer',
58 'PowerOutletSerializer',
59 'PowerPortSerializer',
60 'RearPortSerializer',
61)
64class ConsoleServerPortSerializer(
65 OwnerMixin,
66 NetBoxModelSerializer,
67 CabledObjectSerializer,
68 ConnectedEndpointsSerializer
69):
70 device = DeviceSerializer(nested=True)
71 module = ModuleSerializer(
72 nested=True,
73 fields=('id', 'url', 'display', 'device', 'module_bay'),
74 required=False,
75 allow_null=True
76 )
77 type = ChoiceField(
78 choices=ConsolePortTypeChoices,
79 allow_blank=True,
80 required=False
81 )
82 speed = ChoiceField(
83 choices=ConsolePortSpeedChoices,
84 allow_null=True,
85 required=False
86 )
88 class Meta:
89 model = ConsoleServerPort
90 fields = [
91 'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description',
92 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'connected_endpoints',
93 'connected_endpoints_type', 'connected_endpoints_reachable', 'owner', 'tags', 'custom_fields', 'created',
94 'last_updated', '_occupied',
95 ]
96 brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
99class ConsolePortSerializer(
100 OwnerMixin,
101 NetBoxModelSerializer,
102 CabledObjectSerializer,
103 ConnectedEndpointsSerializer
104):
105 device = DeviceSerializer(nested=True)
106 module = ModuleSerializer(
107 nested=True,
108 fields=('id', 'url', 'display', 'device', 'module_bay'),
109 required=False,
110 allow_null=True
111 )
112 type = ChoiceField(
113 choices=ConsolePortTypeChoices,
114 allow_blank=True,
115 required=False
116 )
117 speed = ChoiceField(
118 choices=ConsolePortSpeedChoices,
119 allow_null=True,
120 required=False
121 )
123 class Meta:
124 model = ConsolePort
125 fields = [
126 'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description',
127 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'connected_endpoints',
128 'connected_endpoints_type', 'connected_endpoints_reachable', 'owner', 'tags', 'custom_fields', 'created',
129 'last_updated', '_occupied',
130 ]
131 brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
134class PowerPortSerializer(
135 OwnerMixin,
136 NetBoxModelSerializer,
137 CabledObjectSerializer,
138 ConnectedEndpointsSerializer
139):
140 device = DeviceSerializer(nested=True)
141 module = ModuleSerializer(
142 nested=True,
143 fields=('id', 'url', 'display', 'device', 'module_bay'),
144 required=False,
145 allow_null=True
146 )
147 type = ChoiceField(
148 choices=PowerPortTypeChoices,
149 allow_blank=True,
150 required=False,
151 allow_null=True
152 )
154 class Meta:
155 model = PowerPort
156 fields = [
157 'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'maximum_draw',
158 'allocated_draw', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
159 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'owner', 'tags',
160 'custom_fields', 'created', 'last_updated', '_occupied',
161 ]
162 brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
165class PowerOutletSerializer(
166 OwnerMixin,
167 NetBoxModelSerializer,
168 CabledObjectSerializer,
169 ConnectedEndpointsSerializer
170):
171 device = DeviceSerializer(nested=True)
172 module = ModuleSerializer(
173 nested=True,
174 fields=('id', 'url', 'display', 'device', 'module_bay'),
175 required=False,
176 allow_null=True
177 )
178 type = ChoiceField(
179 choices=PowerOutletTypeChoices,
180 allow_blank=True,
181 required=False,
182 allow_null=True
183 )
184 power_port = PowerPortSerializer(
185 nested=True,
186 required=False,
187 allow_null=True
188 )
189 feed_leg = ChoiceField(
190 choices=PowerOutletFeedLegChoices,
191 allow_blank=True,
192 required=False,
193 allow_null=True
194 )
195 status = ChoiceField(choices=PowerOutletStatusChoices, required=False)
197 class Meta:
198 model = PowerOutlet
199 fields = [
200 'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'status', 'color',
201 'power_port', 'feed_leg', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers',
202 'link_peers_type', 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable',
203 'owner', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
204 ]
205 brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
208class CoolingIntakeSerializer(OwnerMixin, NetBoxModelSerializer):
209 device = DeviceSerializer(nested=True)
210 module = ModuleSerializer(
211 nested=True,
212 fields=('id', 'url', 'display', 'device', 'module_bay'),
213 required=False,
214 allow_null=True
215 )
216 type = ChoiceField(
217 choices=CoolingConnectorTypeChoices,
218 allow_blank=True,
219 required=False,
220 allow_null=True
221 )
222 diameter_unit = ChoiceField(
223 choices=DiameterUnitChoices,
224 allow_blank=True,
225 required=False,
226 allow_null=True
227 )
228 max_flow_unit = ChoiceField(
229 choices=FlowRateUnitChoices,
230 allow_blank=True,
231 required=False,
232 allow_null=True
233 )
234 cooling_outflow = NestedCoolingOutflowSerializer(
235 required=False,
236 allow_null=True
237 )
239 class Meta:
240 model = CoolingIntake
241 fields = [
242 'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type',
243 'diameter', 'diameter_unit', 'max_flow', 'max_flow_unit', 'cooling_outflow',
244 'description', 'owner', 'tags', 'custom_fields', 'created', 'last_updated',
245 ]
246 brief_fields = ('id', 'url', 'display', 'device', 'name', 'description')
249class CoolingOutflowSerializer(OwnerMixin, NetBoxModelSerializer):
250 device = DeviceSerializer(nested=True)
251 module = ModuleSerializer(
252 nested=True,
253 fields=('id', 'url', 'display', 'device', 'module_bay'),
254 required=False,
255 allow_null=True
256 )
257 type = ChoiceField(
258 choices=CoolingConnectorTypeChoices,
259 allow_blank=True,
260 required=False,
261 allow_null=True
262 )
263 diameter_unit = ChoiceField(
264 choices=DiameterUnitChoices,
265 allow_blank=True,
266 required=False,
267 allow_null=True
268 )
269 cooling_intake = CoolingIntakeSerializer(
270 nested=True,
271 required=False,
272 allow_null=True
273 )
275 class Meta:
276 model = CoolingOutflow
277 fields = [
278 'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type',
279 'diameter', 'diameter_unit', 'cooling_intake', 'description', 'owner', 'tags', 'custom_fields',
280 'created', 'last_updated',
281 ]
282 brief_fields = ('id', 'url', 'display', 'device', 'name', 'description')
285class InterfaceSerializer(
286 MACAddressShortcutMixin,
287 OwnerMixin,
288 NetBoxModelSerializer,
289 CabledObjectSerializer,
290 ConnectedEndpointsSerializer
291):
292 device = DeviceSerializer(nested=True)
293 vdcs = SerializedPKRelatedField(
294 queryset=VirtualDeviceContext.objects.all(),
295 serializer=VirtualDeviceContextSerializer,
296 nested=True,
297 required=False,
298 many=True
299 )
300 module = ModuleSerializer(
301 nested=True,
302 fields=('id', 'url', 'display', 'device', 'module_bay'),
303 required=False,
304 allow_null=True
305 )
306 type = ChoiceField(choices=InterfaceTypeChoices)
307 parent = NestedInterfaceSerializer(required=False, allow_null=True)
308 bridge = NestedInterfaceSerializer(required=False, allow_null=True)
309 bridge_interfaces = NestedInterfaceSerializer(many=True, read_only=True)
310 lag = NestedInterfaceSerializer(required=False, allow_null=True)
311 mode = ChoiceField(choices=InterfaceModeChoices, required=False, allow_blank=True)
312 duplex = ChoiceField(choices=InterfaceDuplexChoices, required=False, allow_blank=True, allow_null=True)
313 rf_role = ChoiceField(choices=WirelessRoleChoices, required=False, allow_blank=True)
314 rf_channel = ChoiceField(choices=WirelessChannelChoices, required=False, allow_blank=True)
315 poe_mode = ChoiceField(choices=InterfacePoEModeChoices, required=False, allow_blank=True)
316 poe_type = ChoiceField(choices=InterfacePoETypeChoices, required=False, allow_blank=True)
317 untagged_vlan = VLANSerializer(nested=True, required=False, allow_null=True)
318 tagged_vlans = SerializedPKRelatedField(
319 queryset=VLAN.objects.all(),
320 serializer=VLANSerializer,
321 nested=True,
322 required=False,
323 many=True
324 )
325 qinq_svlan = VLANSerializer(nested=True, required=False, allow_null=True)
326 vlan_translation_policy = VLANTranslationPolicySerializer(nested=True, required=False, allow_null=True)
327 vrf = VRFSerializer(nested=True, required=False, allow_null=True)
328 l2vpn_termination = L2VPNTerminationSerializer(nested=True, read_only=True, allow_null=True)
329 wireless_link = NestedWirelessLinkSerializer(read_only=True, allow_null=True)
330 wireless_lans = SerializedPKRelatedField(
331 queryset=WirelessLAN.objects.all(),
332 serializer=WirelessLANSerializer,
333 nested=True,
334 required=False,
335 many=True
336 )
337 count_ipaddresses = serializers.IntegerField(read_only=True)
338 count_fhrp_groups = serializers.IntegerField(read_only=True)
339 # Maintains backward compatibility with NetBox <v4.2; also accepts a MAC string on write to
340 # create/update the primary MAC address in a single request.
341 mac_address = serializers.CharField(allow_null=True, required=False)
342 primary_mac_address = MACAddressSerializer(nested=True, required=False, allow_null=True)
343 mac_addresses = MACAddressSerializer(many=True, nested=True, read_only=True, allow_null=True)
344 wwn = serializers.CharField(required=False, default=None, allow_blank=True, allow_null=True)
346 class Meta:
347 model = Interface
348 fields = [
349 'id', 'url', 'display_url', 'display', 'device', 'vdcs', 'module', 'name', 'label', 'type', 'channels',
350 'channel_id', 'enabled',
351 'parent', 'bridge', 'bridge_interfaces', 'lag', 'mtu', 'mac_address', 'primary_mac_address',
352 'mac_addresses', 'speed', 'duplex', 'wwn', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel',
353 'poe_mode', 'poe_type', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'untagged_vlan',
354 'tagged_vlans', 'qinq_svlan', 'vlan_translation_policy', 'mark_connected', 'cable', 'cable_end',
355 'wireless_link', 'link_peers', 'link_peers_type', 'wireless_lans', 'vrf', 'l2vpn_termination',
356 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'owner', 'tags',
357 'custom_fields', 'created', 'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied',
358 ]
359 brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
361 def validate(self, data):
362 # Pop mac_address before model validation — it's a cached_property, not a model field,
363 # and passing it to Interface(**attrs) in ValidatedModelSerializer.validate() would raise TypeError.
364 # data may be an Interface instance (not a dict) in some custom field code paths (#18887).
365 mac_address = _UNSET
366 if isinstance(data, dict): 366 ↛ 368line 366 didn't jump to line 368 because the condition on line 366 was always true
367 mac_address = data.pop('mac_address', _UNSET)
368 self._validate_no_mac_conflict(data, mac_address)
370 if not self.nested and isinstance(data, dict): 370 ↛ 435line 370 didn't jump to line 435 because the condition on line 370 was always true
371 if mac_address not in (_UNSET, None): 371 ↛ 372line 371 didn't jump to line 372 because the condition on line 371 was never true
372 try:
373 EUI(mac_address, version=48)
374 except (AddrFormatError, ValueError, TypeError):
375 raise serializers.ValidationError({
376 'mac_address': _('Enter a valid MAC address (e.g. 00:11:22:33:44:55).')
377 })
379 # Validate 802.1q mode and vlan(s)
380 mode = None
381 tagged_vlans = []
383 # Gather Information
384 if self.instance:
385 mode = data.get('mode') if 'mode' in data.keys() else self.instance.mode
386 untagged_vlan = data.get('untagged_vlan') if 'untagged_vlan' in data.keys() else \
387 self.instance.untagged_vlan
388 qinq_svlan = data.get('qinq_svlan') if 'qinq_svlan' in data.keys() else \
389 self.instance.qinq_svlan
390 tagged_vlans = data.get('tagged_vlans') if 'tagged_vlans' in data.keys() else \
391 self.instance.tagged_vlans.all()
392 else:
393 mode = data.get('mode', None)
394 untagged_vlan = data.get('untagged_vlan') if 'untagged_vlan' in data.keys() else None
395 qinq_svlan = data.get('qinq_svlan') if 'qinq_svlan' in data.keys() else None
396 tagged_vlans = data.get('tagged_vlans') if 'tagged_vlans' in data.keys() else None
398 errors = {}
400 # Non Q-in-Q mode with service vlan set
401 if mode != InterfaceModeChoices.MODE_Q_IN_Q and qinq_svlan: 401 ↛ 402line 401 didn't jump to line 402 because the condition on line 401 was never true
402 errors.update({
403 'qinq_svlan': _("Interface mode does not support q-in-q service vlan")
404 })
405 # Routed mode
406 if not mode: 406 ↛ 418line 406 didn't jump to line 418 because the condition on line 406 was always true
407 # Untagged vlan
408 if untagged_vlan: 408 ↛ 409line 408 didn't jump to line 409 because the condition on line 408 was never true
409 errors.update({
410 'untagged_vlan': _("Interface mode does not support untagged vlan")
411 })
412 # Tagged vlan
413 if tagged_vlans: 413 ↛ 414line 413 didn't jump to line 414 because the condition on line 413 was never true
414 errors.update({
415 'tagged_vlans': _("Interface mode does not support tagged vlans")
416 })
417 # Non-tagged mode
418 elif mode in (InterfaceModeChoices.MODE_TAGGED_ALL, InterfaceModeChoices.MODE_ACCESS) and tagged_vlans:
419 errors.update({
420 'tagged_vlans': _("Interface mode does not support tagged vlans")
421 })
423 if errors: 423 ↛ 424line 423 didn't jump to line 424 because the condition on line 423 was never true
424 raise serializers.ValidationError(errors)
426 # Validate many-to-many VLAN assignments
427 device = self.instance.device if self.instance else data.get('device')
428 for vlan in data.get('tagged_vlans', []): 428 ↛ 429line 428 didn't jump to line 429 because the loop on line 428 never started
429 if vlan.site not in [device.site, None]:
430 raise serializers.ValidationError({
431 'tagged_vlans': f"VLAN {vlan} must belong to the same site as the interface's parent device, "
432 f"or it must be global."
433 })
435 data = super().validate(data)
437 if mac_address is not _UNSET: 437 ↛ 438line 437 didn't jump to line 438 because the condition on line 437 was never true
438 data['mac_address'] = mac_address
440 return data
443class RearPortMappingSerializer(serializers.ModelSerializer):
444 position = serializers.IntegerField(
445 source='rear_port_position'
446 )
447 front_port = serializers.PrimaryKeyRelatedField(
448 queryset=FrontPort.objects.all(),
449 )
451 class Meta:
452 model = PortMapping
453 fields = ('position', 'front_port', 'front_port_position')
456class RearPortSerializer(OwnerMixin, NetBoxModelSerializer, CabledObjectSerializer, PortSerializer):
457 device = DeviceSerializer(nested=True)
458 module = ModuleSerializer(
459 nested=True,
460 fields=('id', 'url', 'display', 'device', 'module_bay'),
461 required=False,
462 allow_null=True
463 )
464 type = ChoiceField(choices=PortTypeChoices)
465 front_ports = RearPortMappingSerializer(
466 source='mappings',
467 many=True,
468 required=False,
469 )
471 class Meta:
472 model = RearPort
473 fields = [
474 'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'positions',
475 'front_ports', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
476 'owner', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
477 ]
478 brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
481class FrontPortMappingSerializer(serializers.ModelSerializer):
482 position = serializers.IntegerField(
483 source='front_port_position'
484 )
485 rear_port = serializers.PrimaryKeyRelatedField(
486 queryset=RearPort.objects.all(),
487 )
489 class Meta:
490 model = PortMapping
491 fields = ('position', 'rear_port', 'rear_port_position')
494class FrontPortSerializer(OwnerMixin, NetBoxModelSerializer, CabledObjectSerializer, PortSerializer):
495 device = DeviceSerializer(nested=True)
496 module = ModuleSerializer(
497 nested=True,
498 fields=('id', 'url', 'display', 'device', 'module_bay'),
499 required=False,
500 allow_null=True
501 )
502 type = ChoiceField(choices=PortTypeChoices)
503 rear_ports = FrontPortMappingSerializer(
504 source='mappings',
505 many=True,
506 required=False,
507 )
509 class Meta:
510 model = FrontPort
511 fields = [
512 'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'positions',
513 'rear_ports', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
514 'owner', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
515 ]
516 brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
519class ModuleBaySerializer(OwnerMixin, NetBoxModelSerializer):
520 device = DeviceSerializer(nested=True)
521 module = ModuleSerializer(
522 nested=True,
523 fields=('id', 'url', 'display'),
524 required=False,
525 allow_null=True,
526 default=None
527 )
528 installed_module = ModuleSerializer(
529 nested=True,
530 fields=('id', 'url', 'display', 'serial', 'description'),
531 required=False,
532 allow_null=True
533 )
534 module_bay_types = SerializedPKRelatedField(
535 queryset=ModuleBayType.objects.all(),
536 serializer=ModuleBayTypeSerializer,
537 nested=True,
538 required=False,
539 many=True
540 )
541 _occupied = serializers.BooleanField(required=False, read_only=True)
542 is_module_compatible = serializers.BooleanField(read_only=True)
544 class Meta:
545 model = ModuleBay
546 fields = [
547 'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'position', 'enabled',
548 'description', 'module_bay_types', 'installed_module', 'owner', 'tags', 'custom_fields', 'created',
549 'last_updated', '_occupied', 'is_module_compatible',
550 ]
551 brief_fields = ('id', 'url', 'display', 'installed_module', 'name', 'enabled', 'description', '_occupied')
554class DeviceBaySerializer(OwnerMixin, NetBoxModelSerializer):
555 device = DeviceSerializer(nested=True)
556 installed_device = DeviceSerializer(nested=True, required=False, allow_null=True)
557 _occupied = serializers.BooleanField(required=False, read_only=True)
559 class Meta:
560 model = DeviceBay
561 fields = [
562 'id', 'url', 'display_url', 'display', 'device', 'name', 'label', 'enabled', 'description',
563 'installed_device', 'owner', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
564 ]
565 brief_fields = ('id', 'url', 'display', 'device', 'name', 'enabled', 'description', '_occupied',)
568class InventoryItemSerializer(OwnerMixin, NetBoxModelSerializer):
569 device = DeviceSerializer(nested=True)
570 parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
571 role = InventoryItemRoleSerializer(nested=True, required=False, allow_null=True)
572 manufacturer = ManufacturerSerializer(nested=True, required=False, allow_null=True, default=None)
573 component_type = ContentTypeField(
574 queryset=ContentType.objects.filter(MODULAR_COMPONENT_MODELS),
575 required=False,
576 allow_null=True
577 )
578 component = GFKSerializerField(read_only=True)
579 _depth = serializers.IntegerField(source='level', read_only=True)
580 status = ChoiceField(choices=InventoryItemStatusChoices, required=False)
582 class Meta:
583 model = InventoryItem
584 fields = [
585 'id', 'url', 'display_url', 'display', 'device', 'parent', 'name', 'label', 'status', 'role',
586 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description', 'component_type',
587 'component_id', 'component', 'owner', 'tags', 'custom_fields', 'created', 'last_updated', '_depth',
588 ]
589 brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', '_depth')