Esempio n. 1
0
    def test_teammate_review_statuses(self):
        user_id = TestConstants.Users.USER1_ID
        reviews = {
            TestConstants.Users.USER2_ID: [
                mri(user_id,
                    self.REQUIRED_QUESTION_ID1,
                    peer=TestConstants.Users.USER2_ID,
                    answer='not empty'),
            ],
            TestConstants.Users.USER3_ID: [
                mri(user_id,
                    self.REQUIRED_QUESTION_ID1,
                    peer=TestConstants.Users.USER3_ID,
                    answer='not empty'),
                mri(user_id,
                    self.REQUIRED_QUESTION_ID2,
                    peer=TestConstants.Users.USER3_ID,
                    answer='other')
            ],
        }
        self._setup_review_items_store(reviews)

        stage_element = self.get_stage(self.go_to_view(student_id=user_id))

        expected_statuses = {
            TestConstants.Users.USER2_ID: ReviewState.INCOMPLETE,
            TestConstants.Users.USER3_ID: ReviewState.COMPLETED
        }
        self._assert_teammate_statuses(stage_element, expected_statuses)
Esempio n. 2
0
    def test_group_statuses(self):
        user_id = TestConstants.Users.USER1_ID
        reviews = {
            TestConstants.Groups.GROUP2_ID: [
                mri(user_id,
                    self.REQUIRED_QUESTION_ID1,
                    group=TestConstants.Groups.GROUP2_ID,
                    answer='not empty'),
            ],
            TestConstants.Groups.GROUP3_ID: [
                mri(user_id,
                    self.REQUIRED_QUESTION_ID1,
                    group=TestConstants.Groups.GROUP3_ID,
                    answer='not empty'),
                mri(user_id,
                    self.REQUIRED_QUESTION_ID2,
                    group=TestConstants.Groups.GROUP3_ID,
                    answer='other')
            ],
        }

        self._setup_review_items_store(reviews)

        self.project_api_mock.get_workgroup_reviewers = mock.Mock(
            return_value=[{
                "id": user_id
            }])
        stage_element = self.get_stage(self.go_to_view(student_id=user_id))

        expected_statuses = {
            TestConstants.Groups.GROUP2_ID: ReviewState.INCOMPLETE,
            TestConstants.Groups.GROUP3_ID: ReviewState.COMPLETED
        }
        self._assert_group_statuses(stage_element, expected_statuses)
Esempio n. 3
0
class TestEvaluationDisplayStage(EvaluationStagesBaseTestMixin, BaseStageTest):
    block_to_test = EvaluationDisplayStage

    def setUp(self):
        super(TestEvaluationDisplayStage, self).setUp()

        self.team_members_mock = self.make_patch(
            self.block_to_test, 'team_members',
            mock.PropertyMock(return_value=[]))
        self.get_reviews_mock = self.make_patch(self.block, 'get_reviews')
        self.get_reviewer_ids_mock = self.make_patch(self.block,
                                                     'get_reviewer_ids')

    def assert_proceeds_calculation(self, should_perform_expensive_part):
        if should_perform_expensive_part:
            self.assertTrue(self.get_reviews_mock.called)
            self.assertTrue(self.get_reviewer_ids_mock.called)
        else:
            self.assertFalse(self.get_reviews_mock.called)
            self.assertFalse(self.get_reviewer_ids_mock.called)

    def test_can_mark_complete_no_reviewers_returns_true(self):
        self.team_members_mock.return_value = []

        self.assertTrue(self.block.can_mark_complete)

    def test_can_mark_complete_no_questions_returns_true(self):
        with patch_obj(self.block_to_test,
                       'required_questions') as patched_required_questions:
            patched_required_questions.return_value = []

            self.assertTrue(self.block.can_mark_complete)

    @ddt.data(
        ([10], ["q1"], [], False),
        ([10], ["q1"], [mri(10, "q1")], True),
        ([10], ["q1"], [mri(10, "q1"), mri(10, "q2")], True),
        ([10], ["q1"], [mri(11, "q1")], False),
        ([10], ["q1"], [mri(10, "q2"), mri(11, "q1")], False),
        ([10, 11], ["q1"], [mri(10, "q1"), mri(11, "q1")], True),
        ([10, 11], ["q1", "q2"], [mri(10, "q1"), mri(11, "q1")], False),
    )
    @ddt.unpack
    def test_can_mark_compete_suite(self, reviewers, questions, reviews,
                                    expected_result):
        self.get_reviewer_ids_mock.return_value = reviewers
        self.get_reviews_mock.return_value = reviews

        with patch_obj(self.block_to_test, 'required_questions',
                       mock.PropertyMock()) as patched_required_questions:
            patched_required_questions.return_value = questions

            self.assertEqual(self.block.can_mark_complete, expected_result)
 def submit_review_items(reviewer_id, group_id, content_id, data):
     new_items = [
         mri(reviewer_id, question_id, content_id=content_id, answer=answer, group=group_id)
         for question_id, answer in data.iteritems()
         if len(answer) > 0
     ]
     store[group_id].extend(new_items)
 def _parse_review_item_string(review_item_string):
     splitted = review_item_string.split(':')
     reviewer, question, group = splitted[:3]
     if len(splitted) > 3:
         answer = splitted[3]
     else:
         answer = None
     return mri(int(reviewer), question, group=group, answer=answer)
Esempio n. 6
0
 def _parse_review_item_string(review_item_string):
     splitted = review_item_string.split(':')
     reviewer, question, group = splitted[:3]
     if len(splitted) > 3:
         answer = splitted[3]
     else:
         answer = None
     return mri(int(reviewer), question, group=group, answer=answer)
Esempio n. 7
0
 def submit_review_items(reviewer_id, group_id, content_id, data):
     new_items = [
         mri(reviewer_id,
             question_id,
             content_id=content_id,
             answer=answer,
             group=group_id)
         for question_id, answer in data.iteritems() if len(answer) > 0
     ]
     store[group_id].extend(new_items)
    def test_teammate_review_statuses(self):
        user_id = TestConstants.Users.USER1_ID
        reviews = {
            TestConstants.Users.USER2_ID: [
                mri(user_id, self.REQUIRED_QUESTION_ID1, peer=TestConstants.Users.USER2_ID, answer='not empty'),
            ],
            TestConstants.Users.USER3_ID: [
                mri(user_id, self.REQUIRED_QUESTION_ID1, peer=TestConstants.Users.USER3_ID, answer='not empty'),
                mri(user_id, self.REQUIRED_QUESTION_ID2, peer=TestConstants.Users.USER3_ID, answer='other')
            ],
        }
        self._setup_review_items_store(reviews)

        stage_element = self.get_stage(self.go_to_view(student_id=user_id))

        expected_statuses = {
            TestConstants.Users.USER2_ID: ReviewState.INCOMPLETE,
            TestConstants.Users.USER3_ID: ReviewState.COMPLETED
        }
        self._assert_teammate_statuses(stage_element, expected_statuses)
    def test_group_statuses(self):
        user_id = TestConstants.Users.USER1_ID
        reviews = {
            TestConstants.Groups.GROUP2_ID: [
                mri(user_id, self.REQUIRED_QUESTION_ID1, group=TestConstants.Groups.GROUP2_ID, answer='not empty'),
            ],
            TestConstants.Groups.GROUP3_ID: [
                mri(user_id, self.REQUIRED_QUESTION_ID1, group=TestConstants.Groups.GROUP3_ID, answer='not empty'),
                mri(user_id, self.REQUIRED_QUESTION_ID2, group=TestConstants.Groups.GROUP3_ID, answer='other')
            ],
        }

        self._setup_review_items_store(reviews)

        self.project_api_mock.get_workgroup_reviewers = mock.Mock(return_value=[{"id": user_id}])
        stage_element = self.get_stage(self.go_to_view(student_id=user_id))

        expected_statuses = {
            TestConstants.Groups.GROUP2_ID: ReviewState.INCOMPLETE,
            TestConstants.Groups.GROUP3_ID: ReviewState.COMPLETED
        }
        self._assert_group_statuses(stage_element, expected_statuses)
class TestProjectApi(TestCase, TestWithPatchesMixin):
    api_server_address = 'http://localhost'

    def setUp(self):
        self.project_api = TypedProjectAPI(self.api_server_address,
                                           dry_run=False)

    def _patch_send_request(self, calls_and_results, missing_callback=None):
        # pylint: disable=unused-argument
        def side_effect(method,
                        url_parts,
                        data=None,
                        query_params=None,
                        no_trailing_slash=False):
            if url_parts in calls_and_results:
                return calls_and_results[url_parts]
            if 'default' in calls_and_results:
                return calls_and_results['default']
            if missing_callback:
                return missing_callback(url_parts)
            return None

        return mock.patch.object(self.project_api, 'send_request',
                                 mock.Mock(side_effect=side_effect))

    def _patch_do_send_request(self, urls_and_results, missing_callback=None):
        # pylint: disable=unused-argument
        def side_effect(method, url, data=None):
            if url in urls_and_results:
                return urls_and_results[url]
            if 'default' in urls_and_results:
                return urls_and_results['default']
            if missing_callback:
                return missing_callback(url)
            raise Exception("Response not found")

        return mock.patch.object(self.project_api, '_do_send_request',
                                 mock.Mock(side_effect=side_effect))

    @ddt.data(
        (["part1", "part2"
          ], None, False, api_server_address + "/part1/part2/", {
              'error': True
          }),
        (["part1", "part2"], None, True, api_server_address + "/part1/part2", {
            'success': True
        }),
        (["part1", 1234, "part2"
          ], None, True, api_server_address + "/part1/1234/part2", {
              'error': True
          }),
        (["part1", "part2", 1234
          ], None, True, api_server_address + "/part1/part2/1234", {
              'success': True
          }),
        ([api_server_address, "part1", "part2"
          ], None, False, api_server_address + "/part1/part2/", {
              'success': True
          }),
        (["part1", "part2", "part3"
          ], None, False, api_server_address + "/part1/part2/part3/", {
              'error': True
          }),
        (["part1"], {
            'qwe': 'rty'
        }, False, api_server_address + "/part1/?qwe=rty", {
            'success': True,
            'data': [1, 2, 3]
        }),
        ([api_server_address, "part1"], {
            'qwe': 'rty'
        }, False, api_server_address + "/part1/?qwe=rty", {}),
        (["part1"], {
            'qwe': 'rty',
            'asd': 'zxc'
        }, False, api_server_address + "/part1/?qwe=rty&asd=zxc", {}),
        (["part1"], {
            'qwe': 'rty',
            'asd': 'zxc'
        }, True, api_server_address + "/part1?qwe=rty&asd=zxc", {}),
    )
    @ddt.unpack
    def test_send_request_no_data(self, url_parts, query_params,
                                  no_trailing_slash, expected_url,
                                  expected_response):
        response = mock.Mock()
        response.read.return_value = json.dumps(expected_response)

        method = mock.Mock(return_value=response)
        result = self.project_api.send_request(
            method,
            url_parts,
            query_params=query_params,
            no_trailing_slash=no_trailing_slash)
        method.assert_called_once_with(expected_url)
        self.assertEqual(result, expected_response)

    # pylint: disable=too-many-arguments
    @ddt.data(
        (["part1", "part2"
          ], None, [123], False, api_server_address + "/part1/part2/", {
              'error': True
          }),
        (["part1", "part2"
          ], None, 'qwerty', True, api_server_address + "/part1/part2", {
              'success': True
          }),
        (["part1", "part2", "part3"], None, {
            'data': 11
        }, False, api_server_address + "/part1/part2/part3/", {
            'error': True
        }),
        (["part1"], {
            'qwe': 'rty'
        }, {
            'var1': 1,
            'var2': 2
        }, False, api_server_address + "/part1/?qwe=rty", {
            'success': True,
            'data': [1, 2, 3]
        }),
        (["part1"], {
            'qwe': 'rty',
            'asd': 'zxc'
        }, {
            'stage': 1,
            'activity': 2
        }, False, api_server_address + "/part1/?qwe=rty&asd=zxc", {}),
        (["part1"], {
            'qwe': 'rty',
            'asd': 'zxc'
        }, {
            'data': None
        }, True, api_server_address + "/part1?qwe=rty&asd=zxc", {}),
    )
    @ddt.unpack
    def test_send_request_with_data(self, url_parts, query_params, data,
                                    no_trailing_slash, expected_url,
                                    expected_response):
        response = mock.Mock()
        response.read.return_value = json.dumps(expected_response)

        method = mock.Mock(return_value=response)
        result = self.project_api.send_request(
            method,
            url_parts,
            data=data,
            query_params=query_params,
            no_trailing_slash=no_trailing_slash)
        method.assert_called_once_with(expected_url, data)
        self.assertEqual(result, expected_response)

    def test_dry_run_does_not_send_request(self):
        method = mock.Mock()
        proj_api = TypedProjectAPI(self.api_server_address, True)
        result = proj_api.send_request(method, ('123', '34'))
        method.assert_not_called()
        self.assertEqual(result, {})

    def test_send_delete_request_returns_none(self):
        with mock.patch(
                'group_project_v2.project_api.api_implementation.DELETE'
        ) as patched_delete:
            result = self.project_api.send_request(patched_delete,
                                                   ('123', '456'))
            self.assertEqual(result, None)

            patched_delete.assert_called_once_with(self.api_server_address +
                                                   '/123/456/')

    @ddt.data(
        ('user1', 'course1', 'xblock:block-1', []),
        ('user1', 'course1', 'xblock:block-1', [1, 5]),
        ('user2', 'course-2', 'xblock:html-block-1', [1, 5, 6]),
        ('user7', 'course-15', 'xblock:construction-block-743', [6, 10, 15]),
    )
    @ddt.unpack
    def test_get_workgroups_to_review(self, user_id, course_id, xblock_id,
                                      assignment_ids):
        def assignment_data_by_id(a_id):
            return {"id": a_id, 'data': 'data' + str(a_id)}

        with mock.patch.object(self.project_api, 'get_review_assignment_groups') as review_assignment_groups, \
                mock.patch.object(self.project_api, 'get_workgroups_for_assignment') as workgroups_for_assignment:

            review_assignment_groups.return_value = [{
                "id": assignment_id
            } for assignment_id in assignment_ids]
            workgroups_for_assignment.side_effect = lambda a_id: [
                assignment_data_by_id(a_id)
            ]

            response = self.project_api.get_workgroups_to_review(
                user_id, course_id, xblock_id)

            review_assignment_groups.assert_called_once_with(
                user_id, course_id, xblock_id)
            self.assertEqual(
                workgroups_for_assignment.mock_calls,
                [mock.call(assignment_id) for assignment_id in assignment_ids])

            self.assertEqual(response, [
                assignment_data_by_id(assignment_id)
                for assignment_id in assignment_ids
            ])

    @ddt.data(
        (1, 'content1', [], []),
        (2, 'content2', [{
            'data': {
                'xblock_id': 'content2'
            },
            'url': 'url1'
        }], ['url1']),
        (3, 'content3', [{
            'data': {
                'xblock_id': 'content2'
            },
            'url': 'url1'
        }, {
            'data': {
                'xblock_id': 'content3'
            },
            'url': 'url2'
        }, {
            'data': {
                'xblock_id': 'content3'
            },
            'url': 'url3'
        }], ['url2', 'url3']),
    )
    @ddt.unpack
    def test_workgroup_reviewers(self, group_id, content_id,
                                 review_assignments, expected_urls):
        calls_and_results = {
            (WORKGROUP_API, group_id, 'groups'): review_assignments
        }

        def missing_callback(url_parts):  # pylint: disable=unused-argument
            return {'users': [1, 2, 3]}

        with self._patch_send_request(
                calls_and_results, missing_callback) as patched_send_request:
            response = self.project_api.get_workgroup_reviewers(
                group_id, content_id)

            self.assertEqual(patched_send_request.mock_calls, [
                mock.call(GET, (WORKGROUP_API, group_id, 'groups'),
                          no_trailing_slash=True)
            ] + [
                mock.call(GET, (expected_url, 'users'))
                for expected_url in expected_urls
            ])

            self.assertEqual(response, [1, 2, 3] * len(expected_urls))

    @ddt.data(
        (1, 2, [mri(1, 'qwe', peer=2),
                mri(1, 'asd', peer=3)], [mri(1, 'qwe', peer=2)]),
        (5, 3, [mri(5, 'qwe', peer=3),
                mri(5, 'asd', peer=3)],
         [mri(5, 'qwe', peer=3), mri(5, 'asd', peer=3)]),
        (11, 12, [mri(11, 'qwe', peer=3),
                  mri(11, 'asd', peer=4)], []),
        (11, 12, [mri(15, 'qwe', peer=12),
                  mri(18, 'asd', peer=12)], []),
    )
    @ddt.unpack
    def test_get_peer_review_items(self, reviewer_id, peer_id, review_items,
                                   expected_result):
        with mock.patch.object(
                self.project_api,
                'get_peer_review_items_for_group') as patched_get_review_items:
            patched_get_review_items.return_value = review_items
            result = self.project_api.get_peer_review_items(
                reviewer_id, peer_id, 'group_id', 'content_id')

            self.assertEqual(result, expected_result)
            patched_get_review_items.assert_called_once_with(
                'group_id', 'content_id')

    @ddt.data(
        (1, [mri(2, 'qwe', peer=1),
             mri(5, 'asd', peer=1)],
         [mri(2, 'qwe', peer=1), mri(5, 'asd', peer=1)]),
        (5, [mri(7, 'qwe', peer=5),
             mri(7, 'asd', peer=5)],
         [mri(7, 'qwe', peer=5), mri(7, 'asd', peer=5)]),
        (11, [mri(16, 'qwe', peer=3),
              mri(18, 'asd', peer=4)], []),
        (11, [mri(16, 'qwe', peer=3),
              mri(18, 'question1', peer=11)], [mri(18, 'question1', peer=11)]),
    )
    @ddt.unpack
    def test_get_user_peer_review_items(self, user_id, review_items,
                                        expected_result):
        with mock.patch.object(
                self.project_api,
                'get_peer_review_items_for_group') as patched_get_review_items:
            patched_get_review_items.return_value = review_items
            result = self.project_api.get_user_peer_review_items(
                user_id, 'group_id', 'content_id')

            self.assertEqual(result, expected_result)
            patched_get_review_items.assert_called_once_with(
                'group_id', 'content_id')

    # pylint: disable=too-many-function-args
    @ddt.data(
        (1, 'content_1', [
            mri(1, 'qwe', peer=7, content_id='content_1'),
            mri(1, 'asd', peer=7, content_id='content_1')
        ], [
            mri(1, 'qwe', peer=7, content_id='content_1'),
            mri(1, 'asd', peer=7, content_id='content_1')
        ]),
        (5, 'content_2', [
            mri(5, 'qwe', peer=14, content_id='content_2'),
            mri(5, 'asd', peer=19, content_id='content_2')
        ], [
            mri(5, 'qwe', peer=14, content_id='content_2'),
            mri(5, 'asd', peer=19, content_id='content_2')
        ]),
        (11, 'content_3', [
            mri(11, 'qwe', peer=3, content_id='content_2'),
            mri(16, 'asd', peer=4, content_id='content_3')
        ], []),
        (11, 'content_4', [
            mri(12, 'qwe', peer=18, content_id='content_4'),
            mri(11, 'question1', peer=18, content_id='content_4')
        ], [mri(11, 'question1', peer=18, content_id='content_4')]),
    )
    @ddt.unpack
    def test_get_workgroup_review_items(self, reviewer_id, content_id,
                                        review_items, expected_result):
        with mock.patch.object(self.project_api,
                               'get_workgroup_review_items_for_group'
                               ) as patched_get_review_items:
            patched_get_review_items.return_value = review_items
            result = self.project_api.get_workgroup_review_items(
                reviewer_id, 'group_id', content_id)

            self.assertEqual(result, expected_result)
            patched_get_review_items.assert_called_once_with(
                'group_id', content_id)

    def assert_project_data(self, project_data, expected_values):
        attrs_to_test = [
            "id", "url", "created", "modified", "course_id", "content_id",
            "organization", "workgroups"
        ]
        for attr in attrs_to_test:
            self.assertEqual(getattr(project_data, attr),
                             expected_values[attr])

    def test_get_project_details(self):
        calls_and_results = {
            (PROJECTS_API, 1):
            canned_responses.Projects.project1['results'][0],
            (PROJECTS_API, 2): canned_responses.Projects.project2['results'][0]
        }

        expected_calls = [
            mock.call(GET, (PROJECTS_API, 1), no_trailing_slash=True),
            mock.call(GET, (PROJECTS_API, 2), no_trailing_slash=True)
        ]

        with self._patch_send_request(
                calls_and_results) as patched_send_request:
            project1 = self.project_api.get_project_details(1)
            project2 = self.project_api.get_project_details(2)

            self.assertEqual(patched_send_request.mock_calls, expected_calls)

        self.assert_project_data(
            project1, canned_responses.Projects.project1['results'][0])
        self.assert_project_data(
            project2, canned_responses.Projects.project2['results'][0])

    @ddt.data(
        ('course1', 'content1'),
        ('course2', 'content2'),
    )
    @ddt.unpack
    def test_get_project_by_content_id(self, course_id, content_id):
        expected_parameters = {
            'course_id': course_id,
            'content_id': content_id
        }
        calls_and_results = {
            (PROJECTS_API, ): canned_responses.Projects.project1
        }

        with self._patch_send_request(
                calls_and_results) as patched_send_request:
            project = self.project_api.get_project_by_content_id(
                course_id, content_id)
            self.assert_project_data(
                project, canned_responses.Projects.project1['results'][0])

            patched_send_request.assert_called_once_with(
                GET, (PROJECTS_API, ), query_params=expected_parameters)

    def test_get_project_by_content_id_fail_if_more_than_one(self):
        calls_and_results = {
            (PROJECTS_API, ): canned_responses.Projects.two_projects
        }
        with self._patch_send_request(calls_and_results), \
                self.assertRaises(AssertionError):
            self.project_api.get_project_by_content_id('irrelevant',
                                                       'irrelevant')

    def test_get_project_by_content_id_return_none_if_not_found(self):
        calls_and_results = {
            (PROJECTS_API, ): canned_responses.Projects.zero_projects
        }
        with self._patch_send_request(calls_and_results):
            project = self.project_api.get_project_by_content_id(
                'irrelevant', 'irrelevant')
            self.assertIsNone(project)

    @ddt.data(
        (1, canned_responses.Workgroups.workgroup1),
        (2, canned_responses.Workgroups.workgroup2),
    )
    @ddt.unpack
    def test_get_workgroup_by_id(self, group_id, expected_result):
        calls_and_results = {
            (WORKGROUP_API, 1): canned_responses.Workgroups.workgroup1,
            (WORKGROUP_API, 2): canned_responses.Workgroups.workgroup2
        }

        with self._patch_send_request(
                calls_and_results) as patched_send_request:
            workgroup = self.project_api.get_workgroup_by_id(group_id)
            patched_send_request.assert_called_once_with(
                GET, (WORKGROUP_API, group_id))

        self.assertEqual(workgroup.id, expected_result['id'])
        self.assertEqual(workgroup.project, expected_result['project'])
        self.assertEqual(len(workgroup.users), len(expected_result['users']))
        self.assertEqual([user.id for user in workgroup.users],
                         [user['id'] for user in expected_result['users']])

    @ddt.data(('course1', 'content1'), ('course1', 'content2'),
              ('course2', 'content3'))
    @ddt.unpack
    def test_get_completions_by_content_id(self, course_id, content_id):
        def build_url(course_id, content_id):
            return self.project_api.build_url(
                (COURSES_API, course_id, 'completions'),
                query_params={'content_id': content_id})

        urls_and_results = {
            build_url('course1', 'content1'):
            canned_responses.Completions.non_paged1,
            build_url('course1', 'content2'):
            canned_responses.Completions.non_paged2,
            build_url('course2', 'content3'):
            canned_responses.Completions.empty,
        }

        expected_url = build_url(course_id, content_id)
        expected_data = urls_and_results.get(expected_url)

        with self._patch_do_send_request(
                urls_and_results) as patched_do_send_request:
            completions = list(
                self.project_api.get_completions_by_content_id(
                    course_id, content_id))
            patched_do_send_request.assert_called_once_with(
                GET, expected_url, None)

        self.assertEqual(len(completions), len(expected_data['results']))
        self.assertEqual([comp.id for comp in completions],
                         [data['id'] for data in expected_data['results']])

    def test_get_completions_by_content_id_paged(self):
        def build_url(course_id, content_id, page_num=None):
            query_params = {'content_id': content_id}
            if page_num:
                query_params['page'] = page_num
            return self.project_api.build_url(
                (COURSES_API, course_id, 'completions'),
                query_params=query_params)

        course, content = 'course1', 'content1'

        urls_and_results = {
            build_url(course, content):
            canned_responses.Completions.paged_page1,
            build_url(course, content, 1):
            canned_responses.Completions.paged_page1,
            build_url(course, content, 2):
            canned_responses.Completions.paged_page2,
            build_url(course, content, 3):
            canned_responses.Completions.paged_page3,
        }
        all_responses = []
        pages = [
            canned_responses.Completions.paged_page1,
            canned_responses.Completions.paged_page2,
            canned_responses.Completions.paged_page3
        ]
        for page in pages:
            all_responses.extend(page['results'])

        expected_calls = [
            mock.call(GET, build_url(course, content), None),
            mock.call(GET, canned_responses.Completions.paged_page1['next'],
                      None),
            mock.call(GET, canned_responses.Completions.paged_page2['next'],
                      None),
        ]

        with self._patch_do_send_request(
                urls_and_results) as patched_do_send_request:
            completions = list(
                self.project_api.get_completions_by_content_id(
                    course, content))
            self.assertEqual(patched_do_send_request.mock_calls,
                             expected_calls)

        self.assertEqual(len(completions), len(all_responses))
        self.assertEqual([comp.id for comp in completions],
                         [data['id'] for data in all_responses])
        self.assertEqual([comp.user_id for comp in completions],
                         [data['user_id'] for data in all_responses])

    @ddt.data(
        ({'foo', 'bar', 'baz'}, 1234, 4321),
        ({'foo', 'bar'}, 1, 2),
        ({'foo'}, 2, 1),
        (set(), 4321, 1234),
    )
    @ddt.unpack
    def test_get_user_roles_for_course(self, roles, user_id, course_id):
        self.project_api.send_request = mock.Mock(return_value=[{
            'role': role,
            'id': user_id
        } for role in roles])
        response = self.project_api.get_user_roles_for_course(
            user_id, course_id)
        self.assertEqual(response, roles)
        self.project_api.send_request.assert_called_once_with(
            GET, ('api/server/courses', course_id, 'roles'),
            query_params={'user_id': user_id})
Esempio n. 11
0
class TestPeerReviewStageReviewStatus(ReviewStageBaseTest,
                                      ReviewStageUserCompletionStatsMixin,
                                      BaseStageTest):
    block_to_test = PeerReviewStage

    def setUp(self):
        super(TestPeerReviewStageReviewStatus, self).setUp()
        self.activity_mock.is_ta_graded = False

    def _set_project_api_responses(self, workgroups, review_items):
        def workgroups_side_effect(user_id, _course_id, _content_id):
            return workgroups.get(user_id, None)

        def review_items_side_effect(workgroup_id, _content_id):
            return review_items.get(workgroup_id, [])

        self.project_api_mock.get_workgroups_to_review.side_effect = workgroups_side_effect
        self.project_api_mock.get_workgroup_review_items_for_group.side_effect = review_items_side_effect

    @staticmethod
    def _parse_review_item_string(review_item_string):
        splitted = review_item_string.split(':')
        reviewer, question, group = splitted[:3]
        if len(splitted) > 3:
            answer = splitted[3]
        else:
            answer = None
        return mri(int(reviewer), question, group=group, answer=answer)

    @ddt.data(
        ([GROUP_ID], ["q1"], {
            GROUP_ID: []
        }, ReviewState.NOT_STARTED),
        ([GROUP_ID], ["q1"], {
            GROUP_ID: [mri(USER_ID, "q1", group=GROUP_ID, answer='1')]
        }, ReviewState.COMPLETED),
        ([GROUP_ID], ["q1"], {
            GROUP_ID: [mri(OTHER_USER_ID, "q1", group=GROUP_ID, answer='1')]
        }, ReviewState.NOT_STARTED),
        ([GROUP_ID], ["q1", "q2"], {
            GROUP_ID: [
                mri(USER_ID, "q1", group=GROUP_ID, answer='1'),
                mri(OTHER_USER_ID, "q1", group=GROUP_ID, answer='1')
            ]
        }, ReviewState.INCOMPLETE),
        ([GROUP_ID], ["q1"], {
            GROUP_ID: [
                mri(USER_ID, "q1", group=GROUP_ID, answer='2'),
                mri(USER_ID, "q2", group=GROUP_ID, answer="1")
            ]
        }, ReviewState.COMPLETED),
        ([GROUP_ID], ["q1", "q2"], {
            GROUP_ID: [mri(USER_ID, "q1", group=GROUP_ID, answer='3')]
        }, ReviewState.INCOMPLETE),
        ([GROUP_ID], ["q1"], {
            GROUP_ID: [
                mri(USER_ID, "q2", group=GROUP_ID, answer='4'),
                mri(USER_ID, "q1", group=OTHER_GROUP_ID, answer='5')
            ]
        }, ReviewState.NOT_STARTED),
        ([GROUP_ID, OTHER_GROUP_ID], ["q1"], {
            GROUP_ID: [mri(USER_ID, "q1", group=GROUP_ID, answer='6')],
            OTHER_GROUP_ID:
            [mri(USER_ID, "q1", group=OTHER_GROUP_ID, answer='7')]
        }, ReviewState.COMPLETED),
        ([GROUP_ID, OTHER_GROUP_ID], ["q1", "q2"], {
            GROUP_ID: [mri(USER_ID, "q1", group=GROUP_ID, answer='7')],
            OTHER_GROUP_ID:
            [mri(USER_ID, "q1", group=OTHER_GROUP_ID, answer='8')]
        }, ReviewState.INCOMPLETE),
        ([GROUP_ID, OTHER_GROUP_ID], ["q1", "q2"], {
            GROUP_ID: [
                mri(USER_ID, "q1", group=GROUP_ID, answer='7'),
                mri(USER_ID, "q2", group=GROUP_ID, answer='7')
            ],
            OTHER_GROUP_ID: [
                mri(USER_ID, "q1", group=OTHER_GROUP_ID, answer='8'),
                mri(USER_ID, "q2", group=GROUP_ID, answer='7')
            ]
        }, ReviewState.INCOMPLETE),
    )
    @ddt.unpack
    def test_review_status(self, groups, questions, reviews, expected_result):
        def get_reviews(group_id, _component_id):
            return reviews.get(group_id, [])

        expected_calls = [
            mock.call(group_id, self.block.activity_content_id)
            for group_id in groups
        ]
        self.project_api_mock.get_workgroup_review_items_for_group.side_effect = get_reviews

        with patch_obj(self.block_to_test, 'review_subjects', mock.PropertyMock()) as patched_review_subjects, \
                patch_obj(self.block_to_test, 'required_questions', mock.PropertyMock()) as patched_questions:
            patched_review_subjects.return_value = [
                WorkgroupDetails(id=rev_id) for rev_id in groups
            ]
            patched_questions.return_value = [
                make_question(q_id, 'irrelevant') for q_id in questions
            ]

            self.assertEqual(self.block.review_status(), expected_result)

        self.assertEqual(
            self.project_api_mock.get_workgroup_review_items_for_group.
            mock_calls, expected_calls)

    @ddt.data(
        # no reviews - not started
        ([GROUP_ID], ["q1"], [], (set(), set())),
        # some reviews - partially completed
        ([GROUP_ID], ["q1", "q2"], ["1:q1:10:a"], (set(), {1})),
        # some reviews - partially completed; other reviewers reviews doe not affect the state
        ([GROUP_ID], ["q1", "q2"], ["1:q1:10:a", "2:q1:10:a", "2:q2:10:a"],
         (set(), {1})),
        # all reviews - completed
        ([GROUP_ID], ["q1", "q2"], ["1:q1:10:a", "1:q2:10:a"], ({1}, set())),
        # no reviews - not started
        ([GROUP_ID, OTHER_GROUP_ID], ["q1", "q2"], [], (set(), set())),
        # some reviews - partially completed
        ([GROUP_ID, OTHER_GROUP_ID], ["q1", "q2"], ["1:q1:10:a", "1:q2:10:b"],
         (set(), {1})),
        # all reviews, but some answers are None - partially completed
        ([GROUP_ID, OTHER_GROUP_ID], ["q1"], ["1:q1:10:a", "1:q1:11"],
         (set(), {1})),
        # all reviews, but some answers are empty - partially completed
        ([GROUP_ID, OTHER_GROUP_ID], ["q1"], ["1:q1:10:a", "1:q1:11:"],
         (set(), {1})),
        # all reviews - completed
        ([GROUP_ID, OTHER_GROUP_ID], ["q1"], ["1:q1:10:a", "1:q1:11:b"],
         ({1}, set())),
    )
    @ddt.unpack
    def test_users_completion_single_user(self, groups_to_review, questions,
                                          review_items, expected_result):
        user_id = 1
        review_items = [
            self._parse_review_item_string(review_item_str)
            for review_item_str in review_items
        ]

        self._set_project_api_responses(
            {user_id: [mk_wg(group_id) for group_id in groups_to_review]},
            {GROUP_ID: review_items})

        self.assert_users_completion(expected_result, questions, [user_id])

        # checks if caching is ok
        expected_calls = [
            mock.call(group_id, self.block.activity_content_id)
            for group_id in groups_to_review
        ]
        self.assertEqual(
            self.project_api_mock.get_workgroup_review_items_for_group.
            mock_calls, expected_calls)

    @ddt.data(
        # no reviews - both not started
        ([GROUP_ID], ["q1", "q2"], [], (set(), set())),
        # u1 some reviews - u1 partially completed
        ([GROUP_ID], ["q1", "q2"], ["1:q1:10:a"], (set(), {1})),
        # u1 all reviews - u1 completed, u2 - not started
        ([GROUP_ID], ["q1", "q2"], ["1:q1:10:a", "1:q2:10:b"], ({1}, set())),
        # u1 some reviews, u2 some reviews - both partially completed
        ([GROUP_ID], ["q1", "q2"], ["1:q1:10:a", "2:q1:10:b"],
         (set(), {1, 2})),
        # u1 all reviews, u2 some reviews - u1 completed, u2 partially completed
        ([GROUP_ID], ["q1", "q2"], ["1:q1:10:a", "1:q2:10:b", "2:q1:10:c"],
         ({1}, {2})),
        # both all reviews - both completed
        ([GROUP_ID], ["q1", "q2"
                      ], ["1:q1:10:a", "1:q2:10:b", "2:q1:10:c", "2:q2:10:d"],
         ({1, 2}, set())),
    )
    @ddt.unpack
    def test_users_completion_same_groups(self, groups_to_review, questions,
                                          review_items, expected_result):
        target_users = [1, 2]
        review_items = [
            self._parse_review_item_string(review_item_str)
            for review_item_str in review_items
        ]

        self._set_project_api_responses(
            {
                user_id: [mk_wg(group_id) for group_id in groups_to_review]
                for user_id in target_users
            }, {GROUP_ID: review_items})

        self.assert_users_completion(expected_result, questions, target_users)
        # checks if caching is ok
        self.project_api_mock.get_workgroup_review_items_for_group.assert_called_once_with(
            GROUP_ID, self.block.activity_content_id)

    @ddt.data(
        # no reviews - both not started
        ([1, 3], {
            GROUP_ID: [1, 3]
        }, ['q1'], {}, (set(), set())),
        # u1 some reviews - u1 partially, u4 - not started
        ([1, 4], {
            GROUP_ID: [1, 4]
        }, ['q1', 'q2'], {
            GROUP_ID: ['1:q1:10:b']
        }, (set(), {1})),
        # u2 all reviews - u2 completed, u3 - not started
        ([2, 3], {
            GROUP_ID: [2, 3]
        }, ['q1'], {
            GROUP_ID: ['2:q1:10:a']
        }, ({2}, set())),
        # u3 all reviews - u3 completed, u1, u2 not started
        ([1, 2, 3], {
            OTHER_GROUP_ID: [1, 2, 3]
        }, ['q1'], {
            OTHER_GROUP_ID: ['3:q1:11:a']
        }, ({3}, set())),
        # u1, u2, u3 all reviews - u1, u2, u3 completed
        ([1, 2, 3], {
            GROUP_ID: [1, 2],
            OTHER_GROUP_ID: [3]
        }, ['q1'], {
            GROUP_ID: ['1:q1:10:a', '2:q1:10:b'],
            OTHER_GROUP_ID: ['3:q1:11:c']
        }, ({1, 2, 3}, set())),
        # u1, u2, u3 all reviews, u4 no reviews - u1, u2, u3 completed, u4 not started
        ([1, 2, 3, 4], {
            GROUP_ID: [1, 2],
            OTHER_GROUP_ID: [3, 4]
        }, ['q1'], {
            GROUP_ID: ['1:q1:10:a', '2:q1:10:b'],
            OTHER_GROUP_ID: ['3:q1:11:c']
        }, ({1, 2, 3}, set())),
        # u1 all reviews, u3 some reviews - u1 completed, u3 partially completed
        ([1, 3], {
            GROUP_ID: [1, 3],
            OTHER_GROUP_ID: [3]
        }, ['q1', 'q2'], {
            GROUP_ID: ['1:q1:10:a', '1:q2:10:b', '3:q1:10:c', '3:q2:10:d'],
            OTHER_GROUP_ID: ['3:q1:11:e']
        }, ({1}, {3})),
        # u1 all reviews, u3 some reviews - u1 completed, u3 partially completed
        ([1, 3], {
            GROUP_ID: [1, 3],
            OTHER_GROUP_ID: [3]
        }, ['q1', 'q2'], {
            GROUP_ID: ['1:q1:10:a', '1:q2:10:b', '3:q1:10:c'],
            OTHER_GROUP_ID: ['3:q1:11:e', '3:q2:11:d']
        }, ({1}, {3})),
    )
    @ddt.unpack
    # pylint:disable=too-many-locals
    def test_users_completion_multiple_groups(self, target_users,
                                              group_reviewers, questions,
                                              review_items, expected_result):
        groups_to_review = defaultdict(list)
        for group_id, reviewers in group_reviewers.iteritems():
            for reviewer in reviewers:
                groups_to_review[reviewer].append(mk_wg(group_id))

        self._set_project_api_responses(
            groups_to_review, {
                group_id:
                [self._parse_review_item_string(item) for item in items]
                for group_id, items in review_items.iteritems()
            })

        self.assert_users_completion(expected_result, questions, target_users)
        # checks if caching is ok
        expected_calls = [
            mock.call(group_id, self.block.activity_content_id)
            for group_id in group_reviewers.keys()
        ]
        self.assertEqual(
            self.project_api_mock.get_workgroup_review_items_for_group.
            mock_calls, expected_calls)

    @ddt.data(
        # no reviewers - not started
        ([], ['q1'], {}, StageState.NOT_STARTED),
        # no reviews - not started
        ([1], ['q1'], {}, StageState.NOT_STARTED),
        # complete review for one group - completed
        ([1], ['q1'], {
            GROUP_ID: ['1:q1:10:a']
        }, StageState.COMPLETED),
        # partial review for one group - partially complete
        ([1], ['q1', 'q2'], {
            GROUP_ID: ['1:q1:10:a']
        }, StageState.INCOMPLETE),
        # complete review for one group with multiple questions - completed
        ([1], ['q1', 'q2'], {
            GROUP_ID: ['1:q1:10:a', '1:q2:10:b']
        }, StageState.COMPLETED),
        # multiple reviewers - no reviews - not started
        ([1, 2], ['q1'], {}, StageState.NOT_STARTED),
        # multiple reviewers - one partial, other not started - partially complete
        ([1, 2], ['q1', 'q2'], {
            GROUP_ID: ['1:q1:10:a']
        }, StageState.INCOMPLETE),
        # multiple reviewers - both partial - partially complete
        ([1, 2], ['q1', 'q2'], {
            GROUP_ID: ['1:q1:10:a', '2:q2:10:b']
        }, StageState.INCOMPLETE),
        # multiple reviewers - one complete, one partial - partially complete
        ([1, 2], ['q1', 'q2'], {
            GROUP_ID: ['1:q1:10:a', '1:q2:10:b', '2:q2:10:b']
        }, StageState.INCOMPLETE),
        # multiple reviewers - both complete - complete
        ([1, 2], ['q1', 'q2'], {
            GROUP_ID: ['1:q1:10:a', '1:q2:10:b', '2:q1:10:c', '2:q2:10:d']
        }, StageState.COMPLETED),
    )
    @ddt.unpack
    def test_get_external_group_status(self, reviewers, questions,
                                       review_items, expected_result):
        group = mk_wg(GROUP_ID, [{"id": 1}])
        self.project_api_mock.get_workgroup_reviewers.return_value = [{
            'id':
            user_id
        } for user_id in reviewers]

        self._set_project_api_responses(
            group, {
                group.id:
                [self._parse_review_item_string(item) for item in items]
                for group_id, items in review_items.iteritems()
            })

        self.assert_group_completion(group, questions, expected_result)
        self.project_api_mock.get_workgroup_reviewers.assert_called_once_with(
            group.id, self.block.activity_content_id)

    @ddt.data(
        # no ta reviewers - not started
        ([], ['q1'], {}, StageState.NOT_STARTED),
        # no reviews - not started
        ([1], ['q1'], {}, StageState.NOT_STARTED),
        # complete review for one group - completed
        ([1], ['q1'], {
            GROUP_ID: ['1:q1:10:a']
        }, StageState.COMPLETED),
        # partial review for one group - partially complete
        ([1], ['q1', 'q2'], {
            GROUP_ID: ['1:q1:10:a']
        }, StageState.INCOMPLETE),
        # complete review for one group with multiple questions - completed
        ([1], ['q1', 'q2'], {
            GROUP_ID: ['1:q1:10:a', '1:q2:10:b']
        }, StageState.COMPLETED),
        # multiple TAs - no reviews - not started
        ([1, 2], ['q1'], {}, StageState.NOT_STARTED),
        # multiple TAs - one partial, other not started - partially complete
        ([1, 2], ['q1', 'q2'], {
            GROUP_ID: ['1:q1:10:a']
        }, StageState.INCOMPLETE),
        # multiple TAs - both partial - partially complete
        ([1, 2], ['q1', 'q2'], {
            GROUP_ID: ['1:q1:10:a', '2:q2:10:b']
        }, StageState.INCOMPLETE),
        # multiple TAs - one complete, other partial - complete
        ([1, 2], ['q1', 'q2'], {
            GROUP_ID: ['1:q1:10:a', '1:q2:10:b', '2:q2:10:c']
        }, StageState.COMPLETED),
        # multiple reviewers - both complete - complete
        ([1, 2], ['q1', 'q2'], {
            GROUP_ID: ['1:q1:10:a', '1:q2:10:b', '2:q2:10:c', '2:q2:10:d']
        }, StageState.COMPLETED),
    )
    @ddt.unpack
    def test_ta_get_external_group_status(self, ta_reviewers, questions,
                                          review_items, expected_result):
        group_to_review = mk_wg(GROUP_ID, [{"id": 1}])
        self.activity_mock.is_ta_graded = True

        self._set_project_api_responses(
            group_to_review, {
                group_to_review.id:
                [self._parse_review_item_string(item) for item in items]
                for group_id, items in review_items.iteritems()
            })

        with patch_obj(self.block, 'is_user_ta') as patched_outsider_allowed:
            patched_outsider_allowed.side_effect = lambda user_id, _course_id: user_id in ta_reviewers

            self.assert_group_completion(group_to_review, questions,
                                         expected_result)
Esempio n. 12
0
class TestGroupProjectGradeEvaluationDisplayXBlock(
        CommonFeedbackDisplayStageTests, StageComponentXBlockTestBase):
    block_to_test = GroupProjectGradeEvaluationDisplayXBlock

    @ddt.data(
        (2, 'content-1', 'q1', [mri(1, "q1"), mri(2, "q1")
                                ], [mri(1, "q1"), mri(2, "q1")]),
        (9, 'content-2', 'q2', [mri(1, "q1"), mri(2, "q1")], []),
        (15, 'content-3', 'q1', [mri(1, "q1"), mri(1, "q2")], [mri(1, "q1")]),
        (15, 'content-3', 'q2', [mri(1, "q1"), mri(1, "q2")], [mri(1, "q2")]),
    )
    @ddt.unpack
    def test_get_feedback(self, group_id, content_id, question_id,
                          feedback_items, expected_result):
        self.project_api_mock.get_workgroup_review_items_for_group = mock.Mock(
            return_value=feedback_items)
        self.stage_mock.activity_content_id = content_id
        self.block.question_id = question_id

        with mock.patch.object(self.block_to_test, 'group_id',
                               mock.PropertyMock(return_value=group_id)):
            result = self.block.get_feedback()

            self.project_api_mock.get_workgroup_review_items_for_group.assert_called_once_with(
                group_id, content_id)
            self.assertEqual(result, expected_result)

    def test_activity_questions(self):
        self.activity_mock.team_evaluation_questions = [1, 2, 3]
        self.activity_mock.peer_review_questions = [4, 5, 6]

        self.assertEqual(self.block.activity_questions, [4, 5, 6])
Esempio n. 13
0
class TestGroupProjectTeamEvaluationDisplayXBlock(
        CommonFeedbackDisplayStageTests, StageComponentXBlockTestBase):
    block_to_test = GroupProjectTeamEvaluationDisplayXBlock

    # pylint: disable=too-many-arguments
    @ddt.data(
        (1, 2, 'content-1', 'q1', [mri(1, "q1"), mri(
            2, "q1")], [mri(1, "q1"), mri(2, "q1")]),
        (3, 9, 'content-2', 'q2', [mri(1, "q1"), mri(2, "q1")], []),
        (7, 15, 'content-3', 'q1', [mri(1, "q1"), mri(1, "q2")
                                    ], [mri(1, "q1")]),
        (7, 15, 'content-3', 'q2', [mri(1, "q1"), mri(1, "q2")
                                    ], [mri(1, "q2")]),
    )
    @ddt.unpack
    def test_get_feedback(self, user_id, group_id, content_id, question_id,
                          feedback_items, expected_result):
        self.project_api_mock.get_user_peer_review_items = mock.Mock(
            return_value=feedback_items)
        self.stage_mock.activity_content_id = content_id
        self.block.question_id = question_id

        with mock.patch.object(self.block_to_test, 'user_id', mock.PropertyMock(return_value=user_id)), \
                mock.patch.object(self.block_to_test, 'group_id', mock.PropertyMock(return_value=group_id)):
            result = self.block.get_feedback()

            self.project_api_mock.get_user_peer_review_items.assert_called_once_with(
                user_id, group_id, content_id)
            self.assertEqual(result, expected_result)

    def test_activity_questions(self):
        self.activity_mock.team_evaluation_questions = [1, 2, 3]
        self.activity_mock.peer_review_questions = [4, 5, 6]

        self.assertEqual(self.block.activity_questions, [1, 2, 3])
Esempio n. 14
0
class TestTeamEvaluationStageStageStatus(ReviewStageUserCompletionStatsMixin, BaseStageTest):
    block_to_test = TeamEvaluationStage

    @ddt.data(
        ([10], ["q1"], [], ReviewState.NOT_STARTED),
        ([10], ["q1"], [mri(USER_ID, "q1", peer=10, answer='1')], ReviewState.COMPLETED),
        ([10], ["q1"], [mri(OTHER_USER_ID, "q1", peer=10, answer='1')], ReviewState.NOT_STARTED),
        (
            [10], ["q1", "q2"],
            [mri(USER_ID, "q1", peer=10, answer='1'), mri(OTHER_USER_ID, "q1", peer=10, answer='1')],
            ReviewState.INCOMPLETE
        ),
        (
            [10], ["q1"],
            [mri(USER_ID, "q1", peer=10, answer='2'), mri(USER_ID, "q2", peer=10, answer="1")],
            ReviewState.COMPLETED
        ),
        (
            [10], ["q1", "q2"],
            [mri(USER_ID, "q1", peer=10, answer='3')],
            ReviewState.INCOMPLETE
        ),
        (
            [10], ["q1"],
            [mri(USER_ID, "q2", peer=10, answer='4'), mri(USER_ID, "q1", peer=11, answer='5')],
            ReviewState.NOT_STARTED
        ),
        (
            [10, 11], ["q1"],
            [mri(USER_ID, "q1", peer=10, answer='6'), mri(USER_ID, "q1", peer=11, answer='7')],
            ReviewState.COMPLETED
        ),
        (
            [10, 11], ["q1", "q2"],
            [mri(USER_ID, "q1", peer=10, answer='7'), mri(USER_ID, "q1", peer=11, answer='8')],
            ReviewState.INCOMPLETE
        ),
    )
    @ddt.unpack
    def test_review_status(self, peers_to_review, questions, reviews, expected_result):
        self.project_api_mock.get_peer_review_items_for_group.return_value = reviews

        with patch_obj(self.block_to_test, 'review_subjects', mock.PropertyMock()) as patched_review_subjects, \
                patch_obj(self.block_to_test, 'required_questions', mock.PropertyMock()) as patched_questions:
            patched_review_subjects.return_value = [ReducedUserDetails(id=rev_id) for rev_id in peers_to_review]
            patched_questions.return_value = [make_question(q_id, 'irrelevant') for q_id in questions]

            self.assertEqual(self.block.review_status(), expected_result)
            self.project_api_mock.get_peer_review_items_for_group.assert_called_once_with(
                self.workgroup_data.id, self.activity_mock.content_id
            )

    def _set_project_api_responses(self, workgroups, review_items):
        def workgroups_side_effect(user_id, _course_id):
            return workgroups.get(user_id, None)

        def review_items_side_effect(workgroup_id, _content_id):
            return review_items.get(workgroup_id, [])

        self.project_api_mock.get_user_workgroup_for_course.side_effect = workgroups_side_effect
        self.project_api_mock.get_peer_review_items_for_group.side_effect = review_items_side_effect

    @staticmethod
    def _parse_review_item_string(review_item_string):
        splitted = review_item_string.split(':')
        reviewer, question, peer = splitted[:3]
        if len(splitted) > 3:
            answer = splitted[3]
        else:
            answer = None
        return mri(int(reviewer), question, peer=peer, answer=answer)

    @ddt.data(
        # no reviews - not started
        ([1, 2], ["q1", "q2"], [], (set(), set())),
        # some reviews - partially completed
        ([1, 2], ["q1", "q2"], ["1:q1:2:a"], (set(), {1})),
        # all reviews - completed
        ([1, 2], ["q1", "q2"], ["1:q1:2:a", "1:q2:2:b"], ({1}, set())),
        # no reviews - not started
        ([1, 2, 3], ["q1", "q2"], [], (set(), set())),
        # some reviews - partially completed
        ([1, 2, 3], ["q1", "q2"], ["1:q1:2:a", "1:q2:2:b"], (set(), {1})),
        # all reviews, but some answers are None - partially completed
        ([1, 2, 3], ["q1", "q2"], ["1:q1:2:a", "1:q2:2:b", "1:q1:3", "1:q2:3:d"], (set(), {1})),
        # all reviews, but some answers are empty - partially completed
        ([1, 2, 3], ["q1", "q2"], ["1:q1:2:a", "1:q2:2:b", "1:q1:3:", "1:q2:3:d"], (set(), {1})),
        # all reviews - completed
        ([1, 2, 3], ["q1", "q2"], ["1:q1:2:a", "1:q2:2:b", "1:q1:3:c", "1:q2:3:d"], ({1}, set())),
    )
    @ddt.unpack
    def test_users_completion_single_user(self, users_in_group, questions, review_items, expected_result):
        user_id = 1
        workgroup_id = 1
        review_items = [self._parse_review_item_string(review_item_str) for review_item_str in review_items]

        self._set_project_api_responses(
            {user_id: mk_wg(workgroup_id, users=[{"id": uid} for uid in users_in_group])},
            {workgroup_id: review_items}
        )

        self.assert_users_completion(expected_result, questions, [user_id])

        # checks if caching is ok
        self.project_api_mock.get_peer_review_items_for_group.assert_called_once_with(
            workgroup_id, self.block.activity_content_id
        )

    @ddt.data(
        # no reviews - both not started
        ([1, 2], ["q1", "q2"], [], (set(), set())),
        # u1 some reviews - u1 partially completed
        ([1, 2], ["q1", "q2"], ["1:q1:2:a"], (set(), {1})),
        # u1 all reviews - u1 completed, u2 - not started
        ([1, 2], ["q1", "q2"], ["1:q1:2:a", "1:q2:2:b"], ({1}, set())),
        # u1 some reviews, u2 some reviews - both partially completed
        ([1, 2], ["q1", "q2"], ["1:q1:2:a", "2:q1:1:b"], (set(), {1, 2})),
        # u1 all reviews, u2 some reviews - u1 completed, u2 partially completed
        ([1, 2], ["q1", "q2"], ["1:q1:2:a", "1:q2:2:b", "2:q1:1:c"], ({1}, {2})),
        # both all reviews - both completed
        ([1, 2], ["q1", "q2"], ["1:q1:2:a", "1:q2:2:b", "2:q1:1:c", "2:q2:1:d"], ({1, 2}, set())),
    )
    @ddt.unpack
    def test_users_completion_same_group_users(self, users_in_group, questions, review_items, expected_result):
        workgroup_id = 1
        workgroup_data = mk_wg(workgroup_id, users=[{"id": uid} for uid in users_in_group])
        review_items = [self._parse_review_item_string(review_item_str) for review_item_str in review_items]

        self._set_project_api_responses(
            {uid: workgroup_data for uid in users_in_group},
            {workgroup_id: review_items}
        )

        self.assert_users_completion(expected_result, questions, users_in_group)
        # checks if caching is ok
        self.project_api_mock.get_peer_review_items_for_group.assert_called_once_with(
            workgroup_id, self.block.activity_content_id
        )

    @ddt.data(
        # no reviews - both not started
        ([1, 3], ['q1'], {}, (set(), set())),
        # u1 some reviews - u1 partially, u4 - not started
        ([1, 4], ['q1', 'q2'], {GROUP_ID: ['1:q1:2:b']}, (set(), {1})),
        # u2 all reviews - u2 completed, u3 - not started
        ([2, 3], ['q1'], {GROUP_ID: ['2:q1:1:a']}, ({2}, set())),
        # u3 all reviews - u3 completed, u1, u2 not started
        ([1, 2, 3], ['q1'], {OTHER_GROUP_ID: ['3:q1:4:a']}, ({3}, set())),
        # u1, u2, u3 all reviews - u1, u2, u3 completed
        (
            [1, 2, 3], ['q1'],
            {GROUP_ID: ['1:q1:2:a', '2:q1:1:b'], OTHER_GROUP_ID: ['3:q1:4:c']}, ({1, 2, 3}, set())
        ),
        # u1, u2, u3 all reviews, u4 no reviews - u1, u2, u3 completed, u4 not started
        (
            [1, 2, 3, 4], ['q1'],
            {GROUP_ID: ['1:q1:2:a', '2:q1:1:b'], OTHER_GROUP_ID: ['3:q1:4:c']}, ({1, 2, 3}, set())
        ),
        # u1 all reviews, u3 some reviews - u1 completed, u3 partially completed
        (
            [1, 3], ['q1', 'q2'],
            {GROUP_ID: ['1:q1:2:a', '1:q2:2:b'], OTHER_GROUP_ID: ['3:q1:4:c']}, ({1}, {3})
        ),
    )
    @ddt.unpack
    def test_users_completion_multiple_groups(self, target_users, questions, review_items, expected_result):
        workgroups = [
            mk_wg(GROUP_ID, users=[{"id": 1}, {"id": 2}]),
            mk_wg(OTHER_GROUP_ID, users=[{"id": 3}, {"id": 4}]),
        ]
        self._set_project_api_responses(
            {1: workgroups[0], 2: workgroups[0], 3: workgroups[1], 4: workgroups[1]},
            {
                group_id: [self._parse_review_item_string(item) for item in items]
                for group_id, items in review_items.items()
            }
        )

        self.assert_users_completion(expected_result, questions, target_users)

        # checks if caching is ok
        expected_calls = [
            mock.call(GROUP_ID, self.block.activity_content_id),
            mock.call(OTHER_GROUP_ID, self.block.activity_content_id)
        ]
        self.assertEqual(self.project_api_mock.get_peer_review_items_for_group.mock_calls, expected_calls)