Skip to content

Commit ce21ee0

Browse files
committed
Merge branch 'release/25.11.0'
2 parents 10b1af5 + 43beb60 commit ce21ee0

File tree

63 files changed

+4355
-2730
lines changed

Some content is hidden

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

63 files changed

+4355
-2730
lines changed

CHANGELOG

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

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

5+
25.11.0 (2025-06-30)
6+
====================
7+
8+
- Crossref DOIs not minting with _v1, OSF is displaying DOI versions with _v1
9+
- When hamming a spammed user, preprints and registrations remain private
10+
- Fix emabrgoed registrations not becoming public after admin date change
11+
- Add v2 enpoint for alternative email confirmation
12+
- Make relationship on v2/nodes for collected_in
13+
- API V2: get action reviews request not listing latest preprint submit/withdraw requests
14+
- Add ability for admin app to change registry that a registration belongs to
15+
- Update to /nodes/<node-id> api
16+
- Subscription filtering not working correctly
17+
- API V2: Serialize registation resource attributes in Node Linked By Registrations list view and Node Linked Registrations list view
18+
519
25.10.0 (2025-06-11)
620
====================
721

admin/nodes/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
re_path(r'^(?P<guid>[a-z0-9]+)/schema_responses/$', views.AdminNodeSchemaResponseView.as_view(),
2020
name='schema-responses'),
2121
re_path(r'^(?P<guid>[a-z0-9]+)/update_embargo/$', views.RegistrationUpdateEmbargoView.as_view(), name='update-embargo'),
22+
re_path(r'^(?P<guid>[a-z0-9]+)/change_provider/$', views.RegistrationChangeProviderView.as_view(), name='change-provider'),
2223
re_path(r'^(?P<guid>[a-z0-9]+)/remove/$', views.NodeDeleteView.as_view(), name='remove'),
2324
re_path(r'^(?P<guid>[a-z0-9]+)/restore/$', views.NodeDeleteView.as_view(), name='restore'),
2425
re_path(r'^(?P<guid>[a-z0-9]+)/confirm_spam/$', views.NodeConfirmSpamView.as_view(), name='confirm-spam'),

admin/nodes/views.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
ListView,
1717
TemplateView,
1818
)
19-
from django.shortcuts import redirect, reverse
19+
from django.shortcuts import redirect, reverse, get_object_or_404
2020
from django.urls import reverse_lazy
2121

2222
from admin.base.utils import change_embargo_date
@@ -33,6 +33,7 @@
3333
NodeLog,
3434
AbstractNode,
3535
Registration,
36+
RegistrationProvider,
3637
RegistrationApproval,
3738
SpamStatus
3839
)
@@ -398,6 +399,28 @@ def post(self, request, *args, **kwargs):
398399
return redirect(self.get_success_url())
399400

400401

402+
class RegistrationChangeProviderView(NodeMixin, View):
403+
""" Allows authorized users to update provider of a registration.
404+
"""
405+
permission_required = ('osf.change_node')
406+
407+
def post(self, request, *args, **kwargs):
408+
provider_id = int(request.POST.get('provider_id'))
409+
provider = get_object_or_404(RegistrationProvider, pk=provider_id)
410+
registration = self.get_object()
411+
412+
try:
413+
provider.validate_schema(registration.registration_schema)
414+
registration.provider = provider
415+
registration.save()
416+
except ValidationError as exc:
417+
messages.error(request, str(exc))
418+
else:
419+
messages.success(request, 'Provider successfully changed.')
420+
421+
return redirect(self.get_success_url())
422+
423+
401424
class NodeSpamList(PermissionRequiredMixin, ListView):
402425
""" Allows authorized users to view a list of nodes that have a particular spam status.
403426
"""

admin/templates/nodes/node.html

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,9 @@ <h2>{{ node.type|cut:'osf.'|title }}: <b>{{ node.title }}</b> <a href="{{ node.a
7878
</tr>
7979
<tr>
8080
<td>Provider</td>
81-
{% if node.provider %}
82-
<td><a href="{{ node | reverse_registration_provider }}">{{ node.provider.name }}</a></td>
83-
{% else %}
84-
<td>None</td>
85-
{% endif %}
81+
<td>
82+
{% include "nodes/registration_provider.html" with node=node %}
83+
</td>
8684
</tr>
8785
<tr>
8886
<td>Parent</td>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{% load node_extras %}
2+
3+
{% if node.provider %}
4+
<form id="provider-list-form" method="post" action="{% url 'nodes:change-provider' guid=node.guid %}">
5+
{% csrf_token %}
6+
<select id="provider-list" name="provider_id" onchange="this.form.submit()">
7+
<option value="{{ node.provider.id }}">{{ node.provider.name }}</option> <!-- default value -->
8+
{% for provider in node.available_providers %}
9+
{% if not provider.id == node.provider.id %}
10+
<option value="{{ provider.id }}">{{ provider.name }}</option>
11+
{% endif %}
12+
{% endfor %}
13+
</select>
14+
</form>
15+
{% else %}
16+
<p>None</p>
17+
{% endif %}

api/base/filters.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,8 @@ class FilterMixin:
160160
LIST_FIELDS = (ser.ListField,)
161161
RELATIONSHIP_FIELDS = (RelationshipField, TargetField)
162162

163+
MULTIPLE_VALUES_FIELDS = ['_id', 'guid._id', 'journal_id', 'moderation_state', 'event_name']
164+
163165
def __init__(self, *args, **kwargs):
164166
super().__init__(*args, **kwargs)
165167
if not self.serializer_class:
@@ -292,7 +294,7 @@ def parse_query_params(self, query_params):
292294
query.get(key).update({
293295
field_name: self._parse_date_param(field, source_field_name, op, value),
294296
})
295-
elif not isinstance(value, int) and source_field_name in ['_id', 'guid._id', 'journal_id', 'moderation_state']:
297+
elif not isinstance(value, int) and source_field_name in self.MULTIPLE_VALUES_FIELDS:
296298
query.get(key).update({
297299
field_name: {
298300
'op': 'in',

api/base/views.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -624,11 +624,15 @@ class BaseLinkedList(JSONAPIBaseView, generics.ListAPIView):
624624

625625
def get_queryset(self):
626626
auth = get_user_auth(self.request)
627+
from api.resources import annotations as resource_annotations
627628

628629
return (
629630
self.get_node().linked_nodes
631+
.annotate(**resource_annotations.make_open_practice_badge_annotations())
630632
.filter(is_deleted=False)
631-
.annotate(region=F('addons_osfstorage_node_settings__region___id'))
633+
.annotate(
634+
region=F('addons_osfstorage_node_settings__region___id'),
635+
)
632636
.exclude(region=None)
633637
.exclude(type='osf.collection', region=None)
634638
.can_view(user=auth.user, private_link=auth.private_link)

api/collections/views.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
from api.base.parsers import JSONAPIMultipleRelationshipsParser, JSONAPIMultipleRelationshipsParserForRegularJSON
1212

1313
from api.base.views import JSONAPIBaseView
14-
from api.base.views import BaseLinkedList
1514
from api.base.views import LinkedNodesRelationship
1615
from api.nodes.utils import NodeOptimizationMixin
1716

@@ -507,7 +506,7 @@ def get_resource(self, check_object_permissions=True):
507506
return self.get_collection_submission(check_object_permissions)
508507

509508

510-
class LinkedNodesList(BaseLinkedList, CollectionMixin, NodeOptimizationMixin):
509+
class LinkedNodesList(JSONAPIBaseView, generics.ListAPIView, CollectionMixin, NodeOptimizationMixin):
511510
"""List of nodes linked to this node. *Read-only*.
512511
513512
Linked nodes are the project/component nodes pointed to by node links. This view will probably replace node_links in the near future.
@@ -560,6 +559,10 @@ class LinkedNodesList(BaseLinkedList, CollectionMixin, NodeOptimizationMixin):
560559
CollectionWriteOrPublic,
561560
base_permissions.TokenHasScope,
562561
)
562+
563+
required_read_scopes = [CoreScopes.COLLECTED_META_READ]
564+
required_write_scopes = [CoreScopes.COLLECTED_META_WRITE]
565+
563566
serializer_class = NodeSerializer
564567
view_category = 'collections'
565568
view_name = 'linked-nodes'
@@ -582,7 +585,7 @@ def get_parser_context(self, http_request):
582585
return res
583586

584587

585-
class LinkedRegistrationsList(BaseLinkedList, CollectionMixin):
588+
class LinkedRegistrationsList(JSONAPIBaseView, generics.ListAPIView, CollectionMixin):
586589
"""List of registrations linked to this node. *Read-only*.
587590
588591
Linked registrations are the registration nodes pointed to by node links.
@@ -656,11 +659,22 @@ class LinkedRegistrationsList(BaseLinkedList, CollectionMixin):
656659
view_category = 'collections'
657660
view_name = 'linked-registrations'
658661

662+
required_read_scopes = [CoreScopes.COLLECTED_META_READ]
663+
required_write_scopes = [CoreScopes.COLLECTED_META_WRITE]
664+
659665
ordering = ('-modified',)
660666

661667
def get_queryset(self):
662668
auth = get_user_auth(self.request)
663-
return Registration.objects.filter(guids__in=self.get_collection().active_guids.all(), is_deleted=False).can_view(user=auth.user, private_link=auth.private_link).order_by('-modified')
669+
return Registration.objects.filter(
670+
guids__in=self.get_collection().active_guids.all(),
671+
is_deleted=False,
672+
).can_view(
673+
user=auth.user,
674+
private_link=auth.private_link,
675+
).order_by(
676+
'-modified',
677+
)
664678

665679
# overrides APIView
666680
def get_parser_context(self, http_request):
@@ -672,7 +686,7 @@ def get_parser_context(self, http_request):
672686
return res
673687

674688

675-
class LinkedPreprintsList(BaseLinkedList, CollectionMixin):
689+
class LinkedPreprintsList(JSONAPIBaseView, generics.ListAPIView, CollectionMixin):
676690
"""List of preprints linked to this collection. *Read-only*.
677691
"""
678692
permission_classes = (
@@ -684,6 +698,9 @@ class LinkedPreprintsList(BaseLinkedList, CollectionMixin):
684698
view_category = 'collections'
685699
view_name = 'linked-preprints'
686700

701+
required_read_scopes = [CoreScopes.COLLECTED_META_READ]
702+
required_write_scopes = [CoreScopes.COLLECTED_META_WRITE]
703+
687704
ordering = ('-modified',)
688705

689706
def get_queryset(self):

api/nodes/serializers.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
from django.core.exceptions import ValidationError
2929
from framework.auth.core import Auth
3030
from framework.exceptions import PermissionsError
31-
from osf.models import Tag
31+
from osf.models import Tag, CollectionSubmission
3232
from rest_framework import serializers as ser
3333
from rest_framework import exceptions
3434
from addons.base.exceptions import InvalidAuthError, InvalidFolderError
@@ -324,6 +324,12 @@ class NodeSerializer(TaxonomizableSerializerMixin, JSONAPISerializer):
324324
related_meta={'count': 'get_node_count'},
325325
)
326326

327+
collected_in = RelationshipField(
328+
related_view='nodes:node-collections',
329+
related_view_kwargs={'node_id': '<_id>'},
330+
related_meta={'count': 'get_collection_count'},
331+
)
332+
327333
comments = RelationshipField(
328334
related_view='nodes:node-comments',
329335
related_view_kwargs={'node_id': '<_id>'},
@@ -486,6 +492,7 @@ class NodeSerializer(TaxonomizableSerializerMixin, JSONAPISerializer):
486492
view_only_links = RelationshipField(
487493
related_view='nodes:node-view-only-links',
488494
related_view_kwargs={'node_id': '<_id>'},
495+
related_meta={'count': 'get_view_only_links_count'},
489496
)
490497

491498
citation = RelationshipField(
@@ -620,6 +627,9 @@ def get_absolute_url(self, obj):
620627
def get_logs_count(self, obj):
621628
return obj.logs.count()
622629

630+
def get_collection_count(self, obj):
631+
return CollectionSubmission.objects.filter(guid___id=obj._id).count()
632+
623633
def get_node_count(self, obj):
624634
"""
625635
Returns the count of a node's direct children that the user has permission to view.
@@ -702,6 +712,9 @@ def get_node_links_count(self, obj):
702712
linked_nodes = obj.linked_nodes.filter(is_deleted=False).exclude(type='osf.collection').exclude(type='osf.registration')
703713
return linked_nodes.can_view(auth.user, auth.private_link).count()
704714

715+
def get_view_only_links_count(self, obj):
716+
return obj.private_links_active.count()
717+
705718
def get_registration_links_count(self, obj):
706719
auth = get_user_auth(self.context['request'])
707720
linked_registrations = obj.linked_nodes.filter(is_deleted=False, type='osf.registration').exclude(type='osf.collection')

api/nodes/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
re_path(r'^(?P<node_id>\w+)/citation/$', views.NodeCitationDetail.as_view(), name=views.NodeCitationDetail.view_name),
2020
re_path(r'^(?P<node_id>\w+)/citation/(?P<style_id>[-\w]+)/$', views.NodeCitationStyleDetail.as_view(), name=views.NodeCitationStyleDetail.view_name),
2121
re_path(r'^(?P<node_id>\w+)/comments/$', views.NodeCommentsList.as_view(), name=views.NodeCommentsList.view_name),
22+
re_path(r'^(?P<node_id>\w+)/collections/$', views.NodeCollectionsList.as_view(), name=views.NodeCollectionsList.view_name),
2223
re_path(r'^(?P<node_id>\w+)/contributors_and_group_members/$', views.NodeContributorsAndGroupMembersList.as_view(), name=views.NodeContributorsAndGroupMembersList.view_name),
2324
re_path(r'^(?P<node_id>\w+)/implicit_contributors/$', views.NodeImplicitContributorsList.as_view(), name=views.NodeImplicitContributorsList.view_name),
2425
re_path(r'^(?P<node_id>\w+)/contributors/$', views.NodeContributorsList.as_view(), name=views.NodeContributorsList.view_name),

0 commit comments

Comments
 (0)