#8198: Rename CustomField.validation_unique to unique (#17325)

* #8198: Rename CustomField.validation_unique to unique

* Update CustomField model documentation
This commit is contained in:
Jeremy Stretch 2024-08-30 13:44:03 -04:00 committed by GitHub
parent 8fff4e2a5d
commit b4dd57f3c7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 62 additions and 62 deletions

View File

@ -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).

View File

@ -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')

View File

@ -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):

View File

@ -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')

View File

@ -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',
) )

View File

@ -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):

View File

@ -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')

View File

@ -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),
), ),
] ]

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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():

View File

@ -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">