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:
parent
f9cebc6fbc
commit
388bc15684
|
@ -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
|
||||
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)
|
||||
argument :filter, SubmissionSearchFilterInputType, required: false
|
||||
argument :order_by, [SubmissionSearchOrderInputType], required: false
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -1,121 +1,5 @@
|
|||
{
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"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)"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"warning_type": "SQL Injection",
|
||||
"warning_code": 0,
|
||||
|
@ -155,8 +39,144 @@
|
|||
"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"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"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": "2018-10-17 14:57:27 -0700",
|
||||
"brakeman_version": "4.3.1"
|
||||
"updated": "2019-03-21 16:01:17 -0600",
|
||||
"brakeman_version": "4.5.0"
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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"})
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue