Skip to content

Commit

Permalink
add the ability to regrade a quiz
Browse files Browse the repository at this point in the history
This commit gives teachers the ability to regrade quizzes by using
different options per quiz question:

* Current Correct Only
* Full Credit (regardless of answer choice)
* Previous and Current Correct
* No Point change (for updating the display of a question)

Test Plan:
  You'll want to run through each question regrade option making sure
  scores change appropriately.

"I seldom end up where I wanted to go, but almost always end up where I
need to be." - Douglas Adams

Change-Id: I9dbb88154cd3ac630bf59dbf3e997a87f75649dc
Reviewed-on: https://gerrit.instructure.com/22018
Tested-by: Jenkins <[email protected]>
Reviewed-by: Derek DeVries <[email protected]>
QA-Review: Myller de Araujo <[email protected]>
Product-Review: Stanley Stuart <[email protected]>
  • Loading branch information
Stanley Stuart committed Aug 19, 2013
1 parent 85236b9 commit 9b76539
Show file tree
Hide file tree
Showing 53 changed files with 1,769 additions and 24 deletions.
10 changes: 10 additions & 0 deletions app/coffeescripts/bundles/quiz_show.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ require [
'quiz_show'
'quiz_rubric'
'message_students'
'jquery.disableWhileLoading'
], (inputMethods) ->
$ ->
inputMethods.setWidths()
Expand All @@ -12,3 +13,12 @@ require [
$(".download_submissions_link").click (event) ->
event.preventDefault()
INST.downloadSubmissions($(this).attr('href'))

# load in regrade versions
if ENV.SUBMISSION_VERSIONS_URL && !ENV.IS_SURVEY
versions = $("#quiz-submission-version-table")
versions.css(height: "100px")
dfd = $.get ENV.SUBMISSION_VERSIONS_URL, (data) ->
versions.html(data)
versions.css(height: "auto")
versions.disableWhileLoading(dfd)
3 changes: 3 additions & 0 deletions app/controllers/quiz_questions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,17 @@ def update
if authorized_action(@quiz, @current_user, :update)
@question = @quiz.quiz_questions.find(params[:id])
question_data = params[:question]
question_data[:regrade_user] = @current_user
question_data ||= {}

if question_data[:quiz_group_id]
@group = @quiz.quiz_groups.find(question_data[:quiz_group_id])
if question_data[:quiz_group_id] != @question.quiz_group_id
@question.quiz_group_id = question_data[:quiz_group_id]
@question.position = @group.quiz_questions.length
end
end

@question.question_data = question_data
@question.save
@quiz.did_edit if @quiz.created?
Expand Down
48 changes: 40 additions & 8 deletions app/controllers/quizzes_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class QuizzesController < ApplicationController
before_filter :require_context
add_crumb(proc { t('#crumbs.quizzes', "Quizzes") }) { |c| c.send :named_context_url, c.instance_variable_get("@context"), :context_quizzes_url }
before_filter { |c| c.active_tab = "quizzes" }
before_filter :get_quiz, :only => [:statistics, :edit, :show, :reorder, :history, :update, :destroy, :moderate, :filters, :read_only, :managed_quiz_data]
before_filter :get_quiz, :only => [:statistics, :edit, :show, :reorder, :history, :update, :destroy, :moderate, :filters, :read_only, :managed_quiz_data, :submission_versions]
before_filter :set_download_submission_dialog_title , only: [:show,:statistics]
# The number of questions that can display "details". After this number, the "Show details" option is disabled
# and the data is not even loaded.
Expand Down Expand Up @@ -158,13 +158,17 @@ def edit
flash[:notice] = t('notices.has_submissions_already', "Keep in mind, some students have already taken or started taking this quiz")
end

regrade_options = Hash[@quiz.current_quiz_question_regrades.map do |qqr|
[qqr.quiz_question_id, qqr.regrade_option]
end]
sections = @context.course_sections.active
hash = { :ASSIGNMENT_ID => @assigment.present? ? @assignment.id : nil,
:ASSIGNMENT_OVERRIDES => assignment_overrides_json(@quiz.overrides_visible_to(@current_user)),
:QUIZ => quiz_json(@quiz, @context, @current_user, session),
:SECTION_LIST => sections.map { |section| { :id => section.id, :name => section.name } },
:QUIZZES_URL => polymorphic_url([@context, :quizzes]),
:CONTEXT_ACTION_SOURCE => :quizzes }
:CONTEXT_ACTION_SOURCE => :quizzes,
:REGRADE_OPTIONS => regrade_options }
append_sis_data(hash)
js_env(hash)
render :action => "new"
Expand Down Expand Up @@ -226,11 +230,9 @@ def show

@assignment = @quiz.assignment
@assignment = @assignment.overridden_for(@current_user) if @assignment
@submission = @quiz.quiz_submissions.find_by_user_id(@current_user.id, :order => 'created_at') rescue nil
if !@current_user || (params[:preview] && @quiz.grants_right?(@current_user, session, :update))
user_code = temporary_user_code
@submission = @quiz.quiz_submissions.find_by_temporary_user_code(user_code)
end

@submission = get_submission

@just_graded = false
if @submission && @submission.needs_grading?(!!params[:take])
@submission.grade_submission(:finished_at => @submission.end_at)
Expand All @@ -240,7 +242,9 @@ def show
if @submission
upload_url = api_v1_quiz_submission_create_file_path(:course_id => @context.id, :quiz_id => @quiz.id)
js_env :UPLOAD_URL => upload_url
js_env :SUBMISSION_VERSIONS_URL => polymorphic_url([@context, @quiz, 'submission_versions'])
end

setup_attachments
submission_counts if @quiz.grants_right?(@current_user, session, :grade) || @quiz.grants_right?(@current_user, session, :read_statistics)
@stored_params = (@submission.temporary_data rescue nil) if params[:take] && @submission && (@submission.untaken? || @submission.preview?)
Expand Down Expand Up @@ -447,13 +451,14 @@ def history
end
@current_submission = @submission
@version_instances = @submission.submitted_versions.sort_by{|v| v.version_number }
@versions = get_versions
params[:version] ||= @version_instances[0].version_number if @submission.untaken? && !@version_instances.empty?
@current_version = true
@version_number = "current"
if params[:version]
@version_number = params[:version].to_i
@unversioned_submission = @submission
@submission = @version_instances.detect{|s| s.version_number >= @version_number}
@submission = @versions.detect{|s| s.version_number >= @version_number}
@submission ||= @unversioned_submission.versions.get(params[:version]).model
@current_version = (@current_submission.version_number == @submission.version_number)
@version_number = "current" if @current_version
Expand Down Expand Up @@ -637,6 +642,19 @@ def setup_headless
@headers = !params[:headless] && !session[:headless_quiz]
end

def submission_versions
if authorized_action(@quiz, @current_user, :read)
@submission = get_submission
@versions = get_versions

if @versions.size > 0
render :layout => false
else
render :nothing => true
end
end
end

protected

def get_quiz
Expand All @@ -645,6 +663,20 @@ def get_quiz
@quiz
end

def get_submission
submission = @quiz.quiz_submissions.find_by_user_id(@current_user.id, :order => 'created_at') rescue nil
if !@current_user || (params[:preview] && @quiz.grants_right?(@current_user, session, :update))
user_code = temporary_user_code
submission = @quiz.quiz_submissions.find_by_temporary_user_code(user_code)
end

submission
end

def get_versions
@submission.submitted_attempts
end

# if this returns false, it's rendering or redirecting, so return from the
# action that called it
def check_lockdown_browser(security_level, redirect_return_url)
Expand Down
12 changes: 12 additions & 0 deletions app/helpers/quizzes_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -453,4 +453,16 @@ def quiz_delete_text(quiz=@quiz)
end
end

def has_regraded_version?(versions)
versions.detect {|v| v.score_before_regrade.present? }
end

def submission_has_regrade?(submission)
submission && submission.score_before_regrade.present?
end

def score_affected_by_regrade?(submission)
submission && submission.score_before_regrade != submission.score
end

end
2 changes: 2 additions & 0 deletions app/messages/notification_types.yml
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@
delay_for: <%= 60*60 %>
- name: Submission Grade Changed
delay_for: <%= 5*60 %>
- name: Quiz Regrade Finished
delay_for: 0

- category: Grading Policies
notifications:
Expand Down
12 changes: 12 additions & 0 deletions app/messages/quiz_regrade_finished.email.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<% define_content :link do %>
<%= HostUrl.protocol %>://<%= HostUrl.context_host(asset.quiz.context) %>/<%= polymorphic_path([asset.quiz.context,asset.quiz]) %>
<% end %>

<% define_content :subject do %>
<%= t :subject, "Quiz Regrade Finished: %{quiz}, %{context}", :quiz => asset.quiz.title, :context => asset.quiz.context.name %>
<% end %>

<%= t :body, "A regrade has finsihed for the quiz %{quiz}", :quiz => asset.quiz %>

<%= t :link_message, "You can view the quiz here:" %>
<%= content :link %>
16 changes: 16 additions & 0 deletions app/messages/quiz_regrade_finished.email.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<% define_content :link do %>
<%= HostUrl.protocol %>://<%= HostUrl.context_host(asset.quiz.context) %>/<%= polymorphic_path([asset.quiz.context,asset.quiz]) %>
<% end %>

<p>
<% define_content :subject do %>
<%= t :subject, "Quiz Regrade Finished: %{quiz}, %{context}", :quiz => asset.quiz.title, :context => asset.quiz.context.name %>
<% end %>
</p>

<p><%= t :body, "A regrade has finsihed for the quiz %{quiz}", :quiz => asset.quiz %></p>

<p>
<%= t :link_message, "You can view the quiz here:" %>
<a href="<%= content :link %>"><%= t :link_message, "You can view the quiz here:" %></a>
</p>
5 changes: 5 additions & 0 deletions app/messages/quiz_regrade_finished.facebook.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<% define_content :link do %>
<%= HostUrl.protocol %>://<%= HostUrl.context_host(asset.quiz.context) %>/<%= polymorphic_path([asset.quiz.context,asset.quiz]) %>
<% end %>

<p><%= t :body, "Quiz Regrade Finished: %{title}", :title => asset.quiz.title %></p>
3 changes: 3 additions & 0 deletions app/messages/quiz_regrade_finished.sms.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<%= t :body_sms, "Quiz Regrade Finished for %{title} in %{context}.", :title => asset.quiz.title, :context => asset.quiz.context %>

<%= t :more_info, "More info at %{url}", :url => HostUrl.context_host(asset.quiz.context) %>
10 changes: 10 additions & 0 deletions app/messages/quiz_regrade_finished.summary.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<% define_content :link do %>
<%= HostUrl.protocol %>://<%= polymorphic_path([asset.quiz.context,asset.quiz]) %>
<% end %>

<% define_content :subject do %>
<%= t :subject, "Quiz Regraded: %{quiz}, %{context}", :quiz => asset.quiz.title, :context => asset.quiz.context.name %>
<% end %>

<%= t :body, "A regrade has finished for your quiz %{title}.", :title => asset.quiz.title, :wrapper => "<b><a href=\"#{content :link}\">\\1</a></b>" %>

4 changes: 4 additions & 0 deletions app/messages/quiz_regrade_finished.twitter.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<% define_content :link do %>
<%= HostUrl.protocol %>://<%= HostUrl.context_host(asset.quiz.context) %>/<%= polymorphic_path([asset.quiz.context,asset.quiz]) %>
<% end %>
<%= t :body, "Canvas Alert - Quiz Regraded : %{quiz}", :quiz => asset.quiz.title %>
1 change: 1 addition & 0 deletions app/models/assessment_question.rb
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ def self.parse_question(qdata, assessment_question=nil)
question = HashWithIndifferentAccess.new
qdata = qdata.with_indifferent_access
previous_data = assessment_question.question_data rescue {}
question[:regrade_option] = qdata[:regrade_option] if qdata[:regrade_option].present?
question[:points_possible] = (qdata[:points_possible] || previous_data[:points_possible] || 0.0).to_f
question[:correct_comments] = check_length(qdata[:correct_comments] || previous_data[:correct_comments] || "", 'correct comments', 5.kilobyte)
question[:incorrect_comments] = check_length(qdata[:incorrect_comments] || previous_data[:incorrect_comments] || "", 'incorrect comments', 5.kilobyte)
Expand Down
24 changes: 24 additions & 0 deletions app/models/quiz.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#

require 'quiz_question_link_migrator'
require 'quiz_regrading'

class Quiz < ActiveRecord::Base
include Workflow
Expand All @@ -43,6 +44,7 @@ class Quiz < ActiveRecord::Base
has_many :quiz_groups, :dependent => :destroy, :order => 'position'
has_many :quiz_statistics, :class_name => 'QuizStatistics', :order => 'created_at'
has_many :attachments, :as => :context, :dependent => :destroy
has_many :quiz_regrades
belongs_to :context, :polymorphic => true
belongs_to :assignment
belongs_to :cloned_item
Expand All @@ -62,6 +64,7 @@ class Quiz < ActiveRecord::Base
before_save :set_defaults
after_save :update_assignment
after_save :touch_context
after_save :regrade_if_published

serialize :quiz_data

Expand Down Expand Up @@ -579,6 +582,7 @@ def generate_submission(user, preview=false)
submission.quiz_data = user_questions
submission.quiz_version = self.version_number
submission.started_at = Time.now
submission.score_before_regrade = nil
submission.end_at = nil
submission.end_at = submission.started_at + (self.time_limit.to_f * 60.0) if self.time_limit
# Admins can take the full quiz whenever they want
Expand Down Expand Up @@ -1127,6 +1131,10 @@ def self.serialization_excludes; [:access_code]; end
scope :active, where("quizzes.workflow_state<>'deleted'")
scope :not_for_assignment, where(:assignment_id => nil)

def teachers
context.teacher_enrollments.map(&:user)
end

def migrate_file_links
QuizQuestionLinkMigrator.migrate_file_links_in_quiz(self)
end
Expand Down Expand Up @@ -1225,4 +1233,20 @@ def validate_draft_state_change
end
end

def regrade_if_published
unless unpublished_changes?
QuizRegrader.send_later_if_production(:regrade!, self)
end
true
end

def current_regrade
QuizRegrade.where(quiz_id: id, quiz_version: version_number).
includes(:quiz_question_regrades => :quiz_question).first
end

def current_quiz_question_regrades
current_regrade ? current_regrade.quiz_question_regrades : []
end

end
16 changes: 16 additions & 0 deletions app/models/quiz_question.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ def update_quiz
end

def question_data=(data)
if data[:regrade_option]
update_question_regrade(data[:regrade_option], data[:regrade_user])
end

if data.is_a?(String)
data = ActiveSupport::JSON.decode(data) rescue nil
elsif data.class == Hash
Expand Down Expand Up @@ -174,4 +178,16 @@ def self.batch_migrate_file_links(ids)
end
end
end

private

def update_question_regrade(regrade_option, regrade_user)
regrade = QuizRegrade.find_or_create_by_quiz_id_and_quiz_version(quiz.id, quiz.version_number) do |qr|
qr.user_id = regrade_user.id
end

question_regrade = QuizQuestionRegrade.find_or_initialize_by_quiz_question_id_and_quiz_regrade_id(id, regrade.id)
question_regrade.regrade_option = regrade_option
question_regrade.save!
end
end
11 changes: 11 additions & 0 deletions app/models/quiz_question_regrade.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class QuizQuestionRegrade < ActiveRecord::Base
attr_accessible :quiz_question_id, :quiz_regrade_id, :regrade_option
belongs_to :quiz_question
belongs_to :quiz_regrade

validates_presence_of :quiz_question_id
validates_presence_of :quiz_regrade_id

delegate :question_data, to: :quiz_question
end

13 changes: 13 additions & 0 deletions app/models/quiz_regrade.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class QuizRegrade < ActiveRecord::Base
attr_accessible :user_id, :quiz_id, :quiz_version
belongs_to :quiz
belongs_to :user
has_many :quiz_regrade_runs
has_many :quiz_question_regrades

validates_presence_of :quiz_version
validates_presence_of :quiz_id
validates_presence_of :user_id

delegate :teachers, to: :quiz
end
24 changes: 24 additions & 0 deletions app/models/quiz_regrade_run.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
class QuizRegradeRun < ActiveRecord::Base
belongs_to :quiz_regrade
attr_accessible :quiz_regrade_id, :started_at, :finished_at
validates_presence_of :quiz_regrade_id

def self.perform(regrade)
run = create!(quiz_regrade_id: regrade.id, started_at: Time.now)
yield
run.finished_at = Time.now
run.save!
end

has_a_broadcast_policy
set_broadcast_policy do |policy|
policy.dispatch :quiz_regrade_finished
policy.to { teachers }
policy.whenever do |run|
old,new = run.changes['finished_at']
!!(new && old.nil?)
end
end

delegate :teachers, :quiz, to: :quiz_regrade
end
Loading

0 comments on commit 9b76539

Please sign in to comment.