Closes #17761: Store empty CharField choices as null

This commit is contained in:
Jeremy Stretch
2024-10-18 16:55:58 -04:00
parent 75270c1aef
commit ef1fdf0a01
23 changed files with 622 additions and 25 deletions
@@ -0,0 +1,287 @@
from django.db import migrations, models
def set_null_values(apps, schema_editor):
"""
Replace empty strings with null values.
"""
Cable = apps.get_model('dcim', 'Cable')
ConsolePort = apps.get_model('dcim', 'ConsolePort')
ConsolePortTemplate = apps.get_model('dcim', 'ConsolePortTemplate')
ConsoleServerPort = apps.get_model('dcim', 'ConsoleServerPort')
ConsoleServerPortTemplate = apps.get_model('dcim', 'ConsoleServerPortTemplate')
Device = apps.get_model('dcim', 'Device')
DeviceType = apps.get_model('dcim', 'DeviceType')
FrontPort = apps.get_model('dcim', 'FrontPort')
Interface = apps.get_model('dcim', 'Interface')
InterfaceTemplate = apps.get_model('dcim', 'InterfaceTemplate')
ModuleType = apps.get_model('dcim', 'ModuleType')
PowerFeed = apps.get_model('dcim', 'PowerFeed')
PowerOutlet = apps.get_model('dcim', 'PowerOutlet')
PowerOutletTemplate = apps.get_model('dcim', 'PowerOutletTemplate')
PowerPort = apps.get_model('dcim', 'PowerPort')
PowerPortTemplate = apps.get_model('dcim', 'PowerPortTemplate')
Rack = apps.get_model('dcim', 'Rack')
RackType = apps.get_model('dcim', 'RackType')
RearPort = apps.get_model('dcim', 'RearPort')
Cable.objects.filter(length_unit='').update(length_unit=None)
Cable.objects.filter(type='').update(type=None)
ConsolePort.objects.filter(cable_end='').update(cable_end=None)
ConsolePort.objects.filter(type='').update(type=None)
ConsolePortTemplate.objects.filter(type='').update(type=None)
ConsoleServerPort.objects.filter(cable_end='').update(cable_end=None)
ConsoleServerPort.objects.filter(type='').update(type=None)
ConsoleServerPortTemplate.objects.filter(type='').update(type=None)
Device.objects.filter(airflow='').update(airflow=None)
Device.objects.filter(face='').update(face=None)
DeviceType.objects.filter(airflow='').update(airflow=None)
DeviceType.objects.filter(subdevice_role='').update(subdevice_role=None)
DeviceType.objects.filter(weight_unit='').update(weight_unit=None)
FrontPort.objects.filter(cable_end='').update(cable_end=None)
Interface.objects.filter(cable_end='').update(cable_end=None)
Interface.objects.filter(mode='').update(mode=None)
Interface.objects.filter(poe_mode='').update(poe_mode=None)
Interface.objects.filter(poe_type='').update(poe_type=None)
Interface.objects.filter(rf_channel='').update(rf_channel=None)
Interface.objects.filter(rf_role='').update(rf_role=None)
InterfaceTemplate.objects.filter(poe_mode='').update(poe_mode=None)
InterfaceTemplate.objects.filter(poe_type='').update(poe_type=None)
InterfaceTemplate.objects.filter(rf_role='').update(rf_role=None)
ModuleType.objects.filter(airflow='').update(airflow=None)
ModuleType.objects.filter(weight_unit='').update(weight_unit=None)
PowerFeed.objects.filter(cable_end='').update(cable_end=None)
PowerOutlet.objects.filter(cable_end='').update(cable_end=None)
PowerOutlet.objects.filter(feed_leg='').update(feed_leg=None)
PowerOutlet.objects.filter(type='').update(type=None)
PowerOutletTemplate.objects.filter(feed_leg='').update(feed_leg=None)
PowerOutletTemplate.objects.filter(type='').update(type=None)
PowerPort.objects.filter(cable_end='').update(cable_end=None)
PowerPort.objects.filter(type='').update(type=None)
PowerPortTemplate.objects.filter(type='').update(type=None)
Rack.objects.filter(airflow='').update(airflow=None)
Rack.objects.filter(form_factor='').update(form_factor=None)
Rack.objects.filter(outer_unit='').update(outer_unit=None)
Rack.objects.filter(weight_unit='').update(weight_unit=None)
RackType.objects.filter(outer_unit='').update(outer_unit=None)
RackType.objects.filter(weight_unit='').update(weight_unit=None)
RearPort.objects.filter(cable_end='').update(cable_end=None)
class Migration(migrations.Migration):
dependencies = [
('dcim', '0193_poweroutlet_color'),
]
operations = [
migrations.AlterField(
model_name='cable',
name='length_unit',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AlterField(
model_name='cable',
name='type',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AlterField(
model_name='consoleport',
name='cable_end',
field=models.CharField(blank=True, max_length=1, null=True),
),
migrations.AlterField(
model_name='consoleport',
name='type',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AlterField(
model_name='consoleporttemplate',
name='type',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AlterField(
model_name='consoleserverport',
name='cable_end',
field=models.CharField(blank=True, max_length=1, null=True),
),
migrations.AlterField(
model_name='consoleserverport',
name='type',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AlterField(
model_name='consoleserverporttemplate',
name='type',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AlterField(
model_name='device',
name='airflow',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AlterField(
model_name='device',
name='face',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AlterField(
model_name='devicetype',
name='airflow',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AlterField(
model_name='devicetype',
name='subdevice_role',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AlterField(
model_name='devicetype',
name='weight_unit',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AlterField(
model_name='frontport',
name='cable_end',
field=models.CharField(blank=True, max_length=1, null=True),
),
migrations.AlterField(
model_name='interface',
name='cable_end',
field=models.CharField(blank=True, max_length=1, null=True),
),
migrations.AlterField(
model_name='interface',
name='mode',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AlterField(
model_name='interface',
name='poe_mode',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AlterField(
model_name='interface',
name='poe_type',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AlterField(
model_name='interface',
name='rf_channel',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AlterField(
model_name='interface',
name='rf_role',
field=models.CharField(blank=True, max_length=30, null=True),
),
migrations.AlterField(
model_name='interfacetemplate',
name='poe_mode',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AlterField(
model_name='interfacetemplate',
name='poe_type',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AlterField(
model_name='interfacetemplate',
name='rf_role',
field=models.CharField(blank=True, max_length=30, null=True),
),
migrations.AlterField(
model_name='moduletype',
name='airflow',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AlterField(
model_name='moduletype',
name='weight_unit',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AlterField(
model_name='powerfeed',
name='cable_end',
field=models.CharField(blank=True, max_length=1, null=True),
),
migrations.AlterField(
model_name='poweroutlet',
name='cable_end',
field=models.CharField(blank=True, max_length=1, null=True),
),
migrations.AlterField(
model_name='poweroutlet',
name='feed_leg',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AlterField(
model_name='poweroutlet',
name='type',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AlterField(
model_name='poweroutlettemplate',
name='feed_leg',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AlterField(
model_name='poweroutlettemplate',
name='type',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AlterField(
model_name='powerport',
name='cable_end',
field=models.CharField(blank=True, max_length=1, null=True),
),
migrations.AlterField(
model_name='powerport',
name='type',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AlterField(
model_name='powerporttemplate',
name='type',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AlterField(
model_name='rack',
name='airflow',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AlterField(
model_name='rack',
name='form_factor',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AlterField(
model_name='rack',
name='outer_unit',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AlterField(
model_name='rack',
name='weight_unit',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AlterField(
model_name='racktype',
name='outer_unit',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AlterField(
model_name='racktype',
name='weight_unit',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AlterField(
model_name='rearport',
name='cable_end',
field=models.CharField(blank=True, max_length=1, null=True),
),
migrations.RunPython(
code=set_null_values,
reverse_code=migrations.RunPython.noop
),
]
+5 -3
View File
@@ -42,7 +42,8 @@ class Cable(PrimaryModel):
verbose_name=_('type'),
max_length=50,
choices=CableTypeChoices,
blank=True
blank=True,
null=True
)
status = models.CharField(
verbose_name=_('status'),
@@ -78,6 +79,7 @@ class Cable(PrimaryModel):
max_length=50,
choices=CableLengthUnitChoices,
blank=True,
null=True
)
# Stores the normalized length (in meters) for database ordering
_abs_length = models.DecimalField(
@@ -206,7 +208,7 @@ class Cable(PrimaryModel):
# Clear length_unit if no length is defined
if self.length is None:
self.length_unit = ''
self.length_unit = None
super().save(*args, **kwargs)
@@ -365,7 +367,7 @@ class CableTermination(ChangeLoggedModel):
termination = self.termination._meta.model.objects.get(pk=self.termination_id)
termination.snapshot()
termination.cable = None
termination.cable_end = ''
termination.cable_end = None
termination.save()
super().delete(*args, **kwargs)
@@ -203,7 +203,8 @@ class ConsolePortTemplate(ModularComponentTemplateModel):
verbose_name=_('type'),
max_length=50,
choices=ConsolePortTypeChoices,
blank=True
blank=True,
null=True
)
component_model = ConsolePort
@@ -237,7 +238,8 @@ class ConsoleServerPortTemplate(ModularComponentTemplateModel):
verbose_name=_('type'),
max_length=50,
choices=ConsolePortTypeChoices,
blank=True
blank=True,
null=True
)
component_model = ConsoleServerPort
@@ -272,7 +274,8 @@ class PowerPortTemplate(ModularComponentTemplateModel):
verbose_name=_('type'),
max_length=50,
choices=PowerPortTypeChoices,
blank=True
blank=True,
null=True
)
maximum_draw = models.PositiveIntegerField(
verbose_name=_('maximum draw'),
@@ -334,7 +337,8 @@ class PowerOutletTemplate(ModularComponentTemplateModel):
verbose_name=_('type'),
max_length=50,
choices=PowerOutletTypeChoices,
blank=True
blank=True,
null=True
)
power_port = models.ForeignKey(
to='dcim.PowerPortTemplate',
@@ -348,6 +352,7 @@ class PowerOutletTemplate(ModularComponentTemplateModel):
max_length=50,
choices=PowerOutletFeedLegChoices,
blank=True,
null=True,
help_text=_('Phase (for three-phase feeds)')
)
@@ -434,18 +439,21 @@ class InterfaceTemplate(ModularComponentTemplateModel):
max_length=50,
choices=InterfacePoEModeChoices,
blank=True,
null=True,
verbose_name=_('PoE mode')
)
poe_type = models.CharField(
max_length=50,
choices=InterfacePoETypeChoices,
blank=True,
null=True,
verbose_name=_('PoE type')
)
rf_role = models.CharField(
max_length=30,
choices=WirelessRoleChoices,
blank=True,
null=True,
verbose_name=_('wireless role')
)
+12 -1
View File
@@ -142,8 +142,9 @@ class CabledObjectModel(models.Model):
cable_end = models.CharField(
verbose_name=_('cable end'),
max_length=1,
choices=CableEndChoices,
blank=True,
choices=CableEndChoices
null=True
)
mark_connected = models.BooleanField(
verbose_name=_('mark connected'),
@@ -283,6 +284,7 @@ class ConsolePort(ModularComponentModel, CabledObjectModel, PathEndpoint, Tracki
max_length=50,
choices=ConsolePortTypeChoices,
blank=True,
null=True,
help_text=_('Physical port type')
)
speed = models.PositiveIntegerField(
@@ -309,6 +311,7 @@ class ConsoleServerPort(ModularComponentModel, CabledObjectModel, PathEndpoint,
max_length=50,
choices=ConsolePortTypeChoices,
blank=True,
null=True,
help_text=_('Physical port type')
)
speed = models.PositiveIntegerField(
@@ -339,6 +342,7 @@ class PowerPort(ModularComponentModel, CabledObjectModel, PathEndpoint, Tracking
max_length=50,
choices=PowerPortTypeChoices,
blank=True,
null=True,
help_text=_('Physical port type')
)
maximum_draw = models.PositiveIntegerField(
@@ -454,6 +458,7 @@ class PowerOutlet(ModularComponentModel, CabledObjectModel, PathEndpoint, Tracki
max_length=50,
choices=PowerOutletTypeChoices,
blank=True,
null=True,
help_text=_('Physical port type')
)
power_port = models.ForeignKey(
@@ -468,6 +473,7 @@ class PowerOutlet(ModularComponentModel, CabledObjectModel, PathEndpoint, Tracki
max_length=50,
choices=PowerOutletFeedLegChoices,
blank=True,
null=True,
help_text=_('Phase (for three-phase feeds)')
)
color = ColorField(
@@ -522,6 +528,7 @@ class BaseInterface(models.Model):
max_length=50,
choices=InterfaceModeChoices,
blank=True,
null=True,
help_text=_('IEEE 802.1Q tagging strategy')
)
parent = models.ForeignKey(
@@ -624,12 +631,14 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
max_length=30,
choices=WirelessRoleChoices,
blank=True,
null=True,
verbose_name=_('wireless role')
)
rf_channel = models.CharField(
max_length=50,
choices=WirelessChannelChoices,
blank=True,
null=True,
verbose_name=_('wireless channel')
)
rf_channel_frequency = models.DecimalField(
@@ -658,12 +667,14 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
max_length=50,
choices=InterfacePoEModeChoices,
blank=True,
null=True,
verbose_name=_('PoE mode')
)
poe_type = models.CharField(
max_length=50,
choices=InterfacePoETypeChoices,
blank=True,
null=True,
verbose_name=_('PoE type')
)
wireless_link = models.ForeignKey(
+8 -3
View File
@@ -118,6 +118,7 @@ class DeviceType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
max_length=50,
choices=SubdeviceRoleChoices,
blank=True,
null=True,
verbose_name=_('parent/child status'),
help_text=_('Parent devices house child devices in device bays. Leave blank '
'if this device type is neither a parent nor a child.')
@@ -126,7 +127,8 @@ class DeviceType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
verbose_name=_('airflow'),
max_length=50,
choices=DeviceAirflowChoices,
blank=True
blank=True,
null=True
)
front_image = models.ImageField(
upload_to='devicetype-images',
@@ -387,7 +389,8 @@ class ModuleType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
verbose_name=_('airflow'),
max_length=50,
choices=ModuleAirflowChoices,
blank=True
blank=True,
null=True
)
clone_fields = ('manufacturer', 'weight', 'weight_unit', 'airflow')
@@ -632,6 +635,7 @@ class Device(
face = models.CharField(
max_length=50,
blank=True,
null=True,
choices=DeviceFaceChoices,
verbose_name=_('rack face')
)
@@ -645,7 +649,8 @@ class Device(
verbose_name=_('airflow'),
max_length=50,
choices=DeviceAirflowChoices,
blank=True
blank=True,
null=True
)
primary_ip4 = models.OneToOneField(
to='ipam.IPAddress',
+7 -4
View File
@@ -83,7 +83,8 @@ class RackBase(WeightMixin, PrimaryModel):
verbose_name=_('outer unit'),
max_length=50,
choices=RackDimensionUnitChoices,
blank=True
blank=True,
null=True
)
mounting_depth = models.PositiveSmallIntegerField(
verbose_name=_('mounting depth'),
@@ -188,7 +189,7 @@ class RackType(RackBase):
# Clear unit if outer width & depth are not set
if self.outer_width is None and self.outer_depth is None:
self.outer_unit = ''
self.outer_unit = None
super().save(*args, **kwargs)
@@ -242,6 +243,7 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, RackBase):
choices=RackFormFactorChoices,
max_length=50,
blank=True,
null=True,
verbose_name=_('form factor')
)
rack_type = models.ForeignKey(
@@ -317,7 +319,8 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, RackBase):
verbose_name=_('airflow'),
max_length=50,
choices=RackAirflowChoices,
blank=True
blank=True,
null=True
)
# Generic relations
@@ -409,7 +412,7 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, RackBase):
# Clear unit if outer width & depth are not set
if self.outer_width is None and self.outer_depth is None:
self.outer_unit = ''
self.outer_unit = None
super().save(*args, **kwargs)
-1
View File
@@ -871,7 +871,6 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_outer_unit(self):
self.assertEqual(Rack.objects.filter(outer_unit__isnull=False).count(), 5)
params = {'outer_unit': RackDimensionUnitChoices.UNIT_MILLIMETER}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+1 -1
View File
@@ -592,7 +592,7 @@ class DeviceTypeTestCase(
'part_number': '123ABC',
'u_height': 2,
'is_full_depth': True,
'subdevice_role': '', # CharField
'subdevice_role': None,
'comments': 'Some comments',
'tags': [t.pk for t in tags],
}