2011-02-01 09:57:29 +08:00
|
|
|
#
|
|
|
|
# Copyright (C) 2011 Instructure, Inc.
|
|
|
|
#
|
|
|
|
# This file is part of Canvas.
|
|
|
|
#
|
|
|
|
# Canvas is free software: you can redistribute it and/or modify it under
|
|
|
|
# the terms of the GNU Affero General Public License as published by the Free
|
|
|
|
# Software Foundation, version 3 of the License.
|
|
|
|
#
|
|
|
|
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
|
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
|
|
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
|
|
# details.
|
|
|
|
#
|
|
|
|
# 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/>.
|
|
|
|
#
|
|
|
|
|
|
|
|
module QuizzesHelper
|
2013-12-28 06:34:47 +08:00
|
|
|
def needs_unpublished_warning?(quiz=@quiz, user=@current_user)
|
2013-11-15 02:22:17 +08:00
|
|
|
if quiz.feature_enabled?(:draft_state)
|
2013-12-28 06:34:47 +08:00
|
|
|
return false unless can_publish(quiz)
|
|
|
|
show_unpublished_changes = true
|
2013-11-14 06:31:15 +08:00
|
|
|
else
|
2013-12-28 06:34:47 +08:00
|
|
|
return false unless can_read(quiz)
|
|
|
|
show_unpublished_changes = can_publish(quiz)
|
2013-11-14 06:31:15 +08:00
|
|
|
end
|
2013-08-07 03:13:33 +08:00
|
|
|
|
2013-12-28 06:34:47 +08:00
|
|
|
!quiz.available? || (show_unpublished_changes && quiz.unpublished_changes?)
|
2013-03-07 06:33:30 +08:00
|
|
|
end
|
|
|
|
|
2013-12-28 06:34:47 +08:00
|
|
|
def can_read(quiz, user=@current_user)
|
|
|
|
can_do(quiz, user, :read)
|
|
|
|
end
|
|
|
|
|
|
|
|
def can_publish(quiz, user=@current_user)
|
|
|
|
can_do(quiz, user, :update) || can_do(quiz, user, :manage)
|
2013-08-07 03:13:33 +08:00
|
|
|
end
|
|
|
|
|
2013-02-14 03:37:17 +08:00
|
|
|
def unpublished_quiz_warning
|
|
|
|
I18n.t("#quizzes.warnings.unpublished_quiz",
|
|
|
|
'*This quiz is unpublished* Only teachers can see the quiz until ' +
|
|
|
|
'it is published.',
|
|
|
|
:wrapper => '<strong class=unpublished_quiz_warning>\1</strong>')
|
|
|
|
end
|
|
|
|
|
|
|
|
def unpublished_changes_warning
|
|
|
|
I18n.t("#quizzes.warnings.unpublished_changes",
|
|
|
|
'*You have made unpublished changes to this quiz.* '+
|
|
|
|
'These changes will not appear for students until you publish or ' +
|
|
|
|
'republish the quiz.',
|
|
|
|
:wrapper => '<strong class=unpublished_quiz_warning>\1</strong>')
|
|
|
|
end
|
|
|
|
|
2014-03-12 01:44:33 +08:00
|
|
|
def draft_state_unsaved_changes_warning
|
|
|
|
I18n.t("#quizzes.warnings.draft_state_unsaved_changes",
|
|
|
|
'*You have made changes to the questions in this quiz.* '+
|
|
|
|
'These changes will not appear for students until you ' +
|
|
|
|
'save the quiz.',
|
|
|
|
:wrapper => '<strong class=unsaved_quiz_warning>\1</strong>')
|
|
|
|
end
|
|
|
|
|
|
|
|
def quiz_published_state_warning(quiz=@quiz)
|
|
|
|
if !quiz.available?
|
|
|
|
unpublished_quiz_warning
|
|
|
|
else
|
|
|
|
if quiz.feature_enabled? :draft_state
|
|
|
|
draft_state_unsaved_changes_warning
|
|
|
|
else
|
|
|
|
unpublished_changes_warning
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def display_save_button?(quiz=@quiz)
|
|
|
|
quiz.available? && quiz.feature_enabled?(:draft_state) && can_publish(quiz)
|
|
|
|
end
|
|
|
|
|
2012-10-10 00:41:11 +08:00
|
|
|
def render_score(score, precision=2)
|
2011-11-22 07:19:04 +08:00
|
|
|
if score.nil?
|
|
|
|
'_'
|
|
|
|
else
|
2012-10-10 00:41:11 +08:00
|
|
|
score.to_f.round(precision).to_s
|
2011-11-22 07:19:04 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-02-14 03:37:17 +08:00
|
|
|
def render_quiz_type(quiz_type)
|
|
|
|
case quiz_type
|
|
|
|
when "practice_quiz"
|
|
|
|
I18n.t('#quizzes.practice_quiz', "Practice Quiz")
|
|
|
|
when "assignment"
|
|
|
|
I18n.t('#quizzes.graded_quiz', "Graded Quiz")
|
|
|
|
when "graded_survey"
|
|
|
|
I18n.t('#quizzes.graded_survey', "Graded Survey")
|
|
|
|
when "survey"
|
|
|
|
I18n.t('#quizzes.ungraded_survey', "Ungraded Survey")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def render_score_to_keep(quiz_scoring_policy)
|
|
|
|
case quiz_scoring_policy
|
|
|
|
when "keep_highest"
|
|
|
|
I18n.t('#quizzes.keep_highest', 'Highest')
|
|
|
|
when "keep_latest"
|
|
|
|
I18n.t('#quizzes.keep_latest', 'Latest')
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
Controlling visibility of student quiz scores
Teachers can now control when, and for how long, students get to see the
correct answers to their quiz submissions.
The patchset introduces two new columns to Quiz and a CSS rework of the
quiz options form. See linked references for more info.
Quiz#show_correct_answers should no longer be directly accessed, use
Quiz#show_correct_answers? instead.
== Test plan
CAs stands for Correct Answers.
- Create a quiz with 3 questions
- Take the quiz as a student, and:
- get 2/3 of the questions answered correctly
- submit it
Case A - Showing CAs the moment the submission goes in:
- Edit the quiz, and:
- Tick the 'Let Students See Their Score Points' checkbox
- Leave both date fields empty
- Save quiz
- Refresh student view
- You should see the CAs
Case B - Showing CAs after some date:
- Edit the quiz, and:
- Tick the 'Let Students See Their Score Points' checkbox
- Set the "Show Correct Answers At" to 3-days from now
- Refresh student view:
- You should not see the CAs
- Edit the quiz, and:
- Set the date to 3-days back
- Refresh student view:
- You should see the CAs
Case C - Hiding CAs after some date:
- Edit the quiz, and:
- Tick the 'Let Students See Their Score Points' checkbox
- Set the "Hide Correct Answers At" to 3-days from now
- Refresh student view:
- You should see the CAs
- You should see an alert that tells you the CAs will be hidden at
the date you chose earlier
- Edit the quiz, and:
- Set the date to 3-days-back
- Refresh student view:
- You should no longer see the CAs
- Alert should read that the answers stopped being visible as of the
date you chose earlier
Case D - Creating a time-frame for showing CAs:
- Edit the quiz, and:
- Tick the 'Let Students See Their Score Points' checkbox
- Set the "Show Correct Answers At" to yesterday
- Set the "Hide Correct Answers At" to 3-days from now
- Refresh student view:
- You should see the CAs
- You should see an alert that tells you the CAs will be visible
between yesterday and 3 days from now
- Edit the quiz, and:
- Set the "Show At" date to tomorrow
- Refresh student view:
- You should no longer see the CAs
- Alert should still tell you that the CAs will be visible starting
tomorrow until 3 days from now
Case E - Choosing a bad range
- Edit the quiz and:
- Tick the 'Let Students See Their Score Points' checkbox
- Set the "Show at" to tomorrow
- Set the "Hide at" to today, or tomorrow
- You should see an error-box
== Notes and really wild things
- http://docs.kodoware.com/canvas/cnvs-8103-take2
- http://docs.kodoware.com/canvas/cnvs-8103 (legacy changes)
refs CNVS-8103, CNVS-9386
Change-Id: Ib241ee5f143b87164105b7541aadac00f38f79ad
Reviewed-on: https://gerrit.instructure.com/25505
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: Myller de Araujo <myller@instructure.com>
Reviewed-by: Derek DeVries <ddevries@instructure.com>
Product-Review: Derek DeVries <ddevries@instructure.com>
2013-10-23 05:10:44 +08:00
|
|
|
def render_show_correct_answers(quiz)
|
|
|
|
if !quiz.show_correct_answers
|
|
|
|
return I18n.t('#options.no', 'No')
|
|
|
|
end
|
|
|
|
|
|
|
|
show_at = quiz.show_correct_answers_at
|
|
|
|
hide_at = quiz.hide_correct_answers_at
|
|
|
|
|
|
|
|
if show_at && hide_at
|
|
|
|
I18n.t('#quizzes.show_and_hide_correct_answers', 'From %{from} to %{to}', {
|
|
|
|
from: datetime_string(quiz.show_correct_answers_at),
|
|
|
|
to: datetime_string(quiz.hide_correct_answers_at)
|
|
|
|
})
|
|
|
|
elsif show_at
|
|
|
|
I18n.t('#quizzes.show_correct_answers_after', 'After %{date}', {
|
|
|
|
date: datetime_string(quiz.show_correct_answers_at)
|
|
|
|
})
|
|
|
|
elsif hide_at
|
|
|
|
I18n.t('#quizzes.show_correct_answers_until', 'Until %{date}', {
|
|
|
|
date: datetime_string(quiz.hide_correct_answers_at)
|
|
|
|
})
|
|
|
|
else
|
|
|
|
I18n.t('#quizzes.show_correct_answers_immediately', 'Immediately')
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def render_correct_answer_protection(quiz)
|
|
|
|
show_at = quiz.show_correct_answers_at
|
|
|
|
hide_at = quiz.hide_correct_answers_at
|
|
|
|
now = Time.now
|
|
|
|
|
|
|
|
# Some labels will be used in more than one case, so we'll pre-define them.
|
|
|
|
labels = {}
|
|
|
|
if hide_at
|
|
|
|
labels[:available_until] = I18n.t('#quizzes.correct_answers_shown_until',
|
|
|
|
'Correct answers are available until %{date}.', {
|
|
|
|
date: datetime_string(quiz.hide_correct_answers_at)
|
|
|
|
})
|
|
|
|
end
|
|
|
|
|
|
|
|
if !quiz.show_correct_answers
|
|
|
|
I18n.t('#quizzes.correct_answers_protected',
|
|
|
|
'Correct answers are hidden.')
|
|
|
|
elsif hide_at.present? && hide_at < now
|
|
|
|
I18n.t('#quizzes.correct_answers_no_longer_available',
|
|
|
|
'Correct answers are no longer available.')
|
|
|
|
elsif show_at.present? && hide_at.present?
|
|
|
|
# If the answers are currently visible, there's no need to show the range
|
|
|
|
# of availability.
|
|
|
|
if now > show_at
|
|
|
|
labels[:available_until]
|
|
|
|
else
|
|
|
|
I18n.t('#quizzes.correct_answers_shown_between',
|
|
|
|
'Correct answers will be available %{from} - %{to}.', {
|
|
|
|
from: datetime_string(show_at),
|
|
|
|
to: datetime_string(hide_at)
|
|
|
|
})
|
|
|
|
end
|
|
|
|
elsif show_at.present?
|
|
|
|
I18n.t('#quizzes.correct_answers_shown_after',
|
|
|
|
'Correct answers will be available on %{date}.', {
|
|
|
|
date: datetime_string(show_at)
|
|
|
|
})
|
|
|
|
elsif hide_at.present?
|
|
|
|
labels[:available_until]
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-02-14 03:37:17 +08:00
|
|
|
def render_show_responses(quiz_hide_results)
|
|
|
|
# "Let Students See Their Quiz Responses?"
|
|
|
|
case quiz_hide_results
|
|
|
|
when "always"
|
|
|
|
I18n.t('#options.no', "No")
|
|
|
|
when "until_after_last_attempt"
|
|
|
|
I18n.t('#quizzes.after_last_attempt', "After Last Attempt")
|
|
|
|
when nil
|
|
|
|
I18n.t('#quizzes.always', "Always")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-05-08 01:10:36 +08:00
|
|
|
def submitted_students_title(quiz, students, logged_out)
|
|
|
|
length = students.length + logged_out.length
|
2013-03-12 04:19:17 +08:00
|
|
|
if quiz.survey?
|
2013-05-08 01:10:36 +08:00
|
|
|
submitted_students_survey_title(length)
|
2013-03-12 04:19:17 +08:00
|
|
|
else
|
2013-05-08 01:10:36 +08:00
|
|
|
submitted_students_quiz_title(length)
|
2013-03-12 04:19:17 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def submitted_students_quiz_title(student_count)
|
|
|
|
I18n.t('#quizzes.headers.submitted_students_quiz_title',
|
|
|
|
{ :zero => "Students who have taken the quiz",
|
|
|
|
:one => "Students who have taken the quiz (%{count})",
|
|
|
|
:other => "Students who have taken the quiz (%{count})" },
|
|
|
|
{ :count => student_count })
|
|
|
|
end
|
|
|
|
|
|
|
|
def submitted_students_survey_title(student_count)
|
|
|
|
I18n.t('#quizzes.headers.submitted_students_survey_title',
|
|
|
|
{ :zero => "Students who have taken the survey",
|
|
|
|
:one => "Students who have taken the survey (%{count})",
|
|
|
|
:other => "Students who have taken the survey (%{count})" },
|
2013-02-14 03:37:17 +08:00
|
|
|
{ :count => student_count })
|
|
|
|
end
|
|
|
|
|
2013-03-12 04:19:17 +08:00
|
|
|
def no_submitted_students_msg(quiz)
|
|
|
|
if quiz.survey?
|
|
|
|
t('#quizzes.messages.no_submitted_students_survey', "No Students have taken the survey yet")
|
|
|
|
else
|
|
|
|
t('#quizzes.messages.no_submitted_students_quiz', "No Students have taken the quiz yet")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def unsubmitted_students_title(quiz, students)
|
|
|
|
if quiz.survey?
|
|
|
|
unsubmitted_students_survey_title(students.length)
|
|
|
|
else
|
|
|
|
unsubmitted_students_quiz_title(students.length)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def unsubmitted_students_quiz_title(student_count)
|
|
|
|
I18n.t('#quizzes.headers.unsubmitted_students_quiz_title',
|
|
|
|
{ :zero => "Student who haven't taken the quiz",
|
|
|
|
:one => "Students who haven't taken the quiz (%{count})",
|
|
|
|
:other => "Students who haven't taken the quiz (%{count})" },
|
|
|
|
{ :count => student_count })
|
|
|
|
end
|
|
|
|
|
|
|
|
def unsubmitted_students_survey_title(student_count)
|
|
|
|
I18n.t('#quizzes.headers.unsubmitted_students_survey_title',
|
|
|
|
{ :zero => "Student who haven't taken the survey",
|
|
|
|
:one => "Students who haven't taken the survey (%{count})",
|
|
|
|
:other => "Students who haven't taken the survey (%{count})" },
|
|
|
|
{ :count => student_count })
|
|
|
|
end
|
|
|
|
|
|
|
|
def no_unsubmitted_students_msg(quiz)
|
|
|
|
if quiz.survey?
|
|
|
|
t('#quizzes.messages.no_unsubmitted_students_survey', "All Students have taken the survey")
|
|
|
|
else
|
|
|
|
t('#quizzes.messages.no_unsubmitted_students_quiz', "All Students have taken the quiz")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2012-11-29 02:37:54 +08:00
|
|
|
QuestionType = Struct.new(:question_type,
|
2013-03-12 04:19:17 +08:00
|
|
|
:entry_type,
|
|
|
|
:display_answers,
|
|
|
|
:answer_type,
|
|
|
|
:multiple_sets,
|
|
|
|
:unsupported)
|
2012-11-29 02:37:54 +08:00
|
|
|
|
2011-02-01 09:57:29 +08:00
|
|
|
def answer_type(question)
|
2012-11-29 02:37:54 +08:00
|
|
|
return QuestionType.new unless question
|
2011-02-01 09:57:29 +08:00
|
|
|
@answer_types_lookup ||= {
|
2012-11-29 02:37:54 +08:00
|
|
|
"multiple_choice_question" => QuestionType.new(
|
|
|
|
"multiple_choice_question",
|
|
|
|
"radio",
|
|
|
|
"multiple",
|
|
|
|
"select_answer",
|
|
|
|
false,
|
|
|
|
false
|
|
|
|
),
|
|
|
|
"true_false_question" => QuestionType.new(
|
|
|
|
"true_false_question",
|
|
|
|
"radio",
|
|
|
|
"multiple",
|
|
|
|
"select_answer",
|
|
|
|
false,
|
|
|
|
false
|
|
|
|
),
|
|
|
|
"short_answer_question" => QuestionType.new(
|
|
|
|
"short_answer_question",
|
|
|
|
"text_box",
|
|
|
|
"single",
|
|
|
|
"select_answer",
|
|
|
|
false,
|
|
|
|
false
|
|
|
|
),
|
|
|
|
"essay_question" => QuestionType.new(
|
|
|
|
"essay_question",
|
|
|
|
"textarea",
|
|
|
|
"single",
|
|
|
|
"text_answer",
|
|
|
|
false,
|
|
|
|
false
|
|
|
|
),
|
2013-04-13 01:49:59 +08:00
|
|
|
"file_upload_question" => QuestionType.new(
|
|
|
|
"file_upload_question",
|
|
|
|
"file",
|
|
|
|
"single",
|
|
|
|
"file_answer",
|
|
|
|
false,
|
|
|
|
false
|
|
|
|
),
|
2012-11-29 02:37:54 +08:00
|
|
|
"matching_question" => QuestionType.new(
|
|
|
|
"matching_question",
|
|
|
|
"matching",
|
|
|
|
"multiple",
|
|
|
|
"matching_answer",
|
|
|
|
false,
|
|
|
|
false
|
|
|
|
),
|
|
|
|
"missing_word_question" => QuestionType.new(
|
|
|
|
"missing_word_question",
|
|
|
|
"select",
|
|
|
|
"multiple",
|
|
|
|
"select_answer",
|
|
|
|
false,
|
|
|
|
false
|
|
|
|
),
|
|
|
|
"numerical_question" => QuestionType.new(
|
|
|
|
"numerical_question",
|
|
|
|
"numerical_text_box",
|
|
|
|
"single",
|
|
|
|
"numerical_answer",
|
|
|
|
false,
|
|
|
|
false
|
|
|
|
),
|
|
|
|
"calculated_question" => QuestionType.new(
|
|
|
|
"calculated_question",
|
|
|
|
"numerical_text_box",
|
|
|
|
"single",
|
|
|
|
"numerical_answer",
|
|
|
|
false,
|
|
|
|
false
|
|
|
|
),
|
|
|
|
"multiple_answers_question" => QuestionType.new(
|
|
|
|
"multiple_answers_question",
|
|
|
|
"checkbox",
|
|
|
|
"multiple",
|
|
|
|
"select_answer",
|
|
|
|
false,
|
|
|
|
false
|
|
|
|
),
|
|
|
|
"fill_in_multiple_blanks_question" => QuestionType.new(
|
|
|
|
"fill_in_multiple_blanks_question",
|
|
|
|
"text_box",
|
|
|
|
"multiple",
|
|
|
|
"select_answer",
|
|
|
|
true,
|
|
|
|
false
|
|
|
|
),
|
|
|
|
"multiple_dropdowns_question" => QuestionType.new(
|
|
|
|
"multiple_dropdowns_question",
|
|
|
|
"select",
|
|
|
|
"none",
|
|
|
|
"select_answer",
|
|
|
|
true,
|
|
|
|
false
|
|
|
|
),
|
|
|
|
"other" => QuestionType.new(
|
|
|
|
"text_only_question",
|
|
|
|
"none",
|
|
|
|
"none",
|
|
|
|
"none",
|
|
|
|
false,
|
|
|
|
false
|
|
|
|
)
|
2011-02-01 09:57:29 +08:00
|
|
|
}
|
|
|
|
res = @answer_types_lookup[question[:question_type]] || @answer_types_lookup["other"]
|
|
|
|
if res.question_type == "text_only_question"
|
|
|
|
res.unsupported = question[:question_type] != "text_only_question"
|
|
|
|
end
|
|
|
|
res
|
|
|
|
end
|
2011-08-17 00:45:56 +08:00
|
|
|
|
2012-08-15 03:59:38 +08:00
|
|
|
# Build the question-level comments. Lists in the order of :correct_comments, :incorrect_comments, :neutral_comments.
|
|
|
|
# ==== Arguments
|
|
|
|
# * <tt>user_answer</tt> - The user_answer hash.
|
|
|
|
# * <tt>question</tt> - The question hash.
|
|
|
|
def question_comment(user_answer, question)
|
|
|
|
correct_text = (hash_get(user_answer, :correct) == true) ? comment_get(question, :correct_comments) : nil
|
|
|
|
incorrect_text = (hash_get(user_answer, :correct) == false) ? comment_get(question, :incorrect_comments) : nil
|
|
|
|
neutral_text = (hash_get(question, :neutral_comments).present?) ? comment_get(question, :neutral_comments) : nil
|
|
|
|
|
|
|
|
text = []
|
|
|
|
text << content_tag(:p, correct_text, {:class => 'correct_comments'}) if correct_text.present?
|
|
|
|
text << content_tag(:p, incorrect_text, {:class => 'incorrect_comments'}) if incorrect_text.present?
|
|
|
|
text << content_tag(:p, neutral_text, {:class => 'neutral_comments'}) if neutral_text.present?
|
|
|
|
if text.empty?
|
|
|
|
''
|
|
|
|
else
|
|
|
|
content_tag(:div, text.join('').html_safe, {:class => 'quiz_comment'})
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2011-08-17 00:45:56 +08:00
|
|
|
def comment_get(hash, field)
|
|
|
|
if html = hash_get(hash, "#{field}_html".to_sym)
|
|
|
|
raw(html)
|
|
|
|
else
|
|
|
|
hash_get(hash, field)
|
|
|
|
end
|
|
|
|
end
|
2011-09-15 01:37:17 +08:00
|
|
|
|
|
|
|
def fill_in_multiple_blanks_question(options)
|
|
|
|
question = hash_get(options, :question)
|
|
|
|
answers = hash_get(options, :answers).dup
|
2012-08-15 03:59:38 +08:00
|
|
|
answer_list = hash_get(options, :answer_list)
|
2011-09-15 01:37:17 +08:00
|
|
|
res = user_content hash_get(question, :question_text)
|
2013-08-31 02:34:18 +08:00
|
|
|
readonly_markup = hash_get(options, :editable) ? " />" : 'readonly="readonly" />'
|
|
|
|
label_attr = "aria-label='#{I18n.t('#quizzes.labels.multiple_blanks_question', "Fill in the blank, read surrounding text")}'"
|
|
|
|
index = 0
|
|
|
|
|
|
|
|
res.gsub! %r{<input.*?name=['"](question_.*?)['"].*?/>} do |match|
|
|
|
|
a = h(answer_list[index])
|
|
|
|
index += 1
|
|
|
|
# If given answer list, insert the values into the text inputs for displaying user's answers.
|
|
|
|
if answer_list && !answer_list.empty?
|
|
|
|
|
2012-08-29 05:59:20 +08:00
|
|
|
# Replace the {{question_BLAH}} template text with the user's answer text.
|
Quiz Submission Questions API - Update
This patch provides support for answering Quiz Questions via the API.
closes CNVS-9844, CNVS-10225
TEST PLAN
---- ----
Testing this will be a bit rough because there are many variations and
validations to cover. I'll spare the validations that are covered by
specs from the test plan.
Create a quiz with a question of *each* type except "Text" and "File
Upload". There's a script that creates a quiz with its questions
automatically for you if you don't want to keep doing this manually. See
references.
> Answering Questions
Now you need to answer each question via the API. Most of them vary in
formats, but they are fully specified in the API documentation page
(along with examples). See DOCUMENTATION for more info.
> Flagging Questions
Flagging, and unflagging, a question is the same regardless of its type,
see the "EXAMPLE REQUESTS" section.
> Access Validations
Here are some generic, non-question based validations to verify. You
should NOT be able to answer a question if:
- the quiz submission has been turned in
- the quiz submission is overdue
- the Access Code for the quiz is invalid
- the IP filter of the Quiz prohibits you from taking the quiz
- the quiz submission :validation_token is incorrectly specified (ie,
other students shouldn't be able to answer your questions)
- you don't specify the latest :attempt, so if the Quiz has multiple
attempts, and this is your 2nd take, you specify an :attempt of 1,
3, or anything but 2 should fail
- NEW: turn quiz into an OQAAT quiz with the "Can't go back" flag on;
the API should not reject all requests to modify any of the
questions with a 501 error saying that type of quizzes is not
supported yet (support will come in CNVS-10224)
> Grading
Also, when you're done answering the questions, take a look at the
grades and make sure everything gets graded just like it does when using
the UI directly.
> Verifying results in the browser
While taking a quiz in the canvas UI, the scripts perform backups in the
background that would overwrite any changes you do via the API. If you
want to verify the changes you make via the API from the UI, you must
append "?backup=false" to the take quiz page URL, something like this:
http://localhost:3000/courses/1/quizzes/1/take?backup=false
Setting that flag will (for now) disable the backup behaviour and should
make things tick.
EXAMPLE REQUESTS
------- --------
Don't forget to set the 'Content-Type' header to 'application/json'!
> Answering a Multiple-Choice question
[PUT] /api/v1/quiz_submissions/:quiz_submission_id/questions/:id
{
"attempt": 1,
"validation_token": "1babd0...",
"answer": 10
}
> Flagging a question
[PUT] /api/v1/quiz_submissions/:quiz_submission_id/questions/:id/flag
{
"attempt": 1,
"validation_token": "1babd0..."
}
> Unflagging a question
[PUT] /api/v1/quiz_submissions/:quiz_submission_id/questions/:id/unflag
{
"attempt": 1,
"validation_token": "1babd0..."
}
DOCUMENTATION
-------------
Run `bundle exec rake doc:api` and check out the Quiz Submission
Questions page. There's an Appendix that contains example requests for
each question type, as well as the errors produced by each handler.
LINKS
-----
- bootstrap script:
https://gist.github.com/amireh/e7e8f835ffbf1d053e4c
- direct link to the API documentation page:
http://canvas.docs.kodoware.com/quiz_submission_questions.html
Change-Id: I9a958323ece8854bc21a24c2affd8dc3972e46d5
Reviewed-on: https://gerrit.instructure.com/27206
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Derek DeVries <ddevries@instructure.com>
QA-Review: Myller de Araujo <myller@instructure.com>
Product-Review: Ahmad Amireh <ahmad@instructure.com>
2013-12-10 17:15:01 +08:00
|
|
|
match = match.sub(/\{\{question_.*?\}\}/, a.to_s).
|
2012-08-29 05:59:20 +08:00
|
|
|
# Match on "/>" but only when at the end of the string and insert "readonly" if set to be readonly
|
|
|
|
sub(/\/\>\Z/, readonly_markup)
|
2012-08-15 03:59:38 +08:00
|
|
|
end
|
2013-08-31 02:34:18 +08:00
|
|
|
|
|
|
|
# add labelling to input element regardless
|
|
|
|
match.sub(/\/\>\Z/, "#{label_attr} />")
|
|
|
|
end
|
|
|
|
|
|
|
|
unless answer_list && !answer_list.empty?
|
2012-08-15 03:59:38 +08:00
|
|
|
answers.delete_if { |k, v| !k.match /^question_#{hash_get(question, :id)}/ }
|
2014-02-04 15:38:37 +08:00
|
|
|
answers.each { |k, v| res.sub! /\{\{#{k}\}\}/, h(v) }
|
2013-08-31 02:34:18 +08:00
|
|
|
res.gsub! /\{\{question_[^}]+\}\}/, ""
|
2012-08-15 03:59:38 +08:00
|
|
|
end
|
2013-08-31 02:34:18 +08:00
|
|
|
|
|
|
|
res
|
2011-09-15 01:37:17 +08:00
|
|
|
end
|
2012-04-19 06:04:40 +08:00
|
|
|
|
|
|
|
def multiple_dropdowns_question(options)
|
|
|
|
question = hash_get(options, :question)
|
|
|
|
answers = hash_get(options, :answers)
|
2012-08-15 03:59:38 +08:00
|
|
|
answer_list = hash_get(options, :answer_list)
|
2012-04-19 06:04:40 +08:00
|
|
|
res = user_content hash_get(question, :question_text)
|
2012-08-15 03:59:38 +08:00
|
|
|
index = 0
|
2012-04-19 06:04:40 +08:00
|
|
|
res.gsub %r{<select.*?name=['"](question_.*?)['"].*?>.*?</select>} do |match|
|
2012-08-15 03:59:38 +08:00
|
|
|
if answer_list && !answer_list.empty?
|
|
|
|
a = answer_list[index]
|
|
|
|
index += 1
|
|
|
|
else
|
|
|
|
a = hash_get(answers, $1)
|
|
|
|
end
|
2012-04-19 06:04:40 +08:00
|
|
|
match.sub(%r{(<option.*?value=['"]#{ERB::Util.h(a)}['"])}, '\\1 selected')
|
|
|
|
end
|
|
|
|
end
|
2012-08-22 04:21:10 +08:00
|
|
|
|
|
|
|
def duration_in_minutes(duration_seconds)
|
|
|
|
if duration_seconds < 60
|
|
|
|
duration_minutes = 0
|
|
|
|
else
|
|
|
|
duration_minutes = (duration_seconds / 60).round
|
|
|
|
end
|
2012-10-30 03:25:40 +08:00
|
|
|
I18n.t("quizzes.helpers.duration_in_minutes",
|
2012-08-22 04:21:10 +08:00
|
|
|
{ :zero => "less than 1 minute",
|
|
|
|
:one => "1 minute",
|
|
|
|
:other => "%{count} minutes" },
|
|
|
|
:count => duration_minutes)
|
|
|
|
end
|
2012-10-10 00:41:11 +08:00
|
|
|
|
|
|
|
def score_out_of_points_possible(score, points_possible, options={})
|
2012-12-11 01:02:29 +08:00
|
|
|
options.reverse_merge!({ :precision => 2 })
|
2012-10-10 00:41:11 +08:00
|
|
|
score_html = \
|
|
|
|
if options[:id] or options[:class] or options[:style] then
|
|
|
|
content_tag('span',
|
|
|
|
render_score(score, options[:precision]),
|
|
|
|
options.slice(:class, :id, :style))
|
|
|
|
else
|
|
|
|
render_score(score, options[:precision])
|
|
|
|
end
|
|
|
|
I18n.t("quizzes.helpers.score_out_of_points_possible", "%{score} out of %{points_possible}",
|
|
|
|
:score => score_html,
|
|
|
|
:points_possible => render_score(points_possible, options[:precision]))
|
|
|
|
end
|
2013-03-07 06:33:30 +08:00
|
|
|
|
2012-12-11 01:02:29 +08:00
|
|
|
def link_to_take_quiz(link_body, opts={})
|
|
|
|
opts = opts.with_indifferent_access
|
|
|
|
class_array = (opts['class'] || "").split(" ")
|
|
|
|
class_array << 'element_toggler' if @quiz.cant_go_back?
|
|
|
|
opts['class'] = class_array.compact.join(" ")
|
|
|
|
opts['aria-controls'] = 'js-sequential-warning-dialogue' if @quiz.cant_go_back?
|
|
|
|
opts['data-method'] = 'post' unless @quiz.cant_go_back?
|
|
|
|
link_to(link_body, take_quiz_url, opts)
|
2012-11-20 03:41:51 +08:00
|
|
|
end
|
|
|
|
|
2012-12-11 01:02:29 +08:00
|
|
|
def take_quiz_url
|
|
|
|
user_id = @current_user && @current_user.id
|
2014-01-15 06:11:27 +08:00
|
|
|
course_quiz_take_path(@context, @quiz, user_id: user_id)
|
2012-11-20 03:41:51 +08:00
|
|
|
end
|
|
|
|
|
2012-12-11 01:02:29 +08:00
|
|
|
def link_to_take_or_retake_poll(opts={})
|
|
|
|
if @submission && !@submission.settings_only?
|
|
|
|
link_to_retake_poll(opts)
|
|
|
|
else
|
|
|
|
link_to_take_poll(opts)
|
|
|
|
end
|
2012-11-20 03:41:51 +08:00
|
|
|
end
|
|
|
|
|
2012-12-11 01:02:29 +08:00
|
|
|
def link_to_take_poll(opts={})
|
|
|
|
link_to_take_quiz(take_poll_message, opts)
|
2012-11-20 03:41:51 +08:00
|
|
|
end
|
|
|
|
|
2012-12-11 01:02:29 +08:00
|
|
|
def link_to_retake_poll(opts={})
|
|
|
|
link_to_take_quiz(retake_poll_message, opts)
|
2012-11-20 03:41:51 +08:00
|
|
|
end
|
|
|
|
|
2012-12-11 01:02:29 +08:00
|
|
|
def link_to_resume_poll(opts = {})
|
|
|
|
link_to_take_quiz(resume_poll_message, opts)
|
2012-11-20 03:41:51 +08:00
|
|
|
end
|
|
|
|
|
2012-12-11 01:02:29 +08:00
|
|
|
def take_poll_message(quiz=@quiz)
|
|
|
|
quiz.survey? ?
|
|
|
|
t('#quizzes.links.take_the_survey', 'Take the Survey') :
|
|
|
|
t('#quizzes.links.take_the_quiz', 'Take the Quiz')
|
2012-11-20 03:41:51 +08:00
|
|
|
end
|
|
|
|
|
2012-12-11 01:02:29 +08:00
|
|
|
def retake_poll_message(quiz=@quiz)
|
|
|
|
quiz.survey? ?
|
2012-11-20 03:41:51 +08:00
|
|
|
t('#quizzes.links.take_the_survey_again', 'Take the Survey Again') :
|
|
|
|
t('#quizzes.links.take_the_quiz_again', 'Take the Quiz Again')
|
|
|
|
end
|
|
|
|
|
2012-12-11 01:02:29 +08:00
|
|
|
def resume_poll_message(quiz=@quiz)
|
|
|
|
quiz.survey? ?
|
|
|
|
t('#quizzes.links.resume_survey', 'Resume Survey') :
|
|
|
|
t('#quizzes.links.resume_quiz', 'Resume Quiz')
|
2012-11-20 03:41:51 +08:00
|
|
|
end
|
|
|
|
|
2013-04-18 05:42:51 +08:00
|
|
|
def attachment_id_for(question)
|
|
|
|
attach = attachment_for(question)
|
|
|
|
attach[:id] if attach.present?
|
|
|
|
end
|
|
|
|
|
|
|
|
def attachment_for(question)
|
|
|
|
key = "question_#{question[:id]}"
|
|
|
|
@attachments[@stored_params[key].try(:first).to_i]
|
|
|
|
end
|
|
|
|
|
2012-12-11 01:02:29 +08:00
|
|
|
def score_to_keep_message(quiz=@quiz)
|
|
|
|
quiz.scoring_policy == "keep_highest" ?
|
|
|
|
t('#quizzes.links.will_keep_highest_score', "Will keep the highest of all your scores") :
|
|
|
|
t('#quizzes.links.will_keep_latest_score', "Will keep the latest of all your scores")
|
2012-11-20 03:41:51 +08:00
|
|
|
end
|
|
|
|
|
2013-08-10 05:06:27 +08:00
|
|
|
def quiz_edit_text(quiz=@quiz)
|
|
|
|
if quiz.survey?
|
|
|
|
I18n.t('titles.edit_survey', 'Edit Survey')
|
|
|
|
else
|
|
|
|
I18n.t('titles.edit_quiz', 'Edit Quiz')
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def quiz_delete_text(quiz=@quiz)
|
|
|
|
if quiz.survey?
|
|
|
|
I18n.t('titles.delete_survey', 'Delete Survey')
|
|
|
|
else
|
|
|
|
I18n.t('titles.delete_quiz', 'Delete Quiz')
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-07-12 01:53:10 +08:00
|
|
|
def submission_has_regrade?(submission)
|
|
|
|
submission && submission.score_before_regrade.present?
|
|
|
|
end
|
|
|
|
|
|
|
|
def score_affected_by_regrade?(submission)
|
2013-08-29 03:26:36 +08:00
|
|
|
submission && submission.score_before_regrade != submission.kept_score
|
2013-07-12 01:53:10 +08:00
|
|
|
end
|
|
|
|
|
2013-10-24 06:43:29 +08:00
|
|
|
def answer_title(selected_answer, correct_answer, show_correct_answers)
|
|
|
|
titles = []
|
|
|
|
if selected_answer
|
|
|
|
titles << I18n.t(:selected_answer, "You selected this answer.")
|
|
|
|
end
|
|
|
|
|
|
|
|
if correct_answer && show_correct_answers
|
|
|
|
titles << I18n.t(:correct_answer, "This was the correct answer.")
|
|
|
|
end
|
|
|
|
|
|
|
|
title = "title=\"#{titles.join(' ')}\"" if titles.length > 0
|
|
|
|
end
|
|
|
|
|
Controlling visibility of student quiz scores
Teachers can now control when, and for how long, students get to see the
correct answers to their quiz submissions.
The patchset introduces two new columns to Quiz and a CSS rework of the
quiz options form. See linked references for more info.
Quiz#show_correct_answers should no longer be directly accessed, use
Quiz#show_correct_answers? instead.
== Test plan
CAs stands for Correct Answers.
- Create a quiz with 3 questions
- Take the quiz as a student, and:
- get 2/3 of the questions answered correctly
- submit it
Case A - Showing CAs the moment the submission goes in:
- Edit the quiz, and:
- Tick the 'Let Students See Their Score Points' checkbox
- Leave both date fields empty
- Save quiz
- Refresh student view
- You should see the CAs
Case B - Showing CAs after some date:
- Edit the quiz, and:
- Tick the 'Let Students See Their Score Points' checkbox
- Set the "Show Correct Answers At" to 3-days from now
- Refresh student view:
- You should not see the CAs
- Edit the quiz, and:
- Set the date to 3-days back
- Refresh student view:
- You should see the CAs
Case C - Hiding CAs after some date:
- Edit the quiz, and:
- Tick the 'Let Students See Their Score Points' checkbox
- Set the "Hide Correct Answers At" to 3-days from now
- Refresh student view:
- You should see the CAs
- You should see an alert that tells you the CAs will be hidden at
the date you chose earlier
- Edit the quiz, and:
- Set the date to 3-days-back
- Refresh student view:
- You should no longer see the CAs
- Alert should read that the answers stopped being visible as of the
date you chose earlier
Case D - Creating a time-frame for showing CAs:
- Edit the quiz, and:
- Tick the 'Let Students See Their Score Points' checkbox
- Set the "Show Correct Answers At" to yesterday
- Set the "Hide Correct Answers At" to 3-days from now
- Refresh student view:
- You should see the CAs
- You should see an alert that tells you the CAs will be visible
between yesterday and 3 days from now
- Edit the quiz, and:
- Set the "Show At" date to tomorrow
- Refresh student view:
- You should no longer see the CAs
- Alert should still tell you that the CAs will be visible starting
tomorrow until 3 days from now
Case E - Choosing a bad range
- Edit the quiz and:
- Tick the 'Let Students See Their Score Points' checkbox
- Set the "Show at" to tomorrow
- Set the "Hide at" to today, or tomorrow
- You should see an error-box
== Notes and really wild things
- http://docs.kodoware.com/canvas/cnvs-8103-take2
- http://docs.kodoware.com/canvas/cnvs-8103 (legacy changes)
refs CNVS-8103, CNVS-9386
Change-Id: Ib241ee5f143b87164105b7541aadac00f38f79ad
Reviewed-on: https://gerrit.instructure.com/25505
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: Myller de Araujo <myller@instructure.com>
Reviewed-by: Derek DeVries <ddevries@instructure.com>
Product-Review: Derek DeVries <ddevries@instructure.com>
2013-10-23 05:10:44 +08:00
|
|
|
def show_correct_answers?(quiz=@quiz, user=@current_user, submission=@submission)
|
|
|
|
@quiz && @quiz.try_rescue(:show_correct_answers?, @current_user, @submission)
|
|
|
|
end
|
|
|
|
|
|
|
|
def correct_answers_protected?(quiz=@quiz, user=@current_user, submission=@submission)
|
|
|
|
if !quiz
|
|
|
|
false
|
|
|
|
elsif !show_correct_answers?(quiz, user, submission)
|
|
|
|
true
|
|
|
|
elsif quiz.hide_correct_answers_at.present?
|
|
|
|
!quiz.grants_right?(user, nil, :grade)
|
|
|
|
end
|
|
|
|
end
|
2013-10-25 00:19:49 +08:00
|
|
|
|
|
|
|
def point_value_for_input(user_answer, question)
|
|
|
|
return user_answer[:points] unless user_answer[:correct] == 'undefined'
|
|
|
|
|
|
|
|
if ["assignment", "practice_quiz"].include?(@quiz.quiz_type)
|
|
|
|
"--"
|
|
|
|
else
|
|
|
|
question[:points_possible] || 0
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2011-02-01 09:57:29 +08:00
|
|
|
end
|