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:
parent
748a8cf8c0
commit
22b61580e0
|
@ -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)
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue