9627 change to NumericArrayField

This commit is contained in:
Arthur Hanson 2024-06-24 11:40:10 -07:00
parent 3c89651076
commit 72ed36fd5b
8 changed files with 34 additions and 67 deletions

View File

@ -12,7 +12,6 @@ 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
@ -472,13 +471,16 @@ class VLANGroupBulkEditForm(NetBoxModelBulkEditForm):
'group_id': '$clustergroup', 'group_id': '$clustergroup',
} }
) )
vlan_id_ranges = NumericRangeArrayField( allowed_vids = NumericArrayField(
required=False, required=False,
label=_('min/max VLAN IDs'),
base_field=forms.IntegerField(),
help_text=_('Comma-separated list of numeric VLAN IDs. A range may be specified using a hyphen.'),
) )
model = VLANGroup model = VLANGroup
fieldsets = ( fieldsets = (
FieldSet('site', 'vlan_id_ranges', 'description'), FieldSet('site', 'allowed_vids', '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

@ -10,7 +10,7 @@ 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, NumericArrayField,
) )
from virtualization.models import VirtualMachine, VMInterface from virtualization.models import VirtualMachine, VMInterface
@ -412,13 +412,16 @@ class VLANGroupImportForm(NetBoxModelImportForm):
required=False, required=False,
label=_('Scope type (app & model)') label=_('Scope type (app & model)')
) )
vlan_id_ranges = NumericRangeArrayField( allowed_vids = NumericArrayField(
required=False, required=False,
label=_('min/max VLAN IDs'),
base_field=forms.IntegerField(),
help_text=_('Comma-separated list of numeric VLAN IDs. A range may be specified using a hyphen.'),
) )
class Meta: class Meta:
model = VLANGroup model = VLANGroup
fields = ('name', 'slug', 'scope_type', 'scope_id', 'vlan_id_ranges', 'description', 'tags') fields = ('name', 'slug', 'scope_type', 'scope_id', 'allowed_vids', 'description', 'tags')
labels = { labels = {
'scope_id': 'Scope ID', 'scope_id': 'Scope ID',
} }

View File

@ -15,7 +15,7 @@ from utilities.exceptions import PermissionsViolation
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,
SlugField, NumericRangeArrayField SlugField
) )
from utilities.forms.rendering import FieldSet, InlineFields, ObjectAttribute, TabbedGroups from utilities.forms.rendering import FieldSet, InlineFields, ObjectAttribute, TabbedGroups
from utilities.forms.widgets import DatePicker from utilities.forms.widgets import DatePicker
@ -633,13 +633,16 @@ class VLANGroupForm(NetBoxModelForm):
} }
) )
slug = SlugField() slug = SlugField()
vlan_id_ranges = NumericRangeArrayField( allowed_vids = NumericArrayField(
required=False required=False,
label=_('min/max VLAN IDs'),
base_field=forms.IntegerField(),
help_text=_('Comma-separated list of numeric VLAN IDs. A range may be specified using a hyphen.'),
) )
fieldsets = ( fieldsets = (
FieldSet('name', 'slug', 'description', 'tags', name=_('VLAN Group')), FieldSet('name', 'slug', 'description', 'tags', name=_('VLAN Group')),
FieldSet('vlan_id_ranges', name=_('Child VLANs')), FieldSet('allowed_vids', name=_('Child VLANs')),
FieldSet( FieldSet(
'scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster', 'scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster',
name=_('Scope') name=_('Scope')
@ -650,7 +653,7 @@ class VLANGroupForm(NetBoxModelForm):
model = VLANGroup model = VLANGroup
fields = [ fields = [
'name', 'slug', 'description', 'scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'name', 'slug', 'description', 'scope_type', 'region', 'sitegroup', 'site', 'location', 'rack',
'clustergroup', 'cluster', 'vlan_id_ranges', 'tags', 'clustergroup', 'cluster', 'allowed_vids', 'tags',
] ]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View File

@ -251,7 +251,7 @@ class VLANType(NetBoxObjectType):
class VLANGroupType(OrganizationalObjectType): class VLANGroupType(OrganizationalObjectType):
vlans: List[VLANType] vlans: List[VLANType]
vlan_id_ranges: List[str] allowed_vids: str
@strawberry_django.field @strawberry_django.field
def scope(self) -> Annotated[Union[ def scope(self) -> Annotated[Union[

View File

@ -30,18 +30,13 @@ class Migration(migrations.Migration):
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='vlangroup', model_name='vlangroup',
name='vlan_id_ranges', name='allowed_vids',
field=django.contrib.postgres.fields.ArrayField( field=django.contrib.postgres.fields.ArrayField(
base_field=django.contrib.postgres.fields.ranges.BigIntegerRangeField(), base_field=models.PositiveSmallIntegerField(),
blank=True, null=True, size=None, default=ipam.models.vlans.get_default_allowed_vids,
default=ipam.models.vlans.get_default_vlan_ids, size=None,
), ),
), ),
migrations.AddField(
model_name='vlangroup',
name='_total_vlan_ids',
field=models.PositiveBigIntegerField(default=4094),
),
migrations.RunPython( migrations.RunPython(
code=move_min_max, code=move_min_max,
reverse_code=migrations.RunPython.noop reverse_code=migrations.RunPython.noop

View File

@ -1,9 +1,8 @@
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.contrib.postgres.fields import ArrayField, BigIntegerRangeField from django.contrib.postgres.fields import ArrayField
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
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.backends.postgresql.psycopg_any import NumericRange
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -21,8 +20,8 @@ __all__ = (
) )
def get_default_vlan_ids(): def get_default_allowed_vids():
return [NumericRange(VLAN_VID_MIN, VLAN_VID_MAX)] return list(range(VLAN_VID_MIN, VLAN_VID_MAX + 1))
class VLANGroup(OrganizationalModel): class VLANGroup(OrganizationalModel):
@ -52,16 +51,11 @@ class VLANGroup(OrganizationalModel):
ct_field='scope_type', ct_field='scope_type',
fk_field='scope_id' fk_field='scope_id'
) )
vlan_id_ranges = ArrayField( allowed_vids = ArrayField(
BigIntegerRangeField(),
verbose_name=_('min/max VLAN IDs'), verbose_name=_('min/max VLAN IDs'),
default=get_default_vlan_ids, base_field=models.PositiveSmallIntegerField(),
help_text=_('Ranges of Minimum, maximum VLAN IDs'), default=get_default_allowed_vids,
blank=True, help_text=_('Ranges of Minimum-maximum child VLAN VID'),
null=True
)
_total_vlan_ids = models.PositiveBigIntegerField(
default=VLAN_VID_MAX - VLAN_VID_MIN + 1
) )
objects = VLANGroupQuerySet.as_manager() objects = VLANGroupQuerySet.as_manager()

View File

@ -1,5 +1,5 @@
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db.models import Count, F, OuterRef, Q, Subquery, Value from django.db.models import Count, F, Func, OuterRef, Q, Subquery, Value
from django.db.models.expressions import RawSQL from django.db.models.expressions import RawSQL
from django.db.models.functions import Round from django.db.models.functions import Round
@ -63,7 +63,8 @@ class VLANGroupQuerySet(RestrictedQuerySet):
return self.annotate( return self.annotate(
vlan_count=count_related(VLAN, 'group'), vlan_count=count_related(VLAN, 'group'),
utilization=Round(F('vlan_count') * 100 / F('_total_vlan_ids'), 2) total_allowed_vids=Func(F('allowed_vids'), function='CARDINALITY'),
utilization=Round(F('vlan_count') * 100 / F('total_allowed_vids'), 2)
) )

View File

@ -7,7 +7,6 @@ from ..utils import parse_numeric_range
__all__ = ( __all__ = (
'NumericArrayField', 'NumericArrayField',
'NumericRangeArrayField',
) )
@ -26,33 +25,3 @@ class NumericArrayField(SimpleArrayField):
if isinstance(value, str): if isinstance(value, str):
value = ','.join([str(n) for n in parse_numeric_range(value)]) value = ','.join([str(n) for n in parse_numeric_range(value)])
return super().to_python(value) return super().to_python(value)
class NumericRangeArrayField(forms.CharField):
"""
A field which allows for array of numeric ranges:
Example: 1-5,7-20,30-50
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not self.help_text:
self.help_text = _(
"Specify one or more numeric ranges separated by commas "
"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):
if isinstance(value, str):
return value
return ranges_to_string(value)
def to_python(self, value):
return string_to_range_array(value)