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}