2020-10-27 00:50:13 +08:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2011-05-13 05:48:18 +08:00
|
|
|
#
|
2017-04-28 03:58:43 +08:00
|
|
|
# Copyright (C) 2011 - present Instructure, Inc.
|
2011-05-13 05:48:18 +08:00
|
|
|
#
|
|
|
|
# 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 Api
|
2011-09-13 23:21:00 +08:00
|
|
|
# find id in collection, by either id or sis_*_id
|
|
|
|
# if the collection is over the users table, `self` is replaced by @current_user.id
|
2023-03-17 03:14:04 +08:00
|
|
|
# if `writable` is true and a shadow record is found, the corresponding primary record will be returned
|
|
|
|
# otherwise a read-only shadow record will be returned, to avoid a silent failure when attempting to save it
|
|
|
|
def api_find(collection, id, account: nil, writable: infer_writable_from_request_method)
|
2023-06-02 06:06:09 +08:00
|
|
|
result = api_find_all(collection, [id], account:).first
|
2015-09-02 04:14:10 +08:00
|
|
|
raise(ActiveRecord::RecordNotFound, "Couldn't find #{collection.name} with API id '#{id}'") unless result
|
2021-09-23 00:25:11 +08:00
|
|
|
|
2023-03-17 03:14:04 +08:00
|
|
|
if result.shadow_record?
|
|
|
|
if writable
|
|
|
|
result.reload
|
|
|
|
else
|
|
|
|
result.readonly!
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2015-09-02 04:14:10 +08:00
|
|
|
result
|
2011-11-15 04:29:30 +08:00
|
|
|
end
|
2011-05-13 05:48:18 +08:00
|
|
|
|
2015-09-02 04:14:10 +08:00
|
|
|
def api_find_all(collection, ids, account: nil)
|
2011-11-15 04:29:30 +08:00
|
|
|
if collection.table_name == User.table_name && @current_user
|
|
|
|
ids = ids.map { |id| (id == "self") ? @current_user.id : id }
|
2011-09-13 23:21:00 +08:00
|
|
|
end
|
2012-10-20 04:25:17 +08:00
|
|
|
if collection.table_name == Account.table_name
|
|
|
|
ids = ids.map do |id|
|
|
|
|
case id
|
|
|
|
when "self"
|
|
|
|
@domain_root_account.id
|
|
|
|
when "default"
|
|
|
|
Account.default.id
|
|
|
|
when "site_admin"
|
|
|
|
Account.site_admin.id
|
|
|
|
else
|
|
|
|
id
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2014-02-12 07:46:44 +08:00
|
|
|
if collection.table_name == EnrollmentTerm.table_name
|
|
|
|
current_term = nil
|
|
|
|
ids = ids.map do |id|
|
|
|
|
case id
|
|
|
|
when "default"
|
|
|
|
@domain_root_account.default_enrollment_term
|
|
|
|
when "current"
|
|
|
|
unless current_term
|
2018-01-16 13:49:45 +08:00
|
|
|
current_terms = @domain_root_account
|
|
|
|
.enrollment_terms
|
|
|
|
.active
|
|
|
|
.where("(start_at<=? OR start_at IS NULL) AND (end_at >=? OR end_at IS NULL) AND NOT (start_at IS NULL AND end_at IS NULL)", Time.now.utc, Time.now.utc)
|
|
|
|
.limit(2)
|
|
|
|
.to_a
|
2014-02-12 07:46:44 +08:00
|
|
|
current_term = (current_terms.length == 1) ? current_terms.first : :nil
|
|
|
|
end
|
|
|
|
(current_term == :nil) ? nil : current_term
|
|
|
|
else
|
|
|
|
id
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2015-09-02 04:14:10 +08:00
|
|
|
Api.sis_relation_for_collection(collection, ids, account || @domain_root_account, @current_user)
|
2011-05-13 05:48:18 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
# map a list of ids and/or sis ids to plain ids.
|
2011-10-05 00:31:34 +08:00
|
|
|
# sis ids that can't be found in the db won't appear in the result, however
|
|
|
|
# AR object ids aren't verified to exist in the db so they'll still be
|
|
|
|
# returned in the result.
|
2014-03-15 05:53:14 +08:00
|
|
|
def self.map_ids(ids, collection, root_account, current_user = nil)
|
2011-11-15 04:29:30 +08:00
|
|
|
sis_mapping = sis_find_sis_mapping_for_collection(collection)
|
2023-04-29 04:59:51 +08:00
|
|
|
columns = sis_parse_ids(ids,
|
|
|
|
sis_mapping[:lookups],
|
|
|
|
current_user,
|
2023-06-02 06:06:09 +08:00
|
|
|
root_account:)
|
2022-05-14 06:15:35 +08:00
|
|
|
result = columns.delete(sis_mapping[:lookups]["id"]) || { ids: [] }
|
2011-11-15 04:29:30 +08:00
|
|
|
unless columns.empty?
|
2015-09-02 04:14:10 +08:00
|
|
|
relation = relation_for_sis_mapping_and_columns(collection, columns, sis_mapping, root_account)
|
|
|
|
# pluck ignores eager_load
|
|
|
|
relation = relation.joins(*relation.eager_load_values) if relation.eager_load_values.present?
|
2022-05-14 06:15:35 +08:00
|
|
|
result[:ids].concat relation.pluck(:id)
|
|
|
|
result[:ids].uniq!
|
|
|
|
result[:ids]
|
2011-10-05 00:31:34 +08:00
|
|
|
end
|
2022-05-14 06:15:35 +08:00
|
|
|
result[:ids]
|
2011-05-13 05:48:18 +08:00
|
|
|
end
|
|
|
|
|
2011-11-10 00:50:16 +08:00
|
|
|
SIS_MAPPINGS = {
|
2011-10-26 23:06:14 +08:00
|
|
|
"courses" =>
|
2015-09-02 04:14:10 +08:00
|
|
|
{ lookups: { "sis_course_id" => "sis_source_id",
|
|
|
|
"id" => "id",
|
|
|
|
"sis_integration_id" => "integration_id",
|
2018-05-11 22:52:54 +08:00
|
|
|
"lti_context_id" => "lti_context_id",
|
|
|
|
"uuid" => "uuid" }.freeze,
|
2015-09-02 04:14:10 +08:00
|
|
|
is_not_scoped_to_account: ["id"].freeze,
|
|
|
|
scope: "root_account_id" }.freeze,
|
2011-10-26 23:06:14 +08:00
|
|
|
"enrollment_terms" =>
|
2015-09-02 04:14:10 +08:00
|
|
|
{ lookups: { "sis_term_id" => "sis_source_id",
|
|
|
|
"id" => "id",
|
|
|
|
"sis_integration_id" => "integration_id" }.freeze,
|
|
|
|
is_not_scoped_to_account: ["id"].freeze,
|
|
|
|
scope: "root_account_id" }.freeze,
|
2011-10-26 23:06:14 +08:00
|
|
|
"users" =>
|
2015-09-02 04:14:10 +08:00
|
|
|
{ lookups: { "sis_user_id" => "pseudonyms.sis_user_id",
|
2015-10-06 02:35:39 +08:00
|
|
|
"sis_login_id" => {
|
|
|
|
column: "LOWER(pseudonyms.unique_id)",
|
|
|
|
transform: ->(id) { QuotedValue.new("LOWER(#{Pseudonym.connection.quote(id)})") }
|
|
|
|
},
|
2015-09-02 04:14:10 +08:00
|
|
|
"id" => "users.id",
|
|
|
|
"sis_integration_id" => "pseudonyms.integration_id",
|
2021-08-05 04:02:54 +08:00
|
|
|
"lti_context_id" => "users.lti_context_id", # leaving for legacy reasons
|
2022-05-14 06:15:35 +08:00
|
|
|
"lti_user_id" => {
|
|
|
|
column: [
|
|
|
|
"users.lti_context_id",
|
|
|
|
"user_past_lti_ids.user_lti_context_id",
|
|
|
|
],
|
|
|
|
joins_needed_for_query: [:past_lti_ids],
|
|
|
|
},
|
2021-08-05 04:02:54 +08:00
|
|
|
"lti_1_1_id" => "users.lti_context_id",
|
|
|
|
"lti_1_3_id" => "users.lti_id",
|
2017-05-09 21:46:44 +08:00
|
|
|
"uuid" => "users.uuid" }.freeze,
|
2022-05-14 06:15:35 +08:00
|
|
|
is_not_scoped_to_account: ["users.id", "users.lti_context_id", "user_past_lti_ids.user_lti_context_id", "users.lti_id", "users.uuid"].freeze,
|
2011-11-15 04:29:30 +08:00
|
|
|
scope: "pseudonyms.account_id",
|
2015-09-02 04:14:10 +08:00
|
|
|
joins: :pseudonym }.freeze,
|
2011-10-26 23:06:14 +08:00
|
|
|
"accounts" =>
|
2015-09-02 04:14:10 +08:00
|
|
|
{ lookups: { "sis_account_id" => "sis_source_id",
|
|
|
|
"id" => "id",
|
|
|
|
"sis_integration_id" => "integration_id",
|
2021-03-18 03:45:33 +08:00
|
|
|
"lti_context_id" => "lti_context_id",
|
|
|
|
"uuid" => "uuid" }.freeze,
|
|
|
|
is_not_scoped_to_account: %w[id lti_context_id uuid].freeze,
|
2015-09-02 04:14:10 +08:00
|
|
|
scope: "root_account_id" }.freeze,
|
2011-10-26 23:06:14 +08:00
|
|
|
"course_sections" =>
|
2015-09-02 04:14:10 +08:00
|
|
|
{ lookups: { "sis_section_id" => "sis_source_id",
|
|
|
|
"id" => "id",
|
|
|
|
"sis_integration_id" => "integration_id" }.freeze,
|
|
|
|
is_not_scoped_to_account: ["id"].freeze,
|
|
|
|
scope: "root_account_id" }.freeze,
|
2013-06-20 11:38:00 +08:00
|
|
|
"groups" =>
|
2015-09-02 04:14:10 +08:00
|
|
|
{ lookups: { "sis_group_id" => "sis_source_id",
|
2022-12-03 01:32:35 +08:00
|
|
|
"lti_context_id" => "lti_context_id",
|
2015-09-02 04:14:10 +08:00
|
|
|
"id" => "id" }.freeze,
|
|
|
|
is_not_scoped_to_account: ["id"].freeze,
|
|
|
|
scope: "root_account_id" }.freeze,
|
2018-01-04 05:24:26 +08:00
|
|
|
"group_categories" =>
|
|
|
|
{ lookups: { "sis_group_category_id" => "sis_source_id",
|
|
|
|
"id" => "id" }.freeze,
|
|
|
|
is_not_scoped_to_account: ["id"].freeze,
|
|
|
|
scope: "root_account_id" }.freeze,
|
2019-06-26 04:26:05 +08:00
|
|
|
"assignments" =>
|
|
|
|
{ lookups: { "sis_assignment_id" => "sis_source_id",
|
|
|
|
"id" => "id",
|
|
|
|
"lti_context_id" => "lti_context_id" }.freeze,
|
|
|
|
is_not_scoped_to_account: ["id"].freeze,
|
|
|
|
scope: "root_account_id" }.freeze,
|
2011-11-10 00:50:16 +08:00
|
|
|
}.freeze
|
2011-09-13 23:21:00 +08:00
|
|
|
|
2021-09-27 23:58:39 +08:00
|
|
|
MAX_ID = ((2**63) - 1)
|
2020-11-14 04:50:42 +08:00
|
|
|
MAX_ID_LENGTH = MAX_ID.to_s.length
|
2023-06-02 06:06:09 +08:00
|
|
|
MAX_ID_RANGE = (-MAX_ID...MAX_ID)
|
|
|
|
ID_REGEX = /\A\d{1,#{MAX_ID_LENGTH}}\z/
|
|
|
|
UUID_REGEX = /\Auuid:(\w{40,})\z/
|
2012-10-20 04:25:17 +08:00
|
|
|
|
2022-05-14 06:15:35 +08:00
|
|
|
def self.not_scoped_to_account?(columns, sis_mapping)
|
|
|
|
flattened_array_of_columns = [columns].flatten
|
|
|
|
not_scoped_to_account_columns = sis_mapping[:is_not_scoped_to_account] || []
|
|
|
|
(flattened_array_of_columns - not_scoped_to_account_columns).empty?
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.sis_parse_id(id, _current_user = nil,
|
2016-03-30 01:58:45 +08:00
|
|
|
root_account: nil)
|
2022-05-14 06:15:35 +08:00
|
|
|
# returns sis_column_name, column_value
|
|
|
|
return "id", id if id.is_a?(Numeric) || id.is_a?(ActiveRecord::Base)
|
2021-09-23 00:25:11 +08:00
|
|
|
|
2011-11-15 04:29:30 +08:00
|
|
|
id = id.to_s.strip
|
2014-05-22 12:55:43 +08:00
|
|
|
case id
|
|
|
|
when /\Ahex:(lti_[\w_]+|sis_[\w_]+):(([0-9A-Fa-f]{2})+)\z/
|
2011-11-15 04:29:30 +08:00
|
|
|
sis_column = $1
|
|
|
|
sis_id = [$2].pack("H*")
|
2014-05-22 12:55:43 +08:00
|
|
|
when /\A(lti_[\w_]+|sis_[\w_]+):(.+)\z/
|
2011-11-15 04:29:30 +08:00
|
|
|
sis_column = $1
|
|
|
|
sis_id = $2
|
2021-11-12 01:20:36 +08:00
|
|
|
when ID_REGEX
|
2022-05-14 06:15:35 +08:00
|
|
|
return "id", (/\A\d+\z/.match?(id) ? id.to_i : id)
|
2021-03-18 03:45:33 +08:00
|
|
|
when UUID_REGEX
|
2022-05-14 06:15:35 +08:00
|
|
|
return "uuid", $1
|
2011-05-13 05:48:18 +08:00
|
|
|
else
|
2011-11-15 04:29:30 +08:00
|
|
|
return nil, nil
|
|
|
|
end
|
|
|
|
|
2022-05-14 06:15:35 +08:00
|
|
|
[sis_column, sis_id]
|
2011-11-15 04:29:30 +08:00
|
|
|
end
|
2011-05-13 05:48:18 +08:00
|
|
|
|
2016-03-30 01:58:45 +08:00
|
|
|
def self.sis_parse_ids(ids, lookups, current_user = nil, root_account: nil)
|
2022-05-14 06:15:35 +08:00
|
|
|
# returns an object like {
|
|
|
|
# "column_name" => {
|
|
|
|
# ids: [column_value, ...].uniq,
|
|
|
|
# joins_needed_for_query: [relation_name, ...] <-- optional
|
|
|
|
# }
|
|
|
|
# }
|
2011-11-15 04:29:30 +08:00
|
|
|
columns = {}
|
|
|
|
ids.compact.each do |id|
|
2023-06-02 06:06:09 +08:00
|
|
|
sis_column, sis_id = sis_parse_id(id, current_user, root_account:)
|
2022-05-14 06:15:35 +08:00
|
|
|
|
|
|
|
next unless sis_column && sis_id
|
|
|
|
|
|
|
|
column = lookups[sis_column]
|
|
|
|
if column.is_a?(Hash)
|
|
|
|
column_name = column[:column]
|
|
|
|
|
|
|
|
if column[:transform]
|
|
|
|
if sis_id.is_a? Array
|
|
|
|
# this means that the MRA override sis_parse_id function turned sis_id into [sis_id, @account]
|
|
|
|
sis_id[0] = column[:transform].call(sis_id[0])
|
|
|
|
else
|
|
|
|
sis_id = column[:transform].call(sis_id)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
if (joins_needed_for_query = column[:joins_needed_for_query])
|
|
|
|
columns[column_name] ||= {}
|
|
|
|
columns[column_name][:joins_needed_for_query] ||= []
|
|
|
|
columns[column_name][:joins_needed_for_query] << joins_needed_for_query
|
|
|
|
end
|
|
|
|
column = column_name
|
|
|
|
end
|
2021-09-23 00:25:11 +08:00
|
|
|
|
2022-05-14 06:15:35 +08:00
|
|
|
next unless column
|
|
|
|
|
|
|
|
columns[column] ||= {}
|
|
|
|
columns[column][:ids] ||= []
|
|
|
|
columns[column][:ids] << sis_id
|
2011-11-15 04:29:30 +08:00
|
|
|
end
|
2022-05-14 06:15:35 +08:00
|
|
|
columns.each_key { |key| columns[key][:ids].uniq! }
|
2011-11-15 04:29:30 +08:00
|
|
|
columns
|
|
|
|
end
|
|
|
|
|
2013-02-13 00:50:20 +08:00
|
|
|
# remove things that don't look like valid database IDs
|
|
|
|
# return in integer format if possible
|
|
|
|
# (note that ID_REGEX may be redefined by a plugin!)
|
|
|
|
def self.map_non_sis_ids(ids)
|
|
|
|
ids.map { |id| id.to_s.strip }.grep(ID_REGEX).map do |id|
|
2021-11-12 01:20:36 +08:00
|
|
|
/\A\d+\z/.match?(id) ? id.to_i : id
|
2013-02-13 00:50:20 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2011-11-15 04:29:30 +08:00
|
|
|
def self.sis_find_sis_mapping_for_collection(collection)
|
|
|
|
SIS_MAPPINGS[collection.table_name] or
|
2011-09-13 23:21:00 +08:00
|
|
|
raise(ArgumentError, "need to add support for table name: #{collection.table_name}")
|
2011-11-15 04:29:30 +08:00
|
|
|
end
|
|
|
|
|
2015-09-02 04:14:10 +08:00
|
|
|
def self.sis_relation_for_collection(collection, ids, sis_root_account, current_user = nil)
|
|
|
|
relation_for_sis_mapping(collection,
|
|
|
|
sis_find_sis_mapping_for_collection(collection),
|
|
|
|
ids,
|
|
|
|
sis_root_account,
|
|
|
|
current_user)
|
2011-11-15 04:29:30 +08:00
|
|
|
end
|
|
|
|
|
2015-09-02 04:14:10 +08:00
|
|
|
def self.relation_for_sis_mapping(relation, sis_mapping, ids, sis_root_account, current_user = nil)
|
|
|
|
relation_for_sis_mapping_and_columns(relation,
|
2016-05-19 02:35:01 +08:00
|
|
|
sis_parse_ids(ids,
|
|
|
|
sis_mapping[:lookups],
|
|
|
|
current_user,
|
|
|
|
root_account: sis_root_account),
|
2015-09-02 04:14:10 +08:00
|
|
|
sis_mapping,
|
|
|
|
sis_root_account)
|
2011-11-15 04:29:30 +08:00
|
|
|
end
|
2011-09-13 23:21:00 +08:00
|
|
|
|
2015-09-02 04:14:10 +08:00
|
|
|
def self.relation_for_sis_mapping_and_columns(relation, columns, sis_mapping, sis_root_account)
|
2011-11-15 04:29:30 +08:00
|
|
|
raise ArgumentError, "sis_root_account required for lookups" unless sis_root_account.is_a?(Account)
|
2015-09-02 04:14:10 +08:00
|
|
|
return relation.none if columns.empty?
|
2021-09-23 00:25:11 +08:00
|
|
|
|
2017-05-02 04:01:50 +08:00
|
|
|
relation = relation.all unless relation.is_a?(ActiveRecord::Relation)
|
2011-11-15 04:29:30 +08:00
|
|
|
|
2022-05-14 06:15:35 +08:00
|
|
|
if columns.keys.flatten.length == 1 && not_scoped_to_account?(columns.keys.first, sis_mapping)
|
|
|
|
queryable_columns = {}
|
|
|
|
columns.each_pair { |column_name, value| queryable_columns[column_name] = value[:ids] }
|
|
|
|
relation = relation.where(queryable_columns)
|
2012-10-20 04:25:17 +08:00
|
|
|
else
|
|
|
|
args = []
|
|
|
|
query = []
|
2022-05-14 06:15:35 +08:00
|
|
|
columns.each_key do |column|
|
|
|
|
relation = relation.left_outer_joins(columns[column][:joins_needed_for_query]) if columns[column][:joins_needed_for_query]
|
|
|
|
if not_scoped_to_account?(column, sis_mapping)
|
|
|
|
conditions = []
|
|
|
|
if column.is_a?(Array)
|
|
|
|
column.each do |column_name|
|
|
|
|
conditions << "#{column_name} IN (?)"
|
|
|
|
args << columns[column][:ids]
|
|
|
|
end
|
|
|
|
else
|
|
|
|
conditions << "#{column} IN (?)"
|
|
|
|
args << columns[column][:ids]
|
|
|
|
end
|
|
|
|
query << conditions.join(" OR ").to_s
|
2012-10-20 04:25:17 +08:00
|
|
|
else
|
|
|
|
raise ArgumentError, "missing scope for collection" unless sis_mapping[:scope]
|
2021-09-23 00:25:11 +08:00
|
|
|
|
2022-05-14 06:15:35 +08:00
|
|
|
ids = columns[column][:ids]
|
2021-11-16 06:24:21 +08:00
|
|
|
if ids.any?(Array)
|
2014-03-15 05:53:14 +08:00
|
|
|
ids_hash = {}
|
|
|
|
ids.each do |id|
|
|
|
|
id = Array(id)
|
|
|
|
account = id.last || sis_root_account
|
|
|
|
ids_hash[account] ||= []
|
|
|
|
ids_hash[account] << id.first
|
|
|
|
end
|
|
|
|
else
|
|
|
|
ids_hash = { sis_root_account => ids }
|
|
|
|
end
|
2017-05-02 04:01:50 +08:00
|
|
|
Shard.partition_by_shard(ids_hash.keys) do |root_accounts_on_shard|
|
|
|
|
sub_query = []
|
|
|
|
sub_args = []
|
|
|
|
root_accounts_on_shard.each do |root_account|
|
|
|
|
ids = ids_hash[root_account]
|
2022-05-14 06:15:35 +08:00
|
|
|
conditions = []
|
|
|
|
if column.is_a?(Array)
|
|
|
|
column.each do |column_name|
|
|
|
|
conditions << "#{column_name} IN (?)"
|
|
|
|
sub_args << ids
|
|
|
|
end
|
|
|
|
else
|
|
|
|
conditions << "#{column} IN (?)"
|
|
|
|
sub_args << ids
|
|
|
|
end
|
|
|
|
sub_query << "(#{sis_mapping[:scope]} = #{root_account.id} AND (#{conditions.join(" OR ")}))"
|
2017-05-02 04:01:50 +08:00
|
|
|
end
|
|
|
|
if Shard.current == relation.primary_shard
|
|
|
|
query.concat(sub_query)
|
|
|
|
args.concat(sub_args)
|
|
|
|
else
|
|
|
|
raise "cross-shard non-ID Api lookups are only supported for users" unless relation.klass == User
|
2021-09-23 00:25:11 +08:00
|
|
|
|
2017-05-02 04:01:50 +08:00
|
|
|
sub_args.unshift(sub_query.join(" OR "))
|
|
|
|
users = relation.klass.joins(sis_mapping[:joins]).where(*sub_args).select(:id, :updated_at).to_a
|
|
|
|
User.preload_shard_associations(users)
|
|
|
|
users.each { |u| u.associate_with_shard(relation.primary_shard, :shadow) }
|
|
|
|
query << "#{relation.table_name}.id IN (?)"
|
|
|
|
args << users
|
|
|
|
end
|
2014-03-15 05:53:14 +08:00
|
|
|
end
|
2012-10-20 04:25:17 +08:00
|
|
|
end
|
2011-09-13 23:21:00 +08:00
|
|
|
end
|
2012-10-20 04:25:17 +08:00
|
|
|
|
|
|
|
args.unshift(query.join(" OR "))
|
2015-09-02 04:14:10 +08:00
|
|
|
relation = relation.where(*args)
|
2022-02-12 01:30:39 +08:00
|
|
|
relation
|
2011-07-06 06:23:35 +08:00
|
|
|
end
|
2011-11-15 04:29:30 +08:00
|
|
|
|
2015-09-02 04:14:10 +08:00
|
|
|
relation = relation.eager_load(sis_mapping[:joins]) if sis_mapping[:joins]
|
|
|
|
relation
|
2011-07-06 06:23:35 +08:00
|
|
|
end
|
2012-01-04 04:30:49 +08:00
|
|
|
|
2020-04-30 08:20:49 +08:00
|
|
|
def self.max_per_page(action = nil)
|
|
|
|
result = Setting.get("api_max_per_page_#{action}", nil)&.to_i if action
|
|
|
|
result || Setting.get("api_max_per_page", "50").to_i
|
2013-11-07 05:03:21 +08:00
|
|
|
end
|
|
|
|
|
2020-04-30 08:20:49 +08:00
|
|
|
def self.per_page(action = nil)
|
|
|
|
result = Setting.get("api_per_page_#{action}", nil)&.to_i if action
|
|
|
|
result || Setting.get("api_per_page", "10").to_i
|
2016-03-04 05:21:29 +08:00
|
|
|
end
|
|
|
|
|
2013-11-08 05:37:20 +08:00
|
|
|
def self.per_page_for(controller, options = {})
|
2020-04-30 08:20:49 +08:00
|
|
|
action = "#{controller.params[:controller]}##{controller.params[:action]}"
|
|
|
|
per_page_requested = controller.params[:per_page] || options[:default] || per_page(action)
|
|
|
|
max = options[:max] || max_per_page(action)
|
2023-04-05 06:26:44 +08:00
|
|
|
per_page_requested.to_i.clamp(1, max.to_i)
|
2012-08-28 02:48:38 +08:00
|
|
|
end
|
|
|
|
|
2011-07-22 00:35:46 +08:00
|
|
|
# Add [link HTTP Headers](http://www.w3.org/Protocols/9707-link-header.html) for pagination
|
2011-09-02 23:34:12 +08:00
|
|
|
# The collection needs to be a will_paginate collection (or act like one)
|
2011-08-23 00:47:40 +08:00
|
|
|
# a new, paginated collection will be returned
|
2013-09-26 03:01:06 +08:00
|
|
|
def self.paginate(collection, controller, base_url, pagination_args = {}, response_args = {})
|
2020-01-11 05:55:16 +08:00
|
|
|
collection = ordered_collection(collection)
|
import ActiveModel::Serializers port and convert quizzes api to it
test plan:
- The quiz api should work like it normally does when you don't pass
an 'Accept: application/vnd.api+json' header.
- The quizzes index page and quiz edit page should work like they
always do.
- Testing the Quizzes API for "jsonapi" style:
- For all requests, you MUST have the "Accept" header set to
"application/vnd.api+json"
- Test all the endpoints (PUT, POST, GET, INDEX, DELETE) like you
normally would, except you'll need to format the data according to
the next few steps:
- For "POST" and "PUT" (create and update) requests, you should send
the data like: { "quizzes": [ { id: 1, title: "blah" } ]
- For all requests (except DELETE), you should get back a response
that looks like: { "quizzes": [ { quiz you requested } ]
- For the "delete" action, you should get a "no content" response
and the request should be successful
Change-Id: Ie91deaeb6772cbe52a0fc46a28ab93a4e3036061
Reviewed-on: https://gerrit.instructure.com/25997
Reviewed-by: Jacob Fugal <jacob@instructure.com>
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: Caleb Guanzon <cguanzon@instructure.com>
Product-Review: Stanley Stuart <stanley@instructure.com>
2013-12-05 03:06:32 +08:00
|
|
|
collection = paginate_collection!(collection, controller, pagination_args)
|
2013-09-26 03:01:06 +08:00
|
|
|
hash = build_links_hash(base_url, meta_for_pagination(controller, collection))
|
|
|
|
links = build_links_from_hash(hash)
|
import ActiveModel::Serializers port and convert quizzes api to it
test plan:
- The quiz api should work like it normally does when you don't pass
an 'Accept: application/vnd.api+json' header.
- The quizzes index page and quiz edit page should work like they
always do.
- Testing the Quizzes API for "jsonapi" style:
- For all requests, you MUST have the "Accept" header set to
"application/vnd.api+json"
- Test all the endpoints (PUT, POST, GET, INDEX, DELETE) like you
normally would, except you'll need to format the data according to
the next few steps:
- For "POST" and "PUT" (create and update) requests, you should send
the data like: { "quizzes": [ { id: 1, title: "blah" } ]
- For all requests (except DELETE), you should get back a response
that looks like: { "quizzes": [ { quiz you requested } ]
- For the "delete" action, you should get a "no content" response
and the request should be successful
Change-Id: Ie91deaeb6772cbe52a0fc46a28ab93a4e3036061
Reviewed-on: https://gerrit.instructure.com/25997
Reviewed-by: Jacob Fugal <jacob@instructure.com>
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: Caleb Guanzon <cguanzon@instructure.com>
Product-Review: Stanley Stuart <stanley@instructure.com>
2013-12-05 03:06:32 +08:00
|
|
|
controller.response.headers["Link"] = links.join(",") unless links.empty?
|
2013-09-26 03:01:06 +08:00
|
|
|
if response_args[:enhanced_return]
|
2023-06-02 06:06:09 +08:00
|
|
|
{ hash:, collection: }
|
2013-09-26 03:01:06 +08:00
|
|
|
else
|
|
|
|
collection
|
|
|
|
end
|
import ActiveModel::Serializers port and convert quizzes api to it
test plan:
- The quiz api should work like it normally does when you don't pass
an 'Accept: application/vnd.api+json' header.
- The quizzes index page and quiz edit page should work like they
always do.
- Testing the Quizzes API for "jsonapi" style:
- For all requests, you MUST have the "Accept" header set to
"application/vnd.api+json"
- Test all the endpoints (PUT, POST, GET, INDEX, DELETE) like you
normally would, except you'll need to format the data according to
the next few steps:
- For "POST" and "PUT" (create and update) requests, you should send
the data like: { "quizzes": [ { id: 1, title: "blah" } ]
- For all requests (except DELETE), you should get back a response
that looks like: { "quizzes": [ { quiz you requested } ]
- For the "delete" action, you should get a "no content" response
and the request should be successful
Change-Id: Ie91deaeb6772cbe52a0fc46a28ab93a4e3036061
Reviewed-on: https://gerrit.instructure.com/25997
Reviewed-by: Jacob Fugal <jacob@instructure.com>
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: Caleb Guanzon <cguanzon@instructure.com>
Product-Review: Stanley Stuart <stanley@instructure.com>
2013-12-05 03:06:32 +08:00
|
|
|
end
|
|
|
|
|
2020-01-11 05:55:16 +08:00
|
|
|
def self.ordered_collection(collection)
|
|
|
|
if collection.is_a?(ActiveRecord::Relation) && collection.order_values.blank?
|
|
|
|
collection = collection.order(collection.primary_key.to_sym)
|
|
|
|
end
|
|
|
|
collection
|
|
|
|
end
|
|
|
|
|
import ActiveModel::Serializers port and convert quizzes api to it
test plan:
- The quiz api should work like it normally does when you don't pass
an 'Accept: application/vnd.api+json' header.
- The quizzes index page and quiz edit page should work like they
always do.
- Testing the Quizzes API for "jsonapi" style:
- For all requests, you MUST have the "Accept" header set to
"application/vnd.api+json"
- Test all the endpoints (PUT, POST, GET, INDEX, DELETE) like you
normally would, except you'll need to format the data according to
the next few steps:
- For "POST" and "PUT" (create and update) requests, you should send
the data like: { "quizzes": [ { id: 1, title: "blah" } ]
- For all requests (except DELETE), you should get back a response
that looks like: { "quizzes": [ { quiz you requested } ]
- For the "delete" action, you should get a "no content" response
and the request should be successful
Change-Id: Ie91deaeb6772cbe52a0fc46a28ab93a4e3036061
Reviewed-on: https://gerrit.instructure.com/25997
Reviewed-by: Jacob Fugal <jacob@instructure.com>
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: Caleb Guanzon <cguanzon@instructure.com>
Product-Review: Stanley Stuart <stanley@instructure.com>
2013-12-05 03:06:32 +08:00
|
|
|
# Returns collection as the first return value, and the meta information hash
|
|
|
|
# as the second return value
|
2016-06-09 01:00:29 +08:00
|
|
|
def self.jsonapi_paginate(collection, controller, base_url, pagination_args = {})
|
import ActiveModel::Serializers port and convert quizzes api to it
test plan:
- The quiz api should work like it normally does when you don't pass
an 'Accept: application/vnd.api+json' header.
- The quizzes index page and quiz edit page should work like they
always do.
- Testing the Quizzes API for "jsonapi" style:
- For all requests, you MUST have the "Accept" header set to
"application/vnd.api+json"
- Test all the endpoints (PUT, POST, GET, INDEX, DELETE) like you
normally would, except you'll need to format the data according to
the next few steps:
- For "POST" and "PUT" (create and update) requests, you should send
the data like: { "quizzes": [ { id: 1, title: "blah" } ]
- For all requests (except DELETE), you should get back a response
that looks like: { "quizzes": [ { quiz you requested } ]
- For the "delete" action, you should get a "no content" response
and the request should be successful
Change-Id: Ie91deaeb6772cbe52a0fc46a28ab93a4e3036061
Reviewed-on: https://gerrit.instructure.com/25997
Reviewed-by: Jacob Fugal <jacob@instructure.com>
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: Caleb Guanzon <cguanzon@instructure.com>
Product-Review: Stanley Stuart <stanley@instructure.com>
2013-12-05 03:06:32 +08:00
|
|
|
collection = paginate_collection!(collection, controller, pagination_args)
|
|
|
|
meta = jsonapi_meta(collection, controller, base_url)
|
2016-06-09 01:00:29 +08:00
|
|
|
hash = build_links_hash(base_url, meta_for_pagination(controller, collection))
|
|
|
|
links = build_links_from_hash(hash)
|
|
|
|
controller.response.headers["Link"] = links.join(",") unless links.empty?
|
import ActiveModel::Serializers port and convert quizzes api to it
test plan:
- The quiz api should work like it normally does when you don't pass
an 'Accept: application/vnd.api+json' header.
- The quizzes index page and quiz edit page should work like they
always do.
- Testing the Quizzes API for "jsonapi" style:
- For all requests, you MUST have the "Accept" header set to
"application/vnd.api+json"
- Test all the endpoints (PUT, POST, GET, INDEX, DELETE) like you
normally would, except you'll need to format the data according to
the next few steps:
- For "POST" and "PUT" (create and update) requests, you should send
the data like: { "quizzes": [ { id: 1, title: "blah" } ]
- For all requests (except DELETE), you should get back a response
that looks like: { "quizzes": [ { quiz you requested } ]
- For the "delete" action, you should get a "no content" response
and the request should be successful
Change-Id: Ie91deaeb6772cbe52a0fc46a28ab93a4e3036061
Reviewed-on: https://gerrit.instructure.com/25997
Reviewed-by: Jacob Fugal <jacob@instructure.com>
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: Caleb Guanzon <cguanzon@instructure.com>
Product-Review: Stanley Stuart <stanley@instructure.com>
2013-12-05 03:06:32 +08:00
|
|
|
[collection, meta]
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.jsonapi_meta(collection, controller, base_url)
|
|
|
|
pagination = meta_for_pagination(controller, collection)
|
|
|
|
|
|
|
|
meta = {
|
|
|
|
per_page: collection.per_page
|
|
|
|
}
|
|
|
|
|
|
|
|
meta.merge!(build_links_hash(base_url, pagination))
|
|
|
|
|
|
|
|
if collection.ordinal_pages?
|
|
|
|
meta[:page] = pagination[:current]
|
|
|
|
meta[:template] = meta[:current].sub(/page=\d+/, "page={page}")
|
|
|
|
end
|
|
|
|
|
|
|
|
meta[:count] = collection.total_entries if collection.total_entries
|
|
|
|
meta[:page_count] = collection.total_pages if collection.total_pages
|
2013-12-19 02:36:42 +08:00
|
|
|
|
import ActiveModel::Serializers port and convert quizzes api to it
test plan:
- The quiz api should work like it normally does when you don't pass
an 'Accept: application/vnd.api+json' header.
- The quizzes index page and quiz edit page should work like they
always do.
- Testing the Quizzes API for "jsonapi" style:
- For all requests, you MUST have the "Accept" header set to
"application/vnd.api+json"
- Test all the endpoints (PUT, POST, GET, INDEX, DELETE) like you
normally would, except you'll need to format the data according to
the next few steps:
- For "POST" and "PUT" (create and update) requests, you should send
the data like: { "quizzes": [ { id: 1, title: "blah" } ]
- For all requests (except DELETE), you should get back a response
that looks like: { "quizzes": [ { quiz you requested } ]
- For the "delete" action, you should get a "no content" response
and the request should be successful
Change-Id: Ie91deaeb6772cbe52a0fc46a28ab93a4e3036061
Reviewed-on: https://gerrit.instructure.com/25997
Reviewed-by: Jacob Fugal <jacob@instructure.com>
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: Caleb Guanzon <cguanzon@instructure.com>
Product-Review: Stanley Stuart <stanley@instructure.com>
2013-12-05 03:06:32 +08:00
|
|
|
{ pagination: meta }
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.paginate_collection!(collection, controller, pagination_args)
|
|
|
|
wrap_pagination_args!(pagination_args, controller)
|
2013-12-19 02:36:42 +08:00
|
|
|
begin
|
|
|
|
paginated = collection.paginate(pagination_args)
|
|
|
|
rescue Folio::InvalidPage
|
2021-08-10 00:44:45 +08:00
|
|
|
# Have to .try(:build_page) because we use some collections (like
|
|
|
|
# PaginatedCollection) that do not conform to the full will_paginate API.
|
|
|
|
if pagination_args[:page].to_s =~ /\d+/ && pagination_args[:page].to_i > 0 && collection.try(:build_page)&.ordinal_pages?
|
2013-12-19 02:36:42 +08:00
|
|
|
# for backwards compatibility we currently require returning [] for
|
|
|
|
# pages beyond the end of an ordinal collection, rather than a 404.
|
|
|
|
paginated = Folio::Ordinal::Page.create
|
|
|
|
paginated.current_page = pagination_args[:page].to_i
|
|
|
|
else
|
|
|
|
# we're not dealing with a simple out-of-bounds on an ordinal
|
|
|
|
# collection, let the exception propagate (and turn into a 404)
|
|
|
|
raise
|
|
|
|
end
|
|
|
|
end
|
import ActiveModel::Serializers port and convert quizzes api to it
test plan:
- The quiz api should work like it normally does when you don't pass
an 'Accept: application/vnd.api+json' header.
- The quizzes index page and quiz edit page should work like they
always do.
- Testing the Quizzes API for "jsonapi" style:
- For all requests, you MUST have the "Accept" header set to
"application/vnd.api+json"
- Test all the endpoints (PUT, POST, GET, INDEX, DELETE) like you
normally would, except you'll need to format the data according to
the next few steps:
- For "POST" and "PUT" (create and update) requests, you should send
the data like: { "quizzes": [ { id: 1, title: "blah" } ]
- For all requests (except DELETE), you should get back a response
that looks like: { "quizzes": [ { quiz you requested } ]
- For the "delete" action, you should get a "no content" response
and the request should be successful
Change-Id: Ie91deaeb6772cbe52a0fc46a28ab93a4e3036061
Reviewed-on: https://gerrit.instructure.com/25997
Reviewed-by: Jacob Fugal <jacob@instructure.com>
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: Caleb Guanzon <cguanzon@instructure.com>
Product-Review: Stanley Stuart <stanley@instructure.com>
2013-12-05 03:06:32 +08:00
|
|
|
paginated
|
2012-09-12 05:12:36 +08:00
|
|
|
end
|
|
|
|
|
import ActiveModel::Serializers port and convert quizzes api to it
test plan:
- The quiz api should work like it normally does when you don't pass
an 'Accept: application/vnd.api+json' header.
- The quizzes index page and quiz edit page should work like they
always do.
- Testing the Quizzes API for "jsonapi" style:
- For all requests, you MUST have the "Accept" header set to
"application/vnd.api+json"
- Test all the endpoints (PUT, POST, GET, INDEX, DELETE) like you
normally would, except you'll need to format the data according to
the next few steps:
- For "POST" and "PUT" (create and update) requests, you should send
the data like: { "quizzes": [ { id: 1, title: "blah" } ]
- For all requests (except DELETE), you should get back a response
that looks like: { "quizzes": [ { quiz you requested } ]
- For the "delete" action, you should get a "no content" response
and the request should be successful
Change-Id: Ie91deaeb6772cbe52a0fc46a28ab93a4e3036061
Reviewed-on: https://gerrit.instructure.com/25997
Reviewed-by: Jacob Fugal <jacob@instructure.com>
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: Caleb Guanzon <cguanzon@instructure.com>
Product-Review: Stanley Stuart <stanley@instructure.com>
2013-12-05 03:06:32 +08:00
|
|
|
def self.wrap_pagination_args!(pagination_args, controller)
|
2017-07-29 05:05:07 +08:00
|
|
|
pagination_args = pagination_args.to_unsafe_h if pagination_args.is_a?(ActionController::Parameters)
|
import ActiveModel::Serializers port and convert quizzes api to it
test plan:
- The quiz api should work like it normally does when you don't pass
an 'Accept: application/vnd.api+json' header.
- The quizzes index page and quiz edit page should work like they
always do.
- Testing the Quizzes API for "jsonapi" style:
- For all requests, you MUST have the "Accept" header set to
"application/vnd.api+json"
- Test all the endpoints (PUT, POST, GET, INDEX, DELETE) like you
normally would, except you'll need to format the data according to
the next few steps:
- For "POST" and "PUT" (create and update) requests, you should send
the data like: { "quizzes": [ { id: 1, title: "blah" } ]
- For all requests (except DELETE), you should get back a response
that looks like: { "quizzes": [ { quiz you requested } ]
- For the "delete" action, you should get a "no content" response
and the request should be successful
Change-Id: Ie91deaeb6772cbe52a0fc46a28ab93a4e3036061
Reviewed-on: https://gerrit.instructure.com/25997
Reviewed-by: Jacob Fugal <jacob@instructure.com>
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: Caleb Guanzon <cguanzon@instructure.com>
Product-Review: Stanley Stuart <stanley@instructure.com>
2013-12-05 03:06:32 +08:00
|
|
|
pagination_args.reverse_merge!(
|
|
|
|
page: controller.params[:page],
|
|
|
|
per_page: per_page_for(controller,
|
|
|
|
default: pagination_args.delete(:default_per_page),
|
|
|
|
max: pagination_args.delete(:max_per_page))
|
2021-09-23 00:25:11 +08:00
|
|
|
)
|
import ActiveModel::Serializers port and convert quizzes api to it
test plan:
- The quiz api should work like it normally does when you don't pass
an 'Accept: application/vnd.api+json' header.
- The quizzes index page and quiz edit page should work like they
always do.
- Testing the Quizzes API for "jsonapi" style:
- For all requests, you MUST have the "Accept" header set to
"application/vnd.api+json"
- Test all the endpoints (PUT, POST, GET, INDEX, DELETE) like you
normally would, except you'll need to format the data according to
the next few steps:
- For "POST" and "PUT" (create and update) requests, you should send
the data like: { "quizzes": [ { id: 1, title: "blah" } ]
- For all requests (except DELETE), you should get back a response
that looks like: { "quizzes": [ { quiz you requested } ]
- For the "delete" action, you should get a "no content" response
and the request should be successful
Change-Id: Ie91deaeb6772cbe52a0fc46a28ab93a4e3036061
Reviewed-on: https://gerrit.instructure.com/25997
Reviewed-by: Jacob Fugal <jacob@instructure.com>
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: Caleb Guanzon <cguanzon@instructure.com>
Product-Review: Stanley Stuart <stanley@instructure.com>
2013-12-05 03:06:32 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def self.meta_for_pagination(controller, collection)
|
|
|
|
{
|
|
|
|
query_parameters: controller.request.query_parameters,
|
|
|
|
per_page: collection.per_page,
|
|
|
|
current: collection.current_page,
|
|
|
|
next: collection.next_page,
|
|
|
|
prev: collection.previous_page,
|
|
|
|
first: collection.first_page,
|
|
|
|
last: collection.last_page,
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
2018-08-28 12:35:19 +08:00
|
|
|
PAGINATION_PARAMS = %i[current next prev first last].freeze
|
2018-08-28 12:35:19 +08:00
|
|
|
LINK_PRIORITY = %i[next last prev current first].freeze
|
2012-09-12 05:12:36 +08:00
|
|
|
EXCLUDE_IN_PAGINATION_LINKS = %w[page per_page access_token api_key].freeze
|
|
|
|
def self.build_links(base_url, opts = {})
|
import ActiveModel::Serializers port and convert quizzes api to it
test plan:
- The quiz api should work like it normally does when you don't pass
an 'Accept: application/vnd.api+json' header.
- The quizzes index page and quiz edit page should work like they
always do.
- Testing the Quizzes API for "jsonapi" style:
- For all requests, you MUST have the "Accept" header set to
"application/vnd.api+json"
- Test all the endpoints (PUT, POST, GET, INDEX, DELETE) like you
normally would, except you'll need to format the data according to
the next few steps:
- For "POST" and "PUT" (create and update) requests, you should send
the data like: { "quizzes": [ { id: 1, title: "blah" } ]
- For all requests (except DELETE), you should get back a response
that looks like: { "quizzes": [ { quiz you requested } ]
- For the "delete" action, you should get a "no content" response
and the request should be successful
Change-Id: Ie91deaeb6772cbe52a0fc46a28ab93a4e3036061
Reviewed-on: https://gerrit.instructure.com/25997
Reviewed-by: Jacob Fugal <jacob@instructure.com>
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: Caleb Guanzon <cguanzon@instructure.com>
Product-Review: Stanley Stuart <stanley@instructure.com>
2013-12-05 03:06:32 +08:00
|
|
|
links = build_links_hash(base_url, opts)
|
2013-09-26 03:01:06 +08:00
|
|
|
build_links_from_hash(links)
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.build_links_from_hash(links)
|
import ActiveModel::Serializers port and convert quizzes api to it
test plan:
- The quiz api should work like it normally does when you don't pass
an 'Accept: application/vnd.api+json' header.
- The quizzes index page and quiz edit page should work like they
always do.
- Testing the Quizzes API for "jsonapi" style:
- For all requests, you MUST have the "Accept" header set to
"application/vnd.api+json"
- Test all the endpoints (PUT, POST, GET, INDEX, DELETE) like you
normally would, except you'll need to format the data according to
the next few steps:
- For "POST" and "PUT" (create and update) requests, you should send
the data like: { "quizzes": [ { id: 1, title: "blah" } ]
- For all requests (except DELETE), you should get back a response
that looks like: { "quizzes": [ { quiz you requested } ]
- For the "delete" action, you should get a "no content" response
and the request should be successful
Change-Id: Ie91deaeb6772cbe52a0fc46a28ab93a4e3036061
Reviewed-on: https://gerrit.instructure.com/25997
Reviewed-by: Jacob Fugal <jacob@instructure.com>
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: Caleb Guanzon <cguanzon@instructure.com>
Product-Review: Stanley Stuart <stanley@instructure.com>
2013-12-05 03:06:32 +08:00
|
|
|
# iterate in order, but only using the keys present from build_links_hash
|
|
|
|
(PAGINATION_PARAMS & links.keys).map do |k|
|
|
|
|
v = links[k]
|
|
|
|
"<#{v}>; rel=\"#{k}\""
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.build_links_hash(base_url, opts = {})
|
2021-11-11 03:36:19 +08:00
|
|
|
base_url += (base_url.include?("?") ? "&" : "?")
|
2012-09-12 05:12:36 +08:00
|
|
|
qp = opts[:query_parameters] || {}
|
|
|
|
qp = qp.with_indifferent_access.except(*EXCLUDE_IN_PAGINATION_LINKS)
|
|
|
|
base_url += "#{qp.to_query}&" if qp.present?
|
2018-08-28 12:35:19 +08:00
|
|
|
|
|
|
|
# Apache limits the HTTP response headers to 8KB total; with lots of query parameters, link headers can exceed this
|
|
|
|
# so prioritize the links we include and don't exceed (by default) 6KB in total
|
|
|
|
max_link_headers_size = Setting.get("pagination_max_link_headers_size", "6144").to_i
|
|
|
|
link_headers_size = 0
|
|
|
|
LINK_PRIORITY.each_with_object({}) do |param, obj|
|
import ActiveModel::Serializers port and convert quizzes api to it
test plan:
- The quiz api should work like it normally does when you don't pass
an 'Accept: application/vnd.api+json' header.
- The quizzes index page and quiz edit page should work like they
always do.
- Testing the Quizzes API for "jsonapi" style:
- For all requests, you MUST have the "Accept" header set to
"application/vnd.api+json"
- Test all the endpoints (PUT, POST, GET, INDEX, DELETE) like you
normally would, except you'll need to format the data according to
the next few steps:
- For "POST" and "PUT" (create and update) requests, you should send
the data like: { "quizzes": [ { id: 1, title: "blah" } ]
- For all requests (except DELETE), you should get back a response
that looks like: { "quizzes": [ { quiz you requested } ]
- For the "delete" action, you should get a "no content" response
and the request should be successful
Change-Id: Ie91deaeb6772cbe52a0fc46a28ab93a4e3036061
Reviewed-on: https://gerrit.instructure.com/25997
Reviewed-by: Jacob Fugal <jacob@instructure.com>
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: Caleb Guanzon <cguanzon@instructure.com>
Product-Review: Stanley Stuart <stanley@instructure.com>
2013-12-05 03:06:32 +08:00
|
|
|
next unless opts[param].present?
|
2021-09-23 00:25:11 +08:00
|
|
|
|
2018-08-28 12:35:19 +08:00
|
|
|
link = "#{base_url}page=#{opts[param]}&per_page=#{opts[:per_page]}"
|
|
|
|
return obj if link_headers_size + link.size > max_link_headers_size
|
2021-11-23 00:36:14 +08:00
|
|
|
|
2018-08-28 12:35:19 +08:00
|
|
|
link_headers_size += link.size
|
|
|
|
obj[param] = link
|
2011-07-22 00:35:46 +08:00
|
|
|
end
|
|
|
|
end
|
2012-01-04 04:30:49 +08:00
|
|
|
|
2018-08-28 12:35:19 +08:00
|
|
|
def self.pagination_params(base_url)
|
|
|
|
if base_url.length > Setting.get("pagination_max_base_url_for_links", "1000").to_i
|
|
|
|
# to prevent Link headers from consuming too much of the 8KB Apache allows in response headers
|
|
|
|
ESSENTIAL_PAGINATION_PARAMS
|
|
|
|
else
|
|
|
|
PAGINATION_PARAMS
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2012-10-18 05:43:39 +08:00
|
|
|
def self.parse_pagination_links(link_header)
|
|
|
|
link_header.split(",").map do |link|
|
|
|
|
url, rel = link.match(/^<([^>]+)>; rel="([^"]+)"/).captures
|
|
|
|
uri = URI.parse(url)
|
|
|
|
raise(ArgumentError, "pagination url is not an absolute uri: #{url}") unless uri.is_a?(URI::HTTP)
|
2021-09-23 00:25:11 +08:00
|
|
|
|
2023-06-02 06:06:09 +08:00
|
|
|
Rack::Utils.parse_nested_query(uri.query).merge(uri:, rel:)
|
2012-10-18 05:43:39 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2011-10-08 05:41:19 +08:00
|
|
|
def media_comment_json(media_object_or_hash)
|
|
|
|
media_object_or_hash = OpenStruct.new(media_object_or_hash) if media_object_or_hash.is_a?(Hash)
|
|
|
|
{
|
|
|
|
"content-type" => "#{media_object_or_hash.media_type}/mp4",
|
2015-10-30 23:15:24 +08:00
|
|
|
"display_name" => media_object_or_hash.title.presence || media_object_or_hash.user_entered_title,
|
2011-10-08 05:41:19 +08:00
|
|
|
"media_id" => media_object_or_hash.media_id,
|
|
|
|
"media_type" => media_object_or_hash.media_type,
|
|
|
|
"url" => user_media_download_url(user_id: @current_user.id,
|
|
|
|
entryId: media_object_or_hash.media_id,
|
|
|
|
type: "mp4",
|
|
|
|
redirect: "1")
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
2018-11-06 06:55:47 +08:00
|
|
|
def self.api_bulk_load_user_content_attachments(htmls, context = nil)
|
2015-09-10 21:38:06 +08:00
|
|
|
regex = context ? %r{/#{context.class.name.tableize}/#{context.id}/files/(\d+)} : %r{/files/(\d+)}
|
|
|
|
|
2013-07-09 03:08:06 +08:00
|
|
|
attachment_ids = []
|
2015-09-10 21:38:06 +08:00
|
|
|
htmls.compact.each do |html|
|
|
|
|
html.scan(regex).each do |match|
|
|
|
|
attachment_ids << match.first
|
|
|
|
end
|
2013-07-09 03:08:06 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
if attachment_ids.blank?
|
|
|
|
{}
|
|
|
|
else
|
|
|
|
attachments = if context.is_a?(User) || context.nil?
|
|
|
|
Attachment.where(id: attachment_ids)
|
|
|
|
else
|
|
|
|
context.attachments.where(id: attachment_ids)
|
|
|
|
end
|
2015-09-10 21:38:06 +08:00
|
|
|
|
2015-09-10 00:23:40 +08:00
|
|
|
attachments.preload(:context).index_by(&:id)
|
2013-07-09 03:08:06 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-11-06 06:55:47 +08:00
|
|
|
def api_bulk_load_user_content_attachments(htmls, context = nil)
|
|
|
|
Api.api_bulk_load_user_content_attachments(htmls, context)
|
|
|
|
end
|
|
|
|
|
2015-03-26 07:03:01 +08:00
|
|
|
PLACEHOLDER_PROTOCOL = "https"
|
|
|
|
PLACEHOLDER_HOST = "placeholder.invalid"
|
|
|
|
|
|
|
|
def get_host_and_protocol_from_request
|
|
|
|
[request.host_with_port, request.ssl? ? "https" : "http"]
|
|
|
|
end
|
|
|
|
|
|
|
|
def resolve_placeholders(content)
|
|
|
|
host, protocol = get_host_and_protocol_from_request
|
2016-01-20 04:23:25 +08:00
|
|
|
# content is a json-encoded string; slashes are escaped (at least in Rails 4.0)
|
|
|
|
content.gsub("#{PLACEHOLDER_PROTOCOL}:\\/\\/#{PLACEHOLDER_HOST}", "#{protocol}:\\/\\/#{host}")
|
|
|
|
.gsub("#{PLACEHOLDER_PROTOCOL}://#{PLACEHOLDER_HOST}", "#{protocol}://#{host}")
|
2015-03-26 07:03:01 +08:00
|
|
|
end
|
|
|
|
|
2015-09-10 00:23:40 +08:00
|
|
|
def user_can_download_attachment?(attachment, context, user)
|
|
|
|
# checking on the context first can improve performance when checking many attachments for admins
|
add granular permissions for course files
Note: we'll want to re-run the data fix-up when we're ready to turn
on the feature flag permanently; in hopes to capture any differences
made to course files permissions between now and then.
Modified the files_controller quota and api_quota permission checks
to make them more lenient in regards to accepting any or all of the
files permissions role overrides. This allows legacy grouping and
new granularized files permissions to live in harmony and be modified
without causing unauthorized errors on the quota resource.
This commit will cover the backend permissions required to granularize
files / folders permission calls, however there will be a follow-up
ps to clean up the course file page to hide elements the user might
not be authorized to use.
closes FOO-130
refs FOO-1501
flag = granular_permissions_course_files
[fsc-max-nodes=18]
[fsc-timeout=30]
Test Plan:
- Run the migration and make sure there are no errors
- With the granular_permissions_course_files FF turned off,
course sections and REST API should work the same with this patch
set checked out as it does in beta/production
- Some things to check:
* How it acts as a teacher, student, and public user
in course files/folders and personal files/folders
with the various settings above toggled to different states
* How it acts as a teacher, student, and public user
in discussions, modules, content migrations/import/exports
(RCE should behave similarly throughout the site)
- With the granular_permissions_course_files feature flag turned on
course files/folders and REST API should work as expected. The same
list checked above should be done so again, but this time:
* Should only be able to upload or add folders if the
Course Files - add permission is enabled for the user's role
* Should only be able to manage file access, usage rights, move,
or rename course files/folders if the Course Files -
edit permission is enabled for the user's role
• Check Toolbar header at the top of Course files
• Check Cog (hamburger menu) to the right of each file/folder
• Check Usage Rights Indicator under usage rights column
that can be found in course and group file pages. This can
be enabled under course settings if not available
* Should only be able to delete course files/folders if the
Course Files - delete permission is enabled for the user's role
* Any given user/role should have full access to their respective
personal files/folders regardless of granted permissions. The
same also applies to a group context with some caveats
• Should not be able to modify file access in a group context
• Should not be able to modify usage rights in personal files
* A student enrollment not granted any file permissions (the default)
should only be able to _view_ and _download_ files unless granted
additional access from an authorizing role
* REST API works as expected
* UI works as expected with no additional javascript errors
Change-Id: Ieb2d10915c274959e8da4c623f7aba11d3540c2b
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/253777
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
QA-Review: Simon Williams <simon@instructure.com>
Product-Review: August Thornton <august@instructure.com>
Reviewed-by: Michael Ziwisky <mziwisky@instructure.com>
2020-11-26 06:51:02 +08:00
|
|
|
context&.grants_any_right?(
|
|
|
|
user,
|
|
|
|
:read_as_admin,
|
|
|
|
*RoleOverride::GRANULAR_FILE_PERMISSIONS
|
|
|
|
) || attachment&.grants_right?(user, nil, :download)
|
2015-09-10 00:23:40 +08:00
|
|
|
end
|
|
|
|
|
2023-04-29 04:59:51 +08:00
|
|
|
def api_user_content(html,
|
|
|
|
context = @context,
|
|
|
|
user = @current_user,
|
|
|
|
preloaded_attachments = {},
|
|
|
|
options = {},
|
|
|
|
is_public = false)
|
2011-11-10 05:39:18 +08:00
|
|
|
return html if html.blank?
|
|
|
|
|
2015-03-26 07:03:01 +08:00
|
|
|
# use the host of the request if available;
|
|
|
|
# use a placeholder host for pre-generated content, which we will replace with the request host when available;
|
|
|
|
# otherwise let HostUrl figure out what host is appropriate
|
|
|
|
if respond_to?(:request)
|
|
|
|
host, protocol = get_host_and_protocol_from_request
|
2018-02-23 04:39:14 +08:00
|
|
|
target_shard = Shard.current
|
2015-03-26 07:03:01 +08:00
|
|
|
elsif respond_to?(:use_placeholder_host?) && use_placeholder_host?
|
|
|
|
host = PLACEHOLDER_HOST
|
|
|
|
protocol = PLACEHOLDER_PROTOCOL
|
2012-04-04 04:17:56 +08:00
|
|
|
else
|
|
|
|
host = HostUrl.context_host(context, @account_domain.try(:host))
|
2012-06-26 23:52:40 +08:00
|
|
|
protocol = HostUrl.protocol
|
2012-04-04 04:17:56 +08:00
|
|
|
end
|
2012-03-28 02:41:55 +08:00
|
|
|
|
2018-09-18 05:08:26 +08:00
|
|
|
html = context.shard.activate do
|
|
|
|
rewriter = UserContent::HtmlRewriter.new(context, user)
|
|
|
|
rewriter.set_handler("files") do |match|
|
|
|
|
UserContent::FilesHandler.new(
|
2023-06-02 06:06:09 +08:00
|
|
|
match:,
|
|
|
|
context:,
|
|
|
|
user:,
|
|
|
|
preloaded_attachments:,
|
|
|
|
is_public:,
|
2018-09-18 05:08:26 +08:00
|
|
|
in_app: (respond_to?(:in_app?, true) && in_app?)
|
|
|
|
).processed_url
|
|
|
|
end
|
|
|
|
rewriter.translate_content(html)
|
2011-10-08 00:36:22 +08:00
|
|
|
end
|
|
|
|
|
2018-02-23 04:39:14 +08:00
|
|
|
url_helper = Html::UrlProxy.new(self,
|
|
|
|
context,
|
|
|
|
host,
|
|
|
|
protocol,
|
2023-06-02 06:06:09 +08:00
|
|
|
target_shard:)
|
2015-12-05 00:57:07 +08:00
|
|
|
account = Context.get_account(context) || @domain_root_account
|
2016-05-18 05:09:28 +08:00
|
|
|
include_mobile = !(respond_to?(:in_app?, true) && in_app?)
|
2021-08-20 22:04:06 +08:00
|
|
|
Html::Content.rewrite_outgoing(
|
2023-04-29 04:59:51 +08:00
|
|
|
html,
|
|
|
|
account,
|
|
|
|
url_helper,
|
2023-06-02 06:06:09 +08:00
|
|
|
include_mobile:,
|
2021-08-20 22:04:06 +08:00
|
|
|
rewrite_api_urls: options[:rewrite_api_urls]
|
|
|
|
)
|
2011-10-08 00:36:22 +08:00
|
|
|
end
|
2012-05-22 00:11:48 +08:00
|
|
|
|
2013-04-13 05:46:13 +08:00
|
|
|
# This removes the verifier parameters that are added to attachment links by api_user_content
|
|
|
|
# and adds context (e.g. /courses/:id/) if it is missing
|
2013-06-18 05:24:00 +08:00
|
|
|
# exception: it leaves user-context file links alone
|
2013-04-13 05:46:13 +08:00
|
|
|
def process_incoming_html_content(html)
|
2016-11-02 01:11:06 +08:00
|
|
|
host, port = [request.host, request.port] if respond_to?(:request)
|
2023-06-02 06:06:09 +08:00
|
|
|
Html::Content.process_incoming(html, host:, port:)
|
2013-04-13 05:46:13 +08:00
|
|
|
end
|
|
|
|
|
2012-05-22 00:11:48 +08:00
|
|
|
def value_to_boolean(value)
|
|
|
|
Canvas::Plugin.value_to_boolean(value)
|
|
|
|
end
|
2012-08-15 05:47:53 +08:00
|
|
|
|
2014-02-26 02:53:21 +08:00
|
|
|
# takes a comma separated string, an array, or nil and returns an array
|
2014-01-07 07:45:51 +08:00
|
|
|
def self.value_to_array(value)
|
2014-02-26 02:53:21 +08:00
|
|
|
value.is_a?(String) ? value.split(",") : (value || [])
|
2014-01-07 07:45:51 +08:00
|
|
|
end
|
|
|
|
|
2014-04-10 05:05:22 +08:00
|
|
|
def self.invalid_time_stamp_error(attribute, message)
|
2020-10-30 22:47:20 +08:00
|
|
|
data = {
|
2015-04-05 10:39:49 +08:00
|
|
|
message: "invalid #{attribute}",
|
|
|
|
exception_message: message
|
2020-10-30 22:47:20 +08:00
|
|
|
}
|
|
|
|
Canvas::Errors.capture("invalid_date_time", data, :info)
|
2014-04-10 05:05:22 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
# regex for valid iso8601 dates
|
2014-04-24 06:37:49 +08:00
|
|
|
ISO8601_REGEX = /^(?<year>[0-9]{4})-
|
2014-04-10 05:05:22 +08:00
|
|
|
(?<month>1[0-2]|0[1-9])-
|
|
|
|
(?<day>3[0-1]|0[1-9]|[1-2][0-9])T
|
|
|
|
(?<hour>2[0-3]|[0-1][0-9]):
|
|
|
|
(?<minute>[0-5][0-9]):
|
|
|
|
(?<second>60|[0-5][0-9])
|
|
|
|
(?<fraction>\.[0-9]+)?
|
2023-06-02 06:06:09 +08:00
|
|
|
(?<timezone>Z|[+-](?:2[0-3]|[0-1][0-9]):[0-5][0-9])?$/x
|
2014-04-10 05:05:22 +08:00
|
|
|
|
|
|
|
# regex for valid dates
|
2023-06-02 06:06:09 +08:00
|
|
|
DATE_REGEX = %r{^\d{4}[- /.](0[1-9]|1[012])[- /.](0[1-9]|[12][0-9]|3[01])$}
|
2014-04-10 05:05:22 +08:00
|
|
|
|
2012-08-15 05:47:53 +08:00
|
|
|
# regex for shard-aware ID
|
|
|
|
ID = '(?:\d+~)?\d+'
|
|
|
|
|
modules api, closes #10404
also modifies the discussion topic and assignment API
controllers to make sure "must_view" requirements are
fulfilled
test plan:
* check the API documentation; ensure it looks okay
* create a course with module items of each supported type
* set completion criteria of each supported type
* create another module, so you can set prerequisites
* use the list modules API and verify its output matches
the course and the documentation
* as a teacher, "state" should be missing
* as a student, "state" should be "locked", "unlocked",
"started", or "completed"
* use the show module API and verify the correct information
is returned for a single module
* use the list module items API and verify the output
* as a teacher, the "completion_requirement" omits the
"completed" flag
* as a student, "completed" should be true or false,
depending on whether the requirement was met
* use the show module API and verify the correct information
is returned for a single module item
* last but not least, verify "must view" requirements can
be fulfilled through the api_data_endpoints supplied
for files, pages, discussions, and assignments
* files are viewed when downloading their content
* pages are viewed by the show action (where content
is returned)
* discussions are viewed when marked read via the
mark_topic_read or mark_all_read actions
* assignments are viewed by the show action
(where description is returned). they are not viewed
if the assignment is locked and the user does not
have access to the content yet.
Change-Id: I0cbbbc542f69215e7b396a501d4d86ff2f76c149
Reviewed-on: https://gerrit.instructure.com/13626
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Simon Williams <simon@instructure.com>
2012-09-12 01:16:48 +08:00
|
|
|
# maps a Canvas data type to an API-friendly type name
|
|
|
|
API_DATA_TYPE = { "Attachment" => "File",
|
|
|
|
"WikiPage" => "Page",
|
|
|
|
"DiscussionTopic" => "Discussion",
|
|
|
|
"Assignment" => "Assignment",
|
2014-01-15 06:11:27 +08:00
|
|
|
"Quizzes::Quiz" => "Quiz",
|
modules api, closes #10404
also modifies the discussion topic and assignment API
controllers to make sure "must_view" requirements are
fulfilled
test plan:
* check the API documentation; ensure it looks okay
* create a course with module items of each supported type
* set completion criteria of each supported type
* create another module, so you can set prerequisites
* use the list modules API and verify its output matches
the course and the documentation
* as a teacher, "state" should be missing
* as a student, "state" should be "locked", "unlocked",
"started", or "completed"
* use the show module API and verify the correct information
is returned for a single module
* use the list module items API and verify the output
* as a teacher, the "completion_requirement" omits the
"completed" flag
* as a student, "completed" should be true or false,
depending on whether the requirement was met
* use the show module API and verify the correct information
is returned for a single module item
* last but not least, verify "must view" requirements can
be fulfilled through the api_data_endpoints supplied
for files, pages, discussions, and assignments
* files are viewed when downloading their content
* pages are viewed by the show action (where content
is returned)
* discussions are viewed when marked read via the
mark_topic_read or mark_all_read actions
* assignments are viewed by the show action
(where description is returned). they are not viewed
if the assignment is locked and the user does not
have access to the content yet.
Change-Id: I0cbbbc542f69215e7b396a501d4d86ff2f76c149
Reviewed-on: https://gerrit.instructure.com/13626
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Simon Williams <simon@instructure.com>
2012-09-12 01:16:48 +08:00
|
|
|
"ContextModuleSubHeader" => "SubHeader",
|
|
|
|
"ExternalUrl" => "ExternalUrl",
|
2013-08-22 06:36:47 +08:00
|
|
|
"ContextExternalTool" => "ExternalTool",
|
|
|
|
"ContextModule" => "Module",
|
|
|
|
"ContentTag" => "ModuleItem" }.freeze
|
|
|
|
|
|
|
|
# matches the other direction, case insensitively
|
|
|
|
def self.api_type_to_canvas_name(api_type)
|
|
|
|
unless @inverse_map
|
|
|
|
m = {}
|
|
|
|
API_DATA_TYPE.each do |k, v|
|
|
|
|
m[v.downcase] = k
|
|
|
|
end
|
|
|
|
@inverse_map = m
|
|
|
|
end
|
|
|
|
return nil unless api_type
|
2021-09-23 00:25:11 +08:00
|
|
|
|
2013-08-22 06:36:47 +08:00
|
|
|
@inverse_map[api_type.downcase]
|
|
|
|
end
|
modules api, closes #10404
also modifies the discussion topic and assignment API
controllers to make sure "must_view" requirements are
fulfilled
test plan:
* check the API documentation; ensure it looks okay
* create a course with module items of each supported type
* set completion criteria of each supported type
* create another module, so you can set prerequisites
* use the list modules API and verify its output matches
the course and the documentation
* as a teacher, "state" should be missing
* as a student, "state" should be "locked", "unlocked",
"started", or "completed"
* use the show module API and verify the correct information
is returned for a single module
* use the list module items API and verify the output
* as a teacher, the "completion_requirement" omits the
"completed" flag
* as a student, "completed" should be true or false,
depending on whether the requirement was met
* use the show module API and verify the correct information
is returned for a single module item
* last but not least, verify "must view" requirements can
be fulfilled through the api_data_endpoints supplied
for files, pages, discussions, and assignments
* files are viewed when downloading their content
* pages are viewed by the show action (where content
is returned)
* discussions are viewed when marked read via the
mark_topic_read or mark_all_read actions
* assignments are viewed by the show action
(where description is returned). they are not viewed
if the assignment is locked and the user does not
have access to the content yet.
Change-Id: I0cbbbc542f69215e7b396a501d4d86ff2f76c149
Reviewed-on: https://gerrit.instructure.com/13626
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Simon Williams <simon@instructure.com>
2012-09-12 01:16:48 +08:00
|
|
|
|
2013-10-23 23:47:41 +08:00
|
|
|
def accepts_jsonapi?
|
2021-11-11 03:36:19 +08:00
|
|
|
!!request.headers["Accept"].to_s.include?("application/vnd.api+json")
|
2013-10-23 23:47:41 +08:00
|
|
|
end
|
Quiz Submissions API - Create & Complete
Allows users to start a "quiz-taking session" via the API by creating
a QuizSubmission and later on completing it.
Note that this patch isn't concerned with actually using the QS to
answer questions. That task will be the concern of a new API controller,
QuizSubmissionQuestions.
closes CNVS-8980
TEST PLAN
---- ----
- Create a quiz
- Keep a tab open on the Moderate Quiz (MQ from now) page
Create the quiz submission (ie, start a quiz-taking session):
- Via the API, as a student:
- POST to /courses/:course_id/quizzes/:quiz_id/submissions
- Verify that you receive a 200 response with the newly created
QuizSubmission in the JSON response.
- Copy the "validation_token" field down, you will need this later
- Go to the MQ tab and verify that it says the student has started a
quiz attempt
Complete the quiz submission (ie, finish a quiz-taking session):
- Via the API, as a student, prepare a request with:
- Method: POST
- URI: /courses/:course_id/quizzes/:quiz_id/submissions/:id/complete
- Parameter "validation_token" to what you copied earlier
- Parameter "attempt" to the current attempt number (starts at 1)
- Now perform the request, and:
- Verify that you receive a 200 response
- Go to the MQ tab and verify that it says the submission has been
completed (ie, Time column reads "finished in X seconds/minutes")
Other stuff to test (failure scenarios):
The first endpoint (one for starting a quiz attempt) should reject your
request in any of the following cases:
- The quiz has been locked
- You are not enrolled in the quiz course
- The Quiz has an Access Code that you either didn't pass, or passed
incorrectly
- The Quiz has an IP filter and you're not in the address range
- You are already taking the quiz (you've created the submission and
did not call /complete yet)
- You are not currently taking the quiz, but you already took it
earlier and the Quiz does not allow for multiple attempts
The second endpoint (one for completing the quiz attempt) should reject
your request in any of the following cases:
- You pass in an invalid "validation_token"
- You already completed that quiz submission (e.g, you called that
endpoint earlier)
Change-Id: Iff8a47859d7477c210de46ea034544d5e2527fb2
Reviewed-on: https://gerrit.instructure.com/27015
Reviewed-by: Derek DeVries <ddevries@instructure.com>
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: Myller de Araujo <myller@instructure.com>
Product-Review: Ahmad Amireh <ahmad@instructure.com>
2013-12-05 22:10:12 +08:00
|
|
|
|
2013-12-03 04:42:07 +08:00
|
|
|
# Return a template url that follows the root links key for the jsonapi.org
|
|
|
|
# standard.
|
|
|
|
def templated_url(method, *args)
|
|
|
|
format = /^\{.*\}$/
|
|
|
|
placeholder = "PLACEHOLDER"
|
|
|
|
|
|
|
|
placeholders = args.each_with_index.map do |arg, index|
|
2021-11-12 01:20:36 +08:00
|
|
|
arg&.match?(format) ? "#{placeholder}#{index}" : arg
|
2013-12-03 04:42:07 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
url = send(method, *placeholders)
|
|
|
|
|
|
|
|
args.each_with_index do |arg, index|
|
2021-11-12 01:20:36 +08:00
|
|
|
url.sub!("#{placeholder}#{index}", arg) if arg&.match?(format)
|
2013-12-03 04:42:07 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
url
|
|
|
|
end
|
2023-03-17 03:14:04 +08:00
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def infer_writable_from_request_method
|
|
|
|
respond_to?(:request) && %w[PUT POST PATCH DELETE].include?(request&.method)
|
|
|
|
end
|
2011-05-13 05:48:18 +08:00
|
|
|
end
|