Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
624f1e3
Add registration callback endpoint and tests
Ostap-Zherebetskyi Apr 3, 2025
940bc3c
add-trailing-comma
Ostap-Zherebetskyi Apr 3, 2025
178badb
fix test_view_classes_have_minimal_set_of_permissions_classes
Ostap-Zherebetskyi Apr 3, 2025
054ce01
remove old Flask registration_callback code and related decorators
Ostap-Zherebetskyi Apr 3, 2025
7507f66
Remove outdated tests
Ostap-Zherebetskyi Apr 14, 2025
494df5b
Merge branch 'hotfix/25.07.1'
mfraezz Apr 21, 2025
b9e5e78
gdpr deletion shouldn't take into account deleted nodes (#11098)
ihorsokhanexoft Apr 21, 2025
2314b1f
Merge pull request #11095 from Ostap-Zherebetskyi/feature/outdated_code
Johnetordoff Apr 21, 2025
7b3acf5
[ENG-7270] Enable Product Team to Force Archive Registrations in the …
antkryt Apr 22, 2025
b5f5d22
[ENG-7798] Parse versioned guid (#11104)
ihorsokhanexoft Apr 22, 2025
c43e24e
[ENG-7263] Fix/eng 7263 (#11090)
Vlad0n20 Apr 22, 2025
73b2a4a
Merge pull request #11089 from Ostap-Zherebetskyi/feature/registratio…
Johnetordoff Apr 23, 2025
a7a3bc6
[ENG-7503] Fix/eng 7503 (#11092)
Vlad0n20 Apr 24, 2025
b233bfe
delete sharev2 push [ENG-7387] (#11032)
aaxelb Apr 24, 2025
90d5b68
[ENG-7716] Allow for reinstatement of previous preprint versions (wit…
antkryt Apr 24, 2025
0d3698c
fix feature for non-contributor admin (#11111)
antkryt Apr 24, 2025
7e0c97b
[ENG-7263] Part 2 (#11110)
Vlad0n20 Apr 24, 2025
a746e7c
[ENG-7263] Fix/eng 7263 part 3 (#11119)
Vlad0n20 Apr 29, 2025
1c9e98b
[ENG-7716] Allow for reinstatement of previous preprint versions (wit…
antkryt Apr 29, 2025
3e83dec
Merge branch 'develop' into upstream/pbs-25-08
brianjgeiger Apr 30, 2025
8935d34
Merge pull request #11124 from CenterForOpenScience/feature/pbs-25-08
brianjgeiger May 2, 2025
098d7f0
Update CHANGELOG and package.json
brianjgeiger May 2, 2025
f7737c5
Merge tag '25.08.0' into develop
brianjgeiger May 2, 2025
136e39a
Merge branch 'release/25.08.0'
brianjgeiger May 2, 2025
094decf
improve action creation test (#11109)
Johnetordoff May 2, 2025
7afb742
[ENG-7811] Reviews Dashboard not displaying contributors for preprint…
antkryt May 2, 2025
d12a1c2
do not make users unregistered when disable (#11112)
antkryt May 2, 2025
2508c9b
return number of contact messages of each admin (#11115)
ihorsokhanexoft May 2, 2025
b2efad2
[ENG-7879] Ability to resync preprints that have missing doi with Cro…
ihorsokhanexoft May 2, 2025
f19bf22
added share update on preprint withdrawal (#11123)
ihorsokhanexoft May 2, 2025
9e2fc2c
try to handle an unknown error (#11127)
ihorsokhanexoft May 2, 2025
288a289
[ENG-7921] Add scopes for applications to full_read and full_write sc…
Johnetordoff May 7, 2025
1fe3759
[ENG-7871] Removed restrictions that prohibit options deletion (#11128)
ihorsokhanexoft May 7, 2025
b046a63
[ENG-7928] Unable to add a user as an admin or moderator to a Socarxi…
antkryt May 7, 2025
8cb38c7
[ENG-4673] Add search by ORCID functionality to admin (#11129)
antkryt May 8, 2025
9f366cd
revert async email sending (#11134)
ihorsokhanexoft May 8, 2025
8d5e107
fix TypeError when check stucked registration
antkryt May 9, 2025
99cf255
Merge tag '25.08.1' into develop
brianjgeiger May 9, 2025
c89429d
Merge branch 'hotfix/25.08.1'
brianjgeiger May 9, 2025
6ccc0fa
Merge pull request #11143 from CenterForOpenScience/feature/pbs-25-09
brianjgeiger May 14, 2025
a69723e
Update changelog and package.json
brianjgeiger May 14, 2025
83259d8
Merge branch 'release/25.09.0'
brianjgeiger May 14, 2025
2328dd6
Merge tag '25.09.0' into develop
brianjgeiger May 14, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@

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

25.09.0 (2025-05-14)
====================

- Accounts confirmed but not registered
- Action requests creation for preprints and nodes not working
- Reviews Dashboard not displaying contributors for preprint provider moderators
- Backend Support for Static Contact Indicator in Institutional Dashboard
- Add ability to remove categories from active collections
- Add scopes for applications to full_read and full_write scopes

25.08.0 (2025-05-02)
====================

- Bugfix and Improvements

25.07.0 (2025-04-21)
====================

- Bugfix and Improvements

25.06.0 (2025-04-07)
====================

Expand Down
6 changes: 1 addition & 5 deletions addons/base/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -431,11 +431,7 @@ def _enqueue_metrics(file_version, file_node, action, auth, from_mfr=False):
def _construct_payload(auth, resource, credentials, waterbutler_settings):

if isinstance(resource, Registration):
callback_url = resource.api_url_for(
'registration_callbacks',
_absolute=True,
_internal=True
)
callback_url = resource.callbacks_url
else:
callback_url = resource.api_url_for(
'create_waterbutler_log',
Expand Down
86 changes: 1 addition & 85 deletions admin/collection_providers/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from django import forms

from framework.utils import sanitize_html
from osf.models import CollectionProvider, CollectionSubmission
from osf.models import CollectionProvider
from admin.base.utils import get_nodelicense_choices, get_defaultlicense_choices, validate_slug


Expand Down Expand Up @@ -74,12 +74,6 @@ def clean_collected_type_choices(self):
type_choices_new = {c.strip(' ') for c in json.loads(self.data.get('collected_type_choices'))}
type_choices_added = type_choices_new - type_choices_old
type_choices_removed = type_choices_old - type_choices_new
for item in type_choices_removed:
if CollectionSubmission.objects.filter(collection=collection_provider.primary_collection,
collected_type=item).exists():
raise forms.ValidationError(
f'Cannot delete "{item}" because it is used as metadata on objects.'
)
else:
# if this is creating a CollectionProvider
type_choices_added = []
Expand All @@ -104,12 +98,6 @@ def clean_status_choices(self):
status_choices_new = {c.strip(' ') for c in json.loads(self.data.get('status_choices'))}
status_choices_added = status_choices_new - status_choices_old
status_choices_removed = status_choices_old - status_choices_new
for item in status_choices_removed:
if CollectionSubmission.objects.filter(collection=collection_provider.primary_collection,
status=item).exists():
raise forms.ValidationError(
f'Cannot delete "{item}" because it is used as metadata on objects.'
)
else:
# if this is creating a CollectionProvider
status_choices_added = []
Expand All @@ -134,12 +122,6 @@ def clean_volume_choices(self):
volume_choices_new = {c.strip(' ') for c in json.loads(self.data.get('volume_choices'))}
volume_choices_added = volume_choices_new - volume_choices_old
volume_choices_removed = volume_choices_old - volume_choices_new
for item in volume_choices_removed:
if CollectionSubmission.objects.filter(collection=collection_provider.primary_collection,
volume=item).exists():
raise forms.ValidationError(
f'Cannot delete "{item}" because it is used as metadata on objects.'
)
else:
# if this is creating a CollectionProvider
volume_choices_added = []
Expand All @@ -164,12 +146,6 @@ def clean_issue_choices(self):
issue_choices_new = {c.strip(' ') for c in json.loads(self.data.get('issue_choices'))}
issue_choices_added = issue_choices_new - issue_choices_old
issue_choices_removed = issue_choices_old - issue_choices_new
for item in issue_choices_removed:
if CollectionSubmission.objects.filter(collection=collection_provider.primary_collection,
issue=item).exists():
raise forms.ValidationError(
f'Cannot delete "{item}" because it is used as metadata on objects.'
)
else:
# if this is creating a CollectionProvider
issue_choices_added = []
Expand All @@ -194,12 +170,6 @@ def clean_program_area_choices(self):
program_area_choices_new = {c.strip(' ') for c in json.loads(self.data.get('program_area_choices'))}
program_area_choices_added = program_area_choices_new - program_area_choices_old
program_area_choices_removed = program_area_choices_old - program_area_choices_new
for item in program_area_choices_removed:
if CollectionSubmission.objects.filter(collection=collection_provider.primary_collection,
program_area=item).exists():
raise forms.ValidationError(
f'Cannot delete "{item}" because it is used as metadata on objects.'
)
else:
# if this is creating a CollectionProvider
program_area_choices_added = []
Expand All @@ -224,16 +194,6 @@ def clean_school_type_choices(self):
updated_choices = {c.strip(' ') for c in json.loads(self.data.get('school_type_choices'))}
added_choices = updated_choices - old_choices
removed_choices = old_choices - updated_choices
active_removed_choices = set(
primary_collection.collectionsubmission_set.filter(
school_type__in=removed_choices
).values_list('school_type', flat=True)
)
if active_removed_choices:
raise forms.ValidationError(
'Cannot remove the following choices for "school_type", as they are '
f'currently in use: {active_removed_choices}'
)
else: # Creating a new CollectionProvider
added_choices = set()
removed_choices = set()
Expand All @@ -253,17 +213,6 @@ def clean_study_design_choices(self):
updated_choices = {c.strip(' ') for c in json.loads(self.data.get('study_design_choices'))}
added_choices = updated_choices - old_choices
removed_choices = old_choices - updated_choices

active_removed_choices = set(
primary_collection.collectionsubmission_set.filter(
study_design__in=removed_choices
).values_list('school_type', flat=True)
)
if active_removed_choices:
raise forms.ValidationError(
'Cannot remove the following choices for "study_design", as they are '
f'currently in use: {active_removed_choices}'
)
else: # Creating a new CollectionProvider
added_choices = set()
removed_choices = set()
Expand All @@ -283,17 +232,6 @@ def clean_disease_choices(self):
updated_choices = {c.strip(' ') for c in json.loads(self.data.get('disease_choices'))}
added_choices = updated_choices - old_choices
removed_choices = old_choices - updated_choices

active_removed_choices = set(
primary_collection.collectionsubmission_set.filter(
disease__in=removed_choices
).values_list('disease', flat=True)
)
if active_removed_choices:
raise forms.ValidationError(
'Cannot remove the following choices for "disease", as they are '
f'currently in use: {active_removed_choices}'
)
else: # Creating a new CollectionProvider
added_choices = set()
removed_choices = set()
Expand All @@ -313,17 +251,6 @@ def clean_data_type_choices(self):
updated_choices = {c.strip(' ') for c in json.loads(self.data.get('data_type_choices'))}
added_choices = updated_choices - old_choices
removed_choices = old_choices - updated_choices

active_removed_choices = set(
primary_collection.collectionsubmission_set.filter(
data_type__in=removed_choices
).values_list('data_type', flat=True)
)
if active_removed_choices:
raise forms.ValidationError(
'Cannot remove the following choices for "data_type", as they are '
f'currently in use: {active_removed_choices}'
)
else: # Creating a new CollectionProvider
added_choices = set()
removed_choices = set()
Expand All @@ -343,17 +270,6 @@ def clean_grade_levels_choices(self):
updated_choices = {c.strip(' ') for c in json.loads(self.data.get('grade_levels_choices'))}
added_choices = updated_choices - old_choices
removed_choices = old_choices - updated_choices

active_removed_choices = set(
primary_collection.collectionsubmission_set.filter(
data_type__in=removed_choices
).values_list('grade_levels', flat=True)
)
if active_removed_choices:
raise forms.ValidationError(
'Cannot remove the following choices for "grade_levels", as they are '
f'currently in use: {active_removed_choices}'
)
else: # Creating a new CollectionProvider
added_choices = set()
removed_choices = set()
Expand Down
4 changes: 3 additions & 1 deletion admin/management/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,12 @@ def post(self, request):
class BulkResync(ManagementCommandPermissionView):

def post(self, request):
missing_dois_only = request.POST.get('missing_preprint_dois_only', False)
sync_doi_metadata.apply_async(kwargs={
'modified_date': timezone.now(),
'batch_size': None,
'dry_run': False
'dry_run': False,
'missing_preprint_dois_only': missing_dois_only
})
messages.success(request, 'Resyncing with CrossRef and DataCite! It will take some time.')
return redirect(reverse('management:commands'))
Expand Down
6 changes: 4 additions & 2 deletions admin/nodes/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@
re_path(r'^(?P<guid>[a-z0-9]+)/reindex_share_node/$', views.NodeReindexShare.as_view(), name='reindex-share-node'),
re_path(r'^(?P<guid>[a-z0-9]+)/reindex_elastic_node/$', views.NodeReindexElastic.as_view(),
name='reindex-elastic-node'),
re_path(r'^(?P<guid>[a-z0-9]+)/restart_stuck_registrations/$', views.RestartStuckRegistrationsView.as_view(),
name='restart-stuck-registrations'),
re_path(r'^(?P<guid>[a-z0-9]+)/remove_stuck_registrations/$', views.RemoveStuckRegistrationsView.as_view(),
name='remove-stuck-registrations'),
re_path(r'^(?P<guid>[a-z0-9]+)/check_archive_status/$', views.CheckArchiveStatusRegistrationsView.as_view(),
name='check-archive-status'),
re_path(r'^(?P<guid>[a-z0-9]+)/force_archive_registration/$', views.ForceArchiveRegistrationsView.as_view(),
name='force-archive-registration'),
re_path(r'^(?P<guid>[a-z0-9]+)/remove_user/(?P<user_id>[a-z0-9]+)/$', views.NodeRemoveContributorView.as_view(),
name='remove-user'),
re_path(r'^(?P<guid>[a-z0-9]+)/modify_storage_usage/$', views.NodeModifyStorageUsage.as_view(),
Expand Down
98 changes: 76 additions & 22 deletions admin/nodes/views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pytz
from enum import Enum
from datetime import datetime
from framework import status

Expand Down Expand Up @@ -26,7 +27,7 @@
from api.share.utils import update_share
from api.caching.tasks import update_storage_usage_cache

from osf.exceptions import NodeStateError
from osf.exceptions import NodeStateError, RegistrationStuckError
from osf.models import (
OSFUser,
NodeLog,
Expand Down Expand Up @@ -672,44 +673,97 @@ def post(self, request, *args, **kwargs):
return redirect(self.get_success_url())


class RestartStuckRegistrationsView(NodeMixin, TemplateView):
""" Allows an authorized user to restart a registrations archive process.
class RemoveStuckRegistrationsView(NodeMixin, View):
""" Allows an authorized user to remove a registrations if it's stuck in the archiving process.
"""
template_name = 'nodes/restart_registrations_modal.html'
permission_required = ('osf.view_node', 'osf.change_node')

def post(self, request, *args, **kwargs):
# Prevents circular imports that cause admin app to hang at startup
from osf.management.commands.force_archive import archive, verify
stuck_reg = self.get_object()
if verify(stuck_reg):
try:
archive(stuck_reg)
messages.success(request, 'Registration archive processes has restarted')
except Exception as exc:
messages.error(request, f'This registration cannot be unstuck due to {exc.__class__.__name__} '
f'if the problem persists get a developer to fix it.')
if Registration.find_failed_registrations().filter(id=stuck_reg.id).exists():
stuck_reg.delete_registration_tree(save=True)
messages.success(request, 'The registration has been deleted')
else:
messages.error(request, 'This registration may not technically be stuck,'
' if the problem persists get a developer to fix it.')

return redirect(self.get_success_url())


class RemoveStuckRegistrationsView(NodeMixin, TemplateView):
""" Allows an authorized user to remove a registrations if it's stuck in the archiving process.
class CheckArchiveStatusRegistrationsView(NodeMixin, View):
"""Allows an authorized user to check a registration archive status.
"""
permission_required = ('osf.view_node', 'osf.change_node')

def get(self, request, *args, **kwargs):
# Prevents circular imports that cause admin app to hang at startup
from osf.management.commands.force_archive import check

registration = self.get_object()

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

try:
archive_status = check(registration)
messages.success(request, archive_status)
except RegistrationStuckError as exc:
messages.error(request, str(exc))

return redirect(self.get_success_url())


class CollisionMode(Enum):
NONE: str = 'none'
SKIP: str = 'skip'
DELETE: str = 'delete'


class ForceArchiveRegistrationsView(NodeMixin, View):
"""Allows an authorized user to force archive registration.
"""
template_name = 'nodes/remove_registrations_modal.html'
permission_required = ('osf.view_node', 'osf.change_node')

def post(self, request, *args, **kwargs):
stuck_reg = self.get_object()
if Registration.find_failed_registrations().filter(id=stuck_reg.id).exists():
stuck_reg.delete_registration_tree(save=True)
messages.success(request, 'The registration has been deleted')
# Prevents circular imports that cause admin app to hang at startup
from osf.management.commands.force_archive import verify, archive, DEFAULT_PERMISSIBLE_ADDONS

registration = self.get_object()
force_archive_params = request.POST

collision_mode = force_archive_params.get('collision_mode', CollisionMode.NONE.value)
delete_collision = CollisionMode.DELETE.value == collision_mode
skip_collision = CollisionMode.SKIP.value == collision_mode

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

addons = set(force_archive_params.getlist('addons', []))
addons.update(DEFAULT_PERMISSIBLE_ADDONS)

try:
verify(registration, permissible_addons=addons, raise_error=True)
except ValidationError as exc:
messages.error(request, str(exc))
return redirect(self.get_success_url())

dry_mode = force_archive_params.get('dry_mode', False)

if dry_mode:
messages.success(request, f"Registration {registration._id} can be archived.")
else:
messages.error(request, 'This registration may not technically be stuck,'
' if the problem persists get a developer to fix it.')
try:
archive(
registration,
permissible_addons=addons,
allow_unconfigured=allow_unconfigured,
skip_collision=skip_collision,
delete_collision=delete_collision,
)
messages.success(request, 'Registration archive process has finished.')
except Exception as exc:
messages.error(request, f'This registration cannot be archived due to {exc.__class__.__name__}: {str(exc)}. '
f'If the problem persists get a developer to fix it.')

return redirect(self.get_success_url())

Expand Down
7 changes: 4 additions & 3 deletions admin/preprint_providers/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,11 +112,12 @@ class PreprintProviderRegisterModeratorOrAdminForm(forms.Form):
""" A form that finds an existing OSF User, and grants permissions to that
user so that they can use the admin app"""

def __init__(self, *args, **kwargs):
provider_id = kwargs.pop('provider_id')
def __init__(self, *args, provider_groups=None, **kwargs):
super().__init__(*args, **kwargs)

provider_groups = provider_groups or Group.objects.none()
self.fields['group_perms'] = forms.ModelMultipleChoiceField(
queryset=Group.objects.filter(name__startswith=f'reviews_preprint_{provider_id}'),
queryset=provider_groups,
required=False,
widget=forms.CheckboxSelectMultiple
)
Expand Down
Loading
Loading