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:
Cameron Matheson 2018-06-06 16:56:16 -06:00
parent c97d6a5887
commit 62d48f37a8
7 changed files with 239 additions and 179 deletions

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,2 @@
class Types::BaseEnum < GraphQL::Schema::Enum
end

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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(