Add API for bulk uploading custom columns

When updating custom columns in database,
we can use the API for efficient bulk uploads

closes GRADE-1350

Test Plan
    - Send a PUT request to
      /api/v1/courses/:id/custom_gradebook_column_data
    - The request body should have
      an array of objects with attributes column_id, user_id, and content
    - An example below
      {column_data:
        [column_id: string, user_id: string, content: string]
      }
    - Make sure the auth token in Postman is configured properly
      (bearer token)
    - Send the request
    - Check to see if the data appears in the gradebook

Change-Id: I90e747d5d92478b1e3dd101e4f254dfd392486ed
Reviewed-on: https://gerrit.instructure.com/156647
Reviewed-by: Spencer Olson <solson@instructure.com>
Reviewed-by: Keith T. Garner <kgarner@instructure.com>
QA-Review: Adrian Packel <apackel@instructure.com>
Tested-by: Jenkins
Product-Review: Keith T. Garner <kgarner@instructure.com>
This commit is contained in:
Ryan Kuang 2018-07-09 16:21:24 -05:00
parent 4c3d0fc3be
commit 83bddc402c
5 changed files with 357 additions and 0 deletions

View File

@ -39,6 +39,7 @@ class CustomGradebookColumnDataApiController < ApplicationController
before_action :require_context, :require_user
include Api::V1::CustomGradebookColumn
include Api::V1::Progress
# @API List entries for a column
#
@ -99,6 +100,51 @@ class CustomGradebookColumnDataApiController < ApplicationController
end
end
# @API Bulk update column data
#
# Set the content of custom columns
#
# @argument column_data[] [Required, Array]
# Column content. Setting this to an empty string will delete the data object.
#
# @example_request
#
# {
# "column_data": [
# {
# "column_id": example_column_id,
# "user_id": example_student_id,
# "content": example_content
# },
# {
# "column_id": example_column_id,
# "user_id": example_student_id,
# "content: example_content
# }
# ]
# }
#
# @returns Progress
def bulk_update
bulk_update_params = params.permit(column_data: [:user_id, :column_id, :content])
column_data_as_array = bulk_update_params.to_h[:column_data]
raise ActionController::BadRequest if column_data_as_array.blank?
column_ids = column_data_as_array.map { |entry| entry.fetch(:column_id) }
cc = @context.custom_gradebook_columns.find(column_ids)
cc.each do |col|
return render_unauthorized_action unless authorized_action? col, @current_user, :read
end
user_ids = column_data_as_array.map { |entry| entry.fetch(:user_id)&.to_i }
return render_unauthorized_action if (user_ids - allowed_users.pluck(:id)).any?
progress = CustomGradebookColumnDatum.queue_bulk_update_custom_columns(@context, column_data_as_array)
render json: progress_json(progress, @current_user, session)
end
def allowed_users
@context.students_visible_to(@current_user, include: %i{inactive completed})
end

View File

@ -29,4 +29,32 @@ class CustomGradebookColumnDatum < ActiveRecord::Base
}
can :update
end
def self.queue_bulk_update_custom_columns(context, column_data)
progress = Progress.create!(context: context, tag: "custom_columns_submissions_update")
progress.process_job(self, :process_bulk_update_custom_columns, {}, context, column_data)
progress
end
def self.process_bulk_update_custom_columns(_, context, column_data)
Delayed::Batch.serial_batch(priority: Delayed::LOW_PRIORITY, n_strand: ["bulk_update_submissions", context.root_account.global_id]) do
custom_gradebook_columns = context.custom_gradebook_columns.preload(:custom_gradebook_column_data)
column_data.each do |data_point|
column_id = data_point.fetch(:column_id)
custom_column = custom_gradebook_columns.find { |custom_col| custom_col.id == column_id.to_i }
next if custom_column.blank?
content = data_point.fetch(:content)
user_id = data_point.fetch(:user_id)
if content.present?
CustomGradebookColumnDatum.unique_constraint_retry do
datum = custom_column.custom_gradebook_column_data.find_or_initialize_by(user_id: user_id)
datum.content = content
datum.save!
end
else
custom_column.custom_gradebook_column_data.find_by(user_id: user_id)&.destroy!
end
end
end
end
end

View File

@ -1913,6 +1913,7 @@ CanvasRails::Application.routes.draw do
prefix = "courses/:course_id/custom_gradebook_columns/:id/data"
get prefix, action: :index, as: "course_custom_gradebook_column_data"
put "#{prefix}/:user_id", action: :update, as: "course_custom_gradebook_column_datum"
put "courses/:course_id/custom_gradebook_column_data", action: :bulk_update, as: "course_custom_gradebook_column_bulk_data"
end
scope(controller: :content_exports_api) do

View File

@ -39,6 +39,7 @@ describe CustomGradebookColumnDataApiController, type: :request do
@user = @teacher
@col = @course.custom_gradebook_columns.create! title: "Notes", position: 1
@second_col = @course.custom_gradebook_columns.create! title: "Notes2", position: 2
end
describe 'index' do
@ -204,4 +205,121 @@ describe CustomGradebookColumnDataApiController, type: :request do
check.("shmarg")
end
end
describe 'bulk update' do
def bulk_update(args)
api_call(:put,
"/api/v1/courses/#{@course.id}/custom_gradebook_column_data",
{
course_id: @course.to_param,
action: "bulk_update",
controller: "custom_gradebook_column_data_api", format: "json"
},
{
"column_data" => [
{
"column_id" => args.first[:column_id],
"user_id" => args.first[:student_id],
"content" => args.first[:content]
}
]
})
end
it 'passes the contents to the api call successfully' do
@user = @teacher
contents = [
{
column_id: @col.to_param,
student_id: @student1.to_param,
content: 'Column 1, Student 1'
}
]
json = bulk_update(contents)
expect(json.fetch('workflow_state')).to eq "queued"
end
it 'passes muliple contents to the api call successfully' do
@user = @teacher
contents = [
{
column_id: @col.to_param,
student_id: @student1.to_param,
content: 'Column 1, Student 1'
},
{
column_id: @second_col.to_param,
student_id: @student2.to_param,
content: 'Column 2, Student 2'
}
]
json = api_call :put,
"/api/v1/courses/#{@course.id}/custom_gradebook_column_data",
{
course_id: @course.to_param,
action: "bulk_update",
controller: "custom_gradebook_column_data_api", format: "json"
},
{
"column_data" => [
{
"column_id" => contents.first[:column_id],
"user_id" => contents.first[:student_id],
"content" => contents.first[:content]
},
{
"column_id" => contents.second[:column_id],
"user_id" => contents.second[:student_id],
"content" => contents.second[:content]
}
]
}
expect(json.fetch('workflow_state')).to eq "queued"
end
it 'throws 401 status when updating non existing student' do
@user = @teacher
contents = [
{
column_id: @col.to_param,
student_id: -1.to_param,
content: 'Non existing student 1'
}
]
bulk_update(contents)
assert_status(401)
end
it 'throws 400 status when passing empty input' do
@user = @teacher
api_call :put,
"/api/v1/courses/#{@course.id}/custom_gradebook_column_data",
{
course_id: @course.to_param,
action: "bulk_update",
controller: "custom_gradebook_column_data_api", format: "json"
}, {}
assert_status(400)
end
it 'throws 400 status when passing empty array in column_data' do
@user = @teacher
api_call :put,
"/api/v1/courses/#{@course.id}/custom_gradebook_column_data",
{
course_id: @course.to_param,
action: "bulk_update",
controller: "custom_gradebook_column_data_api", format: "json"
},
{ "column_data" => [] }
assert_status(400)
end
end
end

View File

@ -0,0 +1,164 @@
#
# Copyright (C) 2018 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
require_relative '../../../spec_helper'
describe Api::V1::CustomGradebookColumnDatum do
describe "custom_gradebook_column_bulk_upload" do
before :once do
course_with_teacher(active_all: true)
@first_student = student_in_course(active_all: true, course: @course).user
@second_student = student_in_course(active_all: true, course: @course).user
@first_col = @course.custom_gradebook_columns.create!(title: "cc1", position: 1)
@second_col = @course.custom_gradebook_columns.create!(title: "cc2", position: 2)
CustomGradebookColumnDatum.process_bulk_update_custom_columns({}, @course, [
{
"column_id": @first_col.id.to_s,
"user_id": @first_student.id.to_s,
"content": "first column, first student"
},
{
"column_id": @first_col.id.to_s,
"user_id": @second_student.id.to_s,
"content": "first column, second student"
},
{
"column_id": @second_col.id.to_s,
"user_id": @first_student.id.to_s,
"content": "second column, first student"
},
{
"column_id": @second_col.id.to_s,
"user_id": @second_student.id.to_s,
"content": "second column, second student"
}
])
end
it "adds a datum for a matching student and column" do
data = @course.custom_gradebook_columns.find_by!(id: @first_col.id).
custom_gradebook_column_data.where(user_id: @first_student.id)
expect(data.count).to eql 1
end
it "checks content exists for the first student in the first column" do
data = @course.custom_gradebook_columns.find_by!(id: @first_col.id).
custom_gradebook_column_data.find_by!(user_id: @first_student.id).content
expect(data).to eql "first column, first student"
end
it "adds data for multiple students for a column" do
data = @course.custom_gradebook_columns.find_by!(id: @first_col.id).
custom_gradebook_column_data
expect(data.count).to eql 2
end
it "adds data for multiple columns" do
data = @course.custom_gradebook_columns.where(id: [@first_col.id, @second_col.id])
expect(data.count).to eql 2
end
it "does not create new columns when column doesn't exist" do
CustomGradebookColumnDatum.process_bulk_update_custom_columns({}, @course, [
{
"column_id": (@second_col.id + 1001).to_s,
"user_id": @second_student.id.to_s,
"content": "first column, second student"
},
])
data = @course.custom_gradebook_columns.where(id: @second_col.id + 1001)
expect(data.count).to eql 0
end
it "updates the content for existing student and column" do
CustomGradebookColumnDatum.process_bulk_update_custom_columns({}, @course, [
{
"column_id": @second_col.id.to_s,
"user_id": @second_student.id.to_s,
"content": "2, 2"
}
])
data = @course.custom_gradebook_columns.find_by!(id: @second_col.id).
custom_gradebook_column_data.find_by!(user_id: @second_student.id).content
expect(data).to eql "2, 2"
end
it "can pass the column ID as a number" do
CustomGradebookColumnDatum.process_bulk_update_custom_columns({}, @course, [
{
"column_id": @second_col.id,
"user_id": @second_student.id,
"content": "2, 2"
}
])
data = @course.custom_gradebook_columns.find_by!(id: @second_col.id).
custom_gradebook_column_data.find_by!(user_id: @second_student.id).content
expect(data).to eql "2, 2"
end
it "can pass the column ID as a string" do
CustomGradebookColumnDatum.process_bulk_update_custom_columns({}, @course, [
{
"column_id": @second_col.id.to_s,
"user_id": @second_student.id.to_s,
"content": "2, 2"
}
])
data = @course.custom_gradebook_columns.find_by!(id: @second_col.id).
custom_gradebook_column_data.find_by!(user_id: @second_student.id).content
expect(data).to eql "2, 2"
end
it "does not update content in deleted columns" do
@course.custom_gradebook_columns.find_by!(id: @second_col.id).
custom_gradebook_column_data.find_by!(user_id: @first_student.id).delete
@course.custom_gradebook_columns.find_by!(id: @second_col.id).
custom_gradebook_column_data.find_by!(user_id: @second_student.id).delete
@course.custom_gradebook_columns.find_by!(id: @second_col.id).delete
CustomGradebookColumnDatum.process_bulk_update_custom_columns({}, @course, [
{
"column_id": @second_col.id.to_s,
"user_id": @second_student.id.to_s,
"content": "3, 2"
},
])
data = @course.custom_gradebook_columns.where(id: @second_col.id)
expect(data.count).to eql 0
end
it "destroys data when uploading empty string" do
CustomGradebookColumnDatum.process_bulk_update_custom_columns({}, @course, [
{
"column_id": @first_col.id.to_s,
"user_id": @first_student.id.to_s,
"content": ""
},
])
data = @course.custom_gradebook_columns.find_by!(id: @first_col.id).
custom_gradebook_column_data.find_by(user_id: @first_student.id)
expect(data).to eql nil
end
end
end