Skip to content

[ENG-7839] [ENG-7766] Add celery task to update Datacite metadata + Use new GV API endpoint to retrieve node's verified links #11117

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
af108e4
gdpr deletion shouldn't take into account deleted nodes (#11098)
ihorsokhanexoft Apr 21, 2025
bb1892c
Remove outdated tests
Ostap-Zherebetskyi Apr 14, 2025
31fe97a
[ENG-7270] Enable Product Team to Force Archive Registrations in the …
antkryt Apr 22, 2025
49f479a
[ENG-7798] Parse versioned guid (#11104)
ihorsokhanexoft Apr 22, 2025
30f8185
[ENG-7263] Fix/eng 7263 (#11090)
Vlad0n20 Apr 22, 2025
3738135
Add registration callback endpoint and tests
Ostap-Zherebetskyi Apr 3, 2025
418c044
add-trailing-comma
Ostap-Zherebetskyi Apr 3, 2025
24f4bd6
fix test_view_classes_have_minimal_set_of_permissions_classes
Ostap-Zherebetskyi Apr 3, 2025
15d7175
remove old Flask registration_callback code and related decorators
Ostap-Zherebetskyi Apr 3, 2025
1bb79cf
[ENG-7503] Fix/eng 7503 (#11092)
Vlad0n20 Apr 24, 2025
94ad504
delete sharev2 push [ENG-7387] (#11032)
aaxelb Apr 24, 2025
6fbef50
[ENG-7716] Allow for reinstatement of previous preprint versions (wit…
antkryt Apr 24, 2025
3688712
fix feature for non-contributor admin (#11111)
antkryt Apr 24, 2025
436cd20
[ENG-7263] Part 2 (#11110)
Vlad0n20 Apr 24, 2025
36abc51
[ENG-7263] Fix/eng 7263 part 3 (#11119)
Vlad0n20 Apr 29, 2025
dc59b8f
[ENG-7716] Allow for reinstatement of previous preprint versions (wit…
antkryt Apr 29, 2025
1655687
Add new task to update node metadata with verified_links
Vlad0n20 Apr 8, 2025
daa2b76
Update task to change node metadata to use gv request to get verified…
Vlad0n20 Apr 29, 2025
b309399
Add mock for gravy valet get_links to fix failed tests
Vlad0n20 May 1, 2025
09df0f1
Fix test_doi_synced_datacite test
Vlad0n20 May 1, 2025
dfdf156
Update PR accordng to comments
Vlad0n20 May 5, 2025
b028166
Add handling empty response from gv
Vlad0n20 May 5, 2025
01a7438
Add handling empty response from gv
Vlad0n20 May 5, 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
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
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
2 changes: 2 additions & 0 deletions admin/preprints/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
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+)/reversion_preprint/$', views.PreprintReVersion.as_view(), name='re-version-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+)/fix_editing/$', views.PreprintFixEditing.as_view(), name='fix-editing'),
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'),
Expand Down
78 changes: 74 additions & 4 deletions admin/preprints/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from django.db import transaction
from django.db.models import F
from django.core.exceptions import PermissionDenied
from django.urls import NoReverseMatch
from django.http import HttpResponse, JsonResponse
from django.contrib import messages
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.shortcuts import redirect
Expand All @@ -10,7 +11,7 @@
FormView,
)
from django.utils import timezone
from django.urls import reverse_lazy
from django.urls import NoReverseMatch, reverse_lazy

from admin.base.views import GuidView
from admin.base.forms import GuidForm
Expand All @@ -19,9 +20,13 @@

from api.share.utils import update_share
from api.providers.workflows import Workflows
from api.preprints.serializers import PreprintSerializer

from osf.exceptions import PreprintStateError
from rest_framework.exceptions import PermissionDenied as DrfPermissionDenied
from framework.exceptions import PermissionsError

from osf.management.commands.fix_preprints_has_data_links_and_why_no_data import process_wrong_why_not_data_preprints
from osf.models import (
SpamStatus,
Preprint,
Expand All @@ -44,6 +49,7 @@
)
from osf.utils.workflows import DefaultStates
from website import search
from website.files.utils import copy_files
from website.preprints.tasks import on_preprint_updated


Expand All @@ -55,8 +61,8 @@ def get_object(self):
preprint.guid = preprint._id
return preprint

def get_success_url(self):
return reverse_lazy('preprints:preprint', kwargs={'guid': self.kwargs['guid']})
def get_success_url(self, guid=None):
return reverse_lazy('preprints:preprint', kwargs={'guid': guid or self.kwargs['guid']})


class PreprintView(PreprintMixin, GuidView):
Expand Down Expand Up @@ -182,6 +188,55 @@ def post(self, request, *args, **kwargs):
return redirect(self.get_success_url())


class PreprintReVersion(PreprintMixin, View):
"""Allows an authorized user to create new version 1 of a preprint based on earlier
primary file version(s). All operations are executed within an atomic transaction.
If any step fails, the entire transaction will be rolled back and no version will be changed.
"""
permission_required = 'osf.change_node'

def post(self, request, *args, **kwargs):
preprint = self.get_object()

file_versions = request.POST.getlist('file_versions')
if not file_versions:
return HttpResponse('At least one file version should be attached.', status=400)

try:
with transaction.atomic():
versions = preprint.get_preprint_versions()
for version in versions:
version.upgrade_version()

new_preprint, data_to_update = Preprint.create_version(
create_from_guid=preprint._id,
assign_version_number=1,
auth=request,
ignore_permission=True,
ignore_existing_versions=True,
)
data_to_update = data_to_update or dict()

primary_file = copy_files(preprint.primary_file, target_node=new_preprint, identifier__in=file_versions)
if primary_file is None:
raise ValueError(f"Primary file {preprint.primary_file.id} doesn't have following versions: {file_versions}") # rollback changes
data_to_update['primary_file'] = primary_file

# FIXME: currently it's not possible to ignore permission when update subjects
# via serializer, remove this logic if deprecated
subjects = data_to_update.pop('subjects', None)
if subjects:
new_preprint.set_subjects_from_relationships(subjects, auth=request, ignore_permission=True)

PreprintSerializer(new_preprint, context={'request': request, 'ignore_permission': True}).update(new_preprint, data_to_update)
except ValueError as exc:
return HttpResponse(str(exc), status=400)
except (PermissionsError, DrfPermissionDenied) as exc:
return HttpResponse(f'Not enough permissions to perform this action : {str(exc)}', status=400)

return JsonResponse({'redirect': self.get_success_url(new_preprint._id)})


class PreprintReindexElastic(PreprintMixin, View):
""" Allows an authorized user to reindex a node in ElasticSearch.
"""
Expand Down Expand Up @@ -525,6 +580,21 @@ def post(self, request, *args, **kwargs):

return redirect(self.get_success_url())

class PreprintFixEditing(PreprintMixin, View):
""" Allows an authorized user to manually fix why not data field.
"""
permission_required = 'osf.change_node'

def post(self, request, *args, **kwargs):
preprint = self.get_object()
process_wrong_why_not_data_preprints(
version_guid=preprint._id,
dry_run=False,
executing_through_command=False,
)

return redirect(self.get_success_url())


class PreprintMakePublic(PreprintMixin, View):
""" Allows an authorized user to manually make a private preprint public.
Expand Down
18 changes: 18 additions & 0 deletions admin/static/js/preprints/preprints.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
$(document).ready(function() {

$("#confirmReversion").on("submit", function (event) {
event.preventDefault();

$.ajax({
url: window.templateVars.reVersionPreprint,
type: "post",
data: $("#re-version-preprint-form").serialize(),
}).success(function (response) {
if (response.redirect) {
window.location.href = response.redirect;
}
}).fail(function (jqXHR, textStatus, error) {
$("#version-validation").text(jqXHR.responseText);
});
});
});
2 changes: 1 addition & 1 deletion admin/templates/nodes/node.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<a href="{% url 'nodes:search' %}" class="btn btn-primary"> <i class="fa fa-search"></i></a>
<a href="{% url 'nodes:node-logs' guid=node.guid %}" class="btn btn-primary">View Logs</a>
{% include "nodes/remove_node.html" with node=node %}
{% include "nodes/restart_stuck_registration.html" with node=node %}
{% include "nodes/registration_force_archive.html" with node=node %}
{% include "nodes/make_private.html" with node=node %}
{% include "nodes/make_public.html" with node=node %}
{% include "nodes/mark_spam.html" with node=node %}
Expand Down
Loading
Loading