Skip to content

Commit

Permalink
Merge branch 'release/25.02.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
mfraezz committed Jan 27, 2025
2 parents cade3e1 + a366fbd commit ba2d354
Show file tree
Hide file tree
Showing 75 changed files with 2,385 additions and 280 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

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

25.01.0 (2025-01-21)
25.02.0 (2025-01-27)
====================
- DOI Versioning - BE Release

25.01.0 (2025-01-21)
====================
- Institutional Access Curator - BE Release

24.11.0 (2024-12-11)
Expand Down
14 changes: 7 additions & 7 deletions addons/base/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,8 @@ def decrypt_and_decode_jwt_payload():


def get_authenticated_resource(resource_id):
resource = AbstractNode.load(resource_id) or Preprint.load(resource_id)
resource, _ = Guid.load_referent(resource_id)

if not resource:
raise HTTPError(http_status.HTTP_404_NOT_FOUND, message='Resource not found.')

Expand Down Expand Up @@ -390,8 +391,7 @@ def get_auth(auth, **kwargs):
waterbutler_data = decrypt_and_decode_jwt_payload()

# Authenticate the resource based on the node_id and handle potential draft nodes
resource = get_authenticated_resource(waterbutler_data['nid'])

resource = get_authenticated_resource(waterbutler_data.get('nid'))
# Authenticate the user using cookie or Oauth if possible
authenticate_user_if_needed(auth, waterbutler_data, resource)

Expand Down Expand Up @@ -529,9 +529,9 @@ def create_waterbutler_log(payload, **kwargs):
auth = payload['auth']
# Don't log download actions
if payload['action'] in DOWNLOAD_ACTIONS:
guid = Guid.load(payload['metadata'].get('nid'))
if guid:
node = guid.referent
guid_id = payload['metadata'].get('nid')

node, _ = Guid.load_referent(guid_id)
return {'status': 'success'}

user = OSFUser.load(auth['id'])
Expand Down Expand Up @@ -890,7 +890,7 @@ def addon_view_or_download_file(auth, path, provider, **kwargs):
extras.pop('_', None) # Clean up our url params a bit
action = extras.get('action', 'view')
guid = kwargs.get('guid')
guid_target = getattr(Guid.load(guid), 'referent', None)
guid_target, _ = Guid.load_referent(guid)
target = guid_target or kwargs.get('node') or kwargs['project']

provider_safe = markupsafe.escape(provider)
Expand Down
8 changes: 5 additions & 3 deletions addons/osfstorage/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ def load_guid_as_target(func):

@functools.wraps(func)
def wrapped(*args, **kwargs):
guid = kwargs.get('guid')
target = getattr(Guid.load(guid), 'referent', None)
target, _ = Guid.load_referent(kwargs.get('guid'))

if not target:
raise HTTPError(
http_status.HTTP_404_NOT_FOUND,
Expand Down Expand Up @@ -88,7 +88,9 @@ def wrapped(payload, *args, **kwargs):
user = OSFUser.load(payload['user'])
# Waterbutler is sending back ['node'] under the destination payload - WB should change to target
target = payload['destination'].get('target') or payload['destination'].get('node')
dest_target = Guid.load(target).referent
dest_target, _ = Guid.load_referent(target)
if not dest_target:
raise HTTPError(http_status.HTTP_404_NOT_FOUND)
source = OsfStorageFileNode.get(payload['source'], kwargs['target'])
dest_parent = OsfStorageFolder.get(payload['destination']['parent'], dest_target)

Expand Down
2 changes: 1 addition & 1 deletion addons/osfstorage/listeners.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def checkin_files_task(node_id, user_id):
Guid = apps.get_model('osf.Guid')
OSFUser = apps.get_model('osf.OSFUser')

node = Guid.load(node_id).referent
node, _ = Guid.load_referent(node_id)
assert isinstance(node, (AbstractNode, Preprint))
user = OSFUser.load(user_id)

Expand Down
6 changes: 3 additions & 3 deletions addons/osfstorage/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from framework.celery_tasks import app
from framework.postcommit_tasks.handlers import enqueue_postcommit_task
from framework.sessions import get_session
from osf.models import BaseFileNode, Guid
from osf.models import BaseFileNode, Guid, Preprint

from addons.osfstorage import settings

Expand All @@ -21,7 +21,7 @@ def enqueue_update_analytics(node, file, version_idx, action='download'):

@app.task(max_retries=5, default_retry_delay=60)
def update_analytics_async(node_id, file_id, version_idx, session_key=None, action='download'):
node = Guid.load(node_id).referent
node, _ = Guid.load_referent(node_id)
file = BaseFileNode.load(file_id)
update_analytics(node, file, version_idx, session_key, action)

Expand All @@ -43,7 +43,7 @@ def update_analytics(node, file, version_idx, session_key, action='download'):
node_info = {
'contributors': contributors
}
resource = node.guids.first()
resource = node.get_guid() if isinstance(node, Preprint) else node.guids.first()

update_counter(resource, file, version=None, action=action, node_info=node_info, session_key=session_key)
update_counter(resource, file, version_idx, action, node_info=node_info, session_key=session_key)
Expand Down
5 changes: 4 additions & 1 deletion addons/osfstorage/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,9 +187,12 @@ def osfstorage_get_metadata(file_node, **kwargs):
@decorators.autoload_filenode(must_be='folder')
def osfstorage_get_children(file_node, **kwargs):
from django.contrib.contenttypes.models import ContentType
from osf.models.preprint import Preprint
user_id = request.args.get('user_id')
user_content_type_id = ContentType.objects.get_for_model(OSFUser).id
user_pk = OSFUser.objects.filter(guids___id=user_id, guids___id__isnull=False).values_list('pk', flat=True).first()
guid_id = file_node.target.get_guid().id if isinstance(file_node.target, Preprint) else file_node.target.guids.first().id,

with connection.cursor() as cursor:
# Read the documentation on FileVersion's fields before reading this code
cursor.execute("""
Expand Down Expand Up @@ -281,7 +284,7 @@ def osfstorage_get_children(file_node, **kwargs):
AND (NOT F.type IN ('osf.trashedfilenode', 'osf.trashedfile', 'osf.trashedfolder'))
""", [
user_content_type_id,
file_node.target.guids.first().id,
guid_id,
user_pk,
user_pk,
user_id,
Expand Down
35 changes: 15 additions & 20 deletions admin/preprints/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,19 @@
re_path(r'^known_spam$', views.PreprintKnownSpamList.as_view(), name='known-spam'),
re_path(r'^known_ham$', views.PreprintKnownHamList.as_view(), name='known-ham'),
re_path(r'^withdrawal_requests$', views.PreprintWithdrawalRequestList.as_view(), name='withdrawal-requests'),
re_path(r'^(?P<guid>[a-z0-9]+)/$', views.PreprintView.as_view(), name='preprint'),
re_path(r'^(?P<guid>[a-z0-9]+)/change_provider/$', views.PreprintProviderChangeView.as_view(), name='preprint-provider'),
re_path(r'^(?P<guid>[a-z0-9]+)/machine_state/$', views.PreprintMachineStateView.as_view(), name='preprint-machine-state'),
re_path(r'^(?P<guid>[a-z0-9]+)/reindex_share_preprint/$', views.PreprintReindexShare.as_view(),
name='reindex-share-preprint'),
re_path(r'^(?P<guid>[a-z0-9]+)/remove_user/(?P<user_id>[a-z0-9]+)/$', views.PreprintRemoveContributorView.as_view(),
name='remove-user'),
re_path(r'^(?P<guid>[a-z0-9]+)/make_private/$', views.PreprintMakePrivate.as_view(), name='make-private'),
re_path(r'^(?P<guid>[a-z0-9]+)/make_public/$', views.PreprintMakePublic.as_view(), name='make-public'),
re_path(r'^(?P<guid>[a-z0-9]+)/remove/$', views.PreprintDeleteView.as_view(), name='remove'),
re_path(r'^(?P<guid>[a-z0-9]+)/restore/$', views.PreprintDeleteView.as_view(), name='restore'),
re_path(r'^(?P<guid>[a-z0-9]+)/confirm_unflag/$', views.PreprintConfirmUnflagView.as_view(), name='confirm-unflag'),
re_path(r'^(?P<guid>[a-z0-9]+)/confirm_spam/$', views.PreprintConfirmSpamView.as_view(), name='confirm-spam'),
re_path(r'^(?P<guid>[a-z0-9]+)/confirm_ham/$', views.PreprintConfirmHamView.as_view(), name='confirm-ham'),
re_path(r'^(?P<guid>[a-z0-9]+)/reindex_elastic_preprint/$', views.PreprintReindexElastic.as_view(),
name='reindex-elastic-preprint'),
re_path(r'^(?P<guid>[a-z0-9]+)/approve_withdrawal/$', views.PreprintApproveWithdrawalRequest.as_view(),
name='approve-withdrawal'),
re_path(r'^(?P<guid>[a-z0-9]+)/reject_withdrawal/$', views.PreprintRejectWithdrawalRequest.as_view(),
name='reject-withdrawal'),
re_path(r'^(?P<guid>\w+)/$', views.PreprintView.as_view(), name='preprint'),
re_path(r'^(?P<guid>\w+)/change_provider/$', views.PreprintProviderChangeView.as_view(), name='preprint-provider'),
re_path(r'^(?P<guid>\w+)/machine_state/$', views.PreprintMachineStateView.as_view(), name='preprint-machine-state'),
re_path(r'^(?P<guid>\w+)/reindex_share_preprint/$', views.PreprintReindexShare.as_view(), name='reindex-share-preprint'),
re_path(r'^(?P<guid>\w+)/remove_user/(?P<user_id>[a-z0-9]+)/$', views.PreprintRemoveContributorView.as_view(), name='remove-user'),
re_path(r'^(?P<guid>\w+)/make_private/$', views.PreprintMakePrivate.as_view(), name='make-private'),
re_path(r'^(?P<guid>\w+)/make_public/$', views.PreprintMakePublic.as_view(), name='make-public'),
re_path(r'^(?P<guid>\w+)/remove/$', views.PreprintDeleteView.as_view(), name='remove'),
re_path(r'^(?P<guid>\w+)/restore/$', views.PreprintDeleteView.as_view(), name='restore'),
re_path(r'^(?P<guid>\w+)/confirm_unflag/$', views.PreprintConfirmUnflagView.as_view(), name='confirm-unflag'),
re_path(r'^(?P<guid>\w+)/confirm_spam/$', views.PreprintConfirmSpamView.as_view(), name='confirm-spam'),
re_path(r'^(?P<guid>\w+)/confirm_ham/$', views.PreprintConfirmHamView.as_view(), name='confirm-ham'),
re_path(r'^(?P<guid>\w+)/reindex_elastic_preprint/$', views.PreprintReindexElastic.as_view(), name='reindex-elastic-preprint'),
re_path(r'^(?P<guid>\w+)/approve_withdrawal/$', views.PreprintApproveWithdrawalRequest.as_view(), name='approve-withdrawal'),
re_path(r'^(?P<guid>\w+)/reject_withdrawal/$', views.PreprintRejectWithdrawalRequest.as_view(), name='reject-withdrawal'),
]
5 changes: 3 additions & 2 deletions admin/preprints/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
class PreprintMixin(PermissionRequiredMixin):

def get_object(self):
preprint = Preprint.objects.get(guids___id=self.kwargs['guid'])
preprint = Preprint.load(self.kwargs['guid'])
# Django template does not like attributes with underscores for some reason
preprint.guid = preprint._id
return preprint
Expand Down Expand Up @@ -324,9 +324,10 @@ class WithdrawalRequestMixin(PermissionRequiredMixin):
permission_required = 'osf.change_preprintrequest'

def get_object(self):
target = Preprint.load(self.kwargs['guid'])
return PreprintRequest.objects.filter(
request_type='withdrawal',
target__guids___id=self.kwargs['guid'],
target=target,
).first()

def get_success_url(self):
Expand Down
4 changes: 2 additions & 2 deletions api/actions/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from api.actions.permissions import ReviewActionPermission
from api.actions.serializers import NodeRequestActionSerializer, ReviewActionSerializer, PreprintRequestActionSerializer
from api.base.exceptions import Conflict
from api.base.filters import ListFilterMixin
from api.base.filters import TargetFilterMixin
from api.base.views import JSONAPIBaseView
from api.base.parsers import (
JSONAPIMultipleRelationshipsParser,
Expand Down Expand Up @@ -110,7 +110,7 @@ def get_object(self):
return action


class ReviewActionListCreate(JSONAPIBaseView, generics.ListCreateAPIView, ListFilterMixin):
class ReviewActionListCreate(JSONAPIBaseView, generics.ListCreateAPIView, TargetFilterMixin):
"""List of review actions viewable by this user
Actions represent state changes and/or comments on a reviewable object (e.g. a preprint)
Expand Down
91 changes: 85 additions & 6 deletions api/base/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@
from rest_framework import serializers as ser
from rest_framework.filters import OrderingFilter
from osf.models import Subject, Preprint
from osf.models.base import GuidMixin
from osf.models.base import GuidMixin, Guid
from functools import cmp_to_key
from framework import sentry

def lowercase(lower):
if hasattr(lower, '__call__'):
Expand Down Expand Up @@ -568,24 +569,102 @@ def get_serializer_method(self, field_name):


class PreprintFilterMixin(ListFilterMixin):
"""View mixin that uses ListFilterMixin, adding postprocessing for preprint querying
"""View mixin for many preprint listing views. It inherits from ListFilterMixin and customize postprocessing for
preprint querying by provider, subjects and versioned `_id`.
Subclasses must define `get_default_queryset()`.
Note: Subclasses must define `get_default_queryset()`.
"""

@staticmethod
def postprocess_versioned_guid_id_query_param(operation):
"""Handle query parameters when filtering on `_id` for preprint which are now versioned.
Note: preprint achieves versioning by using versioned guid, and thus no long has guid or guid._id for every
version. Must convert `guid___id__in=` look-up to `pk__in` look-up.
"""
preprint_pk_list = []
for val in operation['value']:
referent, version = Guid.load_referent(val)
if referent is None:
continue
preprint_pk_list.append(referent.id)
# Override the operation to filter `id__in=preprint_pk_list`
operation['source_field_name'] = 'id__in'
operation['value'] = preprint_pk_list
operation['op'] = 'eq'

def postprocess_query_param(self, key, field_name, operation):
if field_name == 'provider':
operation['source_field_name'] = 'provider___id'

if field_name == 'id':
operation['source_field_name'] = 'guids___id'
PreprintFilterMixin.postprocess_versioned_guid_id_query_param(operation)

if field_name == 'subjects':
self.postprocess_subject_query_param(operation)

def preprints_queryset(self, base_queryset, auth_user, allow_contribs=True, public_only=False):
return Preprint.objects.can_view(
def preprints_queryset(self, base_queryset, auth_user, allow_contribs=True, public_only=False, latest_only=False):
preprints = Preprint.objects.can_view(
base_queryset=base_queryset,
user=auth_user,
allow_contribs=allow_contribs,
public_only=public_only,
)
if latest_only:
preprints = preprints.filter(guids__isnull=False)
return preprints


class TargetFilterMixin(ListFilterMixin):
"""View mixin for multi-content-type list views (e.g. `ReviewActionListCreate`). It inherits from `ListFilterMixin`
and customizes the postprocessing of the `target` field when target is a preprint.
Note: Subclasses must define `get_default_queryset()`.
"""

@staticmethod
def postprocess_preprint_as_target_query_param(operation, target_pk):
"""When target is a preprint, which must be versioned, the traditional non-versioned `guid___id==_id`
look-up no longer works. Must convert it to PK look-up `target__id==pk`.
"""
# Override the operation to filter `target__id==pk`
operation['source_field_name'] = 'target__id'
operation['value'] = target_pk
operation['op'] = 'eq'

def postprocess_query_param(self, key, field_name, operation):
"""Handles a special case when filtering on `target` when `target` is a Preprint.
"""
if field_name == 'target':
referent, version = Guid.load_referent(operation['value'])
if referent:
if version:
TargetFilterMixin.postprocess_preprint_as_target_query_param(operation, referent.id)
else:
super().postprocess_query_param(key, field_name, operation)
else:
sentry.log_message(f'Target object invalid or not found: [target={operation['value']}]')
return
else:
super().postprocess_query_param(key, field_name, operation)


class PreprintAsTargetFilterMixin(TargetFilterMixin):
"""View mixin for preprint related list views (e.g. `PreprintProviderWithdrawRequestList` and `PreprintActionList`).
It inherits from `TargetFilterMixin` and customizes postprocessing the `target` field for preprint.
Note: Subclasses must define `get_default_queryset()`.
"""

def postprocess_query_param(self, key, field_name, operation):
"""Handles a special case when filtering on `target`.
"""
if field_name == 'target':
referent, version = Guid.load_referent(operation['value'])
# A valid preprint must have referent and version
if not referent or not version:
sentry.log_message(f'Preprint invalid or note found: [target={operation['value']}]')
return
TargetFilterMixin.postprocess_preprint_as_target_query_param(operation, referent.id)
else:
super().postprocess_query_param(key, field_name, operation)
3 changes: 1 addition & 2 deletions api/base/pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,8 +238,7 @@ def get_paginated_response(self, data):
class PreprintContributorPagination(NodeContributorPagination):

def get_resource(self, kwargs):
resource_id = kwargs.get('preprint_id')
return Preprint.load(resource_id)
return Preprint.load(kwargs.get('preprint_id'))


class DraftRegistrationContributorPagination(NodeContributorPagination):
Expand Down
12 changes: 9 additions & 3 deletions api/base/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from framework.auth import Auth
from framework.auth.cas import CasResponse
from framework.auth.oauth_scopes import ComposedScopes, normalize_scopes
from osf.models.base import GuidMixin
from osf.models.base import GuidMixin, VersionedGuidMixin
from osf.utils.requests import check_select_for_update
from website import settings as website_settings
from website import util as website_util # noqa
Expand Down Expand Up @@ -103,8 +103,14 @@ def get_object_or_error(model_or_qs, query_or_pk=None, request=None, display_nam
raise NotFound

elif isinstance(query_or_pk, str):
# they passed a 5-char guid as a string
if issubclass(model_cls, GuidMixin):
# If the class is a subclass of `VersionedGuidMixin`, get obj directly from model using `.load()`. The naming
# for `query_or_pk` no longer matches the actual case. It is neither a query nor a pk, but a guid str.
if issubclass(model_cls, VersionedGuidMixin):
obj = model_cls.load(query_or_pk, select_for_update=select_for_update)
# If the class is a subclass of `GuidMixin` (except for `VersionedGuidMixin`), turn it into a query dictionary.
# The naming for `query_or_pk` no longer matches the actual case either. It is neither a query nor a pk, but a
# 5-char guid str. We should be able to use the `.load()` the same way as in the `VersionedGuidMixin` case.
elif issubclass(model_cls, GuidMixin):
# if it's a subclass of GuidMixin we know it's primary_identifier_name
query = {'guids___id': query_or_pk}
else:
Expand Down
5 changes: 3 additions & 2 deletions api/chronos/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,9 @@ def get_serializer_class(self):

def get_default_queryset(self):
user = get_user_auth(self.request).user
preprint_contributors = Preprint.load(self.kwargs['preprint_id'])._contributors
queryset = ChronosSubmission.objects.filter(preprint__guids___id=self.kwargs['preprint_id'])
preprint = Preprint.load(self.kwargs['preprint_id'])
preprint_contributors = preprint._contributors
queryset = ChronosSubmission.objects.filter(preprint=preprint)

# Get the list of stale submissions and queue a task to update them
update_list_id = queryset.filter(
Expand Down
Loading

0 comments on commit ba2d354

Please sign in to comment.