A2: add user search to submissions in graphql

This adds a bunch of search options to the assignment
submissionsConnection in graphql. Not all of the potentially needed
orders and filters are implemented, but this gets the basic concept and
the simple stuff implemented.

closes ADMIN-2324

test plan:
- go to the graphiql interface and query an assignment and its
  submissionsConnection. You should see a bunch of options in the filter
  input of the submissionsConnection.
- try out all the filters and orders and see that you get the results
  that you'd expect
car
Change-Id: I541e333dc544f2258c9c2359124bac0d24277abc
Reviewed-on: https://gerrit.instructure.com/184727
Tested-by: Jenkins
QA-Review: Carl Kibler <ckibler@instructure.com>
Product-Review: Jon Willesen <jonw+gerrit@instructure.com>
Reviewed-by: Carl Kibler <ckibler@instructure.com>
This commit is contained in:
Jon Willesen 2019-03-11 10:27:45 -06:00 committed by Jon Willesen
parent f9cebc6fbc
commit 388bc15684
12 changed files with 605 additions and 185 deletions

View File

@ -357,28 +357,15 @@ module Types
field :submissions_connection, SubmissionType.connection_type, null: true do
description "submissions for this assignment"
argument :filter, SubmissionFilterInputType, required: false
argument :filter, SubmissionSearchFilterInputType, required: false
argument :order_by, [SubmissionSearchOrderInputType], required: false
end
def submissions_connection(filter: nil)
filter ||= {}
course = assignment.context
submissions = assignment.submissions.where(
workflow_state: filter[:states] || DEFAULT_SUBMISSION_STATES
)
if filter[:section_ids].present?
sections = course.course_sections.where(id: filter[:section_ids])
student_ids = course.student_enrollments.where(course_section: sections).pluck(:user_id)
submissions = submissions.where(user_id: student_ids)
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
def submissions_connection(filter: nil, order_by: nil)
filter = filter.to_h
order_by ||= []
filter[:states] ||= DEFAULT_SUBMISSION_STATES
filter[:order_by] = order_by.map(&:to_h)
SubmissionSearch.new(assignment, current_user, session, filter).search
end
field :post_policy, PostPolicyType, null: true

View File

@ -23,12 +23,6 @@ module Types
value :gradedAt, value: :graded_at
end
class OrderDirectionType < BaseEnum
graphql_name "OrderDirection"
value :ascending, value: "ASC"
value :descending, value: "DESC NULLS LAST"
end
class SubmissionOrderInputType < BaseInputObject
graphql_name "SubmissionOrderCriteria"
@ -170,7 +164,8 @@ module Types
)
(order_by || []).each { |order|
submissions = submissions.order("#{order[:field]} #{order[:direction]}")
direction = order[:direction] == 'descending' ? "DESC NULLS LAST" : "ASC"
submissions = submissions.order("#{order[:field]} #{direction}")
}
submissions

View File

@ -0,0 +1,25 @@
#
# Copyright (C) 2019 - 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/>.
#
module Types
class OrderDirectionType < BaseEnum
graphql_name "OrderDirection"
value :ascending
value :descending
end
end

View File

@ -17,9 +17,6 @@
#
module Types
# TODO: move this into SubmissionFilterInputType when 1.8 lands
DEFAULT_SUBMISSION_STATES = %w[submitted pending_review graded].freeze
class SubmissionFilterInputType < Types::BaseInputObject
graphql_name "SubmissionFilterInput"

View File

@ -0,0 +1,38 @@
#
# 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/>.
#
module Types
class SubmissionSearchFilterInputType < Types::BaseInputObject
graphql_name "SubmissionSearchFilterInput"
argument :states, [SubmissionStateType], required: false, default_value: DEFAULT_SUBMISSION_STATES
argument :section_ids, [ID], required: false, prepare: GraphQLHelpers.relay_or_legacy_ids_prepare_func("Section")
argument :enrollment_types, [EnrollmentTypeType], required: false
argument :user_search, String, <<~DESC, required: false
The partial name or full ID of the users to match and return in the
results list. Must be at least 3 characters.
Queries by administrative users will search on SIS ID, login ID, name, or email
address; non-administrative queries will only be compared against name.
DESC
argument :scored_less_than, Float, "Limit results to submissions that scored below the specified value", required: false
argument :scored_more_than, Float, "Limit results to submissions that scored above the specified value", required: false
end
end

View File

@ -0,0 +1,38 @@
#
# 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/>.
#
module Types
class SubmissionSearchOrderFieldInputType < Types::BaseEnum
graphql_name "SubmissionSearchOrderField"
description "The user or submission field to sort by"
# user sorts
value "username"
# submission sorts
value "score"
value "submitted_at"
end
class SubmissionSearchOrderInputType < Types::BaseInputObject
graphql_name "SubmissionSearchOrder"
description "Specify a sort for the results"
argument :field, SubmissionSearchOrderFieldInputType, required: true
argument :direction, OrderDirectionType, required: false
end
end

View File

@ -17,6 +17,8 @@
#
module Types
DEFAULT_SUBMISSION_STATES = %w[submitted pending_review graded].freeze
class SubmissionStateType < BaseEnum
graphql_name "SubmissionState"
@ -24,6 +26,7 @@ module Types
value "unsubmitted"
value "pending_review"
value "graded"
value "ungraded"
value "deleted"
end
end

View File

@ -1,162 +1,182 @@
{
"ignored_warnings": [
{
"note": "Enrollment.active_student_conditions accepts no user input",
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "f4e920699a6767e36d0e54f5c1ccd7a638a3654e8bef6e3ee6fbccffba76c345",
"message": "Possible SQL injection",
"file": "app/models/submission.rb",
"line": 169,
"link": "http://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "joins(\"INNER JOIN #{Enrollment.quoted_table_name} ON #{quoted_table_name}.user_id=#{Enrollment.quoted_table_name}.user_id\").where(needs_grading_conditions).where(Enrollment.active_student_conditions)",
"render_path": null,
"location": {
"type": "method",
"class": "Submission",
"method": "needs_grading"
},
"user_input": "Enrollment.active_student_conditions",
"confidence": "High"
"ignored_warnings": [
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "6c187af95302423b293a6ee5fcc3233c199e0f82d858931c478515c5f2d22cad",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/controllers/outcome_results_controller.rb",
"line": 572,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "Arel.sql((User.sortable_name_order_by_clause(User.quoted_table_name) or \"#{User.sortable_name_order_by_clause(User.quoted_table_name)} DESC\"))",
"render_path": null,
"location": {
"type": "method",
"class": "OutcomeResultsController",
"method": "apply_sort_order"
},
{
"note": "Enrollment.active_student_conditions accepts no user input",
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "b67a9b9726298fc3e829fd40dd141316eff1c8084cdc07e15a57ecc5b0bbacb9",
"message": "Possible SQL injection",
"file": "app/models/submission.rb",
"line": 213,
"link": "http://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "Enrollment.where(Enrollment.active_student_conditions)",
"render_path": null,
"location": {
"type": "method",
"class": "Submission",
"method": "touch_assignments"
},
"user_input": "Enrollment.active_student_conditions",
"confidence": "High"
"user_input": "User.sortable_name_order_by_clause(User.quoted_table_name)",
"confidence": "High",
"note": "No user input is passed in"
},
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "799a44268d5cfe1ff43bcd239e02e4dc5fc10545c370095aaed3ec7e9975df6e",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/controllers/lti/ims/providers/memberships_provider.rb",
"line": 81,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "Submission.active.for_assignment(assignment).where(\"#{outer_user_id_column} = submissions.user_id\")",
"render_path": null,
"location": {
"type": "method",
"class": "Lti::Ims::Providers::MembershipsProvider",
"method": "correlated_assignment_submissions"
},
{
"note": "Submission.needs_grading_conditions accepts no user input",
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "90b239bee8ac22b4c5d6c4d81572f155d3c073446542fa3bad6135b4578e2a91",
"message": "Possible SQL injection",
"file": "app/models/assignment.rb",
"line": 1949,
"link": "http://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "Submission.where(\"assignment_id=assignments.id\").where(Submission.needs_grading_conditions)",
"render_path": null,
"location": {
"type": "method",
"class": "Assignment",
"method": "need_grading_info"
},
"user_input": "Submission.needs_grading_conditions",
"confidence": "High"
"user_input": "outer_user_id_column",
"confidence": "Medium",
"note": "No user input is passed in"
},
{
"note": "Submission.needs_grading_conditions accepts no user input",
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "90b239bee8ac22b4c5d6c4d81572f155d3c073446542fa3bad6135b4578e2a91",
"message": "Possible SQL injection",
"file": "app/models/assignment.rb",
"line": 1949,
"link": "http://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "Submission.where(\"assignment_id=assignments.id\").where(Submission.needs_grading_conditions)",
"render_path": null,
"location": {
"type": "method",
"class": "Assignment",
"method": "need_grading_info"
},
{
"note": "Enrollment.active_student_conditions accepts no user input",
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "aba39acd96b69d6eb24de776b3a972a2d8f68133fe89c477b5acab2cb41a99f6",
"message": "Possible SQL injection",
"file": "app/models/enrollment.rb",
"line": 163,
"link": "http://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "Enrollment.where(Enrollment.active_student_conditions)",
"render_path": null,
"location": {
"type": "method",
"class": "Enrollment",
"method": "touch_assignments"
},
"user_input": "Enrollment.active_student_conditions",
"confidence": "High"
"user_input": "Submission.needs_grading_conditions",
"confidence": "High"
},
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "a22e7dbb90204b4b932c4ca659085a3b6c18f43610450c9c59a0a432bbe081fa",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "lib/submission_search.rb",
"line": 93,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "Arel.sql(\"#{User.sortable_name_order_by_clause(\"users\")} #{(\"DESC NULLS LAST\" or \"ASC\")}\")",
"render_path": null,
"location": {
"type": "method",
"class": "SubmissionSearch",
"method": "add_orders"
},
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "d8e0d77e896b3da629e04233f458668fff1d130a6b969287822c292af1daeaad",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/models/account.rb",
"line": 486,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "self.associated_courses(:include_crosslisted_courses => opts[:include_crosslisted_courses]).active.order(\"#{Course.best_unicode_collation_key(\"courses.name\")} ASC\")",
"render_path": null,
"location": {
"type": "method",
"class": "Account",
"method": "fast_course_base"
},
"user_input": "Course.best_unicode_collation_key(\"courses.name\")",
"confidence": "High",
"note": "No user input is passed in to via opts['order'] in this function (see accounts_controller:sort_order)"
"user_input": "User.sortable_name_order_by_clause(\"users\")",
"confidence": "Medium",
"note": ""
},
{
"note": "Enrollment.active_student_conditions accepts no user input",
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "aba39acd96b69d6eb24de776b3a972a2d8f68133fe89c477b5acab2cb41a99f6",
"message": "Possible SQL injection",
"file": "app/models/enrollment.rb",
"line": 163,
"link": "http://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "Enrollment.where(Enrollment.active_student_conditions)",
"render_path": null,
"location": {
"type": "method",
"class": "Enrollment",
"method": "touch_assignments"
},
{
"warning_type": "Remote Code Execution",
"warning_code": 24,
"fingerprint": "b5ba030e599093d2a5047494736d8a61807935ee92704d7f289da07646c862e4",
"check_name": "UnsafeReflection",
"message": "Unsafe reflection method const_get called with parameter value",
"file": "app/controllers/files_controller.rb",
"line": 810,
"link": "https://brakemanscanner.org/docs/warning_types/remote_code_execution/",
"code": "Object.const_get(params[:context_type])",
"render_path": null,
"location": {
"type": "method",
"class": "FilesController",
"method": "api_capture"
},
"user_input": "params[:context_type]",
"confidence": "High",
"note": "Value is whitelisted"
"user_input": "Enrollment.active_student_conditions",
"confidence": "High"
},
{
"warning_type": "Remote Code Execution",
"warning_code": 24,
"fingerprint": "b5ba030e599093d2a5047494736d8a61807935ee92704d7f289da07646c862e4",
"check_name": "UnsafeReflection",
"message": "Unsafe reflection method const_get called with parameter value",
"file": "app/controllers/files_controller.rb",
"line": 810,
"link": "https://brakemanscanner.org/docs/warning_types/remote_code_execution/",
"code": "Object.const_get(params[:context_type])",
"render_path": null,
"location": {
"type": "method",
"class": "FilesController",
"method": "api_capture"
},
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "6c187af95302423b293a6ee5fcc3233c199e0f82d858931c478515c5f2d22cad",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/controllers/outcome_results_controller.rb",
"line": 572,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "Arel.sql((User.sortable_name_order_by_clause(User.quoted_table_name) or \"#{User.sortable_name_order_by_clause(User.quoted_table_name)} DESC\"))",
"render_path": null,
"location": {
"type": "method",
"class": "OutcomeResultsController",
"method": "apply_sort_order"
},
"user_input": "User.sortable_name_order_by_clause(User.quoted_table_name)",
"confidence": "High",
"note": "No user input is passed in"
"user_input": "params[:context_type]",
"confidence": "High",
"note": "Value is whitelisted"
},
{
"note": "Enrollment.active_student_conditions accepts no user input",
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "b67a9b9726298fc3e829fd40dd141316eff1c8084cdc07e15a57ecc5b0bbacb9",
"message": "Possible SQL injection",
"file": "app/models/submission.rb",
"line": 213,
"link": "http://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "Enrollment.where(Enrollment.active_student_conditions)",
"render_path": null,
"location": {
"type": "method",
"class": "Submission",
"method": "touch_assignments"
},
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "799a44268d5cfe1ff43bcd239e02e4dc5fc10545c370095aaed3ec7e9975df6e",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/controllers/lti/ims/providers/memberships_provider.rb",
"line": 81,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "Submission.active.for_assignment(assignment).where(\"#{outer_user_id_column} = submissions.user_id\")",
"render_path": null,
"location": {
"type": "method",
"class": "Lti::Ims::Providers::MembershipsProvider",
"method": "correlated_assignment_submissions"
},
"user_input": "outer_user_id_column",
"confidence": "Medium",
"note": "No user input is passed in"
}
],
"updated": "2018-10-17 14:57:27 -0700",
"brakeman_version": "4.3.1"
"user_input": "Enrollment.active_student_conditions",
"confidence": "High"
},
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "d8e0d77e896b3da629e04233f458668fff1d130a6b969287822c292af1daeaad",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/models/account.rb",
"line": 486,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "self.associated_courses(:include_crosslisted_courses => opts[:include_crosslisted_courses]).active.order(\"#{Course.best_unicode_collation_key(\"courses.name\")} ASC\")",
"render_path": null,
"location": {
"type": "method",
"class": "Account",
"method": "fast_course_base"
},
"user_input": "Course.best_unicode_collation_key(\"courses.name\")",
"confidence": "High",
"note": "No user input is passed in to via opts['order'] in this function (see accounts_controller:sort_order)"
},
{
"note": "Enrollment.active_student_conditions accepts no user input",
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "f4e920699a6767e36d0e54f5c1ccd7a638a3654e8bef6e3ee6fbccffba76c345",
"message": "Possible SQL injection",
"file": "app/models/submission.rb",
"line": 169,
"link": "http://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "joins(\"INNER JOIN #{Enrollment.quoted_table_name} ON #{quoted_table_name}.user_id=#{Enrollment.quoted_table_name}.user_id\").where(needs_grading_conditions).where(Enrollment.active_student_conditions)",
"render_path": null,
"location": {
"type": "method",
"class": "Submission",
"method": "needs_grading"
},
"user_input": "Enrollment.active_student_conditions",
"confidence": "High"
}
],
"updated": "2019-03-21 16:01:17 -0600",
"brakeman_version": "4.5.0"
}

101
lib/submission_search.rb Normal file
View File

@ -0,0 +1,101 @@
#
# Copyright (C) 2019 - 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 SubmissionSearch
def initialize(assignment, searcher, session, options)
@assignment = assignment
@course = assignment.context
@searcher = searcher
@session = session
@options = options
end
def search
# use all_submissions so state: deleted can be found
submission_search_scope = @assignment.all_submissions
submission_search_scope = add_filters(submission_search_scope)
submission_search_scope = add_order_bys(submission_search_scope)
submission_search_scope
end
def user_search_scope
UserSearch.
for_user_in_context(@options[:user_search], @assignment.context, @searcher, @session, @options).
except(:order)
end
def add_filters(search_scope)
if @options[:states]
search_scope = search_scope.where(workflow_state: @options[:states])
end
if @options[:section_ids].present?
sections = @course.course_sections.where(id: @options[:section_ids])
student_ids = @course.student_enrollments.where(course_section: sections).pluck(:user_id)
search_scope = search_scope.where(user_id: student_ids)
end
if @options[:user_search]
search_scope = search_scope.
where("submissions.user_id IN (SELECT id FROM (#{user_search_scope.to_sql}) AS user_search_ids)")
end
if @options[:enrollment_types].present?
search_scope = search_scope.where(user_id:
@course.enrollments.select(:user_id).where(type: @options[:enrollment_types])
)
end
search_scope = if @course.grants_any_right?(@searcher, @session, :manage_grades, :view_all_grades)
search_scope
elsif @course.grants_right?(@searcher, @session, :read_grades)
# a user can see their own submission
search_scope.where(user_id: @searcher.id)
end
if @options[:scored_less_than]
search_scope = search_scope.where("submissions.score < ?", @options[:scored_less_than])
end
if @options[:scored_more_than]
search_scope = search_scope.where("submissions.score > ?", @options[:scored_more_than])
end
search_scope
end
def add_order_bys(search_scope)
order_bys = Array(@options[:order_by])
order_bys.each do |order_field_direction|
field = order_field_direction[:field]
direction = order_field_direction[:direction] == "descending" ? "DESC NULLS LAST" : "ASC"
search_scope =
case field
when 'username'
order_clause = User.sortable_name_order_by_clause('users')
search_scope.joins(:user).order(Arel.sql("#{order_clause} #{direction}"))
when 'score'
search_scope.order(Arel.sql("submissions.score #{direction}"))
when 'submitted_at'
search_scope.order(Arel.sql("submissions.submitted_at #{direction}"))
else
raise "submission search field '#{field}' is not supported"
end
end
search_scope.order(:user_id)
end
end

View File

@ -133,7 +133,7 @@ type Assignment implements ModuleItemInterface & Node & Timestamped {
Returns the elements in the list that come before the specified cursor.
"""
before: String
filter: SubmissionFilterInput
filter: SubmissionSearchFilterInput
"""
Returns the first _n_ elements from the list.
@ -144,6 +144,7 @@ type Assignment implements ModuleItemInterface & Node & Timestamped {
Returns the last _n_ elements from the list.
"""
last: Int
orderBy: [SubmissionSearchOrder!]
): SubmissionConnection
submissionsDownloads: Int
timeZoneEdited: String
@ -2054,11 +2055,53 @@ enum SubmissionOrderField {
gradedAt
}
input SubmissionSearchFilterInput {
enrollmentTypes: [EnrollmentType!]
"""
Limit results to submissions that scored below the specified value
"""
scoredLessThan: Float
"""
Limit results to submissions that scored above the specified value
"""
scoredMoreThan: Float
sectionIds: [ID!]
states: [SubmissionState!] = [submitted, pending_review, graded]
"""
The partial name or full ID of the users to match and return in the
results list. Must be at least 3 characters.
Queries by administrative users will search on SIS ID, login ID, name, or email
address; non-administrative queries will only be compared against name.
"""
userSearch: String
}
"""
Specify a sort for the results
"""
input SubmissionSearchOrder {
direction: OrderDirection
field: SubmissionSearchOrderField!
}
"""
The user or submission field to sort by
"""
enum SubmissionSearchOrderField {
score
submitted_at
username
}
enum SubmissionState {
deleted
graded
pending_review
submitted
ungraded
unsubmitted
}

View File

@ -179,6 +179,40 @@ describe Types::AssignmentType do
describe "submissionsConnection" do
let_once(:other_student) { student_in_course(course: course, active_all: true).user }
# This is kind of a catch-all test the assignment.submissionsConnection
# graphql plumbing. The submission search specs handle testing the
# implementation. This makes sure the graphql inputs are hooked up right.
# Other tests below were already here to test specific cases, and I think
# they still have value as a sanity check.
it "plumbs through filter options to SubmissionSearch" do
allow(SubmissionSearch).to receive(:new).and_call_original
assignment_type.resolve(<<~GQL, current_user: teacher)
submissionsConnection(
filter: {
states: submitted,
sectionIds: 42,
enrollmentTypes: StudentEnrollment,
userSearch: foo,
scoredLessThan: 3
scoredMoreThan: 1
}
orderBy: {field: username, direction: descending}
) { nodes { _id } }
GQL
expect(SubmissionSearch).to have_received(:new).with(assignment, teacher, nil, {
states: ["submitted"],
section_ids: ["42"],
enrollment_types: ["StudentEnrollment"],
user_search: 'foo',
scored_less_than: 3.0,
scored_more_than: 1.0,
order_by: [{
field: "username",
direction: "descending"
}]
})
end
it "returns 'real' submissions from with permissions" 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"})

View File

@ -0,0 +1,139 @@
#
# Copyright (C) 2019 - 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.rb')
describe SubmissionSearch do
let_once(:course) { Course.create!(workflow_state: "available") }
let_once(:jonah) { User.create!(name: 'Jonah Jameson') }
let_once(:amanda) { User.create!(name: 'Amanda Jones') }
let_once(:mandy) { User.create!(name: 'Mandy Miller') }
let_once(:james) { User.create!(name: 'James Peterson') }
let_once(:peter) { User.create!(name: 'Peter Piper') }
let_once(:students) { [jonah, amanda, mandy, james, peter] }
let_once(:teacher) do
teacher = User.create!(name: 'Teacher Miller')
TeacherEnrollment.create!(user: teacher, course: course, workflow_state: 'active')
teacher
end
let_once(:assignment) do
Assignment.create!(
course: course,
workflow_state: 'active',
submission_types: 'online_text_entry',
title: 'an assignment',
description: 'the body'
)
end
before :once do
students.each do |student|
StudentEnrollment.create!(user: student, course: course, workflow_state: 'active')
end
end
it 'finds all submissions' do
results = SubmissionSearch.new(assignment, teacher, nil, order_by: [{field: 'username'}]).search
expect(results.preload(:user).map(&:user)).to eq students
end
it 'finds submissions with user name search' do
results = SubmissionSearch.new(assignment, teacher, nil,
user_search: 'man',
order_by: [{field: 'username', direction: 'descending'}]).search
expect(results).to eq [
Submission.find_by(user: mandy),
Submission.find_by(user: amanda),
]
end
it 'filters for the specified workflow state' do
assignment.submit_homework(amanda, submission_type: 'online_text_entry', body: 'submission')
results = SubmissionSearch.new(assignment, teacher, nil, states: ['submitted']).search
expect(results).to eq [Submission.find_by(user: amanda)]
end
it 'filters results to specified sections' do
section = course.course_sections.create!
StudentEnrollment.create!(user: amanda, course: course, course_section: section, workflow_state: 'active')
results = SubmissionSearch.new(assignment, teacher, nil, section_ids: [section.id]).search
expect(results).to eq [Submission.find_by(user: amanda)]
end
it 'filters by the enrollment type' do
fake_student = assignment.course.student_view_student
results = SubmissionSearch.new(assignment, teacher, nil, enrollment_types: ['StudentEnrollment']).search
expect(results).not_to include Submission.find_by(user: fake_student)
end
it 'filters by scored less than' do
assignment.grade_student(amanda, score: 42, grader: teacher)
assignment.grade_student(mandy, score: 10, grader: teacher)
results = SubmissionSearch.new(assignment, teacher, nil, scored_less_than: 42).search
expect(results).to eq [Submission.find_by(user: mandy)]
end
it 'filters by scored greater than' do
assignment.grade_student(amanda, score: 42, grader: teacher)
assignment.grade_student(mandy, score: 10, grader: teacher)
results = SubmissionSearch.new(assignment, teacher, nil, scored_more_than: 10).search
expect(results).to eq [Submission.find_by(user: amanda)]
end
it "limits results to just the user's submission if the user is a student" do
results = SubmissionSearch.new(assignment, amanda, nil, {}).search
expect(results).to eq [Submission.find_by(user: amanda)]
end
# order by username tested above
it 'orders by submission score' do
assignment.grade_student(peter, score: 1, grader: teacher)
assignment.grade_student(amanda, score: 2, grader: teacher)
assignment.grade_student(james, score: 3, grader: teacher)
results = SubmissionSearch.new(assignment, teacher, nil, scored_more_than: 0, order_by: [{field: 'score'}]).search
expect(results.preload(:user).map(&:user)).to eq [peter, amanda, james]
end
it 'orders by submission date' do
Timecop.freeze do
assignment.submit_homework(peter, submission_type: 'online_text_entry', body: 'homework', submitted_at: Time.zone.now)
assignment.submit_homework(amanda, submission_type: 'online_text_entry', body: 'homework', submitted_at: Time.zone.now + 1.hour)
results = SubmissionSearch.new(assignment, teacher, nil, states: 'submitted', order_by: [{field: 'submitted_at'}]).search
expect(results.preload(:user).map(&:user)).to eq [peter, amanda]
end
end
it 'orders by multiple fields' do
assignment.grade_student(peter, score: 1, grader: teacher)
assignment.grade_student(amanda, score: 1, grader: teacher)
assignment.grade_student(james, score: 3, grader: teacher)
results = SubmissionSearch.new(assignment, teacher, nil,
scored_more_than: 0,
order_by: [
{field: 'score', direction: 'descending'},
{field: 'username', direction: 'ascending'}
]
).search
expect(results.preload(:user).map(&:user)).to eq [james, amanda, peter]
end
# TODO: implement
it 'filters results to assigned users if assigned_only filter is set'
it 'filters results to specified groups'
it 'orders by submission status' # missing, late, etc.
end