Skip to content

Commit a4976ea

Browse files
committed
Merge remote-tracking branch 'cos/develop' into feature/ror-migration
2 parents a0a9800 + 613ff26 commit a4976ea

File tree

45 files changed

+709
-178
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+709
-178
lines changed

CHANGELOG

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@
22

33
We follow the CalVer (https://calver.org/) versioning scheme: YY.MINOR.MICRO.
44

5+
26.2.1 (2026-02-09)
6+
===================
7+
8+
- Permanently update two notification types
9+
- Link digest to types and fix/log incorrect usage
10+
- Update notifiction dedupe command
11+
12+
26.2.0
13+
======
14+
15+
- TODO: add date and log
16+
517
26.1.6 (2026-01-14)
618
===================
719

admin/nodes/views.py

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
from scripts.approve_registrations import approve_past_pendings
5454

5555
from website import settings, search
56+
from website.archiver.tasks import force_archive
5657

5758

5859
class NodeMixin(PermissionRequiredMixin):
@@ -705,6 +706,12 @@ class NodeReindexShare(NodeMixin, View):
705706
def post(self, request, *args, **kwargs):
706707
node = self.get_object()
707708
update_share(node)
709+
messages.success(
710+
request,
711+
'Reindex request has been sent to SHARE. '
712+
'Changes typically appear in OSF Search within about 5 minutes, '
713+
'subject to background queue load and SHARE availability.'
714+
)
708715
update_admin_log(
709716
user_id=self.request.user.id,
710717
object_id=node._id,
@@ -830,16 +837,20 @@ class CheckArchiveStatusRegistrationsView(NodeMixin, View):
830837

831838
def get(self, request, *args, **kwargs):
832839
# Prevents circular imports that cause admin app to hang at startup
833-
from osf.management.commands.force_archive import check
840+
from osf.management.commands.force_archive import check, DEFAULT_PERMISSIBLE_ADDONS
834841

835842
registration = self.get_object()
836843

837844
if registration.archived:
838845
messages.success(request, f"Registration {registration._id} is archived.")
839846
return redirect(self.get_success_url())
840847

848+
addons = set(DEFAULT_PERMISSIBLE_ADDONS)
849+
for reg in registration.node_and_primary_descendants():
850+
addons.update(reg.registered_from.get_addon_names())
851+
841852
try:
842-
archive_status = check(registration)
853+
archive_status = check(registration, permissible_addons=addons, verify_addons=False)
843854
messages.success(request, archive_status)
844855
except RegistrationStuckError as exc:
845856
messages.error(request, str(exc))
@@ -860,7 +871,7 @@ class ForceArchiveRegistrationsView(NodeMixin, View):
860871

861872
def post(self, request, *args, **kwargs):
862873
# Prevents circular imports that cause admin app to hang at startup
863-
from osf.management.commands.force_archive import verify, archive, DEFAULT_PERMISSIBLE_ADDONS
874+
from osf.management.commands.force_archive import verify, DEFAULT_PERMISSIBLE_ADDONS
864875

865876
registration = self.get_object()
866877
force_archive_params = request.POST
@@ -871,32 +882,33 @@ def post(self, request, *args, **kwargs):
871882

872883
allow_unconfigured = force_archive_params.get('allow_unconfigured', False)
873884

874-
addons = set(registration.registered_from.get_addon_names())
875-
addons.update(DEFAULT_PERMISSIBLE_ADDONS)
885+
addons = set(DEFAULT_PERMISSIBLE_ADDONS)
886+
for reg in registration.node_and_primary_descendants():
887+
addons.update(reg.registered_from.get_addon_names())
876888

877-
try:
878-
verify(registration, permissible_addons=addons, raise_error=True)
879-
except ValidationError as exc:
880-
messages.error(request, str(exc))
881-
return redirect(self.get_success_url())
889+
# No need to verify addons during force archive,
890+
# because we fetched all permissible addons above
891+
verify_addons = False
882892

883-
dry_mode = force_archive_params.get('dry_mode', False)
884-
885-
if dry_mode:
886-
messages.success(request, f"Registration {registration._id} can be archived.")
887-
else:
893+
if force_archive_params.get('dry_mode', False):
894+
# For dry mode, verify synchronously to provide immediate feedback
888895
try:
889-
archive(
890-
registration,
891-
permissible_addons=addons,
892-
allow_unconfigured=allow_unconfigured,
893-
skip_collisions=skip_collision,
894-
delete_collisions=delete_collision,
895-
)
896-
messages.success(request, 'Registration archive process has finished.')
897-
except Exception as exc:
898-
messages.error(request, f'This registration cannot be archived due to {exc.__class__.__name__}: {str(exc)}. '
899-
f'If the problem persists get a developer to fix it.')
896+
verify(registration, permissible_addons=addons, verify_addons=verify_addons, raise_error=True)
897+
messages.success(request, f"Registration {registration._id} can be archived.")
898+
except ValidationError as exc:
899+
messages.error(request, str(exc))
900+
return redirect(self.get_success_url())
901+
else:
902+
# For actual archiving, skip synchronous verification to avoid 502 timeouts
903+
# Verification will be performed asynchronously in the task
904+
force_archive_task = force_archive.delay(
905+
str(registration._id),
906+
permissible_addons=list(addons),
907+
allow_unconfigured=allow_unconfigured,
908+
skip_collisions=skip_collision,
909+
delete_collisions=delete_collision,
910+
)
911+
messages.success(request, f'Registration archive process has started. Task id: {force_archive_task.id}.')
900912

901913
return redirect(self.get_success_url())
902914

admin/preprints/views.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,12 @@ class PreprintReindexShare(PreprintMixin, View):
184184
def post(self, request, *args, **kwargs):
185185
preprint = self.get_object()
186186
update_share(preprint)
187+
messages.success(
188+
request,
189+
'Reindex request has been sent to SHARE. '
190+
'Changes typically appear in OSF Search within about 5 minutes, '
191+
'subject to background queue load and SHARE availability.'
192+
)
187193
update_admin_log(
188194
user_id=self.request.user.id,
189195
object_id=preprint._id,
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<a data-toggle="modal" data-target="#confirmReindexShareUser" class="btn btn-default">SHARE Reindex User Content</a>
2+
<div class="modal" id="confirmReindexShareUser">
3+
<div class="modal-dialog">
4+
<div class="modal-content">
5+
<form class="well" method="post" action="{% url 'users:reindex-share-user' guid=user.guid %}">
6+
<div class="modal-header">
7+
<button type="button" class="close" data-dismiss="modal">x</button>
8+
<h3>Are you sure you want to SHARE reindex all content for this user? {{ user.guid }}</h3>
9+
<p>This will trigger SHARE reindexing for all nodes and preprints where this user is a contributor.</p>
10+
</div>
11+
{% csrf_token %}
12+
<div class="modal-footer">
13+
<input class="btn btn-danger" type="submit" value="Confirm" />
14+
<button type="button" class="btn btn-default" data-dismiss="modal">
15+
Cancel
16+
</button>
17+
</div>
18+
</form>
19+
</div>
20+
</div>
21+
</div>

admin/templates/users/user.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
{% include "users/disable_user.html" with user=user %}
3838
{% include "users/mark_spam.html" with user=user %}
3939
{% include "users/reindex_user_elastic.html" with user=user %}
40+
{% include "users/reindex_user_share.html" with user=user %}
4041
</div>
4142
</div>
4243
</div>

admin/users/urls.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
re_path(r'^(?P<guid>[a-z0-9]+)/get_reset_password/$', views.GetPasswordResetLink.as_view(), name='get-reset-password'),
2727
re_path(r'^(?P<guid>[a-z0-9]+)/reindex_elastic_user/$', views.UserReindexElastic.as_view(),
2828
name='reindex-elastic-user'),
29+
re_path(r'^(?P<guid>[a-z0-9]+)/reindex_share_user/$', views.UserShareReindex.as_view(),
30+
name='reindex-share-user'),
2931
re_path(r'^(?P<guid>[a-z0-9]+)/merge_accounts/$', views.UserMergeAccounts.as_view(), name='merge-accounts'),
3032
re_path(r'^(?P<guid>[a-z0-9]+)/draft_registrations/$', views.UserDraftRegistrationsList.as_view(), name='draft-registrations'),
3133
]

admin/users/views.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
CONFIRM_HAM,
3939
UNFLAG_SPAM,
4040
REINDEX_ELASTIC,
41+
REINDEX_SHARE,
4142
)
4243

4344
from admin.users.forms import (
@@ -560,6 +561,44 @@ def post(self, request, *args, **kwargs):
560561
return redirect(self.get_success_url())
561562

562563

564+
class UserShareReindex(UserMixin, View):
565+
permission_required = 'osf.change_osfuser'
566+
567+
def post(self, request, *args, **kwargs):
568+
from api.share.utils import update_share
569+
user = self.get_object()
570+
571+
nodes_count = user.contributed.count()
572+
preprints_count = user.preprints.filter(deleted=None).count()
573+
574+
for node in user.contributed:
575+
try:
576+
update_share(node)
577+
except Exception as e:
578+
messages.error(request, f'Failed to SHARE reindex node {node._id}: {e}')
579+
580+
for preprint in user.preprints.filter(deleted=None):
581+
try:
582+
update_share(preprint)
583+
except Exception as e:
584+
messages.error(request, f'Failed to SHARE reindex preprint {preprint._id}: {e}')
585+
586+
messages.success(
587+
request,
588+
f'Triggered SHARE reindexing for {nodes_count} nodes and {preprints_count} preprints'
589+
)
590+
591+
update_admin_log(
592+
user_id=self.request.user.id,
593+
object_id=user._id,
594+
object_repr='User',
595+
message=f'SHARE reindexed all content for user {user._id}',
596+
action_flag=REINDEX_SHARE
597+
)
598+
599+
return redirect(self.get_success_url())
600+
601+
563602
class UserDraftRegistrationsList(UserMixin, ListView):
564603
template_name = 'users/draft-registrations.html'
565604
permission_required = 'osf.view_draftregistration'

admin_tests/nodes/test_views.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,7 @@ class TestNodeReindex(AdminTestCase):
303303
def setUp(self):
304304
super().setUp()
305305
self.request = RequestFactory().post('/fake_path')
306+
patch_messages(self.request)
306307

307308
self.user = AuthUserFactory()
308309
self.node = ProjectFactory(creator=self.user)

admin_tests/preprints/test_views.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,7 @@ def test_reindex_preprint_share(self, preprint, req, mock_update_share):
364364
preprint.provider.save()
365365

366366
count = AdminLogEntry.objects.count()
367+
patch_messages(req)
367368
view = views.PreprintReindexShare()
368369
view = setup_log_view(view, req, guid=preprint._id)
369370
mock_update_share.reset_mock()

api/actions/permissions.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,20 @@ def has_object_permission(self, request, view, obj):
4646
else:
4747
# Moderators and node admins can trigger state changes.
4848
is_node_admin = target is not None and target.has_permission(auth.user, osf_permissions.ADMIN)
49-
if not (is_node_admin or auth.user.has_perm('view_submissions', provider)):
50-
return False
49+
is_write_contributor = target is not None and target.has_permission(auth.user, osf_permissions.WRITE)
5150

52-
# User can trigger state changes on this reviewable, but can they use this trigger in particular?
51+
# Validate serializer once and extract trigger
5352
serializer = view.get_serializer(data=request.data)
5453
serializer.is_valid(raise_exception=True)
5554
trigger = serializer.validated_data.get('trigger')
55+
56+
provisional_write_allowed = is_write_contributor and trigger == ReviewTriggers.SUBMIT.value
57+
58+
if not (is_node_admin or auth.user.has_perm('view_submissions', provider) or provisional_write_allowed):
59+
return False
60+
61+
# User can trigger state changes on this reviewable, but can they use this trigger in particular?
5662
permission = TRIGGER_PERMISSIONS[trigger]
63+
if permission is None and is_write_contributor and trigger == ReviewTriggers.SUBMIT.value:
64+
return True
5765
return permission is None or request.user.has_perm(permission, target.provider)

0 commit comments

Comments
 (0)