build rubric import backend

this commit builds the backend for the new rubric import feature. using
the api endpoint "upload", users can create multiple rubrics via csv
which will show up on the Rubrics page of the account or course. with the
new migration, RubricImports are stored in the RubricImport table and
each Rubric is associated with a rubric_import_id. this commit also
handles errors and includes tests.

closes EVAL-4361
closes EVAL-4362
flag=rubric_imports_exports

test plan:
- create a test csv file with the following headers:
  - "Rubric Name"
  - "Criteria Name"
  - "Criteria Description"
  - "Criteria Enable Range" (if feature flag is enabled)
  - "Rating Name"
  - "Rating Description"
  - "Rating Points"
- repeat "Rating Name", "Rating Description", and "Rating Points" in the
  same row to add multiple ratings to a criteria
- send in a POST request with the following url:
http://canvas.docker/api/v1/accounts/1/rubrics/upload
- check the Rubrics page under your account to see your new rubrics
- verify that sending incorrectly formatted rubrics will not add the
  rubrics to rubric builder page
  - RubricImport model will show the errors in the error_data attribute

Change-Id: I342c2ae840bd3f5e8470c7d5b43617c9b6d70308
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/352960
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Migration-Review: Cody Cutrer <cody@instructure.com>
Reviewed-by: Chris Soto <christopher.soto@instructure.com>
Reviewed-by: Derek Williams <derek.williams@instructure.com>
QA-Review: Chris Soto <christopher.soto@instructure.com>
Product-Review: Cameron Ray <cameron.ray@instructure.com>
This commit is contained in:
Ojaswee Chaudhary 2024-07-09 09:12:55 -07:00
parent 748a8cf8c0
commit 22b61580e0
11 changed files with 654 additions and 1 deletions

View File

@ -319,6 +319,25 @@ class RubricsApiController < ApplicationController
render json: used_locations_for(rubric)
end
def upload
return unless authorized_action(@context, @current_user, :manage_rubrics)
file_obj = params[:attachment]
if file_obj.nil?
render json: { message: "No file attached" }, status: :bad_request
end
import = RubricImport.create_with_attachment(
@context, file_obj, @current_user
)
import.schedule
import_response = api_json(import, @current_user, session)
import_response[:user] = user_json(import.user, @current_user, session) if import.user
render json: import_response
end
private
def rubric_assessments(rubric)

View File

@ -97,6 +97,7 @@ class Account < ActiveRecord::Base
has_many :canvadocs_annotation_contexts
has_one :outcome_proficiency, -> { preload(:outcome_proficiency_ratings) }, as: :context, inverse_of: :context, dependent: :destroy
has_one :outcome_calculation_method, as: :context, inverse_of: :context, dependent: :destroy
has_many :rubric_imports, inverse_of: :root_account, foreign_key: :root_account_id
has_many :auditor_authentication_records,
class_name: "Auditors::ActiveRecord::AuthenticationRecord",

159
app/models/rubric_import.rb Normal file
View File

@ -0,0 +1,159 @@
# frozen_string_literal: true
#
# Copyright (C) 2024 - 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 RubricImport < ApplicationRecord
include Workflow
include RubricImporterErrors
belongs_to :account, optional: true
belongs_to :course, optional: true
belongs_to :attachment
belongs_to :root_account, class_name: "Account", inverse_of: :rubric_imports
belongs_to :user
workflow do
state :initializing
state :created do
event :job_started, transitions_to: :importing
end
state :importing do
event :job_completed, transitions_to: :succeeded do
update!(progress: 100)
end
event :job_completed_with_errors, transitions_to: :succeeded_with_errors do
update!(progress: 100)
end
event :job_failed, transitions_to: :failed
end
state :succeeded
state :succeeded_with_errors
state :failed
end
def context
account || course
end
def context=(val)
case val
when Account
self.account = val
when Course
self.course = val
end
end
def self.create_with_attachment(rubric_context, attachment, user = nil)
import = RubricImport.create!(
root_account: rubric_context.root_account,
progress: 0,
workflow_state: :initializing,
user:,
error_count: 0,
error_data: [],
context: rubric_context
)
att = Attachment.create_data_attachment(import, attachment, "rubric_upload_#{import.global_id}.csv")
import.attachment = att
yield import if block_given?
import.workflow_state = :created
import.save!
import
end
def schedule
delay(strand: "RubricImport::run::#{context.root_account.global_id}").run
end
def run
context.root_account.shard.activate do
job_started!
error_data = process_rubrics
unless error_data.empty?
update!(error_count: error_data.count, error_data:)
job_completed_with_errors!
return
end
job_completed!
rescue DataFormatError => e
ErrorReport.log_exception("rubrics_import_data_format", e)
update!(error_count: 1, error_data: [{ message: e.message }])
job_failed!
rescue CSV::MalformedCSVError => e
ErrorReport.log_exception("rubrics_import_csv", e)
update!(error_count: 1, error_data: [{ message: I18n.t("The file is not a valid CSV file."), exception: e.message }])
rescue => e
ErrorReport.log_exception("rubrics_import", e)
update!(error_count: 1, error_data: [{ message: I18n.t("An error occurred while importing rubrics."), exception: e.message }])
job_failed!
end
end
def process_rubrics
rubrics_by_name = RubricCSVImporter.new(attachment).parse
raise DataFormatError, I18n.t("The file is empty or does not contain valid rubric data.") if rubrics_by_name.empty?
total_rubrics = rubrics_by_name.keys.count
error_data = []
rubrics_by_name.each_with_index do |(rubric_name, rubric_data), rubric_index|
raise DataFormatError, I18n.t("Missing 'Rubric Name' for some rubrics.") if rubric_name.blank?
rubric = context.rubrics.build(rubric_imports_id: id)
criteria_hash = {}
rubric_data.each_with_index do |criterion, criterion_index|
raise DataFormatError, "Missing 'Criteria Name' for #{rubric_name}" if criterion[:description].blank?
raise DataFormatError, "Missing ratings for #{criterion[:description]}" if criterion[:ratings].empty?
ratings_hash = {}
criterion[:ratings].each_with_index do |rating, rating_index|
ratings_hash[rating_index.to_s] = {
"description" => rating[:description],
"long_description" => rating[:long_description],
"points" => rating[:points]
}
end
criteria_hash[criterion_index.to_s] = {
"description" => criterion[:description],
"long_description" => criterion[:long_description],
"ratings" => ratings_hash
}
if context.root_account.feature_enabled?(:rubric_criterion_range)
criteria_hash[criterion_index.to_s]["criterion_use_range"] = criterion[:criterion_use_range]
end
end
rubric_params = {
title: rubric_name,
criteria: criteria_hash.with_indifferent_access,
workflow_state: "draft"
}
association_params = { association_object: context }
rubric.update_with_association(user, rubric_params, context, association_params)
update!(progress: ((rubric_index + 1) * 100 / total_rubrics))
rescue DataFormatError => e
error_data << { message: e.message }
rescue ActiveRecord::StatementInvalid => e
error_data << { message: I18n.t("The rubric '%{rubric_name}' could not be saved.", rubric_name:), exception: e.message }
end
error_data
end
end

View File

@ -358,4 +358,16 @@ speedgrader_studio_media_capture:
ci:
state: allowed
development:
state: allowed
state: allowed
rubric_imports_exports:
state: hidden
applies_to: RootAccount
root_opt_in: true
display_name: Rubric Imports and Exports
description: This feature allows users to import and export rubrics from CSV and XML files.
environments:
ci:
state: allowed
development:
state: allowed

View File

@ -2502,6 +2502,8 @@ CanvasRails::Application.routes.draw do
get "courses/:course_id/rubrics/:id", action: :show
get "courses/:course_id/rubrics/:id/used_locations", action: "used_locations", as: "rubrics_course_used_locations"
get "accounts/:account_id/rubrics/:id/used_locations", action: "used_locations", as: "rubrics_account_used_locations"
post "courses/:course_id/rubrics/upload", action: "upload", as: "rubrics_course_upload"
post "accounts/:account_id/rubrics/upload", action: "upload", as: "rubrics_account_upload"
post "courses/:course_id/rubrics", controller: :rubrics, action: :create
put "courses/:course_id/rubrics/:id", controller: :rubrics, action: :update
delete "courses/:course_id/rubrics/:id", controller: :rubrics, action: :destroy

View File

@ -0,0 +1,51 @@
# frozen_string_literal: true
#
# Copyright (C) 2024 - 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 CreateRubricImports < ActiveRecord::Migration[7.0]
tag :predeploy
disable_ddl_transaction!
def up
create_table :rubric_imports, if_not_exists: true do |t|
t.references :root_account, foreign_key: { to_table: :accounts }, index: false, null: false
t.string :workflow_state, null: false
t.references :user, foreign_key: true
t.references :attachment, foreign_key: true
t.integer :progress, default: 0, null: false
t.integer :error_count, default: 0, null: false
t.json :error_data
t.timestamps
t.replica_identity_index
t.references :account, foreign_key: true, index: { where: "account_id IS NOT NULL", if_not_exists: true }, if_not_exists: true
t.references :course, foreign_key: true, index: { where: "course_id IS NOT NULL", if_not_exists: true }, if_not_exists: true
t.check_constraint <<~SQL.squish, name: "require_context"
(account_id IS NOT NULL OR
course_id IS NOT NULL) AND NOT
(account_id IS NOT NULL AND course_id IS NOT NULL)
SQL
end
add_reference :rubrics, :rubric_imports, foreign_key: true, null: true, if_not_exists: true, index: { algorithm: :concurrently, if_not_exists: true }
end
def down
remove_reference :rubrics, :rubric_imports, if_exists: true, index: { algorithm: :concurrently, if_not_exists: true }
drop_table :rubric_imports, if_exists: true
end
end

View File

@ -0,0 +1,30 @@
# frozen_string_literal: true
#
# Copyright (C) 2024 - 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 AddReplicaIdentityToRubricImports < ActiveRecord::Migration[7.1]
tag :predeploy
def up
set_replica_identity :rubric_imports
end
def down
set_replica_identity :rubric_imports, :default
end
end

View File

@ -0,0 +1,95 @@
# frozen_string_literal: true
#
# Copyright (C) 2024 - 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 RubricCSVImporter
include RubricImporterErrors
def initialize(attachment)
@attachment = attachment
end
attr_reader :attachment
def parse
rubric_by_name = Hash.new { |hash, key| hash[key] = [] }
rating_indices = {}
csv_stream do |row|
rating_indices = parse_rating_headers(row) if rating_indices.empty?
rubric_by_name[row["Rubric Name"]] << parse_row(row, rating_indices)
end
rubric_by_name
end
def parse_rating_headers(row)
rating_indices = {
description_indices: [],
long_description_indices: [],
points_indices: []
}
row.headers.each_with_index do |header, index|
next if header.nil?
if header.downcase.include?("rating name")
rating_indices[:description_indices] << index
elsif header.downcase.include?("rating description")
rating_indices[:long_description_indices] << index
elsif header.downcase.include?("rating points")
rating_indices[:points_indices] << index
end
end
rating_indices
end
def parse_row(row, rating_indices)
ratings = rating_indices[:description_indices].map.with_index do |header_index, index|
rating_description = row[header_index]
next if rating_description.nil?
{
description: rating_description,
long_description: row[rating_indices[:long_description_indices][index]],
points: row[rating_indices[:points_indices][index]].to_i
}
end.compact
new_row = {
description: row["Criteria Name"],
long_description: row["Criteria Description"],
ratings:
}
unless row["Criteria Enable Range"].nil?
new_row[:criterion_use_range] = ["true", "1"].include?(row["Criteria Enable Range"].downcase)
end
new_row
end
def csv_stream(&)
csv_file = attachment.open
csv_parse_options = {
col_sep: ",",
headers: true
}
CSV.foreach(csv_file.path, **csv_parse_options, &)
end
end

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
#
# Copyright (C) 2024 - 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 RubricImporterErrors
class DataFormatError < StandardError; end
end

View File

@ -281,6 +281,8 @@
path: "/api/v1/accounts/:account_id/rubrics/:id"
- verb: GET
path: "/api/v1/accounts/:account_id/rubrics/:id/used_locations"
- verb: POST
path: "/api/v1/accounts/:account_id/rubrics/upload"
- verb: GET
path: "/api/v1/accounts/:account_id/scopes"
- verb: POST
@ -1251,6 +1253,8 @@
path: "/api/v1/courses/:course_id/rubrics/:id"
- verb: GET
path: "/api/v1/courses/:course_id/rubrics/:id/used_locations"
- verb: POST
path: "/api/v1/courses/:course_id/rubrics/upload"
- verb: GET
path: "/api/v1/courses/:course_id/search_users"
- verb: GET

View File

@ -0,0 +1,257 @@
# frozen_string_literal: true
#
# Copyright (C) 2024 - 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/>.
#
describe RubricImport do
before :once do
account_model
course_model(account: @account)
user_factory
end
def create_import(attachment = nil, context = @account)
RubricImport.create_with_attachment(context, attachment, @user)
end
it "should create a new rubric import" do
import = create_import(stub_file_data("test.csv", "abc", "text"))
expect(import.workflow_state).to eq("created")
expect(import.progress).to eq(0)
expect(import.error_count).to eq(0)
expect(import.error_data).to eq([])
expect(import.account_id).to eq(@account.id)
expect(import.attachment_id).to eq(Attachment.last.id)
end
it "should create a new rubric import at the course level" do
import = create_import(stub_file_data("test.csv", "abc", "text"), @course)
expect(import.workflow_state).to eq("created")
expect(import.progress).to eq(0)
expect(import.error_count).to eq(0)
expect(import.error_data).to eq([])
expect(import.course_id).to eq(@course.id)
expect(import.attachment_id).to eq(Attachment.last.id)
end
describe "run import" do
let(:rubric_headers) { ["Rubric Name", "Criteria Name", "Criteria Description", "Criteria Enable Range", "Rating Name", "Rating Description", "Rating Points", "Rating Name", "Rating Description", "Rating Points", "Rating Name", "Rating Description", "Rating Points"] }
def generate_csv(rubric_data)
uploaded_csv = CSV.generate do |csv|
csv << rubric_headers
rubric_data.each do |rubric|
csv << rubric
end
end
StringIO.new(uploaded_csv)
end
def create_import_manually(uploaded_data)
attachment = Attachment.create!(context: @account, filename: "test.csv", uploaded_data:)
RubricImport.create!(
context: @account,
root_account: @account,
progress: 0,
workflow_state: :created,
user: @user,
error_count: 0,
error_data: [],
attachment:
)
end
def full_csv
generate_csv([["Rubric 1", "Criteria 1", "Criteria 1 Description", "false", "Rating 1", "Rating 1 Description", "1"]])
end
def multiple_rubrics_csv
generate_csv([
["Rubric 1", "Criteria 1", "Criteria 1 Description", "false", "Rating 1", "Rating 1 Description", "2", "Rating 2", "Rating 2 Description", "1"],
["Rubric 1", "Criteria 2", "Criteria 2 Description", "false", "Rating 1", "Rating 1 Description", "1"],
["Rubric 2", "Criteria 1", "Criteria 1 Description", "false", "Rating 1", "Rating 1 Description", "3", "Rating 2", "Rating 2 Description", "2", "Rating 3", "Rating 3 Description", "1"]
])
end
def missing_rubric_name_csv
generate_csv([["", "Criteria 1", "Criteria 1 Description", "false", "Rating 1", "Rating 1 Description", "1"]])
end
def missing_criteria_name_csv
generate_csv([["Rubric 1", "", "Criteria 1 Description", "false", "Rating 1", "Rating 1 Description", "1"]])
end
def missing_ratings_csv
generate_csv([["Rubric 1", "Criteria 1", "Criteria 1 Description", "false"]])
end
def valid_invalid_csv
generate_csv([
["Rubric 1", "Criteria 1", "Criteria 1 Description", "false", "Rating 1", "Rating 1 Description", "2", "Rating 2", "Rating 2 Description", "1"],
["", "Criteria 1", "Criteria 1 Description", "false", "Rating 1", "Rating 1 Description", "1"]
])
end
def invalid_csv
StringIO.new("invalid csv")
end
describe "succeeded" do
it "should run the import with single rubric and criteria" do
import = create_import_manually(full_csv)
import.run
expect(import.workflow_state).to eq("succeeded")
expect(import.progress).to eq(100)
expect(import.error_count).to eq(0)
expect(import.error_data).to eq([])
rubric = Rubric.find_by(rubric_imports_id: import.id)
expect(rubric.title).to eq("Rubric 1")
expect(rubric.context_id).to eq(@account.id)
expect(rubric.data.length).to eq(1)
expect(rubric.data[0][:description]).to eq("Criteria 1")
expect(rubric.data[0][:long_description]).to eq("Criteria 1 Description")
expect(rubric.data[0][:points]).to eq(1.0)
expect(rubric.data[0][:criterion_use_range]).to be false
expect(rubric.data[0][:ratings].length).to eq(1)
expect(rubric.data[0][:ratings][0][:description]).to eq("Rating 1")
expect(rubric.data[0][:ratings][0][:long_description]).to eq("Rating 1 Description")
expect(rubric.data[0][:ratings][0][:points]).to eq(1.0)
end
it "should run the import with multiple rubrics and criteria" do
import = create_import_manually(multiple_rubrics_csv)
import.run
expect(import.workflow_state).to eq("succeeded")
expect(import.progress).to eq(100)
expect(import.error_count).to eq(0)
expect(import.error_data).to eq([])
rubrics = Rubric.where(rubric_imports_id: import.id)
expect(rubrics.length).to eq(2)
# checking rubric 1
rubric1 = rubrics.find_by(title: "Rubric 1")
expect(rubric1.context_id).to eq(@account.id)
expect(rubric1.data.length).to eq(2)
# checking rubric 1 criteria 1
expect(rubric1.data[0][:description]).to eq("Criteria 1")
expect(rubric1.data[0][:long_description]).to eq("Criteria 1 Description")
expect(rubric1.data[0][:points]).to eq(2.0)
expect(rubric1.data[0][:criterion_use_range]).to be false
expect(rubric1.data[0][:ratings].length).to eq(2)
expect(rubric1.data[0][:ratings][0][:description]).to eq("Rating 1")
expect(rubric1.data[0][:ratings][0][:long_description]).to eq("Rating 1 Description")
expect(rubric1.data[0][:ratings][0][:points]).to eq(2.0)
expect(rubric1.data[0][:ratings][1][:description]).to eq("Rating 2")
expect(rubric1.data[0][:ratings][1][:long_description]).to eq("Rating 2 Description")
expect(rubric1.data[0][:ratings][1][:points]).to eq(1.0)
# checking rubric 1 criteria 2
expect(rubric1.data[1][:description]).to eq("Criteria 2")
expect(rubric1.data[1][:long_description]).to eq("Criteria 2 Description")
expect(rubric1.data[1][:points]).to eq(1.0)
expect(rubric1.data[1][:criterion_use_range]).to be false
expect(rubric1.data[1][:ratings].length).to eq(1)
expect(rubric1.data[1][:ratings][0][:description]).to eq("Rating 1")
expect(rubric1.data[1][:ratings][0][:long_description]).to eq("Rating 1 Description")
# # checking rubric 2
rubric2 = rubrics.find_by(title: "Rubric 2")
expect(rubric2.context_id).to eq(@account.id)
expect(rubric2.data.length).to eq(1)
# # checking rubric 2 criteria 1
expect(rubric2.data[0][:description]).to eq("Criteria 1")
expect(rubric2.data[0][:long_description]).to eq("Criteria 1 Description")
expect(rubric2.data[0][:points]).to eq(3.0)
expect(rubric2.data[0][:ratings][0][:description]).to eq("Rating 1")
expect(rubric2.data[0][:ratings][0][:long_description]).to eq("Rating 1 Description")
expect(rubric2.data[0][:ratings][0][:points]).to eq(3.0)
expect(rubric2.data[0][:ratings][1][:description]).to eq("Rating 2")
expect(rubric2.data[0][:ratings][1][:long_description]).to eq("Rating 2 Description")
expect(rubric2.data[0][:ratings][1][:points]).to eq(2.0)
expect(rubric2.data[0][:ratings][2][:description]).to eq("Rating 3")
expect(rubric2.data[0][:ratings][2][:long_description]).to eq("Rating 3 Description")
expect(rubric2.data[0][:ratings][2][:points]).to eq(1.0)
end
end
describe "succeeded_with_errors" do
it "should succeed with errors if rubric name is missing" do
import = create_import_manually(missing_rubric_name_csv)
import.run
expect(import.workflow_state).to eq("succeeded_with_errors")
expect(import.progress).to eq(100)
expect(import.error_count).to eq(1)
expect(import.error_data).to eq([{ "message" => "Missing 'Rubric Name' for some rubrics." }])
expect(Rubric.all.length).to eq(0)
end
it "should succeed with errors if criteria name is missing" do
import = create_import_manually(missing_criteria_name_csv)
import.run
expect(import.workflow_state).to eq("succeeded_with_errors")
expect(import.progress).to eq(100)
expect(import.error_count).to eq(1)
expect(import.error_data).to eq([{ "message" => "Missing 'Criteria Name' for Rubric 1" }])
expect(Rubric.all.length).to eq(0)
end
it "should succeed with errors if ratings are missing" do
import = create_import_manually(missing_ratings_csv)
import.run
expect(import.workflow_state).to eq("succeeded_with_errors")
expect(import.progress).to eq(100)
expect(import.error_count).to eq(1)
expect(import.error_data).to eq([{ "message" => "Missing ratings for Criteria 1" }])
expect(Rubric.all.length).to eq(0)
end
it "should succeed with errors if some rubrics are invalid and create the valid ones" do
import = create_import_manually(valid_invalid_csv)
import.run
expect(import.workflow_state).to eq("succeeded_with_errors")
expect(import.progress).to eq(100)
expect(import.error_count).to eq(1)
expect(import.error_data).to eq([{ "message" => "Missing 'Rubric Name' for some rubrics." }])
expect(Rubric.all.length).to eq(1)
expect(Rubric.first.title).to eq("Rubric 1")
end
end
describe "failed" do
it "should fail the import if the file does not have valid CSV data" do
import = create_import_manually(invalid_csv)
import.run
expect(import.workflow_state).to eq("failed")
expect(import.progress).to eq(0)
expect(import.error_count).to eq(1)
expect(import.error_data).to eq([{ "message" => I18n.t("The file is empty or does not contain valid rubric data.") }])
end
end
end
end