From c198bd70ec0b4ea9fc3dd369a193995439bba7bf Mon Sep 17 00:00:00 2001 From: Naeem Ilyas Date: Wed, 4 Jul 2018 13:51:55 +0500 Subject: [PATCH] Revert "Backport completion-by-viewing logic" --- .gitignore | 1 - .../public/js/vertical_student_view.js | 8 +- common/lib/xmodule/xmodule/vertical_block.py | 18 +- .../test/acceptance/pages/lms/completion.py | 25 --- .../test/acceptance/pages/lms/courseware.py | 26 +-- .../tests/lms/test_lms_courseware.py | 128 +----------- common/test/db_fixtures/waffle_flags.json | 9 - lms/djangoapps/courseware/views/views.py | 10 - lms/djangoapps/lms_xblock/mixin.py | 18 -- lms/djangoapps/lms_xblock/runtime.py | 3 - lms/static/coffee/src/courseware.coffee | 7 - lms/static/completion/js/.eslintrc.js | 7 - .../completion/js/CompletionOnViewService.js | 44 ----- lms/static/completion/js/ViewedEvent.js | 182 ------------------ .../completion/js/spec/ViewedEvent_spec.js | 98 ---------- lms/static/lms/js/spec/main.js | 1 - .../courseware/courseware-chromeless.html | 14 +- openedx/core/lib/xblock_utils/__init__.py | 3 - requirements/edx/base.txt | 2 +- webpack.config.js | 3 +- 20 files changed, 9 insertions(+), 598 deletions(-) delete mode 100644 common/test/acceptance/pages/lms/completion.py delete mode 100644 common/test/db_fixtures/waffle_flags.json delete mode 100644 lms/static/completion/js/.eslintrc.js delete mode 100644 lms/static/completion/js/CompletionOnViewService.js delete mode 100644 lms/static/completion/js/ViewedEvent.js delete mode 100644 lms/static/completion/js/spec/ViewedEvent_spec.js diff --git a/.gitignore b/.gitignore index 593dc3cf19e7..f50ad217f395 100644 --- a/.gitignore +++ b/.gitignore @@ -20,7 +20,6 @@ cms/envs/private.py *.orig /nbproject .idea/ -.vscode/ .redcar/ codekit-config.json .pycharm_helpers/ diff --git a/common/lib/xmodule/xmodule/assets/vertical/public/js/vertical_student_view.js b/common/lib/xmodule/xmodule/assets/vertical/public/js/vertical_student_view.js index fdc7f9dede29..2f4865356cd3 100644 --- a/common/lib/xmodule/xmodule/assets/vertical/public/js/vertical_student_view.js +++ b/common/lib/xmodule/xmodule/assets/vertical/public/js/vertical_student_view.js @@ -1,15 +1,9 @@ /* JavaScript for Vertical Student View. */ - -/* global markBlocksCompletedOnViewIfNeeded:false */ - window.VerticalStudentView = function(runtime, element) { 'use strict'; - - markBlocksCompletedOnViewIfNeeded(runtime, element); - if (typeof RequireJS === 'undefined') { // eslint-disable-next-line no-console - console.warn('Cannot initialize bookmarks for VerticalStudentView. RequireJS is not defined.'); + console.log('Cannot initialize VerticalStudentView. RequireJS is not defined.'); return; } RequireJS.require(['js/bookmarks/views/bookmark_button'], function(BookmarkButton) { diff --git a/common/lib/xmodule/xmodule/vertical_block.py b/common/lib/xmodule/xmodule/vertical_block.py index 70fd2c04d5cb..d253a1f67e7c 100644 --- a/common/lib/xmodule/xmodule/vertical_block.py +++ b/common/lib/xmodule/xmodule/vertical_block.py @@ -2,10 +2,8 @@ VerticalBlock - an XBlock which renders its children in a column. """ import logging -import os from copy import copy -from django.conf import settings from lxml import etree from xblock.core import XBlock from xblock.fragment import Fragment @@ -25,7 +23,6 @@ @XBlock.needs('user', 'bookmarks') -@XBlock.wants('completion') class VerticalBlock(SequenceFields, XModuleFields, StudioEditableBlock, XmlParserMixin, MakoTemplateBlockBase, XBlock): """ Layout XBlock for rendering subblocks vertically. @@ -58,24 +55,14 @@ def student_view(self, context): if 'username' not in child_context: user_service = self.runtime.service(self, 'user') child_context['username'] = user_service.get_current_user().opt_attrs['edx-platform.username'] - child_blocks = self.get_display_items() - child_blocks_to_complete_on_view = set() - completion_service = self.runtime.service(self, 'completion') - if completion_service and completion_service.completion_tracking_enabled(): - child_blocks_to_complete_on_view = completion_service.blocks_to_mark_complete_on_view(child_blocks) child_context['child_of_vertical'] = True is_child_of_vertical = context.get('child_of_vertical', False) # pylint: disable=no-member - for child in child_blocks: - child_block_context = copy(child_context) - if child in child_blocks_to_complete_on_view: - child_block_context['wrap_xblock_data'] = { - 'mark-completed-on-view-after-delay': completion_service.get_complete_on_view_delay_ms() - } - rendered_child = child.render(STUDENT_VIEW, child_block_context) + for child in self.get_display_items(): + rendered_child = child.render(STUDENT_VIEW, child_context) fragment.add_frag_resources(rendered_child) contents.append({ @@ -92,7 +79,6 @@ def student_view(self, context): 'bookmark_id': u"{},{}".format(child_context['username'], unicode(self.location)) })) - fragment.add_javascript_url(self.runtime.STATIC_URL + 'bundles/CompletionOnViewService.js') fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/vertical_student_view.js')) fragment.initialize_js('VerticalStudentView') diff --git a/common/test/acceptance/pages/lms/completion.py b/common/test/acceptance/pages/lms/completion.py deleted file mode 100644 index b535af7f51ff..000000000000 --- a/common/test/acceptance/pages/lms/completion.py +++ /dev/null @@ -1,25 +0,0 @@ -""" -Mixins for completion. -""" - - -class CompletionOnViewMixin(object): - """ - Methods for testing completion on view. - """ - - def xblock_components_mark_completed_on_view_value(self): - """ - Return the xblock components data-mark-completed-on-view-after-delay value. - """ - return self.q(css=self.xblock_component_selector).attrs(u'data-mark-completed-on-view-after-delay') - - def wait_for_xblock_component_to_be_marked_completed_on_view(self, index=0): - """ - Wait for xblock component to be marked completed on view. - - Arguments - index (int): index of block to wait on. (default is 0) - """ - self.wait_for(lambda: (self.xblock_components_mark_completed_on_view_value()[index] == u'0'), - u'Waiting for xblock to be marked completed on view.') diff --git a/common/test/acceptance/pages/lms/courseware.py b/common/test/acceptance/pages/lms/courseware.py index 6dabe9f2e555..2240808c6089 100644 --- a/common/test/acceptance/pages/lms/courseware.py +++ b/common/test/acceptance/pages/lms/courseware.py @@ -8,13 +8,11 @@ from bok_choy.promise import EmptyPromise from selenium.webdriver.common.action_chains import ActionChains -from common.test.acceptance.pages.lms import BASE_URL from common.test.acceptance.pages.lms.bookmarks import BookmarksPage -from common.test.acceptance.pages.lms.completion import CompletionOnViewMixin from common.test.acceptance.pages.lms.course_page import CoursePage -class CoursewarePage(CoursePage, CompletionOnViewMixin): +class CoursewarePage(CoursePage): """ Course info. """ @@ -589,25 +587,3 @@ def visit_course_outline_page(self): # reload the same page with the course_outline_page flag self.browser.get(self.browser.current_url + "&course_experience.course_outline_page=1") self.wait_for_page() - - -class RenderXBlockPage(PageObject, CompletionOnViewMixin): - """ - render_xblock page. - """ - - xblock_component_selector = '.xblock' - - def __init__(self, browser, block_id): - super(RenderXBlockPage, self).__init__(browser) - self.block_id = block_id - - @property - def url(self): - """ - Construct a URL to the page within the course. - """ - return BASE_URL + "/xblock/" + self.block_id - - def is_browser_on_page(self): - return self.q(css='.course-content').present diff --git a/common/test/acceptance/tests/lms/test_lms_courseware.py b/common/test/acceptance/tests/lms/test_lms_courseware.py index aa500a6d9b02..e86fd7e78dcf 100644 --- a/common/test/acceptance/tests/lms/test_lms_courseware.py +++ b/common/test/acceptance/tests/lms/test_lms_courseware.py @@ -14,7 +14,7 @@ from ...pages.common.auto_auth import AutoAuthPage from ...pages.common.logout import LogoutPage from ...pages.lms.course_home import CourseHomePage -from ...pages.lms.courseware import CoursewarePage, CoursewareSequentialTabPage, RenderXBlockPage +from ...pages.lms.courseware import CoursewarePage, CoursewareSequentialTabPage from ...pages.lms.create_mode import ModeCreationPage from ...pages.lms.dashboard import DashboardPage from ...pages.lms.pay_and_verify import FakePaymentPage, FakeSoftwareSecureVerificationPage, PaymentAndVerificationFlow @@ -947,129 +947,3 @@ def test_subsecton_hidden_after_due_date(self): self.progress_page.visit() self.assertEqual(self.progress_page.scores('Test Section 1', 'Test Subsection 1'), [(0, 1)]) - - -@attr(shard=9) -class CompletionTestCase(UniqueCourseTest, EventsTestMixin): - """ - Test the completion on view functionality. - """ - USERNAME = "STUDENT_TESTER" - EMAIL = "student101@example.com" - COMPLETION_BY_VIEWING_DELAY_MS = '1000' - - def setUp(self): - super(CompletionTestCase, self).setUp() - - self.studio_course_outline = StudioCourseOutlinePage( - self.browser, - self.course_info['org'], - self.course_info['number'], - self.course_info['run'] - ) - - # Install a course with sections/problems, tabs, updates, and handouts - course_fix = CourseFixture( - self.course_info['org'], self.course_info['number'], - self.course_info['run'], self.course_info['display_name'] - ) - - self.html_1_block = XBlockFixtureDesc('html', 'html 1', data="html 1 dummy body") - self.problem_1_block = XBlockFixtureDesc( - 'problem', 'Test Problem 1', data='problem 1 dummy body' - ) - - course_fix.add_children( - XBlockFixtureDesc('chapter', 'Test Section 1').add_children( - XBlockFixtureDesc('sequential', 'Test Subsection 1,1').add_children( - XBlockFixtureDesc('vertical', 'Test Unit 1,1,1').add_children( - XBlockFixtureDesc('html', 'html 1', data="html 1 dummy body"), - XBlockFixtureDesc( - 'html', 'html 2', - data=("html 2 dummy body" * 100) + "End", - ), - XBlockFixtureDesc('problem', 'Test Problem 1', data='problem 1 dummy body'), - ), - XBlockFixtureDesc('vertical', 'Test Unit 1,1,2').add_children( - XBlockFixtureDesc('html', 'html 1', data="html 1 dummy body"), - XBlockFixtureDesc('problem', 'Test Problem 1', data='problem 1 dummy body'), - ), - XBlockFixtureDesc('vertical', 'Test Unit 1,1,2').add_children( - self.html_1_block, - self.problem_1_block, - ), - ), - ), - ).install() - - # Auto-auth register for the course. - AutoAuthPage(self.browser, username=self.USERNAME, email=self.EMAIL, - course_id=self.course_id, staff=False).visit() - - def test_courseware_publish_completion_is_sent_on_view(self): - """ - Test that when viewing courseware XBlocks are correctly marked as completed on view. - """ - courseware_page = CoursewarePage(self.browser, self.course_id) - courseware_page.visit() - courseware_page.wait_for_page() - - # Initially, the first two blocks in the first vertical should be marked as needing to be completed on view. - self.assertEqual( - courseware_page.xblock_components_mark_completed_on_view_value(), - [self.COMPLETION_BY_VIEWING_DELAY_MS, self.COMPLETION_BY_VIEWING_DELAY_MS, None], - ) - # Wait and verify that the first block which is completely visible is marked as completed. - courseware_page.wait_for_xblock_component_to_be_marked_completed_on_view(0) - self.assertEqual( - courseware_page.xblock_components_mark_completed_on_view_value(), - ['0', self.COMPLETION_BY_VIEWING_DELAY_MS, None], - ) - - # Scroll to the bottom of the second block. - courseware_page.scroll_to_element('#html2-end', 'Scroll to end of html 2 block') - # Wait and verify that the second block is also now marked as completed. - courseware_page.wait_for_xblock_component_to_be_marked_completed_on_view(1) - self.assertEqual(courseware_page.xblock_components_mark_completed_on_view_value(), ['0', '0', None]) - - # After page refresh, no blocks in the vertical should be marked as needing to be completed on view. - self.browser.refresh() - courseware_page.wait_for_page() - self.assertEqual(courseware_page.xblock_components_mark_completed_on_view_value(), [None, None, None]) - - courseware_page.go_to_sequential_position(2) - - # Initially, the first block in the second vertical should be marked as needing to be completed on view. - self.assertEqual( - courseware_page.xblock_components_mark_completed_on_view_value(), - [self.COMPLETION_BY_VIEWING_DELAY_MS, None], - ) - # Wait and verify that the first block which is completely visible is marked as completed. - courseware_page.wait_for_xblock_component_to_be_marked_completed_on_view(0) - self.assertEqual(courseware_page.xblock_components_mark_completed_on_view_value(), ['0', None]) - - # After page refresh, no blocks in the vertical should be marked as needing to be completed on view. - self.browser.refresh() - courseware_page.wait_for_page() - self.assertEqual(courseware_page.xblock_components_mark_completed_on_view_value(), [None, None]) - - def test_render_xblock_publish_completion_is_sent_on_view(self): - """ - Test that when viewing a XBlock in render_xblock, it is correctly marked as completed on view. - """ - block_page = RenderXBlockPage(self.browser, self.html_1_block.locator) - block_page.visit() - block_page.wait_for_page() - - # Initially the block should be marked as needing to be completed on view. - self.assertEqual( - block_page.xblock_components_mark_completed_on_view_value(), [self.COMPLETION_BY_VIEWING_DELAY_MS] - ) - # Wait and verify that the block is marked as completed on view. - block_page.wait_for_xblock_component_to_be_marked_completed_on_view(0) - self.assertEqual(block_page.xblock_components_mark_completed_on_view_value(), ['0']) - - # After page refresh, it should not be marked as needing to be completed on view. - self.browser.refresh() - block_page.wait_for_page() - self.assertEqual(block_page.xblock_components_mark_completed_on_view_value(), [None]) diff --git a/common/test/db_fixtures/waffle_flags.json b/common/test/db_fixtures/waffle_flags.json deleted file mode 100644 index 43de53b0a7ce..000000000000 --- a/common/test/db_fixtures/waffle_flags.json +++ /dev/null @@ -1,9 +0,0 @@ -[ - { - "pk": 2, - "fields": { - "name": "completion.enable_completion_tracking", - "active": true - } - } -] diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index 7748c4d2c636..e29be8df6b19 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -1458,15 +1458,6 @@ def render_xblock(request, usage_key_string, check_if_enrolled=True): student_view_context = request.GET.dict() student_view_context['show_bookmark_button'] = False - enable_completion_on_view_service = False - completion_service = block.runtime.service(block, 'completion') - if completion_service and completion_service.completion_tracking_enabled(): - if completion_service.blocks_to_mark_complete_on_view({block}): - enable_completion_on_view_service = True - student_view_context['wrap_xblock_data'] = { - 'mark-completed-on-view-after-delay': completion_service.get_complete_on_view_delay_ms() - } - context = { 'fragment': block.render('student_view', context=student_view_context), 'course': course, @@ -1475,7 +1466,6 @@ def render_xblock(request, usage_key_string, check_if_enrolled=True): 'disable_header': True, 'disable_footer': True, 'disable_window_wrap': True, - 'enable_completion_on_view_service': enable_completion_on_view_service, 'staff_access': bool(has_access(request.user, 'staff', course)), 'xqa_server': settings.FEATURES.get('XQA_SERVER', 'http://your_xqa_server.com'), } diff --git a/lms/djangoapps/lms_xblock/mixin.py b/lms/djangoapps/lms_xblock/mixin.py index 9b60ad14f45f..2e850088a60e 100644 --- a/lms/djangoapps/lms_xblock/mixin.py +++ b/lms/djangoapps/lms_xblock/mixin.py @@ -5,14 +5,12 @@ #from django.utils.translation import ugettext_noop as _ from lazy import lazy from xblock.core import XBlock -from xblock.exceptions import JsonHandlerError from xblock.fields import Boolean, Dict, Scope, String, XBlockMixin from xblock.validation import ValidationMessage from xmodule.modulestore.inheritance import UserPartitionList from xmodule.partitions.partitions import NoSuchUserPartitionError, NoSuchUserPartitionGroupError - # Please do not remove, this is a workaround for Django 1.8. # more information can be found here: https://openedx.atlassian.net/browse/PLAT-902 _ = lambda text: text @@ -33,7 +31,6 @@ def to_json(self, access_dict): @XBlock.needs('partitions') -@XBlock.wants('completion') class LmsBlockMixin(XBlockMixin): """ Mixin that defines fields common to all blocks used in the LMS @@ -182,18 +179,3 @@ def validate(self): ) ) return validation - - @XBlock.json_handler - def publish_completion(self, data, suffix=''): # pylint: disable=unused-argument - """ - Publish completion data from the front end. - """ - completion_service = self.runtime.service(self, 'completion') - if completion_service is None: - raise JsonHandlerError(500, u"No completion service found") - elif not completion_service.completion_tracking_enabled(): - raise JsonHandlerError(404, u"Completion tracking is not enabled and API calls are unexpected") - if not completion_service.should_mark_block_completed_on_view(self): - raise JsonHandlerError(400, u"Block not configured for completion on view.") - self.runtime.publish(self, u"completion", data) - return {u'result': u'ok'} diff --git a/lms/djangoapps/lms_xblock/runtime.py b/lms/djangoapps/lms_xblock/runtime.py index 9ce5fef9e49b..105afd46e157 100644 --- a/lms/djangoapps/lms_xblock/runtime.py +++ b/lms/djangoapps/lms_xblock/runtime.py @@ -1,9 +1,7 @@ """ Module implementing `xblock.runtime.Runtime` functionality for the LMS """ - import xblock.reference.plugins -from completion.services import CompletionService from django.conf import settings from django.core.urlresolvers import reverse @@ -180,7 +178,6 @@ class LmsModuleSystem(LmsCourse, LmsUser, ModuleSystem): # pylint: disable=abst def __init__(self, **kwargs): request_cache_dict = RequestCache.get_request_cache().data services = kwargs.setdefault('services', {}) - services['completion'] = CompletionService(user=kwargs.get('user'), course_key=kwargs.get('course_id')) services['fs'] = xblock.reference.plugins.FSService() services['i18n'] = ModuleI18nService services['library_tools'] = LibraryToolsService(modulestore()) diff --git a/lms/static/coffee/src/courseware.coffee b/lms/static/coffee/src/courseware.coffee index ff44e7b6a20d..49e27c484b8c 100644 --- a/lms/static/coffee/src/courseware.coffee +++ b/lms/static/coffee/src/courseware.coffee @@ -10,13 +10,6 @@ class @Courseware render: -> XBlock.initializeBlocks($('.course-content')) - - courseContentElement = $('.course-content')[0] - blocks = XBlock.initializeBlocks(courseContentElement) - - if (courseContentElement.dataset.enableCompletionOnViewService == 'true') - markBlocksCompletedOnViewIfNeeded(blocks[0].runtime, courseContentElement) - $('.course-content .histogram').each -> id = $(this).attr('id').replace(/histogram_/, '') try diff --git a/lms/static/completion/js/.eslintrc.js b/lms/static/completion/js/.eslintrc.js deleted file mode 100644 index 12cb26eef91e..000000000000 --- a/lms/static/completion/js/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: 'eslint-config-edx', - root: true, - settings: { - 'import/resolver': 'webpack', - }, -}; diff --git a/lms/static/completion/js/CompletionOnViewService.js b/lms/static/completion/js/CompletionOnViewService.js deleted file mode 100644 index 833610ca4c15..000000000000 --- a/lms/static/completion/js/CompletionOnViewService.js +++ /dev/null @@ -1,44 +0,0 @@ -import { ViewedEventTracker } from './ViewedEvent'; - -const completedBlocksKeys = new Set(); - -// eslint-disable-next-line import/prefer-default-export -export function markBlocksCompletedOnViewIfNeeded(runtime, containerElement) { - const blockElements = $(containerElement).find( - '.xblock-student_view[data-mark-completed-on-view-after-delay]', - ).get(); - - if (blockElements.length > 0) { - const tracker = new ViewedEventTracker(); - - blockElements.forEach((blockElement) => { - const markCompletedOnViewAfterDelay = parseInt( - blockElement.dataset.markCompletedOnViewAfterDelay, 10, - ); - if (markCompletedOnViewAfterDelay >= 0) { - tracker.addElement(blockElement, markCompletedOnViewAfterDelay); - } - }); - - tracker.addHandler((blockElement, event) => { - const blockKey = blockElement.dataset.usageId; - if (blockKey && !completedBlocksKeys.has(blockKey)) { - if (event.elementHasBeenViewed) { - $.ajax({ - type: 'POST', - url: runtime.handlerUrl(blockElement, 'publish_completion'), - data: JSON.stringify({ - completion: 1.0, - }), - }).then( - () => { - completedBlocksKeys.add(blockKey); - // eslint-disable-next-line no-param-reassign - blockElement.dataset.markCompletedOnViewAfterDelay = 0; - }, - ); - } - } - }); - } -} diff --git a/lms/static/completion/js/ViewedEvent.js b/lms/static/completion/js/ViewedEvent.js deleted file mode 100644 index fe2d8f3fead1..000000000000 --- a/lms/static/completion/js/ViewedEvent.js +++ /dev/null @@ -1,182 +0,0 @@ -/** Ensure that a function is only run once every `wait` milliseconds */ -function throttle(fn, wait) { - let time = 0; - function delay() { - // Do not call the function until at least `wait` seconds after the - // last time the function was called. - const now = Date.now(); - if (time + wait < now) { - time = now; - fn(); - } - } - return delay; -} - - -export class ElementViewing { - /** - * A wrapper for an HTMLElement that tracks whether the element has been - * viewed or not. - */ - constructor(el, viewedAfterMs, callback) { - this.el = el; - this.viewedAfterMs = viewedAfterMs; - this.callback = callback; - - this.topSeen = false; - this.bottomSeen = false; - this.seenForMs = 0; - this.becameVisibleAt = undefined; - this.hasBeenViewed = false; - } - - getBoundingRect() { - return this.el.getBoundingClientRect(); - } - - /** This element has become visible on screen. - * - * (may be called even when already on screen though) - */ - handleVisible() { - if (!this.becameVisibleAt) { - this.becameVisibleAt = Date.now(); - // We're now visible; after viewedAfterMs, if the top and bottom have been - // seen, this block will count as viewed. - setTimeout( - () => { - this.checkIfViewed(); - }, - this.viewedAfterMs - this.seenForMs, - ); - } - } - - handleNotVisible() { - if (this.becameVisibleAt) { - this.seenForMs = Date.now() - this.becameVisibleAt; - } - this.becameVisibleAt = undefined; - } - - markTopSeen() { - // If this element has been seen for enough time, but the top wasn't visible, it may now be - // considered viewed. - this.topSeen = true; - this.checkIfViewed(); - } - - markBottomSeen() { - this.bottomSeen = true; - this.checkIfViewed(); - } - - getTotalTimeSeen() { - if (this.becameVisibleAt) { - return this.seenForMs + (Date.now() - this.becameVisibleAt); - } - return this.seenForMs; - } - - areViewedCriteriaMet() { - return this.topSeen && this.bottomSeen && (this.getTotalTimeSeen() >= this.viewedAfterMs); - } - - checkIfViewed() { - // User can provide a "now" value for testing purposes. - if (this.hasBeenViewed) { - return; - } - if (this.areViewedCriteriaMet()) { - this.hasBeenViewed = true; - // Report to the tracker that we have been viewed - this.callback(this.el, { elementHasBeenViewed: this.hasBeenViewed }); - } - } -} - - -export class ViewedEventTracker { - /** - * When the top or bottom of an element is first viewed, and the entire - * element is viewed for a specified amount of time, the callback is called, - * passing the element that was viewed, and an event object having the - * following field: - * - * * hasBeenViewed (bool): true if all the conditions for being - * considered "viewed" have been met. - */ - constructor() { - this.elementViewings = new Set(); - this.handlers = []; - this.registerDomHandlers(); - } - - /** Add an element to track. */ - addElement(element, viewedAfterMs) { - this.elementViewings.add( - new ElementViewing( - element, - viewedAfterMs, - (el, event) => this.callHandlers(el, event), - ), - ); - this.updateVisible(); - } - - /** Register a new handler to be called when an element has been viewed. */ - addHandler(handler) { - this.handlers.push(handler); - } - - /** Mark which elements are currently visible. - * - * Also marks when an elements top or bottom has been seen. - * */ - updateVisible() { - this.elementViewings.forEach((elv) => { - if (elv.hasBeenViewed) { - return; - } - - const now = Date.now(); // Use the same "now" for all calculations - const rect = elv.getBoundingRect(); - let visible = false; - - if (rect.top > 0 && rect.top < window.innerHeight) { - elv.markTopSeen(now); - visible = true; - } - if (rect.bottom > 0 && rect.bottom < window.innerHeight) { - elv.markBottomSeen(now); - visible = true; - } - if (rect.top < 0 && rect.bottom > window.innerHeight) { - visible = true; - } - - if (visible) { - elv.handleVisible(now); - } else { - elv.handleNotVisible(now); - } - }); - } - - registerDomHandlers() { - window.onscroll = throttle(() => this.updateVisible(), 100); - window.onresize = throttle(() => this.updateVisible(), 100); - this.updateVisible(); - } - - /** Call the handlers for all newly-viewed elements and pause tracking - * for recently disappeared elements. - */ - callHandlers(el, event) { - this.handlers.forEach((handler) => { - handler(el, event); - }); - } -} - diff --git a/lms/static/completion/js/spec/ViewedEvent_spec.js b/lms/static/completion/js/spec/ViewedEvent_spec.js deleted file mode 100644 index 62662317423c..000000000000 --- a/lms/static/completion/js/spec/ViewedEvent_spec.js +++ /dev/null @@ -1,98 +0,0 @@ -import { ElementViewing, ViewedEventTracker } from '../ViewedEvent'; - - -describe('ViewedTracker', () => { - let existingHTML; - beforeEach(() => { - existingHTML = document.body.innerHTML; - }); - - afterEach(() => { - document.body.innerHTML = existingHTML; - }); - - it('calls the handlers when an element is viewed', () => { - document.body.innerHTML = '
'; - const tracker = new ViewedEventTracker(); - for (const element of document.getElementsByTagName('div')) { // eslint-disable-line no-restricted-syntax - tracker.addElement(element, 1000); - } - const handlerSpy = jasmine.createSpy('handlerSpy'); - tracker.addHandler(handlerSpy); - const elvIter = tracker.elementViewings.values(); - // Pick two elements, and mock them so that one has met the criteria to be viewed, - // and the other hasn't. - const viewed = elvIter.next().value; - spyOn(viewed, 'areViewedCriteriaMet').and.returnValue(true); - viewed.checkIfViewed(); - expect(handlerSpy).toHaveBeenCalledWith(viewed.el, { - elementHasBeenViewed: true, - }); - const unviewed = elvIter.next().value; - spyOn(unviewed, 'areViewedCriteriaMet').and.returnValue(false); - unviewed.checkIfViewed(); - expect(handlerSpy).not.toHaveBeenCalledWith(unviewed.el, jasmine.anything()); - }); -}); - -describe('ElementViewing', () => { - beforeEach(() => { - jasmine.clock().install(); - }); - - afterEach(() => { - jasmine.clock().uninstall(); - }); - - it('calls checkIfViewed when enough time has elapsed', () => { - const viewing = new ElementViewing({}, 500, () => {}); - spyOn(viewing, 'checkIfViewed').and.callThrough(); - viewing.seenForMs = 250; - viewing.handleVisible(); - jasmine.clock().tick(249); - expect(viewing.checkIfViewed).not.toHaveBeenCalled(); - jasmine.clock().tick(1); - expect(viewing.checkIfViewed).toHaveBeenCalled(); - }); - - it('has been viewed after the specified number of milliseconds', () => { - const viewing = new ElementViewing({}, 500, () => {}); - viewing.seenForMs = 250; - spyOn(Date, 'now').and.returnValue(750); - viewing.handleVisible(); - viewing.markTopSeen(); - viewing.markBottomSeen(); - Date.now.and.returnValue(999); - viewing.checkIfViewed(); - expect(viewing.hasBeenViewed).toBeFalsy(); - Date.now.and.returnValue(1000); - jasmine.clock().tick(250); - expect(viewing.hasBeenViewed).toBeTruthy(); - }); - - it('has not been viewed if the bottom has not been seen', () => { - const viewing = new ElementViewing(undefined, 500, () => {}); - viewing.markTopSeen(); - viewing.seenForMs = 500; - expect(viewing.areViewedCriteriaMet()).toBeFalsy(); - viewing.checkIfViewed(); - expect(viewing.hasBeenViewed).toBeFalsy(); - }); - - it('has not been viewed if the top has not been seen', () => { - const viewing = new ElementViewing(undefined, 500, () => {}); - viewing.markBottomSeen(); - viewing.seenForMs = 500; - expect(viewing.areViewedCriteriaMet()).toBeFalsy(); - viewing.checkIfViewed(); - expect(viewing.hasBeenViewed).toBeFalsy(); - }); - - it('does not update time seen if lastSeen is undefined', () => { - const viewing = new ElementViewing(undefined, 500, () => {}); - viewing.becameVisibleAt = undefined; - expect(viewing.becameVisibleAt).toBeUndefined(); - viewing.handleVisible(); - expect(viewing.becameVisibleAt).not.toBeUndefined(); - }); -}); diff --git a/lms/static/lms/js/spec/main.js b/lms/static/lms/js/spec/main.js index 28440b25d926..4395917af681 100644 --- a/lms/static/lms/js/spec/main.js +++ b/lms/static/lms/js/spec/main.js @@ -673,7 +673,6 @@ }); testFiles = [ - 'completion/js/spec/ViewedEvent_spec.js', 'course_bookmarks/js/spec/bookmark_button_view_spec.js', 'course_bookmarks/js/spec/bookmarks_list_view_spec.js', 'course_bookmarks/js/spec/course_bookmarks_factory_spec.js', diff --git a/lms/templates/courseware/courseware-chromeless.html b/lms/templates/courseware/courseware-chromeless.html index 92cb8f694432..6ebabf686009 100644 --- a/lms/templates/courseware/courseware-chromeless.html +++ b/lms/templates/courseware/courseware-chromeless.html @@ -54,15 +54,11 @@ ## codemirror - % if enable_completion_on_view_service: - - - % endif <%static:js group='courseware'/> <%include file="/mathjax_include.html" args="disable_fast_preview=True"/> % if staff_access: - <%include file="xqa_interface.html"/> + <%include file="xqa_interface.html"/> % endif