Repository: learningequality/kolibri

Issue:
inconsistency between difficult question summary and detail reports

### Observed behavior

| summary | detail |
|--|--|
|  "Question 1" | "Repeated addition (1)" |
| "1 of 1 needs help" | three users needing help |
| [Link to page]( http://kolibribeta.learningequality.org/coach/#/4707894249cbe44b7fd142bdb87fe90e/reports/lessons/77c3daea1dcadbdb2f1b8e0a9d278a81/exercises/52afd859b8df5bb1a6a834ddaeaf3f23/questions) | [Link to page](http://kolibribeta.learningequality.org/coach/#/4707894249cbe44b7fd142bdb87fe90e/reports/lessons/77c3daea1dcadbdb2f1b8e0a9d278a81/exercises/52afd859b8df5bb1a6a834ddaeaf3f23/questions/58e624d3713f553fb26c156885e344aa/learners/db6a72b70250db5ed2d69955eece98bd/interactions/0) | 
| ![image](https://user-images.githubusercontent.com/2367265/52757940-4d132180-2fbb-11e9-9140-b09cbe98247c.png) | ![image](https://user-images.githubusercontent.com/2367265/52757930-44bae680-2fbb-11e9-993d-83b4ea3bd6b0.png) |


### Expected behavior

Table summary and detail page should be consistent

### User-facing consequences

possible confusion



### Steps to reproduce

see links to beta site above

### Context

0.12.0 beta 4


----

cc @rtibbles 


Discussion:
might be partly a dupe of #5026?

Resolving patch:
diff --git a/kolibri/plugins/coach/api.py b/kolibri/plugins/coach/api.py
--- a/kolibri/plugins/coach/api.py
+++ b/kolibri/plugins/coach/api.py
@@ -17,6 +17,7 @@
 from kolibri.core.auth.models import FacilityUser
 from kolibri.core.auth.models import LearnerGroup
 from kolibri.core.decorators import query_params_required
+from kolibri.core.exams.models import Exam
 from kolibri.core.lessons.models import Lesson
 from kolibri.core.logger.models import AttemptLog
 from kolibri.core.logger.models import ExamAttemptLog
@@ -188,57 +189,117 @@ def list(self, request, *args, **kwargs):
         return response
 
 
-class DifficultiesPermissions(permissions.BasePermission):
+class ExerciseDifficultiesPermissions(permissions.BasePermission):
 
     # check if requesting user has permission for collection or user
     def has_permission(self, request, view):
-        collection_id = view.kwargs.get('collection_id', None)
+        classroom_id = request.GET.get('classroom_id', None)
+        group_id = request.GET.get('group_id', None)
+        collection_id = group_id or classroom_id
+        lesson_id = request.GET.get('lesson_id', None)
         allowed_roles = [role_kinds.ADMIN, role_kinds.COACH]
+        if lesson_id:
+            try:
+                lesson = Lesson.objects.get(id=lesson_id)
+                classroom = lesson.collection
+                return request.user.has_role_for(allowed_roles, classroom)
+            except (FacilityUser.DoesNotExist, Collection.DoesNotExist, Lesson.DoesNotExist, ValueError):
+                return False
         try:
             return request.user.has_role_for(allowed_roles, Collection.objects.get(pk=collection_id))
         except (FacilityUser.DoesNotExist, Collection.DoesNotExist, ValueError):
             return False
 
 
-@query_params_required(collection_id=str)
-class BaseDifficultQuestionsViewset(viewsets.ViewSet):
-    permission_classes = (permissions.IsAuthenticated, DifficultiesPermissions,)
+# Define a base class so that the inherited class is properly introspectable,
+# rather than being the result of our wrapping
+@query_params_required(classroom_id=str)
+class BaseExerciseDifficultQuestionsViewset(viewsets.ViewSet):
+    pass
 
 
-class ExerciseDifficultQuestionsViewset(BaseDifficultQuestionsViewset):
+class ExerciseDifficultQuestionsViewset(BaseExerciseDifficultQuestionsViewset):
+    permission_classes = (permissions.IsAuthenticated, ExerciseDifficultiesPermissions,)
 
     def retrieve(self, request, pk):
         """
         Get the difficult questions for a particular exercise.
         pk maps to the content_id of the exercise in question.
         """
-        collection_id = self.kwargs['collection_id']
+        classroom_id = request.GET.get('classroom_id', None)
+        group_id = request.GET.get('group_id', None)
+        lesson_id = request.GET.get('lesson_id', None)
         queryset = AttemptLog.objects.filter(masterylog__summarylog__content_id=pk)
-        queryset = HierarchyRelationsFilter(queryset).filter_by_hierarchy(
-            ancestor_collection=collection_id,
-            target_user=F("user"),
-        )
+        if lesson_id is not None:
+            collection_ids = Lesson.objects.get(id=lesson_id).lesson_assignments.values_list('collection_id', flat=True)
+            if group_id is not None:
+                if group_id not in collection_ids and classroom_id not in collection_ids:
+                    # In the special case that the group is not in the lesson assignments
+                    # nor the containing classroom, just return an empty queryset.
+                    queryset = AttemptLog.objects.none()
+            else:
+                # Only filter by all the collections in the lesson if we are not also
+                # filtering by a specific group. Otherwise the group should be sufficient.
+                base_queryset = queryset
+                # Set starting queryset to null, then OR.
+                queryset = AttemptLog.objects.none()
+                for collection_id in collection_ids:
+                    queryset |= HierarchyRelationsFilter(base_queryset).filter_by_hierarchy(
+                        ancestor_collection=collection_id,
+                        target_user=F("user"),
+                    )
+                queryset = queryset.distinct()
+        if group_id is not None:
+            collection_id = group_id or classroom_id
+            queryset = HierarchyRelationsFilter(queryset).filter_by_hierarchy(
+                ancestor_collection=collection_id,
+                target_user=F("user"),
+            )
+
         data = queryset.values('item').annotate(total=Count('correct')).annotate(correct=Sum('correct'))
         return Response(data)
 
 
-class QuizDifficultQuestionsViewset(BaseDifficultQuestionsViewset):
+class QuizDifficultiesPermissions(permissions.BasePermission):
+
+    # check if requesting user has permission for collection or user
+    def has_permission(self, request, view):
+        exam_id = view.kwargs.get('pk', None)
+        if exam_id is None:
+            return False
+        try:
+            collection = Exam.objects.get(id=exam_id).collection
+        except Exam.DoesNotExist:
+            return False
+        allowed_roles = [role_kinds.ADMIN, role_kinds.COACH]
+        try:
+            return request.user.has_role_for(allowed_roles, collection)
+        except (FacilityUser.DoesNotExist, ValueError):
+            return False
+
+
+class QuizDifficultQuestionsViewset(viewsets.ViewSet):
+    permission_classes = (permissions.IsAuthenticated, QuizDifficultiesPermissions,)
 
     def retrieve(self, request, pk):
         """
         Get the difficult questions for a particular quiz.
         """
-        collection_id = self.kwargs['collection_id']
+        group_id = request.GET.get('group_id', None)
         queryset = ExamAttemptLog.objects.filter(examlog__exam=pk)
-        queryset = HierarchyRelationsFilter(queryset).filter_by_hierarchy(
-            ancestor_collection=collection_id,
-            target_user=F("user"),
-        )
+        if group_id is not None:
+            queryset = HierarchyRelationsFilter(queryset).filter_by_hierarchy(
+                ancestor_collection=group_id,
+                target_user=F("user"),
+            )
+            collection_id = group_id
+        else:
+            collection_id = Exam.objects.get(pk=pk).collection_id
         data = queryset.values('item', 'content_id').annotate(correct=Sum('correct'))
 
         # Instead of inferring the totals from the number of logs, use the total
         # number of people who took the exam as our guide, as people who started the exam
-        # but did not attempt the question, are still important.
+        # but did not attempt the question are still important.
         total = HierarchyRelationsFilter(ExamLog.objects.filter(exam_id=pk)).filter_by_hierarchy(
             ancestor_collection=collection_id,
             target_user=F("user"),


Test patch:
diff --git a/kolibri/core/assets/test/exams/utils.spec.js b/kolibri/core/assets/test/exams/utils.spec.js
--- a/kolibri/core/assets/test/exams/utils.spec.js
+++ b/kolibri/core/assets/test/exams/utils.spec.js
@@ -110,51 +110,61 @@ describe('exam utils', function() {
       */
       const expectedOutput = [
         {
+          counterInExercise: 16,
           question_id: 'fc60ecb9f83f505fa31e734e517e6523',
           exercise_id: '69e5e6abf479581483d441b83d7d76f4',
           title: 'Count with small numbers',
         },
         {
+          counterInExercise: 13,
           question_id: 'd4623921a2ef5ddaa39048c0f7a6fe06',
           exercise_id: 'b186a2a3ae8e51dd867614db03eb3783',
           title: 'Find 1 more or 1 less than a number',
         },
         {
+          counterInExercise: 6,
           question_id: '2f5fdbc49ce35310abf49971867ac94e',
           exercise_id: 'b186a2a3ae8e51dd867614db03eb3783',
           title: 'Find 1 more or 1 less than a number',
         },
         {
+          counterInExercise: 15,
           question_id: '19421254d90d520d981bd07fd6ede9b2',
           exercise_id: 'b9444e7d11395946b2e14edb5dc4670f',
           title: 'Count in order',
         },
         {
+          counterInExercise: 1,
           question_id: '1e0ce47a58465b2cb298acd3b893dce5',
           exercise_id: 'b9444e7d11395946b2e14edb5dc4670f',
           title: 'Count in order',
         },
         {
+          counterInExercise: 10,
           question_id: 'd3ac055fa4ad599bbd30a00eaeb93e5e',
           exercise_id: 'b186a2a3ae8e51dd867614db03eb3783',
           title: 'Find 1 more or 1 less than a number',
         },
         {
+          counterInExercise: 1,
           question_id: 'a5f508eb2ba05d429812dc43b577ef03',
           exercise_id: '69e5e6abf479581483d441b83d7d76f4',
           title: 'Count with small numbers',
         },
         {
+          counterInExercise: 17,
           question_id: '8358fbbd0a285e9d99da558094eabd4c',
           exercise_id: '69e5e6abf479581483d441b83d7d76f4',
           title: 'Count with small numbers',
         },
         {
+          counterInExercise: 2,
           question_id: '3aeb023925e35001865091de1fb4d3ae',
           exercise_id: '69e5e6abf479581483d441b83d7d76f4',
           title: 'Count with small numbers',
         },
         {
+          counterInExercise: 3,
           question_id: 'a654dec351af5bdf937566e46b7c2fc3',
           exercise_id: 'b9444e7d11395946b2e14edb5dc4670f',
           title: 'Count in order',
@@ -175,51 +185,61 @@ describe('exam utils', function() {
       const converted = convertExamQuestionSourcesV0V1(questionSources, seed, QUESTION_IDS);
       const expectedOutput = [
         {
+          counterInExercise: 20,
           question_id: 'fc5958e2a67d5cd2bd48962e9e1c35c3',
           exercise_id: '69e5e6abf479581483d441b83d7d76f4',
           title: 'Count with small numbers',
         },
         {
+          counterInExercise: 3,
           question_id: 'beb5eae9491c564fb6bc5b9c1421d085',
           exercise_id: '69e5e6abf479581483d441b83d7d76f4',
           title: 'Count with small numbers',
         },
         {
+          counterInExercise: 2,
           question_id: '3aeb023925e35001865091de1fb4d3ae',
           exercise_id: '69e5e6abf479581483d441b83d7d76f4',
           title: 'Count with small numbers',
         },
         {
+          counterInExercise: 17,
           question_id: '8358fbbd0a285e9d99da558094eabd4c',
           exercise_id: '69e5e6abf479581483d441b83d7d76f4',
           title: 'Count with small numbers',
         },
         {
+          counterInExercise: 6,
           question_id: '4dd2526065ee572998a06b1fdae9cb4b',
           exercise_id: '69e5e6abf479581483d441b83d7d76f4',
           title: 'Count with small numbers',
         },
         {
+          counterInExercise: 5,
           question_id: '10e6239f9cf35b75b0cf75ca7f7e6a14',
           exercise_id: '69e5e6abf479581483d441b83d7d76f4',
           title: 'Count with small numbers',
         },
         {
+          counterInExercise: 12,
           question_id: 'e92a277052cf56e2aaae44338fa0bfec',
           exercise_id: '69e5e6abf479581483d441b83d7d76f4',
           title: 'Count with small numbers',
         },
         {
+          counterInExercise: 9,
           question_id: '6ef7996754de54ad92b660d06436976c',
           exercise_id: '69e5e6abf479581483d441b83d7d76f4',
           title: 'Count with small numbers',
         },
         {
+          counterInExercise: 4,
           question_id: 'b19341ebca9a5bdb817cdc31b8d62993',
           exercise_id: '69e5e6abf479581483d441b83d7d76f4',
           title: 'Count with small numbers',
         },
         {
+          counterInExercise: 8,
           question_id: 'e226724765085214acfb807942918fed',
           exercise_id: '69e5e6abf479581483d441b83d7d76f4',
           title: 'Count with small numbers',
@@ -249,16 +269,19 @@ describe('exam utils', function() {
       const converted = convertExamQuestionSourcesV0V1(questionSources, seed, QUESTION_IDS);
       const expectedOutput = [
         {
+          counterInExercise: 9,
           question_id: '6ef7996754de54ad92b660d06436976c',
           exercise_id: '69e5e6abf479581483d441b83d7d76f4',
           title: 'Count with small numbers',
         },
         {
+          counterInExercise: 2,
           question_id: '952857e446b95c5da36226f59237ffcc',
           exercise_id: 'b9444e7d11395946b2e14edb5dc4670f',
           title: 'Count in order',
         },
         {
+          counterInExercise: 7,
           question_id: '5a56a46b261d5ff7b3f870cf09c6952f',
           exercise_id: 'b186a2a3ae8e51dd867614db03eb3783',
           title: 'Find 1 more or 1 less than a number',
diff --git a/kolibri/plugins/coach/assets/test/showPageActions.spec.js b/kolibri/plugins/coach/assets/test/showPageActions.spec.js
--- a/kolibri/plugins/coach/assets/test/showPageActions.spec.js
+++ b/kolibri/plugins/coach/assets/test/showPageActions.spec.js
@@ -337,14 +337,14 @@ describe('exam report state', () => {
       exerciseContentNodes,
     };
     const questions = [
-      { exercise_id: '12345', question_id: 14, title: 'exercise title 1' },
-      { exercise_id: '12345', question_id: 16, title: 'exercise title 1' },
-      { exercise_id: '12345', question_id: 13, title: 'exercise title 1' },
-      { exercise_id: '54321', question_id: 18, title: 'exercise title 2' },
-      { exercise_id: '12345', question_id: 0, title: 'exercise title 1' },
-      { exercise_id: '54321', question_id: 0, title: 'exercise title 2' },
-      { exercise_id: '12345', question_id: 18, title: 'exercise title 1' },
-      { exercise_id: '12345', question_id: 12, title: 'exercise title 1' },
+      { exercise_id: '12345', question_id: 14, title: 'exercise title 1', counterInExercise: 15 },
+      { exercise_id: '12345', question_id: 16, title: 'exercise title 1', counterInExercise: 17 },
+      { exercise_id: '12345', question_id: 13, title: 'exercise title 1', counterInExercise: 14 },
+      { exercise_id: '54321', question_id: 18, title: 'exercise title 2', counterInExercise: 19 },
+      { exercise_id: '12345', question_id: 0, title: 'exercise title 1', counterInExercise: 1 },
+      { exercise_id: '54321', question_id: 0, title: 'exercise title 2', counterInExercise: 1 },
+      { exercise_id: '12345', question_id: 18, title: 'exercise title 1', counterInExercise: 19 },
+      { exercise_id: '12345', question_id: 12, title: 'exercise title 1', counterInExercise: 13 },
     ];
     expect(examReportStore.getters.examQuestions(state)).toEqual(questions);
   });
@@ -357,16 +357,16 @@ describe('exam report state', () => {
       exerciseContentNodes,
     };
     const questions = [
-      { exercise_id: '123', question_id: 10, title: 'exercise title 5' },
-      { exercise_id: 'xyz', question_id: 8, title: 'exercise title 3' },
-      { exercise_id: 'xyz', question_id: 10, title: 'exercise title 3' },
-      { exercise_id: '123', question_id: 13, title: 'exercise title 5' },
-      { exercise_id: 'abc', question_id: 8, title: 'exercise title 4' },
-      { exercise_id: 'abc', question_id: 17, title: 'exercise title 4' },
-      { exercise_id: '123', question_id: 8, title: 'exercise title 5' },
-      { exercise_id: 'xyz', question_id: 17, title: 'exercise title 3' },
-      { exercise_id: '123', question_id: 17, title: 'exercise title 5' },
-      { exercise_id: 'abc', question_id: 10, title: 'exercise title 4' },
+      { exercise_id: '123', question_id: 10, title: 'exercise title 5', counterInExercise: 11 },
+      { exercise_id: 'xyz', question_id: 8, title: 'exercise title 3', counterInExercise: 9 },
+      { exercise_id: 'xyz', question_id: 10, title: 'exercise title 3', counterInExercise: 11 },
+      { exercise_id: '123', question_id: 13, title: 'exercise title 5', counterInExercise: 14 },
+      { exercise_id: 'abc', question_id: 8, title: 'exercise title 4', counterInExercise: 9 },
+      { exercise_id: 'abc', question_id: 17, title: 'exercise title 4', counterInExercise: 18 },
+      { exercise_id: '123', question_id: 8, title: 'exercise title 5', counterInExercise: 9 },
+      { exercise_id: 'xyz', question_id: 17, title: 'exercise title 3', counterInExercise: 18 },
+      { exercise_id: '123', question_id: 17, title: 'exercise title 5', counterInExercise: 18 },
+      { exercise_id: 'abc', question_id: 10, title: 'exercise title 4', counterInExercise: 11 },
     ];
     expect(examReportStore.getters.examQuestions(state)).toEqual(questions);
   });
diff --git a/kolibri/plugins/coach/test/test_difficult_questions.py b/kolibri/plugins/coach/test/test_difficult_questions.py
new file mode 100644
--- /dev/null
+++ b/kolibri/plugins/coach/test/test_difficult_questions.py
@@ -0,0 +1,535 @@
+from __future__ import absolute_import
+from __future__ import print_function
+from __future__ import unicode_literals
+
+import datetime
+import json
+
+from django.core.urlresolvers import reverse
+from rest_framework.test import APITestCase
+
+from kolibri.core.auth.models import Classroom
+from kolibri.core.auth.models import Facility
+from kolibri.core.auth.models import FacilityUser
+from kolibri.core.auth.models import LearnerGroup
+from kolibri.core.auth.test.helpers import provision_device
+from kolibri.core.content.models import ContentNode
+from kolibri.core.exams.models import Exam
+from kolibri.core.exams.models import ExamAssignment
+from kolibri.core.lessons.models import Lesson
+from kolibri.core.lessons.models import LessonAssignment
+from kolibri.core.logger.models import AttemptLog
+from kolibri.core.logger.models import ContentSessionLog
+from kolibri.core.logger.models import ContentSummaryLog
+from kolibri.core.logger.models import ExamAttemptLog
+from kolibri.core.logger.models import ExamLog
+from kolibri.core.logger.models import MasteryLog
+
+
+class ExerciseDifficultQuestionTestCase(APITestCase):
+
+    def setUp(self):
+        provision_device()
+        self.facility = Facility.objects.create(name='My Facility')
+        self.classroom = Classroom.objects.create(name='My Classroom', parent=self.facility)
+        self.group = LearnerGroup.objects.create(name='My Group', parent=self.classroom)
+
+        self.coach_user = FacilityUser.objects.create(username='admin', facility=self.facility)
+        self.coach_user.set_password('password')
+        self.coach_user.save()
+
+        self.learner_user = FacilityUser.objects.create(username='learner', facility=self.facility)
+        self.learner_user.set_password('password')
+        self.learner_user.save()
+
+        self.facility.add_coach(self.coach_user)
+        self.classroom.add_coach(self.coach_user)
+        self.classroom.add_member(self.learner_user)
+        self.group.add_member(self.learner_user)
+
+        # Need ContentNodes
+        self.channel_id = '15f32edcec565396a1840c5413c92450'
+        self.lesson_id = '15f32edcec565396a1840c5413c92452'
+
+        self.content_ids = [
+            '15f32edcec565396a1840c5413c92451',
+            '15f32edcec565396a1840c5413c92452',
+            '15f32edcec565396a1840c5413c92453',
+        ]
+        self.contentnode_ids = [
+            '25f32edcec565396a1840c5413c92451',
+            '25f32edcec565396a1840c5413c92452',
+            '25f32edcec565396a1840c5413c92453',
+        ]
+        self.node_1 = ContentNode.objects.create(
+            title='Node 1',
+            available=True,
+            id=self.contentnode_ids[0],
+            content_id=self.content_ids[0],
+            channel_id=self.channel_id
+        )
+        self.lesson = Lesson.objects.create(
+            id=self.lesson_id,
+            title='My Lesson',
+            created_by=self.coach_user,
+            collection=self.classroom,
+            resources=json.dumps([{
+                'contentnode_id': self.node_1.id,
+                'content_id': self.node_1.content_id,
+                'channel_id': self.channel_id
+            }])
+        )
+        self.assignment_1 = LessonAssignment.objects.create(
+            lesson=self.lesson,
+            assigned_by=self.coach_user,
+            collection=self.classroom,
+        )
+        self.exercise_difficulties_basename = 'kolibri:coach:exercisedifficulties'
+
+    def test_learner_cannot_access_by_classroom_id(self):
+        learner_user = FacilityUser.objects.create(username='learner', facility=self.facility)
+        learner_user.set_password('password')
+        learner_user.save()
+        self.client.login(username='learner', password='password')
+        response = self.client.get(reverse(
+            self.exercise_difficulties_basename + '-detail',
+            kwargs={'pk': self.content_ids[0]}), data={'classroom_id': self.classroom.id})
+        self.assertEqual(response.status_code, 403)
+
+    def test_learner_cannot_access_by_lesson_id(self):
+        learner_user = FacilityUser.objects.create(username='learner', facility=self.facility)
+        learner_user.set_password('password')
+        learner_user.save()
+        self.client.login(username='learner', password='password')
+        response = self.client.get(reverse(
+            self.exercise_difficulties_basename + '-detail',
+            kwargs={'pk': self.content_ids[0]}), data={'lesson_id': self.lesson.id, 'classroom_id': self.classroom.id})
+        self.assertEqual(response.status_code, 403)
+
+    def test_learner_cannot_access_by_group_id(self):
+        self.client.login(username='learner', password='password')
+        response = self.client.get(reverse(
+            self.exercise_difficulties_basename + '-detail',
+            kwargs={'pk': self.content_ids[0]}), data={'group_id': self.group.id, 'classroom_id': self.classroom.id})
+        self.assertEqual(response.status_code, 403)
+
+    def test_coach_classroom_id_required(self):
+        self.client.login(username=self.coach_user.username, password='password')
+        response = self.client.get(reverse(
+            self.exercise_difficulties_basename + '-detail', kwargs={'pk': self.content_ids[0]}))
+        self.assertEqual(response.status_code, 412)
+
+    def test_coach_no_progress_by_classroom_id(self):
+        self.client.login(username=self.coach_user.username, password='password')
+        response = self.client.get(reverse(
+            self.exercise_difficulties_basename + '-detail',
+            kwargs={'pk': self.content_ids[0]}), data={'classroom_id': self.classroom.id})
+        self.assertEqual(len(response.data), 0)
+
+    def test_coach_no_progress_by_lesson_id(self):
+        self.client.login(username=self.coach_user.username, password='password')
+        response = self.client.get(reverse(
+            self.exercise_difficulties_basename + '-detail',
+            kwargs={'pk': self.content_ids[0]}), data={'lesson_id': self.lesson.id, 'classroom_id': self.classroom.id})
+        self.assertEqual(len(response.data), 0)
+
+    def test_coach_no_progress_by_group_id(self):
+        self.client.login(username=self.coach_user.username, password='password')
+        response = self.client.get(reverse(
+            self.exercise_difficulties_basename + '-detail',
+            kwargs={'pk': self.content_ids[0]}), data={'group_id': self.group.id, 'classroom_id': self.classroom.id})
+        self.assertEqual(len(response.data), 0)
+
+    def _set_one_difficult(self, user):
+        self.sessionlog = ContentSessionLog.objects.create(
+            user=user,
+            content_id=self.content_ids[0],
+            channel_id=self.node_1.channel_id,
+            kind='exercise',
+            progress=0.1,
+            start_timestamp=datetime.datetime.now(),
+        )
+
+        self.summarylog = ContentSummaryLog.objects.create(
+            user=user,
+            content_id=self.content_ids[0],
+            channel_id=self.node_1.channel_id,
+            kind='exercise',
+            progress=0.1,
+            start_timestamp=datetime.datetime.now(),
+        )
+
+        self.masterylog = MasteryLog.objects.create(
+            user=user,
+            summarylog=self.summarylog,
+            start_timestamp=datetime.datetime.now(),
+            mastery_level=1,
+        )
+
+        AttemptLog.objects.create(
+            masterylog=self.masterylog,
+            sessionlog=self.sessionlog,
+            start_timestamp=datetime.datetime.now(),
+            end_timestamp=datetime.datetime.now(),
+            complete=True,
+            correct=0,
+            user=user,
+            item='test',
+        )
+
+    def test_coach_one_difficult_by_classroom_id(self):
+        self._set_one_difficult(self.learner_user)
+        self.client.login(username=self.coach_user.username, password='password')
+        response = self.client.get(reverse(
+            self.exercise_difficulties_basename + '-detail',
+            kwargs={'pk': self.content_ids[0]}), data={'classroom_id': self.classroom.id})
+        self.assertEqual(len(response.data), 1)
+        self.assertEqual(response.data[0]['total'], 1)
+        self.assertEqual(response.data[0]['correct'], 0)
+
+    def test_coach_one_difficult_by_lesson_id(self):
+        self._set_one_difficult(self.learner_user)
+        self.client.login(username=self.coach_user.username, password='password')
+        response = self.client.get(reverse(
+            self.exercise_difficulties_basename + '-detail',
+            kwargs={'pk': self.content_ids[0]}), data={'lesson_id': self.lesson.id, 'classroom_id': self.classroom.id})
+        self.assertEqual(len(response.data), 1)
+        self.assertEqual(response.data[0]['total'], 1)
+        self.assertEqual(response.data[0]['correct'], 0)
+
+    def test_coach_one_difficult_by_lesson_id_repeated_assignment(self):
+        LessonAssignment.objects.create(
+            lesson=self.lesson,
+            assigned_by=self.coach_user,
+            collection=self.group,
+        )
+        self._set_one_difficult(self.learner_user)
+        self.client.login(username=self.coach_user.username, password='password')
+        response = self.client.get(reverse(
+            self.exercise_difficulties_basename + '-detail',
+            kwargs={'pk': self.content_ids[0]}), data={'lesson_id': self.lesson.id, 'classroom_id': self.classroom.id})
+        self.assertEqual(len(response.data), 1)
+        self.assertEqual(response.data[0]['total'], 1)
+        self.assertEqual(response.data[0]['correct'], 0)
+
+    def test_coach_one_difficult_by_group_id(self):
+        self._set_one_difficult(self.learner_user)
+        self.client.login(username=self.coach_user.username, password='password')
+        response = self.client.get(reverse(
+            self.exercise_difficulties_basename + '-detail',
+            kwargs={'pk': self.content_ids[0]}), data={'group_id': self.group.id, 'classroom_id': self.classroom.id})
+        self.assertEqual(len(response.data), 1)
+        self.assertEqual(response.data[0]['total'], 1)
+        self.assertEqual(response.data[0]['correct'], 0)
+
+    def test_coach_two_difficult_by_lesson_id(self):
+        self._set_one_difficult(self.learner_user)
+        AttemptLog.objects.create(
+            masterylog=self.masterylog,
+            sessionlog=self.sessionlog,
+            start_timestamp=datetime.datetime.now(),
+            end_timestamp=datetime.datetime.now(),
+            complete=True,
+            correct=0,
+            user=self.learner_user,
+            item='nottest',
+        )
+        self.client.login(username=self.coach_user.username, password='password')
+        response = self.client.get(reverse(
+            self.exercise_difficulties_basename + '-detail',
+            kwargs={'pk': self.content_ids[0]}), data={'lesson_id': self.lesson.id, 'classroom_id': self.classroom.id})
+        self.assertEqual(len(response.data), 2)
+        self.assertEqual(response.data[0]['total'], 1)
+        self.assertEqual(response.data[0]['correct'], 0)
+        self.assertEqual(response.data[1]['total'], 1)
+        self.assertEqual(response.data[1]['correct'], 0)
+
+    def test_coach_one_difficult_one_not_by_lesson_id(self):
+        self._set_one_difficult(self.learner_user)
+        AttemptLog.objects.create(
+            masterylog=self.masterylog,
+            sessionlog=self.sessionlog,
+            start_timestamp=datetime.datetime.now(),
+            end_timestamp=datetime.datetime.now(),
+            complete=True,
+            correct=1,
+            user=self.learner_user,
+            item='nottest',
+        )
+        self.client.login(username=self.coach_user.username, password='password')
+        response = self.client.get(reverse(
+            self.exercise_difficulties_basename + '-detail',
+            kwargs={'pk': self.content_ids[0]}), data={'lesson_id': self.lesson.id, 'classroom_id': self.classroom.id})
+        self.assertEqual(len(response.data), 2)
+        self.assertTrue(any(map(lambda x: x['total'] == 1 and x['correct'] == 0, response.data)))
+        self.assertTrue(any(map(lambda x: x['total'] == 1 and x['correct'] == 1, response.data)))
+
+    def test_coach_difficult_no_assigned_by_lesson_id(self):
+        self._set_one_difficult(self.learner_user)
+        AttemptLog.objects.create(
+            masterylog=self.masterylog,
+            sessionlog=self.sessionlog,
+            start_timestamp=datetime.datetime.now(),
+            end_timestamp=datetime.datetime.now(),
+            complete=True,
+            correct=1,
+            user=self.learner_user,
+            item='nottest',
+        )
+        LessonAssignment.objects.all().delete()
+        self.client.login(username=self.coach_user.username, password='password')
+        response = self.client.get(reverse(
+            self.exercise_difficulties_basename + '-detail',
+            kwargs={'pk': self.content_ids[0]}), data={'lesson_id': self.lesson.id, 'classroom_id': self.classroom.id})
+        self.assertEqual(len(response.data), 0)
+
+    def test_coach_difficult_no_assigned_by_group_id(self):
+        self._set_one_difficult(self.learner_user)
+        AttemptLog.objects.create(
+            masterylog=self.masterylog,
+            sessionlog=self.sessionlog,
+            start_timestamp=datetime.datetime.now(),
+            end_timestamp=datetime.datetime.now(),
+            complete=True,
+            correct=1,
+            user=self.learner_user,
+            item='nottest',
+        )
+        LessonAssignment.objects.all().delete()
+        self.client.login(username=self.coach_user.username, password='password')
+        response = self.client.get(reverse(
+            self.exercise_difficulties_basename + '-detail',
+            kwargs={'pk': self.content_ids[0]}), data={'group_id': self.group.id, 'classroom_id': self.classroom.id})
+        self.assertEqual(len(response.data), 2)
+        self.assertTrue(any(map(lambda x: x['total'] == 1 and x['correct'] == 0, response.data)))
+        self.assertTrue(any(map(lambda x: x['total'] == 1 and x['correct'] == 1, response.data)))
+
+    def test_coach_difficult_both_assigned_by_lesson_id_group_id(self):
+        self._set_one_difficult(self.learner_user)
+        learner2 = FacilityUser.objects.create(username='learner2', facility=self.facility)
+        self.classroom.add_member(learner2)
+        self._set_one_difficult(learner2)
+        self.client.login(username=self.coach_user.username, password='password')
+        response = self.client.get(reverse(
+            self.exercise_difficulties_basename + '-detail',
+            kwargs={'pk': self.content_ids[0]}),
+            data={'lesson_id': self.lesson.id, 'group_id': self.group.id, 'classroom_id': self.classroom.id})
+        self.assertEqual(len(response.data), 1)
+        self.assertEqual(response.data[0]['total'], 1)
+        self.assertEqual(response.data[0]['correct'], 0)
+
+    def test_coach_difficult_group_id_not_in_lesson(self):
+        self._set_one_difficult(self.learner_user)
+        learner2 = FacilityUser.objects.create(username='learner2', facility=self.facility)
+        self.classroom.add_member(learner2)
+        self._set_one_difficult(learner2)
+        self.group.remove_member(self.learner_user)
+        self.assignment_1.delete()
+        LessonAssignment.objects.create(
+            lesson=self.lesson,
+            assigned_by=self.coach_user,
+            collection=self.group,
+        )
+        self.client.login(username=self.coach_user.username, password='password')
+        response = self.client.get(reverse(
+            self.exercise_difficulties_basename + '-detail',
+            kwargs={'pk': self.content_ids[0]}),
+            data={'lesson_id': self.lesson.id, 'group_id': self.group.id, 'classroom_id': self.classroom.id})
+        self.assertEqual(len(response.data), 0)
+
+
+class QuizDifficultQuestionTestCase(APITestCase):
+
+    def setUp(self):
+        provision_device()
+        self.facility = Facility.objects.create(name='My Facility')
+        self.classroom = Classroom.objects.create(name='My Classroom', parent=self.facility)
+        self.group = LearnerGroup.objects.create(name='My Group', parent=self.classroom)
+
+        self.coach_user = FacilityUser.objects.create(username='admin', facility=self.facility)
+        self.coach_user.set_password('password')
+        self.coach_user.save()
+
+        self.learner_user = FacilityUser.objects.create(username='learner', facility=self.facility)
+        self.learner_user.set_password('password')
+        self.learner_user.save()
+
+        self.facility.add_coach(self.coach_user)
+        self.classroom.add_coach(self.coach_user)
+        self.classroom.add_member(self.learner_user)
+        self.group.add_member(self.learner_user)
+
+        self.quiz = Exam.objects.create(
+            title='My Lesson',
+            creator=self.coach_user,
+            collection=self.classroom,
+            question_count=5,
+        )
+        self.assignment_1 = ExamAssignment.objects.create(
+            exam=self.quiz,
+            assigned_by=self.coach_user,
+            collection=self.classroom,
+        )
+        self.quiz_difficulties_basename = 'kolibri:coach:quizdifficulties'
+        self.content_id = '25f32edcec565396a1840c5413c92451'
+
+    def test_learner_cannot_access(self):
+        learner_user = FacilityUser.objects.create(username='learner', facility=self.facility)
+        learner_user.set_password('password')
+        learner_user.save()
+        self.client.login(username='learner', password='password')
+        response = self.client.get(reverse(
+            self.quiz_difficulties_basename + '-detail', kwargs={'pk': self.quiz.id}))
+        self.assertEqual(response.status_code, 403)
+
+    def test_learner_cannot_access_by_group_id(self):
+        self.client.login(username='learner', password='password')
+        response = self.client.get(reverse(
+            self.quiz_difficulties_basename + '-detail',
+            kwargs={'pk': self.quiz.id}), data={'group_id': self.group.id})
+        self.assertEqual(response.status_code, 403)
+
+    def test_coach_no_progress(self):
+        self.client.login(username=self.coach_user.username, password='password')
+        response = self.client.get(reverse(
+            self.quiz_difficulties_basename + '-detail', kwargs={'pk': self.quiz.id}))
+        self.assertEqual(len(response.data), 0)
+
+    def test_coach_no_progress_by_group_id(self):
+        self.client.login(username=self.coach_user.username, password='password')
+        response = self.client.get(reverse(
+            self.quiz_difficulties_basename + '-detail',
+            kwargs={'pk': self.quiz.id}), data={'group_id': self.group.id})
+        self.assertEqual(len(response.data), 0)
+
+    def _set_one_difficult(self, user):
+        self.examlog = ExamLog.objects.create(
+            user=user,
+            exam=self.quiz,
+        )
+
+        ExamAttemptLog.objects.create(
+            examlog=self.examlog,
+            start_timestamp=datetime.datetime.now(),
+            end_timestamp=datetime.datetime.now(),
+            complete=True,
+            correct=0,
+            user=user,
+            item='test',
+            content_id=self.content_id,
+        )
+
+    def test_coach_one_difficult(self):
+        self._set_one_difficult(self.learner_user)
+        self.client.login(username=self.coach_user.username, password='password')
+        response = self.client.get(reverse(
+            self.quiz_difficulties_basename + '-detail', kwargs={'pk': self.quiz.id}))
+        self.assertEqual(len(response.data), 1)
+        self.assertEqual(response.data[0]['total'], 1)
+        self.assertEqual(response.data[0]['correct'], 0)
+
+    def test_coach_one_two_started_difficult(self):
+        self._set_one_difficult(self.learner_user)
+        self.client.login(username=self.coach_user.username, password='password')
+        response = self.client.get(reverse(
+            self.quiz_difficulties_basename + '-detail', kwargs={'pk': self.quiz.id}))
+        self.assertEqual(len(response.data), 1)
+        self.assertEqual(response.data[0]['total'], 1)
+        self.assertEqual(response.data[0]['correct'], 0)
+
+    def test_coach_one_difficult_repeated_assignment(self):
+        ExamAssignment.objects.create(
+            exam=self.quiz,
+            assigned_by=self.coach_user,
+            collection=self.group,
+        )
+        self._set_one_difficult(self.learner_user)
+        self.client.login(username=self.coach_user.username, password='password')
+        response = self.client.get(reverse(
+            self.quiz_difficulties_basename + '-detail', kwargs={'pk': self.quiz.id}))
+        self.assertEqual(len(response.data), 1)
+        self.assertEqual(response.data[0]['total'], 1)
+        self.assertEqual(response.data[0]['correct'], 0)
+
+    def test_coach_one_difficult_by_group_id(self):
+        self._set_one_difficult(self.learner_user)
+        self.client.login(username=self.coach_user.username, password='password')
+        response = self.client.get(reverse(
+            self.quiz_difficulties_basename + '-detail',
+            kwargs={'pk': self.quiz.id}), data={'group_id': self.group.id})
+        self.assertEqual(len(response.data), 1)
+        self.assertEqual(response.data[0]['total'], 1)
+        self.assertEqual(response.data[0]['correct'], 0)
+
+    def test_coach_two_difficult(self):
+        self._set_one_difficult(self.learner_user)
+        ExamAttemptLog.objects.create(
+            examlog=self.examlog,
+            start_timestamp=datetime.datetime.now(),
+            end_timestamp=datetime.datetime.now(),
+            complete=True,
+            correct=0,
+            user=self.learner_user,
+            item='notatest',
+            content_id=self.content_id,
+        )
+        self.client.login(username=self.coach_user.username, password='password')
+        response = self.client.get(reverse(
+            self.quiz_difficulties_basename + '-detail', kwargs={'pk': self.quiz.id}))
+        self.assertEqual(len(response.data), 2)
+        self.assertEqual(response.data[0]['total'], 1)
+        self.assertEqual(response.data[0]['correct'], 0)
+        self.assertEqual(response.data[1]['total'], 1)
+        self.assertEqual(response.data[1]['correct'], 0)
+
+    def test_coach_one_difficult_one_not(self):
+        self._set_one_difficult(self.learner_user)
+        ExamAttemptLog.objects.create(
+            examlog=self.examlog,
+            start_timestamp=datetime.datetime.now(),
+            end_timestamp=datetime.datetime.now(),
+            complete=True,
+            correct=1,
+            user=self.learner_user,
+            item='notatest',
+            content_id=self.content_id,
+        )
+        self.client.login(username=self.coach_user.username, password='password')
+        response = self.client.get(reverse(
+            self.quiz_difficulties_basename + '-detail', kwargs={'pk': self.quiz.id}))
+        self.assertEqual(len(response.data), 2)
+        self.assertTrue(any(map(lambda x: x['total'] == 1 and x['correct'] == 0, response.data)))
+        self.assertTrue(any(map(lambda x: x['total'] == 1 and x['correct'] == 1, response.data)))
+
+    def test_coach_difficult_by_group_id(self):
+        self._set_one_difficult(self.learner_user)
+        ExamAttemptLog.objects.create(
+            examlog=self.examlog,
+            start_timestamp=datetime.datetime.now(),
+            end_timestamp=datetime.datetime.now(),
+            complete=True,
+            correct=1,
+            user=self.learner_user,
+            item='notatest',
+            content_id=self.content_id,
+        )
+        self.client.login(username=self.coach_user.username, password='password')
+        response = self.client.get(reverse(
+            self.quiz_difficulties_basename + '-detail',
+            kwargs={'pk': self.quiz.id}), data={'group_id': self.group.id})
+        self.assertEqual(len(response.data), 2)
+        self.assertTrue(any(map(lambda x: x['total'] == 1 and x['correct'] == 0, response.data)))
+        self.assertTrue(any(map(lambda x: x['total'] == 1 and x['correct'] == 1, response.data)))
+
+    def test_coach_difficult_both_assigned_by_group_id(self):
+        self._set_one_difficult(self.learner_user)
+        learner2 = FacilityUser.objects.create(username='learner2', facility=self.facility)
+        self.classroom.add_member(learner2)
+        self._set_one_difficult(learner2)
+        self.client.login(username=self.coach_user.username, password='password')
+        response = self.client.get(reverse(
+            self.quiz_difficulties_basename + '-detail',
+            kwargs={'pk': self.quiz.id}), data={'group_id': self.group.id})
+        self.assertEqual(len(response.data), 1)
+        self.assertEqual(response.data[0]['total'], 1)
+        self.assertEqual(response.data[0]['correct'], 0)