1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Minimal implemtnation of custom fields

This commit is contained in:
Jeremy Stretch
2016-08-15 15:24:23 -04:00
parent 550a05487d
commit 6cdb62b67e
32 changed files with 244 additions and 60 deletions

View File

@ -2,6 +2,7 @@ from django import forms
from django.db.models import Count from django.db.models import Count
from dcim.models import Site, Device, Interface, Rack, IFACE_FF_VIRTUAL from dcim.models import Site, Device, Interface, Rack, IFACE_FF_VIRTUAL
from extras.forms import CustomFieldForm
from tenancy.forms import bulkedit_tenant_choices from tenancy.forms import bulkedit_tenant_choices
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms import ( from utilities.forms import (
@ -15,7 +16,7 @@ from .models import Circuit, CircuitType, Provider
# Providers # Providers
# #
class ProviderForm(forms.ModelForm, BootstrapMixin): class ProviderForm(BootstrapMixin, CustomFieldForm):
slug = SlugField() slug = SlugField()
comments = CommentField() comments = CommentField()
@ -82,7 +83,7 @@ class CircuitTypeForm(forms.ModelForm, BootstrapMixin):
# Circuits # Circuits
# #
class CircuitForm(forms.ModelForm, BootstrapMixin): class CircuitForm(BootstrapMixin, CustomFieldForm):
site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.Select(attrs={'filter-for': 'rack'})) site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.Select(attrs={'filter-for': 'rack'}))
rack = forms.ModelChoiceField(queryset=Rack.objects.all(), required=False, label='Rack', rack = forms.ModelChoiceField(queryset=Rack.objects.all(), required=False, label='Rack',
widget=APISelect(api_url='/api/dcim/racks/?site_id={{site}}', widget=APISelect(api_url='/api/dcim/racks/?site_id={{site}}',

View File

@ -3,11 +3,12 @@ from django.db import models
from dcim.fields import ASNField from dcim.fields import ASNField
from dcim.models import Site, Interface from dcim.models import Site, Interface
from extras.models import CustomFieldModel
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.models import CreatedUpdatedModel from utilities.models import CreatedUpdatedModel
class Provider(CreatedUpdatedModel): class Provider(CreatedUpdatedModel, CustomFieldModel):
""" """
Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
stores information pertinent to the user's relationship with the Provider. stores information pertinent to the user's relationship with the Provider.
@ -58,7 +59,7 @@ class CircuitType(models.Model):
return "{}?type={}".format(reverse('circuits:circuit_list'), self.slug) return "{}?type={}".format(reverse('circuits:circuit_list'), self.slug)
class Circuit(CreatedUpdatedModel): class Circuit(CreatedUpdatedModel, CustomFieldModel):
""" """
A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
circuits. Each circuit is also assigned a CircuitType and a Site. A Circuit may be terminated to a specific device circuits. Each circuit is also assigned a CircuitType and a Site. A Circuit may be terminated to a specific device

View File

@ -165,7 +165,7 @@ class RackRoleForm(forms.ModelForm, BootstrapMixin):
# Racks # Racks
# #
class RackForm(forms.ModelForm, BootstrapMixin): class RackForm(BootstrapMixin, CustomFieldForm):
group = forms.ModelChoiceField(queryset=RackGroup.objects.all(), required=False, label='Group', widget=APISelect( group = forms.ModelChoiceField(queryset=RackGroup.objects.all(), required=False, label='Group', widget=APISelect(
api_url='/api/dcim/rack-groups/?site_id={{site}}', api_url='/api/dcim/rack-groups/?site_id={{site}}',
)) ))
@ -405,7 +405,7 @@ class PlatformForm(forms.ModelForm, BootstrapMixin):
# Devices # Devices
# #
class DeviceForm(forms.ModelForm, BootstrapMixin): class DeviceForm(BootstrapMixin, CustomFieldForm):
site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.Select(attrs={'filter-for': 'rack'})) site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.Select(attrs={'filter-for': 'rack'}))
rack = forms.ModelChoiceField(queryset=Rack.objects.all(), widget=APISelect( rack = forms.ModelChoiceField(queryset=Rack.objects.all(), widget=APISelect(
api_url='/api/dcim/racks/?site_id={{site}}', api_url='/api/dcim/racks/?site_id={{site}}',

View File

@ -1,12 +1,14 @@
from collections import OrderedDict from collections import OrderedDict
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import MultipleObjectsReturned, ValidationError from django.core.exceptions import MultipleObjectsReturned, ValidationError
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from django.db.models import Count, Q, ObjectDoesNotExist from django.db.models import Count, Q, ObjectDoesNotExist
from extras.models import CustomFieldModel, CustomField, CustomFieldValue
from extras.rpc import RPC_CLIENTS from extras.rpc import RPC_CLIENTS
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.fields import NullableCharField from utilities.fields import NullableCharField
@ -213,7 +215,7 @@ class SiteManager(NaturalOrderByManager):
return self.natural_order_by('name') return self.natural_order_by('name')
class Site(CreatedUpdatedModel): class Site(CreatedUpdatedModel, CustomFieldModel):
""" """
A Site represents a geographic location within a network; typically a building or campus. The optional facility A Site represents a geographic location within a network; typically a building or campus. The optional facility
field can be used to include an external designation, such as a data center name (e.g. Equinix SV6). field can be used to include an external designation, such as a data center name (e.g. Equinix SV6).
@ -320,7 +322,7 @@ class RackManager(NaturalOrderByManager):
return self.natural_order_by('site__name', 'name') return self.natural_order_by('site__name', 'name')
class Rack(CreatedUpdatedModel): class Rack(CreatedUpdatedModel, CustomFieldModel):
""" """
Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face. Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face.
Each Rack is assigned to a Site and (optionally) a RackGroup. Each Rack is assigned to a Site and (optionally) a RackGroup.
@ -719,7 +721,7 @@ class DeviceManager(NaturalOrderByManager):
return self.natural_order_by('name') return self.natural_order_by('name')
class Device(CreatedUpdatedModel): class Device(CreatedUpdatedModel, CustomFieldModel):
""" """
A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType, A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType,
DeviceRole, and (optionally) a Platform. Device names are not required, however if one is set it must be unique. DeviceRole, and (optionally) a Platform. Device names are not required, however if one is set it must be unique.

View File

@ -10,7 +10,10 @@ class CustomFieldChoiceAdmin(admin.TabularInline):
@admin.register(CustomField) @admin.register(CustomField)
class CustomFieldAdmin(admin.ModelAdmin): class CustomFieldAdmin(admin.ModelAdmin):
inlines = [CustomFieldChoiceAdmin] inlines = [CustomFieldChoiceAdmin]
list_display = ['name', 'type', 'required', 'default', 'description'] list_display = ['name', 'models', 'type', 'required', 'default', 'description']
def models(self, obj):
return ', '.join([ct.name for ct in obj.obj_type.all()])
@admin.register(Graph) @admin.register(Graph)

View File

@ -1,42 +1,38 @@
import six
from django import forms from django import forms
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from .models import CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_TEXT, CustomField from .models import CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CustomField, CustomFieldValue
class CustomFieldForm(forms.ModelForm): class CustomFieldForm(forms.ModelForm):
test_field = forms.IntegerField(widget=forms.HiddenInput())
custom_fields = [] custom_fields = []
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(CustomFieldForm, self).__init__(*args, **kwargs) super(CustomFieldForm, self).__init__(*args, **kwargs)
# Find all CustomFields for this model obj_type = ContentType.objects.get_for_model(self._meta.model)
model = self._meta.model
custom_fields = CustomField.objects.filter(obj_type=ContentType.objects.get_for_model(model))
# Find all CustomFields for this model
custom_fields = CustomField.objects.filter(obj_type=obj_type)
for cf in custom_fields: for cf in custom_fields:
field_name = 'cf_{}'.format(str(cf.name)) field_name = 'cf_{}'.format(str(cf.name))
# Integer # Integer
if cf.type == CF_TYPE_INTEGER: if cf.type == CF_TYPE_INTEGER:
field = forms.IntegerField(blank=not cf.required) field = forms.IntegerField(required=cf.required, initial=cf.default)
# Boolean # Boolean
elif cf.type == CF_TYPE_BOOLEAN: elif cf.type == CF_TYPE_BOOLEAN:
if cf.required: if cf.required:
field = forms.BooleanField(required=False) field = forms.BooleanField(required=False, initial=bool(cf.default))
else: else:
field = forms.NullBooleanField(required=False) field = forms.NullBooleanField(required=False, initial=bool(cf.default))
# Date # Date
elif cf.type == CF_TYPE_DATE: elif cf.type == CF_TYPE_DATE:
field = forms.DateField(blank=not cf.required) field = forms.DateField(required=cf.required, initial=cf.default)
# Select # Select
elif cf.type == CF_TYPE_SELECT: elif cf.type == CF_TYPE_SELECT:
@ -44,9 +40,50 @@ class CustomFieldForm(forms.ModelForm):
# Text # Text
else: else:
field = forms.CharField(max_length=100, blank=not cf.required) field = forms.CharField(max_length=100, required=cf.required, initial=cf.default)
field.model = cf
field.label = cf.label if cf.label else cf.name field.label = cf.label if cf.label else cf.name
field.help_text = cf.description field.help_text = cf.description
self.fields[field_name] = field self.fields[field_name] = field
self.custom_fields.append(field_name) self.custom_fields.append(field_name)
# If editing an existing object, initialize values for all custom fields
if self.instance.pk:
existing_values = CustomFieldValue.objects.filter(obj_type=obj_type, obj_id=self.instance.pk)\
.select_related('field')
for cfv in existing_values:
self.initial['cf_{}'.format(str(cfv.field.name))] = cfv.value
def _save_custom_fields(self):
if self.instance.pk:
obj_type = ContentType.objects.get_for_model(self.instance)
for field_name in self.custom_fields:
try:
cfv = CustomFieldValue.objects.get(field=self.fields[field_name].model, obj_type=obj_type,
obj_id=self.instance.pk)
except CustomFieldValue.DoesNotExist:
cfv = CustomFieldValue(
field=self.fields[field_name].model,
obj_type=obj_type,
obj_id=self.instance.pk
)
if cfv.pk and self.cleaned_data[field_name] is None:
cfv.delete()
elif self.cleaned_data[field_name] is not None:
cfv.value = self.cleaned_data[field_name]
cfv.save()
def save(self, commit=True):
obj = super(CustomFieldForm, self).save(commit)
# Handle custom fields the same way we do M2M fields
if commit:
self._save_custom_fields()
else:
self.save_custom_fields = self._save_custom_fields
return obj

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Generated by Django 1.10 on 2016-08-12 19:52 # Generated by Django 1.10 on 2016-08-15 19:18
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models
@ -20,11 +20,11 @@ class Migration(migrations.Migration):
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('type', models.PositiveSmallIntegerField(choices=[(100, b'Text'), (200, b'Integer'), (300, b'Boolean (true/false)'), (400, b'Date'), (500, b'Selection')], default=100)), ('type', models.PositiveSmallIntegerField(choices=[(100, b'Text'), (200, b'Integer'), (300, b'Boolean (true/false)'), (400, b'Date'), (500, b'Selection')], default=100)),
('name', models.CharField(max_length=50, unique=True)), ('name', models.CharField(max_length=50, unique=True)),
('label', models.CharField(help_text=b'Name of the field as displayed to users', max_length=50)), ('label', models.CharField(blank=True, help_text=b"Name of the field as displayed to users (if not provided, the field's name will be used)", max_length=50)),
('description', models.CharField(blank=True, max_length=100)), ('description', models.CharField(blank=True, max_length=100)),
('required', models.BooleanField(default=False, help_text=b'This field is required when creating new objects')), ('required', models.BooleanField(default=False, help_text=b'Determines whether this field is required when creating new objects or editing an existing object.')),
('default', models.CharField(blank=True, help_text=b'Default value for the field', max_length=100)), ('default', models.CharField(blank=True, help_text=b'Default value for the field. N/A for selection fields.', max_length=100)),
('obj_type', models.ManyToManyField(related_name='custom_fields', to='contenttypes.ContentType')), ('obj_type', models.ManyToManyField(help_text=b'The object(s) to which this field applies.', related_name='custom_fields', to='contenttypes.ContentType', verbose_name=b'Object(s)')),
], ],
options={ options={
'ordering': ['name'], 'ordering': ['name'],
@ -57,6 +57,10 @@ class Migration(migrations.Migration):
'ordering': ['obj_type', 'obj_id'], 'ordering': ['obj_type', 'obj_id'],
}, },
), ),
migrations.AlterUniqueTogether(
name='customfieldvalue',
unique_together=set([('field', 'obj_type', 'obj_id')]),
),
migrations.AlterUniqueTogether( migrations.AlterUniqueTogether(
name='customfieldchoice', name='customfieldchoice',
unique_together=set([('field', 'value')]), unique_together=set([('field', 'value')]),

View File

@ -7,9 +7,8 @@ from django.http import HttpResponse
from django.template import Template, Context from django.template import Template, Context
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from dcim.models import Site
# NOTE: Any model added here MUST have a GenericRelation defined for CustomField
CUSTOMFIELD_MODELS = ( CUSTOMFIELD_MODELS = (
'site', 'rack', 'device', # DCIM 'site', 'rack', 'device', # DCIM
'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', # IPAM 'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', # IPAM
@ -62,21 +61,42 @@ ACTION_CHOICES = (
) )
class CustomFieldModel(object):
def custom_fields(self):
# Find all custom fields applicable to this type of object
content_type = ContentType.objects.get_for_model(self)
fields = CustomField.objects.filter(obj_type=content_type)
# If the object exists, populate its custom fields with values
if hasattr(self, 'pk'):
values = CustomFieldValue.objects.filter(obj_type=content_type, obj_id=self.pk).select_related('field')
values_dict = {cfv.field_id: cfv.value for cfv in values}
return {field: values_dict.get(field.pk) for field in fields}
else:
return {field: None for field in fields}
class CustomField(models.Model): class CustomField(models.Model):
obj_type = models.ManyToManyField(ContentType, related_name='custom_fields', obj_type = models.ManyToManyField(ContentType, related_name='custom_fields', verbose_name='Object(s)',
limit_choices_to={'model__in': CUSTOMFIELD_MODELS}) limit_choices_to={'model__in': CUSTOMFIELD_MODELS},
help_text="The object(s) to which this field applies.")
type = models.PositiveSmallIntegerField(choices=CUSTOMFIELD_TYPE_CHOICES, default=CF_TYPE_TEXT) type = models.PositiveSmallIntegerField(choices=CUSTOMFIELD_TYPE_CHOICES, default=CF_TYPE_TEXT)
name = models.CharField(max_length=50, unique=True) name = models.CharField(max_length=50, unique=True)
label = models.CharField(max_length=50, blank=True, help_text="Name of the field as displayed to users") label = models.CharField(max_length=50, blank=True, help_text="Name of the field as displayed to users (if not "
"provided, the field's name will be used)")
description = models.CharField(max_length=100, blank=True) description = models.CharField(max_length=100, blank=True)
required = models.BooleanField(default=False, help_text="This field is required when creating new objects") required = models.BooleanField(default=False, help_text="Determines whether this field is required when creating "
default = models.CharField(max_length=100, blank=True, help_text="Default value for the field") "new objects or editing an existing object.")
default = models.CharField(max_length=100, blank=True, help_text="Default value for the field. N/A for selection "
"fields.")
class Meta: class Meta:
ordering = ['name'] ordering = ['name']
def __unicode__(self): def __unicode__(self):
return self.label or self.name return self.label or self.name.capitalize()
class CustomFieldValue(models.Model): class CustomFieldValue(models.Model):
@ -90,9 +110,10 @@ class CustomFieldValue(models.Model):
class Meta: class Meta:
ordering = ['obj_type', 'obj_id'] ordering = ['obj_type', 'obj_id']
unique_together = ['field', 'obj_type', 'obj_id']
def __unicode__(self): def __unicode__(self):
return self.value return '{} {}'.format(self.obj, self.field)
@property @property
def value(self): def value(self):
@ -103,17 +124,19 @@ class CustomFieldValue(models.Model):
if self.field.type == CF_TYPE_DATE: if self.field.type == CF_TYPE_DATE:
return self.val_date return self.val_date
if self.field.type == CF_TYPE_SELECT: if self.field.type == CF_TYPE_SELECT:
return CustomFieldChoice.objects.get(pk=self.val_int) return CustomFieldChoice.objects.get(pk=self.val_int) if self.val_int else None
return self.val_char return self.val_char
@value.setter @value.setter
def value(self, value): def value(self, value):
if self.field.type in [CF_TYPE_INTEGER, CF_TYPE_SELECT]: if self.field.type == CF_TYPE_INTEGER:
self.val_int = value self.val_int = value
elif self.field.type == CF_TYPE_BOOLEAN: elif self.field.type == CF_TYPE_BOOLEAN:
self.val_int = bool(value) if value else None self.val_int = bool(value) if value else None
elif self.field.type == CF_TYPE_DATE: elif self.field.type == CF_TYPE_DATE:
self.val_date = value self.val_date = value
elif self.field.type == CF_TYPE_SELECT:
self.val_int = value.id
else: else:
self.val_char = value self.val_char = value
@ -195,7 +218,7 @@ class ExportTemplate(models.Model):
class TopologyMap(models.Model): class TopologyMap(models.Model):
name = models.CharField(max_length=50, unique=True) name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True) slug = models.SlugField(unique=True)
site = models.ForeignKey(Site, related_name='topology_maps', blank=True, null=True) site = models.ForeignKey('dcim.Site', related_name='topology_maps', blank=True, null=True)
device_patterns = models.TextField(help_text="Identify devices to include in the diagram using regular expressions," device_patterns = models.TextField(help_text="Identify devices to include in the diagram using regular expressions,"
"one per line. Each line will result in a new tier of the drawing. " "one per line. Each line will result in a new tier of the drawing. "
"Separate multiple regexes on a line using commas. Devices will be " "Separate multiple regexes on a line using commas. Devices will be "

View File

@ -4,6 +4,7 @@ from django import forms
from django.db.models import Count from django.db.models import Count
from dcim.models import Site, Device, Interface from dcim.models import Site, Device, Interface
from extras.forms import CustomFieldForm
from tenancy.forms import bulkedit_tenant_choices from tenancy.forms import bulkedit_tenant_choices
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms import BootstrapMixin, APISelect, Livesearch, CSVDataField, BulkImportForm, SlugField from utilities.forms import BootstrapMixin, APISelect, Livesearch, CSVDataField, BulkImportForm, SlugField
@ -33,7 +34,7 @@ def bulkedit_vrf_choices():
# VRFs # VRFs
# #
class VRFForm(forms.ModelForm, BootstrapMixin): class VRFForm(BootstrapMixin, CustomFieldForm):
class Meta: class Meta:
model = VRF model = VRF
@ -91,7 +92,7 @@ class RIRForm(forms.ModelForm, BootstrapMixin):
# Aggregates # Aggregates
# #
class AggregateForm(forms.ModelForm, BootstrapMixin): class AggregateForm(BootstrapMixin, CustomFieldForm):
class Meta: class Meta:
model = Aggregate model = Aggregate
@ -149,7 +150,7 @@ class RoleForm(forms.ModelForm, BootstrapMixin):
# Prefixes # Prefixes
# #
class PrefixForm(forms.ModelForm, BootstrapMixin): class PrefixForm(BootstrapMixin, CustomFieldForm):
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label='Site', site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label='Site',
widget=forms.Select(attrs={'filter-for': 'vlan'})) widget=forms.Select(attrs={'filter-for': 'vlan'}))
vlan = forms.ModelChoiceField(queryset=VLAN.objects.all(), required=False, label='VLAN', vlan = forms.ModelChoiceField(queryset=VLAN.objects.all(), required=False, label='VLAN',
@ -309,7 +310,7 @@ class PrefixFilterForm(forms.Form, BootstrapMixin):
# IP addresses # IP addresses
# #
class IPAddressForm(forms.ModelForm, BootstrapMixin): class IPAddressForm(BootstrapMixin, CustomFieldForm):
nat_site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label='Site', nat_site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label='Site',
widget=forms.Select(attrs={'filter-for': 'nat_device'})) widget=forms.Select(attrs={'filter-for': 'nat_device'}))
nat_device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, label='Device', nat_device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, label='Device',
@ -478,7 +479,7 @@ class VLANGroupFilterForm(forms.Form, BootstrapMixin):
# VLANs # VLANs
# #
class VLANForm(forms.ModelForm, BootstrapMixin): class VLANForm(BootstrapMixin, CustomFieldForm):
group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False, label='Group', widget=APISelect( group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False, label='Group', widget=APISelect(
api_url='/api/ipam/vlan-groups/?site_id={{site}}', api_url='/api/ipam/vlan-groups/?site_id={{site}}',
)) ))

View File

@ -7,6 +7,7 @@ from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from dcim.models import Interface from dcim.models import Interface
from extras.models import CustomFieldModel
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.models import CreatedUpdatedModel from utilities.models import CreatedUpdatedModel
@ -39,7 +40,7 @@ STATUS_CHOICE_CLASSES = {
} }
class VRF(CreatedUpdatedModel): class VRF(CreatedUpdatedModel, CustomFieldModel):
""" """
A virtual routing and forwarding (VRF) table represents a discrete layer three forwarding domain (e.g. a routing A virtual routing and forwarding (VRF) table represents a discrete layer three forwarding domain (e.g. a routing
table). Prefixes and IPAddresses can optionally be assigned to VRFs. (Prefixes and IPAddresses not assigned to a VRF table). Prefixes and IPAddresses can optionally be assigned to VRFs. (Prefixes and IPAddresses not assigned to a VRF
@ -93,7 +94,7 @@ class RIR(models.Model):
return "{}?rir={}".format(reverse('ipam:aggregate_list'), self.slug) return "{}?rir={}".format(reverse('ipam:aggregate_list'), self.slug)
class Aggregate(CreatedUpdatedModel): class Aggregate(CreatedUpdatedModel, CustomFieldModel):
""" """
An aggregate exists at the root level of the IP address space hierarchy in NetBox. Aggregates are used to organize An aggregate exists at the root level of the IP address space hierarchy in NetBox. Aggregates are used to organize
the hierarchy and track the overall utilization of available address space. Each Aggregate is assigned to a RIR. the hierarchy and track the overall utilization of available address space. Each Aggregate is assigned to a RIR.
@ -222,7 +223,7 @@ class PrefixQuerySet(models.QuerySet):
return filter(lambda p: p.depth <= limit, queryset) return filter(lambda p: p.depth <= limit, queryset)
class Prefix(CreatedUpdatedModel): class Prefix(CreatedUpdatedModel, CustomFieldModel):
""" """
A Prefix represents an IPv4 or IPv6 network, including mask length. Prefixes can optionally be assigned to Sites and A Prefix represents an IPv4 or IPv6 network, including mask length. Prefixes can optionally be assigned to Sites and
VRFs. A Prefix must be assigned a status and may optionally be assigned a used-define Role. A Prefix can also be VRFs. A Prefix must be assigned a status and may optionally be assigned a used-define Role. A Prefix can also be
@ -295,7 +296,7 @@ class Prefix(CreatedUpdatedModel):
return STATUS_CHOICE_CLASSES[self.status] return STATUS_CHOICE_CLASSES[self.status]
class IPAddress(CreatedUpdatedModel): class IPAddress(CreatedUpdatedModel, CustomFieldModel):
""" """
An IPAddress represents an individual IPv4 or IPv6 address and its mask. The mask length should match what is An IPAddress represents an individual IPv4 or IPv6 address and its mask. The mask length should match what is
configured in the real world. (Typically, only loopback interfaces are configured with /32 or /128 masks.) Like configured in the real world. (Typically, only loopback interfaces are configured with /32 or /128 masks.) Like
@ -398,7 +399,7 @@ class VLANGroup(models.Model):
return "{}?group_id={}".format(reverse('ipam:vlan_list'), self.pk) return "{}?group_id={}".format(reverse('ipam:vlan_list'), self.pk)
class VLAN(CreatedUpdatedModel): class VLAN(CreatedUpdatedModel, CustomFieldModel):
""" """
A VLAN is a distinct layer two forwarding domain identified by a 12-bit integer (1-4094). Each VLAN must be assigned A VLAN is a distinct layer two forwarding domain identified by a 12-bit integer (1-4094). Each VLAN must be assigned
to a Site, however VLAN IDs need not be unique within a Site. A VLAN may optionally be assigned to a VLANGroup, to a Site, however VLAN IDs need not be unique within a Site. A VLAN may optionally be assigned to a VLANGroup,

View File

@ -112,6 +112,9 @@
</tr> </tr>
</table> </table>
</div> </div>
{% with circuit.custom_fields as custom_fields %}
{% include 'inc/custom_fields_panel.html' %}
{% endwith %}
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div class="panel panel-default"> <div class="panel panel-default">

View File

@ -23,6 +23,14 @@
{% render_field form.commit_rate %} {% render_field form.commit_rate %}
</div> </div>
</div> </div>
{% if form.custom_fields %}
<div class="panel panel-default">
<div class="panel-heading"><strong>Custom Fields</strong></div>
<div class="panel-body">
{% render_custom_fields form %}
</div>
</div>
{% endif %}
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"><strong>Termination</strong></div> <div class="panel-heading"><strong>Termination</strong></div>
<div class="panel-body"> <div class="panel-body">

View File

@ -113,6 +113,9 @@
</tr> </tr>
</table> </table>
</div> </div>
{% with provider.custom_fields as custom_fields %}
{% include 'inc/custom_fields_panel.html' %}
{% endwith %}
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<strong>Comments</strong> <strong>Comments</strong>

View File

@ -19,6 +19,14 @@
{% render_field form.admin_contact %} {% render_field form.admin_contact %}
</div> </div>
</div> </div>
{% if form.custom_fields %}
<div class="panel panel-default">
<div class="panel-heading"><strong>Custom Fields</strong></div>
<div class="panel-body">
{% render_custom_fields form %}
</div>
</div>
{% endif %}
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"><strong>Comments</strong></div> <div class="panel-heading"><strong>Comments</strong></div>
<div class="panel-body"> <div class="panel-body">

View File

@ -152,6 +152,9 @@
</tr> </tr>
</table> </table>
</div> </div>
{% with device.custom_fields as custom_fields %}
{% include 'inc/custom_fields_panel.html' %}
{% endwith %}
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">

View File

@ -63,6 +63,14 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
{% if form.custom_fields %}
<div class="panel panel-default">
<div class="panel-heading"><strong>Custom Fields</strong></div>
<div class="panel-body">
{% render_custom_fields form %}
</div>
</div>
{% endif %}
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"><strong>Comments</strong></div> <div class="panel-heading"><strong>Comments</strong></div>
<div class="panel-body"> <div class="panel-body">

View File

@ -140,6 +140,9 @@
</tr> </tr>
</table> </table>
</div> </div>
{% with rack.custom_fields as custom_fields %}
{% include 'inc/custom_fields_panel.html' %}
{% endwith %}
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<strong>Non-Racked Devices</strong> <strong>Non-Racked Devices</strong>

View File

@ -16,6 +16,14 @@
{% render_field form.u_height %} {% render_field form.u_height %}
</div> </div>
</div> </div>
{% if form.custom_fields %}
<div class="panel panel-default">
<div class="panel-heading"><strong>Custom Fields</strong></div>
<div class="panel-body">
{% render_custom_fields form %}
</div>
</div>
{% endif %}
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"><strong>Comments</strong></div> <div class="panel-heading"><strong>Comments</strong></div>
<div class="panel-body"> <div class="panel-body">

View File

@ -119,6 +119,9 @@
</tr> </tr>
</table> </table>
</div> </div>
{% with site.custom_fields as custom_fields %}
{% include 'inc/custom_fields_panel.html' %}
{% endwith %}
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<strong>Comments</strong> <strong>Comments</strong>

View File

@ -14,12 +14,14 @@
{% render_field form.shipping_address %} {% render_field form.shipping_address %}
</div> </div>
</div> </div>
<div class="panel panel-default"> {% if form.custom_fields %}
<div class="panel-heading"><strong>Custom Fields</strong></div> <div class="panel panel-default">
<div class="panel-body"> <div class="panel-heading"><strong>Custom Fields</strong></div>
{% render_custom_fields form %} <div class="panel-body">
{% render_custom_fields form %}
</div>
</div> </div>
</div> {% endif %}
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"><strong>Comments</strong></div> <div class="panel-heading"><strong>Comments</strong></div>
<div class="panel-body"> <div class="panel-body">

View File

@ -0,0 +1,23 @@
{% if custom_fields %}
<div class="panel panel-default">
<div class="panel-heading">
<strong>Custom Fields</strong>
</div>
<table class="table table-hover panel-body">
{% for field, value in custom_fields.items %}
<tr>
<td>{{ field }}</td>
<td>
{% if value %}
{{ value }}
{% elif field.required %}
<span class="text-warning">Not defined</span>
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
</div>
{% endif %}

View File

@ -88,6 +88,11 @@
</table> </table>
</div> </div>
</div> </div>
<div class="col-md-6">
{% with aggregate.custom_fields as custom_fields %}
{% include 'inc/custom_fields_panel.html' %}
{% endwith %}
</div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">

View File

@ -129,6 +129,9 @@
</tr> </tr>
</table> </table>
</div> </div>
{% with ipaddress.custom_fields as custom_fields %}
{% include 'inc/custom_fields_panel.html' %}
{% endwith %}
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
{% with heading='Parent Prefixes' %} {% with heading='Parent Prefixes' %}

View File

@ -51,6 +51,14 @@
{% render_field form.nat_inside %} {% render_field form.nat_inside %}
</div> </div>
</div> </div>
{% if form.custom_fields %}
<div class="panel panel-default">
<div class="panel-heading"><strong>Custom Fields</strong></div>
<div class="panel-body">
{% render_custom_fields form %}
</div>
</div>
{% endif %}
{% endblock %} {% endblock %}
{% block javascript %} {% block javascript %}

View File

@ -109,6 +109,9 @@
</tr> </tr>
</table> </table>
</div> </div>
{% with prefix.custom_fields as custom_fields %}
{% include 'inc/custom_fields_panel.html' %}
{% endwith %}
</div> </div>
<div class="col-md-7"> <div class="col-md-7">
{% if duplicate_prefix_table.rows %} {% if duplicate_prefix_table.rows %}

View File

@ -118,6 +118,9 @@
</tr> </tr>
</table> </table>
</div> </div>
{% with vlan.custom_fields as custom_fields %}
{% include 'inc/custom_fields_panel.html' %}
{% endwith %}
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div class="panel panel-default"> <div class="panel panel-default">

View File

@ -90,6 +90,9 @@
</tr> </tr>
</table> </table>
</div> </div>
{% with vrf.custom_fields as custom_fields %}
{% include 'inc/custom_fields_panel.html' %}
{% endwith %}
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div class="panel panel-default"> <div class="panel panel-default">

View File

@ -73,6 +73,9 @@
</tr> </tr>
</table> </table>
</div> </div>
{% with tenant.custom_fields as custom_fields %}
{% include 'inc/custom_fields_panel.html' %}
{% endwith %}
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<strong>Comments</strong> <strong>Comments</strong>

View File

@ -12,6 +12,14 @@
{% render_field form.description %} {% render_field form.description %}
</div> </div>
</div> </div>
{% if form.custom_fields %}
<div class="panel panel-default">
<div class="panel-heading"><strong>Custom Fields</strong></div>
<div class="panel-body">
{% render_custom_fields form %}
</div>
</div>
{% endif %}
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"><strong>Comments</strong></div> <div class="panel-heading"><strong>Comments</strong></div>
<div class="panel-body"> <div class="panel-body">

View File

@ -1,9 +1,8 @@
from django import forms from django import forms
from django.db.models import Count from django.db.models import Count
from utilities.forms import ( from extras.forms import CustomFieldForm
BootstrapMixin, BulkImportForm, CommentField, CSVDataField, SlugField, from utilities.forms import BootstrapMixin, BulkImportForm, CommentField, CSVDataField, SlugField
)
from .models import Tenant, TenantGroup from .models import Tenant, TenantGroup
@ -48,7 +47,7 @@ class TenantGroupForm(forms.ModelForm, BootstrapMixin):
# Tenants # Tenants
# #
class TenantForm(forms.ModelForm, BootstrapMixin): class TenantForm(BootstrapMixin, CustomFieldForm):
slug = SlugField() slug = SlugField()
comments = CommentField() comments = CommentField()

View File

@ -1,6 +1,7 @@
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db import models from django.db import models
from extras.models import CustomFieldModel
from utilities.models import CreatedUpdatedModel from utilities.models import CreatedUpdatedModel
@ -21,7 +22,7 @@ class TenantGroup(models.Model):
return "{}?group={}".format(reverse('tenancy:tenant_list'), self.slug) return "{}?group={}".format(reverse('tenancy:tenant_list'), self.slug)
class Tenant(CreatedUpdatedModel): class Tenant(CreatedUpdatedModel, CustomFieldModel):
""" """
A Tenant represents an organization served by the NetBox owner. This is typically a customer or an internal A Tenant represents an organization served by the NetBox owner. This is typically a customer or an internal
department. department.

View File

@ -15,6 +15,7 @@ from django.utils.decorators import method_decorator
from django.utils.http import is_safe_url from django.utils.http import is_safe_url
from django.views.generic import View from django.views.generic import View
from extras.forms import CustomFieldForm
from extras.models import ExportTemplate, UserAction from extras.models import ExportTemplate, UserAction
from .error_handlers import handle_protectederror from .error_handlers import handle_protectederror
@ -135,6 +136,8 @@ class ObjectEditView(View):
obj = form.save(commit=False) obj = form.save(commit=False)
obj_created = not obj.pk obj_created = not obj.pk
obj.save() obj.save()
if isinstance(form, CustomFieldForm):
form.save_custom_fields()
msg = u'Created ' if obj_created else u'Modified ' msg = u'Created ' if obj_created else u'Modified '
msg += self.model._meta.verbose_name msg += self.model._meta.verbose_name