graphql: upgrade some objects to class-based api
closes RECNVS-478 Test plan: tests pass Change-Id: I80ed55ce5b8098a5fb2217e1ee7fc0f3f9fc2147 Reviewed-on: https://gerrit.instructure.com/153495 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:
parent
c97d6a5887
commit
62d48f37a8
|
@ -18,5 +18,22 @@
|
|||
|
||||
module Types
|
||||
class ApplicationObjectType < GraphQL::Schema::Object
|
||||
def current_user
|
||||
context[:current_user]
|
||||
end
|
||||
|
||||
def session
|
||||
context[:session]
|
||||
end
|
||||
|
||||
def load_association(assoc)
|
||||
Loaders::AssociationLoader.for(object.class, assoc).load(object).then do
|
||||
if block_given?
|
||||
yield object.send(assoc)
|
||||
else
|
||||
object.send(assoc)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -17,186 +17,182 @@
|
|||
#
|
||||
|
||||
module Types
|
||||
AssignmentType = GraphQL::ObjectType.define do
|
||||
name "Assignment"
|
||||
class AssignmentType < ApplicationObjectType
|
||||
graphql_name "Assignment"
|
||||
|
||||
implements GraphQL::Relay::Node.interface
|
||||
interfaces [Interfaces::TimestampInterface]
|
||||
implements Interfaces::TimestampInterface
|
||||
|
||||
global_id_field :id
|
||||
field :_id, !types.ID, "legacy canvas id", property: :id
|
||||
alias :assignment :object
|
||||
|
||||
field :name, types.String
|
||||
class AssignmentStateType < Types::BaseEnum
|
||||
graphql_name "AssignmentState"
|
||||
description "States that an Assignment can be in"
|
||||
value "unpublished"
|
||||
value "published"
|
||||
value "deleted"
|
||||
end
|
||||
|
||||
field :position, types.Int,
|
||||
"determines the order this assignment is displayed in in its assignment group"
|
||||
field :description, types.String
|
||||
GRADING_TYPES = Hash[
|
||||
Assignment::ALLOWED_GRADING_TYPES.zip(Assignment::ALLOWED_GRADING_TYPES)
|
||||
]
|
||||
|
||||
field :pointsPossible, types.Float,
|
||||
"the assignment is out of this many points",
|
||||
property: :points_possible
|
||||
SUBMISSION_TYPES = %w[
|
||||
attendance
|
||||
discussion_topic
|
||||
external_tool
|
||||
media_recording
|
||||
none
|
||||
not_graded
|
||||
on_paper
|
||||
online_quiz
|
||||
online_text_entry
|
||||
online_upload
|
||||
online_url
|
||||
wiki_page
|
||||
].to_set
|
||||
|
||||
field :dueAt, DateTimeType,
|
||||
class AssignmentSubmissionType < Types::BaseEnum
|
||||
graphql_name "SubmissionType"
|
||||
description "Types of submissions an assignment accepts"
|
||||
SUBMISSION_TYPES.each { |submission_type|
|
||||
value(submission_type)
|
||||
}
|
||||
end
|
||||
|
||||
class AssignmentGradingType < Types::BaseEnum
|
||||
graphql_name "GradingType"
|
||||
Assignment::ALLOWED_GRADING_TYPES.each { |type| value(type) }
|
||||
end
|
||||
|
||||
field :_id, ID, "legacy canvas id", null: false, method: :id
|
||||
|
||||
field :name, String, null: true
|
||||
|
||||
field :position, Int,
|
||||
"determines the order this assignment is displayed in in its assignment group",
|
||||
null: true
|
||||
field :description, String, null: true
|
||||
|
||||
field :points_possible, Float, "the assignment is out of this many points",
|
||||
null: true
|
||||
|
||||
field :due_at, DateTimeType,
|
||||
"when this assignment is due",
|
||||
property: :due_at
|
||||
field :lockAt, DateTimeType, property: :lock_at
|
||||
field :unlockAt, DateTimeType, property: :unlock_at
|
||||
null: true
|
||||
field :lock_at, DateTimeType, null: true
|
||||
field :unlock_at, DateTimeType, null: true
|
||||
|
||||
field :lockInfo, LockInfoType, resolve: ->(assignment, ctx, _) {
|
||||
field :lock_info, LockInfoType, null: true
|
||||
|
||||
def lock_info
|
||||
Loaders::AssociationLoader.for(
|
||||
Assignment,
|
||||
%i[context discussion_topic quiz wiki_page]
|
||||
).load(assignment).then {
|
||||
assignment.low_level_locked_for?(ctx[:current_user],
|
||||
assignment.low_level_locked_for?(current_user,
|
||||
check_policies: true,
|
||||
context: assignment.context)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
field :muted, types.Boolean, property: :muted?
|
||||
field :muted, Boolean, method: :muted?, null: false
|
||||
|
||||
field :state, !AssignmentState, property: :workflow_state
|
||||
field :state, AssignmentStateType, method: :workflow_state, null: false
|
||||
|
||||
field :assignmentGroup, AssignmentGroupType, resolve: ->(assignment, _, _) {
|
||||
field :assignment_group, AssignmentGroupType, null: true
|
||||
def assignment_group
|
||||
# TODO: conditionally load context_module_tags (see locked_for impl.)
|
||||
Loaders::AssociationLoader.for(Assignment, :assignment_group)
|
||||
.load(assignment)
|
||||
.then { assignment.assignment_group }
|
||||
}
|
||||
load_association(:assignment_group)
|
||||
end
|
||||
|
||||
field :quiz, Types::QuizType, resolve: -> (assignment, _, _) {
|
||||
Loaders::AssociationLoader.for(Assignment, :quiz)
|
||||
.load(assignment)
|
||||
.then { assignment.quiz }
|
||||
}
|
||||
field :quiz, Types::QuizType, null: true
|
||||
def quiz
|
||||
load_association(:quiz)
|
||||
end
|
||||
|
||||
field :discussion, Types::DiscussionType, resolve: -> (assignment, _, _) {
|
||||
Loaders::AssociationLoader.for(Assignment, :discussion_topic)
|
||||
.load(assignment)
|
||||
.then { assignment.discussion_topic }
|
||||
}
|
||||
field :discussion, Types::DiscussionType, null: true
|
||||
def discussion
|
||||
load_association(:discussion_topic)
|
||||
end
|
||||
|
||||
field :htmlUrl, UrlType, resolve: ->(assignment, _, ctx) {
|
||||
field :html_url, UrlType, null: true
|
||||
def html_url
|
||||
Rails.application.routes.url_helpers.course_assignment_url(
|
||||
course_id: assignment.context_id,
|
||||
id: assignment.id,
|
||||
host: ctx[:request].host_with_port
|
||||
host: context[:request].host_with_port
|
||||
)
|
||||
}
|
||||
|
||||
field :needsGradingCount, types.Int do
|
||||
# NOTE: this query (as it exists right now) is not batch-able.
|
||||
# make this really expensive cost-wise?
|
||||
resolve ->(assignment, _, ctx) do
|
||||
Assignments::NeedsGradingCountQuery.new(
|
||||
assignment,
|
||||
ctx[:current_user]
|
||||
# TODO course proxy stuff
|
||||
# (actually for some reason not passing along a course proxy doesn't
|
||||
# seem to matter)
|
||||
).count
|
||||
end
|
||||
end
|
||||
|
||||
field :gradingType, AssignmentGradingType, resolve: ->(assignment, _, _) {
|
||||
field :needs_grading_count, Int, null: true
|
||||
def needs_grading_count
|
||||
# NOTE: this query (as it exists right now) is not batch-able.
|
||||
# make this really expensive cost-wise?
|
||||
Assignments::NeedsGradingCountQuery.new(
|
||||
assignment,
|
||||
current_user
|
||||
# TODO course proxy stuff
|
||||
# (actually for some reason not passing along a course proxy doesn't
|
||||
# seem to matter)
|
||||
).count
|
||||
end
|
||||
|
||||
field :grading_type, AssignmentGradingType, null: true
|
||||
def grading_type
|
||||
GRADING_TYPES[assignment.grading_type]
|
||||
}
|
||||
end
|
||||
|
||||
field :submissionTypes, types[!AssignmentSubmissionType],
|
||||
resolve: ->(assignment, _, _) {
|
||||
# there's some weird data in the db so we'll just ignore anything that
|
||||
# doesn't match a value that is expected
|
||||
(SUBMISSION_TYPES & assignment.submission_types_array).to_a
|
||||
}
|
||||
field :submission_types, [AssignmentSubmissionType],
|
||||
null: true
|
||||
def submission_types
|
||||
# there's some weird data in the db so we'll just ignore anything that
|
||||
# doesn't match a value that is expected
|
||||
(SUBMISSION_TYPES & assignment.submission_types_array).to_a
|
||||
end
|
||||
|
||||
field :course, Types::CourseType, resolve: -> (assignment, _, _) {
|
||||
# course is polymorphicly associated with assignment through :context
|
||||
# it could also be queried by assignment.assignment_group.course
|
||||
Loaders::AssociationLoader.for(Assignment, :context)
|
||||
.load(assignment)
|
||||
.then { assignment.context }
|
||||
}
|
||||
field :course, Types::CourseType, null: true
|
||||
def course
|
||||
load_association(:context)
|
||||
end
|
||||
|
||||
field :assignmentGroup, AssignmentGroupType, resolve: ->(assignment, _, _) {
|
||||
Loaders::AssociationLoader.for(Assignment, :assignment_group)
|
||||
.load(assignment)
|
||||
.then { assignment.assignment_group }
|
||||
}
|
||||
field :assignment_group, AssignmentGroupType, null: true
|
||||
def assignment_group
|
||||
load_association(:assignment_group)
|
||||
end
|
||||
|
||||
field :onlyVisibleToOverrides, types.Boolean,
|
||||
field :only_visible_to_overrides, Boolean,
|
||||
"specifies that this assignment is only assigned to students for whom an
|
||||
`AssignmentOverride` applies.",
|
||||
property: :only_visible_to_overrides
|
||||
null: false
|
||||
|
||||
connection :assignmentOverrides, AssignmentOverrideType.connection_type, resolve:
|
||||
->(assignment, _, ctx) {
|
||||
field :assignment_overrides, AssignmentOverrideType.connection_type,
|
||||
null: true
|
||||
def assignment_overrides
|
||||
# this is the assignment overrides index method of loading
|
||||
# overrides... there's also the totally different method found in
|
||||
# assignment_overrides_json. they may not return the same results?
|
||||
# ¯\_(ツ)_/¯
|
||||
AssignmentOverrideApplicator.overrides_for_assignment_and_user(assignment, ctx[:current_user])
|
||||
}
|
||||
AssignmentOverrideApplicator.overrides_for_assignment_and_user(assignment, current_user)
|
||||
end
|
||||
|
||||
connection :submissionsConnection, SubmissionType.connection_type do
|
||||
field :submissions_connection, SubmissionType.connection_type, null: true do
|
||||
description "submissions for this assignment"
|
||||
argument :filter, SubmissionFilterInputType
|
||||
argument :filter, SubmissionFilterInputType, required: false
|
||||
end
|
||||
def submissions_connection(filter:)
|
||||
course = assignment.context
|
||||
|
||||
resolve ->(assignment, args, ctx) {
|
||||
current_user = ctx[:current_user]
|
||||
session = ctx[:session]
|
||||
course = assignment.course
|
||||
submissions = assignment.submissions.where(
|
||||
workflow_state: (filter || {})[:states] || DEFAULT_SUBMISSION_STATES
|
||||
)
|
||||
|
||||
submissions = assignment.submissions.where(
|
||||
workflow_state: (args[:filter] || {})[:states] || DEFAULT_SUBMISSION_STATES
|
||||
)
|
||||
|
||||
if course.grants_any_right?(current_user, session, :manage_grades, :view_all_grades)
|
||||
submissions
|
||||
elsif course.grants_right?(current_user, session, :read_grades)
|
||||
# a user can see their own submission
|
||||
submissions.where(user_id: current_user.id)
|
||||
end
|
||||
}
|
||||
if course.grants_any_right?(current_user, session, :manage_grades, :view_all_grades)
|
||||
submissions
|
||||
elsif course.grants_right?(current_user, session, :read_grades)
|
||||
# a user can see their own submission
|
||||
submissions.where(user_id: current_user.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
AssignmentState = GraphQL::EnumType.define do
|
||||
name "AssignmentState"
|
||||
description "States that an Assignment can be in"
|
||||
value "unpublished"
|
||||
value "published"
|
||||
value "deleted"
|
||||
end
|
||||
|
||||
SUBMISSION_TYPES = %w[
|
||||
attendance
|
||||
discussion_topic
|
||||
external_tool
|
||||
media_recording
|
||||
none
|
||||
not_graded
|
||||
on_paper
|
||||
online_quiz
|
||||
online_text_entry
|
||||
online_upload
|
||||
online_url
|
||||
wiki_page
|
||||
].to_set
|
||||
|
||||
GRADING_TYPES = Hash[
|
||||
Assignment::ALLOWED_GRADING_TYPES.zip(Assignment::ALLOWED_GRADING_TYPES)
|
||||
]
|
||||
|
||||
AssignmentSubmissionType = GraphQL::EnumType.define do
|
||||
name "SubmissionType"
|
||||
description "Types of submissions an assignment accepts"
|
||||
SUBMISSION_TYPES.each { |submission_type|
|
||||
value(submission_type)
|
||||
}
|
||||
end
|
||||
|
||||
AssignmentGradingType = GraphQL::EnumType.define do
|
||||
name "GradingType"
|
||||
Assignment::ALLOWED_GRADING_TYPES.each { |type| value(type) }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
class Types::BaseEnum < GraphQL::Schema::Enum
|
||||
end
|
|
@ -17,13 +17,15 @@
|
|||
#
|
||||
|
||||
module Types
|
||||
DiscussionType = GraphQL::ObjectType.define do
|
||||
name "Discussion"
|
||||
class DiscussionType < ApplicationObjectType
|
||||
graphql_name "Discussion"
|
||||
|
||||
implements GraphQL::Relay::Node.interface
|
||||
interfaces [Interfaces::TimestampInterface]
|
||||
implements Interfaces::TimestampInterface
|
||||
|
||||
global_id_field :id
|
||||
field :_id, !types.ID, "legacy canvas id", property: :id
|
||||
## TODO: something like this
|
||||
#implements Interfaces::LegacyNode
|
||||
|
||||
field :_id, ID, "legacy canvas id", null: false, method: :id
|
||||
end
|
||||
end
|
||||
|
|
|
@ -17,44 +17,75 @@
|
|||
#
|
||||
|
||||
module Types
|
||||
SubmissionType = GraphQL::ObjectType.define do
|
||||
name "Submission"
|
||||
class SubmissionType < ApplicationObjectType
|
||||
graphql_name "Submission"
|
||||
|
||||
implements GraphQL::Relay::Node.interface
|
||||
interfaces [Interfaces::TimestampInterface]
|
||||
implements Interfaces::TimestampInterface
|
||||
|
||||
alias :submission :object
|
||||
|
||||
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 :assignment, AssignmentType, null: true
|
||||
def assignment
|
||||
load_association(:assignment)
|
||||
end
|
||||
|
||||
field :user, UserType, resolve: ->(s, _, _) { Loaders::IDLoader.for(User).load(s.user_id) }
|
||||
field :user, UserType, null: true
|
||||
def user
|
||||
load_association(:user)
|
||||
end
|
||||
|
||||
field :score, types.Float, resolve: SubmissionHelper.protect_submission_grades(:score)
|
||||
field :grade, types.String, resolve: SubmissionHelper.protect_submission_grades(:grade)
|
||||
def protect_submission_grades(attr)
|
||||
submission.user_can_read_grade?(current_user, session) ?
|
||||
submission.send(attr) :
|
||||
nil
|
||||
end
|
||||
private :protect_submission_grades
|
||||
|
||||
field :enteredScore, types.Float,
|
||||
field :score, Float, null: true
|
||||
def score
|
||||
protect_submission_grades(:score)
|
||||
end
|
||||
|
||||
field :grade, String, null: true
|
||||
def grade
|
||||
protect_submission_grades(:grade)
|
||||
end
|
||||
|
||||
field :entered_score, Float,
|
||||
"the submission score *before* late policy deductions were applied",
|
||||
resolve: SubmissionHelper.protect_submission_grades(:entered_score)
|
||||
field :enteredGrade, types.String,
|
||||
null: true
|
||||
def entered_score
|
||||
protect_submission_grades(:entered_score)
|
||||
end
|
||||
|
||||
field :entered_grade, String,
|
||||
"the submission grade *before* late policy deductions were applied",
|
||||
resolve: SubmissionHelper.protect_submission_grades(:entered_grade)
|
||||
null: true
|
||||
def entered_grade
|
||||
protect_submission_grades(:entered_grade)
|
||||
end
|
||||
|
||||
field :deductedPoints, types.Float,
|
||||
field :deducted_points, Float,
|
||||
"how many points are being deducted due to late policy",
|
||||
resolve: SubmissionHelper.protect_submission_grades(:points_deducted)
|
||||
null: true
|
||||
def deducted_points
|
||||
protect_submission_grades(:points_deducted)
|
||||
end
|
||||
|
||||
field :excused, types.Boolean,
|
||||
field :excused, Boolean,
|
||||
"excused assignments are ignored when calculating grades",
|
||||
property: :excused?
|
||||
method: :excused?, null: true
|
||||
|
||||
field :submittedAt, DateTimeType, property: :submitted_at
|
||||
field :gradedAt, DateTimeType, property: :graded_at
|
||||
field :submitted_at, DateTimeType, null: true
|
||||
field :graded_at, DateTimeType, null: true
|
||||
|
||||
field :state, SubmissionStateType, property: :workflow_state
|
||||
field :state, SubmissionStateType, method: :workflow_state, null: false
|
||||
|
||||
field :submissionStatus, types.String, resolve: ->(submission, _, _) {
|
||||
field :submission_status, String, null: true
|
||||
def submission_status
|
||||
if submission.submission_type == "online_quiz"
|
||||
Loaders::AssociationLoader.for(Submission, :quiz_submission).
|
||||
load(submission).
|
||||
|
@ -62,18 +93,8 @@ module Types
|
|||
else
|
||||
submission.submission_status
|
||||
end
|
||||
}
|
||||
|
||||
field :gradingStatus, types.String, property: :grading_status
|
||||
end
|
||||
|
||||
class SubmissionHelper
|
||||
def self.protect_submission_grades(attr)
|
||||
->(submission, _, ctx) {
|
||||
submission.user_can_read_grade?(ctx[:current_user], ctx[:session]) ?
|
||||
submission.send(attr) :
|
||||
nil
|
||||
}
|
||||
end
|
||||
|
||||
field :grading_status, String, null: true
|
||||
end
|
||||
end
|
||||
|
|
|
@ -29,16 +29,18 @@ type Assignment implements Node & Timestamped {
|
|||
dueAt: DateTime
|
||||
gradingType: GradingType
|
||||
htmlUrl: URL
|
||||
|
||||
# ID of the object.
|
||||
id: ID!
|
||||
lockAt: DateTime
|
||||
lockInfo: LockInfo
|
||||
muted: Boolean
|
||||
muted: Boolean!
|
||||
name: String
|
||||
needsGradingCount: Int
|
||||
|
||||
# specifies that this assignment is only assigned to students for whom an
|
||||
# `AssignmentOverride` applies.
|
||||
onlyVisibleToOverrides: Boolean
|
||||
onlyVisibleToOverrides: Boolean!
|
||||
|
||||
# the assignment is out of this many points
|
||||
pointsPossible: Float
|
||||
|
@ -364,6 +366,8 @@ type Discussion implements Node & Timestamped {
|
|||
# legacy canvas id
|
||||
_id: ID!
|
||||
createdAt: DateTime
|
||||
|
||||
# ID of the object.
|
||||
id: ID!
|
||||
updatedAt: DateTime
|
||||
}
|
||||
|
@ -697,9 +701,11 @@ type Submission implements Node & Timestamped {
|
|||
grade: String
|
||||
gradedAt: DateTime
|
||||
gradingStatus: String
|
||||
|
||||
# ID of the object.
|
||||
id: ID!
|
||||
score: Float
|
||||
state: SubmissionState
|
||||
state: SubmissionState!
|
||||
submissionStatus: String
|
||||
submittedAt: DateTime
|
||||
updatedAt: DateTime
|
||||
|
|
|
@ -71,12 +71,28 @@ describe Types::AssignmentType do
|
|||
submission1 = assignment.submit_homework(student, { :body => "sub1", :submission_type => 'online_text_entry' })
|
||||
submission2 = assignment.submit_homework(other_student, { :body => "sub1", :submission_type => 'online_text_entry' })
|
||||
|
||||
expect(assignment_type.submissionsConnection(current_user: teacher).sort).to eq [submission1, submission2]
|
||||
expect(assignment_type.submissionsConnection(current_user: student)).to eq [submission1]
|
||||
expect(
|
||||
assignment_type.submissionsConnection(
|
||||
current_user: teacher,
|
||||
args: {filter: nil}
|
||||
).sort
|
||||
).to eq [submission1, submission2]
|
||||
|
||||
expect(
|
||||
assignment_type.submissionsConnection(
|
||||
current_user: student,
|
||||
args: {filter: nil}
|
||||
)
|
||||
).to eq [submission1]
|
||||
end
|
||||
|
||||
it "can filter submissions according to workflow state" do
|
||||
expect(assignment_type.submissionsConnection(current_user: teacher)).to eq []
|
||||
expect(
|
||||
assignment_type.submissionsConnection(
|
||||
current_user: teacher,
|
||||
args: {filter: nil}
|
||||
)
|
||||
).to eq []
|
||||
|
||||
expect(
|
||||
assignment_type.submissionsConnection(
|
||||
|
|
Loading…
Reference in New Issue