diff --git a/pom.xml b/pom.xml index 086d14c..640ee77 100644 --- a/pom.xml +++ b/pom.xml @@ -79,6 +79,12 @@ mysql-connector-java 8.0.15 + + + org.apache.commons + commons-csv + 1.5 + diff --git a/src/main/java/nl/andrewlalis/teaching_assistant_assistant/TeachingAssistantAssistantApplication.java b/src/main/java/nl/andrewlalis/teaching_assistant_assistant/TeachingAssistantAssistantApplication.java index 984e6f9..22d7739 100644 --- a/src/main/java/nl/andrewlalis/teaching_assistant_assistant/TeachingAssistantAssistantApplication.java +++ b/src/main/java/nl/andrewlalis/teaching_assistant_assistant/TeachingAssistantAssistantApplication.java @@ -27,6 +27,11 @@ public class TeachingAssistantAssistantApplication implements CommandLineRunner @Override public void run(String... args) throws Exception { System.out.println("Running startup..."); - +// +// String exampleDate = "2019/04/15 4:13:41 PM GMT+2 "; +// // Parse the timestamp. +// DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd h:mm:ss a O "); +// ZonedDateTime dateTime = ZonedDateTime.parse(exampleDate, formatter); +// System.out.println("Read time: " + dateTime.toString()); } } diff --git a/src/main/java/nl/andrewlalis/teaching_assistant_assistant/controllers/Courses.java b/src/main/java/nl/andrewlalis/teaching_assistant_assistant/controllers/Courses.java index 6935735..ea5df0e 100644 --- a/src/main/java/nl/andrewlalis/teaching_assistant_assistant/controllers/Courses.java +++ b/src/main/java/nl/andrewlalis/teaching_assistant_assistant/controllers/Courses.java @@ -41,7 +41,6 @@ public class Courses { consumes = "application/x-www-form-urlencoded" ) public String post(@ModelAttribute Course course) { - System.out.println("Object submitted: " + course); this.courseRepository.save(course); return "courses/entity"; } diff --git a/src/main/java/nl/andrewlalis/teaching_assistant_assistant/controllers/courses/ImportStudents.java b/src/main/java/nl/andrewlalis/teaching_assistant_assistant/controllers/courses/ImportStudents.java new file mode 100644 index 0000000..02819db --- /dev/null +++ b/src/main/java/nl/andrewlalis/teaching_assistant_assistant/controllers/courses/ImportStudents.java @@ -0,0 +1,61 @@ +package nl.andrewlalis.teaching_assistant_assistant.controllers.courses; + +import nl.andrewlalis.teaching_assistant_assistant.model.Course; +import nl.andrewlalis.teaching_assistant_assistant.model.people.teams.StudentTeam; +import nl.andrewlalis.teaching_assistant_assistant.model.repositories.CourseRepository; +import nl.andrewlalis.teaching_assistant_assistant.model.repositories.TeamRepository; +import nl.andrewlalis.teaching_assistant_assistant.util.team_importing.StudentTeamImporter; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.List; +import java.util.Optional; + +/** + * Controller for importing students from a CSV sheet. + */ +@Controller +public class ImportStudents { + + private CourseRepository courseRepository; + + private TeamRepository teamRepository; + + protected ImportStudents(CourseRepository courseRepository, TeamRepository teamRepository) { + this.courseRepository = courseRepository; + this.teamRepository = teamRepository; + } + + @GetMapping("/courses/{code}/import_students") + public String get(@PathVariable String code, Model model) { + Optional optionalCourse = this.courseRepository.findByCode(code); + optionalCourse.ifPresent(course -> model.addAttribute("course", course)); + return "courses/import_students"; + } + + @PostMapping( + value = "/courses/{code}/import_students" + ) + public String post(@PathVariable String code, @RequestParam MultipartFile file) { + Optional optionalCourse = this.courseRepository.findByCode(code); + + if (!optionalCourse.isPresent()) { + System.out.println("No course found."); + return "redirect:/courses"; + } + + try { + List studentTeams = StudentTeamImporter.importFromCSV(file.getInputStream(), optionalCourse.get()); + } catch (IOException e) { + e.printStackTrace(); + } + + return "redirect:/courses/{code}"; + } +} diff --git a/src/main/java/nl/andrewlalis/teaching_assistant_assistant/model/Course.java b/src/main/java/nl/andrewlalis/teaching_assistant_assistant/model/Course.java index 25d41db..483b294 100644 --- a/src/main/java/nl/andrewlalis/teaching_assistant_assistant/model/Course.java +++ b/src/main/java/nl/andrewlalis/teaching_assistant_assistant/model/Course.java @@ -77,12 +77,12 @@ public class Course extends BasicEntity { this.code = code; } - public void addStudentGroup(StudentTeam group) { - this.studentTeams.add(group); + public void addStudentTeam(StudentTeam team) { + this.studentTeams.add(team); } - public void addTeachingAssistantGroup(TeachingAssistantTeam group) { - this.teachingAssistantTeams.add(group); + public void addTeachingAssistantTeam(TeachingAssistantTeam team) { + this.teachingAssistantTeams.add(team); } /* diff --git a/src/main/java/nl/andrewlalis/teaching_assistant_assistant/model/people/Person.java b/src/main/java/nl/andrewlalis/teaching_assistant_assistant/model/people/Person.java index 47cef1d..1029403 100644 --- a/src/main/java/nl/andrewlalis/teaching_assistant_assistant/model/people/Person.java +++ b/src/main/java/nl/andrewlalis/teaching_assistant_assistant/model/people/Person.java @@ -26,6 +26,9 @@ public abstract class Person extends BasicEntity { @Column private String emailAddress; + @Column + private String githubUsername; + /** * The list of teams that this person belongs to. Because a person can belong to more than one team, it is implied * that each person exists in only one location in the database. Therefore, if one person is enrolled in two courses @@ -50,12 +53,14 @@ public abstract class Person extends BasicEntity { * @param firstName The person's first name. * @param lastName The person's last name. * @param emailAddress The person's email address. + * @param githubUsername The person's github username; */ - public Person(String firstName, String lastName, String emailAddress) { + public Person(String firstName, String lastName, String emailAddress, String githubUsername) { this(); this.firstName = firstName; this.lastName = lastName; this.emailAddress = emailAddress; + this.githubUsername = githubUsername; } public void assignToTeam(Team team) { @@ -82,8 +87,40 @@ public abstract class Person extends BasicEntity { return this.emailAddress; } + public String getGithubUsername() { + return this.githubUsername; + } + + /** + * Determines if two Persons are equal. They are considered equal when all of the basic identifying information + * about the person is the same, regardless of case. + * @param o The other object. + * @return True if the other object is the same person, or false if not. + */ + @Override + public boolean equals(Object o) { + if (o == null) { + return false; + } + + if (o instanceof Person) { + Person p = (Person) o; + return ( + this.getFirstName().equalsIgnoreCase(p.getFirstName()) + && this.getLastName().equalsIgnoreCase(p.getLastName()) + && this.getEmailAddress().equalsIgnoreCase(p.getEmailAddress()) + && this.getGithubUsername().equalsIgnoreCase(p.getGithubUsername()) + ); + } + + return false; + } + @Override public String toString() { - return this.getFirstName() + ' ' + this.getLastName() + '[' + this.getId() + ']'; + return "First Name: " + this.getFirstName() + + ", Last Name: " + this.getLastName() + + ", Email: " + this.getEmailAddress() + + ", Github Username: " + this.getGithubUsername(); } } diff --git a/src/main/java/nl/andrewlalis/teaching_assistant_assistant/model/people/Student.java b/src/main/java/nl/andrewlalis/teaching_assistant_assistant/model/people/Student.java index e61216f..26963bf 100644 --- a/src/main/java/nl/andrewlalis/teaching_assistant_assistant/model/people/Student.java +++ b/src/main/java/nl/andrewlalis/teaching_assistant_assistant/model/people/Student.java @@ -1,5 +1,6 @@ package nl.andrewlalis.teaching_assistant_assistant.model.people; +import javax.persistence.Column; import javax.persistence.Entity; /** @@ -8,13 +9,52 @@ import javax.persistence.Entity; @Entity public class Student extends Person { + /** + * The student's unique student number, as given by the university. + */ + @Column(unique = true, nullable = false) + private int studentNumber; + /** * Default constructor for JPA. */ protected Student() {} - public Student(String firstName, String lastName, String emailAddress) { - super(firstName, lastName, emailAddress); + /** + * Constructs a new student with all the properties of a Person, and any extra properties. + * @param firstName The student's first name. + * @param lastName The student's last name. + * @param emailAddress The student's email address. + * @param githubUsername The student's Github username. + * @param studentNumber The student's unique student number. + */ + public Student(String firstName, String lastName, String emailAddress, String githubUsername, int studentNumber) { + super(firstName, lastName, emailAddress, githubUsername); + + this.studentNumber = studentNumber; } + public int getStudentNumber() { + return studentNumber; + } + + public void setStudentNumber(int studentNumber) { + this.studentNumber = studentNumber; + } + + /** + * Determines if two students are equal. They are considered equal if their person attributes are the same, or + * their student-specific attributes are equal. + * @param o The other object. + * @return True if the other object is the same student. + */ + @Override + public boolean equals(Object o) { + return super.equals(o) || this.getStudentNumber() == ((Student) o).getStudentNumber(); + } + + @Override + public String toString() { + return super.toString() + ", Student Number: " + this.getStudentNumber(); + } } diff --git a/src/main/java/nl/andrewlalis/teaching_assistant_assistant/model/people/TeachingAssistant.java b/src/main/java/nl/andrewlalis/teaching_assistant_assistant/model/people/TeachingAssistant.java index 19dfcf8..dee492b 100644 --- a/src/main/java/nl/andrewlalis/teaching_assistant_assistant/model/people/TeachingAssistant.java +++ b/src/main/java/nl/andrewlalis/teaching_assistant_assistant/model/people/TeachingAssistant.java @@ -15,7 +15,7 @@ public class TeachingAssistant extends Person { } - public TeachingAssistant(String firstName, String lastName, String emailAddress) { - super(firstName, lastName, emailAddress); + public TeachingAssistant(String firstName, String lastName, String githubUsername, String emailAddress) { + super(firstName, lastName, emailAddress, githubUsername); } } diff --git a/src/main/java/nl/andrewlalis/teaching_assistant_assistant/util/sample_data/CourseGenerator.java b/src/main/java/nl/andrewlalis/teaching_assistant_assistant/util/sample_data/CourseGenerator.java index eecf759..0273c0d 100644 --- a/src/main/java/nl/andrewlalis/teaching_assistant_assistant/util/sample_data/CourseGenerator.java +++ b/src/main/java/nl/andrewlalis/teaching_assistant_assistant/util/sample_data/CourseGenerator.java @@ -56,11 +56,11 @@ public class CourseGenerator extends TestDataGenerator { List studentTeams = this.generateStudentTeams(); List teachingAssistantTeams = this.generateTeachingAssistantTeams(); for (StudentTeam team : studentTeams) { - course.addStudentGroup(team); + course.addStudentTeam(team); team.setCourse(course); } for (TeachingAssistantTeam team : teachingAssistantTeams) { - course.addTeachingAssistantGroup(team); + course.addTeachingAssistantTeam(team); team.setCourse(course); } return course; diff --git a/src/main/java/nl/andrewlalis/teaching_assistant_assistant/util/sample_data/StudentGenerator.java b/src/main/java/nl/andrewlalis/teaching_assistant_assistant/util/sample_data/StudentGenerator.java index 23240c4..01945b5 100644 --- a/src/main/java/nl/andrewlalis/teaching_assistant_assistant/util/sample_data/StudentGenerator.java +++ b/src/main/java/nl/andrewlalis/teaching_assistant_assistant/util/sample_data/StudentGenerator.java @@ -12,6 +12,6 @@ public class StudentGenerator extends PersonGenerator { public Student generate() { String firstName = this.getRandomFirstName(); String lastName = this.getRandomLastName(); - return new Student(firstName, lastName, this.getRandomEmailAddress(firstName, lastName)); + return new Student(firstName, lastName, this.getRandomEmailAddress(firstName, lastName), null, this.getRandomInteger(0, 100000000)); } } diff --git a/src/main/java/nl/andrewlalis/teaching_assistant_assistant/util/sample_data/TeachingAssistantGenerator.java b/src/main/java/nl/andrewlalis/teaching_assistant_assistant/util/sample_data/TeachingAssistantGenerator.java index e4c9a89..8b2c8d4 100644 --- a/src/main/java/nl/andrewlalis/teaching_assistant_assistant/util/sample_data/TeachingAssistantGenerator.java +++ b/src/main/java/nl/andrewlalis/teaching_assistant_assistant/util/sample_data/TeachingAssistantGenerator.java @@ -12,6 +12,6 @@ public class TeachingAssistantGenerator extends PersonGenerator importFromCSV(InputStream fileInputStream, Course course) throws IOException { + Iterable records = CSVFormat.DEFAULT + .withSkipHeaderRecord() + .withHeader("timestamp", "email", "name", "number", "github_username", "has_partner", "partner_name", "partner_number", "partner_email", "partner_github_username") + .parse(new InputStreamReader(fileInputStream)); + + List studentEntries = extractStudentsFromRecords(records); + + for (StudentRecordEntry entry : studentEntries) { + System.out.println(entry.toString()); + } + + return new ArrayList<>(); + + } + + /** + * Extracts all student data from a list of records, and automatically discards outdated responses (those where the + * same student submitted more than once). + * @param records The list of records in the CSV file. + * @return A mapping for each timestamp to + */ + private static List extractStudentsFromRecords(Iterable records) { + List studentEntries = new ArrayList<>(); + + for (CSVRecord record : records) { + // Parse the actual student. + Student s = parseStudentRecordData(record.get("name"), record.get("email"), record.get("github_username"), record.get("number")); + + // Parse the student's preferred partner, if they exist. + Student preferredPartner = null; + if (record.get("has_partner").equalsIgnoreCase("yes")) { + preferredPartner = parseStudentRecordData(record.get("partner_name"), record.get("partner_email"), record.get("partner_github_username"), record.get("partner_number")); + } + + // Parse the timestamp. + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd h:mm:ss a O "); + ZonedDateTime dateTime = ZonedDateTime.parse(record.get("timestamp") + ' ', formatter); + // A space is added because of a java bug: https://stackoverflow.com/questions/37287103/why-does-gmt8-fail-to-parse-with-pattern-o-despite-being-copied-straight-ou + + studentEntries.add(new StudentRecordEntry(dateTime, s, preferredPartner)); + } + + studentEntries = removeDuplicateEntries(studentEntries); + + return studentEntries; + } + + + private static List removeDuplicateEntries(List studentEntries) { + List uniqueStudentEntries = new ArrayList<>(); + + for (StudentRecordEntry entry : studentEntries) { + // Check for if the current entry's student already exists. + boolean duplicateFound = false; + for (StudentRecordEntry existingEntry : uniqueStudentEntries) { + if (entry.getStudent().equals(existingEntry.getStudent())) { + duplicateFound = true; + // Check if the existing entry is older than the new one; it should be overwritten. + if (existingEntry.getDateTime().isBefore(entry.getDateTime())) { + uniqueStudentEntries.remove(existingEntry); + uniqueStudentEntries.add(entry); + break; + } + } + } + + if (!duplicateFound) { + uniqueStudentEntries.add(entry); + } + } + + return uniqueStudentEntries; + } + + /** + * Creates a student object from given entries in a record obtained from a CSV file. + * @param name The name value. + * @param email The email value. + * @param githubUsername The github_username value. + * @param studentNumber The number value. + * @return A student object constructed from the given data. + */ + private static Student parseStudentRecordData(String name, String email, String githubUsername, String studentNumber) { + // Extract a sensible first and last name. + String[] nameSegments = name.split(" "); + String firstName = "No First Name Given"; + String lastName = "No Last Name Given"; + + if (nameSegments.length > 0) { + firstName = nameSegments[0]; + } + + if (nameSegments.length > 1) { + lastName = String.join(" ", Arrays.copyOfRange(nameSegments, 1, nameSegments.length)); + } + + // Extract a sensible github username. + String githubURL = "https://github.com/"; + + if (githubUsername.startsWith(githubURL)) { + githubUsername = githubUsername.substring(githubURL.length()); + } + + // Create the student. + return new Student(firstName, lastName, email, githubUsername, Integer.parseInt(studentNumber)); + } + +} diff --git a/src/main/resources/templates/courses/entity.html b/src/main/resources/templates/courses/entity.html index 7355aa8..511b5b4 100644 --- a/src/main/resources/templates/courses/entity.html +++ b/src/main/resources/templates/courses/entity.html @@ -48,7 +48,7 @@ Import students from CSV diff --git a/src/main/resources/templates/courses/import_students.html b/src/main/resources/templates/courses/import_students.html new file mode 100644 index 0000000..f825b6b --- /dev/null +++ b/src/main/resources/templates/courses/import_students.html @@ -0,0 +1,26 @@ + + + + Import Students via CSV + + + +
+

+ Please select a CSV file to import. +

+ +
+ + + +
+ +
+ + + + + \ No newline at end of file diff --git a/src/main/resources/templates/courses/import_teaching_assistants.html b/src/main/resources/templates/courses/import_teaching_assistants.html new file mode 100644 index 0000000..d538fea --- /dev/null +++ b/src/main/resources/templates/courses/import_teaching_assistants.html @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file