Skip to content

Commit 77b8622

Browse files
authored
[ENG-8832] Most recent numbered version of a preprint fails to show for unversioned URL (#11315)
* fix versioned guids resolving to the last published version instead of last version number * fix migration job * handle initial review_state in fix_versioned_guids job * adjusted tests to pass with new guid resolve strategy * handle the case for withdrawn/rejected preprints * fix missing.all()
1 parent 8b09bd4 commit 77b8622

File tree

4 files changed

+119
-40
lines changed

4 files changed

+119
-40
lines changed

api_tests/preprints/views/test_preprint_versions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -807,7 +807,7 @@ def test_list_versions_pre_mod(self):
807807

808808
# Pre moderation V4 (Pending)
809809
pre_mod_preprint_v4 = PreprintFactory.create_version(
810-
create_from=pre_mod_preprint_v2,
810+
create_from=pre_mod_preprint_v3,
811811
creator=self.creator,
812812
final_machine_state='initial',
813813
is_published=False,
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import logging
2+
3+
from django.contrib.contenttypes.models import ContentType
4+
from django.core.management.base import BaseCommand
5+
from django.db import transaction
6+
from django.db.models import Prefetch
7+
8+
from osf.models import GuidVersionsThrough, Guid, Preprint
9+
from osf.utils.workflows import ReviewStates
10+
11+
logger = logging.getLogger(__name__)
12+
13+
14+
class Command(BaseCommand):
15+
help = 'Fix'
16+
17+
def add_arguments(self, parser):
18+
parser.add_argument(
19+
'--dry-run',
20+
action='store_true',
21+
dest='dry_run',
22+
help='Run the command without saving changes',
23+
)
24+
25+
@transaction.atomic
26+
def handle(self, *args, **options):
27+
dry_run = options.get('dry_run', False)
28+
fix_versioned_guids(dry_run=dry_run)
29+
if dry_run:
30+
transaction.set_rollback(True)
31+
32+
33+
def fix_versioned_guids(dry_run: bool):
34+
content_type = ContentType.objects.get_for_model(Preprint)
35+
versions_queryset = GuidVersionsThrough.objects.order_by('-version')
36+
processed_count = 0
37+
updated_count = 0
38+
skipped_count = 0
39+
errors_count = 0
40+
for guid in (
41+
Guid.objects.filter(content_type=content_type)
42+
.prefetch_related(Prefetch('versions', queryset=versions_queryset))
43+
.iterator(chunk_size=500)
44+
):
45+
processed_count += 1
46+
if not guid.versions:
47+
skipped_count += 1
48+
continue
49+
for version in guid.versions.all():
50+
last_version_object_id = version.object_id
51+
if guid.object_id == last_version_object_id:
52+
skipped_count += 1
53+
break
54+
if version.referent.machine_state not in (ReviewStates.INITIAL.value, ReviewStates.REJECTED.value, ReviewStates.WITHDRAWN.value):
55+
continue
56+
try:
57+
guid.object_id = last_version_object_id
58+
guid.referent = version.referent
59+
if not dry_run:
60+
guid.save()
61+
updated_count += 1
62+
except Exception as e:
63+
logger.error(f"Error occurred during patching {guid._id=}", exc_info=e)
64+
errors_count += 1
65+
66+
if dry_run:
67+
logger.error(
68+
f"Processed: {processed_count}, Would update: {updated_count}, Skipped: {skipped_count}, Errors: {errors_count}"
69+
)
70+
else:
71+
logger.error(
72+
f"Processed: {processed_count}, Updated: {updated_count}, Skipped: {skipped_count}, Errors: {errors_count}"
73+
)

osf/models/preprint.py

Lines changed: 39 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@
5959
)
6060
from django.contrib.postgres.fields import ArrayField
6161
from api.share.utils import update_share
62-
from api.providers.workflows import Workflows
6362

6463
logger = logging.getLogger(__name__)
6564

@@ -1679,27 +1678,13 @@ def run_submit(self, user):
16791678
user: The user triggering this transition.
16801679
"""
16811680
ret = super().run_submit(user=user)
1682-
provider = self.provider
1683-
reviews_workflow = provider.reviews_workflow
1684-
# Only post moderation is relevant for Preprint, and hybrid moderation is included for integrity purpose.
1685-
need_guid_update = any(
1686-
[
1687-
reviews_workflow == Workflows.POST_MODERATION.value,
1688-
reviews_workflow == Workflows.HYBRID_MODERATION.value and
1689-
any([
1690-
provider.get_group('moderator') in user.groups.all(),
1691-
provider.get_group('admin') in user.groups.all()
1692-
])
1693-
]
1694-
)
1695-
# Only update the base guid obj to refer to the new version 1) if the provider is post-moderation, or 2) if the
1696-
# provider is hybrid-moderation and if the user who submits the preprint is a moderator or admin.
1697-
if need_guid_update:
1698-
base_guid_obj = self.versioned_guids.first().guid
1699-
base_guid_obj.referent = self
1700-
base_guid_obj.object_id = self.pk
1701-
base_guid_obj.content_type = ContentType.objects.get_for_model(self)
1702-
base_guid_obj.save()
1681+
1682+
base_guid_obj = self.versioned_guids.first().guid
1683+
base_guid_obj.referent = self
1684+
base_guid_obj.object_id = self.pk
1685+
base_guid_obj.content_type = ContentType.objects.get_for_model(self)
1686+
base_guid_obj.save()
1687+
17031688
return ret
17041689

17051690
def run_accept(self, user, comment, **kwargs):
@@ -1711,14 +1696,6 @@ def run_accept(self, user, comment, **kwargs):
17111696
comment: Text describing why.
17121697
"""
17131698
ret = super().run_accept(user=user, comment=comment, **kwargs)
1714-
reviews_workflow = self.provider.reviews_workflow
1715-
if reviews_workflow == Workflows.PRE_MODERATION.value or reviews_workflow == Workflows.HYBRID_MODERATION.value:
1716-
base_guid_obj = self.versioned_guids.first().guid
1717-
base_guid_obj.referent = self
1718-
base_guid_obj.object_id = self.pk
1719-
base_guid_obj.content_type = ContentType.objects.get_for_model(self)
1720-
base_guid_obj.save()
1721-
17221699
versioned_guid = self.versioned_guids.first()
17231700
if versioned_guid.is_rejected:
17241701
versioned_guid.is_rejected = False
@@ -1734,9 +1711,38 @@ def run_reject(self, user, comment):
17341711
comment: Text describing why.
17351712
"""
17361713
ret = super().run_reject(user=user, comment=comment)
1737-
versioned_guid = self.versioned_guids.first()
1738-
versioned_guid.is_rejected = True
1739-
versioned_guid.save()
1714+
current_version_guid = self.versioned_guids.first()
1715+
current_version_guid.is_rejected = True
1716+
current_version_guid.save()
1717+
1718+
self.rollback_main_guid()
1719+
1720+
return ret
1721+
1722+
def rollback_main_guid(self):
1723+
"""Reset main guid to resolve to last versioned guid which is not withdrawn/rejected if there is one.
1724+
"""
1725+
guid = None
1726+
for version in self.versioned_guids.all()[1:]: # skip first guid as it refers to current version
1727+
guid = version.guid
1728+
if guid.referent.machine_state not in (ReviewStates.REJECTED, ReviewStates.WITHDRAWN):
1729+
break
1730+
if guid:
1731+
guid.referent = self
1732+
guid.object_id = self.pk
1733+
guid.content_type = ContentType.objects.get_for_model(self)
1734+
guid.save()
1735+
1736+
def run_withdraw(self, user, comment):
1737+
"""Override `ReviewableMixin`/`MachineableMixin`.
1738+
Run the 'withdraw' state transition and create a corresponding Action.
1739+
1740+
Params:
1741+
user: The user triggering this transition.
1742+
comment: Text describing why.
1743+
"""
1744+
ret = super().run_withdraw(user=user, comment=comment)
1745+
self.rollback_main_guid()
17401746
return ret
17411747

17421748
@receiver(post_save, sender=Preprint)

tests/test_preprints.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2518,8 +2518,8 @@ def test_unpublished_version_pre_mod_submit_and_accept(self, preprint_pre_mod, c
25182518
assert new_version.is_published is False
25192519
assert new_version.machine_state == ReviewStates.PENDING.value
25202520
guid_obj = new_version.get_guid()
2521-
assert guid_obj.object_id == preprint_pre_mod.pk
2522-
assert guid_obj.referent == preprint_pre_mod
2521+
assert guid_obj.object_id == new_version.pk
2522+
assert guid_obj.referent == new_version
25232523
assert guid_obj.content_type == ContentType.objects.get_for_model(Preprint)
25242524

25252525
new_version.run_accept(moderator, 'comment')
@@ -2552,17 +2552,17 @@ def test_new_version_pre_mod_submit_and_reject(self, preprint_pre_mod, creator,
25522552
assert new_version.is_published is False
25532553
assert new_version.machine_state == ReviewStates.PENDING.value
25542554
guid_obj = new_version.get_guid()
2555-
assert guid_obj.object_id == preprint_pre_mod.pk
2556-
assert guid_obj.referent == preprint_pre_mod
2555+
assert guid_obj.object_id == new_version.pk
2556+
assert guid_obj.referent == new_version
25572557
assert guid_obj.content_type == ContentType.objects.get_for_model(Preprint)
25582558

25592559
new_version.run_reject(moderator, 'comment')
25602560
assert new_version.is_published is False
25612561
assert new_version.machine_state == ReviewStates.REJECTED.value
25622562
assert new_version.versioned_guids.first().is_rejected is True
25632563
guid_obj = new_version.get_guid()
2564-
assert guid_obj.object_id == preprint_pre_mod.pk
2565-
assert guid_obj.referent == preprint_pre_mod
2564+
assert guid_obj.object_id == new_version.pk
2565+
assert guid_obj.referent == new_version
25662566

25672567
def test_unpublished_preprint_post_mod_submit_and_accept(self, unpublished_preprint_post_mod, creator, moderator):
25682568
assert unpublished_preprint_post_mod.is_published is False

0 commit comments

Comments
 (0)