Revert "Merge pull request #18340 from cpennington/fix-studio-xblock-rendering"

This reverts commit 9a70ca78a5, reversing
changes made to efd5b9abbd.

Hopefully fixes EDUCATOR-3053
This commit is contained in:
Eric Fischer
2018-06-15 13:18:27 -04:00
parent 44945dc03c
commit e72549cdbe
162 changed files with 7019 additions and 7642 deletions

View File

@@ -164,6 +164,31 @@ source, template_path = Loader(engine).load_template_source(path)
</script>
</%def>
<%def name="invoke_page_bundle(page_name, class_name=None)">
<%doc>
Loads Javascript onto your page synchronously.
Uses RequireJS in development and a plain script tag in production.
The body of the tag should be a comma-separated list of arguments
to be passed to the page factory specified by the class_name argument.
</%doc>
<%
body = capture(caller.body)
%>
% if class_name:
<script type="text/javascript">
if (typeof pageFactoryArguments == "undefined") {
var pageFactoryArguments = {};
}
% if body:
pageFactoryArguments['${class_name | n, js_escaped_string}'] = [${ body | n, decode.utf8 }];
% else:
pageFactoryArguments['${class_name | n, js_escaped_string}'] = [];
% endif
</script>
% endif
<%self:webpack entry="${page_name}"/>
</%def>
<%def name="require_module(module_name, class_name)">
<%doc>
Loads Javascript onto your page synchronously.

View File

@@ -36,6 +36,11 @@ REQUIREJS_WAIT = {
"jquery", "js/base", "js/models/course", "js/models/settings/advanced",
"js/views/settings/advanced", "codemirror"],
# Unit page
re.compile(r'^Unit \|'): [
"jquery", "js/base", "js/models/xblock_info", "js/views/pages/container",
"js/collections/component_template", "xmodule", "cms/js/main", "xblock/cms.runtime.v1"],
# Content - Outline
# Note that calling your org, course number, or display name, 'course' will mess this up
re.compile(r'^Course Outline \|'): [
@@ -43,27 +48,16 @@ REQUIREJS_WAIT = {
# Dashboard
re.compile(r'^Studio Home \|'): [
"gettext", "js/base",
"js/sock", "gettext", "js/base",
"jquery.ui", "cms/js/main", "underscore"],
# Pages
re.compile(r'^Pages \|'): [
'js/models/explicit_url', 'js/views/tabs', 'cms/js/main', 'xblock/cms.runtime.v1'
'js/models/explicit_url', 'js/views/tabs',
'xmodule', 'cms/js/main', 'xblock/cms.runtime.v1'
],
}
TRUTHY_WAIT = {
# Pages
re.compile(r'^Pages \|'): [
'XBlock'
],
# Unit page
re.compile(r'Unit \|'): [
"jQuery", "XBlock", "ContainerFactory"
],
}
@world.absorb
def wait(seconds):
@@ -72,15 +66,12 @@ def wait(seconds):
@world.absorb
def wait_for_js_to_load():
requirements = None
for test, req in REQUIREJS_WAIT.items():
if test.search(world.browser.title):
world.wait_for_requirejs(req)
requirements = req
break
for test, req in TRUTHY_WAIT.items():
if test.search(world.browser.title):
for var in req:
world.wait_for_js_variable_truthy(var)
world.wait_for_requirejs(requirements)
# Selenium's `execute_async_script` function pauses Selenium's execution
@@ -142,7 +133,7 @@ def wait_for_xmodule():
@world.absorb
def wait_for_mathjax():
"Wait until MathJax is loaded and set up on the page."
world.wait_for_js_variable_truthy("MathJax")
world.wait_for_js_variable_truthy("MathJax.isReady")
class RequireJSError(Exception):

View File

@@ -3,17 +3,3 @@
# Patch the xml libs before anything else.
from safe_lxml import defuse_xml_libs
defuse_xml_libs()
import pytest
@pytest.fixture(autouse=True)
def no_webpack_loader(monkeypatch):
monkeypatch.setattr(
"webpack_loader.templatetags.webpack_loader.render_bundle",
lambda entry, extension=None, config='DEFAULT', attrs='': ''
)
monkeypatch.setattr(
"webpack_loader.utils.get_as_tags",
lambda entry, extension=None, config='DEFAULT', attrs='': []
)

View File

@@ -1,6 +1,7 @@
/* JavaScript for Vertical Student View. */
/* global Set:false */ // false means do not assign to Set
/* global ViewedEventTracker:false */
// The vertical marks blocks complete if they are completable by viewing. The
// global variable SEEN_COMPLETABLES tracks blocks between separate loads of
@@ -8,61 +9,62 @@
// navigates back within a given sequential) to protect against duplicate calls
// to the server.
import BookmarkButton from 'course_bookmarks/js/views/bookmark_button';
import {ViewedEventTracker} from '../../../../../../../../lms/static/completion/js/ViewedEvent.js';
var SEEN_COMPLETABLES = new Set();
window.VerticalStudentView = function(runtime, element) {
'use strict';
var $element = $(element);
var $bookmarkButtonElement = $element.find('.bookmark-button');
return new BookmarkButton({
el: $bookmarkButtonElement,
bookmarkId: $bookmarkButtonElement.data('bookmarkId'),
usageId: $element.data('usageId'),
bookmarked: $element.parent('#seq_content').data('bookmarked'),
apiUrl: $bookmarkButtonElement.data('bookmarksApiUrl')
RequireJS.require(['course_bookmarks/js/views/bookmark_button'], function(BookmarkButton) {
var $element = $(element);
var $bookmarkButtonElement = $element.find('.bookmark-button');
return new BookmarkButton({
el: $bookmarkButtonElement,
bookmarkId: $bookmarkButtonElement.data('bookmarkId'),
usageId: $element.data('usageId'),
bookmarked: $element.parent('#seq_content').data('bookmarked'),
apiUrl: $bookmarkButtonElement.data('bookmarksApiUrl')
});
});
var tracker, vertical, viewedAfter;
var completableBlocks = [];
var vertModDivs = element.getElementsByClassName('vert-mod');
if (vertModDivs.length === 0) {
return;
}
vertical = vertModDivs[0];
$(element).find('.vert').each(function(idx, block) {
if (block.dataset.completableByViewing !== undefined) {
completableBlocks.push(block);
RequireJS.require(['bundles/ViewedEvent'], function() {
var tracker, vertical, viewedAfter;
var completableBlocks = [];
var vertModDivs = element.getElementsByClassName('vert-mod');
if (vertModDivs.length === 0) {
return;
}
});
if (completableBlocks.length > 0) {
viewedAfter = parseInt(vertical.dataset.completionDelayMs, 10);
if (!(viewedAfter >= 0)) {
// parseInt will return NaN if it fails to parse, which is not >= 0.
viewedAfter = 5000;
}
tracker = new ViewedEventTracker(completableBlocks, viewedAfter);
tracker.addHandler(function(block, event) {
var blockKey = block.dataset.id;
if (blockKey && !SEEN_COMPLETABLES.has(blockKey)) {
if (event.elementHasBeenViewed) {
$.ajax({
type: 'POST',
url: runtime.handlerUrl(element, 'publish_completion'),
data: JSON.stringify({
block_key: blockKey,
completion: 1.0
})
}).then(
function() {
SEEN_COMPLETABLES.add(blockKey);
}
);
}
vertical = vertModDivs[0];
$(element).find('.vert').each(function(idx, block) {
if (block.dataset.completableByViewing !== undefined) {
completableBlocks.push(block);
}
});
}
if (completableBlocks.length > 0) {
viewedAfter = parseInt(vertical.dataset.completionDelayMs, 10);
if (!(viewedAfter >= 0)) {
// parseInt will return NaN if it fails to parse, which is not >= 0.
viewedAfter = 5000;
}
tracker = new ViewedEventTracker(completableBlocks, viewedAfter);
tracker.addHandler(function(block, event) {
var blockKey = block.dataset.id;
if (blockKey && !SEEN_COMPLETABLES.has(blockKey)) {
if (event.elementHasBeenViewed) {
$.ajax({
type: 'POST',
url: runtime.handlerUrl(element, 'publish_completion'),
data: JSON.stringify({
block_key: blockKey,
completion: 1.0
})
}).then(
function() {
SEEN_COMPLETABLES.add(blockKey);
}
);
}
}
});
}
});
};

View File

@@ -1,10 +0,0 @@
module.exports = {
extends: 'eslint-config-edx',
root: true,
settings: {
'import/resolver': 'webpack',
},
overrides: {
excludedFiles: 'public/js/*',
},
};

View File

@@ -1 +0,0 @@
public

View File

@@ -0,0 +1,5 @@
window.WordCloud = function(el) {
RequireJS.require(['WordCloudMain'], function(WordCloudMain) {
new WordCloudMain(el);
});
};

View File

@@ -0,0 +1,349 @@
/**
* @file The main module definition for Word Cloud XModule.
*
* Defines a constructor function which operates on a DOM element. Either show the user text inputs so
* he can enter words, or render his selected words along with the word cloud representing the top words.
*
* @module WordCloudMain
*
* @exports WordCloudMain
*
* @external d3, $, RequireJS
*/
(function(requirejs, require, define) {
'use strict';
define('WordCloudMain', [
'gettext',
'edx-ui-toolkit/js/utils/html-utils'
], function(gettext, HtmlUtils) {
function generateUniqueId(wordCloudId, counter) {
return '_wc_' + wordCloudId + '_' + counter;
}
/**
* @function WordCloudMain
*
* This function will process all the attributes from the DOM element passed, taking all of
* the configuration attributes. It will either then attach a callback handler for the click
* event on the button in the case when the user needs to enter words, or it will call the
* appropriate mehtod to generate and render a word cloud from user's enetered words along with
* all of the other words.
*
* @constructor
*
* @param {jQuery} el DOM element where the word cloud will be processed and created.
*/
var WordCloudMain = function(el) {
var _this = this;
this.wordCloudEl = $(el).find('.word_cloud');
// Get the URL to which we will post the users words.
this.ajax_url = this.wordCloudEl.data('ajax-url');
// Dimensions of the box where the word cloud will be drawn.
this.width = 635;
this.height = 635;
// Hide WordCloud container before Ajax request done
this.wordCloudEl.hide();
// Retriveing response from the server as an AJAX request. Attach a callback that will
// be fired on server's response.
$.postWithPrefix(
_this.ajax_url + '/get_state', null,
function(response) {
if (response.status !== 'success') {
return;
}
_this.configJson = response;
}
)
.done(function() {
// Show WordCloud container after Ajax request done
_this.wordCloudEl.show();
if (_this.configJson && _this.configJson.submitted) {
_this.showWordCloud(_this.configJson);
return;
}
});
$(el).find('.save').on('click', function() {
_this.submitAnswer();
});
}; // End-of: var WordCloudMain = function(el) {
/**
* @function submitAnswer
*
* Callback to be executed when the user eneter his words. It will send user entries to the
* server, and upon receiving correct response, will call the function to generate the
* word cloud.
*/
WordCloudMain.prototype.submitAnswer = function() {
var _this = this,
data = {student_words: []};
// Populate the data to be sent to the server with user's words.
this.wordCloudEl.find('input.input-cloud').each(function(index, value) {
data.student_words.push($(value).val());
});
// Send the data to the server as an AJAX request. Attach a callback that will
// be fired on server's response.
$.postWithPrefix(
_this.ajax_url + '/submit', $.param(data),
function(response) {
if (response.status !== 'success') {
return;
}
_this.showWordCloud(response);
}
);
}; // End-of: WordCloudMain.prototype.submitAnswer = function() {
/**
* @function showWordCloud
*
* @param {object} response The response from the server that contains the user's entered words
* along with all of the top words.
*
* This function will set up everything for d3 and launch the draw method. Among other things,
* iw will determine maximum word size.
*/
WordCloudMain.prototype.showWordCloud = function(response) {
var words,
_this = this,
maxSize, minSize, scaleFactor, maxFontSize, minFontSize;
this.wordCloudEl.find('.input_cloud_section').hide();
words = response.top_words;
maxSize = 0;
minSize = 10000;
scaleFactor = 1;
maxFontSize = 200;
minFontSize = 16;
// Find the word with the maximum percentage. I.e. the most popular word.
$.each(words, function(index, word) {
if (word.size > maxSize) {
maxSize = word.size;
}
if (word.size < minSize) {
minSize = word.size;
}
});
// Find the longest word, and calculate the scale appropriately. This is
// required so that even long words fit into the drawing area.
//
// This is a fix for: if the word is very long and/or big, it is discarded by
// for unknown reason.
$.each(words, function(index, word) {
var tempScaleFactor = 1.0,
size = ((word.size / maxSize) * maxFontSize);
if (size * 0.7 * word.text.length > _this.width) {
tempScaleFactor = ((_this.width / word.text.length) / 0.7) / size;
}
if (scaleFactor > tempScaleFactor) {
scaleFactor = tempScaleFactor;
}
});
// Update the maximum font size based on the longest word.
maxFontSize *= scaleFactor;
// Generate the word cloud.
d3.layout.cloud().size([this.width, this.height])
.words(words)
.rotate(function() {
return Math.floor((Math.random() * 2)) * 90;
})
.font('Impact')
.fontSize(function(d) {
var size = (d.size / maxSize) * maxFontSize;
size = size >= minFontSize ? size : minFontSize;
return size;
})
.on('end', function(words, bounds) { // eslint-disable-line no-shadow
// Draw the word cloud.
_this.drawWordCloud(response, words, bounds);
})
.start();
}; // End-of: WordCloudMain.prototype.showWordCloud = function(response) {
/**
* @function drawWordCloud
*
* This function will be called when d3 has finished initing the state for our word cloud,
* and it is ready to hand off the process to the drawing routine. Basically set up everything
* necessary for the actual drwing of the words.
*
* @param {object} response The response from the server that contains the user's entered words
* along with all of the top words.
*
* @param {array} words An array of objects. Each object must have two properties. One property
* is 'text' (the actual word), and the other property is 'size' which represents the number that the
* word was enetered by the students.
*
* @param {array} bounds An array of two objects. First object is the top-left coordinates of the bounding
* box where all of the words fir, second object is the bottom-right coordinates of the bounding box. Each
* coordinate object contains two properties: 'x', and 'y'.
*/
WordCloudMain.prototype.drawWordCloud = function(response, words, bounds) {
// Color words in different colors.
var fill = d3.scale.category20(),
// Will be populated by words the user enetered.
studentWordsKeys = [],
// Comma separated string of user enetered words.
studentWordsStr,
// By default we do not scale.
scale = 1,
// Caсhing of DOM element
cloudSectionEl = this.wordCloudEl.find('.result_cloud_section'),
// Needed for caсhing of d3 group elements
groupEl,
// Iterator for word cloud count for uniqueness
wcCount = 0;
// If bounding rectangle is given, scale based on the bounding box of all the words.
if (bounds) {
scale = 0.5 * Math.min(
this.width / Math.abs(bounds[1].x - this.width / 2),
this.width / Math.abs(bounds[0].x - this.width / 2),
this.height / Math.abs(bounds[1].y - this.height / 2),
this.height / Math.abs(bounds[0].y - this.height / 2)
);
}
$.each(response.student_words, function(word, stat) {
var percent = (response.display_student_percents) ? ' ' + (Math.round(100 * (stat / response.total_count))) + '%' : '';
studentWordsKeys.push(HtmlUtils.interpolateHtml(
'{listStart}{startTag}{word}{endTag}{percent}{listEnd}',
{
listStart: HtmlUtils.HTML('<li>'),
startTag: HtmlUtils.HTML('<strong>'),
word: word,
endTag: HtmlUtils.HTML('</strong>'),
percent: percent,
listEnd: HtmlUtils.HTML('</li>')
}
).toString());
});
studentWordsStr = '' + studentWordsKeys.join('');
cloudSectionEl
.addClass('active');
HtmlUtils.setHtml(
cloudSectionEl.find('.your_words'),
HtmlUtils.HTML(studentWordsStr)
);
HtmlUtils.setHtml(
cloudSectionEl.find('.your_words').end().find('.total_num_words'),
HtmlUtils.interpolateHtml(
gettext('{start_strong}{total}{end_strong} words submitted in total.'),
{
start_strong: HtmlUtils.HTML('<strong>'),
end_strong: HtmlUtils.HTML('</strong>'),
total: response.total_count
}
)
);
$(cloudSectionEl.attr('id') + ' .word_cloud').empty();
// Actual drawing of word cloud.
groupEl = d3.select('#' + cloudSectionEl.attr('id') + ' .word_cloud').append('svg')
.attr('width', this.width)
.attr('height', this.height)
.append('g')
.attr('transform', 'translate(' + (0.5 * this.width) + ',' + (0.5 * this.height) + ')')
.selectAll('text')
.data(words)
.enter()
.append('g')
.attr('data-id', function() {
wcCount = wcCount + 1;
return wcCount;
})
.attr('aria-describedby', function() {
return HtmlUtils.interpolateHtml(
gettext('text_word_{uniqueId} title_word_{uniqueId}'),
{
uniqueId: generateUniqueId(cloudSectionEl.attr('id'), $(this).data('id'))
}
);
});
groupEl
.append('title')
.attr('id', function() {
return HtmlUtils.interpolateHtml(
gettext('title_word_{uniqueId}'),
{
uniqueId: generateUniqueId(cloudSectionEl.attr('id'), $(this).parent().data('id'))
}
);
})
.text(function(d) {
var res = '';
$.each(response.top_words, function(index, value) {
if (value.text === d.text) {
res = value.percent + '%';
return;
}
});
return res;
});
groupEl
.append('text')
.attr('id', function() {
return HtmlUtils.interpolateHtml(
gettext('text_word_{uniqueId}'),
{
uniqueId: generateUniqueId(cloudSectionEl.attr('id'), $(this).parent().data('id'))
}
);
})
.style('font-size', function(d) {
return d.size + 'px';
})
.style('font-family', 'Impact')
.style('fill', function(d, i) {
return fill(i);
})
.attr('text-anchor', 'middle')
.attr('transform', function(d) {
return 'translate(' + [d.x, d.y] + ')rotate(' + d.rotate + ')scale(' + scale + ')';
})
.text(function(d) {
return d.text;
});
}; // End-of: WordCloudMain.prototype.drawWordCloud = function(words, bounds) {
return WordCloudMain;
}); // End-of: define('WordCloudMain', [], function() {
}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function(requirejs, require, define) {

View File

@@ -1,3 +0,0 @@
const WordCloudMain = require('xmodule/assets/word_cloud/src/js/word_cloud_main.js');
window.WordCloud = WordCloudMain.default;

View File

@@ -1,315 +0,0 @@
/**
* @file The main module definition for Word Cloud XModule.
*
* Defines a constructor function which operates on a DOM element. Either
* show the user text inputs so he can enter words, or render his selected
* words along with the word cloud representing the top words.
*
* @module WordCloudMain
*
* @exports WordCloudMain
*
* @external $
*/
import * as HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils';
import d3 from 'd3.min';
import { cloud as d3Cloud } from 'd3.layout.cloud';
import gettext from 'gettext';
function generateUniqueId(wordCloudId, counter) {
return `_wc_${wordCloudId}_${counter}`;
}
/**
* @function WordCloudMain
*
* This function will process all the attributes from the DOM element passed, taking all of
* the configuration attributes. It will either then attach a callback handler for the click
* event on the button in the case when the user needs to enter words, or it will call the
* appropriate mehtod to generate and render a word cloud from user's enetered words along with
* all of the other words.
*
* @constructor
*
* @param {jQuery} el DOM element where the word cloud will be processed and created.
*/
export default class WordCloudMain {
constructor(el) {
this.wordCloudEl = $(el).find('.word_cloud');
// Get the URL to which we will post the users words.
this.ajax_url = this.wordCloudEl.data('ajax-url');
// Dimensions of the box where the word cloud will be drawn.
this.width = 635;
this.height = 635;
// Hide WordCloud container before Ajax request done
this.wordCloudEl.hide();
// Retriveing response from the server as an AJAX request. Attach a callback that will
// be fired on server's response.
$.postWithPrefix(
`${this.ajax_url}/get_state`,
null,
(response) => {
if (response.status !== 'success') {
return;
}
this.configJson = response;
},
)
.done(() => {
// Show WordCloud container after Ajax request done
this.wordCloudEl.show();
if (this.configJson && this.configJson.submitted) {
this.showWordCloud(this.configJson);
}
});
$(el).find('.save').on('click', () => {
this.submitAnswer();
});
}
/**
* @function submitAnswer
*
* Callback to be executed when the user eneter his words. It will send user entries to the
* server, and upon receiving correct response, will call the function to generate the
* word cloud.
*/
submitAnswer() {
const data = { student_words: [] };
// Populate the data to be sent to the server with user's words.
this.wordCloudEl.find('input.input-cloud').each((index, value) => {
data.student_words.push($(value).val());
});
// Send the data to the server as an AJAX request. Attach a callback that will
// be fired on server's response.
$.postWithPrefix(
`${this.ajax_url}/submit`, $.param(data),
(response) => {
if (response.status !== 'success') {
return;
}
this.showWordCloud(response);
},
);
}
/**
* @function showWordCloud
*
* @param {object} response The response from the server that contains the user's entered words
* along with all of the top words.
*
* This function will set up everything for d3 and launch the draw method. Among other things,
* iw will determine maximum word size.
*/
showWordCloud(response) {
const words = response.top_words;
let maxSize = 0;
let minSize = 10000;
let scaleFactor = 1;
let maxFontSize = 200;
const minFontSize = 16;
this.wordCloudEl.find('.input_cloud_section').hide();
// Find the word with the maximum percentage. I.e. the most popular word.
$.each(words, (index, word) => {
if (word.size > maxSize) {
maxSize = word.size;
}
if (word.size < minSize) {
minSize = word.size;
}
});
// Find the longest word, and calculate the scale appropriately. This is
// required so that even long words fit into the drawing area.
//
// This is a fix for: if the word is very long and/or big, it is discarded by
// for unknown reason.
$.each(words, (index, word) => {
let tempScaleFactor = 1.0;
const size = ((word.size / maxSize) * maxFontSize);
if (size * 0.7 * word.text.length > this.width) {
tempScaleFactor = ((this.width / word.text.length) / 0.7) / size;
}
if (scaleFactor > tempScaleFactor) {
scaleFactor = tempScaleFactor;
}
});
// Update the maximum font size based on the longest word.
maxFontSize *= scaleFactor;
// Generate the word cloud.
d3Cloud().size([this.width, this.height])
.words(words)
.rotate(() => Math.floor((Math.random() * 2)) * 90)
.font('Impact')
.fontSize((d) => {
let size = (d.size / maxSize) * maxFontSize;
size = size >= minFontSize ? size : minFontSize;
return size;
})
// Draw the word cloud.
.on('end', (wds, bounds) => this.drawWordCloud(response, wds, bounds))
.start();
}
/**
* @function drawWordCloud
*
* This function will be called when d3 has finished initing the state for our word cloud,
* and it is ready to hand off the process to the drawing routine. Basically set up everything
* necessary for the actual drwing of the words.
*
* @param {object} response The response from the server that contains the user's entered words
* along with all of the top words.
*
* @param {array} words An array of objects. Each object must have two properties. One property
* is 'text' (the actual word), and the other property is 'size' which represents the number that the
* word was enetered by the students.
*
* @param {array} bounds An array of two objects. First object is the top-left coordinates of the bounding
* box where all of the words fir, second object is the bottom-right coordinates of the bounding box. Each
* coordinate object contains two properties: 'x', and 'y'.
*/
drawWordCloud(response, words, bounds) {
// Color words in different colors.
const fill = d3.scale.category20();
// Will be populated by words the user enetered.
const studentWordsKeys = [];
// By default we do not scale.
let scale = 1;
// Caсhing of DOM element
const cloudSectionEl = this.wordCloudEl.find('.result_cloud_section');
// Iterator for word cloud count for uniqueness
let wcCount = 0;
// If bounding rectangle is given, scale based on the bounding box of all the words.
if (bounds) {
scale = 0.5 * Math.min(
this.width / Math.abs(bounds[1].x - (this.width / 2)),
this.width / Math.abs(bounds[0].x - (this.width / 2)),
this.height / Math.abs(bounds[1].y - (this.height / 2)),
this.height / Math.abs(bounds[0].y - (this.height / 2)),
);
}
$.each(response.student_words, (word, stat) => {
const percent = (response.display_student_percents) ? ` ${Math.round(100 * (stat / response.total_count))}%` : '';
studentWordsKeys.push(HtmlUtils.interpolateHtml(
'{listStart}{startTag}{word}{endTag}{percent}{listEnd}',
{
listStart: HtmlUtils.HTML('<li>'),
startTag: HtmlUtils.HTML('<strong>'),
word,
endTag: HtmlUtils.HTML('</strong>'),
percent,
listEnd: HtmlUtils.HTML('</li>'),
},
).toString());
});
// Comma separated string of user enetered words.
const studentWordsStr = studentWordsKeys.join('');
cloudSectionEl
.addClass('active');
HtmlUtils.setHtml(
cloudSectionEl.find('.your_words'),
HtmlUtils.HTML(studentWordsStr),
);
HtmlUtils.setHtml(
cloudSectionEl.find('.your_words').end().find('.total_num_words'),
HtmlUtils.interpolateHtml(
gettext('{start_strong}{total}{end_strong} words submitted in total.'),
{
start_strong: HtmlUtils.HTML('<strong>'),
end_strong: HtmlUtils.HTML('</strong>'),
total: response.total_count,
},
),
);
$(`${cloudSectionEl.attr('id')} .word_cloud`).empty();
// Actual drawing of word cloud.
const groupEl = d3.select(`#${cloudSectionEl.attr('id')} .word_cloud`).append('svg')
.attr('width', this.width)
.attr('height', this.height)
.append('g')
.attr('transform', `translate(${0.5 * this.width},${0.5 * this.height})`)
.selectAll('text')
.data(words)
.enter()
.append('g')
.attr('data-id', () => {
wcCount += 1;
return wcCount;
})
.attr('aria-describedby', () => HtmlUtils.interpolateHtml(
gettext('text_word_{uniqueId} title_word_{uniqueId}'),
{
uniqueId: generateUniqueId(cloudSectionEl.attr('id'), $(this).data('id')),
},
));
groupEl
.append('title')
.attr('id', () => HtmlUtils.interpolateHtml(
gettext('title_word_{uniqueId}'),
{
uniqueId: generateUniqueId(cloudSectionEl.attr('id'), $(this).parent().data('id')),
},
))
.text((d) => {
let res = '';
$.each(response.top_words, (index, value) => {
if (value.text === d.text) {
res = `${value.percent}%`;
}
});
return res;
});
groupEl
.append('text')
.attr('id', () => HtmlUtils.interpolateHtml(
gettext('text_word_{uniqueId}'),
{
uniqueId: generateUniqueId(cloudSectionEl.attr('id'), $(this).parent().data('id')),
},
))
.style('font-size', d => `${d.size}px`)
.style('font-family', 'Impact')
.style('fill', (d, i) => fill(i))
.attr('text-anchor', 'middle')
.attr('transform', d => `translate(${d.x}, ${d.y})rotate(${d.rotate$})scale(${scale})`)
.text(d => d.text);
}
}

View File

@@ -1,56 +0,0 @@
/* eslint-env node */
'use strict';
var path = require('path');
module.exports = {
entry: {
word_cloud: 'word_cloud',
},
output: {
path: path.resolve(__dirname, 'public/js'),
filename: '[name].js',
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
use: 'babel-loader',
},
{
test: /d3.min/,
use: [
'babel-loader',
{
loader: 'exports-loader',
options: {
d3: true,
},
},
],
},
],
},
resolve: {
modules: [
path.resolve(__dirname, 'src/js'),
path.resolve(__dirname, '../../../../../../node_modules'),
],
alias: {
'edx-ui-toolkit': 'edx-ui-toolkit/src/', // @TODO: some paths in toolkit are not valid relative paths
},
extensions: ['.js', '.jsx', '.json'],
},
externals: {
gettext: 'gettext',
canvas: 'canvas',
jquery: 'jQuery',
$: 'jQuery',
underscore: '_',
},
};

View File

@@ -13,18 +13,17 @@
<span tabindex="0" class="spinner" aria-hidden="false" aria-label="${_('Loading video player')}"></span>
<span tabindex="-1" class="btn-play fa fa-youtube-play fa-2x is-hidden" aria-hidden="true" aria-label="${_('Play video')}"></span>
<div class="video-player-pre"></div>
<div class="video-player">
<section class="video-player">
<iframe id="id"></iframe>
</div>
</section>
<div class="video-player-post"></div>
<div class="closed-captions"></div>
<div class="video-controls is-hidden">
<section class="video-controls is-hidden">
<div class="slider"></div>
<div>
<div class="vcr"><div class="vidtime">0:00 / 0:00</div></div>
<div class="secondary-controls"></div>
</div>
</div>
</section>
</article>
</div>
<div class="focus_grabber last"></div>

View File

@@ -19,7 +19,6 @@
<iframe id="id"></iframe>
</section>
<div class="video-player-post"></div>
<div class="closed-captions"></div>
<section class="video-controls is-hidden">
<div class="slider"></div>
<div>

View File

@@ -19,7 +19,6 @@
<iframe id="id"></iframe>
</section>
<div class="video-player-post"></div>
<div class="closed-captions"></div>
<section class="video-controls is-hidden">
<div class="slider"></div>
<div>

View File

@@ -12,15 +12,12 @@
<article class="video-wrapper">
<span tabindex="0" class="spinner" aria-hidden="false" aria-label="${_('Loading video player')}"></span>
<span tabindex="-1" class="btn-play fa fa-youtube-play fa-2x is-hidden" aria-hidden="true" aria-label="${_('Play video')}"></span>
<div class="video-player-pre"></div>
<section class="video-player">
<div id="id"></div>
<h4 class="hd hd-4 video-hls-error is-hidden">
Your browser does not support this video format. Try using a different browser.
</h4>
</section>
<div class="video-player-post"></div>
<div class="closed-captions"></div>
<section class="video-controls is-hidden"></section>
</article>
</div>

View File

@@ -12,12 +12,9 @@
<article class="video-wrapper">
<span tabindex="0" class="spinner" aria-hidden="false" aria-label="${_('Loading video player')}"></span>
<span tabindex="-1" class="btn-play fa fa-youtube-play fa-2x is-hidden" aria-hidden="true" aria-label="${_('Play video')}"></span>
<div class="video-player-pre"></div>
<section class="video-player">
<div id="id"></div>
</section>
<div class="video-player-post"></div>
<div class="closed-captions"></div>
<section class="video-controls is-hidden"></section>
</article>
</div>

View File

@@ -12,12 +12,9 @@
<article class="video-wrapper">
<span tabindex="0" class="spinner" aria-hidden="false" aria-label="${_('Loading video player')}"></span>
<span tabindex="-1" class="btn-play fa fa-youtube-play fa-2x is-hidden" aria-hidden="true" aria-label="${_('Play video')}"></span>
<div class="video-player-pre"></div>
<section class="video-player">
<iframe id="id"></iframe>
</section>
<div class="video-player-post"></div>
<div class="closed-captions"></div>
<section class="video-controls is-hidden"></section>
</article>
</div>

View File

@@ -19,7 +19,6 @@
<iframe id="id"></iframe>
</section>
<div class="video-player-post"></div>
<div class="closed-captions"></div>
<section class="video-controls is-hidden">
<div class="slider"></div>
<div>

View File

@@ -17,7 +17,6 @@
<iframe id="id1"></iframe>
</section>
<div class="video-player-post"></div>
<div class="closed-captions"></div>
<section class="video-controls is-hidden">
<div class="slider"></div>
<div>

View File

@@ -1,82 +0,0 @@
import '../../../../static/js/src/ajax_prefix.js';
import '../../../../static/common/js/vendor/underscore.js';
import '../../../../static/common/js/vendor/backbone.js';
import '../../../../static/js/vendor/CodeMirror/codemirror.js';
import '../../../../static/js/vendor/draggabilly.js';
import '../../../../static/common/js/vendor/jquery.js';
import '../../../../static/common/js/vendor/jquery-migrate.js';
import '../../../../static/js/vendor/jquery.cookie.js';
import '../../../../static/js/vendor/jquery.leanModal.js';
import '../../../../static/js/vendor/jquery.timeago.js';
import '../../../../static/js/vendor/jquery-ui.min.js';
import '../../../../static/js/vendor/jquery.ui.draggable.js';
import '../../../../static/js/vendor/json2.js';
// import '../../../../static/common/js/vendor/moment-with-locales.js';
import '../../../../static/js/vendor/tinymce/js/tinymce/jquery.tinymce.min.js';
import '../../../../static/js/vendor/tinymce/js/tinymce/tinymce.full.min.js';
import '../../../../static/js/src/accessibility_tools.js';
import '../../../../static/js/src/logger.js';
import '../../../../static/js/src/utility.js';
import '../../../../static/js/test/add_ajax_prefix.js';
import '../../../../static/js/test/i18n.js';
import '../../../../static/common/js/vendor/hls.js';
import '../assets/vertical/public/js/vertical_student_view.js';
import '../../../../static/js/vendor/jasmine-imagediff.js';
import '../../../../static/common/js/spec_helpers/jasmine-waituntil.js';
import '../../../../static/common/js/spec_helpers/jasmine-extensions.js';
import '../../../../static/common/js/vendor/sinon.js';
// These libraries are used by the tests (and the code under test)
// but not explicitly imported
import 'jquery.ui';
// These
import './src/video/10_main.js'
import './spec/helper.js'
import './spec/video_helper.js'
// These are the tests that will be run
import './spec/video/async_process_spec.js';
import './spec/video/completion_spec.js';
import './spec/video/events_spec.js';
import './spec/video/general_spec.js';
import './spec/video/html5_video_spec.js';
import './spec/video/initialize_spec.js';
import './spec/video/iterator_spec.js';
import './spec/video/resizer_spec.js';
import './spec/video/sjson_spec.js';
import './spec/video/video_autoadvance_spec.js';
import './spec/video/video_bumper_spec.js';
import './spec/video/video_caption_spec.js';
import './spec/video/video_context_menu_spec.js';
import './spec/video/video_control_spec.js';
import './spec/video/video_events_bumper_plugin_spec.js';
import './spec/video/video_events_plugin_spec.js';
import './spec/video/video_focus_grabber_spec.js';
import './spec/video/video_full_screen_spec.js';
import './spec/video/video_player_spec.js';
import './spec/video/video_play_pause_control_spec.js';
import './spec/video/video_play_placeholder_spec.js';
import './spec/video/video_play_skip_control_spec.js';
import './spec/video/video_poster_spec.js';
import './spec/video/video_progress_slider_spec.js';
import './spec/video/video_quality_control_spec.js';
import './spec/video/video_save_state_plugin_spec.js';
import './spec/video/video_skip_control_spec.js';
import './spec/video/video_speed_control_spec.js';
import './spec/video/video_storage_spec.js';
import './spec/video/video_volume_control_spec.js';
import './spec/time_spec.js';
// overwrite the loaded method and manually start the karma after a delay
// Somehow the code initialized in jQuery's onready doesn't get called before karma auto starts
'use strict';
window.__karma__.loaded = function () {
setTimeout(function () {
window.__karma__.start();
}, 1000);
};

View File

@@ -41,6 +41,7 @@ var options = {
{pattern: 'common_static/js/test/i18n.js', included: true},
{pattern: 'common_static/common/js/vendor/hls.js', included: true},
{pattern: 'public/js/split_test_staff.js', included: true},
{pattern: 'public/js/vertical_student_view.js', included: true},
{pattern: 'src/word_cloud/d3.min.js', included: true},
// Load test utilities
@@ -68,18 +69,13 @@ var options = {
// Make sure the patterns in sourceFiles and specFiles do not match the same file.
// Otherwise Istanbul which is used for coverage tracking will cause tests to not run.
sourceFiles: [
{ pattern: 'src/xmodule.js', included: true, ignoreCoverage: true }, // To prevent getting instrumented twice.
// Load these before the xmodules that use them
{ pattern: 'src/javascript_loader.js', included: true },
{ pattern: 'src/collapsible.js', included: true },
// Load everything else
{pattern: 'src/**/!(video)/!(poll|time).js', included: true}
{pattern: 'src/xmodule.js', included: true, ignoreCoverage: true}, // To prevent getting instrumented twice.
{pattern: 'src/**/*.js', included: true}
],
specFiles: [
{pattern: 'spec/helper.js', included: true, ignoreCoverage: true}, // Helper which depends on source files.
{ pattern: 'spec/**/!(video)/*.js', included: true },
{ pattern: 'spec/!(time_spec|video_helper).js', included: true }
{pattern: 'spec/**/*.js', included: true}
],
fixtureFiles: [
@@ -92,8 +88,6 @@ var options = {
]
};
module.exports = function(config) {
configModule.configure(config, options);
};

View File

@@ -1,45 +0,0 @@
/* eslint-env node */
// Karma config for xmodule suite.
// Docs and troubleshooting tips in common/static/common/js/karma.common.conf.js
'use strict';
var path = require('path');
var configModule = require(path.join(__dirname, 'common_static/common/js/karma.common.conf.js'));
var options = {
useRequireJs: false,
normalizePathsForCoverageFunc: function(appRoot, pattern) {
return pattern;
},
libraryFilesToInclude: [],
libraryFiles: [],
sourceFiles: [],
specFiles: [],
fixtureFiles: [
{pattern: 'fixtures/*.*'},
{pattern: 'fixtures/hls/**/*.*'}
],
runFiles: [
{pattern: 'karma_runner_webpack.js', webpack: true}
],
preprocessors: {}
};
options.runFiles
.filter(function(file) { return file.webpack; })
.forEach(function(file) {
options.preprocessors[file.pattern] = ['webpack'];
});
module.exports = function(config) {
configModule.configure(config, options);
};

View File

@@ -1,5 +1,3 @@
/* global _ */
(function() {
'use strict';
var origAjax = $.ajax;
@@ -176,13 +174,13 @@
settings.url.match(/.+\/problem_(check|reset|show|save)$/)
) {
// Do nothing.
return {};
return;
} else if (settings.url === '/save_user_state') {
return {success: true};
} else if (settings.url.match(new RegExp(jasmine.getFixtures().fixturesPath + '.+', 'g'))) {
return origAjax(settings);
} else {
return $.ajax.and.callThrough();
$.ajax.and.callThrough();
}
});
};
@@ -196,8 +194,21 @@
// Stub jQuery.scrollTo module.
$.fn.scrollTo = jasmine.createSpy('jQuery.scrollTo');
// Stub window.Video.loadYouTubeIFrameAPI()
window.Video.loadYouTubeIFrameAPI = jasmine.createSpy('window.Video.loadYouTubeIFrameAPI').and.returnValue(
function(scriptTag) {
var event = document.createEvent('Event');
if (fixture === 'video.html') {
event.initEvent('load', false, false);
} else {
event.initEvent('error', false, false);
}
scriptTag.dispatchEvent(event);
}
);
jasmine.initializePlayer = function(fixture, params) {
var state, metadata;
var state;
if (_.isString(fixture)) {
// `fixture` is a name of a fixture file.
@@ -217,7 +228,7 @@
// If `params` is an object, assign its properties as data attributes
// to the main video DIV element.
if (_.isObject(params)) {
metadata = _.extend($('#video_id').data('metadata'), params);
var metadata = _.extend($('#video_id').data('metadata'), params);
$('#video_id').data('metadata', metadata);
}

View File

@@ -1,57 +1,56 @@
(function(undefined) {
'use strict';
'use strict';
describe('Time', function() {
describe('format', function() {
describe('with NAN', function() {
it('return a correct time format', function() {
expect(Time.format('string')).toEqual('0:00');
expect(Time.format(void(0))).toEqual('0:00');
});
});
import * as Time from 'time.js';
describe('with duration more than or equal to 1 hour', function() {
it('return a correct time format', function() {
expect(Time.format(3600)).toEqual('1:00:00');
expect(Time.format(7272)).toEqual('2:01:12');
});
});
describe('Time', function() {
describe('format', function() {
describe('with NAN', function() {
it('return a correct time format', function() {
expect(Time.format('string')).toEqual('0:00');
expect(Time.format(void(0))).toEqual('0:00');
describe('with duration less than 1 hour', function() {
it('return a correct time format', function() {
expect(Time.format(1)).toEqual('0:01');
expect(Time.format(61)).toEqual('1:01');
expect(Time.format(3599)).toEqual('59:59');
});
});
});
describe('with duration more than or equal to 1 hour', function() {
it('return a correct time format', function() {
expect(Time.format(3600)).toEqual('1:00:00');
expect(Time.format(7272)).toEqual('2:01:12');
describe('formatFull', function() {
it('gives correct string for times', function() {
var testTimes = [
[0, '00:00:00'], [60, '00:01:00'],
[488, '00:08:08'], [2452, '00:40:52'],
[3600, '01:00:00'], [28800, '08:00:00'],
[144532, '40:08:52'], [190360, '52:52:40'],
[294008, '81:40:08'], [-5, '00:00:00']
];
$.each(testTimes, function(index, times) {
var timeInt = times[0],
timeStr = times[1];
expect(Time.formatFull(timeInt)).toBe(timeStr);
});
});
});
describe('with duration less than 1 hour', function() {
it('return a correct time format', function() {
expect(Time.format(1)).toEqual('0:01');
expect(Time.format(61)).toEqual('1:01');
expect(Time.format(3599)).toEqual('59:59');
describe('convert', function() {
it('return a correct time based on speed modifier', function() {
expect(Time.convert(0, 1, 1.5)).toEqual('0.000');
expect(Time.convert(100, 1, 1.5)).toEqual('66.667');
expect(Time.convert(100, 1.5, 1)).toEqual('150.000');
});
});
});
describe('formatFull', function() {
it('gives correct string for times', function() {
var testTimes = [
[0, '00:00:00'], [60, '00:01:00'],
[488, '00:08:08'], [2452, '00:40:52'],
[3600, '01:00:00'], [28800, '08:00:00'],
[144532, '40:08:52'], [190360, '52:52:40'],
[294008, '81:40:08'], [-5, '00:00:00']
];
$.each(testTimes, function(index, times) {
var timeInt = times[0],
timeStr = times[1];
expect(Time.formatFull(timeInt)).toBe(timeStr);
});
});
});
describe('convert', function() {
it('return a correct time based on speed modifier', function() {
expect(Time.convert(0, 1, 1.5)).toEqual('0.000');
expect(Time.convert(100, 1, 1.5)).toEqual('66.667');
expect(Time.convert(100, 1.5, 1)).toEqual('150.000');
});
});
});
}).call(this);

View File

@@ -78,4 +78,4 @@ function(AsyncProcess) {
});
});
});
}(require));
}(RequireJS.require));

View File

@@ -1,5 +1,7 @@
(function(undefined) {
describe('Video', function() {
var oldOTBD, state;
afterEach(function() {
$('source').remove();
window.VideoState = {};
@@ -9,8 +11,6 @@
describe('constructor', function() {
describe('YT', function() {
var state;
beforeEach(function() {
loadFixtures('video.html');
$.cookie.and.returnValue('0.50');
@@ -18,24 +18,24 @@
describe('by default', function() {
beforeEach(function() {
state = jasmine.initializePlayerYouTube('video_html5.html');
this.state = jasmine.initializePlayerYouTube('video_html5.html');
});
afterEach(function() {
state.storage.clear();
state.videoPlayer.destroy();
this.state.storage.clear();
this.state.videoPlayer.destroy();
});
it('check videoType', function() {
expect(state.videoType).toEqual('youtube');
expect(this.state.videoType).toEqual('youtube');
});
it('set the elements', function() {
expect(state.el).toEqual($('#video_id'));
expect(this.state.el).toEqual($('#video_id'));
});
it('parse the videos', function() {
expect(state.videos).toEqual({
expect(this.state.videos).toEqual({
'0.50': '7tqY6eQzVhE',
'1.0': 'cogebirgzzM',
'1.50': 'abcdefghijkl'
@@ -43,11 +43,11 @@
});
it('parse available video speeds', function() {
expect(state.speeds).toEqual(['0.50', '1.0', '1.50']);
expect(this.state.speeds).toEqual(['0.50', '1.0', '1.50']);
});
it('set current video speed via cookie', function() {
expect(state.speed).toEqual('1.50');
expect(this.state.speed).toEqual('1.50');
});
});
});

View File

@@ -11,8 +11,6 @@
oldOTBD = window.onTouchBasedDevice;
window.onTouchBasedDevice = jasmine
.createSpy('onTouchBasedDevice').and.returnValue(null);
state = jasmine.initializePlayer('video_html5.html');
});
afterEach(function() {

View File

@@ -1,4 +1,4 @@
(function(require) {
(function(requirejs, require, define, undefined) {
'use strict';
require(
@@ -85,7 +85,8 @@ function(Initialize) {
};
});
it('returns duration for the 1.0 speed if speed is not 1.0', function() {
var msg = 'returns duration for the 1.0 speed if speed is not 1.0';
it(msg, function() {
var expected;
state.speed = '1.50';
@@ -104,7 +105,8 @@ function(Initialize) {
expect(expected).toEqual(100);
});
it('returns duration for the 1.0 speed as a fall-back', function() {
var msg = 'returns duration for the 1.0 speed as a fall-back';
it(msg, function() {
var expected;
state.isFlashMode.and.returnValue(true);
@@ -275,21 +277,21 @@ function(Initialize) {
describe('isFlashMode', function() {
it('returns `true` if player in `flash` mode', function() {
var testState = {
var state = {
getPlayerMode: jasmine.createSpy().and.returnValue('flash')
},
isFlashMode = Initialize.prototype.isFlashMode,
actual = isFlashMode.call(testState);
actual = isFlashMode.call(state);
expect(actual).toBeTruthy();
});
it('returns `false` if player is not in `flash` mode', function() {
var testState = {
var state = {
getPlayerMode: jasmine.createSpy().and.returnValue('html5')
},
isFlashMode = Initialize.prototype.isFlashMode,
actual = isFlashMode.call(testState);
actual = isFlashMode.call(state);
expect(actual).toBeFalsy();
});
@@ -297,25 +299,25 @@ function(Initialize) {
describe('isHtml5Mode', function() {
it('returns `true` if player in `html5` mode', function() {
var testState = {
var state = {
getPlayerMode: jasmine.createSpy().and.returnValue('html5')
},
isHtml5Mode = Initialize.prototype.isHtml5Mode,
actual = isHtml5Mode.call(testState);
actual = isHtml5Mode.call(state);
expect(actual).toBeTruthy();
});
it('returns `false` if player is not in `html5` mode', function() {
var testState = {
var state = {
getPlayerMode: jasmine.createSpy().and.returnValue('flash')
},
isHtml5Mode = Initialize.prototype.isHtml5Mode,
actual = isHtml5Mode.call(testState);
actual = isHtml5Mode.call(state);
expect(actual).toBeFalsy();
});
});
});
});
}(require));
}(RequireJS.requirejs, RequireJS.require, RequireJS.define));

View File

@@ -1,6 +1,4 @@
(function(require) {
'use strict';
require(
['video/00_iterator.js'],
function(Iterator) {
@@ -102,4 +100,4 @@ function(Iterator) {
});
});
});
}(require));
}(RequireJS.require));

View File

@@ -1,8 +1,7 @@
(function(require) {
'use strict';
(function(requirejs, require, define, undefined) {
require(
['video/00_resizer.js', 'underscore'],
function(Resizer, _) {
['video/00_resizer.js'],
function(Resizer) {
describe('Resizer', function() {
var html = [
'<div ' +
@@ -59,8 +58,7 @@ function(Resizer, _) {
var resizer = new Resizer(config).align(),
expectedHeight = $container.height(),
realHeight = $element.height(),
expectedWidth = 50,
realWidth;
expectedWidth = 50;
// containerRatio >= elementRatio
expect(realHeight).toBe(expectedHeight);
@@ -77,8 +75,7 @@ function(Resizer, _) {
var resizer = new Resizer(config).setMode('height'),
expectedHeight = $container.height(),
realHeight = $element.height(),
expectedWidth = 50,
realWidth;
expectedWidth = 50;
// containerRatio >= elementRatio
expect(realHeight).toBe(expectedHeight);
@@ -92,15 +89,13 @@ function(Resizer, _) {
});
it('`setElement` works correctly', function() {
var $newElement,
expectedHeight;
$container.append('<div ' +
'id="Another-el" ' +
'style="width:100px; height: 150px;"' +
'>');
$newElement = $('#Another-el');
expectedHeight = $container.height();
var $newElement = $('#Another-el'),
expectedHeight = $container.height();
new Resizer(config).setElement($newElement).alignByHeightOnly();
expect($element.height()).not.toBe(expectedHeight);
@@ -266,4 +261,4 @@ function(Resizer, _) {
});
});
});
}(require));
}(RequireJS.requirejs, RequireJS.require, RequireJS.define));

View File

@@ -64,4 +64,4 @@ function(Sjson) {
});
});
});
}(require));
}(RequireJS.require));

View File

@@ -1,12 +1,8 @@
/* global _, WAIT_TIMEOUT */
(function() {
'use strict';
(function(undefined) {
describe('VideoCaption', function() {
var state, oldOTBD;
var parseIntAttribute = function(element, attrName) {
return parseInt(element.attr(attrName), 10);
return parseInt(element.attr(attrName));
};
beforeEach(function() {
@@ -59,9 +55,8 @@
});
it('add ARIA attributes to transcript control', function() {
var $captionControl;
state = jasmine.initializePlayer();
$captionControl = $('.toggle-transcript');
var $captionControl = $('.toggle-transcript');
expect($captionControl).toHaveAttrs({
'aria-disabled': 'false'
});
@@ -154,10 +149,9 @@
});
it('can destroy itself', function() {
var plugin;
spyOn($, 'ajaxWithPrefix');
state = jasmine.initializePlayer();
plugin = state.videoCaption;
var plugin = state.videoCaption;
spyOn($.fn, 'off').and.callThrough();
state.videoCaption.destroy();
@@ -230,17 +224,16 @@
};
it('if languages more than 1', function() {
var transcripts, langCodes, langLabels;
state = jasmine.initializePlayer();
transcripts = state.config.transcriptLanguages;
langCodes = _.keys(transcripts);
langLabels = _.values(transcripts);
var transcripts = state.config.transcriptLanguages,
langCodes = _.keys(transcripts),
langLabels = _.values(transcripts);
expect($('.langs-list')).toExist();
expect($('.langs-list')).toHandle('click');
$('.langs-list li').each(function() {
$('.langs-list li').each(function(index) {
var code = $(this).data('lang-code'),
link = $(this).find('.control'),
label = link.text();
@@ -251,10 +244,9 @@
});
it('when clicking on link with new language', function() {
var Caption, $link;
state = jasmine.initializePlayer();
Caption = state.videoCaption;
$link = $('.langs-list li[data-lang-code="de"] .control-lang');
var Caption = state.videoCaption,
$link = $('.langs-list li[data-lang-code="de"] .control-lang');
spyOn(Caption, 'fetchCaption');
spyOn(state.storage, 'setItem');
@@ -277,11 +269,9 @@
});
it('when clicking on link with current language', function() {
var Caption, $link;
state = jasmine.initializePlayer();
Caption = state.videoCaption;
$link = $('.langs-list li[data-lang-code="en"] .control-lang');
var Caption = state.videoCaption,
$link = $('.langs-list li[data-lang-code="en"] .control-lang');
spyOn(Caption, 'fetchCaption');
spyOn(state.storage, 'setItem');
@@ -310,11 +300,7 @@
$('.language-menu').focus();
$('.language-menu').trigger(keyPressEvent(KEY.UP));
expect($('.lang')).toHaveClass('is-opened');
expect($('.langs-list')
.find('li')
.last()
.find('.control-lang'))
.toBeFocused();
expect($('.langs-list').find('li').last().find('.control-lang')).toBeFocused();
});
it('closes the language menu on ESC', function() {
@@ -470,10 +456,9 @@
});
});
var originalClearTimeout;
describe('mouse movement', function() {
var originalClearTimeout;
beforeEach(function(done) {
jasmine.clock().install();
state = jasmine.initializePlayer();
@@ -602,7 +587,7 @@
'loaded yet';
it(msg, function() {
Caption.loaded = false;
state.hideCaptions = false;
state.hide_captions = false;
Caption.fetchCaption();
expect($.ajaxWithPrefix).toHaveBeenCalled();
@@ -610,7 +595,7 @@
Caption.loaded = false;
Caption.hideCaptions.calls.reset();
state.hideCaptions = true;
state.hide_captions = true;
Caption.fetchCaption();
expect($.ajaxWithPrefix).toHaveBeenCalled();
@@ -972,6 +957,7 @@
jasmine.waitUntil(function() {
return state.videoCaption.rendered;
}).then(function() {
videoControl = state.videoControl;
$('.subtitles li span[data-index=1]').addClass('current');
state.videoCaption.onResize();
}).always(done);

View File

@@ -1,5 +1,3 @@
import '../helper.js'
(function(undefined) {
'use strict';
var describeInfo, state, oldOTBD;

View File

@@ -1,11 +1,9 @@
/* global YT */
(function(require, define, undefined) {
(function(requirejs, require, define, undefined) {
'use strict';
require(
['video/03_video_player.js', 'hls', 'underscore'],
function(VideoPlayer, HLS, _) {
['video/03_video_player.js', 'hls'],
function(VideoPlayer, HLS) {
describe('VideoPlayer', function() {
var STATUS = window.STATUS,
state,
@@ -1067,4 +1065,4 @@ function(VideoPlayer, HLS, _) {
});
});
});
}(require, define));
}(RequireJS.requirejs, RequireJS.require, RequireJS.define));

View File

@@ -24,7 +24,7 @@
});
it('build the slider', function() {
expect($('.slider').toArray()).toContain(state.videoProgressSlider.slider);
expect($('.slider')).toContain(state.videoProgressSlider.slider);
expect($.fn.slider).toHaveBeenCalledWith({
range: 'min',
min: 0,
@@ -35,7 +35,7 @@
});
it('build the seek handle', function() {
expect($('.ui-slider-handle').toArray())
expect($('.ui-slider-handle'))
.toContain(state.videoProgressSlider.handle);
});

View File

@@ -1,8 +1,5 @@
import * as Time from 'time.js';
(function(undefined) {
'use strict';
describe('VideoPlayer Save State plugin', function() {
var state, oldOTBD;
@@ -45,6 +42,7 @@ import * as Time from 'time.js';
beforeEach(function() {
state.videoPlayer.currentTime = videoPlayerCurrentTime;
spyOn(window.Time, 'formatFull').and.callThrough();
});
it('data is not an object, async is true', function() {
@@ -149,7 +147,9 @@ import * as Time from 'time.js';
positionVal,
true
);
expect(ajaxData.saved_video_position).toBe(Time.formatFull(positionVal));
expect(Time.formatFull).toHaveBeenCalledWith(
positionVal
);
}
expect($.ajax).toHaveBeenCalledWith({
url: state.config.saveStateUrl,

View File

@@ -1,4 +1,4 @@
(function(require, define, undefined) {
(function(requirejs, require, define, undefined) {
require(
['video/00_video_storage.js'],
function(VideoStorage) {
@@ -80,4 +80,4 @@ function(VideoStorage) {
});
});
});
}(require, define));
}(RequireJS.requirejs, RequireJS.require, RequireJS.define));

View File

@@ -1,16 +0,0 @@
(function() {
'use strict';
// Stub window.Video.loadYouTubeIFrameAPI()
window.Video.loadYouTubeIFrameAPI = jasmine.createSpy('window.Video.loadYouTubeIFrameAPI').and.returnValue(
function(scriptTag) {
var event = document.createEvent('Event');
if (fixture === 'video.html') {
event.initEvent('load', false, false);
} else {
event.initEvent('error', false, false);
}
scriptTag.dispatchEvent(event);
}
);
}).call(this);

View File

@@ -155,9 +155,7 @@
return MathJax.Hub.Queue(['Typeset', MathJax.Hub, element]);
});
}
if (window.hasOwnProperty('update_schematics')) {
window.update_schematics();
}
window.update_schematics();
problemPrefix = this.element_id.replace(/problem_/, '');
this.inputs = this.$('[id^="input_' + problemPrefix + '_"]');
this.$('div.action button').click(this.refreshAnswers);

View File

@@ -1888,25 +1888,27 @@ var cktsim = (function() {
}
var vgs = this.type_sign * ckt.get_two_terminal(this.g, this.s, soln);
var vgst = vgs - this.vt;
var gmgs,ids,gds;
if (vgst > 0.0 ) { // vgst < 0, transistor off, no subthreshold here.
if (vgst < vds) { /* Saturation. */
gmgs = this.beta * (1 + (this.lambda * vds)) * vgst;
ids = this.type_sign * 0.5 * gmgs * vgst;
gds = 0.5 * this.beta * vgst * vgst * this.lambda;
} else { /* Linear region */
gmgs = this.beta * (1 + this.lambda * vds);
ids = this.type_sign * gmgs * vds * (vgst - 0.50 * vds);
gds = gmgs * (vgst - vds) + this.beta * this.lambda * vds * (vgst - 0.5 * vds);
gmgs *= vds;
with (this) {
var gmgs,ids,gds;
if (vgst > 0.0 ) { // vgst < 0, transistor off, no subthreshold here.
if (vgst < vds) { /* Saturation. */
gmgs = beta * (1 + (lambda * vds)) * vgst;
ids = type_sign * 0.5 * gmgs * vgst;
gds = 0.5 * beta * vgst * vgst * lambda;
} else { /* Linear region */
gmgs = beta * (1 + lambda * vds);
ids = type_sign * gmgs * vds * (vgst - 0.50 * vds);
gds = gmgs * (vgst - vds) + beta * lambda * vds * (vgst - 0.5 * vds);
gmgs *= vds;
}
ckt.add_to_rhs(d,-ids,rhs); // current flows into the drain
ckt.add_to_rhs(s, ids,rhs); // and out the source
ckt.add_conductance(d,s,gds);
ckt.add_to_G(s,s, gmgs);
ckt.add_to_G(d,s,-gmgs);
ckt.add_to_G(d,g, gmgs);
ckt.add_to_G(s,g,-gmgs);
}
ckt.add_to_rhs(this.d,-ids,rhs); // current flows into the drain
ckt.add_to_rhs(this.s, ids,rhs); // and out the source
ckt.add_conductance(this.d,this.s,gds);
ckt.add_to_G(this.s,this.s, gmgs);
ckt.add_to_G(this.d,this.s,-gmgs);
ckt.add_to_G(this.d,this.g, gmgs);
ckt.add_to_G(this.s,this.g,-gmgs);
}
}

View File

@@ -1,11 +1,5 @@
define(['poll/poll_main.js'], function(PollMain) {
'use strict';
function Poll(el) {
return new PollMain(el);
}
window.Poll = Poll;
return Poll;
});
window.Poll = function(el) {
RequireJS.require(['PollMain'], function(PollMain) {
new PollMain(el);
});
};

View File

@@ -267,9 +267,7 @@
XBlock.initializeBlocks(this.content_container, this.requestToken);
// For embedded circuit simulator exercises in 6.002x
if (window.hasOwnProperty('update_schematics')) {
window.update_schematics();
}
window.update_schematics();
this.position = newPosition;
this.toggleArrows();
this.hookUpContentStateChangeEvent();

View File

@@ -1,42 +1,51 @@
(function(undefined) {
'use strict';
function format(time, formatFull) {
var hours, minutes, seconds;
this.Time = {
format: format,
formatFull: formatFull,
convert: convert
};
if (!_.isFinite(time) || time < 0) {
time = 0;
return;
function format(time, formatFull) {
var hours, minutes, seconds;
if (!_.isFinite(time) || time < 0) {
time = 0;
}
seconds = Math.floor(time);
minutes = Math.floor(seconds / 60);
hours = Math.floor(minutes / 60);
seconds = seconds % 60;
minutes = minutes % 60;
if (formatFull) {
return '' + _pad(hours) + ':' + _pad(minutes) + ':' + _pad(seconds % 60);
} else if (hours) {
return '' + hours + ':' + _pad(minutes) + ':' + _pad(seconds % 60);
} else {
return '' + minutes + ':' + _pad(seconds % 60);
}
}
seconds = Math.floor(time);
minutes = Math.floor(seconds / 60);
hours = Math.floor(minutes / 60);
seconds = seconds % 60;
minutes = minutes % 60;
if (formatFull) {
return '' + _pad(hours) + ':' + _pad(minutes) + ':' + _pad(seconds % 60);
} else if (hours) {
return '' + hours + ':' + _pad(minutes) + ':' + _pad(seconds % 60);
} else {
return '' + minutes + ':' + _pad(seconds % 60);
function formatFull(time) {
// The returned value will not be user-facing. So no need for
// internationalization.
return format(time, true);
}
}
function formatFull(time) {
// The returned value will not be user-facing. So no need for
// internationalization.
return format(time, true);
}
function convert(time, oldSpeed, newSpeed) {
return (time * oldSpeed / newSpeed).toFixed(3);
}
function _pad(number) {
if (number < 10) {
return '0' + number;
} else {
return '' + number;
function convert(time, oldSpeed, newSpeed) {
return (time * oldSpeed / newSpeed).toFixed(3);
}
}
export { format, formatFull, convert }
function _pad(number) {
if (number < 10) {
return '0' + number;
} else {
return '' + number;
}
}
}).call(this);

View File

@@ -241,17 +241,17 @@ function(VideoPlayer, i18n, moment, _) {
// Defines whether or not captions are shown on first viewing.
//
// Option
// this.hideCaptions = true | false
// this.hide_captions = true | false
//
// represents the user's choice of having the subtitles shown or
// hidden. This choice is stored in cookies.
function _configureCaptions(state) {
if (state.config.showCaptions) {
state.hideCaptions = ($.cookie('hide_captions') === 'true');
state.hide_captions = ($.cookie('hide_captions') === 'true');
} else {
state.hideCaptions = true;
state.hide_captions = true;
$.cookie('hide_captions', state.hideCaptions, {
$.cookie('hide_captions', state.hide_captions, {
expires: 3650,
path: '/'
});

View File

@@ -3,8 +3,8 @@
// VideoPlayer module.
define(
'video/03_video_player.js',
['video/02_html5_video.js', 'video/02_html5_hls_video.js', 'video/00_resizer.js', 'hls', 'underscore', '../time.js'],
function(HTML5Video, HTML5HLSVideo, Resizer, HLS, _, Time) {
['video/02_html5_video.js', 'video/02_html5_hls_video.js', 'video/00_resizer.js', 'hls', 'underscore'],
function(HTML5Video, HTML5HLSVideo, Resizer, HLS, _) {
var dfd = $.Deferred(),
VideoPlayer = function(state) {
state.videoPlayer = {};

View File

@@ -1,161 +1,159 @@
(function(requirejs, require, define) {
'use strict';
// VideoControl module.
// VideoControl module.
define(
'video/04_video_control.js',
['time.js'],
function(Time) {
// VideoControl() function - what this module "exports".
return function(state) {
var dfd = $.Deferred();
'video/04_video_control.js',
[],
function() {
// VideoControl() function - what this module "exports".
return function(state) {
var dfd = $.Deferred();
state.videoControl = {};
state.videoControl = {};
_makeFunctionsPublic(state);
_renderElements(state);
_bindHandlers(state);
_makeFunctionsPublic(state);
_renderElements(state);
_bindHandlers(state);
dfd.resolve();
return dfd.promise();
};
dfd.resolve();
return dfd.promise();
};
// ***************************************************************
// Private functions start here.
// ***************************************************************
// ***************************************************************
// Private functions start here.
// ***************************************************************
// function _makeFunctionsPublic(state)
//
// Functions which will be accessible via 'state' object. When called, these functions will
// get the 'state' object as a context.
function _makeFunctionsPublic(state) {
var methodsDict = {
destroy: destroy,
hideControls: hideControls,
show: show,
showControls: showControls,
focusFirst: focusFirst,
updateVcrVidTime: updateVcrVidTime
};
// function _makeFunctionsPublic(state)
//
// Functions which will be accessible via 'state' object. When called, these functions will
// get the 'state' object as a context.
function _makeFunctionsPublic(state) {
var methodsDict = {
destroy: destroy,
hideControls: hideControls,
show: show,
showControls: showControls,
focusFirst: focusFirst,
updateVcrVidTime: updateVcrVidTime
};
state.bindTo(methodsDict, state.videoControl, state);
}
state.bindTo(methodsDict, state.videoControl, state);
}
function destroy() {
this.el.off({
mousemove: this.videoControl.showControls,
keydown: this.videoControl.showControls,
destroy: this.videoControl.destroy,
initialize: this.videoControl.focusFirst
});
function destroy() {
this.el.off({
mousemove: this.videoControl.showControls,
keydown: this.videoControl.showControls,
destroy: this.videoControl.destroy,
initialize: this.videoControl.focusFirst
});
this.el.off('controls:show');
if (this.controlHideTimeout) {
clearTimeout(this.controlHideTimeout);
}
delete this.videoControl;
}
// function _renderElements(state)
//
// Create any necessary DOM elements, attach them, and set their initial configuration. Also
// make the created DOM elements available via the 'state' object. Much easier to work this
// way - you don't have to do repeated jQuery element selects.
function _renderElements(state) {
state.videoControl.el = state.el.find('.video-controls');
state.videoControl.vidTimeEl = state.videoControl.el.find('.vidtime');
if ((state.videoType === 'html5') && (state.config.autohideHtml5)) {
state.videoControl.fadeOutTimeout = state.config.fadeOutTimeout;
state.videoControl.el.addClass('html5');
state.controlHideTimeout = setTimeout(state.videoControl.hideControls, state.videoControl.fadeOutTimeout);
}
}
// function _bindHandlers(state)
//
// Bind any necessary function callbacks to DOM events (click, mousemove, etc.).
function _bindHandlers(state) {
if ((state.videoType === 'html5') && (state.config.autohideHtml5)) {
state.el.on({
mousemove: state.videoControl.showControls,
keydown: state.videoControl.showControls
});
}
if (state.config.focusFirstControl) {
state.el.on('initialize', state.videoControl.focusFirst);
}
state.el.on('destroy', state.videoControl.destroy);
}
// ***************************************************************
// Public functions start here.
// These are available via the 'state' object. Their context ('this' keyword) is the 'state' object.
// The magic private function that makes them available and sets up their context is makeFunctionsPublic().
// ***************************************************************
function focusFirst() {
this.videoControl.el.find('.vcr a, .vcr button').first().focus();
}
function show() {
this.videoControl.el.removeClass('is-hidden');
this.el.trigger('controls:show', arguments);
}
function showControls(event) {
if (!this.controlShowLock) {
if (!this.captionsHidden) {
return;
}
this.controlShowLock = true;
if (this.controlState === 'invisible') {
this.videoControl.el.show();
this.controlState = 'visible';
} else if (this.controlState === 'hiding') {
this.videoControl.el.stop(true, false).css('opacity', 1).show();
this.controlState = 'visible';
} else if (this.controlState === 'visible') {
clearTimeout(this.controlHideTimeout);
}
this.controlHideTimeout = setTimeout(this.videoControl.hideControls, this.videoControl.fadeOutTimeout);
this.controlShowLock = false;
}
}
function hideControls() {
var _this = this;
this.controlHideTimeout = null;
if (!this.captionsHidden) {
return;
}
this.controlState = 'hiding';
this.videoControl.el.fadeOut(this.videoControl.fadeOutTimeout, function() {
_this.controlState = 'invisible';
// If the focus was on the video control or the volume control,
// then we must make sure to close these dialogs. Otherwise, after
// next autofocus, these dialogs will be open, but the focus will
// not be on them.
_this.videoVolumeControl.el.removeClass('open');
_this.videoSpeedControl.el.removeClass('open');
_this.focusGrabber.enableFocusGrabber();
});
}
function updateVcrVidTime(params) {
var endTime = (this.config.endTime !== null) ? this.config.endTime : params.duration;
// in case endTime is accidentally specified as being greater than the video
endTime = Math.min(endTime, params.duration);
this.videoControl.vidTimeEl.html(Time.format(params.time) + ' / ' + Time.format(endTime));
}
this.el.off('controls:show');
if (this.controlHideTimeout) {
clearTimeout(this.controlHideTimeout);
}
);
delete this.videoControl;
}
// function _renderElements(state)
//
// Create any necessary DOM elements, attach them, and set their initial configuration. Also
// make the created DOM elements available via the 'state' object. Much easier to work this
// way - you don't have to do repeated jQuery element selects.
function _renderElements(state) {
state.videoControl.el = state.el.find('.video-controls');
state.videoControl.vidTimeEl = state.videoControl.el.find('.vidtime');
if ((state.videoType === 'html5') && (state.config.autohideHtml5)) {
state.videoControl.fadeOutTimeout = state.config.fadeOutTimeout;
state.videoControl.el.addClass('html5');
state.controlHideTimeout = setTimeout(state.videoControl.hideControls, state.videoControl.fadeOutTimeout);
}
}
// function _bindHandlers(state)
//
// Bind any necessary function callbacks to DOM events (click, mousemove, etc.).
function _bindHandlers(state) {
if ((state.videoType === 'html5') && (state.config.autohideHtml5)) {
state.el.on({
mousemove: state.videoControl.showControls,
keydown: state.videoControl.showControls
});
}
if (state.config.focusFirstControl) {
state.el.on('initialize', state.videoControl.focusFirst);
}
state.el.on('destroy', state.videoControl.destroy);
}
// ***************************************************************
// Public functions start here.
// These are available via the 'state' object. Their context ('this' keyword) is the 'state' object.
// The magic private function that makes them available and sets up their context is makeFunctionsPublic().
// ***************************************************************
function focusFirst() {
this.videoControl.el.find('.vcr a, .vcr button').first().focus();
}
function show() {
this.videoControl.el.removeClass('is-hidden');
this.el.trigger('controls:show', arguments);
}
function showControls(event) {
if (!this.controlShowLock) {
if (!this.captionsHidden) {
return;
}
this.controlShowLock = true;
if (this.controlState === 'invisible') {
this.videoControl.el.show();
this.controlState = 'visible';
} else if (this.controlState === 'hiding') {
this.videoControl.el.stop(true, false).css('opacity', 1).show();
this.controlState = 'visible';
} else if (this.controlState === 'visible') {
clearTimeout(this.controlHideTimeout);
}
this.controlHideTimeout = setTimeout(this.videoControl.hideControls, this.videoControl.fadeOutTimeout);
this.controlShowLock = false;
}
}
function hideControls() {
var _this = this;
this.controlHideTimeout = null;
if (!this.captionsHidden) {
return;
}
this.controlState = 'hiding';
this.videoControl.el.fadeOut(this.videoControl.fadeOutTimeout, function() {
_this.controlState = 'invisible';
// If the focus was on the video control or the volume control,
// then we must make sure to close these dialogs. Otherwise, after
// next autofocus, these dialogs will be open, but the focus will
// not be on them.
_this.videoVolumeControl.el.removeClass('open');
_this.videoSpeedControl.el.removeClass('open');
_this.focusGrabber.enableFocusGrabber();
});
}
function updateVcrVidTime(params) {
var endTime = (this.config.endTime !== null) ? this.config.endTime : params.duration;
// in case endTime is accidentally specified as being greater than the video
endTime = Math.min(endTime, params.duration);
this.videoControl.vidTimeEl.html(Time.format(params.time) + ' / ' + Time.format(endTime));
}
});
}(RequireJS.requirejs, RequireJS.require, RequireJS.define));

View File

@@ -1,6 +1,6 @@
(function(define) {
'use strict';
define('video/09_save_state_plugin.js', ['underscore', 'time.js'], function(_, Time) {
define('video/09_save_state_plugin.js', ['underscore'], function(_) {
/**
* Save state module.
* @exports video/09_save_state_plugin.js

View File

@@ -6,10 +6,8 @@
'video/00_sjson.js',
'video/00_async_process.js',
'edx-ui-toolkit/js/utils/html-utils',
'draggabilly',
'time.js',
'underscore'
], function(Sjson, AsyncProcess, HtmlUtils, Draggabilly, Time, _) {
'draggabilly'
], function(Sjson, AsyncProcess, HtmlUtils, Draggabilly) {
/**
* @desc VideoCaption module exports a function.
*
@@ -208,7 +206,6 @@
case KEY.ENTER:
event.preventDefault();
this.toggleClosedCaptions(event);
// no default
}
},
@@ -221,7 +218,6 @@
case KEY.ENTER:
event.preventDefault();
this.toggle(event);
// no default
}
},
@@ -256,9 +252,7 @@
case KEY.ENTER:
case KEY.SPACE:
return true;
// no default
}
return true;
},
handleKeypress: function(event) {
@@ -277,7 +271,6 @@
case KEY.ESCAPE:
this.closeLanguageMenu(event);
break;
// no default
}
return event.keyCode === KEY.TAB;
@@ -328,11 +321,11 @@
},
openLanguageMenu: function(event) {
event.preventDefault();
var button = this.languageChooserEl,
menu = button.parent().find('.menu');
event.preventDefault();
button
.addClass('is-opened');
@@ -342,9 +335,10 @@
},
closeLanguageMenu: function(event) {
var button = this.languageChooserEl;
event.preventDefault();
var button = this.languageChooserEl;
button
.removeClass('is-opened')
.find('.language-menu')
@@ -372,7 +366,6 @@
case 'keydown':
this.captionKeyDown(event);
break;
// no default
}
},
@@ -537,11 +530,10 @@
notifyOnError: false,
data: data,
success: function(sjson) {
var results, start, captions;
self.sjson = new Sjson(sjson);
results = self.getBoundedCaptions();
start = results.start;
captions = results.captions;
var results = self.getBoundedCaptions();
var start = results.start;
var captions = results.captions;
if (self.loaded) {
if (self.rendered) {
@@ -561,7 +553,7 @@
} else {
self.renderCaption(start, captions);
}
self.hideCaptions(state.hideCaptions, false);
self.hideCaptions(state.hide_captions, false);
HtmlUtils.append(
self.state.el.find('.video-wrapper').parent(),
HtmlUtils.HTML(self.subtitlesEl)
@@ -675,7 +667,7 @@
if (_.keys(languages).length < 2) {
// Remove the menu toggle button
self.container.find('.lang').remove();
return;
return false;
}
this.showLanguageMenu = true;
@@ -705,11 +697,11 @@
$menu.on('click', '.control-lang', function(e) {
var el = $(e.currentTarget).parent(),
captionState = self.state,
state = self.state,
langCode = el.data('lang-code');
if (captionState.lang !== langCode) {
captionState.lang = langCode;
if (state.lang !== langCode) {
state.lang = langCode;
el.addClass('is-active')
.siblings('li')
.removeClass('is-active')
@@ -718,7 +710,7 @@
$(e.currentTarget).attr('aria-pressed', 'true');
captionState.el.trigger('language_menu:change', [langCode]);
state.el.trigger('language_menu:change', [langCode]);
self.fetchCaption();
// update the closed-captions lang attribute
@@ -925,10 +917,10 @@
this.autoScrolling = true;
container.removeClass('focused');
this.currentCaptionIndex = -1;
} else {
// If the focus comes from tabbing, show the outline and turn off
// automatic scrolling.
}
// If the focus comes from tabbing, show the outline and turn off
// automatic scrolling.
else {
this.currentCaptionIndex = captionIndex;
container.addClass('focused');
// The second and second to last elements turn automatic scrolling
@@ -1037,7 +1029,7 @@
*/
updatePlayTime: function(time) {
var state = this.state,
params, newIndex, times;
params, newIndex;
if (this.loaded) {
if (state.isFlashMode()) {
@@ -1045,7 +1037,7 @@
}
time = Math.round(time * 1000 + 100);
times = this.getStartEndTimes();
var times = this.getStartEndTimes();
// if start and end times are defined, limit search.
// else, use the entire list of video captions
params = [time].concat(times);
@@ -1230,10 +1222,11 @@
},
listenForDragDrop: function() {
var captions = document.querySelector('.closed-captions');
var captions = document.querySelector('.closed-captions'),
draggable;
if (typeof Draggabilly === 'function') {
new Draggabilly(captions, {containment: true});
draggable = new Draggabilly(captions, {containment: true});
} else {
console.log('Closed captioning available but not draggable');
}
@@ -1242,25 +1235,25 @@
/**
* @desc Shows/Hides captions and updates the cookie.
*
* @param {boolean} hideCaptions if `true` hides the caption,
* @param {boolean} hide_captions if `true` hides the caption,
* otherwise - show.
* @param {boolean} updateCookie Flag to update or not the cookie.
* @param {boolean} update_cookie Flag to update or not the cookie.
*
*/
hideCaptions: function(hideCaptions, updateCookie, triggerEvent) {
hideCaptions: function(hide_captions, update_cookie, trigger_event) {
var transcriptControlEl = this.transcriptControlEl,
state = this.state,
text;
if (typeof updateCookie === 'undefined') {
updateCookie = true;
if (typeof update_cookie === 'undefined') {
update_cookie = true;
}
if (hideCaptions) {
if (hide_captions) {
state.captionsHidden = true;
state.el.addClass('closed');
text = gettext('Turn on transcripts');
if (triggerEvent) {
if (trigger_event) {
this.state.el.trigger('transcript:hide');
}
@@ -1272,7 +1265,7 @@
state.el.removeClass('closed');
this.scrollCaption();
text = gettext('Turn off transcripts');
if (triggerEvent) {
if (trigger_event) {
this.state.el.trigger('transcript:show');
}
@@ -1290,8 +1283,8 @@
}
this.setSubtitlesHeight();
if (updateCookie) {
$.cookie('hideCaptions', hideCaptions, {
if (update_cookie) {
$.cookie('hide_captions', hide_captions, {
expires: 3650,
path: '/'
});
@@ -1321,7 +1314,7 @@
var height = 0,
state = this.state;
// on page load captionHidden = undefined
if ((state.captionsHidden === undefined && state.hideCaptions) ||
if ((state.captionsHidden === undefined && state.hide_captions) ||
state.captionsHidden === true
) {
// In case of html5 autoshowing subtitles, we adjust height of

View File

@@ -1,8 +1,6 @@
/* globals _ */
/* RequireJS */
(function(require, $) {
'use strict';
// In the case when the Video constructor will be called before RequireJS finishes loading all of the Video
// dependencies, we will have a mock function that will collect all the elements that must be initialized as
// Video elements.
@@ -23,7 +21,7 @@
window.Video(el);
});
return null;
return;
}
// If normal call to `window.Video` constructor, store the element for later initializing.
@@ -36,10 +34,6 @@
// Main module.
require(
/* End RequireJS */
/* Webpack
define(
/* End Webpack */
[
'video/00_video_storage.js',
'video/01_initialize.js',
@@ -73,13 +67,8 @@
VideoBumper, VideoSaveStatePlugin, VideoEventsPlugin, VideoEventsBumperPlugin, VideoPoster,
VideoCompletionHandler, VideoCommands, VideoContextMenu
) {
/* RequireJS */
var youtubeXhr = null,
oldVideo = window.Video;
/* End RequireJS */
/* Webpack
var youtubeXhr = null;
/* End Webpack */
window.Video = function(element) {
var el = $(element).find('.video'),
@@ -128,15 +117,14 @@
initialize(innerState, element);
};
};
var onSequenceChange;
VideoAccessibleMenu(el, {
new VideoAccessibleMenu(el, {
storage: storage,
saveStateUrl: state.metadata.saveStateUrl
});
if (bumperMetadata) {
VideoPoster(el, {
new VideoPoster(el, {
poster: el.data('poster'),
onClick: _.once(function() {
var mainVideoPlayer = player(state);
@@ -163,7 +151,7 @@
}
el.data('video-player-state', state);
onSequenceChange = function() {
var onSequenceChange = function onSequenceChange() {
if (state && state.videoPlayer) {
state.videoPlayer.destroy();
}
@@ -182,13 +170,9 @@
window.Video.loadYouTubeIFrameAPI = initialize.prototype.loadYouTubeIFrameAPI;
/* RequireJS */
// Invoke the mock Video constructor so that the elements stored within it can be processed by the real
// `window.Video` constructor.
oldVideo(null, true);
/* End RequireJS */
}
);
/* RequireJS */
}(window.RequireJS.require, window.jQuery));
/* End RequireJS */

View File

@@ -6,11 +6,9 @@ that is defined by XModules and XModuleDescriptors (javascript and css)
import errno
import hashlib
import json
import logging
import os
import sys
import textwrap
from collections import defaultdict
import django
@@ -119,33 +117,28 @@ def _write_js(output_root, classes):
Write the javascript fragments from all XModules in `classes`
into `output_root` as individual files, hashed by the contents to remove
duplicates
Returns a dictionary mapping class names to the files that they depend on.
"""
file_contents = {}
file_owners = defaultdict(list)
contents = {}
fragment_owners = defaultdict(list)
js_fragments = set()
for class_ in classes:
module_js = class_.get_javascript()
# It will enforce 000 prefix for xmodule.js.
fragment_owners[(0, 'js', module_js.get('xmodule_js'))].append(class_.__name__)
js_fragments.add((0, 'js', module_js.get('xmodule_js')))
for filetype in ('coffee', 'js'):
for idx, fragment in enumerate(module_js.get(filetype, [])):
fragment_owners[(idx + 1, filetype, fragment)].append(class_.__name__)
js_fragments.add((idx + 1, filetype, fragment))
for (idx, filetype, fragment), owners in sorted(fragment_owners.items()):
for idx, filetype, fragment in sorted(js_fragments):
filename = "{idx:0=3d}-{hash}.{type}".format(
idx=idx,
hash=hashlib.md5(fragment).hexdigest(),
type=filetype)
file_contents[filename] = fragment
for owner in owners:
file_owners[owner].append(output_root / filename)
contents[filename] = fragment
_write_files(output_root, file_contents, {'.coffee': '.js'})
_write_files(output_root, contents, {'.coffee': '.js'})
return file_owners
return [output_root / filename for filename in contents.keys()]
def _write_files(output_root, contents, generated_suffix_map=None):
@@ -189,33 +182,6 @@ def _write_files(output_root, contents, generated_suffix_map=None):
LOG.debug("%s unchanged, skipping", output_file)
def write_webpack(output_file, module_files, descriptor_files):
"""
Write all xmodule and xmodule descriptor javascript into module-specific bundles.
The output format should be suitable for smart-merging into an existing webpack configuration.
"""
_ensure_dir(output_file.dirname())
config = {
'entry': {}
}
for (owner, files) in module_files.items() + descriptor_files.items():
unique_files = sorted(set('./{}'.format(file) for file in files))
if len(unique_files) == 1:
unique_files = unique_files[0]
config['entry'][owner] = unique_files
# config['entry']['modules/js/all'] = sorted(set('./{}'.format(file) for file in sum(module_files.values(), [])))
# config['entry']['descriptors/js/all'] = sorted(set('./{}'.format(file) for file in sum(descriptor_files.values(), [])))
with output_file.open('w') as outfile:
outfile.write(
textwrap.dedent(u"""\
module.exports = {config_json};
""").format(config_json=json.dumps(config, indent=4))
)
def main():
"""
Generate
@@ -243,11 +209,10 @@ def main():
args = docopt(main.__doc__)
root = path(args['<output_root>'])
descriptor_files = write_descriptor_js(root / 'descriptors/js')
write_descriptor_js(root / 'descriptors/js')
write_descriptor_styles(root / 'descriptors/css')
module_files = write_module_js(root / 'modules/js')
write_module_js(root / 'modules/js')
write_module_styles(root / 'modules/css')
write_webpack(root / 'webpack.xmodule.config.js', module_files, descriptor_files)
if __name__ == '__main__':

View File

@@ -221,7 +221,7 @@ class ContentTest(unittest.TestCase):
Test that only one filename starts with 000.
"""
output_root = path(u'common/static/xmodule/descriptors/js')
file_owners = _write_js(output_root, _list_descriptors())
js_file_paths = set(file_path for file_path in sum(file_owners.values(), []) if os.path.basename(file_path).startswith('000-'))
js_file_paths = _write_js(output_root, _list_descriptors())
js_file_paths = [file_path for file_path in js_file_paths if os.path.basename(file_path).startswith('000-')]
self.assertEqual(len(js_file_paths), 1)
self.assertIn("XModule.Descriptor = (function() {", open(js_file_paths.pop()).read())
self.assertIn("XModule.Descriptor = (function() {", open(js_file_paths[0]).read())

View File

@@ -23,7 +23,6 @@ from xmodule.studio_editable import StudioEditableBlock
from xmodule.x_module import STUDENT_VIEW, XModuleFields
from xmodule.xml_module import XmlParserMixin
import webpack_loader.utils
log = logging.getLogger(__name__)
@@ -134,8 +133,7 @@ class VerticalBlock(SequenceFields, XModuleFields, StudioEditableBlock, XmlParse
'completion_delay_ms': self.get_completion_delay_ms(completion_service),
}))
for tag in webpack_loader.utils.get_as_tags('VerticalStudentView'):
fragment.add_resource(tag, mimetype='text/html', placement='head')
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/vertical_student_view.js'))
fragment.initialize_js('VerticalStudentView')
return fragment

View File

@@ -129,6 +129,40 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers,
#TODO: For each of the following, ensure that any generated html is properly escaped.
js = {
'js': [
resource_string(module, 'js/src/time.js'),
resource_string(module, 'js/src/video/00_component.js'),
resource_string(module, 'js/src/video/00_video_storage.js'),
resource_string(module, 'js/src/video/00_resizer.js'),
resource_string(module, 'js/src/video/00_async_process.js'),
resource_string(module, 'js/src/video/00_i18n.js'),
resource_string(module, 'js/src/video/00_sjson.js'),
resource_string(module, 'js/src/video/00_iterator.js'),
resource_string(module, 'js/src/video/01_initialize.js'),
resource_string(module, 'js/src/video/025_focus_grabber.js'),
resource_string(module, 'js/src/video/02_html5_video.js'),
resource_string(module, 'js/src/video/02_html5_hls_video.js'),
resource_string(module, 'js/src/video/03_video_player.js'),
resource_string(module, 'js/src/video/035_video_accessible_menu.js'),
resource_string(module, 'js/src/video/04_video_control.js'),
resource_string(module, 'js/src/video/04_video_full_screen.js'),
resource_string(module, 'js/src/video/05_video_quality_control.js'),
resource_string(module, 'js/src/video/06_video_progress_slider.js'),
resource_string(module, 'js/src/video/07_video_volume_control.js'),
resource_string(module, 'js/src/video/08_video_speed_control.js'),
resource_string(module, 'js/src/video/08_video_auto_advance_control.js'),
resource_string(module, 'js/src/video/09_video_caption.js'),
resource_string(module, 'js/src/video/09_play_placeholder.js'),
resource_string(module, 'js/src/video/09_play_pause_control.js'),
resource_string(module, 'js/src/video/09_play_skip_control.js'),
resource_string(module, 'js/src/video/09_skip_control.js'),
resource_string(module, 'js/src/video/09_bumper.js'),
resource_string(module, 'js/src/video/09_save_state_plugin.js'),
resource_string(module, 'js/src/video/09_events_plugin.js'),
resource_string(module, 'js/src/video/09_events_bumper_plugin.js'),
resource_string(module, 'js/src/video/09_poster.js'),
resource_string(module, 'js/src/video/09_completion.js'),
resource_string(module, 'js/src/video/095_video_context_menu.js'),
resource_string(module, 'js/src/video/10_commands.js'),
resource_string(module, 'js/src/video/10_main.js'),
]
}

View File

@@ -91,7 +91,11 @@ class WordCloudFields(object):
class WordCloudModule(WordCloudFields, XModule):
"""WordCloud Xmodule"""
js = {'js': [resource_string(__name__, 'assets/word_cloud/src/js/word_cloud.js')]}
js = {
'js': [
resource_string(__name__, 'js/src/javascript_loader.js'),
],
}
css = {'scss': [resource_string(__name__, 'css/word_cloud/display.scss')]}
js_module_name = "WordCloud"
@@ -246,6 +250,11 @@ class WordCloudModule(WordCloudFields, XModule):
'submitted': self.submitted,
}))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/d3.min.js'))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/d3.layout.cloud.js'))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/word_cloud.js'))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/word_cloud_main.js'))
return fragment
def author_view(self, context):

View File

@@ -252,17 +252,10 @@ def shim_xmodule_js(block, fragment):
"""
Set up the XBlock -> XModule shim on the supplied :class:`web_fragments.fragment.Fragment`
"""
# Delay this import so that it is only used (and django settings are parsed) when
# they are required (rather than at startup)
import webpack_loader.utils
if not fragment.js_init_fn:
fragment.initialize_js('XBlockToXModuleShim')
fragment.json_init_args = {'xmodule-type': block.js_module_name}
for tag in webpack_loader.utils.get_as_tags('XModuleShim'):
fragment.add_resource(tag, mimetype='text/html', placement='head')
class XModuleFields(object):
"""

View File

@@ -4,17 +4,9 @@
(function(define, require) {
'use strict';
/* RequireJS */
define(['jquery', 'underscore', 'gettext', 'common/js/components/views/feedback_notification',
'common/js/components/views/feedback_prompt'],
function($, _, gettext, NotificationView, PromptView) {
/* End RequireJS */
/* Webpack
define(['jquery', 'underscore', 'gettext', 'common/js/components/views/feedback_notification',
'common/js/components/views/feedback_prompt', 'scriptjs'],
function($, _, gettext, NotificationView, PromptView, $script) {
/* End Webpack */
var toggleExpandCollapse, showLoadingIndicator, hideLoadingIndicator, confirmThenRunOperation,
runOperationShowingMessage, withDisabledElement, disableElementWhileRunning,
getScrollOffset, setScrollOffset, setScrollTop, redirect, reload, hasChangedAttributes,
@@ -29,9 +21,11 @@
*/
toggleExpandCollapse = function(target, collapsedClass) {
// Support the old 'collapsed' option until fully switched over to is-collapsed
var collapsed = collapsedClass || 'collapsed';
if (!collapsedClass) {
collapsedClass = 'collapsed';
}
target.closest('.expand-collapse').toggleClass('expand collapse');
target.closest('.is-collapsible, .window').toggleClass(collapsed);
target.closest('.is-collapsible, .window').toggleClass(collapsedClass);
target.closest('.is-collapsible').children('article').slideToggle();
};
@@ -237,20 +231,20 @@
};
// Ensure that sum length of key field values <= ${MAX_SUM_KEY_LENGTH} chars.
validateTotalKeyLength = function(keyFieldSelectors) {
validateTotalKeyLength = function(key_field_selectors) {
var totalLength = _.reduce(
keyFieldSelectors,
key_field_selectors,
function(sum, ele) { return sum + $(ele).val().length; },
0
);
return totalLength <= MAX_SUM_KEY_LENGTH;
};
checkTotalKeyLengthViolations = function(selectors, classes, keyFieldSelectors, messageTpl) {
if (!validateTotalKeyLength(keyFieldSelectors)) {
checkTotalKeyLengthViolations = function(selectors, classes, key_field_selectors, message_tpl) {
if (!validateTotalKeyLength(key_field_selectors)) {
$(selectors.errorWrapper).addClass(classes.shown).removeClass(classes.hiding);
$(selectors.errorMessage).html(
'<p>' + _.template(messageTpl)({limit: MAX_SUM_KEY_LENGTH}) + '</p>'
'<p>' + _.template(message_tpl)({limit: MAX_SUM_KEY_LENGTH}) + '</p>'
);
$(selectors.save).addClass(classes.disabled);
} else {
@@ -265,7 +259,6 @@
*/
loadJavaScript = function(url) {
var deferred = $.Deferred();
/* RequireJS */
require([url],
function() {
deferred.resolve();
@@ -273,12 +266,6 @@
function() {
deferred.reject();
});
/* End RequireJS */
/* Webpack
$script(url, url, function () {
deferred.resolve();
});
/* End Webpack */
return deferred.promise();
};

View File

@@ -45,6 +45,8 @@ var webdriver = require('selenium-webdriver');
var firefox = require('selenium-webdriver/firefox');
var webpackConfig = require(path.join(appRoot, 'webpack.dev.config.js'));
delete webpackConfig.entry;
// The following crazy bit is to work around the webpack.optimize.CommonsChunkPlugin
// plugin. The problem is that it it factors out the code that defines webpackJsonp
// and puts in in the commons JS, which Karma doesn't know to load first. This is a
@@ -54,7 +56,8 @@ var webpackConfig = require(path.join(appRoot, 'webpack.dev.config.js'));
// https://github.com/webpack-contrib/karma-webpack/issues/24#issuecomment-257613167
//
// This should be fixed in v3 of karma-webpack
var commonsChunkPluginIndex = webpackConfig.plugins.findIndex(function(plugin) { return plugin.chunkNames; });
const commonsChunkPluginIndex = webpackConfig.plugins.findIndex(plugin => plugin.chunkNames);
webpackConfig.plugins.splice(commonsChunkPluginIndex, 1);
// Files which are needed by all lms/cms suites.
var commonFiles = {
@@ -80,10 +83,6 @@ var commonFiles = {
]
};
webpackConfig.plugins.splice(commonsChunkPluginIndex, 1);
delete webpackConfig.entry;
/**
* Customize the name attribute in xml testcase element
* @param {Object} browser
@@ -125,8 +124,6 @@ function reporters(config) {
* @return {Object}
*/
function getBasepathAndFilename(filepath) {
var file, dir;
if (!filepath) {
// these will configure the reporters to create report files relative to this karma config file
return {
@@ -134,8 +131,9 @@ function getBasepathAndFilename(filepath) {
file: undefined
};
}
file = filepath.replace(/^.*[\\/]/, '');
dir = filepath.replace(file, '');
var file = filepath.replace(/^.*[\\\/]/, ''),
dir = filepath.replace(file, '');
return {
dir: dir,
@@ -150,13 +148,13 @@ function getBasepathAndFilename(filepath) {
* @return {Object}
*/
function coverageSettings(config) {
var pth = getBasepathAndFilename(config.coveragereportpath);
var path = getBasepathAndFilename(config.coveragereportpath);
return {
dir: pth.dir,
dir: path.dir,
subdir: '.',
includeAllSources: true,
reporters: [
{type: 'cobertura', file: pth.file},
{type: 'cobertura', file: path.file},
{type: 'text-summary'}
]
};
@@ -169,10 +167,10 @@ function coverageSettings(config) {
* @return {Object}
*/
function junitSettings(config) {
var pth = getBasepathAndFilename(config.junitreportpath);
var path = getBasepathAndFilename(config.junitreportpath);
return {
outputDir: pth.dir,
outputFile: pth.file,
outputDir: path.dir,
outputFile: path.file,
suite: 'javascript',
useBrowserName: false,
nameFormatter: junitNameFormatter,
@@ -187,15 +185,14 @@ function junitSettings(config) {
* @return {String}
*/
// I'd like to fix the no-shadow violation on the next line, but it would break this shared conf's API.
function defaultNormalizeFunc(appRoot, pattern) { // eslint-disable-line no-shadow
var pat = pattern;
if (pat.match(/^common\/js/)) {
pat = path.join(appRoot, '/common/static/' + pat);
} else if (pat.match(/^xmodule_js\/common_static/)) {
pat = path.join(appRoot, '/common/static/' +
pat.replace(/^xmodule_js\/common_static\//, ''));
function defaultNormalizeFunc(appRoot, pattern) { // eslint-disable-line no-shadow
if (pattern.match(/^common\/js/)) {
pattern = path.join(appRoot, '/common/static/' + pattern);
} else if (pattern.match(/^xmodule_js\/common_static/)) {
pattern = path.join(appRoot, '/common/static/' +
pattern.replace(/^xmodule_js\/common_static\//, ''));
}
return pat;
return pattern;
}
function normalizePathsForCoverage(files, normalizeFunc, preprocessors) {
@@ -228,7 +225,7 @@ function setDefaults(files) {
return files.map(function(f) {
var file = _.isObject(f) ? f : {pattern: f};
if (!file.included && !file.webpack) {
file.included = false;
f.included = false;
}
return file;
});
@@ -247,7 +244,7 @@ function getBaseConfig(config, useRequireJs) {
'node_modules/bootstrap/dist/js/bootstrap.js',
'node_modules/underscore/underscore.js',
'node_modules/backbone/backbone.js',
'common/static/js/test/i18n.js'
'common/static/js/test/i18n.js',
];
if (useRequireJs) {
@@ -279,21 +276,22 @@ function getBaseConfig(config, useRequireJs) {
var hostname = 'localhost';
var port = 9876;
var customPlugin = {
'framework:custom': ['factory', initFrameworks]
};
if (process.env.hasOwnProperty('BOK_CHOY_HOSTNAME')) {
hostname = process.env.BOK_CHOY_HOSTNAME;
if (hostname === 'edx.devstack.lms') {
port = 19876;
} else {
}
else {
port = 19877;
}
}
initFrameworks.$inject = ['config.files'];
var customPlugin = {
'framework:custom': ['factory', initFrameworks]
};
return {
// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: '',
@@ -372,7 +370,7 @@ function getBaseConfig(config, useRequireJs) {
ChromeDocker: {
base: 'SeleniumWebdriver',
browserName: 'chrome',
getDriver: function() {
getDriver: function () {
return new webdriver.Builder()
.forBrowser('chrome')
.usingServer('http://edx.devstack.chrome:4444/wd/hub')
@@ -382,7 +380,7 @@ function getBaseConfig(config, useRequireJs) {
FirefoxDocker: {
base: 'SeleniumWebdriver',
browserName: 'firefox',
getDriver: function() {
getDriver: function () {
var options = new firefox.Options(),
profile = new firefox.Profile();
profile.setPreference('focusmanager.testmode', true);
@@ -421,9 +419,8 @@ function getBaseConfig(config, useRequireJs) {
}
function configure(config, options) {
var useRequireJs = options.useRequireJs === undefined ? true : options.useRequireJs,
baseConfig = getBaseConfig(config, useRequireJs),
files, filesForCoverage, preprocessors;
var useRequireJs = options.useRequireJs === undefined ? true : useRequireJs,
baseConfig = getBaseConfig(config, useRequireJs);
if (options.includeCommonFiles) {
_.forEach(['libraryFiles', 'sourceFiles', 'specFiles', 'fixtureFiles'], function(collectionName) {
@@ -431,7 +428,7 @@ function configure(config, options) {
});
}
files = _.flatten(
var files = _.flatten(
_.map(
['libraryFilesToInclude', 'libraryFiles', 'sourceFiles', 'specFiles', 'fixtureFiles', 'runFiles'],
function(collectionName) { return options[collectionName] || []; }
@@ -450,7 +447,7 @@ function configure(config, options) {
// We set it to false by default because RequireJS should be used instead.
files = setDefaults(files);
filesForCoverage = _.flatten(
var filesForCoverage = _.flatten(
_.map(
['sourceFiles', 'specFiles'],
function(collectionName) { return options[collectionName]; }
@@ -459,7 +456,7 @@ function configure(config, options) {
// If we give symlink paths to Istanbul, coverage for each path gets tracked
// separately. So we pass absolute paths to the karma-coverage preprocessor.
preprocessors = _.extend(
var preprocessors = _.extend(
{},
options.preprocessors,
normalizePathsForCoverage(filesForCoverage, options.normalizePathsForCoverageFunc, options.preprocessors)

View File

@@ -1,10 +1,11 @@
/**
* Provides helper methods for invoking Studio modal windows in Jasmine tests.
*/
define(['underscore', 'jquery', 'common/js/components/views/feedback_notification', 'common/js/components/views/feedback_prompt',
'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers'],
function(_, $, NotificationView, Prompt, AjaxHelpers) {
'use strict';
(function(define) {
'use strict';
define(['jquery', 'common/js/components/views/feedback_notification', 'common/js/components/views/feedback_prompt',
'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers'],
function($, NotificationView, Prompt, AjaxHelpers) {
var installViewTemplates, createFeedbackSpy, verifyFeedbackShowing,
verifyFeedbackHidden, createNotificationSpy, verifyNotificationShowing,
verifyNotificationHidden, createPromptSpy, confirmPrompt, inlineEdit, verifyInlineEditChange,
@@ -41,11 +42,11 @@ define(['underscore', 'jquery', 'common/js/components/views/feedback_notificatio
return createFeedbackSpy(NotificationView, type || 'Mini');
};
verifyNotificationShowing = function() {
verifyNotificationShowing = function(notificationSpy, text) {
verifyFeedbackShowing.apply(this, arguments);
};
verifyNotificationHidden = function() {
verifyNotificationHidden = function(notificationSpy) {
verifyFeedbackHidden.apply(this, arguments);
};
@@ -62,11 +63,11 @@ define(['underscore', 'jquery', 'common/js/components/views/feedback_notificatio
}
};
verifyPromptShowing = function() {
verifyPromptShowing = function(promptSpy, text) {
verifyFeedbackShowing.apply(this, arguments);
};
verifyPromptHidden = function() {
verifyPromptHidden = function(promptSpy) {
verifyFeedbackHidden.apply(this, arguments);
};
@@ -147,5 +148,5 @@ define(['underscore', 'jquery', 'common/js/components/views/feedback_notificatio
submitAndVerifyFormSuccess: submitAndVerifyFormSuccess,
submitAndVerifyFormError: submitAndVerifyFormError
};
}
);
});
}).call(this, define || RequireJS.define);

View File

@@ -0,0 +1,27 @@
define([], function() {
'use strict';
return function invokePageFactory(name, factory) {
var args;
if (typeof window.pageFactoryArguments === 'undefined') {
throw Error(
'window.pageFactoryArguments must be initialized before calling invokePageFactory(' +
name +
'). Use the <%static:invoke_page_bundle> template tag.'
);
}
args = window.pageFactoryArguments[name];
if (typeof args === 'undefined') {
throw Error(
'window.pageFactoryArguments["' +
name +
'"] must be initialized before calling invokePageFactory(' +
name +
'). Use the <%static:invoke_page_bundle> template tag.'
);
}
factory.apply(null, window.pageFactoryArguments[name]);
};
});

View File

@@ -1,14 +1,14 @@
(function() {
'use strict';
this.XBlock.Runtime.v1 = (function() {
XBlock.Runtime.v1 = (function() {
function v1() {
var block = this;
var _this = this;
this.childMap = function() {
return v1.prototype.childMap.apply(block, arguments);
return v1.prototype.childMap.apply(_this, arguments);
};
this.children = function() {
return v1.prototype.children.apply(block, arguments);
return v1.prototype.children.apply(_this, arguments);
};
}
@@ -17,15 +17,14 @@
};
v1.prototype.childMap = function(block, childName) {
var child, idx, len, ref;
ref = this.children(block);
for (idx = 0, len = ref.length; idx < len; idx++) {
child = ref[idx];
var child, _i, _len, _ref;
_ref = this.children(block);
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
child = _ref[_i];
if (child.name === childName) {
return child;
}
}
return null;
};
/**

View File

@@ -1,9 +0,0 @@
<%!
from openedx.core.djangolib.markup import HTML
%>
<%page expression_filter="h"/>
<%namespace name='static' file='static_content.html'/>
<%static:webpack entry="XModuleShim"/>
${HTML(content)}

View File

@@ -68,7 +68,8 @@ VIDEO_MENUS = {
}
@js_defined('window.Video', 'window.jQuery', 'window.MathJax')
@js_defined('window.Video', 'window.RequireJS.require', 'window.jQuery',
'window.MathJax', 'window.MathJax.isReady')
class VideoPage(PageObject):
"""
Video player in the courseware.

View File

@@ -29,7 +29,6 @@ CLASS_SELECTORS = {
'collapse_bar': '.videolist-extra-videos',
'status': '.transcripts-message-status',
'attach_transcript': '.file-chooser > input[type="file"]',
'basic_metadata': '.basic_metadata_edit',
}
BUTTON_SELECTORS = {
@@ -92,8 +91,8 @@ FIELDS_WO_CLEAR = [
DELAY = 0.5
@js_defined('window.Video', 'window.jQuery', 'window.XModule', 'window.XBlock',
'window.MathJax')
@js_defined('window.Video', 'window.RequireJS.require', 'window.jQuery', 'window.XModule', 'window.XBlock',
'window.MathJax', 'window.MathJax.isReady')
class VideoComponentPage(VideoPage):
"""
CMS Video Component Page
@@ -103,10 +102,8 @@ class VideoComponentPage(VideoPage):
@wait_for_js
def is_browser_on_page(self):
return (
self.q(css='div{0}'.format(CLASS_SELECTORS['video_xmodule'])).present or
self.q(css='div{0}'.format(CLASS_SELECTORS['xblock'])).present
)
return self.q(css='div{0}'.format(CLASS_SELECTORS['video_xmodule'])).present or self.q(
css='div{0}'.format(CLASS_SELECTORS['xblock'])).present
def get_element_selector(self, class_name, vertical=False):
return super(VideoComponentPage, self).get_element_selector(class_name, vertical=vertical)

View File

@@ -740,39 +740,6 @@ class AcceptanceTest(WebAppTest):
# Use long messages so that failures show actual and expected values
self.longMessage = True # pylint: disable=invalid-name
def tearDown(self):
try:
self.browser.get('http://{}:{}'.format(
os.environ.get('BOK_CHOY_HOSTNAME', '127.0.0.1'),
os.environ.get('BOK_CHOY_LMS_PORT', 8003),
))
except: # pylint: disable=bare-except
self.browser.get('http://{}:{}'.format(
os.environ.get('BOK_CHOY_HOSTNAME', '127.0.0.1'),
os.environ.get('BOK_CHOY_CMS_PORT', 8031),
))
logs = self.browser.execute_script("return window.localStorage.getItem('console_log_capture');")
if not logs:
return
logs = json.loads(logs)
log_dir = path('test_root') / 'log'
if 'shard' in os.environ:
log_dir /= "shard_{}".format(os.environ["SHARD"])
log_dir.mkdir_p()
with (log_dir / '{}.browser.log'.format(self.id()[:60])).open('w') as browser_log:
for (message, url, line_no, col_no, stack) in logs:
browser_log.write(u"{}:{}:{}: {}\n {}\n".format(
url,
line_no,
col_no,
message,
(stack or "").replace('\n', '\n ')
))
super(AcceptanceTest, self).tearDown()
class UniqueCourseTest(AcceptanceTest):
"""

View File

@@ -9,8 +9,6 @@ from unittest import skipIf
from mock import patch
from nose.plugins.attrib import attr
from bok_choy.promise import EmptyPromise
from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc
from common.test.acceptance.pages.common.auto_auth import AutoAuthPage
from common.test.acceptance.pages.studio.overview import CourseOutlinePage
@@ -143,11 +141,6 @@ class CMSVideoBaseTest(UniqueCourseTest):
xblock_index: number starting from 1 (0th entry is the unit page itself)
"""
self.unit_page.xblocks[xblock_index].edit()
EmptyPromise(
lambda: self.video.q(css='div.basic_metadata_edit').visible,
"Wait for the basic editor to be open",
timeout=5
).fulfill()
def open_advanced_tab(self):
"""

View File

@@ -26,20 +26,19 @@ from common.test.acceptance.tests.helpers import (
)
VIDEO_SOURCE_PORT = 8777
VIDEO_HOSTNAME = os.environ.get('BOK_CHOY_HOSTNAME', 'localhost')
HTML5_SOURCES = [
'http://{}:{}/gizmo.mp4'.format(VIDEO_HOSTNAME, VIDEO_SOURCE_PORT),
'http://{}:{}/gizmo.webm'.format(VIDEO_HOSTNAME, VIDEO_SOURCE_PORT),
'http://{}:{}/gizmo.ogv'.format(VIDEO_HOSTNAME, VIDEO_SOURCE_PORT),
'http://localhost:{0}/gizmo.mp4'.format(VIDEO_SOURCE_PORT),
'http://localhost:{0}/gizmo.webm'.format(VIDEO_SOURCE_PORT),
'http://localhost:{0}/gizmo.ogv'.format(VIDEO_SOURCE_PORT),
]
HTML5_SOURCES_INCORRECT = [
'http://{}:{}/gizmo.mp99'.format(VIDEO_HOSTNAME, VIDEO_SOURCE_PORT),
'http://localhost:{0}/gizmo.mp99'.format(VIDEO_SOURCE_PORT),
]
HLS_SOURCES = [
'http://{}:{}/hls/history.m3u8'.format(VIDEO_HOSTNAME, VIDEO_SOURCE_PORT),
'http://localhost:{0}/hls/history.m3u8'.format(VIDEO_SOURCE_PORT),
]

View File

@@ -53,7 +53,7 @@ from openedx.core.djangolib.js_utils import js_escaped_string
</%block>
<%block name="page_bundle">
<%static:webpack entry="js/factories/login">
LoginFactory("${reverse('homepage') | n, js_escaped_string}");
</%static:webpack>
<%static:invoke_page_bundle page_name="js/pages/login" class_name="LoginFactory">
"${reverse('homepage') | n, js_escaped_string}"
</%static:invoke_page_bundle>
</%block>