001package edu.pdx.cs.joy.grader;
002
003import com.google.common.annotations.VisibleForTesting;
004import edu.pdx.cs.joy.ParserException;
005import edu.pdx.cs.joy.grader.gradebook.*;
006
007import java.io.*;
008import java.text.NumberFormat;
009import java.text.SimpleDateFormat;
010import java.time.LocalDateTime;
011import java.time.format.DateTimeFormatter;
012import java.time.format.FormatStyle;
013import java.util.*;
014import java.util.function.Supplier;
015import java.util.stream.Stream;
016
017/**
018 * Class that creates a pretty report that summarizes a student's
019 * grades.
020 */
021public class SummaryReport {
022  static final String UNDERGRADUATE_DIRECTORY_NAME = "undergraduates";
023  static final String GRADUATE_DIRECTORY_NAME = "graduates";
024  private static final HashMap<Student, Double> allTotals = new HashMap<>();
025
026  /**
027   * Computes the student's final average and makes a pretty report.
028   */
029  @VisibleForTesting
030  static void dumpReportTo(GradeBook book, Student student,
031                           PrintWriter pw, boolean assignLetterGrades) {
032    NumberFormat format = NumberFormat.getNumberInstance();
033    format.setMinimumFractionDigits(1);
034    format.setMaximumFractionDigits(1);
035
036    Assignment lowestQuiz = null;
037    double best = 0.0;
038    double total = 0.0;
039
040    if (assignLetterGrades) {
041      String studentName = student.getNickName() != null ? student.getNickName() : student.getFirstName();
042      pw.println("Hi, " + studentName + ".  Here are your final grades for \"The Joy of Coding\".");
043      pw.println();
044    }
045
046    pw.println("Grade summary for: " + student.getFullName());
047    SimpleDateFormat df = 
048      new SimpleDateFormat("EEEE MMMM d, yyyy 'at' h:mm a");
049    pw.println("Generated on: " + df.format(new Date()));
050    pw.println("");
051
052    for (String assignmentName : getSortedAssignmentNames(book)) {
053      Assignment assignment = book.getAssignment(assignmentName);
054      if (noStudentHasGradeFor(assignment, book)) {
055        continue;
056      }
057
058      Grade grade = student.getGrade(assignmentName);
059      double score = getScore(grade);
060
061
062//       System.out.println("Examining " + assign + ", score: " + score);
063
064      if (assignment.getType() == Assignment.AssignmentType.QUIZ) {
065        if (lowestQuiz == null) {
066          lowestQuiz = assignment;
067//           System.out.println("Lowest quiz: " + lowestQuiz + 
068//                              ", score: " + score);
069
070        } else {
071          Grade lowestGrade = student.getGrade(lowestQuiz.getName());
072          if (lowestGrade != null && score < lowestGrade.getScore()) {
073            lowestQuiz = assignment;
074//             System.out.println("Lowest quiz: " + lowestQuiz + ", score: "
075//                                + score + ", lowest grade: " +
076//                                student.getGrade(lowestQuiz.getName()));
077          }
078        }
079      }
080
081      StringBuffer line = new StringBuffer();
082      line.append("  ");
083      line.append(assignment.getName());
084      line.append(" (");
085      line.append(assignment.getDescription());
086      line.append(")");
087      if (assignment.getType() == Assignment.AssignmentType.OPTIONAL) {
088        line.append(" (OPTIONAL)");
089      }
090      line.append(": ");
091
092
093      // Skip incompletes and no grades
094      if (grade == null) {
095        if (dueDateHasPassed(assignment)) {
096          line.append(" (MISSING GRADE)");
097
098        } else {
099          line.append(String.format(" (due %s)", formatDueDate(assignment)));
100        }
101
102      } else if (grade.isIncomplete()) {
103        line.append(" (INCOMPLETE)");
104
105      } else if (grade.isNotGraded()) {
106        line.append(" (NOT GRADED)");
107
108      } else {
109        line.append(format.format(score));
110        line.append("/");
111        line.append(format.format(assignment.getPoints()));
112      }
113
114      pw.println(line);
115
116      // Don't count optional assignments toward the maximum point
117      // total
118      if (assignment.getType() != Assignment.AssignmentType.OPTIONAL) {
119        best += assignment.getPoints();
120      }
121
122      total += score;
123    }
124
125    if (lowestQuiz != null) {
126      pw.println("");
127      pw.println("Lowest Quiz grade dropped: " +
128                  lowestQuiz.getName());
129      pw.println("");
130
131      // Subtract lowest quiz grade
132      Grade lowestGrade = student.getGrade(lowestQuiz.getName());
133      total -= (lowestGrade != null ? lowestGrade.getScore() : 0);
134      best -= lowestQuiz.getPoints();
135    }
136
137    // Print out late and resubmitted assignments
138    pw.println("Late assignments:");
139    for (String late : student.getLate()) {
140      pw.println("  " + late);
141    }
142    pw.println();
143
144    pw.println("Resubmitted assignments:");
145    for (String resubmitted : student.getResubmitted()) {
146      pw.println("  " + resubmitted);
147    }
148    pw.println("");
149
150    pw.println("Total grade: " + format.format(total)  + "/" +
151               format.format(best));
152
153    double overallScore = total / best;
154
155    if (assignLetterGrades) {
156      LetterGrade letterGrade = book.getLetterGradeForScore(student.getEnrolledSection(), overallScore * 100.0);
157      student.setLetterGrade(letterGrade);
158    }
159
160    if (student.getLetterGrade() != null) {
161      pw.println("Letter Grade: " + student.getLetterGrade());
162    }
163
164    allTotals.put(student, overallScore);
165  }
166
167  private static String formatDueDate(Assignment assignment) {
168    LocalDateTime dueDate = assignment.getDueDate();
169    return dueDate.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT));
170  }
171
172  @VisibleForTesting
173  static boolean dueDateHasPassed(Assignment assignment) {
174    LocalDateTime dueDate = assignment.getDueDate();
175    LocalDateTime now = LocalDateTime.now();
176    return now.isAfter(dueDate);
177  }
178
179  static boolean noStudentHasGradeFor(Assignment assignment, GradeBook book) {
180    return noSubmissionIsGraded(assignment, book) || allSubmissionsHaveGradeOfZero(assignment, book);
181  }
182
183  private static boolean allSubmissionsHaveGradeOfZero(Assignment assignment, GradeBook book) {
184    return book.studentsStream()
185      .map(student -> getGrade(assignment, student))
186      .allMatch(grade -> grade != null && grade.getScore() == 0.0);
187  }
188
189  private static boolean noSubmissionIsGraded(Assignment assignment, GradeBook book) {
190    return book.studentsStream()
191      .map(student -> getGrade(assignment, student))
192      .noneMatch(grade -> grade != null && !grade.isNotGraded());
193  }
194
195  private static Grade getGrade(Assignment assignment, Student student) {
196    return student.getGrade(assignment.getName());
197  }
198
199  private static double getScore(Grade grade) {
200    // Average non-existent scores as zero
201    double score;
202    if (grade == null) {
203      score = 0.0;
204
205    } else {
206      score = grade.getScore();
207    }
208    return score;
209  }
210
211  private static SortedSet<String> getSortedAssignmentNames(GradeBook book) {
212    return new TreeSet<>(book.getAssignmentNames());
213  }
214
215  private static final PrintWriter out = new PrintWriter(System.out, true);
216  private static final PrintWriter err = new PrintWriter(System.err, true);
217
218  /**
219   * Prints usage information about this main program
220   */
221  private static void usage(String s) {
222    err.println("\n** " + s + "\n");
223    err.println("\njava SummaryReport -assignLetterGrades xmlFile outDir (student)*");
224    err.println("\n");
225    err.println("Generates summary grade reports for the given " +
226                "students.  If student is not \ngiven, then reports " + 
227                "for all students are generated.");
228    err.println("");
229    System.exit(1);
230  }
231
232  /**
233   * Main program that creates summary reports for every student in a
234   * grade book located in a given XML file.
235   */
236  public static void main(String[] args) throws IOException {
237    boolean assignLetterGrades = false;
238    String xmlFileName = null;
239    String outputDirName = null;
240    Collection<String> students = new ArrayList<>();
241
242    for (String arg : args) {
243      if (arg.equals("-assignLetterGrades")) {
244        assignLetterGrades = true;
245
246      } else if (xmlFileName == null) {
247        xmlFileName = arg;
248
249      } else if (outputDirName == null) {
250        outputDirName = arg;
251
252      } else {
253        students.add(arg);
254      }
255    }
256
257    if (xmlFileName == null) {
258      usage("Missing XML file name");
259    }
260
261    if (outputDirName == null) {
262      usage("Missing output dir name");
263      return;
264    }
265
266    String xmlFile = xmlFileName;
267    File outDir = new File(outputDirName);
268    if (!outDir.exists()) {
269      outDir.mkdirs();
270
271    } else if (!outDir.isDirectory()) {
272      usage(outDir + " is not a directory");
273    }
274
275    File file = new File(xmlFile);
276    if (!file.exists()) {
277      usage("Grade book file " + xmlFile + " does not exist");
278    }
279
280    GradeBook book = parseGradeBook(file);
281
282    // Create a SummaryReport for every student
283    Iterable<String> studentIds;
284
285    if (!students.isEmpty()) {
286      studentIds = students;
287
288    } else {
289      studentIds = book.getStudentIds();
290    }
291
292    dumpReports(studentIds, book, outDir, assignLetterGrades);
293
294    // Sort students by totals and print out results:
295    Set<Student> students1 = allTotals.keySet();
296    try (PrintWriter allStudentTotalsFile = new PrintWriter(new FileWriter("all-student-totals.txt"), true)) {
297      printOutStudentTotals(students1, new Writer() {
298        @Override
299        public void write(char[] cbuf, int off, int len) {
300          allStudentTotalsFile.write(cbuf, off, len);
301          out.write(cbuf, off, len);
302        }
303
304        @Override
305        public void flush() {
306          allStudentTotalsFile.flush();
307          out.flush();
308        }
309
310        @Override
311        public void close() {
312          allStudentTotalsFile.close();
313          out.close();
314        }
315      });
316
317      allStudentTotalsFile.flush();
318    }
319
320    saveGradeBookIfDirty(xmlFileName, book);
321
322  }
323
324  @VisibleForTesting
325  static void printOutStudentTotals(Set<Student> allStudents, Writer writer) {
326    PrintWriter pw = new PrintWriter(writer, true);
327    SortedSet<Student> sorted = getStudentSortedByTotalPoints(allStudents);
328
329    pw.println("Undergraduates:");
330    Stream<Student> undergrads = sorted.stream().filter(student -> student.getEnrolledSection() == Student.Section.UNDERGRADUATE);
331    printOutStudentTotals(pw, undergrads);
332
333    pw.println("Graduate Students:");
334    Stream<Student> grads = sorted.stream().filter(student -> student.getEnrolledSection() == Student.Section.GRADUATE);
335    printOutStudentTotals(pw, grads);
336
337  }
338
339  private static void printOutStudentTotals(PrintWriter out, Stream<Student> students) {
340    NumberFormat format = NumberFormat.getPercentInstance();
341
342    students.forEach(student -> {
343      Double d = allTotals.get(student);
344      out.print("  " + student + ": " + format.format(d.doubleValue()));
345
346      if (student.getLetterGrade() != null) {
347        out.print(" " + student.getLetterGrade());
348      }
349
350      out.println();
351    });
352  }
353
354  private static void saveGradeBookIfDirty(String xmlFileName, GradeBook book) {
355    if (book.isDirty()) {
356      try {
357        XmlDumper dumper = new XmlDumper(xmlFileName);
358        dumper.dump(book);
359
360      } catch (IOException ex) {
361        printErrorMessageAndExit("While saving gradebook to " + xmlFileName, ex);
362      }
363    }
364  }
365
366  private static SortedSet<Student> getStudentSortedByTotalPoints(Set<Student> students) {
367    SortedSet<Student> sorted = new TreeSet<>((s1, s2) -> {
368      Double d1 = allTotals.get(s1);
369      Double d2 = allTotals.get(s2);
370      if (d2.compareTo(d1) == 0) {
371        return s1.getId().compareTo(s2.getId());
372      } else {
373        return d2.compareTo(d1);
374      }
375    });
376
377    sorted.addAll(students);
378    return sorted;
379  }
380
381  @VisibleForTesting
382  static void dumpReports(Iterable<String> studentIds, GradeBook book, File outDir, boolean assignLetterGrades) {
383    for (String id : studentIds) {
384      err.println(id);
385
386      Student student = book.getStudent(id).orElseThrow(noStudentWithId(id));
387
388      File outFile = new File(getDirectoryForReportFileForStudent(outDir, student), getReportFileName(id));
389      try {
390        PrintWriter pw =
391          new PrintWriter(new FileWriter(outFile), true);
392        dumpReportTo(book, student, pw, assignLetterGrades);
393
394//         dumpReportTo(book, student, out);
395      } catch (IOException ex) {
396        printErrorMessageAndExit("While writing report to " + outFile, ex);
397      }
398    }
399  }
400
401  private static File getDirectoryForReportFileForStudent(File parentDirectory, Student student) {
402    String directoryName;
403    Student.Section enrolledSection = student.getEnrolledSection();
404    switch (enrolledSection) {
405      case UNDERGRADUATE:
406        directoryName = UNDERGRADUATE_DIRECTORY_NAME;
407        break;
408      case GRADUATE:
409        directoryName = GRADUATE_DIRECTORY_NAME;
410        break;
411      default:
412        throw new IllegalStateException("Don't know directory name for " + enrolledSection);
413    }
414    File directory = new File(parentDirectory, directoryName);
415    directory.mkdirs();
416    return directory;
417  }
418
419  @VisibleForTesting
420  static String getReportFileName(String studentId) {
421    return studentId + ".report";
422  }
423
424  private static GradeBook parseGradeBook(File gradeBookFile) {
425    GradeBook book = null;
426    try {
427      err.println("Parsing " + gradeBookFile);
428      XmlGradeBookParser parser = new XmlGradeBookParser(gradeBookFile);
429      book = parser.parse();
430
431    } catch (FileNotFoundException ex) {
432      printErrorMessageAndExit("** Could not find grade book file: " + gradeBookFile, ex);
433
434    } catch (ParserException | IOException ex) {
435      printErrorMessageAndExit("** Exception while parsing " + gradeBookFile, ex);
436    }
437    return book;
438  }
439
440  private static void printErrorMessageAndExit(String message, Throwable ex) {
441    err.println(message);
442    ex.printStackTrace(err);
443    System.exit(1);
444  }
445
446  @SuppressWarnings("ThrowableInstanceNeverThrown")
447  private static Supplier<? extends IllegalStateException> noStudentWithId(String id) {
448    return () -> new IllegalStateException("No student with id " + id);
449  }
450
451}