diff --git a/pom.xml b/pom.xml index 1d8e0c8..7137563 100644 --- a/pom.xml +++ b/pom.xml @@ -21,6 +21,10 @@ jar + + 2.9.6 + + org.apache.commons @@ -43,6 +47,38 @@ 16.0.2 compile + + org.apache.httpcomponents + httpclient + RELEASE + + + com.fasterxml.jackson.core + jackson-core + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-annotations + ${jackson.version} + + + + org.kohsuke + github-api + 1.93 + + + + org.xerial + sqlite-jdbc + 3.23.1 + \ No newline at end of file diff --git a/src/main/java/nl/andrewlalis/Main.java b/src/main/java/nl/andrewlalis/Main.java index 3aa4015..b6e7202 100644 --- a/src/main/java/nl/andrewlalis/Main.java +++ b/src/main/java/nl/andrewlalis/Main.java @@ -1,16 +1,26 @@ package nl.andrewlalis; -import nl.andrewlalis.model.Team; +import nl.andrewlalis.model.database.Database; +import nl.andrewlalis.git_api.GithubManager; +import nl.andrewlalis.model.Student; +import nl.andrewlalis.model.StudentTeam; +import nl.andrewlalis.ui.control.command.CommandExecutor; +import nl.andrewlalis.ui.control.command.Executable; +import nl.andrewlalis.ui.control.command.executables.ArchiveRepos; +import nl.andrewlalis.ui.control.command.executables.GenerateAssignmentsRepo; +import nl.andrewlalis.ui.control.command.executables.ReadStudentsFileToDB; +import nl.andrewlalis.ui.view.InitializerApp; +import nl.andrewlalis.util.CommandLine; import nl.andrewlalis.util.Logging; import nl.andrewlalis.util.TeamGenerator; +import javax.swing.*; import java.io.IOException; import java.util.List; import java.util.Map; +import java.util.logging.Level; import java.util.logging.Logger; -import nl.andrewlalis.util.CommandLine; - /** * Main program entry point. */ @@ -26,26 +36,48 @@ public class Main { // Initialize logger. try { Logging.setup(true); // TODO: Replace true with command line arg. + } catch (IOException e) { logger.severe("Unable to save log to file."); } - logger.info("Initializer for Github Repositories in Educational Organizations."); + // Command executor which will be used by all actions the user can do. + CommandExecutor executor = new CommandExecutor(); + + // Initialize User Interface. + InitializerApp app = new InitializerApp(executor); + app.begin(); + + Database db = new Database("database/initializer.sqlite"); + db.initialize(); + + executor.registerCommand("readstudents", new ReadStudentsFileToDB(db)); + executor.registerCommand("archiveall", new ArchiveRepos()); + executor.registerCommand("generateassignments", new GenerateAssignmentsRepo()); + + logger.info("GithubManager for Github Repositories in Educational Organizations. Program initialized."); + + + + // Get studentTeams from CSV file. +// List studentTeams = getStudentTeamsFromCSV(userOptions.get("input"), Integer.parseInt(userOptions.get("teamsize"))); +// +// GithubManager githubManager = new GithubManager( +// userOptions.get("organization"), +// userOptions.get("token"), +// "assignments_2018", +// "teaching-assistants", +// "advoop_2018" +// ); - // Get teams from CSV file. - List teams; try { - teams = TeamGenerator.generateFromCSV( - userOptions.get("input"), - Integer.parseInt(userOptions.get("teamsize")) - ); - logger.info("Teams created: " + teams); - } catch (IOException | ArrayIndexOutOfBoundsException e) { - logger.severe("Unable to generate teams from CSV file, exiting."); - System.exit(1); + //githubManager.initializeGithubRepos(studentTeams); + //githubManager.archiveAllRepositories("team"); + } catch (Exception e) { + e.printStackTrace(); } - - - } + + + } diff --git a/src/main/java/nl/andrewlalis/git_api/GithubManager.java b/src/main/java/nl/andrewlalis/git_api/GithubManager.java new file mode 100644 index 0000000..5de6ec7 --- /dev/null +++ b/src/main/java/nl/andrewlalis/git_api/GithubManager.java @@ -0,0 +1,262 @@ +package nl.andrewlalis.git_api; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import nl.andrewlalis.model.Student; +import nl.andrewlalis.model.StudentTeam; +import nl.andrewlalis.model.TATeam; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpPatch; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.kohsuke.github.*; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Logger; + +/** + * This class is responsible for initializing the Github repositories and setting permissions, adding teams, etc. + */ +public class GithubManager { + + /** + * The assignments repository where students will get assignments from. + */ + private GHRepository assignmentsRepo; + + /** + * Github object for API interactions. + */ + private GitHub github; + private GHOrganization organization; + private String accessToken; + + /** + * The logger for outputting debug info. + */ + private static final Logger logger = Logger.getLogger(GithubManager.class.getName()); + static { + logger.setParent(Logger.getGlobal()); + } + + public GithubManager(String organizationName, String accessToken) { + this.accessToken = accessToken; + try { + this.github = GitHub.connectUsingOAuth(accessToken); + this.organization = this.github.getOrganization(organizationName); + } catch (IOException e) { + logger.severe("Unable to make a GithubManager with organization name: " + organizationName + " and access token: " + accessToken); + e.printStackTrace(); + } + } + + /** + * Initializes the github repository for all studentTeams given. + * + * Creates for the entire organization: + * - an assignments repository with protected master branch and TA permissions. + * Creates for each team: + * - a repository + * - protected master branch + * - development branch + * - adds students to repository + * - adds all students to assignments repository. + * @param studentTeams The list of student studentTeams. + * @param teamAll The team of all teaching assistants. + * @param assignmentsRepoName The name of the assignments repo. + * @throws Exception If an error occurs while initializing the github repositories. + */ + public void initializeGithubRepos(List studentTeams, TATeam teamAll, String assignmentsRepoName) throws Exception { + this.setupAssignmentsRepo(assignmentsRepoName, "fuck the police", teamAll.getName()); + + StudentTeam t = new StudentTeam(); + Student s = new Student(3050831, "Andrew Lalis", "andrewlalisofficial@gmail.com", "andrewlalis", null); + t.addMember(s); + t.setId(42); + + this.setupStudentTeam(t, teamAll.getGithubTeam(), "advoop_2018"); + // TODO: Finish this method. + } + + /** + * Sets up the organization's assignments repository, and grants permissions to all teaching assistants. + * @param assignmentsRepoName The name of the assignments repository. + * @param description The description of the repository. + * @param allTeachingAssistants The name of the team consisting of all teaching assistants. + * @throws IOException If an HTTP request failed. + */ + public void setupAssignmentsRepo(String assignmentsRepoName, String description, String allTeachingAssistants) throws IOException { + GHTeam team = this.organization.getTeamByName(allTeachingAssistants); + // Check if the repository already exists. + GHRepository existingRepo = this.organization.getRepository(assignmentsRepoName); + if (existingRepo != null) { + existingRepo.delete(); + logger.fine("Deleted pre-existing assignments repository."); + } + + this.assignmentsRepo = this.createRepository(assignmentsRepoName, team, description, false, true, false); + + if (this.assignmentsRepo == null) { + logger.severe("Could not create assignments repository."); + return; + } + + this.protectMasterBranch(this.assignmentsRepo, team); + + // Grant all teaching assistants write access. + team.add(this.assignmentsRepo, GHOrganization.Permission.ADMIN); + logger.fine("Gave admin rights to all teaching assistants in team: " + team.getName()); + } + + /** + * Creates and sets up a student team's repository, and invites those students to the organization's assignments + * repository as well. + * @param team The student team to set up. + * @param taTeam The team of teaching assistants that is responsible for these students. + * @param prefix The prefix to append to the front of the repo name. + * @throws IOException If an HTTP request fails. + */ + public void setupStudentTeam(StudentTeam team, GHTeam taTeam, String prefix) throws IOException { + // First check that the assignments repo exists, otherwise no invitations can be sent. + if (this.assignmentsRepo == null) { + logger.warning("Assignments repository must be created before student repositories."); + return; + } + + GHRepository repo = this.createRepository(team.generateUniqueName(prefix), taTeam, team.generateRepoDescription(), false, true, false); + + if (repo == null) { + logger.severe("Repository for student team " + team.getId() + " could not be created."); + return; + } + + this.protectMasterBranch(repo, taTeam); + this.createDevelopmentBranch(repo); + + taTeam.add(repo, GHOrganization.Permission.ADMIN); + logger.fine("Added team " + taTeam.getName() + " as admin to repository: " + repo.getName()); + + List users = new ArrayList<>(); + for (Student student : team.getStudents()) { + GHUser user = this.github.getUser(student.getGithubUsername()); + users.add(user); + } + + repo.addCollaborators(users); + this.assignmentsRepo.addCollaborators(users); + } + + /** + * Deletes all repositories in the organization. + * @throws IOException if an error occurs with sending requests. + */ + public void deleteAllRepositories() throws IOException { + List repositories = this.organization.listRepositories().asList(); + for (GHRepository repo : repositories) { + repo.delete(); + } + } + + /** + * Archives all repositories whose name contains the given substring. + * @param sub Any repository containing this substring will be archived. + */ + public void archiveAllRepositories(String sub) throws IOException { + List repositories = this.organization.listRepositories().asList(); + for (GHRepository repo : repositories) { + if (repo.getName().contains(sub)) { + archiveRepository(repo); + } + } + } + + /** + * Archives a repository so that it can no longer be manipulated. + * TODO: Change to using Github API instead of Apache HttpUtils. + * @param repo The repository to archive. + * @throws IOException If an error occurs with the HTTP request. + */ + public void archiveRepository(GHRepository repo) throws IOException { + HttpPatch patch = new HttpPatch("https://api.github.com/repos/" + repo.getFullName() + "?access_token=" + this.accessToken); + CloseableHttpClient client = HttpClientBuilder.create().build(); + ObjectMapper mapper = new ObjectMapper(); + ObjectNode root = mapper.createObjectNode(); + root.put("archived", true); + String json = mapper.writeValueAsString(root); + patch.setEntity(new StringEntity(json)); + HttpResponse response = client.execute(patch); + if (response.getStatusLine().getStatusCode() != 200) { + throw new IOException("Could not archive repository: " + repo.getName() + ". Code: " + response.getStatusLine().getStatusCode()); + } + logger.info("Archived repository: " + repo.getFullName()); + } + + /** + * Protects the master branch of a given repository, and gives admin rights to the given team. + * @param repo The repository to protect the master branch of. + * @param team The team which gets admin rights to the master branch. + */ + @SuppressWarnings("deprecation") + private void protectMasterBranch(GHRepository repo, GHTeam team) { + try { + GHBranchProtectionBuilder protectionBuilder = repo.getBranch("master").enableProtection(); + protectionBuilder.includeAdmins(false); + protectionBuilder.restrictPushAccess(); + protectionBuilder.teamPushAccess(team); + protectionBuilder.addRequiredChecks("ci/circleci"); + protectionBuilder.enable(); + logger.fine("Protected master branch of repository: " + repo.getName()); + } catch (IOException e) { + logger.severe("Could not protect master branch of repository: " + repo.getName()); + e.printStackTrace(); + } + } + + /** + * Creates a development branch for the given repository. + * @param repo The repository to create a development branch for. + */ + private void createDevelopmentBranch(GHRepository repo) { + try { + String sha1 = repo.getBranch(repo.getDefaultBranch()).getSHA1(); + repo.createRef("refs/heads/development", sha1); + logger.fine("Created development branch of repository: " + repo.getName()); + } catch (IOException e) { + logger.severe("Could not create development branch for repository: " + repo.getName() + '\n' + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * Creates a new github repository. + * @param name The name of the repository. + * @param taTeam The team to give admin rights. + * @param description The description of the repository. + * @param hasWiki Whether the repo has a wiki enabled. + * @param hasIssues Whether the repo has issues enabled. + * @param isPrivate Whether or not the repository is private. + * @return The repository that was created, or + */ + private GHRepository createRepository(String name, GHTeam taTeam, String description, boolean hasWiki, boolean hasIssues, boolean isPrivate){ + try { + GHCreateRepositoryBuilder builder = this.organization.createRepository(name); + builder.team(taTeam); + builder.wiki(hasWiki); + builder.issues(hasIssues); + builder.description(description); + builder.gitignoreTemplate("Java"); + builder.private_(isPrivate); // TODO: Change this to true for production + GHRepository repo = builder.create(); + logger.fine("Created repository: " + repo.getName()); + return repo; + } catch (IOException e) { + logger.severe("Could not create repository: " + name + '\n' + e.getMessage()); + e.printStackTrace(); + return null; + } + } + +} diff --git a/src/main/java/nl/andrewlalis/git_api/package-info.java b/src/main/java/nl/andrewlalis/git_api/package-info.java new file mode 100644 index 0000000..097515a --- /dev/null +++ b/src/main/java/nl/andrewlalis/git_api/package-info.java @@ -0,0 +1,7 @@ +/** + * Contains all logic which directly interacts with the Github API. All methods which need to perform an action on a + * repository or team should find it within this package. + * + * @author Andrew Lalis + */ +package nl.andrewlalis.git_api; \ No newline at end of file diff --git a/src/main/java/nl/andrewlalis/model/Person.java b/src/main/java/nl/andrewlalis/model/Person.java new file mode 100644 index 0000000..66b0fb7 --- /dev/null +++ b/src/main/java/nl/andrewlalis/model/Person.java @@ -0,0 +1,89 @@ +package nl.andrewlalis.model; + +/** + * A generic object that students, teaching assistants, and professors can extend from. This covers all the basic + * functionality that applies to anyone in the system. + */ +public abstract class Person { + + /** + * The unique identification number for this person. (P- or S-Number) + */ + protected int number; + + /** + * The person's first and last name. + */ + protected String name; + + /** + * The person's email address. + */ + protected String emailAddress; + + /** + * The person's github username. + */ + protected String githubUsername; + + /** + * Constructs a Person from all the basic information needed. + * @param number Either an S- or P-Number without the letter prefix. + * @param name The first, middle (if applicable) and last name. + * @param emailAddress The email address. (Either university or personal.) + * @param githubUsername The person's github username. + */ + public Person(int number, String name, String emailAddress, String githubUsername){ + this.number = number; + this.name = name; + this.emailAddress = emailAddress; + this.githubUsername = githubUsername; + } + + /** + * Accessors + */ + public int getNumber(){ + return this.number; + } + + public String getName(){ + return this.name; + } + + public String getEmailAddress(){ + return this.emailAddress; + } + + public String getGithubUsername(){ + return this.githubUsername; + } + + /** + * Determines if two persons are the same. This is defined as: + * Two persons are equal if at least one of their personal data points is identical. Because each of the data points + * should be unique to this person alone, if there is any conflict, assume that they are equal. + * @param o The object to compare to. + * @return True if the two persons share personal data, or false if all data is unique among the two. + */ + @Override + public boolean equals(Object o) { + if (!(o instanceof Person)) { + return false; + } + Person p = (Person)o; + return p.getNumber() == this.getNumber() + || p.getEmailAddress().equals(this.getEmailAddress()) + || p.getGithubUsername().equals(this.getGithubUsername()) + || p.getName().equalsIgnoreCase(this.getName()); + } + + /** + * Represents the person as a basic comma-separated string object. + * @return A comma-separated String object. + */ + @Override + public String toString() { + return this.getName() + ", " + this.getNumber() + ", " + this.getEmailAddress() + ", " + this.getGithubUsername(); + } +} diff --git a/src/main/java/nl/andrewlalis/model/Student.java b/src/main/java/nl/andrewlalis/model/Student.java index d268ab5..ec8712b 100644 --- a/src/main/java/nl/andrewlalis/model/Student.java +++ b/src/main/java/nl/andrewlalis/model/Student.java @@ -1,66 +1,34 @@ package nl.andrewlalis.model; -import java.util.ArrayList; import java.util.List; import java.util.Map; /** * Represents one student's github information. */ -public class Student { - - /** - * The student's S-number. - */ - private int number; - - /** - * The student's name. - */ - private String name; - - /** - * The student's email. - */ - private String emailAddress; - - /** - * The student's github username. - */ - private String githubUsername; +public class Student extends Person { /** * A list of partners that the student has said that they would like to be partners with. */ private List preferredPartners; + /** + * Constructs a student similarly to a Person, but with an extra preferredPartners list. + * @param number The student's S-Number. + * @param name The student's name. + * @param emailAddress The student's email address. + * @param githubUsername The student's github username. + * @param preferredPartners A list of this student's preferred partners, as a list of integers representing the + * other students' numbers. + */ public Student(int number, String name, String emailAddress, String githubUsername, List preferredPartners) { - this.number = number; - this.name = name; - this.emailAddress = emailAddress; - this.githubUsername = githubUsername; + super(number, name, emailAddress, githubUsername); this.preferredPartners = preferredPartners; } - @Override - public String toString() { - return this.number + " - " + this.name + " - " + this.emailAddress + " - " + this.githubUsername; - } - - public int getNumber() { - return number; - } - - public String getEmailAddress() { - return emailAddress; - } - - public String getGithubUsername() { - return githubUsername; - } - public List getPreferredPartners() { - return preferredPartners; + return this.preferredPartners; } /** @@ -68,23 +36,12 @@ public class Student { * @param studentMap A mapping from student number to student for all students who have signed up. * @return A team with unknown id, comprised of this student's preferred partners. */ - public Team getPreferredTeam(Map studentMap) { - Team t = new Team(); + public StudentTeam getPreferredTeam(Map studentMap) { + StudentTeam t = new StudentTeam(); for (int partnerNumber : this.getPreferredPartners()) { - t.addStudent(studentMap.get(partnerNumber)); + t.addMember(studentMap.get(partnerNumber)); } - t.addStudent(this); + t.addMember(this); return t; } - - @Override - public boolean equals(Object s) { - if (!(s instanceof Student)) { - return false; - } - Student student = (Student) s; - return student.getNumber() == this.getNumber() - || student.getEmailAddress().equals(this.getEmailAddress()) - || student.getGithubUsername().equals(this.getGithubUsername()); - } } diff --git a/src/main/java/nl/andrewlalis/model/StudentTeam.java b/src/main/java/nl/andrewlalis/model/StudentTeam.java new file mode 100644 index 0000000..8967d27 --- /dev/null +++ b/src/main/java/nl/andrewlalis/model/StudentTeam.java @@ -0,0 +1,76 @@ +package nl.andrewlalis.model; + +import java.util.Arrays; + +/** + * Represents one or more students' collective information. + */ +public class StudentTeam extends Team{ + + public StudentTeam() { + super(-1); + } + + /** + * Gets a list of students, casted from the original Person[]. + * @return An array of Students. + */ + public Student[] getStudents() { + return Arrays.copyOf(this.getMembers(), this.memberCount(), Student[].class); + } + + /** + * Determines if a team is valid, and ready to be added to the Github organization. + * A team is valid if and only if: + * - The student count is equal to the team size. + * - Each student is unique. + * - Each student's preferred partners match all the others. + * @param teamSize The preferred size of teams. + * @return True if the team is valid, and false otherwise. + */ + public boolean isValid(int teamSize) { + if (this.memberCount() == teamSize) { + for (Student studentA : this.getStudents()) { + for (Student studentB : this.getStudents()) { + if (!studentA.equals(studentB) && !studentA.getPreferredPartners().contains(studentB.getNumber())) { + return false; + } + } + } + return true; + } else { + return false; + } + } + + /** + * Generates a unique name which is intended to be used for the repository name of this team. + * @param prefix A prefix to further reduce the chances of duplicate names. + * It is suggested to use something like "2018_OOP" + * @return A string comprised of the prefix, team id, and student number of each team member. + */ + public String generateUniqueName(String prefix) { + StringBuilder sb = new StringBuilder(prefix); + sb.append("_team_").append(this.id); + for (Student s : this.getStudents()) { + sb.append('_').append(s.getNumber()); + } + return sb.toString(); + } + + /** + * Generates a description for the repository, based on the students' names and group number. + * @return A description for the students' repository. + */ + public String generateRepoDescription() { + StringBuilder sb = new StringBuilder(); + sb.append("Group ").append(this.id).append(": "); + for (int i = 0; i < this.memberCount(); i++) { + sb.append(this.getStudents()[i].getName()); + if (i != this.memberCount()-1) { + sb.append(", "); + } + } + return sb.toString(); + } +} diff --git a/src/main/java/nl/andrewlalis/model/TATeam.java b/src/main/java/nl/andrewlalis/model/TATeam.java new file mode 100644 index 0000000..2e252df --- /dev/null +++ b/src/main/java/nl/andrewlalis/model/TATeam.java @@ -0,0 +1,72 @@ +package nl.andrewlalis.model; + +import org.kohsuke.github.GHOrganization; +import org.kohsuke.github.GHTeam; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents a teaching assistant team, which is itself a 'team' in the organization. This class is used for parsing + * json from requests to github to get a list of all teams in the organization. + */ +public class TATeam { + + private List teachingAssistants; + + /** + * The team's display name. + */ + private String name; + + /** + * The team's unique identifier. + */ + private int id; + + /** + * The Github team associated with this team. + */ + private GHTeam githubTeam; + + /** + * Constructs a team without any teaching assistant members. + * @param name The name of the team. + * @param id The unique identifier for this team. + */ + public TATeam(String name, int id) { + this.name = name; + this.id = id; + this.teachingAssistants = new ArrayList(); + } + + /** + * Constructs a team with a list of teaching assistants that are part of it. + * @param teachingAssistants The list of teaching assistants that are part of the team. + */ + public TATeam(List teachingAssistants, String name, int id) { + this.teachingAssistants = teachingAssistants; + this.name = name; + this.id = id; + } + + /** + * Gets the unique identification for this TA team. + * @return An integer representing the id of this team. + */ + public int getId() { + return this.id; + } + + public String getName() { + return this.name; + } + + public GHTeam getGithubTeam() { + return this.githubTeam; + } + + public void setGithubTeam(GHTeam team) { + this.githubTeam = team; + } +} diff --git a/src/main/java/nl/andrewlalis/model/TeachingAssistant.java b/src/main/java/nl/andrewlalis/model/TeachingAssistant.java new file mode 100644 index 0000000..918bb5c --- /dev/null +++ b/src/main/java/nl/andrewlalis/model/TeachingAssistant.java @@ -0,0 +1,16 @@ +package nl.andrewlalis.model; + +public class TeachingAssistant extends Person { + + /** + * Constructs a Teaching Assistant from all the basic information needed, much like its parent, Person. + * + * @param number Either an S- or P-Number without the letter prefix. + * @param name The first, middle (if applicable) and last name. + * @param emailAddress The email address. (Either university or personal.) + * @param githubUsername The person's github username. + */ + public TeachingAssistant(int number, String name, String emailAddress, String githubUsername) { + super(number, name, emailAddress, githubUsername); + } +} diff --git a/src/main/java/nl/andrewlalis/model/Team.java b/src/main/java/nl/andrewlalis/model/Team.java index bd82bd2..f1b23db 100644 --- a/src/main/java/nl/andrewlalis/model/Team.java +++ b/src/main/java/nl/andrewlalis/model/Team.java @@ -1,147 +1,153 @@ package nl.andrewlalis.model; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; /** - * Represents one or more students' collective information. + * An abstract Team object from which both Teaching Assistant and Student teams can be built. A Team consists of a list + * of members, and a unique identification number. */ -public class Team { +public abstract class Team { /** - * The list of students in this team. + * An identification number unique to this team alone. */ - private List students; + protected int id; /** - * The team identification number. + * A list of members of this team. */ - private int id; + private List members; - public Team() { - this.students = new ArrayList<>(); - this.id = -1; + /** + * Constructs this team with the given id. + * @param id The id to assign to this team. + */ + public Team(int id) { + this.id = id; + this.members = new ArrayList<>(); } /** - * Determines if a student is already included in this team. - * @param student A student. - * @return True if the student is in this team, false otherwise. + * @param newId The new id number to assign to this team. */ - public boolean hasStudent(Student student) { - for (Student s : this.students) { - if (s.equals(student)) { + public void setId(int newId) { + this.id = newId; + } + + /** + * @return This team's id number. + */ + public int getId() { + return this.id; + } + + /** + * Adds a new person to this team, only if they do not exist in this team yet. + * @param newMember The new member to add. + */ + public void addMember(Person newMember) { + for (Person person : this.members) { + if (person.equals(newMember)) { + return; + } + } + this.members.add(newMember); + } + + /** + * Removes a person from this team. + * @param person The person to remove. + */ + public void removeMember(Person person) { + this.members.remove(person); + } + + /** + * Checks if this team contains the given person. + * @param person The person to check for. + * @return True if the person is a member of this team, false otherwise. + */ + public boolean containsMember(Person person) { + for (Person p : this.members) { + if (p.equals(person)) { return true; } } return false; } - public int getStudentCount() { - return this.students.size(); - } - - public int getId() { - return this.id; - } - - public void setId(int id) { - this.id = id; - } - - public void setStudents(List students) { - this.students = students; - } - - public List getStudents() { - return this.students; + /** + * Sets the team to be comprised of only the members given in the array. + * @param people The people which will make up the members of this team. + */ + public void setMembers(Person[] people) { + this.members = new ArrayList<>(Arrays.asList(people)); } /** - * Adds a student to this team. - * @param student The student to add. - * @return True if the student could be added, false otherwise. + * Gets a list of people in this team. + * @return A list of people in this team. */ - public boolean addStudent(Student student) { - if (!this.hasStudent(student)) { - this.students.add(student); - return true; - } else { - return false; - } + public Person[] getMembers() { + Person[] people = new Person[this.memberCount()]; + this.members.toArray(people); + return people; } /** - * Determines if a team is valid, and ready to be added to the Github organization. - * A team is valid if and only if: - * - The student count is equal to the team size. - * - Each student is unique. - * - Each student's preferred partners match all the others. - * @param teamSize The preferred size of teams. - * @return True if the team is valid, and false otherwise. + * Gets the number of people in this team. + * @return The number of people in this team. */ - public boolean isValid(int teamSize) { - if (this.getStudentCount() == teamSize) { - List encounteredIds = new ArrayList<>(); - for (Student studentA : this.students) { - for (Student studentB : this.students) { - if (!studentA.equals(studentB) && !studentA.getPreferredPartners().contains(studentB.getNumber())) { - return false; - } - } - } - return true; - } else { - return false; - } + public int memberCount() { + return this.members.size(); } /** - * Generates a unique name which is intended to be used for the repository name of this team. - * @param prefix A prefix to further reduce the chances of duplicate names. - * It is suggested to use something like "2018_OOP" - * @return A string comprised of the prefix, team id, and student number of each team member. + * Determines if another team has the same members as this team. + * @param team The team to compare to this team. + * @return True if the other team has all the same members as this team. */ - public String generateUniqueName(String prefix) { - StringBuilder sb = new StringBuilder(prefix); - sb.append("_team_").append(this.id); - for (Student s : this.students) { - sb.append('_').append(s.getNumber()); - } - return sb.toString(); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder("Team: "); - sb.append(this.id).append('\n'); - for (Student s : this.students) { - sb.append('\t').append(s.toString()).append('\n'); - } - return sb.toString(); - } - - /** - * Determines if one team is equivalent to another. This is determined by if the two teams are comprised of the same - * students, in any order. - * @param o The object to compare to this team. - * @return True if the teams contain the same students, false otherwise. - */ - @Override - public boolean equals(Object o) { - if (o instanceof Team) { - Team t = (Team) o; - if (t.getStudentCount() != this.getStudentCount()) { - return false; - } - for (Student s : this.students) { - if (!t.hasStudent(s)) { + public boolean hasSameMembers(Team team) { + if (this.memberCount() == team.memberCount()) { + for (Person person : this.members) { + if (!team.containsMember(person)) { return false; } } return true; - } else { - return false; } + return false; } + + /** + * Checks if an object is equal to this team. First checks if the other object is a Team, and then if it has the + * same id and team size. If both of those conditions are met, then it will check that all team members are the + * same. + * @param obj The object to check for equality. + * @return True if the two objects represent the same team, or false otherwise. + */ + @Override + public boolean equals(Object obj) { + if (obj instanceof Team) { + Team team = (Team) obj; + return team.getId() == this.getId() && this.hasSameMembers(team); + } + return false; + } + + /** + * @return A String containing a line for each member in the team. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("Team of ").append(this.memberCount()).append(" members:\tID: ").append(this.id).append('\n'); + for (Person person : this.members) { + sb.append(person.toString()).append('\n'); + } + return sb.toString(); + } + } diff --git a/src/main/java/nl/andrewlalis/model/database/Database.java b/src/main/java/nl/andrewlalis/model/database/Database.java new file mode 100644 index 0000000..14c8626 --- /dev/null +++ b/src/main/java/nl/andrewlalis/model/database/Database.java @@ -0,0 +1,261 @@ +package nl.andrewlalis.model.database; + +import nl.andrewlalis.model.*; + +import java.sql.*; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Logger; + +/** + * This class abstracts many of the functions needed for interaction with the application's SQLite database. + */ +public class Database { + + private static final int PERSON_TYPE_STUDENT = 0; + private static final int PERSON_TYPE_TA = 1; + + private static final int TEAM_TYPE_STUDENT = 0; + private static final int TEAM_TYPE_TA = 1; + private static final int TEAM_TYPE_TA_ALL = 2; + + private static final int TEAM_NONE = 1000000; + private static final int TEAM_TA_ALL = 1000001; + + private static final int ERROR_TYPE_TEAM = 0; + private static final int ERROR_TYPE_PERSON = 1; + private static final int ERROR_TYPE_SYSTEM = 2; + + /** + * The connection needed for all queries. + */ + private Connection connection; + + /** + * The logger for outputting debug info. + */ + private static final Logger logger = Logger.getLogger(Database.class.getName()); + static { + logger.setParent(Logger.getGlobal()); + } + + public Database(String databaseFilename) { + try { + this.connection = DriverManager.getConnection("jdbc:sqlite:" + databaseFilename); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + /** + * Initializes the database from the table_init.sql script, which defines the table schema. + * Then, inserts some constant starter data from /sql/insert/types.sql. + * @return True if successful, false if not. + */ + public boolean initialize() { + List tableStatements = Utils.prepareStatementsFromFile("/sql/table_init.sql", this.connection); + for (PreparedStatement statement : tableStatements) { + try { + statement.execute(); + } catch (SQLException e) { + logger.severe("SQLException while executing prepared statement:\n" + statement.toString() + "\nCode: " + e.getErrorCode()); + return false; + } + } + logger.fine("Database tables initialized."); + List insertStatements = Utils.prepareStatementsFromFile("/sql/insert/types.sql", this.connection); + for (PreparedStatement statement : insertStatements) { + try { + statement.execute(); + } catch (SQLException e) { + logger.severe("SQLException while inserting into table:\n" + statement.toString() + "\nCode: " + e.getErrorCode()); + return false; + } + } + logger.fine("Initial types inserted."); + return true; + } + + /** + * Stores a person in the database. + * @param person The person object to store. + * @param personType The type of person to store, using a constant defined above. + * @return True if successful, false otherwise. + */ + private boolean storePerson(Person person, int personType) { + try { + logger.finest("Storing person: " + person); + String sql = "INSERT INTO persons (id, name, email_address, github_username, person_type_id) VALUES (?, ?, ?, ?, ?);"; + PreparedStatement stmt = this.connection.prepareStatement(sql); + stmt.setInt(1, person.getNumber()); + stmt.setString(2, person.getName()); + stmt.setString(3, person.getEmailAddress()); + stmt.setString(4, person.getGithubUsername()); + stmt.setInt(5, personType); + stmt.execute(); + return true; + } catch (SQLException e) { + logger.severe("SQLException while inserting Person: " + person + '\n' + e.getMessage()); + return false; + } + } + + /** + * Retrieves a list of preferred partners that each student has set. + * @param studentId The student id to search by. + * @return A list of student id's for all students that the given student wishes to be their partner. + */ + private List retrievePreferredPartners(int studentId) { + try { + logger.finest("Retrieving preferred partners of student: " + studentId); + String sql = "SELECT partner_id FROM student_preferred_partners WHERE student_id=?;"; + PreparedStatement stmt = this.connection.prepareStatement(sql); + stmt.setInt(1, studentId); + ResultSet results = stmt.executeQuery(); + List partners = new ArrayList<>(); + while (results.next()) { + partners.add(results.getInt(1)); + } + return partners; + } catch (SQLException e) { + logger.severe("SQL Exception while retrieving preferred partners of student: " + studentId + '\n' + e.getMessage()); + e.printStackTrace(); + return new ArrayList<>(); + } + } + + /** + * Retrieves a student by their id. + * @param id The id of the student (student number) + * @return The student corresponding to this number, or null if it could not be found. + */ + public Student retrieveStudent(int id) { + try { + String sql = "SELECT * FROM persons WHERE id=?"; + PreparedStatement stmt = this.connection.prepareStatement(sql); + stmt.setInt(1, id); + ResultSet result = stmt.executeQuery(); + return new Student(id, result.getString("name"), result.getString("email_address"), result.getString("github_username"), this.retrievePreferredPartners(id)); + } catch (SQLException e) { + logger.severe("SQL Exception while retrieving Student.\n" + e.getMessage()); + e.printStackTrace(); + return null; + } + } + + /** + * Stores a teaching assistant without a team. + * @param ta The teaching assistant to store. + * @return True if successful, false otherwise. + */ + public boolean storeTeachingAssistant(TeachingAssistant ta) { + return this.storeTeachingAssistant(ta, TEAM_NONE); + } + + /** + * Stores a teaching assistant in the database. + * @param ta The teaching assistant to store. + * @param teamId The teaching assistant's team id. + * @return True if successful, false otherwise. + */ + public boolean storeTeachingAssistant(TeachingAssistant ta, int teamId) { + if (!storePerson(ta, PERSON_TYPE_TA)) { + return false; + } + try { + String sql = "INSERT INTO teaching_assistants (person_id, team_id) VALUES (?, ?);"; + PreparedStatement stmt = this.connection.prepareStatement(sql); + stmt.setInt(1, ta.getNumber()); + stmt.setInt(2, teamId); + stmt.execute(); + return true; + } catch (SQLException e) { + logger.severe("SQL Exception while inserting TeachingAssistant.\n" + e.getMessage()); + e.printStackTrace(); + return false; + } + } + + /** + * Stores a team in the database. + * @param team The team to store. + * @param type The type of team that this is. + * @return True if successful, false otherwise. + */ + public boolean storeTeam(Team team, int type) { + try { + String sql = "INSERT INTO teams (id, team_type_id) VALUES (?, ?);"; + PreparedStatement stmt = this.connection.prepareStatement(sql); + stmt.setInt(1, team.getId()); + stmt.setInt(2, type); + stmt.execute(); + return true; + } catch (SQLException e) { + logger.severe("SQLException while inserting team: " + team + '\n' + e.getMessage()); + return false; + } + } + + /** + * Stores a list of student teams in the database. + * @param teams The list of teams to store. + * @return True if successful, or false if an error occurred. + */ + public boolean storeStudentTeams(List teams) { + for (StudentTeam team : teams) { + if (!this.storeTeam(team, TEAM_TYPE_STUDENT)) { + return false; + } + for (Student student : team.getStudents()) { + if (!this.storeStudent(student, team.getId())) { + return false; + } + } + } + return true; + } + + /** + * Stores a student without a team. + * @param student The student to store. + * @return True if successful, false otherwise. + */ + public boolean storeStudent(Student student) { + return this.storeStudent(student, TEAM_NONE); + } + + /** + * Stores a student in the database. + * @param student The student to store. + * @param teamId The team id for the team the student is in. + * @return True if the operation was successful, false otherwise. + */ + public boolean storeStudent(Student student, int teamId) { + logger.finest("Storing student: " + student); + if (!storePerson(student, PERSON_TYPE_STUDENT)) { + return false; + } + try { + String sql = "INSERT INTO students (person_id, team_id, chose_partner) VALUES (?, ?, ?);"; + PreparedStatement stmt = this.connection.prepareStatement(sql); + stmt.setInt(1, student.getNumber()); + stmt.setInt(2, teamId); + stmt.setInt(3, student.getPreferredPartners().size() > 0 ? 1 : 0); + stmt.execute(); + // Storing partners. + String sqlPartner = "INSERT INTO student_preferred_partners (student_id, partner_id) VALUES (?, ?);"; + PreparedStatement stmtPartner = this.connection.prepareStatement(sqlPartner); + for (int partnerId : student.getPreferredPartners()) { + stmtPartner.setInt(1, student.getNumber()); + stmtPartner.setInt(2, partnerId); + stmtPartner.execute(); + } + return true; + } catch (SQLException e) { + logger.severe("SQL Exception while inserting Student into database.\n" + e.getMessage()); + e.printStackTrace(); + return false; + } + } + +} diff --git a/src/main/java/nl/andrewlalis/model/database/Utils.java b/src/main/java/nl/andrewlalis/model/database/Utils.java new file mode 100644 index 0000000..e908f1d --- /dev/null +++ b/src/main/java/nl/andrewlalis/model/database/Utils.java @@ -0,0 +1,52 @@ +package nl.andrewlalis.model.database; + +import nl.andrewlalis.util.FileUtils; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Logger; + +/** + * Contains some methods which make database actions much easier. + */ +public class Utils { + + /** + * The logger for outputting debug info. + */ + private static final Logger logger = Logger.getLogger(Utils.class.getName()); + static { + logger.setParent(Logger.getGlobal()); + } + + /** + * Gets an ordered list of prepared statements from a file which contains multiple statements separated by a + * semicolon. This method separates those statements into their own strings, and prepares them individually to be + * executed later. + * @param filename The name of the file which contains the statements. + * @param connection The connection to a database; used to prepare statements. + * @return An ordered list of prepared statements which are based on the contents of the file provided. + */ + public static List prepareStatementsFromFile(String filename, Connection connection) { + String string = FileUtils.readStringFromFile(filename); + if (string == null || string.isEmpty()) { + return new ArrayList<>(); + } + String[] splits = string.split(";"); + List statements = new ArrayList<>(); + for (String split : splits) { + if (split.trim().length() > 1) { + try { + statements.add(connection.prepareStatement(split)); + } catch (SQLException e) { + logger.severe("SQLException while preparing a statement:\n" + split + "\nError Code: " + e.getErrorCode() + '\n' + e.getMessage()); + } + } + } + return statements; + } + +} diff --git a/src/main/java/nl/andrewlalis/model/package-info.java b/src/main/java/nl/andrewlalis/model/package-info.java new file mode 100644 index 0000000..d8465c9 --- /dev/null +++ b/src/main/java/nl/andrewlalis/model/package-info.java @@ -0,0 +1,7 @@ +/** + * Contains all objects which form the conceptual basis for this application, such as Students, Teams, and any other + * abstract data containers. + * + * @author Andrew Lalis + */ +package nl.andrewlalis.model; \ No newline at end of file diff --git a/src/main/java/nl/andrewlalis/ui/control/OutputTextHandler.java b/src/main/java/nl/andrewlalis/ui/control/OutputTextHandler.java new file mode 100644 index 0000000..875c148 --- /dev/null +++ b/src/main/java/nl/andrewlalis/ui/control/OutputTextHandler.java @@ -0,0 +1,45 @@ +package nl.andrewlalis.ui.control; + +import nl.andrewlalis.ui.view.OutputTextPane; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.logging.Handler; +import java.util.logging.LogRecord; + +/** + * A custom handler for printing log messages to the user interface text output pane. + */ +public class OutputTextHandler extends Handler { + + /** + * The pane to which this handler writes. + */ + private OutputTextPane outputPane; + + public OutputTextHandler(OutputTextPane outputPane) { + this.outputPane = outputPane; + } + + @Override + public void publish(LogRecord logRecord) { + DateFormat df = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss"); + String dateString = df.format(new Date(logRecord.getMillis())); + String sourceLocationString = logRecord.getSourceClassName() + "::" + logRecord.getSourceMethodName(); + this.outputPane.printStyled(dateString + ' ', "gray_italics"); + this.outputPane.printStyled(logRecord.getLevel().getName() + ": ", "bold"); + this.outputPane.printStyled(sourceLocationString + "\n\t", "bold"); + this.outputPane.printStyled(logRecord.getMessage() + '\n', "smaller"); + } + + @Override + public void flush() { + + } + + @Override + public void close() throws SecurityException { + + } +} diff --git a/src/main/java/nl/andrewlalis/ui/control/command/CommandExecutor.java b/src/main/java/nl/andrewlalis/ui/control/command/CommandExecutor.java new file mode 100644 index 0000000..dd5d84a --- /dev/null +++ b/src/main/java/nl/andrewlalis/ui/control/command/CommandExecutor.java @@ -0,0 +1,73 @@ +package nl.andrewlalis.ui.control.command; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Logger; + +/** + * Manages parsing an entered string and executing a task based upon information in the command. + */ +public class CommandExecutor { + + /** + * The logger for outputting debug info. + */ + private static final Logger logger = Logger.getLogger(CommandExecutor.class.getName()); + static { + logger.setParent(Logger.getGlobal()); + } + + /** + * A list of named commands which can be executed. + */ + private Map commands; + + public CommandExecutor() { + this.commands = new HashMap<>(); + } + + /** + * Adds a new command to the list of commands which this executor can handle. + * @param commandName The name that the command will be found by. + * @param executable The executable command that is bound to the given name. + */ + public void registerCommand(String commandName, Executable executable) { + this.commands.put(commandName, executable); + logger.fine("Registered command: " + commandName); + } + + /** + * Attempts to execute a command string, or show an error message if an invalid command or argument was entered. + * @param commandString The String command and any arguments that go with it. + */ + public void executeString(String commandString) { + String[] words = commandString.trim().split(" "); + if (words.length < 1) { + logger.warning("No command supplied."); + return; + } + String commandName = words[0]; + String[] args = new String[words.length - 1]; + if (words.length > 1) { + System.arraycopy(words, 1, args, 0, words.length - 1); + } + this.executeCommand(commandName, args); + } + + /** + * Executes a command with the given name, and given arguments. + * @param commandName The name of the command. A command must be registered using registerCommand before it can be + * called here. + * @param args The list of arguments to provide to the command as needed by the executable that was registered. + */ + public void executeCommand(String commandName, String[] args) { + if (this.commands.containsKey(commandName)) { + logger.info(commandName + ' ' + Arrays.toString(args)); + this.commands.get(commandName).execute(args); + } else { + logger.warning(commandName + " is not a valid command."); + } + } + +} diff --git a/src/main/java/nl/andrewlalis/ui/control/command/Executable.java b/src/main/java/nl/andrewlalis/ui/control/command/Executable.java new file mode 100644 index 0000000..77ed4bd --- /dev/null +++ b/src/main/java/nl/andrewlalis/ui/control/command/Executable.java @@ -0,0 +1,17 @@ +package nl.andrewlalis.ui.control.command; + +/** + * Classes which implement this interface tell that they may be 'executed', either via command-line, or through the use + * of user interface actions. + */ +public interface Executable { + + /** + * Runs this Executable's main functionality. + * + * @param args The list of arguments supplied to the executable. + * @return True if successful, false if an error occurred. + */ + boolean execute(String[] args); + +} diff --git a/src/main/java/nl/andrewlalis/ui/control/command/executables/ArchiveRepos.java b/src/main/java/nl/andrewlalis/ui/control/command/executables/ArchiveRepos.java new file mode 100644 index 0000000..5e43269 --- /dev/null +++ b/src/main/java/nl/andrewlalis/ui/control/command/executables/ArchiveRepos.java @@ -0,0 +1,28 @@ +package nl.andrewlalis.ui.control.command.executables; + +import nl.andrewlalis.git_api.GithubManager; + +import java.io.IOException; + +/** + * Represents the action archive all repositories with a certain substring in their name. + * It takes the following arguments: + * + * 1. Repo substring to archive by + */ +public class ArchiveRepos extends GithubExecutable { + + @Override + protected boolean executeWithManager(GithubManager manager, String[] args) { + if (args.length < 1) { + return false; + } + try { + manager.archiveAllRepositories(args[0]); + return true; + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } +} diff --git a/src/main/java/nl/andrewlalis/ui/control/command/executables/GenerateAssignmentsRepo.java b/src/main/java/nl/andrewlalis/ui/control/command/executables/GenerateAssignmentsRepo.java new file mode 100644 index 0000000..2ccb06b --- /dev/null +++ b/src/main/java/nl/andrewlalis/ui/control/command/executables/GenerateAssignmentsRepo.java @@ -0,0 +1,30 @@ +package nl.andrewlalis.ui.control.command.executables; + +import nl.andrewlalis.git_api.GithubManager; + +import java.io.IOException; + +/** + * Generates the assignments repository, with the supplied github manager, as well as the following extra arguments: + * + * 1. The name of the repository. + * 2. Description of the repository. + * 3. Name of TA team containing all members. + */ +public class GenerateAssignmentsRepo extends GithubExecutable { + + @Override + protected boolean executeWithManager(GithubManager manager, String[] args) { + if (args.length < 3) { + return false; + } + try { + manager.setupAssignmentsRepo(args[0], args[1], args[2]); + return true; + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } + +} diff --git a/src/main/java/nl/andrewlalis/ui/control/command/executables/GenerateStudentRepos.java b/src/main/java/nl/andrewlalis/ui/control/command/executables/GenerateStudentRepos.java new file mode 100644 index 0000000..41f7975 --- /dev/null +++ b/src/main/java/nl/andrewlalis/ui/control/command/executables/GenerateStudentRepos.java @@ -0,0 +1,12 @@ +package nl.andrewlalis.ui.control.command.executables; + +import nl.andrewlalis.git_api.GithubManager; + +public class GenerateStudentRepos extends GithubExecutable { + + @Override + protected boolean executeWithManager(GithubManager manager, String[] args) { + return false; + } + +} diff --git a/src/main/java/nl/andrewlalis/ui/control/command/executables/GithubExecutable.java b/src/main/java/nl/andrewlalis/ui/control/command/executables/GithubExecutable.java new file mode 100644 index 0000000..e7362bd --- /dev/null +++ b/src/main/java/nl/andrewlalis/ui/control/command/executables/GithubExecutable.java @@ -0,0 +1,34 @@ +package nl.andrewlalis.ui.control.command.executables; + +import nl.andrewlalis.git_api.GithubManager; +import nl.andrewlalis.ui.control.command.Executable; + +/** + * Represents an executable which interacts with github, and therefore needs access to a Github + * manager to execute. + * + * Requires two arguments: + * 1. The organization name. + * 2. The organization's access token. + */ +public abstract class GithubExecutable implements Executable { + + @Override + public boolean execute(String[] args) { + if (args.length < 2) { + return false; + } + String[] extraArgs = new String[args.length-2]; + System.arraycopy(args, 2, extraArgs, 0, args.length-2); + GithubManager manager = new GithubManager(args[0], args[1]); + return this.executeWithManager(manager, extraArgs); + } + + /** + * Executes a command and provides a github manager with which to perform operations. + * @param manager The GithubManager used to perform actions on the repositories. + * @param args Any additional arguments provided to the executable. + * @return True if successful, or false otherwise. + */ + protected abstract boolean executeWithManager(GithubManager manager, String[] args); +} diff --git a/src/main/java/nl/andrewlalis/ui/control/command/executables/ReadStudentsFileToDB.java b/src/main/java/nl/andrewlalis/ui/control/command/executables/ReadStudentsFileToDB.java new file mode 100644 index 0000000..2d3ddc3 --- /dev/null +++ b/src/main/java/nl/andrewlalis/ui/control/command/executables/ReadStudentsFileToDB.java @@ -0,0 +1,40 @@ +package nl.andrewlalis.ui.control.command.executables; + +import nl.andrewlalis.model.StudentTeam; +import nl.andrewlalis.model.database.Database; +import nl.andrewlalis.ui.control.command.Executable; +import nl.andrewlalis.util.FileUtils; + +import java.util.List; + +/** + * Execute this class to read students from a supplied filename and teamsize, and store their + * information in the database. + * Requires the following arguments: + * + * 1. filename + * 2. teamsize + */ +public class ReadStudentsFileToDB implements Executable { + + /** + * The database used to store the students. + */ + private Database db; + + public ReadStudentsFileToDB(Database db) { + this.db = db; + } + + + @Override + public boolean execute(String[] args) { + if (args.length < 2) { + return false; + } + String filename = args[0]; + int teamSize = Integer.parseUnsignedInt(args[1]); + List teams = FileUtils.getStudentTeamsFromCSV(filename, teamSize); + return this.db.storeStudentTeams(teams); + } +} diff --git a/src/main/java/nl/andrewlalis/ui/control/listeners/ArchiveAllListener.java b/src/main/java/nl/andrewlalis/ui/control/listeners/ArchiveAllListener.java new file mode 100644 index 0000000..6d3cbf2 --- /dev/null +++ b/src/main/java/nl/andrewlalis/ui/control/listeners/ArchiveAllListener.java @@ -0,0 +1,29 @@ +package nl.andrewlalis.ui.control.listeners; + +import nl.andrewlalis.ui.control.command.CommandExecutor; +import nl.andrewlalis.ui.view.InitializerApp; + +import javax.swing.*; +import java.awt.event.ActionEvent; + +/** + * Listens for when the user performs an action with the intent to archive all repositories. + */ +public class ArchiveAllListener extends ExecutableListener { + + public ArchiveAllListener(CommandExecutor executor, InitializerApp app) { + super(executor, app); + } + + @Override + public void actionPerformed(ActionEvent actionEvent) { + String response = JOptionPane.showInputDialog(this.app, "Enter a substring to archive repositories by.", "Enter a substring", JOptionPane.QUESTION_MESSAGE); + if (response != null) { + this.executor.executeCommand("archiveall", new String[]{ + this.app.getOrganizationName(), + this.app.getAccessToken(), + response + }); + } + } +} diff --git a/src/main/java/nl/andrewlalis/ui/control/listeners/CommandFieldKeyListener.java b/src/main/java/nl/andrewlalis/ui/control/listeners/CommandFieldKeyListener.java new file mode 100644 index 0000000..d68ac1e --- /dev/null +++ b/src/main/java/nl/andrewlalis/ui/control/listeners/CommandFieldKeyListener.java @@ -0,0 +1,42 @@ +package nl.andrewlalis.ui.control.listeners; + +import nl.andrewlalis.ui.control.command.CommandExecutor; + +import javax.swing.*; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; + +/** + * This Key Listener listens for when the ENTER key is pressed in the command-line text field, and executes the command + * when that is the case. + */ +public class CommandFieldKeyListener implements KeyListener { + + /** + * This is responsible for parsing and running entered commands. + */ + private CommandExecutor executor; + + public CommandFieldKeyListener(CommandExecutor executor) { + this.executor = executor; + } + + @Override + public void keyTyped(KeyEvent keyEvent) { + + } + + @Override + public void keyPressed(KeyEvent keyEvent) { + + } + + @Override + public void keyReleased(KeyEvent keyEvent) { + if (keyEvent.getKeyCode() == KeyEvent.VK_ENTER) { + JTextField inputField = (JTextField) keyEvent.getComponent(); + this.executor.executeString(inputField.getText()); + inputField.setText(null); + } + } +} diff --git a/src/main/java/nl/andrewlalis/ui/control/listeners/ExecutableListener.java b/src/main/java/nl/andrewlalis/ui/control/listeners/ExecutableListener.java new file mode 100644 index 0000000..33401c0 --- /dev/null +++ b/src/main/java/nl/andrewlalis/ui/control/listeners/ExecutableListener.java @@ -0,0 +1,29 @@ +package nl.andrewlalis.ui.control.listeners; + +import nl.andrewlalis.ui.control.command.CommandExecutor; +import nl.andrewlalis.ui.view.InitializerApp; + +import java.awt.event.ActionListener; + +/** + * An action listener which is pre-set to execute an executable once an action is performed. + * Since these are used for the user interface, an instance of the application is passed, for the purpose of providing + * a parent component for many popups, and to have access to input fields. + */ +public abstract class ExecutableListener implements ActionListener { + + /** + * The executor, with some registered commands that will be executed by listeners which extend this one. + */ + protected CommandExecutor executor; + + /** + * An instance of the UI application. + */ + protected InitializerApp app; + + public ExecutableListener(CommandExecutor executor, InitializerApp app) { + this.executor = executor; + this.app = app; + } +} diff --git a/src/main/java/nl/andrewlalis/ui/control/listeners/GenerateAssignmentsRepoListener.java b/src/main/java/nl/andrewlalis/ui/control/listeners/GenerateAssignmentsRepoListener.java new file mode 100644 index 0000000..6143821 --- /dev/null +++ b/src/main/java/nl/andrewlalis/ui/control/listeners/GenerateAssignmentsRepoListener.java @@ -0,0 +1,36 @@ +package nl.andrewlalis.ui.control.listeners; + +import nl.andrewlalis.ui.control.command.CommandExecutor; +import nl.andrewlalis.ui.view.InitializerApp; + +import javax.swing.*; +import java.awt.event.ActionEvent; + +/** + * Listens for when the user does an action to generate the assignments repository. + */ +public class GenerateAssignmentsRepoListener extends ExecutableListener { + + public GenerateAssignmentsRepoListener(CommandExecutor executor, InitializerApp app) { + super(executor, app); + } + + @Override + public void actionPerformed(ActionEvent actionEvent) { + String repoName = JOptionPane.showInputDialog(this.app, "Enter a name for the assignments repository.", "Repository Name", JOptionPane.QUESTION_MESSAGE); + if (repoName != null) { + String description = JOptionPane.showInputDialog(this.app, "Enter a description for the repository.", "Repository Description", JOptionPane.QUESTION_MESSAGE); + String teamName = JOptionPane.showInputDialog(this.app, "Enter the name of the TA team containing all teaching assistants.", "TA Team Name", JOptionPane.QUESTION_MESSAGE); + if (teamName != null) { + this.executor.executeCommand("generateassignments", new String[]{ + this.app.getOrganizationName(), + this.app.getAccessToken(), + repoName, + description, + teamName + }); + } + } + } + +} diff --git a/src/main/java/nl/andrewlalis/ui/control/listeners/ReadStudentsFileListener.java b/src/main/java/nl/andrewlalis/ui/control/listeners/ReadStudentsFileListener.java new file mode 100644 index 0000000..788c431 --- /dev/null +++ b/src/main/java/nl/andrewlalis/ui/control/listeners/ReadStudentsFileListener.java @@ -0,0 +1,33 @@ +package nl.andrewlalis.ui.control.listeners; + +import nl.andrewlalis.ui.control.command.CommandExecutor; +import nl.andrewlalis.ui.view.InitializerApp; + +import javax.swing.*; +import java.awt.event.ActionEvent; + +/** + * Listens for when the user performs an action to read all students from a file, and output the contents to a database. + */ +public class ReadStudentsFileListener extends ExecutableListener { + + public ReadStudentsFileListener(CommandExecutor executor, InitializerApp app) { + super(executor, app); + } + + @Override + public void actionPerformed(ActionEvent actionEvent) { + JFileChooser chooser = new JFileChooser(); + int fileResponse = chooser.showOpenDialog(this.app); + + if (fileResponse == JFileChooser.APPROVE_OPTION) { + String teamSizeString = JOptionPane.showInputDialog(this.app, "Enter the student team size.", "Team Size", JOptionPane.QUESTION_MESSAGE); + if (teamSizeString != null) { + this.executor.executeCommand("readstudents", new String[]{ + chooser.getSelectedFile().getName(), + teamSizeString + }); + } + } + } +} diff --git a/src/main/java/nl/andrewlalis/ui/view/InitializerApp.java b/src/main/java/nl/andrewlalis/ui/view/InitializerApp.java new file mode 100644 index 0000000..2ff6c2a --- /dev/null +++ b/src/main/java/nl/andrewlalis/ui/view/InitializerApp.java @@ -0,0 +1,203 @@ +package nl.andrewlalis.ui.view; + +import nl.andrewlalis.ui.control.OutputTextHandler; +import nl.andrewlalis.ui.control.command.CommandExecutor; +import nl.andrewlalis.ui.control.command.executables.ArchiveRepos; +import nl.andrewlalis.ui.control.listeners.ArchiveAllListener; +import nl.andrewlalis.ui.control.listeners.CommandFieldKeyListener; +import nl.andrewlalis.ui.control.listeners.GenerateAssignmentsRepoListener; +import nl.andrewlalis.ui.control.listeners.ReadStudentsFileListener; + +import javax.swing.*; +import java.awt.*; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents the main user interface element that is referenced in main to construct the graphic interface. + */ +public class InitializerApp extends JFrame { + + /** + * The window title. + */ + private static final String FRAME_TITLE = "Github Initializer"; + + /** + * A default size of the window on startup. + */ + private static final Dimension SIZE = new Dimension(1000, 600); + + /** + * The pane on which general purpose program output is written. + */ + private OutputTextPane outputTextPane; + + private JTextField organizationField = new JTextField(); + private JTextField accessTokenField = new JTextField(); + private JTextField assignmentsRepoField = new JTextField(); + private JTextField teachingAssistantsField = new JTextField(); + private JTextField studentRepoField = new JTextField(); + + /** + * The executor responsible for performing meaningful actions. + */ + private CommandExecutor executor; + + public InitializerApp(CommandExecutor executor) { + this.executor = executor; + + // UI initialization. + this.initFrame(); + } + + /** + * Begins showing the application + */ + public void begin() { + this.pack(); + this.setVisible(true); + } + + /** + * Initializes the handler which passes logging information to the text pane for display. + */ + private void initLoggingHandler() { + Logger logger = Logger.getGlobal(); + OutputTextHandler handler = new OutputTextHandler(this.outputTextPane); + handler.setLevel(Level.FINE); + logger.addHandler(handler); + } + + /** + * Initializes the frame before display should begin. + */ + private void initFrame() { + this.setTitle(FRAME_TITLE); + this.setDefaultCloseOperation(DISPOSE_ON_CLOSE); + this.setPreferredSize(SIZE); + + JPanel mainPanel = new JPanel(new BorderLayout()); + + mainPanel.add(this.initCommandPanel(), BorderLayout.CENTER); + mainPanel.add(this.initRepoPanel(), BorderLayout.WEST); + mainPanel.add(this.initGithubManagerPanel(), BorderLayout.EAST); + + this.setContentPane(mainPanel); + + this.initLoggingHandler(); + } + + /** + * @return A JPanel containing input for all fields needed to connect to github, plus some commonly used buttons + * which perform actions, as shortcuts for command actions. + */ + private JPanel initGithubManagerPanel() { + JPanel githubManagerPanel = new JPanel(new BorderLayout()); + + // Information input (org name, key, etc.) + JPanel infoInputPanel = new JPanel(); + infoInputPanel.setLayout(new BoxLayout(infoInputPanel, BoxLayout.PAGE_AXIS)); + + infoInputPanel.add(generateTextFieldPanel("Organization Name", this.organizationField)); + this.organizationField.setText("InitializerTesting"); + infoInputPanel.add(generateTextFieldPanel("Access Token", this.accessTokenField)); + this.accessTokenField.setText("haha get your own"); + infoInputPanel.add(generateTextFieldPanel("Assignments Repo Name", this.assignmentsRepoField)); + this.assignmentsRepoField.setText("assignments_2018"); + infoInputPanel.add(generateTextFieldPanel("TA-All Team Name", this.teachingAssistantsField)); + this.teachingAssistantsField.setText("teaching-assistants"); + infoInputPanel.add(generateTextFieldPanel("Student Repo Prefix", this.studentRepoField)); + this.studentRepoField.setText("advoop_2018"); + + githubManagerPanel.add(infoInputPanel, BorderLayout.NORTH); + + // Common actions panel. + JPanel commonActionsPanel = new JPanel(); + commonActionsPanel.setLayout(new BoxLayout(commonActionsPanel, BoxLayout.PAGE_AXIS)); + + JButton archiveAllButton = new JButton("Archive All"); + archiveAllButton.addActionListener(new ArchiveAllListener(this.executor, this)); + commonActionsPanel.add(archiveAllButton); + + JButton generateStudentTeamsButton = new JButton("Read teams from file"); + generateStudentTeamsButton.addActionListener(new ReadStudentsFileListener(this.executor, this)); + commonActionsPanel.add(generateStudentTeamsButton); + + JButton generateAssignmentsRepoButton = new JButton("Generate Assignments Repo"); + generateAssignmentsRepoButton.addActionListener(new GenerateAssignmentsRepoListener(this.executor, this)); + commonActionsPanel.add(generateAssignmentsRepoButton); + + githubManagerPanel.add(commonActionsPanel, BorderLayout.CENTER); + + return githubManagerPanel; + } + + /** + * @return A JPanel containing the command prompt field and output text pane. + */ + private JPanel initCommandPanel() { + JPanel commandPanel = new JPanel(new BorderLayout()); + // Output text pane. + this.outputTextPane = new OutputTextPane(); + JScrollPane scrollPane = new JScrollPane(this.outputTextPane); + scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + // Text enter field and label. + JPanel textEnterPanel = new JPanel(new BorderLayout()); + textEnterPanel.setBorder(BorderFactory.createLoweredBevelBorder()); + textEnterPanel.add(new JLabel("Command:"), BorderLayout.WEST); + JTextField commandField = new JTextField(); + commandField.addKeyListener(new CommandFieldKeyListener(this.executor)); + textEnterPanel.add(commandField, BorderLayout.CENTER); + // Top Label + JLabel commandPanelLabel = new JLabel("Program output", SwingConstants.CENTER); + + commandPanel.add(scrollPane, BorderLayout.CENTER); + commandPanel.add(textEnterPanel, BorderLayout.SOUTH); + commandPanel.add(commandPanelLabel, BorderLayout.NORTH); + return commandPanel; + } + + /** + * @return A JPanel containing a list of repositories. + */ + private JPanel initRepoPanel() { + JPanel repoPanel = new JPanel(); + repoPanel.setLayout(new BoxLayout(repoPanel, BoxLayout.PAGE_AXIS)); + repoPanel.add(new JLabel("Repositories")); + repoPanel.add(new JList()); + return repoPanel; + } + + /** + * Generates a text input field panel. + * @param labelText The text for the label above the panel. + * @param textField A reference to the text field that is used in the panel. + * @return A JPanel containing the label and text field. + */ + private JPanel generateTextFieldPanel(String labelText, JTextField textField) { + JPanel newPanel = new JPanel(new BorderLayout()); + newPanel.add(new JLabel(labelText), BorderLayout.NORTH); + newPanel.add(textField); + newPanel.setBorder(BorderFactory.createEmptyBorder(5, 2, 5, 2)); + return newPanel; + } + + /** + * Gets the organization name entered in the relevant field. + * @return The organization name the user has entered. + */ + public String getOrganizationName() { + return this.organizationField.getText().trim(); + } + + /** + * Gets the oauth access token entered in the relevant field. + * @return The access token the user has entered. + */ + public String getAccessToken() { + return this.accessTokenField.getText().trim(); + } + +} diff --git a/src/main/java/nl/andrewlalis/ui/view/OutputTextPane.java b/src/main/java/nl/andrewlalis/ui/view/OutputTextPane.java new file mode 100644 index 0000000..64569cd --- /dev/null +++ b/src/main/java/nl/andrewlalis/ui/view/OutputTextPane.java @@ -0,0 +1,83 @@ +package nl.andrewlalis.ui.view; + +import javax.swing.*; +import javax.swing.text.BadLocationException; +import javax.swing.text.Style; +import javax.swing.text.StyleConstants; +import javax.swing.text.StyledDocument; +import java.awt.*; +import java.util.HashMap; +import java.util.Map; + +/** + * A custom JTextPane object which provides easy methods to print to the screen. + */ +public class OutputTextPane extends JTextPane { + + /** + * A list of named styles. + */ + private Map styles; + + /** + * A basic constructor to set default properties on this text pane. + */ + public OutputTextPane() { + this.initStyles(); + this.setEditable(false); + this.setAutoscrolls(true); + } + + private void initStyles() { + this.styles = new HashMap<>(); + Style defaultStyle = this.addStyle("default", null); + defaultStyle.addAttribute(StyleConstants.FontFamily, "Lucida Consonle"); + defaultStyle.addAttribute(StyleConstants.FontSize, 12); + this.styles.put("default", defaultStyle); + + Style grayItalics = this.addStyle("gray_italics", defaultStyle); + grayItalics.addAttribute(StyleConstants.Foreground, new Color(128, 128, 128)); + grayItalics.addAttribute(StyleConstants.Italic, true); + this.styles.put("gray_italics", grayItalics); + + Style bold = this.addStyle("bold", defaultStyle); + bold.addAttribute(StyleConstants.Bold, true); + this.styles.put("bold", bold); + + Style smaller = this.addStyle("smaller", defaultStyle); + smaller.addAttribute(StyleConstants.FontSize, 11); + this.styles.put("smaller", smaller); + } + + /** + * Prints some text styled with a style that is defined in initStyles. + * @param text The text to display. + * @param styleName The name of the style to use. + */ + public void printStyled(String text, String styleName) { + StyledDocument doc = this.getStyledDocument(); + Style style = this.styles.get(styleName); + if (style == null) { + style = this.styles.get("default"); + } + try { + doc.insertString(doc.getLength(), text, style); + } catch (BadLocationException e) { + e.printStackTrace(); + } + } + + /** + * Prints a line of text, with a newline character appended at the bottom. + * @param text The text to append. + */ + public void printLine(String text) { + StyledDocument doc = this.getStyledDocument(); + try { + doc.insertString(doc.getLength(), (text + '\n'), null); + } catch (BadLocationException e) { + e.printStackTrace(); + } + } + +} diff --git a/src/main/java/nl/andrewlalis/util/CommandLine.java b/src/main/java/nl/andrewlalis/util/CommandLine.java index 8e4e206..1075d9e 100644 --- a/src/main/java/nl/andrewlalis/util/CommandLine.java +++ b/src/main/java/nl/andrewlalis/util/CommandLine.java @@ -67,7 +67,7 @@ public class CommandLine { options.addOption(organizationInput); // The maximum team size. - Option teamSizeInput = new Option("s", "teamsize", true, "The maximum size of teams to generate."); + Option teamSizeInput = new Option("ts", "teamsize", true, "The maximum size of teams to generate."); teamSizeInput.setRequired(false); options.addOption(teamSizeInput); diff --git a/src/main/java/nl/andrewlalis/util/FileUtils.java b/src/main/java/nl/andrewlalis/util/FileUtils.java new file mode 100644 index 0000000..972ffb1 --- /dev/null +++ b/src/main/java/nl/andrewlalis/util/FileUtils.java @@ -0,0 +1,62 @@ +package nl.andrewlalis.util; + +import nl.andrewlalis.model.StudentTeam; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.List; +import java.util.logging.Logger; + +/** + * Contains some methods which come in handy in lots of other places. + */ +public class FileUtils { + + /** + * The logger for outputting debug info. + */ + private static final Logger logger = Logger.getLogger(FileUtils.class.getName()); + static { + logger.setParent(Logger.getGlobal()); + } + + /** + * Reads the contents of the file specified by the filename into a String. + * @param filename The filename to read the file of, either relative or absolute. + * @return A string containing the file's contents. + */ + public static String readStringFromFile(String filename) { + try (BufferedReader r = new BufferedReader(new InputStreamReader(FileUtils.class.getResourceAsStream(filename)))) { + StringBuilder sb = new StringBuilder(); + String line; + while ((line = r.readLine()) != null) { + sb.append(line).append('\n'); + } + return sb.toString(); + } catch (IOException e) { + e.printStackTrace(); + return null; + } + } + + /** + * Reads a list of students from a CSV file and compiles a list of teams based on their preferred partners. + * @param filename The name of the CSV file. + * @param teamSize The intended size of teams. + * @return A list of student teams. + */ + public static List getStudentTeamsFromCSV(String filename, int teamSize) { + List studentTeams; + try { + studentTeams = TeamGenerator.generateFromCSV(filename, teamSize); + logger.fine("Teams created:\n" + studentTeams); + return studentTeams; + } catch (IOException | ArrayIndexOutOfBoundsException e) { + logger.severe("Unable to generate studentTeams from CSV file, exiting. " + e.getMessage()); + System.exit(1); + return null; + } + } + +} diff --git a/src/main/java/nl/andrewlalis/util/Logging.java b/src/main/java/nl/andrewlalis/util/Logging.java index 625ef1e..9161693 100644 --- a/src/main/java/nl/andrewlalis/util/Logging.java +++ b/src/main/java/nl/andrewlalis/util/Logging.java @@ -14,17 +14,22 @@ public class Logging { public static void setup(boolean verbose) throws IOException { Logger logger = Logger.getGlobal(); - if (verbose) { - logger.setLevel(Level.FINEST); - } else { - logger.setLevel(Level.INFO); - } - - outputFile = new FileHandler("log/latest.txt"); + outputFile = new FileHandler("log/latest.log"); formatter = new SimpleFormatter(); outputFile.setFormatter(formatter); + outputFile.setLevel(Level.FINEST); + + if (verbose) { + Handler systemOut = new ConsoleHandler(); + systemOut.setLevel(Level.ALL); + + logger.addHandler(systemOut); + } logger.addHandler(outputFile); + logger.setLevel(Level.ALL); + + Logger.getLogger("").setLevel(Level.OFF); } diff --git a/src/main/java/nl/andrewlalis/util/TeamGenerator.java b/src/main/java/nl/andrewlalis/util/TeamGenerator.java index fb99e71..b765e7b 100644 --- a/src/main/java/nl/andrewlalis/util/TeamGenerator.java +++ b/src/main/java/nl/andrewlalis/util/TeamGenerator.java @@ -1,7 +1,7 @@ package nl.andrewlalis.util; import nl.andrewlalis.model.Student; -import nl.andrewlalis.model.Team; +import nl.andrewlalis.model.StudentTeam; import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVRecord; @@ -32,11 +32,11 @@ public class TeamGenerator { * @throws IOException If the file is unable to be read. * @throws IllegalArgumentException If an invalid teamsize is given. */ - public static List generateFromCSV(String filename, int teamSize) throws IOException, IllegalArgumentException { - logger.info("Generating teams of size " + teamSize); + public static List generateFromCSV(String filename, int teamSize) throws IOException, IllegalArgumentException { + logger.fine("Generating teams of size " + teamSize); if (teamSize < 1) { logger.severe("Invalid team size."); - throw new IllegalArgumentException("Team size must be greater than or equal to 1. Got " + teamSize); + throw new IllegalArgumentException("StudentTeam size must be greater than or equal to 1. Got " + teamSize); } logger.fine("Parsing CSV file."); Iterable records = CSVFormat.DEFAULT.withFirstRecordAsHeader().parse(new FileReader(filename)); @@ -46,8 +46,8 @@ public class TeamGenerator { try { studentMap = readAllStudents(records, teamSize); } catch (ArrayIndexOutOfBoundsException e) { - logger.severe("Team size does not match column count in records."); - throw new IllegalArgumentException("Team size does not match column count in records."); + logger.severe("StudentTeam size does not match column count in records."); + throw new IllegalArgumentException("StudentTeam size does not match column count in records."); } @@ -72,28 +72,38 @@ public class TeamGenerator { * @param teamSize The preferred maximum size for a team. * @return A list of teams, most of which are of teamSize size. */ - private static List generateAllValidTeams(Map studentMap, int teamSize) { + private static List generateAllValidTeams(Map studentMap, int teamSize) { List singleStudents = new ArrayList<>(studentMap.values()); - List teams = new ArrayList<>(); + List studentTeams = new ArrayList<>(); int teamCount = 1; // For each student, try to make a team from its preferred partners. for (Map.Entry e : studentMap.entrySet()) { - Team t = e.getValue().getPreferredTeam(studentMap); - logger.finest("Checking if student's preferred team is valid: " + t.getStudents()); + StudentTeam newTeam = e.getValue().getPreferredTeam(studentMap); + logger.finest("Checking if student's preferred team is valid:\n" + newTeam); // Check if the team is of a valid size, and is not a duplicate. - // Note that at this stage, singles are treated as teams of 1, and thus not valid for any teamSize > 1. - if (t.isValid(teamSize) && !teams.contains(t)) { - // Once we know this team is completely valid, we remove all the students in it from the list of singles. - t.setId(teamCount++); - singleStudents.removeAll(t.getStudents()); - teams.add(t); - logger.fine("Created team: " + t); + // Note that at this stage, singles are treated as studentTeams of 1, and thus not valid for any teamSize > 1. + if (newTeam.isValid(teamSize)) { + // We know that the team is valid on its own, so now we check if it has members identical to any team already created. + boolean matchFound = false; + for (StudentTeam team : studentTeams) { + if (newTeam.hasSameMembers(team)) { + matchFound = true; + break; + } + } + if (!matchFound) { + // Once we know this team is completely valid, we remove all the students in it from the list of singles. + newTeam.setId(teamCount++); + singleStudents.removeAll(Arrays.asList(newTeam.getStudents())); + studentTeams.add(newTeam); + logger.fine("Created team:\n" + newTeam); + } } } - teams.addAll(mergeSingleStudents(singleStudents, teamSize, teamCount)); - return teams; + studentTeams.addAll(mergeSingleStudents(singleStudents, teamSize, teamCount)); + return studentTeams; } /** @@ -104,21 +114,21 @@ public class TeamGenerator { * @param teamIndex The current number used in assigning an id to the team. * @return A list of teams comprising of single students. */ - private static List mergeSingleStudents(List singleStudents, int teamSize, int teamIndex) { - List teams = new ArrayList<>(); + private static List mergeSingleStudents(List singleStudents, int teamSize, int teamIndex) { + List studentTeams = new ArrayList<>(); while (!singleStudents.isEmpty()) { - Team t = new Team(); + StudentTeam t = new StudentTeam(); t.setId(teamIndex++); - logger.fine("Creating new team of single students: " + t); - while (t.getStudentCount() < teamSize && !singleStudents.isEmpty()) { + logger.fine("Creating new team of single students:\n" + t); + while (t.memberCount() < teamSize && !singleStudents.isEmpty()) { Student s = singleStudents.remove(0); logger.finest("Single student: " + s); - t.addStudent(s); + t.addMember(s); } - teams.add(t); - logger.fine("Created team: " + t); + studentTeams.add(t); + logger.fine("Created team:\n" + t); } - return teams; + return studentTeams; } /** diff --git a/src/main/resources/sql/insert/types.sql b/src/main/resources/sql/insert/types.sql new file mode 100644 index 0000000..3940146 --- /dev/null +++ b/src/main/resources/sql/insert/types.sql @@ -0,0 +1,19 @@ +INSERT INTO person_types (id, name) +VALUES (0, 'student'), + (1, 'teaching-assistant'), + (2, 'professor'); + +INSERT INTO team_types (id, name) +VALUES (0, 'student_team'), + (1, 'teaching_assistant_team'), + (2, 'all_teaching_assistants'), + (3, 'none'); + +INSERT INTO teams (id, team_type_id) +VALUES (1000000, 3), -- None team for all students or TA's without a team. + (1000001, 2); -- Team for all teaching assistants. + +INSERT INTO error_types (id, name) +VALUES (0, 'team_error'), + (1, 'person_error'), + (2, 'system_error'); \ No newline at end of file diff --git a/src/main/resources/sql/table_init.sql b/src/main/resources/sql/table_init.sql new file mode 100644 index 0000000..e5ffa1d --- /dev/null +++ b/src/main/resources/sql/table_init.sql @@ -0,0 +1,140 @@ +PRAGMA foreign_keys = TRUE; +PRAGMA writable_schema = 1; +DELETE FROM sqlite_master WHERE type IN ('table', 'index', 'trigger'); +PRAGMA writable_schema = 0; +VACUUM; + +-- Basic schema design. +CREATE TABLE IF NOT EXISTS person_types ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE +); + +CREATE TABLE IF NOT EXISTS persons ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + email_address TEXT NOT NULL, + github_username TEXT NOT NULL UNIQUE, + person_type_id INTEGER NOT NULL, + FOREIGN KEY (person_type_id) + REFERENCES person_types(id) + ON DELETE CASCADE + ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS team_types ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE +); + +CREATE TABLE IF NOT EXISTS teams ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + team_type_id INTEGER NOT NULL, + FOREIGN KEY (team_type_id) + REFERENCES team_types(id) + ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS teaching_assistant_teams ( + team_id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + FOREIGN KEY (team_id) + REFERENCES teams(id) + ON DELETE CASCADE + ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS student_teams ( + team_id INTEGER PRIMARY KEY, + repository_name TEXT, + group_id INTEGER NOT NULL UNIQUE, + teaching_assistant_team_id INTEGER, + FOREIGN KEY (team_id) + REFERENCES teams(id) + ON DELETE CASCADE + ON UPDATE CASCADE, + FOREIGN KEY (teaching_assistant_team_id) + REFERENCES teaching_assistant_teams(team_id) + ON DELETE CASCADE + ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS students ( + person_id INTEGER PRIMARY KEY, + team_id INTEGER NOT NULL, + chose_partner INTEGER NOT NULL, + FOREIGN KEY (person_id) + REFERENCES persons(id) + ON DELETE CASCADE + ON UPDATE CASCADE, + FOREIGN KEY (team_id) + REFERENCES teams(id) + ON DELETE CASCADE + ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS student_preferred_partners ( + student_id INTEGER PRIMARY KEY, + partner_id INTEGER NOT NULL, + FOREIGN KEY (student_id) + REFERENCES students(person_id) + ON DELETE CASCADE + ON UPDATE CASCADE, + UNIQUE (student_id, partner_id) +); + +CREATE TABLE IF NOT EXISTS teaching_assistants ( + person_id INTEGER PRIMARY KEY, + team_id INTEGER NOT NULL, + FOREIGN KEY (person_id) + REFERENCES persons(id) + ON DELETE CASCADE + ON UPDATE CASCADE, + FOREIGN KEY (team_id) + REFERENCES teams(id) + ON DELETE CASCADE + ON UPDATE CASCADE +); + +-- Error queue storage. +CREATE TABLE IF NOT EXISTS error_types ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE +); + +CREATE TABLE IF NOT EXISTS errors ( + id INTEGER PRIMARY KEY, + timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + error_type_id INTEGER NOT NULL, + message TEXT NOT NULL, + FOREIGN KEY (error_type_id) + REFERENCES error_types(id) + ON DELETE CASCADE + ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS team_errors ( + error_id INTEGER PRIMARY KEY, + team_id INTEGER NOT NULL, + FOREIGN KEY (error_id) + REFERENCES errors(id) + ON DELETE CASCADE + ON UPDATE CASCADE, + FOREIGN KEY (team_id) + REFERENCES teams(id) + ON DELETE CASCADE + ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS person_errors ( + error_id INTEGER PRIMARY KEY, + person_id INTEGER NOT NULL, + FOREIGN KEY (error_id) + REFERENCES errors(id) + ON DELETE CASCADE + ON UPDATE CASCADE, + FOREIGN KEY (person_id) + REFERENCES persons(id) + ON DELETE CASCADE + ON UPDATE CASCADE +); \ No newline at end of file