Merge pull request #2 from rug-advoop/repo_creation

Half of features implemented
This commit is contained in:
Andrew Lalis 2018-08-28 20:26:27 +02:00 committed by GitHub
commit c14ba00107
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 2059 additions and 216 deletions

36
pom.xml
View File

@ -21,6 +21,10 @@
</build>
<packaging>jar</packaging>
<properties>
<jackson.version>2.9.6</jackson.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
@ -43,6 +47,38 @@
<version>16.0.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>RELEASE</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- Github API Object Library -->
<dependency>
<groupId>org.kohsuke</groupId>
<artifactId>github-api</artifactId>
<version>1.93</version>
</dependency>
<!-- SQLite JDBC Driver -->
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.23.1</version>
</dependency>
</dependencies>
</project>

View File

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

View File

@ -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<StudentTeam> 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<GHUser> 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<GHRepository> 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<GHRepository> 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;
}
}
}

View File

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

View File

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

View File

@ -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<Integer> 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<Integer> 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<Integer> 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<Integer, Student> studentMap) {
Team t = new Team();
public StudentTeam getPreferredTeam(Map<Integer, Student> 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());
}
}

View File

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

View File

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

View File

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

View File

@ -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<Student> students;
protected int id;
/**
* The team identification number.
* A list of members of this team.
*/
private int id;
private List<Person> 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)) {
return true;
}
}
return false;
}
public int getStudentCount() {
return this.students.size();
public void setId(int newId) {
this.id = newId;
}
/**
* @return This team's id number.
*/
public int getId() {
return this.id;
}
public void setId(int id) {
this.id = 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;
}
public void setStudents(List<Student> students) {
this.students = students;
}
public List<Student> getStudents() {
return this.students;
this.members.add(newMember);
}
/**
* Adds a student to this team.
* @param student The student to add.
* @return True if the student could be added, false otherwise.
* Removes a person from this team.
* @param person The person to remove.
*/
public boolean addStudent(Student student) {
if (!this.hasStudent(student)) {
this.students.add(student);
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;
} else {
return false;
}
}
return false;
}
/**
* 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.
* 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 boolean isValid(int teamSize) {
if (this.getStudentCount() == teamSize) {
List<Integer> encounteredIds = new ArrayList<>();
for (Student studentA : this.students) {
for (Student studentB : this.students) {
if (!studentA.equals(studentB) && !studentA.getPreferredPartners().contains(studentB.getNumber())) {
return false;
public void setMembers(Person[] people) {
this.members = new ArrayList<>(Arrays.asList(people));
}
/**
* Gets a list of people in this team.
* @return A list of people in this team.
*/
public Person[] getMembers() {
Person[] people = new Person[this.memberCount()];
this.members.toArray(people);
return people;
}
/**
* Gets the number of people in this team.
* @return The number of people in this team.
*/
public int memberCount() {
return this.members.size();
}
/**
* 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 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;
}
/**
* 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.
* 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.
*/
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());
@Override
public boolean equals(Object obj) {
if (obj instanceof Team) {
Team team = (Team) obj;
return team.getId() == this.getId() && this.hasSameMembers(team);
}
return sb.toString();
return false;
}
/**
* @return A String containing a line for each member in the team.
*/
@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');
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();
}
/**
* 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)) {
return false;
}
}
return true;
} else {
return false;
}
}
}

View File

@ -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<PreparedStatement> 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<PreparedStatement> 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<Integer> 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<Integer> 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<StudentTeam> 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;
}
}
}

View File

@ -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<PreparedStatement> prepareStatementsFromFile(String filename, Connection connection) {
String string = FileUtils.readStringFromFile(filename);
if (string == null || string.isEmpty()) {
return new ArrayList<>();
}
String[] splits = string.split(";");
List<PreparedStatement> 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;
}
}

View File

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

View File

@ -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 {
}
}

View File

@ -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<String, Executable> 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.");
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<StudentTeam> teams = FileUtils.getStudentTeamsFromCSV(filename, teamSize);
return this.db.storeStudentTeams(teams);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<StudentTeam> getStudentTeamsFromCSV(String filename, int teamSize) {
List<StudentTeam> 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;
}
}
}

View File

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

View File

@ -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<Team> generateFromCSV(String filename, int teamSize) throws IOException, IllegalArgumentException {
logger.info("Generating teams of size " + teamSize);
public static List<StudentTeam> 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<CSVRecord> 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<Team> generateAllValidTeams(Map<Integer, Student> studentMap, int teamSize) {
private static List<StudentTeam> generateAllValidTeams(Map<Integer, Student> studentMap, int teamSize) {
List<Student> singleStudents = new ArrayList<>(studentMap.values());
List<Team> teams = new ArrayList<>();
List<StudentTeam> studentTeams = new ArrayList<>();
int teamCount = 1;
// For each student, try to make a team from its preferred partners.
for (Map.Entry<Integer, Student> 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)) {
// 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.
t.setId(teamCount++);
singleStudents.removeAll(t.getStudents());
teams.add(t);
logger.fine("Created team: " + t);
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<Team> mergeSingleStudents(List<Student> singleStudents, int teamSize, int teamIndex) {
List<Team> teams = new ArrayList<>();
private static List<StudentTeam> mergeSingleStudents(List<Student> singleStudents, int teamSize, int teamIndex) {
List<StudentTeam> 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;
}
/**

View File

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

View File

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