add functionality to get rules affecting an assignment
refs: CYOE-324 Test plan: Solely an internal API, though can be tested from the rails console: 1. Ensure cyoe config 2. For an assignment, call ConditionalRelease::Service.rule_triggered_by( a, user ) and you should receive back the rule if it exists (nil otherwise) Call ConditionalRelease::Service.rules_assigning( a, user ) and you should get back the rules that assign `a` Call ConditionalRelease::Service.is_trigger/(a, user) and you should get back a boolean if rule_triggered_by would return a rule Change-Id: I877775c2a7b53a0cd786734bed41ab7398fcdb67 Reviewed-on: https://gerrit.instructure.com/91091 Reviewed-by: Christian Prescott <cprescott@instructure.com> Tested-by: Jenkins QA-Review: Jahnavi Yetukuri <jyetukuri@instructure.com> Product-Review: Michael Brewer-Davis <mbd@instructure.com>
This commit is contained in:
parent
6b0333cae8
commit
02964503b9
|
@ -1,34 +0,0 @@
|
|||
#
|
||||
# Copyright (C) 2016 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 ConditionalRelease
|
||||
class ScoreRangeDefaultsController < ApplicationController
|
||||
def index
|
||||
get_context
|
||||
unless ConditionalRelease::Service.enabled_in_context?(@context)
|
||||
return render template: 'shared/errors/404_message', status: :not_found
|
||||
end
|
||||
|
||||
conditional_release_js_env
|
||||
|
||||
render locals: {
|
||||
cr_app_url: ConditionalRelease::Service.edit_rule_url,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -20,26 +20,31 @@ module ConditionalRelease
|
|||
class Service
|
||||
private_class_method :new
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
enabled: false, # required
|
||||
host: nil, # required
|
||||
protocol: nil, # defaults to Canvas
|
||||
DEFAULT_PATHS = {
|
||||
edit_rule_path: "ui/editor",
|
||||
stats_path: "stats",
|
||||
create_account_path: 'api/accounts',
|
||||
content_exports_path: 'api/content_exports',
|
||||
content_imports_path: 'api/content_imports',
|
||||
rules_path: 'api/rules?include[]=all&active=true',
|
||||
rules_summary_path: 'api/rules/summary',
|
||||
select_assignment_set_path: 'api/rules/select_assignment_set'
|
||||
}.freeze
|
||||
|
||||
def self.env_for(context, user = nil, session: nil, assignment: nil, domain: nil, real_user: nil)
|
||||
DEFAULT_CONFIG = {
|
||||
enabled: false, # required
|
||||
host: nil, # required
|
||||
protocol: nil, # defaults to Canvas
|
||||
}.merge(DEFAULT_PATHS).freeze
|
||||
|
||||
def self.env_for(context, user = nil, session: nil, assignment: nil, domain: nil,
|
||||
real_user: nil, include_rule: false)
|
||||
enabled = self.enabled_in_context?(context)
|
||||
env = {
|
||||
CONDITIONAL_RELEASE_SERVICE_ENABLED: enabled
|
||||
}
|
||||
if enabled && user
|
||||
env.merge!({
|
||||
new_env = {
|
||||
CONDITIONAL_RELEASE_ENV: {
|
||||
jwt: jwt_for(context, user, domain, session: session, real_user: real_user),
|
||||
assignment: assignment_attributes(assignment),
|
||||
|
@ -47,7 +52,9 @@ module ConditionalRelease
|
|||
stats_url: stats_url,
|
||||
locale: I18n.locale.to_s
|
||||
}
|
||||
})
|
||||
}
|
||||
new_env[:CONDITIONAL_RELEASE_ENV][:rule] = rule_triggered_by(assignment, user, session) if include_rule
|
||||
env.merge!(new_env)
|
||||
end
|
||||
env
|
||||
end
|
||||
|
@ -73,6 +80,12 @@ module ConditionalRelease
|
|||
data[:rules]
|
||||
end
|
||||
|
||||
def self.clear_active_rules_cache(course)
|
||||
return unless course.present?
|
||||
clear_cache_with_key(active_rules_cache_key(course))
|
||||
clear_cache_with_key(active_rules_reverse_cache_key(course))
|
||||
end
|
||||
|
||||
def self.clear_submissions_cache_for(user)
|
||||
return unless user.present?
|
||||
clear_cache_with_key(submissions_cache_key(user))
|
||||
|
@ -99,22 +112,6 @@ module ConditionalRelease
|
|||
!!(configured? && context.feature_enabled?(:conditional_release))
|
||||
end
|
||||
|
||||
def self.edit_rule_url
|
||||
build_url edit_rule_path
|
||||
end
|
||||
|
||||
def self.stats_url
|
||||
build_url stats_path
|
||||
end
|
||||
|
||||
def self.create_account_url
|
||||
build_url create_account_path
|
||||
end
|
||||
|
||||
def self.rules_summary_url
|
||||
build_url rules_summary_path
|
||||
end
|
||||
|
||||
def self.protocol
|
||||
config[:protocol] || HostUrl.protocol
|
||||
end
|
||||
|
@ -127,32 +124,11 @@ module ConditionalRelease
|
|||
config[:unique_id] || "conditional-release-service@instructure.auth"
|
||||
end
|
||||
|
||||
def self.edit_rule_path
|
||||
config[:edit_rule_path]
|
||||
end
|
||||
|
||||
def self.stats_path
|
||||
config[:stats_path]
|
||||
end
|
||||
|
||||
def self.create_account_path
|
||||
config[:create_account_path]
|
||||
end
|
||||
|
||||
def self.content_exports_url
|
||||
build_url(config[:content_exports_path])
|
||||
end
|
||||
|
||||
def self.content_imports_url
|
||||
build_url(config[:content_imports_path])
|
||||
end
|
||||
|
||||
def self.rules_summary_path
|
||||
config[:rules_summary_path]
|
||||
end
|
||||
|
||||
def self.select_assignment_set_url
|
||||
build_url(config[:select_assignment_set_path])
|
||||
DEFAULT_PATHS.each do |path_name, _path|
|
||||
method_name = path_name.to_s.sub(/_path$/, '_url')
|
||||
Service.define_singleton_method method_name do
|
||||
build_url config[path_name]
|
||||
end
|
||||
end
|
||||
|
||||
# Returns an http response-like hash { code: string, body: string or object }
|
||||
|
@ -176,6 +152,44 @@ module ConditionalRelease
|
|||
{ code: request.code, body: JSON.parse(request.body) }
|
||||
end
|
||||
|
||||
def self.triggers_mastery_paths?(assignment, current_user, session = nil)
|
||||
rule_triggered_by(assignment, current_user, session).present?
|
||||
end
|
||||
|
||||
def self.rule_triggered_by(assignment, current_user, session = nil)
|
||||
return unless assignment.present?
|
||||
return unless enabled_in_context?(assignment.context)
|
||||
|
||||
rules = active_rules(assignment.context, current_user, session)
|
||||
return nil unless rules
|
||||
|
||||
rules.find {|r| r['trigger_assignment'] == assignment.id.to_s}
|
||||
end
|
||||
|
||||
def self.rules_assigning(assignment, current_user, session = nil)
|
||||
reverse_lookup = Rails.cache.fetch(active_rules_reverse_cache_key(assignment.context)) do
|
||||
all_rules = active_rules(assignment.context, current_user, session)
|
||||
return nil unless all_rules
|
||||
|
||||
lookup = {}
|
||||
all_rules.each do |rule|
|
||||
(rule['scoring_ranges'] || []).each do |sr|
|
||||
(sr['assignment_sets'] || []).each do |as|
|
||||
(as['assignments'] || []).each do |a|
|
||||
if a['assignment_id'].present?
|
||||
lookup[a['assignment_id']] ||= []
|
||||
lookup[a['assignment_id']] << rule
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
lookup.each {|_id, rules| rules.uniq!}
|
||||
lookup
|
||||
end
|
||||
reverse_lookup[assignment.id.to_s]
|
||||
end
|
||||
|
||||
class << self
|
||||
private
|
||||
def config_file
|
||||
|
@ -312,6 +326,17 @@ module ConditionalRelease
|
|||
context_type updated_at context_code)
|
||||
end
|
||||
|
||||
def active_rules(course, current_user, session)
|
||||
return unless enabled_in_context?(course)
|
||||
return unless course.grants_any_right?(current_user, session, :read, :manage_assignments)
|
||||
|
||||
Rails.cache.fetch(active_rules_cache_key(course)) do
|
||||
headers = headers_for(course, current_user, domain_for(course), session)
|
||||
request = CanvasHttp.get(rules_url, headers)
|
||||
JSON.parse(request.body)
|
||||
end
|
||||
end
|
||||
|
||||
def rules_cache_key(context, student)
|
||||
['conditional_release_rules', context.global_id, student.global_id].cache_key
|
||||
end
|
||||
|
@ -320,6 +345,14 @@ module ConditionalRelease
|
|||
['conditional_release_submissions', student.global_id].cache_key
|
||||
end
|
||||
|
||||
def active_rules_cache_key(course)
|
||||
['conditional_release', 'active_rules', course.global_id].cache_key
|
||||
end
|
||||
|
||||
def active_rules_reverse_cache_key(course)
|
||||
['conditional_release', 'active_rules_reverse', course.global_id].cache_key
|
||||
end
|
||||
|
||||
def clear_cache_with_key(key)
|
||||
return if key.blank?
|
||||
Rails.cache.delete(key)
|
||||
|
|
|
@ -1,24 +1,30 @@
|
|||
class ConditionalReleaseObserver < ActiveRecord::Observer
|
||||
observe :submission
|
||||
observe :submission,
|
||||
:assignment
|
||||
|
||||
def after_update(submission)
|
||||
clear_caches_for submission
|
||||
def after_update(record)
|
||||
clear_caches_for record
|
||||
end
|
||||
|
||||
def after_create(submission)
|
||||
clear_caches_for submission
|
||||
def after_create(record)
|
||||
clear_caches_for record
|
||||
end
|
||||
|
||||
def after_save(submission)
|
||||
def after_save(record)
|
||||
end
|
||||
|
||||
def after_destroy(submission)
|
||||
clear_caches_for submission
|
||||
def after_destroy(record)
|
||||
clear_caches_for record
|
||||
end
|
||||
|
||||
private
|
||||
def clear_caches_for(submission)
|
||||
ConditionalRelease::Service.clear_submissions_cache_for(submission.student)
|
||||
ConditionalRelease::Service.clear_rules_cache_for(submission.context, submission.student)
|
||||
def clear_caches_for(record)
|
||||
case record
|
||||
when Submission
|
||||
ConditionalRelease::Service.clear_submissions_cache_for(record.student)
|
||||
ConditionalRelease::Service.clear_rules_cache_for(record.context, record.student)
|
||||
when Assignment
|
||||
ConditionalRelease::Service.clear_active_rules_cache(record.context)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,25 +7,3 @@ development:
|
|||
|
||||
# optional. default matches Canvas
|
||||
protocol: https
|
||||
|
||||
# optional. default is 'ui/edit_rule'
|
||||
edit_rule_path: 'ui/edit_rule'
|
||||
|
||||
# optional. default is 'api/accounts'
|
||||
create_account_path: 'api/accounts'
|
||||
|
||||
# optional. default is 'api/content_exports'
|
||||
content_exports_path: 'api/content_exports'
|
||||
|
||||
# optional. default is 'api/content_imports'
|
||||
content_imports_path: 'api/content_imports'
|
||||
|
||||
# optional. default if 'api/rules/summary'
|
||||
rules_summary_path: 'api/rules/summary'
|
||||
|
||||
# optional. email-like id for API user pseudonym.
|
||||
# default conditional-release-service@instructure.auth
|
||||
unique_id: conditional-release-service@instructure.auth
|
||||
|
||||
# optional. default is 'api/rules/select_assignment_set'
|
||||
select_assignment_set_path: 'api/rules/select_assignment_set'
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
#
|
||||
# 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/>.
|
||||
#
|
||||
|
||||
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
|
||||
|
||||
describe ConditionalRelease::ScoreRangeDefaultsController do
|
||||
it 'must have service enabled' do
|
||||
ConditionalRelease::Service.stubs(:enabled_in_context?).returns(false)
|
||||
course
|
||||
get :index, course_id: @course
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
|
||||
context 'enabled' do
|
||||
before do
|
||||
course
|
||||
ConditionalRelease::Service.stubs(:enabled_in_context?).returns(true)
|
||||
ConditionalRelease::Service.stubs(:env_for).returns({ FOO: :bar })
|
||||
end
|
||||
|
||||
it 'adds the appropriate JS environment vars' do
|
||||
get :index, course_id: @course
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(controller.js_env[:FOO]).to eq :bar
|
||||
end
|
||||
end
|
||||
end
|
|
@ -116,6 +116,9 @@ describe ConditionalRelease::Service do
|
|||
describe 'env_for' do
|
||||
before do
|
||||
enable_service
|
||||
stub_config({
|
||||
protocol: 'foo', host: 'bar', rules_path: 'rules'
|
||||
})
|
||||
course_with_student_logged_in(active_all: true)
|
||||
end
|
||||
|
||||
|
@ -166,6 +169,14 @@ describe ConditionalRelease::Service do
|
|||
cr_env = env[:CONDITIONAL_RELEASE_ENV]
|
||||
expect(cr_env[:assignment][:grading_scheme]).to be_nil
|
||||
end
|
||||
|
||||
it 'includes a relevant rule if include_rule is true' do
|
||||
assignment_model course: @course
|
||||
Service.stubs(:rule_triggered_by).returns(nil)
|
||||
env = Service.env_for(@course, @student, domain: 'foo.bar', assignment: @assignment, include_rule: true)
|
||||
cr_env = env[:CONDITIONAL_RELEASE_ENV]
|
||||
expect(cr_env).to have_key :rule
|
||||
end
|
||||
end
|
||||
|
||||
describe 'jwt_for' do
|
||||
|
@ -277,4 +288,94 @@ describe ConditionalRelease::Service do
|
|||
Service.select_mastery_path(@course, @student, @student, 100, 200, nil)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with active_rules' do
|
||||
before(:each) do
|
||||
Service.stubs(:enabled_in_context?).returns(true)
|
||||
Service.stubs(:jwt_for).returns(:jwt)
|
||||
end
|
||||
|
||||
before(:once) do
|
||||
course_with_teacher
|
||||
@a, @b, @c, @d = 4.times.map { assignment_model course: @course }
|
||||
end
|
||||
|
||||
let_once(:default_rules) do
|
||||
[
|
||||
{id: 1, trigger_assignment: @a.id.to_s, scoring_ranges: [{ assignment_sets: [
|
||||
{ assignments: [
|
||||
{ assignment_id: @b.id.to_s },
|
||||
{ assignment_id: @c.id.to_s }]}]}]},
|
||||
{id: 2, trigger_assignment: @b.id.to_s, scoring_ranges: [{ assignment_sets: [
|
||||
{ assignments: [
|
||||
{ assignment_id: @c.id.to_s }]}]}]},
|
||||
{id: 3, trigger_assignment: @c.id.to_s}
|
||||
].as_json
|
||||
end
|
||||
|
||||
describe 'rule_triggered_by' do
|
||||
def cache_active_rules(rules = default_rules)
|
||||
Rails.cache.write ['conditional_release', 'active_rules', @course.global_id].cache_key, rules
|
||||
end
|
||||
|
||||
it 'caches the response of any http call' do
|
||||
enable_cache do
|
||||
CanvasHttp.expects(:get).once.returns stub({ body: default_rules.to_json })
|
||||
Service.rule_triggered_by(@a, @teacher, nil)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with cached rules' do
|
||||
it 'returns a matching rule' do
|
||||
enable_cache do
|
||||
cache_active_rules
|
||||
expect(Service.rule_triggered_by(@a, @teacher, nil)['id']).to eq 1
|
||||
expect(Service.rule_triggered_by(@c, @teacher, nil)['id']).to eq 3
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns nil if no rules are matching' do
|
||||
enable_cache do
|
||||
cache_active_rules
|
||||
expect(Service.rule_triggered_by(@d, @teacher, nil)).to be nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns nil without making request if no assignment is provided' do
|
||||
CanvasHttp.stubs(:get).raises 'should not generate request'
|
||||
Service.rule_triggered_by(nil, @teacher, nil)
|
||||
end
|
||||
|
||||
it 'returns nil without making request if service is not enabled' do
|
||||
Service.stubs(:enabled_in_context?).returns(false)
|
||||
CanvasHttp.stubs(:get).raises 'should not generate request'
|
||||
Service.rule_triggered_by(@a, @teacher, nil)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'rules_assigning' do
|
||||
before(:each) do
|
||||
Service.stubs(:active_rules).returns(default_rules)
|
||||
end
|
||||
|
||||
it 'caches the calculation of the reverse index' do
|
||||
enable_cache do
|
||||
Service.rules_assigning(@a, @teacher, nil)
|
||||
Service.stubs(:active_rules).raises 'should not refetch rules'
|
||||
Service.rules_assigning(@b, @teacher, nil)
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns all rules which matched assignments' do
|
||||
expect(Service.rules_assigning(@b, @teacher, nil).map{|r| r['id']}).to eq [1]
|
||||
expect(Service.rules_assigning(@c, @teacher, nil).map{|r| r['id']}).to eq [1, 2]
|
||||
end
|
||||
|
||||
it 'returns nil if no rules matched assignments' do
|
||||
expect(Service.rules_assigning(@a, @teacher, nil)).to eq nil
|
||||
expect(Service.rules_assigning(@d, @teacher, nil)).to eq nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -19,16 +19,16 @@
|
|||
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
||||
|
||||
describe ConditionalReleaseObserver do
|
||||
before :once do
|
||||
course.offer!
|
||||
|
||||
@module = @course.context_modules.create!(:name => "cyoe module")
|
||||
@assignment = @course.assignments.create!(:name => "cyoe asgn", :submission_types => ["online_text_entry"], :points_possible => 100)
|
||||
@assignment.publish! if @assignment.unpublished?
|
||||
@assignment_tag = @module.add_item(:id => @assignment.id, :type => 'assignment')
|
||||
end
|
||||
|
||||
describe "submission" do
|
||||
before :once do
|
||||
course.offer!
|
||||
|
||||
@module = @course.context_modules.create!(:name => "cyoe module")
|
||||
@assignment = @course.assignments.create!(:name => "cyoe asgn", :submission_types => ["online_text_entry"], :points_possible => 100)
|
||||
@assignment.publish! if @assignment.unpublished?
|
||||
@assignment_tag = @module.add_item(:id => @assignment.id, :type => 'assignment')
|
||||
end
|
||||
|
||||
it "clears cache on create" do
|
||||
ConditionalRelease::Service.expects(:clear_submissions_cache_for).at_least(1)
|
||||
ConditionalRelease::Service.expects(:clear_rules_cache_for).at_least(1)
|
||||
|
@ -50,4 +50,22 @@ describe ConditionalReleaseObserver do
|
|||
@submission.destroy
|
||||
end
|
||||
end
|
||||
|
||||
describe "assignment" do
|
||||
it "clears cache on create" do
|
||||
ConditionalRelease::Service.expects(:clear_active_rules_cache).at_least(1)
|
||||
assignment_model
|
||||
end
|
||||
|
||||
it "clears cache on update" do
|
||||
ConditionalRelease::Service.expects(:clear_active_rules_cache).at_least(1)
|
||||
@assignment.name = "different name"
|
||||
@assignment.save!
|
||||
end
|
||||
|
||||
it "clears cache on delete" do
|
||||
ConditionalRelease::Service.expects(:clear_active_rules_cache).at_least(1)
|
||||
@assignment.destroy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue