Revert "Switch container factory to webpack"
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -3,16 +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='': []
|
||||
)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
module.exports = {
|
||||
extends: 'eslint-config-edx',
|
||||
root: true,
|
||||
settings: {
|
||||
'import/resolver': 'webpack',
|
||||
},
|
||||
overrides: {
|
||||
excludedFiles: 'public/js/*',
|
||||
},
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
public
|
||||
@@ -0,0 +1,5 @@
|
||||
window.WordCloud = function(el) {
|
||||
RequireJS.require(['WordCloudMain'], function(WordCloudMain) {
|
||||
new WordCloudMain(el);
|
||||
});
|
||||
};
|
||||
@@ -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) {
|
||||
@@ -1,3 +0,0 @@
|
||||
const WordCloudMain = require('xmodule/assets/word_cloud/src/js/word_cloud_main.js');
|
||||
|
||||
window.WordCloud = WordCloudMain.default;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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: '_',
|
||||
},
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -194,6 +194,19 @@
|
||||
// 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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -78,4 +78,4 @@ function(AsyncProcess) {
|
||||
});
|
||||
});
|
||||
});
|
||||
}(require));
|
||||
}(RequireJS.require));
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,8 +11,6 @@
|
||||
oldOTBD = window.onTouchBasedDevice;
|
||||
window.onTouchBasedDevice = jasmine
|
||||
.createSpy('onTouchBasedDevice').and.returnValue(null);
|
||||
|
||||
state = jasmine.initializePlayer('video_html5.html');
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
(function(require, define, undefined) {
|
||||
(function(requirejs, require, define, undefined) {
|
||||
'use strict';
|
||||
|
||||
require(
|
||||
@@ -320,4 +320,4 @@ function(Initialize) {
|
||||
});
|
||||
});
|
||||
});
|
||||
}(require, define));
|
||||
}(RequireJS.requirejs, RequireJS.require, RequireJS.define));
|
||||
|
||||
@@ -100,4 +100,4 @@ function(Iterator) {
|
||||
});
|
||||
});
|
||||
});
|
||||
}(require));
|
||||
}(RequireJS.require));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
(function(require, define, undefined) {
|
||||
(function(requirejs, require, define, undefined) {
|
||||
require(
|
||||
['video/00_resizer.js'],
|
||||
function(Resizer) {
|
||||
@@ -261,4 +261,4 @@ function(Resizer) {
|
||||
});
|
||||
});
|
||||
});
|
||||
}(require, define));
|
||||
}(RequireJS.requirejs, RequireJS.require, RequireJS.define));
|
||||
|
||||
@@ -64,4 +64,4 @@ function(Sjson) {
|
||||
});
|
||||
});
|
||||
});
|
||||
}(require));
|
||||
}(RequireJS.require));
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import '../helper.js'
|
||||
|
||||
(function(undefined) {
|
||||
'use strict';
|
||||
var describeInfo, state, oldOTBD;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
(function(require, define, undefined) {
|
||||
(function(requirejs, require, define, undefined) {
|
||||
'use strict';
|
||||
|
||||
require(
|
||||
@@ -1065,4 +1065,4 @@ function(VideoPlayer, HLS) {
|
||||
});
|
||||
});
|
||||
});
|
||||
}(require, define));
|
||||
}(RequireJS.requirejs, RequireJS.require, RequireJS.define));
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = {};
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// VideoControl module.
|
||||
define(
|
||||
'video/04_video_control.js',
|
||||
['time.js'],
|
||||
function(Time) {
|
||||
[],
|
||||
function() {
|
||||
// VideoControl() function - what this module "exports".
|
||||
return function(state) {
|
||||
var dfd = $.Deferred();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,9 +6,8 @@
|
||||
'video/00_sjson.js',
|
||||
'video/00_async_process.js',
|
||||
'edx-ui-toolkit/js/utils/html-utils',
|
||||
'draggabilly',
|
||||
'time.js',
|
||||
], function(Sjson, AsyncProcess, HtmlUtils, Draggabilly, Time) {
|
||||
'draggabilly'
|
||||
], function(Sjson, AsyncProcess, HtmlUtils, Draggabilly) {
|
||||
/**
|
||||
* @desc VideoCaption module exports a function.
|
||||
*
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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__':
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
27
common/static/common/js/utils/page_factory.js
Normal file
27
common/static/common/js/utils/page_factory.js
Normal 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]);
|
||||
};
|
||||
});
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
|
||||
<%static:webpack entry="XModuleShim"/>
|
||||
${content}
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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())).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):
|
||||
"""
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user