Example #1
0
    def get_context_data(self, **kwargs):
        self.presenter = CourseEngagementVideoPresenter(self.access_token, self.course_id)
        context = super(EngagementVideoContentTemplateView, self).get_context_data(**kwargs)
        context.update({
            'sections': self.presenter.sections(),
            'update_message': self.get_last_updated_message(self.presenter.last_updated),
            'no_data_message': self.no_data_message
        })

        return context
Example #2
0
class EngagementVideoContentTemplateView(CourseStructureMixin,
                                         CourseStructureExceptionMixin,
                                         EngagementTemplateView):
    page_title = _('Engagement Videos')
    active_secondary_nav_item = 'videos'
    section_id = None
    subsection_id = None
    # Translators: Do not translate UTC.
    update_message = _(
        'Video data was last updated %(update_date)s at %(update_time)s UTC.')
    no_data_message = _(
        'Looks like no one has watched any videos in these sections.')

    def get_context_data(self, **kwargs):
        self.presenter = CourseEngagementVideoPresenter(
            self.access_token, self.course_id)
        context = super(EngagementVideoContentTemplateView,
                        self).get_context_data(**kwargs)
        context.update({
            'sections':
            self.presenter.sections(),
            'update_message':
            self.get_last_updated_message(self.presenter.last_updated),
            'no_data_message':
            self.no_data_message
        })

        return context
    def get_context_data(self, **kwargs):
        self.presenter = CourseEngagementVideoPresenter(self.access_token, self.course_id)
        context = super(EngagementVideoContentTemplateView, self).get_context_data(**kwargs)
        context.update({
            'sections': self.presenter.sections(),
            'update_message': self.get_last_updated_message(self.presenter.last_updated),
            'no_data_message': self.no_data_message
        })

        return context
    def get_context_data(self, **kwargs):
        self.presenter = CourseEngagementVideoPresenter(self.access_token, self.course_id)
        context = super(EngagementVideoContentTemplateView, self).get_context_data(**kwargs)
        context['js_data']['course'].update({
            'showVideoCount': True,  # overwrite to hide video count column
        })
        context.update({
            'sections': self.presenter.sections(),
            'update_message': self.get_last_updated_message(self.presenter.last_updated),
            'no_data_message': self.no_data_message
        })

        return context
class EngagementVideoContentTemplateView(CourseStructureMixin, CourseStructureExceptionMixin, EngagementTemplateView):
    page_title = _('Engagement Videos')
    active_secondary_nav_item = 'videos'
    section_id = None
    subsection_id = None
    # Translators: Do not translate UTC.
    update_message = _('Video data was last updated %(update_date)s at %(update_time)s UTC.')
    no_data_message = _('Looks like no one has watched any videos in these sections.')

    def get_context_data(self, **kwargs):
        self.presenter = CourseEngagementVideoPresenter(self.access_token, self.course_id)
        context = super(EngagementVideoContentTemplateView, self).get_context_data(**kwargs)
        context.update({
            'sections': self.presenter.sections(),
            'update_message': self.get_last_updated_message(self.presenter.last_updated),
            'no_data_message': self.no_data_message
        })

        return context
class EngagementVideoContentTemplateView(CourseStructureMixin, CourseStructureExceptionMixin, EngagementTemplateView):
    page_title = _('Engagement Videos')
    active_secondary_nav_item = 'videos'
    section_id = None
    subsection_id = None
    # Translators: Do not translate UTC.
    update_message = _('Video data was last updated %(update_date)s at %(update_time)s UTC.')
    no_data_message = _('No videos watched for these exercises.')

    def get_context_data(self, **kwargs):
        self.presenter = CourseEngagementVideoPresenter(self.access_token, self.course_id)
        context = super(EngagementVideoContentTemplateView, self).get_context_data(**kwargs)
        context['js_data']['course'].update({
            'showVideoCount': True,  # overwrite to hide video count column
        })
        context.update({
            'sections': self.presenter.sections(),
            'update_message': self.get_last_updated_message(self.presenter.last_updated),
            'no_data_message': self.no_data_message
        })

        return context
 def setUp(self):
     super(CourseEngagementVideoPresenterTests, self).setUp()
     self.course_id = 'this/course/id'
     self.presenter = CourseEngagementVideoPresenter(None, self.course_id)
class CourseEngagementVideoPresenterTests(SwitchMixin, TestCase):
    SECTION_ID = 'i4x://edX/DemoX/chapter/9fca584977d04885bc911ea76a9ef29e'
    SUBSECTION_ID = 'i4x://edX/DemoX/sequential/07bc32474380492cb34f76e5f9d9a135'
    VIDEO_ID = 'i4x://edX/DemoX/video/0b9e39477cf34507a7a48f74be381fdd'
    VIDEO_1 = VideoFixture()
    VIDEO_2 = VideoFixture()
    VIDEO_3 = VideoFixture()

    def setUp(self):
        super(CourseEngagementVideoPresenterTests, self).setUp()
        self.course_id = 'this/course/id'
        self.presenter = CourseEngagementVideoPresenter(None, self.course_id)

    def test_default_block_data(self):
        self.assertDictEqual(self.presenter.default_block_data, {
            'users_at_start': 0,
            'users_at_end': 0,
            'end_percent': 0,
            'start_only_users': 0,
            'start_only_percent': 0,
        })

    def _create_graded_and_ungraded_course_structure_fixtures(self):
        """
        Create graded and ungraded video sections.
        """
        chapter_fixture = ChapterFixture()
        # a dictionary to access the fixtures easily
        course_structure_fixtures = {
            'chapter': chapter_fixture,
            'course': CourseFixture(org='this', course='course', run='id')
        }

        for grade_status in ['graded', 'ungraded']:
            sequential_fixture = SequentialFixture(graded=grade_status is 'graded').add_children(
                VerticalFixture().add_children(
                    VideoFixture()
                )
            )
            course_structure_fixtures[grade_status] = {
                'sequential': sequential_fixture,
            }
            chapter_fixture.add_children(sequential_fixture)

        course_structure_fixtures['course'].add_children(chapter_fixture)
        return course_structure_fixtures

    @data('graded', 'ungraded')
    @override_settings(CACHES={
        'default': {
            'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
        }
    })
    def test_graded_modes(self, grade_status):
        """
        Ensure that video structure will be retrieved for both graded and ungraded.
        """
        course_structure_fixtures = self._create_graded_and_ungraded_course_structure_fixtures()
        course_fixture = course_structure_fixtures['course']
        chapter_fixture = course_structure_fixtures['chapter']

        with mock.patch('slumber.Resource.get', mock.Mock(return_value=course_fixture.course_structure())):
            with mock.patch('analyticsclient.course.Course.videos',
                            mock.Mock(return_value=utils.get_mock_video_data(course_fixture))):
                # check that we get results for both graded and ungraded
                sequential_fixture = course_structure_fixtures[grade_status]['sequential']
                video_id = sequential_fixture.children[0].children[0].id

                actual_videos = self.presenter.subsection_children(chapter_fixture.id, sequential_fixture.id)
                expected_url = reverse('courses:engagement:video_timeline',
                                       kwargs={
                                           'course_id': self.course_id,
                                           'section_id': chapter_fixture.id,
                                           'subsection_id': sequential_fixture.id,
                                           'video_id': video_id
                                       })
                expected = [{'id': utils.get_encoded_module_id(video_id), 'url': expected_url}]
                self.assertEqual(len(actual_videos), len(expected))
                for index, actual_video in enumerate(actual_videos):
                    self.assertDictContainsSubset(expected[index], actual_video)

    def test_module_id_to_data_id(self):
        opaque_key_id = 'i4x-edX-DemoX-video-0b9e39477cf34507a7a48f74be381fdd'
        module_id = 'i4x://edX/DemoX/video/0b9e39477cf34507a7a48f74be381fdd'
        self.assertEqual(self.presenter.module_id_to_data_id({'id': module_id}), opaque_key_id)

        block_id = 'block-v1:edX+DemoX.1+2014+type@problem+block@466f474fa4d045a8b7bde1b911e095ca'
        self.assertEqual(self.presenter.module_id_to_data_id({'id': block_id}), '466f474fa4d045a8b7bde1b911e095ca')

    def test_post_process_adding_data_to_blocks(self):
        def url_func(parent_block, child_block):
            return '{}-{}'.format(parent_block, child_block)
        user_data = {'users_at_start': 10}
        self.presenter.post_process_adding_data_to_blocks(user_data, 'parent', 'child', url_func)
        self.assertDictContainsSubset({'url': 'parent-child'}, user_data)

        empty_data = {}
        self.presenter.post_process_adding_data_to_blocks(empty_data, 'parent', 'child', None)
        self.assertDictEqual({}, empty_data)

    def test_build_module_url_func(self):
        url_func = self.presenter.build_module_url_func(self.SECTION_ID)
        actual_url = url_func({'id': self.SUBSECTION_ID},
                              {'id': self.VIDEO_ID})
        expected_url = reverse('courses:engagement:video_timeline',
                               kwargs={
                                   'course_id': self.course_id,
                                   'section_id': self.SECTION_ID,
                                   'subsection_id': self.SUBSECTION_ID,
                                   'video_id': self.VIDEO_ID
                               })
        self.assertEqual(actual_url, expected_url)

    def test_build_subsection_url_func(self):
        url_func = self.presenter.build_subsection_url_func(self.SECTION_ID)
        actual_url = url_func({'id': self.SUBSECTION_ID})
        expected_url = reverse('courses:engagement:video_subsection',
                               kwargs={
                                   'course_id': self.course_id,
                                   'section_id': self.SECTION_ID,
                                   'subsection_id': self.SUBSECTION_ID,
                               })
        self.assertEqual(actual_url, expected_url)

    def test_build_section_url_func(self):
        actual_url = self.presenter.build_section_url({'id': self.SECTION_ID})
        expected_url = reverse('courses:engagement:video_section',
                               kwargs={
                                   'course_id': self.course_id,
                                   'section_id': self.SECTION_ID,
                               })
        self.assertEqual(actual_url, expected_url)

    def test_attach_computed_data(self):
        max_users = 15
        start_only_users = 10
        end_users = max_users - start_only_users
        end_percent = end_users / max_users
        module_data = {
            'encoded_module_id': self.VIDEO_ID,
            'users_at_start': max_users,
            'users_at_end': end_users
        }
        self.presenter.attach_computed_data(module_data)
        self.assertDictEqual(module_data, {
            'id': self.VIDEO_ID,
            'users_at_start': max_users,
            'users_at_end': end_users,
            'end_percent': end_percent,
            'start_only_users': start_only_users,
            'start_only_percent': start_only_users / max_users,
        })

    def test_greater_users_at_end(self):
        module_data = {
            'encoded_module_id': self.VIDEO_ID,
            'users_at_start': 0,
            'users_at_end': 1
        }
        self.presenter.attach_computed_data(module_data)
        self.assertDictEqual(module_data, {
            'id': self.VIDEO_ID,
            'users_at_start': 0,
            'users_at_end': 1,
            'end_percent': 1.0,
            'start_only_users': 0,
            'start_only_percent': 0.0,
        })

    def test_attach_aggregated_data_to_parent(self):
        parent = {
            'num_modules': 2,
            'children': [
                {
                    'users_at_start': 60,
                    'users_at_end': 40,
                },
                {
                    'users_at_start': 0,
                    'users_at_end': 0,
                },
            ]
        }
        expected = copy.deepcopy(parent)
        expected.update({
            'users_at_start': 60,
            'users_at_end': 40,
            'index': 1,
            'average_start_only_users': 10,
            'average_users_at_end': 20,
            'end_percent': 2/3,
            'start_only_users': 20,
            'start_only_percent': 1/3,
        })

        self.presenter.attach_aggregated_data_to_parent(0, parent)
        self.assertDictEqual(parent, expected)

    @mock.patch('analyticsclient.course.Course.videos')
    def test_fetch_course_module_data(self, mock_videos):
        factory = CourseEngagementDataFactory()
        videos = factory.videos()
        mock_videos.return_value = videos
        self.assertListEqual(self.presenter.fetch_course_module_data(), videos)

        mock_videos.side_effect = NoVideosError(course_id=self.course_id)
        with self.assertRaises(NoVideosError):
            self.presenter.fetch_course_module_data()

    @mock.patch('analyticsclient.module.Module.video_timeline')
    def test_get_video_timeline(self, mock_timeline):
        factory = CourseEngagementDataFactory()
        video_module = {
            'pipeline_video_id': 'edX/DemoX/Demo_Course|i4x-edX-DemoX-videoalpha-0b9e39477cf34507a7a48f74be381fdd',
            'segment_length': 5,
            'duration': None
        }
        # duration can be null/None
        mock_timeline.return_value = factory.get_video_timeline_api_response()
        actual_timeline = self.presenter.get_video_timeline(video_module)
        expected_timeline = factory.get_presented_video_timeline(duration=495)
        self.assertEqual(100, len(actual_timeline))
        self.assertTimeline(expected_timeline, actual_timeline)

        video_module['duration'] = 499
        mock_timeline.return_value = factory.get_video_timeline_api_response()
        actual_timeline = self.presenter.get_video_timeline(video_module)
        last_segment = expected_timeline[-1].copy()
        last_segment.update({
            'segment': last_segment['segment'] + 1,
            'start_time': video_module['duration']
        })
        expected_timeline.append(last_segment)
        self.assertEqual(101, len(actual_timeline))
        self.assertTimeline(expected_timeline, actual_timeline)

        video_module['duration'] = 501
        expected_timeline[-1].update({
            'start_time': 500,
            'num_users': 0,
            'num_views': 0,
            'num_replays': 0
        })
        last_segment = expected_timeline[-1].copy()
        last_segment.update({
            'segment': last_segment['segment'] + 1,
            'start_time': video_module['duration']
        })
        expected_timeline.append(last_segment)
        mock_timeline.return_value = factory.get_video_timeline_api_response()
        actual_timeline = self.presenter.get_video_timeline(video_module)
        self.assertEqual(102, len(actual_timeline))
        self.assertTimeline(expected_timeline, actual_timeline)

    def assertTimeline(self, expected_timeline, actual_timeline):
        self.assertEqual(len(expected_timeline), len(actual_timeline))
        for expected, actual in zip(expected_timeline, actual_timeline):
            self.assertDictContainsSubset(actual, expected)

    def test_build_live_url(self):
        actual_view_live_url = self.presenter.build_view_live_url('a-url', self.VIDEO_ID)
        self.assertEqual('a-url/{}/jump_to/{}'.format(self.course_id, self.VIDEO_ID), actual_view_live_url)
        self.assertEqual(None, self.presenter.build_view_live_url(None, self.VIDEO_ID))

    @data(
        (CourseFixture().add_children(
            ChapterFixture().add_children(
                SequentialFixture().add_children(
                    VerticalFixture().add_children(
                        VIDEO_1
                    )
                )
            )
        ), VIDEO_1, 0, VIDEO_1),
        (CourseFixture().add_children(
            ChapterFixture().add_children(
                SequentialFixture().add_children(
                    VerticalFixture().add_children(
                        VIDEO_1,
                        VIDEO_2
                    )
                )
            )
        ), VIDEO_1, 1, VIDEO_2),
        (CourseFixture().add_children(
            ChapterFixture().add_children(
                SequentialFixture().add_children(
                    VerticalFixture().add_children(
                        VIDEO_1,
                        VIDEO_2
                    )
                )
            )
        ), VIDEO_2, -1, VIDEO_1),
        (CourseFixture().add_children(
            ChapterFixture().add_children(
                SequentialFixture().add_children(
                    VerticalFixture().add_children(
                        VIDEO_1,
                    ),
                    VerticalFixture().add_children(
                        VIDEO_2,
                    )
                )
            )
        ), VIDEO_1, 1, VIDEO_2),
        (CourseFixture().add_children(
            ChapterFixture().add_children(
                SequentialFixture().add_children(
                    VerticalFixture().add_children(
                        VIDEO_1,
                    ),
                ),
                SequentialFixture().add_children(
                    VerticalFixture().add_children(
                        VIDEO_2,
                    ),
                )
            )
        ), VIDEO_1, 1, VIDEO_2),
        (CourseFixture().add_children(
            ChapterFixture().add_children(
                SequentialFixture().add_children(
                    VerticalFixture().add_children(
                        VIDEO_1,
                    ),
                ),
            ),
            ChapterFixture().add_children(
                SequentialFixture().add_children(
                    VerticalFixture().add_children(
                        VIDEO_2,
                    ),
                ),
            )
        ), VIDEO_1, 1, VIDEO_2),
        (CourseFixture().add_children(
            ChapterFixture().add_children(
                SequentialFixture().add_children(
                    VerticalFixture().add_children(
                        VIDEO_1,
                    ),
                ),
            ),
            ChapterFixture().add_children(
                SequentialFixture().add_children(
                    VerticalFixture().add_children(
                        VIDEO_2
                    ),
                ),
            ),
            ChapterFixture().add_children(
                SequentialFixture().add_children(
                    VerticalFixture().add_children(
                        VIDEO_3
                    ),
                ),
            )
        ), VIDEO_1, 2, VIDEO_3),
        (CourseFixture().add_children(
            ChapterFixture().add_children(
                SequentialFixture().add_children(
                    VerticalFixture().add_children(
                        VIDEO_1
                    )
                )
            )
        ), VIDEO_1, -1, None),
        (CourseFixture().add_children(
            ChapterFixture().add_children(
                SequentialFixture().add_children(
                    VerticalFixture().add_children(
                        VIDEO_1
                    )
                )
            )
        ), VIDEO_1, 1, None),
    )
    @unpack
    @override_settings(CACHES={
        'default': {
            'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
        }
    })
    def test_sibling(self, fixture, start_block, offset, expected_sibling_block):
        """Tests the _sibling method of the `CourseAPIPresenterMixin`."""
        with mock.patch(
            'analyticsclient.course.Course.videos', mock.Mock(return_value=utils.get_mock_video_data(fixture))
        ):
            with mock.patch('slumber.Resource.get', mock.Mock(return_value=fixture.course_structure())):
                sibling = self.presenter.sibling_block(utils.get_encoded_module_id(start_block['id']), offset)
                if expected_sibling_block is None:
                    self.assertIsNone(sibling)
                else:
                    self.assertEqual(sibling['id'], utils.get_encoded_module_id(expected_sibling_block['id']))

    @override_settings(CACHES={
        'default': {
            'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
        }
    })
    def test_sibling_no_data(self):
        """
        Verify that _sibling() skips over siblings with no data (no associated URL).
        """
        fixture = CourseFixture().add_children(
            ChapterFixture().add_children(
                SequentialFixture().add_children(
                    VerticalFixture().add_children(
                        self.VIDEO_1,
                        self.VIDEO_2,  # self.VIDEO_2 will have no data
                        self.VIDEO_3
                    )
                )
            )
        )
        with mock.patch(
            'analyticsclient.course.Course.videos',
            mock.Mock(return_value=utils.get_mock_video_data(fixture, excluded_module_ids=[self.VIDEO_2['id']]))
        ):
            with mock.patch('slumber.Resource.get', mock.Mock(return_value=fixture.course_structure())):
                sibling = self.presenter.sibling_block(utils.get_encoded_module_id(self.VIDEO_1['id']), 1)
                self.assertEqual(sibling['id'], utils.get_encoded_module_id(self.VIDEO_3['id']))

    @data('http://example.com', 'http://example.com/')
    def test_build_render_xblock_url(self, xblock_render_base):
        self.assertIsNone(self.presenter.build_render_xblock_url(None, None))
        expected_url = '/'.join(str(arg).rstrip('/') for arg in [xblock_render_base, self.VIDEO_ID])
        self.assertEqual(expected_url, self.presenter.build_render_xblock_url(xblock_render_base, self.VIDEO_ID))
class CourseEngagementVideoPresenterTests(SwitchMixin, TestCase):
    SECTION_ID = 'i4x://edX/DemoX/chapter/9fca584977d04885bc911ea76a9ef29e'
    SUBSECTION_ID = 'i4x://edX/DemoX/sequential/07bc32474380492cb34f76e5f9d9a135'
    VIDEO_ID = 'i4x://edX/DemoX/video/0b9e39477cf34507a7a48f74be381fdd'

    def setUp(self):
        super(CourseEngagementVideoPresenterTests, self).setUp()
        self.course_id = 'this/course/id'
        self.presenter = CourseEngagementVideoPresenter(None, self.course_id)

    def test_default_block_data(self):
        self.assertDictEqual(self.presenter.default_block_data, {
            'start_views': 0,
            'end_views': 0,
            'end_percent': 0,
            'start_only_views': 0,
            'start_only_percent': 0,
        })

    def test_module_id_to_data_id(self):
        opaque_key_id = 'i4x-edX-DemoX-video-0b9e39477cf34507a7a48f74be381fdd'
        module_id = 'i4x://edX/DemoX/video/0b9e39477cf34507a7a48f74be381fdd'
        self.assertEqual(self.presenter.module_id_to_data_id({'id': module_id}), opaque_key_id)

        block_id = 'block-v1:edX+DemoX.1+2014+type@problem+block@466f474fa4d045a8b7bde1b911e095ca'
        self.assertEqual(self.presenter.module_id_to_data_id({'id': block_id}), '466f474fa4d045a8b7bde1b911e095ca')

    def test_post_process_adding_data_to_blocks(self):
        def url_func(parent_block, child_block):
            return '{}-{}'.format(parent_block, child_block)
        data = {'start_views': 10}
        self.presenter.post_process_adding_data_to_blocks(data, 'parent', 'child', url_func)
        self.assertDictContainsSubset({'url': 'parent-child'}, data)

        data = {}
        self.presenter.post_process_adding_data_to_blocks(data, 'parent', 'child', None)
        self.assertDictEqual({}, data)

    def test_build_module_url_func(self):
        url_func = self.presenter.build_module_url_func(self.SECTION_ID)
        actual_url = url_func({'id': self.SUBSECTION_ID},
                              {'id': self.VIDEO_ID})
        expected_url = reverse('courses:engagement:video_timeline',
                               kwargs={
                                   'course_id': self.course_id,
                                   'section_id': self.SECTION_ID,
                                   'subsection_id': self.SUBSECTION_ID,
                                   'video_id': self.VIDEO_ID
                               })
        self.assertEqual(actual_url, expected_url)

    def test_build_subsection_url_func(self):
        url_func = self.presenter.build_subsection_url_func(self.SECTION_ID)
        actual_url = url_func({'id': self.SUBSECTION_ID})
        expected_url = reverse('courses:engagement:video_subsection',
                               kwargs={
                                   'course_id': self.course_id,
                                   'section_id': self.SECTION_ID,
                                   'subsection_id': self.SUBSECTION_ID,
                               })
        self.assertEqual(actual_url, expected_url)

    def test_build_section_url_func(self):
        actual_url = self.presenter.build_section_url({'id': self.SECTION_ID})
        expected_url = reverse('courses:engagement:video_section',
                               kwargs={
                                   'course_id': self.course_id,
                                   'section_id': self.SECTION_ID,
                               })
        self.assertEqual(actual_url, expected_url)

    def test_attach_computed_data(self):
        data = {
            'encoded_module_id': self.VIDEO_ID,
            'start_views': 15,
            'end_views': 5
        }
        self.presenter.attach_computed_data(data)
        self.assertDictEqual(data, {
            'id': self.VIDEO_ID,
            'start_views': 15,
            'end_views': 5,
            'end_percent': 0.25,
            'start_only_views': 10,
            'start_only_percent': 0.5,
        })

    @mock.patch('analyticsclient.course.Course.videos')
    def test_fetch_course_module_data(self, mock_videos):
        videos = [
            {
                "pipeline_video_id": "edX/DemoX/Demo_Course|i4x-edX-DemoX-video-7e9b434e6de3435ab99bd3fb25bde807",
                "encoded_module_id": "i4x-edX-DemoX-video-7e9b434e6de3435ab99bd3fb25bde807",
                "duration": 257,
                "segment_length": 5,
                "start_views": 10,
                "end_views": 0,
                "created": "2015-04-15T214158"
            },
            {
                "pipeline_video_id": "edX/DemoX/Demo_Course|i4x-edX-DemoX-videoalpha-0b9e39477cf34507a7a48f74be381fdd",
                "encoded_module_id": "i4x-edX-DemoX-videoalpha-0b9e39477cf34507a7a48f74be381fdd",
                "duration": 195,
                "segment_length": 5,
                "start_views": 55,
                "end_views": 0,
                "created": "2015-04-15T214158"
            }
        ]
        mock_videos.return_value = videos
        self.assertListEqual(self.presenter.fetch_course_module_data(), videos)

        mock_videos.side_effect = NoVideosError(course_id=self.course_id)
        with self.assertRaises(NoVideosError):
            self.presenter.fetch_course_module_data()
class CourseEngagementVideoPresenterTests(SwitchMixin, TestCase):
    SECTION_ID = 'i4x://edX/DemoX/chapter/9fca584977d04885bc911ea76a9ef29e'
    SUBSECTION_ID = 'i4x://edX/DemoX/sequential/07bc32474380492cb34f76e5f9d9a135'
    VIDEO_ID = 'i4x://edX/DemoX/video/0b9e39477cf34507a7a48f74be381fdd'

    def setUp(self):
        super(CourseEngagementVideoPresenterTests, self).setUp()
        self.course_id = 'this/course/id'
        self.presenter = CourseEngagementVideoPresenter(None, self.course_id)

    def test_default_block_data(self):
        self.assertDictEqual(self.presenter.default_block_data, {
            'users_at_start': 0,
            'users_at_end': 0,
            'end_percent': 0,
            'start_only_users': 0,
            'start_only_percent': 0,
        })

    def test_module_id_to_data_id(self):
        opaque_key_id = 'i4x-edX-DemoX-video-0b9e39477cf34507a7a48f74be381fdd'
        module_id = 'i4x://edX/DemoX/video/0b9e39477cf34507a7a48f74be381fdd'
        self.assertEqual(self.presenter.module_id_to_data_id({'id': module_id}), opaque_key_id)

        block_id = 'block-v1:edX+DemoX.1+2014+type@problem+block@466f474fa4d045a8b7bde1b911e095ca'
        self.assertEqual(self.presenter.module_id_to_data_id({'id': block_id}), '466f474fa4d045a8b7bde1b911e095ca')

    def test_post_process_adding_data_to_blocks(self):
        def url_func(parent_block, child_block):
            return '{}-{}'.format(parent_block, child_block)
        user_data = {'users_at_start': 10}
        self.presenter.post_process_adding_data_to_blocks(user_data, 'parent', 'child', url_func)
        self.assertDictContainsSubset({'url': 'parent-child'}, user_data)

        empty_data = {}
        self.presenter.post_process_adding_data_to_blocks(empty_data, 'parent', 'child', None)
        self.assertDictEqual({}, empty_data)

    def test_build_module_url_func(self):
        url_func = self.presenter.build_module_url_func(self.SECTION_ID)
        actual_url = url_func({'id': self.SUBSECTION_ID},
                              {'id': self.VIDEO_ID})
        expected_url = reverse('courses:engagement:video_timeline',
                               kwargs={
                                   'course_id': self.course_id,
                                   'section_id': self.SECTION_ID,
                                   'subsection_id': self.SUBSECTION_ID,
                                   'video_id': self.VIDEO_ID
                               })
        self.assertEqual(actual_url, expected_url)

    def test_build_subsection_url_func(self):
        url_func = self.presenter.build_subsection_url_func(self.SECTION_ID)
        actual_url = url_func({'id': self.SUBSECTION_ID})
        expected_url = reverse('courses:engagement:video_subsection',
                               kwargs={
                                   'course_id': self.course_id,
                                   'section_id': self.SECTION_ID,
                                   'subsection_id': self.SUBSECTION_ID,
                               })
        self.assertEqual(actual_url, expected_url)

    def test_build_section_url_func(self):
        actual_url = self.presenter.build_section_url({'id': self.SECTION_ID})
        expected_url = reverse('courses:engagement:video_section',
                               kwargs={
                                   'course_id': self.course_id,
                                   'section_id': self.SECTION_ID,
                               })
        self.assertEqual(actual_url, expected_url)

    def test_attach_computed_data(self):
        max_users = 15
        start_only_users = 10
        end_users = max_users - start_only_users
        end_percent = end_users / max_users
        module_data = {
            'encoded_module_id': self.VIDEO_ID,
            'users_at_start': max_users,
            'users_at_end': end_users
        }
        self.presenter.attach_computed_data(module_data)
        self.assertDictEqual(module_data, {
            'id': self.VIDEO_ID,
            'users_at_start': max_users,
            'users_at_end': end_users,
            'end_percent': end_percent,
            'start_only_users': start_only_users,
            'start_only_percent': start_only_users / max_users,
        })

    @mock.patch('analyticsclient.course.Course.videos')
    def test_fetch_course_module_data(self, mock_videos):
        videos = [
            {
                "pipeline_video_id": "edX/DemoX/Demo_Course|i4x-edX-DemoX-video-7e9b434e6de3435ab99bd3fb25bde807",
                "encoded_module_id": "i4x-edX-DemoX-video-7e9b434e6de3435ab99bd3fb25bde807",
                "duration": 257,
                "segment_length": 5,
                "users_at_start": 10,
                "users_at_end": 0,
                "created": "2015-04-15T214158"
            },
            {
                "pipeline_video_id": "edX/DemoX/Demo_Course|i4x-edX-DemoX-videoalpha-0b9e39477cf34507a7a48f74be381fdd",
                "encoded_module_id": "i4x-edX-DemoX-videoalpha-0b9e39477cf34507a7a48f74be381fdd",
                "duration": 195,
                "segment_length": 5,
                "users_at_start": 55,
                "users_at_end": 0,
                "created": "2015-04-15T214158"
            }
        ]
        mock_videos.return_value = videos
        self.assertListEqual(self.presenter.fetch_course_module_data(), videos)

        mock_videos.side_effect = NoVideosError(course_id=self.course_id)
        with self.assertRaises(NoVideosError):
            self.presenter.fetch_course_module_data()

    @mock.patch('analyticsclient.module.Module.video_timeline')
    def test_get_video_timeline(self, mock_timeline):
        factory = CourseEngagementDataFactory()
        video_module = {
            'pipeline_video_id': 'edX/DemoX/Demo_Course|i4x-edX-DemoX-videoalpha-0b9e39477cf34507a7a48f74be381fdd',
            'segment_length': 5,
            'duration': None
        }
        # duration can be null/None
        mock_timeline.return_value = factory.get_video_timeline_api_response()
        actual_timeline = self.presenter.get_video_timeline(video_module)
        expected_timeline = factory.get_presented_video_timeline(duration=495)
        self.assertEqual(100, len(actual_timeline))
        self.assertTimeline(expected_timeline, actual_timeline)

        video_module['duration'] = 499
        mock_timeline.return_value = factory.get_video_timeline_api_response()
        actual_timeline = self.presenter.get_video_timeline(video_module)
        last_segment = expected_timeline[-1].copy()
        last_segment.update({
            'segment': last_segment['segment'] + 1,
            'start_time': video_module['duration']
        })
        expected_timeline.append(last_segment)
        self.assertEqual(101, len(actual_timeline))
        self.assertTimeline(expected_timeline, actual_timeline)

        video_module['duration'] = 501
        expected_timeline[-1].update({
            'start_time': 500,
            'num_users': 0,
            'num_views': 0,
            'num_replays': 0
        })
        last_segment = expected_timeline[-1].copy()
        last_segment.update({
            'segment': last_segment['segment'] + 1,
            'start_time': video_module['duration']
        })
        expected_timeline.append(last_segment)
        mock_timeline.return_value = factory.get_video_timeline_api_response()
        actual_timeline = self.presenter.get_video_timeline(video_module)
        self.assertEqual(102, len(actual_timeline))
        self.assertTimeline(expected_timeline, actual_timeline)

    def assertTimeline(self, expected_timeline, actual_timeline):
        self.assertEqual(len(expected_timeline), len(actual_timeline))
        for expected, actual in zip(expected_timeline, actual_timeline):
            self.assertDictContainsSubset(actual, expected)

    def test_build_live_url(self):
        actual_view_live_url = self.presenter.build_view_live_url('a-url', self.VIDEO_ID)
        self.assertEqual('a-url/{}/jump_to/{}'.format(self.course_id, self.VIDEO_ID), actual_view_live_url)
        self.assertEqual(None, self.presenter.build_view_live_url(None, self.VIDEO_ID))
 def setUp(self):
     super(CourseEngagementVideoPresenterTests, self).setUp()
     self.course_id = 'this/course/id'
     self.presenter = CourseEngagementVideoPresenter(settings.COURSE_API_KEY, self.course_id)
Example #12
0
 def setUp(self):
     super(CourseEngagementVideoPresenterTests, self).setUp()
     self.course_id = 'this/course/id'
     self.presenter = CourseEngagementVideoPresenter(
         settings.COURSE_API_KEY, self.course_id)