diff --git a/cms/static/sass/elements/_controls.scss b/cms/static/sass/elements/_controls.scss index 833137208e..4b18144b8a 100644 --- a/cms/static/sass/elements/_controls.scss +++ b/cms/static/sass/elements/_controls.scss @@ -188,6 +188,39 @@ } } +// LMS-style CAPA button for consistency with LMS buttons +%btn-lms-style { + border: 1px solid $btn-lms-border; + border-radius: 3px; + box-shadow: inset 0 1px 0 0 $white; + color: $gray-d3; + display: inline-block; + font-size: inherit; + font-weight: bold; + background-color: $btn-lms-background; + background-image: -webkit-linear-gradient($btn-lms-background,$btn-lms-gradient); + background-image: linear-gradient($btn-lms-background,$btn-lms-gradient); + padding: 7px 18px; + text-decoration: none; + text-shadow: 0 1px 0 $btn-lms-shadow; + background-clip: padding-box; + font-size: 0.8125em; + + &:focus, + &:hover { + box-shadow: inset 0 1px 0 0 $btn-lms-shadow-hover; + cursor: pointer; + background-color: $btn-lms-background-hover; + background-image: -webkit-linear-gradient($btn-lms-background-hover,$btn-lms-gradient-hover); + background-image: linear-gradient($btn-lms-background-hover,$btn-lms-gradient-hover); + } + + &:active { + border: 1px solid $btn-lms-border; + box-shadow: inset 0 0 8px 4px $btn-lms-shadow-active,inset 0 0 8px 4px $btn-lms-shadow-active; + } +} + // +Button Element // ==================== .button { diff --git a/cms/static/sass/elements/_xblocks.scss b/cms/static/sass/elements/_xblocks.scss index 61940d0b71..52109ded06 100644 --- a/cms/static/sass/elements/_xblocks.scss +++ b/cms/static/sass/elements/_xblocks.scss @@ -248,6 +248,15 @@ color: $color-visibility-set; } } + + .action { + + .save { + // taking styles from LMS for these Save buttons to maintain consistency + // there is no studio-specific style for these LMS-styled buttons + @extend %btn-lms-style; + } + } } // +Messaging - Xblocks diff --git a/cms/static/sass/partials/_variables.scss b/cms/static/sass/partials/_variables.scss index ceef6c4e1e..1bc0923178 100644 --- a/cms/static/sass/partials/_variables.scss +++ b/cms/static/sass/partials/_variables.scss @@ -82,6 +82,17 @@ $gray-d2: shade($gray,40%); $gray-d3: shade($gray,60%); $gray-d4: shade($gray,80%); +// These define button styles similar to LMS +// The goal here is consistency (until we can overhaul all of this...) +$btn-lms-border: #d2c9c9; +$btn-lms-background: #f1f1f1; +$btn-lms-gradient: #d9d1d1; +$btn-lms-shadow: #fcfbfb; +$btn-lms-shadow-hover: #fefefe; +$btn-lms-background-hover: #e4e4e4; +$btn-lms-gradient-hover: #d1c9c9; +$btn-lms-shadow-active: #cac2c2; + $blue: rgb(0, 159, 230); $blue-l1: tint($blue,20%); $blue-l2: tint($blue,40%); diff --git a/common/lib/xmodule/xmodule/css/word_cloud/display.scss b/common/lib/xmodule/xmodule/css/word_cloud/display.scss index 36bbce6a27..5c015bdd86 100644 --- a/common/lib/xmodule/xmodule/css/word_cloud/display.scss +++ b/common/lib/xmodule/xmodule/css/word_cloud/display.scss @@ -10,12 +10,15 @@ .result_cloud_section.active { display: block; - width: 635px; + width: 100%; height: auto; - margin-left: auto; - margin-right: auto; + margin-top: 1em; + + h3 { + font-size: 100%; + } } .your_words{ font-size: 0.85em; display: block; -} \ No newline at end of file +} diff --git a/common/lib/xmodule/xmodule/js/src/word_cloud/word_cloud_main.js b/common/lib/xmodule/xmodule/js/src/word_cloud/word_cloud_main.js index eee5c828fb..ce003c6951 100644 --- a/common/lib/xmodule/xmodule/js/src/word_cloud/word_cloud_main.js +++ b/common/lib/xmodule/xmodule/js/src/word_cloud/word_cloud_main.js @@ -12,106 +12,110 @@ */ (function(requirejs, require, define) { - define('WordCloudMain', [], function() { - /** - * @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. - */ + '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. + // 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. + // Dimensions of the box where the word cloud will be drawn. this.width = 635; this.height = 635; - // Hide WordCloud container before Ajax request done + // 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. + // 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') { - console.log('ERROR: ' + response.error); + _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; } + }); - _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('input.save').on('click', function() { + $(el).find('.save').on('click', function() { _this.submitAnswer(); }); - }; // End-of: var WordCloudMain = function (el) { + }; // 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. - */ + /** + * @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. + // 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. + // 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') { - console.log('ERROR: ' + response.error); + _this.ajax_url + '/submit', $.param(data), + function(response) { + if (response.status !== 'success') { + return; + } - return; + _this.showWordCloud(response); } + ); + }; // End-of: WordCloudMain.prototype.submitAnswer = function() { - _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. - */ + /** + * @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, @@ -124,9 +128,9 @@ minSize = 10000; scaleFactor = 1; maxFontSize = 200; - minFontSize = 15; + minFontSize = 16; - // Find the word with the maximum percentage. I.e. the most popular word. + // 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; @@ -136,11 +140,11 @@ } }); - // 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. + // 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); @@ -154,136 +158,192 @@ } }); - // Update the maximum font size based on the longest word. + // Update the maximum font size based on the longest word. maxFontSize *= scaleFactor; - // Generate the word cloud. + // 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; + .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; + size = size >= minFontSize ? size : minFontSize; - return size; - }) - .on('end', function(words, bounds) { - // Draw the word cloud. - _this.drawWordCloud(response, words, bounds); - }) - .start(); - }; // End-of: WordCloudMain.prototype.showWordCloud = function (response) { + 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'. - */ + /** + * @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. + // Color words in different colors. var fill = d3.scale.category20(), - // Will be populated by words the user enetered. + // Will be populated by words the user enetered. studentWordsKeys = [], - // Comma separated string of user enetered words. + // Comma separated string of user enetered words. studentWordsStr, - // By default we do not scale. + // By default we do not scale. scale = 1, - // Caсhing of DOM element + // Caсhing of DOM element cloudSectionEl = this.wordCloudEl.find('.result_cloud_section'), - // Needed for caсhing of d3 group elements - groupEl; + // Needed for caсhing of d3 group elements + groupEl, - // If bounding rectangle is given, scale based on the bounding box of all the words. + // 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) - ); + 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('' + word + '' + percent); + studentWordsKeys.push(HtmlUtils.interpolateHtml( + '{listStart}{startTag}{word}{endTag}{percent}{listEnd}', + { + listStart: HtmlUtils.HTML('
  • '), + startTag: HtmlUtils.HTML(''), + word: word, + endTag: HtmlUtils.HTML(''), + percent: percent, + listEnd: HtmlUtils.HTML('
  • ') + } + ).toString()); }); - studentWordsStr = '' + studentWordsKeys.join(', '); + + studentWordsStr = '' + studentWordsKeys.join(''); cloudSectionEl - .addClass('active') - .find('.your_words').html(studentWordsStr) - .end() - .find('.total_num_words').html(response.total_count); + .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(''), + end_strong: HtmlUtils.HTML(''), + total: response.total_count + } + ) + ); $(cloudSectionEl.attr('id') + ' .word_cloud').empty(); - // Actual drawing of word cloud. + // 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('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') - .text(function(d) { - var res = ''; + .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 + '%'; + $.each(response.top_words, function(index, value) { + if (value.text === d.text) { + res = value.percent + '%'; - return; - } + return; + } + }); + + return res; }); - return res; - }); - groupEl - .append('text') - .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) { - + .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) { + }); // End-of: define('WordCloudMain', [], function() { +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function(requirejs, require, define) { diff --git a/common/lib/xmodule/xmodule/word_cloud_module.py b/common/lib/xmodule/xmodule/word_cloud_module.py index 757eea6c6d..4850f6d4cd 100644 --- a/common/lib/xmodule/xmodule/word_cloud_module.py +++ b/common/lib/xmodule/xmodule/word_cloud_module.py @@ -37,20 +37,25 @@ class WordCloudFields(object): """XFields for word cloud.""" display_name = String( display_name=_("Display Name"), - help=_("Display name for this module"), + help=_("The label for this word cloud on the course page."), scope=Scope.settings, default="Word cloud" ) + instructions = String( + display_name=_("Instructions"), + help=_("Add instructions to help learners understand how to use the word cloud. Clear instructions are important, especially for learners who have accessibility requirements."), # nopep8 pylint: disable=C0301 + scope=Scope.settings, + ) num_inputs = Integer( display_name=_("Inputs"), - help=_("Number of text boxes available for students to input words/sentences."), + help=_("The number of text boxes available for learners to add words and sentences."), scope=Scope.settings, default=5, values={"min": 1} ) num_top_words = Integer( display_name=_("Maximum Words"), - help=_("Maximum number of words to be displayed in generated word cloud."), + help=_("The maximum number of words displayed in the generated word cloud."), scope=Scope.settings, default=250, values={"min": 1} @@ -64,7 +69,7 @@ class WordCloudFields(object): # Fields for descriptor. submitted = Boolean( - help=_("Whether this student has posted words to the cloud."), + help=_("Whether this learner has posted words to the cloud."), scope=Scope.user_state, default=False ) @@ -74,7 +79,7 @@ class WordCloudFields(object): default=[] ) all_words = Dict( - help=_("All possible words from all students."), + help=_("All possible words from all learners."), scope=Scope.user_state_summary ) top_words = Dict( @@ -235,11 +240,14 @@ class WordCloudModule(WordCloudFields, XModule): def get_html(self): """Template rendering.""" context = { - 'element_id': self.location.html_id(), - 'element_class': self.location.category, 'ajax_url': self.system.ajax_url, + 'display_name': self.display_name, + 'display_name_default': WordCloudFields.display_name.default, + 'instructions': self.instructions, + 'element_class': self.location.category, + 'element_id': self.location.html_id(), 'num_inputs': self.num_inputs, - 'submitted': self.submitted + 'submitted': self.submitted, } self.content = self.system.render_template('word_cloud.html', context) return self.content diff --git a/common/test/acceptance/pages/lms/instructor_dashboard.py b/common/test/acceptance/pages/lms/instructor_dashboard.py index 4bec22f311..ffb5b48d7c 100644 --- a/common/test/acceptance/pages/lms/instructor_dashboard.py +++ b/common/test/acceptance/pages/lms/instructor_dashboard.py @@ -942,8 +942,8 @@ class SpecialExamsPageAttemptsSection(PageObject): Clicks the "x" to remove the Student's attempt. """ with self.handle_alert(confirm=True): - self.q(css="a.remove-attempt").first.click() - self.wait_for_element_absence("a.remove-attempt", "exam attempt") + self.q(css=".remove-attempt").first.click() + self.wait_for_element_absence(".remove-attempt", "exam attempt") class DataDownloadPage(PageObject): diff --git a/lms/djangoapps/courseware/features/word_cloud.py b/lms/djangoapps/courseware/features/word_cloud.py index b94e143720..e14a0a5307 100644 --- a/lms/djangoapps/courseware/features/word_cloud.py +++ b/lms/djangoapps/courseware/features/word_cloud.py @@ -20,7 +20,7 @@ def view_word_cloud(_step): @step('I press the Save button') def press_the_save_button(_step): - button_css = '.input_cloud_section input.save' + button_css = '.input_cloud_section .save' world.css_click(button_css) diff --git a/lms/djangoapps/courseware/tests/test_word_cloud.py b/lms/djangoapps/courseware/tests/test_word_cloud.py index fba6b6ecec..a7de27f303 100644 --- a/lms/djangoapps/courseware/tests/test_word_cloud.py +++ b/lms/djangoapps/courseware/tests/test_word_cloud.py @@ -241,14 +241,16 @@ class TestWordCloud(BaseTestXmodule): ) def test_word_cloud_constructor(self): - """Make sure that all parameters extracted correclty from xml""" + """Make sure that all parameters extracted correctly from xml""" fragment = self.runtime.render(self.item_descriptor, STUDENT_VIEW) - expected_context = { 'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url, + 'display_name': self.item_descriptor.display_name, + 'display_name_default': 'Word cloud', + 'instructions': self.item_descriptor.instructions, 'element_class': self.item_descriptor.location.category, 'element_id': self.item_descriptor.location.html_id(), 'num_inputs': 5, # default value - 'submitted': False # default value + 'submitted': False, # default value } self.assertEqual(fragment.content, self.runtime.render_template('word_cloud.html', expected_context)) diff --git a/lms/templates/word_cloud.html b/lms/templates/word_cloud.html index 7298ba99fb..2b1f5d148f 100644 --- a/lms/templates/word_cloud.html +++ b/lms/templates/word_cloud.html @@ -1,30 +1,52 @@ +<%page expression_filter="h"/> <%! from django.utils.translation import ugettext as _ %> -
    -
    + % if display_name: +

    ${display_name}

    + % endif + + % if instructions is not None: +
    + % elif display_name: +
    + % else: +
    + % endif + + % if instructions is not None: +
    + ${instructions} +
    + % endif + % for row in range(num_inputs): - + % endfor -
    - -
    -
    +
    + +
    + -
    -

    ${_('Your words:')}

    -

    ${_('Total number of words:')}

    +
    -
    +

    +

    ${_('Your words were:')}

    + + -
    +