Convert all optional choice fields to use a null empty value #10367

Closed
opened 2025-12-29 21:30:32 +01:00 by adam · 1 comment
Owner

Originally created by @jeremystretch on GitHub (Oct 15, 2024).

Originally assigned to: @jeremystretch on GitHub.

NetBox version

v4.1.3

Feature type

Change to existing functionality

Triage priority

N/A

Proposed functionality

Several models include optional choices fields, such as Cable.type. Most (if not all) of these store an empty value as an empty string ('') rather than null. This FR proposes:

  1. Changing the empty value of these fields to null
  2. Introducing migrations to replace all empty string values with null for these fields

The list of specific fields to be updated is TBD, but is expect to include any CharField for which choices is defined and null is False.

Use case

The use of empty strings to represent empty values for these fields deviates from other fields (e.g. integer-based fields) and complicates the logic needed to achieve complex filtering (see #17575 for an example). This change will help us standardize the behavior of the data model in general.

Database changes

All CharFields which define a set of choices will be modified to store empty values as null in the database. Migrations will be included to effect this change on existing data.

External dependencies

N/A

Originally created by @jeremystretch on GitHub (Oct 15, 2024). Originally assigned to: @jeremystretch on GitHub. ### NetBox version v4.1.3 ### Feature type Change to existing functionality ### Triage priority N/A ### Proposed functionality Several models include optional choices fields, such as `Cable.type`. Most (if not all) of these store an empty value as an empty string (`''`) rather than `null`. This FR proposes: 1. Changing the empty value of these fields to `null` 2. Introducing migrations to replace all empty string values with `null` for these fields The list of specific fields to be updated is TBD, but is expect to include any CharField for which `choices` is defined and `null` is False. ### Use case The use of empty strings to represent empty values for these fields deviates from other fields (e.g. integer-based fields) and complicates the logic needed to achieve complex filtering (see #17575 for an example). This change will help us standardize the behavior of the data model in general. ### Database changes All CharFields which define a set of choices will be modified to store empty values as `null` in the database. Migrations will be included to effect this change on existing data. ### External dependencies N/A
adam added the status: acceptedtype: featurecomplexity: mediumnetbox labels 2025-12-29 21:30:32 +01:00
adam closed this issue 2025-12-29 21:30:32 +01:00
Author
Owner

@jeremystretch commented on GitHub (Oct 16, 2024):

These are the affected fields:

  • circuits.CircuitGroupAssignment: priority
  • circuits.CircuitTermination: cable_end
  • dcim.ConsolePort: cable_end, type
  • dcim.ConsoleServerPort: cable_end, type
  • dcim.PowerPort: cable_end, type
  • dcim.PowerOutlet: cable_end, type, feed_leg
  • dcim.Interface: cable_end, mode, rf_role, rf_channel, poe_mode, poe_type
  • dcim.FrontPort: cable_end
  • dcim.RearPort: cable_end
  • dcim.Cable: type, length_unit
  • dcim.ConsolePortTemplate: type
  • dcim.ConsoleServerPortTemplate: type
  • dcim.PowerPortTemplate: type
  • dcim.PowerOutletTemplate: type, feed_leg
  • dcim.InterfaceTemplate: poe_mode, poe_type, rf_role
  • dcim.DeviceType: weight_unit, subdevice_role, airflow
  • dcim.ModuleType: weight_unit, airflow
  • dcim.Device: face, airflow
  • dcim.PowerFeed: cable_end
  • dcim.RackType: weight_unit, outer_unit
  • dcim.Rack: weight_unit, outer_unit, form_factor, airflow
  • ipam.FHRPGroup: auth_type
  • ipam.IPAddress: role
  • extras.CustomFieldChoiceSet: base_choices
  • tenancy.ContactAssignment: priority
  • virtualization.VMInterface: mode
  • vpn.IKEProposal: authentication_algorithm
  • vpn.IKEPolicy: mode
  • vpn.IPSecProposal: encryption_algorithm, authentication_algorithm
  • wireless.WirelessLAN: auth_type, auth_cipher
  • wireless.WirelessLink: auth_type, auth_cipher, distance_unit
Audit script
from django.apps import apps
from django.db import models

for model in apps.get_models():
    fields = []
    for field in model._meta.get_fields():
        if isinstance(field, models.CharField) and field.choices and field.blank and not field.null:
            fields.append(f"`{field.name}`")
    if fields:
        print(f"* {model._meta.label}: {', '.join(fields)}")
@jeremystretch commented on GitHub (Oct 16, 2024): These are the affected fields: * circuits.CircuitGroupAssignment: `priority` * circuits.CircuitTermination: `cable_end` * dcim.ConsolePort: `cable_end`, `type` * dcim.ConsoleServerPort: `cable_end`, `type` * dcim.PowerPort: `cable_end`, `type` * dcim.PowerOutlet: `cable_end`, `type`, `feed_leg` * dcim.Interface: `cable_end`, `mode`, `rf_role`, `rf_channel`, `poe_mode`, `poe_type` * dcim.FrontPort: `cable_end` * dcim.RearPort: `cable_end` * dcim.Cable: `type`, `length_unit` * dcim.ConsolePortTemplate: `type` * dcim.ConsoleServerPortTemplate: `type` * dcim.PowerPortTemplate: `type` * dcim.PowerOutletTemplate: `type`, `feed_leg` * dcim.InterfaceTemplate: `poe_mode`, `poe_type`, `rf_role` * dcim.DeviceType: `weight_unit`, `subdevice_role`, `airflow` * dcim.ModuleType: `weight_unit`, `airflow` * dcim.Device: `face`, `airflow` * dcim.PowerFeed: `cable_end` * dcim.RackType: `weight_unit`, `outer_unit` * dcim.Rack: `weight_unit`, `outer_unit`, `form_factor`, `airflow` * ipam.FHRPGroup: `auth_type` * ipam.IPAddress: `role` * extras.CustomFieldChoiceSet: `base_choices` * tenancy.ContactAssignment: `priority` * virtualization.VMInterface: `mode` * vpn.IKEProposal: `authentication_algorithm` * vpn.IKEPolicy: `mode` * vpn.IPSecProposal: `encryption_algorithm`, `authentication_algorithm` * wireless.WirelessLAN: `auth_type`, `auth_cipher` * wireless.WirelessLink: `auth_type`, `auth_cipher`, `distance_unit` <details> <summary>Audit script</summary> ```python from django.apps import apps from django.db import models for model in apps.get_models(): fields = [] for field in model._meta.get_fields(): if isinstance(field, models.CharField) and field.choices and field.blank and not field.null: fields.append(f"`{field.name}`") if fields: print(f"* {model._meta.label}: {', '.join(fields)}") ``` </details>
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/netbox#10367