|
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, models, IntegrityError |
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
|
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 |
|
@@ -310,7 +317,10 @@ def _fetch_and_generate_field_attrs(
|
310 | 317 | for field in fields:
|
311 | 318 | field_type = FIELD_TYPE_CLASS[field.type]()
|
312 | 319 | if skip_object_fields:
|
313 |
| - if field.type in [CustomFieldTypeChoices.TYPE_OBJECT, CustomFieldTypeChoices.TYPE_MULTIOBJECT]: |
| 320 | + if field.type in [ |
| 321 | + CustomFieldTypeChoices.TYPE_OBJECT, |
| 322 | + CustomFieldTypeChoices.TYPE_MULTIOBJECT, |
| 323 | + ]: |
314 | 324 | continue
|
315 | 325 |
|
316 | 326 | field_name = field.name
|
@@ -453,7 +463,9 @@ def get_model(
|
453 | 463 | "custom_object_type_id": self.id,
|
454 | 464 | }
|
455 | 465 |
|
456 |
| - field_attrs = self._fetch_and_generate_field_attrs(fields, skip_object_fields=skip_object_fields) |
| 466 | + field_attrs = self._fetch_and_generate_field_attrs( |
| 467 | + fields, skip_object_fields=skip_object_fields |
| 468 | + ) |
457 | 469 |
|
458 | 470 | attrs.update(**field_attrs)
|
459 | 471 |
|
@@ -587,7 +599,7 @@ class CustomObjectTypeField(CloningMixin, ExportTemplatesMixin, ChangeLoggedMode
|
587 | 599 | name = models.CharField(
|
588 | 600 | verbose_name=_("name"),
|
589 | 601 | max_length=50,
|
590 |
| - help_text=_("Internal field name, e.g. \"vendor_label\""), |
| 602 | + help_text=_('Internal field name, e.g. "vendor_label"'), |
591 | 603 | validators=(
|
592 | 604 | RegexValidator(
|
593 | 605 | regex=r"^[a-z0-9_]+$",
|
@@ -616,7 +628,9 @@ class CustomObjectTypeField(CloningMixin, ExportTemplatesMixin, ChangeLoggedMode
|
616 | 628 | verbose_name=_("group name"),
|
617 | 629 | max_length=50,
|
618 | 630 | blank=True,
|
619 |
| - help_text=_("Custom object fields within the same group will be displayed together"), |
| 631 | + help_text=_( |
| 632 | + "Custom object fields within the same group will be displayed together" |
| 633 | + ), |
620 | 634 | )
|
621 | 635 | description = models.CharField(
|
622 | 636 | verbose_name=_("description"), max_length=200, blank=True
|
@@ -862,6 +876,40 @@ def clean(self):
|
862 | 876 | {"unique": _("Uniqueness cannot be enforced for boolean fields")}
|
863 | 877 | )
|
864 | 878 |
|
| 879 | + # Check if uniqueness constraint can be applied when changing from non-unique to unique |
| 880 | + if ( |
| 881 | + self.pk |
| 882 | + and self.unique |
| 883 | + and not self.original.unique |
| 884 | + and not self._state.adding |
| 885 | + ): |
| 886 | + field_type = FIELD_TYPE_CLASS[self.type]() |
| 887 | + model_field = field_type.get_model_field(self) |
| 888 | + model = self.custom_object_type.get_model() |
| 889 | + model_field.contribute_to_class(model, self.name) |
| 890 | + |
| 891 | + old_field = field_type.get_model_field(self.original) |
| 892 | + old_field.contribute_to_class(model, self._original_name) |
| 893 | + |
| 894 | + try: |
| 895 | + with connection.schema_editor() as test_schema_editor: |
| 896 | + test_schema_editor.alter_field(model, old_field, model_field) |
| 897 | + # If we get here, the constraint was applied successfully |
| 898 | + # Now raise a custom exception to rollback the test transaction |
| 899 | + raise UniquenessConstraintTestError() |
| 900 | + except UniquenessConstraintTestError: |
| 901 | + # The constraint can be applied, validation passes |
| 902 | + pass |
| 903 | + except IntegrityError: |
| 904 | + # The constraint cannot be applied due to existing non-unique values |
| 905 | + raise ValidationError( |
| 906 | + { |
| 907 | + "unique": _( |
| 908 | + "Custom objects with non-unique values already exist so this action isn't permitted" |
| 909 | + ) |
| 910 | + } |
| 911 | + ) |
| 912 | + |
865 | 913 | # Choice set must be set on selection fields, and *only* on selection fields
|
866 | 914 | if self.type in (
|
867 | 915 | CustomFieldTypeChoices.TYPE_SELECT,
|
|
0 commit comments