예제 #1
0
    def setUp(self):
        super(CalculateUpdatedAggregatorsTestCase, self).setUp()
        self.user = get_user_model().objects.create(
            username='******', email='*****@*****.**')
        self.course_key = CourseKey.from_string('OpenCraft/Onboarding/2018')
        self.blocks = [
            self.course_key.make_usage_key('course', 'course'),
            self.course_key.make_usage_key('chapter', 'course-chapter1'),
            self.course_key.make_usage_key('chapter', 'course-chapter2'),
            self.course_key.make_usage_key('html', 'course-chapter1-block1'),
            self.course_key.make_usage_key('html', 'course-chapter1-block2'),
            self.course_key.make_usage_key('html', 'course-chapter2-block1'),
            self.course_key.make_usage_key('html', 'course-chapter2-block2'),
            # image_explorer is an unregistered block type, and should be
            # treated as EXCLUDED from aggregation.
            self.course_key.make_usage_key('image_explorer',
                                           'course-chapter2-badblock'),
            self.course_key.make_usage_key('chapter', 'course-zeropossible'),
        ]
        patch = mock.patch('completion_aggregator.core.compat',
                           StubCompat(self.blocks))
        patch.start()
        self.addCleanup(patch.stop)

        BlockCompletion.objects.create(
            user=self.user,
            context_key=self.course_key,
            block_key=self.blocks[3],
            completion=1.0,
            modified=now(),
        )
 def test_cohort_signal_handler(self):
     course_key = CourseKey.from_string('course-v1:edX+test+2018')
     user = get_user_model().objects.create(username='******')
     with patch('completion_aggregator.core.compat', StubCompat([])):
         cohort_updated_handler(user, course_key)
         assert StaleCompletion.objects.filter(username=user.username,
                                               course_key=course_key,
                                               force=True).exists()
예제 #3
0
    def setUp(self):
        """
        For the purpose of the tests, we will use the following course
        structure:

                        course
                          |
                +--+---+--^-+----+----+
               /   |   |    |    |     \\
            html html html html other hidden
                                /   \\
                              html hidden

        where `course` and `other` are a completion_mode of AGGREGATOR (but
        only `course` is registered to store aggregations), `html` is
        COMPLETABLE, and `hidden` is EXCLUDED.
        """
        super(AggregationUpdaterTestCase, self).setUp()
        self.agg_modified = now() - timedelta(days=1)
        course_key = CourseKey.from_string('course-v1:edx+course+test')
        stubcompat = StubCompat([
            course_key.make_usage_key('course', 'course'),
            course_key.make_usage_key('html', 'course-html0'),
            course_key.make_usage_key('html', 'course-html1'),
            course_key.make_usage_key('html', 'course-html2'),
            course_key.make_usage_key('html', 'course-html3'),
            course_key.make_usage_key('other', 'course-other'),
            course_key.make_usage_key('hidden', 'course-hidden0'),
            course_key.make_usage_key('html', 'course-other-html4'),
            course_key.make_usage_key('hidden', 'course-other-hidden1'),
        ])
        for compat_module in 'completion_aggregator.core.compat', 'completion_aggregator.core.compat':
            patch = mock.patch(compat_module, stubcompat)
            patch.start()
            self.addCleanup(patch.stop)
        user = get_user_model().objects.create(username='******')
        self.course_key = CourseKey.from_string('course-v1:edx+course+test')
        self.agg, _ = Aggregator.objects.submit_completion(
            user=user,
            course_key=self.course_key,
            block_key=self.course_key.make_usage_key('course', 'course'),
            aggregation_name='course',
            earned=0.0,
            possible=0.0,
            last_modified=self.agg_modified,
        )
        BlockCompletion.objects.create(
            user=user,
            context_key=self.course_key,
            block_key=self.course_key.make_usage_key('html',
                                                     'course-other-html4'),
            completion=1.0,
            modified=now(),
        )
        self.updater = AggregationUpdater(user, self.course_key,
                                          mock.MagicMock())
def compat_patch(course_key):
    """
    Patch compat with a stub including a simple course.
    """
    return patch(
        'completion_aggregator.core.compat',
        StubCompat([
            course_key.make_usage_key('course', 'course'),
            course_key.make_usage_key('vertical', 'course-vertical'),
            course_key.make_usage_key('html', 'course-vertical-html'),
        ]))
    def setUp(self):
        super(CompletionBlockUpdateViewTestCase, self).setUp()
        self.test_user = User.objects.create(username='******')
        self.staff_user = User.objects.create(username='******', is_staff=True)
        self.test_enrollment = self.create_enrollment(
            user=self.test_user,
            course_id=self.course_key,
        )
        self.blocks = [
            self.course_key.make_usage_key('course', 'course'),
            self.course_key.make_usage_key('sequential', 'course-sequence1'),
            self.course_key.make_usage_key('sequential', 'course-sequence2'),
            self.course_key.make_usage_key('html', 'course-sequence1-html1'),
            self.course_key.make_usage_key('html', 'course-sequence1-html2'),
            self.course_key.make_usage_key('html', 'course-sequence1-html3'),
            self.course_key.make_usage_key('html', 'course-sequence1-html4'),
            self.course_key.make_usage_key('html', 'course-sequence1-html5'),
            self.course_key.make_usage_key('html', 'course-sequence2-html6'),
            self.course_key.make_usage_key('html', 'course-sequence2-html7'),
            self.course_key.make_usage_key('html', 'course-sequence2-html8'),
        ]
        compat = StubCompat(self.blocks)
        for compat_import in (
                'completion_aggregator.api.common.compat',
                'completion_aggregator.api.v0.views.compat',
                'completion_aggregator.serializers.compat',
                'completion_aggregator.core.compat',
        ):
            patcher = patch(compat_import, compat)
            patcher.start()
            self.addCleanup(patcher.__exit__, None, None, None)

        self.patch_object(
            CompletionViewMixin,
            'get_authenticators',
            return_value=[OAuth2Authentication(),
                          SessionAuthentication()])
        self.patch_object(CompletionViewMixin,
                          'pagination_class',
                          new_callable=PropertyMock,
                          return_value=PageNumberPagination)
        self.client = APIClient()
        self.client.force_authenticate(user=self.test_user)
        self.update_url = reverse('completion_api_v0:blockcompletion-update',
                                  kwargs={
                                      'course_key':
                                      six.text_type(self.course_key),
                                      'block_key':
                                      six.text_type(self.usage_key)
                                  })
 def setUp(self):
     super(PartialUpdateTest, self).setUp()
     self.user = get_user_model().objects.create()
     self.course_key = CourseKey.from_string('OpenCraft/Onboarding/2018')
     self.blocks = [
         self.course_key.make_usage_key('course', 'course'),
         self.course_key.make_usage_key('chapter', 'course-chapter1'),
         self.course_key.make_usage_key('chapter', 'course-chapter2'),
         self.course_key.make_usage_key('html', 'course-chapter1-block1'),
         self.course_key.make_usage_key('html', 'course-chapter1-block2'),
         self.course_key.make_usage_key('html', 'course-chapter2-block1'),
         self.course_key.make_usage_key('html', 'course-chapter2-block2'),
     ]
     patch = mock.patch('completion_aggregator.core.compat', StubCompat(self.blocks))
     patch.start()
     self.addCleanup(patch.stop)
예제 #7
0
    def setUp(self):
        super(MigrateProgressTestCase, self).setUp()
        self.user = user = User.objects.create_user("test", password="******")
        self.course_key = course_key = CourseKey.from_string(
            'course-v1:edx+course+test')
        self.block_keys = block_keys = [
            course_key.make_usage_key('html', 'course-html{}'.format(idx))
            for idx in range(1, 51)
        ]
        stubcompat = StubCompat(
            [course_key.make_usage_key('course', 'course')] + block_keys)
        for compat_module in 'completion_aggregator.core.compat', 'completion_aggregator.core.compat':
            patch = mock.patch(compat_module, stubcompat)
            patch.start()
            self.addCleanup(patch.stop)

        for idx in range(1, 51):
            block_key = course_key.make_usage_key('html',
                                                  'course-html{}'.format(idx))
            with freeze_time("2020-02-02T02:02:{}".format(idx)):
                CourseModuleCompletion.objects.create(
                    id=idx,
                    user=user,
                    course_id=course_key,
                    content_id=block_key,
                )
            with connection.cursor() as cur:
                cur.execute(
                    """
                    INSERT INTO completion_blockcompletion
                        (user_id, course_key, block_key, block_type, completion, created, modified)
                    VALUES
                        (%s, %s, %s, %s, 1.0, %s, %s);
                    """, [
                        user.id,
                        course_key,
                        block_key,
                        block_key.block_type,
                        "0000-00-00 00:00:00",
                        "0000-00-00 00:00:00",
                    ])
 def course_enrollment_model(self):
     return StubCompat([]).course_enrollment_model()
class CompletionViewTestCase(CompletionAPITestMixin, TestCase):
    """
    Test that the CompletionView renders completion data properly.
    """

    course_key = CourseKey.from_string('edX/toy/2012_Fall')
    other_org_course_key = CourseKey.from_string('otherOrg/toy/2012_Fall')
    list_url = '/v{}/course/'
    detail_url_fmt = '/v{}/course/{}/'
    course_stat_url_fmt = '/v1/stats/{}/'
    course_enrollment_model = StubCompat([]).course_enrollment_model()

    def setUp(self):
        super(CompletionViewTestCase, self).setUp()
        self.test_user = User.objects.create(username='******')
        self.staff_user = User.objects.create(username='******', is_staff=True)
        self.test_enrollment = self.create_enrollment(
            user=self.test_user,
            course_id=self.course_key,
        )
        self.blocks = [
            self.course_key.make_usage_key('course', 'course'),
            self.course_key.make_usage_key('sequential', 'course-sequence1'),
            self.course_key.make_usage_key('sequential', 'course-sequence2'),
            self.course_key.make_usage_key('html', 'course-sequence1-html1'),
            self.course_key.make_usage_key('html', 'course-sequence1-html2'),
            self.course_key.make_usage_key('html', 'course-sequence1-html3'),
            self.course_key.make_usage_key('html', 'course-sequence1-html4'),
            self.course_key.make_usage_key('html', 'course-sequence1-html5'),
            self.course_key.make_usage_key('html', 'course-sequence2-html6'),
            self.course_key.make_usage_key('html', 'course-sequence2-html7'),
            self.course_key.make_usage_key('html', 'course-sequence2-html8'),
        ]
        compat = StubCompat(self.blocks)
        for compat_import in (
                'completion_aggregator.api.common.compat',
                'completion_aggregator.serializers.compat',
                'completion_aggregator.core.compat',
        ):
            patcher = patch(compat_import, compat)
            patcher.start()
            self.addCleanup(patcher.__exit__, None, None, None)

        self.patch_object(
            CompletionViewMixin,
            'get_authenticators',
            return_value=[OAuth2Authentication(),
                          SessionAuthentication()])
        self.patch_object(CompletionViewMixin,
                          'pagination_class',
                          new_callable=PropertyMock,
                          return_value=PageNumberPagination)
        self.mark_completions()
        self.client = APIClient()
        self.client.force_authenticate(user=self.test_user)

    def _get_expected_completion(self,
                                 version,
                                 earned=1.0,
                                 possible=8.0,
                                 percent=0.125):
        """
        Return completion section based on version.
        """
        completion = {
            'earned': earned,
            'possible': possible,
            'percent': percent,
        }
        if version == 0:
            completion['ratio'] = percent
        return completion

    def _get_expected_detail(self,
                             version,
                             values,
                             count=1,
                             previous=None,
                             next_page=None):
        """
        Return base result for detail view based on version.
        """
        if version == 1:
            if isinstance(values, dict):
                values = [values]
            return {
                'count': count,
                'previous': previous,
                'next': next_page,
                'results': values
            }
        else:
            return values

    def assert_expected_list_view(self, version):
        """
        Ensures that the expected data is returned from the versioned list view.
        """
        response = self.client.get(
            self.get_list_url(version, username=self.test_user.username))
        self.assertEqual(response.status_code, 200)
        expected = {
            'count':
            1,
            'previous':
            None,
            'next':
            None,
            'results': [{
                'course_key': 'edX/toy/2012_Fall',
                'completion': self._get_expected_completion(version),
            }],
        }
        self.assertEqual(response.data, expected)

    @ddt.data(0, 1)
    @XBlock.register_temp_plugin(StubCourse, 'course')
    @XBlock.register_temp_plugin(StubSequential, 'sequential')
    @XBlock.register_temp_plugin(StubHTML, 'html')
    @patch.object(AggregationUpdater, 'update')
    def test_list_view(self, version, mock_update):
        self.assert_expected_list_view(version)
        # no stale completions, so aggregations were not updated
        assert mock_update.call_count == 0

    @ddt.data(0, 1)
    @XBlock.register_temp_plugin(StubCourse, 'course')
    @XBlock.register_temp_plugin(StubSequential, 'sequential')
    @XBlock.register_temp_plugin(StubHTML, 'html')
    def test_list_view_stale_completion(self, version):
        """
        Ensure that a stale completion causes the aggregations to be
        recalculated, but not updated in the db, and stale completion is not
        resolved.
        """
        models.StaleCompletion.objects.create(
            username=self.test_user.username,
            course_key=self.course_key,
            block_key=None,
            force=True,
            resolved=False,
        )
        assert models.StaleCompletion.objects.filter(
            resolved=False).count() == 1
        self.assert_expected_list_view(version)
        # assert mock_calculate.call_count == 1
        assert models.StaleCompletion.objects.filter(
            resolved=False).count() == 1

    @ddt.data(0, 1)
    @XBlock.register_temp_plugin(StubCourse, 'course')
    @XBlock.register_temp_plugin(StubSequential, 'sequential')
    @XBlock.register_temp_plugin(StubHTML, 'html')
    def test_list_view_enrolled_no_progress(self, version):
        """
        Test that the completion API returns a record for each course the user is enrolled in,
        even if no progress records exist yet.
        """
        self.create_enrollment(
            user=self.test_user,
            course_id=self.other_org_course_key,
        )
        response = self.client.get(
            self.get_list_url(version, username=self.test_user.username))
        self.assertEqual(response.status_code, 200)
        expected = {
            'count':
            2,
            'previous':
            None,
            'next':
            None,
            'results': [{
                'course_key':
                'edX/toy/2012_Fall',
                'completion':
                self._get_expected_completion(
                    version,
                    earned=1.0,
                    possible=8.0,
                    percent=0.125,
                ),
            }, {
                'course_key':
                'otherOrg/toy/2012_Fall',
                'completion':
                self._get_expected_completion(
                    version,
                    earned=0.0,
                    possible=None,
                    percent=0.0,
                ),
            }],
        }
        self.assertEqual(response.data, expected)

    @ddt.data(0, 1)
    @XBlock.register_temp_plugin(StubCourse, 'course')
    @XBlock.register_temp_plugin(StubSequential, 'sequential')
    @XBlock.register_temp_plugin(StubHTML, 'html')
    def test_list_view_with_sequentials(self, version):
        response = self.client.get(
            self.get_list_url(version,
                              username=self.test_user.username,
                              requested_fields='sequential'))
        self.assertEqual(response.status_code, 200)
        expected = {
            'count':
            1,
            'previous':
            None,
            'next':
            None,
            'results': [{
                'course_key':
                'edX/toy/2012_Fall',
                'completion':
                self._get_expected_completion(version),
                'sequential': [
                    {
                        'course_key':
                        u'edX/toy/2012_Fall',
                        'block_key':
                        u'i4x://edX/toy/sequential/course-sequence1',
                        'completion':
                        self._get_expected_completion(
                            version,
                            earned=1.0,
                            possible=5.0,
                            percent=0.2,
                        ),
                    },
                ]
            }],
        }
        self.assertEqual(response.data, expected)

    def assert_expected_detail_view(self, version):
        """
        Ensures that the expected data is returned from the versioned detail view.
        """
        response = self.client.get(
            self.get_detail_url(version,
                                six.text_type(self.course_key),
                                username=self.test_user.username))
        self.assertEqual(response.status_code, 200)
        expected_values = {
            'course_key': 'edX/toy/2012_Fall',
            'completion': self._get_expected_completion(version)
        }
        expected = self._get_expected_detail(version, expected_values)
        self.assertEqual(response.data, expected)

    @ddt.data((0, True), (0, False), (1, True), (1, False))
    @ddt.unpack
    @XBlock.register_temp_plugin(StubCourse, 'course')
    @XBlock.register_temp_plugin(StubSequential, 'sequential')
    @XBlock.register_temp_plugin(StubHTML, 'html')
    @patch.object(AggregationUpdater, 'update')
    def test_detail_view(self, version, waffle_active, mock_update):
        with override_flag(WAFFLE_AGGREGATE_STALE_FROM_SCRATCH,
                           active=waffle_active):
            self.assert_expected_detail_view(version)
        # no stale completions, so aggregations were not updated
        assert mock_update.call_count == 0

    @ddt.data((0, True), (0, False), (1, True), (1, False))
    @ddt.unpack
    @XBlock.register_temp_plugin(StubCourse, 'course')
    @XBlock.register_temp_plugin(StubSequential, 'sequential')
    @XBlock.register_temp_plugin(StubHTML, 'html')
    def test_detail_view_stale_completion(self, version, waffle_active):
        """
        Ensure that a stale completion causes the aggregations to be recalculated once.

        Verify that the stale completion not resolved.
        """
        models.StaleCompletion.objects.create(
            username=self.test_user.username,
            course_key=self.course_key,
            block_key=None,
            force=False,
        )
        assert models.StaleCompletion.objects.filter(
            resolved=False).count() == 1
        with override_flag(
                'completion_aggregator.aggregate_stale_from_scratch',
                active=waffle_active):
            self.assert_expected_detail_view(version)
        assert models.StaleCompletion.objects.filter(
            resolved=False).count() == 1

    @XBlock.register_temp_plugin(StubCourse, 'course')
    @XBlock.register_temp_plugin(StubSequential, 'sequential')
    @XBlock.register_temp_plugin(StubHTML, 'html')
    def test_detail_view_root_block(self):
        """
        Ensure that a stale completion causes the aggregations to be recalculated once.

        Verify that the stale completion not resolved.
        """
        models.StaleCompletion.objects.create(
            username=self.test_user.username,
            course_key=self.course_key,
            block_key=None,
            force=False,
        )
        response = self.client.get(
            self.get_detail_url(
                1,
                six.text_type(self.course_key),
                username=self.test_user.username,
                root_block=six.text_type(self.blocks[1]),
                requested_fields='sequential',
            ))
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data['results'], [{
            'completion': {
                'earned': 0.0,
                'possible': None,
                'percent': 0.0,
            },
            'course_key':
            six.text_type(self.course_key),
            'sequential': [
                {
                    'course_key': six.text_type(self.course_key),
                    'block_key': six.text_type(self.blocks[1]),
                    'completion': {
                        'earned': 1.0,
                        'possible': 5.0,
                        'percent': 0.2,
                    }
                },
            ],
        }])

    @ddt.data(0, 1)
    @XBlock.register_temp_plugin(StubCourse, 'course')
    @XBlock.register_temp_plugin(StubSequential, 'sequential')
    @XBlock.register_temp_plugin(StubHTML, 'html')
    def test_detail_view_oauth2(self, version):
        """
        Test the detail view using OAuth2 Authentication
        """
        # Try with no authentication:
        self.client.logout()
        response = self.client.get(
            self.get_detail_url(version, self.course_key))
        self.assertEqual(response.status_code, 401)
        # Now, try with a valid token header:
        token = _create_oauth2_token(self.test_user)
        response = self.client.get(
            self.get_detail_url(version,
                                self.course_key,
                                username=self.test_user.username),
            HTTP_AUTHORIZATION="Bearer {0}".format(token))
        self.assertEqual(response.status_code, 200)
        if version == 0:
            self.assertEqual(response.data['completion']['earned'], 1.0)
        else:
            self.assertEqual(
                response.data['results'][0]['completion']['earned'], 1.0)

    @ddt.data(0, 1)
    @XBlock.register_temp_plugin(StubCourse, 'course')
    @XBlock.register_temp_plugin(StubSequential, 'sequential')
    @XBlock.register_temp_plugin(StubHTML, 'html')
    def test_detail_view_not_enrolled(self, version):
        """
        Test that requesting course completions for a course the user is not enrolled in
        will return a 404.
        """
        response = self.client.get(
            self.get_detail_url(version,
                                self.other_org_course_key,
                                username=self.test_user.username))
        self.assertEqual(response.status_code, 404)

    @ddt.data(0, 1)
    @XBlock.register_temp_plugin(StubCourse, 'course')
    @XBlock.register_temp_plugin(StubSequential, 'sequential')
    @XBlock.register_temp_plugin(StubHTML, 'html')
    def test_detail_view_inactive_enrollment(self, version):
        self.test_enrollment.is_active = False
        self.test_enrollment.save()
        response = self.client.get(
            self.get_detail_url(version,
                                self.course_key,
                                username=self.test_user.username))
        self.assertEqual(response.status_code, 404)

    @ddt.data(0, 1)
    @XBlock.register_temp_plugin(StubCourse, 'course')
    @XBlock.register_temp_plugin(StubSequential, 'sequential')
    @XBlock.register_temp_plugin(StubHTML, 'html')
    def test_detail_view_no_completion(self, version):
        """
        Test that requesting course completions for a course which has started, but the user has not yet started,
        will return an empty completion record with its "possible" field filled in.
        """
        self.create_enrollment(
            user=self.test_user,
            course_id=self.other_org_course_key,
        )
        response = self.client.get(
            self.get_detail_url(version,
                                self.other_org_course_key,
                                username=self.test_user.username))
        self.assertEqual(response.status_code, 200)
        expected_values = {
            'course_key':
            'otherOrg/toy/2012_Fall',
            'completion':
            self._get_expected_completion(version,
                                          earned=0.0,
                                          possible=None,
                                          percent=0.0),
        }
        expected = self._get_expected_detail(version, expected_values)
        self.assertEqual(response.data, expected)

    @ddt.data(0, 1)
    @XBlock.register_temp_plugin(StubCourse, 'course')
    @XBlock.register_temp_plugin(StubSequential, 'sequential')
    @XBlock.register_temp_plugin(StubHTML, 'html')
    def test_detail_view_with_sequentials(self, version):
        response = self.client.get(
            self.get_detail_url(version,
                                self.course_key,
                                username=self.test_user.username,
                                requested_fields='sequential'))
        self.assertEqual(response.status_code, 200)
        expected_values = {
            'course_key':
            'edX/toy/2012_Fall',
            'completion':
            self._get_expected_completion(version),
            'sequential': [
                {
                    'course_key':
                    u'edX/toy/2012_Fall',
                    'block_key':
                    u'i4x://edX/toy/sequential/course-sequence1',
                    'completion':
                    self._get_expected_completion(version,
                                                  earned=1.0,
                                                  possible=5.0,
                                                  percent=0.2),
                },
            ]
        }
        expected = self._get_expected_detail(version, expected_values)
        self.assertEqual(response.data, expected)

    @ddt.data(0, 1)
    @XBlock.register_temp_plugin(StubCourse, 'course')
    @XBlock.register_temp_plugin(StubSequential, 'sequential')
    @XBlock.register_temp_plugin(StubHTML, 'html')
    def test_detail_view_staff_requested_user(self, version):
        """
        Test that requesting course completions for a specific user filters out the other enrolled users
        """
        self.client.force_authenticate(self.staff_user)
        response = self.client.get(
            self.get_detail_url(version,
                                self.course_key,
                                username=self.test_user.username))
        self.assertEqual(response.status_code, 200)
        expected_values = {
            'course_key': 'edX/toy/2012_Fall',
            'completion': self._get_expected_completion(version)
        }
        expected = self._get_expected_detail(version, expected_values)
        self.assertEqual(response.data, expected)

    @XBlock.register_temp_plugin(StubCourse, 'course')
    @XBlock.register_temp_plugin(StubSequential, 'sequential')
    @XBlock.register_temp_plugin(StubHTML, 'html')
    @patch.object(AggregationUpdater, 'update')
    def test_detail_view_staff_all_users(self, mock_update):
        """
        Test that staff requesting course completions can see all completions,
        and that the presence of stale completions does not trigger a recalculation.
        """
        # Add an additonal completion for the staff user
        another_user = User.objects.create(username='******')
        self.create_enrollment(
            user=another_user,
            course_id=self.course_key,
        )
        models.Aggregator.objects.submit_completion(
            user=another_user,
            course_key=self.course_key,
            block_key=self.course_key.make_usage_key(
                block_type='sequential', block_id='course-sequence1'),
            aggregation_name='sequential',
            earned=3.0,
            possible=5.0,
            last_modified=timezone.now(),
        )
        models.Aggregator.objects.submit_completion(
            user=another_user,
            course_key=self.course_key,
            block_key=self.course_key.make_usage_key(block_type='course',
                                                     block_id='course'),
            aggregation_name='course',
            earned=3.0,
            possible=12.0,
            last_modified=timezone.now(),
        )
        # Create some stale completions too, to test recalculations are skipped
        for user in (another_user, self.test_user):
            models.StaleCompletion.objects.create(
                username=user.username,
                course_key=self.course_key,
                block_key=None,
                force=False,
            )
        assert models.StaleCompletion.objects.filter(
            resolved=False).count() == 2

        self.client.force_authenticate(self.staff_user)
        response = self.client.get(self.get_detail_url(1, self.course_key))
        self.assertEqual(response.status_code, 200)
        expected_values = [
            {
                'username': '******',
                'course_key': 'edX/toy/2012_Fall',
                'completion': self._get_expected_completion(1)
            },
            {
                'username':
                '******',
                'course_key':
                'edX/toy/2012_Fall',
                'completion':
                self._get_expected_completion(1,
                                              earned=3.0,
                                              possible=12.0,
                                              percent=0.25),
            },
        ]
        expected = self._get_expected_detail(1, expected_values, count=2)
        self.assertEqual(response.data, expected)
        assert mock_update.call_count == 0
        assert models.StaleCompletion.objects.filter(
            resolved=False).count() == 2

    @XBlock.register_temp_plugin(StubCourse, 'course')
    @XBlock.register_temp_plugin(StubSequential, 'sequential')
    @XBlock.register_temp_plugin(StubHTML, 'html')
    def test_detail_view_staff_requested_multiple_users(self):
        """
        Test that requesting course completions for a set of users filters out the other enrolled users
        """
        version = 1
        users = self.create_enrolled_users(3)
        self.create_course_completion_data(users[0], 3.0, 12.0)
        self.create_course_completion_data(users[1], 9.0, 12.0)
        self.create_course_completion_data(users[2], 6.0, 12.0)
        self.client.force_authenticate(self.staff_user)
        user_ids = "{},{}".format(users[0].id, users[2].id)
        response = self.client.get(
            self.get_detail_url(version, self.course_key, user_ids=user_ids))
        self.assertEqual(response.status_code, 200)
        expected_values = [
            {
                'username':
                users[0].username,
                'course_key':
                'edX/toy/2012_Fall',
                'completion':
                self._get_expected_completion(1,
                                              earned=3.0,
                                              possible=12.0,
                                              percent=0.25),
            },
            {
                'username':
                users[2].username,
                'course_key':
                'edX/toy/2012_Fall',
                'completion':
                self._get_expected_completion(1,
                                              earned=6.0,
                                              possible=12.0,
                                              percent=0.5),
            },
        ]
        expected = self._get_expected_detail(version, expected_values, count=2)
        self.assertEqual(response.data, expected)

    @XBlock.register_temp_plugin(StubCourse, 'course')
    @XBlock.register_temp_plugin(StubSequential, 'sequential')
    @XBlock.register_temp_plugin(StubHTML, 'html')
    def test_detail_view_staff_requested_multiple_users_with_post(self):
        """
        Test that requesting course completions for a set of users filters out the other enrolled users
        using POST request values
        """
        version = 1
        users = self.create_enrolled_users(3)
        self.create_course_completion_data(users[0], 3.0, 12.0)
        self.create_course_completion_data(users[1], 9.0, 12.0)
        self.create_course_completion_data(users[2], 6.0, 12.0)
        self.client.force_authenticate(self.staff_user)
        body = {'user_ids': [int(users[0].id), int(users[2].id)]}
        response = self.client.post(self.get_detail_url(
            version, self.course_key),
                                    data=json.dumps(body),
                                    content_type='application/json')
        self.assertEqual(response.status_code, 200)
        expected_values = [
            {
                'username':
                users[0].username,
                'course_key':
                'edX/toy/2012_Fall',
                'completion':
                self._get_expected_completion(1,
                                              earned=3.0,
                                              possible=12.0,
                                              percent=0.25),
            },
            {
                'username':
                users[2].username,
                'course_key':
                'edX/toy/2012_Fall',
                'completion':
                self._get_expected_completion(1,
                                              earned=6.0,
                                              possible=12.0,
                                              percent=0.5),
            },
        ]
        expected = self._get_expected_detail(version, expected_values, count=2)
        self.assertEqual(response.data, expected)

    @XBlock.register_temp_plugin(StubCourse, 'course')
    @XBlock.register_temp_plugin(StubSequential, 'sequential')
    @XBlock.register_temp_plugin(StubHTML, 'html')
    def test_detail_view_staff_requested_username_with_post(self):
        """
        Test that requesting course completions for a defined username
        using POST request values
        """
        version = 1
        users = self.create_enrolled_users(2)
        self.create_course_completion_data(users[0], 3.0, 12.0)
        self.create_course_completion_data(users[1], 9.0, 12.0)
        self.client.force_authenticate(self.staff_user)
        body = {'username': users[0].username}
        response = self.client.post(self.get_detail_url(
            version,
            self.course_key,
        ),
                                    data=json.dumps(body),
                                    content_type='application/json')
        self.assertEqual(response.status_code, 200)
        expected_values = [{
            'course_key':
            'edX/toy/2012_Fall',
            'completion':
            self._get_expected_completion(1,
                                          earned=3.0,
                                          possible=12.0,
                                          percent=0.25),
        }]
        expected = self._get_expected_detail(version, expected_values, count=1)
        self.assertEqual(response.data, expected)

    def _create_cohort(self, owner, users):
        """
        Create and populate a user group, as well as a cohort.
        """
        user_group = empty_compat.course_user_group().objects.create(
            name='test',
            course_id=self.course_key,
            group_type='cohort',
        )
        user_group.users.add(*users)
        owner.cohortmembership_set.add(
            empty_compat.cohort_membership_model().objects.create(
                course_user_group=user_group,
                user=owner,
                course_id=self.course_key,
            ), )

    @XBlock.register_temp_plugin(StubCourse, 'course')
    @XBlock.register_temp_plugin(StubSequential, 'sequential')
    @XBlock.register_temp_plugin(StubHTML, 'html')
    def test_stat_view_course_no_cohorts(self):
        response = self.client.get(
            self.get_course_stat_url(
                'edX/toy/2012_Fall',
                cohorts=1,
                exclude_roles='staff',
            ))

        self.assertEqual(response.status_code, 200)
        data = json.loads(response.content.decode('utf-8'))
        self.assertEqual(data['results'][0]['mean_completion']['earned'], 1.0)
        self.assertEqual(data['results'][0]['mean_completion']['possible'],
                         8.0)
        self.assertEqual(data['results'][0]['mean_completion']['percent'],
                         .125)

    @XBlock.register_temp_plugin(StubCourse, 'course')
    @XBlock.register_temp_plugin(StubSequential, 'sequential')
    @XBlock.register_temp_plugin(StubHTML, 'html')
    def test_stat_view_staff_user_excluded_from_results(self):
        self.create_enrollment(user=self.staff_user, course_id=self.course_key)
        self._create_cohort(self.staff_user, [self.staff_user])
        models.Aggregator.objects.submit_completion(
            user=self.staff_user,
            course_key=self.course_key,
            block_key=self.course_key.make_usage_key(block_type='course',
                                                     block_id='course'),
            aggregation_name='course',
            earned=4.0,
            possible=8.0,
            last_modified=timezone.now(),
        )
        response = self.client.get(
            self.get_course_stat_url('edX/toy/2012_Fall',
                                     cohorts=1,
                                     exclude_roles='staff'))
        data = json.loads(response.content.decode('utf-8'))

        self.assertEqual(data['results'][0]['mean_completion']['earned'], 2.5)
        self.assertEqual(data['results'][0]['mean_completion']['possible'],
                         8.0)

    @XBlock.register_temp_plugin(StubCourse, 'course')
    @XBlock.register_temp_plugin(StubSequential, 'sequential')
    @XBlock.register_temp_plugin(StubHTML, 'html')
    def test_stat_view_unengaged_user(self):
        self.create_enrollment(user=self.staff_user, course_id=self.course_key)
        response = self.client.get(
            self.get_course_stat_url('edX/toy/2012_Fall', ))
        data = json.loads(response.content.decode('utf-8'))
        self.assertEqual(data['results'][0]['mean_completion']['earned'], 0.5)
        self.assertEqual(data['results'][0]['mean_completion']['possible'],
                         8.0)
        self.assertEqual(data['results'][0]['mean_completion']['percent'],
                         0.0625)

    @XBlock.register_temp_plugin(StubCourse, 'course')
    @XBlock.register_temp_plugin(StubSequential, 'sequential')
    @XBlock.register_temp_plugin(StubHTML, 'html')
    def test_stat_view_exclude_user_based_on_role(self):
        beta_user = User.objects.create(username='******')
        self.create_enrollment(user=beta_user, course_id=self.course_key)
        self._create_cohort(beta_user, [beta_user])
        models.Aggregator.objects.submit_completion(
            user=beta_user,
            course_key=self.course_key,
            block_key=self.course_key.make_usage_key(block_type='course',
                                                     block_id='course'),
            aggregation_name='course',
            earned=7.0,
            possible=8.0,
            last_modified=timezone.now(),
        )

        response = self.client.get(
            self.get_course_stat_url('edX/toy/2012_Fall',
                                     cohorts=1,
                                     exclude_roles='beta'))
        data = json.loads(response.content.decode('utf-8'))

        self.assertEqual(data['results'][0]['mean_completion']['earned'], 1.0)
        self.assertEqual(data['results'][0]['mean_completion']['possible'],
                         8.0)

    @XBlock.register_temp_plugin(StubCourse, 'course')
    @XBlock.register_temp_plugin(StubSequential, 'sequential')
    @XBlock.register_temp_plugin(StubHTML, 'html')
    def test_stat_view_multiple_users_correct_calculations(self):
        users_in_cohort = []
        for x in range(1, 5):
            user = User.objects.create(username='******'.format(x))
            users_in_cohort.append(user)
            self.create_enrollment(user=user, course_id=self.course_key)

            models.Aggregator.objects.submit_completion(
                user=user,
                course_key=self.course_key,
                block_key=self.course_key.make_usage_key(block_type='course',
                                                         block_id='course'),
                aggregation_name='course',
                earned=4.0,
                possible=8.0,
                last_modified=timezone.now(),
            )
        self._create_cohort(users_in_cohort[0], users_in_cohort)

        response = self.client.get(
            self.get_course_stat_url('edX/toy/2012_Fall',
                                     cohorts=1,
                                     exclude_roles='staff'))
        data = json.loads(response.content.decode('utf-8'))

        self.assertEqual(data['results'][0]['mean_completion']['possible'],
                         8.0)
        self.assertEqual(data['results'][0]['mean_completion']['earned'], 3.4)
        self.assertEqual(data['results'][0]['mean_completion']['percent'],
                         0.425)

    @ddt.data(0, 1)
    @XBlock.register_temp_plugin(StubCourse, 'course')
    @XBlock.register_temp_plugin(StubSequential, 'sequential')
    @XBlock.register_temp_plugin(StubHTML, 'html')
    def test_invalid_optional_fields(self, version):
        response = self.client.get(
            self.get_detail_url(version,
                                'edX/toy/2012_Fall',
                                username=self.test_user.username,
                                requested_fields="INVALID"))
        self.assertEqual(response.status_code, 400)

    @ddt.data(0, 1)
    @XBlock.register_temp_plugin(StubCourse, 'course')
    @XBlock.register_temp_plugin(StubSequential, 'sequential')
    @XBlock.register_temp_plugin(StubHTML, 'html')
    def test_unauthenticated(self, version):
        self.client.force_authenticate(None)
        detailresponse = self.client.get(
            self.get_detail_url(version, self.course_key))
        self.assertEqual(detailresponse.status_code, 401)
        listresponse = self.client.get(self.get_list_url(version))
        self.assertEqual(listresponse.status_code, 401)

    @ddt.data(0, 1)
    @XBlock.register_temp_plugin(StubCourse, 'course')
    @XBlock.register_temp_plugin(StubSequential, 'sequential')
    @XBlock.register_temp_plugin(StubHTML, 'html')
    def test_request_self(self, version):
        response = self.client.get(
            self.get_list_url(version, username=self.test_user.username))
        self.assertEqual(response.status_code, 200)

    @ddt.data(0, 1)
    @XBlock.register_temp_plugin(StubCourse, 'course')
    @XBlock.register_temp_plugin(StubSequential, 'sequential')
    @XBlock.register_temp_plugin(StubHTML, 'html')
    def test_wrong_user(self, version):
        user = User.objects.create(username='******')
        self.client.force_authenticate(user)
        response = self.client.get(
            self.get_list_url(version, username=self.test_user.username))
        self.assertEqual(response.status_code, 403)

    @ddt.data(0, 1)
    @XBlock.register_temp_plugin(StubCourse, 'course')
    @XBlock.register_temp_plugin(StubSequential, 'sequential')
    @XBlock.register_temp_plugin(StubHTML, 'html')
    def test_no_user(self, version):
        self.client.logout()
        response = self.client.get(self.get_list_url(version))
        self.assertEqual(response.status_code, 401)

    @ddt.data(0, 1)
    @XBlock.register_temp_plugin(StubCourse, 'course')
    @XBlock.register_temp_plugin(StubSequential, 'sequential')
    @XBlock.register_temp_plugin(StubHTML, 'html')
    def test_staff_access(self, version):
        self.client.force_authenticate(self.staff_user)
        response = self.client.get(
            self.get_list_url(version, username=self.test_user.username))
        self.assertEqual(response.status_code, 200)
        expected_completion = self._get_expected_completion(version)
        self.assertEqual(response.data['results'][0]['completion'],
                         expected_completion)

    @ddt.data(0, 1)
    @XBlock.register_temp_plugin(StubCourse, 'course')
    @XBlock.register_temp_plugin(StubSequential, 'sequential')
    @XBlock.register_temp_plugin(StubHTML, 'html')
    def test_staff_access_non_user(self, version):
        self.client.force_authenticate(self.staff_user)
        response = self.client.get(
            self.get_list_url(version, username='******'))
        self.assertEqual(response.status_code, 404)

    @ddt.data(0, 1)
    @XBlock.register_temp_plugin(StubCourse, 'course')
    @XBlock.register_temp_plugin(StubSequential, 'sequential')
    @XBlock.register_temp_plugin(StubHTML, 'html')
    def test_no_staff_access_other_user_detail(self, version):
        self.client.force_authenticate(self.test_user)
        test_user2 = User.objects.create(username='******')
        self.create_enrollment(
            user=test_user2,
            course_id=self.course_key,
        )
        response = self.client.get(
            self.get_detail_url(version,
                                self.course_key,
                                username=test_user2.username))
        self.assertEqual(response.status_code, 403)

    @ddt.data(0, 1)
    @XBlock.register_temp_plugin(StubCourse, 'course')
    @XBlock.register_temp_plugin(StubSequential, 'sequential')
    @XBlock.register_temp_plugin(StubHTML, 'html')
    def test_no_staff_access_other_user(self, version):
        self.client.force_authenticate(self.test_user)
        test_user2 = User.objects.create(username='******')
        self.create_enrollment(
            user=test_user2,
            course_id=self.course_key,
        )
        response = self.client.get(
            self.get_list_url(version, username=test_user2.username))
        self.assertEqual(response.status_code, 403)

    @ddt.data(0, 1)
    @XBlock.register_temp_plugin(StubCourse, 'course')
    @XBlock.register_temp_plugin(StubSequential, 'sequential')
    @XBlock.register_temp_plugin(StubHTML, 'html')
    def test_no_staff_access_no_user(self, version):
        self.client.force_authenticate(self.test_user)
        response = self.client.get(self.get_list_url(version))
        self.assertEqual(response.status_code, 403 if version == 1 else 200)

    @ddt.data(0, 1)
    @XBlock.register_temp_plugin(StubCourse, 'course')
    @XBlock.register_temp_plugin(StubSequential, 'sequential')
    @XBlock.register_temp_plugin(StubHTML, 'html')
    def test_staff_access_no_user(self, version):
        self.client.force_authenticate(self.staff_user)
        response = self.client.get(self.get_list_url(version))
        self.assertEqual(response.status_code, 200)

    def get_course_stat_url(self, course_key, **params):
        """
        Given a course_key and a number of key-value pairs as keyword arguments,
        create a URL to the stats view.
        """
        return append_params(
            self.course_stat_url_fmt.format(six.text_type(course_key)), params)

    def get_detail_url(self, version, course_key, **params):
        """
        Given a course_key and a number of key-value pairs as keyword arguments,
        create a URL to the detail view.
        """
        return append_params(
            self.detail_url_fmt.format(version, six.text_type(course_key)),
            params)

    def get_list_url(self, version, **params):
        """
        Given a number of key-value pairs as keyword arguments,
        create a URL to the list view.
        """
        return append_params(self.list_url.format(version), params)
from django.utils import timezone

from completion.models import BlockCompletion, BlockCompletionManager
from completion_aggregator import models
from completion_aggregator.api.v1.views import CompletionViewMixin
from completion_aggregator.core import AggregationUpdater
from completion_aggregator.utils import WAFFLE_AGGREGATE_STALE_FROM_SCRATCH
from test_utils.compat import StubCompat
from test_utils.test_blocks import StubCourse, StubHTML, StubSequential

try:
    from django.urls import reverse
except ImportError:  # Django 1.8 compatibility
    from django.core.urlresolvers import reverse

empty_compat = StubCompat([])


def _create_oauth2_token(user):
    """
    Create an OAuth2 Access Token for the specified user,
    to test OAuth2-based API authentication

    Returns the token as a string.
    """
    # Use django-oauth-toolkit (DOT) models to create the app and token:
    dot_app = dot_models.Application.objects.create(
        name='test app',
        user=User.objects.create(),
        client_type='confidential',
        authorization_grant_type='authorization-code',
import pytest
from mock import patch
from opaque_keys.edx.keys import CourseKey
from xblock.core import XBlock

from django.contrib.auth.models import User
from django.test import TestCase
from django.utils import timezone

from completion_aggregator import models, serializers
from completion_aggregator.core import AggregationUpdater
from test_utils.compat import StubCompat
from test_utils.test_blocks import StubCourse, StubSequential

stub_compat = StubCompat([
    CourseKey.from_string('course-v1:abc+def+ghi').make_usage_key(
        'course', 'course'),
])


class AggregatorAdapterTestCase(TestCase):
    """
    Test the behavior of the AggregatorAdapter
    """
    def setUp(self):
        super(AggregatorAdapterTestCase, self).setUp()
        self.test_user = User.objects.create()
        self.course_key = CourseKey.from_string("course-v1:z+b+c")

    @XBlock.register_temp_plugin(StubCourse, 'course')
    @XBlock.register_temp_plugin(StubSequential, 'sequential')
    def test_simple_aggregation_structure(self):