Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions admin/nodes/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,7 @@
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'),
re_path(r'^(?P<guid>[a-z0-9]+)/update_permissions/$', views.NodeUpdatePermissionsView.as_view(), name='update-permissions'),
re_path(r'^(?P<guid>[a-z0-9]+)/remove_file/$', views.NodeRemoveFileView.as_view(), name='remove-file'),
re_path(r'^(?P<guid>[a-z0-9]+)/add_osfstorage_file/$', views.NodeAddOsfStorageFileView.as_view(), name='add-osfstorage-file'),
re_path(r'^(?P<guid>[a-z0-9]+)/remove_osfstorage_file/$', views.NodeRemoveOsfStorageFileView.as_view(), name='remove-osfstorage-file'),

]
54 changes: 54 additions & 0 deletions admin/nodes/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from admin.base.views import GuidView
from admin.nodes.forms import AddSystemTagForm, RegistrationDateForm
from admin.notifications.views import delete_selected_notifications
from addons.osfstorage.models import OsfStorageFolder
from api.caching.tasks import update_storage_usage_cache
from api.share.utils import update_share
from framework import status
Expand Down Expand Up @@ -846,6 +847,59 @@ def _remove_file_from_schema_response_blocks(registration, removed_file_id):
return redirect(self.get_success_url())


class NodeAddOsfStorageFileView(NodeMixin, View):
""" Allows an authorized user to add a file to osfstorage of an archived node.
"""
permission_required = 'osf.change_node'

def post(self, request, *args, **kwargs):
registration = self.get_object()
guid_id = request.POST.get('file-guid', '').strip()
guid = Guid.load(guid_id)
if not guid:
messages.error(request, 'No file found with the provided guid.')
return redirect(self.get_success_url())

file = guid.referent
parent_node = registration.registered_from
if not parent_node:
messages.error(request, 'The registration does not have the parent node.')
return redirect(self.get_success_url())

if not parent_node.files.filter(id=file.id).exists():
messages.error(request, 'The file with the provided guid is not part of the parent node.')
return redirect(self.get_success_url())

osfstorage = registration.get_addon('osfstorage')
# copy file to Archive of OSF Storage folder
archive_folder = OsfStorageFolder.objects.filter(
parent=osfstorage.get_root(),
name=osfstorage.archive_folder_name
).first()
file.copy_under(archive_folder)
messages.success(request, 'The file was successfully added.')
return redirect(self.get_success_url())


class NodeRemoveOsfStorageFileView(NodeMixin, View):
""" Allows an authorized user to remove a file from osfstorage of an archived node.
"""
permission_required = 'osf.change_node'

def post(self, request, *args, **kwargs):
registration = self.get_object()
guid_id = request.POST.get('file-guid', '').strip()
guid = Guid.load(guid_id)
if not guid:
messages.error(request, 'No file found with the provided guid.')
return redirect(self.get_success_url())

file = guid.referent
registration.files.filter(id=file.id).delete()
messages.success(request, 'The file was successfully removed.')
return redirect(self.get_success_url())


class RemoveStuckRegistrationsView(NodeMixin, View):
""" Allows an authorized user to remove a registrations if it's stuck in the archiving process.
"""
Expand Down
36 changes: 36 additions & 0 deletions admin/templates/nodes/add_file_to_osfstorage.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{% if node.is_registration and node.archived %}
<a data-toggle="modal" data-target="#confirmAddFileModal" class="btn btn-primary">
Add File (Osfstorage)
</a>
<div id="confirmAddFileModal" class="modal fade well" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<form class="well" method="post" action="{% url 'nodes:add-osfstorage-file' guid=node.guid %}">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">x</button>
<h3>Enter file to add</h3>
</div>
{% csrf_token %}

<div class="modal-body">
<div style="display:flex; align-items:center; gap:12px;">
<label for="file-guid" style="margin:0; white-space:nowrap;">File guid:</label>
<input id="file-guid"
type="text"
name="file-guid"
class="form-control"
required
style="flex:1; min-width:0;">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-danger" name="action" value="ham" type="submit">Confirm</button>
<button type="button" class="btn btn-default" data-dismiss="modal">
Cancel
</button>
</div>
</form>
</div>
</div>
</div>
{% endif %}
8 changes: 7 additions & 1 deletion admin/templates/nodes/node.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
<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/remove_file.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 %}
Expand All @@ -29,6 +28,13 @@
</div>
</div>
</div>
<div class="row">
<br>
<div class="col-md-12">
{% include "nodes/add_file_to_osfstorage.html" with node=node %}
{% include "nodes/remove_file_from_osfstorage.html" with node=node %}
</div>
</div>
<div class="row" style="overflow-x: auto; width: 100%;">
<h2>{{ node.type|cut:'osf.'|title }}: <b>{{ node.title }}</b> <a href="{{ node.absolute_url }}"> ({{node.guid}})</a> </h2>
<table class="table table-striped">
Expand Down
36 changes: 36 additions & 0 deletions admin/templates/nodes/remove_file_from_osfstorage.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{% if node.is_registration and node.archived %}
<a data-toggle="modal" data-target="#confirmRemoveFileModal" class="btn btn-danger">
Remove File (Osfstorage)
</a>
<div id="confirmRemoveFileModal" class="modal fade well" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<form class="well" method="post" action="{% url 'nodes:remove-osfstorage-file' guid=node.guid %}">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">x</button>
<h3>Enter file to remove</h3>
</div>
{% csrf_token %}

<div class="modal-body">
<div style="display:flex; align-items:center; gap:12px;">
<label for="file-guid" style="margin:0; white-space:nowrap;">File guid:</label>
<input id="file-guid"
type="text"
name="file-guid"
class="form-control"
required
style="flex:1; min-width:0;">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-danger" name="action" value="ham" type="submit">Confirm</button>
<button type="button" class="btn btn-default" data-dismiss="modal">
Cancel
</button>
</div>
</form>
</div>
</div>
</div>
{% endif %}
140 changes: 140 additions & 0 deletions admin_tests/nodes/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType

from addons.osfstorage.models import OsfStorageFile
from osf.models import (
AdminLogEntry,
NodeLog,
Expand All @@ -21,9 +22,11 @@
DraftRegistration,
)
from admin.nodes.views import (
NodeAddOsfStorageFileView,
NodeConfirmSpamView,
NodeDeleteView,
NodeRemoveContributorView,
NodeRemoveOsfStorageFileView,
NodeView,
NodeReindexShare,
NodeReindexElastic,
Expand All @@ -40,6 +43,7 @@
)
from admin_tests.utilities import setup_log_view, setup_view, handle_post_view_request
from api_tests.share._utils import mock_update_share
from osf.models.files import Folder
from tests.utils import capture_notifications
from website import settings
from framework.auth.core import Auth
Expand Down Expand Up @@ -905,3 +909,139 @@ def test_embargo_is_reset_after_reversion(self):
self.registration = self.no_moderation_draft.registered_node

assert self.registration.sanction is None


class TestOsfStorageRegistrationFileAdd(AdminTestCase):

def _create_file(self, instance, filename):
return OsfStorageFile.create(
target_object_id=instance.id,
target_content_type=ContentType.objects.get_for_model(instance),
path=f'/{filename}',
name=filename,
materialized_path=f'/{filename}'
)

@property
def _view(self):
return NodeAddOsfStorageFileView()

def check_message(self, expected_message):
assert expected_message == self.request._messages._queued_messages[0].message

def setUp(self):
super().setUp()
self.project = ProjectFactory()
self.project2 = ProjectFactory()
self.registration_registered_from = RegistrationFactory(project=self.project)
self.registration_without_registered_from = RegistrationFactory()
self.registration_without_registered_from.registered_from = None
self.registration_without_registered_from.save()

self.request = RequestFactory().get('/fake_path')
patch_messages(self.request)

def test_no_guid_found(self):
self.request.POST = {'file-guid': '1234'}
view = setup_log_view(self._view, self.request, guid=self.registration_registered_from._id)
view.post(self.request)
self.check_message('No file found with the provided guid.')

def test_no_parent_registration(self):
file = self._create_file(self.project, 'file.txt')
file.save()
file_guid = file.get_guid(create=True)
self.request.POST = {'file-guid': file_guid._id}
view = setup_log_view(self._view, self.request, guid=self.registration_without_registered_from._id)
view.post(self.request)
self.check_message('The registration does not have the parent node.')

def test_file_is_not_attached_to_parent(self):
file = self._create_file(self.project2, 'file.txt')
file.save()
file_guid = file.get_guid(create=True)
self.request.POST = {'file-guid': file_guid._id}
view = setup_log_view(self._view, self.request, guid=self.registration_registered_from._id)
view.post(self.request)
self.check_message('The file with the provided guid is not part of the parent node.')

def test_file_is_added_to_registration_osfstorage(self):
file = self._create_file(self.project, 'file.txt')
file.save()
file_guid = file.get_guid(create=True)
self.request.POST = {'file-guid': file_guid._id}
registration_osfstorage = self.registration_registered_from.get_addon('osfstorage')
# create archive folder for a registration
registration_osfstorage.get_root()._create_child(name=registration_osfstorage.archive_folder_name, kind=Folder)
view = setup_log_view(self._view, self.request, guid=self.registration_registered_from._id)
view.post(self.request)

# check that file is added to registration osfstorage under archive folder
assert registration_osfstorage.get_root().children.get(
name=registration_osfstorage.archive_folder_name
).children.filter(name=file.name).exists()


class TestOsfStorageRegistrationFileRemove(AdminTestCase):

def _create_file(self, instance, filename):
return OsfStorageFile.create(
target_object_id=instance.id,
target_content_type=ContentType.objects.get_for_model(instance),
path=f'/{filename}',
name=filename,
materialized_path=f'/{filename}'
)

@property
def _view(self):
return NodeRemoveOsfStorageFileView()

def check_message(self, expected_message):
assert expected_message == self.request._messages._queued_messages[0].message

def setUp(self):
super().setUp()
self.project = ProjectFactory()
self.registration_registered_from = RegistrationFactory(project=self.project)

self.request = RequestFactory().get('/fake_path')
patch_messages(self.request)

def test_no_guid_found(self):
self.request.POST = {'file-guid': '1234'}
view = setup_log_view(self._view, self.request, guid=self.registration_registered_from._id)
view.post(self.request)
self.check_message('No file found with the provided guid.')

def test_file_is_removed_from_registration_osfstorage(self):
file = self._create_file(self.project, 'file2.txt')
file.save()
file_guid = file.get_guid(create=True)

# create archive folder for a registration
registration_osfstorage = self.registration_registered_from.get_addon('osfstorage')
registration_osfstorage.get_root()._create_child(name=registration_osfstorage.archive_folder_name, kind=Folder)

# add file to osfstorage
self.request.POST = {'file-guid': file_guid._id}
view = setup_log_view(NodeAddOsfStorageFileView(), self.request, guid=self.registration_registered_from._id)
view.post(self.request)

# file exists in archive folder
assert registration_osfstorage.get_root().children.get(
name=registration_osfstorage.archive_folder_name
).children.filter(name=file.name).exists()
# file exists but with different guid
registration_file = self.registration_registered_from.files.get(name=file.name)
registration_file.get_guid(create=True)

# delete this file with different guid
self.request.POST = {'file-guid': registration_file.guids.first()._id}
view = setup_log_view(NodeRemoveOsfStorageFileView(), self.request, guid=self.registration_registered_from._id)
view.post(self.request)
# check that file is removed from registration osfstorage
assert not registration_osfstorage.get_root().children.get(
name=registration_osfstorage.archive_folder_name
).children.exists()
assert not self.registration_registered_from.files.exists()
Loading