diff --git a/docs/models/extras/customfield.md b/docs/models/extras/customfield.md
index 626f320be57..9aab66a36ca 100644
--- a/docs/models/extras/customfield.md
+++ b/docs/models/extras/customfield.md
@@ -57,7 +57,11 @@ A numeric weight used to override alphabetic ordering of fields by name. Custom
### Required
-If checked, this custom field must be populated with a valid value for the object to pass validation.
+If enabled, this custom field must be populated with a valid value for the object to pass validation.
+
+### Unique
+
+If enabled, each object must have a unique value set for this custom field (per object type).
### Description
@@ -116,7 +120,3 @@ For numeric custom fields only. The maximum valid value (optional).
### Validation Regex
For string-based custom fields only. A regular expression used to validate the field's value (optional).
-
-### Uniqueness Validation
-
-If enabled, each object must have a unique value set for this custom field (per object type).
diff --git a/netbox/extras/api/serializers_/customfields.py b/netbox/extras/api/serializers_/customfields.py
index 2c8e7c127c2..a65fafc4e91 100644
--- a/netbox/extras/api/serializers_/customfields.py
+++ b/netbox/extras/api/serializers_/customfields.py
@@ -61,9 +61,10 @@ class Meta:
model = CustomField
fields = [
'id', 'url', 'display_url', 'display', 'object_types', 'type', 'related_object_type', 'data_type',
- 'name', 'label', 'group_name', 'description', 'required', 'search_weight', 'filter_logic', 'ui_visible',
- 'ui_editable', 'is_cloneable', 'default', 'related_object_filter', 'weight', 'validation_minimum', 'validation_maximum',
- 'validation_regex', 'validation_unique', 'choice_set', 'comments', 'created', 'last_updated',
+ 'name', 'label', 'group_name', 'description', 'required', 'unique', 'search_weight', 'filter_logic',
+ 'ui_visible', 'ui_editable', 'is_cloneable', 'default', 'related_object_filter', 'weight',
+ 'validation_minimum', 'validation_maximum', 'validation_regex', 'choice_set', 'comments', 'created',
+ 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')
diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py
index 38e7dfc9de9..4f40ce50017 100644
--- a/netbox/extras/filtersets.py
+++ b/netbox/extras/filtersets.py
@@ -158,9 +158,9 @@ class CustomFieldFilterSet(ChangeLoggedModelFilterSet):
class Meta:
model = CustomField
fields = (
- 'id', 'name', 'label', 'group_name', 'required', 'search_weight', 'filter_logic', 'ui_visible',
+ 'id', 'name', 'label', 'group_name', 'required', 'unique', 'search_weight', 'filter_logic', 'ui_visible',
'ui_editable', 'weight', 'is_cloneable', 'description', 'validation_minimum', 'validation_maximum',
- 'validation_regex', 'validation_unique',
+ 'validation_regex',
)
def search(self, queryset, name, value):
diff --git a/netbox/extras/forms/bulk_edit.py b/netbox/extras/forms/bulk_edit.py
index 74cf65c321b..30d06683b83 100644
--- a/netbox/extras/forms/bulk_edit.py
+++ b/netbox/extras/forms/bulk_edit.py
@@ -44,6 +44,11 @@ class CustomFieldBulkEditForm(BulkEditForm):
required=False,
widget=BulkEditNullBooleanSelect()
)
+ unique = forms.NullBooleanField(
+ label=_('Must be unique'),
+ required=False,
+ widget=BulkEditNullBooleanSelect()
+ )
weight = forms.IntegerField(
label=_('Weight'),
required=False
@@ -79,19 +84,12 @@ class CustomFieldBulkEditForm(BulkEditForm):
label=_('Validation regex'),
required=False
)
- validation_unique = forms.NullBooleanField(
- label=_('Must be unique'),
- required=False,
- widget=BulkEditNullBooleanSelect()
- )
comments = CommentField()
fieldsets = (
- FieldSet('group_name', 'description', 'weight', 'choice_set', name=_('Attributes')),
+ FieldSet('group_name', 'description', 'weight', 'required', 'unique', 'choice_set', name=_('Attributes')),
FieldSet('ui_visible', 'ui_editable', 'is_cloneable', name=_('Behavior')),
- FieldSet(
- 'validation_minimum', 'validation_maximum', 'validation_regex', 'validation_unique', name=_('Validation')
- ),
+ FieldSet('validation_minimum', 'validation_maximum', 'validation_regex', name=_('Validation')),
)
nullable_fields = ('group_name', 'description', 'choice_set')
diff --git a/netbox/extras/forms/bulk_import.py b/netbox/extras/forms/bulk_import.py
index 780adb0d198..55c9cd764f4 100644
--- a/netbox/extras/forms/bulk_import.py
+++ b/netbox/extras/forms/bulk_import.py
@@ -72,10 +72,9 @@ class CustomFieldImportForm(CSVModelForm):
class Meta:
model = CustomField
fields = (
- 'name', 'label', 'group_name', 'type', 'object_types', 'related_object_type', 'required', 'description',
- 'search_weight', 'filter_logic', 'default', 'choice_set', 'weight', 'validation_minimum',
- 'validation_maximum', 'validation_regex', 'validation_unique', 'ui_visible', 'ui_editable', 'is_cloneable',
- 'comments',
+ 'name', 'label', 'group_name', 'type', 'object_types', 'related_object_type', 'required', 'unique',
+ 'description', 'search_weight', 'filter_logic', 'default', 'choice_set', 'weight', 'validation_minimum',
+ 'validation_maximum', 'validation_regex', 'ui_visible', 'ui_editable', 'is_cloneable', 'comments',
)
diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py
index bd3883877b8..05dcf96c476 100644
--- a/netbox/extras/forms/filtersets.py
+++ b/netbox/extras/forms/filtersets.py
@@ -40,12 +40,11 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = (
FieldSet('q', 'filter_id'),
FieldSet(
- 'type', 'related_object_type_id', 'group_name', 'weight', 'required', 'choice_set_id', 'ui_visible',
- 'ui_editable', 'is_cloneable', name=_('Attributes')
- ),
- FieldSet(
- 'validation_minimum', 'validation_maximum', 'validation_regex', 'validation_unique', name=_('Validation')
+ 'type', 'related_object_type_id', 'group_name', 'weight', 'required', 'unique', 'choice_set_id',
+ name=_('Attributes')
),
+ FieldSet('ui_visible', 'ui_editable', 'is_cloneable', name=_('Behavior')),
+ FieldSet('validation_minimum', 'validation_maximum', 'validation_regex', name=_('Validation')),
)
related_object_type_id = ContentTypeMultipleChoiceField(
queryset=ObjectType.objects.with_feature('custom_fields'),
@@ -72,6 +71,13 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
+ unique = forms.NullBooleanField(
+ label=_('Must be unique'),
+ required=False,
+ widget=forms.Select(
+ choices=BOOLEAN_WITH_BLANK_CHOICES
+ )
+ )
choice_set_id = DynamicModelMultipleChoiceField(
queryset=CustomFieldChoiceSet.objects.all(),
required=False,
@@ -106,13 +112,6 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
label=_('Validation regex'),
required=False
)
- validation_unique = forms.NullBooleanField(
- label=_('Must be unique'),
- required=False,
- widget=forms.Select(
- choices=BOOLEAN_WITH_BLANK_CHOICES
- )
- )
class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, FilterForm):
diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py
index e2a95281e72..a45daaf7084 100644
--- a/netbox/extras/forms/model_forms.py
+++ b/netbox/extras/forms/model_forms.py
@@ -69,8 +69,8 @@ class CustomFieldForm(forms.ModelForm):
fieldsets = (
FieldSet(
- 'object_types', 'name', 'label', 'group_name', 'description', 'type', 'required', 'validation_unique',
- 'default', name=_('Custom Field')
+ 'object_types', 'name', 'label', 'group_name', 'description', 'type', 'required', 'unique', 'default',
+ name=_('Custom Field')
),
FieldSet(
'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'weight', 'is_cloneable', name=_('Behavior')
diff --git a/netbox/extras/migrations/0118_customfield_uniqueness.py b/netbox/extras/migrations/0118_customfield_uniqueness.py
index 8babca696c6..b7693aa24e8 100644
--- a/netbox/extras/migrations/0118_customfield_uniqueness.py
+++ b/netbox/extras/migrations/0118_customfield_uniqueness.py
@@ -10,7 +10,7 @@ class Migration(migrations.Migration):
operations = [
migrations.AddField(
model_name='customfield',
- name='validation_unique',
+ name='unique',
field=models.BooleanField(default=False),
),
]
diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py
index 6c39080b7c6..839a6ace91c 100644
--- a/netbox/extras/models/customfields.py
+++ b/netbox/extras/models/customfields.py
@@ -129,7 +129,12 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
required = models.BooleanField(
verbose_name=_('required'),
default=False,
- help_text=_("If true, this field is required when creating new objects or editing an existing object.")
+ help_text=_("This field is required when creating new objects or editing an existing object.")
+ )
+ unique = models.BooleanField(
+ verbose_name=_('must be unique'),
+ default=False,
+ help_text=_("The value of this field must be unique for the assigned object")
)
search_weight = models.PositiveSmallIntegerField(
verbose_name=_('search weight'),
@@ -189,11 +194,6 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
'example, ^[A-Z]{3}$
will limit values to exactly three uppercase letters.'
)
)
- validation_unique = models.BooleanField(
- verbose_name=_('must be unique'),
- default=False,
- help_text=_('The value of this field must be unique for the assigned object')
- )
choice_set = models.ForeignKey(
to='CustomFieldChoiceSet',
on_delete=models.PROTECT,
@@ -229,9 +229,9 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
objects = CustomFieldManager()
clone_fields = (
- 'object_types', 'type', 'related_object_type', 'group_name', 'description', 'required', 'search_weight',
- 'filter_logic', 'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex',
- 'validation_unique', 'choice_set', 'ui_visible', 'ui_editable', 'is_cloneable',
+ 'object_types', 'type', 'related_object_type', 'group_name', 'description', 'required', 'unique',
+ 'search_weight', 'filter_logic', 'default', 'weight', 'validation_minimum', 'validation_maximum',
+ 'validation_regex', 'choice_set', 'ui_visible', 'ui_editable', 'is_cloneable',
)
class Meta:
@@ -349,9 +349,9 @@ def clean(self):
})
# Uniqueness can not be enforced for boolean fields
- if self.validation_unique and self.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
+ if self.unique and self.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
raise ValidationError({
- 'validation_unique': _("Uniqueness cannot be enforced for boolean fields")
+ 'unique': _("Uniqueness cannot be enforced for boolean fields")
})
# Choice set must be set on selection fields, and *only* on selection fields
diff --git a/netbox/extras/tables/tables.py b/netbox/extras/tables/tables.py
index 8a4e802096a..e538c488ec0 100644
--- a/netbox/extras/tables/tables.py
+++ b/netbox/extras/tables/tables.py
@@ -65,6 +65,10 @@ class CustomFieldTable(NetBoxTable):
verbose_name=_('Required'),
false_mark=None
)
+ unique = columns.BooleanColumn(
+ verbose_name=_('Validate Uniqueness'),
+ false_mark=None
+ )
ui_visible = columns.ChoiceFieldColumn(
verbose_name=_('Visible')
)
@@ -99,19 +103,18 @@ class CustomFieldTable(NetBoxTable):
validation_regex = tables.Column(
verbose_name=_('Validation Regex'),
)
- validation_unique = columns.BooleanColumn(
- verbose_name=_('Validate Uniqueness'),
- )
class Meta(NetBoxTable.Meta):
model = CustomField
fields = (
'pk', 'id', 'name', 'object_types', 'label', 'type', 'related_object_type', 'group_name', 'required',
- 'default', 'description', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'is_cloneable',
- 'weight', 'choice_set', 'choices', 'validation_minimum', 'validation_maximum', 'validation_regex',
- 'validation_unique', 'comments', 'created', 'last_updated',
+ 'unique', 'default', 'description', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable',
+ 'is_cloneable', 'weight', 'choice_set', 'choices', 'validation_minimum', 'validation_maximum',
+ 'validation_regex', 'comments', 'created', 'last_updated',
+ )
+ default_columns = (
+ 'pk', 'name', 'object_types', 'label', 'group_name', 'type', 'required', 'unique', 'description',
)
- default_columns = ('pk', 'name', 'object_types', 'label', 'group_name', 'type', 'required', 'description')
class CustomFieldChoiceSetTable(NetBoxTable):
diff --git a/netbox/extras/tests/test_customfields.py b/netbox/extras/tests/test_customfields.py
index 009ae8798a2..697b756ecb3 100644
--- a/netbox/extras/tests/test_customfields.py
+++ b/netbox/extras/tests/test_customfields.py
@@ -1143,7 +1143,7 @@ def test_regex_validation(self):
def test_uniqueness_validation(self):
# Create a unique custom field
cf_text = CustomField.objects.get(name='text_field')
- cf_text.validation_unique = True
+ cf_text.unique = True
cf_text.save()
# Set a value on site 1
diff --git a/netbox/netbox/models/features.py b/netbox/netbox/models/features.py
index 43eb089d6ee..45eb7008112 100644
--- a/netbox/netbox/models/features.py
+++ b/netbox/netbox/models/features.py
@@ -288,7 +288,7 @@ def clean(self):
))
# Validate uniqueness if enforced
- if custom_fields[field_name].validation_unique and value not in CUSTOMFIELD_EMPTY_VALUES:
+ if custom_fields[field_name].unique and value not in CUSTOMFIELD_EMPTY_VALUES:
if self._meta.model.objects.exclude(pk=self.pk).filter(**{
f'custom_field_data__{field_name}': value
}).exists():
diff --git a/netbox/templates/extras/customfield.html b/netbox/templates/extras/customfield.html
index 13c2e869178..dbdbd057f76 100644
--- a/netbox/templates/extras/customfield.html
+++ b/netbox/templates/extras/customfield.html
@@ -38,6 +38,10 @@