create ProvisionalGrade table and model

- extracted some shared behavior between submissions and provisional grades
- improved organization in submissions

refs: CNVS-22006
Change-Id: I4207893362e4548d1d76d066322f3b333357807a
Reviewed-on: https://gerrit.instructure.com/59643
Reviewed-by: Jeremy Stanley <jeremy@instructure.com>
Tested-by: Jenkins
QA-Review: Jahnavi Yetukuri <jyetukuri@instructure.com>
Product-Review: Derek Bender <dbender@instructure.com>
This commit is contained in:
Derek Bender 2015-08-03 23:26:42 -06:00
parent 0f329d3960
commit d6eaed579f
11 changed files with 161 additions and 45 deletions

View File

@ -1,4 +1,3 @@
if CANVAS_RAILS3
gem 'rails', '3.2.22'
gem 'rack', '1.4.5'

View File

@ -17,6 +17,7 @@ group :test do
gem 'rspec-rails', '3.2.3'
gem 'rspec-legacy_formatters', '1.0.0'
gem 'rspec-collection_matchers', '1.1.2'
gem 'shoulda-matchers', '2.8.0'
gem 'rubocop', require: false
gem 'rubocop-rspec', require: false

View File

@ -70,7 +70,6 @@ class GradingPeriod < ActiveRecord::Base
.last == self
end
def overlapping?
overlaps.active.exists?
end

View File

@ -0,0 +1,5 @@
module ModeratedGrading
def self.table_name_prefix
'moderated_grading_'
end
end

View File

@ -0,0 +1,23 @@
class ModeratedGrading::ProvisionalGrade < ActiveRecord::Base
include Canvas::GradeValidations
attr_accessible :grade, :score, :position
belongs_to :submission
belongs_to :scorer, class_name: 'User'
validates :position, uniqueness: { scope: :submission_id }
validates :position, presence: true
validates :scorer, presence: true
validates :submission, presence: true
def valid?(*)
set_graded_at if grade_changed?
super
end
private
def set_graded_at
self.graded_at = Time.zone.now
end
end

View File

@ -19,11 +19,16 @@
require 'atom'
class Submission < ActiveRecord::Base
include Canvas::GradeValidations
include CustomValidations
include SendToStream
include Workflow
attr_protected :submitted_at
attr_readonly :assignment_id
attr_accessor :visible_to_user,
:skip_grade_calc
belongs_to :attachment # this refers to the screenshot of the submission if it is a url submission
belongs_to :assignment
belongs_to :user
@ -31,15 +36,16 @@ class Submission < ActiveRecord::Base
belongs_to :group
belongs_to :media_object
belongs_to :student, :class_name => 'User', :foreign_key => :user_id
belongs_to :quiz_submission, :class_name => 'Quizzes::QuizSubmission'
has_many :submission_comments, :order => 'created_at', :dependent => :destroy
has_many :visible_submission_comments, :class_name => 'SubmissionComment', :order => 'created_at, id', :conditions => { :hidden => false }
has_many :hidden_submission_comments, :class_name => 'SubmissionComment', :order => 'created_at, id', :conditions => { :hidden => true }
has_many :assessment_requests, :as => :asset
has_many :assigned_assessments, :class_name => 'AssessmentRequest', :as => :assessor_asset
belongs_to :quiz_submission, :class_name => 'Quizzes::QuizSubmission'
has_one :rubric_assessment, :as => :artifact, :conditions => {:assessment_type => "grading"}
has_many :rubric_assessments, :as => :artifact
has_many :attachment_associations, :as => :context
has_many :provisional_grades, class_name: 'ModeratedGrading::ProvisionalGrade'
has_one :rubric_assessment, :as => :artifact, :conditions => {:assessment_type => "grading"}
# we no longer link submission comments and conversations, but we haven't fixed up existing
# linked conversations so this relation might be useful
@ -49,23 +55,29 @@ class Submission < ActiveRecord::Base
has_many :content_participations, :as => :content
EXPORTABLE_ATTRIBUTES = [
:id, :body, :url, :attachment_id, :grade, :score, :submitted_at, :assignment_id, :user_id, :submission_type, :workflow_state, :created_at, :updated_at, :group_id,
:attachment_ids, :processed, :process_attempts, :grade_matches_current_submission, :published_score, :published_grade, :graded_at, :student_entered_score, :grader_id,
:media_comment_id, :media_comment_type, :quiz_submission_id, :submission_comments_count, :has_rubric_assessment, :attempt, :context_code, :media_object_id,
:turnitin_data, :has_admin_comment, :cached_due_date
]
:id, :body, :url, :attachment_id, :grade, :score, :submitted_at,
:assignment_id, :user_id, :submission_type, :workflow_state,
:created_at, :updated_at, :group_id, :attachment_ids, :processed,
:process_attempts, :grade_matches_current_submission, :published_score,
:published_grade, :graded_at, :student_entered_score, :grader_id,
:media_comment_id, :media_comment_type, :quiz_submission_id,
:submission_comments_count, :has_rubric_assessment, :attempt,
:context_code, :media_object_id, :turnitin_data, :has_admin_comment,
:cached_due_date
].freeze
EXPORTABLE_ASSOCIATIONS = [
:attachment, :assignment, :user, :grader, :group, :media_object, :student, :submission_comments, :assessment_requests, :assigned_assessments, :quiz_submission,
:rubric_assessment, :rubric_assessments, :attachments, :content_participations
]
:attachment, :assignment, :user, :grader, :group, :media_object,
:student, :submission_comments, :assessment_requests,
:assigned_assessments, :quiz_submission, :rubric_assessment,
:rubric_assessments, :attachments, :content_participations
].freeze
serialize :turnitin_data, Hash
validates_presence_of :assignment_id, :user_id
validates_length_of :body, :maximum => maximum_long_text_length, :allow_nil => true, :allow_blank => true
validates_length_of :published_grade, :maximum => maximum_string_length, :allow_nil => true, :allow_blank => true
validates_length_of :grade, :maximum => maximum_string_length, :allow_nil => true, :allow_blank => true
include CustomValidations
validates_as_url :url
scope :with_comments, -> { includes(:submission_comments) }
@ -93,6 +105,16 @@ class Submission < ActiveRecord::Base
where("submissions.assignment_id IN (SELECT assignments.id FROM assignments WHERE assignments.context_id = ? AND assignments.context_type = 'Course')", course)
}
workflow do
state :submitted do
event :grade_it, :transitions_to => :graded
end
state :unsubmitted
state :pending_review
state :graded
end
# see #needs_grading?
def self.needs_grading_conditions
conditions = <<-SQL
@ -790,17 +812,6 @@ class Submission < ActiveRecord::Base
connection.after_transaction_commit { Auditors::GradeChange.record(self) }
end
include Workflow
workflow do
state :submitted do
event :grade_it, :transitions_to => :graded
end
state :unsubmitted
state :pending_review
state :graded
end
scope :with_assignment, -> { joins(:assignment).where("assignments.workflow_state <> 'deleted'")}
scope :graded, -> { where("submissions.grade IS NOT NULL") }

View File

@ -0,0 +1,30 @@
class CreateModeratedGradingProvisionalGrades < ActiveRecord::Migration
tag :predeploy
def up
create_table :moderated_grading_provisional_grades do |t|
t.string :grade
t.float :score
t.timestamp :graded_at
t.integer :position, null: false, limit: 8
t.references :scorer, null: false, limit: 8
t.references :submission, null: false, limit: 8
t.timestamps
end
add_index :moderated_grading_provisional_grades, :submission_id
add_index :moderated_grading_provisional_grades,
[:submission_id, :position],
unique: true,
name: :idx_mg_provisional_grades_unique_submission_position
add_foreign_key :moderated_grading_provisional_grades, :submissions
add_foreign_key :moderated_grading_provisional_grades,
:users,
column: :scorer_id
end
def down
drop_table :moderated_grading_provisional_grades
end
end

View File

@ -8,7 +8,10 @@ module Canvas
def self.active_record_foreign_key_check(name, type, options)
if name.to_s =~ /_id\z/ && type.to_s == 'integer' && options[:limit].to_i < 8
raise ArgumentError, "All foreign keys need to be 8-byte integers. #{name} looks like a foreign key to me, please add this option: `:limit => 8`"
raise ArgumentError, <<-EOS
All foreign keys need to be at least 8-byte integers. #{name}
looks like a foreign key, please add this option: `:limit => 8`
EOS
end
end
@ -51,12 +54,21 @@ module Canvas
# sanity check the file
unless configs.is_a?(Hash)
raise "Invalid config/cache_store.yml: Root is not a hash. See comments in config/cache_store.yml.example"
raise <<-EOS
Invalid config/cache_store.yml: Root is not a hash. See comments in
config/cache_store.yml.example
EOS
end
non_hashes = configs.keys.select { |k| !configs[k].is_a?(Hash) }
non_hashes.reject! { |k| configs[k].is_a?(String) && configs[configs[k]].is_a?(Hash) }
raise "Invalid config/cache_store.yml: Some keys are not hashes: #{non_hashes.join(', ')}. See comments in config/cache_store.yml.example" unless non_hashes.empty?
unless non_hashes.empty?
raise <<-EOS
Invalid config/cache_store.yml: Some keys are not hashes:
#{non_hashes.join(', ')}. See comments in
config/cache_store.yml.example
EOS
end
configs.each do |env, config|
if config.is_a?(String)

View File

@ -0,0 +1,9 @@
require 'active_support/concern'
module Canvas::GradeValidations
extend ActiveSupport::Concern
included do
validates_length_of :grade, :maximum => maximum_string_length, :allow_nil => true, :allow_blank => true
end
end

View File

@ -0,0 +1,30 @@
require 'spec_helper'
describe ModeratedGrading::ProvisionalGrade do
subject(:provisional_grade) do
submission.provisional_grades.new(grade: 'A', score: 100.0, position: 1, scorer: user).tap do |grade|
grade.scorer = user
end
end
let(:submission) { assignment.submissions.create!(user: user) }
let(:assignment) { course.assignments.create! }
let(:course) { Course.create! }
let(:user) { User.create! }
let(:now) { Time.zone.now }
it { is_expected.to be_valid }
it { is_expected.to validate_presence_of(:position) }
it { is_expected.to validate_presence_of(:scorer) }
it { is_expected.to validate_presence_of(:submission) }
it { is_expected.to validate_uniqueness_of(:position).scoped_to(:submission_id) }
describe '#graded_at when a grade changes' do
it { expect(provisional_grade.graded_at).to be_nil }
it 'updates the graded_at timestamp' do
Timecop.freeze(now) do
provisional_grade.update_attributes(grade: 'B')
expect(provisional_grade.graded_at).to eql now
end
end
end
end

View File

@ -815,19 +815,19 @@ describe Submission do
describe "graded?" do
it "is false before graded" do
s, _ = @assignment.find_or_create_submission(@user)
expect(s.graded?).to eql false
submission, _ = @assignment.find_or_create_submission(@user)
expect(submission).to_not be_graded
end
it "is true for graded assignments" do
s, _ = @assignment.grade_student(@user, grade: 1)
expect(s.graded?).to eql true
submission, _ = @assignment.grade_student(@user, grade: 1)
expect(submission).to be_graded
end
it "is also true for excused assignments" do
s, _ = @assignment.find_or_create_submission(@user)
s.excused = true
expect(s.graded?).to eql true
submission, _ = @assignment.find_or_create_submission(@user)
submission.excused = true
expect(submission).to be_graded
end
end
@ -835,25 +835,22 @@ describe Submission do
let(:submission) { Submission.new }
it "returns false when its not autograded" do
assignment = stub(:muted? => false)
@submission = Submission.new
expect(@submission.autograded?).to eq false
submission = Submission.new
expect(submission).to_not be_autograded
@submission.grader_id = Shard.global_id_for(@user.id)
expect(@submission.autograded?).to eq false
submission.grader_id = Shard.global_id_for(@user.id)
expect(submission).to_not be_autograded
end
it "returns true when its autograded" do
assignment = stub(:muted? => false)
@submission = Submission.new
@submission.grader_id = -1
expect(@submission.autograded?).to eq true
submission = Submission.new
submission.grader_id = -1
expect(submission).to be_autograded
end
end
describe "past_due" do
before :once do
u1 = @user
submission_spec_model
@submission1 = @submission