Skip to content

Commit 0bb3784

Browse files
Merge remote-tracking branch 'upstream/feature/pbs-25-25' into feature/institution_affiliation_admin
2 parents 64e9f9b + 43b36a5 commit 0bb3784

30 files changed

+399
-121
lines changed

CHANGELOG

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

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

5+
25.18.2 (2025-12-19)
6+
====================
7+
8+
- Remove users from sitemap
9+
- Fix error when making registrations public
10+
- Fix PR templates
11+
12+
25.18.1 (2025-12-10)
13+
====================
14+
15+
- Add missing migrations
16+
17+
25.18.0
18+
=======
19+
20+
- TODO: add date and notes
21+
22+
25.17.7 ~ 25.17.10
23+
==================
24+
25+
- TODO: add date and notes
26+
527
25.17.6 (2025-10-11)
628
====================
729

PULL_REQUEST_TEMPLATE.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@
1515

1616
## QA Notes
1717

18-
Please make verification statements inspired by your code and what your code touches.
19-
- Verify
20-
- Verify
2118

22-
What are the areas of risk?
23-
24-
Any concerns/considerations/questions that development raised?
19+
<!-- Please make verification statements inspired by your code and what your code touches.
20+
- Verify
21+
- Verify
22+
What are the areas of risk?
23+
Any concerns/considerations/questions that development raised?
24+
-->
2525

2626
## Documentation
2727

admin/nodes/views.py

Lines changed: 15 additions & 15 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):
@@ -832,16 +833,19 @@ class CheckArchiveStatusRegistrationsView(NodeMixin, View):
832833

833834
def get(self, request, *args, **kwargs):
834835
# Prevents circular imports that cause admin app to hang at startup
835-
from osf.management.commands.force_archive import check
836+
from osf.management.commands.force_archive import check, DEFAULT_PERMISSIBLE_ADDONS
836837

837838
registration = self.get_object()
838839

839840
if registration.archived:
840841
messages.success(request, f"Registration {registration._id} is archived.")
841842
return redirect(self.get_success_url())
842843

844+
addons = set(registration.registered_from.get_addon_names())
845+
addons.update(DEFAULT_PERMISSIBLE_ADDONS)
846+
843847
try:
844-
archive_status = check(registration)
848+
archive_status = check(registration, permissible_addons=addons)
845849
messages.success(request, archive_status)
846850
except RegistrationStuckError as exc:
847851
messages.error(request, str(exc))
@@ -862,7 +866,7 @@ class ForceArchiveRegistrationsView(NodeMixin, View):
862866

863867
def post(self, request, *args, **kwargs):
864868
# Prevents circular imports that cause admin app to hang at startup
865-
from osf.management.commands.force_archive import verify, archive, DEFAULT_PERMISSIBLE_ADDONS
869+
from osf.management.commands.force_archive import verify, DEFAULT_PERMISSIBLE_ADDONS
866870

867871
registration = self.get_object()
868872
force_archive_params = request.POST
@@ -887,18 +891,14 @@ def post(self, request, *args, **kwargs):
887891
if dry_mode:
888892
messages.success(request, f"Registration {registration._id} can be archived.")
889893
else:
890-
try:
891-
archive(
892-
registration,
893-
permissible_addons=addons,
894-
allow_unconfigured=allow_unconfigured,
895-
skip_collisions=skip_collision,
896-
delete_collisions=delete_collision,
897-
)
898-
messages.success(request, 'Registration archive process has finished.')
899-
except Exception as exc:
900-
messages.error(request, f'This registration cannot be archived due to {exc.__class__.__name__}: {str(exc)}. '
901-
f'If the problem persists get a developer to fix it.')
894+
force_archive_task = force_archive.delay(
895+
str(registration._id),
896+
permissible_addons=list(addons),
897+
allow_unconfigured=allow_unconfigured,
898+
skip_collisions=skip_collision,
899+
delete_collisions=delete_collision,
900+
)
901+
messages.success(request, f'Registration archive process has started. Task id: {force_archive_task.id}.')
902902

903903
return redirect(self.get_success_url())
904904

admin/templates/institutions/detail.html

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@
2424
<a class="btn btn-danger" href={% url 'institutions:delete' institution.id %}>Delete institution</a>
2525
{% endif %}
2626
{% if perms.osf.change_institution %}
27-
{% if institution.deactivated is None %}
28-
<a class="btn btn-danger" href={% url 'institutions:deactivate' institution.id %}>Deactivate institution</a>
29-
{% else %}
30-
<a class="btn btn-danger" href={% url 'institutions:reactivate' institution.id %}>Reactivate institution</a>
31-
{% endif %}
27+
{% if institution.deactivated is None %}
28+
<a class="btn btn-danger" href={% url 'institutions:deactivate' institution.id %}>Deactivate institution</a>
29+
{% else %}
30+
<a class="btn btn-danger" href={% url 'institutions:reactivate' institution.id %}>Reactivate institution</a>
31+
{% endif %}
3232
<a class="btn btn-primary" href={% url 'institutions:affiliations' institution.id %}>Affiliations</a>
3333
{% endif %}
3434
{% if perms.osf.change_institution %}

api/guids/views.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from furl import furl
22
from django import http
3+
from osf.models.base import VersionedGuidMixin
34
from rest_framework.exceptions import NotFound
45
from rest_framework import permissions as drf_permissions
56
from rest_framework import generics
@@ -70,11 +71,17 @@ def get(self, request, **kwargs):
7071
raise NotFound
7172

7273
def get_redirect_url(self, **kwargs):
73-
guid = Guid.load(kwargs['guids'])
74-
if guid:
75-
referent = guid.referent
74+
guid_str, version_number = Guid.split_guid(kwargs['guids'])
75+
guid = Guid.load(guid_str)
76+
if not guid:
77+
return None
78+
referent = guid.referent
79+
if version_number and isinstance(referent, VersionedGuidMixin):
80+
# if the guid string contains a version number and referent is versionable
81+
return referent.get_versioned_absolute_api_v2_url(version_number)
82+
else:
83+
# if the guid string doesn't have a version number or the referent is not versionable
7684
if getattr(referent, 'absolute_api_v2_url', None):
7785
return referent.absolute_api_v2_url
7886
else:
7987
raise EndpointNotImplementedError()
80-
return None

api/nodes/views.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -545,13 +545,17 @@ def get_serializer_context(self):
545545
return context
546546

547547
def perform_destroy(self, instance):
548-
node = self.get_resource()
548+
node: Node = self.get_resource()
549549
auth = get_user_auth(self.request)
550550
if node.visible_contributors.count() == 1 and instance.visible:
551551
raise ValidationError('Must have at least one visible contributor')
552552
removed = node.remove_contributor(instance, auth)
553553
if not removed:
554554
raise ValidationError('Must have at least one registered admin contributor')
555+
propagate = self.request.query_params.get('propagate_to_children') == 'true'
556+
if propagate:
557+
for child_node in node.get_nodes(_contributors__in=[instance.user]):
558+
child_node.remove_contributor(instance, auth)
555559

556560

557561
class NodeImplicitContributorsList(JSONAPIBaseView, generics.ListAPIView, ListFilterMixin, NodeMixin):

api_tests/guids/views/test_guid_detail.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from api.base.settings.defaults import API_BASE
55
from osf_tests.factories import (
66
AuthUserFactory,
7+
PreprintFactory,
78
ProjectFactory,
89
RegistrationFactory,
910
CommentFactory,
@@ -28,6 +29,18 @@ def project(self):
2829
def registration(self):
2930
return RegistrationFactory()
3031

32+
@pytest.fixture()
33+
def versioned_preprint(self, user):
34+
preprint = PreprintFactory(reviews_workflow='pre-moderation')
35+
PreprintFactory.create_version(
36+
create_from=preprint,
37+
creator=user,
38+
final_machine_state='accepted',
39+
is_published=True,
40+
set_doi=False
41+
)
42+
return preprint
43+
3144
def test_redirects(self, app, project, registration, user):
3245
# test_redirect_to_node_view
3346
url = f'/{API_BASE}guids/{project._id}/'
@@ -125,6 +138,38 @@ def test_redirects_through_view_only_link(self, app, project, user):
125138
assert res.status_code == 302
126139
assert res.location == redirect_url
127140

141+
def test_redirects_with_version_for_versionable_objects(self, app, versioned_preprint, user):
142+
# if you go to the guids endpoint with just the guid without version number
143+
url = f'/{API_BASE}guids/{versioned_preprint.versioned_guids.first().guid._id}/'
144+
res = app.get(url, auth=user.auth)
145+
redirect_url = f'{API_DOMAIN}{API_BASE}preprints/{versioned_preprint.versioned_guids.first().guid._id}_v2/'
146+
assert res.status_code == 302
147+
assert res.location == redirect_url
148+
149+
# if you go to the guids endpoint with just the guid with a version number
150+
url = f'/{API_BASE}guids/{versioned_preprint.versioned_guids.first().guid._id}_v2/'
151+
res = app.get(url, auth=user.auth)
152+
redirect_url = f'{API_DOMAIN}{API_BASE}preprints/{versioned_preprint.versioned_guids.first().guid._id}_v2/'
153+
assert res.status_code == 302
154+
assert res.location == redirect_url
155+
156+
url = f'/{API_BASE}guids/{versioned_preprint.versioned_guids.first().guid._id}_v1/'
157+
res = app.get(url, auth=user.auth)
158+
redirect_url = f'{API_DOMAIN}{API_BASE}preprints/{versioned_preprint.versioned_guids.first().guid._id}_v1/'
159+
assert res.status_code == 302
160+
assert res.location == redirect_url
161+
162+
url = f'/{API_BASE}guids/{versioned_preprint.versioned_guids.first().guid._id}_v3/'
163+
res = app.get(url, auth=user.auth, expect_errors=True)
164+
assert res.status_code == 404
165+
166+
def test_redirects_with_version_for_unversionable_objects(self, app, project, user):
167+
url = f'/{API_BASE}guids/{project._id}_v12/'
168+
res = app.get(url, auth=user.auth, expect_errors=True)
169+
redirect_url = f'{API_DOMAIN}{API_BASE}nodes/{project._id}/'
170+
assert res.status_code == 302
171+
assert res.location == redirect_url
172+
128173
def test_resolves(self, app, project, user):
129174
# test_resolve_query_param
130175
url = '{}{}guids/{}/?resolve=false'.format(

osf/management/commands/force_archive.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,13 @@
3636
from addons.osfstorage.models import OsfStorageFile, OsfStorageFolder, OsfStorageFileNode
3737
from framework import sentry
3838
from framework.exceptions import HTTPError
39+
from osf import features
3940
from osf.models import AbstractNode, Node, NodeLog, Registration, BaseFileNode
4041
from osf.models.files import TrashedFileNode
42+
from osf.utils.requests import get_current_request
4143
from osf.exceptions import RegistrationStuckRecoverableException, RegistrationStuckBrokenException
4244
from api.base.utils import waterbutler_api_url_for
45+
from api.waffle.utils import flag_is_active
4346
from scripts import utils as script_utils
4447
from website.archiver import ARCHIVER_SUCCESS
4548
from website.settings import ARCHIVE_TIMEOUT_TIMEDELTA, ARCHIVE_PROVIDER, COOKIE_NAME
@@ -149,9 +152,11 @@ def complete_archive_target(reg, addon_short_name):
149152

150153
def perform_wb_copy(reg, node_settings, delete_collisions=False, skip_collisions=False):
151154
src, dst, user = reg.archive_job.info()
152-
if dst.files.filter(name=node_settings.archive_folder_name.replace('/', '-')).exists():
155+
dst_storage = dst.get_addon('osfstorage')
156+
archive_name = node_settings.archive_folder_name.replace('/', '-')
157+
if dst_storage.get_root().children.filter(name=archive_name).exists():
153158
if not delete_collisions and not skip_collisions:
154-
raise Exception('Archive folder for {} already exists. Investigate manually and rerun with either --delete-collisions or --skip-collisions')
159+
raise Exception(f'Archive folder for {archive_name} already exists. Investigate manually and rerun with either --delete-collisions or --skip-collisions')
155160
if delete_collisions:
156161
archive_folder = dst.files.exclude(type='osf.trashedfolder').get(name=node_settings.archive_folder_name.replace('/', '-'))
157162
logger.info(f'Removing {archive_folder}')
@@ -393,12 +398,23 @@ def archive(registration, *args, permissible_addons=DEFAULT_PERMISSIBLE_ADDONS,
393398
logger.info(f'Preparing to archive {reg._id}')
394399
for short_name in permissible_addons:
395400
node_settings = reg.registered_from.get_addon(short_name)
401+
if not node_settings and short_name != 'osfstorage' and flag_is_active(get_current_request(), features.ENABLE_GV):
402+
# get_addon() returns None for addons when archive is running inside of
403+
# the celery task. In this case, try to get addon settings from the GV
404+
try:
405+
from website.archiver.tasks import get_addon_from_gv
406+
node_settings = get_addon_from_gv(reg.registered_from, short_name, reg.registered_from.creator)
407+
except Exception as e:
408+
logger.warning(f'Could not load {short_name} from GV: {e}')
409+
396410
if not hasattr(node_settings, '_get_file_tree'):
397411
# Excludes invalid or None-type
412+
logger.warning(f"Skipping {short_name} for {registration._id}.")
398413
continue
399414
if not node_settings.configured:
400415
if not allow_unconfigured:
401416
raise Exception(f'{reg._id}: {short_name} on {reg.registered_from._id} is not configured. If this is permissible, re-run with `--allow-unconfigured`.')
417+
logger.warning(f"{short_name} is not configured for {registration._id}.")
402418
continue
403419
if not reg.archive_job.get_target(short_name) or reg.archive_job.get_target(short_name).status == ARCHIVER_SUCCESS:
404420
continue
@@ -486,7 +502,7 @@ def verify_registrations(registration_ids, permissible_addons):
486502
else:
487503
SKIPPED.append(reg)
488504

489-
def check(reg):
505+
def check(reg, *args, **kwargs):
490506
"""Check registration status. Raise exception if registration stuck."""
491507
logger.info(f'Checking {reg._id}')
492508
if reg.is_deleted:
@@ -503,7 +519,7 @@ def check(reg):
503519
still_archiving = not archive_tree_finished
504520
if still_archiving and root_job.datetime_initiated < expired_if_before:
505521
logger.warning(f'Registration {reg._id} is stuck in archiving')
506-
if verify(reg):
522+
if verify(reg, *args, **kwargs):
507523
raise RegistrationStuckRecoverableException(f'Registration {reg._id} is stuck and verified recoverable')
508524
else:
509525
raise RegistrationStuckBrokenException(f'Registration {reg._id} is stuck and verified broken')

0 commit comments

Comments
 (0)