|
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