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:
parent
bee9e84046
commit
ff3ae90fce
|
@ -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!
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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')
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue