diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py index ed6033963..45f9e1b10 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -2,6 +2,7 @@ from rest_framework import serializers from circuits.models import Provider, CircuitType, Circuit from dcim.api.serializers import SiteNestedSerializer, InterfaceNestedSerializer +from extras.api.serializers import CustomFieldsSerializer from tenancy.api.serializers import TenantNestedSerializer @@ -9,11 +10,12 @@ from tenancy.api.serializers import TenantNestedSerializer # Providers # -class ProviderSerializer(serializers.ModelSerializer): +class ProviderSerializer(CustomFieldsSerializer, serializers.ModelSerializer): class Meta: model = Provider - fields = ['id', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments'] + fields = ['id', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', + 'custom_fields'] class ProviderNestedSerializer(ProviderSerializer): @@ -43,7 +45,7 @@ class CircuitTypeNestedSerializer(CircuitTypeSerializer): # Circuits # -class CircuitSerializer(serializers.ModelSerializer): +class CircuitSerializer(CustomFieldsSerializer, serializers.ModelSerializer): provider = ProviderNestedSerializer() type = CircuitTypeNestedSerializer() tenant = TenantNestedSerializer() @@ -53,7 +55,7 @@ class CircuitSerializer(serializers.ModelSerializer): class Meta: model = Circuit fields = ['id', 'cid', 'provider', 'type', 'tenant', 'site', 'interface', 'install_date', 'port_speed', - 'upstream_speed', 'commit_rate', 'xconnect_id', 'comments'] + 'upstream_speed', 'commit_rate', 'xconnect_id', 'comments', 'custom_fields'] class CircuitNestedSerializer(CircuitSerializer): diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py index 74cc6656d..35b13e492 100644 --- a/netbox/circuits/api/views.py +++ b/netbox/circuits/api/views.py @@ -3,22 +3,23 @@ from rest_framework import generics from circuits.models import Provider, CircuitType, Circuit from circuits.filters import CircuitFilter +from extras.api.views import CustomFieldModelAPIView from . import serializers -class ProviderListView(generics.ListAPIView): +class ProviderListView(CustomFieldModelAPIView, generics.ListAPIView): """ List all providers """ - queryset = Provider.objects.all() + queryset = Provider.objects.prefetch_related('custom_field_values') serializer_class = serializers.ProviderSerializer -class ProviderDetailView(generics.RetrieveAPIView): +class ProviderDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView): """ Retrieve a single provider """ - queryset = Provider.objects.all() + queryset = Provider.objects.prefetch_related('custom_field_values') serializer_class = serializers.ProviderSerializer @@ -38,18 +39,20 @@ class CircuitTypeDetailView(generics.RetrieveAPIView): serializer_class = serializers.CircuitTypeSerializer -class CircuitListView(generics.ListAPIView): +class CircuitListView(CustomFieldModelAPIView, generics.ListAPIView): """ List circuits (filterable) """ - queryset = Circuit.objects.select_related('type', 'tenant', 'provider', 'site', 'interface__device') + queryset = Circuit.objects.select_related('type', 'tenant', 'provider', 'site', 'interface__device')\ + .prefetch_related('custom_field_values') serializer_class = serializers.CircuitSerializer filter_class = CircuitFilter -class CircuitDetailView(generics.RetrieveAPIView): +class CircuitDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView): """ Retrieve a single circuit """ - queryset = Circuit.objects.select_related('type', 'tenant', 'provider', 'site', 'interface__device') + queryset = Circuit.objects.select_related('type', 'tenant', 'provider', 'site', 'interface__device')\ + .prefetch_related('custom_field_values') serializer_class = serializers.CircuitSerializer diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py index 54afd5c29..fd4fdf634 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -1,9 +1,10 @@ +from django.contrib.contenttypes.fields import GenericRelation from django.core.urlresolvers import reverse from django.db import models from dcim.fields import ASNField from dcim.models import Site, Interface -from extras.models import CustomFieldModel +from extras.models import CustomFieldModel, CustomFieldValue from tenancy.models import Tenant from utilities.models import CreatedUpdatedModel @@ -21,6 +22,7 @@ class Provider(CreatedUpdatedModel, CustomFieldModel): noc_contact = models.TextField(blank=True, verbose_name='NOC contact') admin_contact = models.TextField(blank=True, verbose_name='Admin contact') comments = models.TextField(blank=True) + custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id') class Meta: ordering = ['name'] @@ -79,6 +81,7 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel): xconnect_id = models.CharField(max_length=50, blank=True, verbose_name='Cross-connect ID') pp_info = models.CharField(max_length=100, blank=True, verbose_name='Patch panel/port(s)') comments = models.TextField(blank=True) + custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id') class Meta: ordering = ['provider', 'cid'] diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 706bf4a52..786c8f717 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -6,6 +6,7 @@ from dcim.models import ( DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackRole, RACK_FACE_FRONT, RACK_FACE_REAR, Site, ) +from extras.api.serializers import CustomFieldsSerializer from tenancy.api.serializers import TenantNestedSerializer @@ -13,13 +14,13 @@ from tenancy.api.serializers import TenantNestedSerializer # Sites # -class SiteSerializer(serializers.ModelSerializer): +class SiteSerializer(CustomFieldsSerializer, serializers.ModelSerializer): tenant = TenantNestedSerializer() class Meta: model = Site fields = ['id', 'name', 'slug', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address', 'comments', - 'count_prefixes', 'count_vlans', 'count_racks', 'count_devices', 'count_circuits'] + 'custom_fields', 'count_prefixes', 'count_vlans', 'count_racks', 'count_devices', 'count_circuits'] class SiteNestedSerializer(SiteSerializer): @@ -68,7 +69,7 @@ class RackRoleNestedSerializer(RackRoleSerializer): # -class RackSerializer(serializers.ModelSerializer): +class RackSerializer(CustomFieldsSerializer, serializers.ModelSerializer): site = SiteNestedSerializer() group = RackGroupNestedSerializer() tenant = TenantNestedSerializer() @@ -77,7 +78,7 @@ class RackSerializer(serializers.ModelSerializer): class Meta: model = Rack fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'type', 'width', - 'u_height', 'comments'] + 'u_height', 'comments', 'custom_fields'] class RackNestedSerializer(RackSerializer): @@ -237,7 +238,7 @@ class DeviceIPAddressNestedSerializer(serializers.ModelSerializer): fields = ['id', 'family', 'address'] -class DeviceSerializer(serializers.ModelSerializer): +class DeviceSerializer(CustomFieldsSerializer, serializers.ModelSerializer): device_type = DeviceTypeNestedSerializer() device_role = DeviceRoleNestedSerializer() tenant = TenantNestedSerializer() @@ -252,7 +253,7 @@ class DeviceSerializer(serializers.ModelSerializer): model = Device fields = ['id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', - 'primary_ip6', 'comments'] + 'primary_ip6', 'comments', 'custom_fields'] def get_parent_device(self, obj): try: diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 5bde03a6e..193a16ad3 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -13,29 +13,30 @@ from dcim.models import ( InterfaceConnection, Manufacturer, Module, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackRole, Site, ) from dcim import filters -from .exceptions import MissingFilterException -from . import serializers +from extras.api.views import CustomFieldModelAPIView from extras.api.renderers import BINDZoneRenderer, FlatJSONRenderer from utilities.api import ServiceUnavailable +from .exceptions import MissingFilterException +from . import serializers # # Sites # -class SiteListView(generics.ListAPIView): +class SiteListView(CustomFieldModelAPIView, generics.ListAPIView): """ List all sites """ - queryset = Site.objects.select_related('tenant') + queryset = Site.objects.select_related('tenant').prefetch_related('custom_field_values') serializer_class = serializers.SiteSerializer -class SiteDetailView(generics.RetrieveAPIView): +class SiteDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView): """ Retrieve a single site """ - queryset = Site.objects.select_related('tenant') + queryset = Site.objects.select_related('tenant').prefetch_related('custom_field_values') serializer_class = serializers.SiteSerializer @@ -84,20 +85,20 @@ class RackRoleDetailView(generics.RetrieveAPIView): # Racks # -class RackListView(generics.ListAPIView): +class RackListView(CustomFieldModelAPIView, generics.ListAPIView): """ List racks (filterable) """ - queryset = Rack.objects.select_related('site', 'group', 'tenant') + queryset = Rack.objects.select_related('site', 'group__site', 'tenant').prefetch_related('custom_field_values') serializer_class = serializers.RackSerializer filter_class = filters.RackFilter -class RackDetailView(generics.RetrieveAPIView): +class RackDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView): """ Retrieve a single rack """ - queryset = Rack.objects.select_related('site', 'group', 'tenant') + queryset = Rack.objects.select_related('site', 'group__site', 'tenant').prefetch_related('custom_field_values') serializer_class = serializers.RackDetailSerializer @@ -209,24 +210,25 @@ class PlatformDetailView(generics.RetrieveAPIView): # Devices # -class DeviceListView(generics.ListAPIView): +class DeviceListView(CustomFieldModelAPIView, generics.ListAPIView): """ List devices (filterable) """ queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'tenant', 'platform', 'rack__site', 'parent_bay').prefetch_related('primary_ip4__nat_outside', - 'primary_ip6__nat_outside') + 'primary_ip6__nat_outside', + 'custom_field_values') serializer_class = serializers.DeviceSerializer filter_class = filters.DeviceFilter renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + [BINDZoneRenderer, FlatJSONRenderer] -class DeviceDetailView(generics.RetrieveAPIView): +class DeviceDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView): """ Retrieve a single device """ queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'tenant', 'platform', - 'rack__site', 'parent_bay') + 'rack__site', 'parent_bay').prefetch_related('custom_field_values') serializer_class = serializers.DeviceSerializer diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index a6bc733c5..b8185881f 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -2,6 +2,7 @@ from collections import OrderedDict from django.conf import settings from django.contrib.contenttypes.models import ContentType +from django.contrib.contenttypes.fields import GenericRelation from django.core.exceptions import MultipleObjectsReturned, ValidationError from django.core.urlresolvers import reverse from django.core.validators import MaxValueValidator, MinValueValidator @@ -228,6 +229,7 @@ class Site(CreatedUpdatedModel, CustomFieldModel): physical_address = models.CharField(max_length=200, blank=True) shipping_address = models.CharField(max_length=200, blank=True) comments = models.TextField(blank=True) + custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id') objects = SiteManager() @@ -339,6 +341,7 @@ class Rack(CreatedUpdatedModel, CustomFieldModel): u_height = models.PositiveSmallIntegerField(default=42, verbose_name='Height (U)', validators=[MinValueValidator(1), MaxValueValidator(100)]) comments = models.TextField(blank=True) + custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id') objects = RackManager() @@ -752,6 +755,7 @@ class Device(CreatedUpdatedModel, CustomFieldModel): primary_ip6 = models.OneToOneField('ipam.IPAddress', related_name='primary_ip6_for', on_delete=models.SET_NULL, blank=True, null=True, verbose_name='Primary IPv6') comments = models.TextField(blank=True) + custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id') objects = DeviceManager() diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index e0f24cfe7..a7dbeb5ed 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -1,6 +1,29 @@ from rest_framework import serializers -from extras.models import Graph +from extras.models import CF_TYPE_SELECT, CustomFieldChoice, Graph + + +class CustomFieldsSerializer(serializers.Serializer): + """ + Extends a ModelSerializer to render any CustomFields and their values associated with an object. + """ + custom_fields = serializers.SerializerMethodField() + + def get_custom_fields(self, obj): + fields = {cf.name: None for cf in self.context['view'].custom_fields} + for cfv in obj.custom_field_values.all(): + if cfv.field.type == CF_TYPE_SELECT: + fields[cfv.field.name] = CustomFieldChoiceSerializer(instance=cfv.value).data + else: + fields[cfv.field.name] = cfv.value + return fields + + +class CustomFieldChoiceSerializer(serializers.ModelSerializer): + + class Meta: + model = CustomFieldChoice + fields = ['id', 'value'] class GraphSerializer(serializers.ModelSerializer): diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index 81c4e9170..ee83ddf3d 100644 --- a/netbox/extras/api/views.py +++ b/netbox/extras/api/views.py @@ -1,9 +1,8 @@ import graphviz from rest_framework import generics from rest_framework.views import APIView -import tempfile -from wsgiref.util import FileWrapper +from django.contrib.contenttypes.models import ContentType from django.db.models import Q from django.http import Http404, HttpResponse from django.shortcuts import get_object_or_404 @@ -15,6 +14,17 @@ from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_P from .serializers import GraphSerializer +class CustomFieldModelAPIView(object): + """ + Include the applicable set of CustomField in the view context. + """ + + def __init__(self): + super(CustomFieldModelAPIView, self).__init__() + self.content_type = ContentType.objects.get_for_model(self.queryset.model) + self.custom_fields = self.content_type.custom_fields.all() + + class GraphListView(generics.ListAPIView): """ Returns a list of relevant graphs diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index a24e1454c..8d56d5516 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -1,6 +1,7 @@ from rest_framework import serializers from dcim.api.serializers import SiteNestedSerializer, InterfaceNestedSerializer +from extras.api.serializers import CustomFieldsSerializer from ipam.models import VRF, Role, RIR, Aggregate, Prefix, IPAddress, VLAN, VLANGroup from tenancy.api.serializers import TenantNestedSerializer @@ -9,12 +10,12 @@ from tenancy.api.serializers import TenantNestedSerializer # VRFs # -class VRFSerializer(serializers.ModelSerializer): +class VRFSerializer(CustomFieldsSerializer, serializers.ModelSerializer): tenant = TenantNestedSerializer() class Meta: model = VRF - fields = ['id', 'name', 'rd', 'tenant', 'enforce_unique', 'description'] + fields = ['id', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'custom_fields'] class VRFNestedSerializer(VRFSerializer): @@ -70,12 +71,12 @@ class RIRNestedSerializer(RIRSerializer): # Aggregates # -class AggregateSerializer(serializers.ModelSerializer): +class AggregateSerializer(CustomFieldsSerializer, serializers.ModelSerializer): rir = RIRNestedSerializer() class Meta: model = Aggregate - fields = ['id', 'family', 'prefix', 'rir', 'date_added', 'description'] + fields = ['id', 'family', 'prefix', 'rir', 'date_added', 'description', 'custom_fields'] class AggregateNestedSerializer(AggregateSerializer): @@ -106,7 +107,7 @@ class VLANGroupNestedSerializer(VLANGroupSerializer): # VLANs # -class VLANSerializer(serializers.ModelSerializer): +class VLANSerializer(CustomFieldsSerializer, serializers.ModelSerializer): site = SiteNestedSerializer() group = VLANGroupNestedSerializer() tenant = TenantNestedSerializer() @@ -114,7 +115,8 @@ class VLANSerializer(serializers.ModelSerializer): class Meta: model = VLAN - fields = ['id', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'display_name'] + fields = ['id', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'display_name', + 'custom_fields'] class VLANNestedSerializer(VLANSerializer): @@ -127,7 +129,7 @@ class VLANNestedSerializer(VLANSerializer): # Prefixes # -class PrefixSerializer(serializers.ModelSerializer): +class PrefixSerializer(CustomFieldsSerializer, serializers.ModelSerializer): site = SiteNestedSerializer() vrf = VRFTenantSerializer() tenant = TenantNestedSerializer() @@ -136,7 +138,8 @@ class PrefixSerializer(serializers.ModelSerializer): class Meta: model = Prefix - fields = ['id', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'description'] + fields = ['id', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'description', + 'custom_fields'] class PrefixNestedSerializer(PrefixSerializer): @@ -149,14 +152,15 @@ class PrefixNestedSerializer(PrefixSerializer): # IP addresses # -class IPAddressSerializer(serializers.ModelSerializer): +class IPAddressSerializer(CustomFieldsSerializer, serializers.ModelSerializer): vrf = VRFTenantSerializer() tenant = TenantNestedSerializer() interface = InterfaceNestedSerializer() class Meta: model = IPAddress - fields = ['id', 'family', 'address', 'vrf', 'tenant', 'interface', 'description', 'nat_inside', 'nat_outside'] + fields = ['id', 'family', 'address', 'vrf', 'tenant', 'interface', 'description', 'nat_inside', 'nat_outside', + 'custom_fields'] class IPAddressNestedSerializer(IPAddressSerializer): diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index 78d2b7f8e..5400331da 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -3,6 +3,7 @@ from rest_framework import generics from ipam.models import VRF, Role, RIR, Aggregate, Prefix, IPAddress, VLAN, VLANGroup from ipam import filters +from extras.api.views import CustomFieldModelAPIView from . import serializers @@ -10,20 +11,20 @@ from . import serializers # VRFs # -class VRFListView(generics.ListAPIView): +class VRFListView(CustomFieldModelAPIView, generics.ListAPIView): """ List all VRFs """ - queryset = VRF.objects.select_related('tenant') + queryset = VRF.objects.select_related('tenant').prefetch_related('custom_field_values') serializer_class = serializers.VRFSerializer filter_class = filters.VRFFilter -class VRFDetailView(generics.RetrieveAPIView): +class VRFDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView): """ Retrieve a single VRF """ - queryset = VRF.objects.select_related('tenant') + queryset = VRF.objects.select_related('tenant').prefetch_related('custom_field_values') serializer_class = serializers.VRFSerializer @@ -71,20 +72,20 @@ class RIRDetailView(generics.RetrieveAPIView): # Aggregates # -class AggregateListView(generics.ListAPIView): +class AggregateListView(CustomFieldModelAPIView, generics.ListAPIView): """ List aggregates (filterable) """ - queryset = Aggregate.objects.select_related('rir') + queryset = Aggregate.objects.select_related('rir').prefetch_related('custom_field_values') serializer_class = serializers.AggregateSerializer filter_class = filters.AggregateFilter -class AggregateDetailView(generics.RetrieveAPIView): +class AggregateDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView): """ Retrieve a single aggregate """ - queryset = Aggregate.objects.select_related('rir') + queryset = Aggregate.objects.select_related('rir').prefetch_related('custom_field_values') serializer_class = serializers.AggregateSerializer @@ -92,20 +93,22 @@ class AggregateDetailView(generics.RetrieveAPIView): # Prefixes # -class PrefixListView(generics.ListAPIView): +class PrefixListView(CustomFieldModelAPIView, generics.ListAPIView): """ List prefixes (filterable) """ - queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role') + queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')\ + .prefetch_related('custom_field_values') serializer_class = serializers.PrefixSerializer filter_class = filters.PrefixFilter -class PrefixDetailView(generics.RetrieveAPIView): +class PrefixDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView): """ Retrieve a single prefix """ - queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role') + queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')\ + .prefetch_related('custom_field_values') serializer_class = serializers.PrefixSerializer @@ -113,22 +116,22 @@ class PrefixDetailView(generics.RetrieveAPIView): # IP addresses # -class IPAddressListView(generics.ListAPIView): +class IPAddressListView(CustomFieldModelAPIView, generics.ListAPIView): """ List IP addresses (filterable) """ queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device', 'nat_inside')\ - .prefetch_related('nat_outside') + .prefetch_related('nat_outside', 'custom_field_values') serializer_class = serializers.IPAddressSerializer filter_class = filters.IPAddressFilter -class IPAddressDetailView(generics.RetrieveAPIView): +class IPAddressDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView): """ Retrieve a single IP address """ queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device', 'nat_inside')\ - .prefetch_related('nat_outside') + .prefetch_related('nat_outside', 'custom_field_values') serializer_class = serializers.IPAddressSerializer @@ -157,18 +160,18 @@ class VLANGroupDetailView(generics.RetrieveAPIView): # VLANs # -class VLANListView(generics.ListAPIView): +class VLANListView(CustomFieldModelAPIView, generics.ListAPIView): """ List VLANs (filterable) """ - queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role') + queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role').prefetch_related('custom_field_values') serializer_class = serializers.VLANSerializer filter_class = filters.VLANFilter -class VLANDetailView(generics.RetrieveAPIView): +class VLANDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView): """ Retrieve a single VLAN """ - queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role') + queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role').prefetch_related('custom_field_values') serializer_class = serializers.VLANSerializer diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index b9bf0e5fd..7a64705a3 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -1,6 +1,7 @@ from netaddr import IPNetwork, cidr_merge from django.conf import settings +from django.contrib.contenttypes.fields import GenericRelation from django.core.exceptions import ValidationError from django.core.urlresolvers import reverse from django.core.validators import MaxValueValidator, MinValueValidator @@ -8,7 +9,7 @@ from django.db import models from django.db.models.expressions import RawSQL from dcim.models import Interface -from extras.models import CustomFieldModel +from extras.models import CustomFieldModel, CustomFieldValue from tenancy.models import Tenant from utilities.models import CreatedUpdatedModel @@ -53,6 +54,7 @@ class VRF(CreatedUpdatedModel, CustomFieldModel): enforce_unique = models.BooleanField(default=True, verbose_name='Enforce unique space', help_text="Prevent duplicate prefixes/IP addresses within this VRF") description = models.CharField(max_length=100, blank=True) + custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id') class Meta: ordering = ['name'] @@ -105,6 +107,7 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel): rir = models.ForeignKey('RIR', related_name='aggregates', on_delete=models.PROTECT, verbose_name='RIR') date_added = models.DateField(blank=True, null=True) description = models.CharField(max_length=100, blank=True) + custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id') class Meta: ordering = ['family', 'prefix'] @@ -241,6 +244,7 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel): status = models.PositiveSmallIntegerField('Status', choices=PREFIX_STATUS_CHOICES, default=1) role = models.ForeignKey('Role', related_name='prefixes', on_delete=models.SET_NULL, blank=True, null=True) description = models.CharField(max_length=100, blank=True) + custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id') objects = PrefixQuerySet.as_manager() @@ -332,6 +336,7 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel): nat_inside = models.OneToOneField('self', related_name='nat_outside', on_delete=models.SET_NULL, blank=True, null=True, verbose_name='NAT IP (inside)') description = models.CharField(max_length=100, blank=True) + custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id') objects = IPAddressManager() @@ -436,6 +441,7 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel): status = models.PositiveSmallIntegerField('Status', choices=VLAN_STATUS_CHOICES, default=1) role = models.ForeignKey('Role', related_name='vlans', on_delete=models.SET_NULL, blank=True, null=True) description = models.CharField(max_length=100, blank=True) + custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id') class Meta: ordering = ['site', 'group', 'vid'] diff --git a/netbox/tenancy/api/serializers.py b/netbox/tenancy/api/serializers.py index 30a4a3ca1..3e8d019e7 100644 --- a/netbox/tenancy/api/serializers.py +++ b/netbox/tenancy/api/serializers.py @@ -1,5 +1,6 @@ from rest_framework import serializers +from extras.api.serializers import CustomFieldsSerializer from tenancy.models import Tenant, TenantGroup @@ -24,12 +25,12 @@ class TenantGroupNestedSerializer(TenantGroupSerializer): # Tenants # -class TenantSerializer(serializers.ModelSerializer): +class TenantSerializer(CustomFieldsSerializer, serializers.ModelSerializer): group = TenantGroupNestedSerializer() class Meta: model = Tenant - fields = ['id', 'name', 'slug', 'group', 'comments'] + fields = ['id', 'name', 'slug', 'group', 'comments', 'custom_fields'] class TenantNestedSerializer(TenantSerializer): diff --git a/netbox/tenancy/api/views.py b/netbox/tenancy/api/views.py index ca8ab11d7..963af6e44 100644 --- a/netbox/tenancy/api/views.py +++ b/netbox/tenancy/api/views.py @@ -3,6 +3,7 @@ from rest_framework import generics from tenancy.models import Tenant, TenantGroup from tenancy.filters import TenantFilter +from extras.api.views import CustomFieldModelAPIView from . import serializers @@ -22,18 +23,18 @@ class TenantGroupDetailView(generics.RetrieveAPIView): serializer_class = serializers.TenantGroupSerializer -class TenantListView(generics.ListAPIView): +class TenantListView(CustomFieldModelAPIView, generics.ListAPIView): """ List tenants (filterable) """ - queryset = Tenant.objects.select_related('group') + queryset = Tenant.objects.select_related('group').prefetch_related('custom_field_values') serializer_class = serializers.TenantSerializer filter_class = TenantFilter -class TenantDetailView(generics.RetrieveAPIView): +class TenantDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView): """ Retrieve a single tenant """ - queryset = Tenant.objects.select_related('group') + queryset = Tenant.objects.select_related('group').prefetch_related('custom_field_values') serializer_class = serializers.TenantSerializer diff --git a/netbox/tenancy/models.py b/netbox/tenancy/models.py index 4c914dc70..8c1bf5fa4 100644 --- a/netbox/tenancy/models.py +++ b/netbox/tenancy/models.py @@ -1,7 +1,8 @@ +from django.contrib.contenttypes.fields import GenericRelation from django.core.urlresolvers import reverse from django.db import models -from extras.models import CustomFieldModel +from extras.models import CustomFieldModel, CustomFieldValue from utilities.models import CreatedUpdatedModel @@ -32,6 +33,7 @@ class Tenant(CreatedUpdatedModel, CustomFieldModel): group = models.ForeignKey('TenantGroup', related_name='tenants', blank=True, null=True, on_delete=models.SET_NULL) description = models.CharField(max_length=100, blank=True, help_text="Long-form name (optional)") comments = models.TextField(blank=True) + custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id') class Meta: ordering = ['group', 'name']