Repository: hydroshare/hydroshare

Issue:
Extra precautions to confirm a user intends to delete a resource
The last month I've had two support issues investigating a missing resource only to discover the user had unknowingly deleted their resource.

This is up for discussion if others would like to voice opinions and what measures should be taken.  I think we should simply add a textbox that forces the user to type in DELETE before clicking the ok button when deleting a resource. This should be updated both on the resource landing page as well as the my-resources page (for bulk delete).


Discussion:
I know this happens - as I think it happened to one of my students. However, I'm not sure what can be done to improve the dialogs that pop up. If you click the trash can icon on the resource landing page to delete the resource, you get this:

![image](https://user-images.githubusercontent.com/3485902/84806895-a3371100-afc3-11ea-96b6-1b31c04828c6.png)

If you select the resource on the My Resources page and then click the trash can icon, you get this:

![image](https://user-images.githubusercontent.com/3485902/84806960-bf3ab280-afc3-11ea-9e64-e746c677de1a.png)

It seems like the messaging is pretty clear, so I'm not sure how people are missing this. I've used other systems that have multiple dialogs that pop up, with the second one being the "Are you sure you are sure?" dialog. I'm not sure about asking the user to type in "DELETE".
I suggested typing DELETE or something to make sure the user reads the warning before pressing confirm.  I think it may be similar enough to deleting a file that people just click confirm without realizing they pressed the wrong button.  The 2 recent examples where a user accidentally deleted their resources were done while the users were heavily modifying their resources.  

There is precedence to forcing a user to type when permanently deleting something, Github and other services have this practice.
Looking at other options for design here, your suggestion of typing "DELETE" seems to be pretty universally recognized and would be the simplest to implement of any of the other options I saw (e.g., creating a recoverable trash folder, creating undo functionality, hiding rather than deleting content, etc.). I could get behind this.
I was about to post a new issue but luckily found this one. We live in a forgiving virtual environment where they're always ways to "recover" deleted files at least for a period of time. It's like a standard these days. So users expect this from HydroShare as well. I just deleted a resource that had a nice abstract. I wish I could recover it to copy that abstract :(

The message is clear here but supporting file recovery in addition to it would be a huge plus.

![image](https://user-images.githubusercontent.com/3268971/103491296-bb477400-4ddf-11eb-9b85-e91b03513ad2.png)


Resolving patch:
diff --git a/hs_core/urls.py b/hs_core/urls.py
--- a/hs_core/urls.py
+++ b/hs_core/urls.py
@@ -26,7 +26,7 @@
         views.delete_file, name='delete_file'),
     url(r'^_internal/(?P<shortkey>[A-z0-9]+)/delete-multiple-files/$',
         views.delete_multiple_files, name='delete_multiple_files'),
-    url(r'^_internal/(?P<shortkey>[0-9a-f-]+)/delete-resource/$',
+    url(r'^_internal/(?P<shortkey>[0-9a-f-]+)/delete-resource/(?P<usertext>[A-z]+)/$',
         views.delete_resource, name='delete_resource'),
     url(r'^_internal/(?P<shortkey>[0-9a-f-]+)/create-new-version-resource/$',
         views.create_new_version_resource, name='create_resource_version'),
diff --git a/hs_core/views/__init__.py b/hs_core/views/__init__.py
--- a/hs_core/views/__init__.py
+++ b/hs_core/views/__init__.py
@@ -733,11 +733,14 @@ def delete_multiple_files(request, shortkey, *args, **kwargs):
     return HttpResponseRedirect(request.META['HTTP_REFERER'])
 
 
-def delete_resource(request, shortkey, *args, **kwargs):
+def delete_resource(request, shortkey, usertext, *args, **kwargs):
     res, _, user = authorize(request, shortkey, needed_permission=ACTION_TO_AUTHORIZE.DELETE_RESOURCE)
+    if usertext != "DELETE":
+        return HttpResponse("'usertext' path parameter must be provided with value 'DELETE'",
+                            status=status.HTTP_400_BAD_REQUEST)
     if res.metadata.relations.all().filter(type='isReplacedBy').exists():
-        raise ValidationError('An obsoleted resource in the middle of the obsolescence chain '
-                              'cannot be deleted.')
+        return HttpResponse('An obsoleted resource in the middle of the obsolescence chain cannot be deleted.',
+                            status=status.HTTP_400_BAD_REQUEST)
     if request.is_ajax():
         task_id = get_resource_delete_task(shortkey)
         if not task_id:


Test patch:
diff --git a/hs_collection_resource/tests/test_collection.py b/hs_collection_resource/tests/test_collection.py
--- a/hs_collection_resource/tests/test_collection.py
+++ b/hs_collection_resource/tests/test_collection.py
@@ -116,7 +116,7 @@ def setUp(self):
         self.url_to_update_collection = base_url + "/update-collection/"
         self.url_to_collection_member_permission = base_url + "/collection-member-permission/{1}/"
         self.url_to_set_resource_flag = base_url + "/set-resource-flag/"
-        self.url_to_delete_resource = base_url + "/delete-resource/"
+        self.url_to_delete_resource = base_url + "/delete-resource/DELETE/"
         self.url_to_update_collection_for_deleted_resources = \
             base_url + "/update-collection-for-deleted-resources/"
         self.url_to_calculate_collection_coverages = \
diff --git a/hs_core/tests/api/views/test_delete_resource.py b/hs_core/tests/api/views/test_delete_resource.py
--- a/hs_core/tests/api/views/test_delete_resource.py
+++ b/hs_core/tests/api/views/test_delete_resource.py
@@ -43,7 +43,7 @@ def test_delete_resource(self):
         # here we are testing the delete_resource view function
 
         self.assertEqual(BaseResource.objects.count(), 1)
-        url_params = {'shortkey': self.gen_res.short_id}
+        url_params = {'shortkey': self.gen_res.short_id, 'usertext': "DELETE"}
         url = reverse('delete_resource', kwargs=url_params)
         request = self.factory.post(url, data={})
         request.user = self.user
@@ -51,8 +51,22 @@ def test_delete_resource(self):
         request.META['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'
         self.set_request_message_attributes(request)
         self.add_session_to_request(request)
-        response = delete_resource(request, shortkey=self.gen_res.short_id)
+        response = delete_resource(request, shortkey=self.gen_res.short_id, usertext="DELETE")
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         response_dict = json.loads(response.content.decode())
         self.assertNotEqual(response_dict['status'], 'Failed')
         self.assertEqual(BaseResource.objects.count(), 0)
+
+    def test_delete_resource_bad_usertext(self):
+        # test a 400 is returned when usertext path parameter is not equal to DELETE
+        self.assertEqual(BaseResource.objects.count(), 1)
+        url_params = {'shortkey': self.gen_res.short_id, "usertext": "delete"}
+        url = reverse('delete_resource', kwargs=url_params)
+        request = self.factory.post(url, data={})
+        request.user = self.user
+        # make it a ajax request
+        request.META['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'
+        self.set_request_message_attributes(request)
+        self.add_session_to_request(request)
+        response = delete_resource(request, shortkey=self.gen_res.short_id, usertext="delete")
+        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
diff --git a/ref_ts/tests/test_ref_ts.py b/ref_ts/tests/test_ref_ts.py
--- a/ref_ts/tests/test_ref_ts.py
+++ b/ref_ts/tests/test_ref_ts.py
@@ -37,6 +37,7 @@ def setUp(self):
         self.url_to_create_ref_time_series = "/hsapi/_internal/create-ref-time-series/"
         self.url_to_download_resource_files = "/hsapi/_internal/{0}/download-refts-bag/"
 
+    @skip("TODO: This should be mocked, his url is responding with a 403 frequently")
     def test_get_his_central_urls(self):
         self.assertEqual(1, 1)
         resp = self.api_client.get(self.url_to_get_his_urls)