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()
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)
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):