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:
@ -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}}',
|
||||||
|
@ -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
|
||||||
|
@ -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}}',
|
||||||
|
@ -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.
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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')]),
|
||||||
|
@ -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 "
|
||||||
|
@ -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}}',
|
||||||
))
|
))
|
||||||
|
@ -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,
|
||||||
|
@ -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">
|
||||||
|
@ -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">
|
||||||
|
@ -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>
|
||||||
|
@ -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">
|
||||||
|
@ -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">
|
||||||
|
@ -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">
|
||||||
|
@ -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>
|
||||||
|
@ -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">
|
||||||
|
@ -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>
|
||||||
|
@ -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">
|
||||||
|
23
netbox/templates/inc/custom_fields_panel.html
Normal file
23
netbox/templates/inc/custom_fields_panel.html
Normal 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 %}
|
@ -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">
|
||||||
|
@ -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' %}
|
||||||
|
@ -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 %}
|
||||||
|
@ -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 %}
|
||||||
|
@ -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">
|
||||||
|
@ -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">
|
||||||
|
@ -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>
|
||||||
|
@ -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">
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
|
Reference in New Issue
Block a user