quiz html answers closes #7176

test plan:
1. edit a quiz
2. add a multiple choice question
3. click the pencil on the first answer
4. add stuff to tiny
5. click done
6. click "update question"
7. reload page, note the HTML answer
8. repeat now that the answer is saved (make sure
   new and existing questions are handled
   identically)
9. Mess around w/ the question types to ensure
   no weird behavior

Change-Id: Idaf741777635fd2b697747a5d331a6b7e34dee8d
Reviewed-on: https://gerrit.instructure.com/8823
Tested-by: Hudson <hudson@instructure.com>
Reviewed-by: Zach Wily <zach@instructure.com>
This commit is contained in:
Ryan Florence 2012-02-20 09:59:22 -07:00
parent 57e7b1cc48
commit cfeeb75f89
14 changed files with 348 additions and 40 deletions

View File

@ -0,0 +1,73 @@
define ['i18n!editor', 'jquery', 'tinymce.editor_box'], (I18n, $) ->
##
# Toggles an element between a rich text editor and itself
class EditorToggle
options:
# text to display in the "done" button
doneText: I18n.t 'done_as_in_finished', 'Done'
##
# @param {jQueryEl} @el
# @param {Object} options
constructor: (@el, options) ->
@options = $.extend {}, @options, options
@textArea = @createTextArea()
@done = @createDone()
@content = $.trim @el.html()
@editing = false
##
# Toggles between editing the content and displaying it
# @api public
toggle: ->
if not @editing
@edit()
else
@display()
##
# Converts the element to an editor
# @api public
edit: ->
@textArea.val @el.html()
@textArea.insertBefore @el
@el.detach()
@done.insertAfter @textArea
@textArea.editorBox()
@editing = true
##
# Converts the editor to an element
# @api public
display: ->
@content = @textArea._justGetCode()
@textArea.val @content
@el.html @content
@el.insertBefore @textArea
@textArea._removeEditor()
@textArea.detach()
@done.detach()
# so tiny doesn't hang on to this instance
@textArea.attr 'id', ''
@editing = false
##
# creates the textarea tinymce uses for the editor
# @api private
createTextArea: ->
$('<textarea/>')
.css('width', '100%') # tiny mimics the width of the textarea
.addClass('editor-toggle')
##
# creates the "done" button used to exit the editor
# @api private
createDone: ->
$('<a/>')
.html(@options.doneText)
.addClass('button edit-html-done edit_html_done')
.click => @display()

View File

@ -0,0 +1,76 @@
define ['jquery', 'compiled/editor/EditorToggle'], ($, EditorToggle) ->
##
# Toggles a multiple choice quiz answer between an editor and an element
class MultipleChoiceToggle extends EditorToggle
##
# @param {jQuery} @editButton - the edit button to trigger the toggle
# @param {Object} options - options for EditorToggle
# @api public
constructor: (@editButton, options) ->
@cacheElements()
super @answer.find('.answer_html'), options
##
# Finds all the relevant elements from the perspective of the edit button
# that toggles the element between itself and an editor
# @api private
cacheElements: ->
@answer = @editButton.parents '.answer'
@answerText = @answer.find 'input[name=answer_text]'
@answerText.hide()
@input = @answer.find 'input[name=answer_html]'
##
# Extends EditorToggle::display to @toggleIfEmpty and sets the hidden
# input's value to the content from the editor
# @api public
display: ->
super
@toggleIfEmpty()
@input.val @content
@answerText.val '' if @content is ''
##
# Extends EditorToggle::edit to always hide the original input
# in case it was shown because the editor content was empty
# @api public
edit: ->
super
@answerText.hide()
if @content is ''
@textArea._setContentCode @answerText.val()
else
@textArea._setContentCode @content
##
# Shows the original <input type=text> that the editor replaces and hides
# the HTML display element, also sets @input value to '' so the quizzes.js
# hooks don't think its an html answer
# @api public
showAnswerText: ->
@answerText.show()
@el.hide()
@input.val ''
##
# Shows the HTML element and hides the origina input
# @api public
showEl: ->
@answerText.hide()
@el.show()
##
# If the editor has no content, it will show the original input
# @api public
toggleIfEmpty: ->
if @isEmpty() then @showAnswerText() else @showEl()
##
# Determines if the editor has any content
# @returns {Boolean}
# @api private
isEmpty: ->
$.trim(@content) is ''

View File

@ -558,12 +558,10 @@ form.question_form
:-moz-border-radius 5px
.answer.hover
:border 1px solid #ddd
.answer .delete_answer_link
:display block
.answer.hover .delete_answer_link
:background url(/images/delete_circle.png) no-repeat center center
.answer .delete_answer_link:focus
:background url(/images/delete_circle.png) no-repeat center center
.answer .question_actions
display: none
.answer.hover .question_actions
display: block
.answer_comments
:-moz-border-radius 5px
:border 2px solid #faa
@ -879,3 +877,14 @@ ul#quiz_versions
.question select
max-width: 100%
.edit-html-done
float: right
.question_actions .atr-edit
display: none
.multiple_choice_question .question_actions .atr-edit,
.multiple_answers_question .question_actions .atr-edit
display: inline-block

View File

@ -1,11 +1,11 @@
.defaultSkin table.mceLayout
border: 1px solid #ddd
-moz-border-radius: 5px
padding: 6px
margin: 10px 0
background-color: transparent
tr.mceLast
td.mceIframeContainer
border: 2px solid #ccc
padding: 4px
tr.mceFirst
td.mceToolbar
border-top: 0
@ -47,4 +47,4 @@
td
padding: 0
a.mceAction,a.mceOpen
border-color: #ccc
border-color: #ccc

View File

@ -62,11 +62,13 @@
<span class="id">&nbsp;</span>
<span class="match_id">&nbsp;</span>
</div>
</td><td style="vertical-align: top; padding-top: 0.5em; text-align: right; width: 20px;">
<div style="width: 20px;">
<a href="#" class="delete_answer_link no-hover" title="<%= t('titles.delete_answer', "Delete this Answer") %>"><%= image_tag "blank.png", :alt => t(:delete, "Delete") %></a>
</td><td style="vertical-align: top; padding-top: 0.5em; text-align: right; width: 40px;">
<div class="question_actions">
<a href="#" class="edit_html atr-edit" title="<%= t('titles.edit_as_html', "Edit HTML") %>"><%= t('titles.edit_as_html', "Edit HTML") %></a>
<a href="#" class="delete_answer_link atr-delete" title="<%= t('titles.delete_answer', "Delete this Answer") %>"><%= t('titles.delete_answer', "Delete this Answer") %></a>
</div>
</td>
</tr>
</table>
</div>

View File

@ -15,13 +15,14 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
require([
define([
'i18n!quizzes',
'jquery' /* $ */,
'calcCmd',
'str/htmlEscape',
'str/pluralize',
'wikiSidebar',
'compiled/editor/MultipleChoiceToggle',
'jquery.ajaxJSON' /* ajaxJSON */,
'jquery.instructure_date_and_time' /* time_field, datetime_field */,
'jquery.instructure_forms' /* formSubmit, fillFormData, getFormData, formErrors, errorBox */,
@ -38,7 +39,7 @@ require([
'vendor/jquery.scrollTo' /* /\.scrollTo/ */,
'jqueryui/sortable' /* /\.sortable/ */,
'jqueryui/tabs' /* /\.tabs/ */
], function(I18n, $, calcCmd, htmlEscape, pluralize, wikiSidebar) {
], function(I18n, $, calcCmd, htmlEscape, pluralize, wikiSidebar, MultipleChoiceToggle) {
// TODO: refactor this... it's not going to be horrible, but it will
// take a little bit of work. I just wrapped it in a closure for now
@ -178,7 +179,21 @@ require([
if (question_type == "matching_question") {
$answer.removeClass('correct_answer');
}
// won't exist if they've never clicked the edit button
var htmlToggle = $answer.find('.edit_html').data('editorToggle')
var supportsHTMLAnswers = question_type === 'multiple_choice_question' || question_type === 'multiple_answers_question'
if (htmlToggle && supportsHTMLAnswers) {
// some answer types share the same text fields, so we show it
htmlToggle.showAnswerText();
} else if (htmlToggle) {
// call display so the editor gets closed and we display the HTML next
// time we're editing an answer type that supports HTML answers
htmlToggle.display();
}
},
questionContentCounter: 0,
showFormQuestion: function($form) {
@ -777,6 +792,7 @@ require([
var $answer = $("#form_answer_template").clone(true).attr('id', '');
$answer.find(".answer_type").hide().filter("." + answer.answer_type).show();
answer.answer_weight = parseFloat(answer.answer_weight);
if (isNaN(answer.answer_weight)) { answer.answer_weight = 0; }
quiz.updateFormAnswer($answer, answer, true);
$answer.find('input[placeholder]').placeholder();
@ -1897,7 +1913,7 @@ require([
$("#question_form_template").submit(function(event) {
event.preventDefault();
event.stopPropagation();
event.stopPropagation();
var $displayQuestion = $(this).prev();
var $form = $(this);
var $answers = $form.find(".answer");
@ -1907,6 +1923,10 @@ require([
values: ['question_type', 'question_name', 'question_points', 'correct_comments', 'incorrect_comments', 'neutral_comments',
'question_text', 'answer_selection_type', 'text_after_answers', 'matching_answer_incorrect_matches']
});
// save any open html answers
$form.find('.edit_html_done').trigger('click');
questionData.assessment_question_bank_id = $(".question_bank_id").text() || ""
var error_text = null;
if (questionData.question_type == 'calculated_question') {
@ -1947,6 +1967,7 @@ require([
var data = $answer.getFormData();
data.blank_id = $answer.find(".blank_id").text();
data.answer_text = $answer.find("input[name='answer_text']:visible").val();
data.answer_html = $answer.find(".answer_html").html();
if (questionData.question_type == "true_false_question") {
data.answer_text = (i == 0) ? I18n.t('true', "True") : I18n.t('false', "False");
}
@ -2369,9 +2390,7 @@ require([
wikiSidebar.attachToEditor($("#quiz_description"));
}
setTimeout(function() {
$("#quiz_description").editorBox();
}, 2000);
$("#quiz_description").editorBox();
$(".toggle_description_views_link").click(function(event) {
event.preventDefault();
@ -2755,5 +2774,20 @@ require([
$question.triggerHandler('settings_change', false);
}).change();
}
// attach HTML answers but only when they click the button
$('#questions').delegate('.edit_html', 'click', function(event) {
event.preventDefault();
var $this = $(this);
var toggler = $this.data('editorToggle');
// create toggler instance on the first click
if (!toggler) {
toggler = new MultipleChoiceToggle($this);
$this.data('editorToggle', toggler);
}
toggler.toggle();
});
});

View File

@ -443,6 +443,7 @@ define([
if(editor && editor.execCommand) {
editor.execCommand.apply(editor, arguments);
}
return this;
};
$.fn._justGetCode = function() {

View File

@ -41,16 +41,6 @@ define([
// Only allow non-intrusive types to be auto-opened (i.e. don't
// allow auto-playing of video files on page load)
var autoShowContentClasses = ['instructure_scribd_file'];
setInterval(function() {
promptInteraction.counter = (promptInteraction.counter || 0) + 1;
if(promptInteraction.counter > 5) {
promptInteraction.counter = 0;
if (promptInteraction.hasChanged) {
promptInteraction.hasChanged = false;
$("#instructure_link_prompt_form .prompt").triggerHandler('data_update');
}
}
}, 100);
ed.addCommand('instructureLinks', function() {
var $editor = $("#" + ed.id);
if ($editor.data('enable_bookmarking')) {
@ -148,10 +138,7 @@ define([
$editor.editorBox('create_link', $(event.target).closest('img').attr('alt'));
$box.dialog('close');
});
$box.find("#instructure_link_prompt_form .prompt").bind('change keypress', function(e) {
promptInteraction.counter = 0;
promptInteraction.hasChanged = true;
}).bind('data_update', function() {
$box.find("#instructure_link_prompt_form .prompt").bind('change keyup', function() {
$("#instructure_link_prompt .actions").empty();
var val = $(this).val();
// If the user changes the link then it should no longer

View File

@ -16,7 +16,7 @@
.defaultSkin .mceExternalClose {position:absolute; top:3px; right:3px; width:7px; height:7px; background:url(../../img/icons.gif) -820px 0}
/* Layout */
.defaultSkin table.mceLayout {border:0; border-left:1px solid #CCC; border-right:1px solid #CCC}
.defaultSkin table.mceLayout {border:0;}
.defaultSkin table.mceLayout tr.mceFirst td {border-top:1px solid #CCC}
.defaultSkin table.mceLayout tr.mceLast td {border-bottom:1px solid #CCC}
.defaultSkin table.mceToolbar, .defaultSkin tr.mceFirst .mceToolbar tr td, .defaultSkin tr.mceLast .mceToolbar tr td {border:0; margin:0; padding:0;}

View File

@ -1,8 +1,6 @@
.defaultSkin table.mceLayout {
border: 0;
background-color: transparent; }
.defaultSkin table.mceLayout tr.mceLast td.mceIframeContainer {
border: 2px solid #ccc; }
.defaultSkin table.mceLayout tr.mceFirst td.mceToolbar {
border-top: 0;
background: #ccc url(../images/tinybg.png) repeat-x top left;
@ -31,5 +29,3 @@
border-color: transparent; }
.defaultSkin table.mceLayout tr.mceFirst td.mceToolbar table.mceToolbar tr td td {
padding: 0; }
.defaultSkin table.mceLayout tr.mceFirst td.mceToolbar table.mceToolbar tr td td a.mceAction, .defaultSkin table.mceLayout tr.mceFirst td.mceToolbar table.mceToolbar tr td td a.mceOpen {
border-color: #ccc; }

View File

@ -104,4 +104,12 @@ shared_examples_for "quizzes selenium tests" do
el.find_element(:css, "textarea").should be_displayed
el.find_element(:css, "textarea").send_keys(text)
end
def edit_first_question
question = driver.find_element :css, '.display_question'
driver.action.move_to(question).perform
driver.find_element(:css, '.edit_question_link').click
wait_for_animations
end
end

View File

@ -306,4 +306,124 @@ describe "quizzes question creation" do
questions[1].should have_class("true_false_question")
questions[2].should have_class("short_answer_question")
end
end
context "html answers" do
def edit_first_html_answer(question_type=nil)
edit_first_question
click_option('.question_form:visible .question_type', question_type) if question_type
driver.execute_script "$('.answer').addClass('hover');"
find_with_jquery('.edit_html:visible').click
end
def close_first_html_answer
driver.find_element(:css, '.edit-html-done').click
end
it "should allow HTML answers for multiple choice" do
skip_if_ie 'Out of memory'
quiz_with_new_questions
edit_first_html_answer
type_in_tiny '.answer:eq(2) textarea', 'HTML'
close_first_html_answer
html = driver.execute_script "return $('.answer:eq(2) .answer_html').html()"
html.should == '<p>HTML</p>'
find_with_jquery('.question_form:visible').submit
refresh_page
edit_first_question
html = driver.execute_script "return $('.answer:eq(2) .answer_html').html()"
html.should == '<p>HTML</p>'
end
it "should allow HTML answers for multiple answers" do
skip_if_ie 'Out of memory'
quiz_with_new_questions
edit_first_html_answer 'Multiple Answers'
type_in_tiny '.answer:eq(2) textarea', 'HTML'
close_first_html_answer
html = driver.execute_script "return $('.answer:eq(2) .answer_html').html()"
html.should == '<p>HTML</p>'
find_with_jquery('.question_form:visible').submit
refresh_page
edit_first_question
html = driver.execute_script "return $('.answer:eq(2) .answer_html').html()"
html.should == '<p>HTML</p>'
end
def check_for_no_edit_button(option)
click_option('.question_form:visible .question_type', option)
driver.execute_script "$('.answer').addClass('hover');"
find_with_jquery('.edit_html:visible').should be_nil
end
it "should not show the edit html button for question types besides multiple choice and multiple answers" do
quiz_with_new_questions
edit_first_question
check_for_no_edit_button 'True/False'
check_for_no_edit_button 'Fill In the Blank'
check_for_no_edit_button 'Fill In Multiple Blanks'
check_for_no_edit_button 'Multiple Dropdowns'
check_for_no_edit_button 'Matching'
check_for_no_edit_button 'Numerical Answer'
end
it "should restore normal input when html answer is empty" do
quiz_with_new_questions
edit_first_html_answer
type_in_tiny '.answer:eq(2) textarea', 'HTML'
# clear tiny
driver.execute_script "$('.answer:eq(2) textarea')._setContentCode('')"
close_first_html_answer
input_length = driver.execute_script "return $('.answer:eq(2) input[name=answer_text]:visible').length"
input_length.should == 1
end
it "should populate the editor and input elements properly" do
quiz_with_new_questions
# add text to regular input
edit_first_question
input = find_with_jquery 'input[name=answer_text]:visible'
input.click
input.send_keys 'ohai'
find_with_jquery('.question_form:visible').submit
wait_for_ajax_requests
# open it up in the editor, make sure the text matches the input
edit_first_html_answer
content = driver.execute_script "return $('.answer:eq(2) textarea')._justGetCode()"
content.should == '<p>ohai</p>'
# clear it out, make sure the original input is empty also
driver.execute_script "$('.answer:eq(2) textarea')._setContentCode('')"
close_first_html_answer
value = driver.execute_script "return $('input[name=answer_text]:visible')[0].value"
value.should == ''
end
it "should save open html answers when the question is submitted for multiple choice" do
quiz_with_new_questions
edit_first_html_answer
type_in_tiny '.answer:eq(2) textarea', 'HTML'
find_with_jquery('.question_form:visible').submit
refresh_page
edit_first_question
html = driver.execute_script "return $('.answer:eq(2) .answer_html').html()"
html.should == '<p>HTML</p>'
end
it "should save open html answers when the question is submitted for multiple answers" do
quiz_with_new_questions
edit_first_html_answer 'Multiple Answers'
type_in_tiny '.answer:eq(2) textarea', 'HTML'
find_with_jquery('.question_form:visible').submit
refresh_page
edit_first_question
html = driver.execute_script "return $('.answer:eq(2) .answer_html').html()"
html.should == '<p>HTML</p>'
end
end
end

View File

@ -30,6 +30,7 @@ describe "quizzes questions" do
question.find_element(:css, ".add_answer_link").click
answers = question.find_elements(:css, ".form_answers > .answer")
answers.length.should eql(4)
driver.action.move_to(answers[3]).perform
answers[3].find_element(:css, ".delete_answer_link").click
answers = question.find_elements(:css, "div.form_answers > div.answer")
answers.length.should eql(3)
@ -275,4 +276,4 @@ describe "quizzes questions" do
select[:selectedIndex].should eql "1"
end
end
end
end

View File

@ -250,3 +250,4 @@ describe "quizzes" do
driver.find_element(:css, '#content .question_name').should include_text("Question 1")
end
end