add more search capabilities to audit endpoint
This adds more search capabilities to the audit endpoint to give maximum flexibility to support the new gradebook history page. This adds the ability to search for course AND any combination of the following: assignment, grader, student. Permissions were not touched in this patchset, so only admins can access currently. closes CNVS-37299 test plan: - Create a course with two teachers, two assignments, and two student. - Grade the students for the assignments and change the grades a few times as each teacher - Visit the following API endpoints with appropriate ids confirming that appropriate data is returned: o /api/v1/audit/grade_change/courses/:course_id/assignments/:assignment_id o /api/v1/audit/grade_change/courses/:course_id/assignments/:assignment_id/graders/:grader_id o /api/v1/audit/grade_change/courses/:course_id/assignments/:assignment_id/graders/:grader_id/students/:student_id o /api/v1/audit/grade_change/courses/:course_id/assignments/:assignment_id/students/:student_id o /api/v1/audit/grade_change/courses/:course_id/graders/:grader_id o /api/v1/audit/grade_change/courses/:course_id/graders/:grader_id/students/:student_id o /api/v1/audit/grade_change/courses/:course_id/students/:student_id Change-Id: I86648896d49d7228ddce04864e80221da9b5f96d Reviewed-on: https://gerrit.instructure.com/114146 Reviewed-by: Shahbaz Javeed <sjaveed@instructure.com> Tested-by: Jenkins Reviewed-by: Derek Bender <djbender@instructure.com> QA-Review: Anju Reddy <areddy@instructure.com> Product-Review: Keith T. Garner <kgarner@instructure.com>
This commit is contained in:
parent
7207e78d09
commit
d441418d7e
|
@ -136,16 +136,15 @@ class GradeChangeAuditApiController < AuditorApiController
|
|||
# @returns [GradeChangeEvent]
|
||||
#
|
||||
def for_assignment
|
||||
return render_unauthorized_action unless authorize
|
||||
|
||||
@assignment = Assignment.active.find(params[:assignment_id])
|
||||
unless @assignment.context.root_account == @domain_root_account
|
||||
raise ActiveRecord::RecordNotFound, "Couldn't find assignment with API id '#{params[:assignment_id]}'"
|
||||
end
|
||||
if authorize
|
||||
events = Auditors::GradeChange.for_assignment(@assignment, query_options)
|
||||
render_events(events, polymorphic_url([:api_v1, :audit_grade_change, @assignment]))
|
||||
else
|
||||
render_unauthorized_action
|
||||
end
|
||||
|
||||
events = Auditors::GradeChange.for_assignment(@assignment, query_options)
|
||||
render_events(events, polymorphic_url([:api_v1, :audit_grade_change, @assignment]))
|
||||
end
|
||||
|
||||
# @API Query by course.
|
||||
|
@ -161,13 +160,11 @@ class GradeChangeAuditApiController < AuditorApiController
|
|||
# @returns [GradeChangeEvent]
|
||||
#
|
||||
def for_course
|
||||
@course = @domain_root_account.all_courses.active.find(params[:course_id])
|
||||
if authorize
|
||||
events = Auditors::GradeChange.for_course(@course, query_options)
|
||||
render_events(events, polymorphic_url([:api_v1, :audit_grade_change, @course]))
|
||||
else
|
||||
render_unauthorized_action
|
||||
end
|
||||
return render_unauthorized_action unless authorize
|
||||
|
||||
@course = @domain_root_account.all_courses.find(params[:course_id])
|
||||
events = Auditors::GradeChange.for_course(@course, query_options)
|
||||
render_events(events, polymorphic_url([:api_v1, :audit_grade_change, @course]))
|
||||
end
|
||||
|
||||
# @API Query by student.
|
||||
|
@ -183,16 +180,15 @@ class GradeChangeAuditApiController < AuditorApiController
|
|||
# @returns [GradeChangeEvent]
|
||||
#
|
||||
def for_student
|
||||
return render_unauthorized_action unless authorize
|
||||
|
||||
@student = User.active.find(params[:student_id])
|
||||
unless @domain_root_account.associated_user?(@student)
|
||||
raise ActiveRecord::RecordNotFound, "Couldn't find user with API id '#{params[:student_id]}'"
|
||||
end
|
||||
if authorize
|
||||
events = Auditors::GradeChange.for_root_account_student(@domain_root_account, @student, query_options)
|
||||
render_events(events, api_v1_audit_grade_change_student_url(@student))
|
||||
else
|
||||
render_unauthorized_action
|
||||
end
|
||||
|
||||
events = Auditors::GradeChange.for_root_account_student(@domain_root_account, @student, query_options)
|
||||
render_events(events, api_v1_audit_grade_change_student_url(@student))
|
||||
end
|
||||
|
||||
# @API Query by grader.
|
||||
|
@ -208,16 +204,45 @@ class GradeChangeAuditApiController < AuditorApiController
|
|||
# @returns [GradeChangeEvent]
|
||||
#
|
||||
def for_grader
|
||||
return render_unauthorized_action unless authorize
|
||||
|
||||
@grader = User.active.find(params[:grader_id])
|
||||
unless @domain_root_account.associated_user?(@grader)
|
||||
raise ActiveRecord::RecordNotFound, "Couldn't find user with API id '#{params[:grader_id]}'"
|
||||
end
|
||||
if authorize
|
||||
events = Auditors::GradeChange.for_root_account_grader(@domain_root_account, @grader, query_options)
|
||||
render_events(events, api_v1_audit_grade_change_grader_url(@grader))
|
||||
else
|
||||
render_unauthorized_action
|
||||
|
||||
events = Auditors::GradeChange.for_root_account_grader(@domain_root_account, @grader, query_options)
|
||||
render_events(events, api_v1_audit_grade_change_grader_url(@grader))
|
||||
end
|
||||
|
||||
def for_course_and_other_parameters
|
||||
return render_unauthorized_action unless authorize
|
||||
|
||||
course = @domain_root_account.all_courses.find(params[:course_id])
|
||||
|
||||
args = { course: course }
|
||||
args[:assignment] = course.assignments.find(params[:assignment_id]) if params[:assignment_id]
|
||||
args[:grader] = course.all_users.find(params[:grader_id]) if params[:grader_id]
|
||||
args[:student] = course.all_users.find(params[:student_id]) if params[:student_id]
|
||||
|
||||
url_method = if args[:assignment] && args[:grader] && args[:student]
|
||||
:api_v1_audit_grade_change_course_assignment_grader_student_url
|
||||
elsif args[:assignment] && args[:grader]
|
||||
:api_v1_audit_grade_change_course_assignment_grader_url
|
||||
elsif args[:assignment] && args[:student]
|
||||
:api_v1_audit_grade_change_course_assignment_student_url
|
||||
elsif args[:assignment]
|
||||
:api_v1_audit_grade_change_course_assignment_url
|
||||
elsif args[:grader] && args[:student]
|
||||
:api_v1_audit_grade_change_course_grader_student_url
|
||||
elsif args[:grader]
|
||||
:api_v1_audit_grade_change_course_grader_url
|
||||
elsif args[:student]
|
||||
:api_v1_audit_grade_change_course_student_url
|
||||
end
|
||||
|
||||
events = Auditors::GradeChange.for_course_and_other_arguments(course, args, query_options)
|
||||
render_events(events, send(url_method, args))
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -177,6 +177,59 @@ class Auditors::GradeChange
|
|||
entry_proc lambda{ |record| [record.root_account, record.student] }
|
||||
key_proc lambda{ |root_account, student| [root_account.global_id, student.global_id] }
|
||||
end
|
||||
|
||||
add_index :course_assignment do
|
||||
table :grade_changes_by_course_assignment
|
||||
entry_proc lambda { |record| [record.course, record.assignment] }
|
||||
key_proc lambda { |course, assignment| [course.global_id, assignment.global_id] }
|
||||
end
|
||||
|
||||
add_index :course_assignment_grader do
|
||||
table :grade_changes_by_course_assignment_grader
|
||||
entry_proc lambda { |record|
|
||||
[record.course, record.assignment, record.grader] if record.grader && !record.submission.autograded?
|
||||
}
|
||||
key_proc lambda { |course, assignment, grader| [course.global_id, assignment.global_id, grader.global_id] }
|
||||
end
|
||||
|
||||
add_index :course_assignment_grader_student do
|
||||
table :grade_change_by_course_assignment_grader_student
|
||||
entry_proc lambda { |record|
|
||||
if record.grader && !record.submission.autograded?
|
||||
[record.course, record.assignment, record.grader, record.student]
|
||||
end
|
||||
}
|
||||
key_proc lambda { |course, assignment, grader, student|
|
||||
[course.global_id, assignment.global_id, grader.global_id, student.global_id]
|
||||
}
|
||||
end
|
||||
|
||||
add_index :course_assignment_student do
|
||||
table :grade_changes_by_course_assignment_student
|
||||
entry_proc lambda { |record| [record.course, record.assignment, record.student] }
|
||||
key_proc lambda { |course, assignment, student| [course.global_id, assignment.global_id, student.global_id] }
|
||||
end
|
||||
|
||||
add_index :course_grader do
|
||||
table :grade_changes_by_course_grader
|
||||
entry_proc lambda { |record| [record.course, record.grader] if record.grader && !record.submission.autograded? }
|
||||
key_proc lambda { |course, grader| [course.global_id, grader.global_id] }
|
||||
end
|
||||
|
||||
add_index :course_grader_student do
|
||||
table :grade_changes_by_course_grader_student
|
||||
entry_proc lambda { |record|
|
||||
[record.course, record.grader, record.student] if record.grader && !record.submission.autograded?
|
||||
}
|
||||
key_proc lambda { |course, grader, student| [course.global_id, grader.global_id, student.global_id] }
|
||||
end
|
||||
|
||||
add_index :course_student do
|
||||
table :grade_changes_by_course_student
|
||||
entry_proc lambda { |record| [record.course, record.student] }
|
||||
key_proc lambda { |course, student| [course.global_id, student.global_id] }
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def self.record(submission, event_type=nil)
|
||||
|
@ -211,4 +264,42 @@ class Auditors::GradeChange
|
|||
Auditors::GradeChange::Stream.for_assignment(assignment, options)
|
||||
end
|
||||
end
|
||||
|
||||
# These are the groupings this method expects to receive:
|
||||
# course assignment
|
||||
# course assignment grader
|
||||
# course assignment grader student
|
||||
# course assignment student
|
||||
# course grader
|
||||
# course grader student
|
||||
# course student
|
||||
def self.for_course_and_other_arguments(course, arguments, options={})
|
||||
course.shard.activate do
|
||||
if arguments[:assignment] && arguments[:grader] && arguments[:student]
|
||||
Auditors::GradeChange::Stream.for_course_assignment_grader_student(course,
|
||||
arguments[:assignment], arguments[:grader], arguments[:student], options)
|
||||
|
||||
elsif arguments[:assignment] && arguments[:grader]
|
||||
Auditors::GradeChange::Stream.for_course_assignment_grader(course, arguments[:assignment],
|
||||
arguments[:grader], options)
|
||||
|
||||
elsif arguments[:assignment] && arguments[:student]
|
||||
Auditors::GradeChange::Stream.for_course_assignment_student(course, arguments[:assignment],
|
||||
arguments[:student], options)
|
||||
|
||||
elsif arguments[:assignment]
|
||||
Auditors::GradeChange::Stream.for_course_assignment(course, arguments[:assignment], options)
|
||||
|
||||
elsif arguments[:grader] && arguments[:student]
|
||||
Auditors::GradeChange::Stream.for_course_grader_student(course, arguments[:grader], arguments[:student],
|
||||
options)
|
||||
|
||||
elsif arguments[:grader]
|
||||
Auditors::GradeChange::Stream.for_course_grader(course, arguments[:grader], options)
|
||||
|
||||
elsif arguments[:student]
|
||||
Auditors::GradeChange::Stream.for_course_student(course, arguments[:student], options)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -117,6 +117,7 @@ class Course < ActiveRecord::Base
|
|||
has_many :course_account_associations
|
||||
has_many :non_unique_associated_accounts, -> { order('course_account_associations.depth') }, source: :account, through: :course_account_associations
|
||||
has_many :users, -> { distinct }, through: :enrollments, source: :user
|
||||
has_many :all_users, -> { distinct }, through: :all_enrollments, source: :user
|
||||
has_many :current_users, -> { distinct }, through: :current_enrollments, source: :user
|
||||
has_many :all_current_users, -> { distinct }, through: :all_current_enrollments, source: :user
|
||||
has_many :group_categories, -> {where(deleted_at: nil) }, as: :context, inverse_of: :context
|
||||
|
|
|
@ -1015,6 +1015,20 @@ CanvasRails::Application.routes.draw do
|
|||
get 'audit/grade_change/courses/:course_id', action: :for_course, as: 'audit_grade_change_course'
|
||||
get 'audit/grade_change/students/:student_id', action: :for_student, as: 'audit_grade_change_student'
|
||||
get 'audit/grade_change/graders/:grader_id', action: :for_grader, as: 'audit_grade_change_grader'
|
||||
get 'audit/grade_change/courses/:course_id/assignments/:assignment_id',
|
||||
action: :for_course_and_other_parameters, as: 'audit_grade_change_course_assignment'
|
||||
get 'audit/grade_change/courses/:course_id/assignments/:assignment_id/graders/:grader_id',
|
||||
action: :for_course_and_other_parameters, as: 'audit_grade_change_course_assignment_grader'
|
||||
get 'audit/grade_change/courses/:course_id/assignments/:assignment_id/graders/:grader_id/students/:student_id',
|
||||
action: :for_course_and_other_parameters, as: 'audit_grade_change_course_assignment_grader_student'
|
||||
get 'audit/grade_change/courses/:course_id/assignments/:assignment_id/students/:student_id',
|
||||
action: :for_course_and_other_parameters, as: 'audit_grade_change_course_assignment_student'
|
||||
get 'audit/grade_change/courses/:course_id/graders/:grader_id',
|
||||
action: :for_course_and_other_parameters, as: 'audit_grade_change_course_grader'
|
||||
get 'audit/grade_change/courses/:course_id/graders/:grader_id/students/:student_id',
|
||||
action: :for_course_and_other_parameters, as: 'audit_grade_change_course_grader_student'
|
||||
get 'audit/grade_change/courses/:course_id/students/:student_id',
|
||||
action: :for_course_and_other_parameters, as: 'audit_grade_change_course_student'
|
||||
end
|
||||
|
||||
scope(controller: :course_audit_api) do
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
#
|
||||
# Copyright (C) 2017 - 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 CassandraAddAdditionalGradeChangeIndexesForGradebookHistory < ActiveRecord::Migration[4.2]
|
||||
tag :predeploy
|
||||
|
||||
include Canvas::Cassandra::Migration
|
||||
|
||||
def self.cassandra_cluster
|
||||
'auditors'
|
||||
end
|
||||
|
||||
def self.indexes
|
||||
%w(
|
||||
grade_changes_by_course_assignment
|
||||
grade_changes_by_course_assignment_grader
|
||||
grade_change_by_course_assignment_grader_student
|
||||
grade_changes_by_course_assignment_student
|
||||
grade_changes_by_course_grader
|
||||
grade_changes_by_course_grader_student
|
||||
grade_changes_by_course_student
|
||||
)
|
||||
end
|
||||
|
||||
def self.up
|
||||
compression_params = if cassandra.db.use_cql3?
|
||||
"WITH compression = { 'sstable_compression' : 'DeflateCompressor' }"
|
||||
else
|
||||
"WITH compression_parameters:sstable_compression='DeflateCompressor'"
|
||||
end
|
||||
|
||||
indexes.each do |index_name|
|
||||
cassandra.execute %{
|
||||
CREATE TABLE #{index_name} (
|
||||
key text,
|
||||
ordered_id text,
|
||||
id text,
|
||||
PRIMARY KEY (key, ordered_id)
|
||||
) #{compression_params}}
|
||||
end
|
||||
end
|
||||
|
||||
def self.down
|
||||
indexes.each do |index_name|
|
||||
cassandra.execute %{DROP TABLE #{index_name};}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,29 @@
|
|||
# Copyright (C) 2017 - 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 InitNewGradeHistoryAuditLogIndexes < ActiveRecord::Migration[4.2]
|
||||
tag :postdeploy
|
||||
|
||||
def self.up
|
||||
DataFixup::InitNewGradeHistoryAuditLogIndexes.send_later_if_production_enqueue_args(
|
||||
:run, {
|
||||
priority: Delayed::LOW_PRIORITY,
|
||||
strand: "init_new_grade_history_audit_log_indexes:#{Shard.current.database_server.id}"
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,140 @@
|
|||
#
|
||||
# Copyright (C) 2017 - 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 DataFixup
|
||||
class InitNewGradeHistoryAuditLogIndexes
|
||||
|
||||
def self.run
|
||||
new.build_indexes
|
||||
end
|
||||
|
||||
SEARCH_CQL = %{
|
||||
SELECT id, created_at, context_id, assignment_id, grader_id, student_id
|
||||
FROM grade_changes
|
||||
WHERE token(id) > token(?)
|
||||
LIMIT ?
|
||||
}.freeze
|
||||
|
||||
INDEX_METHODS = [
|
||||
:add_course_assignment_index,
|
||||
:add_course_assignment_grader_index,
|
||||
:add_course_assignment_grader_student_index,
|
||||
:add_course_assignment_student_index,
|
||||
:add_course_grader_index,
|
||||
:add_course_grader_student_index,
|
||||
:add_course_student_index
|
||||
].freeze
|
||||
|
||||
READ_BATCH_SIZE = 1000
|
||||
WRITE_BATCH_SIZE = 400
|
||||
|
||||
def build_indexes
|
||||
new_index_entries = []
|
||||
last_seen_id = nil
|
||||
done = false
|
||||
until done
|
||||
done, last_seen_id = read_and_process_batch(last_seen_id, new_index_entries)
|
||||
end
|
||||
write_batch(new_index_entries)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def read_and_process_batch(starting_key, index_entries)
|
||||
last_id = nil
|
||||
result = database.execute(SEARCH_CQL, starting_key, READ_BATCH_SIZE)
|
||||
return true, nil if result.rows == 0
|
||||
result.fetch do |row|
|
||||
last_id = row['id']
|
||||
INDEX_METHODS.each do |method|
|
||||
result = self.send(method, row)
|
||||
index_entries << result if result
|
||||
end
|
||||
end
|
||||
write_in_batches(index_entries)
|
||||
return false, last_id
|
||||
end
|
||||
|
||||
ResultStruct = Struct.new(:index, :record, :key)
|
||||
|
||||
def write_in_batches(batch)
|
||||
while batch.size >= WRITE_BATCH_SIZE
|
||||
write_batch(batch.shift(WRITE_BATCH_SIZE))
|
||||
end
|
||||
end
|
||||
|
||||
def write_batch(batch)
|
||||
return if batch.empty?
|
||||
database.batch { batch.each { |r| r.index.insert(r.record, r.key) } }
|
||||
end
|
||||
|
||||
def database
|
||||
@database ||= Canvas::Cassandra::DatabaseBuilder.from_config(:auditors)
|
||||
end
|
||||
|
||||
def add_course_assignment_index(row)
|
||||
index = Auditors::GradeChange::Stream.course_assignment_index
|
||||
key = [row['context_id'], row['assignment_id']]
|
||||
ResultStruct.new(index, OpenStruct.new(row.to_hash), key)
|
||||
end
|
||||
|
||||
def add_course_assignment_grader_index(row)
|
||||
return unless row['grader_id']
|
||||
|
||||
index = Auditors::GradeChange::Stream.course_assignment_grader_index
|
||||
key = [row['context_id'], row['assignment_id'], row['grader_id']]
|
||||
ResultStruct.new(index, OpenStruct.new(row.to_hash), key)
|
||||
end
|
||||
|
||||
def add_course_assignment_grader_student_index(row)
|
||||
return unless row['grader_id']
|
||||
|
||||
index = Auditors::GradeChange::Stream.course_assignment_grader_student_index
|
||||
key = [row['context_id'], row['assignment_id'], row['grader_id'], row['student_id']]
|
||||
ResultStruct.new(index, OpenStruct.new(row.to_hash), key)
|
||||
end
|
||||
|
||||
def add_course_assignment_student_index(row)
|
||||
index = Auditors::GradeChange::Stream.course_assignment_student_index
|
||||
key = [row['context_id'], row['assignment_id'], row['student_id']]
|
||||
ResultStruct.new(index, OpenStruct.new(row.to_hash), key)
|
||||
end
|
||||
|
||||
def add_course_grader_index(row)
|
||||
return unless row['grader_id']
|
||||
|
||||
index = Auditors::GradeChange::Stream.course_grader_index
|
||||
key = [row['context_id'], row['grader_id']]
|
||||
ResultStruct.new(index, OpenStruct.new(row.to_hash), key)
|
||||
end
|
||||
|
||||
def add_course_grader_student_index(row)
|
||||
return unless row['grader_id']
|
||||
|
||||
index = Auditors::GradeChange::Stream.course_grader_student_index
|
||||
key = [row['context_id'], row['grader_id'], row['student_id']]
|
||||
ResultStruct.new(index, OpenStruct.new(row.to_hash), key)
|
||||
end
|
||||
|
||||
def add_course_student_index(row)
|
||||
index = Auditors::GradeChange::Stream.course_student_index
|
||||
key = [row['context_id'], row['student_id']]
|
||||
ResultStruct.new(index, OpenStruct.new(row.to_hash), key)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -85,6 +85,51 @@ describe "GradeChangeAudit API", type: :request do
|
|||
api_call_as_user(@viewing_user, :get, path, arguments, {}, {}, options.slice(:expected_status))
|
||||
end
|
||||
|
||||
def fetch_for_course_and_other_contexts(contexts, options={})
|
||||
expected_contexts = [:course, :assignment, :grader, :student].freeze
|
||||
sorted_contexts = contexts.select { |key,_| expected_contexts.include?(key) }.
|
||||
sort_by { |key, _| expected_contexts.index(key) }
|
||||
|
||||
arguments = sorted_contexts.map { |key, value| ["#{key}_id".to_sym, value.id] }.to_h
|
||||
arguments.merge!({
|
||||
controller: 'grade_change_audit_api',
|
||||
action: 'for_course_and_other_parameters',
|
||||
format: 'json'
|
||||
})
|
||||
|
||||
query_string = []
|
||||
|
||||
per_page = options.delete(:per_page)
|
||||
if per_page
|
||||
arguments[:per_page] = per_page.to_s
|
||||
query_string << "per_page=#{arguments[:per_page]}"
|
||||
end
|
||||
|
||||
start_time = options.delete(:start_time)
|
||||
if start_time
|
||||
arguments[:start_time] = start_time.iso8601
|
||||
query_string << "start_time=#{arguments[:start_time]}"
|
||||
end
|
||||
|
||||
end_time = options.delete(:end_time)
|
||||
if end_time
|
||||
arguments[:end_time] = end_time.iso8601
|
||||
query_string << "end_time=#{arguments[:end_time]}"
|
||||
end
|
||||
|
||||
account = options.delete(:account)
|
||||
if account
|
||||
arguments[:account_id] = Shard.global_id_for(account).to_s
|
||||
query_string << "account_id=#{arguments[:account_id]}"
|
||||
end
|
||||
|
||||
path_args = sorted_contexts.map { |key, value| "#{key.to_s.pluralize}/#{value.id}" }.join('/')
|
||||
|
||||
path = "/api/v1/audit/grade_change/#{path_args}"
|
||||
path += "?" + query_string.join('&') if query_string.present?
|
||||
api_call_as_user(@viewing_user, :get, path, arguments, {}, {}, options.slice(:expected_status))
|
||||
end
|
||||
|
||||
def expect_event_for_context(context, event, options={})
|
||||
json = options.delete(:json)
|
||||
json ||= fetch_for_context(context, options)
|
||||
|
@ -93,6 +138,14 @@ describe "GradeChangeAudit API", type: :request do
|
|||
json
|
||||
end
|
||||
|
||||
def expect_event_for_course_and_contexts(contexts, event, options={})
|
||||
json = options.delete(:json)
|
||||
json ||= fetch_for_course_and_other_contexts(contexts, options)
|
||||
expect(json['events'].map{ |e| [e['id'], e['event_type']] })
|
||||
.to include([event.id, event.event_type])
|
||||
json
|
||||
end
|
||||
|
||||
def forbid_event_for_context(context, event, options={})
|
||||
json = options.delete(:json)
|
||||
json ||= fetch_for_context(context, options)
|
||||
|
@ -101,12 +154,48 @@ describe "GradeChangeAudit API", type: :request do
|
|||
json
|
||||
end
|
||||
|
||||
def forbid_event_for_course_and_contexts(contexts, event, options={})
|
||||
json = options.delete(:json)
|
||||
json ||= fetch_for_course_and_contexts(contexts, options)
|
||||
expect(json['events'].map{ |e| [e['id'], e['event_type']] })
|
||||
.not_to include([event.id, event.event_type])
|
||||
json
|
||||
end
|
||||
|
||||
def test_course_and_contexts
|
||||
# course assignment
|
||||
contexts = { course: @course, assignment: @assignment }
|
||||
yield(contexts)
|
||||
# course assignment grader
|
||||
contexts[:grader] = @teacher
|
||||
yield(contexts)
|
||||
# course assignment grader student
|
||||
contexts[:student] = @student
|
||||
yield(contexts)
|
||||
# course assignment student
|
||||
contexts.delete(:grader)
|
||||
yield(contexts)
|
||||
# course student
|
||||
contexts.delete(:assignment)
|
||||
yield(contexts)
|
||||
# course grader
|
||||
contexts = { course: @course, grader: @teacher}
|
||||
yield(contexts)
|
||||
# course grader student
|
||||
contexts[:student] = @student
|
||||
yield(contexts)
|
||||
end
|
||||
|
||||
context "nominal cases" do
|
||||
it "should include events at context endpoint" do
|
||||
expect_event_for_context(@assignment, @event)
|
||||
expect_event_for_context(@course, @event)
|
||||
expect_event_for_context(@student, @event, type: "student")
|
||||
expect_event_for_context(@teacher, @event, type: "grader")
|
||||
|
||||
test_course_and_contexts do |contexts|
|
||||
expect_event_for_course_and_contexts(contexts, @event)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -121,6 +210,7 @@ describe "GradeChangeAudit API", type: :request do
|
|||
|
||||
it "should recognize :start_time" do
|
||||
json = expect_event_for_context(@assignment, @event, start_time: 12.hours.ago)
|
||||
|
||||
forbid_event_for_context(@assignment, @event2, start_time: 12.hours.ago, json: json)
|
||||
|
||||
json = expect_event_for_context(@course, @event, start_time: 12.hours.ago)
|
||||
|
@ -131,6 +221,11 @@ describe "GradeChangeAudit API", type: :request do
|
|||
|
||||
json = expect_event_for_context(@teacher, @event, type: "grader", start_time: 12.hours.ago)
|
||||
forbid_event_for_context(@teacher, @event2, type: "grader", start_time: 12.hours.ago, json: json)
|
||||
|
||||
test_course_and_contexts do |contexts|
|
||||
json = expect_event_for_course_and_contexts(contexts, @event, start_time: 12.hours.ago)
|
||||
forbid_event_for_course_and_contexts(contexts, @event2, start_time: 12.hours.ago, json: json)
|
||||
end
|
||||
end
|
||||
|
||||
it "should recognize :end_time" do
|
||||
|
@ -145,6 +240,11 @@ describe "GradeChangeAudit API", type: :request do
|
|||
|
||||
json = expect_event_for_context(@teacher, @event2, type: "grader", end_time: 12.hours.ago)
|
||||
forbid_event_for_context(@teacher, @event, type: "grader", end_time: 12.hours.ago, json: json)
|
||||
|
||||
test_course_and_contexts do |contexts|
|
||||
json = expect_event_for_course_and_contexts(contexts, @event2, end_time: 12.hours.ago)
|
||||
forbid_event_for_course_and_contexts(contexts, @event, end_time: 12.hours.ago, json: json)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -154,9 +254,27 @@ describe "GradeChangeAudit API", type: :request do
|
|||
fetch_for_context(@assignment, expected_status: 404)
|
||||
end
|
||||
|
||||
it "should 404 for inactive courses" do
|
||||
it "should allow inactive assignments when used with a course" do
|
||||
@assignment.destroy
|
||||
fetcher = lambda do |contexts|
|
||||
fetch_for_course_and_other_contexts(contexts, expected_status: 200)
|
||||
end
|
||||
contexts = {course: @course, assignment: @assignment}
|
||||
fetcher.call(contexts)
|
||||
contexts[:grader] = @teacher
|
||||
fetcher.call(contexts)
|
||||
contexts[:student] = @student
|
||||
fetcher.call(contexts)
|
||||
contexts.delete(:grader)
|
||||
fetcher.call(contexts)
|
||||
end
|
||||
|
||||
it "should allow inactive courses" do
|
||||
@course.destroy
|
||||
fetch_for_context(@course, expected_status: 404)
|
||||
fetch_for_context(@course, expected_status: 200)
|
||||
test_course_and_contexts do |contexts|
|
||||
fetch_for_course_and_other_contexts(contexts, expected_status: 200)
|
||||
end
|
||||
end
|
||||
|
||||
it "should 404 for inactive students" do
|
||||
|
@ -164,10 +282,40 @@ describe "GradeChangeAudit API", type: :request do
|
|||
fetch_for_context(@student, expected_status: 404, type: "student")
|
||||
end
|
||||
|
||||
it "should allow inactive students when used with a course" do
|
||||
@student.destroy
|
||||
fetcher = lambda do |contexts|
|
||||
fetch_for_course_and_other_contexts(contexts, expected_status: 200)
|
||||
end
|
||||
contexts = {course: @course, assignment: @assignment, grader: @teacher, student: @student}
|
||||
fetcher.call(contexts)
|
||||
contexts.delete(:grader)
|
||||
fetcher.call(contexts)
|
||||
contexts.delete(:assignment)
|
||||
fetcher.call(contexts)
|
||||
contexts = {course: @course, student: @student}
|
||||
fetcher.call(contexts)
|
||||
end
|
||||
|
||||
it "should 404 for inactive grader" do
|
||||
@teacher.destroy
|
||||
fetch_for_context(@teacher, expected_status: 404, type: "grader")
|
||||
end
|
||||
|
||||
it "shoudl allow inactive graders when used with a course" do
|
||||
@teacher.destroy
|
||||
fetcher = lambda do |contexts|
|
||||
fetch_for_course_and_other_contexts(contexts, expected_status: 200)
|
||||
end
|
||||
contexts = {course: @course, assignment: @assignment, grader: @teacher, student: @student}
|
||||
fetcher.call(contexts)
|
||||
contexts.delete(:student)
|
||||
fetcher.call(contexts)
|
||||
contexts.delete(:assignment)
|
||||
fetcher.call(contexts)
|
||||
contexts[:student] = @student
|
||||
fetcher.call(contexts)
|
||||
end
|
||||
end
|
||||
|
||||
describe "permissions" do
|
||||
|
@ -178,15 +326,22 @@ describe "GradeChangeAudit API", type: :request do
|
|||
fetch_for_context(@assignment, expected_status: 401)
|
||||
fetch_for_context(@student, expected_status: 401, type: "student")
|
||||
fetch_for_context(@teacher, expected_status: 401, type: "grader")
|
||||
test_course_and_contexts do |contexts|
|
||||
fetch_for_course_and_other_contexts(contexts, expected_status: 401)
|
||||
end
|
||||
end
|
||||
|
||||
it "should not authorize the endpoints with revoking the :view_grade_changes permission" do
|
||||
RoleOverride.manage_role_override(@account_user.account, @account_user.role, :view_grade_changes.to_s, :override => false)
|
||||
RoleOverride.manage_role_override(@account_user.account, @account_user.role,
|
||||
:view_grade_changes.to_s, :override => false)
|
||||
|
||||
fetch_for_context(@course, expected_status: 401)
|
||||
fetch_for_context(@assignment, expected_status: 401)
|
||||
fetch_for_context(@student, expected_status: 401, type: "student")
|
||||
fetch_for_context(@teacher, expected_status: 401, type: "grader")
|
||||
test_course_and_contexts do |contexts|
|
||||
fetch_for_course_and_other_contexts(contexts, expected_status: 401)
|
||||
end
|
||||
end
|
||||
|
||||
it "should not allow other account models" do
|
||||
|
@ -194,10 +349,13 @@ describe "GradeChangeAudit API", type: :request do
|
|||
LoadAccount.stubs(:default_domain_root_account).returns(new_root_account)
|
||||
@viewing_user = user_with_pseudonym(account: new_root_account)
|
||||
|
||||
fetch_for_context(@course, expected_status: 404)
|
||||
fetch_for_context(@assignment, expected_status: 404)
|
||||
fetch_for_context(@student, expected_status: 404, type: "student")
|
||||
fetch_for_context(@teacher, expected_status: 404, type: "grader")
|
||||
fetch_for_context(@course, expected_status: 401)
|
||||
fetch_for_context(@assignment, expected_status: 401)
|
||||
fetch_for_context(@student, expected_status: 401, type: "student")
|
||||
fetch_for_context(@teacher, expected_status: 401, type: "grader")
|
||||
test_course_and_contexts do |contexts|
|
||||
fetch_for_course_and_other_contexts(contexts, expected_status: 401)
|
||||
end
|
||||
end
|
||||
|
||||
context "sharding" do
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
#
|
||||
# Copyright (C) 2017 - 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 'spec_helper'
|
||||
|
||||
describe DataFixup::InitNewGradeHistoryAuditLogIndexes do
|
||||
def insert_cql(use_grader: false)
|
||||
%{
|
||||
INSERT INTO grade_changes (id, created_at, context_id, assignment_id,
|
||||
#{'grader_id, ' if use_grader} student_id)
|
||||
VALUES (?, ?, ?, ?, #{'?,' if use_grader} ?) USING TTL ?
|
||||
}
|
||||
end
|
||||
|
||||
def search_table_cql(table_name)
|
||||
"SELECT id FROM #{table_name}"
|
||||
end
|
||||
|
||||
NO_GRADER_REQUIRED_TABLES = %w(
|
||||
grade_changes_by_course_assignment
|
||||
grade_changes_by_course_assignment_student
|
||||
grade_changes_by_course_student
|
||||
).freeze
|
||||
|
||||
GRADER_REQUIRED_TABLES = %w(
|
||||
grade_changes_by_course_assignment_grader
|
||||
grade_change_by_course_assignment_grader_student
|
||||
grade_changes_by_course_grader
|
||||
grade_changes_by_course_grader_student
|
||||
).freeze
|
||||
|
||||
INDEX_TABLES = (NO_GRADER_REQUIRED_TABLES + GRADER_REQUIRED_TABLES).freeze
|
||||
|
||||
before(:each) do
|
||||
@database = Canvas::Cassandra::DatabaseBuilder.from_config(:auditors)
|
||||
|
||||
@values = [
|
||||
[
|
||||
'08d87bfc-a679-4f5d-9315-470a5fc7d7d0',
|
||||
Time.new(2016, 10, 18, 23, 24, 38).utc,
|
||||
10000000000018,
|
||||
10000000000116,
|
||||
10000000000001,
|
||||
10000000000006,
|
||||
1.year
|
||||
],
|
||||
[
|
||||
'fc85afda-538e-4fcb-a7fb-45697c551b71',
|
||||
Time.new(2017, 6, 26, 16, 40, 5).utc,
|
||||
10000000000028,
|
||||
10000000000144,
|
||||
10000000000003,
|
||||
1.year
|
||||
]
|
||||
]
|
||||
|
||||
@values.each do |v|
|
||||
@database.execute(insert_cql(use_grader: v.size > 6), *v)
|
||||
end
|
||||
end
|
||||
|
||||
it 'creates all the new indexes for records with grader ids' do
|
||||
DataFixup::InitNewGradeHistoryAuditLogIndexes.run
|
||||
|
||||
INDEX_TABLES.each do |table_name|
|
||||
cql = search_table_cql(table_name)
|
||||
ids = []
|
||||
@database.execute(cql).fetch do |row|
|
||||
ids << row['id']
|
||||
end
|
||||
|
||||
expect(ids).to include(@values.first.first)
|
||||
end
|
||||
end
|
||||
|
||||
it 'creates the expected subset of indexes for records without grader ids' do
|
||||
DataFixup::InitNewGradeHistoryAuditLogIndexes.run
|
||||
|
||||
INDEX_TABLES.each do |table_name|
|
||||
cql = search_table_cql(table_name)
|
||||
ids = []
|
||||
@database.execute(cql).fetch do |row|
|
||||
ids << row['id']
|
||||
end
|
||||
|
||||
if GRADER_REQUIRED_TABLES.include?(table_name)
|
||||
expect(ids).not_to include(@values.second.first)
|
||||
else
|
||||
expect(ids).to include(@values.second.first)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -48,13 +48,44 @@ describe Auditors::GradeChange do
|
|||
Timecop.freeze(@event_time) { @event = Auditors::GradeChange.record(@submission) }
|
||||
end
|
||||
|
||||
def test_course_and_other_contexts
|
||||
# course assignment
|
||||
contexts = { assignment: @assignment }
|
||||
yield contexts
|
||||
# course assignment grader
|
||||
contexts[:grader] = @teacher
|
||||
yield contexts
|
||||
# course assignment grader student
|
||||
contexts[:student] = @student
|
||||
yield contexts
|
||||
# course assignment student
|
||||
contexts.delete(:grader)
|
||||
yield contexts
|
||||
# course grader
|
||||
contexts = { grader: @teacher }
|
||||
yield contexts
|
||||
# course grader student
|
||||
contexts[:student] = @student
|
||||
yield contexts
|
||||
# course student
|
||||
contexts.delete(:grader)
|
||||
yield contexts
|
||||
end
|
||||
|
||||
context "nominal cases" do
|
||||
it "should include event" do
|
||||
expect(@event.created_at).to eq @event_time
|
||||
expect(Auditors::GradeChange.for_assignment(@assignment).paginate(:per_page => 5)).to include(@event)
|
||||
expect(Auditors::GradeChange.for_course(@course).paginate(:per_page => 5)).to include(@event)
|
||||
expect(Auditors::GradeChange.for_root_account_student(@account, @student).paginate(:per_page => 5)).to include(@event)
|
||||
expect(Auditors::GradeChange.for_root_account_grader(@account, @teacher).paginate(:per_page => 5)).to include(@event)
|
||||
expect(Auditors::GradeChange.for_root_account_student(@account, @student).
|
||||
paginate(:per_page => 5)).to include(@event)
|
||||
expect(Auditors::GradeChange.for_root_account_grader(@account, @teacher).
|
||||
paginate(:per_page => 5)).to include(@event)
|
||||
|
||||
test_course_and_other_contexts do |contexts|
|
||||
expect(Auditors::GradeChange.for_course_and_other_arguments(@course, contexts).
|
||||
paginate(:per_page => 5)).to include(@event)
|
||||
end
|
||||
end
|
||||
|
||||
it "should include event for nil grader" do
|
||||
|
@ -65,7 +96,14 @@ describe Auditors::GradeChange do
|
|||
|
||||
expect(Auditors::GradeChange.for_assignment(@assignment).paginate(:per_page => 5)).to include(@event)
|
||||
expect(Auditors::GradeChange.for_course(@course).paginate(:per_page => 5)).to include(@event)
|
||||
expect(Auditors::GradeChange.for_root_account_student(@account, @student).paginate(:per_page => 5)).to include(@event)
|
||||
expect(Auditors::GradeChange.for_root_account_student(@account, @student).
|
||||
paginate(:per_page => 5)).to include(@event)
|
||||
expect(Auditors::GradeChange.for_course_and_other_arguments(@course, {assignment: @assignment}).
|
||||
paginate(:per_page => 5)).to include(@event)
|
||||
expect(Auditors::GradeChange.for_course_and_other_arguments(@course, {assignment: @assignment,
|
||||
student: @student}).paginate(:per_page => 5)).to include(@event)
|
||||
expect(Auditors::GradeChange.for_course_and_other_arguments(@course, {student: @student}).
|
||||
paginate(:per_page => 5)).to include(@event)
|
||||
end
|
||||
|
||||
it "should include event for auto grader" do
|
||||
|
@ -77,7 +115,14 @@ describe Auditors::GradeChange do
|
|||
|
||||
expect(Auditors::GradeChange.for_assignment(@assignment).paginate(:per_page => 5)).to include(@event)
|
||||
expect(Auditors::GradeChange.for_course(@course).paginate(:per_page => 5)).to include(@event)
|
||||
expect(Auditors::GradeChange.for_root_account_student(@account, @student).paginate(:per_page => 5)).to include(@event)
|
||||
expect(Auditors::GradeChange.for_root_account_student(@account, @student).
|
||||
paginate(:per_page => 5)).to include(@event)
|
||||
expect(Auditors::GradeChange.for_course_and_other_arguments(@course, {assignment: @assignment}).
|
||||
paginate(:per_page => 5)).to include(@event)
|
||||
expect(Auditors::GradeChange.for_course_and_other_arguments(@course, {assignment: @assignment,
|
||||
student: @student}).paginate(:per_page => 5)).to include(@event)
|
||||
expect(Auditors::GradeChange.for_course_and_other_arguments(@course, {student: @student}).
|
||||
paginate(:per_page => 5)).to include(@event)
|
||||
end
|
||||
|
||||
it "should set request_id" do
|
||||
|
@ -92,10 +137,14 @@ describe Auditors::GradeChange do
|
|||
for_assignment = Auditors::GradeChange.for_assignment(@assignment)
|
||||
for_course = Auditors::GradeChange.for_course(@course)
|
||||
for_root_account_student = Auditors::GradeChange.for_root_account_student(@account, @student)
|
||||
|
||||
expect(for_assignment.paginate(per_page: 5)).to include(@event)
|
||||
expect(for_course.paginate(per_page: 5)).to include(@event)
|
||||
expect(for_root_account_student.paginate(per_page: 5)).to include(@event)
|
||||
|
||||
test_course_and_other_contexts do |contexts|
|
||||
expect(Auditors::GradeChange.for_course_and_other_arguments(@course, contexts).paginate(per_page: 5)).
|
||||
to include(@event)
|
||||
end
|
||||
end
|
||||
|
||||
it "reports formerly excused submissions" do
|
||||
|
@ -111,6 +160,10 @@ describe Auditors::GradeChange do
|
|||
expect(for_assignment.paginate(per_page: 5)).to include(@event)
|
||||
expect(for_course.paginate(per_page: 5)).to include(@event)
|
||||
expect(for_root_account_student.paginate(per_page: 5)).to include(@event)
|
||||
test_course_and_other_contexts do |contexts|
|
||||
expect(Auditors::GradeChange.for_course_and_other_arguments(@course, contexts).paginate(per_page: 5)).
|
||||
to include(@event)
|
||||
end
|
||||
end
|
||||
|
||||
it "records excused_before and excused_after as booleans on initial grading" do
|
||||
|
|
Loading…
Reference in New Issue