9627 bulk import / edit

This commit is contained in:
Arthur Hanson 2024-06-21 13:36:14 -07:00
parent 35a5165c87
commit 7628145982
6 changed files with 60 additions and 15 deletions

View File

@ -10,6 +10,7 @@ from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountF
from netbox.api.serializers import NetBoxModelSerializer from netbox.api.serializers import NetBoxModelSerializer
from tenancy.api.serializers_.tenants import TenantSerializer from tenancy.api.serializers_.tenants import TenantSerializer
from utilities.api import get_serializer_for_model from utilities.api import get_serializer_for_model
from utilities.data import ranges_to_string, string_to_range_array
from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer
from .roles import RoleSerializer from .roles import RoleSerializer
@ -21,6 +22,14 @@ __all__ = (
) )
class NumericRangeArraySerializer(serializers.BaseSerializer):
def to_internal_value(self, data):
return string_to_range_array(data)
def to_representation(self, instance):
return ranges_to_string(data)
class VLANGroupSerializer(NetBoxModelSerializer): class VLANGroupSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail') url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail')
scope_type = ContentTypeField( scope_type = ContentTypeField(
@ -37,11 +46,12 @@ class VLANGroupSerializer(NetBoxModelSerializer):
# Related object counts # Related object counts
vlan_count = RelatedObjectCountField('vlans') vlan_count = RelatedObjectCountField('vlans')
vlan_id_ranges = NumericRangeArraySerializer()
class Meta: class Meta:
model = VLANGroup model = VLANGroup
fields = [ fields = [
'id', 'url', 'display', 'name', 'slug', 'scope_type', 'scope_id', 'scope', 'id', 'url', 'display', 'name', 'slug', 'scope_type', 'scope_id', 'scope', 'vlan_id_ranges',
'description', 'tags', 'custom_fields', 'created', 'last_updated', 'vlan_count', 'utilization' 'description', 'tags', 'custom_fields', 'created', 'last_updated', 'vlan_count', 'utilization'
] ]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'vlan_count') brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'vlan_count')

View File

@ -12,6 +12,7 @@ from tenancy.models import Tenant
from utilities.forms import add_blank_choice from utilities.forms import add_blank_choice
from utilities.forms.fields import ( from utilities.forms.fields import (
CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, NumericArrayField, CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, NumericArrayField,
NumericRangeArrayField,
) )
from utilities.forms.rendering import FieldSet from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import BulkEditNullBooleanSelect from utilities.forms.widgets import BulkEditNullBooleanSelect
@ -471,10 +472,11 @@ class VLANGroupBulkEditForm(NetBoxModelBulkEditForm):
'group_id': '$clustergroup', 'group_id': '$clustergroup',
} }
) )
vlan_id_ranges = NumericRangeArrayField()
model = VLANGroup model = VLANGroup
fieldsets = ( fieldsets = (
FieldSet('site', 'description'), FieldSet('site', 'vlan_id_ranges', 'description'),
FieldSet( FieldSet(
'scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster', name=_('Scope') 'scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster', name=_('Scope')
), ),

View File

@ -9,7 +9,8 @@ from ipam.models import *
from netbox.forms import NetBoxModelImportForm from netbox.forms import NetBoxModelImportForm
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms.fields import ( from utilities.forms.fields import (
CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVModelMultipleChoiceField, SlugField CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVModelMultipleChoiceField, SlugField,
NumericRangeArrayField,
) )
from virtualization.models import VirtualMachine, VMInterface from virtualization.models import VirtualMachine, VMInterface
@ -411,10 +412,11 @@ class VLANGroupImportForm(NetBoxModelImportForm):
required=False, required=False,
label=_('Scope type (app & model)') label=_('Scope type (app & model)')
) )
vlan_id_ranges = NumericRangeArrayField()
class Meta: class Meta:
model = VLANGroup model = VLANGroup
fields = ('name', 'slug', 'scope_type', 'scope_id', 'description', 'tags') fields = ('name', 'slug', 'scope_type', 'scope_id', 'vlan_id_ranges', 'description', 'tags')
labels = { labels = {
'scope_id': 'Scope ID', 'scope_id': 'Scope ID',
} }

View File

@ -11,7 +11,7 @@ from ipam.choices import *
from ipam.constants import * from ipam.constants import *
from ipam.querysets import VLANQuerySet, VLANGroupQuerySet from ipam.querysets import VLANQuerySet, VLANGroupQuerySet
from netbox.models import OrganizationalModel, PrimaryModel from netbox.models import OrganizationalModel, PrimaryModel
from utilities.data import check_ranges_overlap from utilities.data import check_ranges_overlap, ranges_to_string
from virtualization.models import VMInterface from virtualization.models import VMInterface
__all__ = ( __all__ = (
@ -129,7 +129,7 @@ class VLANGroup(OrganizationalModel):
@property @property
def vlan_ranges(self): def vlan_ranges(self):
return ','.join([f"{vlan_range.lower}-{vlan_range.upper}" for vlan_range in self.vlan_id_ranges]) return ranges_to_string(self.vlan_id_ranges)
class VLAN(PrimaryModel): class VLAN(PrimaryModel):

View File

@ -8,7 +8,9 @@ __all__ = (
'deepmerge', 'deepmerge',
'drange', 'drange',
'flatten_dict', 'flatten_dict',
'ranges_to_string',
'shallow_compare_dict', 'shallow_compare_dict',
'string_to_range_array',
) )
@ -129,3 +131,31 @@ def check_ranges_overlap(ranges):
return True return True
return False return False
def ranges_to_string(ranges):
"""
Generate a human-friendly string from a set of ranges. Intended for use with ArrayField.
For example:
[1-100, 200-300] => "1-100, 200-300"
"""
return ', '.join([f"{val.lower}-{val.upper}" for val in value])
def string_to_range_array(value):
"""
Given a string in the format "1-100, 200-300" create an array of ranges. Intended for use with ArrayField.
For example:
"1-100, 200-300" => [1-100, 200-300]
"""
if not value:
return None
ranges = value.split(",")
values = []
for dash_range in value.split(','):
if '-' not in dash_range:
return None
lower, upper = dash_range.split('-')
values.append(NumericRange(int(lower), int(upper)))
return values

View File

@ -2,6 +2,7 @@ from django import forms
from django.contrib.postgres.forms import SimpleArrayField from django.contrib.postgres.forms import SimpleArrayField
from django.db.backends.postgresql.psycopg_any import NumericRange from django.db.backends.postgresql.psycopg_any import NumericRange
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from utilities.data import ranges_to_string, string_to_range_array
from ..utils import parse_numeric_range from ..utils import parse_numeric_range
@ -42,17 +43,17 @@ class NumericRangeArrayField(forms.CharField):
"Example: <code>1-5,20-30</code>" "Example: <code>1-5,20-30</code>"
) )
def clean(self, value):
if value and not self.to_python(value):
raise forms.ValidationError(
_("Invalid ranges ({value}). Must be range of number '100-200' and ranges must be in ascending order.").format(value=value)
)
return super().clean(value)
def prepare_value(self, value): def prepare_value(self, value):
if isinstance(value, str): if isinstance(value, str):
return value return value
return ','.join([f"{val.lower}-{val.upper}" for val in value]) return ranges_to_string(value)
def to_python(self, value): def to_python(self, value):
if not value: return string_to_range_array(value)
return None
ranges = value.split(",")
values = []
for dash_range in value.split(','):
lower, upper = dash_range.split('-')
values.append(NumericRange(int(lower), int(upper)))
return values