Skip to content

Commit 136e39a

Browse files
committed
Merge branch 'release/25.08.0'
2 parents 494df5b + 098d7f0 commit 136e39a

Some content is hidden

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

55 files changed

+1534
-1137
lines changed

.docker-compose.gv.env

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
ALLOWED_HOSTS="localhost,192.168.168.167"
1+
ALLOWED_HOSTS="localhost,192.168.168.167,127.0.0.1"
22
CORS_ALLOWED_ORIGINS="http://localhost:5000,http://192.168.168.167:5000"
33
OSFDB_HOST=192.168.168.167
44
POSTGRES_HOST="$OSFDB_HOST"
@@ -7,5 +7,10 @@ OSF_API_BASE_URL="http://192.168.168.167:8000"
77
POSTGRES_USER="postgres"
88
POSTGRES_DB="gravyvalet"
99
SECRET_KEY="secret"
10-
PYTHONUNBUFFERED=0 # This when set to 0 will allow print statements to be visible in the Docker logs
10+
PYTHONUNBUFFERED=1 # This when set to 0 will allow print statements to be visible in the Docker logs
11+
OSF_AUTH_COOKIE_NAME=osf
12+
OSF_SENSITIVE_DATA_SECRET="TrainglesAre5Squares"
13+
OSF_SENSITIVE_DATA_SALT="yusaltydough"
14+
DEBUG=1
15+
1116

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.08.0 (2025-05-02)
6+
====================
7+
8+
- Bugfix and Improvements
9+
10+
25.07.0 (2025-04-21)
11+
====================
12+
13+
- Bugfix and Improvements
14+
515
25.06.0 (2025-04-07)
616
====================
717

addons/base/views.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -431,11 +431,7 @@ def _enqueue_metrics(file_version, file_node, action, auth, from_mfr=False):
431431
def _construct_payload(auth, resource, credentials, waterbutler_settings):
432432

433433
if isinstance(resource, Registration):
434-
callback_url = resource.api_url_for(
435-
'registration_callbacks',
436-
_absolute=True,
437-
_internal=True
438-
)
434+
callback_url = resource.callbacks_url
439435
else:
440436
callback_url = resource.api_url_for(
441437
'create_waterbutler_log',

admin/nodes/urls.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,12 @@
2727
re_path(r'^(?P<guid>[a-z0-9]+)/reindex_share_node/$', views.NodeReindexShare.as_view(), name='reindex-share-node'),
2828
re_path(r'^(?P<guid>[a-z0-9]+)/reindex_elastic_node/$', views.NodeReindexElastic.as_view(),
2929
name='reindex-elastic-node'),
30-
re_path(r'^(?P<guid>[a-z0-9]+)/restart_stuck_registrations/$', views.RestartStuckRegistrationsView.as_view(),
31-
name='restart-stuck-registrations'),
3230
re_path(r'^(?P<guid>[a-z0-9]+)/remove_stuck_registrations/$', views.RemoveStuckRegistrationsView.as_view(),
3331
name='remove-stuck-registrations'),
32+
re_path(r'^(?P<guid>[a-z0-9]+)/check_archive_status/$', views.CheckArchiveStatusRegistrationsView.as_view(),
33+
name='check-archive-status'),
34+
re_path(r'^(?P<guid>[a-z0-9]+)/force_archive_registration/$', views.ForceArchiveRegistrationsView.as_view(),
35+
name='force-archive-registration'),
3436
re_path(r'^(?P<guid>[a-z0-9]+)/remove_user/(?P<user_id>[a-z0-9]+)/$', views.NodeRemoveContributorView.as_view(),
3537
name='remove-user'),
3638
re_path(r'^(?P<guid>[a-z0-9]+)/modify_storage_usage/$', views.NodeModifyStorageUsage.as_view(),

admin/nodes/views.py

Lines changed: 76 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import pytz
2+
from enum import Enum
23
from datetime import datetime
34
from framework import status
45

@@ -26,7 +27,7 @@
2627
from api.share.utils import update_share
2728
from api.caching.tasks import update_storage_usage_cache
2829

29-
from osf.exceptions import NodeStateError
30+
from osf.exceptions import NodeStateError, RegistrationStuckError
3031
from osf.models import (
3132
OSFUser,
3233
NodeLog,
@@ -672,44 +673,97 @@ def post(self, request, *args, **kwargs):
672673
return redirect(self.get_success_url())
673674

674675

675-
class RestartStuckRegistrationsView(NodeMixin, TemplateView):
676-
""" Allows an authorized user to restart a registrations archive process.
676+
class RemoveStuckRegistrationsView(NodeMixin, View):
677+
""" Allows an authorized user to remove a registrations if it's stuck in the archiving process.
677678
"""
678-
template_name = 'nodes/restart_registrations_modal.html'
679679
permission_required = ('osf.view_node', 'osf.change_node')
680680

681681
def post(self, request, *args, **kwargs):
682-
# Prevents circular imports that cause admin app to hang at startup
683-
from osf.management.commands.force_archive import archive, verify
684682
stuck_reg = self.get_object()
685-
if verify(stuck_reg):
686-
try:
687-
archive(stuck_reg)
688-
messages.success(request, 'Registration archive processes has restarted')
689-
except Exception as exc:
690-
messages.error(request, f'This registration cannot be unstuck due to {exc.__class__.__name__} '
691-
f'if the problem persists get a developer to fix it.')
683+
if Registration.find_failed_registrations().filter(id=stuck_reg.id).exists():
684+
stuck_reg.delete_registration_tree(save=True)
685+
messages.success(request, 'The registration has been deleted')
692686
else:
693687
messages.error(request, 'This registration may not technically be stuck,'
694688
' if the problem persists get a developer to fix it.')
695689

696690
return redirect(self.get_success_url())
697691

698692

699-
class RemoveStuckRegistrationsView(NodeMixin, TemplateView):
700-
""" Allows an authorized user to remove a registrations if it's stuck in the archiving process.
693+
class CheckArchiveStatusRegistrationsView(NodeMixin, View):
694+
"""Allows an authorized user to check a registration archive status.
695+
"""
696+
permission_required = ('osf.view_node', 'osf.change_node')
697+
698+
def get(self, request, *args, **kwargs):
699+
# Prevents circular imports that cause admin app to hang at startup
700+
from osf.management.commands.force_archive import check
701+
702+
registration = self.get_object()
703+
704+
if registration.archived:
705+
messages.success(request, f"Registration {registration._id} is archived.")
706+
return redirect(self.get_success_url())
707+
708+
try:
709+
archive_status = check(registration)
710+
messages.success(request, archive_status)
711+
except RegistrationStuckError as exc:
712+
messages.error(request, str(exc))
713+
714+
return redirect(self.get_success_url())
715+
716+
717+
class CollisionMode(Enum):
718+
NONE: str = 'none'
719+
SKIP: str = 'skip'
720+
DELETE: str = 'delete'
721+
722+
723+
class ForceArchiveRegistrationsView(NodeMixin, View):
724+
"""Allows an authorized user to force archive registration.
701725
"""
702-
template_name = 'nodes/remove_registrations_modal.html'
703726
permission_required = ('osf.view_node', 'osf.change_node')
704727

705728
def post(self, request, *args, **kwargs):
706-
stuck_reg = self.get_object()
707-
if Registration.find_failed_registrations().filter(id=stuck_reg.id).exists():
708-
stuck_reg.delete_registration_tree(save=True)
709-
messages.success(request, 'The registration has been deleted')
729+
# Prevents circular imports that cause admin app to hang at startup
730+
from osf.management.commands.force_archive import verify, archive, DEFAULT_PERMISSIBLE_ADDONS
731+
732+
registration = self.get_object()
733+
force_archive_params = request.POST
734+
735+
collision_mode = force_archive_params.get('collision_mode', CollisionMode.NONE.value)
736+
delete_collision = CollisionMode.DELETE.value == collision_mode
737+
skip_collision = CollisionMode.SKIP.value == collision_mode
738+
739+
allow_unconfigured = force_archive_params.get('allow_unconfigured', False)
740+
741+
addons = set(force_archive_params.getlist('addons', []))
742+
addons.update(DEFAULT_PERMISSIBLE_ADDONS)
743+
744+
try:
745+
verify(registration, permissible_addons=addons, raise_error=True)
746+
except ValidationError as exc:
747+
messages.error(request, str(exc))
748+
return redirect(self.get_success_url())
749+
750+
dry_mode = force_archive_params.get('dry_mode', False)
751+
752+
if dry_mode:
753+
messages.success(request, f"Registration {registration._id} can be archived.")
710754
else:
711-
messages.error(request, 'This registration may not technically be stuck,'
712-
' if the problem persists get a developer to fix it.')
755+
try:
756+
archive(
757+
registration,
758+
permissible_addons=addons,
759+
allow_unconfigured=allow_unconfigured,
760+
skip_collision=skip_collision,
761+
delete_collision=delete_collision,
762+
)
763+
messages.success(request, 'Registration archive process has finished.')
764+
except Exception as exc:
765+
messages.error(request, f'This registration cannot be archived due to {exc.__class__.__name__}: {str(exc)}. '
766+
f'If the problem persists get a developer to fix it.')
713767

714768
return redirect(self.get_success_url())
715769

admin/preprints/urls.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@
1313
re_path(r'^(?P<guid>\w+)/change_provider/$', views.PreprintProviderChangeView.as_view(), name='preprint-provider'),
1414
re_path(r'^(?P<guid>\w+)/machine_state/$', views.PreprintMachineStateView.as_view(), name='preprint-machine-state'),
1515
re_path(r'^(?P<guid>\w+)/reindex_share_preprint/$', views.PreprintReindexShare.as_view(), name='reindex-share-preprint'),
16+
re_path(r'^(?P<guid>\w+)/reversion_preprint/$', views.PreprintReVersion.as_view(), name='re-version-preprint'),
1617
re_path(r'^(?P<guid>\w+)/remove_user/(?P<user_id>[a-z0-9]+)/$', views.PreprintRemoveContributorView.as_view(), name='remove-user'),
1718
re_path(r'^(?P<guid>\w+)/make_private/$', views.PreprintMakePrivate.as_view(), name='make-private'),
19+
re_path(r'^(?P<guid>\w+)/fix_editing/$', views.PreprintFixEditing.as_view(), name='fix-editing'),
1820
re_path(r'^(?P<guid>\w+)/make_public/$', views.PreprintMakePublic.as_view(), name='make-public'),
1921
re_path(r'^(?P<guid>\w+)/remove/$', views.PreprintDeleteView.as_view(), name='remove'),
2022
re_path(r'^(?P<guid>\w+)/restore/$', views.PreprintDeleteView.as_view(), name='restore'),

admin/preprints/views.py

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
from django.db import transaction
12
from django.db.models import F
23
from django.core.exceptions import PermissionDenied
3-
from django.urls import NoReverseMatch
4+
from django.http import HttpResponse, JsonResponse
45
from django.contrib import messages
56
from django.contrib.auth.mixins import PermissionRequiredMixin
67
from django.shortcuts import redirect
@@ -10,7 +11,7 @@
1011
FormView,
1112
)
1213
from django.utils import timezone
13-
from django.urls import reverse_lazy
14+
from django.urls import NoReverseMatch, reverse_lazy
1415

1516
from admin.base.views import GuidView
1617
from admin.base.forms import GuidForm
@@ -19,9 +20,13 @@
1920

2021
from api.share.utils import update_share
2122
from api.providers.workflows import Workflows
23+
from api.preprints.serializers import PreprintSerializer
2224

2325
from osf.exceptions import PreprintStateError
26+
from rest_framework.exceptions import PermissionDenied as DrfPermissionDenied
27+
from framework.exceptions import PermissionsError
2428

29+
from osf.management.commands.fix_preprints_has_data_links_and_why_no_data import process_wrong_why_not_data_preprints
2530
from osf.models import (
2631
SpamStatus,
2732
Preprint,
@@ -44,6 +49,7 @@
4449
)
4550
from osf.utils.workflows import DefaultStates
4651
from website import search
52+
from website.files.utils import copy_files
4753
from website.preprints.tasks import on_preprint_updated
4854

4955

@@ -55,8 +61,8 @@ def get_object(self):
5561
preprint.guid = preprint._id
5662
return preprint
5763

58-
def get_success_url(self):
59-
return reverse_lazy('preprints:preprint', kwargs={'guid': self.kwargs['guid']})
64+
def get_success_url(self, guid=None):
65+
return reverse_lazy('preprints:preprint', kwargs={'guid': guid or self.kwargs['guid']})
6066

6167

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

184190

191+
class PreprintReVersion(PreprintMixin, View):
192+
"""Allows an authorized user to create new version 1 of a preprint based on earlier
193+
primary file version(s). All operations are executed within an atomic transaction.
194+
If any step fails, the entire transaction will be rolled back and no version will be changed.
195+
"""
196+
permission_required = 'osf.change_node'
197+
198+
def post(self, request, *args, **kwargs):
199+
preprint = self.get_object()
200+
201+
file_versions = request.POST.getlist('file_versions')
202+
if not file_versions:
203+
return HttpResponse('At least one file version should be attached.', status=400)
204+
205+
try:
206+
with transaction.atomic():
207+
versions = preprint.get_preprint_versions()
208+
for version in versions:
209+
version.upgrade_version()
210+
211+
new_preprint, data_to_update = Preprint.create_version(
212+
create_from_guid=preprint._id,
213+
assign_version_number=1,
214+
auth=request,
215+
ignore_permission=True,
216+
ignore_existing_versions=True,
217+
)
218+
data_to_update = data_to_update or dict()
219+
220+
primary_file = copy_files(preprint.primary_file, target_node=new_preprint, identifier__in=file_versions)
221+
if primary_file is None:
222+
raise ValueError(f"Primary file {preprint.primary_file.id} doesn't have following versions: {file_versions}") # rollback changes
223+
data_to_update['primary_file'] = primary_file
224+
225+
# FIXME: currently it's not possible to ignore permission when update subjects
226+
# via serializer, remove this logic if deprecated
227+
subjects = data_to_update.pop('subjects', None)
228+
if subjects:
229+
new_preprint.set_subjects_from_relationships(subjects, auth=request, ignore_permission=True)
230+
231+
PreprintSerializer(new_preprint, context={'request': request, 'ignore_permission': True}).update(new_preprint, data_to_update)
232+
except ValueError as exc:
233+
return HttpResponse(str(exc), status=400)
234+
except (PermissionsError, DrfPermissionDenied) as exc:
235+
return HttpResponse(f'Not enough permissions to perform this action : {str(exc)}', status=400)
236+
237+
return JsonResponse({'redirect': self.get_success_url(new_preprint._id)})
238+
239+
185240
class PreprintReindexElastic(PreprintMixin, View):
186241
""" Allows an authorized user to reindex a node in ElasticSearch.
187242
"""
@@ -525,6 +580,21 @@ def post(self, request, *args, **kwargs):
525580

526581
return redirect(self.get_success_url())
527582

583+
class PreprintFixEditing(PreprintMixin, View):
584+
""" Allows an authorized user to manually fix why not data field.
585+
"""
586+
permission_required = 'osf.change_node'
587+
588+
def post(self, request, *args, **kwargs):
589+
preprint = self.get_object()
590+
process_wrong_why_not_data_preprints(
591+
version_guid=preprint._id,
592+
dry_run=False,
593+
executing_through_command=False,
594+
)
595+
596+
return redirect(self.get_success_url())
597+
528598

529599
class PreprintMakePublic(PreprintMixin, View):
530600
""" Allows an authorized user to manually make a private preprint public.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
$(document).ready(function() {
2+
3+
$("#confirmReversion").on("submit", function (event) {
4+
event.preventDefault();
5+
6+
$.ajax({
7+
url: window.templateVars.reVersionPreprint,
8+
type: "post",
9+
data: $("#re-version-preprint-form").serialize(),
10+
}).success(function (response) {
11+
if (response.redirect) {
12+
window.location.href = response.redirect;
13+
}
14+
}).fail(function (jqXHR, textStatus, error) {
15+
$("#version-validation").text(jqXHR.responseText);
16+
});
17+
});
18+
});

admin/templates/nodes/node.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
<a href="{% url 'nodes:search' %}" class="btn btn-primary"> <i class="fa fa-search"></i></a>
1818
<a href="{% url 'nodes:node-logs' guid=node.guid %}" class="btn btn-primary">View Logs</a>
1919
{% include "nodes/remove_node.html" with node=node %}
20-
{% include "nodes/restart_stuck_registration.html" with node=node %}
20+
{% include "nodes/registration_force_archive.html" with node=node %}
2121
{% include "nodes/make_private.html" with node=node %}
2222
{% include "nodes/make_public.html" with node=node %}
2323
{% include "nodes/mark_spam.html" with node=node %}

0 commit comments

Comments
 (0)