Repository: learningequality/kolibri

Issue:
Make it easier to cancel Tasks safely
### Observed behavior

If you try to GET a non-existent task you get a 500 error:
<details>
```
NoResultFound at /api/tasks/27f698db011c4c0f827f10f8a74e982d/
No row was found for one()
Request Method:	GET
Request URL:	http://localhost:8000/api/tasks/27f698db011c4c0f827f10f8a74e982d/
Django Version:	1.11.13
Exception Type:	NoResultFound
Exception Value:	
No row was found for one()
Exception Location:	/Users/jon/.venvs/kolibri/lib/python2.7/site-packages/sqlalchemy/orm/query.py in one, line 2890
Python Executable:	/Users/jon/.venvs/kolibri/bin/python
Python Version:	2.7.10
Python Path:	
['/Users/jon/Github/kolibri/kolibri/dist/cext/cp27/macosx-10.13-intel',
 '/Users/jon/Github/kolibri/kolibri/dist',
 '',
 '/Users/jon/Github/kolibri',
 '/usr/local/lib/python2.7/site-packages',
 '/Users/jon/.venvs/kolibri/lib/python27.zip',
 '/Users/jon/.venvs/kolibri/lib/python2.7',
 '/Users/jon/.venvs/kolibri/lib/python2.7/plat-darwin',
 '/Users/jon/.venvs/kolibri/lib/python2.7/plat-mac',
 '/Users/jon/.venvs/kolibri/lib/python2.7/plat-mac/lib-scriptpackages',
 '/Users/jon/.venvs/kolibri/lib/python2.7/lib-tk',
 '/Users/jon/.venvs/kolibri/lib/python2.7/lib-old',
 '/Users/jon/.venvs/kolibri/lib/python2.7/lib-dynload',
 '/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7',
 '/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/plat-darwin',
 '/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/lib-tk',
 '/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/plat-mac',
 '/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/plat-mac/lib-scriptpackages',
 '/Users/jon/.venvs/kolibri/lib/python2.7/site-packages',
 '/Users/jon/Github/kolibri/kolibri/dist/py2only']
Server time:	Fri, 18 May 2018 14:41:04 -0700
```
</details>

Same if you try to cancel it.


### Expected behavior

Make GETing and canceling non-existent Tasks safer:

1. GET -> 404
1. POST task/cancel -> 200 probably, since canceling a non-existent task doesn't change anything



Discussion:
When Importing Generating New Listings can take a very long time. It would be nice to have a cancel button. 

Resolving patch:
diff --git a/kolibri/tasks/api.py b/kolibri/tasks/api.py
--- a/kolibri/tasks/api.py
+++ b/kolibri/tasks/api.py
@@ -3,6 +3,7 @@
 
 from django.apps.registry import AppRegistryNotReady
 from django.core.management import call_command
+from django.http.response import Http404
 from django.utils.translation import gettext_lazy as _
 from iceqube.common.classes import State
 from iceqube.exceptions import UserCancelledError
@@ -11,6 +12,7 @@
 from rest_framework.decorators import list_route
 from rest_framework.response import Response
 from six import string_types
+from sqlalchemy.orm.exc import NoResultFound
 
 from .client import get_client
 from kolibri.content.permissions import CanManageContent
@@ -50,8 +52,11 @@ def create(self, request):
         pass
 
     def retrieve(self, request, pk=None):
-        task = _job_to_response(get_client().status(pk))
-        return Response(task)
+        try:
+            task = _job_to_response(get_client().status(pk))
+            return Response(task)
+        except NoResultFound:
+            raise Http404('Task with {pk} not found'.format(pk=pk))
 
     def destroy(self, request, pk=None):
         # unimplemented for now.
@@ -306,8 +311,10 @@ def canceltask(self, request):
         if not isinstance(request.data['task_id'], string_types):
             raise serializers.ValidationError(
                 "The 'task_id' should be a string.")
-
-        get_client().cancel(request.data['task_id'])
+        try:
+            get_client().cancel(request.data['task_id'])
+        except NoResultFound:
+            pass
         get_client().clear(force=True)
         return Response({})
 


Test patch:
diff --git a/kolibri/tasks/test/test_api.py b/kolibri/tasks/test/test_api.py
--- a/kolibri/tasks/test/test_api.py
+++ b/kolibri/tasks/test/test_api.py
@@ -1,6 +1,8 @@
 from django.core.urlresolvers import reverse
+from mock import MagicMock
 from mock import patch
 from rest_framework.test import APITestCase
+from sqlalchemy.orm.exc import NoResultFound
 
 from kolibri.auth.models import Facility
 from kolibri.auth.models import FacilityUser
@@ -24,3 +26,17 @@ def setUp(self):
     def test_task_cancel(self, get_client_mock):
         response = self.client.post(reverse('task-canceltask'), {'task_id': '1'}, format='json')
         self.assertEqual(response.data, {})
+
+    def test_task_cancel_no_task(self, get_client_mock):
+        get_client_return_mock = MagicMock()
+        get_client_return_mock.cancel.side_effect = NoResultFound()
+        get_client_mock.return_value = get_client_return_mock
+        response = self.client.post(reverse('task-canceltask'), {'task_id': '1'}, format='json')
+        self.assertEqual(response.status_code, 200)
+
+    def test_task_get_no_task(self, get_client_mock):
+        get_client_return_mock = MagicMock()
+        get_client_return_mock.status.side_effect = NoResultFound()
+        get_client_mock.return_value = get_client_return_mock
+        response = self.client.get(reverse('task-detail', kwargs={'pk': '1'}), {'task_id': '1'}, format='json')
+        self.assertEqual(response.status_code, 404)