mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Merge pull request #4050 from netbox-community/568-customfield-csv-import
Closes #568: Extend CSV import to support custom fields
This commit is contained in:
@ -1,5 +1,9 @@
|
|||||||
# v2.7.4 (FUTURE)
|
# v2.7.4 (FUTURE)
|
||||||
|
|
||||||
|
## Enhancements
|
||||||
|
|
||||||
|
* [#568](https://github.com/netbox-community/netbox/issues/568) - Allow custom fields to be imported and exported using CSV
|
||||||
|
|
||||||
## Bug Fixes
|
## Bug Fixes
|
||||||
|
|
||||||
* [#4043](https://github.com/netbox-community/netbox/issues/4043) - Fix toggling of required fields in custom scripts
|
* [#4043](https://github.com/netbox-community/netbox/issues/4043) - Fix toggling of required fields in custom scripts
|
||||||
|
@ -2,7 +2,9 @@ from django import forms
|
|||||||
from taggit.forms import TagField
|
from taggit.forms import TagField
|
||||||
|
|
||||||
from dcim.models import Region, Site
|
from dcim.models import Region, Site
|
||||||
from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
from extras.forms import (
|
||||||
|
AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldModelForm, CustomFieldModelCSVForm,
|
||||||
|
)
|
||||||
from tenancy.forms import TenancyFilterForm, TenancyForm
|
from tenancy.forms import TenancyFilterForm, TenancyForm
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
@ -17,7 +19,7 @@ from .models import Circuit, CircuitTermination, CircuitType, Provider
|
|||||||
# Providers
|
# Providers
|
||||||
#
|
#
|
||||||
|
|
||||||
class ProviderForm(BootstrapMixin, CustomFieldForm):
|
class ProviderForm(BootstrapMixin, CustomFieldModelForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
tags = TagField(
|
tags = TagField(
|
||||||
@ -46,7 +48,7 @@ class ProviderForm(BootstrapMixin, CustomFieldForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ProviderCSVForm(forms.ModelForm):
|
class ProviderCSVForm(CustomFieldModelCSVForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -160,7 +162,7 @@ class CircuitTypeCSVForm(forms.ModelForm):
|
|||||||
# Circuits
|
# Circuits
|
||||||
#
|
#
|
||||||
|
|
||||||
class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
tags = TagField(
|
tags = TagField(
|
||||||
required=False
|
required=False
|
||||||
@ -188,7 +190,7 @@ class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class CircuitCSVForm(forms.ModelForm):
|
class CircuitCSVForm(CustomFieldModelCSVForm):
|
||||||
provider = forms.ModelChoiceField(
|
provider = forms.ModelChoiceField(
|
||||||
queryset=Provider.objects.all(),
|
queryset=Provider.objects.all(),
|
||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
|
@ -13,7 +13,8 @@ from timezone_field import TimeZoneFormField
|
|||||||
|
|
||||||
from circuits.models import Circuit, Provider
|
from circuits.models import Circuit, Provider
|
||||||
from extras.forms import (
|
from extras.forms import (
|
||||||
AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm, LocalConfigContextFilterForm
|
AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldModelCSVForm, CustomFieldFilterForm, CustomFieldModelForm,
|
||||||
|
LocalConfigContextFilterForm,
|
||||||
)
|
)
|
||||||
from ipam.constants import BGP_ASN_MAX, BGP_ASN_MIN
|
from ipam.constants import BGP_ASN_MAX, BGP_ASN_MIN
|
||||||
from ipam.models import IPAddress, VLAN
|
from ipam.models import IPAddress, VLAN
|
||||||
@ -215,7 +216,7 @@ class RegionFilterForm(BootstrapMixin, forms.Form):
|
|||||||
# Sites
|
# Sites
|
||||||
#
|
#
|
||||||
|
|
||||||
class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
class SiteForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||||
region = TreeNodeChoiceField(
|
region = TreeNodeChoiceField(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
@ -263,7 +264,7 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class SiteCSVForm(forms.ModelForm):
|
class SiteCSVForm(CustomFieldModelCSVForm):
|
||||||
status = CSVChoiceField(
|
status = CSVChoiceField(
|
||||||
choices=SiteStatusChoices,
|
choices=SiteStatusChoices,
|
||||||
required=False,
|
required=False,
|
||||||
@ -459,7 +460,7 @@ class RackRoleCSVForm(forms.ModelForm):
|
|||||||
# Racks
|
# Racks
|
||||||
#
|
#
|
||||||
|
|
||||||
class RackForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
class RackForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||||
group = ChainedModelChoiceField(
|
group = ChainedModelChoiceField(
|
||||||
queryset=RackGroup.objects.all(),
|
queryset=RackGroup.objects.all(),
|
||||||
chains=(
|
chains=(
|
||||||
@ -504,7 +505,7 @@ class RackForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class RackCSVForm(forms.ModelForm):
|
class RackCSVForm(CustomFieldModelCSVForm):
|
||||||
site = forms.ModelChoiceField(
|
site = forms.ModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
@ -897,7 +898,7 @@ class ManufacturerCSVForm(forms.ModelForm):
|
|||||||
# Device types
|
# Device types
|
||||||
#
|
#
|
||||||
|
|
||||||
class DeviceTypeForm(BootstrapMixin, CustomFieldForm):
|
class DeviceTypeForm(BootstrapMixin, CustomFieldModelForm):
|
||||||
slug = SlugField(
|
slug = SlugField(
|
||||||
slug_source='model'
|
slug_source='model'
|
||||||
)
|
)
|
||||||
@ -1516,7 +1517,7 @@ class PlatformCSVForm(forms.ModelForm):
|
|||||||
# Devices
|
# Devices
|
||||||
#
|
#
|
||||||
|
|
||||||
class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||||
site = forms.ModelChoiceField(
|
site = forms.ModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
@ -1724,7 +1725,7 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
self.initial['rack'] = self.instance.parent_bay.device.rack_id
|
self.initial['rack'] = self.instance.parent_bay.device.rack_id
|
||||||
|
|
||||||
|
|
||||||
class BaseDeviceCSVForm(forms.ModelForm):
|
class BaseDeviceCSVForm(CustomFieldModelCSVForm):
|
||||||
device_role = forms.ModelChoiceField(
|
device_role = forms.ModelChoiceField(
|
||||||
queryset=DeviceRole.objects.all(),
|
queryset=DeviceRole.objects.all(),
|
||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
@ -4241,7 +4242,7 @@ class PowerPanelFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
# Power feeds
|
# Power feeds
|
||||||
#
|
#
|
||||||
|
|
||||||
class PowerFeedForm(BootstrapMixin, CustomFieldForm):
|
class PowerFeedForm(BootstrapMixin, CustomFieldModelForm):
|
||||||
site = ChainedModelChoiceField(
|
site = ChainedModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
@ -4286,7 +4287,7 @@ class PowerFeedForm(BootstrapMixin, CustomFieldForm):
|
|||||||
self.initial['site'] = self.instance.power_panel.site
|
self.initial['site'] = self.instance.power_panel.site
|
||||||
|
|
||||||
|
|
||||||
class PowerFeedCSVForm(forms.ModelForm):
|
class PowerFeedCSVForm(CustomFieldModelCSVForm):
|
||||||
site = forms.ModelChoiceField(
|
site = forms.ModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
|
@ -1,17 +1,14 @@
|
|||||||
from collections import OrderedDict
|
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
|
||||||
from taggit.forms import TagField
|
from taggit.forms import TagField
|
||||||
|
|
||||||
from dcim.models import DeviceRole, Platform, Region, Site
|
from dcim.models import DeviceRole, Platform, Region, Site
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect,
|
add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect,
|
||||||
CommentField, ContentTypeSelect, DatePicker, DateTimePicker, FilterChoiceField, LaxURLField, JSONField,
|
CommentField, ContentTypeSelect, DateTimePicker, FilterChoiceField, JSONField, SlugField, StaticSelect2,
|
||||||
SlugField, StaticSelect2, BOOLEAN_WITH_BLANK_CHOICES,
|
BOOLEAN_WITH_BLANK_CHOICES,
|
||||||
)
|
)
|
||||||
from .choices import *
|
from .choices import *
|
||||||
from .models import ConfigContext, CustomField, CustomFieldValue, ImageAttachment, ObjectChange, Tag
|
from .models import ConfigContext, CustomField, CustomFieldValue, ImageAttachment, ObjectChange, Tag
|
||||||
@ -21,102 +18,41 @@ from .models import ConfigContext, CustomField, CustomFieldValue, ImageAttachmen
|
|||||||
# Custom fields
|
# Custom fields
|
||||||
#
|
#
|
||||||
|
|
||||||
def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=False):
|
class CustomFieldModelForm(forms.ModelForm):
|
||||||
"""
|
|
||||||
Retrieve all CustomFields applicable to the given ContentType
|
|
||||||
"""
|
|
||||||
field_dict = OrderedDict()
|
|
||||||
custom_fields = CustomField.objects.filter(obj_type=content_type)
|
|
||||||
if filterable_only:
|
|
||||||
custom_fields = custom_fields.exclude(filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED)
|
|
||||||
|
|
||||||
for cf in custom_fields:
|
|
||||||
field_name = 'cf_{}'.format(str(cf.name))
|
|
||||||
initial = cf.default if not bulk_edit else None
|
|
||||||
|
|
||||||
# Integer
|
|
||||||
if cf.type == CustomFieldTypeChoices.TYPE_INTEGER:
|
|
||||||
field = forms.IntegerField(required=cf.required, initial=initial)
|
|
||||||
|
|
||||||
# Boolean
|
|
||||||
elif cf.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
|
|
||||||
choices = (
|
|
||||||
(None, '---------'),
|
|
||||||
(1, 'True'),
|
|
||||||
(0, 'False'),
|
|
||||||
)
|
|
||||||
if initial is not None and initial.lower() in ['true', 'yes', '1']:
|
|
||||||
initial = 1
|
|
||||||
elif initial is not None and initial.lower() in ['false', 'no', '0']:
|
|
||||||
initial = 0
|
|
||||||
else:
|
|
||||||
initial = None
|
|
||||||
field = forms.NullBooleanField(
|
|
||||||
required=cf.required, initial=initial, widget=StaticSelect2(choices=choices)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Date
|
|
||||||
elif cf.type == CustomFieldTypeChoices.TYPE_DATE:
|
|
||||||
field = forms.DateField(required=cf.required, initial=initial, widget=DatePicker())
|
|
||||||
|
|
||||||
# Select
|
|
||||||
elif cf.type == CustomFieldTypeChoices.TYPE_SELECT:
|
|
||||||
choices = [(cfc.pk, cfc) for cfc in cf.choices.all()]
|
|
||||||
if not cf.required or bulk_edit or filterable_only:
|
|
||||||
choices = [(None, '---------')] + choices
|
|
||||||
# Check for a default choice
|
|
||||||
default_choice = None
|
|
||||||
if initial:
|
|
||||||
try:
|
|
||||||
default_choice = cf.choices.get(value=initial).pk
|
|
||||||
except ObjectDoesNotExist:
|
|
||||||
pass
|
|
||||||
field = forms.TypedChoiceField(
|
|
||||||
choices=choices, coerce=int, required=cf.required, initial=default_choice, widget=StaticSelect2()
|
|
||||||
)
|
|
||||||
|
|
||||||
# URL
|
|
||||||
elif cf.type == CustomFieldTypeChoices.TYPE_URL:
|
|
||||||
field = LaxURLField(required=cf.required, initial=initial)
|
|
||||||
|
|
||||||
# Text
|
|
||||||
else:
|
|
||||||
field = forms.CharField(max_length=255, required=cf.required, initial=initial)
|
|
||||||
|
|
||||||
field.model = cf
|
|
||||||
field.label = cf.label if cf.label else cf.name.replace('_', ' ').capitalize()
|
|
||||||
if cf.description:
|
|
||||||
field.help_text = cf.description
|
|
||||||
|
|
||||||
field_dict[field_name] = field
|
|
||||||
|
|
||||||
return field_dict
|
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldForm(forms.ModelForm):
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
self.custom_fields = []
|
|
||||||
self.obj_type = ContentType.objects.get_for_model(self._meta.model)
|
self.obj_type = ContentType.objects.get_for_model(self._meta.model)
|
||||||
|
self.custom_fields = []
|
||||||
|
self.custom_field_values = {}
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Add all applicable CustomFields to the form
|
self._append_customfield_fields()
|
||||||
custom_fields = []
|
|
||||||
for name, field in get_custom_fields_for_model(self.obj_type).items():
|
|
||||||
self.fields[name] = field
|
|
||||||
custom_fields.append(name)
|
|
||||||
self.custom_fields = custom_fields
|
|
||||||
|
|
||||||
# If editing an existing object, initialize values for all custom fields
|
def _append_customfield_fields(self):
|
||||||
|
"""
|
||||||
|
Append form fields for all CustomFields assigned to this model.
|
||||||
|
"""
|
||||||
|
# Retrieve initial CustomField values for the instance
|
||||||
if self.instance.pk:
|
if self.instance.pk:
|
||||||
existing_values = CustomFieldValue.objects.filter(
|
for cfv in CustomFieldValue.objects.filter(
|
||||||
obj_type=self.obj_type,
|
obj_type=self.obj_type,
|
||||||
obj_id=self.instance.pk
|
obj_id=self.instance.pk
|
||||||
).prefetch_related('field')
|
).prefetch_related('field'):
|
||||||
for cfv in existing_values:
|
self.custom_field_values[cfv.field.name] = cfv.serialized_value
|
||||||
self.initial['cf_{}'.format(str(cfv.field.name))] = cfv.serialized_value
|
|
||||||
|
# Append form fields; assign initial values if modifying and existing object
|
||||||
|
for cf in CustomField.objects.filter(obj_type=self.obj_type):
|
||||||
|
field_name = 'cf_{}'.format(cf.name)
|
||||||
|
if self.instance.pk:
|
||||||
|
self.fields[field_name] = cf.to_form_field(set_initial=False)
|
||||||
|
self.fields[field_name].initial = self.custom_field_values.get(cf.name)
|
||||||
|
else:
|
||||||
|
self.fields[field_name] = cf.to_form_field()
|
||||||
|
|
||||||
|
# Annotate the field in the list of CustomField form fields
|
||||||
|
self.custom_fields.append(field_name)
|
||||||
|
|
||||||
def _save_custom_fields(self):
|
def _save_custom_fields(self):
|
||||||
|
|
||||||
@ -151,6 +87,19 @@ class CustomFieldForm(forms.ModelForm):
|
|||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
class CustomFieldModelCSVForm(CustomFieldModelForm):
|
||||||
|
|
||||||
|
def _append_customfield_fields(self):
|
||||||
|
|
||||||
|
# Append form fields
|
||||||
|
for cf in CustomField.objects.filter(obj_type=self.obj_type):
|
||||||
|
field_name = 'cf_{}'.format(cf.name)
|
||||||
|
self.fields[field_name] = cf.to_form_field(for_csv_import=True)
|
||||||
|
|
||||||
|
# Annotate the field in the list of CustomField form fields
|
||||||
|
self.custom_fields.append(field_name)
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldBulkEditForm(BulkEditForm):
|
class CustomFieldBulkEditForm(BulkEditForm):
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@ -160,15 +109,14 @@ class CustomFieldBulkEditForm(BulkEditForm):
|
|||||||
self.obj_type = ContentType.objects.get_for_model(self.model)
|
self.obj_type = ContentType.objects.get_for_model(self.model)
|
||||||
|
|
||||||
# Add all applicable CustomFields to the form
|
# Add all applicable CustomFields to the form
|
||||||
custom_fields = get_custom_fields_for_model(self.obj_type, bulk_edit=True).items()
|
custom_fields = CustomField.objects.filter(obj_type=self.obj_type)
|
||||||
for name, field in custom_fields:
|
for cf in custom_fields:
|
||||||
# Annotate non-required custom fields as nullable
|
# Annotate non-required custom fields as nullable
|
||||||
if not field.required:
|
if not cf.required:
|
||||||
self.nullable_fields.append(name)
|
self.nullable_fields.append(cf.name)
|
||||||
field.required = False
|
self.fields[cf.name] = cf.to_form_field(set_initial=False, enforce_required=False)
|
||||||
self.fields[name] = field
|
|
||||||
# Annotate this as a custom field
|
# Annotate this as a custom field
|
||||||
self.custom_fields.append(name)
|
self.custom_fields.append(cf.name)
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldFilterForm(forms.Form):
|
class CustomFieldFilterForm(forms.Form):
|
||||||
@ -180,10 +128,11 @@ class CustomFieldFilterForm(forms.Form):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Add all applicable CustomFields to the form
|
# Add all applicable CustomFields to the form
|
||||||
custom_fields = get_custom_fields_for_model(self.obj_type, filterable_only=True).items()
|
custom_fields = CustomField.objects.filter(obj_type=self.obj_type).exclude(
|
||||||
for name, field in custom_fields:
|
filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED
|
||||||
field.required = False
|
)
|
||||||
self.fields[name] = field
|
for cf in custom_fields:
|
||||||
|
self.fields[cf.name] = cf.to_form_field(set_initial=True, enforce_required=False)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
|
from django import forms
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
@ -14,6 +15,7 @@ from django.utils.text import slugify
|
|||||||
from taggit.models import TagBase, GenericTaggedItemBase
|
from taggit.models import TagBase, GenericTaggedItemBase
|
||||||
|
|
||||||
from utilities.fields import ColorField
|
from utilities.fields import ColorField
|
||||||
|
from utilities.forms import CSVChoiceField, DatePicker, LaxURLField, StaticSelect2, add_blank_choice
|
||||||
from utilities.utils import deepmerge, render_jinja2
|
from utilities.utils import deepmerge, render_jinja2
|
||||||
from .choices import *
|
from .choices import *
|
||||||
from .constants import *
|
from .constants import *
|
||||||
@ -280,6 +282,75 @@ class CustomField(models.Model):
|
|||||||
return self.choices.get(pk=int(serialized_value))
|
return self.choices.get(pk=int(serialized_value))
|
||||||
return serialized_value
|
return serialized_value
|
||||||
|
|
||||||
|
def to_form_field(self, set_initial=True, enforce_required=True, for_csv_import=False):
|
||||||
|
"""
|
||||||
|
Return a form field suitable for setting a CustomField's value for an object.
|
||||||
|
|
||||||
|
set_initial: Set initial date for the field. This should be False when generating a field for bulk editing.
|
||||||
|
enforce_required: Honor the value of CustomField.required. Set to False for filtering/bulk editing.
|
||||||
|
for_csv_import: Return a form field suitable for bulk import of objects in CSV format.
|
||||||
|
"""
|
||||||
|
initial = self.default if set_initial else None
|
||||||
|
required = self.required if enforce_required else False
|
||||||
|
|
||||||
|
# Integer
|
||||||
|
if self.type == CustomFieldTypeChoices.TYPE_INTEGER:
|
||||||
|
field = forms.IntegerField(required=required, initial=initial)
|
||||||
|
|
||||||
|
# Boolean
|
||||||
|
elif self.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
|
||||||
|
choices = (
|
||||||
|
(None, '---------'),
|
||||||
|
(1, 'True'),
|
||||||
|
(0, 'False'),
|
||||||
|
)
|
||||||
|
if initial is not None and initial.lower() in ['true', 'yes', '1']:
|
||||||
|
initial = 1
|
||||||
|
elif initial is not None and initial.lower() in ['false', 'no', '0']:
|
||||||
|
initial = 0
|
||||||
|
else:
|
||||||
|
initial = None
|
||||||
|
field = forms.NullBooleanField(
|
||||||
|
required=required, initial=initial, widget=StaticSelect2(choices=choices)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Date
|
||||||
|
elif self.type == CustomFieldTypeChoices.TYPE_DATE:
|
||||||
|
field = forms.DateField(required=required, initial=initial, widget=DatePicker())
|
||||||
|
|
||||||
|
# Select
|
||||||
|
elif self.type == CustomFieldTypeChoices.TYPE_SELECT:
|
||||||
|
choices = [(cfc.pk, cfc.value) for cfc in self.choices.all()]
|
||||||
|
|
||||||
|
if not required:
|
||||||
|
choices = add_blank_choice(choices)
|
||||||
|
|
||||||
|
# Set the initial value to the PK of the default choice, if any
|
||||||
|
if set_initial:
|
||||||
|
default_choice = self.choices.filter(value=self.default).first()
|
||||||
|
if default_choice:
|
||||||
|
initial = default_choice.pk
|
||||||
|
|
||||||
|
field_class = CSVChoiceField if for_csv_import else forms.ChoiceField
|
||||||
|
field = field_class(
|
||||||
|
choices=choices, required=required, initial=initial, widget=StaticSelect2()
|
||||||
|
)
|
||||||
|
|
||||||
|
# URL
|
||||||
|
elif self.type == CustomFieldTypeChoices.TYPE_URL:
|
||||||
|
field = LaxURLField(required=required, initial=initial)
|
||||||
|
|
||||||
|
# Text
|
||||||
|
else:
|
||||||
|
field = forms.CharField(max_length=255, required=required, initial=initial)
|
||||||
|
|
||||||
|
field.model = self
|
||||||
|
field.label = self.label if self.label else self.name.replace('_', ' ').capitalize()
|
||||||
|
if self.description:
|
||||||
|
field.help_text = self.description
|
||||||
|
|
||||||
|
return field
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldValue(models.Model):
|
class CustomFieldValue(models.Model):
|
||||||
field = models.ForeignKey(
|
field = models.ForeignKey(
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.test import TestCase
|
from django.test import Client, TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
|
from dcim.forms import SiteCSVForm
|
||||||
from dcim.models import Site
|
from dcim.models import Site
|
||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
from extras.models import CustomField, CustomFieldValue, CustomFieldChoice
|
from extras.models import CustomField, CustomFieldValue, CustomFieldChoice
|
||||||
from utilities.testing import APITestCase
|
from utilities.testing import APITestCase, create_test_user
|
||||||
from virtualization.models import VirtualMachine
|
from virtualization.models import VirtualMachine
|
||||||
|
|
||||||
|
|
||||||
@ -364,3 +365,113 @@ class CustomFieldChoiceAPITest(APITestCase):
|
|||||||
self.assertEqual(self.cf_choice_1.pk, response.data[self.cf_1.name][self.cf_choice_1.value])
|
self.assertEqual(self.cf_choice_1.pk, response.data[self.cf_1.name][self.cf_choice_1.value])
|
||||||
self.assertEqual(self.cf_choice_2.pk, response.data[self.cf_1.name][self.cf_choice_2.value])
|
self.assertEqual(self.cf_choice_2.pk, response.data[self.cf_1.name][self.cf_choice_2.value])
|
||||||
self.assertEqual(self.cf_choice_3.pk, response.data[self.cf_2.name][self.cf_choice_3.value])
|
self.assertEqual(self.cf_choice_3.pk, response.data[self.cf_2.name][self.cf_choice_3.value])
|
||||||
|
|
||||||
|
|
||||||
|
class CustomFieldImportTest(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
|
||||||
|
user = create_test_user(
|
||||||
|
permissions=[
|
||||||
|
'dcim.view_site',
|
||||||
|
'dcim.add_site',
|
||||||
|
]
|
||||||
|
)
|
||||||
|
self.client = Client()
|
||||||
|
self.client.force_login(user)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
|
||||||
|
custom_fields = (
|
||||||
|
CustomField(name='text', type=CustomFieldTypeChoices.TYPE_TEXT),
|
||||||
|
CustomField(name='integer', type=CustomFieldTypeChoices.TYPE_INTEGER),
|
||||||
|
CustomField(name='boolean', type=CustomFieldTypeChoices.TYPE_BOOLEAN),
|
||||||
|
CustomField(name='date', type=CustomFieldTypeChoices.TYPE_DATE),
|
||||||
|
CustomField(name='url', type=CustomFieldTypeChoices.TYPE_URL),
|
||||||
|
CustomField(name='select', type=CustomFieldTypeChoices.TYPE_SELECT),
|
||||||
|
)
|
||||||
|
for cf in custom_fields:
|
||||||
|
cf.save()
|
||||||
|
cf.obj_type.set([ContentType.objects.get_for_model(Site)])
|
||||||
|
|
||||||
|
CustomFieldChoice.objects.bulk_create((
|
||||||
|
CustomFieldChoice(field=custom_fields[5], value='Choice A'),
|
||||||
|
CustomFieldChoice(field=custom_fields[5], value='Choice B'),
|
||||||
|
CustomFieldChoice(field=custom_fields[5], value='Choice C'),
|
||||||
|
))
|
||||||
|
|
||||||
|
def test_import(self):
|
||||||
|
"""
|
||||||
|
Import a Site in CSV format, including a value for each CustomField.
|
||||||
|
"""
|
||||||
|
data = (
|
||||||
|
('name', 'slug', 'cf_text', 'cf_integer', 'cf_boolean', 'cf_date', 'cf_url', 'cf_select'),
|
||||||
|
('Site 1', 'site-1', 'ABC', '123', 'True', '2020-01-01', 'http://example.com/1', 'Choice A'),
|
||||||
|
('Site 2', 'site-2', 'DEF', '456', 'False', '2020-01-02', 'http://example.com/2', 'Choice B'),
|
||||||
|
('Site 3', 'site-3', '', '', '', '', '', ''),
|
||||||
|
)
|
||||||
|
csv_data = '\n'.join(','.join(row) for row in data)
|
||||||
|
|
||||||
|
response = self.client.post(reverse('dcim:site_import'), {'csv': csv_data})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
# Validate data for site 1
|
||||||
|
custom_field_values = {
|
||||||
|
cf.name: value for cf, value in Site.objects.get(name='Site 1').get_custom_fields().items()
|
||||||
|
}
|
||||||
|
self.assertEqual(len(custom_field_values), 6)
|
||||||
|
self.assertEqual(custom_field_values['text'], 'ABC')
|
||||||
|
self.assertEqual(custom_field_values['integer'], 123)
|
||||||
|
self.assertEqual(custom_field_values['boolean'], True)
|
||||||
|
self.assertEqual(custom_field_values['date'], date(2020, 1, 1))
|
||||||
|
self.assertEqual(custom_field_values['url'], 'http://example.com/1')
|
||||||
|
self.assertEqual(custom_field_values['select'].value, 'Choice A')
|
||||||
|
|
||||||
|
# Validate data for site 2
|
||||||
|
custom_field_values = {
|
||||||
|
cf.name: value for cf, value in Site.objects.get(name='Site 2').get_custom_fields().items()
|
||||||
|
}
|
||||||
|
self.assertEqual(len(custom_field_values), 6)
|
||||||
|
self.assertEqual(custom_field_values['text'], 'DEF')
|
||||||
|
self.assertEqual(custom_field_values['integer'], 456)
|
||||||
|
self.assertEqual(custom_field_values['boolean'], False)
|
||||||
|
self.assertEqual(custom_field_values['date'], date(2020, 1, 2))
|
||||||
|
self.assertEqual(custom_field_values['url'], 'http://example.com/2')
|
||||||
|
self.assertEqual(custom_field_values['select'].value, 'Choice B')
|
||||||
|
|
||||||
|
# No CustomFieldValues should be created for site 3
|
||||||
|
obj_type = ContentType.objects.get_for_model(Site)
|
||||||
|
site3 = Site.objects.get(name='Site 3')
|
||||||
|
self.assertFalse(CustomFieldValue.objects.filter(obj_type=obj_type, obj_id=site3.pk).exists())
|
||||||
|
self.assertEqual(CustomFieldValue.objects.count(), 12) # Sanity check
|
||||||
|
|
||||||
|
def test_import_missing_required(self):
|
||||||
|
"""
|
||||||
|
Attempt to import an object missing a required custom field.
|
||||||
|
"""
|
||||||
|
# Set one of our CustomFields to required
|
||||||
|
CustomField.objects.filter(name='text').update(required=True)
|
||||||
|
|
||||||
|
form_data = {
|
||||||
|
'name': 'Site 1',
|
||||||
|
'slug': 'site-1',
|
||||||
|
}
|
||||||
|
|
||||||
|
form = SiteCSVForm(data=form_data)
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
self.assertIn('cf_text', form.errors)
|
||||||
|
|
||||||
|
def test_import_invalid_choice(self):
|
||||||
|
"""
|
||||||
|
Attempt to import an object with an invalid choice selection.
|
||||||
|
"""
|
||||||
|
form_data = {
|
||||||
|
'name': 'Site 1',
|
||||||
|
'slug': 'site-1',
|
||||||
|
'cf_select': 'Choice X'
|
||||||
|
}
|
||||||
|
|
||||||
|
form = SiteCSVForm(data=form_data)
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
self.assertIn('cf_select', form.errors)
|
||||||
|
@ -4,7 +4,9 @@ from django.core.validators import MaxValueValidator, MinValueValidator
|
|||||||
from taggit.forms import TagField
|
from taggit.forms import TagField
|
||||||
|
|
||||||
from dcim.models import Device, Interface, Rack, Region, Site
|
from dcim.models import Device, Interface, Rack, Region, Site
|
||||||
from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
from extras.forms import (
|
||||||
|
AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldModelCSVForm, CustomFieldModelForm, CustomFieldFilterForm,
|
||||||
|
)
|
||||||
from tenancy.forms import TenancyFilterForm, TenancyForm
|
from tenancy.forms import TenancyFilterForm, TenancyForm
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
@ -31,7 +33,7 @@ IPADDRESS_MASK_LENGTH_CHOICES = add_blank_choice([
|
|||||||
# VRFs
|
# VRFs
|
||||||
#
|
#
|
||||||
|
|
||||||
class VRFForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
class VRFForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||||
tags = TagField(
|
tags = TagField(
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
@ -49,7 +51,7 @@ class VRFForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class VRFCSVForm(forms.ModelForm):
|
class VRFCSVForm(CustomFieldModelCSVForm):
|
||||||
tenant = forms.ModelChoiceField(
|
tenant = forms.ModelChoiceField(
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
@ -144,7 +146,7 @@ class RIRFilterForm(BootstrapMixin, forms.Form):
|
|||||||
# Aggregates
|
# Aggregates
|
||||||
#
|
#
|
||||||
|
|
||||||
class AggregateForm(BootstrapMixin, CustomFieldForm):
|
class AggregateForm(BootstrapMixin, CustomFieldModelForm):
|
||||||
tags = TagField(
|
tags = TagField(
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
@ -166,7 +168,7 @@ class AggregateForm(BootstrapMixin, CustomFieldForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class AggregateCSVForm(forms.ModelForm):
|
class AggregateCSVForm(CustomFieldModelCSVForm):
|
||||||
rir = forms.ModelChoiceField(
|
rir = forms.ModelChoiceField(
|
||||||
queryset=RIR.objects.all(),
|
queryset=RIR.objects.all(),
|
||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
@ -263,7 +265,7 @@ class RoleCSVForm(forms.ModelForm):
|
|||||||
# Prefixes
|
# Prefixes
|
||||||
#
|
#
|
||||||
|
|
||||||
class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||||
site = forms.ModelChoiceField(
|
site = forms.ModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
@ -341,7 +343,7 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
self.fields['vrf'].empty_label = 'Global'
|
self.fields['vrf'].empty_label = 'Global'
|
||||||
|
|
||||||
|
|
||||||
class PrefixCSVForm(forms.ModelForm):
|
class PrefixCSVForm(CustomFieldModelCSVForm):
|
||||||
vrf = FlexibleModelChoiceField(
|
vrf = FlexibleModelChoiceField(
|
||||||
queryset=VRF.objects.all(),
|
queryset=VRF.objects.all(),
|
||||||
to_field_name='rd',
|
to_field_name='rd',
|
||||||
@ -584,7 +586,7 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
|
|||||||
# IP addresses
|
# IP addresses
|
||||||
#
|
#
|
||||||
|
|
||||||
class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm):
|
class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModelForm):
|
||||||
interface = forms.ModelChoiceField(
|
interface = forms.ModelChoiceField(
|
||||||
queryset=Interface.objects.all(),
|
queryset=Interface.objects.all(),
|
||||||
required=False
|
required=False
|
||||||
@ -751,7 +753,7 @@ class IPAddressBulkCreateForm(BootstrapMixin, forms.Form):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
@ -771,7 +773,7 @@ class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
self.fields['vrf'].empty_label = 'Global'
|
self.fields['vrf'].empty_label = 'Global'
|
||||||
|
|
||||||
|
|
||||||
class IPAddressCSVForm(forms.ModelForm):
|
class IPAddressCSVForm(CustomFieldModelCSVForm):
|
||||||
vrf = FlexibleModelChoiceField(
|
vrf = FlexibleModelChoiceField(
|
||||||
queryset=VRF.objects.all(),
|
queryset=VRF.objects.all(),
|
||||||
to_field_name='rd',
|
to_field_name='rd',
|
||||||
@ -1087,7 +1089,7 @@ class VLANGroupFilterForm(BootstrapMixin, forms.Form):
|
|||||||
# VLANs
|
# VLANs
|
||||||
#
|
#
|
||||||
|
|
||||||
class VLANForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||||
site = forms.ModelChoiceField(
|
site = forms.ModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
@ -1135,7 +1137,7 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class VLANCSVForm(forms.ModelForm):
|
class VLANCSVForm(CustomFieldModelCSVForm):
|
||||||
site = forms.ModelChoiceField(
|
site = forms.ModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
@ -1310,7 +1312,7 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
|||||||
# Services
|
# Services
|
||||||
#
|
#
|
||||||
|
|
||||||
class ServiceForm(BootstrapMixin, CustomFieldForm):
|
class ServiceForm(BootstrapMixin, CustomFieldModelForm):
|
||||||
port = forms.IntegerField(
|
port = forms.IntegerField(
|
||||||
min_value=SERVICE_PORT_MIN,
|
min_value=SERVICE_PORT_MIN,
|
||||||
max_value=SERVICE_PORT_MAX
|
max_value=SERVICE_PORT_MAX
|
||||||
|
@ -4,7 +4,9 @@ from django import forms
|
|||||||
from taggit.forms import TagField
|
from taggit.forms import TagField
|
||||||
|
|
||||||
from dcim.models import Device
|
from dcim.models import Device
|
||||||
from extras.forms import AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldForm
|
from extras.forms import (
|
||||||
|
AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldModelForm, CustomFieldModelCSVForm,
|
||||||
|
)
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
APISelect, APISelectMultiple, BootstrapMixin, FilterChoiceField, FlexibleModelChoiceField, SlugField,
|
APISelect, APISelectMultiple, BootstrapMixin, FilterChoiceField, FlexibleModelChoiceField, SlugField,
|
||||||
StaticSelect2Multiple
|
StaticSelect2Multiple
|
||||||
@ -68,7 +70,7 @@ class SecretRoleCSVForm(forms.ModelForm):
|
|||||||
# Secrets
|
# Secrets
|
||||||
#
|
#
|
||||||
|
|
||||||
class SecretForm(BootstrapMixin, CustomFieldForm):
|
class SecretForm(BootstrapMixin, CustomFieldModelForm):
|
||||||
plaintext = forms.CharField(
|
plaintext = forms.CharField(
|
||||||
max_length=SECRET_PLAINTEXT_MAX_LENGTH,
|
max_length=SECRET_PLAINTEXT_MAX_LENGTH,
|
||||||
required=False,
|
required=False,
|
||||||
@ -116,7 +118,7 @@ class SecretForm(BootstrapMixin, CustomFieldForm):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class SecretCSVForm(forms.ModelForm):
|
class SecretCSVForm(CustomFieldModelCSVForm):
|
||||||
device = FlexibleModelChoiceField(
|
device = FlexibleModelChoiceField(
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from taggit.forms import TagField
|
from taggit.forms import TagField
|
||||||
|
|
||||||
from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
from extras.forms import (
|
||||||
|
AddRemoveTagsForm, CustomFieldModelForm, CustomFieldBulkEditForm, CustomFieldModelCSVForm, CustomFieldFilterForm,
|
||||||
|
)
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
APISelect, APISelectMultiple, BootstrapMixin, ChainedFieldsMixin, ChainedModelChoiceField, CommentField,
|
APISelect, APISelectMultiple, BootstrapMixin, ChainedFieldsMixin, ChainedModelChoiceField, CommentField,
|
||||||
FilterChoiceField, SlugField,
|
FilterChoiceField, SlugField,
|
||||||
@ -38,7 +40,7 @@ class TenantGroupCSVForm(forms.ModelForm):
|
|||||||
# Tenants
|
# Tenants
|
||||||
#
|
#
|
||||||
|
|
||||||
class TenantForm(BootstrapMixin, CustomFieldForm):
|
class TenantForm(BootstrapMixin, CustomFieldModelForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
tags = TagField(
|
tags = TagField(
|
||||||
@ -57,7 +59,7 @@ class TenantForm(BootstrapMixin, CustomFieldForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class TenantCSVForm(forms.ModelForm):
|
class TenantCSVForm(CustomFieldModelForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
group = forms.ModelChoiceField(
|
group = forms.ModelChoiceField(
|
||||||
queryset=TenantGroup.objects.all(),
|
queryset=TenantGroup.objects.all(),
|
||||||
|
@ -88,15 +88,27 @@ class ObjectListView(View):
|
|||||||
Export the queryset of objects as comma-separated value (CSV), using the model's to_csv() method.
|
Export the queryset of objects as comma-separated value (CSV), using the model's to_csv() method.
|
||||||
"""
|
"""
|
||||||
csv_data = []
|
csv_data = []
|
||||||
|
custom_fields = []
|
||||||
|
|
||||||
# Start with the column headers
|
# Start with the column headers
|
||||||
headers = ','.join(self.queryset.model.csv_headers)
|
headers = self.queryset.model.csv_headers.copy()
|
||||||
csv_data.append(headers)
|
|
||||||
|
# Add custom field headers, if any
|
||||||
|
if hasattr(self.queryset.model, 'get_custom_fields'):
|
||||||
|
for custom_field in self.queryset.model().get_custom_fields():
|
||||||
|
headers.append(custom_field.name)
|
||||||
|
custom_fields.append(custom_field.name)
|
||||||
|
|
||||||
|
csv_data.append(','.join(headers))
|
||||||
|
|
||||||
# Iterate through the queryset appending each object
|
# Iterate through the queryset appending each object
|
||||||
for obj in self.queryset:
|
for obj in self.queryset:
|
||||||
data = csv_format(obj.to_csv())
|
data = obj.to_csv()
|
||||||
csv_data.append(data)
|
|
||||||
|
for custom_field in custom_fields:
|
||||||
|
data += (obj.cf.get(custom_field, ''),)
|
||||||
|
|
||||||
|
csv_data.append(csv_format(data))
|
||||||
|
|
||||||
return '\n'.join(csv_data)
|
return '\n'.join(csv_data)
|
||||||
|
|
||||||
|
@ -6,7 +6,9 @@ from dcim.choices import InterfaceModeChoices
|
|||||||
from dcim.constants import INTERFACE_MTU_MAX, INTERFACE_MTU_MIN
|
from dcim.constants import INTERFACE_MTU_MAX, INTERFACE_MTU_MIN
|
||||||
from dcim.forms import INTERFACE_MODE_HELP_TEXT
|
from dcim.forms import INTERFACE_MODE_HELP_TEXT
|
||||||
from dcim.models import Device, DeviceRole, Interface, Platform, Rack, Region, Site
|
from dcim.models import Device, DeviceRole, Interface, Platform, Rack, Region, Site
|
||||||
from extras.forms import AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldForm, CustomFieldFilterForm
|
from extras.forms import (
|
||||||
|
AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldModelCSVForm, CustomFieldModelForm, CustomFieldFilterForm,
|
||||||
|
)
|
||||||
from ipam.models import IPAddress, VLANGroup, VLAN
|
from ipam.models import IPAddress, VLANGroup, VLAN
|
||||||
from tenancy.forms import TenancyFilterForm, TenancyForm
|
from tenancy.forms import TenancyFilterForm, TenancyForm
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
@ -74,7 +76,7 @@ class ClusterGroupCSVForm(forms.ModelForm):
|
|||||||
# Clusters
|
# Clusters
|
||||||
#
|
#
|
||||||
|
|
||||||
class ClusterForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
class ClusterForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
tags = TagField(
|
tags = TagField(
|
||||||
required=False
|
required=False
|
||||||
@ -98,7 +100,7 @@ class ClusterForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ClusterCSVForm(forms.ModelForm):
|
class ClusterCSVForm(CustomFieldModelCSVForm):
|
||||||
type = forms.ModelChoiceField(
|
type = forms.ModelChoiceField(
|
||||||
queryset=ClusterType.objects.all(),
|
queryset=ClusterType.objects.all(),
|
||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
@ -327,7 +329,7 @@ class ClusterRemoveDevicesForm(ConfirmationForm):
|
|||||||
# Virtual Machines
|
# Virtual Machines
|
||||||
#
|
#
|
||||||
|
|
||||||
class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||||
cluster_group = forms.ModelChoiceField(
|
cluster_group = forms.ModelChoiceField(
|
||||||
queryset=ClusterGroup.objects.all(),
|
queryset=ClusterGroup.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
@ -430,7 +432,7 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
self.fields['primary_ip6'].widget.attrs['readonly'] = True
|
self.fields['primary_ip6'].widget.attrs['readonly'] = True
|
||||||
|
|
||||||
|
|
||||||
class VirtualMachineCSVForm(forms.ModelForm):
|
class VirtualMachineCSVForm(CustomFieldModelCSVForm):
|
||||||
status = CSVChoiceField(
|
status = CSVChoiceField(
|
||||||
choices=VirtualMachineStatusChoices,
|
choices=VirtualMachineStatusChoices,
|
||||||
required=False,
|
required=False,
|
||||||
|
Reference in New Issue
Block a user