17558 raise validation error if removing used choice from ChoiceSet (#17591)

* 17558 raise validation error if removing choice from choiceset that is currently used

* 17558 raise validation error if removing choice from choiceset that is currently used

* 17558 raise validation error if removing choice from choiceset that is currently used

* 17558 add tests

* 17558 add tests

* Tightened up choice evaluation logic a bit; cleaned up test

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
This commit is contained in:
Arthur Hanson
2024-09-30 10:17:01 -07:00
committed by GitHub
parent 5013a6c692
commit a9fee5cd32
2 changed files with 100 additions and 0 deletions

View File

@@ -785,6 +785,12 @@ class CustomFieldChoiceSet(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel
def __str__(self):
return self.name
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Cache the initial set of choices for comparison under clean()
self._original_extra_choices = self.__dict__.get('extra_choices')
def get_absolute_url(self):
return reverse('extras:customfieldchoiceset', args=[self.pk])
@@ -818,6 +824,32 @@ class CustomFieldChoiceSet(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel
if not self.base_choices and not self.extra_choices:
raise ValidationError(_("Must define base or extra choices."))
# Check whether any choices have been removed. If so, check whether any of the removed
# choices are still set in custom field data for any object.
original_choices = set([
c[0] for c in self._original_extra_choices
]) if self._original_extra_choices else set()
current_choices = set([
c[0] for c in self.extra_choices
]) if self.extra_choices else set()
if removed_choices := original_choices - current_choices:
for custom_field in self.choices_for.all():
for object_type in custom_field.object_types.all():
model = object_type.model_class()
for choice in removed_choices:
# Form the query based on the type of custom field
if custom_field.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
query_args = {f"custom_field_data__{custom_field.name}__contains": choice}
else:
query_args = {f"custom_field_data__{custom_field.name}": choice}
# Raise a ValidationError if there are any objects which still reference the removed choice
if model.objects.filter(models.Q(**query_args)).exists():
raise ValidationError(
_(
"Cannot remove choice {choice} as there are {model} objects which reference it."
).format(choice=choice, model=object_type)
)
def save(self, *args, **kwargs):
# Sort choices if alphabetical ordering is enforced