Got CSV reading to work properly.

This commit is contained in:
Andrew Lalis 2019-04-17 22:16:05 +02:00 committed by andrewlalis
parent 2d9932eeb4
commit 7dff57ac98
16 changed files with 381 additions and 17 deletions

View File

@ -79,6 +79,12 @@
<artifactId>mysql-connector-java</artifactId>
<version>8.0.15</version>
</dependency>
<!-- CSV Format dependency -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-csv</artifactId>
<version>1.5</version>
</dependency>
</dependencies>
<build>

View File

@ -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());
}
}

View File

@ -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";
}

View File

@ -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<Course> 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<Course> optionalCourse = this.courseRepository.findByCode(code);
if (!optionalCourse.isPresent()) {
System.out.println("No course found.");
return "redirect:/courses";
}
try {
List<StudentTeam> studentTeams = StudentTeamImporter.importFromCSV(file.getInputStream(), optionalCourse.get());
} catch (IOException e) {
e.printStackTrace();
}
return "redirect:/courses/{code}";
}
}

View File

@ -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);
}
/*

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -56,11 +56,11 @@ public class CourseGenerator extends TestDataGenerator<Course> {
List<StudentTeam> studentTeams = this.generateStudentTeams();
List<TeachingAssistantTeam> 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;

View File

@ -12,6 +12,6 @@ public class StudentGenerator extends PersonGenerator<Student> {
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));
}
}

View File

@ -12,6 +12,6 @@ public class TeachingAssistantGenerator extends PersonGenerator<TeachingAssistan
public TeachingAssistant generate() {
String firstName = this.getRandomFirstName();
String lastName = this.getRandomLastName();
return new TeachingAssistant(firstName, lastName, this.getRandomEmailAddress(firstName, lastName));
return new TeachingAssistant(firstName, lastName, null, this.getRandomEmailAddress(firstName, lastName));
}
}

View File

@ -0,0 +1,44 @@
package nl.andrewlalis.teaching_assistant_assistant.util.team_importing;
import nl.andrewlalis.teaching_assistant_assistant.model.people.Student;
import java.time.ZonedDateTime;
/**
* Represents a pair of students, one of which has indicated that the other is their partner.
*/
public class StudentRecordEntry {
private Student student;
private Student partnerStudent;
private ZonedDateTime dateTime;
public StudentRecordEntry(ZonedDateTime dateTime, Student student, Student partnerStudent) {
this.student = student;
this.partnerStudent = partnerStudent;
this.dateTime = dateTime;
}
public Student getStudent() {
return this.student;
}
public Student getPartnerStudent() {
return this.partnerStudent;
}
public ZonedDateTime getDateTime() {
return this.dateTime;
}
public boolean hasPartner() {
return this.partnerStudent != null;
}
@Override
public String toString() {
return "Entry at: " + this.dateTime.toString() + "\n\tStudent: " + this.getStudent() + "\n\tPreferred partner: " + this.getPartnerStudent();
}
}

View File

@ -0,0 +1,136 @@
package nl.andrewlalis.teaching_assistant_assistant.util.team_importing;
import nl.andrewlalis.teaching_assistant_assistant.model.Course;
import nl.andrewlalis.teaching_assistant_assistant.model.people.Student;
import nl.andrewlalis.teaching_assistant_assistant.model.people.teams.StudentTeam;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVRecord;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
/**
* Provides some methods to streamline the process of transforming a CSV file of student data into a list of valid teams
* which can be used for a course.
*/
public class StudentTeamImporter {
/**
* Imports data from the given File Input Stream into the course.
* @param fileInputStream An input stream representing the contents of a CSV file.
* @param course The course to add the students to.
* @return A list of valid teams that the given file describes.
*/
public static List<StudentTeam> importFromCSV(InputStream fileInputStream, Course course) throws IOException {
Iterable<CSVRecord> 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<StudentRecordEntry> 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<StudentRecordEntry> extractStudentsFromRecords(Iterable<CSVRecord> records) {
List<StudentRecordEntry> 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<StudentRecordEntry> removeDuplicateEntries(List<StudentRecordEntry> studentEntries) {
List<StudentRecordEntry> 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));
}
}

View File

@ -48,7 +48,7 @@
<a th:href="@{/courses/{code}/import_students(code=${course.getCode()})}">Import students from CSV</a>
</div>
<div class="sidebar_block">
<a th:href="@{/courses/{code}/import_teaching_assistants(code=${course.getCode()})}">Import teaching assistants from CSV</a>
<a th:disabled="true" th:href="@{/courses/{code}/import_teaching_assistants(code=${course.getCode()})}">Import teaching assistants from CSV</a>
</div>
</div>

View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" th:replace="~{layouts/basic_page :: layout (~{::title}, ~{::#content}, ~{::#sidebar_content})}">
<head>
<title>Import Students via CSV</title>
</head>
<body>
<div id="content">
<p>
Please select a CSV file to import.
</p>
<form method="post" action="#" enctype="multipart/form-data" th:action="@{/courses/{code}/import_students(code=${course.getCode()})}">
<label for="file_input">File:</label>
<input id="file_input" type="file" name="file" accept="text/csv"/>
<button type="submit">Submit</button>
</form>
</div>
<div id="sidebar_content">
</div>
</body>
</html>

View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
</body>
</html>