Repository: learningequality/kolibri

Issue:
Out of exam when dealing with input type question 
### Observed behavior
When learner is trying to enter the answer of  input type question, exam automatically gets closed.
Cases:
1. When enter more than two digit answer.
2. When learner is trying to erase the answer(in short backspace takes user out of the exam).

Channel ID : 9ba9b274853845dba7863dd66687568a (Input type question).

Note: This is just sample channel. This issue is arising across the channel.

Please watch attached video for more clarification.
[VID_20180821_164643.zip](https://github.com/learningequality/kolibri/files/2306054/VID_20180821_164643.zip)

### Expected behavior
Exam must accept the answer without exiting the exam.


### Steps to reproduce
1. Create exam which contains input type questions.
2. Enter the answer more than 2 digits and try to erase the answer. Learner automatically gets out of the exam.

### Context
 * Kolibri version : Kolibri 0.9.4b3
 * Operating system : Ubuntu 14.04
 * Browser : Chrome

### Screenshot
![screenshot from 2018-08-16 18-37-45](https://user-images.githubusercontent.com/12776071/44210471-34e55580-a184-11e8-8cff-8f9a081df54a.png)
![screenshot from 2018-08-16 18-37-54](https://user-images.githubusercontent.com/12776071/44210473-357dec00-a184-11e8-85b8-42f3bce58f4d.png)

### Error
![screenshot from 2018-08-21 17-01-17](https://user-images.githubusercontent.com/12776071/44399099-e0671f00-a563-11e8-861d-55c048b451d4.png)





Discussion:

Resolving patch:
diff --git a/kolibri/logger/permissions.py b/kolibri/logger/permissions.py
--- a/kolibri/logger/permissions.py
+++ b/kolibri/logger/permissions.py
@@ -1,6 +1,9 @@
-from kolibri.auth.permissions.base import BasePermissions, lookup_field_with_fks
+from django.core.exceptions import ObjectDoesNotExist
 from rest_framework import permissions
 
+from kolibri.auth.permissions.base import BasePermissions
+from kolibri.auth.permissions.base import lookup_field_with_fks
+
 
 class AnyoneCanWriteAnonymousLogs(BasePermissions):
     """
@@ -42,10 +45,22 @@ class ExamActivePermissions(permissions.BasePermission):
     """
 
     def has_permission(self, request, view):
+        from kolibri.logger.models import ExamAttemptLog
         # as `has_object_permission` isn't called for POST/create, we need to check here
-        if (request.method == "POST" or request.method == "PATCH") and request.data:
-            validated_data = view.serializer_class().to_internal_value(_ensure_raw_dict(request.data))
+        examlog = None
+        if request.data:
+            if request.method == "POST":
+                validated_data = view.serializer_class().to_internal_value(_ensure_raw_dict(request.data))
+                examlog = validated_data['examlog']
+            elif request.method == "PATCH":
+                try:
+                    lookup_url_kwarg = view.lookup_url_kwarg or view.lookup_field
+                    examattemptlog = ExamAttemptLog.objects.get(id=view.kwargs[lookup_url_kwarg])
+                    examlog = examattemptlog.examlog
+                except (ValueError, ObjectDoesNotExist):
+                    pass
+        if examlog:
             # Make sure the examlog is not closed and the exam is active
-            return not validated_data['examlog'].closed and validated_data['examlog'].exam.active
+            return not examlog.closed and examlog.exam.active
 
         return True


Test patch:
diff --git a/kolibri/logger/test/test_api.py b/kolibri/logger/test/test_api.py
--- a/kolibri/logger/test/test_api.py
+++ b/kolibri/logger/test/test_api.py
@@ -2,7 +2,6 @@
 Tests that ensure the correct items are returned from api calls.
 Also tests whether the users with permissions can create logs.
 """
-
 import csv
 import datetime
 import uuid
@@ -12,21 +11,27 @@
 from rest_framework import status
 from rest_framework.test import APITestCase
 
-from kolibri.content.models import ChannelMetadata, ContentNode
+from ..models import ContentSessionLog
+from ..models import ContentSummaryLog
+from ..models import UserSessionLog
+from ..serializers import ContentSessionLogSerializer
+from ..serializers import ContentSummaryLogSerializer
+from .factory_logger import ContentSessionLogFactory
+from .factory_logger import ContentSummaryLogFactory
+from .factory_logger import FacilityUserFactory
+from .factory_logger import UserSessionLogFactory
+from kolibri.auth.test.helpers import create_superuser
+from kolibri.auth.test.helpers import provision_device
+from kolibri.auth.test.test_api import ClassroomFactory
+from kolibri.auth.test.test_api import DUMMY_PASSWORD
+from kolibri.auth.test.test_api import FacilityFactory
+from kolibri.auth.test.test_api import LearnerGroupFactory
+from kolibri.content.models import ChannelMetadata
+from kolibri.content.models import ContentNode
 from kolibri.core.exams.models import Exam
+from kolibri.logger.models import ExamAttemptLog
 from kolibri.logger.models import ExamLog
 
-from .factory_logger import (
-    FacilityUserFactory, ContentSessionLogFactory,
-    ContentSummaryLogFactory,
-    UserSessionLogFactory
-)
-
-from ..models import ContentSessionLog, ContentSummaryLog, UserSessionLog
-from ..serializers import ContentSessionLogSerializer, ContentSummaryLogSerializer
-from kolibri.auth.test.test_api import FacilityFactory, ClassroomFactory, LearnerGroupFactory, DUMMY_PASSWORD
-from kolibri.auth.test.helpers import create_superuser, provision_device
-
 class ContentSessionLogAPITestCase(APITestCase):
 
     def setUp(self):
@@ -413,5 +418,43 @@ def test_examlog_closed_permissions(self):
         response = self.client.post(reverse('examattemptlog-list'), data=self.examattemptdata)
         self.assertEqual(response.status_code, 403)
 
+    def test_exam_not_active_patch_permissions(self):
+        # Regression test for #4162
+        examattemptdata = {
+            "item": "test",
+            "start_timestamp": timezone.now(),
+            "end_timestamp": timezone.now(),
+            "correct": 0,
+            "user": self.user1,
+            "examlog": self.examlog,
+            "content_id": "77b57a14a1f0466bb27ea7de8ff468be",
+            "channel_id": "77b57a14a1f0466bb27ea7de8ff468be",
+        }
+        self.client.login(username=self.user1.username, password=DUMMY_PASSWORD, facility=self.facility)
+        examattemptlog = ExamAttemptLog.objects.create(**examattemptdata)
+        self.exam.active = False
+        self.exam.save()
+        response = self.client.patch(reverse('examattemptlog-detail', kwargs={'pk': examattemptlog.id}), {"start_timestamp": timezone.now()}, format="json")
+        self.assertEqual(response.status_code, 403)
+
+    def test_examlog_closed_patch_permissions(self):
+        # Regression test for #4162
+        examattemptdata = {
+            "item": "test",
+            "start_timestamp": timezone.now(),
+            "end_timestamp": timezone.now(),
+            "correct": 0,
+            "user": self.user1,
+            "examlog": self.examlog,
+            "content_id": "77b57a14a1f0466bb27ea7de8ff468be",
+            "channel_id": "77b57a14a1f0466bb27ea7de8ff468be",
+        }
+        self.client.login(username=self.user1.username, password=DUMMY_PASSWORD, facility=self.facility)
+        examattemptlog = ExamAttemptLog.objects.create(**examattemptdata)
+        self.examlog.closed = True
+        self.examlog.save()
+        response = self.client.patch(reverse('examattemptlog-detail', kwargs={'pk': examattemptlog.id}), {"start_timestamp": timezone.now()}, format="json")
+        self.assertEqual(response.status_code, 403)
+
     def tearDown(self):
         self.client.logout()