mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 01:41:22 -06:00
* #8198: Rename CustomField.validation_unique to unique * Update CustomField model documentation
This commit is contained in:
parent
8fff4e2a5d
commit
b4dd57f3c7
@ -57,7 +57,11 @@ A numeric weight used to override alphabetic ordering of fields by name. Custom
|
|||||||
|
|
||||||
### Required
|
### 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
|
### Description
|
||||||
|
|
||||||
@ -116,7 +120,3 @@ For numeric custom fields only. The maximum valid value (optional).
|
|||||||
### Validation Regex
|
### Validation Regex
|
||||||
|
|
||||||
For string-based custom fields only. A regular expression used to validate the field's value (optional).
|
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).
|
|
||||||
|
@ -61,9 +61,10 @@ class CustomFieldSerializer(ValidatedModelSerializer):
|
|||||||
model = CustomField
|
model = CustomField
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display_url', 'display', 'object_types', 'type', 'related_object_type', 'data_type',
|
'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',
|
'name', 'label', 'group_name', 'description', 'required', 'unique', 'search_weight', 'filter_logic',
|
||||||
'ui_editable', 'is_cloneable', 'default', 'related_object_filter', 'weight', 'validation_minimum', 'validation_maximum',
|
'ui_visible', 'ui_editable', 'is_cloneable', 'default', 'related_object_filter', 'weight',
|
||||||
'validation_regex', 'validation_unique', 'choice_set', 'comments', 'created', 'last_updated',
|
'validation_minimum', 'validation_maximum', 'validation_regex', 'choice_set', 'comments', 'created',
|
||||||
|
'last_updated',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||||
|
|
||||||
|
@ -158,9 +158,9 @@ class CustomFieldFilterSet(ChangeLoggedModelFilterSet):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = CustomField
|
model = CustomField
|
||||||
fields = (
|
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',
|
'ui_editable', 'weight', 'is_cloneable', 'description', 'validation_minimum', 'validation_maximum',
|
||||||
'validation_regex', 'validation_unique',
|
'validation_regex',
|
||||||
)
|
)
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
|
@ -44,6 +44,11 @@ class CustomFieldBulkEditForm(BulkEditForm):
|
|||||||
required=False,
|
required=False,
|
||||||
widget=BulkEditNullBooleanSelect()
|
widget=BulkEditNullBooleanSelect()
|
||||||
)
|
)
|
||||||
|
unique = forms.NullBooleanField(
|
||||||
|
label=_('Must be unique'),
|
||||||
|
required=False,
|
||||||
|
widget=BulkEditNullBooleanSelect()
|
||||||
|
)
|
||||||
weight = forms.IntegerField(
|
weight = forms.IntegerField(
|
||||||
label=_('Weight'),
|
label=_('Weight'),
|
||||||
required=False
|
required=False
|
||||||
@ -79,19 +84,12 @@ class CustomFieldBulkEditForm(BulkEditForm):
|
|||||||
label=_('Validation regex'),
|
label=_('Validation regex'),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
validation_unique = forms.NullBooleanField(
|
|
||||||
label=_('Must be unique'),
|
|
||||||
required=False,
|
|
||||||
widget=BulkEditNullBooleanSelect()
|
|
||||||
)
|
|
||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
|
|
||||||
fieldsets = (
|
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('ui_visible', 'ui_editable', 'is_cloneable', name=_('Behavior')),
|
||||||
FieldSet(
|
FieldSet('validation_minimum', 'validation_maximum', 'validation_regex', name=_('Validation')),
|
||||||
'validation_minimum', 'validation_maximum', 'validation_regex', 'validation_unique', name=_('Validation')
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
nullable_fields = ('group_name', 'description', 'choice_set')
|
nullable_fields = ('group_name', 'description', 'choice_set')
|
||||||
|
|
||||||
|
@ -72,10 +72,9 @@ class CustomFieldImportForm(CSVModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = CustomField
|
model = CustomField
|
||||||
fields = (
|
fields = (
|
||||||
'name', 'label', 'group_name', 'type', 'object_types', 'related_object_type', 'required', 'description',
|
'name', 'label', 'group_name', 'type', 'object_types', 'related_object_type', 'required', 'unique',
|
||||||
'search_weight', 'filter_logic', 'default', 'choice_set', 'weight', 'validation_minimum',
|
'description', 'search_weight', 'filter_logic', 'default', 'choice_set', 'weight', 'validation_minimum',
|
||||||
'validation_maximum', 'validation_regex', 'validation_unique', 'ui_visible', 'ui_editable', 'is_cloneable',
|
'validation_maximum', 'validation_regex', 'ui_visible', 'ui_editable', 'is_cloneable', 'comments',
|
||||||
'comments',
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -40,12 +40,11 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
|
|||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id'),
|
FieldSet('q', 'filter_id'),
|
||||||
FieldSet(
|
FieldSet(
|
||||||
'type', 'related_object_type_id', 'group_name', 'weight', 'required', 'choice_set_id', 'ui_visible',
|
'type', 'related_object_type_id', 'group_name', 'weight', 'required', 'unique', 'choice_set_id',
|
||||||
'ui_editable', 'is_cloneable', name=_('Attributes')
|
name=_('Attributes')
|
||||||
),
|
|
||||||
FieldSet(
|
|
||||||
'validation_minimum', 'validation_maximum', 'validation_regex', 'validation_unique', name=_('Validation')
|
|
||||||
),
|
),
|
||||||
|
FieldSet('ui_visible', 'ui_editable', 'is_cloneable', name=_('Behavior')),
|
||||||
|
FieldSet('validation_minimum', 'validation_maximum', 'validation_regex', name=_('Validation')),
|
||||||
)
|
)
|
||||||
related_object_type_id = ContentTypeMultipleChoiceField(
|
related_object_type_id = ContentTypeMultipleChoiceField(
|
||||||
queryset=ObjectType.objects.with_feature('custom_fields'),
|
queryset=ObjectType.objects.with_feature('custom_fields'),
|
||||||
@ -72,6 +71,13 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
|
|||||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
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(
|
choice_set_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=CustomFieldChoiceSet.objects.all(),
|
queryset=CustomFieldChoiceSet.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
@ -106,13 +112,6 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
|
|||||||
label=_('Validation regex'),
|
label=_('Validation regex'),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
validation_unique = forms.NullBooleanField(
|
|
||||||
label=_('Must be unique'),
|
|
||||||
required=False,
|
|
||||||
widget=forms.Select(
|
|
||||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, FilterForm):
|
class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, FilterForm):
|
||||||
|
@ -69,8 +69,8 @@ class CustomFieldForm(forms.ModelForm):
|
|||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet(
|
FieldSet(
|
||||||
'object_types', 'name', 'label', 'group_name', 'description', 'type', 'required', 'validation_unique',
|
'object_types', 'name', 'label', 'group_name', 'description', 'type', 'required', 'unique', 'default',
|
||||||
'default', name=_('Custom Field')
|
name=_('Custom Field')
|
||||||
),
|
),
|
||||||
FieldSet(
|
FieldSet(
|
||||||
'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'weight', 'is_cloneable', name=_('Behavior')
|
'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'weight', 'is_cloneable', name=_('Behavior')
|
||||||
|
@ -10,7 +10,7 @@ class Migration(migrations.Migration):
|
|||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='customfield',
|
model_name='customfield',
|
||||||
name='validation_unique',
|
name='unique',
|
||||||
field=models.BooleanField(default=False),
|
field=models.BooleanField(default=False),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -129,7 +129,12 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
required = models.BooleanField(
|
required = models.BooleanField(
|
||||||
verbose_name=_('required'),
|
verbose_name=_('required'),
|
||||||
default=False,
|
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(
|
search_weight = models.PositiveSmallIntegerField(
|
||||||
verbose_name=_('search weight'),
|
verbose_name=_('search weight'),
|
||||||
@ -189,11 +194,6 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
'example, <code>^[A-Z]{3}$</code> will limit values to exactly three uppercase letters.'
|
'example, <code>^[A-Z]{3}$</code> 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(
|
choice_set = models.ForeignKey(
|
||||||
to='CustomFieldChoiceSet',
|
to='CustomFieldChoiceSet',
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
@ -229,9 +229,9 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
objects = CustomFieldManager()
|
objects = CustomFieldManager()
|
||||||
|
|
||||||
clone_fields = (
|
clone_fields = (
|
||||||
'object_types', 'type', 'related_object_type', 'group_name', 'description', 'required', 'search_weight',
|
'object_types', 'type', 'related_object_type', 'group_name', 'description', 'required', 'unique',
|
||||||
'filter_logic', 'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex',
|
'search_weight', 'filter_logic', 'default', 'weight', 'validation_minimum', 'validation_maximum',
|
||||||
'validation_unique', 'choice_set', 'ui_visible', 'ui_editable', 'is_cloneable',
|
'validation_regex', 'choice_set', 'ui_visible', 'ui_editable', 'is_cloneable',
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -349,9 +349,9 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
})
|
})
|
||||||
|
|
||||||
# Uniqueness can not be enforced for boolean fields
|
# 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({
|
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
|
# Choice set must be set on selection fields, and *only* on selection fields
|
||||||
|
@ -65,6 +65,10 @@ class CustomFieldTable(NetBoxTable):
|
|||||||
verbose_name=_('Required'),
|
verbose_name=_('Required'),
|
||||||
false_mark=None
|
false_mark=None
|
||||||
)
|
)
|
||||||
|
unique = columns.BooleanColumn(
|
||||||
|
verbose_name=_('Validate Uniqueness'),
|
||||||
|
false_mark=None
|
||||||
|
)
|
||||||
ui_visible = columns.ChoiceFieldColumn(
|
ui_visible = columns.ChoiceFieldColumn(
|
||||||
verbose_name=_('Visible')
|
verbose_name=_('Visible')
|
||||||
)
|
)
|
||||||
@ -99,19 +103,18 @@ class CustomFieldTable(NetBoxTable):
|
|||||||
validation_regex = tables.Column(
|
validation_regex = tables.Column(
|
||||||
verbose_name=_('Validation Regex'),
|
verbose_name=_('Validation Regex'),
|
||||||
)
|
)
|
||||||
validation_unique = columns.BooleanColumn(
|
|
||||||
verbose_name=_('Validate Uniqueness'),
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = CustomField
|
model = CustomField
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'object_types', 'label', 'type', 'related_object_type', 'group_name', 'required',
|
'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',
|
'unique', 'default', 'description', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable',
|
||||||
'weight', 'choice_set', 'choices', 'validation_minimum', 'validation_maximum', 'validation_regex',
|
'is_cloneable', 'weight', 'choice_set', 'choices', 'validation_minimum', 'validation_maximum',
|
||||||
'validation_unique', 'comments', 'created', 'last_updated',
|
'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):
|
class CustomFieldChoiceSetTable(NetBoxTable):
|
||||||
|
@ -1143,7 +1143,7 @@ class CustomFieldAPITest(APITestCase):
|
|||||||
def test_uniqueness_validation(self):
|
def test_uniqueness_validation(self):
|
||||||
# Create a unique custom field
|
# Create a unique custom field
|
||||||
cf_text = CustomField.objects.get(name='text_field')
|
cf_text = CustomField.objects.get(name='text_field')
|
||||||
cf_text.validation_unique = True
|
cf_text.unique = True
|
||||||
cf_text.save()
|
cf_text.save()
|
||||||
|
|
||||||
# Set a value on site 1
|
# Set a value on site 1
|
||||||
|
@ -288,7 +288,7 @@ class CustomFieldsMixin(models.Model):
|
|||||||
))
|
))
|
||||||
|
|
||||||
# Validate uniqueness if enforced
|
# 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(**{
|
if self._meta.model.objects.exclude(pk=self.pk).filter(**{
|
||||||
f'custom_field_data__{field_name}': value
|
f'custom_field_data__{field_name}': value
|
||||||
}).exists():
|
}).exists():
|
||||||
|
@ -38,6 +38,10 @@
|
|||||||
<th scope="row">{% trans "Required" %}</th>
|
<th scope="row">{% trans "Required" %}</th>
|
||||||
<td>{% checkmark object.required %}</td>
|
<td>{% checkmark object.required %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Must be Unique" %}</th>
|
||||||
|
<td>{% checkmark object.unique %}</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Cloneable" %}</th>
|
<th scope="row">{% trans "Cloneable" %}</th>
|
||||||
<td>{% checkmark object.is_cloneable %}</td>
|
<td>{% checkmark object.is_cloneable %}</td>
|
||||||
@ -128,10 +132,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<th scope="row">{% trans "Must be Unique" %}</th>
|
|
||||||
<td>{% checkmark object.validation_unique %}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
Loading…
Reference in New Issue
Block a user