Merge pull request #2 from andrewlalis/data_import

Data import
This commit is contained in:
Andrew Lalis 2019-05-10 13:14:15 +02:00 committed by GitHub
commit 6f441fdafe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 1161 additions and 89 deletions

View File

@ -1,17 +1,32 @@
package nl.andrewlalis.teaching_assistant_assistant.controllers; package nl.andrewlalis.teaching_assistant_assistant.controllers;
import nl.andrewlalis.teaching_assistant_assistant.model.Course;
import nl.andrewlalis.teaching_assistant_assistant.model.people.Student;
import nl.andrewlalis.teaching_assistant_assistant.model.repositories.CourseRepository;
import nl.andrewlalis.teaching_assistant_assistant.model.repositories.StudentRepository; import nl.andrewlalis.teaching_assistant_assistant.model.repositories.StudentRepository;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.Optional;
/**
* Controller for operations dealing with the global collection of students, not particular to one course.
*/
@Controller @Controller
public class Students { public class Students {
private StudentRepository studentRepository; private static final String NO_COURSE = "NO_COURSE_SELECTED";
protected Students(StudentRepository studentRepository) { private StudentRepository studentRepository;
private CourseRepository courseRepository;
protected Students(StudentRepository studentRepository, CourseRepository courseRepository) {
this.studentRepository = studentRepository; this.studentRepository = studentRepository;
this.courseRepository = courseRepository;
} }
@GetMapping("/students") @GetMapping("/students")
@ -19,4 +34,35 @@ public class Students {
model.addAttribute("students", this.studentRepository.findAll()); model.addAttribute("students", this.studentRepository.findAll());
return "students"; return "students";
} }
@GetMapping("/students/create")
public String getCreate(Model model) {
model.addAttribute("student", new Student("First Name", "Last Name", "Email Address", "Github Username", 1234567));
model.addAttribute("courses", this.courseRepository.findAll());
return "students/create";
}
@PostMapping(
value = "/students/create",
consumes = "application/x-www-form-urlencoded"
)
public String postCreate(
@ModelAttribute Student newStudent,
@RequestParam(value = "course_code", required = false) String courseCode
) {
this.studentRepository.save(newStudent);
if (courseCode != null && !courseCode.equals(NO_COURSE)) {
Optional<Course> optionalCourse = this.courseRepository.findByCode(courseCode);
optionalCourse.ifPresent(course -> {
course.addParticipant(newStudent);
newStudent.assignToCourse(course);
this.courseRepository.save(course);
this.studentRepository.save(newStudent);
});
}
return "redirect:/students";
}
} }

View File

@ -0,0 +1,69 @@
package nl.andrewlalis.teaching_assistant_assistant.controllers.courses.entity.student_teams;
import nl.andrewlalis.teaching_assistant_assistant.model.Course;
import nl.andrewlalis.teaching_assistant_assistant.model.people.teams.StudentTeam;
import nl.andrewlalis.teaching_assistant_assistant.model.repositories.CourseRepository;
import nl.andrewlalis.teaching_assistant_assistant.model.repositories.StudentTeamRepository;
import nl.andrewlalis.teaching_assistant_assistant.services.StudentTeamService;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
/**
* Controller for the action to merge all single teams in a course.
* TODO: Implement this functionality automatically.
*/
@Controller
public class MergeSingleTeams {
private Logger logger = LogManager.getLogger(MergeSingleTeams.class);
private CourseRepository courseRepository;
private StudentTeamRepository studentTeamRepository;
private StudentTeamService studentTeamService;
protected MergeSingleTeams(CourseRepository courseRepository, StudentTeamRepository studentTeamRepository, StudentTeamService studentTeamService) {
this.courseRepository = courseRepository;
this.studentTeamRepository = studentTeamRepository;
this.studentTeamService = studentTeamService;
}
@GetMapping("/courses/{code}/student_teams/merge_single_teams")
public String get(@PathVariable String code) {
Optional<Course> optionalCourse = this.courseRepository.findByCode(code);
if (optionalCourse.isPresent()) {
Course course = optionalCourse.get();
List<StudentTeam> singleTeams = this.getAllSingleTeams(course);
singleTeams.forEach(team -> logger.info("Team " + team.getId() + " is a single team."));
// while (singleTeams.size() > 1) {
// StudentTeam single1 = singleTeams.remove(0);
// StudentTeam single2 = singleTeams.remove(0);
//
// // Todo: use a service here and when removing a team in another location to avoid duplication.
// StudentTeam newTeam = this.studentTeamService.createNewStudentTeam(course);
// }
}
return "redirect:/courses/{code}/student_teams";
}
private List<StudentTeam> getAllSingleTeams(Course course) {
List<StudentTeam> allTeams = course.getStudentTeams();
List<StudentTeam> singleTeams = new ArrayList<>();
for (StudentTeam team : allTeams) {
if (team.getMembers().size() == 1) {
singleTeams.add(team);
}
}
return singleTeams;
}
}

View File

@ -1,21 +1,45 @@
package nl.andrewlalis.teaching_assistant_assistant.controllers.courses.entity.student_teams; package nl.andrewlalis.teaching_assistant_assistant.controllers.courses.entity.student_teams;
import nl.andrewlalis.teaching_assistant_assistant.model.Course;
import nl.andrewlalis.teaching_assistant_assistant.model.people.Student;
import nl.andrewlalis.teaching_assistant_assistant.model.people.teams.StudentTeam; import nl.andrewlalis.teaching_assistant_assistant.model.people.teams.StudentTeam;
import nl.andrewlalis.teaching_assistant_assistant.model.people.teams.TeachingAssistantTeam;
import nl.andrewlalis.teaching_assistant_assistant.model.repositories.CourseRepository;
import nl.andrewlalis.teaching_assistant_assistant.model.repositories.StudentRepository;
import nl.andrewlalis.teaching_assistant_assistant.model.repositories.StudentTeamRepository; import nl.andrewlalis.teaching_assistant_assistant.model.repositories.StudentTeamRepository;
import nl.andrewlalis.teaching_assistant_assistant.model.repositories.TeachingAssistantTeamRepository;
import nl.andrewlalis.teaching_assistant_assistant.services.StudentTeamService;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
import java.util.Optional; import java.util.Optional;
@Controller @Controller
public class StudentTeamEntity { public class StudentTeamEntity {
private StudentTeamRepository studentTeamRepository; private StudentTeamRepository studentTeamRepository;
private CourseRepository courseRepository;
private StudentRepository studentRepository;
private TeachingAssistantTeamRepository teachingAssistantTeamRepository;
private StudentTeamService studentTeamService;
protected StudentTeamEntity(StudentTeamRepository studentTeamRepository) { protected StudentTeamEntity(
StudentTeamRepository studentTeamRepository,
CourseRepository courseRepository,
StudentRepository studentRepository,
TeachingAssistantTeamRepository teachingAssistantTeamRepository,
StudentTeamService studentTeamService
) {
this.studentTeamRepository = studentTeamRepository; this.studentTeamRepository = studentTeamRepository;
this.courseRepository = courseRepository;
this.studentRepository = studentRepository;
this.teachingAssistantTeamRepository = teachingAssistantTeamRepository;
this.studentTeamService = studentTeamService;
} }
/** /**
@ -32,4 +56,163 @@ public class StudentTeamEntity {
return "courses/entity/student_teams/entity"; return "courses/entity/student_teams/entity";
} }
@GetMapping("/courses/{courseCode}/student_teams/create")
public String getCreate(@PathVariable String courseCode, Model model) {
Optional<Course> optionalCourse = this.courseRepository.findByCode(courseCode);
optionalCourse.ifPresent(course -> model.addAttribute("course", course));
return "courses/entity/student_teams/create";
}
/**
* Mapping for creating a new student team.
* @param courseCode The course code.
* @param model The view model.
* @return A redirect to the list of student teams.
*/
@PostMapping("/courses/{courseCode}/student_teams/create")
public String postCreate(@PathVariable String courseCode, Model model) {
Optional<Course> optionalCourse = this.courseRepository.findByCode(courseCode);
optionalCourse.ifPresent(course -> this.studentTeamService.createNewStudentTeam(course));
return "redirect:/courses/{courseCode}/student_teams";
}
@GetMapping("/courses/{courseCode}/student_teams/{teamId}/add_student")
public String getAddStudent(@PathVariable String courseCode, @PathVariable long teamId, Model model) {
Optional<Course> optionalCourse = this.courseRepository.findByCode(courseCode);
Optional<StudentTeam> optionalStudentTeam = this.studentTeamRepository.findById(teamId);
if (optionalCourse.isPresent() && optionalStudentTeam.isPresent()) {
List<Student> eligibleStudents = optionalCourse.get().getStudents();
eligibleStudents.sort((s1, s2) -> s1.getLastName().compareToIgnoreCase(s2.getLastName()));
model.addAttribute("course", optionalCourse.get());
model.addAttribute("student_team", optionalStudentTeam.get());
model.addAttribute("eligible_students", eligibleStudents);
}
return "courses/entity/student_teams/entity/add_student";
}
/**
* Mapping for adding a new student to this team.
* @param courseCode The course code.
* @param teamId The id of the team to add the student to.
* @param studentId The id of an existing student to add to this team.
* @return A redirect to the list of student teams.
*/
@PostMapping("/courses/{courseCode}/student_teams/{teamId}/add_student")
public String postAddStudent(
@PathVariable String courseCode,
@PathVariable long teamId,
@RequestParam(value = "student_id") long studentId
) {
Optional<Course> optionalCourse = this.courseRepository.findByCode(courseCode);
Optional<StudentTeam> optionalStudentTeam = this.studentTeamRepository.findById(teamId);
Optional<Student> optionalStudent = this.studentRepository.findById(studentId);
if (optionalCourse.isPresent() && optionalStudentTeam.isPresent() && optionalStudent.isPresent()) {
this.studentTeamService.addStudent(optionalStudentTeam.get(), optionalStudent.get());
}
return "redirect:/courses/{courseCode}/student_teams/{teamId}";
}
@GetMapping("/courses/{courseCode}/student_teams/{teamId}/assign_teaching_assistant_team")
public String getAssignTeachingAssistantTeam(
@PathVariable String courseCode,
@PathVariable long teamId,
Model model
) {
Optional<Course> optionalCourse = this.courseRepository.findByCode(courseCode);
Optional<StudentTeam> optionalStudentTeam = this.studentTeamRepository.findById(teamId);
if (optionalCourse.isPresent() && optionalStudentTeam.isPresent()) {
model.addAttribute("course", optionalCourse.get());
model.addAttribute("student_team", optionalStudentTeam.get());
}
return "courses/entity/student_teams/entity/assign_teaching_assistant_team";
}
/**
* Endpoint for assigning a teaching assistant team to this student team.
* @param courseCode The course code.
* @param teamId The id of the student team.
* @param teachingAssistantTeamId The id of the teaching assistant team.
* @return A redirect to the team responsible.
*/
@PostMapping("/courses/{courseCode}/student_teams/{teamId}/assign_teaching_assistant_team")
public String postAssignTeachingAssistantTeam(
@PathVariable String courseCode,
@PathVariable long teamId,
@RequestParam(value = "teaching_assistant_team_id") long teachingAssistantTeamId
) {
Optional<Course> optionalCourse = this.courseRepository.findByCode(courseCode);
Optional<StudentTeam> optionalStudentTeam = this.studentTeamRepository.findById(teamId);
Optional<TeachingAssistantTeam> optionalTeachingAssistantTeam = this.teachingAssistantTeamRepository.findById(teachingAssistantTeamId);
if (optionalCourse.isPresent() && optionalStudentTeam.isPresent()) {
TeachingAssistantTeam teachingAssistantTeam = null;
if (optionalTeachingAssistantTeam.isPresent()) {
teachingAssistantTeam = optionalTeachingAssistantTeam.get();
}
this.studentTeamService.assignTeachingAssistantTeam(optionalStudentTeam.get(), teachingAssistantTeam);
}
return "redirect:/courses/{courseCode}/student_teams/{teamId}";
}
/**
* Endpoint for removing a student from the student team.
* @param courseCode The code for the course.
* @param teamId The id of the team.
* @param studentId The student's id.
* @return A redirect to the team after the student is removed.
*/
@GetMapping("/courses/{courseCode}/student_teams/{teamId}/remove_student/{studentId}")
public String getRemoveStudent(
@PathVariable String courseCode,
@PathVariable long teamId,
@PathVariable long studentId
) {
Optional<Course> optionalCourse = this.courseRepository.findByCode(courseCode);
Optional<StudentTeam> optionalStudentTeam = this.studentTeamRepository.findById(teamId);
Optional<Student> optionalStudent = this.studentRepository.findById(studentId);
if (optionalCourse.isPresent() && optionalStudentTeam.isPresent() && optionalStudent.isPresent()) {
this.studentTeamService.removeStudent(optionalStudentTeam.get(), optionalStudent.get());
}
return "redirect:/courses/{courseCode}/student_teams/{teamId}";
}
@GetMapping("/courses/{courseCode}/student_teams/{teamId}/generate_repository")
public String getGenerateRepository(
@PathVariable String courseCode,
@PathVariable long teamId
) {
Optional<Course> optionalCourse = this.courseRepository.findByCode(courseCode);
Optional<StudentTeam> optionalStudentTeam = this.studentTeamRepository.findById(teamId);
if (optionalCourse.isPresent() && optionalStudentTeam.isPresent()) {
this.studentTeamService.generateRepository(optionalStudentTeam.get());
}
return "redirect:/courses/{courseCode}/student_teams/{teamId}";
}
@GetMapping("/courses/{courseCode}/student_teams/{teamId}/remove")
public String remove(@PathVariable String courseCode, @PathVariable long teamId) {
Optional<Course> optionalCourse = this.courseRepository.findByCode(courseCode);
Optional<StudentTeam> optionalStudentTeam = this.studentTeamRepository.findById(teamId);
if (optionalCourse.isPresent() && optionalStudentTeam.isPresent()) {
this.studentTeamService.removeTeam(optionalStudentTeam.get());
}
return "redirect:/courses/{courseCode}/student_teams";
}
} }

View File

@ -0,0 +1,51 @@
package nl.andrewlalis.teaching_assistant_assistant.controllers.courses.entity.student_teams;
import nl.andrewlalis.teaching_assistant_assistant.model.Course;
import nl.andrewlalis.teaching_assistant_assistant.model.people.teams.StudentTeam;
import nl.andrewlalis.teaching_assistant_assistant.model.repositories.CourseRepository;
import nl.andrewlalis.teaching_assistant_assistant.util.github.GithubManager;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import java.io.IOException;
import java.util.Optional;
/**
* Updates branch protection for all student repositories in a given course.
*/
@Controller
public class UpdateBranchProtection {
private CourseRepository courseRepository;
protected UpdateBranchProtection(CourseRepository courseRepository) {
this.courseRepository = courseRepository;
}
@GetMapping("/courses/{code}/student_teams/branch_protection_update")
public String get(@PathVariable String code) {
Optional<Course> optionalCourse = this.courseRepository.findByCode(code);
optionalCourse.ifPresent(course -> {
GithubManager manager;
try {
manager = new GithubManager(course.getApiKey());
} catch (IOException e) {
e.printStackTrace();
return;
}
for (StudentTeam team : course.getStudentTeams()) {
try {
manager.updateBranchProtection(course.getGithubOrganizationName(), team.getGithubRepositoryName(), team.getAssignedTeachingAssistantTeam().getGithubTeamName());
System.out.println("Updated branch protection for repository " + team.getGithubRepositoryName());
} catch (IOException e) {
e.printStackTrace();
System.err.println("Error occurred while trying to enable branch protection for repository " + team.getId());
}
}
});
return "redirect:/courses/{code}/student_teams";
}
}

View File

@ -63,8 +63,6 @@ public class InviteAllToRepository {
keys.add(rawKey.trim()); keys.add(rawKey.trim());
} }
String fullRepositoryName = course.getGithubOrganizationName() + '/' + repositoryName;
int inviteCounter = 0; int inviteCounter = 0;
GithubManager manager; GithubManager manager;
try { try {
@ -80,24 +78,30 @@ public class InviteAllToRepository {
if (inviteCounter == 50) { if (inviteCounter == 50) {
System.out.println("Used up 50 invites on key."); System.out.println("Used up 50 invites on key.");
try { try {
if (keys.isEmpty()) {
System.err.println("No more keys.");
failedNames.addAll(githubUsernames);
break;
}
manager = new GithubManager(keys.remove(0)); manager = new GithubManager(keys.remove(0));
inviteCounter = 0; inviteCounter = 0;
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
failedNames.addAll(githubUsernames); failedNames.addAll(githubUsernames);
return; break;
} }
} }
String username = githubUsernames.remove(0); String username = githubUsernames.remove(0);
try { try {
manager.addCollaborator(fullRepositoryName, username, "pull"); manager.addCollaborator(course.getGithubOrganizationName(), repositoryName, username, "pull");
inviteCounter++; inviteCounter++;
System.out.println("\tInvited " + username); System.out.println("\tInvited " + username);
} catch (IOException e) { } catch (IOException e) {
//e.printStackTrace(); //e.printStackTrace();
System.err.println("Could not add " + username + " to repository " + fullRepositoryName); System.err.println("Could not add " + username + " to repository " + repositoryName + ": " + e.getMessage());
failedNames.add(username); failedNames.add(username);
inviteCounter = 50; // Try to use a different key if possible.
} }
} }

View File

@ -1,11 +1,17 @@
package nl.andrewlalis.teaching_assistant_assistant.controllers.students; package nl.andrewlalis.teaching_assistant_assistant.controllers.students;
import nl.andrewlalis.teaching_assistant_assistant.model.Course;
import nl.andrewlalis.teaching_assistant_assistant.model.people.Student; import nl.andrewlalis.teaching_assistant_assistant.model.people.Student;
import nl.andrewlalis.teaching_assistant_assistant.model.people.teams.Team;
import nl.andrewlalis.teaching_assistant_assistant.model.repositories.CourseRepository;
import nl.andrewlalis.teaching_assistant_assistant.model.repositories.StudentRepository; import nl.andrewlalis.teaching_assistant_assistant.model.repositories.StudentRepository;
import nl.andrewlalis.teaching_assistant_assistant.model.repositories.TeamRepository;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import java.util.Optional; import java.util.Optional;
@ -13,9 +19,13 @@ import java.util.Optional;
public class StudentEntity { public class StudentEntity {
private StudentRepository studentRepository; private StudentRepository studentRepository;
private TeamRepository teamRepository;
private CourseRepository courseRepository;
protected StudentEntity(StudentRepository studentRepository) { protected StudentEntity(StudentRepository studentRepository, TeamRepository teamRepository, CourseRepository courseRepository) {
this.studentRepository = studentRepository; this.studentRepository = studentRepository;
this.teamRepository = teamRepository;
this.courseRepository = courseRepository;
} }
@GetMapping("/students/{id}") @GetMapping("/students/{id}")
@ -24,4 +34,52 @@ public class StudentEntity {
optionalStudent.ifPresent(student -> model.addAttribute("student", student)); optionalStudent.ifPresent(student -> model.addAttribute("student", student));
return "students/entity"; return "students/entity";
} }
@GetMapping("/students/{id}/edit")
public String getEdit(@PathVariable long id, Model model) {
Optional<Student> optionalStudent = this.studentRepository.findById(id);
optionalStudent.ifPresent(student -> model.addAttribute("student", student));
return "students/entity/edit";
}
@PostMapping(
value = "/students/{id}/edit",
consumes = "application/x-www-form-urlencoded"
)
public String post(@ModelAttribute Student editedStudent, @PathVariable long id) {
Optional<Student> optionalStudent = this.studentRepository.findById(id);
optionalStudent.ifPresent(student -> {
student.setFirstName(editedStudent.getFirstName());
student.setLastName(editedStudent.getLastName());
student.setEmailAddress(editedStudent.getEmailAddress());
student.setGithubUsername(editedStudent.getGithubUsername());
student.setStudentNumber(editedStudent.getStudentNumber());
this.studentRepository.save(student);
});
return "redirect:/students/{id}";
}
@GetMapping("/students/{id}/remove")
public String getRemove(@PathVariable long id) {
Optional<Student> optionalStudent = this.studentRepository.findById(id);
optionalStudent.ifPresent(student -> {
for (Team team : student.getTeams()) {
team.removeMember(student);
student.removeFromAssignedTeam(team);
this.teamRepository.save(team);
}
for (Course course : student.getCourses()) {
course.removeParticipant(student);
student.removeFromAssignedCourse(course);
this.courseRepository.save(course);
}
this.studentRepository.delete(student);
});
return "redirect:/students";
}
} }

View File

@ -6,6 +6,7 @@ import nl.andrewlalis.teaching_assistant_assistant.model.people.Student;
import nl.andrewlalis.teaching_assistant_assistant.model.people.TeachingAssistant; import nl.andrewlalis.teaching_assistant_assistant.model.people.TeachingAssistant;
import nl.andrewlalis.teaching_assistant_assistant.model.people.teams.StudentTeam; import nl.andrewlalis.teaching_assistant_assistant.model.people.teams.StudentTeam;
import nl.andrewlalis.teaching_assistant_assistant.model.people.teams.TeachingAssistantTeam; import nl.andrewlalis.teaching_assistant_assistant.model.people.teams.TeachingAssistantTeam;
import nl.andrewlalis.teaching_assistant_assistant.model.people.teams.Team;
import javax.persistence.*; import javax.persistence.*;
import java.util.ArrayList; import java.util.ArrayList;
@ -110,6 +111,10 @@ public class Course extends BasicEntity {
this.studentTeams.add(team); this.studentTeams.add(team);
} }
public void removeStudentTeam(StudentTeam team) {
this.studentTeams.remove(team);
}
public void addTeachingAssistantTeam(TeachingAssistantTeam team) { public void addTeachingAssistantTeam(TeachingAssistantTeam team) {
this.teachingAssistantTeams.add(team); this.teachingAssistantTeams.add(team);
} }
@ -201,4 +206,18 @@ public class Course extends BasicEntity {
} }
return sb.toString(); return sb.toString();
} }
public int getNumberOfStudentsInTeams() {
int sum = 0;
for (Student s : this.getStudents()) {
for (Team team : s.getTeams()) {
if (team.getCourse().equals(this)) {
sum++;
break;
}
}
}
return sum;
}
} }

View File

@ -80,12 +80,20 @@ public abstract class Person extends BasicEntity {
} }
} }
public void removeFromAssignedTeam(Team team) {
this.teams.remove(team);
}
public void assignToCourse(Course course) { public void assignToCourse(Course course) {
if (!this.courses.contains(course)) { if (!this.courses.contains(course)) {
this.courses.add(course); this.courses.add(course);
} }
} }
public void removeFromAssignedCourse(Course course) {
this.courses.remove(course);
}
/* /*
Getters and Setters Getters and Setters
*/ */
@ -94,10 +102,18 @@ public abstract class Person extends BasicEntity {
return this.firstName; return this.firstName;
} }
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() { public String getLastName() {
return this.lastName; return this.lastName;
} }
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getFullName() { public String getFullName() {
return this.getFirstName() + ' ' + this.getLastName(); return this.getFirstName() + ' ' + this.getLastName();
} }
@ -106,10 +122,18 @@ public abstract class Person extends BasicEntity {
return this.emailAddress; return this.emailAddress;
} }
public void setEmailAddress(String emailAddress) {
this.emailAddress = emailAddress;
}
public String getGithubUsername() { public String getGithubUsername() {
return this.githubUsername; return this.githubUsername;
} }
public void setGithubUsername(String githubUsername) {
this.githubUsername = githubUsername;
}
public List<Course> getCourses() { public List<Course> getCourses() {
return this.courses; return this.courses;
} }

View File

@ -50,7 +50,17 @@ public class Student extends Person {
*/ */
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
return super.equals(o) || this.getStudentNumber() == ((Student) o).getStudentNumber(); if (super.equals(o)) {
return true;
}
if (!(o instanceof Student)) {
return false;
}
Student s = (Student) o;
return this.getStudentNumber() == s.getStudentNumber();
} }
@Override @Override

View File

@ -1,5 +1,6 @@
package nl.andrewlalis.teaching_assistant_assistant.model.people.teams; package nl.andrewlalis.teaching_assistant_assistant.model.people.teams;
import nl.andrewlalis.teaching_assistant_assistant.model.Course;
import nl.andrewlalis.teaching_assistant_assistant.model.assignments.grades.AssignmentGrade; import nl.andrewlalis.teaching_assistant_assistant.model.assignments.grades.AssignmentGrade;
import nl.andrewlalis.teaching_assistant_assistant.model.people.Person; import nl.andrewlalis.teaching_assistant_assistant.model.people.Person;
import nl.andrewlalis.teaching_assistant_assistant.model.people.Student; import nl.andrewlalis.teaching_assistant_assistant.model.people.Student;
@ -41,6 +42,10 @@ public class StudentTeam extends Team {
*/ */
public StudentTeam() {} public StudentTeam() {}
public StudentTeam(Course course) {
super(course);
}
public List<Student> getStudents() { public List<Student> getStudents() {
List<Person> people = super.getMembers(); List<Person> people = super.getMembers();
List<Student> students = new ArrayList<>(); List<Student> students = new ArrayList<>();

View File

@ -47,6 +47,15 @@ public abstract class Team extends BasicEntity {
this.members = new ArrayList<>(); this.members = new ArrayList<>();
} }
/**
* Publicly available constructor in which a course is required.
* @param course The course that this team is in.
*/
public Team(Course course) {
this();
this.setCourse(course);
}
public void addMember(Person person) { public void addMember(Person person) {
if (!this.containsMember(person)) { if (!this.containsMember(person)) {
this.members.add(person); this.members.add(person);

View File

@ -0,0 +1,213 @@
package nl.andrewlalis.teaching_assistant_assistant.services;
import nl.andrewlalis.teaching_assistant_assistant.model.Course;
import nl.andrewlalis.teaching_assistant_assistant.model.people.Student;
import nl.andrewlalis.teaching_assistant_assistant.model.people.teams.StudentTeam;
import nl.andrewlalis.teaching_assistant_assistant.model.people.teams.TeachingAssistantTeam;
import nl.andrewlalis.teaching_assistant_assistant.model.repositories.CourseRepository;
import nl.andrewlalis.teaching_assistant_assistant.model.repositories.StudentRepository;
import nl.andrewlalis.teaching_assistant_assistant.model.repositories.StudentTeamRepository;
import nl.andrewlalis.teaching_assistant_assistant.model.repositories.TeachingAssistantTeamRepository;
import nl.andrewlalis.teaching_assistant_assistant.util.github.GithubManager;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.List;
/**
* This service is used to control the manipulation, creation and deletion of student teams.
*/
@Service
public class StudentTeamService {
private Logger logger = LogManager.getLogger(StudentTeamService.class);
private StudentTeamRepository studentTeamRepository;
private StudentRepository studentRepository;
private CourseRepository courseRepository;
private TeachingAssistantTeamRepository teachingAssistantTeamRepository;
public StudentTeamService(
StudentTeamRepository studentTeamRepository,
StudentRepository studentRepository,
CourseRepository courseRepository,
TeachingAssistantTeamRepository teachingAssistantTeamRepository
) {
this.studentTeamRepository = studentTeamRepository;
this.studentRepository = studentRepository;
this.courseRepository = courseRepository;
this.teachingAssistantTeamRepository = teachingAssistantTeamRepository;
}
/**
* Creates a new empty student team.
* @param course The course that the student team is in.
* @return The newly created student team.
*/
public StudentTeam createNewStudentTeam(Course course) {
StudentTeam newTeam = new StudentTeam(course);
course.addStudentTeam(newTeam);
this.courseRepository.save(course);
logger.info("Created new team: " + newTeam.getId());
return newTeam;
}
/**
* Creates a new student team, and populates it with some students and automatically assigns a teaching assistant
* team.
* @param course The course to which the team will belong.
* @param students The list of students to add to the team.
* @param teachingAssistantTeam The teaching assistant team responsible for this new team.
* @return The newly created student team.
*/
public StudentTeam createNewStudentTeam(Course course, List<Student> students, TeachingAssistantTeam teachingAssistantTeam) {
StudentTeam emptyTeam = this.createNewStudentTeam(course);
emptyTeam.setAssignedTeachingAssistantTeam(teachingAssistantTeam);
teachingAssistantTeam.addAssignedStudentTeam(emptyTeam);
this.teachingAssistantTeamRepository.save(teachingAssistantTeam);
for (Student student : students) {
this.addStudent(emptyTeam, student);
}
return emptyTeam;
}
/**
* Adds a new student to this team.
* @param team The team to add the student to.
* @param student The student to add.
*/
public void addStudent(StudentTeam team, Student student) {
team.addMember(student);
student.assignToTeam(team);
this.studentTeamRepository.save(team);
this.studentRepository.save(student);
}
/**
* Removes a single student from a team, and if that team has a github repository, tries to remove the student from
* that as well.
* @param studentTeam The student team to remove the student from.
* @param student The student to remove.
*/
public void removeStudent(StudentTeam studentTeam, Student student) {
studentTeam.removeMember(student);
student.removeFromAssignedTeam(studentTeam);
this.studentTeamRepository.save(studentTeam);
this.studentRepository.save(student);
if (studentTeam.getGithubRepositoryName() != null) {
try {
logger.debug("Removing " + student.getGithubUsername() + " from repository " + studentTeam.getGithubRepositoryName());
GithubManager manager = new GithubManager(studentTeam.getCourse().getApiKey());
manager.removeCollaborator(studentTeam, student);
} catch (IOException e) {
logger.catching(e);
logger.error("Could not remove student from repository: " + studentTeam.getGithubRepositoryName());
}
}
logger.info("Removed student " + student.getFullName() + " from team " + studentTeam.getId());
}
/**
* Assigns a new teaching assistant team to a student team.
* @param studentTeam The student team.
* @param teachingAssistantTeam The teaching assistant team to assign the student team to.
* This may be null.
*/
public void assignTeachingAssistantTeam(StudentTeam studentTeam, TeachingAssistantTeam teachingAssistantTeam) {
TeachingAssistantTeam oldTeachingAssistantTeam = studentTeam.getAssignedTeachingAssistantTeam();
if (oldTeachingAssistantTeam != null) {
oldTeachingAssistantTeam.removeAssignedStudentTeam(studentTeam);
studentTeam.setAssignedTeachingAssistantTeam(null);
this.teachingAssistantTeamRepository.save(oldTeachingAssistantTeam);
}
if (teachingAssistantTeam != null) {
studentTeam.setAssignedTeachingAssistantTeam(teachingAssistantTeam);
teachingAssistantTeam.addAssignedStudentTeam(studentTeam);
this.teachingAssistantTeamRepository.save(teachingAssistantTeam);
}
this.studentTeamRepository.save(studentTeam);
logger.info("Assigned teaching assistant team " + teachingAssistantTeam + " to student team " + studentTeam.getId());
}
/**
* Uses a {@link GithubManager} to generate a repository for this student team.
* @param team The team to generate a repository for.
*/
public void generateRepository(StudentTeam team) {
if (team.getGithubRepositoryName() == null) {
try {
GithubManager manager = new GithubManager(team.getCourse().getApiKey());
String name = manager.generateStudentTeamRepository(team);
team.setGithubRepositoryName(name);
this.studentTeamRepository.save(team);
} catch (IOException e) {
logger.error("Could not generate repository.");
}
} else {
logger.warn("Repository " + team.getGithubRepositoryName() + " already exists.");
}
}
/**
* Removes the given team, archives the repository if one exists.
* @param team The team to remove.
*/
public void removeTeam(StudentTeam team) {
Course course = team.getCourse();
// Remove the student team at all costs!
if (team.getGithubRepositoryName() != null) {
// First remove all student collaborators.
try {
GithubManager manager = new GithubManager(course.getApiKey());
manager.deactivateRepository(team);
} catch (IOException e) {
e.printStackTrace();
logger.error("Could not deactivate repository.");
}
}
// Remove all students from this team.
for (Student s : team.getStudents()) {
s.removeFromAssignedTeam(team);
team.removeMember(s);
this.studentRepository.save(s);
}
// Remove the TA team assignment.
TeachingAssistantTeam teachingAssistantTeam = team.getAssignedTeachingAssistantTeam();
teachingAssistantTeam.removeAssignedStudentTeam(team);
team.setAssignedTeachingAssistantTeam(null);
this.teachingAssistantTeamRepository.save(teachingAssistantTeam);
// Remove the repository from the course and delete it.
course.removeStudentTeam(team);
this.studentTeamRepository.delete(team);
this.courseRepository.save(course);
logger.info("Removed team " + team.getId());
}
/**
* Merges all teams consisting of a single student so that new teams are generated.
*
* TODO: Make this team size independent.
*/
public void mergeSingleTeams() {
}
}

View File

@ -6,24 +6,30 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import nl.andrewlalis.teaching_assistant_assistant.model.people.Student; import nl.andrewlalis.teaching_assistant_assistant.model.people.Student;
import nl.andrewlalis.teaching_assistant_assistant.model.people.teams.StudentTeam; import nl.andrewlalis.teaching_assistant_assistant.model.people.teams.StudentTeam;
import org.apache.http.HttpResponse; import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpPatch;
import org.apache.http.client.methods.HttpPut; import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.StringEntity; import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.kohsuke.github.*; import org.kohsuke.github.*;
import java.io.File; import java.io.*;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URL; import java.net.URL;
import java.nio.file.Files; import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Calendar; import java.util.Calendar;
import java.util.List;
import java.util.stream.Collectors;
/** /**
* Encapsulates much of the github functionality that is needed. * Encapsulates much of the github functionality that is needed.
*/ */
public class GithubManager { public class GithubManager {
Logger logger = LogManager.getLogger(GithubManager.class);
private GitHub github; private GitHub github;
private String apiKey; private String apiKey;
@ -53,13 +59,14 @@ public class GithubManager {
* @return The name of the created repository. * @return The name of the created repository.
*/ */
public String generateStudentTeamRepository(StudentTeam team) { public String generateStudentTeamRepository(StudentTeam team) {
GHOrganization organization; logger.info("Generating repository for student team " + team.getId());
GHOrganization organization;
try { try {
organization = this.github.getOrganization(team.getCourse().getGithubOrganizationName()); organization = this.github.getOrganization(team.getCourse().getGithubOrganizationName());
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
System.err.println("Could not get Github organization with name: " + team.getCourse().getGithubOrganizationName()); logger.error("Could not get Github organization with name: " + team.getCourse().getGithubOrganizationName());
return null; return null;
} }
@ -69,12 +76,11 @@ public class GithubManager {
// Get the TA team which manages this repository. // Get the TA team which manages this repository.
GHTeam teachingAssistantGithubTeam; GHTeam teachingAssistantGithubTeam;
try { try {
teachingAssistantGithubTeam = organization.getTeamByName(team.getAssignedTeachingAssistantTeam().getGithubTeamName()); teachingAssistantGithubTeam = organization.getTeamByName(team.getAssignedTeachingAssistantTeam().getGithubTeamName());
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
System.err.println("Could not get team by name: " + team.getAssignedTeachingAssistantTeam().getGithubTeamName()); logger.error("Could not get team by name: " + team.getAssignedTeachingAssistantTeam().getGithubTeamName());
return null; return null;
} }
@ -87,50 +93,56 @@ public class GithubManager {
repositoryBuilder.autoInit(false); repositoryBuilder.autoInit(false);
GHRepository repository; GHRepository repository;
try { try {
repository = repositoryBuilder.create(); logger.debug("Creating empty repository " + repositoryName);
repository = repositoryBuilder.create();
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
System.err.println("Could not create repository: " + repositoryName); logger.error("Could not create repository: " + repositoryName);
return null; return null;
} }
try { try {
logger.debug("Assigning teaching assistant team " + teachingAssistantGithubTeam.getName() + " to repository.");
this.addRepositoryToTeam(teachingAssistantGithubTeam, repository); this.addRepositoryToTeam(teachingAssistantGithubTeam, repository);
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
System.err.println("Could not add repository " + repositoryName + " to team " + teachingAssistantGithubTeam.getName()); logger.error("Could not add repository " + repositoryName + " to team " + teachingAssistantGithubTeam.getName());
return null; return null;
} }
try { try {
logger.debug("Adding starting file to the repository.");
this.addStarterFile(repository, "program_resources/getting_started.md", "getting_started.md"); this.addStarterFile(repository, "program_resources/getting_started.md", "getting_started.md");
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
System.err.println("Could not add the starter file to the repository: " + repositoryName); logger.error("Could not add the starter file to the repository: " + repositoryName);
return null; return null;
} }
try { try {
logger.debug("Creating development branch.");
this.createDevelopmentBranch(repository); this.createDevelopmentBranch(repository);
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
System.err.println("Could not create development branch for repository: " + repositoryName); logger.error("Could not create development branch for repository: " + repositoryName);
return null; return null;
} }
try { try {
logger.debug("Adding protections to the master branch.");
this.protectMasterBranch(repository, teachingAssistantGithubTeam); this.protectMasterBranch(repository, teachingAssistantGithubTeam);
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
System.err.println("Could not add protections to the master branch of " + repositoryName); logger.error("Could not add protections to the master branch of " + repositoryName);
return null; return null;
} }
try { try {
logger.debug("Adding students as collaborators.");
this.addStudentsAsCollaborators(repository, team); this.addStudentsAsCollaborators(repository, team);
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
System.err.println("Could not add students as collaborators to " + repositoryName); logger.error("Could not add students as collaborators to " + repositoryName);
return null; return null;
} }
@ -159,13 +171,13 @@ public class GithubManager {
protectionBuilder.includeAdmins(false); protectionBuilder.includeAdmins(false);
protectionBuilder.restrictPushAccess(); protectionBuilder.restrictPushAccess();
protectionBuilder.teamPushAccess(adminTeam); protectionBuilder.teamPushAccess(adminTeam);
protectionBuilder.addRequiredChecks("ci/circleci"); protectionBuilder.addRequiredChecks("ci/circleci: build");
protectionBuilder.enable(); protectionBuilder.enable();
} }
private void addStudentsAsCollaborators(GHRepository repository, StudentTeam studentTeam) throws IOException { private void addStudentsAsCollaborators(GHRepository repository, StudentTeam studentTeam) throws IOException {
for (Student student : studentTeam.getStudents()) { for (Student student : studentTeam.getStudents()) {
this.addCollaborator(repository.getFullName(), student.getGithubUsername(), "push"); this.addCollaborator(repository.getOwnerName(), repository.getName(), student.getGithubUsername(), "push");
} }
} }
@ -175,9 +187,9 @@ public class GithubManager {
repository.createRef("refs/heads/development", sha1); repository.createRef("refs/heads/development", sha1);
} }
public void addCollaborator(String repositoryName, String githubUsername, String permission) throws IOException { public void addCollaborator(String organizationName, String repositoryName, String githubUsername, String permission) throws IOException {
try { try {
String url = "https://api.github.com/repos/" + repositoryName + "/collaborators/" + githubUsername + "?access_token=" + this.apiKey; String url = "https://api.github.com/repos/" + organizationName + '/' + repositoryName + "/collaborators/" + githubUsername + "?access_token=" + this.apiKey;
HttpPut put = new HttpPut(url); HttpPut put = new HttpPut(url);
CloseableHttpClient client = HttpClientBuilder.create().build(); CloseableHttpClient client = HttpClientBuilder.create().build();
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
@ -188,15 +200,88 @@ public class GithubManager {
HttpResponse response = client.execute(put); HttpResponse response = client.execute(put);
if (response.getStatusLine().getStatusCode() != 201) { if (response.getStatusLine().getStatusCode() != 201) {
throw new IOException("Error adding collaborator via url " + url + " : " + response.getStatusLine().getStatusCode() + " " + response.getStatusLine().getReasonPhrase()); String content = new BufferedReader(new InputStreamReader(response.getEntity().getContent()))
.lines().collect(Collectors.joining("\n"));
throw new IOException("Error adding collaborator via url " + url + " : " + response.getStatusLine().getStatusCode() + " " + response.getStatusLine().getReasonPhrase() + "\n" + content);
} }
} catch (JsonProcessingException | UnsupportedEncodingException e) { } catch (JsonProcessingException | UnsupportedEncodingException e) {
e.printStackTrace(); e.printStackTrace();
} }
} }
public void removeCollaborator(StudentTeam studentTeam, Student student) throws IOException {
GHOrganization organization = this.github.getOrganization(studentTeam.getCourse().getGithubOrganizationName());
GHRepository repository = organization.getRepository(studentTeam.getGithubRepositoryName());
GHUser user = this.github.getUser(student.getGithubUsername());
repository.removeCollaborators(user);
}
/**
* Deactivates a repository by removing all collaborator students, unassigning the repository from the TA team that
* was responsible for it, and archiving it.
* @param studentTeam The student team for which to archive.
* @throws IOException If an io exception occurred, duh!
*/
public void deactivateRepository(StudentTeam studentTeam) throws IOException {
GHOrganization organization = this.github.getOrganization(studentTeam.getCourse().getGithubOrganizationName());
GHRepository repository = organization.getRepository(studentTeam.getGithubRepositoryName());
List<GHUser> users = new ArrayList<>();
for (Student s : studentTeam.getStudents()) {
users.add(this.github.getUser(s.getGithubUsername()));
}
//repository.removeCollaborators(users);
GHTeam taTeam = organization.getTeamByName(studentTeam.getAssignedTeachingAssistantTeam().getGithubTeamName());
taTeam.remove(repository);
this.archiveRepository(repository);
}
private void addRepositoryToTeam(GHTeam team, GHRepository repository) throws IOException { private void addRepositoryToTeam(GHTeam team, GHRepository repository) throws IOException {
team.add(repository, GHOrganization.Permission.ADMIN); team.add(repository, GHOrganization.Permission.ADMIN);
} }
/**
* Updates branch protection for a given repository. That is, removes old branch protection and reinstates it to
* follow updated circleci conventions.
* @param organizationName The name of the organization.
* @param repositoryName The name of the repository.
* @param teamName The name of the team responsible for this repository.
* @throws IOException If an error occurs with any actions.
*/
public void updateBranchProtection(String organizationName, String repositoryName, String teamName) throws IOException {
GHOrganization organization = this.github.getOrganization(organizationName);
GHRepository repository = organization.getRepository(repositoryName);
GHTeam team = organization.getTeamByName(teamName);
repository.getBranch("master").disableProtection();
GHBranchProtectionBuilder builder = repository.getBranch("master").enableProtection();
builder.includeAdmins(false);
builder.restrictPushAccess();
builder.teamPushAccess(team);
builder.addRequiredChecks("ci/circleci: build");
builder.enable();
}
/**
* 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.
*/
private void archiveRepository(GHRepository repo) throws IOException {
HttpPatch patch = new HttpPatch("https://api.github.com/repos/" + repo.getFullName() + "?access_token=" + this.apiKey);
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());
}
}
} }

View File

@ -39,4 +39,14 @@ body {
table, th, td { table, th, td {
border: 1px solid black; border: 1px solid black;
border-collapse: collapse; border-collapse: collapse;
}
.page_row {
width: 100%;
margin-top: 10px;
margin-bottom: 10px;
}
label {
font-weight: bold;
} }

View File

@ -6,22 +6,23 @@
<body> <body>
<div id="content"> <div id="content">
<table> <h1>Courses</h1>
<tr> <hr>
<th>Name</th>
<th>Code</th> <ul>
<th>Created at</th> <li th:each="course: ${courses}">
<th>Students</th> <h2><a th:href="@{/courses/{code}(code=${course.getCode()})}" th:text="${course.getName()}"></a></h2>
</tr> <ul>
<tr th:each="course: ${courses}"> <li>Code: <span th:text="${course.getCode()}"></span></li>
<td> <li>Created on: <span th:text="${course.getCreatedOn()}"></span></li>
<a th:href="@{/courses/{code}(code=${course.getCode()})}" th:text="${course.getName()}"></a> <li>Students: <span th:text="${course.getStudents().size()}"></span></li>
</td> <li>Teaching Assistants: <span th:text="${course.getTeachingAssistants().size()}"></span></li>
<td th:text="${course.getCode()}"></td> <li>Student Teams: <span th:text="${course.getStudentTeams().size()}"></span></li>
<td th:text="${course.getCreatedOn()}"></td> <li>Teaching Assistant Teams: <span th:text="${course.getTeachingAssistantTeams().size()}"></span></li>
<td th:text="${course.getStudents().size()}"></td> <li>Number of Active Students (in a team): <span th:text="${course.getNumberOfStudentsInTeams()}"></span></li>
</tr> </ul>
</table> </li>
</ul>
</div> </div>
<div id="sidebar"> <div id="sidebar">

View File

@ -6,30 +6,37 @@
<body> <body>
<div id="content"> <div id="content">
<p> <h1>Create New Course</h1>
Create your course here
</p>
<form action="#" th:action="@{/courses}" th:object="${course}" method="post"> <form action="#" th:action="@{/courses}" th:object="${course}" method="post">
<label for="course_name_input">Name:</label> <div class="page_row">
<input id="course_name_input" type="text" th:field="*{name}" required/> <label for="course_name_input">Name:</label>
<input id="course_name_input" type="text" th:field="*{name}" required/>
</div>
<label for="course_code_input">Code:</label> <div class="page_row">
<input id="course_code_input" type="text" th:field="*{code}" required/> <label for="course_code_input">Code:</label>
<input id="course_code_input" type="text" th:field="*{code}" required/>
</div>
<label for="course_github_organization_name_input">Github Organization:</label> <div class="page_row">
<input id="course_github_organization_name_input" type="text" th:field="*{githubOrganizationName}" required/> <label for="course_github_organization_name_input">Github Organization:</label>
<input id="course_github_organization_name_input" type="text" th:field="*{githubOrganizationName}" required/>
</div>
<label for="course_github_api_key_input">Github API Key:</label> <div class="page_row">
<input id="course_github_api_key_input" type="text" th:field="*{apiKey}" required/> <label for="course_github_api_key_input">Github API Key:</label>
<input id="course_github_api_key_input" type="text" th:field="*{apiKey}" required/>
</div>
<div class="page_row">
<button type="submit">Submit</button>
</div>
<button type="submit">Submit</button>
</form> </form>
</div> </div>
<div id="sidebar"> <div id="sidebar">
<div class="sidebar_block">
block
</div>
</div> </div>
</body> </body>

View File

@ -6,14 +6,22 @@
<body> <body>
<div id="content"> <div id="content">
<h1>Import Students from CSV File</h1>
<p> <p>
Please select a CSV file to import. Use this form to import student data from a CSV which has been generated by a Google form. As of right now, the column headers are hard-coded, but in the future it will be possible to dynamically import data from any CSV file by custom header format definitions.
</p> </p>
<form method="post" action="#" enctype="multipart/form-data" th:action="@{/courses/{code}/import_students(code=${course.getCode()})}"> <form method="post" action="#" enctype="multipart/form-data" th:action="@{/courses/{code}/import_students(code=${course.getCode()})}">
<label for="file_input">File:</label> <div class="page_row">
<input id="file_input" type="file" name="file" accept="text/csv"/> <label for="file_input">File:</label>
<button type="submit">Submit</button> <input id="file_input" type="file" name="file" accept="text/csv"/>
</div>
<div class="page_row">
<button type="submit">Submit</button>
</div>
</form> </form>
</div> </div>

View File

@ -57,12 +57,21 @@
<div class="sidebar_block"> <div class="sidebar_block">
<a th:href="@{/courses/{code}/student_teams/generate_repositories(code=${course.getCode()})}">Generate Repositories</a> <a th:href="@{/courses/{code}/student_teams/generate_repositories(code=${course.getCode()})}">Generate Repositories</a>
</div> </div>
<div class="sidebar_block">
<a th:href="@{/courses/{code}/student_teams/branch_protection_update(code=${course.getCode()})}">Update Branch Protection</a>
</div>
<div class="sidebar_block"> <div class="sidebar_block">
<a th:href="@{/courses/{code}/student_teams/export(code=${course.getCode()})}">Export</a> <a th:href="@{/courses/{code}/student_teams/export(code=${course.getCode()})}">Export</a>
</div> </div>
<div class="sidebar_block"> <div class="sidebar_block">
<a th:href="@{/courses/{code}/student_teams/export_contact_info(code=${course.getCode()})}">Export Team Contact Details</a> <a th:href="@{/courses/{code}/student_teams/export_contact_info(code=${course.getCode()})}">Export Team Contact Details</a>
</div> </div>
<div class="sidebar_block">
<a th:href="@{/courses/{code}/student_teams/create(code=${course.getCode()})}">Create New Student Team</a>
</div>
<div class="sidebar_block">
<a th:href="@{/courses/{code}/student_teams/merge_single_teams(code=${course.getCode()})}">Merge Single Teams</a>
</div>
</div> </div>
</body> </body>

View File

@ -0,0 +1,28 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" th:replace="~{layouts/basic_page :: layout (~{::title}, ~{::#content}, ~{::#sidebar})}">
<head>
<title>Create Student Team</title>
</head>
<body>
<div id="content">
<h1>Create a New Student Team</h1>
<p>
Creates a new student team for the <span th:text="${course.getName()}"></span> course, without any assigned TA team or student members.
</p>
<form action="#" th:action="@{/courses/{code}/student_teams/create(code=${course.getCode()})}" enctype="application/x-www-form-urlencoded" method="post">
<div class="page_row">
<button type="submit">Submit</button>
</div>
</form>
</div>
<div id="sidebar">
</div>
</body>
</html>

View File

@ -16,7 +16,7 @@
<span th:text="${student_team.getGithubRepositoryName()}"></span> <span th:text="${student_team.getGithubRepositoryName()}"></span>
</a> </a>
</li> </li>
<li> <li th:if="${student_team.getAssignedTeachingAssistantTeam() != null}">
Assigned Teaching Assistant Team: Assigned Teaching Assistant Team:
<a <a
th:if="${student_team.getAssignedTeachingAssistantTeam() != null}" th:if="${student_team.getAssignedTeachingAssistantTeam() != null}"
@ -31,6 +31,10 @@
<a th:href="@{/students/{id}(id=${student.getId()})}"> <a th:href="@{/students/{id}(id=${student.getId()})}">
<span th:text="${student.getFullName()}"></span> <span th:text="${student.getFullName()}"></span>
</a> </a>
(<a
th:href="@{/courses/{code}/student_teams/{id}/remove_student/{student_id}
(code=${student_team.getCourse().getCode()}, id=${student_team.getId()}, student_id=${student.getId()})}"
>Remove From This Team</a>)
</li> </li>
</ul> </ul>
</li> </li>
@ -39,17 +43,37 @@
</div> </div>
<div id="sidebar"> <div id="sidebar">
<div class="sidebar_block"> <div class="sidebar_block" th:if="${student_team.getGithubRepositoryName() == null && student_team.getAssignedTeachingAssistantTeam() != null}">
<a <a
th:if="${student_team.getGithubRepositoryName() == null}"
th:href="@{/courses/{code}/student_teams/{id}/generate_repository th:href="@{/courses/{code}/student_teams/{id}/generate_repository
(code=${student_team.getCourse().getCode()}, id=${student_team.getAssignedTeachingAssistantTeam().getId()})}" (code=${student_team.getCourse().getCode()}, id=${student_team.getId()})}"
>Generate Repository</a> >Generate Repository</a>
</div>
<div class="sidebar_block" th:if="${student_team.getGithubRepositoryName() != null}">
<a <a
th:if="${student_team.getGithubRepositoryName() != null}"
th:href="@{/courses/{code}/student_teams/{id}/delete_repository th:href="@{/courses/{code}/student_teams/{id}/delete_repository
(code=${student_team.getCourse().getCode()}, id=${student_team.getAssignedTeachingAssistantTeam().getId()})}" (code=${student_team.getCourse().getCode()}, id=${student_team.getAssignedTeachingAssistantTeam().getId()})}"
>Delete Repository</a> >Archive Repository</a>
</div>
<div class="sidebar_block">
<a th:href="@{/courses/{code}/student_teams/{id}/add_student(code=${student_team.getCourse().getCode()}, id=${student_team.getId()})}">
Add Student To Team
</a>
</div>
<div class="sidebar_block">
<a th:href="@{/courses/{code}/student_teams/{id}/assign_teaching_assistant_team(code=${student_team.getCourse().getCode()}, id=${student_team.getId()})}">
Assign Teaching Assistant Team
</a>
</div>
<div class="sidebar_block">
<a th:href="@{/courses/{code}/student_teams/{id}/remove(code=${student_team.getCourse().getCode()}, id=${student_team.getId()})}">Remove This Team</a>
<p>
Removes this team permanently, and archives the repository.
</p>
</div> </div>
</div> </div>

View File

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" th:replace="~{layouts/basic_page :: layout (~{::title}, ~{::#content}, ~{::#sidebar})}">
<head>
<title>Add Student To Team</title>
</head>
<body>
<div id="content">
<h1>Add a Student to Team <span th:text="${student_team.getId()}"></span></h1>
<form
action="#"
th:action="@{/courses/{code}/student_teams/{id}/add_student(code=${course.getCode()}, id=${student_team.getId()})}"
enctype="application/x-www-form-urlencoded"
method="post"
>
<div class="page_row">
<label for="student_select">Select a Student to Add:</label>
<select id="student_select" name="student_id" required>
<option th:each="student: ${eligible_students}" th:value="${student.getId()}" th:text="${student.getFullName()}"></option>
</select>
</div>
<div class="page_row">
<button type="submit">Submit</button>
</div>
</form>
</div>
<div id="sidebar">
</div>
</body>
</html>

View File

@ -0,0 +1,39 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" th:replace="~{layouts/basic_page :: layout (~{::title}, ~{::#content}, ~{::#sidebar})}">
<head>
<title>Assign Teaching Assistant Team</title>
</head>
<body>
<div id="content">
<h1>Assign a Teaching Assistant Team to Student Team <span th:text="${student_team.getId()}"></span></h1>
<form
action="#"
th:action="@{/courses/{code}/student_teams/{id}/assign_teaching_assistant_team(code=${course.getCode()}, id=${student_team.getId()})}"
enctype="application/x-www-form-urlencoded"
method="post"
>
<div class="page_row">
<label for="ta_team_select">Select a Team:</label>
<select id="ta_team_select" name="teaching_assistant_team_id">
<option value="-1">None</option>
<option
th:each="team: ${course.getTeachingAssistantTeams()}" th:value="${team.getId()}"
th:text="${'Team ' + team.getId() + ' (Manages ' + team.getAssignedStudentTeams().size() + ' student teams)'}"
></option>
</select>
</div>
<div class="page_row">
<button type="submit">Submit</button>
</div>
</form>
</div>
<div id="sidebar">
</div>
</body>
</html>

View File

@ -10,6 +10,11 @@
<h1>Students in <span th:text="${course.getName()}"></span></h1> <h1>Students in <span th:text="${course.getName()}"></span></h1>
<table> <table>
<tr>
<th>Student</th>
<th>Student Number</th>
<th>Email Address</th>
</tr>
<tr th:each="student: ${course.getStudents()}"> <tr th:each="student: ${course.getStudents()}">
<td> <td>
<a th:href="@{/students/{id}(id=${student.getId()})}"><span th:text="${student.getFullName()}"></span></a> <a th:href="@{/students/{id}(id=${student.getId()})}"><span th:text="${student.getFullName()}"></span></a>

View File

@ -19,16 +19,25 @@
enctype="application/x-www-form-urlencoded" enctype="application/x-www-form-urlencoded"
th:action="@{/courses/{code}/students/invite_all(code=${course.getCode()})}" th:action="@{/courses/{code}/students/invite_all(code=${course.getCode()})}"
> >
<label for="repository_name_input">Repository Name:</label> <div class="page_row">
<input id="repository_name_input" type="text" name="repository_name"/> <label for="repository_name_input">Repository Name:</label>
<input id="repository_name_input" type="text" name="repository_name"/>
</div>
<label for="keys_input">Api Keys (separate by newline):</label> <div class="page_row">
<textarea id="keys_input" name="api_keys"></textarea> <label for="keys_input">Api Keys (separate by newline):</label>
<textarea id="keys_input" name="api_keys"></textarea>
</div>
<label for="usernames_input">Usernames (separate by newline):</label> <div class="page_row">
<textarea id="usernames_input" name="usernames"></textarea> <label for="usernames_input">Usernames (separate by newline):</label>
<textarea id="usernames_input" name="usernames"></textarea>
</div>
<button type="submit">Submit</button> <div class="page_row">
<button type="submit">Submit</button>
</div>
</form> </form>
</div> </div>

View File

@ -6,17 +6,17 @@
<body> <body>
<footer th:fragment="footer" class="footer_bar"> <footer th:fragment="footer" class="footer_bar">
<link rel="stylesheet" href="../../../resources/static/css/footer.css" th:href="@{/css/footer.css}"/> <!-- <link rel="stylesheet" href="../../../resources/static/css/footer.css" th:href="@{/css/footer.css}"/>-->
<div class="third"> <!-- <div class="third">-->
First third of footer <!-- First third of footer-->
</div> <!-- </div>-->
<div class="third"> <!-- <div class="third">-->
Middle of footer <!-- Middle of footer-->
</div> <!-- </div>-->
<div class="third"> <!-- <div class="third">-->
Right side of footer. <!-- Right side of footer.-->
</div> <!-- </div>-->
</footer> </footer>

View File

@ -37,12 +37,17 @@
></a> ></a>
</div> </div>
</td> </td>
<td>
<a th:href="@{/students/{id}/remove(id=${student.getId()})}">Remove</a>
</td>
</tr> </tr>
</table> </table>
</div> </div>
<div id="sidebar"> <div id="sidebar">
<div class="sidebar_block">
<a th:href="@{/students/create}">Create Student</a>
</div>
</div> </div>
</body> </body>

View File

@ -0,0 +1,60 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" th:replace="~{layouts/basic_page :: layout (~{::title}, ~{::#content}, ~{::#sidebar})}">
<head>
<title>Create a Student</title>
</head>
<body>
<div id="content">
<h1>Create New Student</h1>
<form action="#" th:action="@{/students/create}" th:object="${student}" method="post" enctype="application/x-www-form-urlencoded">
<div class="page_row">
<label for="student_first_name_input">First Name:</label>
<input id="student_first_name_input" type="text" th:field="*{firstName}" required />
</div>
<div class="page_row">
<label for="student_last_name_input">Last Name:</label>
<input id="student_last_name_input" type="text" th:field="*{lastName}" required />
</div>
<div class="page_row">
<label for="student_email_input">Email Address:</label>
<input id="student_email_input" type="email" th:field="*{emailAddress}" required />
</div>
<div class="page_row">
<label for="student_github_username_input">Github Username:</label>
<input id="student_github_username_input" type="text" th:field="*{githubUsername}" required />
</div>
<div class="page_row">
<label for="student_number_input">Student Number:</label>
<input id="student_number_input" type="number" th:field="*{studentNumber}" required />
</div>
<div class="page_row">
<label for="student_course_select">Select a course to add the student to:</label>
<select id="student_course_select" name="course_code">
<option value="NO_COURSE_SELECTED">None</option>
<option
th:each="course: ${courses}"
th:value="${course.getCode()}"
th:text="${course.getName()}"
></option>
</select>
</div>
<div class="page_row">
<button type="submit">Submit</button>
</div>
</form>
</div>
<div id="sidebar">
</div>
</body>
</html>

View File

@ -30,7 +30,9 @@
</div> </div>
<div id="sidebar"> <div id="sidebar">
<div class="sidebar_block">
<a th:href="@{/students/{id}/edit(id=${student.getId()})}">Edit</a>
</div>
</div> </div>
</body> </body>

View File

@ -0,0 +1,54 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" th:replace="~{layouts/basic_page :: layout (~{::title}, ~{::#content}, ~{::#sidebar})}">
<head>
<title>Edit Student</title>
</head>
<body>
<div id="content">
<h1>Edit Student: <span th:text="${student.getFullName()}"></span></h1>
<form
action="#"
th:action="@{/students/{id}/edit(id=${student.getId()})}" th:object="${student}"
method="post"
>
<div class="page_row">
<label for="first_name_input">First Name:</label>
<input id="first_name_input" type="text" th:field="*{firstName}" required />
</div>
<div class="page_row">
<label for="last_name_input">Last Name:</label>
<input id="last_name_input" type="text" th:field="*{lastName}" required />
</div>
<div class="page_row">
<label for="email_input">Email Address:</label>
<input id="email_input" type="email" th:field="*{emailAddress}" required />
</div>
<div class="page_row">
<label for="github_username_input">Github Username:</label>
<input id="github_username_input" type="text" th:field="*{githubUsername}" required />
</div>
<div class="page_row">
<label for="student_number_input">Student Number:</label>
<input id="student_number_input" type="number" th:field="*{studentNumber}" required />
</div>
<div class="page_row">
<button type="submit">Submit</button>
</div>
</form>
</div>
<div id="sidebar">
</div>
</body>
</html>