Skip to content

Commit a1aa899

Browse files
authored
Merge pull request #11674 from ihorsokhanexoft/feature/ENG-10283
[ENG-10283] added ability to add/remove files of an archived registration under osf storage
2 parents 1448490 + 60b11cd commit a1aa899

6 files changed

Lines changed: 314 additions & 0 deletions

File tree

admin/nodes/urls.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,7 @@
5252
re_path(r'^(?P<guid>[a-z0-9]+)/system_tags/(?P<tag_id>[a-z0-9]+)/remove/$', views.NodeRemoveSystemTag.as_view(), name='remove-system-tag'),
5353
re_path(r'^(?P<guid>[a-z0-9]+)/update_permissions/$', views.NodeUpdatePermissionsView.as_view(), name='update-permissions'),
5454
re_path(r'^(?P<guid>[a-z0-9]+)/remove_file/$', views.NodeRemoveFileView.as_view(), name='remove-file'),
55+
re_path(r'^(?P<guid>[a-z0-9]+)/add_osfstorage_file/$', views.NodeAddOsfStorageFileView.as_view(), name='add-osfstorage-file'),
56+
re_path(r'^(?P<guid>[a-z0-9]+)/remove_osfstorage_file/$', views.NodeRemoveOsfStorageFileView.as_view(), name='remove-osfstorage-file'),
57+
5558
]

admin/nodes/views.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from admin.base.views import GuidView
2626
from admin.nodes.forms import AddSystemTagForm, RegistrationDateForm
2727
from admin.notifications.views import delete_selected_notifications
28+
from addons.osfstorage.models import OsfStorageFolder
2829
from api.caching.tasks import update_storage_usage_cache
2930
from api.share.utils import update_share
3031
from framework import status
@@ -53,6 +54,7 @@
5354
REINDEX_SHARE,
5455
REINDEX_ELASTIC,
5556
)
57+
from osf.models.files import File
5658
from osf.utils.permissions import ADMIN, API_CONTRIBUTOR_PERMISSIONS
5759
from scripts.approve_registrations import approve_past_pendings
5860
from website import settings, search
@@ -897,6 +899,72 @@ def _remove_file_from_schema_response_blocks(registration, removed_file_id):
897899
return redirect(self.get_success_url())
898900

899901

902+
class NodeAddOsfStorageFileView(NodeMixin, View):
903+
""" Allows an authorized user to add a file to osfstorage of an archived node.
904+
"""
905+
permission_required = 'osf.change_node'
906+
907+
def post(self, request, *args, **kwargs):
908+
registration = self.get_object()
909+
guid_id = request.POST.get('file-guid', '').strip()
910+
guid = Guid.load(guid_id)
911+
if not guid:
912+
messages.error(request, 'No file found with the provided guid.')
913+
return redirect(self.get_success_url())
914+
915+
file = guid.referent
916+
if not isinstance(file, File):
917+
messages.error(request, 'The guid provided does not correspond to a file.')
918+
return redirect(self.get_success_url())
919+
920+
parent_node = registration.registered_from
921+
if not parent_node:
922+
messages.error(request, 'The registration does not have the parent node.')
923+
return redirect(self.get_success_url())
924+
925+
if not parent_node.files.filter(id=file.id).exists():
926+
messages.error(request, 'The file with the provided guid is not part of the parent node.')
927+
return redirect(self.get_success_url())
928+
929+
osfstorage = registration.get_addon('osfstorage')
930+
# copy file to Archive of OSF Storage folder
931+
archive_folder = OsfStorageFolder.objects.filter(
932+
parent=osfstorage.get_root(),
933+
name=osfstorage.archive_folder_name
934+
).first()
935+
file.copy_under(archive_folder)
936+
messages.success(request, 'The file was successfully added.')
937+
return redirect(self.get_success_url())
938+
939+
940+
class NodeRemoveOsfStorageFileView(NodeMixin, View):
941+
""" Allows an authorized user to remove a file from osfstorage of an archived node.
942+
"""
943+
permission_required = 'osf.change_node'
944+
945+
def post(self, request, *args, **kwargs):
946+
registration = self.get_object()
947+
guid_id = request.POST.get('file-guid', '').strip()
948+
guid = Guid.load(guid_id)
949+
if not guid:
950+
messages.error(request, 'No file found with the provided guid.')
951+
return redirect(self.get_success_url())
952+
953+
file = guid.referent
954+
if not isinstance(file, File):
955+
messages.error(request, 'The guid provided does not correspond to a file.')
956+
return redirect(self.get_success_url())
957+
958+
registration_file = registration.files.filter(id=file.id)
959+
if not registration_file.exists():
960+
messages.error(request, 'The file with the provided guid is not part of the registration.')
961+
return redirect(self.get_success_url())
962+
963+
registration_file.delete()
964+
messages.success(request, 'The file was successfully removed.')
965+
return redirect(self.get_success_url())
966+
967+
900968
class RemoveStuckRegistrationsView(NodeMixin, View):
901969
""" Allows an authorized user to remove a registrations if it's stuck in the archiving process.
902970
"""
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{% if node.is_registration and node.archived %}
2+
<a data-toggle="modal" data-target="#confirmAddFileModal" class="btn btn-primary">
3+
Add File (Osfstorage)
4+
</a>
5+
<div id="confirmAddFileModal" class="modal fade well" tabindex="-1" role="dialog">
6+
<div class="modal-dialog" role="document">
7+
<div class="modal-content">
8+
<form class="well" method="post" action="{% url 'nodes:add-osfstorage-file' guid=node.guid %}">
9+
<div class="modal-header">
10+
<button type="button" class="close" data-dismiss="modal">x</button>
11+
<h3>Enter file to add</h3>
12+
</div>
13+
{% csrf_token %}
14+
15+
<div class="modal-body">
16+
<div style="display:flex; align-items:center; gap:12px;">
17+
<label for="file-guid" style="margin:0; white-space:nowrap;">File guid:</label>
18+
<input id="file-guid"
19+
type="text"
20+
name="file-guid"
21+
class="form-control"
22+
required
23+
style="flex:1; min-width:0;">
24+
</div>
25+
</div>
26+
<div class="modal-footer">
27+
<button class="btn btn-danger" name="action" type="submit">Confirm</button>
28+
<button type="button" class="btn btn-default" data-dismiss="modal">
29+
Cancel
30+
</button>
31+
</div>
32+
</form>
33+
</div>
34+
</div>
35+
</div>
36+
{% endif %}

admin/templates/nodes/node.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@
2929
</div>
3030
</div>
3131
</div>
32+
<div class="row">
33+
<br>
34+
<div class="col-md-12">
35+
{% include "nodes/add_file_to_osfstorage.html" with node=node %}
36+
{% include "nodes/remove_file_from_osfstorage.html" with node=node %}
37+
</div>
38+
</div>
3239
<div class="row" style="overflow-x: auto; width: 100%;">
3340
<h2>{{ node.type|cut:'osf.'|title }}: <b>{{ node.title }}</b> <a href="{{ node.absolute_url }}"> ({{node.guid}})</a> </h2>
3441
<table class="table table-striped">
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{% if node.is_registration and node.archived %}
2+
<a data-toggle="modal" data-target="#confirmRemoveFileModal" class="btn btn-danger">
3+
Remove File (Osfstorage)
4+
</a>
5+
<div id="confirmRemoveFileModal" class="modal fade well" tabindex="-1" role="dialog">
6+
<div class="modal-dialog" role="document">
7+
<div class="modal-content">
8+
<form class="well" method="post" action="{% url 'nodes:remove-osfstorage-file' guid=node.guid %}">
9+
<div class="modal-header">
10+
<button type="button" class="close" data-dismiss="modal">x</button>
11+
<h3>Enter file to remove</h3>
12+
</div>
13+
{% csrf_token %}
14+
15+
<div class="modal-body">
16+
<div style="display:flex; align-items:center; gap:12px;">
17+
<label for="file-guid" style="margin:0; white-space:nowrap;">File guid:</label>
18+
<input id="file-guid"
19+
type="text"
20+
name="file-guid"
21+
class="form-control"
22+
required
23+
style="flex:1; min-width:0;">
24+
</div>
25+
</div>
26+
<div class="modal-footer">
27+
<button class="btn btn-danger" name="action" type="submit">Confirm</button>
28+
<button type="button" class="btn btn-default" data-dismiss="modal">
29+
Cancel
30+
</button>
31+
</div>
32+
</form>
33+
</div>
34+
</div>
35+
</div>
36+
{% endif %}

admin_tests/nodes/test_views.py

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from django.contrib.auth.models import Permission
1212
from django.contrib.contenttypes.models import ContentType
1313

14+
from addons.osfstorage.models import OsfStorageFile
1415
from osf.models import (
1516
AdminLogEntry,
1617
NodeLog,
@@ -21,9 +22,11 @@
2122
DraftRegistration,
2223
)
2324
from admin.nodes.views import (
25+
NodeAddOsfStorageFileView,
2426
NodeConfirmSpamView,
2527
NodeDeleteView,
2628
NodeRemoveContributorView,
29+
NodeRemoveOsfStorageFileView,
2730
NodeView,
2831
NodeReindexShare,
2932
NodeReindexElastic,
@@ -40,6 +43,7 @@
4043
)
4144
from admin_tests.utilities import setup_log_view, setup_view, handle_post_view_request
4245
from api_tests.share._utils import mock_update_share
46+
from osf.models.files import Folder
4347
from tests.utils import capture_notifications
4448
from website import settings
4549
from framework.auth.core import Auth
@@ -905,3 +909,163 @@ def test_embargo_is_reset_after_reversion(self):
905909
self.registration = self.no_moderation_draft.registered_node
906910

907911
assert self.registration.sanction is None
912+
913+
914+
class TestOsfStorageRegistrationFileAdd(AdminTestCase):
915+
916+
def _create_file(self, instance, filename):
917+
return OsfStorageFile.create(
918+
target_object_id=instance.id,
919+
target_content_type=ContentType.objects.get_for_model(instance),
920+
path=f'/{filename}',
921+
name=filename,
922+
materialized_path=f'/{filename}'
923+
)
924+
925+
@property
926+
def _view(self):
927+
return NodeAddOsfStorageFileView()
928+
929+
def check_message(self, expected_message):
930+
assert expected_message == self.request._messages._queued_messages[0].message
931+
932+
def setUp(self):
933+
super().setUp()
934+
self.project = ProjectFactory()
935+
self.project2 = ProjectFactory()
936+
self.registration_registered_from = RegistrationFactory(project=self.project)
937+
self.registration_without_registered_from = RegistrationFactory()
938+
self.registration_without_registered_from.registered_from = None
939+
self.registration_without_registered_from.save()
940+
941+
self.request = RequestFactory().get('/fake_path')
942+
patch_messages(self.request)
943+
944+
def test_no_guid_found(self):
945+
self.request.POST = {'file-guid': '1234'}
946+
view = setup_log_view(self._view, self.request, guid=self.registration_registered_from._id)
947+
view.post(self.request)
948+
self.check_message('No file found with the provided guid.')
949+
950+
def test_guid_is_not_file(self):
951+
project = ProjectFactory()
952+
self.request.POST = {'file-guid': project._id}
953+
view = setup_log_view(self._view, self.request, guid=self.registration_registered_from._id)
954+
view.post(self.request)
955+
self.check_message('The guid provided does not correspond to a file.')
956+
957+
def test_no_parent_registration(self):
958+
file = self._create_file(self.project, 'file.txt')
959+
file.save()
960+
file_guid = file.get_guid(create=True)
961+
self.request.POST = {'file-guid': file_guid._id}
962+
view = setup_log_view(self._view, self.request, guid=self.registration_without_registered_from._id)
963+
view.post(self.request)
964+
self.check_message('The registration does not have the parent node.')
965+
966+
def test_file_is_not_attached_to_parent(self):
967+
file = self._create_file(self.project2, 'file.txt')
968+
file.save()
969+
file_guid = file.get_guid(create=True)
970+
self.request.POST = {'file-guid': file_guid._id}
971+
view = setup_log_view(self._view, self.request, guid=self.registration_registered_from._id)
972+
view.post(self.request)
973+
self.check_message('The file with the provided guid is not part of the parent node.')
974+
975+
def test_file_is_added_to_registration_osfstorage(self):
976+
file = self._create_file(self.project, 'file.txt')
977+
file.save()
978+
file_guid = file.get_guid(create=True)
979+
self.request.POST = {'file-guid': file_guid._id}
980+
registration_osfstorage = self.registration_registered_from.get_addon('osfstorage')
981+
# create archive folder for a registration
982+
registration_osfstorage.get_root()._create_child(name=registration_osfstorage.archive_folder_name, kind=Folder)
983+
view = setup_log_view(self._view, self.request, guid=self.registration_registered_from._id)
984+
view.post(self.request)
985+
986+
# check that file is added to registration osfstorage under archive folder
987+
assert registration_osfstorage.get_root().children.get(
988+
name=registration_osfstorage.archive_folder_name
989+
).children.filter(name=file.name).exists()
990+
991+
992+
class TestOsfStorageRegistrationFileRemove(AdminTestCase):
993+
994+
def _create_file(self, instance, filename):
995+
return OsfStorageFile.create(
996+
target_object_id=instance.id,
997+
target_content_type=ContentType.objects.get_for_model(instance),
998+
path=f'/{filename}',
999+
name=filename,
1000+
materialized_path=f'/{filename}'
1001+
)
1002+
1003+
@property
1004+
def _view(self):
1005+
return NodeRemoveOsfStorageFileView()
1006+
1007+
def check_message(self, expected_message):
1008+
assert expected_message == self.request._messages._queued_messages[0].message
1009+
1010+
def setUp(self):
1011+
super().setUp()
1012+
self.project = ProjectFactory()
1013+
self.registration_registered_from = RegistrationFactory(project=self.project)
1014+
1015+
self.request = RequestFactory().get('/fake_path')
1016+
patch_messages(self.request)
1017+
1018+
def test_no_guid_found(self):
1019+
self.request.POST = {'file-guid': '1234'}
1020+
view = setup_log_view(self._view, self.request, guid=self.registration_registered_from._id)
1021+
view.post(self.request)
1022+
self.check_message('No file found with the provided guid.')
1023+
1024+
def test_guid_is_not_file(self):
1025+
project = ProjectFactory()
1026+
self.request.POST = {'file-guid': project._id}
1027+
view = setup_log_view(self._view, self.request, guid=self.registration_registered_from._id)
1028+
view.post(self.request)
1029+
self.check_message('The guid provided does not correspond to a file.')
1030+
1031+
def test_file_not_attached_to_registration(self):
1032+
file = self._create_file(self.project, 'file2.txt')
1033+
file.save()
1034+
file_guid = file.get_guid(create=True)
1035+
1036+
self.request.POST = {'file-guid': file_guid._id}
1037+
view = setup_log_view(self._view, self.request, guid=self.registration_registered_from._id)
1038+
view.post(self.request)
1039+
self.check_message('The file with the provided guid is not part of the registration.')
1040+
1041+
def test_file_is_removed_from_registration_osfstorage(self):
1042+
file = self._create_file(self.project, 'file2.txt')
1043+
file.save()
1044+
file_guid = file.get_guid(create=True)
1045+
1046+
# create archive folder for a registration
1047+
registration_osfstorage = self.registration_registered_from.get_addon('osfstorage')
1048+
registration_osfstorage.get_root()._create_child(name=registration_osfstorage.archive_folder_name, kind=Folder)
1049+
1050+
# add file to osfstorage
1051+
self.request.POST = {'file-guid': file_guid._id}
1052+
view = setup_log_view(NodeAddOsfStorageFileView(), self.request, guid=self.registration_registered_from._id)
1053+
view.post(self.request)
1054+
1055+
# file exists in archive folder
1056+
assert registration_osfstorage.get_root().children.get(
1057+
name=registration_osfstorage.archive_folder_name
1058+
).children.filter(name=file.name).exists()
1059+
# file exists but with different guid
1060+
registration_file = self.registration_registered_from.files.get(name=file.name)
1061+
registration_file.get_guid(create=True)
1062+
1063+
# delete this file with different guid
1064+
self.request.POST = {'file-guid': registration_file.guids.first()._id}
1065+
view = setup_log_view(NodeRemoveOsfStorageFileView(), self.request, guid=self.registration_registered_from._id)
1066+
view.post(self.request)
1067+
# check that file is removed from registration osfstorage
1068+
assert not registration_osfstorage.get_root().children.get(
1069+
name=registration_osfstorage.archive_folder_name
1070+
).children.exists()
1071+
assert not self.registration_registered_from.files.exists()

0 commit comments

Comments
 (0)