{"record":{"id":"58d9647bf9602fe4","repo":"instructure/canvas-lms","slug":"course-hasn-t-been-converted","errorCode":null,"errorMessage":"course hasn't been converted","messagePattern":"course hasn't been converted","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"lib/canvas/migration/helpers/selective_content_formatter.rb","lineNumber":61,"sourceCode":"    def initialize(migration = nil, base_url = nil, global_identifiers:)\n      @migration = migration\n      @base_url = base_url\n      @global_identifiers = global_identifiers\n    end\n\n    def valid_type?(type = nil)\n      type.nil? || SELECTIVE_CONTENT_TYPES.any? { |t| t[0] == type } || type.start_with?(\"submodules_\") || type.start_with?(\"learning_outcome_groups_\")\n    end\n\n    def get_content_list(type = nil, source = nil)\n      raise \"unsupported migration type\" unless valid_type?(type)\n\n      if !@migration || @migration.migration_type == \"course_copy_importer\"\n        get_content_from_course(type, source)\n      elsif @migration.overview_attachment\n        get_content_from_overview(type)\n      else\n        raise \"course hasn't been converted\"\n      end\n    end\n\n    private\n\n    def property_prefix\n      @migration ? \"copy\" : \"select\"\n    end\n\n    # pulls the available items from the overview attachment on the content migration\n    def get_content_from_overview(type = nil)\n      course_data = Rails.cache.fetch([\"migration_selective_cache\", @migration.shard, @migration].cache_key, expires_in: 5.minutes) do\n        att = @migration.overview_attachment.open\n        data = JSON.parse(att.read)\n        data = separate_announcements(data)\n        data[\"attachments\"] ||= data[\"file_map\"]&.values\n        data[\"quizzes\"] ||= data[\"assessments\"]\n        data[\"context_modules\"] ||= data[\"modules\"]","sourceCodeStart":43,"sourceCodeEnd":79,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/lib/canvas/migration/helpers/selective_content_formatter.rb#L43-L79","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Check @migration.migration_type and ensure the import is not a course_copy_importer when expecting overview-based content","Verify the migration's overview_attachment exists (migration.attachment / exported_attachment) before calling get_content_list; if missing, re-run the export","If content should come from the course, use a course_copy_importer migration type or call get_content_from_course directly","Guard the call: only invoke get_content_list when migration.overview_attachment.present?"],"exampleFix":"// before\nformatter = SelectiveContentFormatter.new(@migration)\nlist = formatter.get_content_list('assignments')\n// after\nif @migration.migration_type == 'course_copy_importer' || @migration.overview_attachment\n  list = SelectiveContentFormatter.new(@migration).get_content_list('assignments')\nelse\n  raise 'no overview attachment available for selective content'\nend","handlingStrategy":"validation","validationCode":"raise 'no content source' unless migration&.migration_type == 'course_copy_importer' || migration&.overview_attachment","typeGuard":"def content_list_available?(migration) = !migration.nil? && (migration.migration_type == 'course_copy_importer' || migration.overview_attachment.present?)","tryCatchPattern":"begin\n  list = formatter.get_content_list(type)\nrescue RuntimeError => e\n  raise unless e.message == \"course hasn't been converted\"\n  list = []\nend","preventionTips":["Confirm migration type and overview attachment before building the formatter","Re-run exports that failed to produce an overview attachment","Prefer course_copy_importer path when content lives in the course"],"tags":["migration","content-import","invalid-state"],"backgroundTag":"internal-invariant-violation","analyzedSha":"1c9f0bb8013ed69c4f2efe11fd483025469b7e6c","analyzedAt":"2026-09-15T20:33:18.891Z","contentChangedAt":"2026-09-15T20:33:18.891Z","schemaVersion":2},"datasetVersion":"2026-09-23T02:17:17.105Z"}