instructure/canvas-lms · error

course hasn't been converted

Error message

course hasn't been converted

What it means

SelectiveContentFormatter#provides_content_list formats the migration content inventory. If no migration object is present, the migration is a course copy (content comes from the course itself), or there is no overview attachment, and none of the earlier branches apply, the formatter has no source to enumerate and raises this RuntimeError instead of silently returning an empty list.

Solutions

  1. Check @migration.migration_type and ensure the import is not a course_copy_importer when expecting overview-based content
  2. Verify the migration's overview_attachment exists (migration.attachment / exported_attachment) before calling get_content_list; if missing, re-run the export
  3. If content should come from the course, use a course_copy_importer migration type or call get_content_from_course directly
  4. Guard the call: only invoke get_content_list when migration.overview_attachment.present?

Example fix

// before
formatter = SelectiveContentFormatter.new(@migration)
list = formatter.get_content_list('assignments')
// after
if @migration.migration_type == 'course_copy_importer' || @migration.overview_attachment
  list = SelectiveContentFormatter.new(@migration).get_content_list('assignments')
else
  raise 'no overview attachment available for selective content'
end
Defensive patterns

Strategy: validation

Validate before calling

raise 'no content source' unless migration&.migration_type == 'course_copy_importer' || migration&.overview_attachment

Type guard

def content_list_available?(migration) = !migration.nil? && (migration.migration_type == 'course_copy_importer' || migration.overview_attachment.present?)

Try / catch

begin
  list = formatter.get_content_list(type)
rescue RuntimeError => e
  raise unless e.message == "course hasn't been converted"
  list = []
end

Prevention

When it happens

Trigger: Calling get_content_list on a formatter built with a @migration that is nil, has migration_type 'course_copy_importer' (handled by the first branch), and lacks an @migration.overview_attachment, so the elsif fails and the else raises. Concretely: a non-course-copy migration (e.g. a zip or QTI import) whose overview attachment was never generated or was deleted.

Common situations: Developers customizing selective content UIs hit this when testing migrations whose course export overview file wasn't attached (failed/interupted export), or when the formatter is constructed without a migration object in specs/scripts.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15). Data as JSON: /api/errors/58d9647bf9602fe4. Report an issue: GitHub.

Appendix: source

Thrown at lib/canvas/migration/helpers/selective_content_formatter.rb:61

    def initialize(migration = nil, base_url = nil, global_identifiers:)
      @migration = migration
      @base_url = base_url
      @global_identifiers = global_identifiers
    end

    def valid_type?(type = nil)
      type.nil? || SELECTIVE_CONTENT_TYPES.any? { |t| t[0] == type } || type.start_with?("submodules_") || type.start_with?("learning_outcome_groups_")
    end

    def get_content_list(type = nil, source = nil)
      raise "unsupported migration type" unless valid_type?(type)

      if !@migration || @migration.migration_type == "course_copy_importer"
        get_content_from_course(type, source)
      elsif @migration.overview_attachment
        get_content_from_overview(type)
      else
        raise "course hasn't been converted"
      end
    end

    private

    def property_prefix
      @migration ? "copy" : "select"
    end

    # pulls the available items from the overview attachment on the content migration
    def get_content_from_overview(type = nil)
      course_data = Rails.cache.fetch(["migration_selective_cache", @migration.shard, @migration].cache_key, expires_in: 5.minutes) do
        att = @migration.overview_attachment.open
        data = JSON.parse(att.read)
        data = separate_announcements(data)
        data["attachments"] ||= data["file_map"]&.values
        data["quizzes"] ||= data["assessments"]
        data["context_modules"] ||= data["modules"]

View on GitHub (pinned to 1c9f0bb801)