Skip to content

Commit 83259d8

Browse files
committed
Merge branch 'release/25.09.0'
2 parents c89429d + a69723e commit 83259d8

File tree

27 files changed

+620
-296
lines changed

27 files changed

+620
-296
lines changed

CHANGELOG

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

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

5+
25.09.0 (2025-05-14)
6+
====================
7+
8+
- Accounts confirmed but not registered
9+
- Action requests creation for preprints and nodes not working
10+
- Reviews Dashboard not displaying contributors for preprint provider moderators
11+
- Backend Support for Static Contact Indicator in Institutional Dashboard
12+
- Add ability to remove categories from active collections
13+
- Add scopes for applications to full_read and full_write scopes
14+
515
25.08.0 (2025-05-02)
616
====================
717

admin/collection_providers/forms.py

Lines changed: 1 addition & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from django import forms
44

55
from framework.utils import sanitize_html
6-
from osf.models import CollectionProvider, CollectionSubmission
6+
from osf.models import CollectionProvider
77
from admin.base.utils import get_nodelicense_choices, get_defaultlicense_choices, validate_slug
88

99

@@ -74,12 +74,6 @@ def clean_collected_type_choices(self):
7474
type_choices_new = {c.strip(' ') for c in json.loads(self.data.get('collected_type_choices'))}
7575
type_choices_added = type_choices_new - type_choices_old
7676
type_choices_removed = type_choices_old - type_choices_new
77-
for item in type_choices_removed:
78-
if CollectionSubmission.objects.filter(collection=collection_provider.primary_collection,
79-
collected_type=item).exists():
80-
raise forms.ValidationError(
81-
f'Cannot delete "{item}" because it is used as metadata on objects.'
82-
)
8377
else:
8478
# if this is creating a CollectionProvider
8579
type_choices_added = []
@@ -104,12 +98,6 @@ def clean_status_choices(self):
10498
status_choices_new = {c.strip(' ') for c in json.loads(self.data.get('status_choices'))}
10599
status_choices_added = status_choices_new - status_choices_old
106100
status_choices_removed = status_choices_old - status_choices_new
107-
for item in status_choices_removed:
108-
if CollectionSubmission.objects.filter(collection=collection_provider.primary_collection,
109-
status=item).exists():
110-
raise forms.ValidationError(
111-
f'Cannot delete "{item}" because it is used as metadata on objects.'
112-
)
113101
else:
114102
# if this is creating a CollectionProvider
115103
status_choices_added = []
@@ -134,12 +122,6 @@ def clean_volume_choices(self):
134122
volume_choices_new = {c.strip(' ') for c in json.loads(self.data.get('volume_choices'))}
135123
volume_choices_added = volume_choices_new - volume_choices_old
136124
volume_choices_removed = volume_choices_old - volume_choices_new
137-
for item in volume_choices_removed:
138-
if CollectionSubmission.objects.filter(collection=collection_provider.primary_collection,
139-
volume=item).exists():
140-
raise forms.ValidationError(
141-
f'Cannot delete "{item}" because it is used as metadata on objects.'
142-
)
143125
else:
144126
# if this is creating a CollectionProvider
145127
volume_choices_added = []
@@ -164,12 +146,6 @@ def clean_issue_choices(self):
164146
issue_choices_new = {c.strip(' ') for c in json.loads(self.data.get('issue_choices'))}
165147
issue_choices_added = issue_choices_new - issue_choices_old
166148
issue_choices_removed = issue_choices_old - issue_choices_new
167-
for item in issue_choices_removed:
168-
if CollectionSubmission.objects.filter(collection=collection_provider.primary_collection,
169-
issue=item).exists():
170-
raise forms.ValidationError(
171-
f'Cannot delete "{item}" because it is used as metadata on objects.'
172-
)
173149
else:
174150
# if this is creating a CollectionProvider
175151
issue_choices_added = []
@@ -194,12 +170,6 @@ def clean_program_area_choices(self):
194170
program_area_choices_new = {c.strip(' ') for c in json.loads(self.data.get('program_area_choices'))}
195171
program_area_choices_added = program_area_choices_new - program_area_choices_old
196172
program_area_choices_removed = program_area_choices_old - program_area_choices_new
197-
for item in program_area_choices_removed:
198-
if CollectionSubmission.objects.filter(collection=collection_provider.primary_collection,
199-
program_area=item).exists():
200-
raise forms.ValidationError(
201-
f'Cannot delete "{item}" because it is used as metadata on objects.'
202-
)
203173
else:
204174
# if this is creating a CollectionProvider
205175
program_area_choices_added = []
@@ -224,16 +194,6 @@ def clean_school_type_choices(self):
224194
updated_choices = {c.strip(' ') for c in json.loads(self.data.get('school_type_choices'))}
225195
added_choices = updated_choices - old_choices
226196
removed_choices = old_choices - updated_choices
227-
active_removed_choices = set(
228-
primary_collection.collectionsubmission_set.filter(
229-
school_type__in=removed_choices
230-
).values_list('school_type', flat=True)
231-
)
232-
if active_removed_choices:
233-
raise forms.ValidationError(
234-
'Cannot remove the following choices for "school_type", as they are '
235-
f'currently in use: {active_removed_choices}'
236-
)
237197
else: # Creating a new CollectionProvider
238198
added_choices = set()
239199
removed_choices = set()
@@ -253,17 +213,6 @@ def clean_study_design_choices(self):
253213
updated_choices = {c.strip(' ') for c in json.loads(self.data.get('study_design_choices'))}
254214
added_choices = updated_choices - old_choices
255215
removed_choices = old_choices - updated_choices
256-
257-
active_removed_choices = set(
258-
primary_collection.collectionsubmission_set.filter(
259-
study_design__in=removed_choices
260-
).values_list('school_type', flat=True)
261-
)
262-
if active_removed_choices:
263-
raise forms.ValidationError(
264-
'Cannot remove the following choices for "study_design", as they are '
265-
f'currently in use: {active_removed_choices}'
266-
)
267216
else: # Creating a new CollectionProvider
268217
added_choices = set()
269218
removed_choices = set()
@@ -283,17 +232,6 @@ def clean_disease_choices(self):
283232
updated_choices = {c.strip(' ') for c in json.loads(self.data.get('disease_choices'))}
284233
added_choices = updated_choices - old_choices
285234
removed_choices = old_choices - updated_choices
286-
287-
active_removed_choices = set(
288-
primary_collection.collectionsubmission_set.filter(
289-
disease__in=removed_choices
290-
).values_list('disease', flat=True)
291-
)
292-
if active_removed_choices:
293-
raise forms.ValidationError(
294-
'Cannot remove the following choices for "disease", as they are '
295-
f'currently in use: {active_removed_choices}'
296-
)
297235
else: # Creating a new CollectionProvider
298236
added_choices = set()
299237
removed_choices = set()
@@ -313,17 +251,6 @@ def clean_data_type_choices(self):
313251
updated_choices = {c.strip(' ') for c in json.loads(self.data.get('data_type_choices'))}
314252
added_choices = updated_choices - old_choices
315253
removed_choices = old_choices - updated_choices
316-
317-
active_removed_choices = set(
318-
primary_collection.collectionsubmission_set.filter(
319-
data_type__in=removed_choices
320-
).values_list('data_type', flat=True)
321-
)
322-
if active_removed_choices:
323-
raise forms.ValidationError(
324-
'Cannot remove the following choices for "data_type", as they are '
325-
f'currently in use: {active_removed_choices}'
326-
)
327254
else: # Creating a new CollectionProvider
328255
added_choices = set()
329256
removed_choices = set()
@@ -343,17 +270,6 @@ def clean_grade_levels_choices(self):
343270
updated_choices = {c.strip(' ') for c in json.loads(self.data.get('grade_levels_choices'))}
344271
added_choices = updated_choices - old_choices
345272
removed_choices = old_choices - updated_choices
346-
347-
active_removed_choices = set(
348-
primary_collection.collectionsubmission_set.filter(
349-
data_type__in=removed_choices
350-
).values_list('grade_levels', flat=True)
351-
)
352-
if active_removed_choices:
353-
raise forms.ValidationError(
354-
'Cannot remove the following choices for "grade_levels", as they are '
355-
f'currently in use: {active_removed_choices}'
356-
)
357273
else: # Creating a new CollectionProvider
358274
added_choices = set()
359275
removed_choices = set()

admin/management/views.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,10 +150,12 @@ def post(self, request):
150150
class BulkResync(ManagementCommandPermissionView):
151151

152152
def post(self, request):
153+
missing_dois_only = request.POST.get('missing_preprint_dois_only', False)
153154
sync_doi_metadata.apply_async(kwargs={
154155
'modified_date': timezone.now(),
155156
'batch_size': None,
156-
'dry_run': False
157+
'dry_run': False,
158+
'missing_preprint_dois_only': missing_dois_only
157159
})
158160
messages.success(request, 'Resyncing with CrossRef and DataCite! It will take some time.')
159161
return redirect(reverse('management:commands'))

admin/preprint_providers/forms.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,12 @@ class PreprintProviderRegisterModeratorOrAdminForm(forms.Form):
112112
""" A form that finds an existing OSF User, and grants permissions to that
113113
user so that they can use the admin app"""
114114

115-
def __init__(self, *args, **kwargs):
116-
provider_id = kwargs.pop('provider_id')
115+
def __init__(self, *args, provider_groups=None, **kwargs):
117116
super().__init__(*args, **kwargs)
117+
118+
provider_groups = provider_groups or Group.objects.none()
118119
self.fields['group_perms'] = forms.ModelMultipleChoiceField(
119-
queryset=Group.objects.filter(name__startswith=f'reviews_preprint_{provider_id}'),
120+
queryset=provider_groups,
120121
required=False,
121122
widget=forms.CheckboxSelectMultiple
122123
)

admin/preprint_providers/views.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from django.contrib.auth.mixins import PermissionRequiredMixin
1313
from django.forms.models import model_to_dict
1414
from django.shortcuts import redirect, render
15+
from django.utils.functional import cached_property
1516

1617
from admin.base import settings
1718
from admin.base.forms import ImportFileForm
@@ -459,14 +460,18 @@ class PreprintProviderRegisterModeratorOrAdmin(PermissionRequiredMixin, FormView
459460
template_name = 'preprint_providers/register_moderator_admin.html'
460461
form_class = PreprintProviderRegisterModeratorOrAdminForm
461462

463+
@cached_property
464+
def target_provider(self):
465+
return PreprintProvider.objects.get(id=self.kwargs['preprint_provider_id'])
466+
462467
def get_form_kwargs(self):
463468
kwargs = super().get_form_kwargs()
464-
kwargs['provider_id'] = self.kwargs['preprint_provider_id']
469+
kwargs['provider_groups'] = self.target_provider.group_objects
465470
return kwargs
466471

467472
def get_context_data(self, **kwargs):
468473
context = super().get_context_data(**kwargs)
469-
context['provider_name'] = PreprintProvider.objects.get(id=self.kwargs['preprint_provider_id']).name
474+
context['provider_name'] = self.target_provider.name
470475
return context
471476

472477
def form_valid(self, form):
@@ -477,13 +482,7 @@ def form_valid(self, form):
477482
raise Http404(f'OSF user with id "{user_id}" not found. Please double check.')
478483

479484
for group in form.cleaned_data.get('group_perms'):
480-
osf_user.groups.add(group)
481-
split = group.name.split('_')
482-
group_type = split[0]
483-
if group_type == 'reviews':
484-
provider_id = split[2]
485-
provider = PreprintProvider.objects.get(id=provider_id)
486-
provider.notification_subscriptions.get(event_name='new_pending_submissions').add_user_to_subscription(osf_user, 'email_transactional')
485+
self.target_provider.add_to_group(osf_user, group)
487486

488487
osf_user.save()
489488
messages.success(self.request, f'Permissions update successful for OSF User {osf_user.username}!')

admin/templates/management/commands.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ <h4> <u>Ban spam users by regular expression</u>
7474
<label>Nodes:</label> <input type="checkbox" name="node_ban" checked /><br>
7575
<label>Registrations:</label> <input type="checkbox" name="registration_ban" checked /><br>
7676
<label>Preprints:</label> <input type="checkbox" name="preprint_ban" checked /><br>
77-
<input class="btn btn-danger" type="submit" value="Run" style="color: red" />
77+
<input class="btn btn-danger" type="submit" value="Run" style="color: white" />
7878
</form>
7979
</ul>
8080
<section>
@@ -133,6 +133,7 @@ <h4><u>Resync with CrossRef and DataCite</u></h4>
133133
<form method="post"
134134
action="{% url 'management:bulk-resync'%}">
135135
{% csrf_token %}
136+
<label>Only preprints missing DOI:</label> <input type="checkbox" name="missing_preprint_dois_only"/><br>
136137
<nav>
137138
<input class="btn btn-success" type="submit" value="Run" />
138139
</nav>

admin/users/forms.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class UserSearchForm(forms.Form):
1414
guid = forms.CharField(label='guid', min_length=5, max_length=5, required=False) # TODO: Move max to 6 when needed
1515
name = forms.CharField(label='name', required=False)
1616
email = forms.EmailField(label='email', required=False)
17+
orcid = forms.CharField(label='orcid', required=False)
1718

1819

1920
class MergeUserForm(forms.Form):

admin/users/views.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from framework.auth.core import generate_verification_key
2626

2727
from website import search
28+
from website.settings import EXTERNAL_IDENTITY_PROFILE
2829

2930
from osf.models.admin_log_entry import (
3031
update_admin_log,
@@ -126,6 +127,7 @@ def form_valid(self, form):
126127
guid = form.cleaned_data['guid']
127128
name = form.cleaned_data['name']
128129
email = form.cleaned_data['email']
130+
orcid = form.cleaned_data['orcid']
129131
if name:
130132
return redirect(reverse('users:search-list', kwargs={'name': name}))
131133

@@ -148,6 +150,18 @@ def form_valid(self, form):
148150

149151
return redirect(reverse('users:user', kwargs={'guid': guid}))
150152

153+
if orcid:
154+
external_id_provider = EXTERNAL_IDENTITY_PROFILE.get('OrcidProfile')
155+
user = get_user(external_id_provider=external_id_provider, external_id=orcid)
156+
157+
if not user:
158+
return page_not_found(
159+
self.request,
160+
AttributeError(f'resource with id "{orcid}" not found.')
161+
)
162+
163+
return redirect(reverse('users:user', kwargs={'guid': user._id}))
164+
151165
return super().form_valid(form)
152166

153167

@@ -456,9 +470,6 @@ def get_context_data(self, **kwargs):
456470

457471
class GetUserConfirmationLink(GetUserLink):
458472
def get_link(self, user):
459-
if user.is_confirmed:
460-
return f'User {user._id} is already confirmed'
461-
462473
if user.deleted or user.is_merged:
463474
return f'User {user._id} is deleted or merged'
464475

admin_tests/preprints/test_views.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -536,7 +536,8 @@ def withdrawal_request(self, preprint, submitter):
536536
withdrawal_request.run_submit(submitter)
537537
return withdrawal_request
538538

539-
def test_can_approve_withdrawal_request(self, withdrawal_request, submitter, preprint, admin):
539+
@mock.patch('osf.models.preprint.update_or_enqueue_on_preprint_updated')
540+
def test_can_approve_withdrawal_request(self, mocked_function, withdrawal_request, submitter, preprint, admin):
540541
assert withdrawal_request.machine_state == DefaultStates.PENDING.value
541542
original_comment = withdrawal_request.comment
542543

@@ -552,6 +553,12 @@ def test_can_approve_withdrawal_request(self, withdrawal_request, submitter, pre
552553
assert withdrawal_request.machine_state == DefaultStates.ACCEPTED.value
553554
assert original_comment == withdrawal_request.target.withdrawal_justification
554555

556+
# share update is triggered when update "date_withdrawn" and "withdrawal_justification" throughout withdrawal process
557+
updated_fields = mocked_function.call_args[1]['saved_fields']
558+
assert 'date_withdrawn' in updated_fields
559+
assert 'withdrawal_justification' in updated_fields
560+
assert preprint.SEARCH_UPDATE_FIELDS.intersection(updated_fields)
561+
555562
def test_can_reject_withdrawal_request(self, withdrawal_request, admin, preprint):
556563
assert withdrawal_request.machine_state == DefaultStates.PENDING.value
557564

0 commit comments

Comments
 (0)