Skip to content
Merged
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
16 changes: 16 additions & 0 deletions addons/osfstorage/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,22 @@ def test_delete_root_node(self):
assert BaseFileNode.objects.get(_id=folder._id).type == 'osf.trashedfolder'
assert BaseFileNode.objects.get(_id=file._id).type == 'osf.trashedfile'

def test_restore_deleted_file_without_deleted_field(self):
assert models.TrashedFileNode.objects.exists() is False

child = self.node_settings.get_root().append_file('Test')
child.delete()

trashed_file = models.TrashedFileNode.objects.first()
restored_file = trashed_file.restore()

assert restored_file.deleted is None
assert restored_file.deleted_on is None
# None because we do not set deleted_by when delete the child
assert restored_file.deleted_by is None

assert models.TrashedFileNode.objects.exists() is False

def test_delete_file(self):
child = self.node_settings.get_root().append_file('Test')
field_names = [f.name for f in child._meta.get_fields() if not f.is_relation and f.name not in ['id', 'content_type_pk']]
Expand Down
2 changes: 2 additions & 0 deletions admin/management/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,6 @@
name='remove_orcid_from_user_social'),
re_path(r'^migrate_funder_names_to_ror', views.MigrateFunderNamesToRor.as_view(),
name='migrate_funder_names_to_ror'),
re_path(r'^fix_restored_trashed_files', views.FixRestoredTrashedFiles.as_view(),
name='fix_restored_trashed_files'),
]
8 changes: 8 additions & 0 deletions admin/management/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,3 +228,11 @@ def post(self, request):
for _line in _out_io.getvalue().split('\n'):
messages.info(request, _line)
return redirect(reverse('management:commands'))


class FixRestoredTrashedFiles(ManagementCommandPermissionView):

def post(self, request):
call_command('fix_restored_trashed_files')
messages.success(request, 'Restored trashed files have been successfully fixed.')
return redirect(reverse('management:commands'))
13 changes: 13 additions & 0 deletions admin/templates/management/commands.html
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,19 @@ <h4><u>Update ROR funder names to be consistent and proper.</u></h4>
</nav>
</form>
</section>
<section>
<h4><u>Fix restored files</u></h4>
<p>
Use this management command to fix restored files that were previously trashed.
</p>
<form method="post"
action="{% url 'management:fix_restored_trashed_files'%}">
{% csrf_token %}
<nav>
<input class="btn btn-success" type="submit" value="Run" />
</nav>
</form>
</section>
</div>
</section>
{% endblock %}
41 changes: 41 additions & 0 deletions osf/management/commands/fix_restored_trashed_files.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""
Clears deleted field value for all restored OsfStorageFileNode objects
as restore() method did not remove it and such files after restore from TrashedFileNode are not shown on UI
"""
import logging

from django.db import transaction
from django.core.management.base import BaseCommand

from addons.osfstorage.models import OsfStorageFileNode


logger = logging.getLogger(__name__)

class Command(BaseCommand):

def add_arguments(self, parser):
parser.add_argument(
'--dry-run',
action='store_true',
help='Run without making changes',
)

def handle(self, *args, **options):
files_to_fix = OsfStorageFileNode.objects.filter(deleted__isnull=False)
file_ids = list(files_to_fix.values_list('_id', flat=True))
dry_run = options.get('dry_run', False)

if dry_run:
logger.info(f'Running in dry-run mode, the following files would be fixed: {file_ids}')
self.stdout.write(f'Running in dry-run mode, the following files would be fixed: {file_ids}')
return

with transaction.atomic():
for file in files_to_fix:
file.deleted = None

OsfStorageFileNode.objects.bulk_update(files_to_fix, ['deleted'], batch_size=1000)

logger.info(f'The following files have been fixed: {file_ids}')
self.stdout.write(f'The following files have been fixed: {file_ids}')
5 changes: 4 additions & 1 deletion osf/models/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,9 @@ def restore(self, recursive=True, parent=None, save=True, deleted_on=None, clien

type_cls = File if self.is_file else Folder

self.deleted = None
self.deleted_on = None

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's set self.deleted_on to None here. And then in the TrashedFolder's restore, we cache the deleted_on before the call to super().restore() and then use that for child lookups.

self.recast(self._resolve_class(type_cls)._typedmodels_type)

if save:
Expand Down Expand Up @@ -757,10 +760,10 @@ def restore(self, recursive=True, parent=None, save=True, deleted_on=None):
:param deleted_on:
:return:
"""
deleted_on = deleted_on or self.deleted_on
tf = super().restore(recursive=True, parent=None, save=True, deleted_on=None)

if not self.is_file and recursive:
deleted_on = deleted_on or self.deleted_on
for child in TrashedFileNode.objects.filter(parent=self.id, deleted_on=deleted_on):
child.restore(recursive=True, save=save, deleted_on=deleted_on)
return tf
Expand Down
Loading