001package edu.pdx.cs.joy.grader.canvas; 002 003import com.google.common.annotations.VisibleForTesting; 004import com.opencsv.CSVReader; 005import com.opencsv.exceptions.CsvValidationException; 006 007import java.io.IOException; 008import java.io.Reader; 009import java.util.ArrayList; 010import java.util.List; 011import java.util.SortedMap; 012import java.util.TreeMap; 013import java.util.regex.Matcher; 014import java.util.regex.Pattern; 015 016public class CanvasGradesCSVParser implements CanvasGradesCSVColumnNames { 017 private static final Pattern assignmentNamePattern = Pattern.compile("(.+) \\((\\d+)\\)"); 018 private static final String[] ignoredColumnNames = new String[] { 019 "ID", 020 "SIS User ID", 021 SECTION_COLUMN 022 }; 023 024 private static final String[] ignoredColumnNameContains = new String[] { 025 "Current Points", 026 "Final Points", 027 "Current Score", 028 "Final Score" 029 }; 030 031 032 private int studentNameColumn; 033 private int studentIdColumn; 034 private int canvasIdColumn; 035 private int sectionIdColumn; 036 private final SortedMap<Integer, GradesFromCanvas.CanvasAssignment> columnToAssignment = new TreeMap<>(); 037 private final GradesFromCanvas grades; 038 039 public CanvasGradesCSVParser(Reader reader) throws IOException { 040 CSVReader csv = new CSVReader(reader); 041 try { 042 extractColumnNamesFromFirstLineOfCsv(csv.readNext()); 043 csv.readNext(); 044 extractPossiblePointsFromThirdLineOfCsv(csv.readNext()); 045 046 grades = new GradesFromCanvas(); 047 048 String[] studentLine; 049 while ((studentLine = csv.readNext()) != null) { 050 addStudentAndGradesFromLineOfCsv(studentLine); 051 } 052 053 } catch (CsvValidationException ex) { 054 throw new IOException("While parsing CSV", ex); 055 } 056 057 } 058 059 private void addStudentAndGradesFromLineOfCsv(String[] studentLine) { 060 GradesFromCanvas.CanvasStudent student = createStudentFrom(studentLine); 061 062 if (!student.getFirstName().equals("Test")) { 063 this.grades.addStudent(student); 064 } 065 066 addGradesFromLineOfCsv(student, studentLine); 067 } 068 069 private void addGradesFromLineOfCsv(GradesFromCanvas.CanvasStudent student, String[] studentLine) { 070 this.columnToAssignment.forEach((column, assignment) -> { 071 String score = studentLine[column]; 072 if (isInterestingScore(score)) { 073 student.setScore(assignment, parseScore(score)); 074 } 075 }); 076 077 } 078 079 private boolean isInterestingScore(String score) { 080 return !"".equals(score) && !"N/A".equals(score); 081 } 082 083 private double parseScore(String score) { 084 return Double.parseDouble(score); 085 } 086 087 private GradesFromCanvas.CanvasStudent createStudentFrom(String[] studentLine) { 088 String studentName = studentLine[studentNameColumn]; 089 Pattern studentNamePattern = Pattern.compile("(.*), (.*)"); 090 Matcher matcher = studentNamePattern.matcher(studentName); 091 if (matcher.matches()) { 092 GradesFromCanvas.CanvasStudentBuilder builder = GradesFromCanvas.newStudent(); 093 builder.setFirstName(matcher.group(2)); 094 builder.setLastName(matcher.group(1)); 095 builder.setLoginId(studentLine[studentIdColumn]); 096 builder.setCanvasId(studentLine[canvasIdColumn]); 097 builder.setSection(studentLine[sectionIdColumn]); 098 099 return builder.create(); 100 101 } else { 102 throw new IllegalStateException("Can't parse student name \"" + studentName + "\""); 103 } 104 } 105 106 private void extractPossiblePointsFromThirdLineOfCsv(String[] secondLine) { 107 this.columnToAssignment.forEach((column, assignment) -> { 108 String possiblePointsText = secondLine[column]; 109 try { 110 assignment.setPossiblePoints(Double.parseDouble(possiblePointsText)); 111 112 } catch (NumberFormatException ex) { 113 throw new IllegalStateException("Can't parse points \"" + possiblePointsText + "\" for " + assignment.getName()); 114 } 115 }); 116 117 } 118 119 private void extractColumnNamesFromFirstLineOfCsv(String[] firstLine) { 120 for (int i = 0; i < firstLine.length; i++) { 121 String cell = firstLine[i]; 122 switch (cell) { 123 case STUDENT_COLUMN: 124 this.studentNameColumn = i; 125 break; 126 case "SIS Login ID": 127 this.studentIdColumn = i; 128 break; 129 case ID_COLUMN: 130 this.canvasIdColumn = i; 131 break; 132 case SECTION_COLUMN: 133 this.sectionIdColumn = i; 134 break; 135 default: 136 if (!isColumnIgnored(cell)) { 137 addAssignment(cell, i); 138 } 139 } 140 } 141 142 } 143 144 @VisibleForTesting 145 static GradesFromCanvas.CanvasAssignment createAssignment(String assignmentText) { 146 Matcher matcher = assignmentNamePattern.matcher(assignmentText); 147 if (matcher.matches()) { 148 String name = matcher.group(1); 149 String idText = matcher.group(2); 150 return new GradesFromCanvas.CanvasAssignment(name, Integer.parseInt(idText)); 151 152 } else { 153 throw new IllegalStateException("Can't create Assignment from \"" + assignmentText + "\""); 154 } 155 } 156 157 private void addAssignment(String assignmentText, int column) { 158 this.columnToAssignment.put(column, createAssignment(assignmentText)); 159 160 } 161 162 private boolean isColumnIgnored(String columnName) { 163 for (String ignored : ignoredColumnNames) { 164 if (ignored.equals(columnName)) { 165 return true; 166 } 167 } 168 for (String ignored : ignoredColumnNameContains) { 169 if (columnName.contains(ignored)) { 170 return true; 171 } 172 } 173 return false; 174 } 175 176 public List<GradesFromCanvas.CanvasAssignment> getAssignments() { 177 return new ArrayList<>(this.columnToAssignment.values()); 178 } 179 180 public GradesFromCanvas getGrades() { 181 return this.grades; 182 } 183 184}