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