Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 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
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
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