diff --git a/src/main/java/nl/andrewlalis/teaching_assistant_assistant/controllers/Students.java b/src/main/java/nl/andrewlalis/teaching_assistant_assistant/controllers/Students.java index 4086ce3..3b4a14b 100644 --- a/src/main/java/nl/andrewlalis/teaching_assistant_assistant/controllers/Students.java +++ b/src/main/java/nl/andrewlalis/teaching_assistant_assistant/controllers/Students.java @@ -1,17 +1,32 @@ 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 org.springframework.stereotype.Controller; import org.springframework.ui.Model; 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 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.courseRepository = courseRepository; } @GetMapping("/students") @@ -19,4 +34,35 @@ public class Students { model.addAttribute("students", this.studentRepository.findAll()); 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 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"; + } } diff --git a/src/main/java/nl/andrewlalis/teaching_assistant_assistant/controllers/courses/entity/student_teams/MergeSingleTeams.java b/src/main/java/nl/andrewlalis/teaching_assistant_assistant/controllers/courses/entity/student_teams/MergeSingleTeams.java new file mode 100644 index 0000000..1d5ac35 --- /dev/null +++ b/src/main/java/nl/andrewlalis/teaching_assistant_assistant/controllers/courses/entity/student_teams/MergeSingleTeams.java @@ -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 optionalCourse = this.courseRepository.findByCode(code); + if (optionalCourse.isPresent()) { + Course course = optionalCourse.get(); + List 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 getAllSingleTeams(Course course) { + List allTeams = course.getStudentTeams(); + List singleTeams = new ArrayList<>(); + for (StudentTeam team : allTeams) { + if (team.getMembers().size() == 1) { + singleTeams.add(team); + } + } + return singleTeams; + } + +} diff --git a/src/main/java/nl/andrewlalis/teaching_assistant_assistant/controllers/courses/entity/student_teams/StudentTeamEntity.java b/src/main/java/nl/andrewlalis/teaching_assistant_assistant/controllers/courses/entity/student_teams/StudentTeamEntity.java index aeae752..18cf28d 100644 --- a/src/main/java/nl/andrewlalis/teaching_assistant_assistant/controllers/courses/entity/student_teams/StudentTeamEntity.java +++ b/src/main/java/nl/andrewlalis/teaching_assistant_assistant/controllers/courses/entity/student_teams/StudentTeamEntity.java @@ -1,21 +1,45 @@ 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.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.services.StudentTeamService; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import java.util.List; import java.util.Optional; @Controller public class StudentTeamEntity { 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.courseRepository = courseRepository; + this.studentRepository = studentRepository; + this.teachingAssistantTeamRepository = teachingAssistantTeamRepository; + this.studentTeamService = studentTeamService; } /** @@ -32,4 +56,163 @@ public class StudentTeamEntity { return "courses/entity/student_teams/entity"; } + + @GetMapping("/courses/{courseCode}/student_teams/create") + public String getCreate(@PathVariable String courseCode, Model model) { + Optional 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 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 optionalCourse = this.courseRepository.findByCode(courseCode); + Optional optionalStudentTeam = this.studentTeamRepository.findById(teamId); + + if (optionalCourse.isPresent() && optionalStudentTeam.isPresent()) { + List 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 optionalCourse = this.courseRepository.findByCode(courseCode); + Optional optionalStudentTeam = this.studentTeamRepository.findById(teamId); + Optional 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 optionalCourse = this.courseRepository.findByCode(courseCode); + Optional 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 optionalCourse = this.courseRepository.findByCode(courseCode); + Optional optionalStudentTeam = this.studentTeamRepository.findById(teamId); + Optional 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 optionalCourse = this.courseRepository.findByCode(courseCode); + Optional optionalStudentTeam = this.studentTeamRepository.findById(teamId); + Optional 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 optionalCourse = this.courseRepository.findByCode(courseCode); + Optional 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 optionalCourse = this.courseRepository.findByCode(courseCode); + Optional optionalStudentTeam = this.studentTeamRepository.findById(teamId); + + if (optionalCourse.isPresent() && optionalStudentTeam.isPresent()) { + this.studentTeamService.removeTeam(optionalStudentTeam.get()); + } + + return "redirect:/courses/{courseCode}/student_teams"; + } } diff --git a/src/main/java/nl/andrewlalis/teaching_assistant_assistant/controllers/courses/entity/student_teams/UpdateBranchProtection.java b/src/main/java/nl/andrewlalis/teaching_assistant_assistant/controllers/courses/entity/student_teams/UpdateBranchProtection.java new file mode 100644 index 0000000..41636b0 --- /dev/null +++ b/src/main/java/nl/andrewlalis/teaching_assistant_assistant/controllers/courses/entity/student_teams/UpdateBranchProtection.java @@ -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 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"; + } +} diff --git a/src/main/java/nl/andrewlalis/teaching_assistant_assistant/controllers/courses/entity/students/InviteAllToRepository.java b/src/main/java/nl/andrewlalis/teaching_assistant_assistant/controllers/courses/entity/students/InviteAllToRepository.java index 4a791d3..1803284 100644 --- a/src/main/java/nl/andrewlalis/teaching_assistant_assistant/controllers/courses/entity/students/InviteAllToRepository.java +++ b/src/main/java/nl/andrewlalis/teaching_assistant_assistant/controllers/courses/entity/students/InviteAllToRepository.java @@ -63,8 +63,6 @@ public class InviteAllToRepository { keys.add(rawKey.trim()); } - String fullRepositoryName = course.getGithubOrganizationName() + '/' + repositoryName; - int inviteCounter = 0; GithubManager manager; try { @@ -80,24 +78,30 @@ public class InviteAllToRepository { if (inviteCounter == 50) { System.out.println("Used up 50 invites on key."); try { + if (keys.isEmpty()) { + System.err.println("No more keys."); + failedNames.addAll(githubUsernames); + break; + } manager = new GithubManager(keys.remove(0)); inviteCounter = 0; } catch (IOException e) { e.printStackTrace(); failedNames.addAll(githubUsernames); - return; + break; } } String username = githubUsernames.remove(0); try { - manager.addCollaborator(fullRepositoryName, username, "pull"); + manager.addCollaborator(course.getGithubOrganizationName(), repositoryName, username, "pull"); inviteCounter++; System.out.println("\tInvited " + username); } catch (IOException e) { //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); + inviteCounter = 50; // Try to use a different key if possible. } } diff --git a/src/main/java/nl/andrewlalis/teaching_assistant_assistant/controllers/students/StudentEntity.java b/src/main/java/nl/andrewlalis/teaching_assistant_assistant/controllers/students/StudentEntity.java index db914de..9fd8547 100644 --- a/src/main/java/nl/andrewlalis/teaching_assistant_assistant/controllers/students/StudentEntity.java +++ b/src/main/java/nl/andrewlalis/teaching_assistant_assistant/controllers/students/StudentEntity.java @@ -1,11 +1,17 @@ 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.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.TeamRepository; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; 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.PostMapping; import java.util.Optional; @@ -13,9 +19,13 @@ import java.util.Optional; public class StudentEntity { 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.teamRepository = teamRepository; + this.courseRepository = courseRepository; } @GetMapping("/students/{id}") @@ -24,4 +34,52 @@ public class StudentEntity { optionalStudent.ifPresent(student -> model.addAttribute("student", student)); return "students/entity"; } + + @GetMapping("/students/{id}/edit") + public String getEdit(@PathVariable long id, Model model) { + Optional 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 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 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"; + } } diff --git a/src/main/java/nl/andrewlalis/teaching_assistant_assistant/model/Course.java b/src/main/java/nl/andrewlalis/teaching_assistant_assistant/model/Course.java index 8c33111..d1dfe0b 100644 --- a/src/main/java/nl/andrewlalis/teaching_assistant_assistant/model/Course.java +++ b/src/main/java/nl/andrewlalis/teaching_assistant_assistant/model/Course.java @@ -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.teams.StudentTeam; import nl.andrewlalis.teaching_assistant_assistant.model.people.teams.TeachingAssistantTeam; +import nl.andrewlalis.teaching_assistant_assistant.model.people.teams.Team; import javax.persistence.*; import java.util.ArrayList; @@ -110,6 +111,10 @@ public class Course extends BasicEntity { this.studentTeams.add(team); } + public void removeStudentTeam(StudentTeam team) { + this.studentTeams.remove(team); + } + public void addTeachingAssistantTeam(TeachingAssistantTeam team) { this.teachingAssistantTeams.add(team); } @@ -201,4 +206,18 @@ public class Course extends BasicEntity { } 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; + } } diff --git a/src/main/java/nl/andrewlalis/teaching_assistant_assistant/model/people/Person.java b/src/main/java/nl/andrewlalis/teaching_assistant_assistant/model/people/Person.java index 70245af..5d7446c 100644 --- a/src/main/java/nl/andrewlalis/teaching_assistant_assistant/model/people/Person.java +++ b/src/main/java/nl/andrewlalis/teaching_assistant_assistant/model/people/Person.java @@ -80,12 +80,20 @@ public abstract class Person extends BasicEntity { } } + public void removeFromAssignedTeam(Team team) { + this.teams.remove(team); + } + public void assignToCourse(Course course) { if (!this.courses.contains(course)) { this.courses.add(course); } } + public void removeFromAssignedCourse(Course course) { + this.courses.remove(course); + } + /* Getters and Setters */ @@ -94,10 +102,18 @@ public abstract class Person extends BasicEntity { return this.firstName; } + public void setFirstName(String firstName) { + this.firstName = firstName; + } + public String getLastName() { return this.lastName; } + public void setLastName(String lastName) { + this.lastName = lastName; + } + public String getFullName() { return this.getFirstName() + ' ' + this.getLastName(); } @@ -106,10 +122,18 @@ public abstract class Person extends BasicEntity { return this.emailAddress; } + public void setEmailAddress(String emailAddress) { + this.emailAddress = emailAddress; + } + public String getGithubUsername() { return this.githubUsername; } + public void setGithubUsername(String githubUsername) { + this.githubUsername = githubUsername; + } + public List getCourses() { return this.courses; } diff --git a/src/main/java/nl/andrewlalis/teaching_assistant_assistant/model/people/Student.java b/src/main/java/nl/andrewlalis/teaching_assistant_assistant/model/people/Student.java index 26963bf..74856c4 100644 --- a/src/main/java/nl/andrewlalis/teaching_assistant_assistant/model/people/Student.java +++ b/src/main/java/nl/andrewlalis/teaching_assistant_assistant/model/people/Student.java @@ -50,7 +50,17 @@ public class Student extends Person { */ @Override 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 diff --git a/src/main/java/nl/andrewlalis/teaching_assistant_assistant/model/people/teams/StudentTeam.java b/src/main/java/nl/andrewlalis/teaching_assistant_assistant/model/people/teams/StudentTeam.java index dad4d47..a201008 100644 --- a/src/main/java/nl/andrewlalis/teaching_assistant_assistant/model/people/teams/StudentTeam.java +++ b/src/main/java/nl/andrewlalis/teaching_assistant_assistant/model/people/teams/StudentTeam.java @@ -1,5 +1,6 @@ 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.people.Person; import nl.andrewlalis.teaching_assistant_assistant.model.people.Student; @@ -41,6 +42,10 @@ public class StudentTeam extends Team { */ public StudentTeam() {} + public StudentTeam(Course course) { + super(course); + } + public List getStudents() { List people = super.getMembers(); List students = new ArrayList<>(); diff --git a/src/main/java/nl/andrewlalis/teaching_assistant_assistant/model/people/teams/Team.java b/src/main/java/nl/andrewlalis/teaching_assistant_assistant/model/people/teams/Team.java index 0e81635..c121694 100644 --- a/src/main/java/nl/andrewlalis/teaching_assistant_assistant/model/people/teams/Team.java +++ b/src/main/java/nl/andrewlalis/teaching_assistant_assistant/model/people/teams/Team.java @@ -47,6 +47,15 @@ public abstract class Team extends BasicEntity { 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) { if (!this.containsMember(person)) { this.members.add(person); diff --git a/src/main/java/nl/andrewlalis/teaching_assistant_assistant/services/StudentTeamService.java b/src/main/java/nl/andrewlalis/teaching_assistant_assistant/services/StudentTeamService.java new file mode 100644 index 0000000..24f29a2 --- /dev/null +++ b/src/main/java/nl/andrewlalis/teaching_assistant_assistant/services/StudentTeamService.java @@ -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 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() { + + } + +} diff --git a/src/main/java/nl/andrewlalis/teaching_assistant_assistant/util/github/GithubManager.java b/src/main/java/nl/andrewlalis/teaching_assistant_assistant/util/github/GithubManager.java index 98ab14a..9b7334e 100644 --- a/src/main/java/nl/andrewlalis/teaching_assistant_assistant/util/github/GithubManager.java +++ b/src/main/java/nl/andrewlalis/teaching_assistant_assistant/util/github/GithubManager.java @@ -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.teams.StudentTeam; import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpPatch; import org.apache.http.client.methods.HttpPut; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.kohsuke.github.*; -import java.io.File; -import java.io.IOException; -import java.io.UnsupportedEncodingException; +import java.io.*; import java.net.URL; import java.nio.file.Files; +import java.util.ArrayList; import java.util.Calendar; +import java.util.List; +import java.util.stream.Collectors; /** * Encapsulates much of the github functionality that is needed. */ public class GithubManager { + Logger logger = LogManager.getLogger(GithubManager.class); + private GitHub github; private String apiKey; @@ -53,13 +59,14 @@ public class GithubManager { * @return The name of the created repository. */ public String generateStudentTeamRepository(StudentTeam team) { - GHOrganization organization; + logger.info("Generating repository for student team " + team.getId()); + GHOrganization organization; try { organization = this.github.getOrganization(team.getCourse().getGithubOrganizationName()); } catch (IOException e) { 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; } @@ -69,12 +76,11 @@ public class GithubManager { // Get the TA team which manages this repository. GHTeam teachingAssistantGithubTeam; - try { teachingAssistantGithubTeam = organization.getTeamByName(team.getAssignedTeachingAssistantTeam().getGithubTeamName()); } catch (IOException e) { 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; } @@ -87,50 +93,56 @@ public class GithubManager { repositoryBuilder.autoInit(false); GHRepository repository; try { - repository = repositoryBuilder.create(); + logger.debug("Creating empty repository " + repositoryName); + repository = repositoryBuilder.create(); } catch (IOException e) { e.printStackTrace(); - System.err.println("Could not create repository: " + repositoryName); + logger.error("Could not create repository: " + repositoryName); return null; } try { + logger.debug("Assigning teaching assistant team " + teachingAssistantGithubTeam.getName() + " to repository."); this.addRepositoryToTeam(teachingAssistantGithubTeam, repository); } catch (IOException e) { 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; } try { + logger.debug("Adding starting file to the repository."); this.addStarterFile(repository, "program_resources/getting_started.md", "getting_started.md"); } catch (IOException e) { 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; } try { + logger.debug("Creating development branch."); this.createDevelopmentBranch(repository); } catch (IOException e) { 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; } try { + logger.debug("Adding protections to the master branch."); this.protectMasterBranch(repository, teachingAssistantGithubTeam); } catch (IOException e) { 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; } try { + logger.debug("Adding students as collaborators."); this.addStudentsAsCollaborators(repository, team); } catch (IOException e) { 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; } @@ -159,13 +171,13 @@ public class GithubManager { protectionBuilder.includeAdmins(false); protectionBuilder.restrictPushAccess(); protectionBuilder.teamPushAccess(adminTeam); - protectionBuilder.addRequiredChecks("ci/circleci"); + protectionBuilder.addRequiredChecks("ci/circleci: build"); protectionBuilder.enable(); } private void addStudentsAsCollaborators(GHRepository repository, StudentTeam studentTeam) throws IOException { 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); } - 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 { - 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); CloseableHttpClient client = HttpClientBuilder.create().build(); ObjectMapper mapper = new ObjectMapper(); @@ -188,15 +200,88 @@ public class GithubManager { HttpResponse response = client.execute(put); 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) { 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 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 { 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()); + } + } + } diff --git a/src/main/resources/static/css/style.css b/src/main/resources/static/css/style.css index e1d6b8e..ee7e261 100644 --- a/src/main/resources/static/css/style.css +++ b/src/main/resources/static/css/style.css @@ -39,4 +39,14 @@ body { table, th, td { border: 1px solid black; border-collapse: collapse; +} + +.page_row { + width: 100%; + margin-top: 10px; + margin-bottom: 10px; +} + +label { + font-weight: bold; } \ No newline at end of file diff --git a/src/main/resources/templates/courses.html b/src/main/resources/templates/courses.html index 42b41e7..a225f7a 100644 --- a/src/main/resources/templates/courses.html +++ b/src/main/resources/templates/courses.html @@ -6,22 +6,23 @@
- - - - - - - - - - - - - -
NameCodeCreated atStudents
- -
+

Courses

+
+ +
    +
  • +

    +
      +
    • Code:
    • +
    • Created on:
    • +
    • Students:
    • +
    • Teaching Assistants:
    • +
    • Student Teams:
    • +
    • Teaching Assistant Teams:
    • +
    • Number of Active Students (in a team):
    • +
    +
  • +
diff --git a/src/main/resources/templates/courses/entity/student_teams/create.html b/src/main/resources/templates/courses/entity/student_teams/create.html new file mode 100644 index 0000000..6aa2f77 --- /dev/null +++ b/src/main/resources/templates/courses/entity/student_teams/create.html @@ -0,0 +1,28 @@ + + + + Create Student Team + + + +
+

Create a New Student Team

+ +

+ Creates a new student team for the course, without any assigned TA team or student members. +

+ +
+ +
+ +
+
+
+ + + + + \ No newline at end of file diff --git a/src/main/resources/templates/courses/entity/student_teams/entity.html b/src/main/resources/templates/courses/entity/student_teams/entity.html index dc1af36..fa04e6f 100644 --- a/src/main/resources/templates/courses/entity/student_teams/entity.html +++ b/src/main/resources/templates/courses/entity/student_teams/entity.html @@ -16,7 +16,7 @@ -
  • +
  • Assigned Teaching Assistant Team: + (Remove From This Team)
  • @@ -39,17 +43,37 @@ diff --git a/src/main/resources/templates/students/create.html b/src/main/resources/templates/students/create.html new file mode 100644 index 0000000..3d5a5d2 --- /dev/null +++ b/src/main/resources/templates/students/create.html @@ -0,0 +1,60 @@ + + + + Create a Student + + + +
    +

    Create New Student

    + +
    +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + +
    + +
    +
    + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/students/entity.html b/src/main/resources/templates/students/entity.html index 4576c60..a21baeb 100644 --- a/src/main/resources/templates/students/entity.html +++ b/src/main/resources/templates/students/entity.html @@ -30,7 +30,9 @@ diff --git a/src/main/resources/templates/students/entity/edit.html b/src/main/resources/templates/students/entity/edit.html new file mode 100644 index 0000000..8dd4cff --- /dev/null +++ b/src/main/resources/templates/students/entity/edit.html @@ -0,0 +1,54 @@ + + + + Edit Student + + + +
    +

    Edit Student:

    + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + +
    +
    + +
    + + + + + \ No newline at end of file