|  | 
| 12 | 12 | # from django.contrib.contenttypes.management import create_contenttypes | 
| 13 | 13 | from django.contrib.contenttypes.models import ContentType | 
| 14 | 14 | from django.core.validators import RegexValidator, ValidationError | 
| 15 |  | -from django.db import connection, models | 
|  | 15 | +from django.db import connection, IntegrityError, models, transaction | 
| 16 | 16 | from django.db.models import Q | 
| 17 | 17 | from django.db.models.functions import Lower | 
| 18 | 18 | from django.db.models.signals import pre_delete | 
|  | 
| 52 | 52 | from netbox_custom_objects.constants import APP_LABEL, RESERVED_FIELD_NAMES | 
| 53 | 53 | from netbox_custom_objects.field_types import FIELD_TYPE_CLASS | 
| 54 | 54 | 
 | 
|  | 55 | + | 
|  | 56 | +class UniquenessConstraintTestError(Exception): | 
|  | 57 | +    """Custom exception used to signal successful uniqueness constraint test.""" | 
|  | 58 | + | 
|  | 59 | +    pass | 
|  | 60 | + | 
|  | 61 | + | 
| 55 | 62 | USER_TABLE_DATABASE_NAME_PREFIX = "custom_objects_" | 
| 56 | 63 | 
 | 
| 57 | 64 | 
 | 
| @@ -872,6 +879,43 @@ def clean(self): | 
| 872 | 879 |                 {"unique": _("Uniqueness cannot be enforced for boolean fields")} | 
| 873 | 880 |             ) | 
| 874 | 881 | 
 | 
|  | 882 | +        # Check if uniqueness constraint can be applied when changing from non-unique to unique | 
|  | 883 | +        if ( | 
|  | 884 | +            self.pk | 
|  | 885 | +            and self.unique | 
|  | 886 | +            and not self.original.unique | 
|  | 887 | +            and not self._state.adding | 
|  | 888 | +        ): | 
|  | 889 | +            field_type = FIELD_TYPE_CLASS[self.type]() | 
|  | 890 | +            model_field = field_type.get_model_field(self) | 
|  | 891 | +            model = self.custom_object_type.get_model() | 
|  | 892 | +            model_field.contribute_to_class(model, self.name) | 
|  | 893 | + | 
|  | 894 | +            old_field = field_type.get_model_field(self.original) | 
|  | 895 | +            old_field.contribute_to_class(model, self._original_name) | 
|  | 896 | + | 
|  | 897 | +            try: | 
|  | 898 | +                with transaction.atomic(): | 
|  | 899 | +                    with connection.schema_editor() as test_schema_editor: | 
|  | 900 | +                        test_schema_editor.alter_field(model, old_field, model_field) | 
|  | 901 | +                        # If we get here, the constraint was applied successfully | 
|  | 902 | +                        # Now raise a custom exception to rollback the test transaction | 
|  | 903 | +                        raise UniquenessConstraintTestError() | 
|  | 904 | +            except UniquenessConstraintTestError: | 
|  | 905 | +                # The constraint can be applied, validation passes | 
|  | 906 | +                pass | 
|  | 907 | +            except IntegrityError: | 
|  | 908 | +                # The constraint cannot be applied due to existing non-unique values | 
|  | 909 | +                raise ValidationError( | 
|  | 910 | +                    { | 
|  | 911 | +                        "unique": _( | 
|  | 912 | +                            "Custom objects with non-unique values already exist so this action isn't permitted" | 
|  | 913 | +                        ) | 
|  | 914 | +                    } | 
|  | 915 | +                ) | 
|  | 916 | +            finally: | 
|  | 917 | +                self.custom_object_type.clear_model_cache(self.custom_object_type.id) | 
|  | 918 | + | 
| 875 | 919 |         # Choice set must be set on selection fields, and *only* on selection fields | 
| 876 | 920 |         if self.type in ( | 
| 877 | 921 |             CustomFieldTypeChoices.TYPE_SELECT, | 
|  | 
0 commit comments