From a96552b32dd62d50a6fa5cdfe2771eb4c059c031 Mon Sep 17 00:00:00 2001 From: Cedric Ong Date: Sat, 20 Apr 2024 04:01:22 +0800 Subject: [PATCH] temp commit --- .../cases/InstructorCoursesPageE2ETest.java | 2 +- .../e2e/cases/sql/BaseE2ETestCase.java | 11 + .../sql/InstructorCoursesPageE2ETest.java | 211 ++++++++++++ .../pageobjects/InstructorCoursesPage.java | 81 +++++ .../data/InstructorCoursesPageE2ETestSql.json | 318 ++++++++++++++++++ .../java/teammates/sqllogic/api/Logic.java | 35 ++ .../sqllogic/core/AccountsLogic.java | 10 + .../teammates/sqllogic/core/CoursesLogic.java | 40 +++ .../teammates/sqllogic/core/UsersLogic.java | 24 ++ .../ui/webapi/CreateCourseAction.java | 39 +-- .../BaseTestCaseWithSqlDatabaseAccess.java | 4 + 11 files changed, 752 insertions(+), 23 deletions(-) create mode 100644 src/e2e/java/teammates/e2e/cases/sql/InstructorCoursesPageE2ETest.java create mode 100644 src/e2e/resources/data/InstructorCoursesPageE2ETestSql.json diff --git a/src/e2e/java/teammates/e2e/cases/InstructorCoursesPageE2ETest.java b/src/e2e/java/teammates/e2e/cases/InstructorCoursesPageE2ETest.java index 2f95cb043ea..d4fd551d9bb 100644 --- a/src/e2e/java/teammates/e2e/cases/InstructorCoursesPageE2ETest.java +++ b/src/e2e/java/teammates/e2e/cases/InstructorCoursesPageE2ETest.java @@ -101,7 +101,7 @@ public void classSetup() { BACKDOOR.deleteCourse(copyCourse2.getId()); } - @Test + @Test(enabled = false) @Override public void testAll() { String instructorId = sqlTestData.accounts.get("instructor").getGoogleId(); diff --git a/src/e2e/java/teammates/e2e/cases/sql/BaseE2ETestCase.java b/src/e2e/java/teammates/e2e/cases/sql/BaseE2ETestCase.java index 6878da94e44..ad72526465e 100644 --- a/src/e2e/java/teammates/e2e/cases/sql/BaseE2ETestCase.java +++ b/src/e2e/java/teammates/e2e/cases/sql/BaseE2ETestCase.java @@ -20,6 +20,7 @@ import teammates.e2e.util.BackDoor; import teammates.e2e.util.EmailAccount; import teammates.e2e.util.TestProperties; +import teammates.storage.sqlentity.Course; import teammates.storage.sqlentity.FeedbackQuestion; import teammates.storage.sqlentity.FeedbackResponse; import teammates.storage.sqlentity.FeedbackSession; @@ -27,6 +28,7 @@ import teammates.test.BaseTestCaseWithSqlDatabaseAccess; import teammates.test.FileHelper; import teammates.test.ThreadHelper; +import teammates.ui.output.CourseData; import teammates.ui.output.FeedbackQuestionData; import teammates.ui.output.FeedbackResponseData; import teammates.ui.output.FeedbackSessionData; @@ -242,6 +244,15 @@ protected SqlDataBundle doRemoveAndRestoreDataBundle(SqlDataBundle testData) { } } + CourseData getCourse(String courseId) { + return BACKDOOR.getCourseData(courseId); + } + + @Override + protected CourseData getCourse(Course course) { + return getCourse(course.getId()); + } + FeedbackQuestionData getFeedbackQuestion(String courseId, String feedbackSessionName, int qnNumber) { return BACKDOOR.getFeedbackQuestionData(courseId, feedbackSessionName, qnNumber); } diff --git a/src/e2e/java/teammates/e2e/cases/sql/InstructorCoursesPageE2ETest.java b/src/e2e/java/teammates/e2e/cases/sql/InstructorCoursesPageE2ETest.java new file mode 100644 index 00000000000..b7f99110ce4 --- /dev/null +++ b/src/e2e/java/teammates/e2e/cases/sql/InstructorCoursesPageE2ETest.java @@ -0,0 +1,211 @@ +package teammates.e2e.cases.sql; + +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.util.HashSet; +import java.util.Set; + +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import teammates.common.util.AppUrl; +import teammates.common.util.Const; +import teammates.e2e.pageobjects.InstructorCoursesPage; +import teammates.storage.sqlentity.Course; +import teammates.storage.sqlentity.FeedbackSession; +import teammates.storage.sqlentity.Instructor; +import teammates.storage.sqlentity.Student; + +/** + * SUT: {@link Const.WebPageURIs#INSTRUCTOR_COURSES_PAGE}. + */ +public class InstructorCoursesPageE2ETest extends BaseE2ETestCase { + private Course[] courses = new Course[4]; + private Course newCourse; + private Course copyCourse; + private Course copyCourse2; + private FeedbackSession copySession; + private FeedbackSession copySession2; + + @Override + protected void prepareTestData() { + testData = removeAndRestoreDataBundle( + loadSqlDataBundle("/InstructorCoursesPageE2ETestSql.json")); + + courses[0] = testData.courses.get("CS1101"); + courses[1] = testData.courses.get("CS2104"); + courses[2] = testData.courses.get("CS2105"); + courses[3] = testData.courses.get("CS1231"); + FeedbackSession session = testData.feedbackSessions.get("session"); + Instructor instructor = testData.instructors.get("instructorCS1231"); + + newCourse = new Course("tm.e2e.ICs.CS4100", "New Course", "Asia/Singapore", "TEAMMATES Test Institute 1"); + newCourse.setCreatedAt(Instant.now()); + + copyCourse = new Course("tm.e2e.ICs.CS5000", "Copy Course", "Asia/Singapore", "TEAMMATES Test Institute 1"); + copyCourse.setCreatedAt(Instant.now()); + + copyCourse2 = new Course("tm.e2e.ICs.CS6000", "Copy Course 2", "Asia/Singapore", "TEAMMATES Test Institute 1"); + copyCourse2.setCreatedAt(Instant.now()); + + copySession = new FeedbackSession("Second Session", copyCourse, instructor.getEmail(), session.getInstructions(), + ZonedDateTime.now(ZoneId.of(copyCourse.getTimeZone())).plus(Duration.ofDays(2)) + .truncatedTo(ChronoUnit.HOURS).toInstant(), + ZonedDateTime.now(ZoneId.of(copyCourse.getTimeZone())).plus(Duration.ofDays(7)) + .truncatedTo(ChronoUnit.HOURS).toInstant(), + ZonedDateTime.now(ZoneId.of(copyCourse.getTimeZone())).minus(Duration.ofDays(28)) + .truncatedTo(ChronoUnit.HOURS).toInstant(), + Const.TIME_REPRESENTS_LATER, session.getGracePeriod(), session.isOpeningEmailEnabled(), + session.isClosingEmailEnabled(), session.isPublishedEmailEnabled()); + + copySession2 = new FeedbackSession("Second Session", copyCourse2, instructor.getEmail(), copySession.getInstructions(), + copySession.getStartTime(), copySession.getEndTime(), copySession.getSessionVisibleFromTime(), copySession.getResultsVisibleFromTime(), + copySession.getGracePeriod(), copySession.isOpeningEmailEnabled(), copySession.isClosingEmailEnabled(), copySession.isPublishedEmailEnabled()); + } + + @BeforeClass + public void classSetup() { + BACKDOOR.deleteCourse(newCourse.getId()); + BACKDOOR.deleteCourse(copyCourse.getId()); + BACKDOOR.deleteCourse(copyCourse2.getId()); + } + + @Test + @Override + public void testAll() { + String instructorId = testData.accounts.get("instructor").getGoogleId(); + AppUrl url = createFrontendUrl(Const.WebPageURIs.INSTRUCTOR_COURSES_PAGE); + InstructorCoursesPage coursesPage = loginToPage(url, InstructorCoursesPage.class, instructorId); + + ______TS("verify loaded data"); + coursesPage.sortByCourseId(); + Course[] activeCourses = { courses[0], courses[3], courses[1] }; + Course[] deletedCourses = { courses[2] }; + + coursesPage.verifyActiveCoursesDetails(activeCourses); + coursesPage.verifyDeletedCoursesDetails(deletedCourses); + + ______TS("verify statistics"); + verifyActiveCourseStatistics(coursesPage, courses[0]); + + ______TS("verify cannot modify without permissions"); + coursesPage.verifyNotModifiable(courses[0].getId()); + + ______TS("add new course"); + Course[] activeCoursesWithNewCourse = { courses[0], courses[3], courses[1], newCourse }; + coursesPage.addCourse(newCourse); + + coursesPage.verifyStatusMessage("The course has been added."); + coursesPage.sortByCourseId(); + coursesPage.verifyActiveCoursesDetails(activeCoursesWithNewCourse); + verifyPresentInDatabase(newCourse); + + ______TS("copy course with session of modified timings"); + Course[] activeCoursesWithCopyCourse = { courses[0], courses[3], courses[1], newCourse, copyCourse }; + coursesPage.copyCourse(courses[3].getId(), copyCourse); + + coursesPage.waitForConfirmationModalAndClickOk(); + coursesPage.sortByCourseId(); + coursesPage.verifyActiveCoursesDetails(activeCoursesWithCopyCourse); + verifyPresentInDatabase(copyCourse); + verifyPresentInDatabase(copySession); + + ______TS("copy course with session of same timings"); + Course[] activeCoursesWithCopyCourse2 = { courses[0], courses[3], courses[1], newCourse, copyCourse, copyCourse2 }; + coursesPage.copyCourse(copyCourse.getId(), copyCourse2); + coursesPage.verifyStatusMessage("The course has been added."); + coursesPage.sortByCourseId(); + coursesPage.verifyActiveCoursesDetails(activeCoursesWithCopyCourse2); + verifyPresentInDatabase(copyCourse2); + verifyPresentInDatabase(copySession2); + + ______TS("move active course to recycle bin"); + newCourse.setDeletedAt(Instant.now()); + Course[] deletedCoursesWithNewCourse = { newCourse, courses[2] }; + coursesPage.moveCourseToRecycleBin(newCourse.getId()); + + coursesPage.verifyStatusMessage("The course " + newCourse.getId() + " has been deleted. " + + "You can restore it from the Recycle Bin manually."); + coursesPage.verifyNumActiveCourses(5); + coursesPage.verifyDeletedCoursesDetails(deletedCoursesWithNewCourse); + assertTrue(BACKDOOR.isCourseInRecycleBin(newCourse.getId())); + + ______TS("restore active course"); + newCourse.setDeletedAt(null); + Course[] activeCoursesWithNewCourseSortedByCreationDate = + { copyCourse2, copyCourse, newCourse, courses[1], courses[3], courses[0] }; + coursesPage.restoreCourse(newCourse.getId()); + + coursesPage.verifyStatusMessage("The course " + newCourse.getId() + " has been restored."); + coursesPage.waitForPageToLoad(); + coursesPage.verifyNumDeletedCourses(1); + // No need to call sortByCreationDate() here because it is the default sort in DESC order + coursesPage.verifyActiveCoursesDetails(activeCoursesWithNewCourseSortedByCreationDate); + assertFalse(BACKDOOR.isCourseInRecycleBin(newCourse.getId())); + + ______TS("permanently delete course"); + coursesPage.moveCourseToRecycleBin(newCourse.getId()); + coursesPage.deleteCourse(newCourse.getId()); + + coursesPage.verifyStatusMessage("The course " + newCourse.getId() + + " has been permanently deleted."); + coursesPage.verifyNumDeletedCourses(1); + verifyAbsentInDatabase(newCourse); + + ______TS("restore all"); + coursesPage.moveCourseToRecycleBin(courses[1].getId()); + Course[] activeCoursesWithRestored = { courses[0], courses[3], courses[1], courses[2], copyCourse, copyCourse2 }; + coursesPage.restoreAllCourses(); + + coursesPage.verifyStatusMessage("All courses have been restored."); + coursesPage.waitForPageToLoad(); + coursesPage.sortByCourseId(); + coursesPage.verifyActiveCoursesDetails(activeCoursesWithRestored); + coursesPage.verifyNumDeletedCourses(0); + assertFalse(BACKDOOR.isCourseInRecycleBin(courses[1].getId())); + assertFalse(BACKDOOR.isCourseInRecycleBin(courses[2].getId())); + + ______TS("permanently delete all"); + coursesPage.moveCourseToRecycleBin(courses[1].getId()); + coursesPage.moveCourseToRecycleBin(courses[2].getId()); + coursesPage.deleteAllCourses(); + + coursesPage.verifyStatusMessage("All courses have been permanently deleted."); + coursesPage.verifyNumActiveCourses(4); + coursesPage.verifyNumDeletedCourses(0); + verifyAbsentInDatabase(courses[1]); + verifyAbsentInDatabase(courses[2]); + } + + private void verifyActiveCourseStatistics(InstructorCoursesPage coursesPage, Course course) { + int numSections = 0; + int numTeams = 0; + int numStudents = 0; + int numUnregistered = 0; + Set sections = new HashSet<>(); + Set teams = new HashSet<>(); + + for (Student student : testData.students.values()) { + if (!student.getCourse().equals(course)) { + continue; + } + if (!sections.contains(student.getSectionName())) { + sections.add(student.getSectionName()); + numSections++; + } + if (!teams.contains(student.getTeamName())) { + teams.add(student.getTeamName()); + numTeams++; + } + if (student.getGoogleId() == null) { + numUnregistered++; + } + numStudents++; + } + coursesPage.verifyActiveCourseStatistics(course, Integer.toString(numSections), Integer.toString(numTeams), + Integer.toString(numStudents), Integer.toString(numUnregistered)); + } +} diff --git a/src/e2e/java/teammates/e2e/pageobjects/InstructorCoursesPage.java b/src/e2e/java/teammates/e2e/pageobjects/InstructorCoursesPage.java index 2240ae1c117..98b25eac0c5 100644 --- a/src/e2e/java/teammates/e2e/pageobjects/InstructorCoursesPage.java +++ b/src/e2e/java/teammates/e2e/pageobjects/InstructorCoursesPage.java @@ -15,6 +15,7 @@ import teammates.common.datatransfer.attributes.CourseAttributes; import teammates.common.util.TimeHelper; +import teammates.storage.sqlentity.Course; /** * Represents the "Courses" page for Instructors. @@ -78,6 +79,12 @@ public void verifyActiveCoursesDetails(CourseAttributes[] courses) { verifyTableBodyValues(getActiveCoursesTable(), courseDetails); } + public void verifyActiveCoursesDetails(Course[] courses) { + String[][] courseDetails = getCourseDetails(courses); + // use verifyTableBodyValues as active courses are sorted + verifyTableBodyValues(getActiveCoursesTable(), courseDetails); + } + public void verifyActiveCourseStatistics(CourseAttributes course, String numSections, String numTeams, String numStudents, String numUnregistered) { showStatistics(course.getId()); @@ -87,6 +94,15 @@ public void verifyActiveCourseStatistics(CourseAttributes course, String numSect verifyTableRowValues(getActiveTableRow(course.getId()), courseDetail); } + public void verifyActiveCourseStatistics(Course course, String numSections, String numTeams, + String numStudents, String numUnregistered) { + showStatistics(course.getId()); + String[] courseDetail = { course.getId(), course.getName(), + TimeHelper.formatInstant(course.getCreatedAt(), course.getTimeZone(), "d MMM yyyy"), + numSections, numTeams, numStudents, numUnregistered }; + verifyTableRowValues(getActiveTableRow(course.getId()), courseDetail); + } + public void verifyArchivedCoursesDetails(CourseAttributes[] courses) { showArchiveTable(); this.waitUntilAnimationFinish(); @@ -97,6 +113,16 @@ public void verifyArchivedCoursesDetails(CourseAttributes[] courses) { } } + public void verifyArchivedCoursesDetails(Course[] courses) { + showArchiveTable(); + this.waitUntilAnimationFinish(); + String[][] courseDetails = getCourseDetails(courses); + for (int i = 0; i < courses.length; i++) { + // use verifyTableRowValues as archive courses are not sorted + verifyTableRowValues(getArchivedTableRow(courses[i].getId()), courseDetails[i]); + } + } + public void verifyDeletedCoursesDetails(CourseAttributes[] courses) { showDeleteTable(); this.waitUntilAnimationFinish(); @@ -107,6 +133,16 @@ public void verifyDeletedCoursesDetails(CourseAttributes[] courses) { } } + public void verifyDeletedCoursesDetails(Course[] courses) { + showDeleteTable(); + this.waitUntilAnimationFinish(); + String[][] courseDetails = getDeletedCourseDetails(courses); + for (int i = 0; i < courses.length; i++) { + // use verifyTableRowValues as deleted courses are not sorted + verifyTableRowValues(getDeletedTableRow(courses[i].getId()), courseDetails[i]); + } + } + public void verifyNotModifiable(String courseId) { // verify enroll button is disabled int courseRowNumber = getRowNumberOfCourse(courseId); @@ -143,6 +179,17 @@ public void addCourse(CourseAttributes newCourse) { click(submitButton); } + public void addCourse(Course newCourse) { + click(addCourseButton); + + fillTextBox(courseIdTextBox, newCourse.getId()); + fillTextBox(courseNameTextBox, newCourse.getName()); + selectCourseInstitute(newCourse.getInstitute()); + selectNewTimeZone(newCourse.getTimeZone()); + + click(submitButton); + } + public void showStatistics(String courseId) { try { click(getShowStatisticsLink(courseId)); @@ -174,6 +221,20 @@ public void copyCourse(String courseId, CourseAttributes newCourse) { waitUntilAnimationFinish(); } + public void copyCourse(String courseId, Course newCourse) { + WebElement otherActionButton = getOtherActionsButton(courseId); + click(otherActionButton); + click(getCopyButton(courseId)); + waitForPageToLoad(); + + fillTextBox(copyCourseIdTextBox, newCourse.getId()); + fillTextBox(copyCourseNameTextBox, newCourse.getName()); + selectCopyTimeZone(newCourse.getTimeZone()); + click(copyCourseButton); + + waitUntilAnimationFinish(); + } + public void moveCourseToRecycleBin(String courseId) { WebElement otherActionButton = getOtherActionsButton(courseId); click(otherActionButton); @@ -269,6 +330,16 @@ private String[][] getCourseDetails(CourseAttributes[] courses) { return courseDetails; } + private String[][] getCourseDetails(Course[] courses) { + String[][] courseDetails = new String[courses.length][3]; + for (int i = 0; i < courses.length; i++) { + String[] courseDetail = { courses[i].getId(), courses[i].getName(), + getDateString(courses[i].getCreatedAt()) }; + courseDetails[i] = courseDetail; + } + return courseDetails; + } + private String getDateString(Instant instant) { return getDisplayedDateTime(instant, ZoneId.systemDefault().getId(), "d MMM yyyy"); } @@ -283,6 +354,16 @@ private String[][] getDeletedCourseDetails(CourseAttributes[] courses) { return courseDetails; } + private String[][] getDeletedCourseDetails(Course[] courses) { + String[][] courseDetails = new String[courses.length][4]; + for (int i = 0; i < courses.length; i++) { + String[] courseDetail = {courses[i].getId(), courses[i].getName(), + getDateString(courses[i].getCreatedAt()), getDateString(courses[i].getDeletedAt()) }; + courseDetails[i] = courseDetail; + } + return courseDetails; + } + private WebElement getRestoreAllButton() { return browser.driver.findElement(By.id("btn-restore-all")); } diff --git a/src/e2e/resources/data/InstructorCoursesPageE2ETestSql.json b/src/e2e/resources/data/InstructorCoursesPageE2ETestSql.json new file mode 100644 index 00000000000..14426c0346a --- /dev/null +++ b/src/e2e/resources/data/InstructorCoursesPageE2ETestSql.json @@ -0,0 +1,318 @@ +{ + "accounts": { + "instructor": { + "id": "00000000-0000-4000-8000-000000000001", + "googleId": "tm.e2e.ICs.instructor", + "name": "Teammates Demo Instr", + "email": "ICs.instructor@gmail.tmt" + }, + "student1InCS1101": { + "id": "00000000-0000-4000-8000-000000000002", + "googleId": "tm.e2e.ICs.student1", + "name": "Student 1", + "email": "ICs.student1@gmail.tmt" + }, + "student2InCS1101": { + "id": "00000000-0000-4000-8000-000000000003", + "googleId": "tm.e2e.ICs.student2", + "name": "Student 2", + "email": "ICs.student2@gmail.tmt" + } + }, + "accountRequests": {}, + "courses": { + "CS1101": { + "id": "tm.e2e.ICs.CS1101", + "name": "Programming Methodology", + "timeZone": "Asia/Singapore", + "institute": "TEAMMATES Test Institute 1", + "createdAt": "2012-04-02T12:00:00Z" + }, + "CS1231": { + "id": "tm.e2e.ICs.CS1231", + "name": "Discrete Structures", + "timeZone": "Asia/Singapore", + "institute": "TEAMMATES Test Institute 1", + "createdAt": "2012-04-02T12:00:00Z" + }, + "CS2104": { + "id": "tm.e2e.ICs.CS2104", + "name": "Programming Language Concept", + "timeZone": "Asia/Singapore", + "institute": "TEAMMATES Test Institute 1", + "createdAt": "2013-04-02T12:00:00Z" + }, + "CS2105": { + "id": "tm.e2e.ICs.CS2105", + "name": "Computer Networks", + "timeZone": "Asia/Singapore", + "institute": "TEAMMATES Test Institute 1", + "deletedAt": "2014-04-12T12:00:00Z", + "createdAt": "2014-04-02T12:00:00Z" + } + }, + "sections": { + "tm.e2e.ICs.CS1101-Section1": { + "id": "00000000-0000-4000-8000-000000000201", + "course": { + "id": "tm.e2e.ICs.CS1101" + }, + "name": "Section 1" + } + }, + "teams": { + "tm.e2e.ICs.CS1101-Section1-Team1": { + "id": "00000000-0000-4000-8000-000000000301", + "section": { + "id": "00000000-0000-4000-8000-000000000201" + }, + "name": "Team 1" + }, + "tm.e2e.ICs.CS1101-Section1-Team2": { + "id": "00000000-0000-4000-8000-000000000302", + "section": { + "id": "00000000-0000-4000-8000-000000000201" + }, + "name": "Team 2" + }, + "tm.e2e.ICs.CS1101-Section1-Team3": { + "id": "00000000-0000-4000-8000-000000000303", + "section": { + "id": "00000000-0000-4000-8000-000000000201" + }, + "name": "Team 3" + } + }, + "deadlineExtensions": {}, + "instructors": { + "instructorCS1101": { + "isDisplayedToStudents": true, + "displayName": "Instructor", + "role": "INSTRUCTOR_PERMISSION_ROLE_CUSTOM", + "privileges": { + "courseLevel": { + "canModifyCourse": false, + "canModifyInstructor": true, + "canModifySession": true, + "canModifyStudent": false, + "canViewStudentInSections": true, + "canViewSessionInSections": true, + "canSubmitSessionInSections": true, + "canModifySessionCommentsInSections": true + }, + "sectionLevel": {}, + "sessionLevel": {} + }, + "id": "00000000-0000-4000-8000-000000000501", + "courseId": "tm.e2e.ICs.CS1101", + "course": { + "id": "tm.e2e.ICs.CS1101" + }, + "account": { + "id": "00000000-0000-4000-8000-000000000001" + }, + "name": "Teammates Demo Instr", + "email": "ICs.instructor@gmail.tmt" + }, + "instructorCS1231": { + "isDisplayedToStudents": true, + "displayName": "Instructor", + "role": "INSTRUCTOR_PERMISSION_ROLE_CUSTOM", + "privileges": { + "courseLevel": { + "canModifyCourse": true, + "canModifyInstructor": true, + "canModifySession": true, + "canModifyStudent": true, + "canViewStudentInSections": true, + "canViewSessionInSections": true, + "canSubmitSessionInSections": true, + "canModifySessionCommentsInSections": true + }, + "sectionLevel": {}, + "sessionLevel": {} + }, + "id": "00000000-0000-4000-8000-000000000502", + "courseId": "tm.e2e.ICs.CS1231", + "course": { + "id": "tm.e2e.ICs.CS1231" + }, + "account": { + "id": "00000000-0000-4000-8000-000000000001" + }, + "name": "Teammates Demo Instr", + "email": "ICs.instructor@gmail.tmt" + }, + "instructorCS2104": { + "isDisplayedToStudents": true, + "displayName": "Instructor", + "role": "INSTRUCTOR_PERMISSION_ROLE_COOWNER", + "privileges": { + "courseLevel": { + "canModifyCourse": true, + "canModifyInstructor": true, + "canModifySession": true, + "canModifyStudent": true, + "canViewStudentInSections": true, + "canViewSessionInSections": true, + "canSubmitSessionInSections": true, + "canModifySessionCommentsInSections": true + }, + "sectionLevel": {}, + "sessionLevel": {} + }, + "id": "00000000-0000-4000-8000-000000000503", + "courseId": "tm.e2e.ICs.CS2104", + "course": { + "id": "tm.e2e.ICs.CS2104" + }, + "account": { + "id": "00000000-0000-4000-8000-000000000001" + }, + "name": "Teammates Demo Instr", + "email": "ICs.instructor@gmail.tmt" + }, + "instructorCS2105": { + "isDisplayedToStudents": true, + "displayName": "Instructor", + "role": "INSTRUCTOR_PERMISSION_ROLE_COOWNER", + "privileges": { + "courseLevel": { + "canModifyCourse": true, + "canModifyInstructor": true, + "canModifySession": true, + "canModifyStudent": true, + "canViewStudentInSections": true, + "canViewSessionInSections": true, + "canSubmitSessionInSections": true, + "canModifySessionCommentsInSections": true + }, + "sectionLevel": {}, + "sessionLevel": {} + }, + "id": "00000000-0000-4000-8000-000000000504", + "courseId": "tm.e2e.ICs.CS2105", + "course": { + "id": "tm.e2e.ICs.CS2105" + }, + "account": { + "id": "00000000-0000-4000-8000-000000000001" + }, + "name": "Teammates Demo Inst", + "email": "ICs.instructor@gmail.tmt" + } + }, + "students": { + "student1InCS1101": { + "comments": "", + "id": "00000000-0000-4000-8000-000000000601", + "courseId": "tm.e2e.ICs.CS1101", + "course": { + "id": "tm.e2e.ICs.CS1101" + }, + "account": { + "id": "00000000-0000-4000-8000-000000000002" + }, + "team": { + "id": "00000000-0000-4000-8000-000000000301" + }, + "name": "student1", + "email": "ICs.student1@gmail.tmt" + }, + "student2InCS1101": { + "comments": "", + "id": "00000000-0000-4000-8000-000000000602", + "courseId": "tm.e2e.ICs.CS1101", + "course": { + "id": "tm.e2e.ICs.CS1101" + }, + "account": { + "id": "00000000-0000-4000-8000-000000000003" + }, + "team": { + "id": "00000000-0000-4000-8000-000000000302" + }, + "name": "student2", + "email": "ICs.student2@gmail.tmt" + }, + "UnauthenticatedStudentInCS1101": { + "comments": "", + "id": "00000000-0000-4000-8000-000000000603", + "courseId": "tm.e2e.ICs.CS1101", + "course": { + "id": "tm.e2e.ICs.CS1101" + }, + "team": { + "id": "00000000-0000-4000-8000-000000000303" + }, + "name": "student3", + "email": "ICs.student3@gmail.tmt" + } + }, + "feedbackSessions": { + "session": { + "id": "00000000-0000-4000-8000-000000000701", + "course": { + "id": "tm.e2e.ICs.CS1231" + }, + "name": "Second Session", + "creatorEmail": "backdoor@gmail.tmt", + "instructions": "

Instructions for Second session

", + "startTime": "2012-04-02T00:00:00Z", + "endTime": "2012-05-01T00:00:00Z", + "sessionVisibleFromTime": "2012-04-02T00:00:00Z", + "resultsVisibleFromTime": "2012-05-02T00:00:00Z", + "gracePeriod": 10, + "isOpeningEmailEnabled": true, + "isClosingEmailEnabled": true, + "isPublishedEmailEnabled": true, + "isOpeningSoonEmailSent": false, + "isOpenEmailSent": false, + "isClosingSoonEmailSent": false, + "isClosedEmailSent": false, + "isPublishedEmailSent": false, + "createdAt": "2012-04-01T23:59:00Z" + } + }, + "feedbackQuestions": { + "qn1": { + "questionDetails": { + "shouldAllowRichText": true, + "questionType": "TEXT", + "questionText": "Testing question text" + }, + "id": "00000000-0000-4000-8000-000000000801", + "feedbackSession": { + "id": "00000000-0000-4000-8000-000000000701" + }, + "questionNumber": 1, + "description": "Testing description", + "giverType": "STUDENTS", + "recipientType": "TEAMS", + "numOfEntitiesToGiveFeedbackTo": 2, + "showResponsesTo": [ + "STUDENTS", + "INSTRUCTORS", + "OWN_TEAM_MEMBERS", + "RECEIVER" + ], + "showGiverNameTo": [ + "STUDENTS", + "INSTRUCTORS", + "OWN_TEAM_MEMBERS", + "RECEIVER" + ], + "showRecipientNameTo": [ + "STUDENTS", + "INSTRUCTORS", + "OWN_TEAM_MEMBERS", + "RECEIVER" + ] + } + }, + "feedbackResponses": {}, + "feedbackResponseComments": {}, + "feedbackSessionLogs": {}, + "notifications": {}, + "readNotifications": {} +} diff --git a/src/main/java/teammates/sqllogic/api/Logic.java b/src/main/java/teammates/sqllogic/api/Logic.java index 659f27750c5..c6a59a78121 100644 --- a/src/main/java/teammates/sqllogic/api/Logic.java +++ b/src/main/java/teammates/sqllogic/api/Logic.java @@ -219,6 +219,13 @@ public Account getAccountForGoogleId(String googleId) { return accountsLogic.getAccountForGoogleId(googleId); } + /** + * Gets an account by googleId. + */ + public Account getAccountForGoogleIdWithTransaction(String googleId) { + return accountsLogic.getAccountForGoogleIdWithTransaction(googleId); + } + /** * Get a list of accounts associated with email provided. */ @@ -363,6 +370,20 @@ public Course createCourse(Course course) throws InvalidParametersException, Ent return coursesLogic.createCourse(course); } + /** + * Creates a course and instructor. + * @param instructorAccount the account of the instructor creating the course. + * @param course the course to create. + * @return the created course. + * @throws InvalidParametersException if the course or instructor is not valid. + * @throws EntityAlreadyExistsException if the course or instructor already exists. + */ + public Course createCourseAndInstructorWithTransaction(Account instructorAccount, Course course) + throws InvalidParametersException, EntityAlreadyExistsException { + return coursesLogic.createCourseAndInstructorWithTransaction(instructorAccount, course); + } + + /** * Deletes a course by course id. * @param courseId of course. @@ -874,6 +895,13 @@ public Instructor getInstructorByGoogleId(String courseId, String googleId) { return usersLogic.getInstructorByGoogleId(courseId, googleId); } + /** + * Gets an instructor by associated {@code googleId}. + */ + public Instructor getInstructorByGoogleIdWithTransaction(String courseId, String googleId) { + return usersLogic.getInstructorByGoogleIdWithTransaction(courseId, googleId); + } + /** * Gets list of instructors by {@code googleId}. */ @@ -998,6 +1026,13 @@ public boolean canInstructorCreateCourse(String googleId, String institute) { return usersLogic.canInstructorCreateCourse(googleId, institute); } + /** + * Checks if an instructor with {@code googleId} can create a course with {@code institute}. + */ + public boolean canInstructorCreateCourseWithTransaction(String googleId, String institute) { + return usersLogic.canInstructorCreateCourseWithTransaction(googleId, institute); + } + /** * Gets student associated with {@code id}. * diff --git a/src/main/java/teammates/sqllogic/core/AccountsLogic.java b/src/main/java/teammates/sqllogic/core/AccountsLogic.java index 49948080448..6136abe4b4d 100644 --- a/src/main/java/teammates/sqllogic/core/AccountsLogic.java +++ b/src/main/java/teammates/sqllogic/core/AccountsLogic.java @@ -69,6 +69,16 @@ public Account getAccountForGoogleId(String googleId) { return accountsDb.getAccountByGoogleId(googleId); } + /** + * Gets an account by googleId. + */ + public Account getAccountForGoogleIdWithTransaction(String googleId) { + HibernateUtil.beginTransaction(); + Account account = getAccountForGoogleId(googleId); + HibernateUtil.commitTransaction(); + return account; + } + /** * Gets accounts associated with email. */ diff --git a/src/main/java/teammates/sqllogic/core/CoursesLogic.java b/src/main/java/teammates/sqllogic/core/CoursesLogic.java index 5153c03baba..65cfecbb84a 100644 --- a/src/main/java/teammates/sqllogic/core/CoursesLogic.java +++ b/src/main/java/teammates/sqllogic/core/CoursesLogic.java @@ -7,10 +7,15 @@ import java.util.List; import java.util.stream.Collectors; +import teammates.common.datatransfer.InstructorPermissionRole; +import teammates.common.datatransfer.InstructorPrivileges; import teammates.common.exception.EntityAlreadyExistsException; import teammates.common.exception.EntityDoesNotExistException; import teammates.common.exception.InvalidParametersException; +import teammates.common.util.Const.InstructorPermissionRoleNames; +import teammates.common.util.HibernateUtil; import teammates.storage.sqlapi.CoursesDb; +import teammates.storage.sqlentity.Account; import teammates.storage.sqlentity.Course; import teammates.storage.sqlentity.FeedbackSession; import teammates.storage.sqlentity.Instructor; @@ -60,6 +65,41 @@ public Course createCourse(Course course) throws InvalidParametersException, Ent return coursesDb.createCourse(course); } + + /** + * Creates a course and instructor. + * + * @return the created course + * @throws InvalidParametersException if the course or instructor is not valid + * @throws EntityAlreadyExistsException if the course or instructor already exists in the + * database. + */ + public Course createCourseAndInstructorWithTransaction(Account instructorAccount, Course courseToCreate) + throws InvalidParametersException, EntityAlreadyExistsException { + + HibernateUtil.beginTransaction(); + + Course createdCourse; + try { + createdCourse = createCourse(courseToCreate); + + InstructorPrivileges privileges = new InstructorPrivileges( + InstructorPermissionRoleNames.INSTRUCTOR_PERMISSION_ROLE_COOWNER); + Instructor instructor = new Instructor(createdCourse, instructorAccount.getName(), instructorAccount.getEmail(), + true, instructorAccount.getName(), InstructorPermissionRole.INSTRUCTOR_PERMISSION_ROLE_COOWNER, + privileges); + instructor.setAccount(instructorAccount); + usersLogic.createInstructor(instructor); + } catch (EntityAlreadyExistsException | InvalidParametersException e) { + HibernateUtil.rollbackTransaction(); + throw e; + } + + HibernateUtil.commitTransaction(); + + return createdCourse; + } + /** * Gets a course by course id. * diff --git a/src/main/java/teammates/sqllogic/core/UsersLogic.java b/src/main/java/teammates/sqllogic/core/UsersLogic.java index bfac68a3f36..fe2253418c0 100644 --- a/src/main/java/teammates/sqllogic/core/UsersLogic.java +++ b/src/main/java/teammates/sqllogic/core/UsersLogic.java @@ -22,6 +22,7 @@ import teammates.common.exception.SearchServiceException; import teammates.common.exception.StudentUpdateException; import teammates.common.util.Const; +import teammates.common.util.HibernateUtil; import teammates.common.util.RequestTracer; import teammates.common.util.SanitizationHelper; import teammates.storage.sqlapi.UsersDb; @@ -276,6 +277,17 @@ public Instructor getInstructorByGoogleId(String courseId, String googleId) { return usersDb.getInstructorByGoogleId(courseId, googleId); } + /** + * Gets an instructor by associated {@code googleId}. + */ + public Instructor getInstructorByGoogleIdWithTransaction(String courseId, String googleId) { + HibernateUtil.beginTransaction(); + Instructor instructor = getInstructorByGoogleId(courseId, googleId); + HibernateUtil.commitTransaction(); + + return instructor; + } + /** * Searches instructors in the whole system. Used by admin only. * @@ -989,6 +1001,18 @@ public boolean canInstructorCreateCourse(String googleId, String institute) { .anyMatch(course -> institute.equals(course.getInstitute())); } + /** + * Checks if an instructor with {@code googleId} can create a course with + * {@code institute} + * (ie. has an existing course(s) with the same {@code institute}). + */ + public boolean canInstructorCreateCourseWithTransaction(String googleId, String institute) { + HibernateUtil.beginTransaction(); + boolean canInstructorCreateCourse = canInstructorCreateCourse(googleId, institute); + HibernateUtil.commitTransaction(); + return canInstructorCreateCourse; + } + /** * Utility function to convert user list to email-user map for faster email lookup. * diff --git a/src/main/java/teammates/ui/webapi/CreateCourseAction.java b/src/main/java/teammates/ui/webapi/CreateCourseAction.java index 8e6af9fab83..384072e4e92 100644 --- a/src/main/java/teammates/ui/webapi/CreateCourseAction.java +++ b/src/main/java/teammates/ui/webapi/CreateCourseAction.java @@ -1,14 +1,12 @@ package teammates.ui.webapi; -import java.util.List; -import java.util.Objects; - -import teammates.common.datatransfer.attributes.CourseAttributes; -import teammates.common.datatransfer.attributes.InstructorAttributes; import teammates.common.exception.EntityAlreadyExistsException; import teammates.common.exception.InvalidParametersException; import teammates.common.util.Const; import teammates.common.util.FieldValidator; +import teammates.storage.sqlentity.Account; +import teammates.storage.sqlentity.Course; +import teammates.storage.sqlentity.Instructor; import teammates.ui.output.CourseData; import teammates.ui.request.CourseCreateRequest; import teammates.ui.request.InvalidHttpRequestBodyException; @@ -18,6 +16,11 @@ */ public class CreateCourseAction extends Action { + @Override + public boolean isTransactionNeeded() { + return false; + } + @Override AuthType getMinAuthLevel() { return AuthType.LOGGED_IN; @@ -31,13 +34,8 @@ void checkSpecificAccessControl() throws UnauthorizedAccessException { String institute = getNonNullRequestParamValue(Const.ParamsNames.INSTRUCTOR_INSTITUTION); - List existingInstructors = logic.getInstructorsForGoogleId(userInfo.getId()); - boolean canCreateCourse = existingInstructors - .stream() - .filter(InstructorAttributes::hasCoownerPrivileges) - .map(instructor -> logic.getCourse(instructor.getCourseId())) - .filter(Objects::nonNull) - .anyMatch(course -> institute.equals(course.getInstitute())); + boolean canCreateCourse = sqlLogic.canInstructorCreateCourseWithTransaction(userInfo.getId(), institute); + if (!canCreateCourse) { throw new UnauthorizedAccessException("You are not allowed to create a course under this institute. " + "If you wish to do so, please request for an account under the institute.", true); @@ -60,27 +58,24 @@ public JsonResult execute() throws InvalidHttpRequestBodyException, InvalidOpera String newCourseName = courseCreateRequest.getCourseName(); String institute = getNonNullRequestParamValue(Const.ParamsNames.INSTRUCTOR_INSTITUTION); - CourseAttributes courseAttributes = - CourseAttributes.builder(newCourseId) - .withName(newCourseName) - .withTimezone(newCourseTimeZone) - .withInstitute(institute) - .build(); + Course course = new Course(newCourseId, newCourseName, newCourseTimeZone, institute); try { - logic.createCourseAndInstructor(userInfo.getId(), courseAttributes); + Account instructorAccount = sqlLogic.getAccountForGoogleIdWithTransaction(userInfo.getId()); + course = sqlLogic.createCourseAndInstructorWithTransaction(instructorAccount, course); - InstructorAttributes instructorCreatedForCourse = logic.getInstructorForGoogleId(newCourseId, userInfo.getId()); + Instructor instructorCreatedForCourse = sqlLogic.getInstructorByGoogleIdWithTransaction(newCourseId, userInfo.getId()); taskQueuer.scheduleInstructorForSearchIndexing(instructorCreatedForCourse.getCourseId(), instructorCreatedForCourse.getEmail()); } catch (EntityAlreadyExistsException e) { - throw new InvalidOperationException("The course ID " + courseAttributes.getId() + throw new InvalidOperationException("The course ID " + course.getId() + " has been used by another course, possibly by some other user." + " Please try again with a different course ID.", e); } catch (InvalidParametersException e) { throw new InvalidHttpRequestBodyException(e); } - return new JsonResult(new CourseData(logic.getCourse(newCourseId))); + CourseData courseData = new CourseData(course); + return new JsonResult(courseData); } } diff --git a/src/test/java/teammates/test/BaseTestCaseWithSqlDatabaseAccess.java b/src/test/java/teammates/test/BaseTestCaseWithSqlDatabaseAccess.java index c5663428800..21ed748ae3a 100644 --- a/src/test/java/teammates/test/BaseTestCaseWithSqlDatabaseAccess.java +++ b/src/test/java/teammates/test/BaseTestCaseWithSqlDatabaseAccess.java @@ -226,11 +226,15 @@ private ApiOutput getEntity(BaseEntity entity) { return getFeedbackSession((FeedbackSession) entity); } else if (entity instanceof FeedbackResponse) { return getFeedbackResponse((FeedbackResponse) entity); + } else if (entity instanceof Course) { + return getCourse((Course) entity); } else { throw new RuntimeException("Unknown entity type"); } } + protected abstract CourseData getCourse(Course course); + protected abstract FeedbackQuestionData getFeedbackQuestion(FeedbackQuestion fq); protected abstract FeedbackSessionData getFeedbackSession(FeedbackSession fq);