graphql: add submission type

closes CNVS-37581

Test plan: list the submissions in a course (make sure to test the
  studentIds and orderBy params)

Change-Id: Ica59cb08981ce37fb7e61e83fe891edda73a0b40
Reviewed-on: https://gerrit.instructure.com/121084
Tested-by: Jenkins
Reviewed-by: Jonathan Featherstone <jfeatherstone@instructure.com>
QA-Review: Collin Parrish <cparrish@instructure.com>
Product-Review: Cameron Matheson <cameron@instructure.com>
This commit is contained in:
Cameron Matheson 2017-08-01 02:52:16 -06:00
parent f433ba9643
commit 7a821f6f32
6 changed files with 224 additions and 21 deletions

View File

@ -4,16 +4,38 @@ module GraphQLHelpers
# will get a standard canvas id
def self.relay_or_legacy_id_prepare_func(expected_type)
Proc.new do |relay_or_legacy_id, ctx|
begin
self.parse_relay_or_legacy_id(relay_or_legacy_id, expected_type)
rescue InvalidIDError => e
GraphQL::ExecutionError.new(e.message)
end
end
end
def self.relay_or_legacy_ids_prepare_func(expected_type)
Proc.new do |relay_or_legacy_ids, ctx|
begin
relay_or_legacy_ids.map { |relay_or_legacy_id, ctx|
self.parse_relay_or_legacy_id(relay_or_legacy_id, expected_type)
}
rescue InvalidIDError => e
GraphQL::ExecutionError.new(e.message)
end
end
end
def self.parse_relay_or_legacy_id(relay_or_legacy_id, expected_type)
if relay_or_legacy_id =~ /\A\d+\Z/
relay_or_legacy_id
else
type, id = GraphQL::Schema::UniqueWithinType.decode(relay_or_legacy_id)
if (type != expected_type || id.nil?)
GraphQL::ExecutionError.new("expected an id for #{expected_type}")
raise InvalidIDError.new("expected an id for #{expected_type}")
else
id
end
end
end
end
class InvalidIDError < StandardError; end
end

View File

@ -45,6 +45,55 @@ module Types
GradingPeriod.for(course).order(:start_date)
}
end
connection :submissionsConnection, SubmissionType.connection_type do
description "all the submissions for assignments in this course"
argument :studentIds, !types[types.ID], "Only return submissions for the given students.",
prepare: GraphQLHelpers.relay_or_legacy_ids_prepare_func("User")
argument :orderBy, types[SubmissionOrderInputType]
resolve ->(course, args, ctx) {
current_user = ctx[:current_user]
session = ctx[:session]
user_ids = args[:studentIds].map(&:to_i)
if course.grants_any_right?(current_user, session, :manage_grades, :view_all_grades)
# TODO: make a preloader for this???
allowed_user_ids = course.apply_enrollment_visibility(course.all_student_enrollments, current_user).pluck(:user_id)
allowed_user_ids &= user_ids
elsif course.grants_right?(current_user, session, :read_grades)
allowed_user_ids = user_ids & [current_user.id]
else
allowed_user_ids = []
end
submissions = Submission.active.joins(:assignment).where(
user_id: allowed_user_ids,
assignment_id: course.assignments.published
).where.not(workflow_state: "unsubmitted")
(args[:orderBy] || []).each { |order|
submissions = submissions.order(order[:field] => order[:direction])
}
submissions
}
end
end
SubmissionOrderInputType = GraphQL::InputObjectType.define do
name "SubmissionOrderCriteria"
argument :field, !GraphQL::EnumType.define {
name "SubmissionOrderField"
value "_id", value: "id"
value "gradedAt", value: "graded_at"
}
argument :direction, GraphQL::EnumType.define {
name "OrderDirection"
value "ascending", value: "ASC"
value "descending", value: "DESC"
}
end
CourseWorkflowState = GraphQL::EnumType.define do

View File

@ -0,0 +1,25 @@
module Types
SubmissionType = GraphQL::ObjectType.define do
name "Submission"
implements GraphQL::Relay::Node.interface
global_id_field :id
# not doing a legacy canvas id since they aren't used in the rest api
field :assignment, AssignmentType,
resolve: ->(s, _, _) { Loaders::IDLoader.for(Assignment).load(s.assignment_id) }
field :user, UserType, resolve: ->(s, _, _) { Loaders::IDLoader.for(User).load(s.user_id) }
field :score, types.Float
field :grade, types.String
field :excused, types.Boolean,
"excused assignments are ignored when calculating grades",
property: :excused?
field :submittedAt, TimeType, property: :submitted_at
field :gradedAt, TimeType, property: :graded_at
end
end

View File

@ -46,4 +46,26 @@ describe GraphQLHelpers do
).to be_a(GraphQL::ExecutionError)
end
end
context "relay_or_legacy_ids_prepare_func" do
let :user1234 { "VXNlci0xMjM0" }
let :user5678 { "VXNlci01Njc4" }
let :ctx { nil }
it "works for valid ids" do
expect(
GraphQLHelpers.relay_or_legacy_ids_prepare_func("User").call(
[user1234, user5678], nil
)
).to eq ["1234", "5678"]
end
it "returns an error for bad ids" do
expect(
GraphQLHelpers.relay_or_legacy_ids_prepare_func("Course").call(
[user1234, user5678], nil
)
).to be_a(GraphQL::ExecutionError)
end
end
end

View File

@ -2,7 +2,7 @@ require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
require File.expand_path(File.dirname(__FILE__) + '/../../helpers/graphql_type_tester')
describe Types::CourseType do
let_once(:course) { Course.create! name: "TEST" }
let_once(:course) { course_with_student(active_all: true); @course }
let(:course_type) { GraphQLTypeTester.new(Types::CourseType, course) }
it "works" do
@ -11,30 +11,85 @@ describe Types::CourseType do
end
describe "assignmentsConnection" do
let_once(:teacher) {
teacher_in_course(active_all: true, course: course)
@teacher
}
let_once(:student) {
student_in_course(active_all: true, course: course)
@student
}
let_once(:assignment) {
course.assignments.create! name: "asdf", workflow_state: "unpublished"
}
it "only returns visible assignments" do
expect(course_type.assignmentsConnection(current_user: teacher).size).to eq 1
expect(course_type.assignmentsConnection(current_user: student).size).to eq 0
expect(course_type.assignmentsConnection(current_user: @teacher).size).to eq 1
expect(course_type.assignmentsConnection(current_user: @student).size).to eq 0
end
end
describe "sectionsConnection" do
it "only includes active sections" do
section1 = course.course_sections.create!(name: "Delete Me")
section2 = course.course_sections.create!(name: "Keep Me")
expect(course_type.sectionsConnection.size).to eq 2
section1.destroy
expect(course_type.sectionsConnection.size).to eq 1
end
end
context "submissionsConnection" do
before(:once) do
a1 = course.assignments.create! name: "one", points_possible: 10
a2 = course.assignments.create! name: "two", points_possible: 10
@student1 = @student
student_in_course(active_all: true)
@student2 = @student
@student1a1_submission, _ = a1.grade_student(@student1, grade: 1, grader: @teacher)
@student1a2_submission, _ = a2.grade_student(@student1, grade: 9, grader: @teacher)
@student2a1_submission, _ = a1.grade_student(@student2, grade: 5, grader: @teacher)
@student1a1_submission.update_attribute :graded_at, 4.days.ago
@student1a2_submission.update_attribute :graded_at, 2.days.ago
@student2a1_submission.update_attribute :graded_at, 3.days.ago
end
it "returns submissions for specified students" do
expect(
course_type.submissionsConnection(
current_user: @teacher,
args: {
studentIds: [@student1.id.to_s, @student2.id.to_s],
orderBy: [{field: "id", direction: "asc"}],
}
)
).to eq [
@student1a1_submission,
@student1a2_submission,
@student2a1_submission
].sort_by(&:id)
end
it "doesn't let students see other student's submissions" do
expect(
course_type.submissionsConnection(
current_user: @student2,
args: {
studentIds: [@student1.id.to_s, @student2.id.to_s],
}
)
).to eq [@student2a1_submission]
end
it "takes sorting criteria" do
expect(
course_type.submissionsConnection(
current_user: @teacher,
args: {
studentIds: [@student1.id.to_s, @student2.id.to_s],
orderBy: [{field: "graded_at", direction: "desc"}],
}
)
).to eq [
@student1a2_submission,
@student2a1_submission,
@student1a1_submission,
]
end
end
end

View File

@ -0,0 +1,30 @@
#
# Copyright (C) 2017 - 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 File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
require File.expand_path(File.dirname(__FILE__) + '/../../helpers/graphql_type_tester')
describe Types::SubmissionType do
let(:submission) { Submission.new(score: 5) }
let(:submission_type) { GraphQLTypeTester.new(Types::SubmissionType, submission) }
it "works" do
expect(submission_type.score).to eq submission.score
expect(submission_type.excused).to eq false
end
end