From fdef49c25e38538ac138437b6d8237185ceba68a Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 13 Jun 2026 11:08:13 +0000 Subject: [PATCH] Make save method atomic --- .../InvenTree/common/migrations/0044_note.py | 9 +++++++ src/backend/InvenTree/common/models.py | 25 +++++++++++++------ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/backend/InvenTree/common/migrations/0044_note.py b/src/backend/InvenTree/common/migrations/0044_note.py index 31045b8795..7213ac9252 100644 --- a/src/backend/InvenTree/common/migrations/0044_note.py +++ b/src/backend/InvenTree/common/migrations/0044_note.py @@ -121,4 +121,13 @@ class Migration(migrations.Migration): related_name='images', ), ), + # Add constraint to ensure that only one 'primary' note exists per model instance + migrations.AddConstraint( + model_name="note", + constraint=models.UniqueConstraint( + condition=models.Q(("primary", True)), + fields=("model_type", "model_id"), + name="unique_primary_note_per_model", + ), + ), ] diff --git a/src/backend/InvenTree/common/models.py b/src/backend/InvenTree/common/models.py index 98b082b1df..ef10fcca71 100644 --- a/src/backend/InvenTree/common/models.py +++ b/src/backend/InvenTree/common/models.py @@ -3000,21 +3000,34 @@ class Note( verbose_name = _('Note') verbose_name_plural = _('Notes') + constraints = [ + models.UniqueConstraint( + fields=['model_type', 'model_id'], + condition=models.Q(primary=True), + name='unique_primary_note_per_model', + ) + ] + @staticmethod def get_api_url() -> str: """Return the API URL associated with the Parameter model.""" return reverse('api-note-list') + @transaction.atomic def save(self, *args, **kwargs): """Perform custom save checks before saving a Note instance.""" self.check_save() - others = Note.objects.filter( - model_type=self.model_type, model_id=self.model_id - ).exclude(pk=self.pk) + # Lock sibling notes to serialize concurrent primary-flag updates + siblings = ( + Note.objects + .select_for_update() + .filter(model_type=self.model_type, model_id=self.model_id) + .exclude(pk=self.pk) + ) # If this is the *only* note for this model instance, then set it as the primary note - if not others.exists(): + if not siblings.exists(): self.primary = True self.clean() @@ -3023,9 +3036,7 @@ class Note( # Once this note is saved, mark other notes as non-primary if self.primary: - Note.objects.filter( - model_type=self.model_type, model_id=self.model_id - ).exclude(pk=self.pk).update(primary=False) + siblings.update(primary=False) self.cleanup_images()