GraphQL for outcome proficiencies

closes OUT-3836

flag=account_level_mastery_scales

test plan:
  - log in as an admin: canvas.docker
  - load GraphQL UI: canvas.docker/graphiql
  - test the folowing actions:
    mutations:
    * createOutcomeProficiency
    * updateOutcomeProficiency
    * deleteOutcomeProficiency

Change-Id: Id495c25f6cb8c8b30a60c181086982b218e2ab65
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/244575
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Michael Brewer-Davis <mbd@instructure.com>
Reviewed-by: Pat Renner <prenner@instructure.com>
QA-Review: Brian Watson <bwatson@instructure.com>
Product-Review: Augusto Callejas <acallejas@instructure.com>
This commit is contained in:
Augusto Callejas 2020-08-06 17:32:20 -10:00
parent bee9e84046
commit ff3ae90fce
20 changed files with 612 additions and 100 deletions

View File

@ -141,16 +141,7 @@ class OutcomeProficiencyApiController < ApplicationController
private
def update_ratings(proficiency, context = nil)
# update existing ratings & create any new ratings
proficiency_params['ratings'].each_with_index do |val, idx|
if idx <= proficiency.outcome_proficiency_ratings.count - 1
proficiency.outcome_proficiency_ratings[idx].assign_attributes(val.to_hash.symbolize_keys)
else
proficiency.outcome_proficiency_ratings.build(val)
end
end
# delete unused ratings
proficiency.outcome_proficiency_ratings[proficiency_params['ratings'].length..-1].each(&:mark_for_destruction)
proficiency.replace_ratings(proficiency_params['ratings'])
proficiency.context = context if context
proficiency.workflow_state = 'active'
proficiency.save!

View File

@ -0,0 +1,30 @@
#
# 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/>.
#
class Mutations::CreateOutcomeProficiency < Mutations::OutcomeProficiencyBase
graphql_name "CreateOutcomeProficiency"
# input arguments
argument :context_type, String, required: true
argument :context_id, ID, required: true
argument :proficiency_ratings, [Mutations::OutcomeProficiencyRatingCreate], required: true
def resolve(input:)
upsert(input)
end
end

View File

@ -30,7 +30,8 @@ class Mutations::DeleteOutcomeCalculationMethod < Mutations::BaseMutation
end
def resolve(input:)
record = OutcomeCalculationMethod.active.find_by(id: input[:id])
record_id = GraphQLHelpers.parse_relay_or_legacy_id(input[:id], "OutcomeCalculationMethod")
record = OutcomeCalculationMethod.active.find_by(id: record_id)
raise GraphQL::ExecutionError, "Unable to find OutcomeCalculationMethod" if record.nil?
raise GraphQL::ExecutionError, "insufficient permission" unless record.context.grants_right? current_user, :manage_outcomes
context[:deleted_models][:outcome_calculation_method] = record

View File

@ -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/>.
#
class Mutations::DeleteOutcomeProficiency < Mutations::BaseMutation
graphql_name "DeleteOutcomeProficiency"
# input arguments
argument :id, ID, required: true
# the return data if the delete is successful
field :outcome_proficiency_id, ID, null: false
def self.outcome_proficiency_id_log_entry(_entry, context)
context[:deleted_models][:outcome_proficiency].context
end
def resolve(input:)
record_id = GraphQLHelpers.parse_relay_or_legacy_id(input[:id], "OutcomeProficiency")
record = OutcomeProficiency.active.find_by(id: record_id)
raise GraphQL::ExecutionError, "Unable to find OutcomeProficiency" if record.nil?
raise GraphQL::ExecutionError, "insufficient permission" unless record.context.grants_right? current_user, :manage_outcomes
context[:deleted_models][:outcome_proficiency] = record
record.destroy
{outcome_proficiency_id: record.id}
end
end

View File

@ -0,0 +1,84 @@
#
# 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/>.
#
class Mutations::OutcomeProficiencyRatingCreate < GraphQL::Schema::InputObject
argument :color, String, required: true
argument :description, String, required: true
argument :mastery, Boolean, required: true
argument :points, Float, required: true
end
class Mutations::OutcomeProficiencyBase < Mutations::BaseMutation
# the return data if the create/update is successful
field :outcome_proficiency, Types::OutcomeProficiencyType, null: true
def self.outcome_proficiency_log_entry(outcome_proficiency, _ctx)
outcome_proficiency.context
end
protected
def attrs(input)
{
outcome_proficiency_ratings: input[:proficiency_ratings].map do |rating|
OutcomeProficiencyRating.new(**rating)
end
}
end
def context_taken?(record)
error = record.errors.first
error && error[0] == :context_id && error[1] == "has already been taken"
end
def fetch_context(input)
return if input[:context_type].blank?
context = begin
context_type = Object.const_get(input[:context_type])
context_type.find_by(id: input[:context_id])
rescue
raise GraphQL::ExecutionError, "invalid context type"
end
raise GraphQL::ExecutionError, "context not found" if context.nil?
check_permission(context)
context
end
def check_permission(context)
raise GraphQL::ExecutionError, "insufficient permission" unless context.grants_right? current_user, :manage_outcomes
end
def upsert(input, existing_record = nil)
context = fetch_context(input)
record = existing_record || OutcomeProficiency.find_by(context: context)
if record
record.assign_attributes(workflow_state: 'active')
record.replace_ratings(input[:proficiency_ratings])
record.assign_attributes(context: context) unless context.nil?
else
record = OutcomeProficiency.new(context: context, **attrs(input.to_h))
end
if record.save
{outcome_proficiency: record}
elsif existing_record.nil? && context_taken?(record)
upsert(input)
else
errors_for(record)
end
end
end

View File

@ -21,13 +21,12 @@ class Mutations::UpdateOutcomeCalculationMethod < Mutations::OutcomeCalculationM
# input arguments
argument :id, ID, required: true
argument :context_type, String, required: false
argument :context_id, ID, required: false
argument :calculation_method, String, required: false
argument :calculation_int, Integer, required: false
def resolve(input:)
record = OutcomeCalculationMethod.find_by(id: input[:id])
record_id = GraphQLHelpers.parse_relay_or_legacy_id(input[:id], "OutcomeCalculationMethod")
record = OutcomeCalculationMethod.find_by(id: record_id)
raise GraphQL::ExecutionError, "Unable to find OutcomeCalculationMethod" if record.nil?
check_permission(record.context)
upsert(input, record)

View File

@ -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/>.
#
class Mutations::UpdateOutcomeProficiency < Mutations::OutcomeProficiencyBase
graphql_name "UpdateOutcomeProficiency"
# input arguments
argument :id, ID, required: true
argument :proficiency_ratings, [Mutations::OutcomeProficiencyRatingCreate], required: false
def resolve(input:)
record_id = GraphQLHelpers.parse_relay_or_legacy_id(input[:id], "OutcomeProficiency")
record = OutcomeProficiency.find_by(id: record_id)
raise GraphQL::ExecutionError, "Unable to find OutcomeProficiency" if record.nil?
check_permission(record.context)
upsert(input, record)
end
end

View File

@ -43,10 +43,6 @@ module Types
field :outcome_calculation_method, OutcomeCalculationMethodType, null: true
def outcome_calculation_method
return nil unless account.grants_any_right?(
current_user, session,
:read
)
# This does a recursive lookup of parent accounts, not sure how we could
# batch load it in a reasonable way.
account.resolved_outcome_calculation_method

View File

@ -95,12 +95,23 @@ module Types
load_association(:account)
end
# TODO: restore when OUT-3878 is complete
# field :outcome_proficiency, OutcomeProficiencyType, null: true
# def outcome_proficiency
# # This does a recursive lookup of parent accounts, not sure how we could
# # batch load it in a reasonable way.
# course.resolved_outcome_proficiency
# end
# field :proficiency_ratings_connection, ProficiencyRatingType.connection_type, null: true
# def proficiency_ratings_connection
# # This does a recursive lookup of parent accounts, not sure how we could
# # batch load it in a reasonable way.
# outcome_proficiency&.outcome_proficiency_ratings
# end
field :outcome_calculation_method, OutcomeCalculationMethodType, null: true
def outcome_calculation_method
return nil unless course.grants_any_right?(
current_user, session,
:read
)
# This does a recursive lookup of parent accounts, not sure how we could
# batch load it in a reasonable way.
course.resolved_outcome_calculation_method

View File

@ -52,6 +52,9 @@ class Types::MutationType < Types::ApplicationObjectType
Sets the post policy for the course, with an option to override and delete
existing assignment post policies.
DESC
field :create_outcome_proficiency, mutation: Mutations::CreateOutcomeProficiency
field :update_outcome_proficiency, mutation: Mutations::UpdateOutcomeProficiency
field :delete_outcome_proficiency, mutation: Mutations::DeleteOutcomeProficiency
field :create_outcome_calculation_method, mutation: Mutations::CreateOutcomeCalculationMethod
field :update_outcome_calculation_method, mutation: Mutations::UpdateOutcomeCalculationMethod
field :delete_outcome_calculation_method, mutation: Mutations::DeleteOutcomeCalculationMethod

View File

@ -22,6 +22,8 @@ module Types
implements Interfaces::LegacyIDInterface
global_id_field :id
field :calculation_method, String, null: false
field :calculation_int, Integer, null: true
field :context_type, String, null: false

View File

@ -22,6 +22,8 @@ module Types
implements Interfaces::LegacyIDInterface
global_id_field :id
field :context_type, String, null: false
field :context_id, Integer, null: false

View File

@ -55,6 +55,19 @@ class OutcomeProficiency < ApplicationRecord
}
end
def replace_ratings(ratings)
# update existing ratings & create any new ratings
ratings.each_with_index do |val, idx|
if idx <= outcome_proficiency_ratings.count - 1
outcome_proficiency_ratings[idx].assign_attributes(val.to_hash.symbolize_keys)
else
outcome_proficiency_ratings.build(val)
end
end
# delete unused ratings
outcome_proficiency_ratings[ratings.length..-1].each(&:mark_for_destruction)
end
private
def next_ratings

View File

@ -0,0 +1,159 @@
#
# 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 "spec_helper"
require_relative "../graphql_spec_helper"
describe Mutations::CreateOutcomeProficiency do
before :once do
@account = Account.default
@course = @account.courses.create!
@admin = account_admin_user(account: @account)
@teacher = @course.enroll_teacher(User.create!, enrollment_state: 'active').user
end
def execute_with_input(create_input, user_executing: @admin)
mutation_command = <<~GQL
mutation {
createOutcomeProficiency(input: {
#{create_input}
}) {
outcomeProficiency {
_id
contextId
contextType
locked
proficiencyRatingsConnection(first: 10) {
nodes {
_id
color
description
mastery
points
}
}
}
errors {
attribute
message
}
}
}
GQL
context = {current_user: user_executing, request: ActionDispatch::TestRequest.create, session: {}}
CanvasSchema.execute(mutation_command, context: context)
end
let(:good_query) do
<<~QUERY
contextType: "Account"
contextId: #{@account.id}
proficiencyRatings: [
{
color: "FFFFFF"
description: "white"
mastery: true
points: 1.0
}
]
QUERY
end
it "creates an outcome proficiency" do
result = execute_with_input(good_query)
expect(result.dig('errors')).to be_nil
expect(result.dig('data', 'createOutcomeProficiency', 'errors')).to be_nil
result = result.dig('data', 'createOutcomeProficiency', 'outcomeProficiency')
record = OutcomeProficiency.find(result.dig('_id'))
expect(record.context).to eq @account
expect(result.dig('contextType')).to eq 'Account'
expect(result.dig('contextId')).to eq @account.id
expect(result.dig('locked')).to eq false
ratings = result.dig('proficiencyRatingsConnection', 'nodes')
expect(ratings.length).to eq 1
expect(ratings[0]['color']).to eq 'FFFFFF'
expect(ratings[0]['description']).to eq 'white'
expect(ratings[0]['mastery']).to eq true
expect(ratings[0]['points']).to eq 1.0
end
it "restores previously soft-deleted record" do
original_record = outcome_proficiency_model(@account)
original_record.destroy
result = execute_with_input(good_query)
result = result.dig('data', 'createOutcomeProficiency', 'outcomeProficiency')
record = OutcomeProficiency.find(result.dig('_id'))
expect(record.id).to eq original_record.id
end
context 'errors' do
def expect_error(result, message)
errors = result.dig('errors') || result.dig('data', 'createOutcomeProficiency', 'errors')
expect(errors).not_to be_nil
expect(errors[0]['message']).to match(/#{message}/)
end
it "requires manage_outcomes permission" do
result = execute_with_input(good_query, user_executing: @teacher)
expect_error(result, 'insufficient permission')
end
it "invalid context type" do
query = <<~QUERY
contextType: "Foobar"
contextId: 1
proficiencyRatings: []
QUERY
result = execute_with_input(query)
expect_error(result, 'invalid context type')
end
it "invalid context id" do
query = <<~QUERY
contextType: "Account"
contextId: -1
proficiencyRatings: []
QUERY
result = execute_with_input(query)
expect_error(result, 'context not found')
end
it "retries on concurrent create" do
original_record = outcome_proficiency_model(@account)
first = true
# Return nil on the first find_by call and then
# call the original method on subsequent calls
# to simulate a write occurring between the first
# call to find_by and save
allow(OutcomeProficiency).to receive(:find_by).and_wrap_original do |m, *args|
if first
first = false
nil
else
m.call(*args)
end
end
result = execute_with_input(good_query)
expect(result.dig('errors')).to be_nil
expect(result.dig('data', 'createOutcomeProficiency', 'errors')).to be_nil
result = result.dig('data', 'createOutcomeProficiency', 'outcomeProficiency')
record = OutcomeProficiency.find(result.dig('_id'))
expect(record.id).to eq original_record.id
end
end
end

View File

@ -47,7 +47,7 @@ describe Mutations::DeleteOutcomeCalculationMethod do
CanvasSchema.execute(mutation_command, context: context)
end
it "deletes an outcome calculation method" do
it "deletes an outcome calculation method with legacy id" do
query = <<~QUERY
id: #{original_record.id}
QUERY
@ -57,6 +57,16 @@ describe Mutations::DeleteOutcomeCalculationMethod do
expect(result.dig('data', 'deleteOutcomeCalculationMethod', 'outcomeCalculationMethodId')).to eq original_record.id.to_s
end
it "deletes an outcome calculation method with relay id" do
query = <<~QUERY
id: #{GraphQLHelpers.relay_or_legacy_id_prepare_func('OutcomeCalculationMethod').call(original_record.id.to_s)}
QUERY
result = execute_with_input(query)
expect(result.dig('errors')).to be_nil
expect(result.dig('data', 'deleteOutcomeCalculationMethod', 'errors')).to be_nil
expect(result.dig('data', 'deleteOutcomeCalculationMethod', 'outcomeCalculationMethodId')).to eq original_record.id.to_s
end
context 'errors' do
def expect_error(result, message)
errors = result.dig('errors') || result.dig('data', 'deleteOutcomeCalculationMethod', 'errors')
@ -74,7 +84,7 @@ describe Mutations::DeleteOutcomeCalculationMethod do
it "invalid id" do
query = <<~QUERY
id: -100
id: 0
QUERY
result = execute_with_input(query)
expect_error(result, 'Unable to find OutcomeCalculationMethod')

View File

@ -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 "spec_helper"
require_relative "../graphql_spec_helper"
describe Mutations::DeleteOutcomeProficiency do
before :once do
@account = Account.default
@course = @account.courses.create!
@admin = account_admin_user(account: @account)
@teacher = @course.enroll_teacher(User.create!, enrollment_state: 'active').user
end
let(:original_record) { outcome_proficiency_model(@account) }
def execute_with_input(delete_input, user_executing: @admin)
mutation_command = <<~GQL
mutation {
deleteOutcomeProficiency(input: {
#{delete_input}
}) {
outcomeProficiencyId
errors {
attribute
message
}
}
}
GQL
context = {current_user: user_executing, deleted_models: {}, request: ActionDispatch::TestRequest.create, session: {}}
CanvasSchema.execute(mutation_command, context: context)
end
it "deletes an outcome proficency with legacy id" do
query = <<~QUERY
id: #{original_record.id}
QUERY
result = execute_with_input(query)
expect(result.dig('errors')).to be_nil
expect(result.dig('data', 'deleteOutcomeProficiency', 'errors')).to be_nil
expect(result.dig('data', 'deleteOutcomeProficiency', 'outcomeProficiencyId')).to eq original_record.id.to_s
end
it "deletes an outcome proficency with relay id" do
query = <<~QUERY
id: #{GraphQLHelpers.relay_or_legacy_id_prepare_func('OutcomeProficiency').call(original_record.id.to_s)}
QUERY
result = execute_with_input(query)
expect(result.dig('errors')).to be_nil
expect(result.dig('data', 'deleteOutcomeProficiency', 'errors')).to be_nil
expect(result.dig('data', 'deleteOutcomeProficiency', 'outcomeProficiencyId')).to eq original_record.id.to_s
end
context 'errors' do
def expect_error(result, message)
errors = result.dig('errors') || result.dig('data', 'deleteOutcomeProficiency', 'errors')
expect(errors).not_to be_nil
expect(errors[0]['message']).to match(/#{message}/)
end
it "requires manage_outcomes permission" do
query = <<~QUERY
id: #{original_record.id}
QUERY
result = execute_with_input(query, user_executing: @teacher)
expect_error(result, 'insufficient permission')
end
it "invalid id" do
query = <<~QUERY
id: 0
QUERY
result = execute_with_input(query)
expect_error(result, 'Unable to find OutcomeProficiency')
end
it "does not delete a record twice" do
original_record.destroy
query = <<~QUERY
id: #{original_record.id}
QUERY
result = execute_with_input(query)
expect_error(result, 'Unable to find OutcomeProficiency')
end
end
end

View File

@ -79,8 +79,6 @@ describe Mutations::UpdateOutcomeCalculationMethod do
original_record.destroy
query = <<~QUERY
id: #{original_record.id}
contextType: "Course"
contextId: #{@course.id}
calculationMethod: "highest"
calculationInt: null
QUERY
@ -102,41 +100,15 @@ describe Mutations::UpdateOutcomeCalculationMethod do
it "requires manage_outcomes permission" do
query = <<~QUERY
id: #{original_record.id}
contextType: "Course"
contextId: #{@course.id}
calculationMethod: "highest"
QUERY
result = execute_with_input(query, user_executing: @student)
expect_error(result, 'insufficient permission')
end
it "invalid context type" do
query = <<~QUERY
id: #{original_record.id}
contextType: "Foobar"
contextId: 1
calculationMethod: "highest"
QUERY
result = execute_with_input(query)
expect_error(result, 'invalid context type')
end
it "invalid context id" do
query = <<~QUERY
id: #{original_record.id}
contextType: "Course"
contextId: -100
calculationMethod: "highest"
QUERY
result = execute_with_input(query)
expect_error(result, 'context not found')
end
it "invalid calculation method" do
query = <<~QUERY
id: #{original_record.id}
contextType: "Course"
contextId: #{@course.id}
calculationMethod: "foobaz"
QUERY
result = execute_with_input(query)
@ -146,45 +118,11 @@ describe Mutations::UpdateOutcomeCalculationMethod do
it "invalid calculation int" do
query = <<~QUERY
id: #{original_record.id}
contextType: "Course"
contextId: #{@course.id}
calculationMethod: "highest"
calculationInt: 100
QUERY
result = execute_with_input(query)
expect_error(result, 'invalid calculation_int for this calculation_method')
end
context "with another course" do
let(:other_course) { @account.courses.create! }
context "with teacher enrolled in course" do
before { other_course.enroll_teacher(@teacher, enrollment_state: 'active') }
it "fails to update context" do
new_record = outcome_calculation_method_model(other_course)
query = <<~QUERY
id: #{new_record.id}
contextType: "Course"
contextId: #{@course.id}
QUERY
result = execute_with_input(query)
expect_error(result, 'has already been taken')
end
end
context "with teacher not in course" do
it "fails to update context" do
new_record = outcome_calculation_method_model(other_course)
query = <<~QUERY
id: #{new_record.id}
contextType: "Course"
contextId: #{@course.id}
QUERY
result = execute_with_input(query)
expect_error(result, 'insufficient permission')
end
end
end
end
end

View File

@ -0,0 +1,111 @@
#
# 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 "spec_helper"
require_relative "../graphql_spec_helper"
describe Mutations::UpdateOutcomeProficiency do
before :once do
@account = Account.default
@course = @account.courses.create!
@admin = account_admin_user(account: @account)
@teacher = @course.enroll_teacher(User.create!, enrollment_state: 'active').user
end
let!(:original_record) { outcome_proficiency_model(@account) }
let(:good_query) do
<<~QUERY
id: #{original_record.id}
proficiencyRatings: [
{
color: "FFFFFF"
description: "white"
mastery: true
points: 1.0
}
]
QUERY
end
def execute_with_input(update_input, user_executing: @admin)
mutation_command = <<~GQL
mutation {
updateOutcomeProficiency(input: {
#{update_input}
}) {
outcomeProficiency {
_id
contextId
contextType
locked
proficiencyRatingsConnection(first: 10) {
nodes {
_id
color
description
mastery
points
}
}
}
errors {
attribute
message
}
}
}
GQL
context = {current_user: user_executing, request: ActionDispatch::TestRequest.create, session: {}}
CanvasSchema.execute(mutation_command, context: context)
end
it "updates an outcome proficiency" do
result = execute_with_input(good_query)
expect(result.dig('errors')).to be_nil
expect(result.dig('data', 'updateOutcomeProficiency', 'errors')).to be_nil
result = result.dig('data', 'updateOutcomeProficiency', 'outcomeProficiency')
ratings = result.dig('proficiencyRatingsConnection', 'nodes')
expect(ratings.length).to eq 1
expect(ratings[0]['color']).to eq 'FFFFFF'
expect(ratings[0]['description']).to eq 'white'
expect(ratings[0]['mastery']).to eq true
expect(ratings[0]['points']).to eq 1.0
end
it "restores previously soft-deleted record" do
original_record.destroy
result = execute_with_input(good_query)
result = result.dig('data', 'updateOutcomeProficiency', 'outcomeProficiency')
record = OutcomeProficiency.find(result.dig('_id'))
expect(record.id).to eq original_record.id
end
context 'errors' do
def expect_error(result, message)
errors = result.dig('errors') || result.dig('data', 'updateOutcomeCalculationMethod', 'errors')
expect(errors).not_to be_nil
expect(errors[0]['message']).to match(/#{message}/)
end
it "requires manage_outcomes permission" do
result = execute_with_input(good_query, user_executing: @student)
expect_error(result, 'insufficient permission')
end
end
end

View File

@ -58,13 +58,6 @@ describe Types::AccountType do
account_type.resolve('outcomeCalculationMethod { _id }')
).to eq account.outcome_calculation_method.id.to_s
end
it 'requires read permission' do
outcome_calculation_method_model(account)
expect(
account_type.resolve('outcomeCalculationMethod { _id }', current_user: @student)
).to be_nil
end
end
it 'works for courses' do

View File

@ -163,13 +163,6 @@ describe Types::CourseType do
course_type.resolve('outcomeCalculationMethod { _id }', current_user: @teacher)
).to eq course.account.outcome_calculation_method.id.to_s
end
it "requires read permission" do
outcome_calculation_method_model(course.account)
expect(
course_type.resolve('outcomeCalculationMethod { _id }', current_user: user_model)
).to be_nil
end
end
describe "sectionsConnection" do