{"record":{"id":"a6e6d5fd35f63b01","repo":"instructure/canvas-lms","slug":"file-has-no-outcomes-data","errorCode":null,"errorMessage":"File has no outcomes data","messagePattern":"File has no outcomes data","errorType":"validation","errorClass":"ParseError","httpStatus":null,"severity":"error","filePath":"lib/outcomes/csv_importer.rb","lineNumber":85,"sourceCode":"        raise DataFormatError, I18n.t(\"Database error (%{err})\", err: e.message)\n      end\n      status = {\n        errors: file_errors,\n        progress: 100\n      }\n      yield status\n    end\n\n    def parse_file\n      headers = nil\n      total = file_line_count\n      raise ParseError, I18n.t(\"File has no data\") if total < 1\n\n      separator = test_header_i18n\n      rows = CSV.new(@file, col_sep: separator).to_enum\n      rows.with_index(1).each_slice(BATCH_SIZE) do |batch|\n        headers ||= validate_headers(*batch.shift)\n        raise ParseError, I18n.t(\"File has no outcomes data\") if batch.empty?\n\n        errors = parse_batch(headers, batch)\n        status = {\n          errors:,\n          progress: (batch.last[1].to_f / total * 100).floor\n        }\n        yield status\n      end\n    end\n\n    def parse_batch(headers, batch)\n      Account.transaction do\n        results = batch.map do |row, line|\n          utf8_row = row.map(&method(:check_encoding))\n          import_row(headers, utf8_row) unless utf8_row.all?(&:blank?)\n          []\n        rescue ParseError, InvalidDataError => e\n          [[line, e.message]]","sourceCodeStart":67,"sourceCodeEnd":103,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/lib/outcomes/csv_importer.rb#L67-L103","documentation":"In Outcomes::CsvImporter#parse_file, after the first batch of rows is consumed for header validation, the remaining batch is checked; if it is empty the file contained a header but no outcome rows, so ParseError 'File has no outcomes data' is raised. This distinguishes a header-only CSV from the completely empty case.","triggerScenarios":"Importing an outcomes CSV that contains only the header row (or fewer than BATCH_SIZE rows where the only row was consumed as the header), so the first batch.shift empties the batch.","commonSituations":"Users upload the untouched CSV template with headers but never filled in data rows; a filter/export step dropped all data rows; row endings/encoding caused all data rows to be skipped leaving only the header.","solutions":["Add at least one outcome data row beneath the header and re-import.","Verify the header row matches the expected outcomes import format so data rows are not misparsed.","Check line endings/encoding (e.g. BOM or CR-only files) that could prevent data rows from being read.","Warn users in the UI when the selected file has only one line before submitting."],"exampleFix":"// before (header only)\n\"title,vendor_guid,display_name\\n\"\n// after\n\"title,vendor_guid,display_name\\nMy Outcome,vo-1,My Outcome\\n\"","handlingStrategy":"validation","validationCode":"rows = CSV.read(path)\nraise 'header-only CSV' if rows.size < 2","typeGuard":"def has_data_rows?(path)\n  rows = CSV.read(path)\n  rows.is_a?(Array) && rows.size > 1\nend","tryCatchPattern":"begin\n  importer.run\nrescue Outcomes::CsvImporter::ParseError => e\n  flash[:error] = e.message # 'File has no outcomes data'\nend","preventionTips":["Require at least one data row below the header in templates.","Warn users when the selected file has only one line.","Check line endings/encoding so data rows are not skipped during parsing."],"tags":["csv","import","outcomes","empty-data"],"backgroundTag":"empty-result-set","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"}