Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 53 additions & 5 deletions netbox_custom_objects/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# from django.contrib.contenttypes.management import create_contenttypes
from django.contrib.contenttypes.models import ContentType
from django.core.validators import RegexValidator, ValidationError
from django.db import connection, models
from django.db import connection, models, IntegrityError
from django.db.models import Q
from django.db.models.functions import Lower
from django.db.models.signals import pre_delete
Expand Down Expand Up @@ -52,6 +52,13 @@
from netbox_custom_objects.constants import APP_LABEL
from netbox_custom_objects.field_types import FIELD_TYPE_CLASS


class UniquenessConstraintTestError(Exception):
"""Custom exception used to signal successful uniqueness constraint test."""

pass


USER_TABLE_DATABASE_NAME_PREFIX = "custom_objects_"


Expand Down Expand Up @@ -310,7 +317,10 @@ def _fetch_and_generate_field_attrs(
for field in fields:
field_type = FIELD_TYPE_CLASS[field.type]()
if skip_object_fields:
if field.type in [CustomFieldTypeChoices.TYPE_OBJECT, CustomFieldTypeChoices.TYPE_MULTIOBJECT]:
if field.type in [
CustomFieldTypeChoices.TYPE_OBJECT,
CustomFieldTypeChoices.TYPE_MULTIOBJECT,
]:
continue

field_name = field.name
Expand Down Expand Up @@ -453,7 +463,9 @@ def get_model(
"custom_object_type_id": self.id,
}

field_attrs = self._fetch_and_generate_field_attrs(fields, skip_object_fields=skip_object_fields)
field_attrs = self._fetch_and_generate_field_attrs(
fields, skip_object_fields=skip_object_fields
)

attrs.update(**field_attrs)

Expand Down Expand Up @@ -587,7 +599,7 @@ class CustomObjectTypeField(CloningMixin, ExportTemplatesMixin, ChangeLoggedMode
name = models.CharField(
verbose_name=_("name"),
max_length=50,
help_text=_("Internal field name, e.g. \"vendor_label\""),
help_text=_('Internal field name, e.g. "vendor_label"'),
validators=(
RegexValidator(
regex=r"^[a-z0-9_]+$",
Expand Down Expand Up @@ -616,7 +628,9 @@ class CustomObjectTypeField(CloningMixin, ExportTemplatesMixin, ChangeLoggedMode
verbose_name=_("group name"),
max_length=50,
blank=True,
help_text=_("Custom object fields within the same group will be displayed together"),
help_text=_(
"Custom object fields within the same group will be displayed together"
),
)
description = models.CharField(
verbose_name=_("description"), max_length=200, blank=True
Expand Down Expand Up @@ -862,6 +876,40 @@ def clean(self):
{"unique": _("Uniqueness cannot be enforced for boolean fields")}
)

# Check if uniqueness constraint can be applied when changing from non-unique to unique
if (
self.pk
and self.unique
and not self.original.unique
and not self._state.adding
):
field_type = FIELD_TYPE_CLASS[self.type]()
model_field = field_type.get_model_field(self)
model = self.custom_object_type.get_model()
model_field.contribute_to_class(model, self.name)

old_field = field_type.get_model_field(self.original)
old_field.contribute_to_class(model, self._original_name)

try:
with connection.schema_editor() as test_schema_editor:
test_schema_editor.alter_field(model, old_field, model_field)
# If we get here, the constraint was applied successfully
# Now raise a custom exception to rollback the test transaction
raise UniquenessConstraintTestError()
except UniquenessConstraintTestError:
# The constraint can be applied, validation passes
pass
except IntegrityError:
# The constraint cannot be applied due to existing non-unique values
raise ValidationError(
{
"unique": _(
"Custom objects with non-unique values already exist so this action isn't permitted"
)
}
)

# Choice set must be set on selection fields, and *only* on selection fields
if self.type in (
CustomFieldTypeChoices.TYPE_SELECT,
Expand Down