port conditional release model logic and specs
test plan: * associations and other model logic should correspond to conditional release (aside from changes being made for the migration) * specs run closes #LA-1094 Change-Id: I2ae698e11a00b8dbda3dbd839d54cbc067190afc Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/239176 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Reviewed-by: Jeremy Stanley <jeremy@instructure.com> QA-Review: Jeremy Stanley <jeremy@instructure.com> Product-Review: Jeremy Stanley <jeremy@instructure.com>
This commit is contained in:
parent
bd3cf180b7
commit
6a1af4d748
|
@ -66,4 +66,6 @@ group :test do
|
|||
|
||||
gem 'parallel_tests'
|
||||
gem 'flakey_spec_catcher', require: false
|
||||
|
||||
gem 'factory_bot', '5.2.0', require: false
|
||||
end
|
||||
|
|
|
@ -17,5 +17,16 @@
|
|||
|
||||
module ConditionalRelease
|
||||
class AssignmentSet < ActiveRecord::Base
|
||||
include Deletion
|
||||
|
||||
belongs_to :scoring_range, required: true
|
||||
has_many :assignment_set_associations, -> { active.order(position: :asc) }, inverse_of: :assignment_set, dependent: :destroy
|
||||
accepts_nested_attributes_for :assignment_set_associations, allow_destroy: true
|
||||
acts_as_list :scope => {:scoring_range => self, :deleted_at => nil}
|
||||
has_one :rule, through: :scoring_range
|
||||
|
||||
def self.collect_associations(sets)
|
||||
sets.map(&:assignment_set_associations).flatten.sort_by(&:id).uniq(&:assignment_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -17,5 +17,35 @@
|
|||
|
||||
module ConditionalRelease
|
||||
class AssignmentSetAction < ActiveRecord::Base
|
||||
include Deletion
|
||||
|
||||
validates :action, inclusion: { in: %w(assign unassign) }
|
||||
validates :source, inclusion: { in: %w(grade_change select_assignment_set) }
|
||||
validates :student_id, presence: true
|
||||
validates :actor_id, presence: true
|
||||
validates :assignment_set_id, presence: true
|
||||
belongs_to :assignment_set
|
||||
|
||||
scope :latest, -> {
|
||||
select('DISTINCT ON (assignment_set_id, student_id) id').
|
||||
order('assignment_set_id, student_id, created_at DESC')
|
||||
}
|
||||
|
||||
def self.current_assignments(student_id_or_ids, sets=nil)
|
||||
conditions = { student_id: student_id_or_ids }
|
||||
conditions[:assignment_set] = sets if sets
|
||||
self.where(id: self.latest.where(conditions), action: 'assign')
|
||||
end
|
||||
|
||||
def self.create_from_sets(assigned, unassigned, opts={})
|
||||
opts[:actor_id] ||= opts[:student_id]
|
||||
|
||||
[['assign', assigned], ['unassign', unassigned]].each do |action, sets|
|
||||
sets = Array.wrap(sets)
|
||||
sets.each do |set|
|
||||
create! opts.merge(action: action, assignment_set: set, root_account_id: set.root_account_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -17,5 +17,26 @@
|
|||
|
||||
module ConditionalRelease
|
||||
class AssignmentSetAssociation < ActiveRecord::Base
|
||||
include Deletion
|
||||
|
||||
validates :assignment_id, presence: true
|
||||
validates :assignment_id, uniqueness: { scope: :assignment_set_id, conditions: -> { active } }
|
||||
validate :not_trigger
|
||||
|
||||
acts_as_list :scope => {:assignment_set => self, :deleted_at => nil}
|
||||
|
||||
belongs_to :assignment_set, required: true
|
||||
belongs_to :assignment
|
||||
has_one :scoring_range, through: :assignment_set
|
||||
has_one :rule, through: :assignment_set
|
||||
|
||||
delegate :course_id, to: :rule, allow_nil: true
|
||||
|
||||
private
|
||||
def not_trigger
|
||||
if rule && assignment_id == rule.trigger_assignment_id
|
||||
errors.add(:assignment_id, "can't match rule trigger_assignment_id")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
#
|
||||
# Copyright (C) 2020 - present 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/>.
|
||||
|
||||
# Basic bounds validations for classes including :upper_bound and :lower_bound attributes
|
||||
module ConditionalRelease
|
||||
module BoundsValidations
|
||||
extend ActiveSupport::Concern
|
||||
include ActiveModel::Validations
|
||||
|
||||
included do
|
||||
validates :lower_bound, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
|
||||
validates :upper_bound, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
|
||||
validate :lower_bound_less_than_upper_bound
|
||||
validate :bound_must_exist
|
||||
end
|
||||
|
||||
private
|
||||
def lower_bound_less_than_upper_bound
|
||||
if lower_bound.is_a?(Numeric) && upper_bound.is_a?(Numeric) && lower_bound > upper_bound
|
||||
errors.add(:base, 'lower bound must be less than upper bound')
|
||||
end
|
||||
end
|
||||
|
||||
def bound_must_exist
|
||||
errors.add(:base, 'one bound must exist') unless lower_bound || upper_bound
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,41 @@
|
|||
#
|
||||
# Copyright (C) 2020 - present 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/>.
|
||||
|
||||
# yes this isn't really any different than SoftDeletable but this preserves the db structure
|
||||
# from conditional_release and workflow_state kind of sucks anyway
|
||||
module ConditionalRelease
|
||||
module Deletion
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
scope :active, -> { where(:deleted_at => nil) }
|
||||
|
||||
alias_method :destroy_permanently!, :destroy
|
||||
def destroy
|
||||
return true if deleted_at.present?
|
||||
self.deleted_at = Time.now.utc
|
||||
run_callbacks(:destroy) { save! }
|
||||
end
|
||||
|
||||
def restore
|
||||
self.deleted_at = nil
|
||||
save!
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -17,5 +17,29 @@
|
|||
|
||||
module ConditionalRelease
|
||||
class Rule < ActiveRecord::Base
|
||||
include Deletion
|
||||
|
||||
validates :course_id, presence: true
|
||||
validates :trigger_assignment_id, presence: true
|
||||
belongs_to :trigger_assignment, :class_name => "Assignment"
|
||||
|
||||
belongs_to :course
|
||||
has_many :scoring_ranges, -> { active.order(position: :asc) }, inverse_of: :rule, dependent: :destroy
|
||||
has_many :assignment_sets, -> { active }, through: :scoring_ranges
|
||||
has_many :assignment_set_associations, -> { active.order(position: :asc) }, through: :scoring_ranges
|
||||
accepts_nested_attributes_for :scoring_ranges, allow_destroy: true
|
||||
|
||||
scope :with_assignments, -> do
|
||||
having_assignments = joins(all_includes).group(Arel.sql("conditional_release_rules.id"))
|
||||
preload(all_includes).where(id: having_assignments.pluck(:id))
|
||||
end
|
||||
|
||||
def self.all_includes
|
||||
{ scoring_ranges: { assignment_sets: :assignment_set_associations } }
|
||||
end
|
||||
|
||||
def assignment_sets_for_score(score)
|
||||
AssignmentSet.where(scoring_range: scoring_ranges.for_score(score)).preload(:assignment_set_associations)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -17,5 +17,22 @@
|
|||
|
||||
module ConditionalRelease
|
||||
class RuleTemplate < ActiveRecord::Base
|
||||
include Deletion
|
||||
|
||||
validates :name, presence: true
|
||||
validates :context_id, presence: true
|
||||
validates :context_type, inclusion: { in: %w(Account Course) }
|
||||
|
||||
belongs_to :context, polymorphic: [:course, :account]
|
||||
has_many :scoring_range_templates, -> { active }, inverse_of: :rule_template, dependent: :destroy
|
||||
accepts_nested_attributes_for :scoring_range_templates, allow_destroy: true
|
||||
|
||||
def build_rule
|
||||
rule = Rule.new root_account_id: root_account_id
|
||||
scoring_range_templates.each do |t|
|
||||
rule.scoring_ranges << t.build_scoring_range
|
||||
end
|
||||
rule
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -17,5 +17,33 @@
|
|||
|
||||
module ConditionalRelease
|
||||
class ScoringRange < ActiveRecord::Base
|
||||
include BoundsValidations
|
||||
include Deletion
|
||||
|
||||
belongs_to :rule, required: true
|
||||
has_many :assignment_sets, -> { active.order(position: :asc) }, inverse_of: :scoring_range, dependent: :destroy
|
||||
has_many :assignment_set_associations, -> { active.order(position: :asc) }, through: :assignment_sets
|
||||
accepts_nested_attributes_for :assignment_sets, allow_destroy: true
|
||||
|
||||
delegate :course_id, to: :rule
|
||||
|
||||
acts_as_list :scope => {:rule => self, :deleted_at => nil}
|
||||
|
||||
scope :for_score, lambda { |score|
|
||||
where(arel_table[:upper_bound].gt(score).or(arel_table[:upper_bound].eq(nil)).
|
||||
and(arel_table[:lower_bound].lteq(score).or(arel_table[:lower_bound].eq(nil))))
|
||||
}
|
||||
|
||||
def contains_score(score)
|
||||
return false unless score
|
||||
return false if lower_bound.present? && lower_bound > score
|
||||
return false if upper_bound.present? && upper_bound <= score
|
||||
true
|
||||
end
|
||||
|
||||
def assignment_sets
|
||||
super.build if super.empty?
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -17,5 +17,15 @@
|
|||
|
||||
module ConditionalRelease
|
||||
class ScoringRangeTemplate < ActiveRecord::Base
|
||||
include BoundsValidations
|
||||
include Deletion
|
||||
|
||||
belongs_to :rule_template, required: true
|
||||
|
||||
delegate :context_id, :context_type, to: :rule_template
|
||||
|
||||
def build_scoring_range
|
||||
ScoringRange.new upper_bound: upper_bound, lower_bound: lower_bound
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
#
|
||||
# Copyright (C) 2020 - present 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/>.
|
||||
|
||||
require_relative 'factory_bot_spec_helper'
|
||||
|
||||
RSpec.shared_examples 'a soft-deletable model' do
|
||||
it { is_expected.to have_db_column(:deleted_at) }
|
||||
|
||||
it 'adds a deleted_at where clause when requested' do
|
||||
expect(described_class.active.all.to_sql).to include('"deleted_at" IS NULL')
|
||||
end
|
||||
|
||||
it 'skips adding the deleted_at where clause normally' do
|
||||
# sorry - no default scopes
|
||||
expect(described_class.all.to_sql).not_to include('deleted_at')
|
||||
end
|
||||
|
||||
it 'soft deletes' do
|
||||
instance = create described_class.name.underscore.sub("conditional_release/", "").to_sym
|
||||
instance.destroy!
|
||||
expect(described_class.exists?(instance.id)).to be true
|
||||
expect(described_class.active.exists?(instance.id)).to be false
|
||||
end
|
||||
|
||||
it 'allows duplicates on unique attributes when one instance is soft deleted' do
|
||||
instance = create described_class.name.underscore.sub("conditional_release/", "").to_sym
|
||||
copy = instance.clone
|
||||
instance.destroy!
|
||||
expect { copy.save! }.to_not raise_error
|
||||
end
|
||||
end
|
|
@ -0,0 +1,22 @@
|
|||
#
|
||||
# Copyright (C) 2020 - present 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/>.
|
||||
#
|
||||
FactoryBot.define do
|
||||
factory :assignment do
|
||||
context :factory => :course
|
||||
end
|
||||
end
|
|
@ -0,0 +1,27 @@
|
|||
#
|
||||
# Copyright (C) 2020 - present 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/>.
|
||||
#
|
||||
FactoryBot.define do
|
||||
factory :assignment_set_action, class: ConditionalRelease::AssignmentSetAction do
|
||||
action { 'assign' }
|
||||
source { 'grade_change' }
|
||||
student_id { generate(:user_id) }
|
||||
actor_id { generate(:user_id) }
|
||||
assignment_set
|
||||
root_account_id { Account.default.id }
|
||||
end
|
||||
end
|
|
@ -0,0 +1,24 @@
|
|||
#
|
||||
# Copyright (C) 2020 - present 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/>.
|
||||
#
|
||||
FactoryBot.define do
|
||||
factory :assignment_set_association, class: ConditionalRelease::AssignmentSetAssociation do
|
||||
association :assignment_set
|
||||
assignment
|
||||
root_account_id { Account.default.id }
|
||||
end
|
||||
end
|
|
@ -0,0 +1,33 @@
|
|||
#
|
||||
# Copyright (C) 2020 - present 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/>.
|
||||
#
|
||||
FactoryBot.define do
|
||||
factory :assignment_set, class: ConditionalRelease::AssignmentSet do
|
||||
scoring_range
|
||||
root_account_id { Account.default.id }
|
||||
|
||||
factory :assignment_set_with_assignments do
|
||||
transient do
|
||||
assignment_count { 1 }
|
||||
end
|
||||
|
||||
after(:create) do |assignment_set, evaluator|
|
||||
create_list :assignment_set_association, evaluator.assignment_count, assignment_set: assignment_set
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,44 @@
|
|||
#
|
||||
# Copyright (C) 2020 - present 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/>.
|
||||
#
|
||||
FactoryBot.define do
|
||||
factory :rule_template, class: ConditionalRelease::RuleTemplate do
|
||||
sequence(:name) { |n| "rule template #{n}" }
|
||||
context :factory => :course
|
||||
root_account_id { Account.default.id }
|
||||
|
||||
factory :rule_template_with_scoring_ranges do
|
||||
transient do
|
||||
scoring_range_template_count { 2 }
|
||||
end
|
||||
|
||||
after(:create) do |template, evaluator|
|
||||
values = (0..evaluator.scoring_range_template_count).collect { |i| i * 11 }
|
||||
create_list(
|
||||
:scoring_range_template,
|
||||
evaluator.scoring_range_template_count,
|
||||
rule_template: template
|
||||
) do |range_template|
|
||||
# give ascending bounds
|
||||
range_template.lower_bound = values.shift
|
||||
range_template.upper_bound = values[0]
|
||||
range_template.save!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,50 @@
|
|||
#
|
||||
# Copyright (C) 2020 - present 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/>.
|
||||
#
|
||||
FactoryBot.define do
|
||||
sequence(:user_id) { |n| 10000 + n }
|
||||
|
||||
factory :rule, class: ConditionalRelease::Rule do
|
||||
root_account_id { Account.default.id }
|
||||
course :factory => :course
|
||||
trigger_assignment :factory => :assignment
|
||||
|
||||
factory :rule_with_scoring_ranges do
|
||||
transient do
|
||||
scoring_range_count { 2 }
|
||||
assignment_set_count { 1 }
|
||||
assignment_count { 2 }
|
||||
end
|
||||
|
||||
after(:create) do |rule, evaluator|
|
||||
values = (0..evaluator.scoring_range_count).collect { |i| i * 1.0 / evaluator.scoring_range_count }
|
||||
create_list(
|
||||
:scoring_range_with_assignments,
|
||||
evaluator.scoring_range_count,
|
||||
rule: rule,
|
||||
assignment_set_count: evaluator.assignment_set_count,
|
||||
assignment_count: evaluator.assignment_count
|
||||
) do |range|
|
||||
# give ascending bounds
|
||||
range.lower_bound = values.shift
|
||||
range.upper_bound = values[0]
|
||||
range.save!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,25 @@
|
|||
#
|
||||
# Copyright (C) 2020 - present 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/>.
|
||||
#
|
||||
FactoryBot.define do
|
||||
factory :scoring_range_template, class: ConditionalRelease::ScoringRangeTemplate do
|
||||
rule_template
|
||||
lower_bound { 65 }
|
||||
upper_bound { 95 }
|
||||
root_account_id { Account.default.id }
|
||||
end
|
||||
end
|
|
@ -0,0 +1,38 @@
|
|||
#
|
||||
# Copyright (C) 2020 - present 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/>.
|
||||
#
|
||||
FactoryBot.define do
|
||||
factory :scoring_range, aliases: [:scoring_range_with_bounds], class: ConditionalRelease::ScoringRange do
|
||||
rule
|
||||
lower_bound { 65 }
|
||||
upper_bound { 95 }
|
||||
root_account_id { Account.default.id }
|
||||
|
||||
factory :scoring_range_with_assignments do
|
||||
transient do
|
||||
assignment_set_count { 1 }
|
||||
assignment_count { 2 }
|
||||
end
|
||||
|
||||
after(:create) do |range, evaluator|
|
||||
create_list :assignment_set_with_assignments, evaluator.assignment_set_count,
|
||||
scoring_range: range,
|
||||
assignment_count: evaluator.assignment_count
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,22 @@
|
|||
#
|
||||
# Copyright (C) 2020 - present 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/>.
|
||||
#
|
||||
FactoryBot.define do
|
||||
factory :course do
|
||||
|
||||
end
|
||||
end
|
|
@ -0,0 +1,37 @@
|
|||
#
|
||||
# Copyright (C) 2020 - present 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/>.
|
||||
|
||||
require_relative 'spec_helper'
|
||||
|
||||
# NOTE/RANT: bringing this in should not be construed as an endorsement of FactoryBot
|
||||
# i honestly have no idea how to use it
|
||||
# but for whatever reason the conditional_release peeps liked it
|
||||
# and bringing it into canvas seemed like the easiest solution
|
||||
# since absorbing the code into the hivemind is going to be hard enough
|
||||
# without having to rewrite all their specs into a canvas-y way
|
||||
|
||||
require 'factory_bot'
|
||||
RSpec.configure do |config|
|
||||
config.include FactoryBot::Syntax::Methods
|
||||
|
||||
config.before(:suite) do
|
||||
unless FactoryBot.definition_file_paths == %w{spec/factory_bot} # already loaded
|
||||
FactoryBot.definition_file_paths = %w{spec/factory_bot}
|
||||
FactoryBot.find_definitions
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,125 @@
|
|||
#
|
||||
# Copyright (C) 2020 - present 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/>.
|
||||
#
|
||||
|
||||
require_relative '../../conditional_release_spec_helper'
|
||||
require_dependency "conditional_release/assignment_set_action"
|
||||
|
||||
module ConditionalRelease
|
||||
describe AssignmentSetAction, :type => :model do
|
||||
it_behaves_like 'a soft-deletable model'
|
||||
|
||||
it "must have student_id and actor_id" do
|
||||
set = create :assignment_set
|
||||
[:student_id, :actor_id].each do |attr|
|
||||
action = build :assignment_set_action, assignment_set: set
|
||||
action.send("#{attr}=", nil)
|
||||
expect(action.valid?).to be false
|
||||
action.send("#{attr}=", '')
|
||||
expect(action.valid?).to be false
|
||||
action.send("#{attr}=", 'person')
|
||||
expect(action.valid?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
it "must have action" do
|
||||
set = create :assignment_set
|
||||
action = build :assignment_set_action, assignment_set: set
|
||||
action.action= nil
|
||||
expect(action.valid?).to be false
|
||||
action.action= ''
|
||||
expect(action.valid?).to be false
|
||||
action.action= 'assign'
|
||||
expect(action.valid?).to be true
|
||||
end
|
||||
|
||||
it "must have source" do
|
||||
set = create :assignment_set
|
||||
action = build :assignment_set_action, assignment_set: set
|
||||
action.source= nil
|
||||
expect(action.valid?).to be false
|
||||
action.source= ''
|
||||
expect(action.valid?).to be false
|
||||
action.source= 'grade_change'
|
||||
expect(action.valid?).to be true
|
||||
end
|
||||
|
||||
it "must have an assignment_set_id" do
|
||||
set = create :assignment_set
|
||||
action = build :assignment_set_action
|
||||
action.assignment_set_id = nil
|
||||
expect(action.valid?).to be false
|
||||
action.assignment_set_id = set.id
|
||||
expect(action.valid?).to be true
|
||||
end
|
||||
|
||||
it "should be valid when assignment_set does not exist" do
|
||||
action = create :assignment_set_action
|
||||
set_id = action.assignment_set.id
|
||||
action.assignment_set.destroy!
|
||||
expect(action.reload.valid?).to be true
|
||||
expect(action.assignment_set_id).to eq set_id
|
||||
end
|
||||
|
||||
describe "self.latest" do
|
||||
it "should select only the most recent Action for each Set and user_id" do
|
||||
actions = []
|
||||
actions << create(:assignment_set_action, student_id: 2, assignment_set: create(:assignment_set))
|
||||
set = create :assignment_set
|
||||
actions << create(:assignment_set_action, student_id: 1, assignment_set: set)
|
||||
actions.concat Array.new(2) { create :assignment_set_action, student_id: 2, assignment_set: set }
|
||||
actions.last.update_attribute(:created_at, 1.hour.ago)
|
||||
expect(AssignmentSetAction.latest).to eq actions[0..2]
|
||||
end
|
||||
end
|
||||
|
||||
describe "self.current_assignments" do
|
||||
it "should select only actions that have not been unassigned" do
|
||||
set = create :assignment_set
|
||||
create(:assignment_set_action, action: 'assign', student_id: 'user', assignment_set: set, created_at: 1.hour.ago)
|
||||
create(:assignment_set_action, action: 'unassign', student_id: 'user', assignment_set: set)
|
||||
expect(AssignmentSetAction.current_assignments('user')).to eq []
|
||||
recent = create(:assignment_set_action, action: 'assign', student_id: 'user', assignment_set: set)
|
||||
expect(AssignmentSetAction.current_assignments('user')).to eq [recent]
|
||||
end
|
||||
|
||||
it "should select only actions for the specified sets" do
|
||||
actions = Array.new(3) { create(:assignment_set_action, student_id: 'user') }
|
||||
selected_sets = actions[1..2].map(&:assignment_set)
|
||||
expect(AssignmentSetAction.current_assignments('user', selected_sets).order(:id)).to eq actions[1..2]
|
||||
end
|
||||
end
|
||||
|
||||
describe "self.create_from_sets" do
|
||||
it 'should create records' do
|
||||
range = create :scoring_range_with_assignments, assignment_set_count: 4
|
||||
assigned = range.assignment_sets[0..1]
|
||||
unassigned = range.assignment_sets[2..3]
|
||||
audit_opts = { student_id: 'Will', actor_id: 'Sean', source: 'grade_change' }
|
||||
AssignmentSetAction.create_from_sets(assigned, unassigned, audit_opts)
|
||||
assigned.each do |s|
|
||||
set_action = AssignmentSetAction.find_by(audit_opts.merge(assignment_set_id: s.id, action: 'assign'))
|
||||
expect(set_action).to be_present
|
||||
end
|
||||
unassigned.each do |s|
|
||||
set_action = AssignmentSetAction.find_by(audit_opts.merge(assignment_set_id: s.id, action: 'unassign'))
|
||||
expect(set_action).to be_present
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,49 @@
|
|||
#
|
||||
# Copyright (C) 2020 - present 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/>.
|
||||
#
|
||||
|
||||
require_relative '../../conditional_release_spec_helper'
|
||||
require_dependency "conditional_release/assignment_set_association"
|
||||
|
||||
module ConditionalRelease
|
||||
describe AssignmentSetAssociation, :type => :model do
|
||||
it_behaves_like 'a soft-deletable model'
|
||||
|
||||
it 'must have an assignment_id' do
|
||||
assignment = build :assignment_set_association
|
||||
assignment.assignment_id = nil
|
||||
expect(assignment.valid?).to be false
|
||||
end
|
||||
|
||||
it 'enforces unique assignment_id in assignment_set' do
|
||||
asg = create :assignment_set_association
|
||||
dup = build :assignment_set_association, assignment_id: asg.assignment_id
|
||||
asg.assignment_set.assignment_set_associations << dup
|
||||
expect(dup.valid?).to eq false
|
||||
expect(dup.errors['assignment_id'].to_s).to match(/taken/)
|
||||
expect(asg.assignment_set.valid?).to eq false
|
||||
expect(asg.assignment_set.errors['assignment_set_associations.assignment_id'].to_s).to match(/taken/)
|
||||
end
|
||||
|
||||
it 'enforces not having the same assigment_id as the trigger_assignment of its rule' do
|
||||
asg = create :assignment_set_association
|
||||
asg.assignment_id = asg.rule.trigger_assignment_id
|
||||
expect(asg.valid?).to eq false
|
||||
expect(asg.errors['assignment_id'].to_s).to match(/trigger/)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,31 @@
|
|||
#
|
||||
# Copyright (C) 2020 - present 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/>.
|
||||
#
|
||||
|
||||
require_relative '../../conditional_release_spec_helper'
|
||||
require_dependency "conditional_release/assignment_set"
|
||||
|
||||
module ConditionalRelease
|
||||
describe AssignmentSet, :type => :model do
|
||||
it_behaves_like 'a soft-deletable model'
|
||||
|
||||
it 'must have a scoring_range_id' do
|
||||
assignment_set = build :assignment_set, scoring_range_id: nil
|
||||
expect(assignment_set.valid?).to eq false
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,73 @@
|
|||
#
|
||||
# Copyright (C) 2020 - present 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/>.
|
||||
#
|
||||
|
||||
require_relative '../../spec_helper'
|
||||
require_dependency "conditional_release/bounds_validations"
|
||||
|
||||
module ConditionalRelease
|
||||
describe BoundsValidations do
|
||||
class BoundsValidationsSpec
|
||||
include ActiveModel::Validations
|
||||
include BoundsValidations
|
||||
attr_accessor :upper_bound, :lower_bound
|
||||
end
|
||||
|
||||
before(:once) do
|
||||
@subject = BoundsValidationsSpec.new
|
||||
end
|
||||
|
||||
it 'has to have a bound' do
|
||||
@subject.upper_bound = @subject.lower_bound = nil
|
||||
expect(@subject.valid?).to be false
|
||||
expect(@subject.errors).to include(:base)
|
||||
end
|
||||
|
||||
it 'can have a single lower bound' do
|
||||
@subject.upper_bound = nil
|
||||
@subject.lower_bound = 2
|
||||
expect(@subject.valid?).to be true
|
||||
end
|
||||
|
||||
it 'can have a single upper bound' do
|
||||
@subject.upper_bound = 10
|
||||
@subject.lower_bound = nil
|
||||
expect(@subject.valid?).to be true
|
||||
end
|
||||
|
||||
it 'has to have numbers for bounds' do
|
||||
@subject.upper_bound = 'foo'
|
||||
@subject.lower_bound = { bar: :baz }
|
||||
expect(@subject.valid?).to be false
|
||||
expect(@subject.errors).to include(:upper_bound, :lower_bound)
|
||||
end
|
||||
|
||||
it 'has to have upper_bound > lower_bound' do
|
||||
@subject.upper_bound = 10
|
||||
@subject.lower_bound = 90
|
||||
expect(@subject.valid?).to be false
|
||||
expect(@subject.errors).to include(:base)
|
||||
end
|
||||
|
||||
it 'has to have non-negative bounds' do
|
||||
@subject.upper_bound = -1
|
||||
@subject.lower_bound = -3
|
||||
expect(@subject.valid?).to be false
|
||||
expect(@subject.errors).to include(:upper_bound, :lower_bound)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,102 @@
|
|||
#
|
||||
# Copyright (C) 2020 - present 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/>.
|
||||
#
|
||||
|
||||
require_relative '../../conditional_release_spec_helper'
|
||||
require_dependency "conditional_release/rule"
|
||||
|
||||
module ConditionalRelease
|
||||
describe Rule, :type => :model do
|
||||
it_behaves_like 'a soft-deletable model'
|
||||
|
||||
describe 'rule definition' do
|
||||
it 'must have a root account id' do
|
||||
rule = build :rule
|
||||
rule.root_account_id = nil
|
||||
expect(rule.valid?).to be false
|
||||
rule.root_account_id = ''
|
||||
expect(rule.valid?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe 'assignment_sets_for_score' do
|
||||
before :once do
|
||||
@rule = create :rule
|
||||
create :scoring_range_with_assignments,
|
||||
rule: @rule,
|
||||
lower_bound: 90,
|
||||
upper_bound: nil,
|
||||
assignment_set_count: 1
|
||||
create :scoring_range_with_assignments,
|
||||
rule: @rule,
|
||||
lower_bound: 70,
|
||||
upper_bound: 90,
|
||||
assignment_set_count: 1
|
||||
create :scoring_range_with_assignments,
|
||||
rule: @rule,
|
||||
lower_bound: 50,
|
||||
upper_bound: 70,
|
||||
assignment_set_count: 1
|
||||
end
|
||||
|
||||
it 'must apply all scoring ranges' do
|
||||
expect(@rule.assignment_sets_for_score(91).length).to eq(1)
|
||||
# create a range that crosses the ranges above
|
||||
create :scoring_range_with_assignments,
|
||||
rule: @rule,
|
||||
lower_bound: 80,
|
||||
upper_bound: 95,
|
||||
assignment_set_count: 2
|
||||
expect(@rule.assignment_sets_for_score(91).length).to eq(3)
|
||||
end
|
||||
|
||||
it 'must return [] if no assignments match' do
|
||||
expect(@rule.assignment_sets_for_score(10)).to eq([])
|
||||
end
|
||||
|
||||
it 'must return [] if no scoring ranges are defined' do
|
||||
rule = create :rule
|
||||
expect(rule.assignment_sets_for_score(10)).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with_assignments' do
|
||||
before do
|
||||
@rule1 = create :rule_with_scoring_ranges
|
||||
@rule1.scoring_ranges.last.assignment_sets.destroy_all
|
||||
@rule2 = create :rule_with_scoring_ranges, assignment_set_count: 0
|
||||
@rule3 = create :rule_with_scoring_ranges, assignment_count: 0
|
||||
@rule4 = create :rule_with_scoring_ranges,
|
||||
scoring_range_count: 1,
|
||||
assignment_set_count: 1,
|
||||
assignment_count: 1
|
||||
@rule5 = create :rule
|
||||
end
|
||||
|
||||
let(:rules) { Rule.with_assignments.to_a }
|
||||
|
||||
it 'only returns rules with assignments' do
|
||||
expect(rules).to match_array [@rule1, @rule4]
|
||||
end
|
||||
|
||||
it 'returns complete rules when assignments are present' do
|
||||
rule = rules.find{ |r| r.id == @rule1.id }
|
||||
expect(rule.scoring_ranges.length).to eq @rule1.scoring_ranges.length
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,94 @@
|
|||
#
|
||||
# Copyright (C) 2020 - present 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/>.
|
||||
#
|
||||
|
||||
require_relative '../../conditional_release_spec_helper'
|
||||
require_dependency "conditional_release/rule_template"
|
||||
|
||||
module ConditionalRelease
|
||||
describe RuleTemplate, :type => :model do
|
||||
it_behaves_like 'a soft-deletable model'
|
||||
|
||||
describe 'rule template definition' do
|
||||
before do
|
||||
@rule_template = build :rule_template
|
||||
end
|
||||
|
||||
it 'cannot have null context_type' do
|
||||
@rule_template.context_type = nil
|
||||
expect(@rule_template.valid?).to be false
|
||||
end
|
||||
|
||||
it 'must have a context' do
|
||||
[Account, Course].each do |valid_klass|
|
||||
@rule_template.context = valid_klass.create!
|
||||
expect(@rule_template.valid?).to be true
|
||||
end
|
||||
expect {
|
||||
@rule_template.context = User.create!
|
||||
}.to raise_error(ActiveRecord::AssociationTypeMismatch)
|
||||
end
|
||||
|
||||
it 'cannot have a null context id' do
|
||||
@rule_template.context_id = nil
|
||||
expect(@rule_template.valid?).to be false
|
||||
end
|
||||
|
||||
it 'cannot have a null name' do
|
||||
@rule_template.name = nil
|
||||
expect(@rule_template.valid?).to be false
|
||||
end
|
||||
|
||||
it 'cannot have a null root account id' do
|
||||
@rule_template.root_account_id = nil
|
||||
expect(@rule_template.valid?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe 'build_rule' do
|
||||
it 'has the same account id' do
|
||||
root_account = Account.create!
|
||||
template = create :rule_template, :root_account_id => root_account.id
|
||||
rule = template.build_rule
|
||||
expect(rule.root_account_id).to eq root_account.id
|
||||
end
|
||||
|
||||
it 'has the same number of ranges' do
|
||||
template = create :rule_template_with_scoring_ranges, scoring_range_template_count: 5
|
||||
rule = template.build_rule
|
||||
expect(rule.scoring_ranges.length).to eq 5
|
||||
end
|
||||
|
||||
it 'has the same values for ranges' do
|
||||
template = create :rule_template_with_scoring_ranges, scoring_range_template_count: 9
|
||||
rule = template.build_rule
|
||||
template.scoring_range_templates.each_with_index do |sr_template, i|
|
||||
range = rule.scoring_ranges[i]
|
||||
expect(range.upper_bound).to eq sr_template.upper_bound
|
||||
expect(range.lower_bound).to eq sr_template.lower_bound
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not save rules or ranges' do
|
||||
template = create :rule_template_with_scoring_ranges, scoring_range_template_count: 1
|
||||
rule = template.build_rule
|
||||
expect(rule.new_record?).to be true
|
||||
expect(rule.scoring_ranges.first.new_record?).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,157 @@
|
|||
#
|
||||
# Copyright (C) 2020 - present 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/>.
|
||||
#
|
||||
|
||||
require_relative '../../conditional_release_spec_helper'
|
||||
require_dependency "conditional_release/scoring_range"
|
||||
|
||||
module ConditionalRelease
|
||||
describe ScoringRange, :type => :model do
|
||||
it_behaves_like 'a soft-deletable model'
|
||||
|
||||
describe 'scoring range definition' do
|
||||
it 'must have at least one bound' do
|
||||
range = build :scoring_range
|
||||
range.lower_bound = range.upper_bound = nil
|
||||
expect(range.valid?).to be false
|
||||
end
|
||||
|
||||
it 'must have lower bound less than upper' do
|
||||
range = build :scoring_range
|
||||
range.lower_bound = 100
|
||||
range.upper_bound = 1
|
||||
expect(range.valid?).to be false
|
||||
end
|
||||
|
||||
it 'can have null bounds' do
|
||||
range = build :scoring_range
|
||||
range.lower_bound = nil
|
||||
range.upper_bound = 100
|
||||
expect(range.valid?).to be true
|
||||
range.lower_bound = 99
|
||||
range.upper_bound = nil
|
||||
expect(range.valid?).to be true
|
||||
end
|
||||
|
||||
it 'must have non-negative bounds' do
|
||||
range = build :scoring_range
|
||||
range.lower_bound = -10
|
||||
expect(range.valid?).to be false
|
||||
range = build :scoring_range
|
||||
range.upper_bound = -10
|
||||
expect(range.valid?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe 'for_score' do
|
||||
before do
|
||||
@rule = create :rule
|
||||
@range = create :scoring_range, rule: @rule
|
||||
create :assignment_set_association, scoring_range: @range
|
||||
end
|
||||
|
||||
it 'must return an empty relation when nothing matches' do
|
||||
expect(ScoringRange.for_score(-10000).count).to eq 0
|
||||
end
|
||||
|
||||
it 'must apply bounds when both assigned' do
|
||||
@range.upper_bound = 80
|
||||
@range.lower_bound = 40
|
||||
@range.save!
|
||||
expect(@rule.scoring_ranges.for_score(90).count).to eq 0
|
||||
expect(@rule.scoring_ranges.for_score(50).count).to eq 1
|
||||
expect(@rule.scoring_ranges.for_score(30).count).to eq 0
|
||||
end
|
||||
|
||||
it 'must apply upper bound as > score' do
|
||||
@range.upper_bound = 90
|
||||
@range.save!
|
||||
expect(@rule.scoring_ranges.for_score(90.001).count).to eq 0
|
||||
expect(@rule.scoring_ranges.for_score(90).count).to eq 0
|
||||
expect(@rule.scoring_ranges.for_score(89.999).count).to eq 1
|
||||
end
|
||||
|
||||
it 'must apply lower bound as <= score' do
|
||||
@range.lower_bound = 40
|
||||
@range.save!
|
||||
expect(@rule.scoring_ranges.for_score(40.001).count).to eq 1
|
||||
expect(@rule.scoring_ranges.for_score(40).count).to eq 1
|
||||
expect(@rule.scoring_ranges.for_score(39.999).count).to eq 0
|
||||
end
|
||||
|
||||
it 'must apply correctly when only upper bound' do
|
||||
@range.upper_bound = 20
|
||||
@range.lower_bound = nil
|
||||
@range.save!
|
||||
expect(@rule.scoring_ranges.for_score(-10).count).to eq 1
|
||||
expect(@rule.scoring_ranges.for_score(10).count).to eq 1
|
||||
expect(@rule.scoring_ranges.for_score(30).count).to eq 0
|
||||
end
|
||||
|
||||
it 'must apply correctly when only lower bound' do
|
||||
@range.lower_bound = 10
|
||||
@range.upper_bound = nil
|
||||
@range.save!
|
||||
expect(@rule.scoring_ranges.for_score(-10).count).to eq 0
|
||||
expect(@rule.scoring_ranges.for_score(20).count).to eq 1
|
||||
expect(@rule.scoring_ranges.for_score(1000).count).to eq 1
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe 'contains_score' do
|
||||
before do
|
||||
@rule = create :rule
|
||||
@range = create :scoring_range, rule: @rule
|
||||
create :assignment_set_association, scoring_range: @range
|
||||
end
|
||||
|
||||
it 'must properly evaluate a bound of 0' do
|
||||
@range.lower_bound = nil
|
||||
@range.upper_bound = 0
|
||||
@range.save!
|
||||
range2 = create :scoring_range, rule: @rule
|
||||
range2.lower_bound = 0
|
||||
range2.upper_bound = 1
|
||||
range2.save!
|
||||
expect(@rule.scoring_ranges.first.contains_score(0)).to be false
|
||||
expect(@rule.scoring_ranges.last.contains_score(0)).to be true
|
||||
end
|
||||
|
||||
it 'must properly evaluate bounds' do
|
||||
@range.lower_bound = 1
|
||||
@range.upper_bound = 2
|
||||
@range.save!
|
||||
expect(@rule.scoring_ranges.first.contains_score(2)).to be false
|
||||
expect(@rule.scoring_ranges.last.contains_score(1)).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe '#assignment_sets' do
|
||||
it 'builds a assignment_set if one does not exist' do
|
||||
range = create :scoring_range_with_assignments, assignment_set_count: 0
|
||||
expect(AssignmentSet.count).to eq 0
|
||||
expect(range.assignment_sets.length).to eq 1
|
||||
end
|
||||
|
||||
it 'returns existing assignment_sets' do
|
||||
range = create :scoring_range_with_assignments, assignment_set_count: 2
|
||||
expect(range.assignment_sets.length).to eq 2
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,78 @@
|
|||
#
|
||||
# Copyright (C) 2020 - present 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/>.
|
||||
#
|
||||
|
||||
require_relative '../../conditional_release_spec_helper'
|
||||
require_dependency "conditional_release/scoring_range_template"
|
||||
|
||||
module ConditionalRelease
|
||||
describe ScoringRangeTemplate, :type => :model do
|
||||
it_behaves_like 'a soft-deletable model'
|
||||
|
||||
describe 'scoring range definition' do
|
||||
before do
|
||||
@scoring_range_template = build :scoring_range_template
|
||||
end
|
||||
|
||||
it 'uses bounds validations' do
|
||||
@scoring_range_template.upper_bound = nil
|
||||
@scoring_range_template.lower_bound = nil
|
||||
expect(@scoring_range_template.valid?).to be false
|
||||
|
||||
@scoring_range_template.upper_bound = 10
|
||||
@scoring_range_template.lower_bound = 30
|
||||
expect(@scoring_range_template.valid?).to be false
|
||||
end
|
||||
|
||||
it 'must have an associated rule template' do
|
||||
@scoring_range_template.rule_template = nil
|
||||
expect(@scoring_range_template.valid?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe 'build_scoring_range' do
|
||||
it 'has the same bounds' do
|
||||
template = create :scoring_range_template
|
||||
range = template.build_scoring_range
|
||||
expect(range.upper_bound).to eq template.upper_bound
|
||||
expect(range.lower_bound).to eq template.lower_bound
|
||||
end
|
||||
|
||||
it 'works with null bounds' do
|
||||
template = create :scoring_range_template, upper_bound: nil
|
||||
range = template.build_scoring_range
|
||||
expect(range.upper_bound).to be nil
|
||||
|
||||
template = create :scoring_range_template, lower_bound: nil
|
||||
range = template.build_scoring_range
|
||||
expect(range.lower_bound).to be nil
|
||||
end
|
||||
|
||||
it 'does not assign assignments' do
|
||||
template = create :scoring_range_template
|
||||
range = template.build_scoring_range
|
||||
expect(range.assignment_set_associations.count).to be 0
|
||||
end
|
||||
|
||||
it 'does not save to database' do
|
||||
template = create :scoring_range_template
|
||||
range = template.build_scoring_range
|
||||
expect(range.new_record?).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue