diff --git a/admin_tests/preprints/test_views.py b/admin_tests/preprints/test_views.py index d70eff1ef35..ef5624264dd 100644 --- a/admin_tests/preprints/test_views.py +++ b/admin_tests/preprints/test_views.py @@ -1062,3 +1062,47 @@ def test_copies_primary_file_from_source_guid(self): recovered = Preprint.load('abcde') assert recovered.primary_file is not None assert recovered.primary_file.copied_from_id == source_file.id + + def _orphan_guid(self, provider): + # Leave a base Guid with a dangling referent (the deleted-preprint state we recover from). + # Detach the GenericRelation first so deleting the preprint doesn't cascade the Guid away. + from osf.models import Guid + pp = PreprintFactory(provider=provider) + guid_str = pp.get_guid()._id + Guid.objects.filter(_id=guid_str).update(object_id=None, content_type=None) + pp.versioned_guids.all().delete() + Preprint.objects.filter(id=pp.id).delete() + return guid_str + + def test_recover_reuses_orphaned_guid(self): + from osf.models import Guid + guid_str = self._orphan_guid(self.provider) + assert Guid.objects.filter(_id=guid_str).exists() + assert Guid.objects.get(_id=guid_str).referent is None + assert Preprint.load(guid_str) is None + + response = self._post(self._base_data(guid=guid_str)) + assert response.status_code == 302 + + recovered = Preprint.load(guid_str) + assert recovered is not None + assert recovered._id == f'{guid_str}_v1' + assert recovered.title == 'Recovered Title' + assert Guid.objects.filter(_id=guid_str).count() == 1 + assert Guid.objects.get(_id=guid_str).referent == recovered + assert AdminLogEntry.objects.filter(action_flag=PREPRINT_RECOVERED).exists() + + def test_create_rejects_guid_pointing_at_live_preprint(self): + from django.core.exceptions import ValidationError + live = PreprintFactory(provider=self.provider) + guid_str = live.get_guid()._id + with pytest.raises(ValidationError, match='GUID cannot be manually assigned'): + Preprint.create( + provider=self.provider, + title='x', + creator=self.user, + description='y', + manual_guid=guid_str, + ) + # The live preprint's guid still points at it, untouched. + assert Preprint.load(guid_str).id == live.id diff --git a/osf/models/preprint.py b/osf/models/preprint.py index fe1e1ae8e59..7ec57d6ea33 100644 --- a/osf/models/preprint.py +++ b/osf/models/preprint.py @@ -356,9 +356,17 @@ def create(cls, provider, title, creator, description, manual_guid=None, manual_ preprint.save(guid_ready=False) # Step 2: Create the base guid obj if manual_guid: - if not check_manually_assigned_guid(manual_guid): + existing_guid = Guid.objects.filter(_id=manual_guid).first() + if existing_guid is not None: + # Reuse an orphaned guid so deleted preprints can be recovered at their + # original id; a guid still pointing at a live object is a real collision. + if existing_guid.referent is not None: + raise ValidationError(f'GUID cannot be manually assigned: guid_str={manual_guid}.') + base_guid_obj = existing_guid + elif check_manually_assigned_guid(manual_guid): + base_guid_obj = Guid.objects.create(_id=manual_guid) + else: raise ValidationError(f'GUID cannot be manually assigned: guid_str={manual_guid}.') - base_guid_obj = Guid.objects.create(_id=manual_guid) else: base_guid_obj = Guid.objects.create() base_guid_obj.referent = preprint