diff --git a/app/controllers/rubrics_api_controller.rb b/app/controllers/rubrics_api_controller.rb
index fd03a0c9597..0a15a077f80 100644
--- a/app/controllers/rubrics_api_controller.rb
+++ b/app/controllers/rubrics_api_controller.rb
@@ -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)
diff --git a/app/models/account.rb b/app/models/account.rb
index 0c21ad7f1a6..ae906f295de 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -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",
diff --git a/app/models/rubric_import.rb b/app/models/rubric_import.rb
new file mode 100644
index 00000000000..5fe4c4e71d6
--- /dev/null
+++ b/app/models/rubric_import.rb
@@ -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 .
+#
+
+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
diff --git a/config/feature_flags/apogee_release_flags.yml b/config/feature_flags/apogee_release_flags.yml
index 312f2337f06..6c564541eb4 100644
--- a/config/feature_flags/apogee_release_flags.yml
+++ b/config/feature_flags/apogee_release_flags.yml
@@ -358,4 +358,16 @@ speedgrader_studio_media_capture:
ci:
state: allowed
development:
- state: allowed
\ No newline at end of file
+ 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
+
\ No newline at end of file
diff --git a/config/routes.rb b/config/routes.rb
index 8351b41e936..f70cdf3a4d7 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -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
diff --git a/db/migrate/20240709153926_create_rubric_imports.rb b/db/migrate/20240709153926_create_rubric_imports.rb
new file mode 100644
index 00000000000..bd9ee3079c2
--- /dev/null
+++ b/db/migrate/20240709153926_create_rubric_imports.rb
@@ -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 .
+
+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
diff --git a/db/migrate/20240722160756_add_replica_identity_to_rubric_imports.rb b/db/migrate/20240722160756_add_replica_identity_to_rubric_imports.rb
new file mode 100644
index 00000000000..9359dc64a29
--- /dev/null
+++ b/db/migrate/20240722160756_add_replica_identity_to_rubric_imports.rb
@@ -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 .
+
+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
diff --git a/lib/rubric_csv_importer.rb b/lib/rubric_csv_importer.rb
new file mode 100644
index 00000000000..d238085cd09
--- /dev/null
+++ b/lib/rubric_csv_importer.rb
@@ -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 .
+#
+
+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
diff --git a/lib/rubric_importer_errors.rb b/lib/rubric_importer_errors.rb
new file mode 100644
index 00000000000..cf95096876e
--- /dev/null
+++ b/lib/rubric_importer_errors.rb
@@ -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 .
+#
+
+module RubricImporterErrors
+ class DataFormatError < StandardError; end
+end
diff --git a/spec/lib/token_scopes/last_known_scopes.yml b/spec/lib/token_scopes/last_known_scopes.yml
index e731481b9cc..47ee5036ff0 100644
--- a/spec/lib/token_scopes/last_known_scopes.yml
+++ b/spec/lib/token_scopes/last_known_scopes.yml
@@ -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
diff --git a/spec/models/rubric_import_spec.rb b/spec/models/rubric_import_spec.rb
new file mode 100644
index 00000000000..e3563335fa2
--- /dev/null
+++ b/spec/models/rubric_import_spec.rb
@@ -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 .
+#
+
+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