Add setOverrideScore mutation
closes GRADE-84 Test plan: Note that you can also test this via the /graphiql endpoint on local Canvas installation once you've logged in (you can use the query string from the heredoc below). - Identify a Score object you want to update and its corresponding Enrollment object - In a Rails console: > mutation_str = <<~STR mutation { setOverrideScore(input: { enrollmentId: <the enrollment ID> gradingPeriodId: <ID of associated grading period if exists> overrideScore: 123.4567 <or whatever> }) { grades { gradingPeriod { id _id } overrideScore } errors { attribute message } } } STR > user = User.find(<a user ID with appropriate permissions>) > CanvasSchema.execute(mutation_str, context: {current_user: user}) - You should get back a response with no errors and a score-like object (technically a Grades type in GraphQL) - The properties of the returned object should include the override score you set and, if you updated a score belonging to a grading period, the GraphQL and Rails ID fields for the grading period (in the gradingPeriod attribute) - Check the Score object with the matching enrollment/grading period to make sure your call did indeed update it - Also try the following: - Excluding the gradingPeriodId field from the input hash (i.e., update the score associated with an enrollment and not belonging to a grading period) - Passing "null" (without the quotes) for the overrideScore; this should clear an existing value - The following should return errors and no score data: - Passing in malformed values for overrideScore or the ID fields - Passing in an ID for a non-existent enrollment/grading period - Running the CanvasSchema.execute command with current_user set to a user who does not have permissions to modify the score in question (e.g., a student, or generally someone who's not a teacher in that course) Change-Id: I2b5c6a80842a6febdb1e398a41aa71ea37cdb064 Reviewed-on: https://gerrit.instructure.com/172472 Tested-by: Jenkins Reviewed-by: Cameron Matheson <cameron@instructure.com> Reviewed-by: Gary Mei <gmei@instructure.com> QA-Review: Adrian Packel <apackel@instructure.com> Product-Review: Keith Garner <kgarner@instructure.com>
This commit is contained in:
parent
284d565b37
commit
7fa746b077
|
@ -0,0 +1,48 @@
|
|||
#
|
||||
# Copyright (C) 2018 - 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::SetOverrideScore < Mutations::BaseMutation
|
||||
graphql_name "SetOverrideScore"
|
||||
|
||||
argument :enrollment_id, ID, required: true, prepare: GraphQLHelpers.relay_or_legacy_id_prepare_func("Enrollment")
|
||||
argument :grading_period_id, ID, required: false, prepare: GraphQLHelpers.relay_or_legacy_id_prepare_func("GradingPeriod")
|
||||
argument :override_score, Float, required: false
|
||||
|
||||
field :grades, Types::GradesType, null: true
|
||||
|
||||
def resolve(input:)
|
||||
enrollment_id = input[:enrollment_id]
|
||||
grading_period_id = input[:grading_period_id]
|
||||
|
||||
enrollment = Enrollment.find(enrollment_id)
|
||||
score_params = grading_period_id.present? ? {grading_period_id: grading_period_id} : nil
|
||||
score = enrollment.find_score(score_params)
|
||||
raise ActiveRecord::RecordNotFound if score.blank?
|
||||
|
||||
if authorized_action?(score.course, :manage_grades)
|
||||
score.override_score = input[:override_score]
|
||||
if score.save
|
||||
{grades: score}
|
||||
else
|
||||
errors_for(score)
|
||||
end
|
||||
end
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
raise GraphQL::ExecutionError, "not found"
|
||||
end
|
||||
end
|
|
@ -23,15 +23,28 @@ module Types
|
|||
description "Contains grade information for a course or grading period"
|
||||
|
||||
field :current_score, Float, <<~DESC, null: true
|
||||
The current score includes all graded assignments
|
||||
The current score includes all graded assignments, excluding muted submissions.
|
||||
DESC
|
||||
field :unposted_current_score, Float, <<~DESC, null: true
|
||||
The current score includes all graded assignments, including muted submissions.
|
||||
DESC
|
||||
field :current_grade, String, null: true
|
||||
field :unposted_current_grade, String, null: true
|
||||
|
||||
field :final_score, Float, <<~DESC, null: true
|
||||
The final score includes all assignments
|
||||
(ungraded assignments are counted as 0 points)
|
||||
The final score includes all assignments, excluding muted submissions
|
||||
(ungraded assignments are counted as 0 points).
|
||||
DESC
|
||||
field :unposted_final_score, Float, <<~DESC, null: true
|
||||
The final score includes all assignments, including muted submissions
|
||||
(ungraded assignments are counted as 0 points).
|
||||
DESC
|
||||
field :final_grade, String, null: true
|
||||
field :unposted_final_grade, String, null: true
|
||||
|
||||
field :override_score, Float, <<~DESC, null: true
|
||||
The override score. Supersedes the computed final score if set.
|
||||
DESC
|
||||
|
||||
field :grading_period, GradingPeriodType, null: true
|
||||
def grading_period
|
||||
|
|
|
@ -20,4 +20,8 @@ class Types::MutationType < Types::ApplicationObjectType
|
|||
graphql_name "Mutation"
|
||||
|
||||
field :create_group_in_set, mutation: Mutations::CreateGroupInSet
|
||||
field :set_override_score, <<~DESC, mutation: Mutations::SetOverrideScore
|
||||
Sets the overridden final score for the associated enrollment, optionally limited to a specific
|
||||
grading period. This will supersede the computed final score/grade if present.
|
||||
DESC
|
||||
end
|
||||
|
|
|
@ -26,8 +26,9 @@ class Score < ActiveRecord::Base
|
|||
has_one :score_metadata
|
||||
|
||||
validates :enrollment, presence: true
|
||||
validates :current_score, :unposted_current_score, :final_score,
|
||||
:unposted_final_score, numericality: true, allow_nil: true
|
||||
validates :current_score, :unposted_current_score,
|
||||
:final_score, :unposted_final_score, :override_score,
|
||||
numericality: true, allow_nil: true
|
||||
|
||||
validate :scorable_association_check
|
||||
|
||||
|
|
|
@ -495,14 +495,26 @@ enum EnrollmentWorkflowState {
|
|||
type Grades {
|
||||
currentGrade: String
|
||||
|
||||
# The current score includes all graded assignments
|
||||
# The current score includes all graded assignments, excluding muted submissions.
|
||||
currentScore: Float
|
||||
finalGrade: String
|
||||
|
||||
# The final score includes all assignments
|
||||
# (ungraded assignments are counted as 0 points)
|
||||
# The final score includes all assignments, excluding muted submissions
|
||||
# (ungraded assignments are counted as 0 points).
|
||||
finalScore: Float
|
||||
gradingPeriod: GradingPeriod
|
||||
|
||||
# The override score. Supersedes the computed final score if set.
|
||||
overrideScore: Float
|
||||
unpostedCurrentGrade: String
|
||||
|
||||
# The current score includes all graded assignments, including muted submissions.
|
||||
unpostedCurrentScore: Float
|
||||
unpostedFinalGrade: String
|
||||
|
||||
# The final score includes all assignments, including muted submissions
|
||||
# (ungraded assignments are counted as 0 points).
|
||||
unpostedFinalScore: Float
|
||||
}
|
||||
|
||||
type GradingPeriod implements Node & Timestamped {
|
||||
|
@ -699,6 +711,10 @@ type Module implements Node & Timestamped {
|
|||
|
||||
type Mutation {
|
||||
createGroupInSet(input: CreateGroupInSetInput!): CreateGroupInSetPayload
|
||||
|
||||
# Sets the overridden final score for the associated enrollment, optionally limited to a specific
|
||||
# grading period. This will supersede the computed final score/grade if present.
|
||||
setOverrideScore(input: SetOverrideScoreInput!): SetOverrideScorePayload
|
||||
}
|
||||
|
||||
# An object with an ID.
|
||||
|
@ -835,6 +851,19 @@ enum SelfSignupPolicy {
|
|||
restricted
|
||||
}
|
||||
|
||||
# Autogenerated input type of SetOverrideScore
|
||||
input SetOverrideScoreInput {
|
||||
enrollmentId: ID!
|
||||
gradingPeriodId: ID
|
||||
overrideScore: Float
|
||||
}
|
||||
|
||||
# Autogenerated return type of SetOverrideScore
|
||||
type SetOverrideScorePayload {
|
||||
errors: [ValidationError!]
|
||||
grades: Grades
|
||||
}
|
||||
|
||||
# basic information about a students activity in a course
|
||||
type StudentSummaryAnalytics {
|
||||
pageViews: PageViewAnalysis
|
||||
|
|
|
@ -0,0 +1,138 @@
|
|||
#
|
||||
# Copyright (C) 2018 - 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"
|
||||
|
||||
describe Mutations::SetOverrideScore do
|
||||
let!(:account) { Account.create! }
|
||||
let!(:course) { account.courses.create! }
|
||||
let!(:student_enrollment) { course.enroll_student(User.create!, enrollment_state: 'active') }
|
||||
let!(:grading_period) do
|
||||
group = account.grading_period_groups.create!(title: "a test group")
|
||||
group.enrollment_terms << course.enrollment_term
|
||||
|
||||
group.grading_periods.create!(
|
||||
title: "a grading period",
|
||||
start_date: 1.week.ago,
|
||||
end_date: 1.week.from_now,
|
||||
close_date: 2.weeks.from_now
|
||||
)
|
||||
end
|
||||
let!(:teacher) { course.enroll_teacher(User.create!, enrollment_state: 'active').user }
|
||||
|
||||
let(:score_for_enrollment) { student_enrollment.find_score }
|
||||
let(:score_for_grading_period) { student_enrollment.find_score(grading_period_id: grading_period.id) }
|
||||
|
||||
before(:each) do
|
||||
course.assignments.create!(title: "hi", grading_type: "points", points_possible: 1000)
|
||||
end
|
||||
|
||||
def mutation_str(enrollment_id: student_enrollment.id, grading_period_id: nil, override_score: 45.0)
|
||||
override_value = override_score || 'null'
|
||||
input_string = "enrollmentId: #{enrollment_id} overrideScore: #{override_value}"
|
||||
input_string += " gradingPeriodId: #{grading_period_id}" if grading_period_id.present?
|
||||
|
||||
<<~GQL
|
||||
mutation {
|
||||
setOverrideScore(input: {
|
||||
#{input_string}
|
||||
}) {
|
||||
grades {
|
||||
gradingPeriod {
|
||||
id
|
||||
_id
|
||||
}
|
||||
overrideScore
|
||||
}
|
||||
errors {
|
||||
attribute
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
GQL
|
||||
end
|
||||
|
||||
context "when executed by a user with permission to set override scores" do
|
||||
let(:context) { {current_user: teacher} }
|
||||
|
||||
describe "returned values" do
|
||||
it "returns the ID of the grading period in the gradingPeriodId field if the score has a grading period" do
|
||||
result = CanvasSchema.execute(mutation_str(grading_period_id: grading_period.id), context: context)
|
||||
expect(result.dig("data", "setOverrideScore", "grades", "gradingPeriod", "_id")).to eq grading_period.id.to_s
|
||||
end
|
||||
|
||||
it "does not return a value for gradingPeriod if the score has no grading period" do
|
||||
result = CanvasSchema.execute(mutation_str, context: context)
|
||||
expect(result.dig("data", "setOverrideScore", "grades", "gradingPeriod")).to be nil
|
||||
end
|
||||
|
||||
it "returns the newly-set score in the overrideScore field" do
|
||||
result = CanvasSchema.execute(mutation_str, context: context)
|
||||
expect(result.dig("data", "setOverrideScore", "grades", "overrideScore")).to eq 45.0
|
||||
end
|
||||
|
||||
it "returns a null value for the newly-set score in the overrideScore field if the score is cleared" do
|
||||
result = CanvasSchema.execute(mutation_str(override_score: nil), context: context)
|
||||
expect(result.dig("data", "setOverrideScore", "grades", "overrideScore")).to be nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "model changes" do
|
||||
it "updates the score belonging to a given enrollment ID with no grading period specified" do
|
||||
CanvasSchema.execute(mutation_str, context: context)
|
||||
expect(score_for_enrollment.override_score).to eq 45.0
|
||||
end
|
||||
|
||||
it "updates the score belonging to a given enrollment ID with a grading period specified" do
|
||||
CanvasSchema.execute(mutation_str(grading_period_id: grading_period.id), context: context)
|
||||
expect(score_for_grading_period.override_score).to eq 45.0
|
||||
end
|
||||
|
||||
it "nullifies the override_score for the associated score object if a null value is passed" do
|
||||
score_for_enrollment.update!(override_score: 99.0)
|
||||
CanvasSchema.execute(mutation_str(override_score: nil), context: context)
|
||||
expect(score_for_enrollment.reload.override_score).to be nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "error handling" do
|
||||
it "returns an error if passed an invalid enrollment ID" do
|
||||
result = CanvasSchema.execute(mutation_str(enrollment_id: 0), context: context)
|
||||
expect(result.dig("errors", 0, "message")).to eq "not found"
|
||||
end
|
||||
|
||||
it "returns an error if passed a valid enrollment ID but an invalid grading period ID" do
|
||||
result = CanvasSchema.execute(mutation_str(grading_period_id: 0), context: context)
|
||||
expect(result.dig("errors", 0, "message")).to eq "not found"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when the caller does not have the manage_grades permission" do
|
||||
it "returns an error" do
|
||||
result = CanvasSchema.execute(mutation_str, context: {current_user: student_enrollment.user})
|
||||
expect(result.dig("errors", 0, "message")).to eq "not found"
|
||||
end
|
||||
|
||||
it "does not return data pertaining to the score in question" do
|
||||
result = CanvasSchema.execute(mutation_str, context: {current_user: student_enrollment.user})
|
||||
expect(result.dig("data", "setOverrideScore")).to be nil
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,97 @@
|
|||
#
|
||||
# Copyright (C) 2018 - 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_relative '../../helpers/graphql_type_tester'
|
||||
|
||||
describe Types::GradesType do
|
||||
let!(:account) { Account.create! }
|
||||
let!(:course) { account.courses.create!(grading_standard_enabled: true) }
|
||||
let!(:student_enrollment) { course.enroll_student(User.create!, enrollment_state: 'active') }
|
||||
let!(:grading_period) do
|
||||
group = account.grading_period_groups.create!(title: "a test group")
|
||||
group.enrollment_terms << course.enrollment_term
|
||||
|
||||
group.grading_periods.create!(
|
||||
title: "Pleistocene",
|
||||
start_date: 1.week.ago,
|
||||
end_date: 1.week.from_now,
|
||||
close_date: 2.weeks.from_now
|
||||
)
|
||||
end
|
||||
let!(:teacher) { course.enroll_teacher(User.create!, enrollment_state: 'active').user }
|
||||
|
||||
let(:enrollment_type) { GraphQLTypeTester.new(student_enrollment, current_user: teacher) }
|
||||
|
||||
before(:each) do
|
||||
score = student_enrollment.find_score(grading_period_id: grading_period.id)
|
||||
score.update!(
|
||||
current_score: 68.0,
|
||||
final_score: 78.1,
|
||||
override_score: 88.2,
|
||||
unposted_current_score: 71.3,
|
||||
unposted_final_score: 81.4
|
||||
)
|
||||
end
|
||||
|
||||
def resolve_grades_field(field)
|
||||
enrollment_type.resolve("grades { #{field} }", current_user: teacher)
|
||||
end
|
||||
|
||||
describe "fields" do
|
||||
it "resolves the currentScore field to the corresponding Score's current_score" do
|
||||
expect(resolve_grades_field("currentScore")).to eq 68.0
|
||||
end
|
||||
|
||||
it "resolves the finalScore field to the corresponding Score's final_score" do
|
||||
expect(resolve_grades_field("finalScore")).to eq 78.1
|
||||
end
|
||||
|
||||
it "resolves the overrideScore field to the corresponding Score's override_score" do
|
||||
expect(resolve_grades_field("overrideScore")).to eq 88.2
|
||||
end
|
||||
|
||||
it "resolves the unpostedCurrentScore field to the corresponding Score's unposted_current_score" do
|
||||
expect(resolve_grades_field("unpostedCurrentScore")).to eq 71.3
|
||||
end
|
||||
|
||||
it "resolves the unpostedFinalScore field to the corresponding Score's unposted_final_score" do
|
||||
expect(resolve_grades_field("unpostedFinalScore")).to eq 81.4
|
||||
end
|
||||
|
||||
it "resolves the currentGrade field to the corresponding Score's current_grade" do
|
||||
expect(resolve_grades_field("currentGrade")).to eq 'D+'
|
||||
end
|
||||
|
||||
it "resolves the finalGrade field to the corresponding Score's final_grade" do
|
||||
expect(resolve_grades_field("finalGrade")).to eq 'C+'
|
||||
end
|
||||
|
||||
it "resolves the unpostedCurrentGrade field to the corresponding Score's unposted_current_grade" do
|
||||
expect(resolve_grades_field("unpostedCurrentGrade")).to eq 'C-'
|
||||
end
|
||||
|
||||
it "resolves the unpostedFinalGrade field to the corresponding Score's unposted_final_grade" do
|
||||
expect(resolve_grades_field("unpostedFinalGrade")).to eq 'B-'
|
||||
end
|
||||
|
||||
it "resolves the gradingPeriod field to the score's associated grading period" do
|
||||
expect(resolve_grades_field("gradingPeriod { title }")).to eq 'Pleistocene'
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue