[bug] Fix part duplicate checks (#12574)

* [bug] Fix part duplicate checks

* Additional unit tests
This commit is contained in:
Oliver
2026-08-08 19:11:46 +10:00
committed by GitHub
parent cf2f64c83a
commit 5c798f4237
3 changed files with 78 additions and 5 deletions
+14 -5
View File
@@ -1053,11 +1053,20 @@ class Part(
raise ValidationError(_('Duplicate part revision already exists.'))
# Ensure unique across (Name, revision, IPN) (as specified)
if (self.revision or self.IPN) and (
Part.objects
.exclude(pk=self.pk)
.filter(name=self.name, revision=self.revision, IPN=self.IPN)
.exists()
# Note: the 'unique_part' database constraint only rejects a row when
# *all three* fields are non-null (Postgres treats NULL as distinct from
# NULL), so mirror that here rather than skipping whenever either field
# is merely blank - an empty string ('') still collides with another
# empty string at the database level, unlike None/NULL.
if (
self.IPN is not None
and self.revision is not None
and (
Part.objects
.exclude(pk=self.pk)
.filter(name=self.name, revision=self.revision, IPN=self.IPN)
.exists()
)
):
raise ValidationError(
_('Part with this Name, IPN and Revision already exists.')
+30
View File
@@ -1638,6 +1638,36 @@ class PartCreationTests(PartAPITestBase):
self.assertFalse(response.data['active'])
self.assertFalse(response.data['purchaseable'])
def test_create_duplicate_no_ipn_revision(self):
"""Test that creating a duplicate part (same name, no IPN/revision) returns a 400.
Regression test for a bug where the duplicate-name check was skipped
whenever IPN and revision were both blank, letting the request fall
through to an unhandled database IntegrityError (500) instead of a
proper validation error (400).
"""
url = reverse('api-part-list')
data = {
'name': 'TEST1',
'category': 1,
'assembly': True,
'component': True,
'consumable': False,
'is_template': False,
'purchaseable': False,
'salable': False,
'testable': False,
'trackable': False,
'virtual': False,
}
self.post(url, data, expected_code=201)
# Attempting to create the exact same part again must be rejected cleanly
response = self.post(url, data, expected_code=400)
self.assertIn('non_field_errors', response.data)
def test_notes_on_create(self):
"""Test that notes can be set when creating a Part."""
list_url = reverse('api-part-list')
+34
View File
@@ -219,6 +219,40 @@ class PartTest(TestCase):
with self.assertRaises(ValidationError):
part_2.validate_unique()
def test_duplicate_no_ipn_revision(self):
"""Test that we cannot create a duplicate Part when IPN and revision are both blank strings.
Regression test: the underlying 'unique_part' database constraint treats
blank IPN/revision values (empty strings, as sent by the API when these
fields are omitted) as equal, so validate_unique() must catch this case
too, rather than raising an unhandled IntegrityError. Note that this is
distinct from *unset* (None/NULL) values, which the database constraint
does *not* treat as colliding (see test_revisions).
"""
cat = PartCategory.objects.get(pk=1)
Part.objects.create(
category=cat,
name='dupe_test',
description='description',
IPN='',
revision='',
)
part = Part(
category=cat,
name='dupe_test',
description='description',
IPN='',
revision='',
)
with self.assertRaises(ValidationError):
part.validate_unique()
with self.assertRaises(ValidationError):
part.save()
def test_attributes(self):
"""Test Part attributes."""
self.assertEqual(self.r1.name, 'R_2K2_0805')